Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Execution Context

Understanding exec vs pure context is fundamental to writing correct Wirescript. This distinction reflects how Brickadia’s wire graph engine actually executes: some gates define continuous signal relationships (pure), while others execute imperatively in response to events (exec).

Contents

Two Contexts

Pure Context

Pure context is the default. Code in pure context defines continuous signal-flow relationships – like wiring gates together. Pure expressions re-evaluate whenever their inputs change.

What runs in pure context:

  • Top-level let bindings
  • var initializers
  • out bindings
  • buffer expressions
  • Chip output expressions
// All of these are pure context:
var count: int = 0                              // initializer is pure
let doubled = count.Value * 2                   // let binding is pure
out result = doubled                            // out binding is pure
buffer prev = count.Value                       // buffer expression is pure

Exec Context

Exec context represents imperative, sequential execution triggered by an event. Code in exec context runs once per trigger, in order, along an exec chain.

What runs in exec context:

  • on handler bodies
  • Code after on handlers at the same scope level (exec union)
  • Named chip bodies when the chip has ref parameters
  • Exec function calls (DisplayText, Random, SetLocation, etc.)
// This is exec context:
on RoundStart() {
  count = count + 1          // assignment requires exec
  let r = Random(0, 10)      // Random is an exec call
  if r == 0 {                // if statement requires exec
    emit special             // emit requires exec
  }
  DisplayText(ctrl, "Go!")   // DisplayText is an exec call
}

What Requires Exec Context

The following operations are only valid in exec context:

OperationWhy
var = expr (assignment)Writing to a variable gate requires an exec chain
if { ... } (statement)Conditional execution requires an exec branch gate
emit eventFiring an event requires an exec signal
Exec function callsFunctions like Random, DisplayText, SetLocation have exec pins
*var (explicit deref)Reads the variable’s current-tick value via Var_Get; not allowed in pure context (WS006)

The typechecker emits error WS007 when any of these appear outside exec context:

var n: int = 0
n = 1              // ERROR: WS007 -- var write 'n' outside an exec context

How to Enter Exec Context

1. Handler Bodies

The primary way to enter exec context is through an on handler:

on RoundStart() {
  // Everything in here is exec context
  count = 0
  score = 0
}

2. Exec Union After Handlers

After a handler at the same scope level, subsequent statements automatically enter exec context. This is called “exec union” – the combined exit of all preceding handlers flows into later statements.

var count: int = 0

on RoundStart() {
  count = 0
}

on CharacterDied() -> (c) {
  count = count + 1
}

// This code runs in exec context -- after EITHER handler fires,
// the exec chain continues here:
if count > 10 {
  // ...
}

This models how the wire graph works: the exec output of each handler merges into a union that feeds into subsequent exec nodes.

3. Chips with Ref Parameters

When a named chip has any ref T parameters, its body runs in exec context automatically. This is because reading/writing variable references requires exec:

chip Increment(n: ref int) {
  // Body is automatically exec context because 'n' is ref
  n = n + 1
}

4. Explicit Exec Argument

Exec functions called outside a handler can receive an explicit exec named argument:

// No enclosing handler, but providing exec explicitly
let r = Random(0, 10, exec = myTrigger)

This wires myTrigger as the exec input of the Random gate.

The same convention works for any exec gate — array and map methods included. Because the trigger, not the surrounding chain, drives the gate, the call becomes a leaf and is legal in a pure sink. A per-index, always-nonzero trigger like i + 1 turns an array/map read into a single-gate lookup wired straight from an out binding:

out c: color = lut.get(i, exec = i + 1).Value   // pure — no handler needed
let v = scores.get(k, exec = Change(k).exec)     // re-reads when k changes

The same convention applies to user-defined exec chips and mods: outside an exec context, pass their trigger as exec = .... The call then also returns the completion exec as an .exec field on the result record, so callers can await r.exec or on r.exec { }. See Exec Chips.

Reading Variables: Exec vs Pure

The behavior of a bare variable name depends on context:

In Exec Context

A var x: int has type ref int internally, but in exec context, the bare name x auto-dereferences to type int when used in expressions. When passed to a *T parameter (e.g., inc(x) where inc takes *int), it stays as a reference:

on RoundStart() {
  // x auto-derefs: reads as int, writes as int
  x = x + 1
  let doubled = x * 2   // x is int here
}

Note: Even pure expressions like x * 2 use a Var_Get gate (exec) to read x when inside an exec context. This ensures the read is sequenced correctly in the exec chain — the Var_Get fires at the right point and its value output feeds the pure * gate. In pure context, x reads directly from the PseudoVar’s Value port (no exec gate).

You can also use *x as an explicit deref — it compiles to the same Var_Get gate and is equivalent to the bare name in exec context:

on tick {
  let a = x    // implicit deref via Var_Get
  let b = *x   // explicit deref — identical result
}

*x in pure context is an error (WS006): use .Value for pure reads.

In Pure Context

In pure context, the bare name x refers to the variable reference (ref int), not the value. To read the value, use .Value or .prev:

var count: int = 0

// Pure context:
out current = count.Value    // .Value reads the current int value
out previous = count.prev    // .prev reads the previous tick's value

// This would be an error (WS006) if the context expected int:
// out bad = count + 1       // count is ref int, not int, in pure context

.Value vs .prev vs *var

