Expressions
Expressions compute values. In Wirescript, expressions map to wire graph gates – each operator or function call becomes one or more gates with their ports wired together.
Contents
- Operator Precedence
- Arithmetic Operators
- Comparison Operators
- Logical Operators
- Bitwise Operators
- String Concatenation
- String Interpolation
- Conditional Expressions (if-then-else)
- Atom Literals
- Record Literals
- Tuple Literals
- Field Access
- Index Access
- Tuple Pick
- Function Calls
- Ref and Deref
- Parenthesized Expressions
- Gotchas
- Asset References
- Prefab References
Operator Precedence
Operators are listed from lowest (loosest binding) to highest (tightest binding):
| Precedence | Operators | Associativity | Description |
|---|---|---|---|
| 2 | || ^^ | Left | Logical OR, Logical XOR |
| 3 | && | Left | Logical AND |
| 4 | | | Left | Bitwise OR |
| 5 | ^ | Left | Bitwise XOR |
| 6 | & | Left | Bitwise AND |
| 7 | == != is | Left | Equality, enum variant test |
| 8 | < <= > >= | Left | Comparison |
| 9 | << >> | Left | Bitwise shift |
| 10 | + - .. | Left | Addition, subtraction, string concat |
| 11 | * / % | Left | Multiplication, division, modulo |
| 12 | ** | Right | Exponentiation |
| – | - ! ~ * ref | – | Unary prefix operators |
| – | .field [i] (args) | Left | Postfix: field access, index, call |
Parentheses ( ) override precedence as usual.
// ** is right-associative:
let x = 2 ** 3 ** 2 // = 2 ** (3 ** 2) = 2 ** 9 = 512
// Standard math precedence:
let y = 1 + 2 * 3 // = 1 + (2 * 3) = 7
// String concat at same level as +/-:
let s = "x=" .. x + 1 // = "x=" .. (x + 1) -- careful!
Arithmetic Operators
| Operator | Operation | Operand Types | Result Type |
|---|---|---|---|
+ | Addition | int, int | int |
+ | Addition | float, float | float |
+ | Addition | int, float or float, int | float |
+ | Addition | int, bool or bool, int | int |
- | Subtraction | (same as +) | (same as +) |
* | Multiplication | (same as +) | (same as +) |
/ | Division | (same as +) | (same as +) |
% | Modulo | (same as +) | (same as +) |
+ - * / % | Vector math | vector, vector (or vector + scalar) | vector |
+ - * / % | Color math | color, color (or color + scalar) | color |
+ - * / % | Rotation math | quat, quat / rotator, rotator (or a mix) | quat/rotator |
+ - * / % | Object operand | int/float + an object (player, entity, …) | numeric |
** | Exponentiation | int/float (not vectors) | (same as +) |
-x | Negation (unary) | int | int |
-x | Negation (unary) | float | float |
Mixed int/float arithmetic promotes the result to float. bool values are treated as 0/1 when mixed with int.
+ - * / % also operate component-wise on two vector operands, lowering to
the same math gates (whose inputs accept the vector, f64 and i64 wire
variants). Mixing a vector with a scalar broadcasts the scalar across the
components (v * 2.0, 10.0 * v, v / 4); the result is a vector. The
Scale helper still works for explicit vector–scalar scaling.
Colors work the same way — the math gate’s variant set also covers color
(LinearColor), so + - * / % operate RGBA channel-wise on two color
operands, and mixing a color with a number broadcasts that number across the
channels: add one to lighten (c + 0.1), multiply to scale (tint * 0.5 dims
every channel including alpha), either direction (0.1 + c, 2 * c). The result
is a color.
The same gates also accept the rotation family (quat / rotator), so q1 * q2
composes two rotations. Same-type operands keep their type; a quat/rotator
mix yields a quat (freely coercible back to a rotator — see
types).
An object operand (a controller/character/entity —
e.g. a player) no longer coerces directly to an int on a math gate, so it is
routed through (obj || false) first: 1 + player lowers to
add(1, or(player, false)). The || gate coerces the object to a value the math
gate accepts. This is automatic — just write the arithmetic.
let a = 10 + 3 // 13: int
let b = 10.0 + 3 // 13.0: float
let c = 2 ** 10 // 1024: int
let d = -42 // -42: int (negative literal folded at parse time)
let p = Vec(1.0, 2.0, 3.0) + Vec(4.0, 5.0, 6.0) // (5, 7, 9): vector
let q = Vec(1.0, 2.0, 3.0) * 2.0 // (2, 4, 6): vector × scalar
let blend = c1 * 0.5 + c2 * 0.5 // RGBA channel-wise color blend: color
let lighter = tint + 0.1 // add a number to every channel: color
let dimmer = tint * 0.5 // multiply every channel: color
let spin = a1.ToRotation() * a2.ToRotation() // compose two rotations: quat
let n = 1 + player // object → (player || false)
Comparison Operators
All comparison operators return bool.
| Operator | Operation | Operand Types |
|---|---|---|
== | Equal | any wire variant pair |
!= | Not equal | (same as ==) |
< | Less than | (same as ==) |
<= | Less or equal | (same as ==) |
> | Greater than | (same as ==) |
>= | Greater or equal | (same as ==) |
Comparison accepts all wire variant types (int, float, bool, string, entity, controller, character) in any combination.
value is Enum.Variant tests which variant an enum value holds and also
returns bool; see enums.
let isZero = count == 0
let isPositive = score > 0
let sameTeam = teamA == teamB
Logical Operators
| Operator | Operation | Operand Types | Result |
|---|---|---|---|
&& | Logical AND | any wire variant pair | bool |
|| | Logical OR | any wire variant pair | bool |
^^ | Logical XOR | any wire variant pair | bool |
! | Logical NOT (unary) | any wire variant | bool |
Wire variant types are: bool, int, float, exec, string, entity, controller, character. The engine coerces all of these to bool on bool ports (truthy/falsy). This means exec values (true for one frame) work directly in logical expressions:
in reset: bool
in start: exec
on reset || start { ... }
let canMove = isAlive && !isFrozen
let either = a ^^ b
!(a && b) and !(a || b) are automatically fused into single NAND/NOR gates by the compiler.
Bitwise Operators
All bitwise operators produce int results. Operands are int; a float or bool operand coerces to int first (1.5 & 2 truncates 1.5 to 1), so only a non-numeric operand (string, vector, …) is a WS011 error.
| Operator | Operation |
|---|---|
& | Bitwise AND |
| | Bitwise OR |
^ | Bitwise XOR |
~ | Bitwise NOT (unary) |
<< | Left shift |
>> | Right shift |
~(a & b) and ~(a | b) are automatically fused into single NAND/NOR gates.
let mask = 0xFF
let high = (value >> 8) & mask
let combined = a | b
let flipped = ~flags
let shifted = 1 << bitIndex
String Concatenation
The .. operator concatenates strings. It automatically converts numeric types to their string representation.
| Left | Right | Result |
|---|---|---|
string | string | string |
string | int | string |
int | string | string |
string | float | string |
float | string | string |
int | int | string |
let greeting = "Hello, " .. name
let label = "Score: " .. score
let coords = x .. ", " .. y .. ", " .. z
String Interpolation
Strings (both "double" and 'single' quoted) support ${expr} interpolation. The embedded expression is evaluated and converted to a string.
let msg = "Player ${name} scored ${points} points"
let debug = 'pos=(${pos.x}, ${pos.y}, ${pos.z})'
let nested = "result: ${a + b * c}"
Interpolated expressions can be arbitrarily complex:
let status = "Health: ${if hp > 50 then "OK" else "LOW"}"
Use \$ to include a literal $:
let price = "Cost: \$${amount}"
Both arms evaluate. An
ifexpression compiles to aSelectfed by both results, so the arm that is not chosen is computed and discarded. The branch picks which value is used, it does not skip work. Two consequences: an expensive expression in the untaken arm still costs its gates every tick, and an arm that would fail on bad input (an out-of-range slice, a divide by zero) still runs – gates yield a default rather than faulting, which is why this is safe, but it means an arm cannot be used to guard another.
Conditional Expressions (if-then-else)
The if-then-else expression evaluates to one of two values based on a condition. It is a pure expression that compiles to a Select gate.
let abs = if x < 0 then -x else x
let label = if count == 1 then "item" else "items"
let clamped = if v > max then max else if v < min then min else v
Syntax: if <condition> then <true-expr> else <false-expr>
then and else may also start their own continuation lines:
let intel = if playerCount <= 6
then "You have a teammate"
else "You are alone"
Block Expressions
Branches can be block expressions { stmts...; value } with locally-scoped let bindings:
let result = if x > 0 then {
let doubled = x * 2
let offset = doubled + 1
offset
} else {
0
}
let bindings inside a block expression are scoped to that block — they are not accessible outside. The block’s value is its last expression.
Block expressions stay pure (Select gate) as long as they only contain let bindings. They can be used anywhere an expression is expected:
let norm = { let len = sqrt(x*x + y*y); len }
The result type is the widening join of both branches – the same rule
that infers a generic type parameter from its call-site
arguments: numeric branches widen to a common numeric type (int widens to
float), and object branches widen toward entity. The branches are not
required to already match:
let value = if flag then 42 else 3.14 // type: float (int widens to float)
If the branches have no common widening (e.g. int and vector), it’s a
compile error:
in v: vector
let bad = if flag then 1 else v
// ERROR WS003: if-then-else branch type mismatch: then is int, else is
// vector (no common widening)
Conditional expressions can be nested and used anywhere an expression is valid:
let score = baseScore + (if hasBonus then 100 else 0)
chip let emptyCount = (if c0 == 0 then 1 else 0) + (if c1 == 0 then 1 else 0)
Atom Literals
An atom literal :name is a compile-time int constant – the deterministic
xxHash64 (seed 0) hash of name. It’s a readable stand-in for a hand-picked
magic number: a key for an int-keyed map, or an enum-like tag.
var scores: Map<int, int> = { :red => 10, :blue => 20 }
if team == :red { ... }
The name may contain letters, digits, _, and - (the first character must
be a letter or _ – a leading digit or - doesn’t lex as an atom start).
:my-text is a single atom, not :my followed by a subtraction – write
:a - b, with spaces, when you mean :a minus b. Atoms only lex where a value is expected
(after =, an operator, (, ,, [, =>, …); a : immediately after
something that already reads as a value – a type annotation (x: int), a
record field ({ x: 1 }), or a map’s string/atom key separator ("red": 1,
:red: 1) – stays a plain colon instead.
There’s no runtime string-to-hash gate: an atom’s value is always resolved
at compile time, so :name can only appear as a literal, never built from a
runtime string.
Record Literals
Record literals construct values of a named record type. Fields are specified as name: expr pairs inside braces:
type Point = { x: int, y: int }
let p: Point = { x: 1, y: 2 }
Shorthand syntax: when a field name matches a variable in scope, you can omit the value:
var x: int = 0
var y: int = 0
let p: Point = { x, y } // equivalent to { x: x, y: y }
Spread operator: copy all fields from an existing record, then override specific fields:
let a: Point = { x: 1, y: 2 }
let b: Point = { ...a, y: 99 } // b.x == 1, b.y == 99
Later fields override spread fields. Multiple spreads are allowed.
A record value is a compile-time abstraction – it produces no wire graph gates, and each field resolves directly to the underlying binding of its value expression. A record used as storage (a var, array, or map) is the exception: it decomposes into one gate per field. See Records as storage.
Tuple Literals
Tuple literals construct tuple values with positional elements:
let pair = (1, 2)
let triple = (true, 3.14, "hello")
Access elements with .0, .1, .2, etc.
Field Access
Use dot notation to access fields on values:
let x = position.x // vector field
let r = myColor.r // color field
let p = myRotator.pitch // rotator field
Vector / color / rotation components
Every component-typed value can be read one component at a time:
| Type | Components |
|---|---|
vector | .x .y .z |
color | .r .g .b .a |
rotator | .pitch .yaw .roll |
quat | .x .y .z .w |
The names are also accepted upper-case. They work on any expression of that
type — a Vec(...) literal, an input, a stored variable, or a let binding —
and lower to the matching Split gate (SplitVector, SplitColor, SplitRotation,
SplitQuaternion), which outputs the single float component:
in a: vector
in b: vector
let sum = a + b // vector
let height = sum.z // float — the z component
in tint: color
let red = tint.r // float
in facing: rotator
let turn = facing.yaw // float — degrees
let spin = a.ToRotation() // quat
let scalar = spin.w // float
Reading several components of the same value costs one gate, not one per component: the split gates are identical and merge.
A component name belongs to exactly one type, so borrowing one (v.w,
facing.x, tint.z) is a WS010 error rather than a silent misread.
Variable Fields
Variables have special .Value and .prev fields:
var count: int = 0
// .Value -- reads the current value (delayed read, works in pure context)
out currentCount = count.Value
// .prev -- reads the previous tick's value (change detection)
chip let changed = count != count.prev
In exec context, a bare variable name auto-dereferences to its value when used in expressions (arithmetic, comparisons, etc.). When passed to a *T parameter, the variable stays as a reference. In pure context, the bare name refers to the ref T (the variable reference itself). Use .Value to read the value in pure context.
Record Fields
Functions and chips that return records allow field access on the result:
let input = InputReader(char)
let fwd = input.Forward // float
let rgt = input.Right // float
let jmp = input.Jump // bool
Exec Field (.exec)
.exec names the exec output of an exec-returning call. Use it whenever you
need to reference that completion signal — to sequence on it (await), handle it
(on), or wire it into another gate’s exec =.
Three cases resolve through .exec:
-
A call that returns a bare
exec. Some builtins produce an exec whose underlying port has a gate-specific name (e.g. aChangedetector’s exec port isOnChanged)..execdenotes that exec directly:let ch = Change(score) // exec — fires when `score` changes on ch.exec { // handle it ctrl.DisplayText("score changed") } -
A chip or mod called with
exec =. An exec chip/mod call returns its completion exec as an.execfield on the result record, so callers can sequence on the body having finished:let t = InitTables(exec = reset) on start { emit reset await t.exec // resumes after InitTables ran } -
An event that also carries data. A data-carrying event expression (a custom event with data outputs) is a record whose exec output comes first, so
.execnames that trigger explicitly and lets the event compose with other exec signals:in reset: exec on Union(reset, CustomEvent("ping").exec) { // fires on reset OR the event ctrl.DisplayText("announced") }
Union also takes an exec receiver, so a.Union(b) is Union(a, b) and a wide
fan-in reads as a left-associative chain instead of nested Union(Union(...)):
in reset: exec
on reset.Union(CustomEvent("ping").exec).Union(Change(score)) {
ctrl.DisplayText("announced")
}
See Explicit Exec Argument and Exec Chips.
Index Access
Use bracket notation to index into arrays:
let item = myArray[i]
// item.value -- the element value
// item.bOutOfBounds -- bool, true if index was out of range
Tuple Pick
Use .N (dot followed by an integer) to pick an element from a tuple:
let pair = someTupleExpr
let first = pair.0
let second = pair.1
Function Calls
Call functions and chips by name with parenthesized arguments:
// Positional arguments
let dist = Distance(posA, posB)
let s = sin(angle)
// Named arguments (kwargs)
DisplayText(ctrl, "Hello",
positionX = 0.0,
positionY = -100.0,
fontSize = 24
)
Named arguments use name = value syntax. They can be mixed with positional arguments, but positional arguments must come first (matching the parameter order).
Running an exec gate with exec = <trigger>. Any exec-gate call — a builtin
like Random, an array/map method, or a user-defined exec chip/mod — accepts a
reserved exec = <trigger> named argument that drives its exec input. This lets
an exec gate run outside a handler: it fires each time the trigger’s value
changes rather than riding the surrounding exec chain, so the call becomes a leaf
that is legal in a pure binding.
// Pure output binding — the get fires whenever `i` changes (i + 1 is never 0):
out c: color = lut.get(i, exec = i + 1).Value
// Outside any handler — provide the trigger explicitly:
let r = Random(0, 10, exec = someTrigger)
Pair it with .exec to sequence on the result. See
Explicit Exec Argument.
// First positional args, then named
let v = Vec(1.0, 2.0, 3.0)
// All named
DisplayText(target = ctrl, text = msg, fontSize = 30)
Ref and Deref
The ref keyword creates a reference to a value. The * prefix operator dereferences a reference.
// ref creates a reference
let r = ref someVar
// * dereferences
let val = *r
In practice, ref and * are primarily used in chip parameter passing. When a chip parameter has type ref T (or *T), you pass a variable and the chip can read/write it:
mod increment(counter: *int) {
counter = counter + 1
}
var n: int = 0
on tick {
increment(n) // passes ref to n; n is mutated
}
*var — Explicit Deref in Exec Context
Inside exec context, prefixing a variable with * explicitly reads its current-tick value via a Var_Get gate. This is identical to the implicit auto-deref that happens with a bare variable name in exec context — it exists for clarity or to disambiguate:
var x: int = 0
on tick {
let a = x // implicit deref (Var_Get)
let b = *x // explicit deref (same Var_Get, same result)
}
*var is not allowed in pure context — it produces error WS006 (“use .Value for pure reads”). In pure context, use x.Value instead.
Variable Read Modes Summary
| Expression | Context | Gate | Meaning |
|---|---|---|---|
x (bare) | Exec | Var_Get | Current tick’s value |
*x | Exec | Var_Get | Current tick’s value (explicit) |
*x | Pure | — | Error WS006 — use .Value |
x.Value | Pure or Exec | .Value port | Previous tick’s value (delayed) |
x.prev | Pure or Exec | .Value port | Previous tick’s value (same as .Value) |
x (bare) | Pure | — | Variable reference (ref T, not the value) |
Parenthesized Expressions
Parentheses group expressions to override precedence:
let result = (a + b) * c
let check = !(x && y)
Gotchas
Bitwise & is lower precedence than ==/!=
This matches C. x >> 31 & 1 != 0 parses as x >> 31 & (1 != 0). Always parenthesize:
let bit = ((x >> 31) & 1) != 0 // correct
let bad = x >> 31 & 1 != 0 // wrong — compares 1 != 0 first
Chips encapsulate state
Chip internal var/array are not accessible from outside. Only declared -> outputs are visible. For shared mutable state, use top-level declarations and mod macros.
The reverse works, though: a chip body can reference top-level vars,
arrays, and buffers freely — wire refs cross chip boundaries.
Asset References
$AssetType/AssetName references an external asset the world embeds by name —
weapons, pickups, projectiles, and audio/font descriptors:
let weapon = $BRItemBase/Weapon_Pistol
let beep = $BrickOneShotAudioDescriptor/BOSA_Buttons_Button_1_Press
The editor completes asset references: typing $ offers the asset types, and
$Type/ offers that type’s asset names (from the embedded asset catalog). An
asset reference is meant to be passed to a gate that takes an asset (e.g. an
inventory or audio gate).
Prefab References
A $ reference whose path begins with . or / is a prefab file
reference — it points at a .brz prefab archive rather than a named catalog
asset:
$./file.brz— relative to the current source file’s directory.$/abs/path/file.brz— a filesystem-absolute path.
A reference ending in .ws is a source prefab: the file is compiled at
reference time and the result embedded, so a prefab can be kept as readable
source next to the program that spawns it. Its own $./… references resolve
relative to that .ws file. Source prefabs may nest 8 deep.
Pass either form to SpawnPrefab:
on spawn {
SpawnPrefab(prefab = $./turret.brz, offset = Vec(0.0, 0.0, 50.0))
SpawnPrefab(prefab = $./minion.ws, offset = Vec(0.0, 0.0, 50.0))
}
A prefab can also be written inline, as a $ followed by a triple-backtick
block of Wirescript compiled as a nested prefab. The spawned chip’s source sits
at the spawn site, and it closes over nothing — it communicates by targeted
custom events like any other prefab. Inline blocks may nest 8 deep.
var subject: character
on CharacterSpawned() -> (who) {
let e = SpawnPrefab(prefab = $```
var owner: character
on CustomEvent("init") -> (p: character) {
owner = p
on ServerUptime() {
if owner.GetUserId() == "" { ReadBrickGrid().DestroySpawnedPrefab() }
}
}
```, lifetime = 0.0, limit = 64)
subject = who // a var: a constant argument emits no wire
e.SendCustomEvent("init", subject)
}
Why this form exists. A loop advances one iteration per tick, so a central chip sweeping N entities every tick costs N ticks per pass. Spawning one chip per entity moves that work into parallel gate instances, and the per-tick cost stops depending on N. Note that the nested
on ServerUptime()above is not registered by the init handler — the wire graph is static, so its trigger is live from the moment the prefab spawns, beforeinitlands, which is why real code guards it on an init-set value. See per-entity fan-out and Loops.
At compile the referenced .brz is read and embedded into the output
bundle (content-addressed at Prefabs/Uploads/<hash>.brz), and the gate’s
Prefab property is set to that embedded path — so the compiled program is
self-contained. Typing $./ completes available .brz files (the editor scans
the project directory; the web playground has a Prefabs panel where you upload
or drag in .brz files).