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:
@@ -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
188
src/syntax/keywords.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>),
|
||||
|
||||
14899
src/syntax/parser.rs
14899
src/syntax/parser.rs
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
367
src/syntax/resource_linking_tests.rs
Normal file
367
src/syntax/resource_linking_tests.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
324
src/syntax/schedule_composition_tests.rs
Normal file
324
src/syntax/schedule_composition_tests.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user