feat: implement storybook DSL with template composition and validation

Add complete domain-specific language for authoring narrative content for
agent simulations.

Features:
- Complete parser using LALRPOP + logos lexer
- Template composition (includes + multiple inheritance)
- Strict mode validation for templates
- Reserved keyword protection
- Semantic validators (trait ranges, schedule overlaps, life arcs, behaviors)
- Name resolution and cross-reference tracking
- CLI tool (validate, inspect, query commands)
- Query API with filtering
- 260 comprehensive tests (unit, integration, property-based)

Implementation phases:
- Phase 1 (Parser): Complete
- Phase 2 (Resolution + Validation): Complete
- Phase 3 (Public API + CLI): Complete

BREAKING CHANGE: Initial implementation
This commit is contained in:
2026-02-08 13:24:35 +00:00
commit 9c20dd4092
59 changed files with 25484 additions and 0 deletions

546
tests/cli_integration.rs Normal file
View File

@@ -0,0 +1,546 @@
//! Integration tests for the CLI tool
//!
//! These tests verify that the `sb` command-line tool works correctly
//! by testing it against real project files.
use std::{
fs,
path::PathBuf,
process::Command,
};
use tempfile::TempDir;
/// Helper to get the path to the compiled sb binary
fn sb_binary() -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push("sb");
path
}
/// Helper to create a temporary test project
fn create_test_project() -> TempDir {
let dir = TempDir::new().unwrap();
// Create a valid character file
fs::write(
dir.path().join("test.sb"),
r#"
character Martha {
age: 34
trust: 0.8
}
character David {
age: 42
health: 0.9
}
"#,
)
.unwrap();
dir
}
/// Helper to create a project with errors
fn create_invalid_project() -> TempDir {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("errors.sb"),
r#"
character Martha {
age: 200
trust: 1.5
}
"#,
)
.unwrap();
dir
}
#[test]
fn test_validate_valid_project() {
let project = create_test_project();
let output = Command::new(sb_binary())
.arg("validate")
.arg(project.path())
.output()
.expect("Failed to execute sb validate");
assert!(
output.status.success(),
"Validation should succeed for valid project. Stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✓ Validation successful"));
assert!(stdout.contains("Characters: 2"));
}
#[test]
fn test_validate_invalid_project() {
let project = create_invalid_project();
let output = Command::new(sb_binary())
.arg("validate")
.arg(project.path())
.output()
.expect("Failed to execute sb validate");
assert!(
!output.status.success(),
"Validation should fail for invalid project"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("Found 2 errors"));
assert!(stderr.contains("Trait 'age' has value 200"));
assert!(stderr.contains("Trait 'trust' has value 1.5"));
}
#[test]
fn test_validate_single_file() {
let project = create_test_project();
let file_path = project.path().join("test.sb");
let output = Command::new(sb_binary())
.arg("validate")
.arg(&file_path)
.output()
.expect("Failed to execute sb validate");
assert!(
output.status.success(),
"Validation should succeed for valid file"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✓ Validation successful"));
}
#[test]
fn test_validate_nonexistent_path() {
let output = Command::new(sb_binary())
.arg("validate")
.arg("/nonexistent/path/to/project")
.output()
.expect("Failed to execute sb validate");
assert!(!output.status.success(), "Should fail for nonexistent path");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("Path does not exist"));
}
#[test]
fn test_inspect_character() {
let project = create_test_project();
let output = Command::new(sb_binary())
.arg("inspect")
.arg("Martha")
.arg("--path")
.arg(project.path())
.output()
.expect("Failed to execute sb inspect");
assert!(output.status.success(), "Inspect should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Character: Martha"));
assert!(stdout.contains("age"));
assert!(stdout.contains("34"));
}
#[test]
fn test_inspect_nonexistent_entity() {
let project = create_test_project();
let output = Command::new(sb_binary())
.arg("inspect")
.arg("NonExistent")
.arg("--path")
.arg(project.path())
.output()
.expect("Failed to execute sb inspect");
assert!(output.status.success(), "Inspect runs even if not found");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("not found"));
}
#[test]
fn test_validate_empty_project() {
let dir = TempDir::new().unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(!output.status.success(), "Should fail for empty project");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("No .sb files found"));
}
#[test]
fn test_validate_shows_multiple_errors() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("multi_error.sb"),
r#"
character Alice {
age: 200
trust: 1.5
bond: -0.2
}
character Bob {
age: -10
love: 3.0
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
!output.status.success(),
"Should fail with validation errors"
);
let stderr = String::from_utf8_lossy(&output.stderr);
// Should show all 5 errors (non-fail-fast)
assert!(stderr.contains("Found 5 errors"));
assert!(stderr.contains("age")); // age: 200
assert!(stderr.contains("trust")); // trust: 1.5
assert!(stderr.contains("bond")); // bond: -0.2
assert!(stderr.contains("-10")); // age: -10
assert!(stderr.contains("love")); // love: 3.0
}
#[test]
fn test_cross_file_name_resolution() {
let dir = TempDir::new().unwrap();
// Create multiple files with characters
fs::write(
dir.path().join("file1.sb"),
r#"
character Martha {
age: 34
trust: 0.8
}
"#,
)
.unwrap();
fs::write(
dir.path().join("file2.sb"),
r#"
character David {
age: 42
health: 0.9
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
output.status.success(),
"Should successfully load and validate multiple files"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✓ Validation successful"));
assert!(stdout.contains("Characters: 2"));
}
#[test]
fn test_cross_file_duplicate_detection() {
let dir = TempDir::new().unwrap();
// Create two files with the same character name
fs::write(
dir.path().join("file1.sb"),
r#"
character Martha {
age: 34
}
"#,
)
.unwrap();
fs::write(
dir.path().join("file2.sb"),
r#"
character Martha {
age: 42
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
!output.status.success(),
"Should fail with duplicate definition error"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("Duplicate definition of 'Martha'"));
}
// ===== Template Composition Tests =====
#[test]
fn test_template_composition_with_includes() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("templates.sb"),
r#"
// Base template
template Being {
alive: true
}
// Template that includes Being
template Human {
include Being
kind: "human"
}
// Character with template composition
character Martha from Human {
firstName: "Martha"
age: 34
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
output.status.success(),
"Template composition with includes should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✓ Validation successful!"));
assert!(stdout.contains("Characters: 1"));
}
#[test]
fn test_template_composition_multiple_templates() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("templates.sb"),
r#"
template Physical {
height: 0
weight: 0
}
template Mental {
iq: 0
}
character David from Physical, Mental {
height: 180
weight: 75
iq: 120
firstName: "David"
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
output.status.success(),
"Multiple template inheritance should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✓ Validation successful!"));
}
#[test]
fn test_strict_template_validation_success() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("strict.sb"),
r#"
template Person strict {
age: 18..100
firstName: ""
}
character Martha from Person {
age: 34
firstName: "Martha"
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
output.status.success(),
"Strict template with concrete values should succeed"
);
}
#[test]
fn test_strict_template_validation_failure() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("strict_fail.sb"),
r#"
template Person strict {
age: 18..100
}
character Martha from Person {
firstName: "Martha"
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
!output.status.success(),
"Strict template with range value should fail"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("strict template"));
assert!(stderr.contains("range value"));
}
#[test]
fn test_template_chained_includes() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("chained.sb"),
r#"
template Being {
alive: true
}
template Human {
include Being
kind: "human"
}
template Person strict {
include Human
age: 18..100
}
character Martha from Person {
age: 34
firstName: "Martha"
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
output.status.success(),
"Chained template includes should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✓ Validation successful!"));
}
#[test]
fn test_reserved_keyword_field_name_fails_at_parse() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("reserved.sb"),
r#"
character Martha {
species: "human"
age: 34
}
"#,
)
.unwrap();
let output = Command::new(sb_binary())
.arg("validate")
.arg(dir.path())
.output()
.expect("Failed to execute sb validate");
assert!(
!output.status.success(),
"Field with reserved keyword name should fail at parse time"
);
let stderr = String::from_utf8_lossy(&output.stderr);
// Parser catches this as UnrecognizedToken before validation
assert!(stderr.contains("Parse error") || stderr.contains("UnrecognizedToken"));
}

