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

Wirescript Changelog

1.10.1

Fixes

  • A match expression whose arms are records or enum values compiles, choosing per leaf field. The record spelling dropped the statement silently; the enum spelling was a WS071 per arm.
  • let m = if c then a else b / let m = match s { .. } bind a record per field, calls in an arm included.
  • A multi-output result chosen by an if/match picks each port separately. One Select over port 0 meant c.Found read the value and bFound was wired nowhere.
  • A mod/chip with one record-typed output hands back the record itself, as typecheck already reported. Keyed by the output name, r = mk(1) wrote no field.
  • (match s { .. }).field reads that field, distributing over the arms like if does.
  • A mod record argument accepts a container element or a record-valued if/match, as its chip twin already did.
  • An expression with no value read as one is a WS072 error. A no-output mod/chip typed as any, so o = noret(1) wired the caller’s exec continuation in as data.
  • A namespace reached two levels deep resolves. The seal over a namespaced body also hid the alias that module imported for itself.
  • A namespaced let initialized through such an alias carries its type, so reading it is no longer a WS002.
  • An import * as alias is file-local, so an importer and the module it imports may both use the same one. Both tables were keyed by name alone, so the inner namespace was dropped and every reference through it was an unknown identifier.
  • A namespaced record type (Ns.Point) resolves to its fields, so a var or array declared with one decomposes and keeps its element type. It typed as any and loaded empty.
  • Two import * as in one file under the same alias is a WS012; unreported, the second shadowed the first and every reference read whichever module came last.