AccessContextGateMeaning
x (bare)ExecVar_GetCurrent tick’s value (auto-deref)
*xExecVar_GetCurrent tick’s value (explicit deref, same as bare)
*xPureError WS006 — use .Value
x (bare)PureVariable reference (ref T)
x.ValueEither.Value portPrevious tick’s value (delayed read)
x.prevEither.Value portPrevious tick’s value (same as .Value)

.prev is essential for change detection:

// Detect when count changes
chip let changed = count != count.prev

on changed {
  DisplayText(ctrl, "Count changed!", fontSize = 24)
}

Handler Exec Chains

Handlers create exec chains – sequences of exec gates connected by their exec output pins. Each statement in a handler body is a link in the chain:

on RoundStart() {
  count = 0            // Exec node 1
  score = 0            // Exec node 2 (chained after 1)
  let r = Random(0,5)  // Exec node 3 (chained after 2)
  if r == 0 {          // Exec branch node (chained after 3)
    bonus = 100        //   Then-branch exec
  }
  // Exec continues after the if
}

The wire graph executes these in sequence: event fires, then node 1, then node 2, then node 3, then the branch.

Exec Context in Conditional Expressions vs Statements

There are two different if constructs with different context requirements:

if-then-else Expression (Pure)

The if-then-else expression is pure – it selects between two values based on a condition and produces a value. No exec context needed.

// Pure -- works anywhere
let abs = if x < 0 then -x else x
let label = if count == 1 then "item" else "items"

if { } Statement (Exec)

The if statement conditionally executes a block. It requires exec context.

// Exec -- must be inside a handler or after one
on trigger {
  if score > highScore {
    highScore = score
  }
}

Summary of Context Rules

ConstructContextNotes
var x = expr initializerPureInitializer is pure even though var is mutable
let x = exprPure (at top level)Inside handlers: shares handler’s exec context
out x = exprPureAlways pure – outputs are continuous signals
buffer x = exprPureAlways pure
on trigger { ... } bodyExecPrimary way to enter exec
After on at same levelExecExec union from all preceding handlers
chip { ... } body (anon)Inherits parentShares parent’s context
chip Name(ref params) { ... }ExecAuto-exec from ref params
chip Name(value params) { ... }Pure (by default)Unless handlers inside create exec
mod Name(params) { ... }Inherits call siteInlined, takes caller’s context
await exprExecRewires exec continuation to expr
emit nameExecBare emit requires exec
emit name = exprEitherValue emit works in pure or exec
buffer(...) emit nameExecBuffered emit — delays delivery a tick+, legalises loops
let name: execEitherDeclares local exec signal

Await

await suspends the current exec chain and resumes when the awaited expression fires. It rewires ctx.current_exec – no state machine, just exec redirection.

on start {
  doSetup()
  await ready              // pause until 'ready' fires
  doMain()                 // resumes here
}

Capture Values

on start {
  let pos = await entity.GetLocation() on moveComplete
  use(pos)
}

let x = await val on trigger sets exec continuation to trigger and binds val to x.

Race (first trigger wins)

on start {
  await timeout || userInput || cancel
}

|| in the expression creates a Union gate – first exec wins.

Local Exec Signals

let name: exec declares a local synchronization point. emit name fires it from any handler. await name or on name listens for it — the listener runs whenever the signal fires, regardless of source order or which handler emits it.

let done: exec

on compute { emit done }
on start { await done }

An on name handler is the fan-out form — its body runs every time the signal fires, from any emitter (handy for menu-style actions):

let up: exec

on tick { if doubleTapped() { emit up } }
on up { ctrl.DisplayText("menu up") }   // runs on every `emit up`

Loops (buffered back-edge)

An emit after an await of the same signal is a loop back-edge. It must be buffered — buffer emit (1 tick) or buffer(N) / buffer(0.5s) — because every wire-graph cycle must cross a tick barrier (WS005). Each iteration then advances one buffer period. Loop state lives in vars (persist across iterations, reset per call) or rides the signal as a ferried payload (emit loop = { ... } / let { ... } = await loop). See statements — Loops for the full pattern and its per-gate cost.

var index = 0
let loop: exec
buffer emit loop
await loop
if index < arr.length() {
  BroadcastChatMessage(arr[index])
  index += 1
  buffer emit loop      // next iteration, one tick later
}

The entry kick must be buffered too, not just the back edge. emit fires immediately, while the await on the next line is what ARMS the resume, so a plain emit loop is gone before anything is listening: the chain parks at the await, neither branch is ever reached, and no completion signal is emitted. There is no diagnostic, and the statements before the await still run, so it reads as half-working rather than stopped. buffer emit lands on the following tick, after the await has armed.

Sleep / SleepTicks

Sleep and SleepTicks are buffer gates that delay a value. Combined with await and _, they create timed delays:

on start {
  await Sleep(_, delay = 2.0)        // wait 2 seconds
  doAfterDelay()
}

on start {
  await SleepTicks(_, delay = 120)   // wait 120 ticks (~2s at 60Hz)
  doAfterDelay()
}

_ inside an await expression is the armed flag – a bool that becomes true when the await is armed. The buffer gate delays this bool, and the await resumes when the delayed output transitions to true.

Important Notes

  • await only works in exec context – it modifies the exec chain.
  • await works inside if blocks naturally.
  • Each await creates an armed flag (static bool) that guards the continuation. The continuation fires exactly once per arming.
  • _ inside await is typed as bool (the armed flag). It is only valid inside await expressions.
  • let foo = await 1 is valid but dangerous: the pure value pulses once, so the continuation runs immediately and never again.
  • Multiple sequential awaits chain: each one gets its own armed flag and redirects exec.