Entity-Behavior Programming — a behavioral specification language for modeling event-driven systems
define entity Player {
property hp float default 500.0
property level int default 1
has PlayerCombatBehavior
}
define behavior PlayerCombatBehavior {
state Alive initial;
state Dead terminal;
on event Attacked {
set self.hp = self.hp - event.damage
if (self.hp <= 0.0) { transition Dead }
}
}
define scenario QuickTest {
spawn Player count 1 with hp = 80.0
emit Attacked { damage: 80.0 }
let players = entities_of("Player")
assert (players[0].alive == false) "player died correctly"
}
$ yuspec1 test demo.yus
Assertions: 1 passed, 0 failed PASS
YUSPEC (Your Universal Specification Language) is a declarative behavioral specification language built on the Entity-Behavior Programming (EBP) paradigm.
Programs are collections of Entities (typed archetypes with property tables) and Behaviors (composable finite-state machines that react to events, timeouts, and conditions). Communication happens exclusively through a global EventBus — entities never hold direct references to each other.
Important distinction: YUSPEC is a modeling and simulation language. It does not send real TCP packets, read physical sensor data, or perform actual deployments. It models the behavioral logic of these systems — the state machines, event flows, and decision rules — in a unified, testable notation.
| Concept | YUSPEC construct |
|---|---|
| Data | define entity — typed property table |
| Logic | define behavior — FSM with event handlers & rules |
| Communication | emit / on event — pub-sub EventBus |
| Testing | define scenario — assert, expect, log |
| Grouping | define zone — named runtime container |
The same language models behavioral logic across different problem spaces. All examples are FSM-based simulations — YUSPEC's strength is providing a unified notation for event-driven state machines regardless of the domain:
| Domain | Example |
| 🎮 Game Development | examples/game/01_mmo.yus — MMO RPG with combat, quests, leveling |
| 🌐 Network Protocols | examples/network/01_tcp_handshake.yus — TCP state machine |
| 📋 Workflow Automation | examples/workflow/01_approval.yus — multi-stage approval + escalation |
| ⚡ Distributed Systems | examples/distributed/01_orchestration.yus — canary deployment |
| 🌡️ IoT / Robotics | examples/iot/01_sensor.yus — sensor + HVAC controller |
| 🚦 Simulation | examples/simulation/01_traffic.yus — traffic lights + vehicles |
| ✅ Test Scripting | examples/testing/01_scenario.yus — YUSPEC as a testing DSL |
Describe what exists (entities, states, events), not how to iterate over them.
Multiple behaviors can coexist on a single entity, each evolving its own state independently. Behaviors are defined once and reused across many entity types.
define scenario is a first-class language construct. assert and expect
give structured pass/fail reporting with zero boilerplate.
- CMake 3.16+
- C++17 compiler: MSVC 2019+, GCC 9+, or Clang 10+
git clone https://github.com/Fovane/yuspec.git
cd yuspec
# Configure
cmake -S . -B build
# Build the CLI
cmake --build build --target yuspec1 --config DebugOn Windows (MSVC):
cmake -S . -B build -G "Visual Studio 17 2022"
cmake --build build --target yuspec1 --config Debug# Run all test scenarios
./build/Debug/yuspec1 test examples/testing/01_scenario.yus
# Run the MMO game example
./build/Debug/yuspec1 test examples/game/01_mmo.yus
# Validate syntax only
./build/Debug/yuspec1 validate examples/network/01_tcp_handshake.yus
# Execute a zone
./build/Debug/yuspec1 run examples/workflow/01_approval.yusyuspec1 tokens <file.yus> Dump token stream (debug)
yuspec1 parse <file.yus> Print AST
yuspec1 validate <file.yus> Semantic analysis only
yuspec1 run <file.yus> Execute all zones
yuspec1 test <file.yus> Run all scenarios, report pass/fail
Options:
--verbose Print tick trace and event log
--ticks N Max simulation ticks (default: 10000)
--tick-ms N Milliseconds per tick (default: 16)
--zone NAME Run a specific zone
--scenario NAME Run a specific scenario
define entity Monster {
property monster_id int default 0
property hp float default 100.0
property alive bool default true
has MonsterAI
has LootDropBehavior
}
Spawn instances inside a zone or scenario:
spawn Monster count 5 with monster_id = 1, hp = 200.0
define behavior MonsterAI {
state Idle initial timeout 3s;
state Chasing timeout 10s;
state Attacking;
state Dead terminal;
// State-gated transition
on timeout from Idle -> Chasing {
log "Monster spotted player!"
}
// Universal handler — fires from ANY state
on event PlayerAttack {
let net = event.damage - self.defense
if (net < 1.0) { set net = 1.0 }
set self.hp = self.hp - net
if (self.hp <= 0.0) {
emit EntityDied { entity_id: self.monster_id }
transition Dead
}
}
// Condition-based rule
rule AggroCheck
when self.hp < 30.0
then { log "Monster fleeing!" }
}
define event PlayerAttack {
property attacker_id int
property target_id int
property damage float
property skill string
}
Emit anywhere:
emit PlayerAttack { attacker_id: 1, target_id: 2, damage: 75.0, skill: "slash" }
define scenario CombatTest {
spawn Player count 1 with player_id = 1, hp = 500.0, attack = 70.0
spawn Monster count 1 with monster_id = 100, hp = 80.0, defense = 5.0
emit PlayerAttack { attacker_id: 1, target_id: 100, damage: 70.0, skill: "basic" }
wait 100ms
let monsters = entities_of("Monster")
assert (monsters[0].alive == false) "monster should be dead"
expect EntityDied "EntityDied should have been emitted"
}
| Type | Example literals |
|---|---|
int |
42, -7, 0 |
float |
3.14, -0.5, 1.0 |
bool |
true, false |
string |
"hello world" |
duration |
100ms, 3s, 1m, 2h |
list |
[1, 2, 3] |
map |
{ key: value } |
any |
escape hatch, no type check |
str(v) Convert any value to string
int(v) Convert to integer
float(v) Convert to float
sqrt(x) Square root
abs(x) Absolute value
max(a, b) Maximum
min(a, b) Minimum
floor(x) Floor
ceil(x) Ceiling
rand() Random float in [0, 1)
rand_int(a, b) Random int in [a, b]
len(list) List length
entities_of(t) All live Entity instances of type t
yuspec/
├── compiler/
│ ├── include/yuspec/
│ │ ├── v1_token.h TK enum, Token, SrcPos, Diag
│ │ ├── v1_ast.h All Expr/Action/Decl variant types
│ │ ├── v1_lexer.h
│ │ ├── v1_parser.h
│ │ └── v1_sema.h
│ └── src/
│ ├── v1_lexer.cpp Tokenizer
│ ├── v1_parser.cpp Recursive descent parser
│ └── v1_sema.cpp Two-pass semantic analysis
│
├── runtime/
│ ├── include/yuspec_rt/
│ │ ├── v1_value.h Value tagged-union, Env scope chain
│ │ ├── v1_ecs.h World, Entity, EntityId
│ │ ├── v1_event_bus.h EventBus, Event, history
│ │ ├── v1_sm_runtime.h SMInstance, SMManager
│ │ └── v1_interpreter.h Interpreter, Signal, RunConfig
│ └── src/
│ ├── v1_sm_runtime.cpp FSM lifecycle + Signal propagation
│ └── v1_interpreter.cpp Tree-walking executor
│
├── tools/yuspec1_cli/
│ └── main.cpp CLI entry point
│
└── examples/ 7 domain examples
Source text
│
▼
Lexer (v1_lexer.cpp)
│ Token stream
▼
Parser (v1_parser.cpp)
│ std::variant AST
▼
Sema (v1_sema.cpp)
│ Pass 1: collect declarations
│ Pass 2: resolve refs, type-check
▼
Interpreter (v1_interpreter.cpp)
│ Tree-walk execution
▼
Output / test results
World Entity lifecycle — create / destroy / tick / reset
EventBus Publish-subscribe — emit_tracked / subscribe / history
SMInstance Per-entity FSM — start / tick / handle_event / enter_state
SMManager Manages all active SMInstances
Interpreter exec_actions / eval_expr / run_scenario
Signal Propagation — the key correctness property:
execute_actions captures the Signal::Transition returned by the interpreter
and stores it as pending_transition_. After every action block (event handler,
on_enter, timeout), any pending transition is applied immediately — this makes
transition X inside event handlers work correctly.
| Scenario | Passed | Failed | Result |
|---|---|---|---|
| BasicVariablesTest | 3 | 0 | PASS |
| ArithmeticTest | 4 | 0 | PASS |
| StringTest | 4 | 0 | PASS |
| BooleanTest | 4 | 0 | PASS |
| EntityTest | 4 | 0 | PASS |
| EventTest | 3 | 0 | PASS |
| StateMachineTest | 8 | 0 | PASS |
| ControlFlowTest | 4 | 0 | PASS |
| Total | 34 | 0 | PASS |
| Scenario | Passed | Failed | Result |
|---|---|---|---|
| CombatTest | 2 | 0 | PASS |
| KillQuestTest | 2 | 0 | PASS |
| LevelUpTest | 3 | 0 | PASS |
| SkillCombatTest | 2 | 0 | PASS |
| PlayerDeathTest | 2 | 0 | PASS |
| Total | 11 | 0 | PASS |
We believe in being honest about what YUSPEC v1.0 can and cannot do.
YUSPEC models the behavioral logic of systems. It does not:
- Send real network packets (TCP example models the state machine, not the wire protocol)
- Read physical sensors (IoT example models thresholds and reactions, not hardware I/O)
- Perform actual deployments (distributed example models orchestration logic, not infrastructure)
The value proposition is a unified behavioral notation, not runtime integration with external systems. Think of it as "executable specifications" — closer to TLA+ or Alloy in intent, but with a developer-friendly syntax.
The current runtime evaluates AST nodes directly. Approximate performance:
| Approach | Relative Speed |
|---|---|
| Tree-walking (current) | 1x (baseline) |
| Bytecode VM (planned v1.2) | ~20-50x |
| JIT (future) | ~100-500x |
For simulations up to ~1,000 entities, v1.0 is adequate. For 10,000+ entities, a bytecode VM is necessary. This is the #1 priority for v1.2.
The single global EventBus checks every handler on every emit. With 1,000 entities
× 5 handlers each, that's 5,000 callback checks per event. Current mitigations: none.
Planned:
- Event filtering / topic-based routing
- Entity-scoped event channels
- Circular event chain detection (infinite loop prevention)
- Event tracing / visualization for debugging
v1.0 provides basic types (int, float, bool, string, duration, list, map)
and an any escape hatch. Notable gaps:
| Missing Feature | Impact |
|---|---|
define enum |
No DamageType.Physical — must use magic strings |
| Generic types | No list<int> — all lists are heterogeneous |
| Entity reference type | No property target: Entity<Monster> |
| Algebraic data types | No Option<T>, Result<T,E> |
The any type is an honest admission that the type system is incomplete.
Full type inference and custom types are planned for v1.1.
All definitions must live in a single .yus file. No import, no namespaces,
no behavior libraries. This is the #2 priority for v1.1.
YUSPEC v1.0 behaviors are flat finite-state machines (Moore-Mealy hybrid). Features available in Harel Statecharts / XState that are not supported:
| Statecharts Feature | Status in YUSPEC v1.0 | Impact |
|---|---|---|
| Hierarchical (nested) states | Not supported | Cannot model Combat.Melee vs Combat.Ranged as sub-states |
| Parallel (orthogonal) regions | Not supported | An entity cannot be in Moving AND Attacking simultaneously within one behavior |
| History states (shallow/deep) | Not supported | Cannot "return to previous state" after an interrupt |
| Guards on transitions | Partial — via rule when |
Rules evaluate each tick, not on transition edges |
| Entry/exit actions | on_enter only |
No on_exit actions when leaving a state |
Workaround: Multiple behaviors on one entity simulate parallel regions (e.g., has MovementBehavior + has CombatBehavior), but they share no state and communicate only via EventBus.
Planned: Hierarchical states and parallel regions are under evaluation for v2.0. The design question is whether added complexity justifies the expressiveness gain for YUSPEC's target use cases.
YUSPEC v1.0 has while loops and foreach iteration, making it theoretically
capable of general computation. However:
whileloops have a hard guard of 100,000 iterations — the interpreter forcibly breaks after this limit to prevent infinite loops- There are no user-defined functions (no recursion possible)
- The primary execution model is event-reactive: tick loop → evaluate rules → dispatch events → repeat
This makes YUSPEC bounded Turing-complete in practice:
| Property | Status |
|---|---|
while loops |
Yes, with 100K iteration guard |
foreach iteration |
Yes, over lists/maps |
| Recursion | No (no define function in v1.0) |
| Unbounded computation | No (tick limit + while guard) |
| Termination guarantee | Yes — all programs terminate |
This is a deliberate strength, not a weakness. YUSPEC programs are guaranteed to terminate. There is no halting problem. Every scenario, every simulation, every test will finish. This makes YUSPEC safe for automated testing pipelines and CI/CD integration. The 100K while-guard and configurable
--tickslimit ensure bounded execution time.
When define function is added in v1.1, recursion will become possible. At that
point, a recursion depth limit will be added to preserve the termination guarantee.
YUSPEC v1.0 uses a permissive/null-propagation error model. There is no
try/catch, no exceptions visible to YUSPEC code, and no explicit error type.
| Situation | Runtime behavior |
|---|---|
Access non-existent property (self.foo where foo not defined) |
Returns null — no crash |
Out-of-bounds list access (players[99] on empty list) |
Returns null — no crash |
Type mismatch in arithmetic ("hello" - 5) |
Returns null or 0 — silent |
entities_of("NonExistent") |
Returns empty list [] |
entities_of("Player")[0] on empty list |
Returns null — no crash |
| Division by zero | Returns null (float: inf/nan) |
| Unknown function call | Returns null |
| Event handler throws internally | C++ std::runtime_error — crashes the interpreter |
Design rationale: The permissive model was chosen because behavioral simulations
should be resilient to missing data. In a 1000-entity simulation, one monster with
a missing property should not crash the entire run. null propagation ensures
graceful degradation.
The cost: Silent failures. If you misspell self.heatlh instead of self.health,
you get null instead of a compile error. The semantic analyzer catches some of these
(undeclared properties in define entity), but runtime property access is unchecked.
Planned improvements (v1.1):
--strictmode: runtime null-access raises a diagnostic instead of silently returning null- Optional property types:
property hp float required— omitting it inspawnis a compile error - Runtime type mismatch warnings in
--verbosemode on_errorhandler in behaviors for controlled error recovery
define enum— enumerated types:define enum DamageType { Physical, Magical, True }define function— named functions with recursion depth limit- Import system:
import "path/file.yus" --strictmode: null-access becomes a diagnostic, type mismatches warn at runtimeon_errorhandler in behaviors for controlled error recovery- String interpolation:
"Player {self.name} has {self.hp} HP" - Entity reference types:
property target: Entity<Monster> - Full type inference — remove need for
anyescape hatch
- Bytecode compiler + VM — target 20-50x speedup over tree-walking
- Event filtering / topic-based routing — solve O(N×M) dispatch
- Circular event chain detection — prevent infinite loops at runtime
- Event tracing:
--traceflag dumps full event flow graph - Parallel zones with message passing
- Persistent state: serialize/deserialize World snapshots
- Recursion depth limit enforcement for
define function
- Language Server Protocol (LSP)
- VS Code extension: syntax highlighting, hover docs, inline errors
- Visual FSM editor + event flow visualization
- Profiler: per-behavior tick timing, event throughput metrics
- Hierarchical states (Statecharts-style nested states) — under evaluation
- Parallel regions (orthogonal state machines within one behavior)
- History states (shallow/deep return-to-previous)
on_exitactions for state cleanup- Network-transparent EventBus (bridge to real systems via adapters)
- Plugin API: custom event sources (real sensors, network sockets, databases)
- Cluster mode: entities auto-sharded across nodes
- Hot-reload behaviors without stopping simulation
define behavior TCPConnection {
state Closed initial;
state SynSent;
state Established timeout 30s;
state FinWait;
state TimeWait timeout 2s;
on event SynAckReceived from SynSent -> Established {
emit AckSent { conn_id: self.conn_id }
log "Connection " + str(self.conn_id) + " established"
}
on timeout from Established -> FinWait {
emit FinSent { conn_id: self.conn_id }
}
}
define behavior DeploymentController {
state Idle initial;
state Canary retry 3;
state HealthCheck;
state FullRollout terminal;
state Rollback terminal;
on event HealthCheckPassed from HealthCheck -> FullRollout {
emit DeploymentComplete { service: self.service_name, version: self.new_version }
log "Full rollout successful"
}
on event HealthCheckFailed from HealthCheck -> Rollback {
emit RollbackStarted { service: self.service_name }
log "Rolling back!"
}
}
define behavior TemperatureSensor {
state Normal initial;
state Warning;
state Critical;
rule OverheatWarning
when self.temperature > 70.0 and self.temperature <= 85.0
then { emit TempWarning { sensor_id: self.sensor_id, temp: self.temperature } }
rule OverheatCritical
when self.temperature > 85.0
then {
emit TempCritical { sensor_id: self.sensor_id, temp: self.temperature }
transition Critical
}
}
See CONTRIBUTING.md for how to build, test, and submit changes.
Bug reports → GitHub Issues
Discussion → GitHub Discussions
MIT — Copyright (c) 2026 Yücel Sabah (sabahgamestudios.com)