Editor

  • The LSP watches the workspace’s .ws files, so a module changed or created on disk while closed refreshes the files importing it. Only edits to OPEN documents did, leaving stale diagnostics about a version that was gone.
  • A record-literal key hovers wherever the literal is written, including a var, an array initializer, and a literal nested in another; only let x: T = { resolved before.

1.10.0

  • value is Enum.Variant tests which variant an enum value holds, the shorthand for comparing discriminants. It binds like ==, and a test whose two sides are both constant folds away.

Fixes

  • on Clock takes only interval and enabled. pulseOn, onTime and offTime are inert fields of the gate’s data struct, so passing them baked config the gate never reads; naming one is now a WS041 error.

1.9.0

  • Const-heavy programs compile about a third faster. Lowering deep-copied the whole constant table on every expression it lowered.
  • A program with many enum variants and match arms compiles dramatically faster. Exhaustiveness checking and match lowering were each cubic in the arm count.
  • Large programs compile 10% to 15% faster, from fewer allocations in the fold and CSE passes and faster hashing in emit and layout.
  • A source prefab referenced from several sites is compiled once and embedded once, so a program that spawns the same $./file.ws repeatedly produces a much smaller bundle.
  • Deeply nested chips, deeply nested record types, and files with many imports no longer slow down faster than they grow.

brdb

  • Updated to brdb 0.11.0, which writes a save roughly three times faster.
  • Each schema struct compiles into a write plan once and replays per instance, instead of re-resolving every field’s type for every component.
  • Brick and wire chunks use specialized encoders when the loaded schema matches the expected shape, and fall back to the generic writer when it does not.
  • Archive blobs are compressed in parallel.

Fixes

  • A union-trigger handler (on a | b { emit r = v }) compiles. Its body lowers once per part, so one emit became two drivers on the output and failed at emit with no earlier diagnostic.

1.8.1

  • Refreshed the baked gate inventory from the current game build: the string gates carry their clearer display names (Contains is now String Contains, Format Text is now Format String, Length is now Get String Length). No gates, ports, or behavior changed.
  • Added an external-asset catalog (data/asset_inventory.simple.json) covering 832 assets across 8 descriptor types, generated by scripts/gen_assets.mjs from the in-game dump. Weapon one-shot audio is renamed BOSA_* to OSA_*, the smoke grenade is gone, and aircraft wheel-engine audio plus an electrical-arc particle system are new.

Fixes

  • Reading a reference-typed record field yields its value, like reading a var. var f: float = state.a on a *T parameter reported expected float, got *float, and an if-then-else over two such fields reported a branch mismatch.
  • rec.field.Value reads the field’s backing variable. Only x.Value on a plain identifier resolved, so the field spelling lowered to a placeholder and left its consumer unwired.
  • A record literal passed to a *T parameter is a WS008 error when a reference field has no variable behind it. An in port or expression has no storage to write back to, and the call previously compiled with the caller wiring a value where the body read a reference.
  • A chip with a *T parameter binds each call site to its own argument. Every call after the first reused the first instance’s captured variables, so writes through the reference landed on the first argument and the later arguments’ variables were left unwired.
  • &x and ref x bind a chip’s reference, array, record, or map parameter to the caller’s storage. Only the mod form stripped the sigil, so the chip form left the parameter’s pin unfed and silently dropped every write through it.

1.8.0

  • *T on a record, tuple, or enum parameter distributes over its fields, so a chip or mod can write through to the caller’s storage (chip M(s: *T) { s.a = x }). None of these has a single wire to reference, so the ref now names each backing gate: a record’s fields, a tuple’s elements, or an enum’s discriminant and payload slots.
  • Tuple variables get one backing gate per element, so g.0 = v writes. A tuple var collapsed to a single gate and element writes emitted nothing.

Fixes

  • A field read directly on a multi-output chip call (Chip(x).field) wires the named output. Only the two-step let r = Chip(x) / r.field spelling resolved it; the inline form compiled to a placeholder wired to nothing.
  • A record used where one value is expected is a WS071 error instead of a silent placeholder. A record is several wires, so "x=" .. rec had no value to concatenate and dropped the operand.

1.7.4

  • unsafe <value>.<Variant>.<field> reads or writes one enum payload slot directly, without testing the tag. Reading a variant the value is not returns stale contents, and writing leaves the tag alone. Contextual keyword, so the name stays usable.

Fixes

  • A declaration inside a chip { } nested in a handler initializes on that handler’s chain when its initializer calls something. It was held off the chain like a top-level chip’s, which dropped the call and then reported a missing exec context.

1.7.3

Fixes

  • Assigning a match / if let / let else capture writes the matched enum’s payload slot in place, when the scrutinee is storage. A container element is read by value, so its captures stay read-only.
  • Assigning a field with no storage behind it (an enum payload field, a misspelled record field, a field of a scalar) is a WS007 error instead of compiling to nothing.
  • An enum payload field that needs container storage is a WS069 error, at the declaration or at a generic instantiation that picks one (Option<int[]>). The slot could never be filled, so the value read back empty.

1.7.2

Fixes

  • A record or enum value spans several wires, and every position that collapsed one to a single wire is fixed. Most of these compiled clean and emitted nothing at all.
    • Producing one: a record-valued if/then/else selects per leaf, in assignment, out and chip-argument position alike; a record-returning call used as a nested record-literal field writes every leaf, where the array and map forms had left the columns at different lengths; an enum payload that is itself a record or another enum is constructed field by field; a declaration initializer bakes a record payload instead of zeroing it.
    • Storing one: an enum-element array or map decomposes into parallel columns, one per tag and payload slot, so pushes and element reads carry the value; an enum-typed record field gets its own tag and payload storage rather than collapsing to one gate.
    • Crossing a boundary: a nested record in/out port creates a pin per leaf instead of stopping one level down; a record-typed chip signature output gets one pin per field, so the body emits and the caller’s pins are wired; &p on a record variable passed to a *P parameter binds the caller’s record instead of dropping the whole body.
    • Copying one: a whole-record copy carries array and map fields instead of skipping them.
    • Matching on one: a match or if let accepts a container element (match m[k], match arr[i]) or a .Value projection off a map get as its scrutinee. Only a named binding of the same read resolved before, so the inline form hit a placeholder.
    • Referencing one: &p.a on a record field is accepted, since each field is its own storage gate. It lowered correctly and was rejected by WS008 anyway.
    • A record or enum as a custom-event data param is now WS068, since a data slot is one wire and cannot carry one.
  • A scalar initializer is baked in the declared type, so var x: float = 0 builds a float Variable gate instead of an integer one. Applies to every bool/int/float pair, to record and enum-payload fields, and to a var-backed output’s default (out y: float = 0).
  • A component read straight off a call (d.ToRotation().ToEuler().Yaw, v.SplitVec().y) reads that call’s own port; it used to emit a second Split gate fed by the first one’s primary output, yielding the wrong component.
  • A local exec signal consumed inside a chip compiles. The union cleanup redirected and pruned using one chip’s wires at a time, deleting a node the chip’s own wire still named, which failed the compile with a dropped-wire error.
  • .Value / .prev opens a variable that spans several storage gates (a record, an enum); it used to emit a placeholder.
  • A var initializer naming a constant (var x: float = K) bakes that constant.

1.7.1

  • enum is a contextual keyword: it opens a declaration only when a type name follows, so it stays usable as a variable, parameter, or field name.
  • A rotator’s .pitch/.yaw/.roll and a quat’s .x/.y/.z/.w now read their components, lowering to a Split gate the way a vector’s .x already did; several components of one value share a single gate.
  • Each language-reference page opens with a Contents list of its sections, regenerated by just doc-toc and gated by just doc-check.

Fixes

  • A constant Rotation(...) passed to a quaternion port (v.Rotate(Rotation(0, -90, 0))) is converted and baked into the gate, instead of panicking the compiler.

1.7.0

  • enum types (tagged unions): enum Shape { Empty, Circle(float), Box { w: float, h: float } } supports C-like members, positional and named payloads, explicit = N discriminants (later members auto-number from there; a collision is WS064), and generics (enum Box<T> { Value(T), Empty }). Enums are nominal, so two identically-shaped ones are different types. .Discriminant reads a value’s tag as an int, and on a variant path (Shape.Circle.Discriminant) it folds to a compile-time constant.
  • match branches on a variant and binds its payload, both as an expression (comma-separated value arms, lowering to a Select tree) and as a statement (block arms, lowering to Branch/Union). Coverage is checked: an uncovered variant is WS054 and names the missing patterns, an arm that can never run is WS061, and patterns nest into payloads including other enums. A wrong bracket form for a variant’s shape is WS065, and .Discriminant or match on a non-enum is WS066.
  • if let and let else are single-variant refutable binds: if let Some(x) = o { ... } else { ... } runs the block only for that variant, and let Some(x) = o else { return } binds into the surrounding scope with a required diverging else (WS062).
  • Built-in Option<T> and Result<T, E> prelude enums, with their variants usable bare (Some/None/Ok/Err); a payload-less bare variant needs a type annotation to pin its parameters (WS063).
  • Built-in game enums for the game’s own config enums (easing function and direction, brick direction, color space, text justification and typeface): they need no declaration, .Discriminant gives their real integer value, and a variant such as EasingFunction.Bounce passes directly as the matching gate config argument alongside the older bare-name form.
  • Enum and int conversion: value.ToInt() is an alias for .Discriminant, and Enum.FromInt(n) builds a value from a (possibly runtime) int tag with payloads defaulted to zero. The EnumToInt / IntToEnum builtins (renamed from EnumToInteger / IntegerToEnum) are the gate-backed twins: they now require an enum-typed value instead of accepting any, fold a compile-time-known value to its tag, and use the game gate at runtime. IntToEnum’s result enum type is pinned by the target (WS063 when it can’t be).
  • Editor support for enums: completion of variants, enum type names, and .Discriminant; a fill-missing-arms code action; and hover and go-to-definition on enum types, variants, and named payload fields.

Fixes

  • Calling a mod that reads or writes a container in its body (m.get(k), arr[i], m.set(...)) from a pure position now reports WS007, instead of silently wiring the container reference into the caller. This now also covers a container reached through the mod’s own parameter (mod f(m: Map<int, int>) { m.get(0) }) or a body-local, not just a top-level one.
  • A multi-return mod whose single output is a record now yields the branch actually taken at runtime; previously one return’s record literal leaked to the caller and folded the result to a fixed, often wrong, value.
  • A reference inside an emit, await, or buffer statement now counts as a use, so a genuinely-used import is no longer falsely reported unused (WS014) or dropped by Organize Imports.

1.6.2

  • Raise microchips so nested ones are not underground

Fixes

  • Reading a namespaced out or in member (L.count, L.level) now resolves to the port’s value instead of reporting “not found in namespace” and lowering to a placeholder.
  • A namespaced defaulted output with no emit (out count = counter) is a single direct drive again, instead of also getting a backing variable that fanned two wires into its port (which failed to load).
  • Indexing a record-array or record-map column (pts.x[i], m.x[k]) now reads the field’s value instead of typing any and dropping to a placeholder; it works in expressions (a comparison, arithmetic), not just a bare let.

1.6.1

  • .exec names an event’s exec output, so a data-carrying event composes into Union(...); Union also takes an exec receiver, so a.Union(b) chains left-associatively.

Fixes

  • An inline record-returning mod call read by field (f(x).sum) now projects the field instead of an _Unsupported placeholder.
  • A record-typed module output (out p: Point = { .. }) now dissolves into per-field boundary pins instead of dangling.
  • A whole-array assignment from a record-array literal (arr = [rec, rec]) now rebuilds the array instead of being silently dropped.
  • A constant read through an import * as ns namespace (ns.NAME) now folds, so it works in constant-only positions (array initializer, channel name, label).
  • Spreading a tuple that came from a multi-output mod call (f(n, ...g()), or a bound let t = g() then f(n, ...t)) now expands its elements instead of dropping them.
  • A field or index access on an aggregate-typed if expression ((if c then a else b).x, (if c then a else b)[i]) now distributes over the branches instead of dropping to a placeholder.
  • A nested record passed as a chip parameter now wires every field, instead of collapsing the inner record to one unwired input.
  • A tuple parameter on a chip (chip f((a, b): (int, int)) { }) now binds its elements, instead of leaving the body reading unresolved values.
  • LSP: hovering a record field written through an array index (arr[i].field = v) now shows the field’s type.
  • An on handler in an import * as module no longer resolves a free name against the importing file’s state; an undefined name there is now reported instead of silently wiring to a same-named importer variable.
  • State imported from one file both plainly and through import * as (or via import { g } plus import { g as h }) now shares one storage gate instead of duplicating into two that drift apart.
  • A namespace a module imports privately (import * as B) no longer leaks to files that import that module; naming it from another file is an undefined identifier.
  • A namespaced output written by more than one emit, or by a conditional or defaulted one, now routes through a backing variable instead of fanning two wires into its port (which failed to load).

1.6.0

  • A ...tuple spread now expands into a call’s positional arguments. SendGlobalCustomEvent(name, ...t) splats a tuple across the event’s data slots, and f(...t) / f(a, ...rest) binds each element to consecutive parameters (a tuple literal ...(a, b), a bound tuple, or a field chain reaching one). Over-filling a fixed-arity callee reports the usual arity error, and spreading a non-tuple is a WS003.
  • A mod can take a trailing ...rest variadic parameter that captures every argument past its fixed params. Each call site binds rest to a compile-time tuple of the extra args, and a ...rest in the body splats them onward, so mod broadcast(name: const string, ...rest) { SendGlobalCustomEvent(name, ...rest) } forwards any number of values. A call must still supply the fixed params (fewer is WS022). Only mods may be variadic; a ...rest on a chip is WS052.
  • await CustomEvent("c") can now capture the event’s data inline: let n: int = await CustomEvent("c") suspends until the event fires and binds the first data output (typed by the annotation). A tuple let (p, t) = ... captures data outputs positionally. Annotate the type or the wire defaults to a float (WS055 warns when it can’t be determined). Bare await CustomEvent("c") (no binding) just waits.
  • A tuple return’s parentheses are optional: return a, b, c returns the same tuple as return (a, b, c), and .0/.1 access the elements.

Fixes

  • Compile progress now accounts for embedded prefabs: the reported step total grows by one per $./file reference and inline $```...``` block, so the editor indicator advances through them as it compiles.
  • The editor’s compile status indicator shows only while a compile is actually running.
  • SpawnPrefab(..., destroyAll = sig) fed a local let sig: exec signal wires the signal into the spawner’s destroy pin, so those prefabs are destroyed (a local exec let folds to a placeholder 0, so it needs this dedicated wiring).
  • wirescript-check runs through lowering, so it surfaces the same _Unsupported-placeholder warnings compile emits.
  • New WS053: a plain emit X in the same chain as a following await X warns that the kick fires before the await arms (parking the chain forever); use buffer emit X.
  • A record passed to a chip wires its fields: a record literal argument (Foo({ x: a, y: 2 })), a field whose value is a var (read by value, not through the reference port), and a destructured chip parameter (chip f({ x, y }: P)) all bind correctly.
  • Assigning a field of a let, input-port, or literal record (p.x = 5 where p is not a var) reports WS007. A var-backed record field stays assignable.
  • == and != between two whole record values report WS004. Comparing a multi-output result to a scalar (arr.pop() == 5) still works.
  • A ...rest parameter in a destructured mod parameter types as the record of the remaining fields, and a tuple-typed field inside a record annotation (type T = { pair: (int, int) }) type-checks correctly.
  • A record literal used as a spread source ({ ...{ ...p, y: 2 }, x: 9 }) or read directly (({ x: 1, y: 2 }).x) resolves its fields.
  • A nested record inside a record array or map decomposes to leaf columns like a flat field: arr[i].inner.a / m[k].inner.a read and write a single leaf, arr[i].inner / m[k].inner read and write the whole sub-record, and let row = arr[i] binds the element as a record value.
  • Namespaced member access is stricter. Reading a missing or non-value member (L.nope, or a namespaced out port) and a nested path (A.B.bar()) report WS002; a named-argument call through a namespace reports WS022 when the positional count is wrong. A namespaced container read (S.scores.get(k)), &S.g, and an import used only as a handler trigger type-check cleanly.
  • An if expression written directly inside a string interpolation ("${if n < 10 then "0" else ""}${n}") emits its comparison and select.
  • Passing a non-lvalue (a literal, expression, let, arr[i], or call result) to a *T/ref parameter reports WS008.
  • A statement written after a nested on handler inside a handler body stays on the outer exec chain rather than binding to the nested handler’s trigger.
  • New WS057: emit X is an error when X is not an out port or a let ...: exec signal (an input port, a var, or an out/signal declared outside an enclosing named chip).
  • New WS056: let v = await sig on a signal that carries no payload is an error. Emit a value (emit sig = ...) or capture one with await <expr> on sig.
  • New WS058 (warning): an exec statement that never runs is flagged
  • A tuple return (a, b) to a mod with named multi-outputs wires each element to the matching output in declaration order, so let (a, b) = f() reads the returned values.
  • Three constructs that type-checked but built no working circuit now report an error instead of compiling to a silent miscompile: a component field that isn’t on its type (v.r on a vector, color.x) reports WS010 rather than emitting a wrong-typed SplitColor/SplitVector; a negated-union or double-negation handler trigger (on !(a | b), on !!x) reports WS001 rather than dropping the whole handler; and assigning to a non-lvalue (f() = 5) reports WS007 rather than dropping the assignment.
  • Emit rejects a wire fan-in (two sources driving one input port, such as a duplicate out o) with a compile error, rather than writing a format-valid save the game refuses to load.
  • Unary negate (-x) resolves correctly during generic-mod monomorphization, so a generic -x emits a negate gate with the right numeric type.
  • A constant Substring with a very large length clamps to the string end instead of overflowing into a panic.
  • New WS059: Change/Changed (and the Edge detectors) on a reference or container (a Map, an array, a *T ref, a zone, or a teleport) is an error, since a change/edge detector watches a single wire value and those carry none. Watch a scalar the container produces instead.
  • A map access gate (m.get(k) and friends) is colored by its value type, like the var and array access gates, rather than a neutral grey.
  • A get/set on a reference-passed container input (in m: Map<K,V> / in xs: T[]) is tagged with the input port’s name, matching how an access on a stored var is tagged.

1.5.0

  • Added a null literal. It adopts its target type and produces that type’s zero/unset value (entity/character/controller -> an unset object, a number -> 0, bool -> false, string -> "", vector/rotation/color -> zero) in any typed position: a var/out initializer, an assignment, a call argument, or a record field. null for a container, record, or reference-only type has no value and reports WS051; a bare let x = null with no target types as any.
  • Records can now be stored. A record variable, array, or map (var p: Point, var pts: Point[], Map<K, Point>) decomposes into one backing gate per field, so field reads/writes, whole-record assignment (p = { x, y }, p = q), and the container methods all lower to real gates: arrays get push/pop/insert/remove/fill/resize/swap/reverse/clear/length plus element access (pts[i], pts[i].x, pts[i] = rec, p = pts[i]); maps get set/get/has/remove/clear/length/keys plus m[k] access. A record variable used to collapse to a single gate whose fields read a bogus vector swizzle, and a record pushed into an array lowered to a placeholder that did nothing.
  • A stored record with a field that has no storable value (a ref, zone, teleport, prefab reference, or exec) now reports WS049 instead of silently backing it with an unusable gate.
  • A record-container method with no per-field meaning (sort/shuffle, the aggregates sum/min/max/average, find, and the dual-container append/copyFrom/slice/values) now reports WS050 instead of silently lowering to a no-op placeholder.
  • A constant record-array/map initializer (var pts: Point[] = [{ x: 1, y: 2 }]) now bakes each field’s column into its backing container; it used to compile clean but silently start empty. A record array’s per-field arrays are also reachable directly as pts.field (struct-of-arrays access) — index, read, and aggregate a single column (pts.x[i], pts.x.sum(), pts.x.min()). pts.field.sort(descending?) sorts the whole record by that field, keeping rows intact.

1.4.5

  • An operator or sibling call inside a namespaced module’s on handler (import * as L from "lib" where lib has on ReadBrickGrid() { arr.push(n << 10) }) is now type-checked, so it lowers to a real gate instead of _Unsupported. Namespaced handlers began lowering in 1.4.4 but typecheck never descended into their bodies, so operators got no resolution.
  • --dump-ir node locations now include the source file (@ lib.ws:3:1) and render each node’s snippet from that file. Imported nodes used to show the entry file’s text at their offset and an ambiguous @ line:col; the IR ranges themselves were already correct, only the dump’s preview read the wrong source.

1.4.4

  • A module imported as a namespace (import * as L from "lib") now runs its top-level on handlers.
  • Two import * as namespaces (or a local declaration plus an imported one) that share a state name now get distinct storage gates.
  • A var/array/map/buffer declared inside a handler, if, or block now gets its own storage gate instead of silently reusing an outer same-named one.
  • &x passed to a *T/ref parameter now binds the caller’s var, so writes through it land.
  • An or-triggered handler (on a | b { ... }) now runs its body when either trigger fires instead of silently dropping the whole handler.
  • Two different modules declaring the same top-level name, merged via import "m" or import { x }, now report WS013 instead of silently collapsing both onto one storage gate.
  • A container mutation (arr.push(x), m.set(k, v)) outside an exec context now reports WS007 instead of silently lowering to a placeholder that does nothing.
  • A parse error in an imported file now surfaces instead of being silently swallowed; the identical source only errored when compiled as the entry file.
  • An anonymous chip (chip { … } / chip on t { … }) in an imported module now runs; it was filtered out before the merge and silently dropped along with its writes.
  • A single emit out = <expr> inside an if is now gated by the branch instead of wired unconditionally to the output; a guarded write no longer becomes permanent.
  • A let that shadows an in/out port of the same name now reports WS013 instead of silently hijacking it (let go before on go, with in go: exec, left the exec input dead). Ordinary value shadowing (let a = 1; let a = 2) is unaffected.
  • An output reached by an emit plus a default initializer (out r = 0 then emit r = …), or by emits split across a handler and an anonymous chip { … }, is now var-backed instead of driven by two wires - a load-breaking fan-in on the output rerouter.
  • A let aliasing an input-port array or map (in a: int[] then let x = a) now resolves for x[i] and container methods, like a var alias; it used to lower the index to a placeholder.
  • A namespaced mod’s body now resolves its OWN module’s siblings, vars, and constants. Two imported modules with a same-named private helper no longer make one module’s public functions run the other’s code, and a namespaced mod mutating g now writes its own module’s storage gate instead of the last-imported namespace’s.
  • A container method READ outside an exec context (out r = arr.length()) now reports WS007 instead of silently lowering to a placeholder, matching the pure index-read rule. A const receiver or an explicit exec = <trigger> arg is exempt.
  • A captured handler with a non-Event trigger (let e = on go { … } where go is an exec input, var, or let) now runs its body and captures its exit as e; the body used to be dropped entirely unless the trigger was a built-in event.

1.4.3

  • An imported let no longer clobbers a same-named declaration in the importing file. An imported let start overwrote the file’s own in start: exec, so on start bound the imported value instead of the input and silently dropped the handler body.
  • Void container operations (push/clear/set, the keys/values fills) now type as never, so using their result as a value (let r = a.push(x)) is a type error instead of silently accepted.
  • Top-level on handlers in an imported file now run; they were silently dropped, and an on <expr> handler additionally left a dangling trigger gate with no body.
  • A let aliasing an array or map (let ar = a) now resolves for indexing, writes, and methods (ar[0], ar[0] = x, ar.push(...)) instead of lowering to a placeholder or silently dropping the write.
  • A namespaced member (ns.name) whose bare name the importing file also owns is now reachable as ns.name, so ns.tuple.0 resolves instead of falling through to the local binding.

1.4.2

  • Folding enabled by default now that it’s stable
  • Inline nested-prefab blocks now compile in the browser build. An inline nested-prefab block passed to SpawnPrefab (the $-fenced source form) is compiled to its own prefab and embedded, matching the native CLI. The browser previously rejected any inline nested block with “no nested compiler configured for this compile”; dragged-in $./file.brz prefab references already worked. Blocks nested past a fixed depth fail with a clear error instead of hanging.
  • An import read only in the config of an event handler inside a chip body is no longer reported as unused. The 1.4.1 fix covered handlers at module level but not the statement path a chip body takes, so on CustomEvent(CH) inside a chip warned WS014 while the identical handler outside one did not, and Organize Imports would then delete the import and leave the handler naming nothing.

1.4.1

  • A record assigned to a mod output (mod f() -> (o: Rec) { out o = rec }) now reaches the caller. A record has no single value port, so the output silently carried a placeholder that read a default, and everything downstream of it did too, including a record result passed straight to another mod (Take(Make())). The warning only appeared when the result went unused, so the broken cases were the quiet ones.
  • An imported module’s root-level in and out ports are now declared and reachable as ns.name. They were dropped entirely, so on ns.trigger { ... } matched nothing and silently discarded the whole handler body while both stages reported the file clean. Root-level var, array, map and buffer members already worked.
  • An import read only in an event handler’s config (on Clock(interval = TICK)) is no longer reported as unused. The scan walked the handler body but not its config args, so a constant used only to configure the gate warned WS014, and Organize Imports would then delete it.
  • Two import * as namespaces that export the same member name now stay distinct. A.foo and B.foo had both resolved to whichever module was imported last, so a field access on the other read the wrong value and lowered to a placeholder while type-checking reported the file clean.

1.4.0

  • const compile-time evaluation: const bindings, const parameters (f(name: const string, v: int)) and const mod declarations are evaluated at compile time and can be used anywhere a literal is required, such as gate config and custom-event channel names. Const expressions compose freely (operators, constructor arguments, destructuring, indexing, collection assembly) and emit no gates. An if on a const condition drops its untaken branch, and a const that fails to be compile-time is reported at the binding.
  • @layout("cube") no longer emits per-gate name labels, var tags, or chip-brick labels. Plane headers, the shell label, and a runtime @label(expr) are kept.
  • GetInputs: samples a player’s twelve controls once when the exec chain reaches it, the exec-form counterpart of InputReader. Same field names, so char.GetInputs().PressedQ reads like the splitter, and its operand also accepts a persistent player.
  • An exec builtin called with receiver syntax in pure position now reports WS007. The check only covered the plain spelling, so GetLocation(e) was an error while e.GetLocation() compiled to a placeholder that read a default.
  • A local binding that shadows an import * as ns alias now reports WS002 at the call. A parameter or let named ns made every ns.f(...) in that scope resolve against the local value, which has no such member, so the call typed as any and compiled to a placeholder that did nothing while type-checking reported the file clean. The failure then surfaced wherever the any was finally consumed, on a line that was not the mistake.
  • LSP: Hover gate estimates now refresh as you type, so a mod added or renamed since the last save shows a count instead of none.
  • LSP: Hovering a namespaced call (ns.f()) now shows its gate estimate.

1.3.0

  • An array/map method on a record field (g.ready.sum()) now types its result instead of any, so arithmetic on it works. Args are checked through the chain too.
  • A type alias whose body names another alias (type Rect = { a: Point, ... }) now expands all the way down when imported, so ns.rect.a.x keeps its type instead of reading as any.
  • Namespace members (import * as ns) written without an annotation, or bound by a destructuring let, now carry their record type across the import too.
  • Compiler source reorganised

1.2.0

  • map[key] subscript syntax - m[k] and m[k] = v now work on a Map<K, V>, desugaring to the same get/set the m.get(k) / m.set(k, v) methods use (m[k] reads the value, auto-unwrapping the found flag; m[k] = v writes it). The read types as the value type V and, like array indexing, only works in an exec context. Previously this parsed and type-checked but silently did nothing (the read produced 0, the write was dropped).
  • Fill record fields (editor code action) - inside a record literal whose expected type is a record (let x: Card = { … }), the lightbulb / Ctrl+. offers Fill record fields, inserting every missing field with a type-appropriate default and recursing into nested records — e.g. { foo: "", bar: { baz: 0 } }. Present fields are kept, so a partial literal completes; nested, aliased, and imported record types are resolved server-side.

Fixes

  • Output values (return, emit out = …, statement-level out name = …) are now checked against the declared output type (WS003) instead of baking a mismatch into the wire.
  • Conflicting generic-builtin arguments (Select(c, 5, "hello"), Swap) now error (WS033) instead of widening to any.
  • let t: (A, B) = … and let x: R = { … } now check the value against the annotation (WS003), not just field names.
  • A multi-output result used as a scalar unwraps only through its first field — a later-field match wired the wrong port (now WS003).
  • A tuple literal no longer collapses to a scalar (let x: int = (1, "abc") is WS003).
  • A heterogeneous array literal in a handler (xs = [1, "hello", 2]) reports WS003 on the odd element.
  • An any[] / Map<any, …> parameter now accepts a concrete array/map argument.
  • == / != on two vector/rotator/quat/color values is now accepted (ordering stays scalar-only).
  • Assigning to a scalar let reports WS007 instead of silently emitting no gate.
  • An unknown named argument on a mod/chip call reports WS041 instead of being dropped.
  • A typo’d event config/input name (on Clock(intreval = …)) reports WS041 instead of no-opping.
  • An out-of-range integer literal reports a parse error instead of compiling to 0.
  • A captured event inside a handler (let x = on Clock(1.0) { … }) reports one clear “top level only” error instead of misleading tail-parse errors.
  • A var read after await re-reads fresh, so a value changed during the wait is visible.
  • ns.myValue on an import * as ns now reads the real value and type — it typed as any and compiled to a placeholder reading 0, while only ns.f(...) calls worked.
  • A chip that writes a global no longer leaves a stale read after the call; the next read is fresh. (Inline mods were already correct.)
  • A chip called in two contexts (pure-then-exec, captured-vs-passed arg, or same name in two modules) compiles a separate body per context instead of reusing the first’s mis-wired one.
  • A chip with multiple returns, and an output emitted from multiple sites, each route through one holder variable instead of fanning two wires into one pin (a load failure).
  • A chip … -> (sig: exec) that does emit sig now wires the emit to the output (was silently dropped).
  • A wire whose endpoint can’t be resolved is now a compile error, not a silently dropped wire in a shipped save.
  • The formatter no longer splits a :kebab-case atom literal at its hyphen.
  • A field access on a scalar (x.whatever on an int) reports WS010 instead of silently reading the whole value. Projecting a single-output result by its output name (f.result) still works; a mis-typed one is caught.

Performance

  • Repeated calculations build one gate and share it. Write x + 1 in three outputs, or call the same mod twice, and you get one gate instead of a copy per use. (State-holding and @nofold gates are left alone.)
  • A variable read is reused across an if when the branch doesn’t touch it, instead of being re-read afterward. A variable a branch writes still re-reads fresh, so it’s never stale.
  • The editor keeps up better while you type — ~20% less work per keystroke, and no longer multiplied by your open tab count. It parses the file once instead of twice, leaves hover’s gate-count estimates to open/save, and re-analyzes only the open files that import what you changed.
  • Imports resolve against unsaved edits. A file importing something you’re editing was analyzed against the last saved bytes on disk until you hit save.

1.1.1

  • Scope-aware rename & find-references - rename/textDocument/references now resolve the identifier under the cursor to its exact binding instead of matching the name as text, so they never touch comments, strings, a same-named type, another scope’s binding, or an unrelated file; renaming an exported symbol updates its importers (and a local alias stays local). (This replaced the old textual scan.)
  • Semantic type highlighting - the LSP emits semantic tokens so a name in type position highlights as a type (including user type aliases the grammar’s builtin list can’t know), while every value identifier is highlighted uniformly - so a binding whose name collides with a type keyword (a character capture) or a builtin function (a value named round) reads as a plain identifier, not a type or a call. Genuine builtin calls are unaffected. Atom literals (:name, kebab-case allowed like :kebab-casea, including a :name: map key) highlight as the integer constants they compile to; hovering an atom shows the compile-time xxHash64 value it resolves to, and find-references gathers every use of that atom across the workspace.
  • .ws source prefabs - a prefab reference may now point at a .ws source file, SpawnPrefab(prefab = $./control.ws, …), which compiles that file into a prefab and embeds it on the spot - the file form of an inline $```…``` block. Errors in the referenced file surface on the $./control.ws reference itself, so a broken prefab underlines where it’s used. .brz archives work as before; any other extension is WS019.
  • Fixed: a no-receiver gate called with a receiver - x.SweepSimple(…) / x.Sweep(…) (the sweep gates act on their own brick and take no receiver) now reports a clear WS036 error instead of silently type-checking as any and lowering to a do-nothing placeholder.
  • Fixed: @fold dropping a runtime @label - a runtime @label(<expr>) (on a var or, blank-line-separated, on the root microchip) is wired into its text at emit time, so a folded module’s dead-code sweep no longer prunes the label’s source as “unused” and silently drops the label.

1.1.0

  • Events as expressions - an event called as an expression emits its gate and yields its exec, so an event composes in expressions like Union(RoundStart(), other). It takes the same inputs as the on form (Clock(interval = 2.0)), and a data-carrying event exposes its outputs by field access (CharacterSpawned().character); a bare call auto-unwraps to the exec. The on E { ... } trigger form is unchanged.
  • LSP: named-argument values complete in-scope identifiers - typing the value of a named argument (itemName = <here>) now offers the in-scope identifiers, not the call’s other argument names (those are only offered when completing an argument name). Enum/asset value slots still complete their specific members.
  • Event output capture (on ... -> ...) - an event handler binds its data outputs in a trailing capture instead of inside the event call: on Foo() -> (a, b) (tuple, positional - the cleanest form, for any event) or on Foo() -> { field: local } (record, by field name - named events, for subset/rename). on <call> -> ... also triggers on any exec-producing call - a mod/chip call, or a gate driven by on Foo(exec = x) -> (...) - auto-extracting the call’s exec output; a general call that exposes no exec (or an exec = with nowhere to attach) is a WS043 error. A single untyped output may drop the parens: -> who is shorthand for -> (who).
  • Custom-event data-type inference - an unannotated custom-event receiver slot takes its type from the matching in-unit SendCustomEvent on that channel; when none is inferable the slot defaults to float and warns WS042 (which replaces the old WS029 “annotate this param” lint). A written type goes in the tuple capture: on CustomEvent("ch") -> (x: int).
  • Whole-grid interaction events - on WholeGridInteracted() -> (character, held) fires when the grid is interacted with; on WholeGridTargeted() -> (character, damage, weapon, weaponName) fires when it is hit.
  • Maps work wherever arrays do - a Map<K, V> can now be a mod/chip parameter, an in/out port, or a record field (not just a file-scope var). A container method whose receiver isn’t a container is WS044, not a silent no-op.
  • Generic value builtins - Select, Swap, Sleep, SleepTicks, and Tween carry their argument’s type instead of any, so Select(c, 1, 2) is an int.
  • Tuple arguments match tuple parameters - passing a tuple value or literal to a (A, B)-typed parameter type-checks.
  • Ref-insensitive record fields - a record value matches a record type when fields differ only in ref/array exposure (a *T field and a plain T are interchangeable at a call boundary, as parameters already are).
  • Tighter builtin argument types - DisplayText/PrintToConsole/Fmt/SetTag take string, SetLeaderboard/IncLeaderboard take int, and SpawnPrefab takes a prefab reference.
  • Fixed: exec after an emitting mod - a statement following a mod whose body ends in emit no longer drops to an unsupported placeholder.

Migrating to 1.1.0

The on handler form now binds event outputs with ->, and no longer accepts data params (or their types) inside the event call.

  • Move data outputs to -> - on CharacterDied(character, killer) { ... } becomes on CharacterDied() -> (character, killer) { ... }. Config and inputs stay inside the parens: on ChatCommand("greet", "help") -> (controller, args), on ZoneEntered(zone = z) -> (character). Config-only handlers are unchanged (on Clock(interval = 2.0) { ... }, on RoundStart() { ... }).
  • Custom-event types move to the capture - on CustomEvent("dmg", amount: int) { ... } becomes on CustomEvent("dmg") -> (amount: int) { ... }; an omitted type is inferred from the sender, or warns WS042.
  • Event triggers are calls - an event trigger is written with (): on RoundStart(), not on RoundStart. The no-parens form - a handler head (on RoundStart { }) or a captured-event alias (let x = on RoundStart) - is now an error. The () marks the event as a gate/call, uniform with the on <call> -> (...) model.
  • WS029 is removed - the “annotate your custom-event param” lint is replaced by inference plus WS042.

1.0.0

  • Maps (Map<K, V>) - a keyed variable collection paralleling arrays: var scores: Map<string, int>, keyed by int/string/object reference and holding any wire-storable value, with exec-context methods set/get/has/remove/clear/copyFrom/length/keys/values (get gives { Value, Found }, auto-unwrapping to Value). A non-int/string/object key type is a WS039 error.
  • Map literals - { k => v } keys by any expression, "s": v / :atom: v by a string/atom/int literal, and [expr] => v by a computed key; a fully-constant literal bakes the map pre-populated at rest, {} is an empty map, and m = { ... } in a handler desugars to clear() plus one set per entry in source order.
  • Atom literals (:name) - a compile-time int constant (the deterministic xxHash64 hash of the name), a readable stand-in for a magic number as an int-map key or enum-like tag; it only ever resolves at compile time, never from a runtime string.
  • Generic type syntax - Array<V> and Ref<V> are exact aliases of V[] and *V.
  • Gate config properties - a gate’s non-wire settings-menu fields (checkboxes, dropdowns, values) are now settable as optional, constant-only call args, both by friendly alias and by raw game name (SweepSimple(Direction = X_Negative, ...), p.DisplayText("hi", typeface = Bold)); enum args take bare member names validated against the game’s enum list, and an unknown name or non-constant value is a WS028 error.
  • Custom events - SendCustomEvent(name, data...) pulses every on CustomEvent("name", a: int, b: float, ...) receiver on that channel with up to 8 typed data values; a receiver fires the tick after the send, and an untyped receiver param warns (WS029).
  • Custom-event signature check and navigation - a SendCustomEvent on a constant channel whose data types disagree with the matching receiver’s declared params warns (WS030), and go-to-definition on a send-site channel-name string jumps to the receiver.
  • zone & teleport reference types - rerouter-only component references (like a var ref): passable through ports and parameters but not storable in a var/array/buffer (WS025) or selected with if-then-else (WS031). Zone events’ zone input is now zone-typed, and Teleport/RelativeTeleport dest/source are now teleport-typed - teleporting to a raw position uses SetLocation.
  • Clock reads as an event - on Clock(interval = 2.0, enabled = running) { ... } runs its body on each pulse; interval and enabled are wire inputs (constant or dynamic, so the clock toggles at runtime) and pulseOn/onTime/offTime are constant-only config.
  • New entity/character builtins - GetSpeed, GetVelocityAtPoint, GetEntityTeam/SetEntityTeam, IsFrozen, DestroySpawned/DestroySpawnedPrefab, a character ammo family (GetAmmo/GrantAmmo/SetAmmo, weapon-chamber ammo, GetInventoryEntry, GetCurrentInventorySlot), GetOwnTransform, and ForceRespawn(player) (also player.ForceRespawn()).
  • New date/time and conversion builtins - GetUnixTime, FormatDate, Remap, LogicalShiftRight, EnumToInteger/IntegerToEnum, ItemToPickup, ConvertColor, and ToCharCode/FromCharCode; ParseInt/ParseNumber gained a .Success flag and auto-unwrap to their parsed value.
  • Zone array fills - arr.fillFromZoneEntities(zone, tagFilter?) / fillFromZonePlayers(...) populate an array from a zone, and arr.sortMultiple(other, ...) sorts a value array plus up to seven parallel arrays together.
  • SpawnExplosion - an exec gate spawning an explosion of a given projectile/explosion class, with optional instigator, offset, scale, and damage.
  • PlayClientAudio - player.PlayClientAudio(audio, volume?, pitch?) plays a non-spatial one-shot for a single player (also PlayClientAudio(player, audio, ...)); the descriptor is an inlined $BrickOneShotAudioDescriptor/... asset reference, like the other Play* audio gates.
  • Expanded InputReader - reads look axes and key/button states (Up, Pitch/Yaw/Roll, MouseWheel, PressedC/E/Q/LeftMouse/RightMouse) alongside Forward/Right.
  • DisplayText returns a textId - capture the returned int to update or clear the same on-screen text later, with new color/outline/shadow/spacing/wrap styling params; position/anchor/scale/pivot/shadowOffset are Vector2D layout properties on the reworked gate.
  • GetDamage gives { Damage, DamageLimit } - auto-unwraps to Damage where a float is expected, and .DamageLimit reads the death threshold.
  • Richer event outputs - the fired-weapon event exposes the weapon and its name, CharacterDied the killer’s weapon and name, and ControllerJoined/ControllerLeft the player’s user name; Sweep/SweepSimple results carry a HitColor.
  • Player-reference gates target persistent player-state - DisplayText, chat/leaderboard/team setters, and the join/left/chat events resolve the current build’s persistent player-state; existing controller-typed scripts keep working unchanged.
  • Fixed fixed-size component arrays failing to load - weapon ammo resources and mesh colors are native fixed-length arrays; emit now pads them to their full length so a save that sets one loads instead of rejecting on size.
  • LSP: config-aware completions and hovers - enum sibling completion, hovers for events, enum values, Clock, and settings-menu config fields, and asset-type dropdown completions for $Type/Name config refs.
  • @label(<expr>) labels - @label accepts an expression, not just a string literal. A constant folds to baked text; on a top-level var a runtime value becomes a dynamic label, wired live into the variable’s floating text - including a variable labelling itself with its own value (@label(x) var x). A blank-line-separated @label(<expr>) at the very top of the file labels the root microchip rather than a declaration, and may forward-reference declarations below it. A runtime @label on a port/chip (which has no wireable text) is a WS040 error.
  • @invisible - hide a port’s rerouter brick and label, or - as a top-of-file annotation - the whole microchip shell (hidden, non-colliding, no labels), for microchips that spawn other microchips.
  • Inline nested prefabs - a triple-backtick block prefixed with $ compiles an isolated inner program and embeds it as a prefab, usable directly as a SpawnPrefab argument. The inner block can import but shares no wires with the outer file (it is a separate prefab); its diagnostics, highlighting, and completions all resolve against the inner program.
  • on <expr> triggers - an on handler can fire on an arbitrary boolean expression - a comparison (on hp <= 0), a method/index result (on a.Dot(b) > 0.0, on arr[i] > 0), a negation (on !flag), or a bare variable - not only a named event or input.
  • Chained method calls across lines - a call chain may continue with a leading . on the next line (f(...) then .g(...) below it); the formatter indents the continuation.
  • Fixed a var initializer missing its = - var x: int 5 silently dropped the value; it now reports a missing = error (and still recovers by taking the expression as the value).
  • Callable gate builtins - every variable/array/map wire gate is now callable as a function named after the in-game gate; this now includes the array-fill gates (FillArrayFromPlayers(arr), FillArrayFromTeamMembers(arr, team), GetPlayersInZone(arr, zone), GetEntitiesInZone(arr, zone)), the function-call twins of the arr.fillFrom* methods.
  • Callable exec-flow gates - Union(a, b) merges two exec signals into one (fires when either fires); Branch(cond, exec) routes an exec to .A or .B on a condition.
  • Fixed multi-output builtin field names - a gate returning several outputs (Edge, Branch, …) now exposes them under their friendly names (Edge(b).Rising/.Falling, Branch(c, e).A/.B) as intended, instead of only the raw port names.
  • on <call>() triggers - a builtin call that returns an exec (on ServerUptime(), on Change(v)) fires the handler on that exec, desugaring like any other on <expr> trigger - distinct from the event-with-args form (on Clock(...)).
  • LSP: scope-aware completion & hover - the editor now resolves a name to its in-scope declaration, so a name reused across scopes (a string at file scope, a character[] inside a handler) is read as the one actually visible at the cursor rather than the first declaration found. This applies to receiver. member completion (the correct method table), the bare-identifier list (a single entry with the in-scope type, no leak of a handler-local into file scope), and hover (the declaration under the cursor shows its own type).
  • LSP: default values in gate hovers - hovering a gate/builtin (e.g. SpawnPrefab, Sweep, DisplayText) now shows a table of its parameter and settings-menu defaults (limit = 5, distance = 100, fontSize = 16, …), read from the same source the emitter uses; enum defaults show the member name (direction = X_Positive). Hovering a named argument (fontSize = ) shows that field’s default too.
  • DisplayText layout is per-axis - the Vector2D layout ports (position/anchor/scale/pivot/shadowOffset) are set per axis: positionX/positionY, anchorX/anchorY, scaleX/scaleY, pivotX/pivotY, shadowOffsetX/shadowOffsetY. A constant axis bakes the property (an unset axis keeps its default); a runtime value wires its X/Y sub-port.
  • LSP: gate hovers render composite defaults - composite/color parameter defaults show as their constructor or sRGB hex (outlineColor = #181425), and a Vector2D sub-port axis shows that axis of its parent default (anchorY = 0.5).
  • Unknown call arguments now error - a named argument that matches no parameter and no settings-menu config field is a WS041 error instead of being silently dropped (a typo’d argument name that previously did nothing). The universal exec = override and a variadic call’s trailing options are exempt.

Migrating from 0.x

1.0.0 tightens several call signatures and reworks a few gates, so some .ws that compiled under 0.x needs edits.

  • Teleporting to a position - Teleport/RelativeTeleport dest/source are now the teleport reference type, so e.Teleport(Vec(x, y, z)) and e.Teleport(other) no longer typecheck. Use e.SetLocation(Vec(x, y, z)) to move to a raw position, or wire a teleport point into an in p: teleport port and e.Teleport(p).
  • Zone events take a zone reference - the events’ zone input is now zone-typed (and tagFilter is string), so zone = e with an entity/brick value errors. Feed an in z: zone port from a Zone brick: on ZoneEntered(character, zone = z) { ... }.
  • zone / teleport are reference-only - like a var ref, they can’t be stored in a var/array/buffer (WS025) or picked with an if-then-else (WS031); pass them straight through ports and parameters.
  • DisplayText.outlineSize/fontSize are now int - pass an integer. The per-axis layout args (positionX/positionY/anchorX/anchorY/scaleX/scaleY) are unchanged and keep working; pivotX/pivotY/shadowOffsetX/shadowOffsetY are new axes on the reworked gate.
  • Swap result fields renamed - Swap(cond, a, b) returns { Output, OutputB } (was { a, b }); read r.Output / r.OutputB (a bare r still auto-unwraps to the first value).
  • InputReader().Jump removed - the movement record dropped Jump; read the new axis/button fields instead (Up, Pitch/Yaw/Roll, MouseWheel, PressedC/E/Q/LeftMouse/RightMouse).
  • BrickChanged / BrickRemoved lost their brick output - these events no longer carry a brick value, so on BrickChanged(brick) { ... } won’t bind; drop the parameter (on BrickChanged { ... }).
  • RotToDir removed - its gate no longer exists in the build; use q.ToDirection() (takes a quat) to turn a rotation into a forward direction.
  • array declaration keyword removed - declare container variables with var instead: var scores: int[] (was array scores: int[]). Storage and behavior is identical - only the keyword changed - so the fix is a mechanical rename. Using array as a declaration keyword is now a parse error that points at the var form.

0.20.0

  • @layout("code") – source-shaped gate layout - a row per source line, expressions left to right, widely-read values down reusable gutter lanes, and own-line // comments rendered onto the plane.
  • @layout("cube") – compact 3D packing - stacks gates into brick layers without analysing the wire graph. Minimal brick mass, no visible dataflow.
  • @flat – inline every chip onto one grid - no microchip bricks or nested planes; chip-wall crossings become ordinary wires. Behavior unchanged.
  • Every chip-wall crossing now routes through a labeled boundary pin - crossings get their own MicrochipInput/MicrochipOutput rerouter, even when not a declared parameter. Constant arguments still inline.

0.19.0

  • Constant expressions in var / array initializers - an initializer may name a top-level let constant and compute with it: array mask: int[] = [1 << C_FLAG, WIDTH * HEIGHT].
  • A named import now carries the constants an imported array is built from - the dependency closure skipped array initializers, so the import failed with “unknown identifier”.
  • Fixed a spurious “unused import” on a constant used only by an array initializer - WS014 fired and Organize Imports would delete it, breaking the table it fed.
  • Fixed a playground rename clobbering another file - the active-file pointer stayed on another file, so the next autosave overwrote it.

0.18.0

  • exec = on any exec call - an exec-gate call (array method, builtin, mod/chip) can run off an explicit exec = <trigger>, e.g. an array read in a pure binding: lut.get(i, exec = i + 1).
  • Fixed emit output = value fan-in - it wired both the value and the exec to the output’s input pin, failing to load; now just the value.
  • Certified constant folding - pure gates whose inputs are known constants are evaluated at compile time against the in-game-certified semantics table, constant-selector Selects short-circuit, constant-condition branches truncate their dead side (including across chip boundaries), all before layout. Opaque(...) and @nofold exempt code. The pass is opt-in while it stabilizes: enable it with a module-level @fold (or --fold); --no-fold (or a module-level @nofold) disables it and always wins over @fold. String concatenation, string methods, and ${...} interpolation now fold too, byte-exact against the game’s own text rendering, alongside vector, rotation, color, and quaternion constructors and their certified math operations.
  • Fixed returning a record through another mod - return f(x) or return r with a record value wired the caller to a phantom node whose wires were silently dropped at emit. The record’s fields now forward to the caller.
  • any type - an in/let/mod-or-chip-param/-output can now be annotated any, an operator-wildcard type that resolves real overloads (x & 1, x == "y", …) instead of erroring on an unknown type. It works anywhere, but the side effects of whichever overload gets picked are on you. A var/static var/array/buffer can’t store one: a variable gate needs one concrete wire type to hold, so an explicit any there is now a compile error.
  • String truthiness - a string now coerces to bool wherever a bool is expected (an if condition, a bool-typed let/var, a bool port or chip param). The coercion compiles to an inserted != "" compare gate, so the semantics are deterministic: empty is false, everything else — including "0" and "false" — is true. (Strings wired into bool ports manually via any still get the gates’ native content-aware truthiness, where "0"/"false" are also falsy.)

0.17.1

  • Fixed a constant on a data-only port failing the build - DisplayText.fontSize and 13 other params name settable fields with no wire input; binding one emitted a wire emit rejects. Now written as data, with a test pinning the list.
  • Fixed destructuring a builtin multi-output call binding nothing - let { Forward, Right } = c.InputReader() left every name an unwired placeholder. Fields now bind to the gate’s ports, and an unknown field errors with a suggestion instead of binding silently.
  • LSP reports lowering and emit errors on save - Live analysis stays typecheck-only, so lowering problems only surfaced on an explicit Compile. Saving now runs the full pipeline and publishes its diagnostics.
  • New arr.get(index) - A checked read giving { Value, OutOfBounds }; used bare it is the element. Completion now offers those fields on arr[i]. too.
  • Blend is the math blend gate - An alias for lerp, accepting any math variant (float/int/vector/rotator/quat/color), as do lerp, Easing, and Tween. The colour-space gate is now ColorBlend.
  • Opaque hovers with its own docs - It showed the Rerouter gate’s blurb, which says nothing about the fold-hiding and type-erasing behaviour it exists for.
  • Fixed .Value on a multi-output result - a.pop().Value typed as the whole record, so every use of it mismatched.
  • Fixed a type alias not resolving through a namespace import - import * as T with mod f() -> MyType failed with “unknown type”. Aliases now inline as they do for a named import, and T.MyType parses as a qualified type.
  • GetLeaderboard returns int - It was typed any, so arithmetic on its result had no operator overload.
  • bool arithmetic with two bools - bool + bool (and - * / %) now promotes to int, matching bool/int mixes and the bitwise ops; (a && b) + (c && d) compiles.

0.17.0

  • Opaque(x) builtin + @nofold annotation - Opaque passes a value through a rerouter and hides it from constant folding; @nofold suppresses folding for a declaration, or for the whole file when placed at the top separated by a blank line. No-op placements warn. Groundwork for gate-semantics verification circuits.
  • Gate semantics probe - probes/gate_semantics.ws prints every probed gate interaction to the console on paste; scripts/gen_semantics.mjs turns the dump into data/gate_semantics.json, and scripts/gen_verifier.mjs generates probes/verify_semantics.ws, which re-asserts every recorded case in-game.
  • Fixed a chip output named x/y/z/r/g/b/a reading garbage - Those names collide with vector/color component access, so reading one split the scalar and returned a component instead of the output. Field access now splits only when the value really is a vector or color.
  • Fixed a chip called from inside a nested anon chip never firing - Its exec trigger stayed at the root, and an exec pulse cannot cross into an instance grid nested inside another anon chip. Partition now routes boundary-pin wires into the module that directly contains the instance.
  • A constant argument to a chip no longer costs a gate per instance - F(1) materialized a _Var in the caller and wired it across the boundary. The constant now folds into the instance itself and its input pin is dropped, matching what the equivalent mod emits.
  • Fixed a tuple-destructured mod parameter binding nothing - mod f((a, b): (int, int)) left every name unbound, so the body silently computed on zeros.
  • Fixed let (a, b) = t on a tuple value - Both names bound nothing, and the shape was rejected as a non-tuple (WS010).
  • out f(x) is now a parse error - The trailing call was dropped and re-parsed as a separate declaration, leaving a bare port.
  • Fixed a namespace import lost through a re-export - Ns didn’t travel with the imported declarations calling through it, so every Ns.f(...) silently did nothing at runtime.
  • Fixed a namespaced call losing its return type - Ns.f(x) typed as any, so Ns.f(x) + 1 failed operator resolution (WS004) and dropped the expression.
  • New tree-sitter grammar - editors/tree-sitter-wirescript/, with highlight/locals/indent queries.
  • Docs: dropped match expressions - Reserved keyword, but the parser has no expression form for it.
  • New docs page: Best Practices - Gate count and scaling: why every call site is a copy (for mod and chip alike), the call-site multiplier, single-dispatch event queues, deferred flags, and bitmask state.

0.16.4 - 2026-07-17

  • Fixed a constant shared across two chips reading 0 in one of them - A literal used as a wired operand (e.g. x * 4) inside two separate chip { ... } blocks was merged by constant-deduplication into a single gate before anon-chip partitioning, leaving the second chip’s operand wired across the chip boundary — where emit’s per-module literal inlining can’t reach it, so the operand silently read its port default (0). Deduplication now groups by owning chip, keeping a shared constant once per chip.
  • Fixed on handlers bound to Change(x) - Change’s OnChanged output is now typed exec (was any), so let c = Change(x) + on c { ... } fires on the change pulse.
  • then may start its own line in an if expression - let x = if cond followed by indented then ... / else ... lines now parses; the formatter indents both keywords one level as expression continuations.
  • Fixed transitive imports resolving in the wrong order - When an imported file had imports of its own, its declarations were placed before the ones it imported, so any call into a deeper module was a use-before-declaration (WS021) or lowered against a missing declaration. This surfaced through a file that only re-exports another (import "b" alone in a.ws). Nested imports now resolve ahead of the importing file’s own declarations, matching how the entry file already behaved.

0.16.3 - 2026-07-16

  • Compiler is ~2x faster on large projects - mimalloc in the native binaries, thin LTO, a single-pass anon-chip partition, a quadratic wire-scan fix in inline chip calls, and Arc-shared ports/templates: lowering −69%, end-to-end −42%, lowering allocations −46%.
  • Compiled output is deterministic - Anon-chip partitioning iterated a randomly-ordered set, so emitted gate/wire structure varied run to run; chips now partition in sorted order and repeated compiles produce identical graphs.
  • New fuzz_programs example - Seeded grammar fuzzer that hunts silent miscompiles: programs with no error diagnostics whose output has _Unsupported gates, duplicate/fan-in wires, or dangling endpoints. Findings write to a gitignored fuzz_findings/.

0.16.2 - 2026-07-15

  • Fixed LSP crash (stack overflow) on large programs - The cycle-analysis SCC walk is now iterative, and every compile* entry point runs on a worker thread with a 256 MiB reserved stack.
  • Fixed LSP crash on multi-byte text - Hover/completion word scanners now step past characters by their real width, and member-receiver lookup converts the cursor column from chars to bytes.
  • Stale compile-command diagnostics clear on edit - Editing or saving any .ws file clears the previous Compile command’s diagnostics; the next explicit compile repopulates them.
  • Rename applies to every reference find-references sees - Three textDocument/rename fixes:
    • Open files match by canonical path and references are deduplicated, so edits are no longer doubled and rejected.
    • import { foo } rewrites to import { bar }; shorthand expands to { foo: bar }, and value-position names are untouched.
    • Rename works from any reference site (u.foo, record fields); built-in event names refuse rename.
  • Compiler is ~15% faster end-to-end - Internal tables use the Fx hasher (crate::collections) instead of SipHash: lowering −15%/−26%, cycle analysis −45%/−55%, layout −36%, world building −21%/−33%. Map iteration is now deterministic, so output is more stable run-to-run.
  • ~30% fewer allocations during lowering - Chip declarations are shared via Arc instead of deep-cloned per call, and scope keys ride the interner; mod-heavy programs lower ~14% faster. New count_allocs example reports per-stage allocation counts/bytes.

0.16.1 - 2026-07-15

  • chip let labels the chip with its binding name - chip let x = ... now shows the binding name(s) as its display label; an explicit @label(...) still overrides.
  • Wider vertical gap between chip-pane rows - The wall layout’s row-to-row gutter was widened so stacked chip planes read as separated.
  • Fixed repeated chip calls sharing wire endpoints - Later instances of the same chip (foo(0), foo(1)) wired their boundaries to the first instance and failed to load (“Failed to connect wire”). Boundary wires now remap to each instance’s own nodes.
  • Hover on a namespace member shows its signature - u.foo (via import * as u) now shows the full signature and exec-ness, matching a direct call’s hover.
  • Unresolved namespace/method call is now a hard error (WS002) - ns.foo(...) whose base is not in scope errors at the dangling identifier instead of lowering to a silent _Unsupported gate.
  • Organize Imports preserves namespace, bare, and multi-line imports - Alt+Shift+O now keeps every import form (namespace/bare imports are never pruned; unused named imports still are) and sorts a namespace import before a named import from the same module.

0.16.0 - 2026-07-14

  • Fixed field triggers on a local in handlers - on x.field (and negated on !x.field) now fires the matching output port instead of the local’s default port.
  • Duplicate constant gates merged per chip - A repeated constant is emitted once and fanned out. Pure gates with no wired input only; Random/stateful detectors are never merged; cut ~1200 gates on a large project.
  • LSP: member completion after receiver. wins inside a call arg - Call(arg = recv.<here> completes recv’s members (records complete their fields, including in on handlers); Call(<here> still completes params.
  • LSP: more completion contexts -
    • import * as u then u.<here> lists the module’s members.
    • pos.<here> on a var pos: vector offers type methods + swizzle (x/y/z, r/g/b/a) alongside .Value/.prev; static var gets .Value/.prev.
    • Values typed by a type Foo = { ... } alias complete Foo’s fields.
    • User mod/chip/fn calls complete their param names instead of the global list.
    • All-required calls (Vec(<here>)) offer their params; method calls drop the bound receiver param.
    • @-annotation list adds @label and @closed.
    • Native LSP and web playground share these paths.
  • Doc comments on record-type fields - A /// on a field inside type T = { ... } now parses (was a parse error) and shows on hover of that field.
  • Fixed hover on a namespace alias - Hovering u in import * as u shows namespace u and lists its members (was namespace u: unknown).
  • VS Code formatter (Prettier plugin) - Adds a space after commas; splits long braced imports (fill) and binary-op statements (one operator per line, lowest precedence first) at 100 cols; joins } else {; honors // fmt-ignore (standalone guards the next line, trailing its own). /// doc comments auto-continue on Enter.
  • Opened-plane headers space the doc off the title - A blank line now separates the size-96 title from the chip/module doc comment.
  • Warn on asset/reference values in an array initializer (WS024) - Assets ($Type/Name) and prefab refs can’t bake into a constant array/var initializer; build with .push(...) in an exec handler. All reference types (entity/character/controller/brick/prefab/assets) share the object wire and can’t be inlined.
  • Module doc comments stay separate from the first declaration - A top-of-file /// block followed by a blank line (or // comment) is the module doc (root plane header); a block directly above a declaration still documents it.

0.15.0 - 2026-07-13

  • Color arithmetic - + - * / % operate RGBA channel-wise on two color operands; a scalar broadcasts across channels (tint * 0.5). Same PrimMath gate as vectors/rotations.
  • Random is polymorphic - min/max may be vector, rotator, quat, or color; each component rolls independently and the same type is returned (Random(Vec(0,0,0), Vec(1,1,1)) → point in the unit cube). Scalar int form unchanged.
  • Fixed anonymous-record mod returns - A mod returning a record literal (return { head: ..., rest: ... }) now destructures into per-field sources, so each field wires to its own value (was one _Unsupported gate).
  • Non-root chips compile open by default - Opened planes stack as a wall above the compiled microchip (root at bottom, deeper nesting higher). New @closed collapses a chip but keeps its wall slot; open chip is now a no-op.
  • New @label("text") annotation - Display-text override for chip labels/headers and in/out port labels (stacks with @side in any order); the wiring-UI port name is unchanged.
  • Opened planes render a header - A size-96 title (the @label text, else the chip name) plus the chip’s /// doc comment, on an invisible brick at the plane’s top edge.

0.14.1 - 2026-07-13

  • LSP: fixed a return <expr> mod mislabeled exec on hover - return alone no longer forces the exec label; only an exec op in the returned expression (e.g. an array read) does.
  • Pruned duplicate constants from dual imports - A module imported via both import * as x and a named import no longer ships its top-level let constants twice; fully-disconnected pure gates and orphan literals are pruned.

0.14.0 - 2026-07-12

  • Port-side rerouter pins - @left/@right/@top/@bottom on a top-level port (same line or the line above) emits a pre-wired rerouter brick flush against that side of the microchip. Ports keep declaration order per side (ins/outs interleave), each pin is labeled with its port name; annotations inside chip {}/mod bodies error (WS023).

0.13.1 - 2026-07-11

  • Fixed array.pop() returning 0 - Both gate outputs are now declared: .Value reads the popped element and .IsEmpty reads bIsEmpty (true once the array is empty after the pop).
  • Fixed buffer initializers inside chip/mod/handler bodies never wiring - The initializer expression was silently dropped, leaving the buffer’s input dangling.
  • Silently-dropped var initializers now warn (WSP001) - Warns on a non-constant init in pure position, any non-constant static var init, and an exec-context array-var init that isn’t an array literal. Use a let for a pure computed binding, or assign inside an exec handler.

0.13.0 - 2026-07-10

  • ~4x faster compiles on large projects (5.9s → 1.5s):
    • Each chip is laid out exactly once; the pre-emit layout pass no longer recurses into children.
    • Layout: one toposort bucketed per connected component; prebuilt consumer map + O(1) occupancy checks.
    • Emit: gate-data schema classification and interned names resolved once per gate class; no per-brick String clones.
    • Lower: dead exec-union pruning is a single incremental worklist pass.
  • brdb 0.8.0 - Unset component fields skip a defaults scan and two error-String allocations per field; brz index compression actually works (its size guard was dead code).
  • Fixed field access on a call result dropping the call - arr.find(x).Found / .Index now keep the call.
  • Fixed a standalone chip losing its exec output - An exec-bearing body ending in return <value> now ships the output.
  • Fixed out X = X emitting no wire - Applies when the output shares its name with a var/array.
  • LSP: hover on a call-result field resolves its type - ids.find(x).Found resolves from the call’s record.
  • LSP: goto-definition on a namespaced call resolves in the imported file - u.foo with import * as u no longer jumps to a same-named local decl.
  • Chip exec I/O gates are labeled - Exec gates say exec; the anonymous -> type return output says return (synthesized ports had no label).
  • ControllerJoined/ControllerLeft expose the player’s id - on ControllerLeft(controller, userId) (string); stable when the controller is torn down on disconnect.
  • Calling a chip/mod before it is declared is a hard error (WS021) - In both the compiler and the LSP (was a silent placeholder reading its default 0).
  • in X: T[] array inputs are first-class - An array-typed in port supports array methods (X.length(), X.push(v), …) and passes to a mod/chip’s T[] parameter.
  • Namespaced module members resolve inside their own mods - import * as ns only; named imports were unaffected.
  • Chip/mod calls check their argument count (WS022) - Hard error in both the compiler and the LSP (a wrong count silently left a param unbound or dropped an arg). An exec = trigger isn’t counted; a spread arg skips the check.

0.12.3 - 2026-07-10

  • Anonymous-chip constants - Fixed literal constants not reaching anonymous chips.

0.12.2 - 2026-07-09

  • ReadBrickGrid() - New builtin.

0.12.1 - 2026-07-09

  • Zone events bind their Zone input - on ZoneEntered(character, zone = z) wires z into the event gate’s Zone port, so a wired in port selects the watched zone. Covers ZoneEntered/ZoneLeft, EntityZoneEntered/Left, ProjectileZoneEntered/Left, BrickChanged/BrickRemoved.
  • Fixed a false recursion flag - An imported namespaced identifier no longer triggers it when conflicting with a local identifier.

0.12.0 - 2026-07-09

Language / Compiler

  • Emitted saves label their elements with text decals - The top-level chip is titled with the entry file’s stem (or --name); named chips, variables/arrays, and microchip I/O gates get diagonal floating name labels. _-prefixed ports stay unlabeled.
  • Var/array exec gates tag their variable - Var_Get/Var_Set/Var_Increment and array-var gates carry a smaller tag naming the accessed variable, traced through the ref wire (works across chip boundaries for captured vars).

0.11.0 - 2026-07-08

Language / Compiler

  • Gate data mappings derive from game data - Struct names and field lists come from the game-extracted pair table + schema, so new gates need no table edits. Stale entries for components the game lacks were dropped.
  • Vector/Rotation literals embed into gate data - e.SetLocation(Vec(0.0, 0.0, 100.0)) bakes the vector into the gate instead of spawning a wired MakeVector. Split* inputs still materialize.
  • Exhaustive gate-data write audit - A test serializes a literal into every representable field of every game component through the real writer; a failure names the gate and field.
  • Record literals as call args bind their fields - { a: 1, b: 2 } passed to a destructured (f({ a, b }: P)) or whole-record (f(p: P)) param now lowers the fields.
  • String constants inline as wire variants - Ports that can’t hold an inline variant keep the real gate.
  • Chips capture the whole enclosing scope - let/in/event-param references now resolve; constants clone into the chip, so let K = 2 used as arr.push(K) bakes 2 into the gate.

Bug Fixes

  • min/max and 14 more expression gates embed literals - min, max, sign, round, exp, ln, the hyperbolics, Deg2Rad/Rad2Deg, BitCount, and ScaleVec no longer drop literal args like min(a, 3.0).
  • ScaleVec wires to the real ports - Input/Scalar instead of the nonexistent InputA/InputB.
  • Destructuring record literals - Now properly lowered to bindings.

0.10.2 - 2026-07-08

Language / Compiler

  • emit/await loops with buffer emit - buffer emit sig (1 tick), buffer(N), buffer(0.5s), buffer(myVar), or buffer(delay, hold) inserts the Buffer(Ticks|Seconds) gate a wire-graph cycle needs. Constants bake into the gate; variables wire the duration port.
  • Payload ferrying - emit sig = value stores the value in hidden per-signal vars (one per record field); let x = await sig / let { a, b } = await sig reads it back. Cost: one Var_Set per field per emit, one Var_Get per field at the await.
  • Body-level let x: exec wires correctly - await x on a body-declared signal no longer lowers to a dead placeholder.
  • Signals are scoped per declaration - Two mods each declaring let loop: exec no longer share one signal; hubs are keyed per declaration and resolved through the scope.
  • Handler-local array vars re-init correctly - var nums = [1,2,3] in a body rebuilds via clear + push instead of wiring a nonexistent VarRef port.
  • Layout no longer panics on multi-cycle SCCs - Feedback-edge removal iterates until acyclic, so two loops sharing a chain lay out.

Language / Compiler (types)

  • entity coerces to character/controller - Character/controller receiver methods and typed params accept entity values (e.g. Sweep’s HitEntity), wiring directly with no adapter gate.

Bug Fixes

  • CharacterDamaged attacker is character-typed - Was entity, which receiver methods and typed params rejected. The weapon binding stays entity.
  • ShowStatusMessage and 12 more gates - Literal args now persist.
  • Recursive chip/mod calls error instead of crashing - Now a WS020 error.

Editor / IDE

  • Named-arg hovers only fire on the arg name - In delay = delay, hovering the value shows the symbol, not the param docs.
  • Method/call hovers only fire on the actual access - Array-method hovers require a .method access; builtin call/method hovers require recv.method or name(. A bare identifier (e.g. var sum = 0) hovers as itself.

0.10.1 - 2026-07-07

Language / Compiler

  • Asset references are entity-typed and usable as values - $Type/Name is entity (was any), so weapon == $BRItemBase/Weapon_Pickaxe type-checks instead of erroring (WS004). As a value it materializes into the matching *Reference gate (ItemReference, AudioReference, EntityTypeReference, … by asset type), which outputs the asset as an entity wire.
  • DisplayText gained an easing param - The interpolation curve for transition ("Linear" / "EaseIn" / "EaseOut" / "EaseInOut"), a property-only enum like justify.

0.10.0 - 2026-07-07

Bug Fixes

  • character and controller wire directly - No more GetFromEntity adapter, an admin-only gate that got blocked on paste for non-admins.
  • Gate brick colours no longer double-darkened - Colours emit as the intended sRGB values instead of being pre-multiplied by γ=2.2.
  • Multi-byte string chars survive emit - The lexer reads whole UTF-8 chars, so /é no longer mangle.
  • Long templates no longer drop values - FormatText has only 7 substitution inputs; templates with more ${...} values split across chained gates.
  • on <local exec signal> fires across handlers - emit sig in one handler triggers on sig in another, regardless of source order.
  • Lexer no longer panics on stray multi-byte chars - A non-ASCII char outside a string (e.g. ) is now UTF-8-safe instead of crashing the LSP.
  • FindPlayer is an exec gate returning character - Has Exec/ExecOut ports and emits the found player’s character; was mis-declared pure returning entity.

Editor / IDE

  • $ reference highlighting + hovers - Prefab ($./x.brz) and asset ($Type/Name) refs get TextMate scopes and hovers; prefab hovers show the resolved path and (in the LSP) whether the file exists.
  • Prefab refs are navigable - A resolvable $./file.brz is a clickable link / go-to-definition target (Ctrl/Cmd-click or F12).
  • Missing prefab files warn - The LSP flags a $./file.brz that isn’t on disk or lacks the .brz extension.
  • Playground uploads .brz prefabs - A Prefabs panel (upload + drag-drop) stores files as browser blobs (IndexedDB), offers them in $./ completion, and embeds them at compile.
  • Named-arg completion + hover work in multi-line calls - The enclosing-call scan covers the whole call (skipping strings/comments), not just the current line.
  • Enum-valued args complete their values - A named arg backed by a schema enum (e.g. justify) completes its variants (Left / Center / Right), auto-quoted when no quote is open.

Language / Compiler

  • Prefab references embed a .brz into SpawnPrefab - $./file.brz (relative) / $/abs.brz (absolute) embeds the archive content-addressed (brdb 0.7 add_prefab) and sets the gate’s Prefab path. .brz required (WS019); resolution pluggable via EmitOptions::prefab_resolver.

0.9.0 - 2026-07-07

New Builtins

  • Split edge/change detectors - Edge(bool) -> {Rising, Falling: bool}, EdgeExec(float) -> {Rising, Falling: exec}, Changed(any) -> bool, Change(any) -> any.

Bug Fixes

  • SpawnPrefab gained a velocity param - The gate’s SpawnVelocity input.
  • SpawnPrefab() - Compiles again.

Gate Catalog / Data

  • Gate inventory regenerated - 314 -> 316 entries (the two exec detectors).
  • Edge Detector emit mapping key fixed - The class name was missing Type, so its component data was never written.

Language / Compiler

  • exec = named arg on chip and mod calls - Pass a trigger when calling exec chips/mods outside an exec context. The call returns the completion exec as an exec result field: await r.exec / on r.exec { }.
  • Import dependency pulling fixed - Imports pull same-file deps in record/array literals, emit values, await exprs, and buffer inits; type aliases inline into imported let/var/out/buffer/in annotations, not just chip/mod params.
  • WS013 understands emit - The unassigned-output check counts emit x (= expr) and plain assigns anywhere in the body, per-output.
  • Named chip bodies capture top-level state - Free references to outer vars, arrays, buffers, and record bindings resolve against the caller’s scope.

Parser

  • Multi-line array literals - Newlines allowed after [, around commas, and before ], with optional trailing comma - mirroring call-arg rules. Covers top-level array initializers and runtime foo = [...] rebuilds.

Editor / IDE

  • Formatter indents multi-line array literals - Both formatters (native, prettier plugin) track [/] depth like (/); delimiter scanning stops at // so comments don’t skew indentation.
  • Formatter: one indent level per line - A line opening several groups (f(x, {) indents its continuation once, not once per delimiter; the closing }) returns to the opener’s level.
  • One “Wirescript” entry in the formatter picker - The extension keeps its prettier formatter and sends provideFormatting: false so the LSP doesn’t register a duplicate.
  • Prefab path completion - $./ (or $/) completes .brz refs: the native LSP scans the document’s directory; the wasm playground offers dragged-in files via a new optional prefabs_json registry.

0.8.0 - 2026-07-06

New Builtins and Methods

  • Chat / messaging - ctrl.ShowChatMessage(msg) (per-player whisper), ctrl.ShowMessageBox(msg, title?) (modal popup), and global BroadcastChatMessage(msg) / BroadcastStatusMessage(msg, flash?).
  • Audio - entity.PlayAudioAt($BrickOneShotAudioDescriptor/..., volume?, pitch?, innerRadius?, maxDistance?, spatialized?) plays a one-shot at an entity (characters work); PlayGlobalAudio(audio, volume?, pitch?) plays for everyone. The descriptor is an inlined $ asset reference.
  • Entity tags - entity.SetTag("...") / entity.GetTag() -> string attach an arbitrary string to any entity and read it back; zones can filter on tags.
  • FindPlayer(name) - Pure value gate looking up a player entity by name.
  • Change(input) - Any-typed companion to Edge: pulses the input value through when it changes.
  • Quaternion raw components - Quat(x, y, z, w), q.SplitQuat() -> {X, Y, Z, W}, a.QuatDot(b).
  • Inventory family - char.AddInventoryItem(item) / SetInventoryItem(item, slot?), AddInventoryBrick(brick, size?) / SetInventoryBrick(...), AddInventoryEntity(entityType) / SetInventoryEntity(...), and AddInventoryItemAdv / SetInventoryItemAdv with overrides (damage, speed, scale, itemName, projectile). Asset args are $Type/Name references.

New Events

  • CharacterDamaged(character, damage, attacker, attackerWeapon, attackerWeaponName) - A character took damage.
  • EntityZoneEntered / EntityZoneLeft (entity) and ProjectileZoneEntered / ProjectileZoneLeft (character, projectile, weapon, weaponName) - Zone events beyond characters; the projectile events’ character is the shooter.

Compiler / Output

  • Generic asset-field emission - Gates with a class/object data field (AudioDescriptor, Item, EntityType, BrickAsset, ProjectileOverride) register inlined $ asset references in the world’s external-asset table automatically. Binary encoding needs in-game verification.

Gate Catalog / Data

  • Gate inventory regenerated - 288 -> 314 entries (26 new classes); the messaging/tag/zone-event/quaternion/inventory gates are wired into the language.
  • brdb data regenerated - Component _max schema (286 structs) and component_db.rs (296 type mappings). assets/external.rs kept the previous full catalog (the dump referenced only 14 assets).
  • Deliberately not exposed as builtins - The *Reference gates ($Type/Name covers them), Convert/ColorConvert (implicit coercions cover them), and AddInventoryEntry (opaque nested struct; GiveWeapon covers it).

0.7.0 - 2026-07-05

Language Features

  • Scalar var type inference - var foo = "" is a string var, var n = 0 an int var, var f = 1.5 a float var (also bools, negatives, interpolated strings). A non-literal initializer refines from its expression (var v = Vec(1.0, 2.0, 3.0) is vector), same as buffers.
  • Everything casts to string - All variant-able primitives (numbers, floats, bools, vectors, rotators, colors, entities, characters, controllers, bricks, prefabs) coerce to string: let s: string = 5 is a cast, not a WS016 warning, and .. accepts any of them ("hi " .. player). Unannotated array vars also infer constructor elements (var pts = [Vec(1.0, 1.0, 1.0)] is vector[]).
  • Color() returns color - Was any; matches ColorSRGB/ColorHex/Blend.

Constant Folding

  • Vec/Rotation/Color on literal args fold to constants - var v = Vec(1.0, 2.0, 3.0) bakes into the Variable gate’s initial value, and constant constructors are legal top-level array initializer elements (loads pre-populated).
  • Folded constants inline into consumers - A constant Vec(...) lands as a literal in the consuming gate’s data (Var_Set, math operands, select branches, arr.push); wire-only consumers (SetLocation/Teleport, component splits, chip inputs) get a Make* gate materialized, never silently zeroed.
  • Vars and arrays of every wire variant - rotator (zero), quat (identity), and color (opaque white) vars get type-matched initial values; rotator[]/quat[]/color[] back onto the typed array variants (WireGraphRotatorArray/QuatArray/LinearColorArray) instead of doubles.

Dependencies

  • Requires brdb 0.6.3 - The wire variant gained Rotator/Quat/LinearColor members plus matching typed array variants; only WireGraphEnumWrapper remains unmapped.

0.6.0 - 2026-06-30

  • Data regenerated - Gate inventory (285 -> 288: new Convert / FindPlayer gates), the brdb component _max schema (258 structs), and component_db.rs.
  • Sweep upgrade - The raycast Sweep(...) gate gained optional per-channel flags: detectBricks, detectPlayers1detectPlayers4, detectPhysics, detectMap, and ignoreOwningGrid.

0.5.0 - 2026-06-29

Language Features

  • quat type + rotation/quaternion builtins - A quat primitive (distinct from the euler rotator) plus dir.ToRotation(), q.ToDirection(), v.Rotate(q), q.Invert(), from.RotationTo(to), a.AngleTo(b), a.Slerp(b, alpha), axis.RotationByAngle(angle), q.ToAxisAngle(), and Rotation(p, y, r) / r.ToEuler() for the euler rotator.
  • sRGB / hex color builtins - ColorSRGB(r, g, b, a) and ColorHex("#rrggbb") constructors; c.ToSRGB() / c.ToHex() / a.Blend(b, alpha) receivers.
  • Cycle(count) / Toggle() - Stateful exec value gates (advance a counter / flip a bool each exec pulse).
  • User definitions shadow builtins - A chip/mod/fn named like a builtin (e.g. chip Toggle) takes precedence at the call site.
  • Asset references - $AssetType/AssetName (e.g. $BRItemBase/Weapon_Pistol) references an external asset embedded by name, encoded as an external-asset-table index on emit. Completion: $ offers asset types, $Type/ that type’s names (from the brdb catalog).
  • HasRole / GiveWeapon - ctrl.HasRole("Admin") -> bool (role is a config string); char.GiveWeapon($BRItemBase/Weapon_Pistol, slot) sets an inventory slot to an item asset (builds the nested EntryPlan). Binary encoding needs in-game verification.

Gate Catalog / Output

  • Gate inventory refreshed - Adds 26 new gate classes; the rotation/quaternion, sRGB-color, and cycle/toggle ones are wired into the language, with component data structs registered for .brdb output.

0.4.0 - 2026-06-28

Language Features

  • Pre-initialized arrays - array foo: int[] = [1, 2, -3] writes literal contents (numbers incl. negatives, strings, bools) straight into the array gate, loading pre-populated. A non-literal top-level element is a clear error.
  • Inferred array-typed vars - var foo = [1, 2, 3] infers int[] and lowers to the same array gate as an array declaration; it indexes and iterates as a real array.
  • Runtime array assignment + spread - In an exec handler, foo = [a, 1, ...other, 5] rebuilds an array var: clear -> push each item -> append each ...spread. Elements can be any runtime value; a spread splices another array in place.
  • Array methods, one source of truth - Every method derives from a single catalog::arrays table: completion offers the full set on any array-typed value, return types come from gate output ports. find returns { Index, Found, Value } (auto-unwraps to Index); pop/min/max expose .IsEmpty; insert/swap/slice expose .OutOfBounds.
  • GetAim replaces AimOrigin/AimDirection - A character’s camera/aim is one gate returning char.GetAim().Origin / .Direction; reading both fields shares a single gate. The separate calls are removed.
  • Chat command config - on ChatCommand("greet", "Greets the player", player, args): string literals fill CommandName then HelpText in order (or named Description = "..."), and bare identifiers still bind the event outputs (controller, arguments).

Bug Fixes

  • Vector components on stored values - .x/.y/.z (and color .r/.g/.b/.a) work on a vector held in a variable or let binding via the SplitVector/SplitColor gates, not just an inline Vec(...).
  • Vec(...) literal arguments - Constant components are no longer dropped to 0 at emit; MakeVector gained its component-data mapping.

Compiler / Output

  • Gate defaults resolve from component_db - Unspecified data fields are omitted so the brdb writer fills them from STRUCT_DEFAULTS; DisplayText’s FontSize/Lifetime now resolve to the game defaults (16 / 5) instead of 0.

0.3.0 - 2026-06-27

Language Features

  • String variables - var/static var of type string store in a Variable gate (the WireGraphVariant gained a str member). The WS018 “strings can’t be stored in vars” diagnostic is gone.
  • Native string equality - == / != on strings lower directly to the CompareEqual / CompareNotEqual gates. The contains(a,b) && length(a) == length(b) workaround is removed.
  • Vector arithmetic - + - * / % operate component-wise on two vectors, and a scalar operand (v * 2.0, 10.0 * v, v / 4) broadcasts - all on the same MathAdd/Subtract/Multiply/Divide/Modulo gates. The Scale helper still works.
  • Any-variant variables - A var can hold any WireGraphVariant member (int, float, bool, string, vector, object types); typed vars get a type-matched initial value instead of a number default.
  • Typed arrays - The declared element type selects the backing WireGraphArrayVariant member (int -> Int64, float -> Double, plus Bool/String/Vector/Object), so elements keep their declared type.

Gate Catalog

  • Regenerated inventory - Rebuilt from the in-game dump via a new checked-in generator (scripts/gen_inventory.mjs): adds 76 gate classes (ArrayVar exec, Gamemode/Controller/Character, string ParseInt/ParseNumber, reference gates) and types 86 previously-any ports. 175 -> 260 entries.
  • Refreshed brdb component tables - component_db.rs regenerated from the same dump so the new gates emit; the removed Gamemode_EndRound gate is gone.

New Builtins and Methods

  • Array methods - insert, find, sort(desc?), reverse, sum, min, max, average, swap, fill, resize, append, copyFrom, slice, fillFromPlayers, fillFromTeam join push/pop/length/remove/clear/shuffle. Every ArrayVar gate is reachable.
  • Easing - Easing(a, b, blend, fn?, dir?) and Tween(target, duration, fn?, dir?); function/direction pass as an int or enum-name literal ("Quad", "InOut", …) resolved against the engine’s EBREasingFunction/EBREasingDirection enums.
  • Timer - Timer(limit, restart?, pause?, resume?) returns { Time: float, Expired: exec }; the controls are optional exec inputs and Expired works with on/await.
  • String parsing - ParseInt(s) -> int and ParseNumber(s) -> float (also s.ParseInt() / s.ParseNumber()).
  • Controller - GetUserName, GetUserId, GetDisplayName, IsTrusted, HasPermission, SetCanRespawn, SetTeamPinned.
  • Character - GetDamage, SetDamage, IncDamage, SetTempPermission.
  • Entity - SetFrozen.
  • Gamemode - PlayerWins / TeamWins (replace the removed imperative EndRound gate and builtin), GetCurrentRound, SetTeam, GetTeamName, GetTeamLeaderboardValue / SetTeamLeaderboardValue / IncrementTeamLeaderboardValue.
  • Misc - PrintToConsole, DeltaTime, ServerUptime, NearlyEqual, Dampen.

Compiler / Output

  • Prefab output - Compiled programs emit a Brickadia prefab (type: "Prefab" + Meta/Prefab.json with brick bounds from the microchip shell) instead of a world, so the .brz pastes like a native copied selection (Ctrl+V) with a correct preview.
  • Loads on current builds - A bundle embeds only the component structs the program uses plus transitive schema deps, written dependency-first - matching game bundles. Replaces the full-catalog embed recent builds reject; real programs stay within the per-schema struct limit.

0.2.0

Language Features

  • emit target = expr - Set output value and fire exec in one statement. Works in both pure and exec contexts.
  • await expr - Suspend exec chain and resume when expression fires. Armed-flag guard ensures one-shot execution (~7 gates per await).
  • let name: exec - Local exec signals. emit name fires them from any handler; await name or on name listens.
  • let x = await val on trigger - Capture a value when a trigger fires.
  • await a || b - Race semantics via normal binary expressions.
  • _ placeholder in await - Resolves to the armed flag (bool). Enables await Sleep(_, delay = 1.0).
  • Logical/comparison operator coercion - &&, ||, ^^, !, ==, !=, <, >, <=, >= now accept all wire variant types (bool, int, float, exec, string, entity, controller, character, brick, prefab).

Builtin Functions

  • Sleep(input, delay?, hold?) - BufferSeconds gate. Delays a value by seconds.
  • SleepTicks(input, delay?, hold?) - BufferTicks gate. Delays a value by ticks.

Compiler

  • compile_to_world - New compile path returning brdb::World for .brdb output.
  • CLI .brdb support - just compile file.ws -o file.brdb emits SQLite saves.
  • Compile progress - LSP sends wirescript/compileProgress notifications; VS Code extension shows step counter in status bar.
  • ** (pow) fix - Now wires to Input/Exponent ports instead of InputA/InputB.
  • BRZ double-write fix - Fixed to_brz_vec writing the archive twice (exact 2x file size).

Editor / IDE

  • Inlay type hints - Ctrl+Alt shows inferred types for let/buffer bindings. Works in VS Code and the web playground.
  • Hover gate estimates - Hovering chips/mods/handlers/if-blocks shows estimated gate and microchip counts. Call-graph expansion sums callee costs recursively.
  • Record field hover fix - cpu.regs, cpu.cpsr and nested field access now show types correctly.
  • on handler hover - Shows gate estimate for the handler scope.
  • if hover estimates - Shows gate cost for the if/else scope.
  • Tuple display - Records with numeric keys show as (bool, int) instead of {0: bool, 1: int}.
  • await keyword highlighting - Added to VS Code tmLanguage and Monaco monarch tokenizer.

Playground

  • Inlay hints provider - wirescript_inlay_hints WASM binding + Monaco InlayHintsProvider. Hidden by default, shown on Ctrl+Alt.
  • New async_signals.ws example - Demonstrates emit-value, await, local exec signals, Sleep.

Documentation

  • Updated statements.md with emit-value, local exec signals, await, Sleep/SleepTicks.
  • Updated exec-context.md with await section, _ placeholder, Sleep examples.
  • Updated builtins.md with Sleep/Delay section.
  • Updated expressions.md with operator coercion for all wire variant types.
  • Updated types.md with exec->bool coercion.
  • Removed fn keyword references from docs.
  • just compile-brdb recipe added.

Test Files

  • projects/tests/src/ - New in-game test suite: test_await_emit.ws, test_variables.ws, test_operators.ws, test_control_flow.ws, test_chips_mods.ws, test_strings.ws.
  • crates/wirescript/tests/ - Integration tests: await_test.rs, emit_value.rs, local_exec.rs.

0.1.0

Language Features

  • Records & tuples - User-defined record types (type Point = { x: int, y: int }), record literals, destructuring (let { x, y } = p), spread operator ({ ...p, y: 99 }), tuple types and literals
  • Spread in call args - Pass record fields as named parameters: foo({ ...defaults, x: 1 })
  • Destructured params - mod dist({ x, y }: Point) -> int { ... } in mods and chips
  • on expr syntax - Trigger handlers on arbitrary exec expressions, not just named events
  • Exportable vars/buffers/arrays - var, buffer, and array declarations are now importable across files
  • String var error (WS018) - var s: string now errors at typecheck time (Brickadia runtime doesn’t support string variables)
  • Ref/deref improvements - LSP completions on arrays and refs, output ref/deref fixes

Editor / IDE

  • Record field hovers - Hover shows State.counter: *int for record fields and type declaration fields
  • Spread type validation - Extra fields from spread are caught with errors pointing at the ...expr span
  • Chip/mod context hover - Hovering chip/mod keywords shows whether the block is pure or exec
  • Event parameter hovers - Hover on event handlers shows parameter types
  • Mod/chip return type hovers - Hover shows -> (result: int) return types
  • Formatter fixes - Multi-line function call args indented correctly; operator continuation lines indented
  • type keyword highlighting - VS Code extension highlights user-defined type names

Playground

  • Docs panel refactor - Docs fetched from docs/*.md instead of inline JS (~1900 lines removed from docs.js)
  • Examples loaded from files - Playground examples loaded from sdk/examples/*.ws via fetch instead of hardcoded JS
  • New records.ws example - Demonstrates records, destructuring, spread, and tuples

Bug Fixes

  • Fix branch scoping - variables declared in if/else branches no longer leak across branches
  • Fix string comparison gate using wrong variant
  • Fix inline modules adding extra microchip outputs
  • Fix return expr in pure mods
  • Fix on var.value not lowering handler body
  • Fix emit not chaining union gates for multiple emit paths
  • Fix import not pulling in same-file dependencies of imported declarations
  • Fix array index access requiring exec context
  • Fix array .length() / .pop() returning Any type
  • Fix string wire port emits with literal variant values

0.0.0

Language Features

  • Standalone chip instantiation - Named chips with -> (outputs) now compile to real child microchips, one per call site. Cross-chip wires resolve automatically.
  • static var - Variables that persist across rounds: static var highScore: int = 0
  • return expr - Return values from chips and mods
  • Single-output auto-unwrap - chip Foo() -> (result: int) returns int directly instead of {result: int}
  • Block expressions - { stmts; expr } as expressions
  • Compound assignment - +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
  • ^^ logical XOR operator - a ^^ b is true when exactly one operand is true
  • let type annotations - let x: int = expr
  • Array params are always pass-by-reference - mod init(arr: int[]) passes the array by reference without needing *
  • fn deprecation - fn declarations emit a warning (WS015) suggesting let instead

Builtin Functions (30 new)

  • Select/Swap - Select(cond, a, b), Swap(cond, a, b) -> {a, b}
  • String ops (all receiver on string) - s.Length(), s.Contains(search), s.StartsWith(prefix), s.EndsWith(suffix), s.Find(search), s.Substring(start, len), s.Replace(search, repl), s.Split(delim) -> {Left, Right}, s.ToLower(), s.ToUpper(), s.Trim()
  • Math - tan, log(x, base), lerp(a, b, t), fmod(a, b)
  • Vector/Color - v.SplitVec() -> {x, y, z}, c.SplitColor() -> {r, g, b, a}
  • Edge detector - Edge(input) -> {rising, falling}
  • Gamemode - EndRound(winner?), GetTeamByName(name)
  • Character - ShowHint(char, title, text)
  • Controller - ShowStatusMessage(ctrl, message)
  • Bitwise - BitNand(a, b), BitNor(a, b)
  • Renamed MakeColor -> Color
  • 93% gate coverage - 163 of 175 Brickadia gates supported

Events

  • ChatCommand - on ChatCommand(controller, arguments) { ... }

Compiler Optimizations

  • NAND/NOR gate fusion - !(a && b) compiles to a single NAND gate instead of NOT + AND. Same for !(a || b) -> NOR, ~(a & b) -> BitwiseNAND, ~(a | b) -> BitwiseNOR.
  • 7.2x faster chip compile - Schema parse caching + lower zstd level cuts chip program compile from 334ms to 46ms
  • Receiver syntax on all vector ops - v.Normalize(), a.Distance(b), v.Magnitude(), etc. now work as chained calls with correct type inference

Editor / IDE

  • Cross-file go-to-definition - Clicking an imported symbol jumps to its declaration in the source file. Clicking an import path opens that file.
  • Hover on if keywords - Shows whether the block is in exec or pure context
  • Unused import/output warnings - Warnings for imported symbols and outputs that aren’t used
  • wirescript-check CLI - Standalone type checker binary
  • VS Code extension auto-reload - Extension reloads when the LSP binary changes

Removals

  • ArrayRef type removed - arrays are always references, use int[] everywhere
  • event keyword removed (was already deprecated)