# Wax Agent Language Reference Compact syntax reference designed to put the Wax language surface into a coding agent's context. It gives one example per feature, with no compiler architecture or implementation deep-dives. For full semantics, design rationale, or edge cases, see the [Wax Language Specification](https://waxlang.dev/spec-source). ## Module skeleton ```wax namespace Game::Player; // optional, must be first stmt; nested with :: import Debug, Error from Wax; import Texture2D, LoadTexture from Gfx::Paint::Textures; import Texture as GfxTexture from Gfx::Resources; // alias import Matrix from @Wax/Math-Tools::Linear; // exact package identity api fn Main() { // host-callable entry point Debug.Log("Hello, Wax!"); } ``` Package projects declare dependency aliases in `wax.json`; `wax install` writes exact versions to `wax.lock` and materializes sources under `Packages/`. An import can use its local alias or an exact scoped identity. Exact package identities are ASCII case-insensitive before `::`; TitleCase is the source style while manifests, lockfiles, URLs, and package commands use lowercase. Package aliases begin with an uppercase ASCII letter, keeping them distinct from Wax keywords. The official registry needs no configuration: use `wax login` once, then `wax publish` to publish a deterministic archive or `wax install` to consume dependencies. Neither npm nor Node.js is required. Registry and token environment overrides remain available for private registries and CI. See the [package quickstart](https://waxlang.dev/docs/packages/quickstart) for the complete workflow and the [package documentation](https://waxlang.dev/docs/packages) for exact options and behavior. `/** ... */` provides plain documentation for the named declaration immediately following it. Indentation and one line break may intervene; a blank line or another comment leaves the block unattached. Optional leading `*` decoration is removed, and `@` words have no special meaning. `/**/` and `/*** ... */` banners remain ordinary block comments; use `/** */` for an empty documentation block. ```wax /** Starts a new game for the selected player. */ api fn StartGame(string player) { } ``` The short snippets below assume the `Debug` and `Error` imports shown in this skeleton. Other ordinary uppercase stdlib types are imported where they are introduced. ## Variables, constants, type inference ```wax int counter = 0; // explicit type var name = "Wax"; // inferred (string) const float Pi = 3.14159f; // compile-time constant ``` ## Built-in types Lowercase type spellings are reserved, import-free built-ins. This includes numeric/core types, vectors, matrices, colors, `quaternion`, `timespan`, `angle`, `timepoint`, and the SIMD families. Every user-declared type must begin with an uppercase letter. Uppercase nominal type names use ordinary lookup and must be declared in the current source or imported; the stdlib type `StringBuilder` therefore requires an import. The uppercase declarations used internally to back lowercase built-ins are compiler implementation details and cannot be imported by application modules. ```wax // Reserved core integers (signed | unsigned) sbyte int8 | byte uint8 // 8-bit short int16 | ushort uint16 // 16-bit int int32 | uint uint32 // 32-bit (default integer literal type) long int64 | ulong uint64 // 64-bit // Floats float float32 // 32-bit double float64 // 64-bit (default fractional literal type) // Other bool char string object void json opaque secret // Reserved algebraic shape keywords float2 float3 float4 // vectors float3x2 float3x3 float4x4 // matrices color8 color16 color32 // colors (8/16/32 bit-per-channel RGBA) quaternion // 3D rotation timespan // duration (nanosecond precision) angle // angular value (radians/degrees aware) timepoint // monotonic clock sample // Wax stdlib nominal type (import required) StringBuilder // mutable string construction ``` ```wax import StringBuilder from Wax; ``` ## Numeric literals ```wax 42 // int 42L 42u 42ul // long, uint, ulong 3.14 // double 3.14f // float 1.23e4 // scientific 0xDE_AD_BE_EF // hex with underscores 0b0101_0101 // binary 1_000_000 // underscore separators ``` ## String / char literals ```wax 'A' // char (UTF-32 scalar) 'Ω' '\u{1F600}' '\U0001D11E' // unicode escapes "hello" // string (UTF-8) "User: $name" // simple interpolation "Sum: ${a + b}" // expression interpolation """multiline raw string""" // multiline / raw ``` ## Operators | Category | Operators | |---|---| | Arithmetic | `+ - * / %` | | Comparison | `== != < > <= >=` | | Logical | `&& \|\| !` | | Bitwise | `& \| ^ ~ << >>` | | Null-conditional | `?.` (chain), `??` (coalesce), `!` (force-unwrap, panics on null) | | Type | `is`, `as`, `as?`, `as!` | | Assignment | `= += -= *= /= %= &= \|= ^= <<= >>= ??=` | Shift counts are masked to the operand width (`n & 31` for 32-bit, `n & 63` for 64-bit), so `<<`/`>>` are total for any count on every supported target. Numeric `as` conversions never trap. Integer narrowing keeps the destination width's low bits. Floating-point to integer conversion truncates toward zero, then saturates to the destination range; NaN becomes zero. ```wax string? maybe = GetName(); int32 len = maybe?.length ?? 0; // null-conditional + coalesce string forced = maybe!; // panics if null if (obj is Animal) { ... } // type test Animal? a = obj as? Animal; // safe cast (`as?` → null on miss, so nullable) Animal a = obj as! Animal; // force cast (panics on fail) ``` ## Null safety ```wax string nonNull = "x"; // cannot be null string? nullable = null; // explicitly nullable if (nullable != null) { Debug.Log(nullable.length); // direct local checks refine reads on this path } // Safe-unwrap with if if (nullable) |v| { Debug.Log(v); } // Multi-condition if: the comma list is `&&` (short-circuit). Payload slots map // positionally; a plain bool gates without a slot, and `_` discards a slot. if (nullable, other) |v, w| { Debug.Log(v); } // both must be non-null if (nullable, flag) |v| { Debug.Log(v); } // `flag` just gates // Conditional unwrap with ternary string s = nullable ? |v| v : "default"; // Default value string s = nullable ?? "default"; // Capture-or-bail with `??`: the RHS may instead be a diverging term // (`throw` / `panic` / `unreachable`). The result is the LHS non-null type, // and the null branch bails instead of yielding a value. string got = nullable ?? throw new Error("missing"); // in a `throws` fn string got = nullable ?? panic("missing"); // `guard` unwraps-or-bails and the binding escapes FLAT into the enclosing // scope (live after the statement). The `else` runs on failure and must diverge. guard (nullable) |v| else return; Debug.Log(v); // `v` is non-null here guard (nullable, other) |v, w| else { return; } // multi-bind, both escape guard (hp > 10, target) |_, t| else return; // `_` gates a bool; `t` binds guard (hp > 10) else return; // boolean-only precondition ``` Full nullable rules (chaining, propagation, force-unwrap panic semantics): [Wax Language Specification § Null Safety](https://waxlang.dev/spec-source#primitives-and-literals). ## Control flow ```wax // if / else if (x > 10) { ... } else if (x > 0) { ... } else { ... } // if with payload unwrap if (GetUser()) |user| { Debug.Log(user.name); } // switch — every case body is a block switch (value) { case 1: { ... } case 2, 3: { ... } // multi-value case default: { ... } } // ternary int abs = x < 0 ? -x : x; int port = cfg.HasPort ? cfg.Port : throw new Error("no port"); // an arm may diverge (throw/panic/unreachable) // loops for (int i = 0; i < 10; i++) { ... } while (cond) { ... } do { ... } while (cond); foreach (items) |item| { ... } foreach (items) |item, index| { ... } foreach (dict.Keys()) |k| { ... } // dictionaries iterate via Keys()/Values() // loop control break; continue; ``` ### Deterministic parallel kernels `parallel` applies one restricted kernel to the half-open index range `[start, end)`. Aligned bindings expose the element at the current logical index; `|index|` is optional. ```wax parallel (0, destination.size) : ( readonly int32 value from source, out int32 result from destination, ) |index| { result = value * scale + index; } ``` Bindings are `readonly`, `ref`, or `out` and may come from arrays, `List`, `Span`, or `ReadOnlySpan` when the requested access is legal. Bounds, resource lengths, and every writable overlap are checked before any iteration runs. A rejected preflight therefore leaves all resources unchanged. The body cannot allocate, access statics or host/API state, perform dynamic dispatch, nest `parallel`, or publish reference-bearing output. Captures are read-only data values. These restrictions let every logical index produce the same result regardless of worker count, chunk size, or completion order. Execution is synchronous: all iterations join before the following statement. The native C runtime may use persistent workers for admitted kernels; Wasm and instrumented/profiled native builds use the same kernel serially. Setting `WAX_THREAD_COUNT` to `0` or `1` forces the serial oracle. Positive `WAX_PARALLEL_CHUNK_SIZE` and `WAX_PARALLEL_MIN_SIZE` values override native chunk size and the minimum range size used for worker dispatch. A `continue` at the top level of the body ends only the current logical invocation and copies assigned `ref`/`out` values back first. `out` definite assignment is enforced on that path. `break` and enclosing-function `return` cannot leave the kernel. ## Functions ```wax fn Add(int a, int b) : int { return a + b; } fn Greet(string name) { ... } // void return implicit // Default params fn Configure(string id, int timeout = 5000, bool retry = true) { ... } // Named args Configure(.id = "x", .retry = false, .timeout = 100); // ref / out fn Swap(ref int a, ref int b) { var t = a; a = b; b = t; } fn TryParse(string s, out int result) : bool { ... } // ref returns fn Slot(int[] scores, int i) : ref int { return ref scores[i]; } ``` **No overloading.** A name resolves to exactly one declaration. ## Lambdas / closures / function types ```wax // Function type fn<(int, int) : int> add = (a, b) => a + b; fn<() : void> log = () => Debug.Log("hi"); // Closure (captures) int captured = 42; fn<() : void> f = () => Debug.Log("$captured"); // Trailing closure (a required fn param may come last, after defaulted ones) Retry(3) |attempt| { Debug.Log("attempt $attempt"); }; // Named function reference fn<(int) : int> doubler = MyDouble; ``` ## Classes ```wax class Player { public string name; private int score = 0; public readonly int id; // can be set in ctor only constructor(string name, int id) { this.name = name; this.id = id; } public fn AddScore(int n) { score += n; } // Property public get Score() : int { return score; } public set Score(int value) { score = value; } } // Named constructor class User { public string name; public bool isGuest; constructor(string name) { this.name = name; this.isGuest = false; } constructor Guest() { this.name = "anonymous"; this.isGuest = true; } } fn MakeGuest() : User { return new User.Guest(); } ``` ## Inheritance ```wax class Animal { public virtual fn MakeSound() : string { return "..."; } } class Dog extends Animal { public override fn MakeSound() : string { return "Woof"; } public sealed override fn ... { ... } // prevent further override } abstract class Shape { public abstract fn Area() : float; } // Constructor chaining (no overloading, so a chaining ctor must be named) class Sub extends Base { constructor(int x) : base(x) { ... } constructor Zero() : this(0) { ... } // chain to other ctor } ``` Modifiers: `public protected internal private`, `static`, `virtual override sealed abstract`, `readonly`. Construction safety: every field must be definitely assigned before the constructor exits. Inside a constructor, calling a virtual/abstract/override member on `this` or letting `this` escape (argument, store, return, capture) is an error — a derived class's fields aren't assigned yet. Non-virtual helpers are fine when they don't do either transitively; a helper's field assignments count toward definite assignment (`constructor() { Init(); } fn Init() { this.name = "x"; }` works). `sealed` classes (and structs) may do anything with `this` once all fields are assigned. ## Structs (value types) ```wax struct Range { int start; int end; constructor(int start, int end) { this.start = start; this.end = end; } fn Length() : int { return end - start; } } ``` Structs are copied by value, allocated on the stack. No inheritance. Use `sizeof(T)`. ## Compact field headers ```wax struct Point(float x, float y); // two public fields + a constructor class User(string name, int32 id = 0); // an entry default is a ctor default struct Pair(T first, T second) where T is struct; struct Vec(float x, float y) { // a body may follow instead of `;` fn LengthSquared() : float { return x * x + y * y; } } class Sprite(int32 layer) implements IDrawable { // interfaces are allowed public fn Draw() { ... } public get Layer() : int { return layer; } } class Enemy(int32 health) extends Actor; // extends is allowed too Point p = new Point(1f, 2f); Point q = new Point(.y = 2f, .x = 1f); User guest = new User("anonymous"); ``` Shorthand for fields plus construction, on `struct` and `class` only. Header order is field-layout order and positional-argument order; entry names are the field names and the named-argument names. Every generated field is public and writable, so a body field or a second unnamed constructor that collides with the header is a duplicate. `ref`, `out`, `scoped`, and visibility modifiers are refused on an entry. A compact class may implement interfaces and extend a base; the generated constructor takes the implicit `: base()`, so a base whose constructor requires arguments is the usual missing-base-call error. `is` is refused — a constraint declaration has no fields. ## Interfaces ```wax interface IDrawable { fn Draw(); get Layer() : int; // property in interface } class Sprite implements IDrawable { public fn Draw() { ... } public get Layer() : int { return 0; } } // Explicit implementation (resolves conflicts) class Multi implements IA, IB { fn IA.Method() { ... } fn IB.Method() { ... } } ``` ## Enums ```wax enum Status { case Idle; case Running; case Done; } enum Priority extends int16 { // explicit backing type via `extends` case Low = 0; case Medium = 10; case High = 20; } // Flags (powers of 2) flags Permission { case Read; case Write; case Execute; } var p = Permission.Read | Permission.Write; if ((p & Permission.Write) == Permission.Write) { ... } // membership: bitwise & // Enum with members enum Severity { case Low; case Medium; case High; public fn Multiplier() : float { switch (this) { case Low: { return 0.5f; } case Medium: { return 1.0f; } case High: { return 2.0f; } } return 0.0f; } } ``` ## Generics ```wax import Comparable from Wax; class Box { T value; public fn Get() : T { return value; } } fn Identity(T item) : T { return item; } // Where clauses fn Max(T a, T b) : T where T is Comparable { ... } // Constraints constraint Newable is class { constructor(); } constraint Numeric is struct { static fn Zero() : this; } // `this` = the implementing type fn Sum(T[] items) : T where T is Numeric { ... } // Type arguments are inferred from the arguments, including from lambda bodies: // once a lambda's parameter types are fixed by the other arguments (or the // receiver), its body's type binds whatever the delegate's return type left open. fn Map(T[] items, fn<(T) : U> f) : U[] { ... } var lengths = Map(names, (n) => n.length); // T = string, U = int ``` Constraints provide zero-cost static polymorphism (especially for structs); interfaces provide runtime polymorphism for classes. ## Collections ```wax import List from Wax; import Dictionary from Wax; import Set from Wax; import Stack from Wax; import Queue from Wax; int[] nums = new int[10]; // fixed array int[][] grid = new int[3][4]; // jagged // Heap-array size arithmetic is checked at allocation time; ordinary int math still wraps. List list = new List(); list.Add(1); list[0] = 99; Dictionary map = new Dictionary(); map["key"] = 42; Set tags = new Set(); Stack stk = new Stack(); Queue q = new Queue(); ``` ## `lateinit` arrays `new T[n]` is illegal for a non-null element `T` — it would leave null holes. A `lateinit T[]` is the checked, fill-slot-by-slot alternative: reads panic on an unassigned slot, slots never un-assign once written (monotone), and viewing it as dense values first proves the region is hole-free. ```wax lateinit string[] names = new lateinit string[3]; // 3 unassigned slots names[0] = "ann"; // checked write bool has0 = names.IsAssigned(0); // per-slot test (no panic) names.Fill("?"); // fills every slot + proves dense string[] dense = names.AsArray(); // whole-array verify -> dense view (no copy) var win = names.Slice(0, 2); // window verify -> Span (readable) var w = names.ToWriteOnlySpan(); // WriteOnlySpan; needs no proof lateinit string[] wide = dense; // dense T[] widens implicitly ``` Full rules: [`ArrayNullSafety.md`](https://waxlang.dev/agent-reference). ## Indexers ```wax class Grid { public get this(int x, int y) : Cell { ... } public set this(int x, int y, Cell c) { ... } } var c = grid[1, 2]; grid[1, 2] = new Cell(); ``` ## Spans ```wax import Span, ReadOnlySpan from Wax; scoped Span view = arr.ToSpan(); // mutable borrowed view scoped ReadOnlySpan ro = bytes.ToReadOnlySpan(); // immutable borrowed view scoped Span slice = arr.Slice(0, 10); // bounds-checked sub-view ``` An array converts to a span implicitly only in argument position; elsewhere ask for the view explicitly. All spans are scoped borrows and cannot be stored in heap objects, arrays, statics, or closures. Indexing and slicing are bounds-checked relative to the current view. `Span` writes through to its backing storage; `ReadOnlySpan` exposes no mutating operations. Indexing a read-only span returns its element by value. Members of a struct element cannot be modified in place; copy the element to a local first. A class element still refers to the same object, so its fields may be modified, but the span slot itself cannot be assigned or passed by `ref`/`out`. `StringSlice` is the corresponding scoped, immutable text view. It is backed by `ReadOnlySpan` over UTF-8 bytes. It is distinct from `Span`, whose elements are 32-bit Unicode scalar values. String and `StringSlice` slicing is byte-indexed but must preserve UTF-8 scalar boundaries. Strings always contain valid UTF-8. Use `string.FromUtf8(bytes)` for checked construction, `string.FromUtf8Lossy(bytes)` when replacement with U+FFFD is appropriate, and `value.ToReadOnlySpan()` to inspect the encoded bytes. `stackalloc` creates a scoped `Span` backed by scratch stack memory. A bare sized stackalloc is zero-initialized, so it is valid only when zero is a valid `T` value (or the size is the compile-time constant `0`). Ref-bearing elements must be filled by an initializer list or generator; `lateinit` is not supported for stackalloc. ```wax import Span from Wax; var tmp = new stackalloc int32[128]; // all zeros var none = new stackalloc Foo[] {}; // empty dense span var pair = new stackalloc string[] { "x", "y" }; // dense refs var objs = new stackalloc Foo[10] |i| { yield new Foo(i); }; // one yield per slot ``` Stackalloc spans are scoped values: pass them to callees, but do not return them or store them where they can outlive the current scope. ## SIMD (128-bit vectors) Width-explicit 128-bit value types for data-parallel kernels. Map to native SSE/NEON and to wasm `v128`, bit-identical. Lane types: `int8x16`/`uint8x16`, `int16x8`/`uint16x8`, `int32x4`/`uint32x4`, `int64x2`/`uint64x2`, `float32x4`, `float64x2`. (A distinct family from the `float2/3/4` geometry vectors.) ```wax int32x4 a = int32x4.Splat(7); // broadcast scalar to all lanes int32x4 b = new int32x4(10, 20, 30, 40); // one arg per lane int32x4 z = int32x4.Zero(); int32x4 s = a + b; // element-wise (+ - * & | ^ << >>, / on floats) int32x4 m = a.Max(b); // Min/Max/Abs/Sqrt/AddSat/... int32x4 mask = a.Gt(b); // compares -> integer mask int32x4 bln = mask.Select(a, b); // mask ? a : b int32 x = a.Lane(0); // const-index lane read int32 y = a[i]; // runtime-index read (bounds-checked) a[i] = 99; // runtime-index write (bounds-checked) int32 total = a.ReduceSum(); // horizontal reduce (ReduceSum/Min/Max) float32x4 v = float32x4.LoadElems(data, i); // typed load from a Span v.StoreElems(data, i); ``` Vectors are value types (never heap/GC). ## Iterators (pipeline) ```wax import List from Wax; List evenSquares = items ->Filter(|x| => x % 2 == 0) ->Map(|x| => x * x) ->Take(10) ->ToList(); // Existing function/delegate references are also accepted: // items->Filter(IsEven)->Map(Square)->ToList() // items->Any(IsEven) // items->With(0)->Fold(Add) // items->With(CompareItems)->Sorted() // items->With(CompareItems)->SortedDescending() // items->With(CompareItems)->Min() // items->With(CompareKeys)->SortedBy(ProjectKey) // items->MinBy(ProjectKey) // items->With(CompareKeys)->MinBy(ProjectKey) // items->SumBy(ProjectValue) // a->With(b)->Zip(AddPair)->ToList() // Prefer payload syntax over inline lambda arguments: |x| => ... does not allocate. // Most element payloads can bind the current stream index, plus source/provenance index: |x, i, source_i| => ... // Sources: new Range(start, end), new Range(start, end).Step(step), // arrays, spans, collections, ToSpan(), ToReadOnlySpan(), // opaque public instance non-generic non-throwing non-state MoveNext()/Current() sources // Transformers: Filter, Map, FilterMap, NotNull, FlatMap, Flatten, Enumerate, Take, Skip, // TakeLast, SkipLast, TakeWhile, SkipWhile, Distinct, DistinctBy, Reverse, Scan, Window, Chunk, // GroupBy, Repeat, PadTo, Inspect, With, Zip, Chain // Sinks: Sum, SumBy, Average, AverageBy, AverageOrElse, AverageByOrElse, Count, Min, Max, MinOrElse, MaxOrElse, // MinBy, MaxBy, MinByOrElse, MaxByOrElse, Fold, Reduce, ReduceOrElse, // First, Last, FirstOrElse, LastOrElse, Singular, SingularOrElse, Any, Empty, All, None, AtLeast, AtMost, Exactly, // Contains, FindIndex, FindLastIndex, IndexOf, LastIndexOf, ToList, ToListWithCapacity, ToArray, AppendToList, // ToSet, AppendToSet, ToDictionary, AppendToDictionary, // Sorted, SortedDescending, SortedBy, SortedDescendingBy, // SortedStable, SortedStableDescending, SortedStableBy, SortedStableDescendingBy, Partition, Join, ForEach // Sorted* is deterministic but NOT stable; SortedStable* keeps equal elements in source order. // Count semantics: Take/TakeLast/Repeat with n <= 0 yield empty; Skip/SkipLast/PadTo with n <= 0 are no-ops; // Window/Chunk and Range.Step require positive counts/steps. // ToArray is direct and requires an exact-size pipeline; use ToList after filters. // FindIndex/FindLastIndex are predicate searches; IndexOf/LastIndexOf are value searches. // Opaque MoveNext()/Current() sources are single-pass and unknown-size; use ToList before buffering/indexed/known-size operations. int32 index = items->Filter(|x| => x.ready)->IndexOf(target); // index in the filtered stream ``` ## Error handling ```wax error FileError { public string path; constructor(string path) : base("cannot read $path") { this.path = path; } } fn ReadConfig() : string throws FileError { ... } // one error type fn Fetch() : string throws NetworkError | TimeoutError { ... } // or several, `|`-separated fn Risky() throws error { ... } // `error` = any, same as bare `throws` // try / catch as expression with yield — typed catches narrow, a bare `catch` closes the set string result = try ReadConfig() catch FileError |e| { yield e.path; } catch |e| { yield ""; }; // Implicit propagation (no catch → caller's throws clause) fn Outer() : string throws FileError { return try ReadConfig(); // propagates FileError } // Rethrow / translate fn Load() : string throws ConfigError { return try ReadConfig() catch |e| { throw new ConfigError("bad config"); }; } // Panics: uncatchable, terminate the app — a statement, not a throw, so no `throws` clause panic("invalid program state"); ``` Full try/catch semantics (typed catches, exhaustiveness, yield, propagation, inheritance interaction): [Wax Language Specification § Error Handling](https://waxlang.dev/spec-source#error-handling). ## json type ```wax json data = { name: "Alice", age: 30, tags: ["admin", "user"], }; string n = data.name as! string; // crash if not a string int32 a = data.age as! int; // crashing cast -- crash if not an int int32? b = data.age as? int; // nullable cast string firstTag = data.tags[0]!; // A missing path reads as null string? city = data.address.city; // Spread json merged = { ...defaults, ...overrides }; // Iteration — arrays iterate directly; objects go through GetKeys()/GetValues() foreach (data.tags) |tag| { ... } // one binding: walks a json ARRAY foreach (data) |key, value| { ... } // two bindings: walks a json OBJECT's // properties in insertion order // (string key, json value); panics on // a non-object, naming the kind ``` ## secret type A `secret` holds text the user is typing that must never reach a recording or snapshot — a password mid-entry. The live value is real (UTF-32 scalars, one per element); the protection is about artifacts: snapshots and recordings carry zeros in exactly its content ranges, structure and lengths intact. ```wax import TextData from Wax; import TextIntent from Wax; class LoginForm { public secret password; constructor() { password = new secret(); } } fn ReadInput(LoginForm form) : void { foreach (TextData.events) |t| { if (t.intent == TextIntent.Insert || t.intent == TextIntent.InsertFromPaste) { form.password.AppendText(t); } if (t.intent == TextIntent.DeleteBackward) { if (form.password.Count > 0) { form.password.RemoveAt(form.password.Count - 1); } } } } ``` API: `Count`, `Append(uint32)`, `AppendRun(scoped ReadOnlySpan)`, `AppendText(TextEvent)`, `Insert`, `RemoveAt`, `RemoveRange`, `Clear`, `At(int32) : uint32`, `Reveal() : string`. String interpolation and `ToString` render the fixed ``; `==`, hashing, map/set keys, and ordering are refused (no `Equals`/`GetHashCode`/`CompareTo`). `secret[]`, `secret?`, generic instantiation, fields, and locals are ordinary. Reading content out — `At`, `Reveal` — is legal, and it is the one deliberate determinism divergence: a replayed run sees zeros where the live run saw content, so values derived from secret content fail replay verification. Derive at the moment of use (hand the password to a `host fn`), not into stored state. A `secret` may cross a required `host fn` parameter or return — exactly `secret`, not `secret?` or `secret[]`. Every other boundary position refuses it by name: `api fn` parameters and returns, because api arguments are taped verbatim into recordings, and host-optional (`host? fn`) imports, because they take the fallback crossing. A direct field of a required channel may also be `secret`; its live value is plaintext, while its recorded body is zero-filled. Nested, nullable, event, and optional-channel positions are refused (WAX0661). The full threat model — what is and is not promised, including the frame- granular timing disclosure — is in `Docs/RecordingFormat.md`. ## Modules and visibility ```wax public // exported from module (the default when no modifier is written) internal // module-private protected // subclasses only private // declaring type only — type-private, not instance-private: inside the // declaring type any instance is reachable (`other.secret`), and all // instantiations of one generic type share the domain. Subclasses do not. // Visibility override — opt in explicitly to non-public members. // At an access site, name the member's visibility to reach it; the keyword is // validated against its real visibility (wrong keyword = compile error), and // it works regardless of which module/type you're in: var h = w.private.health; // read a private member w.private.health = 0; // ...write too var v = b.protected.value; // protected var d = o.internal.node.private.next; // chains through nested non-public state // To import a non-public SYMBOL from another namespace, use the matching // escape hatch (cross-namespace): import private Secret from Other; import protected Helper from Other; // `internal` (module-private) is importable only within the same // module — there is no cross-module internal import. // Top-level decls public fn Helper() { ... } public const int MaxSize = 1024; public static int counter = 0; // app-global mutable state // Static reference types are nullable by default. Non-null exceptions are // single-dim constant arrays, blittable struct values, and class refs whose // initializer is proven safe before normal execution starts. static int32[] kSin = new int32[] { 0, 70, 100, 70, 0 }; static Vec2[] kPts = new Vec2[] { new Vec2 { .x = 1, .y = 2 } }; static Vec2 kOrigin = new Vec2 { .x = 0, .y = 0 }; // not new Vec2(0,0) class StartupConfig { public int32 maxSize; public constructor() { this.maxSize = MaxSize; } } static StartupConfig startup = new StartupConfig(); ``` `host? fn` is top-level only. `api fn` and `host fn` (see Host Communication below) may be top-level or grouped inside a `static class`; static-class boundary functions are namespace grouping only and have no instance receiver. ## State system (UI / state machines) ```wax state fn CounterButton() { state int clicks = 0; // initialized once, persists across calls if (Button("Click")) clicks++; Label("Clicked $clicks times"); } // Lifecycle hooks (create on first entry; inline destroy at the next lifecycle edge) state fn Tab() { state Subscription sub = Subscribe(...); state.destroy => { sub.Cancel(); } } // Persistent across visibility — hoist to a class field, not state. class TabView { List cachedItems = new List(); // survives tab toggles fn<() : void> Draw = state () => { if (cachedItems.size == 0) { cachedItems = FetchItems(); } DrawList(cachedItems); }; } // Stateful loop (explicit `state` modifier required) state foreach (items) |item| key item.id { state int viewCount = 0; viewCount++; } ``` `state` not allowed on class methods. For component-owned state use a stateful lambda field: `fn<() : void> update = state () => { ... };`. Full state semantics (lifetime rules, scope trees, hook ordering, error handling, capture rules, composition patterns): [Wax Language Specification § The State System](https://waxlang.dev/spec-source#the-state-system). ## Host communication ```wax // Host → Wax: declare what host provides host fn Log(string msg); host fn GetPrice(int32 id) : float throws; // failable host? fn LogAnalytics(string ev, json data); // optional host slot; use nullable delegate syntax LogAnalytics?.("heartbeat", {}); // invokes only when the host supplied it static class Console { host fn Write(string msg); // host slot: Console_Write } // Wax → Host: expose Wax fns to host api fn GetVersion() : string { return "1.0"; } api fn DoThing(int32 mode, json config) { ... } static class DebugApi { api fn Ping() : int32 { return 1; } // boundary name: DebugApi_Ping } // `readonly` api fn: a checked transient call, omitted from the recording. readonly api fn GetScore() : int32 { return gScore; } ``` **Boundary types only**: primitives, `string`, `json`, `opaque`, enums, vector types (`float2`/`float3`/`float4`), matrix types (`float3x2`/`float3x3`/`float4x4`), color types (`color8`/`color16`/`color32`), `quaternion`/`timespan`/`angle`/`timepoint`, and arrays of these. No raw pointers, no host callbacks, no async. **Recording & `readonly`**: `api fn` calls are normally recorded for deterministic replay. The compiler automatically omits pure reads. A `readonly api fn` has a stronger checked boundary: it may read pre-call state, allocate and mutate objects created during that invocation, and write explicit `WriteOnlySpan` outputs, but it cannot mutate pre-call objects or let invocation-owned references escape. Its temporary heap state is discarded on return or panic, and the call never enters the recording. Use it for high-volume inspection and derived presentation such as rendering into a host-owned output buffer. ## Query overlays Debugger queries live in a project's `Queries/` source root. Ordinary `waxc` builds and checks never discover or compile them, and a recording carries none of them: `waxdbg` compiles the query files a command supplies — through `--file`, `--query-dir`, or the workspace query library of a bound project — against the recorded application's ordinary source graph, after capture. They are not part of the application image and add no branch or runtime cost to normal execution. ```wax // Queries/Player.wax import GetPlayer from Game; struct PlayerRow { int32 health; bool alive; } query fn PlayerState() : PlayerRow { var player = GetPlayer(); return new PlayerRow { .health = player.health, .alive = player.health > 0 }; } ``` A root RETURNS exactly one value per invocation, and its declared return type is the schema the debugger publishes and renders. The result vocabulary includes numeric scalars, `bool`, `char`, `string`, enums, vectors, matrices, colors, `quaternion`, `angle`, time values, `StableRef`, `T?`, `T[]`, structs, `List`, and the `Wax::Recording` types `Frame`, `FrameRange`, `QueryScanStatus`, and `QueryFailure`. Other collection types have no defined row order and must be projected into a list or array. The declared return type may also be a class, which returns its identity. A root declared `: void` returns nothing and renders as `null`. The `query` modifier is rejected everywhere outside a recognized `Queries/` root. A row carries contents, never identity, so a class-typed field is spelled `StableRef` — the object's cross-frame stable id, which renders as `@` and which `inspect` takes back. A class converts to one implicitly: ```wax // Queries/Monsters.wax import StableRef from Wax; import Entity, GetBoss from Game; struct BossRow { StableRef target; int32 health; } query fn Boss() : BossRow { var boss = GetBoss(); return new BossRow { .target = boss.target, .health = boss.health }; } ``` A nullable class reference converts to `StableRef?` and preserves null. Use `StableRef.FromValue(value)` when the nullable conversion should be explicit. A root may also TAKE a typed argument struct, which the debugger publishes as the root's argument schema and decodes `--args` against before the body runs. Its fields use the return vocabulary with no outer class exception and no result-only collection types. Sequences use `T[]` rather than `List`. An object is named by id here too: ```wax struct BossArgs { int32 minHealth; StableRef of; } query fn Weaker(BossArgs a) : bool { guard (a.of.Resolve()) |entity| else return false; return entity.health < a.minHealth; } ``` `Resolve()` turns an inbound id back into the object, against the frame the cursor is on rather than the frame the id came from — which is what lets one id be passed across a whole scan range. It answers null when that frame's heap does not hold the object (not yet allocated and already collected are the same answer), and it is an INGRESS only: inside a query the whole replay heap is already reachable, so nothing else needs it. Calling it outside a query invocation panics. An id of the wrong CLASS is refused when the arguments are decoded, before the body runs, naming what the id points at and what was declared — a subclass id satisfies a base-class target, and the id's type is read from the id itself, so an id naming an object of the wrong type is refused whether or not that object is still alive. `waxdbg recording query rec.wxs --frame 120 --name Game.Weaker --args '{"minHealth":50,"of":"@2c01"}'`. Enum fields are written by case name, a `T?` field may be omitted, and every other field is required — a mismatch is refused by field name before the query runs, naming an array element by its index (`ids[2]`, `rows[1].depth`). A ref may only name an object the replay already held; taking one of an object the query itself allocated panics, because that id is reissued to an unrelated object once the query's mutations roll back. Roots take no arguments or exactly one argument, which may be `json` or a typed struct. The debugger selects and invokes a root against a replayed frame and reads its result. Full query and CLI workflow: [`Waxdbg.md`](https://waxlang.dev/docs/tools/waxdbg). ### Investigations An investigation groups discoverable debugger work and named cases inside a query overlay: ```wax struct PlayerArgs { int32 id; } investigation Players("Inspect recorded players") { /** Return the selected player's health. */ fn Health(PlayerArgs args) : int32 { return ReadHealth(args); } private fn ReadHealth(PlayerArgs args) : int32 { return args.id; } case "First player" => Health(new PlayerArgs { .id = 1 }); } ``` Public members are runner entries with published argument and result schemas. Private members are helpers. A named `case` is a closed call with arguments authored in source and accepts no runtime overrides. Investigation members are non-generic and non-throwing, take no argument or one typed struct, and use the same result vocabulary as query roots. Investigations exist only in query overlays and cannot be constructed or called by ordinary application code. ## Frame-stream channels Per-frame host → Wax push, recorded and replayable. `host channel` is a top-level declaration carrying only fields; every field is implicitly static, read-only, and one boundary slot. ```wax import ReadOnlySpan from Wax; import Input from Wax; import Key from Wax; host channel Net { int32 threshold; // value channel: a REGISTER, holds until the host changes it int32? limit; // nullable register; ground state is null ReadOnlySpan seeds; // array-payload register, view-typed event int32 inbox; // event channel: this frame's BATCH, empty by default } host channel? Screen { // OPTIONAL: whole block present or absent, per frame float2 size; float scale; } fn Tick() : void { if (Net.threshold > 0) { ... } foreach (Net.inbox) |m| { Handle(m); } // empties again next frame if (Screen) |s| { Layout(s.size); } // optional channels read through a capture if (Input.Pressed(Key.Space)) { Jump(); } // stdlib input accumulator } ``` Everything the host stages during frame N appears at the start of frame N+1, all at once — nothing staged inside a frame is visible in it. A value channel is a register (last value wins, persists); an `event T name;` field is a per-frame batch (empty unless the host pushed) and reads as `ReadOnlySpan`. Event and array-register reads mint a fresh view over the frame's storage: iterate, index, copy with `->ToList()`, pass as an argument — but never store, return, or capture one. The reverse direction is an `api channel`: Wax writes register fields during an active frame and the host reads owned copies while the app is quiescent. Values hold until overwritten; `FrameBegin` clears only the `GetChanges()` write bits. A bit reports any store made during that frame. Reads during a frame, during Wax execution, or from another thread are refused. Stores add nothing to the recording because replay re-executes them into ordinary heap state. Registers begin at a logical all-zero value, with string leaves initialized to real empty strings; enums must admit raw zero, and aggregate field initializers are refused. A `List` register or `event T` batch is the app's own list whose elements the host copies out, and a `json` register is a document the frame's close stringifies for the host; neither carries a change bit. Outbound channels refuse optional, nullable, `secret`, span, and bare-array shapes; use a `readonly api fn` with `WriteOnlySpan` for bulk pull output. Mutating methods cannot be called on a value register, and references into one cannot escape; assign its fields or the complete register so each change has an exact store site. A `List` register and an `event T` batch are the outbound mirrors of the inbound span register and batch. Both are the app's own list -- append, clear, replace, alias -- and the host reads the elements and their count. The register persists; the batch gets a fresh empty list at every `FrameBegin`, so a reference taken last frame keeps last frame's contents. Neither has a `GetChanges()` bit: the count is the answer. ```wax import List from Wax; struct Sample { public int32 id; public string label; } api channel Telemetry { int32 spawned; float fps; string status; List samples; event Sample readings; } fn Tick() : void { Telemetry.spawned++; Telemetry.fps = 60.0f; Telemetry.status = "ready"; Telemetry.samples.Add(Telemetry.spawned); Telemetry.readings.Add(new Sample { .id = 7, .label = "one" }); } ``` A channel is a singleton: no `new`, no user-declared methods, no constructor, no type parameters, no base type, and never a value. It is also top-level only because its slots are global. The direction is required: it names the side that fills the block, the way `host fn` and `api fn` name the side that implements them, so there is no bare `channel`. Neither direction permits assigning the channel object itself, passing it `ref`/`out`, giving a field a declaration-site initializer, or reading it from a static initializer. The compiler adds `GetChanges()`, whose returned value has one bool per value register; a declaration with no value register has no `GetChanges()`. For a `host channel`, Wax cannot write a field. Value change bits report canonical byte changes and event bits report a non-empty batch. Registers may carry admitted value aggregates with immutable `string` and frozen `json` members; an in-place json edit rooted directly at an inbound register is a compile-time error and `Clone()` gives a mutable copy. No aggregate may contain an array; use a sibling `ReadOnlySpan` register. Every host channel gets a stable inbound subscription, while only reachable reads create its heap object and per-frame materialization. Registers that are neither nullable nor optional need a bind-time initial value through generated `_SetChannelInitialValues`, so hostless `waxc --oneshot` / `--standalone` builds refuse a declaration containing one. An optional host channel needs none and observes absent; in exchange its registers must be ref-free, with no event or nullable field. Stdlib channels: `InputData.events`, the raw key/mouse/window batch (plus `Input.Down`/`Pressed`/`Released`/`MousePosition`/`Modifiers`); `TextData.events`, text entry as editing intents whose ranges index the sibling per-frame `TextData.text` read-only span; the optional `FrameInfo` (`size`, `scale`, `focused`); and the optional `TextInputState` — the host-owned text-input mode, requested with `TextInput.Begin()`/`End()` and answered by the host on the block. Full surface, the `Key` code space, and the `TextIntent` vocabulary: [`FrameStream.md`](https://waxlang.dev/agent-reference#frame-stream-channels). ## Cross-references Full semantics for every feature: [Wax Language Specification](https://waxlang.dev/spec-source). Grammar: [`wax-grammar.ebnf`](https://waxlang.dev/spec).