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
udpcapability.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 withudp.send/udp.send_to, receive withudp.recv, answer whoever reached you withudp.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.bindadditionally 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 loudErrors, never silent. In a receive loop useudp.recv_batch(sock, max)rather thanudp.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=asynceachudp.recvcosts 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 — whilerecv_batchclears 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 — noasync, noawait; only the platform differs. See Send datagrams with UDP,examples/udp_echo.scua, andexamples/dns_query.scua(a real DNS query, by hand). - Run an HTTP server — the
servecapability.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).reqis{ method, path, headers, body }(header names lowercased,bodybytes); 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 ithttp.servedoesn't exist. A handler fault or a returnedErrorbecomes 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 andexamples/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 anErroryou can test withe.fault).map_all(xs, f, { concurrency = 8 })is the parallel map. Noasync/await, no promises — and no?on the call (the failures live in the slots). See Run many things at once andexamples/wait_all.scua. b"…"byte-string literals. Write bytes inline —b"ok",b"",b"a\nb"— the same escapes as regular strings. Handy forbody = b"ok"and anywhere abytesvalue is wanted (a bareborbirdis still an ordinary name).- Non-blocking I/O —
--io=async. By default SCUA I/O is blocking.scua --io=async …runsfs/httpcalls on an offload pool so that underwait_all/map_allthe 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 returnsError(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 columnf.filter(col.matches(re))— compiledregexover a string columnf.search(needle[, { columns, ignore_case, regex }])— multi-column shortcut (fixed-string or regex; a compiledreis always regex) See Data tables andexamples/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 stringsys.args_table()/sys.parse_args(RecordType)— named CLI flags + stdin JSON into a typed record (withwhererefinements); bad usage exits 2sys.emit/sys.fail/sys.exit— one JSON result on stdout, diagnostics on stderr, chosen exit status (decimal/moneyencode as lossless strings)scua schema <file> --type T— JSON Schema draft 2020-12 for a record/enumscua pack— generated teach-a-model in-context pack (compact/full) See Write tools for AI agents andexamples/agent_tool.scua.
- A
regexmodule — 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)returnsOk(re)or a clearError(backreferences, lookaround, and unsupported constructs fail loudly at compile — never a silent mis-match, matching ripgrep's default engine). Thenregex.is_match,regex.find,regex.find_all,regex.captures(with named groups), andregex.replace($1/${name}substitution). Offsets are 1-based codepoints and the engine is codepoint-native. No capability needed.\w \s \dare ASCII in v1 (\p{L}and multiline are planned). See Match text with regular expressions. fs.grepwith regex — pass a compiledregexvalue 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 anError, not a crash).{ only_matching = true }emits each matched substring (with astopfield) rather than the whole line — the regex equivalent ofgrep -o.str.matches/str.find_re/str.replace_re— run a compiledregexover an in-memory string: a boolean test, the leftmost match record, and capture-aware replace ($1/${name}), the same engine asregex.*andfs.grepwith the string first.fs.greptoken-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 makefs.grepfar 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.grepcontext lines —{ context = N }(or{ before = N }/{ after = N }) attaches the N lines around each match asbeforeandafterstring lists on the hit, the waygrep -C/-B/-Ado. When two matches are close their windows merge, so no line is ever shown twice.fs.grep_textrenders the context indented under each match.fs.grep_text(...)— the same search rendered as one compactpath:line: textstring, ready to paste into a prompt or a log without walking a list of records.
#Fixed
..onbytesnow 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 .. bytesnow yieldsbytes. And mixing is a loud error, never a coercion:cannot concatenate `bytes` with a string — `..` never coerces between bytes and text, namingbytes.from_string/bytes.to_stringas the fix. Text concatenation is unchanged.http.serveandnet.readno 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 intonc) 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.acceptin a run without--allow-serveused 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'sconcurrencyis 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=onand 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-statsprints 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 withpersistyet — 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 thedecimalengine — 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), andmoney ÷ 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 withmoney.of(amount, code); format, split penny-exact, round to the currency's places, and convert at an explicit rate withmoney.format,money.split,money.round, andmoney.convert. Seeexamples/money.scua. - Exact-double numeric buffers
(
{ f64 }). The element-typed array family now includesf64: 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 andexamples/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: alocalized({ 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);@msgevaluates ICU MessageFormat for plurals and gender ({count, plural, one {# message} other {# messages}}); and@stripprunes authoring scaffolding from the output.render_trackedadditionally returns the read-set with per-input provenance, for precise cache invalidation.renderis 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 andexamples/render.scua+examples/localization.scua. - Dates and times (the
timemodule + adatetimetype). Turn the millisecond clock into civil dates you can read, format, and parse. Build one withtime.now(),time.at(ms), ortime.of(2026, 6, 29, 14, 0); read its fields as methods —dt.year(),dt.weekday(),dt.parts(); render withdt.format("YYYY-MM-DD HH:mm")ordt.iso(); and read strings back withtime.parse("2026-06-29T12:00:00Z")→Ok(datetime)/Error. Adatetimecarries 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"), andtime.parse_duration("2h5m"). It's a fixed-offset Gregorian calendar — no time-zone database, DST, or leap seconds (yet). Indt.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 andexamples/time.scua. - System locale & timezone.
sys.locale()andsys.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.ofdefault to it), which makes it easy to pin a fixed environment in tests. See Set the locale and timezone andexamples/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)andf.with(name, expr), where column names are written bare (f.filter(region == "EU" and amount > 100.00d)), or afn(row) … endfor arbitrary per-row logic; combining and reshaping —f.join(other, key[, "inner"|"left"]),f.pivot(index, columns, values), andf.unpivot(ids, values)(melt: stack wide columns into a longname/valuepair). A frame prints as an aligned table. The headline is exact money: adecimalcolumn totals, groups, and computes exactly (no float drift). You can also parse CSV text straight into a frame withcsv(text)— cells are typed per value, so money cells become exactdecimals. See the data tables guide andexamples/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 cappedprintpreview).- Column names and types are checked at compile time.
When a frame is built from a
frame({ … })literal, a mistyped column — intotal/mean/column/sort_by/pick/group_sum/join/pivot, or written bare in afilter/withpredicate — 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 (aframeargument, or one fromcsv/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(...), usingscua_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 theArrowSchema/ArrowArraypair straight toscua_frame_from_arrow(...)(int/float/utf8/decimal128 columns; nulls becomenil). See Drive a script from your engine.
- Built-in geometry shapes. Six new value types —
rect,aabox,sphere,capsule,ray, andplane— alongsidevec/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), andshape + vectranslates a shape. See the math guide andexamples/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 (abytesvalue also answersb.to_hex()andb.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 themat2/mat3equivalents) — 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
colorvalue type. Colors are now first-class, alongsidevec/mat/quat. Acoloris four float components (r/g/b/a) stored in linear space, so blending and arithmetic are correct by default. Build them withrgb/rgba(linear),srgb/srgbaandhsv/hsva(decoded to linear),color_hex("#rrggbb"), or a#rrggbb/#rrggbbaa/#rgb/#rgbahex literal. About 148 CSS named colors are available ascolor_<name>()(e.g.color_cornflowerblue()), pluscolor_transparent(). Operate on them withlerp,lighten,darken,with_alpha,inverted, andluminance, and read them back withto_hex/to_srgb; the.r/.g/.b/.aswizzles and the+/* scalaroperators 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. Acoloris its own type and never silently mixes with avec4. See the math guide andexamples/colors.scua.
#0.10.0 — 2026-06-28
#Changed
enum,flags, andgatenow use braces. Their bodies are a comma-separated{ … }list instead of anend-delimited one, matchingrecord—enum 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) }).contractis not affected — it still ends withend. This is a breaking change: existingenum/flags/gatedeclarations written in the old… endform 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
gfxorrlgraphics binding), script code in an imported module can now bring it in withimport 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(andtostring,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 namedgates, and mark a record fieldgated SomeGate.project(value, RecordType, audience)returns a copy containing only the fields that audience may see — ungated fields always, agatedfield 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)andhash.sha256(data)give a stable content hash. A newcryptomodule 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
durablemodule.durable.encode(value, schema_version?)turns a value into a typed, versioned binary blob, anddurable.decode(blob)turns it back — preservingint/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-levelErroryou canmatch.durable.version(blob)reads the stamped schema version. The functions are pure (no capability); write the bytes to disk withfs. See Evolve your saved data safely.
#Changed
- A
migratehook's result is now type-checked. Previously a record with amigratehook had a weaker load boundary than one without — a wrong-typed value could be laundered through the hook into a typed field (afloatinto anint). 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/
anydata is assigned into a record type, the field-type checks,migratehooks, andwhererefinements 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 badwinson 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 typedanyand 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 bareundefined 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/askfrom 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 locatedexpression nested too deeplyerror. 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, ortypealias (including host-API types from a--!declarefile) — 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-rootscua.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 aSoundwhere aTextureis wanted, or reading a field off a handle, is flagged, mirroring what the runtime already enforces. Thehandlekeyword is only meaningful in a--!declarefile 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
--!declarefile 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.tomlis found from subdirectories. The language server now walks up from the open file to the nearestscua.toml(like other editors/servers), so one manifest at the project root covers scripts insrc/,examples/, etc. — you no longer need ascua.tomlbeside every script.
#Added
- Vec values cross the embedding boundary. A host
native can now read a
vec2/vec3/vec4passed as an argument and return one, so a binding'sf(vec2) -> vec2functions 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_SPACEwith no call, the same as a module function. See Embed SCUA in a host program. (For binding authors:scua_register_consts+ aconstmember in the generated--!declarefile.) - 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_pcallnow 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_persistnow 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
quatandmat2/mat3/mat4are now accepted in type annotations (likevec2/vec3/vec4already 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 Cfloat*, 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 inscua_pin_bytes/scua_arg_bytes).
#Fixed
- Two vector/matrix/quaternion values with equal components now
compare equal (
vec2(3, 4) == vec2(3, 4)istrue); 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 alet 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, andwait_forconstructs.
#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/matchas 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/Errorresults, the?propagation operator,try/rescue, andpcall. - 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
bytestype for binary data, internedkeyidentities, and an efficient stringbuilder. - Numerics for real work: exact
decimalmoney, 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 viascua.toml.