View File

@@ -0,0 +1,30 @@
// Multiple errors in one file
// This demonstrates non-fail-fast error collection - all errors reported at once!
character Martha {
age: 200
trust: 1.5
bond: -0.3
}
character David {
age: -5
love: 2.0
}
life_arc Growth {
state child {
on age > 18 -> adult
}
state adult {
on age > 65 -> senior
on retired -> elderly
}
}
schedule BadSchedule {
08:00 -> 12:00: work
11:30 -> 13:00: lunch
12:30 -> 17:00: more_work
}

View File

@@ -0,0 +1,6 @@
// Error: Missing colon after field name
// This demonstrates the UnexpectedToken parse error
character Martha {
age 34
}

View File

@@ -0,0 +1,8 @@
// Error: Trait value outside valid range
// Demonstrates TraitOutOfRange validation error
character Martha {
age: 34
trust: 1.5
bond: -0.2
}

View File

@@ -0,0 +1,12 @@
// Error: Transition to undefined state
// Demonstrates UnknownLifeArcState validation error
life_arc Growth {
state child {
on age > 18 -> adult
}
state adult {
on age > 65 -> senior
}
}

View File

@@ -0,0 +1,8 @@
// Error: Schedule blocks overlap in time
// Demonstrates ScheduleOverlap validation error
schedule DailyRoutine {
08:00 -> 12:30: work
12:00 -> 13:00: lunch
13:00 -> 17:00: work
}

