Live Variables¶
Unreleased Feature
Live variables have not yet been released. This chapter documents planned functionality that is not currently available.
A live variable recomputes its value whenever something it depends on changes. You state the relationship once, and Verse tracks which variables were read while evaluating it and re-evaluates when any of them is updated.
If A is defined in terms of B, you do not write a callback or an observer to keep them in step, and you cannot forget to. The intent that A always reflects some function of B is in the declaration itself.
Live variables build a foundation for reactive programming constructs, including await, upon, and when. Understanding live variables is essential for working with Verse's event-driven programming model, particularly for game development scenarios where many values must stay synchronized.
Live Expressions¶
A live expression establishes a dynamic relationship between a variable and a guard. Once established, the target is automatically re-evaluated whenever any of the guard's dependencies change, keeping the variable in sync.
var X:int = 0
var Y:int = 0
set live X = Y+1 # X now tracks Y
set Y = 5
X = 6 # X followed Y without being assigned
In the above, set live X = Y+1 is a live expression, the target is the previously declared variable X and the guard is the expression Y+1 with a dependency on variable Y.
Live variables extend mutable variables (see Mutability) with automated dependency tracking: any variable read during the evaluation of the guard expression is tracked. When any of those variables change, the guard is re-evaluated, and the target variable updates automatically.
Declaration Forms¶
Live variables can be declared in several ways, each suited to different use cases:
# Live variable declaration
var live X:int = Exp
# Live assignment to existing variable
var X:int = 0
# ... later ...
set live X = Exp
# Immutable live variable
live Y:int = Exp
# Variable with a function type (with <reads> effect)
var X: F = Exp # Initial value computed normally
var live X: F = Exp # Initial value tracked for dependencies
# Immutable variable with a function type (with <reads> effect)
X: F = Exp # Initial value computed normally
live X: F = Exp # Initial value tracked for dependencies
# Input-output variable pairs
var In->Out: F = Exp # Initial value computed normally
var live In->Out: F = Exp # Initial value tracked for dependencies
In->Out: F = Exp # Initial value computed normally
live In->Out: F = Exp # Initial value tracked for dependencies
The most common form, var live X = Exp, creates a mutable variable whose initial value comes from evaluating the guard and subsequently updates whenever dependencies change. The guard expression can read other variables, and those reads are tracked to establish the dependency relationship.
The assignment form, set live X = Exp, converts an existing variable into a live variable by attaching a guard. This is useful when you need to make a variable reactive after initialization or conditionally based on program state.
Immutable live variables, declared with just live Y = Exp, cannot be directly written but still update automatically when their guard's dependencies change. This provides a read-only reactive value, useful for derived computations that should never be manually overridden.
Those three forms differ only in how the guard is attached; once attached, they all behave alike:
var Source:int = 1
var live Mutable:int = Source + 1 # live from its declaration
live Immutable:int = Source + 2 # never assigned, still tracks Source
var Attached:int = 0
set live Attached = Source + 3 # made live after initialization
set Source = 10
Mutable = 11
Immutable = 12
Attached = 13
When a variable's type is a function with the <reads> effect, as in X: F = Exp or var X: F = Exp, the variable becomes live through its type (assignments are filtered through the function, and changes to the function's dependencies trigger recalculation). The live keyword in the declaration determines whether the initial expression Exp is also tracked for dependencies. Without live, Exp is evaluated once; with live, dependencies in Exp are tracked and can trigger updates before the first assignment.
Input-output pairs, written In->Out: F = Exp or var In->Out: F = Exp, create two variables where one captures raw values and the other holds transformed values. Again, the live keyword controls whether the initial expression Exp is tracked for dependencies.
The following sections detail these more complex forms.
Functions as Types¶
Verse allows functions to be used as types for variables. When a function with the <reads> effect is used as a type, the variable automatically becomes live, updating whenever the function's dependencies change.
var Mult:int = 2
Multiply(Arg: int)<reads>:int = Arg * Mult
var X : Multiply
set X = 10 # X gets 20
set Mult = 1 # X gets 10
In this example, Multiply serves dual roles: it is both a function and a type for variable X.
When Multiply is used as a type, as in var X : Multiply, several things happen:
- The storage type of
Xbecomesint(the function's return type) - Values assigned to
Xmust beint(the function's parameter type) - Each assignment passes through the function:
set X = 10callsMultiply(10)and stores the result
Multiply also acts as a live expression: because it has a <reads> effect (it reads mutable variable Mult), the variable declaration becomes a live expression with Multiply as its guard. This creates two ways the value changes:
- Direct assignment:
set X = 10filters the value throughMultiply, storing 20 - Dependency updates:
set Mult = 1triggers recalculation, updatingXto 10
This pattern elegantly combines transformation (every write is filtered) with reactivity (changes to dependencies trigger updates).
Input-Output Variables¶
Input-output variable pairs capture both raw input values and their transformed outputs. The syntax var In->Out:F=Exp creates two related variables where Out is the writable variable and In automatically stores the untransformed value before it passes through function F.
This pattern elegantly handles common game scenarios where values must stay within dynamic constraints. Consider health that must remain within bounds:
health_clamp := class:
var Lower:int = 0
var Upper:int = 100
Evaluate(Value:int)<reads>:int =
if (Value < Lower) then Lower else if (Value > Upper) then Upper else Value
Bounds := health_clamp{}
var BaseHealth->Health: Bounds.Evaluate = 50
set Health = 75 # BaseHealth = 75, Health = 75
set Health = 120 # BaseHealth = 120, Health = 100 (clamped)
set Bounds.Upper = 60 # BaseHealth = 120, Health = 60 (reclamped)
When you write to Health, two things happen:
- The raw value is stored in
BaseHealth - The value is passed through
Bounds.Evaluate, and the result is stored inHealth
Because Bounds.Evaluate has a <reads> effect (it reads the mutable variables Lower and Upper), this becomes a live expression. When the constraints change, Health is automatically recalculated from BaseHealth.
How It Works¶
The declaration var BaseHealth->Health: Bounds.Evaluate = 50 creates a live expression where:
BaseHealthstores the raw input value (read-only from external perspective)Healthstores the clamped value (read-write)Bounds.Evaluateis the transformation function with a<reads>effect
The object Bounds is an instance of class health_clamp with mutable bounds Lower and Upper. Because Evaluate reads these mutable variables, changes to them trigger recalculation:
set Health=75— The value passes through unchanged, so bothBaseHealthandHealthbecome 75set Health=120— ExceedsUpper, soBaseHealthbecomes 120 butHealthbecomes 100set Bounds.Upper=60— The constraint changes, triggering recalculation:Healthupdates to 60 whileBaseHealthremains 120
Using an instance method like Bounds.Evaluate allows multiple independent clamps in the same context, each with its own dynamic bounds.
Access Control¶
The scope of input and output variables can be controlled independently by adding access specifiers: for example var In<private>->Out<public>:t makes the base value private while exposing the constrained value publicly.
Restricted Effects and Stability¶
Live variable guards cannot have the <writes> effect. This fundamental restriction prevents side effects during guard evaluation, which Verse must be able to perform freely whenever dependencies change.
var X:int = 0
var Calls:int = 0
set live X = block:
set Calls += 1 # ERROR: a guard cannot have the writes effect
Calls
Live variables with interdependencies can form cycles. When target expressions use idempotent operations and values are comparable, these cycles can naturally converge to fixed points.
var X:int = 2
var Y:int = 2
set live X = if (Y < 0) then 0 else Y - 1
set live Y = if (X < 0) then 0 else X - 1
X = -1 # stabilized
Y = 0
If the type of the variable is comparable, the guards are re-evaluated until values stabilize. In this example, X decrements to -1, Y clamps to 0, and X would recompute but produces -1 again, so the system stabilizes.
However, cycles without proper termination conditions can diverge. Verse cannot prevent all divergence—care must be taken when designing interdependent live variables.
This has a subtle implication: since any variable might become live after creation, reading any variable must be assumed to potentially trigger guard evaluation and, in the worst case, trigger a cycle. The effect system accounts for this: the <writes> effect implies <diverges> because any write might trigger cyclic live variable evaluation. The following illustrates a cyclic definition when X is larger than 0:
var X:int = 1
var live Y:int = if (X>0) then X+1 else 0
set live X = Y # Y feeds X feeds Y: never stabilizes
Tracking Dependencies¶
Live variables track dependencies dynamically at runtime, not statically from source code. A variable becomes a dependency only when it is actually read during evaluation, not merely when it appears in the guard expression:
- Runtime tracking: Dependencies are determined by which variables are actually accessed during each evaluation
- Transitive tracking: Dependencies include variables read in called functions
- Dynamic changes: The dependency set can change from one evaluation to the next
Consider this example:
var X:int = 1
var Y:int = 2
var Z:int = 3
Choose(Value:int)<reads>:int =
if (Value > 0) then X else Y
var live W:int = Choose(Z)
W = 1 # dependencies: {Z, X}
set Z = 0
W = 2 # dependencies: {Z, Y}
Initially, Choose(Z) reads Z (which is 3) and evaluates the then branch, reading X, yielding W=1 with dependencies {Z, X}.
After set Z=0, the change to Z triggers re-evaluation. Now Choose(Z) reads Z (which is 0) and evaluates the else branch, reading Y. This results in W=2 with new dependencies {Z, Y}.
Notice how Y became a dependency only when the execution path changed. If X is subsequently modified, W will not update because X is no longer in the dependency set. This dynamic tracking ensures that live variables only react to changes that actually affect their current value.
Turning Off Liveness¶
A live variable established through its guard (not its type) can be turned off by a subsequent regular assignment.
var X:int = 0
var Y:int = 5
set live X = Y # X is now live, tracking Y
set Y = 10
X = 10
set X = 20 # X is now a regular variable again
set Y = 15
X = 20 # no longer tracking Y
This allows temporary reactive behavior that can be disabled when no longer needed. However, variables that are live through their type expression remain live permanently—their reactive behavior is intrinsic to their type.
Reactive Constructs¶
Live variables form the foundation for three reactive constructs that handle asynchronous events without explicit callbacks: await, upon, and when.
The await Expression¶
The await expression suspends execution until a target expression succeeds, providing a synchronization primitive for asynchronous programming.
gauge := class:
var Level:int = 0
DoubleWhenFull(Source:gauge, Target:gauge)<suspends>:void =
await{Source.Level > 10} # suspends until the guard succeeds
set Target.Level = Source.Level * 2
The target expression is evaluated immediately. If it fails, the task suspends. Verse tracks which variables were read during evaluation. Whenever those variables change, the guard is re-evaluated. If it succeeds, execution resumes immediately.
The practical implications are that you can write code that naturally expresses "wait for this condition" without manually managing event handlers or callback registration. The code suspends at the await point and resumes exactly when the condition becomes true.
The guard expression must have effects <reads><computes><decides> (see Effects)—it can read and compute but cannot write. This ensures re-evaluation is side-effect free. The body of await also cannot contain branch expressions, since branch requires a <suspends> context and the guard must remain side-effect free.
The upon Expression¶
The upon expression provides one-shot reactive behavior: when a condition becomes true, execute some code once. Unlike await, which resumes the current task, upon creates a new concurrent task that runs when triggered.
var Health:int = 100
var IsDead:logic = false
upon(Health <= 0):
set IsDead = true
set Health = 50
IsDead = false # the guard still fails
set Health = 0
IsDead = true # the body ran
set IsDead = false
set Health = -10
IsDead = false # the upon is spent; it fires only once
The upon expression evaluates its guard immediately and records the variables read. It then yields a task(t) where t is the result type of the body, representing the pending reactive behavior. When dependencies change, the guard is re-evaluated. If it succeeds, the body executes once in a new concurrent task, and the upon completes.
This one-shot behavior makes upon perfect for state transitions and event notifications. When a threshold is crossed, when a resource becomes available, when a timer expires—these scenarios naturally map to upon's "fire once when condition becomes true" semantics.
The body must have the <transacts> effect (see Effects), allowing it to read and write variables (including other live variables), with execution guaranteed to be atomic with respect to notifications.
The when Expression¶
The when expression provides continuous reactive behavior: every time a condition is true, execute some code. This creates a persistent observer that runs whenever its guard succeeds.
var Score:int = 0
var Updates:int = 0
when(Score):
set Updates += 1
Updates = 1 # the guard succeeds at once, so the body runs at once
set Score = 100
Updates = 2 # and again on every write to Score
set Score = 100
Updates = 3 # even when the value written is unchanged
The when expression evaluates its guard immediately. If the guard succeeds, the body executes. Then it records the variables read by the guard and yields a task(void). Whenever one of those dependencies is written and the guard succeeds, the body executes again, creating a continuous observation loop. The trigger is the write itself, not a change in value: assigning a variable the value it already held still re-runs the body. Inside a batch, by contrast, repeated writes collapse into a single notification.
This makes when ideal for maintaining derived state and responding to ongoing changes. Synchronizing UI with game state, updating AI behavior based on player actions, or maintaining consistency between related variables all benefit from when's persistent reactivity.
var X:int = 2
var Y:int = 2
when(Y):
Z := if (Y < 0) then 0 else Y - 1
if (Z <> X):
set X = Z
when(X):
Z := if (X < 0) then 0 else X - 1
if (Z <> Y):
set Y = Z
X = -1 # stabilized
Y = 0
The body executes with the <transacts> effect, and the when immediately re-registers after each execution, creating the continuous observation pattern.
Cancellation¶
All three reactive constructs—await, upon, and when—return a task that can be canceled, allowing dynamic control over reactive behavior.
StopObserving()<suspends>:void =
var X:int = 0
var Y:int = 0
Watch := upon(X > 5):
set Y = X
Watch.Cancel() # drops the dependency on X
set X = 10 # Y remains 0
Canceling a task immediately removes all dependency tracking and prevents the associated code from running. This provides fine-grained control over the lifecycle of reactive behaviors, allowing you to enable and disable observations based on game state or user actions.
The batch Expression¶
The batch expression groups multiple variable updates together, delaying notifications until the entire group completes. This prevents intermediate states from triggering reactive behaviors and ensures observers see consistent snapshots of related changes.
var X:int = 0
var Y:int = 0
var Notices:int = 0
var NoticesInBatch:int = 0
when(X > 1 and Y < 10):
set Notices += 100 # never fires: X > 1 and Y < 10 never hold together
when(X):
set Notices += 1
Notices = 1 # the second when fired once, on registration
batch:
set X = 2
set Y = 10
set X += 5
set NoticesInBatch = Notices
X = 7
NoticesInBatch = 1 # nothing was notified inside the batch
Notices = 2 # the three writes collapsed into one notification
Inside a batch block, variable updates occur immediately but notifications to awaiting tasks and reactive constructs are deferred. When the batch completes, all pending notifications fire in the order their triggers occurred, but observers see the final consistent state rather than intermediate values.
If the same notification occurs twice, only the first of them will be delivered.
Batch expressions nest: notifications are delayed until all enclosing batches complete. This composability ensures that no matter how deeply nested your code, you can guarantee atomic updates of related variables.
The body of a batch must not have the <suspends> effect—all operations must complete immediately. This ensures batch blocks have well-defined boundaries and can't leave the system in an inconsistent state by suspending mid-update.
Issues and Patterns¶
API Design¶
Any variable appearing in the public interface of a class or module can be made live by external code, potentially violating class invariants. To avoid this, one could limit the exposure of mutable variables or at least use access modifiers to control this:
score_board := class:
var Raw:int = 0
var<private> live Shown<public>:int = Raw + 1
Here Shown is publicly visible for reading but can only be updated by the class itself. This prevents external code from attaching arbitrary guards that might break the class's invariants.
Failures and Liveness¶
Live variable updates and reactive construct triggers are integrated in the failure semantics of Verse. When there is a failure, live variable updates are rolled back and their notifications are suppressed.
var X:int = 0
var Y:int = 0
var Fired:int = 0
upon(X > 0):
set Fired += 1
if:
set live X = Y + 5 # establishes the live relationship
false? # ...but the transaction fails
set Y = 10
X = 0 # the live relationship was rolled back
Fired = 0 # and its notification was suppressed
This ensures that reactive behaviors only observe committed changes, maintaining consistency even in the presence of speculative execution and failure.
Derived Synchronization¶
A common pattern is for multiple UI elements to reflect the same game state, when provides automatic synchronization:
var PlayerScore:int = 0
var DisplayedScore:int = 0
var ScoreText:string = ""
when(PlayerScore):
set DisplayedScore = PlayerScore
set ScoreText = "Score: {PlayerScore}"
set PlayerScore = 42
DisplayedScore = 42
ScoreText = "Score: 42"
Every change to PlayerScore automatically updates both the numeric display value and the formatted text, keeping the UI consistent without manual coordination.
Conditional Reactivity¶
Live variables can track different sources based on conditions:
var UseAlternate:logic = false
var PrimaryValue:int = 10
var AlternateValue:int = 20
var CurrentValue:int = 0
set live CurrentValue =
if (UseAlternate?) then AlternateValue else PrimaryValue
CurrentValue = 10
set UseAlternate = true
CurrentValue = 20
set AlternateValue = 30
CurrentValue = 30
set PrimaryValue = 15
CurrentValue = 30 # PrimaryValue is no longer a dependency
The dependency tracking is dynamic: when the condition changes, the set of tracked variables changes accordingly, allowing flexible reactive routing.
Resource Loading¶
Use upon for one-time initialization when resources become available:
resource_manager := class:
var TextureLoaded:logic = false
var ModelLoaded:logic = false
Initialize()<suspends>:void =
upon(TextureLoaded? and ModelLoaded?):
StartGame()
This pattern eliminates manual tracking of loading state. When both resources finish loading, the game starts automatically.
Modifier Stack (Under Consideration)¶
Not Finalized
The design of modifier_stack has not been finalized; material presented here is likely to change, and no part of it is accepted by the shipping compiler.
Game development often requires applying multiple modifiers to a single value. For instance, a player's health might need to be clamped to a valid range, temporarily boosted by a health potion and automatically recomputed when dependencies change.
The modifier_stack pattern provides a composable solution using live variables and function-as-type, allowing ordered transformations that automatically update when any modifier's dependencies change.
The modifier stack consists of three components:
modifier_interface(t)- An interface for modifiers that transform values of typetmodifier_stack(t)- A container that orders and composes modifiers- Live variable - Uses
modifier_stack.Evaluateas its type for automatic reactivity
When you assign to a live variable with a modifier stack type, the value flows through each modifier in position order, and the final result is stored. Because modifier_stack.Evaluate has the <reads> effect, changes to any modifier's dependencies (or adding/removing modifiers) trigger automatic recalculation.
The public API is as follows:
modifier_interface(t : type) := interface:
Evaluate(Value:t)<reads> : t
modifier_stack(t:type) := class:
# Insert a Modifier at Position; return a cancelable used to remove the Modifier.
AddModifier<final>(Modifier:modifier_interface(t), Position:rational)<transacts>: cancelable
# Returns the input Value evaluated against each modifier in the stack in position order.
Evaluate<final>(Value:t)<reads> : t
The AddModifier method returns a cancelable which can be used to remove the inserted modifier. Removing a modifier triggers recalculation of any live variable associated with this stack.
For example, consider the following, which creates a live variable Health filtered through a modifier stack. It demonstrates two modifiers working together: a magic_potion that multiplies health, and a clamp that bounds values within a range. The variable automatically recomputes when the multiplier changes or when modifiers are added to the stack.
magic_potion := class(modifier_interface(float)):
var Value:float
Evaluate<override>(Arg:float)<reads>:float = Arg * Value
clamp := class(modifier_interface(float)):
var Low:float
var High:float
Evaluate<override>(Arg:float)<reads>:float =
if (Arg<Low) then Low else { if (Arg>High) then High else Arg }
Potion := magic_potion{ Value:= 2.0 }
Bounds := clamp{Low:=1.0, High:= 12.0 }
HealthStack := modifier_stack(float){}
RevokePotion := HealthStack.AddModifier(Potion, 0.0) # Apply first (position 0.0)
HealthStack.AddModifier(Bounds, 1.0) # Apply second (position 1.0)
var Health : HealthStack.Evaluate = 5.0 # 5.0 * 2.0 = 10.0 (then clamped to [1.0, 12.0])
set Potion.Value = 3.0 # 5.0 * 3.0 = 15.0 (clamped to 12.0)
RevokePotion.Cancel() # 5.0 (no potion, just clamp to [1.0, 12.0])
The value flows through modifiers in position order:
- Initial: 5.0 → Potion (×2.0) → 10.0 → Bounds → 10.0
- After changing
Potion.Value: 5.0 → Potion (×3.0) → 15.0 → Bounds → 12.0 - After removing potion: 5.0 → Bounds → 5.0
There are plans to enforce via the compiler that: each modifier instance can only be added to one stack, and each stack instance can be associated with one variable. This will enable future features where modifier stacks maintain state specific to their associated live variable.
Common Errors¶
Live Without Dependencies¶
Defining a live variable with no dependencies that can change is unnecessary and misleading. Similarly, a live variable that only depends on immutable values will never update:
var live X:int = 10 # X is 10 and will never change
X = 10
Limit:int = 10
var live Y:int = Limit+1 # Y is 11 and will never change
Y = 11
In both cases, the variable does not update automatically, so the program behaves identically without the live keyword. The live annotation falsely suggests reactive behavior where none exists.
Since Limit is immutable, Y has no mutable dependencies and will remain at 11 forever. The live declaration is pointless.
Function-as-Type Confusion¶
A subtle error occurs when trying to make a variable live through a function type:
var Mult:int = 10
Multiply(Value:int):type{_(:int):int} =
Fun(Arg:int):int = Value * Arg
Fun
var X:Multiply(Mult) = 10 # X = 100
set Mult = 20 # X is still 100 (not live!)
This code is mistaken. The programmer likely thought that Multiply(Mult) would make X live because the expression has a <reads> effect (it reads Mult) and returns a function type int->int.
The error is this: for a variable to be live through its type, the returned function itself must have the <reads> effect, not the expression that produces the function.
To see why, consider this equivalent transformation:
MFun := Multiply(Mult)
var X:MFun = 10
Now it is clear that X is not live—MFun is just a function value with type int->int, and that function does not have a <reads> effect.
The correct approach is the one shown in Functions as Types, where the function used as a type directly has the <reads> effect: there Multiply itself has <reads>, so using it as a type makes X live.
If the same function has to be reused with different variables as dependent, one can package it in an object as shown earlier.
Evolution¶
When publishing a new version of a system, it is allowed to remove live from a variable definition. This forward compatibility guarantee means that reactive behavior is an implementation detail that can be optimized away without breaking client code.
Converting a regular variable to a live variable in a new version is generally safe if the computed value matches what the previous version maintained manually. However, if external code depends on being able to set arbitrary values, this could break expectations.
The ability to cancel reactive constructs provides an important upgrade path: code that creates when or upon observers can later be modified to cancel them under different conditions without breaking existing behavior.