SCUA

Manual

Changelog

What changed in each release of SCUA that you, as a script author or a host embedder, are likely to care about. Bug fixes and internal plumbing that don't change how you write or run code are left out.

Versions follow MAJOR.MINOR.PATCH. While SCUA is pre-1.0, minor releases may include breaking changes; those are called out explicitly.

#0.13.0 — 2026-07-13

#Added

  • The JIT now runs on x86-64 too. Hot loops compile to native code on x86-64 (macOS and Linux), not just ARM64. Nothing else changes: it's off by default, you opt in with --jit=on, and the output is identical to the interpreter. On a platform without JIT support the flag is still accepted and ignored, so it's safe to leave in a launch command everywhere. See Make scripts fast.
  • Datagrams — the udp capability. udp.connect(host, port) opens a socket locked to one peer (the DNS/QUIC/game-client shape); udp.bind(addr, port) opens one that hears from anyone. Send with udp.send / udp.send_to, receive with udp.recv, answer whoever reached you with udp.reply. Read the warnings, because UDP is not like anything else here: it does not deliver your packet, and it is not encrypted. A lost datagram and a slow peer are indistinguishable, so the timeout and the retry are yours — that's the trade you're buying, and it's why games and DNS want it (a late packet is worse than a lost one). Datagrams are their own grant: --allow-udp=1.1.1.1:53 — never implied by --allow-net, because a datagram can hurt a third party. The grant names addresses (ADDR[:PORT], CIDR[:PORT]), not just "yes": an unscoped UDP send is "may packet anything on the internet", which isn't a capability. Names are resolved then checked, so a name resolving outside the allowlist is refused. udp.bind additionally needs --allow-serve — hearing from strangers is listen authority on top of datagram authority, so granting one never smuggles in the other. Two guardrails you will meet: a peer may be sent at most 3× the bytes it has sent you (RFC 9000's anti-amplification rule — without it a forged sender address turns your server into a DDoS reflector), and payloads are capped at 1200 bytes (QUIC's portable-safe floor; a fragmented datagram is all-or-nothing, so fragmenting multiplies your loss). Both are loud Errors, never silent. In a receive loop use udp.recv_batch(sock, max) rather than udp.recv: it waits for one datagram and then takes everything else already queued (never waiting for the batch to fill — that would trade away the latency you chose UDP for). It matters more than it sounds. Under --io=async each udp.recv costs a thread handoff, capping a one-at-a-time loop near 3,500 datagrams/s — under what a 64-player 60 Hz game server needs — while recv_batch clears 13,600/s. One exchange → recv; a stream → recv_batch. With --io=async, a receive inside an actor handler parks instead of blocking the worker, so other partitions keep running while you wait for a packet. The script is unchanged — no async, no await; only the platform differs. See Send datagrams with UDP, examples/udp_echo.scua, and examples/dns_query.scua (a real DNS query, by hand).
  • Run an HTTP server — the serve capability. http.serve(handler, { bind = "127.0.0.1:8080" }) serves requests; the host owns accept/parsing/framing and your script owns the handler (fn(req) -> resp). req is { method, path, headers, body } (header names lowercased, body bytes); return { status = 200, headers = {}, body = b"ok" }. Listening is its own grant — scua --allow-serve=127.0.0.1:8080 — deliberately separate from --allow-net (calling out never grants listening); without it http.serve doesn't exist. A handler fault or a returned Error becomes a 500 (its text withheld unless you pass --serve-debug). Raw TCP too: net.listen(addr, port) / net.accept(l) under the same grant. See Serve HTTP requests and examples/serve.scua.
  • Run many things at once — wait_all / map_all. wait_all([fn() … end, …]) runs every zero-arg arm and waits for all of them, returning their outcomes in order; one arm failing never cancels the others (a faulting arm's slot is an Error you can test with e.fault). map_all(xs, f, { concurrency = 8 }) is the parallel map. No async/await, no promises — and no ? on the call (the failures live in the slots). See Run many things at once and examples/wait_all.scua.
  • b"…" byte-string literals. Write bytes inline — b"ok", b"", b"a\nb" — the same escapes as regular strings. Handy for body = b"ok" and anywhere a bytes value is wanted (a bare b or bird is still an ordinary name).
  • Non-blocking I/O — --io=async. By default SCUA I/O is blocking. scua --io=async … runs fs/http calls on an offload pool so that under wait_all/map_all the waits actually overlap. Results are identical either way — it's an optimization, not a requirement. If a partition is snapshotted or migrated mid-request, that call returns Error(interrupted) and is never silently re-issued (no double-sent POST); reconcile against the durable side rather than blind-retry. See Write handlers that survive snapshots.
  • Frame column search. Grep a data table the same way you grep files/strings:
    • f.filter(col.contains("lit")) — fixed-string substring on a string column
    • f.filter(col.matches(re)) — compiled regex over a string column
    • f.search(needle[, { columns, ignore_case, regex }]) — multi-column shortcut (fixed-string or regex; a compiled re is always regex) See Data tables and examples/frames.scua.
  • Agent-tool I/O (sys + CLI helpers). One-shot tools for agents and harnesses:
    • sys.stdin() — whole launch-time stdin as one UTF-8 string
    • sys.args_table() / sys.parse_args(RecordType) — named CLI flags + stdin JSON into a typed record (with where refinements); bad usage exits 2
    • sys.emit / sys.fail / sys.exit — one JSON result on stdout, diagnostics on stderr, chosen exit status (decimal/money encode as lossless strings)
    • scua schema <file> --type T — JSON Schema draft 2020-12 for a record/enum
    • scua pack — generated teach-a-model in-context pack (compact/full) See Write tools for AI agents and examples/agent_tool.scua.
  • A regex module — ReDoS-safe regular expressions. A linear-time, no-backtracking engine (RE2 / Rust / ripgrep dialect) that can never hang on a pathological pattern, so it's safe to run on user- or model-supplied patterns. regex.compile(pattern) returns Ok(re) or a clear Error (backreferences, lookaround, and unsupported constructs fail loudly at compile — never a silent mis-match, matching ripgrep's default engine). Then regex.is_match, regex.find, regex.find_all, regex.captures (with named groups), and regex.replace ($1/${name} substitution). Offsets are 1-based codepoints and the engine is codepoint-native. No capability needed. \w \s \d are ASCII in v1 (\p{L} and multiline are planned). See Match text with regular expressions.
  • fs.grep with regex — pass a compiled regex value as the pattern, or a string with { regex = true }, to search files with a regular expression instead of a fixed string (a bad pattern is an Error, not a crash). { only_matching = true } emits each matched substring (with a stop field) rather than the whole line — the regex equivalent of grep -o.
  • str.matches / str.find_re / str.replace_re — run a compiled regex over an in-memory string: a boolean test, the leftmost match record, and capture-aware replace ($1/${name}), the same engine as regex.* and fs.grep with the string first.
  • fs.grep token-economy output shapes — ask for the cheapest sufficient answer instead of paying for whole lines you'll only post-process. { files_with_matches = true } returns just the matching paths (stopping at each file's first hit); { count_only = true } returns a per-file count (or, with { matches = true }, the number of individual matches); { invert = true } selects the lines that do not match; { max_per_file = N } caps matches per file. These make fs.grep far cheaper to hand to an AI agent, which greps constantly and budgets tokens on the result shape. Fixed-string only for now — regex is the next step.
  • fs.grep context lines{ context = N } (or { before = N } / { after = N }) attaches the N lines around each match as before and after string lists on the hit, the way grep -C/-B/-A do. When two matches are close their windows merge, so no line is ever shown twice. fs.grep_text renders the context indented under each match.
  • fs.grep_text(...) — the same search rendered as one compact path:line: text string, ready to paste into a prompt or a log without walking a list of records.