View File

@@ -0,0 +1,55 @@
# Compiler Error Examples
This directory contains example `.sb` files that demonstrate each type of error
the Storybook compiler can detect. Each file is intentionally incorrect to showcase
the error messages and helpful hints.
## How to Run
To see all error messages, validate each file individually:
```bash
# From the storybook root directory
cargo build --release
# Run each file to see its error
./target/release/sb validate tests/compiler_errors/01_unexpected_token.sb
./target/release/sb validate tests/compiler_errors/02_unexpected_eof.sb
./target/release/sb validate tests/compiler_errors/03_invalid_token.sb
# ... etc
```
Or use this script to show all errors:
```bash
#!/bin/bash
for file in tests/compiler_errors/*.sb; do
echo "═══════════════════════════════════════════════════════════"
echo "File: $(basename $file)"
echo "═══════════════════════════════════════════════════════════"
cargo run --bin sb -- validate "$file" 2>&1 || true
echo ""
done
```
## Error Categories
### Parse Errors (Syntax)
- `01_unexpected_token.sb` - Missing colon after field name
- `02_unexpected_eof.sb` - Incomplete declaration
- `03_invalid_token.sb` - Invalid character in syntax
- `04_unclosed_prose.sb` - Prose block missing closing `---`
### Validation Errors (Semantics)
- `05_trait_out_of_range.sb` - Trait value outside 0.0-1.0 range
- `06_age_out_of_range.sb` - Age value outside 0-150 range
- `07_unknown_life_arc_state.sb` - Transition to undefined state
- `08_schedule_overlap.sb` - Schedule blocks overlap in time
- `09_unknown_behavior_action.sb` - Undefined behavior tree action
- `10_duplicate_field.sb` - Same field name used twice
- `11_relationship_bond_out_of_range.sb` - Bond value outside 0.0-1.0 range
Each error includes:
- ✓ Clear error message explaining what went wrong
- ✓ Helpful hint on how to fix it
- ✓ Context-specific suggestions

