Liya is a runtime for building programs around explicit causality.
Values form a directed causal graph: external facts enter through sources, pure computations derive new values, state nodes carry information across updates, invariants validate candidate worlds, and effects connect validated state to the outside world.
outside world
│
▼
source ──► signal ──► state
│ │ │
└──────────┴──────────┤
▼
invariant
│
valid world?
│
▼
effect ──► outside world
This model is useful for controllers, simulations, robotics, agents, reactive systems, and other programs where causal structure, safe state transitions, and explainability matter.
Equations, controllers, domain objects, and algorithms remain ordinary Julia code. Liya gives them an explicit causal runtime.
v0.1.0 release candidate
The current runtime includes:
- explicit causal graphs;
- pure reactive signals;
- temporal state;
- atomic multi-source transactions;
- rollback before effects;
- invariants;
- counterfactual execution;
- causal history;
- historical explanations;
- graph inspection;
- Graphviz export;
- explicit external effects.
From a local checkout:
using Pkg
Pkg.develop(path="/path/to/Liya.jl")Then:
using Liyausing Liya
rt = Runtime()
source!(rt, :temperature, 20.0)
signal!(rt, :error, [:temperature]) do temperature
temperature - 22.0
end
signal!(rt, :too_hot, [:temperature]) do temperature
temperature > 30.0
end
invariant!(rt, :safe_temperature, [:temperature]) do temperature
temperature < 100.0
end
effect!(rt, :report, [:temperature, :error, :too_hot]) do temperature, error, too_hot
println(
"temperature=$temperature ",
"error=$error ",
"too_hot=$too_hot"
)
end
set!(rt, :temperature, 35.0)The update propagates through the causal graph:
temperature
├──► error
├──► too_hot
└─────────────┐
▼
safe_temperature
│
▼
report
Inspect the result:
getvalue(rt, :temperature)
getvalue(rt, :error)
getvalue(rt, :too_hot)Ask Liya why a node changed:
why(rt, :report)Liya programs are built from five node kinds.
A source represents information supplied to the graph from outside.
rt = Runtime()
source!(rt, :temperature, 20.0)
source!(rt, :pressure, 1.0)Sources change through transactions:
set!(rt, :temperature, 25.0)or:
update!(rt,
:temperature => 25.0,
:pressure => 1.1,
)A source can contain any ordinary Julia value:
struct Pose
x::Float64
y::Float64
end
source!(rt, :pose, Pose(0.0, 0.0))A signal derives a value from other nodes.
signal!(rt, :double, [:x]) do x
x * 2
endMultiple dependencies are passed to the function in declaration order:
signal!(rt, :distance, [:x, :y]) do x, y
sqrt(x^2 + y^2)
endSignals form the pure computational layer of the graph.
Their returned value becomes the node's value and may trigger downstream computation.
:x ───────┐
▼
:double
│
▼
:quadruple
Example:
source!(rt, :x, 10)
signal!(rt, :double, [:x]) do x
x * 2
end
signal!(rt, :quadruple, [:double]) do double
double * 2
end
set!(rt, :x, 7)
getvalue(rt, :double) # 14
getvalue(rt, :quadruple) # 28Propagation continues only when a derived value actually changes.
A state node carries information between committed transactions.
source!(rt, :acceleration, 0.0)
source!(rt, :dt, 0.1)
state!(rt, :velocity, 0.0, [:acceleration, :dt]) do velocity, acceleration, dt
velocity + acceleration * dt
endThe first argument is the state's previous committed value.
The remaining arguments correspond to its declared dependencies.
Conceptually:
velocity(t + 1) =
f(
velocity(t),
acceleration(t),
dt(t)
)
This gives Liya an explicit representation of discrete-time memory.
For systems that advance with time even while numerical inputs stay unchanged, use an explicit clock source:
source!(rt, :tick, 0)
source!(rt, :acceleration, 1.0)
source!(rt, :dt, 0.1)
state!(rt, :velocity, 0.0, [:tick, :acceleration, :dt]) do velocity, _, acceleration, dt
velocity + acceleration * dt
end
set!(rt, :tick, 1)
set!(rt, :tick, 2)
set!(rt, :tick, 3)An invariant defines a condition required for a candidate world to commit.
invariant!(rt, :safe_temperature, [:temperature]) do temperature
temperature < 100.0
endA successful update:
set!(rt, :temperature, 50.0)A rejected update:
set!(rt, :temperature, 150.0)When validation fails, Liya restores the previous internal world and emits no effects.
Invariants may depend on derived values:
invariant!(rt, :motor_limits, [:motor_command]) do command
all(abs.(command) .<= 1.0)
endThis makes constraints part of the causal graph itself.
An effect performs an action after the candidate world has been validated.
effect!(rt, :report, [:temperature]) do temperature
println("temperature = $temperature")
endEffects are the boundary between the Liya graph and external behavior.
Typical effects include:
effect!(rt, :drive, [:motor_command]) do command
send_motor_command(command)
endeffect!(rt, :store, [:measurement]) do measurement
save_measurement(measurement)
endeffect!(rt, :publish, [:state]) do state
publish_state(state)
endThe return value of an effect is ignored.
Effects run after all affected signals and state nodes have been evaluated and all affected invariants have passed.
set! updates one source:
set!(rt, :temperature, 35.0)This is the convenience form of a one-source transaction.
update! changes several sources atomically:
update!(rt,
:x => 3.0,
:y => 4.0,
)All changes belong to one candidate world.
Derived nodes observe the complete updated frame:
old world
x = 1
y = 2
│
│ update!(x => 3, y => 4)
▼
candidate world
x = 3
y = 4
│
▼
derived computation
│
▼
invariants
│
▼
commit
This is especially useful for sensor frames, physical systems, synchronized observations, and controller inputs.
A transaction follows a defined sequence:
1. apply source changes
2. recompute affected signals
3. advance affected state nodes
4. validate affected invariants
5. commit the internal world
6. emit affected effects
7. record causal history
Errors during computation or validation restore the previous internal state.
Effects run after commit because they may interact with external systems.
If an effect throws, Liya raises EffectFailure while preserving the already validated internal world.
attempt evaluates a possible update without committing it.
proposal = attempt(
rt,
:temperature => 45.0,
)Inspect the resulting candidate world:
proposal.valid
proposal[:temperature]
proposal[:error]
proposal[:fan_power]Inspect values that would change:
diff(proposal)Example:
proposal = attempt(rt, :temperature => 30.0)
for change in diff(proposal)
println(
change.name,
": ",
change.before,
" -> ",
change.after
)
endPossible output:
temperature: 20.0 -> 30.0
error: -2.0 -> 8.0
The committed runtime remains unchanged:
getvalue(rt, :temperature)Rejected counterfactuals expose their failed invariant:
proposal = attempt(rt, :temperature => 150.0)
proposal.valid
# false
proposal.failed_invariant
# :safe_temperature
proposal.error
# InvariantViolation(...)This makes attempt useful for planning, simulation, validation, decision systems, and model-based control.
getvalue(rt, :motor_command)previous(rt, :motor_command)or:
getprevious(rt, :motor_command)causes(rt, :motor_command)This returns the dependencies that actually caused the most recent change.
dependencies(rt, :motor_command)dependents(rt, :temperature)why(rt, :motor_command)Example causal trace:
motor_command [signal]
collective_thrust [signal]
vertical_accel_command [signal]
altitude_error [signal]
flight [source]
The trace follows the actual causes of the latest change.
For boolean nodes:
why_not(rt, :attack)why_not reports the current dependency values and the causal context associated with the false result.
This is useful for decision graphs such as:
enemy_visible
│
▼
can_attack
│
▼
attack
Every committed transaction receives a monotonically increasing revision.
revision(rt)Inspect runtime history:
history(rt)Inspect history for one node:
history(rt, :motor_command)A history entry records causal information associated with a committed revision.
This allows historical explanations:
why(
rt,
:motor_command;
revision=12,
)Conceptually:
revision 10
target_z changed
↓
altitude_error changed
↓
motor_command changed
revision 11
velocity changed
↓
motor_command changed
revision 12
target_z + velocity changed
↓
motor_command changed
History size can be bounded when constructing the runtime.
describe(rt)Example:
temperature [source ] <- external
error [signal ] <- temperature
too_hot [signal ] <- temperature
safe_temperature [invariant] <- temperature
report [effect ] <- temperature, error, too_hot
Generate DOT source:
dot_source = dot(rt)graph is available as an alias:
graph(rt)Write the graph to disk:
write_dot("liya.dot", rt)The generated graph distinguishes:
source
signal
state
invariant
effect
This gives Liya programs a direct visual representation of their causal structure.
using Liya
rt = Runtime()
source!(rt, :target, 22.0)
source!(rt, :temperature, 20.0)
signal!(rt, :error, [:target, :temperature]) do target, temperature
target - temperature
end
signal!(rt, :heater_power, [:error]) do error
clamp(error / 10.0, 0.0, 1.0)
end
invariant!(rt, :safe_temperature, [:temperature]) do temperature
temperature < 100.0
end
effect!(rt, :apply_heater, [:heater_power]) do power
println("heater power = $power")
end
describe(rt)
set!(rt, :temperature, 18.0)
println("error = ", getvalue(rt, :error))
println("power = ", getvalue(rt, :heater_power))
why(rt, :apply_heater)
proposal = attempt(rt, :temperature => 30.0)
println("candidate valid = ", proposal.valid)
for change in diff(proposal)
println(
change.name,
": ",
change.before,
" -> ",
change.after
)
endThe repository includes executable examples for the main runtime features.
A guided tour through the runtime:
julia --project=. examples/playground.jlIt demonstrates:
- sources;
- signals;
- effects;
- invariants;
- rollback;
- atomic transactions;
- counterfactual execution;
- causal inspection;
- history;
- state;
- graph visualization.
julia --project=. examples/basic.jljulia --project=. examples/transactions.jljulia --project=. examples/counterfactual.jljulia --project=. examples/stateful_system.jljulia --project=. examples/sumo_robot.jljulia --project=. examples/drone/run.jlThe drone example combines Liya with ordinary Julia physics and control code.
Its causal graph includes:
target_z
│
├─────────────┐
▼ ▼
flight ──► altitude_error
│
▼
vertical_accel_command
│
▼
collective_thrust
│
▼
motor_command
│ │
▼ ▼
motor_limits apply_motor_command
Run the complete test suite:
julia --project=. -e 'using Pkg; Pkg.test()'The suite covers:
- source registration;
- signal propagation;
- unchanged-value propagation;
- atomic transactions;
- rollback;
- counterfactual execution;
- object identity preservation;
- state nodes;
- causal history;
- graph inspection;
why;why_not;- effect failure semantics;
- history configuration;
- drone-controller equivalence.
The drone regression test compares the Liya implementation with an equivalent imperative Julia controller over 2,000 simulation steps.
Runtime
Graph construction
────────────────────────────────────
source! external input
signal! pure derived value
state! temporal derived state
invariant! world constraint
effect! external action
Transactions
────────────────────────────────────
set! one-source transaction
update! atomic transaction
attempt counterfactual transaction
diff candidate value changes
Values and history
────────────────────────────────────
getvalue
getprevious
previous
revision
history
Causality
────────────────────────────────────
why
why_not
causes
dependencies
dependents
Inspection
────────────────────────────────────
describe
dot
graph
write_dot
Dependencies are declared directly:
signal!(rt, :c, [:a, :b]) do a, b
...
endThe graph itself records the causal structure of the program.
Signals express deterministic transformations between graph values.
inputs
↓
pure computation
↓
derived value
state! represents memory as a first-class causal concept.
previous state
+
current causes
↓
next state
Related source changes enter the graph together through update!.
Invariants validate candidate worlds before effects interact with external systems.
attempt evaluates possible worlds using the same graph semantics as committed transactions.
Current values, previous values, actual causes, static dependencies, revisions, history, and causal traces are inspectable through the runtime.
The causal program can be viewed directly as a graph.
Liya.jl/
├── Project.toml
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
│
├── src/
│ └── Liya.jl
│
├── test/
│ └── runtests.jl
│
├── examples/
│ ├── playground.jl
│ ├── basic.jl
│ ├── transactions.jl
│ ├── counterfactual.jl
│ ├── stateful_system.jl
│ ├── sumo_robot.jl
│ └── drone/
│
└── docs/
├── guide.md
└── api.md
MIT. See LICENSE.
