Wirescript Changelog
1.10.1
Fixes
- A
matchexpression whose arms are records or enum values compiles, choosing per leaf field. The record spelling dropped the statement silently; the enum spelling was aWS071per 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/matchpicks each port separately. One Select over port 0 meantc.Foundread the value andbFoundwas wired nowhere. - A
mod/chipwith 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 { .. }).fieldreads that field, distributing over the arms likeifdoes.- A
modrecord argument accepts a container element or a record-valuedif/match, as itschiptwin already did. - An expression with no value read as one is a
WS072error. A no-outputmod/chiptyped asany, soo = 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
letinitialized through such an alias carries its type, so reading it is no longer aWS002. - An
import * asalias 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 asanyand loaded empty. - Two
import * asin one file under the same alias is aWS012; unreported, the second shadowed the first and every reference read whichever module came last.
Editor
- The LSP watches the workspace’s
.wsfiles, 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; onlylet x: T = {resolved before.
1.10.0
value is Enum.Varianttests 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 Clocktakes onlyintervalandenabled.pulseOn,onTimeandoffTimeare inert fields of the gate’s data struct, so passing them baked config the gate never reads; naming one is now aWS041error.
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.wsrepeatedly 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 (
Containsis nowString Contains,Format Textis nowFormat String,Lengthis nowGet 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 byscripts/gen_assets.mjsfrom the in-game dump. Weapon one-shot audio is renamedBOSA_*toOSA_*, 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.aon a*Tparameter reportedexpected float, got *float, and an if-then-else over two such fields reported a branch mismatch. rec.field.Valuereads the field’s backing variable. Onlyx.Valueon a plain identifier resolved, so the field spelling lowered to a placeholder and left its consumer unwired.- A record literal passed to a
*Tparameter is aWS008error when a reference field has no variable behind it. Aninport 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
chipwith a*Tparameter 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. &xandref xbind achip’s reference, array, record, or map parameter to the caller’s storage. Only themodform stripped the sigil, so the chip form left the parameter’s pin unfed and silently dropped every write through it.
1.8.0
*Ton 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 = vwrites. A tuplevarcollapsed 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-steplet r = Chip(x)/r.fieldspelling resolved it; the inline form compiled to a placeholder wired to nothing. - A record used where one value is expected is a
WS071error instead of a silent placeholder. A record is several wires, so"x=" .. rechad 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 elsecapture 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
WS007error instead of compiling to nothing. - An enum payload field that needs container storage is a
WS069error, 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/elseselects per leaf, in assignment,outand 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/outport 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;&pon a record variable passed to a*Pparameter 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
matchorif letaccepts a container element (match m[k],match arr[i]) or a.Valueprojection off a mapgetas its scrutinee. Only a named binding of the same read resolved before, so the inline form hit a placeholder. - Referencing one:
&p.aon a record field is accepted, since each field is its own storage gate. It lowered correctly and was rejected byWS008anyway. - A record or enum as a custom-event data param is now
WS068, since a data slot is one wire and cannot carry one.
- Producing one: a record-valued
- A scalar initializer is baked in the declared type, so
var x: float = 0builds 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
execsignal consumed inside achipcompiles. 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/.prevopens a variable that spans several storage gates (a record, an enum); it used to emit a placeholder.- A
varinitializer naming a constant (var x: float = K) bakes that constant.
1.7.1
enumis 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/.rolland aquat’s.x/.y/.z/.wnow read their components, lowering to a Split gate the way avector’s.xalready 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-tocand gated byjust 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
enumtypes (tagged unions):enum Shape { Empty, Circle(float), Box { w: float, h: float } }supports C-like members, positional and named payloads, explicit= Ndiscriminants (later members auto-number from there; a collision isWS064), and generics (enum Box<T> { Value(T), Empty }). Enums are nominal, so two identically-shaped ones are different types..Discriminantreads a value’s tag as anint, and on a variant path (Shape.Circle.Discriminant) it folds to a compile-time constant.matchbranches on a variant and binds its payload, both as an expression (comma-separated value arms, lowering to aSelecttree) and as a statement (block arms, lowering toBranch/Union). Coverage is checked: an uncovered variant isWS054and names the missing patterns, an arm that can never run isWS061, and patterns nest into payloads including other enums. A wrong bracket form for a variant’s shape isWS065, and.Discriminantormatchon a non-enum isWS066.if letandlet elseare single-variant refutable binds:if let Some(x) = o { ... } else { ... }runs the block only for that variant, andlet Some(x) = o else { return }binds into the surrounding scope with a required divergingelse(WS062).- Built-in
Option<T>andResult<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,
.Discriminantgives their real integer value, and a variant such asEasingFunction.Bouncepasses directly as the matching gate config argument alongside the older bare-name form. - Enum and int conversion:
value.ToInt()is an alias for.Discriminant, andEnum.FromInt(n)builds a value from a (possibly runtime) int tag with payloads defaulted to zero. TheEnumToInt/IntToEnumbuiltins (renamed fromEnumToInteger/IntegerToEnum) are the gate-backed twins: they now require an enum-typed value instead of acceptingany, 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 (WS063when 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
modthat reads or writes a container in its body (m.get(k),arr[i],m.set(...)) from a pure position now reportsWS007, instead of silently wiring the container reference into the caller. This now also covers a container reached through themod’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
modwhose 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, orbufferstatement 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
outorinmember (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 typinganyand dropping to a placeholder; it works in expressions (a comparison, arithmetic), not just a barelet.
1.6.1
.execnames an event’s exec output, so a data-carrying event composes intoUnion(...);Unionalso takes an exec receiver, soa.Union(b)chains left-associatively.
Fixes
- An inline record-returning
modcall read by field (f(x).sum) now projects the field instead of an_Unsupportedplaceholder. - 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 nsnamespace (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
modcall (f(n, ...g()), or a boundlet t = g()thenf(n, ...t)) now expands its elements instead of dropping them. - A field or index access on an aggregate-typed
ifexpression ((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
onhandler in animport * asmodule 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 viaimport { g }plusimport { 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
...tuplespread now expands into a call’s positional arguments.SendGlobalCustomEvent(name, ...t)splats a tuple across the event’s data slots, andf(...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 aWS003. - A
modcan take a trailing...restvariadic parameter that captures every argument past its fixed params. Each call site bindsrestto a compile-time tuple of the extra args, and a...restin the body splats them onward, somod broadcast(name: const string, ...rest) { SendGlobalCustomEvent(name, ...rest) }forwards any number of values. A call must still supply the fixed params (fewer isWS022). Onlymods may be variadic; a...reston achipisWS052. 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 tuplelet (p, t) = ...captures data outputs positionally. Annotate the type or the wire defaults to a float (WS055warns when it can’t be determined). Bareawait CustomEvent("c")(no binding) just waits.- A tuple return’s parentheses are optional:
return a, b, creturns the same tuple asreturn (a, b, c), and.0/.1access the elements.
Fixes
- Compile progress now accounts for embedded prefabs: the reported step total grows by one per
$./filereference 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 locallet sig: execsignal wires the signal into the spawner’s destroy pin, so those prefabs are destroyed (a localexecletfolds to a placeholder0, so it needs this dedicated wiring).wirescript-checkruns through lowering, so it surfaces the same_Unsupported-placeholder warningscompileemits.- New
WS053: a plainemit Xin the same chain as a followingawait Xwarns that the kick fires before the await arms (parking the chain forever); usebuffer emit X. - A record passed to a
chipwires its fields: a record literal argument (Foo({ x: a, y: 2 })), a field whose value is avar(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 = 5wherepis not avar) reportsWS007. Avar-backed record field stays assignable. ==and!=between two whole record values reportWS004. Comparing a multi-output result to a scalar (arr.pop() == 5) still works.- A
...restparameter in a destructuredmodparameter 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.aread and write a single leaf,arr[i].inner/m[k].innerread and write the whole sub-record, andlet 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 namespacedoutport) and a nested path (A.B.bar()) reportWS002; a named-argument call through a namespace reportsWS022when 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
ifexpression 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/refparameter reportsWS008. - A statement written after a nested
onhandler inside a handler body stays on the outer exec chain rather than binding to the nested handler’s trigger. - New
WS057:emit Xis an error whenXis not anoutport or alet ...: execsignal (an input port, avar, or anout/signal declared outside an enclosing namedchip). - New
WS056:let v = await sigon a signal that carries no payload is an error. Emit a value (emit sig = ...) or capture one withawait <expr> on sig. - New
WS058(warning): an exec statement that never runs is flagged - A tuple
return (a, b)to amodwith named multi-outputs wires each element to the matching output in declaration order, solet (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.ron a vector,color.x) reportsWS010rather than emitting a wrong-typedSplitColor/SplitVector; a negated-union or double-negation handler trigger (on !(a | b),on !!x) reportsWS001rather than dropping the whole handler; and assigning to a non-lvalue (f() = 5) reportsWS007rather 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-modmonomorphization, so a generic-xemits a negate gate with the right numeric type. - A constant
Substringwith a very large length clamps to the string end instead of overflowing into a panic. - New
WS059:Change/Changed(and theEdgedetectors) on a reference or container (aMap, an array, a*Tref, azone, or ateleport) 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 storedvaris tagged.
1.5.0
- Added a
nullliteral. 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: avar/outinitializer, an assignment, a call argument, or a record field.nullfor a container, record, or reference-only type has no value and reportsWS051; a barelet x = nullwith no target types asany. - 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 getpush/pop/insert/remove/fill/resize/swap/reverse/clear/lengthplus element access (pts[i],pts[i].x,pts[i] = rec,p = pts[i]); maps getset/get/has/remove/clear/length/keysplusm[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, orexec) now reportsWS049instead of silently backing it with an unusable gate. - A record-container method with no per-field meaning (
sort/shuffle, the aggregatessum/min/max/average,find, and the dual-containerappend/copyFrom/slice/values) now reportsWS050instead 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 aspts.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
onhandler (import * as L from "lib"where lib hason 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-irnode 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-levelonhandlers. - Two
import * asnamespaces (or a local declaration plus an imported one) that share a state name now get distinct storage gates. - A
var/array/map/bufferdeclared inside a handler,if, or block now gets its own storage gate instead of silently reusing an outer same-named one. &xpassed 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"orimport { x }, now reportWS013instead of silently collapsing both onto one storage gate. - A container mutation (
arr.push(x),m.set(k, v)) outside an exec context now reportsWS007instead 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 anifis now gated by the branch instead of wired unconditionally to the output; a guarded write no longer becomes permanent. - A
letthat shadows an in/out port of the same name now reportsWS013instead of silently hijacking it (let gobeforeon go, within go: exec, left the exec input dead). Ordinary value shadowing (let a = 1; let a = 2) is unaffected. - An output reached by an
emitplus a default initializer (out r = 0thenemit r = …), or by emits split across a handler and an anonymouschip { … }, is now var-backed instead of driven by two wires - a load-breaking fan-in on the output rerouter. - A
letaliasing an input-port array or map (in a: int[]thenlet x = a) now resolves forx[i]and container methods, like avaralias; 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
helperno longer make one module’s public functions run the other’s code, and a namespaced mod mutatinggnow 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 reportsWS007instead of silently lowering to a placeholder, matching the pure index-read rule. Aconstreceiver or an explicitexec = <trigger>arg is exempt. - A captured handler with a non-Event trigger (
let e = on go { … }wheregois an exec input,var, orlet) now runs its body and captures its exit ase; the body used to be dropped entirely unless the trigger was a built-in event.
1.4.3
- An imported
letno longer clobbers a same-named declaration in the importing file. An importedlet startoverwrote the file’s ownin start: exec, soon startbound the imported value instead of the input and silently dropped the handler body. - Void container operations (
push/clear/set, thekeys/valuesfills) now type asnever, so using their result as a value (let r = a.push(x)) is a type error instead of silently accepted. - Top-level
onhandlers in an imported file now run; they were silently dropped, and anon <expr>handler additionally left a dangling trigger gate with no body. - A
letaliasing 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 asns.name, sons.tuple.0resolves 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.brzprefab 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 warnedWS014while 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
inandoutports are now declared and reachable asns.name. They were dropped entirely, soon ns.trigger { ... }matched nothing and silently discarded the whole handler body while both stages reported the file clean. Root-levelvar,array,mapandbuffermembers 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 warnedWS014, and Organize Imports would then delete it. - Two
import * asnamespaces that export the same member name now stay distinct.A.fooandB.foohad 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
constcompile-time evaluation:constbindings,constparameters (f(name: const string, v: int)) andconst moddeclarations 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. Anifon a const condition drops its untaken branch, and aconstthat 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 ofInputReader. Same field names, sochar.GetInputs().PressedQreads 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, soGetLocation(e)was an error whilee.GetLocation()compiled to a placeholder that read a default. - A local binding that shadows an
import * as nsalias now reportsWS002at the call. A parameter orletnamednsmade everyns.f(...)in that scope resolve against the local value, which has no such member, so the call typed asanyand compiled to a placeholder that did nothing while type-checking reported the file clean. The failure then surfaced wherever theanywas 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 ofany, 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, sons.rect.a.xkeeps its type instead of reading asany. - Namespace members (
import * as ns) written without an annotation, or bound by a destructuringlet, now carry their record type across the import too. - Compiler source reorganised
1.2.0
map[key]subscript syntax -m[k]andm[k] = vnow work on aMap<K, V>, desugaring to the same get/set them.get(k)/m.set(k, v)methods use (m[k]reads the value, auto-unwrapping the found flag;m[k] = vwrites it). The read types as the value typeVand, 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-levelout 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 toany. let t: (A, B) = …andlet 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")isWS003). - A heterogeneous array literal in a handler (
xs = [1, "hello", 2]) reportsWS003on the odd element. - An
any[]/Map<any, …>parameter now accepts a concrete array/map argument. ==/!=on twovector/rotator/quat/colorvalues is now accepted (ordering stays scalar-only).- Assigning to a scalar
letreportsWS007instead of silently emitting no gate. - An unknown named argument on a
mod/chipcall reportsWS041instead of being dropped. - A typo’d event config/input name (
on Clock(intreval = …)) reportsWS041instead 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
varread afterawaitre-reads fresh, so a value changed during the wait is visible. ns.myValueon animport * as nsnow reads the real value and type — it typed asanyand compiled to a placeholder reading 0, while onlyns.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 doesemit signow 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-caseatom literal at its hyphen. - A field access on a scalar (
x.whateveron anint) reportsWS010instead 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 + 1in three outputs, or call the samemodtwice, and you get one gate instead of a copy per use. (State-holding and@nofoldgates are left alone.) - A variable read is reused across an
ifwhen 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/referencesnow 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
typealiases 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 (acharactercapture) or a builtin function (a value namedround) 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. .wssource prefabs - a prefab reference may now point at a.wssource 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.wsreference itself, so a broken prefab underlines where it’s used..brzarchives work as before; any other extension isWS019.- 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 clearWS036error instead of silently type-checking asanyand lowering to a do-nothing placeholder. - Fixed:
@folddropping a runtime@label- a runtime@label(<expr>)(on avaror, 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 theonform (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. Theon 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) oron Foo() -> { field: local }(record, by field name - named events, for subset/rename).on <call> -> ...also triggers on any exec-producing call - amod/chipcall, or a gate driven byon Foo(exec = x) -> (...)- auto-extracting the call’s exec output; a general call that exposes no exec (or anexec =with nowhere to attach) is aWS043error. A single untyped output may drop the parens:-> whois shorthand for-> (who). - Custom-event data-type inference - an unannotated custom-event receiver slot takes its type from the matching in-unit
SendCustomEventon that channel; when none is inferable the slot defaults tofloatand warnsWS042(which replaces the oldWS029“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 amod/chipparameter, anin/outport, or a record field (not just a file-scopevar). A container method whose receiver isn’t a container isWS044, not a silent no-op. - Generic value builtins -
Select,Swap,Sleep,SleepTicks, andTweencarry their argument’s type instead ofany, soSelect(c, 1, 2)is anint. - 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
*Tfield and a plainTare interchangeable at a call boundary, as parameters already are). - Tighter builtin argument types -
DisplayText/PrintToConsole/Fmt/SetTagtakestring,SetLeaderboard/IncLeaderboardtakeint, andSpawnPrefabtakes aprefabreference. - Fixed: exec after an emitting mod - a statement following a
modwhose body ends inemitno 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) { ... }becomeson 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) { ... }becomeson CustomEvent("dmg") -> (amount: int) { ... }; an omitted type is inferred from the sender, or warnsWS042. - Event triggers are calls - an event trigger is written with
():on RoundStart(), noton 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 theon <call> -> (...)model. WS029is removed - the “annotate your custom-event param” lint is replaced by inference plusWS042.
1.0.0
- Maps (
Map<K, V>) - a keyed variable collection paralleling arrays:var scores: Map<string, int>, keyed byint/string/object reference and holding any wire-storable value, with exec-context methodsset/get/has/remove/clear/copyFrom/length/keys/values(getgives{ Value, Found }, auto-unwrapping toValue). A non-int/string/object key type is aWS039error. - Map literals -
{ k => v }keys by any expression,"s": v/:atom: vby a string/atom/int literal, and[expr] => vby a computed key; a fully-constant literal bakes the map pre-populated at rest,{}is an empty map, andm = { ... }in a handler desugars toclear()plus onesetper entry in source order. - Atom literals (
:name) - a compile-timeintconstant (the deterministic xxHash64 hash of the name), a readable stand-in for a magic number as anint-map key or enum-like tag; it only ever resolves at compile time, never from a runtime string. - Generic type syntax -
Array<V>andRef<V>are exact aliases ofV[]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 aWS028error. - Custom events -
SendCustomEvent(name, data...)pulses everyon 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
SendCustomEventon 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&teleportreference 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’zoneinput is nowzone-typed, andTeleport/RelativeTeleportdest/sourceare nowteleport-typed - teleporting to a raw position usesSetLocation.Clockreads as an event -on Clock(interval = 2.0, enabled = running) { ... }runs its body on each pulse;intervalandenabledare wire inputs (constant or dynamic, so the clock toggles at runtime) andpulseOn/onTime/offTimeare 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, andForceRespawn(player)(alsoplayer.ForceRespawn()). - New date/time and conversion builtins -
GetUnixTime,FormatDate,Remap,LogicalShiftRight,EnumToInteger/IntegerToEnum,ItemToPickup,ConvertColor, andToCharCode/FromCharCode;ParseInt/ParseNumbergained a.Successflag and auto-unwrap to their parsed value. - Zone array fills -
arr.fillFromZoneEntities(zone, tagFilter?)/fillFromZonePlayers(...)populate an array from a zone, andarr.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 (alsoPlayClientAudio(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) alongsideForward/Right. DisplayTextreturns atextId- capture the returnedintto update or clear the same on-screen text later, with new color/outline/shadow/spacing/wrap styling params;position/anchor/scale/pivot/shadowOffsetareVector2Dlayout properties on the reworked gate.GetDamagegives{ Damage, DamageLimit }- auto-unwraps toDamagewhere a float is expected, and.DamageLimitreads the death threshold.- Richer event outputs - the fired-weapon event exposes the
weaponand its name,CharacterDiedthe killer’s weapon and name, andControllerJoined/ControllerLeftthe player’s user name;Sweep/SweepSimpleresults carry aHitColor. - 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; existingcontroller-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/Nameconfig refs. @label(<expr>)labels -@labelaccepts an expression, not just a string literal. A constant folds to baked text; on a top-levelvara 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@labelon a port/chip (which has no wireable text) is aWS040error.@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 aSpawnPrefabargument. The inner block canimportbut 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 - anonhandler 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
varinitializer missing its=-var x: int 5silently dropped the value; it now reports amissing =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 thearr.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.Aor.Bon 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 otheron <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
stringat file scope, acharacter[]inside a handler) is read as the one actually visible at the cursor rather than the first declaration found. This applies toreceiver.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. DisplayTextlayout is per-axis - theVector2Dlayout 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 itsX/Ysub-port.- LSP: gate hovers render composite defaults - composite/color parameter defaults show as their constructor or sRGB hex (
outlineColor = #181425), and aVector2Dsub-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
WS041error instead of being silently dropped (a typo’d argument name that previously did nothing). The universalexec =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/RelativeTeleportdest/sourceare now theteleportreference type, soe.Teleport(Vec(x, y, z))ande.Teleport(other)no longer typecheck. Usee.SetLocation(Vec(x, y, z))to move to a raw position, or wire a teleport point into anin p: teleportport ande.Teleport(p). - Zone events take a
zonereference - the events’zoneinput is nowzone-typed (andtagFilterisstring), sozone = ewith an entity/brick value errors. Feed anin z: zoneport from a Zone brick:on ZoneEntered(character, zone = z) { ... }. zone/teleportare reference-only - like a var ref, they can’t be stored in avar/array/buffer(WS025) or picked with an if-then-else (WS031); pass them straight through ports and parameters.DisplayText.outlineSize/fontSizeare nowint- pass an integer. The per-axis layout args (positionX/positionY/anchorX/anchorY/scaleX/scaleY) are unchanged and keep working;pivotX/pivotY/shadowOffsetX/shadowOffsetYare new axes on the reworked gate.Swapresult fields renamed -Swap(cond, a, b)returns{ Output, OutputB }(was{ a, b }); readr.Output/r.OutputB(a barerstill auto-unwraps to the first value).InputReader().Jumpremoved - the movement record droppedJump; read the new axis/button fields instead (Up,Pitch/Yaw/Roll,MouseWheel,PressedC/E/Q/LeftMouse/RightMouse).BrickChanged/BrickRemovedlost their brick output - these events no longer carry abrickvalue, soon BrickChanged(brick) { ... }won’t bind; drop the parameter (on BrickChanged { ... }).RotToDirremoved - its gate no longer exists in the build; useq.ToDirection()(takes aquat) to turn a rotation into a forward direction.arraydeclaration keyword removed - declare container variables withvarinstead:var scores: int[](wasarray scores: int[]). Storage and behavior is identical - only the keyword changed - so the fix is a mechanical rename. Usingarrayas a declaration keyword is now a parse error that points at thevarform.
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/MicrochipOutputrerouter, even when not a declared parameter. Constant arguments still inline.
0.19.0
- Constant expressions in
var/arrayinitializers - an initializer may name a top-levelletconstant 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 explicitexec = <trigger>, e.g. an array read in a pure binding:lut.get(i, exec = i + 1).- Fixed
emit output = valuefan-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@nofoldexempt 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)orreturn rwith 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. anytype - anin/let/mod-or-chip-param/-output can now be annotatedany, 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. Avar/static var/array/buffercan’t store one: a variable gate needs one concrete wire type to hold, so an explicitanythere is now a compile error.- String truthiness - a
stringnow coerces toboolwherever a bool is expected (anifcondition, a bool-typedlet/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 viaanystill 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.fontSizeand 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 onarr[i].too. Blendis the math blend gate - An alias forlerp, accepting any math variant (float/int/vector/rotator/quat/color), as dolerp,Easing, andTween. The colour-space gate is nowColorBlend.Opaquehovers 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
.Valueon a multi-output result -a.pop().Valuetyped as the whole record, so every use of it mismatched. - Fixed a type alias not resolving through a namespace import -
import * as Twithmod f() -> MyTypefailed with “unknown type”. Aliases now inline as they do for a named import, andT.MyTypeparses as a qualified type. GetLeaderboardreturnsint- It was typedany, so arithmetic on its result had no operator overload.boolarithmetic with two bools -bool + bool(and- * / %) now promotes toint, matchingbool/intmixes and the bitwise ops;(a && b) + (c && d)compiles.
0.17.0
Opaque(x)builtin +@nofoldannotation -Opaquepasses a value through a rerouter and hides it from constant folding;@nofoldsuppresses 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.wsprints every probed gate interaction to the console on paste;scripts/gen_semantics.mjsturns the dump intodata/gate_semantics.json, andscripts/gen_verifier.mjsgeneratesprobes/verify_semantics.ws, which re-asserts every recorded case in-game. - Fixed a chip output named
x/y/z/r/g/b/areading 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
chipcalled 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
chipno longer costs a gate per instance -F(1)materialized a_Varin 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 equivalentmodemits. - Fixed a tuple-destructured
modparameter binding nothing -mod f((a, b): (int, int))left every name unbound, so the body silently computed on zeros. - Fixed
let (a, b) = ton 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 -
Nsdidn’t travel with the imported declarations calling through it, so everyNs.f(...)silently did nothing at runtime. - Fixed a namespaced call losing its return type -
Ns.f(x)typed asany, soNs.f(x) + 1failed operator resolution (WS004) and dropped the expression. - New tree-sitter grammar -
editors/tree-sitter-wirescript/, with highlight/locals/indent queries. - Docs: dropped
matchexpressions - 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
modandchipalike), 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 separatechip { ... }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
onhandlers bound toChange(x)-Change’sOnChangedoutput is now typedexec(wasany), solet c = Change(x)+on c { ... }fires on the change pulse. thenmay start its own line in an if expression -let x = if condfollowed by indentedthen .../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 ina.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_programsexample - Seeded grammar fuzzer that hunts silent miscompiles: programs with no error diagnostics whose output has_Unsupportedgates, duplicate/fan-in wires, or dangling endpoints. Findings write to a gitignoredfuzz_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
.wsfile clears the previous Compile command’s diagnostics; the next explicit compile repopulates them. - Rename applies to every reference find-references sees - Three
textDocument/renamefixes:- Open files match by canonical path and references are deduplicated, so edits are no longer doubled and rejected.
import { foo }rewrites toimport { 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
Arcinstead of deep-cloned per call, and scope keys ride the interner; mod-heavy programs lower ~14% faster. Newcount_allocsexample reports per-stage allocation counts/bytes.
0.16.1 - 2026-07-15
chip letlabels 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(viaimport * 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_Unsupportedgate. - 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 negatedon !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>completesrecv’s members (records complete their fields, including inonhandlers);Call(<here>still completes params. - LSP: more completion contexts -
import * as uthenu.<here>lists the module’s members.pos.<here>on avar pos: vectoroffers type methods + swizzle (x/y/z,r/g/b/a) alongside.Value/.prev;static vargets.Value/.prev.- Values typed by a
type Foo = { ... }alias completeFoo’s fields. - User
mod/chip/fncalls 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@labeland@closed.- Native LSP and web playground share these paths.
- Doc comments on record-type fields - A
///on a field insidetype T = { ... }now parses (was a parse error) and shows on hover of that field. - Fixed hover on a namespace alias - Hovering
uinimport * as ushowsnamespace uand lists its members (wasnamespace 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 constantarray/varinitializer; 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 twocoloroperands; a scalar broadcasts across channels (tint * 0.5). Same PrimMath gate as vectors/rotations. Randomis polymorphic -min/maxmay bevector,rotator,quat, orcolor; each component rolls independently and the same type is returned (Random(Vec(0,0,0), Vec(1,1,1))→ point in the unit cube). Scalarintform unchanged.- Fixed anonymous-record mod returns - A
modreturning a record literal (return { head: ..., rest: ... }) now destructures into per-field sources, so each field wires to its own value (was one_Unsupportedgate). - Non-root chips compile open by default - Opened planes stack as a wall above the compiled microchip (root at bottom, deeper nesting higher). New
@closedcollapses a chip but keeps its wall slot;open chipis now a no-op. - New
@label("text")annotation - Display-text override for chip labels/headers andin/outport labels (stacks with@sidein any order); the wiring-UI port name is unchanged. - Opened planes render a header - A size-96 title (the
@labeltext, 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 mislabeledexecon hover -returnalone 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 xand a named import no longer ships its top-levelletconstants twice; fully-disconnected pure gates and orphan literals are pruned.
0.14.0 - 2026-07-12
- Port-side rerouter pins -
@left/@right/@top/@bottomon 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 insidechip {}/modbodies error (WS023).
0.13.1 - 2026-07-11
- Fixed
array.pop()returning0- Both gate outputs are now declared:.Valuereads the popped element and.IsEmptyreadsbIsEmpty(true once the array is empty after the pop). - Fixed
bufferinitializers inside chip/mod/handler bodies never wiring - The initializer expression was silently dropped, leaving the buffer’s input dangling. - Silently-dropped
varinitializers now warn (WSP001) - Warns on a non-constant init in pure position, any non-constantstatic varinit, and an exec-context array-var init that isn’t an array literal. Use aletfor 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/.Indexnow keep the call. - Fixed a standalone
chiplosing its exec output - An exec-bearing body ending inreturn <value>now ships the output. - Fixed
out X = Xemitting 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).Foundresolves from the call’s record. - LSP: goto-definition on a namespaced call resolves in the imported file -
u.foowithimport * as uno longer jumps to a same-named local decl. - Chip exec I/O gates are labeled - Exec gates say
exec; the anonymous-> typereturn output saysreturn(synthesized ports had no label). ControllerJoined/ControllerLeftexpose 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-typedinport supports array methods (X.length(),X.push(v), …) and passes to a mod/chip’sT[]parameter.- Namespaced module members resolve inside their own mods -
import * as nsonly; 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
Zoneinput -on ZoneEntered(character, zone = z)wireszinto the event gate’sZoneport, so a wiredinport selects the watched zone. CoversZoneEntered/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_Incrementand 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 wiredMakeVector.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, solet K = 2used asarr.push(K)bakes2into the gate.
Bug Fixes
min/maxand 14 more expression gates embed literals -min,max,sign,round,exp,ln, the hyperbolics,Deg2Rad/Rad2Deg,BitCount, andScaleVecno longer drop literal args likemin(a, 3.0).ScaleVecwires to the real ports -Input/Scalarinstead of the nonexistentInputA/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), orbuffer(delay, hold)inserts theBuffer(Ticks|Seconds)gate a wire-graph cycle needs. Constants bake into the gate; variables wire the duration port. - Payload ferrying -
emit sig = valuestores the value in hidden per-signal vars (one per record field);let x = await sig/let { a, b } = await sigreads it back. Cost: oneVar_Setper field per emit, oneVar_Getper field at the await. - Body-level
let x: execwires correctly -await xon a body-declared signal no longer lowers to a dead placeholder. - Signals are scoped per declaration - Two mods each declaring
let loop: execno 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 nonexistentVarRefport. - 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)
entitycoerces tocharacter/controller- Character/controller receiver methods and typed params accept entity values (e.g.Sweep’sHitEntity), wiring directly with no adapter gate.
Bug Fixes
CharacterDamagedattacker ischaracter-typed - Wasentity, which receiver methods and typed params rejected. The weapon binding staysentity.ShowStatusMessageand 12 more gates - Literal args now persist.- Recursive chip/mod calls error instead of crashing - Now a
WS020error.
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
.methodaccess; builtin call/method hovers requirerecv.methodorname(. 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/Nameisentity(wasany), soweapon == $BRItemBase/Weapon_Pickaxetype-checks instead of erroring (WS004). As a value it materializes into the matching*Referencegate (ItemReference,AudioReference,EntityTypeReference, … by asset type), which outputs the asset as an entity wire. DisplayTextgained aneasingparam - The interpolation curve fortransition("Linear"/"EaseIn"/"EaseOut"/"EaseInOut"), a property-only enum likejustify.
0.10.0 - 2026-07-07
Bug Fixes
characterandcontrollerwire directly - No moreGetFromEntityadapter, 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 -
FormatTexthas only 7 substitution inputs; templates with more${...}values split across chained gates. on <local exec signal>fires across handlers -emit sigin one handler triggerson sigin 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. FindPlayeris an exec gate returningcharacter- Has Exec/ExecOut ports and emits the found player’s character; was mis-declared pure returningentity.
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.brzis a clickable link / go-to-definition target (Ctrl/Cmd-click or F12). - Missing prefab files warn - The LSP flags a
$./file.brzthat isn’t on disk or lacks the.brzextension. - Playground uploads
.brzprefabs - 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
.brzintoSpawnPrefab-$./file.brz(relative) /$/abs.brz(absolute) embeds the archive content-addressed (brdb 0.7add_prefab) and sets the gate’sPrefabpath..brzrequired (WS019); resolution pluggable viaEmitOptions::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
SpawnPrefabgained avelocityparam - The gate’sSpawnVelocityinput.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 anexecresult field:await r.exec/on r.exec { }.- Import dependency pulling fixed - Imports pull same-file deps in record/array literals,
emitvalues,awaitexprs, and buffer inits; type aliases inline into importedlet/var/out/buffer/inannotations, not just chip/mod params. - WS013 understands
emit- The unassigned-output check countsemit 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-levelarrayinitializers and runtimefoo = [...]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: falseso the LSP doesn’t register a duplicate. - Prefab path completion -
$./(or$/) completes.brzrefs: the native LSP scans the document’s directory; the wasm playground offers dragged-in files via a new optionalprefabs_jsonregistry.
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 globalBroadcastChatMessage(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() -> stringattach 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 toEdge: 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(...), andAddInventoryItemAdv/SetInventoryItemAdvwith overrides (damage,speed,scale,itemName,projectile). Asset args are$Type/Namereferences.
New Events
CharacterDamaged(character, damage, attacker, attackerWeapon, attackerWeaponName)- A character took damage.EntityZoneEntered/EntityZoneLeft(entity) andProjectileZoneEntered/ProjectileZoneLeft(character,projectile,weapon,weaponName) - Zone events beyond characters; the projectile events’characteris the shooter.
Compiler / Output
- Generic asset-field emission - Gates with a
class/objectdata 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
_maxschema (286 structs) andcomponent_db.rs(296 type mappings).assets/external.rskept the previous full catalog (the dump referenced only 14 assets). - Deliberately not exposed as builtins - The
*Referencegates ($Type/Namecovers them),Convert/ColorConvert(implicit coercions cover them), andAddInventoryEntry(opaque nested struct;GiveWeaponcovers it).
0.7.0 - 2026-07-05
Language Features
- Scalar var type inference -
var foo = ""is a string var,var n = 0an int var,var f = 1.5a float var (also bools, negatives, interpolated strings). A non-literal initializer refines from its expression (var v = Vec(1.0, 2.0, 3.0)isvector), 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 = 5is 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)]isvector[]). Color()returnscolor- Wasany; matchesColorSRGB/ColorHex/Blend.
Constant Folding
Vec/Rotation/Coloron 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 aMake*gate materialized, never silently zeroed. - Vars and arrays of every wire variant -
rotator(zero),quat(identity), andcolor(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
brdb0.6.3 - The wire variant gainedRotator/Quat/LinearColormembers plus matching typed array variants; onlyWireGraphEnumWrapperremains unmapped.
0.6.0 - 2026-06-30
- Data regenerated - Gate inventory (285 -> 288: new
Convert/FindPlayergates), the brdb component_maxschema (258 structs), andcomponent_db.rs. - Sweep upgrade - The raycast
Sweep(...)gate gained optional per-channel flags:detectBricks,detectPlayers1–detectPlayers4,detectPhysics,detectMap, andignoreOwningGrid.
0.5.0 - 2026-06-29
Language Features
quattype + rotation/quaternion builtins - Aquatprimitive (distinct from the eulerrotator) plusdir.ToRotation(),q.ToDirection(),v.Rotate(q),q.Invert(),from.RotationTo(to),a.AngleTo(b),a.Slerp(b, alpha),axis.RotationByAngle(angle),q.ToAxisAngle(), andRotation(p, y, r)/r.ToEuler()for the euler rotator.- sRGB / hex color builtins -
ColorSRGB(r, g, b, a)andColorHex("#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/fnnamed 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 nestedEntryPlan). 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
.brdboutput.
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]infersint[]and lowers to the same array gate as anarraydeclaration; 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::arraystable: completion offers the full set on any array-typed value, return types come from gate output ports.findreturns{ Index, Found, Value }(auto-unwraps to Index);pop/min/maxexpose.IsEmpty;insert/swap/sliceexpose.OutOfBounds. GetAimreplacesAimOrigin/AimDirection- A character’s camera/aim is one gate returningchar.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 fillCommandNamethenHelpTextin order (or namedDescription = "..."), 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 orletbinding via the SplitVector/SplitColor gates, not just an inlineVec(...). Vec(...)literal arguments - Constant components are no longer dropped to0at emit;MakeVectorgained 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’sFontSize/Lifetimenow resolve to the game defaults (16/5) instead of0.
0.3.0 - 2026-06-27
Language Features
- String variables -
var/static varof typestringstore in a Variable gate (the WireGraphVariant gained astrmember). TheWS018“strings can’t be stored in vars” diagnostic is gone. - Native string equality -
==/!=on strings lower directly to theCompareEqual/CompareNotEqualgates. Thecontains(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 sameMathAdd/Subtract/Multiply/Divide/Modulogates. TheScalehelper still works. - Any-variant variables - A
varcan 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
WireGraphArrayVariantmember (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, stringParseInt/ParseNumber, reference gates) and types 86 previously-anyports. 175 -> 260 entries. - Refreshed brdb component tables -
component_db.rsregenerated from the same dump so the new gates emit; the removedGamemode_EndRoundgate is gone.
New Builtins and Methods
- Array methods -
insert,find,sort(desc?),reverse,sum,min,max,average,swap,fill,resize,append,copyFrom,slice,fillFromPlayers,fillFromTeamjoinpush/pop/length/remove/clear/shuffle. Every ArrayVar gate is reachable. - Easing -
Easing(a, b, blend, fn?, dir?)andTween(target, duration, fn?, dir?); function/direction pass as an int or enum-name literal ("Quad","InOut", …) resolved against the engine’sEBREasingFunction/EBREasingDirectionenums. - Timer -
Timer(limit, restart?, pause?, resume?)returns{ Time: float, Expired: exec }; the controls are optional exec inputs andExpiredworks withon/await. - String parsing -
ParseInt(s) -> intandParseNumber(s) -> float(alsos.ParseInt()/s.ParseNumber()). - Controller -
GetUserName,GetUserId,GetDisplayName,IsTrusted,HasPermission,SetCanRespawn,SetTeamPinned. - Character -
GetDamage,SetDamage,IncDamage,SetTempPermission. - Entity -
SetFrozen. - Gamemode -
PlayerWins/TeamWins(replace the removed imperativeEndRoundgate 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.jsonwith brick bounds from the microchip shell) instead of a world, so the.brzpastes 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 namefires them from any handler;await nameoron namelistens.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). Enablesawait 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 returningbrdb::Worldfor.brdboutput.- CLI
.brdbsupport -just compile file.ws -o file.brdbemits SQLite saves. - Compile progress - LSP sends
wirescript/compileProgressnotifications; VS Code extension shows step counter in status bar. **(pow) fix - Now wires toInput/Exponentports instead ofInputA/InputB.- BRZ double-write fix - Fixed
to_brz_vecwriting the archive twice (exact 2x file size).
Editor / IDE
- Inlay type hints - Ctrl+Alt shows inferred types for
let/bufferbindings. 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.cpsrand nested field access now show types correctly. onhandler hover - Shows gate estimate for the handler scope.ifhover 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}. awaitkeyword highlighting - Added to VS Code tmLanguage and Monaco monarch tokenizer.
Playground
- Inlay hints provider -
wirescript_inlay_hintsWASM binding + MonacoInlayHintsProvider. Hidden by default, shown on Ctrl+Alt. - New
async_signals.wsexample - Demonstrates emit-value, await, local exec signals, Sleep.
Documentation
- Updated
statements.mdwith emit-value, local exec signals, await, Sleep/SleepTicks. - Updated
exec-context.mdwith await section,_placeholder, Sleep examples. - Updated
builtins.mdwith Sleep/Delay section. - Updated
expressions.mdwith operator coercion for all wire variant types. - Updated
types.mdwith exec->bool coercion. - Removed
fnkeyword references from docs. just compile-brdbrecipe 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 exprsyntax - Trigger handlers on arbitrary exec expressions, not just named events- Exportable vars/buffers/arrays -
var,buffer, andarraydeclarations are now importable across files - String
varerror (WS018) -var s: stringnow 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: *intfor record fields and type declaration fields - Spread type validation - Extra fields from spread are caught with errors pointing at the
...exprspan - Chip/mod context hover - Hovering
chip/modkeywords 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
typekeyword highlighting - VS Code extension highlights user-defined type names
Playground
- Docs panel refactor - Docs fetched from
docs/*.mdinstead of inline JS (~1900 lines removed fromdocs.js) - Examples loaded from files - Playground examples loaded from
sdk/examples/*.wsvia fetch instead of hardcoded JS - New
records.wsexample - Demonstrates records, destructuring, spread, and tuples
Bug Fixes
- Fix branch scoping - variables declared in
if/elsebranches no longer leak across branches - Fix string comparison gate using wrong variant
- Fix inline modules adding extra microchip outputs
- Fix
return exprin pure mods - Fix
on var.valuenot lowering handler body - Fix emit not chaining union gates for multiple
emitpaths - Fix import not pulling in same-file dependencies of imported declarations
- Fix array index access requiring exec context
- Fix array
.length()/.pop()returningAnytype - 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 = 0return expr- Return values from chips and mods- Single-output auto-unwrap -
chip Foo() -> (result: int)returnsintdirectly instead of{result: int} - Block expressions -
{ stmts; expr }as expressions - Compound assignment -
+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>= ^^logical XOR operator -a ^^ bis true when exactly one operand is truelettype annotations -let x: int = expr- Array params are always pass-by-reference -
mod init(arr: int[])passes the array by reference without needing* fndeprecation -fndeclarations emit a warning (WS015) suggestingletinstead
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
ifkeywords - 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-checkCLI - Standalone type checker binary- VS Code extension auto-reload - Extension reloads when the LSP binary changes
Removals
ArrayReftype removed - arrays are always references, useint[]everywhereeventkeyword removed (was already deprecated)