#Fixed

  • .. on bytes now concatenates bytes instead of silently producing garbage. b"abc" .. b"abc" used to render each operand as its debug text and join those, giving you the 26-character string "bytes(616263)bytes(616263)" rather than 6 bytes — so building a payload the obvious way (a loop, or an HTTP body, or a datagram) quietly put a hex dump on the wire. bytes .. bytes now yields bytes. And mixing is a loud error, never a coercion: cannot concatenate `bytes` with a string — `..` never coerces between bytes and text, naming bytes.from_string / bytes.to_string as the fix. Text concatenation is unchanged.
  • http.serve and net.read no longer stall against clients that keep the connection open. A socket read waited for a full internal buffer instead of returning the bytes that had arrived, so a keep-alive client (curl, a browser, another service) that sent a request and held the connection open never got its response — while a client that closed its write side (piping into nc) worked, which is how it hid. Reads now return as soon as bytes arrive.

#Improved

  • Calling a serve verb without the grant now names the fix. http.serve / net.listen / net.accept in a run without --allow-serve used to fail at runtime with a generic "attempt to call a non-function"; it is now a compile-time error that says what to grant: `http.serve` requires the `serve` capability … run with `--allow-serve=ADDR:PORT`.
  • map_all's concurrency is host-capped. Asking for more than the per-partition in-flight-I/O ceiling (64) now clamps to it — the extra work queues and still all completes, it just never holds more than 64 operations in flight at once.

