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.
1061 lines
30 KiB
Plaintext
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,
|
|
}
|
|
}
|