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:
546
tests/cli_integration.rs
Normal file
546
tests/cli_integration.rs
Normal 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"));
|
||||
}
|
||||
30
tests/compiler_errors/00_multiple_errors.sb
Normal file
30
tests/compiler_errors/00_multiple_errors.sb
Normal 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
|
||||
}
|
||||
6
tests/compiler_errors/01_unexpected_token.sb
Normal file
6
tests/compiler_errors/01_unexpected_token.sb
Normal file
@@ -0,0 +1,6 @@
|
||||
// Error: Missing colon after field name
|
||||
// This demonstrates the UnexpectedToken parse error
|
||||
|
||||
character Martha {
|
||||
age 34
|
||||
}
|
||||
8
tests/compiler_errors/05_trait_out_of_range.sb
Normal file
8
tests/compiler_errors/05_trait_out_of_range.sb
Normal 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
|
||||
}
|
||||
12
tests/compiler_errors/07_unknown_life_arc_state.sb
Normal file
12
tests/compiler_errors/07_unknown_life_arc_state.sb
Normal 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
|
||||
}
|
||||
}
|
||||
8
tests/compiler_errors/08_schedule_overlap.sb
Normal file
8
tests/compiler_errors/08_schedule_overlap.sb
Normal 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
|
||||
}
|
||||
55
tests/compiler_errors/README.md
Normal file
55
tests/compiler_errors/README.md
Normal 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
|
||||
26
tests/compiler_errors/run_examples.sh
Executable file
26
tests/compiler_errors/run_examples.sh
Executable 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 "════════════════════════════════════════════════════════════════"
|
||||
54
tests/examples/all_types.sb
Normal file
54
tests/examples/all_types.sb
Normal 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
|
||||
}
|
||||
50
tests/examples/behavior_and_lifearc.sb
Normal file
50
tests/examples/behavior_and_lifearc.sb
Normal 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
|
||||
}
|
||||
}
|
||||
68
tests/examples/bidirectional_relationships.sb
Normal file
68
tests/examples/bidirectional_relationships.sb
Normal 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"
|
||||
}
|
||||
34
tests/examples/comparisons.sb
Normal file
34
tests/examples/comparisons.sb
Normal 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
|
||||
}
|
||||
}
|
||||
40
tests/examples/equality.sb
Normal file
40
tests/examples/equality.sb
Normal 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
|
||||
}
|
||||
}
|
||||
113
tests/examples/field_access.sb
Normal file
113
tests/examples/field_access.sb
Normal 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
|
||||
}
|
||||
}
|
||||
95
tests/examples/logical_operators.sb
Normal file
95
tests/examples/logical_operators.sb
Normal 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
|
||||
}
|
||||
}
|
||||
76
tests/examples/name_resolution.sb
Normal file
76
tests/examples/name_resolution.sb
Normal 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
|
||||
89
tests/examples/override_values.sb
Normal file
89
tests/examples/override_values.sb
Normal 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
|
||||
}
|
||||
}
|
||||
74
tests/examples/relationship_merging.sb
Normal file
74
tests/examples/relationship_merging.sb
Normal 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
|
||||
37
tests/examples/use_statements.sb
Normal file
37
tests/examples/use_statements.sb
Normal 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
|
||||
}
|
||||
54
tests/examples/validation_errors.sb
Normal file
54
tests/examples/validation_errors.sb
Normal 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
|
||||
// }
|
||||
// }
|
||||
Reference in New Issue
Block a user