2026-02-09 22:06:25 +00:00
|
|
|
/**
|
|
|
|
|
* Tree-sitter grammar for Storybook DSL
|
|
|
|
|
*
|
|
|
|
|
* This grammar defines the syntax for the Storybook narrative DSL,
|
|
|
|
|
* including characters, templates, relationships, life arcs, behaviors, etc.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
module.exports = grammar({
|
|
|
|
|
name: 'storybook',
|
|
|
|
|
|
|
|
|
|
// externals: $ => [
|
|
|
|
|
// $._prose_block_content,
|
|
|
|
|
// $._prose_block_end
|
|
|
|
|
// ],
|
|
|
|
|
|
|
|
|
|
extras: $ => [
|
|
|
|
|
/\s/, // Whitespace
|
|
|
|
|
$.line_comment,
|
|
|
|
|
$.block_comment
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
conflicts: $ => [
|
2026-02-14 14:29:29 +00:00
|
|
|
[$.path_segments],
|
2026-02-14 17:43:26 +00:00
|
|
|
[$.sub_concept_enum_body, $.sub_concept_record_body],
|
|
|
|
|
[$.uses_behaviors]
|
2026-02-09 22:06:25 +00:00
|
|
|
],
|
|
|
|
|
|
|
|
|
|
word: $ => $.identifier,
|
|
|
|
|
|
|
|
|
|
rules: {
|
|
|
|
|
// Top-level structure
|
|
|
|
|
source_file: $ => repeat($.declaration),
|
|
|
|
|
|
|
|
|
|
// Comments
|
|
|
|
|
line_comment: $ => token(seq('//', /.*/)),
|
|
|
|
|
block_comment: $ => token(seq('/*', /[^*]*\*+([^/*][^*]*\*+)*/, '/')),
|
|
|
|
|
|
|
|
|
|
// Declarations
|
|
|
|
|
declaration: $ => choice(
|
|
|
|
|
$.use_declaration,
|
2026-02-14 17:43:26 +00:00
|
|
|
$.character_declaration,
|
|
|
|
|
$.template_declaration,
|
|
|
|
|
$.life_arc_declaration,
|
|
|
|
|
$.schedule_declaration,
|
|
|
|
|
$.behavior_declaration,
|
|
|
|
|
$.institution_declaration,
|
|
|
|
|
$.relationship_declaration,
|
|
|
|
|
$.location_declaration,
|
|
|
|
|
$.species_declaration,
|
2026-02-14 14:29:29 +00:00
|
|
|
$.enum_declaration,
|
|
|
|
|
$.concept_declaration,
|
|
|
|
|
$.sub_concept,
|
|
|
|
|
$.concept_comparison
|
2026-02-09 22:06:25 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Use declarations
|
|
|
|
|
use_declaration: $ => seq(
|
|
|
|
|
'use',
|
|
|
|
|
$.path_segments,
|
|
|
|
|
optional(choice(
|
|
|
|
|
seq('::', '{', commaSep1($.identifier), '}'),
|
|
|
|
|
seq('::', '*')
|
|
|
|
|
)),
|
|
|
|
|
';'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
path: $ => $.path_segments,
|
|
|
|
|
|
|
|
|
|
path_segments: $ => sep1($.identifier, token('::')),
|
|
|
|
|
|
|
|
|
|
// Character declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
character_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'character',
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
optional(seq(':', field('species', $.identifier))),
|
|
|
|
|
optional(field('template', $.template_clause)),
|
2026-02-14 17:43:26 +00:00
|
|
|
field('body', $.character_body)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
character_body: $ => seq(
|
|
|
|
|
'{',
|
|
|
|
|
repeat($.field),
|
|
|
|
|
'}'
|
2026-02-09 22:06:25 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
|
|
template_clause: $ => seq('from', commaSep1($.identifier)),
|
|
|
|
|
|
|
|
|
|
// Template declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
template_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'template',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 14:29:29 +00:00
|
|
|
optional(seq(':', field('species', $.identifier))),
|
2026-02-09 22:06:25 +00:00
|
|
|
optional('strict'),
|
2026-02-14 17:43:26 +00:00
|
|
|
field('body', $.template_body)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
template_body: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'{',
|
2026-02-14 17:43:26 +00:00
|
|
|
repeat(choice(
|
|
|
|
|
$.include,
|
|
|
|
|
$.uses_behaviors,
|
|
|
|
|
$.uses_schedule,
|
|
|
|
|
$.field
|
|
|
|
|
)),
|
2026-02-09 22:06:25 +00:00
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
uses_behaviors: $ => seq(
|
|
|
|
|
'uses',
|
|
|
|
|
'behaviors',
|
|
|
|
|
':',
|
|
|
|
|
commaSep1($.identifier)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
uses_schedule: $ => seq(
|
|
|
|
|
'uses',
|
|
|
|
|
choice('schedule', 'schedules'),
|
|
|
|
|
':',
|
|
|
|
|
choice(
|
|
|
|
|
$.identifier,
|
|
|
|
|
seq('[', commaSep1($.identifier), ']')
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-09 22:06:25 +00:00
|
|
|
include: $ => seq('include', $.identifier),
|
|
|
|
|
|
|
|
|
|
// Fields (key: value pairs)
|
|
|
|
|
field: $ => choice(
|
|
|
|
|
seq(
|
|
|
|
|
field('name', $.dotted_path),
|
|
|
|
|
':',
|
|
|
|
|
field('value', $.value)
|
|
|
|
|
),
|
|
|
|
|
$.prose_block
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
dotted_path: $ => sep1($.identifier, '.'),
|
|
|
|
|
|
|
|
|
|
// Values
|
|
|
|
|
value: $ => choice(
|
|
|
|
|
$.integer,
|
|
|
|
|
$.float,
|
|
|
|
|
$.string,
|
|
|
|
|
$.boolean,
|
|
|
|
|
$.range,
|
|
|
|
|
$.time,
|
|
|
|
|
$.duration,
|
|
|
|
|
$.path,
|
|
|
|
|
$.prose_block,
|
|
|
|
|
$.list,
|
|
|
|
|
$.object,
|
|
|
|
|
$.override
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
integer: $ => /-?[0-9]+/,
|
|
|
|
|
|
|
|
|
|
float: $ => /-?[0-9]+\.[0-9]+/,
|
|
|
|
|
|
|
|
|
|
string: $ => /"([^"\\]|\\.)*"/,
|
|
|
|
|
|
|
|
|
|
boolean: $ => choice('true', 'false'),
|
|
|
|
|
|
|
|
|
|
range: $ => choice(
|
|
|
|
|
seq($.integer, '..', $.integer),
|
|
|
|
|
seq($.float, '..', $.float)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
time: $ => /[0-9]{2}:[0-9]{2}(:[0-9]{2})?/,
|
|
|
|
|
|
2026-02-13 20:21:31 +00:00
|
|
|
duration: $ => /[0-9]+[dhms]([0-9]+[dhms])*/,
|
2026-02-09 22:06:25 +00:00
|
|
|
|
|
|
|
|
list: $ => seq('[', commaSep($.value), ']'),
|
|
|
|
|
|
|
|
|
|
object: $ => $.block,
|
|
|
|
|
|
|
|
|
|
block: $ => seq('{', repeat($.field), '}'),
|
|
|
|
|
|
|
|
|
|
// Override (@base { remove field, field: value })
|
|
|
|
|
override: $ => seq(
|
|
|
|
|
'@',
|
|
|
|
|
$.path,
|
|
|
|
|
'{',
|
|
|
|
|
repeat($.override_op),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
override_op: $ => choice(
|
|
|
|
|
seq('remove', $.identifier),
|
|
|
|
|
seq('append', $.field),
|
|
|
|
|
$.field
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Prose blocks (---tag content ---)
|
|
|
|
|
prose_block: $ => seq(
|
|
|
|
|
field('marker', $.prose_marker),
|
|
|
|
|
field('tag', $.identifier),
|
|
|
|
|
optional(/[^\n]*/), // Rest of opening line
|
|
|
|
|
field('content', $.prose_content),
|
|
|
|
|
field('end', $.prose_marker)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
prose_marker: $ => '---',
|
|
|
|
|
|
|
|
|
|
// Capture prose content as a single token for markdown injection
|
|
|
|
|
prose_content: $ => token(prec(-1, repeat1(choice(
|
|
|
|
|
/[^\-]+/, // Any non-dash characters
|
|
|
|
|
/-[^\-]/, // Single dash not followed by another dash
|
|
|
|
|
/-\-[^\-]/, // Two dashes not followed by another dash
|
|
|
|
|
)))),
|
|
|
|
|
|
|
|
|
|
// Life arc declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
life_arc_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'life_arc',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 17:43:26 +00:00
|
|
|
optional(field('requires', $.requires_clause)),
|
2026-02-09 22:06:25 +00:00
|
|
|
'{',
|
|
|
|
|
repeat($.field),
|
2026-02-14 17:43:26 +00:00
|
|
|
repeat($.state_block),
|
2026-02-09 22:06:25 +00:00
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
requires_clause: $ => seq(
|
|
|
|
|
'requires',
|
|
|
|
|
'{',
|
|
|
|
|
commaSep1($.required_field),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
required_field: $ => seq(
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
':',
|
|
|
|
|
field('type', $.identifier)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
state_block: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'state',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 17:43:26 +00:00
|
|
|
field('body', $.state_body)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
state_body: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'{',
|
|
|
|
|
optional($.on_enter),
|
|
|
|
|
repeat($.field),
|
|
|
|
|
repeat($.transition),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
on_enter: $ => seq('on', 'enter', $.block),
|
|
|
|
|
|
|
|
|
|
transition: $ => seq(
|
|
|
|
|
'on',
|
|
|
|
|
field('condition', $.expression),
|
|
|
|
|
'->',
|
|
|
|
|
field('target', $.identifier)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Schedule declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
schedule_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'schedule',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 17:43:26 +00:00
|
|
|
optional(seq('extends', field('extends', $.identifier))),
|
|
|
|
|
field('body', $.schedule_body)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
schedule_body: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'{',
|
2026-02-14 17:43:26 +00:00
|
|
|
repeat(choice(
|
|
|
|
|
$.field,
|
|
|
|
|
$.schedule_block,
|
|
|
|
|
$.override_block,
|
|
|
|
|
$.recurrence_block
|
|
|
|
|
)),
|
2026-02-09 22:06:25 +00:00
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
// Named block: block name { time range, action, fields }
|
2026-02-09 22:06:25 +00:00
|
|
|
schedule_block: $ => seq(
|
2026-02-14 17:43:26 +00:00
|
|
|
'block',
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
'{',
|
|
|
|
|
field('time_range', $.time_range),
|
|
|
|
|
repeat($.block_field),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Override block: override name { time range, action, fields }
|
|
|
|
|
override_block: $ => seq(
|
|
|
|
|
'override',
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
'{',
|
|
|
|
|
field('time_range', $.time_range),
|
|
|
|
|
repeat($.block_field),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Recurrence: recurrence Name on DayOfWeek { blocks }
|
|
|
|
|
recurrence_block: $ => seq(
|
|
|
|
|
'recurrence',
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
'on',
|
|
|
|
|
field('day', $.identifier),
|
|
|
|
|
'{',
|
|
|
|
|
repeat1($.schedule_block),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
time_range: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
field('start', $.time),
|
|
|
|
|
'->',
|
|
|
|
|
field('end', $.time),
|
2026-02-14 17:43:26 +00:00
|
|
|
optional(',')
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
block_field: $ => seq(
|
|
|
|
|
field('name', $.identifier),
|
2026-02-09 22:06:25 +00:00
|
|
|
':',
|
2026-02-14 17:43:26 +00:00
|
|
|
field('value', choice($.identifier, $.string, $.integer, $.float))
|
2026-02-09 22:06:25 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Behavior tree declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
behavior_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'behavior',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 17:43:26 +00:00
|
|
|
field('body', $.behavior_body)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
behavior_body: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'{',
|
|
|
|
|
repeat($.field),
|
|
|
|
|
field('root', $.behavior_node),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
behavior_node: $ => choice(
|
|
|
|
|
$.selector_node,
|
|
|
|
|
$.sequence_node,
|
2026-02-13 20:21:31 +00:00
|
|
|
$.condition_node,
|
|
|
|
|
$.if_decorator_node,
|
2026-02-14 17:43:26 +00:00
|
|
|
$.repeat_node,
|
2026-02-13 20:21:31 +00:00
|
|
|
$.decorator_node,
|
2026-02-09 22:06:25 +00:00
|
|
|
$.action_node,
|
|
|
|
|
$.subtree_node
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-13 20:21:31 +00:00
|
|
|
// Selector node: choose { ... }
|
|
|
|
|
selector_node: $ => seq(
|
|
|
|
|
'choose',
|
|
|
|
|
optional(field('label', $.identifier)),
|
|
|
|
|
'{',
|
|
|
|
|
repeat1($.behavior_node),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Sequence node: then { ... }
|
|
|
|
|
sequence_node: $ => seq(
|
|
|
|
|
'then',
|
|
|
|
|
optional(field('label', $.identifier)),
|
|
|
|
|
'{',
|
|
|
|
|
repeat1($.behavior_node),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Condition node: if(expr) or when(expr) - NO BRACES
|
|
|
|
|
condition_node: $ => seq(
|
|
|
|
|
choice('if', 'when'),
|
|
|
|
|
'(',
|
|
|
|
|
field('condition', $.expression),
|
|
|
|
|
')'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// If decorator: if(expr) { child } - WITH BRACES
|
|
|
|
|
if_decorator_node: $ => seq(
|
|
|
|
|
'if',
|
|
|
|
|
'(',
|
|
|
|
|
field('condition', $.expression),
|
|
|
|
|
')',
|
|
|
|
|
'{',
|
|
|
|
|
field('child', $.behavior_node),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
2026-02-09 22:06:25 +00:00
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
// Repeat node: repeat { child } or repeat(N) { child } or repeat(min..max) { child }
|
|
|
|
|
repeat_node: $ => seq(
|
|
|
|
|
'repeat',
|
|
|
|
|
optional(field('params', choice(
|
|
|
|
|
seq('(', $.integer, ')'),
|
|
|
|
|
seq('(', $.integer, '..', $.integer, ')'),
|
|
|
|
|
seq('(', $.duration, ')')
|
|
|
|
|
))),
|
|
|
|
|
'{',
|
|
|
|
|
field('child', $.behavior_node),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Decorator node: retry/timeout/etc { child }
|
2026-02-13 20:21:31 +00:00
|
|
|
decorator_node: $ => seq(
|
|
|
|
|
field('decorator', $.decorator_keyword),
|
|
|
|
|
optional(field('params', $.decorator_params)),
|
|
|
|
|
'{',
|
|
|
|
|
field('child', $.behavior_node),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
decorator_keyword: $ => choice(
|
|
|
|
|
'invert',
|
|
|
|
|
'retry',
|
|
|
|
|
'timeout',
|
|
|
|
|
'cooldown',
|
|
|
|
|
'succeed_always',
|
|
|
|
|
'fail_always'
|
|
|
|
|
),
|
2026-02-09 22:06:25 +00:00
|
|
|
|
2026-02-13 20:21:31 +00:00
|
|
|
decorator_params: $ => seq(
|
|
|
|
|
'(',
|
|
|
|
|
choice(
|
|
|
|
|
// min..max range (for repeat)
|
|
|
|
|
seq($.integer, '..', $.integer),
|
|
|
|
|
// N (for repeat, retry)
|
|
|
|
|
$.integer,
|
|
|
|
|
// duration (for timeout, cooldown)
|
|
|
|
|
$.duration
|
|
|
|
|
),
|
|
|
|
|
')'
|
|
|
|
|
),
|
2026-02-09 22:06:25 +00:00
|
|
|
|
2026-02-13 20:21:31 +00:00
|
|
|
// Action node: action_name or action_name(params)
|
2026-02-09 22:06:25 +00:00
|
|
|
action_node: $ => choice(
|
|
|
|
|
seq($.identifier, '(', commaSep($.action_param), ')'),
|
|
|
|
|
$.identifier
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
action_param: $ => choice(
|
|
|
|
|
// Named parameter: name: value
|
|
|
|
|
seq($.dotted_path, ':', $.value),
|
|
|
|
|
// Positional parameter: just value
|
|
|
|
|
$.value
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-13 20:21:31 +00:00
|
|
|
// Subtree node: include path::to::subtree
|
|
|
|
|
subtree_node: $ => seq('include', $.path),
|
2026-02-09 22:06:25 +00:00
|
|
|
|
|
|
|
|
// Institution declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
institution_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'institution',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 17:43:26 +00:00
|
|
|
field('body', $.block)
|
2026-02-09 22:06:25 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Relationship declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
relationship_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'relationship',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 17:43:26 +00:00
|
|
|
field('body', $.relationship_body)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
relationship_body: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'{',
|
|
|
|
|
repeat1($.participant),
|
|
|
|
|
repeat($.field),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
participant: $ => choice(
|
2026-02-13 20:21:31 +00:00
|
|
|
// name as role { fields } (role optional)
|
2026-02-09 22:06:25 +00:00
|
|
|
seq($.path, 'as', $.identifier, $.block),
|
2026-02-13 20:21:31 +00:00
|
|
|
// name { fields } (block required)
|
|
|
|
|
seq($.path, $.block)
|
2026-02-09 22:06:25 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Location declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
location_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'location',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 17:43:26 +00:00
|
|
|
field('body', $.block)
|
2026-02-09 22:06:25 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Species declaration
|
2026-02-14 17:43:26 +00:00
|
|
|
species_declaration: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'species',
|
|
|
|
|
field('name', $.identifier),
|
2026-02-14 17:43:26 +00:00
|
|
|
field('body', $.species_body)
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
species_body: $ => seq(
|
2026-02-09 22:06:25 +00:00
|
|
|
'{',
|
|
|
|
|
repeat($.include),
|
2026-02-14 17:43:26 +00:00
|
|
|
repeat($.species_field),
|
2026-02-09 22:06:25 +00:00
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
species_field: $ => choice(
|
|
|
|
|
// Field with range: name: min..max
|
|
|
|
|
seq(
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
':',
|
|
|
|
|
field('value', $.range)
|
|
|
|
|
),
|
|
|
|
|
// Field with single value: name: value
|
|
|
|
|
seq(
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
':',
|
|
|
|
|
field('value', choice($.integer, $.float, $.boolean, $.string))
|
|
|
|
|
),
|
|
|
|
|
$.prose_block
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-09 22:06:25 +00:00
|
|
|
// Enum declaration
|
|
|
|
|
enum_declaration: $ => seq(
|
|
|
|
|
'enum',
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
'{',
|
|
|
|
|
commaSep1($.identifier),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-14 14:29:29 +00:00
|
|
|
// Concept declaration - base type with no structure
|
|
|
|
|
concept_declaration: $ => seq(
|
|
|
|
|
'concept',
|
2026-02-14 17:43:26 +00:00
|
|
|
field('name', $.identifier)
|
2026-02-14 14:29:29 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Sub-concept declaration - enum or record subtype with dot notation
|
|
|
|
|
sub_concept: $ => seq(
|
|
|
|
|
'sub_concept',
|
|
|
|
|
field('parent', $.identifier),
|
|
|
|
|
'.',
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
'{',
|
|
|
|
|
field('body', choice(
|
|
|
|
|
$.sub_concept_record_body,
|
|
|
|
|
$.sub_concept_enum_body
|
|
|
|
|
)),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Enum form: Variant1, Variant2, Variant3
|
|
|
|
|
sub_concept_enum_body: $ => commaSep1($.identifier),
|
|
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
// Record form: field_name: value, field_name: value
|
2026-02-14 14:29:29 +00:00
|
|
|
sub_concept_record_body: $ => commaSep1($.sub_concept_field),
|
|
|
|
|
|
|
|
|
|
sub_concept_field: $ => seq(
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
':',
|
2026-02-14 17:43:26 +00:00
|
|
|
field('value', choice($.integer, $.float, $.string, $.boolean))
|
2026-02-14 14:29:29 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
|
|
any_type: $ => 'any',
|
|
|
|
|
|
|
|
|
|
// Concept comparison - compile-time pattern matching
|
|
|
|
|
concept_comparison: $ => seq(
|
|
|
|
|
'concept_comparison',
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
'{',
|
|
|
|
|
commaSep1($.variant_pattern),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// A named variant with field conditions
|
|
|
|
|
variant_pattern: $ => seq(
|
|
|
|
|
field('name', $.identifier),
|
|
|
|
|
':',
|
|
|
|
|
'{',
|
|
|
|
|
commaSep1($.field_condition),
|
|
|
|
|
'}'
|
|
|
|
|
),
|
|
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
// Field condition: field_name: any OR field_name: Type is Value [or Type is Value]*
|
2026-02-14 14:29:29 +00:00
|
|
|
field_condition: $ => seq(
|
2026-02-14 17:43:26 +00:00
|
|
|
field('name', $.identifier),
|
2026-02-14 14:29:29 +00:00
|
|
|
':',
|
2026-02-14 17:43:26 +00:00
|
|
|
field('condition', choice(
|
|
|
|
|
$.any_type,
|
|
|
|
|
$.is_condition
|
|
|
|
|
))
|
2026-02-14 14:29:29 +00:00
|
|
|
),
|
|
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
// Is condition: Type is Value [or Type is Value]*
|
|
|
|
|
is_condition: $ => seq(
|
|
|
|
|
$.is_value,
|
|
|
|
|
repeat(seq('or', $.is_value))
|
2026-02-14 14:29:29 +00:00
|
|
|
),
|
|
|
|
|
|
2026-02-14 17:43:26 +00:00
|
|
|
is_value: $ => seq(
|
|
|
|
|
field('field', $.identifier),
|
2026-02-14 14:29:29 +00:00
|
|
|
'is',
|
2026-02-14 17:43:26 +00:00
|
|
|
field('value', $.identifier)
|
2026-02-14 14:29:29 +00:00
|
|
|
),
|
|
|
|
|
|
2026-02-09 22:06:25 +00:00
|
|
|
// Expressions (for conditions in life arcs)
|
|
|
|
|
expression: $ => choice(
|
|
|
|
|
$.or_expression,
|
|
|
|
|
$.and_expression,
|
|
|
|
|
$.not_expression,
|
|
|
|
|
$.comparison,
|
|
|
|
|
$.field_access,
|
|
|
|
|
$.primary_expression
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
or_expression: $ => prec.left(1, seq(
|
|
|
|
|
$.expression,
|
|
|
|
|
'or',
|
|
|
|
|
$.expression
|
|
|
|
|
)),
|
|
|
|
|
|
|
|
|
|
and_expression: $ => prec.left(2, seq(
|
|
|
|
|
$.expression,
|
|
|
|
|
'and',
|
|
|
|
|
$.expression
|
|
|
|
|
)),
|
|
|
|
|
|
|
|
|
|
not_expression: $ => prec(3, seq('not', $.expression)),
|
|
|
|
|
|
|
|
|
|
comparison: $ => prec.left(4, seq(
|
|
|
|
|
$.expression,
|
|
|
|
|
field('operator', choice('is', '>', '>=', '<', '<=')),
|
|
|
|
|
$.expression
|
|
|
|
|
)),
|
|
|
|
|
|
|
|
|
|
field_access: $ => prec.left(5, seq(
|
|
|
|
|
$.expression,
|
|
|
|
|
'.',
|
|
|
|
|
$.identifier
|
|
|
|
|
)),
|
|
|
|
|
|
|
|
|
|
primary_expression: $ => choice(
|
|
|
|
|
'self',
|
|
|
|
|
'other',
|
|
|
|
|
$.integer,
|
|
|
|
|
$.float,
|
|
|
|
|
$.string,
|
|
|
|
|
$.boolean,
|
|
|
|
|
$.path
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Identifiers
|
|
|
|
|
identifier: $ => /[a-zA-Z_][a-zA-Z0-9_]*/,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper to create comma-separated lists with optional trailing comma
|
|
|
|
|
*/
|
|
|
|
|
function commaSep(rule) {
|
|
|
|
|
return optional(commaSep1(rule));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function commaSep1(rule) {
|
|
|
|
|
return seq(rule, repeat(seq(',', rule)), optional(','));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper to create separator-based lists (e.g., :: or .)
|
|
|
|
|
*/
|
|
|
|
|
function sep1(rule, separator) {
|
|
|
|
|
return seq(rule, repeat(seq(separator, rule)));
|
|
|
|
|
}
|