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 logic gates execute: 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              // WS007 error: 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 gates work: 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, including array and map methods. The trigger drives the gate instead of the surrounding chain, so 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, mods and fns: 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 { }, or write on f(args, exec = trigger) -> (results) { } directly. A fn call’s completion is its Function Call’s Completed, so it is there even when the fn returns nothing. See Exec Chips and fn.

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, a bare name x reads the var’s value directly from the PseudoVar’s Value port. This is the same value .Value and .prev read (see the note above). &x is x’s ref as a value. An unannotated out fed a bare variable name is flagged (WS017): annotate it T for the value, or write out h: *T = &x for the ref. A *T output carries a ref, so a bare x there is a type mismatch (WS003). See Reference values.

var count: int = 0

// Pure context: a bare name reads the value
out current: int = count     // same as count.Value
out previous = count.prev    // .prev reads the previous tick's value
out sum = count + 1          // arithmetic reads the value too

// The ref, explicitly:
out h: *int = &count

// An unannotated bare name is ambiguous between value and ref (WS017):
// out either = count

.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)
*xPurenoneError WS006: use .Value
x (bare)Pure.Value portThe same read as x.Value; an unannotated out of it is WS017
&xEitherVarRef portx’s ref, as a value
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)
}

Reading an Output Port

An output port reads as the value driving it, so one port can feed another:

in a: int
out y: int = a + 1
out z: int = y * 2

A port resolves only when no variable, binding, input, buffer or parameter owns the name, so var count alongside out count = count reads the variable.

A port holds no state, so reading one inside its own value is a loop with no tick barrier and reports WS005. Route it through a buffer or a variable.

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 gates execute these in sequence: event fires, then node 1, then node 2, then node 3, then the branch.

Forking the Exec Chain

A call in statement position joins the chain: the statement after it runs when the call has finished. Reading .exec off the call instead binds the call’s own exec output as an exec value and leaves the chain where it was, so the call runs as a branch and the following statement runs right away.

emit in value position names the exec chain point the handler has reached, as a value of type exec. A branch forks from that point.

var m: int = 0
var n: int = 0
in start: exec

chip Later() -> (count: int) {
  m = m + 1
  out count = m.Value
}

on start {
  let d = Later().exec   // forks: Later runs, and the chain carries on
  n = 1                  // runs alongside Later, not after it
  await d                // rejoins once Later has finished
  n = 2
}

One exec output driving two exec inputs runs both, so the fork needs no gate of its own: the same chain point feeds the branch and the next statement.

emit is also what you hand to an exec = argument to trigger a call from the current chain point. That call never joins the chain, so it forks the same way, and its completion comes back on the result’s .exec field:

var m: int = 0
var n: int = 0
in start: exec

chip Later() -> (count: int) {
  m = m + 1
  out count = m.Value
}

on start {
  n = 1
  let d = Later(exec = emit)   // Later fires from this chain point
  n = 2                        // and so does this, in the same pass
  await d.exec                 // rejoins once Later has run
}

Both spellings compile to the same fan-out. Prefer exec = emit when the call site is choosing the trigger, and <call>.exec when an ordinary call that would otherwise splice into the chain should run beside it instead.

let _ = <call>.exec fires and forgets: _ discards the binding only, and the call still runs.

.exec needs a call that runs on an exec chain: a chip, a fn, or a mod whose body runs exec statements. A mod whose body is all values has no completion to name, and .exec on it is WS043.

emit as a value is meaningful only in exec context. Pure context has no chain point to name, so it reports WS007 rather than resolving to nothing.

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, because 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 that delays delivery a tick+ and legalizes 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. There is no state machine, only 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
}

|| waits for whichever operand fires first. The compiler joins the operands with an OR gate.

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, with buffer emit (1 tick) or buffer(N) / buffer(0.5s), because every cycle of gates 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 is carried on 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 emit that starts the loop must be buffered as well as the back edge. emit fires immediately, and the await on the next line is what arms the resume. A plain emit loop therefore fires 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 the program looks half-working instead of 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, because it modifies the exec chain.
  • await works inside if blocks.
  • 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.