#0.12.0 — 2026-07-06

#Added

  • An optional JIT — hot loops compile to native code. SCUA now has a just-in-time compiler. It's off by default (the interpreter runs everything); turn it on with --jit=on and hot loops are compiled to native machine code as they run. The output is identical to the interpreter, down to the byte, so it's safe to toggle. Game math (vectors, quaternions, matrices) and string-keyed tables benefit most — on the benchmark suite the JIT lands ahead of LuaJIT overall (the vector entity loop runs several times faster). It's ARM64-native (Apple Silicon and ARM Linux) and a harmless no-op elsewhere. See Make scripts fast; --jit-stats prints what it did. The interpreter stays the default and the whole thing compiles out of a client build for platforms that forbid runtime code generation.
  • reserve(array, n) — pre-size an array in one step. When you know how big an array will get, xs.reserve(n) allocates the capacity up front so the fill loop never re-allocates on the way up. Length is unchanged; shrinking is a no-op; returns the array so it chains. The win grows with the buffer — a large element-typed buffer ({ f64 }, { f32 }, …) goes straight to its final home.

#Improved

  • Big element-typed buffers no longer weigh 4–5× their size at runtime. Large { f64 }/ { f32 }/{ vecN } buffers now live in their own memory blocks that the garbage collector moves by reference instead of copying, and the runtime returns freed memory to the OS instead of holding peak usage forever. An 8-million-element { f64 } buffer now costs almost exactly its own 64 MB — previously ~300 MB — and programs whose memory use spikes and then drops give the memory back. Buffers over 256 MB can now exist at runtime (they can't be saved with persist yet — saving one reports an error).
  • Exact money — a first-class value type that carries its currency. Write a money literal as an amount and an ISO 4217 code: 19.99 USD, 500 JPY, -42.50 EUR. The amount is exact (it reuses the decimal engine — never a float), and the currency travels with the value through arithmetic, printing, and storage. Arithmetic is dimensional and safe by construction: money ± money (same currency), money ×/÷ a number (quantity or rate), and money ÷ money → a plain ratio; adding a bare number to money, multiplying two amounts, or mixing currencies is a compile-time error with a clear message (currency matching is checked at run time). Build one dynamically with money.of(amount, code); format, split penny-exact, round to the currency's places, and convert at an explicit rate with money.format, money.split, money.round, and money.convert. See examples/money.scua.
  • Exact-double numeric buffers ({ f64 }). The element-typed array family now includes f64: a packed, unboxed buffer of exact doubles. Where { f32 } is single-precision (and rounds a value with more precision than a 32-bit float holds), { f64 } stores each number exactly at 8 bytes — and still halves the memory of the boxed default. Use it when precision matters (a high-precision simulation, an exact coordinate); { f32 } stays the choice for GPU vertex data. See the collections guide and examples/typed-arrays.scua.
  • Rendering and localization (render). Bake a config table against caller-supplied data: render(source, locale, external) resolves @{path} references and returns a fresh payload. The config names abstract slots (self/... for itself, external/... for your data) and never the shape of who renders it. Directive blocks build config beyond strings — @ref (pull a subtree), @raw (emit literally), @when (include a subtree only when a slot is truthy; gated features just disappear), @match (pick a variant), and @each (a bounded map over a list). Localization lives next to your data: a localized({ en_US = "...", es_ES = "..." }) value collapses to the active locale (BCP-47 tags normalize, missing tags fall back to the primary subtag); @{date:date.long} renders a date in the locale (1 de febrero de 2023); @msg evaluates ICU MessageFormat for plurals and gender ({count, plural, one {# message} other {# messages}}); and @strip prunes authoring scaffolding from the output. render_tracked additionally returns the read-set with per-input provenance, for precise cache invalidation. render is pure, total, and runs under hard limits, so even a config you didn't write can't hang or exhaust memory. See the rendering and localization guide and examples/render.scua + examples/localization.scua.
  • Dates and times (the time module + a datetime type). Turn the millisecond clock into civil dates you can read, format, and parse. Build one with time.now(), time.at(ms), or time.of(2026, 6, 29, 14, 0); read its fields as methods — dt.year(), dt.weekday(), dt.parts(); render with dt.format("YYYY-MM-DD HH:mm") or dt.iso(); and read strings back with time.parse("2026-06-29T12:00:00Z")Ok(datetime)/Error. A datetime carries the UTC offset it's read at, prints as ISO-8601, and survives a durable save/load. Durations stay plain milliseconds: time.add(dt, 90min), time.diff(a, b), time.humanize(ms) ("1h 30m"), and time.parse_duration("2h5m"). It's a fixed-offset Gregorian calendar — no time-zone database, DST, or leap seconds (yet). In dt.format() the month/weekday name tokens (MMMM/dddd) are still reserved; for a localized month name use the render layer's @fmt (see the rendering bullet above). See the dates and times guide and examples/time.scua.
  • System locale & timezone. sys.locale() and sys.timezone() report the locale tag and UTC offset the program runs under — no capability grant needed. They're neutral by default ("und" and UTC), so runs are reproducible; set them at launch with --locale=de-DE --tz=+02:00, or read the real device with --locale=host --tz=host. The timezone feeds the date layer (time.now()/time.of default to it), which makes it easy to pin a fixed environment in tests. See Set the locale and timezone and examples/locale.scua.
  • Data tables (frame). A small columnar data table for totalling, sorting, and grouping data — spreadsheet-style answers in a few lines. Build one from a table of columns: frame({ region = ["EU", "US"], amount = [10.50d, 20.00d] }), then query it with chained operations: f.rows(), f.column(name), f.head(n), f.pick(names), f.sort_by(name[, descending]), f.total(name), f.mean(name), f.group_sum(key, value), and — filtering rows and adding computed columns — f.filter(predicate) and f.with(name, expr), where column names are written bare (f.filter(region == "EU" and amount > 100.00d)), or a fn(row) … end for arbitrary per-row logic; combining and reshaping — f.join(other, key[, "inner"|"left"]), f.pivot(index, columns, values), and f.unpivot(ids, values) (melt: stack wide columns into a long name/value pair). A frame prints as an aligned table. The headline is exact money: a decimal column totals, groups, and computes exactly (no float drift). You can also parse CSV text straight into a frame with csv(text) — cells are typed per value, so money cells become exact decimals. See the data tables guide and examples/frames.scua.
    • f.to_table() renders a frame as a DuckDB-style box-drawing table, returned as a string for printing or copying into a report/chat/comment (numeric columns right-aligned; every row included, unlike the capped print preview).
    • Column names and types are checked at compile time. When a frame is built from a frame({ … }) literal, a mistyped column — in total/mean/column/sort_by/pick/group_sum/join/pivot, or written bare in a filter/with predicate — is reported before the program runs, with a "did you mean" suggestion. A reducer that sums or averages (total/mean/group_sum/pivot) also checks the column is numeric, so summing a text column is caught up front. The schema follows the chain. Frames whose columns aren't known up front (a frame argument, or one from csv/join/pivot/a host) are checked at runtime instead.
    • Host embedders can populate a frame from C/Zig: build each column with the existing array APIs and assemble it with the new scua_new_frame(...), using scua_decimal(coeff, scale) for exact-money columns (no float round-trip). It's the Arrow-shaped copy-in, so a third party can back a frame with SQLite/Parquet/DuckDB without SCUA depending on Arrow or a database. Data that already comes as an Arrow C Data Interface batch can skip the per-column glue and pass the ArrowSchema/ArrowArray pair straight to scua_frame_from_arrow(...) (int/float/utf8/decimal128 columns; nulls become nil). See Drive a script from your engine.
  • Built-in geometry shapes. Six new value types — rect, aabox, sphere, capsule, ray, and plane — alongside vec/color. Build them with flat constructors (sphere(center, radius), aabox(min, max), ray(origin, dir), …) and operate on them by calling the operation on the shape: s.volume(), box.contains(p), box.closest_point(p), s.area(), s.center(), s.size(), s.radius(), cap.length(), plane.distance(p), r.point_at(t), and more. intersect(a, b) tests whether two shapes overlap or hit (aabox×aabox, sphere×sphere, rect×rect, ray×sphere, ray×plane, ray×aabox, either order), and shape + vec translates a shape. See the math guide and examples/geometry.scua.

#Changed

  • Operations now read best on the receiver. Across the math types, the operation goes on the value — write v.length(), c.to_hex(), s.volume(). The same name now works across types without clashing (a bytes value also answers b.to_hex() and b.slice(start, end)). The older bare-function forms (length(v), to_hex(c)) still work, so existing code keeps running, but the receiver form is the one to learn and what the docs now use.
  • Matrix and quaternion constructors read under the type. Write mat4.translate(v), mat4.rotate(q), mat4.scale(v), mat4.id(), quat.axis_angle(axis, angle), quat.id() (and the mat2/mat3 equivalents) — the type name groups its constructors instead of an underscore prefix. The flat names (mat4_translate, quat_axis_angle, …) still work.

#0.11.0 — 2026-06-28

#Added

  • A color value type. Colors are now first-class, alongside vec/mat/quat. A color is four float components (r/g/b/a) stored in linear space, so blending and arithmetic are correct by default. Build them with rgb/rgba (linear), srgb/srgba and hsv/hsva (decoded to linear), color_hex("#rrggbb"), or a #rrggbb / #rrggbbaa / #rgb / #rgba hex literal. About 148 CSS named colors are available as color_<name>() (e.g. color_cornflowerblue()), plus color_transparent(). Operate on them with lerp, lighten, darken, with_alpha, inverted, and luminance, and read them back with to_hex / to_srgb; the .r/.g/.b/.a swizzles and the + / * scalar operators work as for vectors. Because storage is linear, lerp(#000000, #ffffff, 0.5) is a light grey (#bcbcbc), the math-correct midpoint — not the naive #808080. A color is its own type and never silently mixes with a vec4. See the math guide and examples/colors.scua.

#0.10.0 — 2026-06-28

#Changed

  • enum, flags, and gate now use braces. Their bodies are a comma-separated { … } list instead of an end-delimited one, matching recordenum Dir { North, East, South, West }, flags Layer { Ground, Player, Wall }, gate Owner { Audience.Server, Audience.Client }. Discriminants, payloads, and comments are unchanged (enum Key { Escape = 27, Space, Enter }, enum Shape { Circle(float), Rect(int, int) }). contract is not affected — it still ends with end. This is a breaking change: existing enum/flags/gate declarations written in the old … end form must be rewritten with braces.

#0.9.1 — 2026-06-28

#Added

  • Host-provided modules can be imported from any file. When a host embeds SCUA and registers a native module (e.g. a gfx or rl graphics binding), script code in an imported module can now bring it in with import gfx — the same way it imports the standard library — instead of only the entry script being able to see it. So host-API-using code can live across modules, not just in the entry file.

#Fixed

  • The always-available builtins now work inside an imported module. floor, min, max, abs, clamp, round, ceil, sort (and tostring, type, assert, format, key, builder, and {x:.2f}-style number formatting) used to fail with "attempt to call a non-function" when called from a function defined in an imported module — they only worked in the entry script. They are now genuinely ambient everywhere, so shared logic that does any math (suit = floor(id / 13), clamps, easing) can live in a module like any other code.

#0.9.0 — 2026-06-28

#Added

  • Audience gates: project state for who's allowed to see it. Declare audiences as an enum, group them into named gates, and mark a record field gated SomeGate. project(value, RecordType, audience) returns a copy containing only the fields that audience may see — ungated fields always, a gated field only when the audience is in its gate. This is the safe way to send a cut-down view of authoritative server state to a client without leaking server-only fields (a bonus timer, an upcoming drop), instead of hand-stripping fields and hoping you didn't miss one. Fields are open by default, so you only annotate the secrets. Projection recurses into nested records and { Record } collections (a secret nested anywhere is dropped) and returns a deep copy that shares nothing mutable with the source; the audience must be an enum value. (A static "what does this audience receive" leak-report is still planned.)
  • Verify and protect your saves. durable.verify_roundtrip(v) confirms a value survives save→load with nothing lost (a QA/CI guard, exact thanks to byte-determinism). durable.digest(v) and hash.sha256(data) give a stable content hash. A new crypto module signs and verifies those digests with Ed25519 (crypto.keypair(seed), crypto.sign(msg, secret), crypto.verify(msg, sig, public)) — so a server can sign a save's digest and a client can verify it but not forge it (cheat-resistant saves with almost no backend). Keys are bytes the host supplies; never ship a secret key in a client. See Verifying a save.
  • Save and load your own data with the durable module. durable.encode(value, schema_version?) turns a value into a typed, versioned binary blob, and durable.decode(blob) turns it back — preserving int/float/decimal/big/string/bytes/key (unlike JSON), with a layout decoupled from memory so old saves keep loading as your records change. Decoding into a typed record migrates and deeply validates it; a corrupt blob is a value-level Error you can match. durable.version(blob) reads the stamped schema version. The functions are pure (no capability); write the bytes to disk with fs. See Evolve your saved data safely.

#Changed

  • A migrate hook's result is now type-checked. Previously a record with a migrate hook had a weaker load boundary than one without — a wrong-typed value could be laundered through the hook into a typed field (a float into an int). The boundary now type-checks the hook's output as strictly as a direct typed assignment, so migration can't admit a mistyped field.
  • The typed boundary now checks nested data, not just the top level. When loose/any data is assigned into a record type, the field-type checks, migrate hooks, and where refinements now run on nested records and the elements of a typed { Record } collection, all the way down — not only the outermost record. So a corrupt or stale value buried inside a loaded tree (a bad wins on a player inside a season, an un-migrated nested record) is caught where it lives instead of silently sailing through. This includes self-recursive types — a linked list or tree (Node { value: int, next: Node? }) is now checked, migrated, and refined at every level, to the data's true depth, not just near the top. Fields typed any and scalar collections like { int } are still trusted (no declared shape to check). A cyclic value (one that points back at itself), or one nested deeper than a few hundred levels, faults at the boundary rather than loading — a cycle can't be persisted anyway.
  • Clearer error when a record type is used as a value. Writing Player{ … } (the struct-literal syntax from other languages) or using a record type name on its own no longer reports a bare undefined name. The compiler now explains that a record type isn't a value and shows the construction form: let x: Player = { … }.

#Fixed

  • Reassigning a variable to a record/array that refers to itself no longer corrupts it. Inside a function, node = { v = i, next = node } (the natural way to build a linked list or tree by accumulating state) used to make the new value point at itself instead of the previous one. It now reads the old value correctly, so building hierarchical structures in a loop — and persisting them — works.
  • A partition that sends a message to itself no longer crashes. A tell/ask from a partition to itself copies the message within one heap; for a message carrying a string, bytes, key, vector, or path that could corrupt memory and crash. Self-messaging is now safe.
  • Deeply nested expressions report an error instead of crashing. Pathologically nested input — thousands of nested parentheses, a very long chain of unary not/-, or a deep ** chain — used to overflow the stack and crash the compiler. It now stops with a located expression nested too deeply error. This matters for any tool that compiles untrusted SCUA source.

#0.8.1 — 2026-06-24

#Added

  • Editor: hover and go-to-definition on type names. Hovering a declared type in a script — a handle, record, enum, flags, or type alias (including host-API types from a --!declare file) — now shows what it is, and go-to-definition jumps to its declaration, across files. Previously only functions and constants did.

#Fixed

  • Editor: host-API hover/jump now works from subdirectories. Hovering or jumping to a declared gfx.* member from a script in a subdirectory now finds the project-root scua.toml (the earlier fix covered error diagnostics but not the hover/jump index).

#0.8.0 — 2026-06-24

#Added

  • Opaque handle types for host APIs. A host's declaration file can now name an opaque resource with handle Name (e.g. handle Texture), so a script's editor type-checks resource arguments and returns by name — passing a Sound where a Texture is wanted, or reading a field off a handle, is flagged, mirroring what the runtime already enforces. The handle keyword is only meaningful in a --!declare file and reserves nothing in ordinary scripts. See Embed SCUA in a host program.

#Fixed

  • Editor: PascalCase host-function names highlight correctly. A declared fn LoadTexture(...) (the common native-binding casing) is now parsed/highlighted as a function, not mis-flagged.

#0.7.0 — 2026-06-24

#Fixed

  • Editor: host-API calls no longer show a spurious "undefined name" error. With a host's --!declare file loaded, rl.DrawTriangle (and any declared namespace) hovered and type-checked but still drew a red squiggle from a second diagnostic pass; that pass now honors the declarations.
  • Editor: scua.toml is found from subdirectories. The language server now walks up from the open file to the nearest scua.toml (like other editors/servers), so one manifest at the project root covers scripts in src/, examples/, etc. — you no longer need a scua.toml beside every script.

#Added

  • Vec values cross the embedding boundary. A host native can now read a vec2/vec3/vec4 passed as an argument and return one, so a binding's f(vec2) -> vec2 functions take and give first-class vectors (with swizzles and vec math) instead of flattened number pairs or {x, y} records. See Drive a script from your engine. (For binding authors: scua_to_vec / scua_vec / scua_return_vec.)

#0.6.0 — 2026-06-24

#Added

  • Host constants. An embedding host can now expose named scalar values (a binding's key codes, blend modes, flags) as plain fields of a module — a script reads gfx.KEY_SPACE with no call, the same as a module function. See Embed SCUA in a host program. (For binding authors: scua_register_consts + a const member in the generated --!declare file.)
  • Re-entrant callbacks. A host can call a script function from inside another native that the script invoked mid-frame (a log handler, an input or audio hook) — scua_pcall now nests safely instead of corrupting the in-flight turn. See Drive a script from your engine.
  • Owner-thread safety. A partition is now bound to one thread; touching it from another (e.g. an audio library's callback thread) is rejected (SCUA_ERR_THREAD) rather than racing the collector. Marshal off-thread work to the owner thread.

#Changed

  • Saving refuses while a host resource is reachable. If a script is holding an opaque host-resource handle (a texture, sound, font, … from an embedding host) that would be reached by a save, scua_persist now fails instead of writing a handle that dangles after reload. Release such resources before saving; this keeps "the partition is the save file" honest.

#Fixed

  • The math type names quat and mat2/mat3/mat4 are now accepted in type annotations (like vec2/ vec3/vec4 already were), so a function or binding signature can name them without a spurious "unknown type" error.

#0.5.0 — 2026-06-24

#Added

  • Element-typed numeric buffers. Annotate an array with a numeric element type — { f32 }, { i32 }, { u8 }/{ u16 }/{ u32 }, or a packed { vec2 }/{ vec3 }/{ vec4 } — and it stores its elements unboxed and contiguous, like a C float*, while still indexing, pushing, slicing, and iterating like any array. A value that doesn't fit the element type is a clean, located fault instead of a silent wrap. This is how a script builds vertex/point/pixel data that a host engine uploads with no copying. See Collections and Drive a script from your engine.
  • Embedding API for typed buffers. Hosts can inspect a buffer's element kind/size/count, build one from a contiguous block, and pin its packed bytes for a GPU upload (scua_buffer_info, scua_new_buffer, and typed-buffer support in scua_pin_bytes/scua_arg_bytes).

#Fixed

  • Two vector/matrix/quaternion values with equal components now compare equal (vec2(3, 4) == vec2(3, 4) is true); previously they only compared equal if they were the same object.

#0.4.0 — 2026-06-23

#Added

  • Editor support for a host's API. A host that registers native functions (e.g. an engine binding) can ship a declarations file so your editor gives you hover docs, signature help, and type-checking for those functions — the same experience you get for the language's own built-ins. The file is read by the tooling only; it never runs. See Drive a script from your engine.

#0.3.2 — 2026-06-23

#Fixed

  • A native registered under a namespace (e.g. rl.GetMousePosition()) now returns all of its values to a let x, y = … binding, not just the first.

#0.3.0 — 2026-06-23

#Added

  • Host embedding API (drive scripts from C). SCUA embeds in your own program like Lua: link one static library and one C header and own your frame loop. The host can call into a loaded script (update(dt)/draw()), hold script values across frames as durable handles, exchange structured values and multiple return values, pass opaque native resources (textures, sounds) that a script holds without a raw pointer ever touching managed memory, and pin a script-built byte buffer for a zero-copy upload. See Embed SCUA in a host program and Drive a script from your engine.

#0.2.0 — 2026-06-23

#Changed

  • Editor tooling (syntax highlighting and grammar) brought in lockstep with the language, including the interface, select, and wait_for constructs.

#0.1.0 — 2026-06-21

First public release — the prebuilt scua runtime, the libscua embedding SDK, the full manual, runnable examples, and editor tooling (LSP, debugger, Zed/VS Code). The language at launch already included:

  • A Lua-feel core with optional types. Dynamic by default; add type annotations only where correctness matters, and they're checked at the boundaries. Functions, closures, multiple return values, if/match as expressions, : ranges.
  • Structured data with persistence for free. Arrays, tables, records, and first-class paths into nested data; save a unit of state to a flat blob and load it back with no serialization code, including schema migration as your records change.
  • Pattern matching, enums, flags, and contracts for expressing and validating shapes and invariants.
  • An error model built on Ok/Error results, the ? propagation operator, try/rescue, and pcall.
  • Partitions and actors for isolated, message-passing state (state/on/ask/tell), in-partition concurrency (spawn, wait, durations), and selective receive (select/wait_for) — with live state that can be saved and migrated between processes.
  • Strings done right: UTF-8 text with interpolation and format specs, a distinct bytes type for binary data, interned key identities, and an efficient string builder.
  • Numerics for real work: exact decimal money, huge-magnitude big numbers for idle games, and a games math library (vec/mat/quat).
  • Capabilities, granted by the host: files (fs), networking (net), environment variables (env), command-line arguments, and TOML/INI config — all default-deny until explicitly granted.
  • Modules and import, a standard library (including JSON and content search), logging with compile-time stripping, and conditional compilation.
  • Tooling: a formatter (scua fmt), a test runner (scua test), a terminal debugger (scua debug), and named build profiles via scua.toml.