Files
storybook/src/syntax/parser.lalrpop

580 lines
14 KiB
Plaintext
Raw Normal View History

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),
<e:EnumDecl> => Declaration::Enum(e),
};
// ===== Use declarations =====
UseDecl: UseDecl = {
"use" <path:Path> ";" => UseDecl {
path,
kind: UseKind::Single,
span: Span::new(0, 0), // TODO: track actual spans
},
"use" <base:PathSegments> "::" "{" <items:Comma<Ident>> "}" ";" => UseDecl {
path: base,
kind: UseKind::Grouped(items),
span: Span::new(0, 0),
},
"use" <path:PathSegments> "::" "*" ";" => UseDecl {
path,
kind: UseKind::Wildcard,
span: Span::new(0, 0),
},
};
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 = {
"character" <name:Ident> <species:(":" <Ident>)?> <template:TemplateClause?> "{" <fields:Field*> "}" => Character {
name,
species,
fields,
template,
span: Span::new(0, 0),
}
};
TemplateClause: Vec<String> = {
"from" <t:Ident> <rest:("," <Ident>)*> => {
let mut templates = vec![t];
templates.extend(rest);
templates
}
};
// ===== Template =====
Template: Template = {
"template" <name:Ident> <strict:"strict"?> "{" <includes:Include*> <fields:Field*> "}" => Template {
name,
fields,
strict: strict.is_some(),
includes,
span: Span::new(0, 0),
}
};
Include: String = {
"include" <name:Ident> => name
};
// ===== Fields =====
Field: Field = {
<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),
}
};
Value: Value = {
<IntLit> => Value::Int(<>),
<FloatLit> => Value::Float(<>),
<StringLit> => Value::String(<>),
<BoolLit> => Value::Bool(<>),
<lo:IntLit> ".." <hi:IntLit> => Value::Range(
Box::new(Value::Int(lo)),
Box::new(Value::Int(hi))
),
<lo:FloatLit> ".." <hi:FloatLit> => Value::Range(
Box::new(Value::Float(lo)),
Box::new(Value::Float(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 }
}
};
ProseBlock: ProseBlock = {
ProseBlockToken
};
Override: Override = {
"@" <base:Path> "{" <overrides:OverrideOp*> "}" => Override {
base,
overrides,
span: Span::new(0, 0),
}
};
OverrideOp: OverrideOp = {
"remove" <name:Ident> => OverrideOp::Remove(name),
"append" <f:Field> => OverrideOp::Append(f),
<f:Field> => OverrideOp::Set(f),
};
// ===== Life Arc =====
LifeArc: LifeArc = {
"life_arc" <name:Ident> "{" <fields:Field*> <states:ArcState*> "}" => LifeArc {
name,
states,
span: Span::new(0, 0),
}
};
ArcState: 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,
condition: cond,
span: Span::new(0, 0),
}
};
// ===== Schedule =====
Schedule: Schedule = {
"schedule" <name:Ident> "{" <fields:Field*> <blocks:ScheduleBlock*> "}" => Schedule {
name,
blocks,
span: Span::new(0, 0),
}
};
ScheduleBlock: ScheduleBlock = {
<start:Time> "->" <end:Time> ":" <activity:Ident> "{" <fields:Field*> "}" => ScheduleBlock {
start,
end,
activity,
fields,
span: Span::new(0, 0),
}
};
// ===== Behavior Trees =====
Behavior: Behavior = {
"behavior" <name:Ident> "{" <fields:Field*> <root:BehaviorNode> "}" => Behavior {
name,
root,
span: Span::new(0, 0),
}
};
BehaviorNode: BehaviorNode = {
<SelectorNode>,
<SequenceNode>,
<RepeatNode>,
<ActionNode>,
<SubTreeNode>,
};
SelectorNode: BehaviorNode = {
"?" "{" <nodes:BehaviorNode+> "}" => BehaviorNode::Selector(nodes),
};
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<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),
};
// ===== Institution =====
Institution: Institution = {
"institution" <name:Ident> "{" <fields:Field*> "}" => Institution {
name,
fields,
span: Span::new(0, 0),
}
};
// ===== Relationship =====
Relationship: Relationship = {
"relationship" <name:Ident> "{" <participants:Participant+> <fields:Field*> "}" => Relationship {
name,
participants,
fields,
span: Span::new(0, 0),
}
};
Participant: Participant = {
// Participant with inline block after name
<name:Path> "{" <fields:Field*> "}" => Participant {
role: None,
name,
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> = {
"self" "{" <fields:Field*> "}" => fields
};
OtherBlock: Vec<Field> = {
"other" "{" <fields:Field*> "}" => fields
};
// ===== Location =====
Location: Location = {
"location" <name:Ident> "{" <fields:Field*> "}" => Location {
name,
fields,
span: Span::new(0, 0),
}
};
// ===== Species =====
Species: Species = {
"species" <name:Ident> "{" <includes:Include*> <fields:Field*> "}" => Species {
name,
includes,
fields,
span: Span::new(0, 0),
}
};
// ===== Enum =====
EnumDecl: EnumDecl = {
"enum" <name:Ident> "{" <variants:Comma<Ident>> "}" => EnumDecl {
name,
variants,
span: Span::new(0, 0),
}
};
// ===== 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 = {
<IntLit> => Expr::IntLit(<>),
<FloatLit> => Expr::FloatLit(<>),
<StringLit> => Expr::StringLit(<>),
<BoolLit> => Expr::BoolLit(<>),
};
// ===== 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,
"enum" => Token::Enum,
"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,
"true" => Token::True,
"false" => Token::False,
// Literals
Ident => Token::Ident(<String>),
IntLit => Token::IntLit(<i64>),
FloatLit => Token::FloatLit(<f64>),
StringLit => Token::StringLit(<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,
}
}