release: Storybook v0.2.0 - Major syntax and features update

BREAKING CHANGES:
- Relationship syntax now requires blocks for all participants
- Removed self/other perspective blocks from relationships
- Replaced 'guard' keyword with 'if' for behavior tree decorators

Language Features:
- Add tree-sitter grammar with improved if/condition disambiguation
- Add comprehensive tutorial and reference documentation
- Add SBIR v0.2.0 binary format specification
- Add resource linking system for behaviors and schedules
- Add year-long schedule patterns (day, season, recurrence)
- Add behavior tree enhancements (named nodes, decorators)

Documentation:
- Complete tutorial series (9 chapters) with baker family examples
- Complete reference documentation for all language features
- SBIR v0.2.0 specification with binary format details
- Added locations and institutions documentation

Examples:
- Convert all examples to baker family scenario
- Add comprehensive working examples

Tooling:
- Zed extension with LSP integration
- Tree-sitter grammar for syntax highlighting
- Build scripts and development tools

Version Updates:
- Main package: 0.1.0 → 0.2.0
- Tree-sitter grammar: 0.1.0 → 0.2.0
- Zed extension: 0.1.0 → 0.2.0
- Storybook editor: 0.1.0 → 0.2.0
This commit is contained in:
2026-02-13 21:52:03 +00:00
parent 80332971b8
commit 16deb5d237
290 changed files with 90316 additions and 5827 deletions

View File

