Repository navigation
v0.9.0 — code generator improvements
0.9.0 — 2026-08-19
The code generator stops being a convenience for simple functions and becomes
the way an addon is written: zero-copy binary data, arrays of structs, classes
with native Mojo state, nullable arguments, and async without the old caps.
Plus the tutorial that was missing when the CLI shipped, and the allocator
evidence the async string result was owed.
Changed
- The argument-fetch and type-check chain in
generate-addon.mjslived in five
copies (sync, async, class constructor, instance method, static method) that
had to be edited in lockstep for any new token or arity — and had already
drifted. They now share oneemitArgPreamble. Verified behaviour-preserving:
src/generated/, the kitchen-sink output (every emitter branch) and the
tutorial addon all regenerate byte-identical to the pre-refactor output.
Added
- A concurrent-async stress suite, run under a checking allocator in CI.
tests/async_stress.test.jsputs many string-returning async calls in
flight at once — mixed sizes, interleaved with numeric async, with forced GC
between rounds — and a new non-requiredasync-stressjob runs it under a
checking allocator: Guard Malloc on macOS (which does engage on GitHub
runners — the job probes for theGuardMalloc[node-…]banner and warns if
that ever stops being true, since a strippedDYLD_INSERT_LIBRARIESis not
an error and would check nothing while going green), always the
libmalloc knobsMallocScribble/MallocGuardEdges/MallocErrorAbortwhich
the system allocator honours directly, andMALLOC_PERTURB_/MALLOC_CHECK_
on Linux. This
is the evidence the asyncstringresult was missing: the reasoning for why
a Mojo String may cross to a worker thread was sound, but nothing had run the
allocator in checking mode against it. jest is invoked directly rather than
through npx, which dropsDYLD_INSERT_LIBRARIES. asyncLabel(s)— a string-returning async export on the demo addon, so the
new capability has a runtime target where the jest suite already points.npm run generate:docssays what it documents. It runs typedoc over
build/index.d.ts— the demo addon's surface — while being named and
titled as if it covered the framework. Output moves to
docs/api-demo-addon/with a matching title. A reference for the framework
itself is scoped indocs/plan-api-reference.md, whose finding is that 87%
of the 397 public framework defs carry no doc comment at all, so the
bottleneck is content rather than tooling.- Async generation: no argument cap, and
stringresults. The four-argument
limit came from the entry callback's arity chain, not from anything about
threads, and disappeared when that chain was unified — async now uses the
same heap-argv path as sync.returns = "string"also works: the data
struct holds a MojoString, moved rather than copied in its move
constructor. The rule that this struct may hold "only simple types" was
folklore; the constraints that actually bind on the worker thread are no
N-API calls and no dlopen/dlsym, and CLAUDE.md now says so. Concurrency is
covered by the stress suite above, added in this same release. - Nullable arguments are real now.
args = ["string?"]hands the Mojo
function anOptional[String]—Nonefor JS null or undefined, a
converted value otherwise — and the typed check sits inside the null test,
so a wrong type still raises the descriptive TypeError instead of falling
into the generic catch. Works for number, string, boolean, the integer
tokens and declared structs. This replaces the rejection added earlier in
this cycle, which existed because the.d.tsadvertised| nullwhile the
extract converted unconditionally.number[],float64arrayandbuffer
are still refused, now with the reason: an absent array is an empty one, and
an absent zero-copy view has no buffer. - Classes can keep native Mojo state.
state = "<struct>"plus
constructor_mojo_fnmakes the generator heap-allocate a declared struct,
wrap it onto the instance, and hand it to everymojo_fnmember — a
mutating method takes itmut, a getter borrows it — with the GC finalizer
emitted alongside. Previously a generated class had nowhere to put Mojo data
and had to stash values as JS properties. Instances are stamped with a
128-bit type tag derived (deterministically, so regeneration is byte-stable)
from the class name, and every member verifies it, so borrowing a method onto
a foreign object raises a TypeError instead of reinterpreting memory.
mojo_fnon setters and static methods is rejected rather than ignored. - Generated structs implement
ToJsValue/FromJsValue, and<struct>[]
works everywhere a type token is accepted. Declaring[structs.config]has
always given youconfigas a token; it now also givesconfig[], in
argument and return position, mapping toList[ConfigData]in Mojo and
Config[]in TypeScript. The array form is carried entirely by the trait
conformance plus the existing parametricto_js_array/from_js_array— no
per-struct array emitter. - Zero-copy binary tokens in the code generator.
float64arrayas an
argument hands the Mojo function aSpan[Float64]aliasing the JS
Float64Array's own backing store; as a return type the function hands back
aMojoFloat64Arraywhose allocation JavaScript adopts. No copy in either
direction — the story Mojo exists for, previously reachable only from a
hand-written callback.buffergives aSpan[Byte]view over a Node
Bufferand is argument-only: there is no Mojo-ownedBuffertype to hand
back without copying, soreturns = "buffer"is rejected with that reason
rather than silently copying. TypeScript emitsFloat64ArrayandBuffer.
The input Span aliases engine-owned memory and is valid only for the
duration of the call. - docs/TUTORIAL.md — the CLI's missing half. Walks from
napi-mojo initto a published-shaped addon: a pure function, a nullable
return, a struct in both directions, async work on a worker thread, and a
class. Its finished addon is a real fixture,examples/tutorial/, and a new
CI step generates, compiles and calls every one of its exports on both
platforms — so the doc's snippets cannot rot the way prose examples do.