Files
storybook/src/syntax/parser.lalrpop
Sienna Meridian Satterwhite 47fafdc2bf feat(lang): complete extends to modifies keyword migration
This commit completes the migration started in the previous commit,
updating all remaining files:

- Lexer: Changed token from Extends to Modifies
- Parser: Updated lalrpop grammar rules and AST field names
- AST: Renamed Schedule.extends field to modifies
- Grammar: Updated tree-sitter grammar.js
- Tree-sitter: Regenerated parser.c and node-types.json
- Examples: Updated baker-family work schedules
- Tests: Updated schedule composition tests and corpus
- Docs: Updated all reference documentation and tutorials
- Validation: Updated error messages and validation logic
- Package: Bumped version to 0.3.1 in all package manifests

All 554 tests pass.
2026-02-16 22:55:04 +00:00

1061 lines
30 KiB
Plaintext

use crate::syntax::ast::*;
use crate::syntax::lexer::Token;
grammar;
// ===== Top-level =====
pub File: File = {
<declarations:Declaration*> => File { declarations }
};
Declaration: Declaration = {
<u:UseDecl> => Declaration::Use(u),
<c:Character> => Declaration::Character(c),
<t:Template> => Declaration::Template(t),
<l:LifeArc> => Declaration::LifeArc(l),
<s:Schedule> => Declaration::Schedule(s),
<b:Behavior> => Declaration::Behavior(b),
<i:Institution> => Declaration::Institution(i),
<r:Relationship> => Declaration::Relationship(r),
<loc:Location> => Declaration::Location(loc),
<sp:Species> => Declaration::Species(sp),
<concept:ConceptDecl> => Declaration::Concept(concept),
<sub:SubConceptDecl> => Declaration::SubConcept(sub),
<comp:ConceptComparisonDecl> => Declaration::ConceptComparison(comp),
};
// ===== Use declarations =====
UseDecl: UseDecl = {
<start:@L> "use" <path:Path> ";" <end:@R> => UseDecl {
path,
kind: UseKind::Single,
span: Span::new(start, end),
},
<start:@L> "use" <base:PathSegments> "::" "{" <items:Comma<Ident>> "}" ";" <end:@R> => UseDecl {
path: base,
kind: UseKind::Grouped(items),
span: Span::new(start, end),
},
<start:@L> "use" <path:PathSegments> "::" "*" ";" <end:@R> => UseDecl {
path,
kind: UseKind::Wildcard,
span: Span::new(start, end),
},
};
Path: Vec<String> = {
<PathSegments>
};
PathSegments: Vec<String> = {
<Ident> => vec![<>],
<mut v:PathSegments> "::" <i:Ident> => {
v.push(i);
v
}
};
DottedPath: Vec<String> = {
<Ident> => vec![<>],
<mut v:DottedPath> "." <i:Ident> => {
v.push(i);
v
}
};
// ===== Character =====
Character: Character = {
<start:@L> "character" <name:Ident> <species:(":" <Ident>)?> <template:TemplateClause?> "{" <body:CharacterBody> "}" <end:@R> => {
Character {
name,
species,
fields: body.0,
template,
uses_behaviors: body.1,
uses_schedule: body.2,
span: Span::new(start, end),
}
}
};
// Character body can contain fields and uses clauses in any order
CharacterBody: (Vec<Field>, Option<Vec<BehaviorLink>>, Option<Vec<String>>) = {
<items:CharacterBodyItem*> => {
let mut fields = Vec::new();
let mut uses_behaviors = None;
let mut uses_schedule = None;
for item in items {
match item {
CharacterBodyItem::Field(f) => fields.push(f),
CharacterBodyItem::UsesBehaviors(b) => uses_behaviors = Some(b),
CharacterBodyItem::UsesSchedule(s) => uses_schedule = Some(s),
}
}
(fields, uses_behaviors, uses_schedule)
}
};
CharacterBodyItem: CharacterBodyItem = {
<Field> => CharacterBodyItem::Field(<>),
<UsesBehaviorsClause> => CharacterBodyItem::UsesBehaviors(<>),
<UsesScheduleClause> => CharacterBodyItem::UsesSchedule(<>),
};
TemplateClause: Vec<String> = {
"from" <t:Ident> <rest:("," <Ident>)*> => {
let mut templates = vec![t];
templates.extend(rest);
templates
}
};
// uses behaviors: [...]
UsesBehaviorsClause: Vec<BehaviorLink> = {
"uses" "behaviors" ":" "[" <links:Comma<BehaviorLinkItem>> "]" => links,
};
// Individual behavior link: { tree: BehaviorName, priority: high, when: condition }
BehaviorLinkItem: BehaviorLink = {
<start:@L> "{" <fields:BehaviorLinkField+> "}" <end:@R> => {
let mut tree = None;
let mut condition = None;
let mut priority = Priority::Normal;
for field in fields {
match field {
BehaviorLinkField::Tree(t) => tree = Some(t),
BehaviorLinkField::Condition(c) => condition = Some(c),
BehaviorLinkField::Priority(p) => priority = p,
}
}
BehaviorLink {
tree: tree.expect("behavior link must have 'tree' field"),
condition,
priority,
span: Span::new(start, end),
}
}
};
// Fields within a behavior link
BehaviorLinkField: BehaviorLinkField = {
"tree" ":" <path:Path> ","? => BehaviorLinkField::Tree(path),
"when" ":" <expr:Expr> ","? => BehaviorLinkField::Condition(expr),
"priority" ":" <p:PriorityLevel> ","? => BehaviorLinkField::Priority(p),
};
PriorityLevel: Priority = {
<s:Ident> => match s.as_str() {
"low" => Priority::Low,
"normal" => Priority::Normal,
"high" => Priority::High,
"critical" => Priority::Critical,
_ => Priority::Normal, // Default to normal for invalid values
},
};
// uses schedule: ScheduleName or uses schedules: [Name1, Name2]
UsesScheduleClause: Vec<String> = {
"uses" "schedule" ":" <name:Ident> => vec![name],
"uses" "schedules" ":" "[" <names:Comma<Ident>> "]" => names,
};
// ===== Template =====
Template: Template = {
<start:@L> "template" <name:Ident> <species_base:(":" <Ident>)?> <strict:"strict"?> "{" <body:TemplateBodyItem*> "}" <end:@R> => {
let mut fields = Vec::new();
let mut includes = Vec::new();
let mut uses_behaviors = None;
let mut uses_schedule = None;
for item in body {
match item {
TemplateBodyItem::Field(f) => fields.push(f),
TemplateBodyItem::Include(inc) => includes.push(inc),
TemplateBodyItem::UsesBehaviors(b) => uses_behaviors = Some(b),
TemplateBodyItem::UsesSchedule(s) => uses_schedule = Some(s),
}
}
Template {
name,
species_base,
fields,
strict: strict.is_some(),
includes,
uses_behaviors,
uses_schedule,
span: Span::new(start, end),
}
}
};
// Template body items (fields, includes, uses behaviors, uses schedule)
TemplateBodyItem: TemplateBodyItem = {
<Field> => TemplateBodyItem::Field(<>),
"include" <name:Ident> => TemplateBodyItem::Include(name),
<TemplateUsesBehaviorsClause> => TemplateBodyItem::UsesBehaviors(<>),
<TemplateUsesScheduleClause> => TemplateBodyItem::UsesSchedule(<>),
};
// Template-level behavior links (simple list, no priorities/conditions)
TemplateUsesBehaviorsClause: Vec<BehaviorLink> = {
<start:@L> "uses" "behaviors" ":" <first:Ident> <rest:("," <Ident>)*> <end:@R> => {
let mut names = vec![first];
names.extend(rest);
let span = Span::new(start, end);
names.into_iter().map(|name| BehaviorLink {
tree: vec![name],
condition: None,
priority: Priority::Normal,
span: span.clone(),
}).collect()
},
};
// Template-level schedule links
TemplateUsesScheduleClause: Vec<String> = {
"uses" "schedule" ":" <name:Ident> => vec![name],
};
// Template/Species include clause
Include: String = {
"include" <name:Ident> => name
};
// ===== Fields =====
Field: Field = {
<start:@L> <path:DottedPath> ":" <value:Value> <end:@R> => Field {
name: path.join("."),
value,
span: Span::new(start, end),
},
<start:@L> <pb:ProseBlock> <end:@R> => Field {
name: pb.tag.clone(),
value: Value::ProseBlock(pb),
span: Span::new(start, end),
}
};
Value: Value = {
<NumberLit> => Value::Number(<>),
<DecimalLit> => Value::Decimal(<>),
<TextLit> => Value::Text(<>),
<BoolLit> => Value::Boolean(<>),
"any" => Value::Any,
<lo:NumberLit> ".." <hi:NumberLit> => Value::Range(
Box::new(Value::Number(lo)),
Box::new(Value::Number(hi))
),
<lo:DecimalLit> ".." <hi:DecimalLit> => Value::Range(
Box::new(Value::Decimal(lo)),
Box::new(Value::Decimal(hi))
),
<t:Time> => Value::Time(t),
<d:Duration> => Value::Duration(d),
<p:Path> => Value::Identifier(p),
<ProseBlock> => Value::ProseBlock(<>),
"[" <values:Comma<Value>> "]" => Value::List(values),
"{" <fields:Field*> "}" => Value::Object(fields),
<Override> => Value::Override(<>),
};
BoolLit: bool = {
"true" => true,
"false" => false,
};
Time: Time = {
<s:TimeLit> => {
let parts: Vec<&str> = s.split(':').collect();
let hour = parts[0].parse().unwrap_or(0);
let minute = parts[1].parse().unwrap_or(0);
let second = if parts.len() > 2 {
parts[2].parse().unwrap_or(0)
} else {
0
};
Time { hour, minute, second }
}
};
Duration: Duration = {
<s:DurationLit> => {
let mut hours = 0;
let mut minutes = 0;
let mut seconds = 0;
let mut num = String::new();
for ch in s.chars() {
if ch.is_ascii_digit() {
num.push(ch);
} else {
let val: u32 = num.parse().unwrap_or(0);
match ch {
'h' => hours = val,
'm' => minutes = val,
's' => seconds = val,
_ => {}
}
num.clear();
}
}
Duration { hours, minutes, seconds }
}
};
// Duration string for decorator timeouts/cooldowns (e.g., "5s", "30m", "2h", "1d")
BehaviorDurationLit: String = {
<s:DurationLit> => s
};
ProseBlock: ProseBlock = {
ProseBlockToken
};
Override: Override = {
<start:@L> "@" <base:Path> "{" <overrides:OverrideOp*> "}" <end:@R> => Override {
base,
overrides,
span: Span::new(start, end),
}
};
OverrideOp: OverrideOp = {
"remove" <name:Ident> => OverrideOp::Remove(name),
"append" <f:Field> => OverrideOp::Append(f),
<f:Field> => OverrideOp::Set(f),
};
// ===== Life Arc =====
LifeArc: LifeArc = {
<start:@L> "life_arc" <name:Ident> <reqs:RequiresClause?> "{" <fields:Field*> <states:ArcState*> "}" <end:@R> => LifeArc {
name,
required_fields: reqs.unwrap_or_default(),
states,
span: Span::new(start, end),
}
};
RequiresClause: Vec<FieldRequirement> = {
"requires" "{" <reqs:Comma<FieldReq>> "}" => reqs,
};
FieldReq: FieldRequirement = {
<start:@L> <name:Ident> ":" <type_name:Ident> <end:@R> => FieldRequirement {
name,
type_name,
span: Span::new(start, end),
}
};
ArcState: ArcState = {
<start:@L> "state" <name:Ident> "{" <on_enter:OnEnter?> <fields:Field*> <transitions:Transition*> "}" <end:@R> => ArcState {
name,
on_enter,
transitions,
span: Span::new(start, end),
}
};
OnEnter: Vec<Field> = {
"on" "enter" "{" <fields:Field*> "}" => fields
};
Transition: Transition = {
<start:@L> "on" <cond:Expr> "->" <to:Ident> <end:@R> => Transition {
to,
condition: cond,
span: Span::new(start, end),
}
};
// ===== Schedule =====
Schedule: Schedule = {
// Simple schedule: schedule Name { ... }
<start:@L> "schedule" <name:Ident> "{" <body:ScheduleBody> "}" <end:@R> => Schedule {
name,
modifies: None,
fields: body.0,
blocks: body.1,
recurrences: body.2,
span: Span::new(start, end),
},
// Modifying schedule: schedule Name modifies Base { ... }
<start:@L> "schedule" <name:Ident> "modifies" <base:Ident> "{" <body:ScheduleBody> "}" <end:@R> => Schedule {
name,
modifies: Some(base),
fields: body.0,
blocks: body.1,
recurrences: body.2,
span: Span::new(start, end),
}
};
// Schedule body can contain fields (prose blocks), blocks, and recurrence patterns
ScheduleBody: (Vec<Field>, Vec<ScheduleBlock>, Vec<RecurrencePattern>) = {
<items:ScheduleBodyItem*> => {
let mut fields = Vec::new();
let mut blocks = Vec::new();
let mut recurrences = Vec::new();
for item in items {
match item {
ScheduleBodyItem::Field(f) => fields.push(f),
ScheduleBodyItem::Block(b) => blocks.push(b),
ScheduleBodyItem::Recurrence(r) => recurrences.push(r),
}
}
(fields, blocks, recurrences)
}
};
ScheduleBodyItem: ScheduleBodyItem = {
<Field> => ScheduleBodyItem::Field(<>),
<ScheduleBlock> => ScheduleBodyItem::Block(<>),
<RecurrencePattern> => ScheduleBodyItem::Recurrence(<>),
};
ScheduleBlock: ScheduleBlock = {
// Legacy syntax: time -> time : activity { fields }
<s:@L> <start:Time> "->" <end:Time> ":" <activity:Ident> "{" <fields:Field*> "}" <e:@R> => ScheduleBlock {
name: None,
is_override: false,
start,
end,
activity,
action: None,
temporal_constraint: None,
fields,
span: Span::new(s, e),
},
// Named block: block name { time, action, fields }
<s:@L> "block" <name:Ident> "{" <content:BlockContent> "}" <e:@R> => ScheduleBlock {
name: Some(name),
is_override: false,
start: content.0,
end: content.1,
activity: String::new(), // Empty for new syntax
action: content.2,
temporal_constraint: None,
fields: content.3,
span: Span::new(s, e),
},
// Override block: override name { time, action, fields }
<s:@L> "override" <name:Ident> "{" <content:BlockContent> "}" <e:@R> => ScheduleBlock {
name: Some(name),
is_override: true,
start: content.0,
end: content.1,
activity: String::new(), // Empty for new syntax
action: content.2,
temporal_constraint: None,
fields: content.3,
span: Span::new(s, e),
}
};
// Block content: time range, optional action, and fields
BlockContent: (Time, Time, Option<Vec<String>>, Vec<Field>) = {
<items:BlockContentItem+> => {
let mut start = None;
let mut end = None;
let mut action = None;
let mut fields = Vec::new();
for item in items {
match item {
BlockContentItem::TimeRange(s, e) => {
start = Some(s);
end = Some(e);
}
BlockContentItem::Field(f) => {
if f.name == "action" {
// Extract action as qualified path from identifier value
if let Value::Identifier(path) = &f.value {
action = Some(path.clone());
}
} else {
fields.push(f);
}
}
}
}
(
start.expect("block must have time range"),
end.expect("block must have time range"),
action,
fields
)
}
};
BlockContentItem: BlockContentItem = {
<start:Time> "->" <end:Time> ","? => BlockContentItem::TimeRange(start, end),
<Field> => BlockContentItem::Field(<>),
};
// Recurrence pattern: recurrence Name on DayOfWeek { blocks }
RecurrencePattern: RecurrencePattern = {
<start:@L> "recurrence" <name:Ident> "on" <day:Ident> "{" <blocks:ScheduleBlock+> "}" <end:@R> => RecurrencePattern {
name,
constraint: TemporalConstraint::DayOfWeek(day),
blocks,
span: Span::new(start, end),
}
};
// ===== Behavior Trees =====
Behavior: Behavior = {
<start:@L> "behavior" <name:Ident> "{" <fields:Field*> <root:BehaviorNode> "}" <end:@R> => Behavior {
name,
root,
span: Span::new(start, end),
}
};
BehaviorNode: BehaviorNode = {
<SelectorNode>,
<SequenceNode>,
<ConditionNode>,
<DecoratorNode>,
<ActionNode>,
<SubTreeNode>,
};
// Selector node: choose { ... } or choose label { ... }
SelectorNode: BehaviorNode = {
"choose" <label:Ident?> "{" <children:BehaviorNode+> "}" => BehaviorNode::Selector {
label,
children,
},
};
// Sequence node: then { ... } or then label { ... }
SequenceNode: BehaviorNode = {
"then" <label:Ident?> "{" <children:BehaviorNode+> "}" => BehaviorNode::Sequence {
label,
children,
},
};
// Condition node: if(expr) or when(expr)
// if(expr) { child } is the decorator form (replaces old "guard" keyword)
ConditionNode: BehaviorNode = {
"if" "(" <condition:Expr> ")" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::If(condition),
child: Box::new(child),
},
"if" "(" <condition:Expr> ")" => BehaviorNode::Condition(condition),
"when" "(" <condition:Expr> ")" => BehaviorNode::Condition(condition),
};
// Decorator node: keyword [params] { child }
DecoratorNode: BehaviorNode = {
<DecoratorRepeat>,
<DecoratorRepeatN>,
<DecoratorRepeatRange>,
<DecoratorInvert>,
<DecoratorRetry>,
<DecoratorTimeout>,
<DecoratorCooldown>,
<DecoratorSucceedAlways>,
<DecoratorFailAlways>,
};
DecoratorRepeat: BehaviorNode = {
"repeat" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::Repeat,
child: Box::new(child),
},
};
DecoratorRepeatN: BehaviorNode = {
"repeat" "(" <n:NumberLit> ")" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::RepeatN(n as u32),
child: Box::new(child),
},
};
DecoratorRepeatRange: BehaviorNode = {
"repeat" "(" <min:NumberLit> ".." <max:NumberLit> ")" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::RepeatRange(min as u32, max as u32),
child: Box::new(child),
},
};
DecoratorInvert: BehaviorNode = {
"invert" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::Invert,
child: Box::new(child),
},
};
DecoratorRetry: BehaviorNode = {
"retry" "(" <n:NumberLit> ")" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::Retry(n as u32),
child: Box::new(child),
},
};
DecoratorTimeout: BehaviorNode = {
"timeout" "(" <duration:BehaviorDurationLit> ")" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::Timeout(duration),
child: Box::new(child),
},
};
DecoratorCooldown: BehaviorNode = {
"cooldown" "(" <duration:BehaviorDurationLit> ")" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::Cooldown(duration),
child: Box::new(child),
},
};
DecoratorSucceedAlways: BehaviorNode = {
"succeed_always" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::SucceedAlways,
child: Box::new(child),
},
};
DecoratorFailAlways: BehaviorNode = {
"fail_always" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::FailAlways,
child: Box::new(child),
},
};
// Action node: action_name or action_name(params)
ActionNode: BehaviorNode = {
<name:Ident> "(" <params:Comma<ActionParam>> ")" => BehaviorNode::Action(name, params),
<name:Ident> => BehaviorNode::Action(name, vec![]),
};
ActionParam: Field = {
// Named parameter: field: value
<start:@L> <path:DottedPath> ":" <value:Value> <end:@R> => Field {
name: path.join("."),
value,
span: Span::new(start, end),
},
// Positional parameter: just a value (use empty string as field name)
<start:@L> <value:Value> <end:@R> => Field {
name: String::new(),
value,
span: Span::new(start, end),
},
};
// Subtree node: include path::to::subtree
SubTreeNode: BehaviorNode = {
"include" <path:Path> => BehaviorNode::SubTree(path),
};
// ===== Institution =====
Institution: Institution = {
<start:@L> "institution" <name:Ident> "{" <body:InstitutionBody> "}" <end:@R> => {
Institution {
name,
fields: body.0,
uses_behaviors: body.1,
uses_schedule: body.2,
span: Span::new(start, end),
}
}
};
// Institution body can contain fields and uses clauses in any order
InstitutionBody: (Vec<Field>, Option<Vec<BehaviorLink>>, Option<Vec<String>>) = {
<items:InstitutionBodyItem*> => {
let mut fields = Vec::new();
let mut uses_behaviors = None;
let mut uses_schedule = None;
for item in items {
match item {
InstitutionBodyItem::Field(f) => fields.push(f),
InstitutionBodyItem::UsesBehaviors(b) => uses_behaviors = Some(b),
InstitutionBodyItem::UsesSchedule(s) => uses_schedule = Some(s),
}
}
(fields, uses_behaviors, uses_schedule)
}
};
InstitutionBodyItem: InstitutionBodyItem = {
<Field> => InstitutionBodyItem::Field(<>),
<UsesBehaviorsClause> => InstitutionBodyItem::UsesBehaviors(<>),
<UsesScheduleClause> => InstitutionBodyItem::UsesSchedule(<>),
};
// ===== Relationship =====
Relationship: Relationship = {
<start:@L> "relationship" <name:Ident> "{" <participants:Participant+> <fields:Field*> "}" <end:@R> => Relationship {
name,
participants,
fields,
span: Span::new(start, end),
}
};
Participant: Participant = {
// Participant with role and block (block required)
<start:@L> <name:Path> "as" <role:Ident> "{" <fields:Field*> "}" <end:@R> => Participant {
name,
role: Some(role),
fields,
span: Span::new(start, end),
},
// Participant without role (block still required)
<start:@L> <name:Path> "{" <fields:Field*> "}" <end:@R> => Participant {
name,
role: None,
fields,
span: Span::new(start, end),
},
};
// ===== Location =====
Location: Location = {
<start:@L> "location" <name:Ident> "{" <fields:Field*> "}" <end:@R> => Location {
name,
fields,
span: Span::new(start, end),
}
};
// ===== Species =====
Species: Species = {
<start:@L> "species" <name:Ident> "{" <includes:Include*> <fields:Field*> "}" <end:@R> => Species {
name,
includes,
fields,
span: Span::new(start, end),
}
};
// ===== Enum =====
// ===== Type System Declarations =====
ConceptDecl: ConceptDecl = {
<start:@L> "concept" <name:Ident> <end:@R> => ConceptDecl {
name,
span: Span::new(start, end),
}
};
SubConceptDecl: SubConceptDecl = {
// Enum-like sub_concept: sub_concept Cup.Type { Small, Medium, Large }
<start:@L> "sub_concept" <parent:Ident> "." <name:Ident> "{" <variants:Comma<Ident>> "}" <end:@R> => {
SubConceptDecl {
name,
parent_concept: parent,
kind: SubConceptKind::Enum { variants },
span: Span::new(start, end),
}
},
// Record-like sub_concept with at least one field: sub_concept Cup.Material { weight: 100 }
<start:@L> "sub_concept" <parent:Ident> "." <name:Ident> "{" <first:Ident> ":" <first_val:Value> <rest:("," <Ident> ":" <Value>)*> ","? "}" <end:@R> => {
let field_span = Span::new(start, end);
let mut fields = vec![Field {
name: first,
value: first_val,
span: field_span.clone(),
}];
for (field_name, field_val) in rest {
fields.push(Field {
name: field_name,
value: field_val,
span: field_span.clone(),
});
}
SubConceptDecl {
name,
parent_concept: parent,
kind: SubConceptKind::Record { fields },
span: Span::new(start, end),
}
},
};
ConceptComparisonDecl: ConceptComparisonDecl = {
<start:@L> "concept_comparison" <name:Ident> "{" <variants:Comma<VariantPattern>> "}" <end:@R> => ConceptComparisonDecl {
name,
variants,
span: Span::new(start, end),
}
};
VariantPattern: VariantPattern = {
<start:@L> <name:Ident> ":" "{" <conditions:Comma<FieldCondition>> "}" <end:@R> => VariantPattern {
name,
conditions,
span: Span::new(start, end),
}
};
FieldCondition: FieldCondition = {
<start:@L> <field:Ident> ":" "any" <end:@R> => FieldCondition {
field_name: field,
condition: Condition::Any,
span: Span::new(start, end),
},
<start:@L> <field:Ident> ":" <cond:IsCondition> <end:@R> => FieldCondition {
field_name: field,
condition: Condition::Is(cond),
span: Span::new(start, end),
},
};
// Parse "FieldName is Value1 or FieldName is Value2" and extract the values
IsCondition: Vec<String> = {
<first:IsValue> <rest:("or" <IsValue>)*> => {
let mut values = vec![first];
values.extend(rest);
values
}
};
IsValue: String = {
<field:Ident> "is" <value:Ident> => value
};
// ===== Expressions =====
// Expression grammar with proper precedence:
// or > and > not > field_access > comparison > term
Expr: Expr = {
<OrExpr>,
};
// Logical OR (lowest precedence)
OrExpr: Expr = {
<left:OrExpr> "or" <right:AndExpr> => {
Expr::Logical(
Box::new(left),
LogicalOp::Or,
Box::new(right)
)
},
<AndExpr>,
};
// Logical AND
AndExpr: Expr = {
<left:AndExpr> "and" <right:NotExpr> => {
Expr::Logical(
Box::new(left),
LogicalOp::And,
Box::new(right)
)
},
<NotExpr>,
};
// Unary NOT
NotExpr: Expr = {
"not" <expr:NotExpr> => {
Expr::Unary(
UnaryOp::Not,
Box::new(expr)
)
},
<ComparisonExpr>,
};
// Comparison expressions
ComparisonExpr: Expr = {
// Equality: field access or path is (literal or identifier)
<left:FieldAccessExpr> "is" <right:FieldAccessExpr> => {
Expr::Comparison(
Box::new(left),
CompOp::Eq,
Box::new(right)
)
},
// Comparison: field access or path > literal/identifier, etc.
<left:FieldAccessExpr> <op:InequalityOp> <right:FieldAccessExpr> => {
Expr::Comparison(
Box::new(left),
op,
Box::new(right)
)
},
// Just a field access expression
<FieldAccessExpr>,
};
// Field access with dot notation (binds tightest)
FieldAccessExpr: Expr = {
<base:FieldAccessExpr> "." <field:Ident> => {
Expr::FieldAccess(
Box::new(base),
field
)
},
<PrimaryExpr>,
};
// Primary expressions (atoms)
PrimaryExpr: Expr = {
"self" => Expr::Identifier(vec!["self".to_string()]),
"other" => Expr::Identifier(vec!["other".to_string()]),
<Literal>,
<Path> => Expr::Identifier(<>),
};
InequalityOp: CompOp = {
">" => CompOp::Gt,
">=" => CompOp::Ge,
"<" => CompOp::Lt,
"<=" => CompOp::Le,
};
Literal: Expr = {
<NumberLit> => Expr::NumberLit(<>),
<DecimalLit> => Expr::DecimalLit(<>),
<TextLit> => Expr::TextLit(<>),
<BoolLit> => Expr::BooleanLit(<>),
};
// ===== Helpers =====
Comma<T>: Vec<T> = {
<v:(<T> ",")*> <e:T?> => match e {
None => v,
Some(e) => {
let mut v = v;
v.push(e);
v
}
}
};
// ===== Token conversion =====
extern {
type Location = usize;
type Error = crate::syntax::ParseError;
enum Token {
// Keywords
"use" => Token::Use,
"character" => Token::Character,
"template" => Token::Template,
"life_arc" => Token::LifeArc,
"schedule" => Token::Schedule,
"behavior" => Token::Behavior,
"institution" => Token::Institution,
"relationship" => Token::Relationship,
"location" => Token::Location,
"species" => Token::Species,
"concept" => Token::Concept,
"sub_concept" => Token::SubConcept,
"concept_comparison" => Token::ConceptComparison,
"any" => Token::Any,
"requires" => Token::Requires,
"state" => Token::State,
"on" => Token::On,
"enter" => Token::Enter,
"as" => Token::As,
"self" => Token::SelfKw,
"other" => Token::Other,
"remove" => Token::Remove,
"append" => Token::Append,
"forall" => Token::ForAll,
"exists" => Token::Exists,
"in" => Token::In,
"where" => Token::Where,
"and" => Token::And,
"or" => Token::Or,
"not" => Token::Not,
"strict" => Token::Strict,
"include" => Token::Include,
"from" => Token::From,
"is" => Token::Is,
"uses" => Token::Uses,
"behaviors" => Token::Behaviors,
"schedules" => Token::Schedules,
"tree" => Token::Tree,
"priority" => Token::Priority,
"modifies" => Token::Modifies,
"override" => Token::Override,
"recurrence" => Token::Recurrence,
"season" => Token::Season,
"block" => Token::Block,
"true" => Token::True,
"false" => Token::False,
// Behavior tree keywords
"choose" => Token::Choose,
"then" => Token::Then,
"if" => Token::If,
"when" => Token::When,
"repeat" => Token::Repeat,
"invert" => Token::Invert,
"retry" => Token::Retry,
"timeout" => Token::Timeout,
"cooldown" => Token::Cooldown,
"succeed_always" => Token::SucceedAlways,
"fail_always" => Token::FailAlways,
// Literals
Ident => Token::Ident(<String>),
NumberLit => Token::NumberLit(<i64>),
DecimalLit => Token::DecimalLit(<f64>),
TextLit => Token::TextLit(<String>),
TimeLit => Token::TimeLit(<String>),
DurationLit => Token::DurationLit(<String>),
ProseBlockToken => Token::ProseBlock(<ProseBlock>),
// Punctuation
"{" => Token::LBrace,
"}" => Token::RBrace,
"(" => Token::LParen,
")" => Token::RParen,
"[" => Token::LBracket,
"]" => Token::RBracket,
":" => Token::Colon,
"::" => Token::ColonColon,
";" => Token::Semicolon,
"," => Token::Comma,
"." => Token::Dot,
".." => Token::DotDot,
"*" => Token::Star,
"?" => Token::Question,
"@" => Token::At,
// Operators
">" => Token::Gt,
">=" => Token::Ge,
"<" => Token::Lt,
"<=" => Token::Le,
"->" => Token::Arrow,
}
}