@@ -1,13 +1,52 @@
/// Source location for error reporting
/// Source location for error reporting with line/column information
#[derive(Debug, Clone, PartialEq)]
pub struct Span {
pub start: usize,
pub end: usize,
pub start_line: usize, // 0-indexed line number
pub start_col: usize, // 0-indexed column number
pub end_line: usize,
pub end_col: usize,
}
impl Span {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
Self {
start,
end,
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
}
}
pub fn with_position(
start: usize,
end: usize,
start_line: usize,
start_col: usize,
end_line: usize,
end_col: usize,
) -> Self {
Self {
start,
end,
start_line,
start_col,
end_line,
end_col,
}
}
/// Convert to LSP Position for the start
pub fn start_position(&self) -> (u32, u32) {
(self.start_line as u32, self.start_col as u32)
}
/// Convert to LSP Position for the end
pub fn end_position(&self) -> (u32, u32) {
(self.end_line as u32, self.end_col as u32)
}
}
@@ -48,6 +87,36 @@ pub enum UseKind {
Wildcard, // use foo::*
}
/// Link to a behavior tree with optional conditions and priority
#[derive(Debug, Clone, PartialEq)]
pub struct BehaviorLink {
pub tree: Vec<String>, // Qualified path to behavior tree
pub condition: Option<Expr>, // Optional when clause
pub priority: Priority, // Execution priority
pub span: Span,
}
/// Priority levels for behavior selection
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Priority {
Low,
Normal,
High,
Critical,
}
impl Priority {
pub fn from_str(s: &str) -> Option<Self> {
match s {
| "low" => Some(Priority::Low),
| "normal" => Some(Priority::Normal),
| "high" => Some(Priority::High),
| "critical" => Some(Priority::Critical),
| _ => None,
}
}
}
/// Character definition
#[derive(Debug, Clone, PartialEq)]
pub struct Character {
@@ -55,6 +124,9 @@ pub struct Character {
pub species: Option<String>, // `: Species` - what the character fundamentally is
pub fields: Vec<Field>,
pub template: Option<Vec<String>>, // `from Template1, Template2`
pub uses_behaviors: Option<Vec<BehaviorLink>>, // `uses behaviors: [...]`
pub uses_schedule: Option<Vec<String>>, /* `uses schedule: ScheduleName` or `uses schedules:
* [...]` */
pub span: Span,
}
@@ -65,6 +137,9 @@ pub struct Template {
pub fields: Vec<Field>,
pub strict: bool,
pub includes: Vec<String>,
pub uses_behaviors: Option<Vec<BehaviorLink>>, // `uses behaviors: [...]`
pub uses_schedule: Option<Vec<String>>, /* `uses schedule: ScheduleName` or `uses
* schedules: [...]` */
pub span: Span,
}
@@ -155,23 +230,66 @@ pub struct Transition {
pub span: Span,
}
/// Schedule definition
/// Schedule definition with composition support
#[derive(Debug, Clone, PartialEq)]
pub struct Schedule {
pub name: String,
pub extends: Option<String>, // Base schedule to extend
pub blocks: Vec<ScheduleBlock>,
pub recurrences: Vec<RecurrencePattern>, // Recurring events
pub fields: Vec<Field>, // Documentation prose blocks, metadata
pub span: Span,
}
/// A time block in a schedule
#[derive(Debug, Clone, PartialEq)]
pub struct ScheduleBlock {
pub name: Option<String>, // Block name for override system
pub is_override: bool, // Whether this block overrides a base block
pub start: Time,
pub end: Time,
pub activity: String,
pub activity: String, // DEPRECATED: kept for backward compatibility
pub action: Option<Vec<String>>, // Behavior reference (new way)
pub temporal_constraint: Option<TemporalConstraint>, // When this block applies
pub fields: Vec<Field>,
pub span: Span,
}
/// Temporal constraint for when a schedule block applies
#[derive(Debug, Clone, PartialEq)]
pub enum TemporalConstraint {
Season(String), // Applies during specific season (enum value)
DayOfWeek(String), // Applies on specific day of week (enum value)
Month(String), // Applies during specific month (enum value)
DateRange(String, String), // Applies between two dates (TODO: date type)
}
/// Recurring event pattern
#[derive(Debug, Clone, PartialEq)]
pub struct RecurrencePattern {
pub name: String, // Event name (e.g., "MarketDay")
pub constraint: TemporalConstraint, // When it recurs (e.g., "on Earthday")
pub blocks: Vec<ScheduleBlock>, // What happens during the event
pub span: Span,
}
// ===== Parser Helper Types for Schedules =====
/// Helper for parsing schedule bodies with flexible ordering
#[derive(Debug, Clone, PartialEq)]
pub enum ScheduleBodyItem {
Field(Field),
Block(ScheduleBlock),
Recurrence(RecurrencePattern),
}
/// Helper for parsing schedule block content
#[derive(Debug, Clone, PartialEq)]
pub enum BlockContentItem {
TimeRange(Time, Time),
Field(Field),
}
/// Behavior tree definition
#[derive(Debug, Clone, PartialEq)]
pub struct Behavior {
@@ -182,19 +300,73 @@ pub struct Behavior {
#[derive(Debug, Clone, PartialEq)]
pub enum BehaviorNode {
Selector(Vec<BehaviorNode>), // ? operator
Sequence(Vec<BehaviorNode>), // > operator (context-dependent)
Selector {
label: Option<String>,
children: Vec<BehaviorNode>,
},
Sequence {
label: Option<String>,
children: Vec<BehaviorNode>,
},
Condition(Expr),
Action(String, Vec<Field>), // Action name + parameters
Decorator(String, Box<BehaviorNode>),
Decorator {
decorator_type: DecoratorType,
child: Box<BehaviorNode>,
},
SubTree(Vec<String>), // Reference to another behavior
}
#[derive(Debug, Clone, PartialEq)]
pub enum DecoratorType {
Repeat, // infinite loop
RepeatN(u32), // N times
RepeatRange(u32, u32), // min..max times
Invert,
Retry(u32), // max attempts
Timeout(String), // duration string (e.g., "5s", "30m", "2h")
Cooldown(String), // duration string (e.g., "5s", "30m", "2h")
If(Expr),
SucceedAlways,
FailAlways,
}
// BehaviorDuration is used for decorator timeouts/cooldowns (single unit)
// whereas Duration (above) is for general time literals (compound: 2h30m)
#[derive(Debug, Clone, PartialEq)]
pub struct BehaviorDuration {
pub value: u32,
pub unit: DurationUnit,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DurationUnit {
Days,
Hours,
Minutes,
Seconds,
}
impl BehaviorDuration {
pub fn to_milliseconds(&self) -> u64 {
let base_ms = self.value as u64;
match self.unit {
| DurationUnit::Days => base_ms * 24 * 60 * 60 * 1000,
| DurationUnit::Hours => base_ms * 60 * 60 * 1000,
| DurationUnit::Minutes => base_ms * 60 * 1000,
| DurationUnit::Seconds => base_ms * 1000,
}
}
}
/// Institution definition
#[derive(Debug, Clone, PartialEq)]
pub struct Institution {
pub name: String,
pub fields: Vec<Field>,
pub uses_behaviors: Option<Vec<BehaviorLink>>, // `uses behaviors: [...]`
pub uses_schedule: Option<Vec<String>>, /* `uses schedule: ScheduleName` or `uses
* schedules: [...]` */
pub span: Span,
}
@@ -209,10 +381,9 @@ pub struct Relationship {
#[derive(Debug, Clone, PartialEq)]
pub struct Participant {
pub role: Option<String>, // "as parent"
pub name: Vec<String>, // Qualified path
pub self_block: Option<Vec<Field>>,
pub other_block: Option<Vec<Field>>,
pub role: Option<String>, // "as parent" (optional)
pub fields: Vec<Field>, // Participant-specific fields (required block)
pub span: Span,
}
@@ -284,3 +455,40 @@ pub enum QuantifierKind {
ForAll,
Exists,
}
// ===== Parser Helper Types =====
// These enums are used internally by the LALRPOP parser to handle flexible
// ordering
/// Helper for parsing character/institution bodies with flexible ordering
#[derive(Debug, Clone, PartialEq)]
pub enum CharacterBodyItem {
Field(Field),
UsesBehaviors(Vec<BehaviorLink>),
UsesSchedule(Vec<String>),
}
/// Helper for parsing institution bodies with flexible ordering
#[derive(Debug, Clone, PartialEq)]
pub enum InstitutionBodyItem {
Field(Field),
UsesBehaviors(Vec<BehaviorLink>),
UsesSchedule(Vec<String>),
}
/// Helper for parsing template body items with flexible ordering
#[derive(Debug, Clone, PartialEq)]
pub enum TemplateBodyItem {
Field(Field),
Include(String),
UsesBehaviors(Vec<BehaviorLink>),
UsesSchedule(Vec<String>),
}
/// Helper for parsing behavior link fields
#[derive(Debug, Clone, PartialEq)]
pub enum BehaviorLinkField {
Tree(Vec<String>),
Condition(Expr),
Priority(Priority),
}

188
src/syntax/keywords.rs Normal file
View File

@@ -0,0 +1,188 @@
//! Shared keyword definitions derived from the lexer Token enum
//!
//! This module provides structured access to Storybook language keywords
//! without duplicating the keyword strings across different LSP modules.
//! All keywords are defined in the Token enum in lexer.rs using the
//! #[token(...)] attribute.
use crate::syntax::lexer::Token;
/// Top-level declaration keywords that start a new declaration
pub const DECLARATION_KEYWORDS: &[&str] = &[
"character",
"template",
"species",
"behavior",
"life_arc",
"relationship",
"institution",
"location",
"enum",
"schedule",
];
/// All structural keywords (declarations + use)
pub const STRUCTURAL_KEYWORDS: &[&str] = &[
"use",
"character",
"template",
"species",
"behavior",
"life_arc",
"relationship",
"institution",
"location",
"enum",
"schedule",
];
/// Keywords used in behavior tree definitions
pub const BEHAVIOR_KEYWORDS: &[&str] = &[
"choose",
"then",
"if",
"when",
"repeat",
"invert",
"retry",
"timeout",
"cooldown",
// "guard" removed - use "if" instead
"succeed_always",
"fail_always",
];
/// Modifier keywords used in various contexts
pub const MODIFIER_KEYWORDS: &[&str] = &["strict", "include", "from", "as"];
/// State machine keywords
pub const STATE_KEYWORDS: &[&str] = &["state", "on", "enter"];
/// Expression keywords
pub const EXPRESSION_KEYWORDS: &[&str] =
&["forall", "exists", "in", "where", "and", "or", "not", "is"];
/// Special identifier keywords
pub const IDENTIFIER_KEYWORDS: &[&str] = &["self", "other"];
/// Operation keywords
pub const OPERATION_KEYWORDS: &[&str] = &["remove", "append"];
/// Boolean literals
pub const BOOLEAN_LITERALS: &[&str] = &["true", "false"];
/// Check if a string is a top-level declaration keyword
pub fn is_declaration_keyword(s: &str) -> bool {
DECLARATION_KEYWORDS.contains(&s)
}
/// Check if a string is any structural keyword (includes 'use')
pub fn is_structural_keyword(s: &str) -> bool {
STRUCTURAL_KEYWORDS.contains(&s)
}
/// Check if a string is a behavior tree keyword
pub fn is_behavior_keyword(s: &str) -> bool {
BEHAVIOR_KEYWORDS.contains(&s)
}
/// Check if a token is a declaration keyword token
pub fn token_is_declaration_keyword(token: &Token) -> bool {
matches!(
token,
Token::Character |
Token::Template |
Token::Species |
Token::Behavior |
Token::LifeArc |
Token::Relationship |
Token::Institution |
Token::Location |
Token::Enum |
Token::Schedule
)
}
/// Check if a token is a structural keyword token (includes Use)
pub fn token_is_structural_keyword(token: &Token) -> bool {
matches!(
token,
Token::Use |
Token::Character |
Token::Template |
Token::Species |
Token::Behavior |
Token::LifeArc |
Token::Relationship |
Token::Institution |
Token::Location |
Token::Enum |
Token::Schedule
)
}
/// Get the string representation of a declaration token
pub fn declaration_token_to_str(token: &Token) -> Option<&'static str> {
match token {
| Token::Character => Some("character"),
| Token::Template => Some("template"),
| Token::Species => Some("species"),
| Token::Behavior => Some("behavior"),
| Token::LifeArc => Some("life_arc"),
| Token::Relationship => Some("relationship"),
| Token::Institution => Some("institution"),
| Token::Location => Some("location"),
| Token::Enum => Some("enum"),
| Token::Schedule => Some("schedule"),
| _ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_declaration_keywords() {
assert!(is_declaration_keyword("character"));
assert!(is_declaration_keyword("template"));
assert!(is_declaration_keyword("behavior"));
assert!(!is_declaration_keyword("use"));
assert!(!is_declaration_keyword("unknown"));
}
#[test]
fn test_structural_keywords() {
assert!(is_structural_keyword("character"));
assert!(is_structural_keyword("use"));
assert!(!is_structural_keyword("if"));
}
#[test]
fn test_behavior_keywords() {
assert!(is_behavior_keyword("choose"));
assert!(is_behavior_keyword("repeat"));
assert!(!is_behavior_keyword("character"));
}
#[test]
fn test_token_checks() {
assert!(token_is_declaration_keyword(&Token::Character));
assert!(token_is_declaration_keyword(&Token::Behavior));
assert!(!token_is_declaration_keyword(&Token::Use));
assert!(token_is_structural_keyword(&Token::Use));
assert!(token_is_structural_keyword(&Token::Character));
assert!(!token_is_structural_keyword(&Token::If));
}
#[test]
fn test_token_to_str() {
assert_eq!(
declaration_token_to_str(&Token::Character),
Some("character")
);
assert_eq!(declaration_token_to_str(&Token::Behavior), Some("behavior"));
assert_eq!(declaration_token_to_str(&Token::Use), None);
}
}

View File

@@ -69,11 +69,56 @@ pub enum Token {
From,
#[token("is")]
Is,
#[token("uses")]
Uses,
#[token("behaviors")]
Behaviors,
#[token("schedules")]
Schedules,
#[token("tree")]
Tree,
#[token("priority")]
Priority,
#[token("extends")]
Extends,
#[token("override")]
Override,
#[token("recurrence")]
Recurrence,
#[token("season")]
Season,
#[token("block")]
Block,
#[token("true")]
True,
#[token("false")]
False,
// Behavior tree keywords
#[token("choose")]
Choose,
#[token("then")]
Then,
#[token("if")]
If,
#[token("when")]
When,
#[token("repeat")]
Repeat,
#[token("invert")]
Invert,
#[token("retry")]
Retry,
#[token("timeout")]
Timeout,
#[token("cooldown")]
Cooldown,
// "guard" keyword removed - use "if" instead (Token::If)
#[token("succeed_always")]
SucceedAlways,
#[token("fail_always")]
FailAlways,
// Identifiers and literals
#[regex(r"[a-zA-Z_][a-zA-Z0-9_]*", |lex| lex.slice().to_string())]
Ident(String),

View File

@@ -1,6 +1,7 @@
#![allow(unused_assignments)] // False positives in error enum fields used by thiserror
pub mod ast;
pub mod keywords;
pub mod lexer;
// Parser is generated by LALRPOP
@@ -13,6 +14,12 @@ pub use parser::FileParser;
#[cfg(test)]
mod prop_tests;
#[cfg(test)]
mod resource_linking_tests;
#[cfg(test)]
mod schedule_composition_tests;
use miette::Diagnostic;
use thiserror::Error;

View File

@@ -66,15 +66,44 @@ DottedPath: Vec<String> = {
// ===== Character =====
Character: Character = {
"character" <name:Ident> <species:(":" <Ident>)?> <template:TemplateClause?> "{" <fields:Field*> "}" => Character {
name,
species,
fields,
template,
span: Span::new(0, 0),
"character" <name:Ident> <species:(":" <Ident>)?> <template:TemplateClause?> "{" <body:CharacterBody> "}" => {
Character {
name,
species,
fields: body.0,
template,
uses_behaviors: body.1,
uses_schedule: body.2,
span: Span::new(0, 0),
}
}
};
// 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];
@@ -83,18 +112,116 @@ TemplateClause: Vec<String> = {
}
};
// ===== Template =====
// uses behaviors: [...]
UsesBehaviorsClause: Vec<BehaviorLink> = {
"uses" "behaviors" ":" "[" <links:Comma<BehaviorLinkItem>> "]" => links,
};
Template: Template = {
"template" <name:Ident> <strict:"strict"?> "{" <includes:Include*> <fields:Field*> "}" => Template {
name,
fields,
strict: strict.is_some(),
includes,
span: Span::new(0, 0),
// Individual behavior link: { tree: BehaviorName, priority: high, when: condition }
BehaviorLinkItem: BehaviorLink = {
"{" <fields:BehaviorLinkField+> "}" => {
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(0, 0),
}
}
};
// 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 = {
"template" <name:Ident> <strict:"strict"?> "{" <body:TemplateBodyItem*> "}" => {
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,
fields,
strict: strict.is_some(),
includes,
uses_behaviors,
uses_schedule,
span: Span::new(0, 0),
}
}
};
// 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> = {
"uses" "behaviors" ":" <first:Ident> <rest:("," <Ident>)*> => {
let mut names = vec![first];
names.extend(rest);
names.into_iter().map(|name| BehaviorLink {
tree: vec![name],
condition: None,
priority: Priority::Normal,
span: Span::new(0, 0),
}).collect()
},
};
// Template-level schedule links
TemplateUsesScheduleClause: Vec<String> = {
"uses" "schedule" ":" <name:Ident> => vec![name],
};
// Template/Species include clause
Include: String = {
"include" <name:Ident> => name
};
@@ -181,6 +308,11 @@ Duration: Duration = {
}
};
// Duration string for decorator timeouts/cooldowns (e.g., "5s", "30m", "2h", "1d")
BehaviorDurationLit: String = {
<s:DurationLit> => s
};
ProseBlock: ProseBlock = {
ProseBlockToken
};
@@ -233,20 +365,140 @@ Transition: Transition = {
// ===== Schedule =====
Schedule: Schedule = {
"schedule" <name:Ident> "{" <fields:Field*> <blocks:ScheduleBlock*> "}" => Schedule {
// Simple schedule: schedule Name { ... }
"schedule" <name:Ident> "{" <body:ScheduleBody> "}" => Schedule {
name,
blocks,
extends: None,
fields: body.0,
blocks: body.1,
recurrences: body.2,
span: Span::new(0, 0),
},
// Extending schedule: schedule Name extends Base { ... }
"schedule" <name:Ident> "extends" <base:Ident> "{" <body:ScheduleBody> "}" => Schedule {
name,
extends: Some(base),
fields: body.0,
blocks: body.1,
recurrences: body.2,
span: Span::new(0, 0),
}
};
// 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 }
<start:Time> "->" <end:Time> ":" <activity:Ident> "{" <fields:Field*> "}" => ScheduleBlock {
name: None,
is_override: false,
start,
end,
activity,
action: None,
temporal_constraint: None,
fields,
span: Span::new(0, 0),
},
// Named block: block name { time, action, fields }
"block" <name:Ident> "{" <content:BlockContent> "}" => 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(0, 0),
},
// Override block: override name { time, action, fields }
"override" <name:Ident> "{" <content:BlockContent> "}" => 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(0, 0),
}
};
// 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 = {
"recurrence" <name:Ident> "on" <day:Ident> "{" <blocks:ScheduleBlock+> "}" => RecurrencePattern {
name,
constraint: TemporalConstraint::DayOfWeek(day),
blocks,
span: Span::new(0, 0),
}
};
@@ -263,23 +515,116 @@ Behavior: Behavior = {
BehaviorNode: BehaviorNode = {
<SelectorNode>,
<SequenceNode>,
<RepeatNode>,
<ConditionNode>,
<DecoratorNode>,
<ActionNode>,
<SubTreeNode>,
};
// Selector node: choose { ... } or choose label { ... }
SelectorNode: BehaviorNode = {
"?" "{" <nodes:BehaviorNode+> "}" => BehaviorNode::Selector(nodes),
"choose" <label:Ident?> "{" <children:BehaviorNode+> "}" => BehaviorNode::Selector {
label,
children,
},
};
// Sequence node: then { ... } or then label { ... }
SequenceNode: BehaviorNode = {
">" "{" <nodes:BehaviorNode+> "}" => BehaviorNode::Sequence(nodes),
"then" <label:Ident?> "{" <children:BehaviorNode+> "}" => BehaviorNode::Sequence {
label,
children,
},
};
RepeatNode: BehaviorNode = {
"*" "{" <node:BehaviorNode> "}" => BehaviorNode::Decorator("repeat".to_string(), Box::new(node)),
// 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:IntLit> ")" "{" <child:BehaviorNode> "}" => BehaviorNode::Decorator {
decorator_type: DecoratorType::RepeatN(n as u32),
child: Box::new(child),
},
};
DecoratorRepeatRange: BehaviorNode = {
"repeat" "(" <min:IntLit> ".." <max:IntLit> ")" "{" <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:IntLit> ")" "{" <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![]),
@@ -300,20 +645,50 @@ ActionParam: Field = {
},
};
// Subtree node: include path::to::subtree
SubTreeNode: BehaviorNode = {
"@" <path:Path> => BehaviorNode::SubTree(path),
"include" <path:Path> => BehaviorNode::SubTree(path),
};
// ===== Institution =====
Institution: Institution = {
"institution" <name:Ident> "{" <fields:Field*> "}" => Institution {
name,
fields,
span: Span::new(0, 0),
"institution" <name:Ident> "{" <body:InstitutionBody> "}" => {
Institution {
name,
fields: body.0,
uses_behaviors: body.1,
uses_schedule: body.2,
span: Span::new(0, 0),
}
}
};
// 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 = {
@@ -326,40 +701,22 @@ Relationship: Relationship = {
};
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
// Participant with role and block (block required)
<name:Path> "as" <role:Ident> "{" <fields:Field*> "}" => Participant {
name,
role: Some(role),
name,
self_block: Some(fields),
other_block: None,
fields,
span: Span::new(0, 0),
},
// Participant without blocks (bare name)
<name:Path> => Participant {
// Participant without role (block still required)
<name:Path> "{" <fields:Field*> "}" => Participant {
name,
role: None,
name,
self_block: None,
other_block: None,
fields,
span: Span::new(0, 0),
},
};
SelfBlock: Vec<Field> = {
"self" "{" <fields:Field*> "}" => fields
};
OtherBlock: Vec<Field> = {
"other" "{" <fields:Field*> "}" => fields
};
// ===== Location =====
Location: Location = {
@@ -540,9 +897,32 @@ extern {
"include" => Token::Include,
"from" => Token::From,
"is" => Token::Is,
"uses" => Token::Uses,
"behaviors" => Token::Behaviors,
"schedules" => Token::Schedules,
"tree" => Token::Tree,
"priority" => Token::Priority,
"extends" => Token::Extends,
"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>),
IntLit => Token::IntLit(<i64>),

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,20 @@ fn valid_ident() -> impl Strategy<Value = String> {
"not" |
"is" |
"true" |
"false"
"false" |
// Behavior tree keywords
"if" |
"when" |
"choose" |
"then" |
"include" |
"repeat" |
"invert" |
"retry" |
"timeout" |
"cooldown" |
"succeed_always" |
"fail_always"
)
})
}
@@ -167,7 +180,8 @@ proptest! {
fn test_keywords_are_distinct_from_idents(
keyword in prop::sample::select(vec![
"character", "template", "enum", "use", "self", "other",
"and", "or", "not", "is", "true", "false"
"and", "or", "not", "is", "true", "false",
"if", "when", "choose", "then", "include"
])
) {
let lexer = Lexer::new(keyword);
@@ -305,7 +319,7 @@ fn valid_relationship() -> impl Strategy<Value = String> {
.collect::<Vec<_>>()
.join("\n");
format!(
"relationship {} {{\n {}\n {}\n{}\n}}",
"relationship {} {{\n {} {{ }}\n {} {{ }}\n{}\n}}",
name, person1, person2, fields_str
)
})
@@ -600,13 +614,13 @@ fn valid_behavior_node_depth(depth: u32) -> BoxedStrategy<String> {
// Base case: just actions or subtrees
prop_oneof![
valid_action_node(),
valid_ident().prop_map(|name| format!("@{}", name)),
valid_ident().prop_map(|name| format!("include {}", name)),
]
.boxed()
} else {
// Recursive case: can be action, subtree, selector, or sequence
let action = valid_action_node();
let subtree = valid_ident().prop_map(|name| format!("@{}", name));
let subtree = valid_ident().prop_map(|name| format!("include {}", name));
let selector = prop::collection::vec(valid_behavior_node_depth(depth - 1), 1..3).prop_map(
|children| {
@@ -615,7 +629,7 @@ fn valid_behavior_node_depth(depth: u32) -> BoxedStrategy<String> {
.map(|c| format!(" {}", c))
.collect::<Vec<_>>()
.join("\n");
format!("? {{\n{}\n }}", children_str)
format!("choose {{\n{}\n }}", children_str)
},
);
@@ -626,7 +640,7 @@ fn valid_behavior_node_depth(depth: u32) -> BoxedStrategy<String> {
.map(|c| format!(" {}", c))
.collect::<Vec<_>>()
.join("\n");
format!("> {{\n{}\n }}", children_str)
format!("then {{\n{}\n }}", children_str)
},
);
@@ -706,7 +720,7 @@ proptest! {
#[test]
fn test_behavior_tree_deeply_nested(name in valid_ident()) {
let input = format!(
"behavior {} {{\n > {{\n ? {{\n > {{\n action\n }}\n }}\n }}\n}}",
"behavior {} {{\n then {{\n choose {{\n then {{\n action\n }}\n }}\n }}\n}}",
name
);
let lexer = Lexer::new(&input);
@@ -738,7 +752,7 @@ proptest! {
subtree_path in prop::collection::vec(valid_ident(), 1..3)
) {
let path = subtree_path.join("::");
let input = format!("behavior {} {{\n @{}\n}}", name, path);
let input = format!("behavior {} {{\n include {}\n}}", name, path);
let lexer = Lexer::new(&input);
let parser = FileParser::new();
let result = parser.parse(lexer);
@@ -754,7 +768,7 @@ proptest! {
.map(|c| format!(" {}", c))
.collect::<Vec<_>>()
.join("\n");
let input = format!("behavior {} {{\n ? {{\n{}\n }}\n}}", name, children_str);
let input = format!("behavior {} {{\n choose {{\n{}\n }}\n}}", name, children_str);
let lexer = Lexer::new(&input);
let parser = FileParser::new();
let result = parser.parse(lexer);
@@ -770,7 +784,7 @@ proptest! {
.map(|c| format!(" {}", c))
.collect::<Vec<_>>()
.join("\n");
let input = format!("behavior {} {{\n > {{\n{}\n }}\n}}", name, children_str);
let input = format!("behavior {} {{\n then {{\n{}\n }}\n}}", name, children_str);
let lexer = Lexer::new(&input);
let parser = FileParser::new();
let result = parser.parse(lexer);

View File

@@ -0,0 +1,367 @@
//! Tests for resource linking syntax (uses behaviors/schedules)
use crate::syntax::{
ast::*,
lexer::Lexer,
FileParser,
};
#[test]
fn test_character_with_single_behavior_link() {
let input = r#"
character Martha: Human {
age: 42
uses behaviors: [
{ tree: BakeryWork, priority: normal }
]
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::Character(c) => {
assert_eq!(c.name, "Martha");
assert_eq!(c.species.as_ref().unwrap(), "Human");
// Check behavior links
assert!(c.uses_behaviors.is_some());
let behaviors = c.uses_behaviors.as_ref().unwrap();
assert_eq!(behaviors.len(), 1);
assert_eq!(behaviors[0].tree, vec!["BakeryWork"]);
assert_eq!(behaviors[0].priority, Priority::Normal);
assert!(behaviors[0].condition.is_none());
},
| _ => panic!("Expected Character declaration"),
}
}
#[test]
fn test_character_with_multiple_behavior_links() {
let input = r#"
character Martha: Human {
uses behaviors: [
{ tree: HandleUrgentNeeds, priority: critical },
{ tree: BakeryWork, priority: normal, when: time.hour >= 5 and time.hour < 13 },
{ tree: Idle, priority: low }
]
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Character(c) => {
let behaviors = c.uses_behaviors.as_ref().unwrap();
assert_eq!(behaviors.len(), 3);
assert_eq!(behaviors[0].tree, vec!["HandleUrgentNeeds"]);
assert_eq!(behaviors[0].priority, Priority::Critical);
assert_eq!(behaviors[1].tree, vec!["BakeryWork"]);
assert_eq!(behaviors[1].priority, Priority::Normal);
assert!(behaviors[1].condition.is_some()); // Has when clause
assert_eq!(behaviors[2].tree, vec!["Idle"]);
assert_eq!(behaviors[2].priority, Priority::Low);
},
| _ => panic!("Expected Character declaration"),
}
}
#[test]
fn test_character_with_schedule_link() {
let input = r#"
character Martha: Human {
uses schedule: BakerSchedule
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Character(c) => {
assert!(c.uses_schedule.is_some());
let schedules = c.uses_schedule.as_ref().unwrap();
assert_eq!(schedules.len(), 1);
assert_eq!(schedules[0], "BakerSchedule");
},
| _ => panic!("Expected Character declaration"),
}
}
#[test]
fn test_character_with_multiple_schedules() {
let input = r#"
character Martha: Human {
uses schedules: [WorkdaySchedule, WeekendSchedule]
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Character(c) => {
let schedules = c.uses_schedule.as_ref().unwrap();
assert_eq!(schedules.len(), 2);
assert_eq!(schedules[0], "WorkdaySchedule");
assert_eq!(schedules[1], "WeekendSchedule");
},
| _ => panic!("Expected Character declaration"),
}
}
#[test]
fn test_character_with_behaviors_and_schedule() {
let input = r#"
character Martha: Human {
age: 42
uses behaviors: [
{ tree: BakeryWork, priority: normal }
]
profession: "Baker"
uses schedule: BakerSchedule
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Character(c) => {
// Check fields
assert_eq!(c.fields.len(), 2);
// Check behaviors
assert!(c.uses_behaviors.is_some());
let behaviors = c.uses_behaviors.as_ref().unwrap();
assert_eq!(behaviors.len(), 1);
// Check schedule
assert!(c.uses_schedule.is_some());
let schedules = c.uses_schedule.as_ref().unwrap();
assert_eq!(schedules.len(), 1);
},
| _ => panic!("Expected Character declaration"),
}
}
#[test]
fn test_institution_with_behavior_links() {
let input = r#"
institution Bakery {
type: "Business"
uses behaviors: [
{ tree: BakeryOperations, priority: high }
]
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Institution(inst) => {
assert_eq!(inst.name, "Bakery");
assert!(inst.uses_behaviors.is_some());
let behaviors = inst.uses_behaviors.as_ref().unwrap();
assert_eq!(behaviors.len(), 1);
assert_eq!(behaviors[0].tree, vec!["BakeryOperations"]);
assert_eq!(behaviors[0].priority, Priority::High);
},
| _ => panic!("Expected Institution declaration"),
}
}
#[test]
fn test_qualified_path_behavior_tree() {
let input = r#"
character Martha: Human {
uses behaviors: [
{ tree: village::baker::BakeryWork, priority: normal }
]
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Character(c) => {
let behaviors = c.uses_behaviors.as_ref().unwrap();
assert_eq!(behaviors[0].tree, vec!["village", "baker", "BakeryWork"]);
},
| _ => panic!("Expected Character declaration"),
}
}
#[test]
fn test_all_priority_levels() {
let input = r#"
character Test: Human {
uses behaviors: [
{ tree: A, priority: low },
{ tree: B, priority: normal },
{ tree: C, priority: high },
{ tree: D, priority: critical }
]
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Character(c) => {
let behaviors = c.uses_behaviors.as_ref().unwrap();
assert_eq!(behaviors[0].priority, Priority::Low);
assert_eq!(behaviors[1].priority, Priority::Normal);
assert_eq!(behaviors[2].priority, Priority::High);
assert_eq!(behaviors[3].priority, Priority::Critical);
},
| _ => panic!("Expected Character declaration"),
}
}
#[test]
fn test_template_with_simple_behavior_links() {
let input = r#"
template Baker {
uses behaviors: BakingSkills, CustomerService
specialty: "bread"
baking_skill: 0.0..1.0
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Template(t) => {
assert_eq!(t.name, "Baker");
// Check behavior links
assert!(t.uses_behaviors.is_some());
let behaviors = t.uses_behaviors.as_ref().unwrap();
assert_eq!(behaviors.len(), 2);
assert_eq!(behaviors[0].tree, vec!["BakingSkills"]);
assert_eq!(behaviors[0].priority, Priority::Normal);
assert_eq!(behaviors[1].tree, vec!["CustomerService"]);
assert_eq!(behaviors[1].priority, Priority::Normal);
},
| _ => panic!("Expected Template declaration"),
}
}
#[test]
fn test_template_with_schedule_link() {
let input = r#"
template Baker {
uses schedule: BakerSchedule
specialty: "bread"
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Template(t) => {
assert_eq!(t.name, "Baker");
// Check schedule link
assert!(t.uses_schedule.is_some());
let schedules = t.uses_schedule.as_ref().unwrap();
assert_eq!(schedules.len(), 1);
assert_eq!(schedules[0], "BakerSchedule");
},
| _ => panic!("Expected Template declaration"),
}
}
#[test]
fn test_template_with_behaviors_and_schedule() {
let input = r#"
template Baker {
include Person
uses behaviors: BakingSkills, CustomerService
uses schedule: BakerSchedule
specialty: "bread"
baking_skill: 0.0..1.0
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Template(t) => {
assert_eq!(t.name, "Baker");
// Check include
assert_eq!(t.includes.len(), 1);
assert_eq!(t.includes[0], "Person");
// Check behavior links
assert!(t.uses_behaviors.is_some());
let behaviors = t.uses_behaviors.as_ref().unwrap();
assert_eq!(behaviors.len(), 2);
// Check schedule link
assert!(t.uses_schedule.is_some());
let schedules = t.uses_schedule.as_ref().unwrap();
assert_eq!(schedules.len(), 1);
},
| _ => panic!("Expected Template declaration"),
}
}

View File

@@ -0,0 +1,324 @@
//! Tests for year-long composable schedule system
use crate::syntax::{
ast::*,
lexer::Lexer,
FileParser,
};
#[test]
fn test_simple_schedule_backward_compat() {
let input = r#"
schedule DailyRoutine {
09:00 -> 17:00 : work {
place: "Office"
}
18:00 -> 19:00 : dinner {}
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.name, "DailyRoutine");
assert!(s.extends.is_none());
assert_eq!(s.blocks.len(), 2);
assert_eq!(s.recurrences.len(), 0);
// First block
assert_eq!(s.blocks[0].activity, "work");
assert!(s.blocks[0].name.is_none());
assert!(!s.blocks[0].is_override);
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_schedule_extends() {
let input = r#"
schedule BakerSchedule extends BaseWorkday {
block work { 05:00 -> 13:00, action: BakingWork }
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.name, "BakerSchedule");
assert_eq!(s.extends, Some("BaseWorkday".to_string()));
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_named_block_with_action() {
let input = r#"
schedule WorkSchedule {
block work { 09:00 -> 17:00, action: WorkBehavior }
block lunch { 12:00 -> 13:00, action: EatLunch }
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.blocks.len(), 2);
// First block
assert_eq!(s.blocks[0].name, Some("work".to_string()));
assert!(!s.blocks[0].is_override);
assert_eq!(s.blocks[0].action, Some(vec!["WorkBehavior".to_string()]));
assert_eq!(s.blocks[0].start.hour, 9);
assert_eq!(s.blocks[0].end.hour, 17);
// Second block
assert_eq!(s.blocks[1].name, Some("lunch".to_string()));
assert_eq!(s.blocks[1].action, Some(vec!["EatLunch".to_string()]));
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_override_block() {
let input = r#"
schedule BakerSchedule extends BaseWorkday {
override work { 05:00 -> 13:00, action: BakingWork }
block prep { 03:00 -> 05:00, action: PrepBread }
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.blocks.len(), 2);
// Override block
assert_eq!(s.blocks[0].name, Some("work".to_string()));
assert!(s.blocks[0].is_override);
assert_eq!(s.blocks[0].action, Some(vec!["BakingWork".to_string()]));
// Regular named block
assert_eq!(s.blocks[1].name, Some("prep".to_string()));
assert!(!s.blocks[1].is_override);
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_recurrence_pattern() {
let input = r#"
schedule WeeklySchedule {
recurrence MarketDay on Earthday {
block market { 08:00 -> 13:00, action: SellAtMarket }
block restock { 14:00 -> 16:00, action: RestockGoods }
}
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.recurrences.len(), 1);
let recurrence = &s.recurrences[0];
assert_eq!(recurrence.name, "MarketDay");
assert_eq!(
recurrence.constraint,
TemporalConstraint::DayOfWeek("Earthday".to_string())
);
assert_eq!(recurrence.blocks.len(), 2);
// First block in recurrence
assert_eq!(recurrence.blocks[0].name, Some("market".to_string()));
assert_eq!(
recurrence.blocks[0].action,
Some(vec!["SellAtMarket".to_string()])
);
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_qualified_path_action() {
let input = r#"
schedule WorkSchedule {
block work { 09:00 -> 17:00, action: village::baker::BakingWork }
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(
s.blocks[0].action,
Some(vec![
"village".to_string(),
"baker".to_string(),
"BakingWork".to_string()
])
);
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_mixed_blocks_and_recurrences() {
let input = r#"
schedule CompleteSchedule extends BaseSchedule {
block morning { 06:00 -> 12:00, action: MorningRoutine }
recurrence Weekend on Spiritday {
block leisure { 10:00 -> 18:00, action: RelaxAndPlay }
}
override evening { 18:00 -> 22:00, action: EveningRoutine }
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.name, "CompleteSchedule");
assert_eq!(s.extends, Some("BaseSchedule".to_string()));
assert_eq!(s.blocks.len(), 2); // morning and override evening
assert_eq!(s.recurrences.len(), 1); // Weekend
// Check we have both regular and override blocks
let has_regular = s.blocks.iter().any(|b| !b.is_override);
let has_override = s.blocks.iter().any(|b| b.is_override);
assert!(has_regular && has_override);
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_block_with_fields() {
let input = r#"
schedule WorkSchedule {
block work {
09:00 -> 17:00
action: WorkBehavior
intensity: "high"
place: "office"
}
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.blocks[0].fields.len(), 2); // intensity and place
assert_eq!(s.blocks[0].action, Some(vec!["WorkBehavior".to_string()]));
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_empty_schedule() {
let input = r#"
schedule EmptySchedule {
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.blocks.len(), 0);
assert_eq!(s.recurrences.len(), 0);
},
| _ => panic!("Expected Schedule declaration"),
}
}
#[test]
fn test_schedule_only_recurrences() {
let input = r#"
schedule EventSchedule {
recurrence Festival on FirstDayOfSummer {
block celebration { 10:00 -> 22:00, action: Celebrate }
}
recurrence MarketDay on Earthday {
block market { 08:00 -> 14:00, action: TradingBehavior }
}
}
"#;
let lexer = Lexer::new(input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let file = result.unwrap();
match &file.declarations[0] {
| Declaration::Schedule(s) => {
assert_eq!(s.blocks.len(), 0);
assert_eq!(s.recurrences.len(), 2);
},
| _ => panic!("Expected Schedule declaration"),
}
}