feat(life-arc): add field requirements for life arcs

Added optional requires clause to life_arc declarations:
  life_arc Baker requires { skill_level: Number } { ... }
Includes new FieldRequirement AST type, requires keyword token,
and parser support for the requires clause.
This commit is contained in:
2026-02-14 14:30:11 +00:00
parent c49b00a2dc
commit 51c4f33a31
8 changed files with 7029 additions and 5864 deletions

View File

@@ -210,6 +210,7 @@ fn test_validation_error_generic() {
fn test_unknown_life_arc_state_error() {
let life_arc = LifeArc {
name: "Growth".to_string(),
required_fields: vec![],
states: vec![
ArcState {
name: "child".to_string(),

View File

@@ -829,6 +829,7 @@ mod tests {
fn test_life_arc_valid_transitions() {
let life_arc = LifeArc {
name: "Test".to_string(),
required_fields: vec![],
states: vec![
ArcState {
name: "start".to_string(),
@@ -859,6 +860,7 @@ mod tests {
fn test_life_arc_invalid_transition() {
let life_arc = LifeArc {
name: "Test".to_string(),
required_fields: vec![],
states: vec![ArcState {
name: "start".to_string(),
on_enter: None,

View File

@@ -158,6 +158,7 @@ proptest! {
let life_arc = LifeArc {
name: "Test".to_string(),
required_fields: vec![],
states: vec![
ArcState {
name: state1_name.clone(),

View File

@@ -217,10 +217,19 @@ pub enum OverrideOp {
#[derive(Debug, Clone, PartialEq)]
pub struct LifeArc {
pub name: String,
pub required_fields: Vec<FieldRequirement>,
pub states: Vec<ArcState>,
pub span: Span,
}
/// A required field declaration in a life arc
#[derive(Debug, Clone, PartialEq)]
pub struct FieldRequirement {
pub name: String,
pub type_name: String,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ArcState {
pub name: String,

View File

@@ -37,6 +37,8 @@ pub enum Token {
ConceptComparison,
#[token("any")]
Any,
#[token("requires")]
Requires,
#[token("state")]
State,
#[token("on")]

View File

@@ -338,13 +338,26 @@ OverrideOp: OverrideOp = {
// ===== Life Arc =====
LifeArc: LifeArc = {
"life_arc" <name:Ident> "{" <fields:Field*> <states:ArcState*> "}" => LifeArc {
"life_arc" <name:Ident> <reqs:RequiresClause?> "{" <fields:Field*> <states:ArcState*> "}" => LifeArc {
name,
required_fields: reqs.unwrap_or_default(),
states,
span: Span::new(0, 0),
}
};
RequiresClause: Vec<FieldRequirement> = {
"requires" "{" <reqs:Comma<FieldReq>> "}" => reqs,
};
FieldReq: FieldRequirement = {
<name:Ident> ":" <type_name:Ident> => FieldRequirement {
name,
type_name,
span: Span::new(0, 0),
}
};
ArcState: ArcState = {
"state" <name:Ident> "{" <on_enter:OnEnter?> <fields:Field*> <transitions:Transition*> "}" => ArcState {
name,
@@ -963,6 +976,7 @@ extern {
"sub_concept" => Token::SubConcept,
"concept_comparison" => Token::ConceptComparison,
"any" => Token::Any,
"requires" => Token::Requires,
"state" => Token::State,
"on" => Token::On,
"enter" => Token::Enter,

File diff suppressed because it is too large Load Diff

View File

@@ -270,3 +270,70 @@ template Knight: Human {
| _ => panic!("Expected Template declaration"),
}
}
// ===== Life arc requires clause tests =====
#[test]
fn test_life_arc_with_requires() {
let input = r#"
life_arc Baker requires { skill_level: Number } {
state Apprentice {
}
}
"#;
let file = parse(input);
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::LifeArc(la) => {
assert_eq!(la.name, "Baker");
assert_eq!(la.required_fields.len(), 1);
assert_eq!(la.required_fields[0].name, "skill_level");
assert_eq!(la.required_fields[0].type_name, "Number");
assert_eq!(la.states.len(), 1);
},
| _ => panic!("Expected LifeArc declaration"),
}
}
#[test]
fn test_life_arc_without_requires() {
let input = r#"
life_arc Growth {
state Child {
}
}
"#;
let file = parse(input);
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::LifeArc(la) => {
assert_eq!(la.name, "Growth");
assert!(la.required_fields.is_empty());
},
| _ => panic!("Expected LifeArc declaration"),
}
}
#[test]
fn test_life_arc_multiple_requirements() {
let input = r#"
life_arc Career requires { skill_level: Number, experience: Number, title: Text } {
state Junior {
}
}
"#;
let file = parse(input);
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::LifeArc(la) => {
assert_eq!(la.required_fields.len(), 3);
assert_eq!(la.required_fields[0].name, "skill_level");
assert_eq!(la.required_fields[1].name, "experience");
assert_eq!(la.required_fields[2].name, "title");
},
| _ => panic!("Expected LifeArc declaration"),
}
}