SCUA

News

SCUA 0.26.0 is out

September 15, 2026

A library written in SCUA now keeps its own names. An enum declared in a module is that module's type, two libraries can each declare a record Item with a describe method, and a type a module declares can be named through its import alias, so let err: sqlite.Error = { code = 5, message = "abort" } builds the library's own record. Records no longer have a fixed width, a parameter typed T? can be left off, and an enum that carries data has a JSON spelling. One change is breaking, the enum one, so read the upgrade section before you install.

#Modules own their enums

Until now enum Outcome in one file and enum Outcome in another were one type, and the second declaration was refused. In 0.26.0 an enum or flags type belongs to the module that declares it. Two libraries can each declare Outcome, you can declare your own Kind beside a library's, a value of one never matches the other's patterns, and the compiler refuses to mix them. Values still print by their declared name, so output does not change.

Methods work the same way. A method is looked up on the record it was declared on, not on the record's name, so two modules' record Item types keep their own describe.

#Naming a module's types

let err: sqlite.Error = { code = 5, message = "abort" } checks the literal against the library's declaration and builds the library's own runtime type: its methods dispatch on your instance, it is sealed, and its where refinements hold. Until now a caller either hand-wrote a near-miss table or the library had to ship a constructor. Ask for a type the module does not declare and the message lists the ones it does. A method a library declares on its record also type-checks on a value the library returned, as in lib.make("x").describe().

A record literal returned from a function declared to return that record is a real record. fn make(n: string) -> Player return { name = n } end hands back a sealed Player with its defaults filled and its methods callable from any file, where it used to hand back a plain table. A module may also export a method under a different name: return { query_frame = to_frame } makes db.query_frame(sql) work.

#Modules in subdirectories

import lib/mid names a module by its path and binds its last segment, or whatever you write after as. A module's own imports look beside the module first, so lib/mid.scua doing import helper finds lib/helper.scua wherever the package is placed, before walking the search directories. Names that used to collide silently now refuse, with the fix spelled out: a module two search directories both provide, an import alias that is also a top-level function or a built-in name, or a file of yours named after a built-in module.

For hosts, scua_add_module_path is the C API twin of --mod-path. A package's examples/ can import the module in its parent directory under a host, which is the layout every package already uses.

#Records of any width

The limit was 16 fields, the width at which a plain table switches to hash-map storage. A record is a fixed layout that never needed that switch, so the limit is gone. A binding surface with one field per bound function, 26 for a SQLite wrapper or hundreds for a larger library, is a record like any other. Plain tables are unchanged.

A record literal may also leave out defaulted fields wherever a record is expected: as an argument to a parameter typed Player, as the value returned from a function declared -> Player, or assigned to a Player variable. When the literal is wrong the error names the field, missing field 'hp' of type number, rather than describing the whole table.

#Optional parameters and checked ? calls

A parameter typed T? may be left off. fn open(path: string, opts: Options?) can be called as open(path), and opts arrives as nil, which is what the type already allowed. Everything after an omittable parameter is omittable too, as with defaults.

A call written f(x)? is now checked like f(x). The ? operator used to skip every check on the expression inside it, so a wrong argument count could pass unnoticed until a plain call failed. Code that relied on that gap with a parameter that is neither defaulted nor T? is refused at the call now.

#Tagged enums in JSON

A variant that carries data encodes as { "kind": "Text", "value": "hi" }, with value an array for several fields and omitted for none. A decoded object of that shape becomes the variant again wherever a record field, or a { Item } array field, is declared with the enum, so match works on what a JSON caller sent. A wrong kind or a wrongly sized value is refused at the boundary, by field. scua schema publishes such a field as a oneOf over the variants, and it now defines every record it refers to, at any depth, so a consumer resolving a reference finds the record. Ok(…) and Error(…) encode the same way.

#Bytes values have methods

Every bytes function is a method on a bytes value: b.to_array(), b.to_string(), b.slice(a, b) and the rest, with no import bytes needed.

#For source builds with the FFI

The dynamic FFI, still opt-in for source builds made with -Dffi=true, holds up to 256 declared native types per partition, twice what it did, and a binding that reaches the limit is told so by name, with the way out. Native libraries and FFI has the setup and the limits.

#Upgrading

Check two things before you install 0.26.0.

Delete any verbatim copy of a library's enum you kept to match its values. It is a separate type now, and the compiler hints to say so.

Drain or re-save anything persisted that holds values of a module-declared enum. Such saves and hibernated sessions are refused on load with a schema mismatch. Saves holding only your own file's enums load exactly as before, and durable and cluster data written by 0.25.0 still loads when the enum name is unambiguous in the reading program.

The full changelog has the detail.