View File

@@ -0,0 +1,26 @@
#!/bin/bash
# Script to run all compiler error examples and see the error messages
cd "$(dirname "$0")/../.."
echo "════════════════════════════════════════════════════════════════"
echo "STORYBOOK COMPILER ERRORS - EXAMPLES"
echo "════════════════════════════════════════════════════════════════"
echo ""
for file in tests/compiler_errors/*.sb; do
if [ -f "$file" ]; then
echo "═══════════════════════════════════════════════════════════════════"
echo "File: $(basename "$file")"
echo "═══════════════════════════════════════════════════════════════════"
cat "$file" | head -3 | tail -2 # Show the comment lines
echo ""
cargo run --quiet --bin sb -- validate "$file" 2>&1 || true
echo ""
echo ""
fi
done
echo "════════════════════════════════════════════════════════════════"
echo "ALL EXAMPLES COMPLETE"
echo "════════════════════════════════════════════════════════════════"

View File

@@ -0,0 +1,54 @@
// Test all static types
character Martha {
age: 34
name: "Martha Baker"
}
template GenericPerson {
age: 20..60
energy: 0.5..1.0
}
enum BondType {
romantic,
familial,
friendship
}
institution Bakery {
name: "Martha's Bakery"
address: downtown
capacity: 10
}
location Downtown {
name: "Downtown District"
population: 50000
}
species Human {
lifespan: 80
intelligence: high
}
schedule DailyRoutine {
06:00 -> 07:00: wake_up
07:00 -> 09:00: breakfast
09:00 -> 17:00: work
17:00 -> 18:00: dinner
22:00 -> 06:00: sleep
}
relationship Spousal {
Martha
David
bond: 0.9
relationship_type: romantic
}
relationship ParentChild {
Martha as parent
Tommy as child
bond: 1.0
}

View File

@@ -0,0 +1,50 @@
// Test behavior trees and life arcs
life_arc Childhood {
state infant {
on ready -> toddler
}
state toddler {
on ready -> child
}
state child {
on ready -> teen
}
}
behavior SimpleBehavior {
walk_around
}
behavior SequenceBehavior {
> {
check_energy
move_to_location
perform_action
}
}
behavior SelectorBehavior {
? {
try_option_a
try_option_b
fallback
}
}
behavior NestedBehavior {
> {
? {
check_condition_a
check_condition_b
}
perform_action
}
}
behavior WithSubtree {
> {
@helpers::check_preconditions
main_action
}
}

View File

@@ -0,0 +1,68 @@
// Test bidirectional relationship resolution
// Relationships can be declared from either participant's perspective
// Simple relationship with no self/other blocks
relationship Friendship {
Alice
Bob
bond: 0.8
years_known: 5
}
// Relationship with roles
relationship Marriage {
Martha as spouse
David as spouse
bond: 0.9
anniversary: "2015-06-20"
}
// Relationship with self/other blocks from one perspective
relationship ParentChild {
Martha as parent self {
responsibility: 1.0
protective: 0.9
} other {
dependent: 0.8
}
Tommy as child
}
// Asymmetric relationship - different roles
relationship EmployerEmployee {
Martha as employer self {
authority: 0.9
} other {
respect: 0.8
}
Elena as employee
}
// Complex relationship with shared and participant-specific fields
relationship RomanticPartnership {
Alice as partner self {
love: 0.95
trust: 0.9
} other {
attraction: 0.85
respect: 0.95
}
Charlie as partner
// Shared fields
commitment: 0.85
compatibility: 0.9
}
// Multiple relationships between same people with different names
relationship Friendship2 {
Alice
Charlie
bond: 0.7
}
relationship Coworkers {
Alice
Charlie
workplace: "TechCorp"
}

View File

@@ -0,0 +1,34 @@
// Test comparison expressions in life arcs
life_arc AgeProgression {
state child {
on age > 12 -> teen
}
state teen {
on age >= 18 -> adult
}
state adult {
on age > 65 -> senior
}
}
life_arc EnergyStates {
state rested {
on energy < 0.3 -> tired
}
state tired {
on energy <= 0.1 -> exhausted
}
state exhausted {
on energy >= 0.5 -> rested
}
}
life_arc HealthStates {
state healthy {
on health < 50 -> sick
}
state sick {
on health >= 80 -> healthy
}
}

View File

@@ -0,0 +1,40 @@
// Test equality expressions in life arcs
life_arc NameCheck {
state checking {
on name is "Alice" -> found_alice
on name is "Bob" -> found_bob
}
state found_alice {
on ready -> checking
}
state found_bob {
on ready -> checking
}
}
life_arc StatusCheck {
state monitoring {
on status is active -> active_state
on status is inactive -> inactive_state
}
state active_state {
on status is inactive -> inactive_state
}
state inactive_state {
on status is active -> active_state
}
}
life_arc FlagCheck {
state idle {
on completed is true -> done
on completed is false -> working
}
state working {
on completed is true -> done
}
state done {
on completed is false -> working
}
}

View File

@@ -0,0 +1,113 @@
// Test field access in relationship contexts
relationship Marriage {
PersonA as spouse
PersonB as spouse
self {
bond: 0.8
}
other {
bond: 0.8
}
}
life_arc RelationshipDynamics {
state stable {
// Field access with comparisons
on self.bond < 0.3 -> troubled
on other.bond < 0.3 -> troubled
on self.bond > 0.9 and other.bond > 0.9 -> thriving
}
state troubled {
on self.bond > 0.7 and other.bond > 0.7 -> stable
on self.bond < 0.1 or other.bond < 0.1 -> broken
}
state thriving {
on self.bond < 0.8 or other.bond < 0.8 -> stable
}
state broken {
on self.bond > 0.5 and other.bond > 0.5 -> troubled
}
}
life_arc CharacterStates {
state monitoring {
// Field access with self
on self.age > 18 -> adult
on self.energy < 0.2 -> exhausted
on self.health < 30 -> sick
// Field access with equality
on self.status is active -> active_state
on self.ready is true -> ready_state
}
state adult {
on self.age < 18 -> monitoring
}
state exhausted {
on self.energy > 0.7 -> monitoring
}
state sick {
on self.health > 80 -> monitoring
}
state active_state {
on self.status is inactive -> monitoring
}
state ready_state {
on self.ready is false -> monitoring
}
}
life_arc ComplexFieldAccess {
state checking {
// Nested field access patterns
on self.stats.health > 50 -> healthy
on other.profile.age < 18 -> young_other
// Field access with logical operators
on self.energy > 0.5 and self.health > 70 -> strong
on not self.ready -> waiting
on self.completed is true or other.completed is true -> done
// Mixed field access and regular identifiers
on self.score > threshold -> passed
on other.level is beginner and difficulty > 5 -> too_hard
}
state healthy {
on self.stats.health < 30 -> checking
}
state young_other {
on other.profile.age >= 18 -> checking
}
state strong {
on self.energy < 0.3 or self.health < 50 -> checking
}
state waiting {
on self.ready -> checking
}
state done {
on self.completed is false and other.completed is false -> checking
}
state passed {
on self.score < threshold -> checking
}
state too_hard {
on other.level is advanced or difficulty < 3 -> checking
}
}

View File

@@ -0,0 +1,95 @@
// Test logical operators in life arc transitions
life_arc ComplexConditions {
state monitoring {
// AND operator
on age > 18 and status is active -> adult_active
on energy > 0.5 and health > 80 -> healthy_energetic
// OR operator
on tired or hungry -> needs_rest
on age < 5 or age > 65 -> dependent
// NOT operator
on not ready -> waiting
on not completed -> in_progress
}
state adult_active {
on age < 18 or status is inactive -> monitoring
}
state healthy_energetic {
on energy < 0.3 or health < 50 -> monitoring
}
state needs_rest {
on not tired and not hungry -> monitoring
}
state dependent {
on age >= 5 and age <= 65 -> monitoring
}
state waiting {
on ready -> monitoring
}
state in_progress {
on completed -> monitoring
}
}
life_arc NestedLogic {
state checking {
// Complex nested conditions
on age > 18 and status is active and energy > 0.5 -> triple_and
on tired or hungry or sick -> any_problem
on not ready and not completed -> both_false
// Mixed operators
on age > 21 and status is verified or is_admin -> allowed
on health > 50 and not sick or emergency -> proceed
}
state triple_and {
on age < 18 or status is inactive or energy < 0.5 -> checking
}
state any_problem {
on not tired and not hungry and not sick -> checking
}
state both_false {
on ready or completed -> checking
}
state allowed {
on age < 21 and status is unverified and not is_admin -> checking
}
state proceed {
on health < 50 and sick and not emergency -> checking
}
}
life_arc BooleanLogic {
state idle {
// Boolean literals with operators
on enabled is true and paused is false -> running
on enabled is false or error is true -> stopped
on not initialized -> initializing
}
state running {
on enabled is false or paused is true -> idle
}
state stopped {
on enabled is true and error is false -> idle
}
state initializing {
on initialized -> idle
}
}

View File

@@ -0,0 +1,76 @@
// Test name resolution and duplicate detection
// These are all unique names - should register successfully
character Alice {
age: 30
name: "Alice Smith"
}
character Bob {
age: 35
name: "Bob Jones"
}
template PersonTemplate {
age: 18..80
health: 0.0..1.0
}
enum Status {
active,
inactive,
pending
}
life_arc AgeProgression {
state young {
on age > 18 -> adult
}
state adult {
on age > 65 -> senior
}
state senior {}
}
schedule DailyRoutine {
06:00 -> 08:00: wake_up
08:00 -> 17:00: work
17:00 -> 22:00: evening
22:00 -> 06:00: sleep
}
behavior SimpleBehavior {
walk_around
}
institution Library {
name: "City Library"
capacity: 100
}
relationship Friendship {
Alice
Bob
bond: 0.8
}
location Park {
name: "Central Park"
}
species Human {
lifespan: 80
}
// All names above are unique and should be registered in the name table
// The name table can be queried by kind:
// - Characters: Alice, Bob
// - Templates: PersonTemplate
// - Enums: Status
// - LifeArcs: AgeProgression
// - Schedules: DailyRoutine
// - Behaviors: SimpleBehavior
// - Institutions: Library
// - Relationships: Friendship
// - Locations: Park
// - Species: Human

View File

@@ -0,0 +1,89 @@
// Test override as field values
template HumanNeeds {
sleep: 0.8
food: 0.7
social: 0.5
health: 0.6
}
template BakerSchedule {
work_start: 6
work_end: 14
lunch_time: 12
}
// Override in field value - set operations
character Alice {
name: "Alice"
needs: @HumanNeeds {
sleep: 0.9
social: 0.7
}
}
// Override with remove operation
character Bob {
name: "Bob"
needs: @HumanNeeds {
remove social
sleep: 0.6
}
}
// Override with append operation
character Carol {
name: "Carol"
needs: @HumanNeeds {
append creativity: 0.8
food: 0.9
}
}
// Override with mixed operations
character David {
name: "David"
needs: @HumanNeeds {
sleep: 0.95
remove social
append exercise: 0.7
}
}
// Multiple overrides in same character
character Elena {
name: "Elena"
needs: @HumanNeeds {
sleep: 0.7
food: 0.8
}
daily_schedule: @BakerSchedule {
work_start: 5
remove lunch_time
}
}
// Empty override (inherits all)
character Frank {
name: "Frank"
needs: @HumanNeeds {
}
}
// Only removes
character Grace {
name: "Grace"
needs: @HumanNeeds {
remove sleep
remove food
}
}
// Only appends
character Henry {
name: "Henry"
needs: @HumanNeeds {
append rest: 0.5
append work: 0.8
}
}

View File

@@ -0,0 +1,74 @@
// Demonstration of relationship merging
// The same relationship can be declared multiple times from different perspectives
// The resolver will merge them into a single relationship
// First, define characters
character Alice {
age: 30
name: "Alice"
}
character Bob {
age: 32
name: "Bob"
}
// Declare the relationship from Alice's perspective
// In a multi-file system, this might be in alice.sb
relationship Friendship_AliceBob {
Alice self {
// Alice's feelings about the friendship
trust: 0.9
enjoyment: 0.95
} other {
// How Alice perceives Bob
reliability: 0.85
humor: 0.9
}
Bob
}
// Same relationship from Bob's perspective
// In a multi-file system, this might be in bob.sb
relationship Friendship_AliceBob {
Bob self {
// Bob's feelings about the friendship
trust: 0.85
enjoyment: 0.9
} other {
// How Bob perceives Alice
reliability: 0.95
humor: 0.8
}
Alice
}
// The resolver will:
// 1. Recognize these as the same relationship (same participants + name)
// 2. Merge the self/other blocks appropriately
// 3. Validate that shared fields (if any) have the same values
// Example with shared fields
relationship Professional_AliceBob {
Alice self {
respect: 0.9
}
Bob
// Shared field - must have same value in all declarations
workplace: "TechCorp"
}
// Same relationship, same shared field value
relationship Professional_AliceBob {
Bob self {
respect: 0.85
}
Alice
// This MUST match the value in the other declaration
workplace: "TechCorp"
}
// Note: If the shared field values differed, the resolver would
// report a validation error about conflicting values

View File

@@ -0,0 +1,37 @@
// Test use statement syntax
// Note: Multi-file resolution not yet implemented,
// but syntax is parsed and validated
// Single import - import one specific item
use characters::Martha;
use templates::GenericPerson;
use enums::BondType;
// Grouped import - import multiple items from same module
use characters::{David, Tommy, Elena};
use behaviors::{WorkAtBakery, SocialInteraction, DailyRoutine};
// Wildcard import - import everything from a module
use locations::*;
use schedules::*;
// Nested paths work too
use world::characters::npcs::Merchant;
use schema::core::needs::Hunger;
// After imports, define local declarations
character LocalCharacter {
age: 25
name: "Local Person"
}
template LocalTemplate {
age: 20..60
energy: 0.5..1.0
}
enum LocalEnum {
option_a,
option_b,
option_c
}

View File

@@ -0,0 +1,54 @@
// Test semantic validation errors
// Valid bond values (should parse and validate)
relationship GoodFriendship {
Alice
Bob
bond: 0.8
}
// Invalid bond value - too high (should validate with error)
// relationship BadFriendship1 {
// Carol
// David
// bond: 1.5 // Error: bond > 1.0
// }
// Invalid bond value - negative (should validate with error)
// relationship BadFriendship2 {
// Elena
// Frank
// bond: -0.1 // Error: bond < 0.0
// }
// Valid age
character YoungPerson {
age: 25
}
// Invalid age - negative (commented to allow file to parse)
// character InvalidPerson1 {
// age: -5 // Error: age < 0
// }
// Invalid age - too high (commented to allow file to parse)
// character InvalidPerson2 {
// age: 200 // Error: age > 150
// }
// Valid life arc with proper transitions
life_arc ValidLifeArc {
state start {
on ready -> end
}
state end {
// Terminal state
}
}
// Invalid life arc - transition to non-existent state (commented)
// life_arc InvalidLifeArc {
// state start {
// on ready -> nonexistent // Error: state 'nonexistent' not defined
// }
// }