feat(parser): add action parameters, repeater decorator, and named participants
Add syntax enhancements for more expressive behavior trees and relationships.
Action parameters:
- Support typed/positional parameters: WaitDuration(2.0)
- Support named parameters: SetValue(field: value)
- Enable inline values without field names
Repeater decorator:
- Add * { node } syntax for repeating behavior nodes
- Maps to Decorator("repeat", node)
Named participant blocks:
- Replace self/other blocks with named participant syntax
- Support multi-party relationships
- Example: Alice { role: seeker } instead of self { role: seeker }
Schedule block syntax:
- Require braces for schedule blocks to support narrative fields
- Update tests to use new syntax: 04:00 -> 06:00: Activity { }
This commit is contained in:
@@ -52,6 +52,7 @@ pub enum UseKind {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Character {
|
||||
pub name: String,
|
||||
pub species: Option<String>, // `: Species` - what the character fundamentally is
|
||||
pub fields: Vec<Field>,
|
||||
pub template: Option<Vec<String>>, // `from Template1, Template2`
|
||||
pub span: Span,
|
||||
@@ -142,6 +143,7 @@ pub struct LifeArc {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ArcState {
|
||||
pub name: String,
|
||||
pub on_enter: Option<Vec<Field>>,
|
||||
pub transitions: Vec<Transition>,
|
||||
pub span: Span,
|
||||
}
|
||||
@@ -166,6 +168,7 @@ pub struct ScheduleBlock {
|
||||
pub start: Time,
|
||||
pub end: Time,
|
||||
pub activity: String,
|
||||
pub fields: Vec<Field>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
@@ -225,6 +228,7 @@ pub struct Location {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Species {
|
||||
pub name: String,
|
||||
pub includes: Vec<String>,
|
||||
pub fields: Vec<Field>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ pub enum Token {
|
||||
State,
|
||||
#[token("on")]
|
||||
On,
|
||||
#[token("enter")]
|
||||
Enter,
|
||||
#[token("as")]
|
||||
As,
|
||||
#[token("self")]
|
||||
@@ -180,6 +182,7 @@ pub struct Lexer<'a> {
|
||||
position: usize,
|
||||
state: LexerState,
|
||||
normal_lexer: Option<logos::Lexer<'a, Token>>,
|
||||
lexer_base_offset: usize, // Offset of the substring that normal_lexer is lexing
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
@@ -189,6 +192,7 @@ impl<'a> Lexer<'a> {
|
||||
position: 0,
|
||||
state: LexerState::Normal,
|
||||
normal_lexer: Some(Token::lexer(source)),
|
||||
lexer_base_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +261,7 @@ impl<'a> Lexer<'a> {
|
||||
let start = content_start.saturating_sub(tag.len() + 4); // Include opening ---tag
|
||||
self.position = content_end + 3; // Skip closing ---
|
||||
self.state = LexerState::Normal;
|
||||
self.lexer_base_offset = self.position; // Update base offset for new substring
|
||||
self.normal_lexer = Some(Token::lexer(&self.source[self.position..]));
|
||||
|
||||
let prose_block = super::ast::ProseBlock {
|
||||
@@ -295,19 +300,25 @@ impl<'a> Iterator for Lexer<'a> {
|
||||
match token {
|
||||
| Ok(Token::ProseMarker) => {
|
||||
// Switch to prose mode
|
||||
let marker_pos = span.start;
|
||||
self.position = marker_pos;
|
||||
// span is relative to the substring that logos is lexing; add base offset
|
||||
self.position = self.lexer_base_offset + span.start;
|
||||
self.state = LexerState::ProseTag;
|
||||
self.normal_lexer = None;
|
||||
self.scan_prose_tag()
|
||||
},
|
||||
| Ok(tok) => {
|
||||
self.position = span.end;
|
||||
Some((span.start, tok, span.end))
|
||||
// Adjust span to be relative to original source
|
||||
let absolute_start = self.lexer_base_offset + span.start;
|
||||
let absolute_end = self.lexer_base_offset + span.end;
|
||||
self.position = absolute_end;
|
||||
Some((absolute_start, tok, absolute_end))
|
||||
},
|
||||
| Err(_) => {
|
||||
self.position = span.end;
|
||||
Some((span.start, Token::Error, span.end))
|
||||
// Adjust span to be relative to original source
|
||||
let absolute_start = self.lexer_base_offset + span.start;
|
||||
let absolute_end = self.lexer_base_offset + span.end;
|
||||
self.position = absolute_end;
|
||||
Some((absolute_start, Token::Error, absolute_end))
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -393,6 +404,38 @@ The bakery had a no-nonsense policy.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_prose_blocks() {
|
||||
let input = r#"
|
||||
---description
|
||||
First prose block content.
|
||||
---
|
||||
---details
|
||||
Second prose block content.
|
||||
---
|
||||
"#;
|
||||
let lexer = Lexer::new(input);
|
||||
let tokens: Vec<Token> = lexer.map(|(_, tok, _)| tok).collect();
|
||||
|
||||
assert_eq!(tokens.len(), 2, "Should have exactly 2 prose block tokens");
|
||||
|
||||
match &tokens[0] {
|
||||
| Token::ProseBlock(pb) => {
|
||||
assert_eq!(pb.tag, "description");
|
||||
assert!(pb.content.contains("First prose block"));
|
||||
},
|
||||
| _ => panic!("Expected first ProseBlock, got {:?}", tokens[0]),
|
||||
}
|
||||
|
||||
match &tokens[1] {
|
||||
| Token::ProseBlock(pb) => {
|
||||
assert_eq!(pb.tag, "details");
|
||||
assert!(pb.content.contains("Second prose block"));
|
||||
},
|
||||
| _ => panic!("Expected second ProseBlock, got {:?}", tokens[1]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_duration_literals() {
|
||||
let input = "08:30 14:45:00 2h30m 45m";
|
||||
|
||||
@@ -55,11 +55,20 @@ PathSegments: Vec<String> = {
|
||||
}
|
||||
};
|
||||
|
||||
DottedPath: Vec<String> = {
|
||||
<Ident> => vec![<>],
|
||||
<mut v:DottedPath> "." <i:Ident> => {
|
||||
v.push(i);
|
||||
v
|
||||
}
|
||||
};
|
||||
|
||||
// ===== Character =====
|
||||
|
||||
Character: Character = {
|
||||
"character" <name:Ident> <template:TemplateClause?> "{" <fields:Field*> "}" => Character {
|
||||
"character" <name:Ident> <species:(":" <Ident>)?> <template:TemplateClause?> "{" <fields:Field*> "}" => Character {
|
||||
name,
|
||||
species,
|
||||
fields,
|
||||
template,
|
||||
span: Span::new(0, 0),
|
||||
@@ -93,10 +102,15 @@ Include: String = {
|
||||
// ===== Fields =====
|
||||
|
||||
Field: Field = {
|
||||
<name:Ident> ":" <value:Value> => Field {
|
||||
name,
|
||||
<path:DottedPath> ":" <value:Value> => Field {
|
||||
name: path.join("."),
|
||||
value,
|
||||
span: Span::new(0, 0),
|
||||
},
|
||||
<pb:ProseBlock> => Field {
|
||||
name: pb.tag.clone(),
|
||||
value: Value::ProseBlock(pb),
|
||||
span: Span::new(0, 0),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -188,7 +202,7 @@ OverrideOp: OverrideOp = {
|
||||
// ===== Life Arc =====
|
||||
|
||||
LifeArc: LifeArc = {
|
||||
"life_arc" <name:Ident> "{" <states:ArcState*> "}" => LifeArc {
|
||||
"life_arc" <name:Ident> "{" <fields:Field*> <states:ArcState*> "}" => LifeArc {
|
||||
name,
|
||||
states,
|
||||
span: Span::new(0, 0),
|
||||
@@ -196,13 +210,18 @@ LifeArc: LifeArc = {
|
||||
};
|
||||
|
||||
ArcState: ArcState = {
|
||||
"state" <name:Ident> "{" <transitions:Transition*> "}" => ArcState {
|
||||
"state" <name:Ident> "{" <on_enter:OnEnter?> <fields:Field*> <transitions:Transition*> "}" => ArcState {
|
||||
name,
|
||||
on_enter,
|
||||
transitions,
|
||||
span: Span::new(0, 0),
|
||||
}
|
||||
};
|
||||
|
||||
OnEnter: Vec<Field> = {
|
||||
"on" "enter" "{" <fields:Field*> "}" => fields
|
||||
};
|
||||
|
||||
Transition: Transition = {
|
||||
"on" <cond:Expr> "->" <to:Ident> => Transition {
|
||||
to,
|
||||
@@ -214,7 +233,7 @@ Transition: Transition = {
|
||||
// ===== Schedule =====
|
||||
|
||||
Schedule: Schedule = {
|
||||
"schedule" <name:Ident> "{" <blocks:ScheduleBlock*> "}" => Schedule {
|
||||
"schedule" <name:Ident> "{" <fields:Field*> <blocks:ScheduleBlock*> "}" => Schedule {
|
||||
name,
|
||||
blocks,
|
||||
span: Span::new(0, 0),
|
||||
@@ -222,10 +241,11 @@ Schedule: Schedule = {
|
||||
};
|
||||
|
||||
ScheduleBlock: ScheduleBlock = {
|
||||
<start:Time> "->" <end:Time> ":" <activity:Ident> => ScheduleBlock {
|
||||
<start:Time> "->" <end:Time> ":" <activity:Ident> "{" <fields:Field*> "}" => ScheduleBlock {
|
||||
start,
|
||||
end,
|
||||
activity,
|
||||
fields,
|
||||
span: Span::new(0, 0),
|
||||
}
|
||||
};
|
||||
@@ -233,7 +253,7 @@ ScheduleBlock: ScheduleBlock = {
|
||||
// ===== Behavior Trees =====
|
||||
|
||||
Behavior: Behavior = {
|
||||
"behavior" <name:Ident> "{" <root:BehaviorNode> "}" => Behavior {
|
||||
"behavior" <name:Ident> "{" <fields:Field*> <root:BehaviorNode> "}" => Behavior {
|
||||
name,
|
||||
root,
|
||||
span: Span::new(0, 0),
|
||||
@@ -243,6 +263,7 @@ Behavior: Behavior = {
|
||||
BehaviorNode: BehaviorNode = {
|
||||
<SelectorNode>,
|
||||
<SequenceNode>,
|
||||
<RepeatNode>,
|
||||
<ActionNode>,
|
||||
<SubTreeNode>,
|
||||
};
|
||||
@@ -255,11 +276,30 @@ SequenceNode: BehaviorNode = {
|
||||
">" "{" <nodes:BehaviorNode+> "}" => BehaviorNode::Sequence(nodes),
|
||||
};
|
||||
|
||||
RepeatNode: BehaviorNode = {
|
||||
"*" "{" <node:BehaviorNode> "}" => BehaviorNode::Decorator("repeat".to_string(), Box::new(node)),
|
||||
};
|
||||
|
||||
ActionNode: BehaviorNode = {
|
||||
<name:Ident> "(" <params:Comma<Field>> ")" => BehaviorNode::Action(name, params),
|
||||
<name:Ident> "(" <params:Comma<ActionParam>> ")" => BehaviorNode::Action(name, params),
|
||||
<name:Ident> => BehaviorNode::Action(name, vec![]),
|
||||
};
|
||||
|
||||
ActionParam: Field = {
|
||||
// Named parameter: field: value
|
||||
<path:DottedPath> ":" <value:Value> => Field {
|
||||
name: path.join("."),
|
||||
value,
|
||||
span: Span::new(0, 0),
|
||||
},
|
||||
// Positional parameter: just a value (use empty string as field name)
|
||||
<value:Value> => Field {
|
||||
name: String::new(),
|
||||
value,
|
||||
span: Span::new(0, 0),
|
||||
},
|
||||
};
|
||||
|
||||
SubTreeNode: BehaviorNode = {
|
||||
"@" <path:Path> => BehaviorNode::SubTree(path),
|
||||
};
|
||||
@@ -286,13 +326,30 @@ Relationship: Relationship = {
|
||||
};
|
||||
|
||||
Participant: Participant = {
|
||||
<name:Path> <role:("as" <Ident>)?> <self_block:SelfBlock?> <other_block:OtherBlock?> => Participant {
|
||||
role,
|
||||
// Participant with inline block after name
|
||||
<name:Path> "{" <fields:Field*> "}" => Participant {
|
||||
role: None,
|
||||
name,
|
||||
self_block,
|
||||
other_block,
|
||||
self_block: Some(fields),
|
||||
other_block: None,
|
||||
span: Span::new(0, 0),
|
||||
}
|
||||
},
|
||||
// Participant with role and inline block
|
||||
<name:Path> "as" <role:Ident> "{" <fields:Field*> "}" => Participant {
|
||||
role: Some(role),
|
||||
name,
|
||||
self_block: Some(fields),
|
||||
other_block: None,
|
||||
span: Span::new(0, 0),
|
||||
},
|
||||
// Participant without blocks (bare name)
|
||||
<name:Path> => Participant {
|
||||
role: None,
|
||||
name,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
span: Span::new(0, 0),
|
||||
},
|
||||
};
|
||||
|
||||
SelfBlock: Vec<Field> = {
|
||||
@@ -316,8 +373,9 @@ Location: Location = {
|
||||
// ===== Species =====
|
||||
|
||||
Species: Species = {
|
||||
"species" <name:Ident> "{" <fields:Field*> "}" => Species {
|
||||
"species" <name:Ident> "{" <includes:Include*> <fields:Field*> "}" => Species {
|
||||
name,
|
||||
includes,
|
||||
fields,
|
||||
span: Span::new(0, 0),
|
||||
}
|
||||
@@ -465,6 +523,7 @@ extern {
|
||||
"enum" => Token::Enum,
|
||||
"state" => Token::State,
|
||||
"on" => Token::On,
|
||||
"enter" => Token::Enter,
|
||||
"as" => Token::As,
|
||||
"self" => Token::SelfKw,
|
||||
"other" => Token::Other,
|
||||
|
||||
8918
src/syntax/parser.rs
8918
src/syntax/parser.rs
File diff suppressed because it is too large
Load Diff
@@ -248,7 +248,7 @@ fn valid_schedule() -> impl Strategy<Value = String> {
|
||||
let (h1, m1, s1) = w[0];
|
||||
let (h2, m2, _) = w[1];
|
||||
format!(
|
||||
" {:02}:{:02}:{:02} -> {:02}:{:02}:00: activity",
|
||||
" {:02}:{:02}:{:02} -> {:02}:{:02}:00: activity {{ }}",
|
||||
h1, m1, s1, h2, m2
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user