Skip to content

v0.9.0 — code generator improvements

Choose a tag to compare

@codetalcott codetalcott released this 19 Aug 06:10
· 183 commits to main since this release
e917697

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.mjs lived 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 one emitArgPreamble. 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.js puts many string-returning async calls in
    flight at once — mixed sizes, interleaved with numeric async, with forced GC
    between rounds — and a new non-required async-stress job runs it under a
    checking allocator: Guard Malloc on macOS (which does engage on GitHub
    runners — the job probes for the GuardMalloc[node-…] banner and warns if
    that ever stops being true, since a stripped DYLD_INSERT_LIBRARIES is not
    an error and would check nothing while going green), always the
    libmalloc knobs MallocScribble/MallocGuardEdges/MallocErrorAbort which
    the system allocator honours directly, and MALLOC_PERTURB_/MALLOC_CHECK_
    on Linux. This
    is the evidence the async string result 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 drops DYLD_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:docs says 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 in docs/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 string results. 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 Mojo String, 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 an Optional[String] — None for 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.ts advertised | null while the
    extract converted unconditionally. number[], float64array and buffer
    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_fn makes the generator heap-allocate a declared struct,
    wrap it onto the instance, and hand it to every mojo_fn member — a
    mutating method takes it mut, 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_fn on 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 you config as a token; it now also gives config[], in
    argument and return position, mapping to List[ConfigData] in Mojo and
    Config[] in TypeScript. The array form is carried entirely by the trait
    conformance plus the existing parametric to_js_array/from_js_array — no
    per-struct array emitter.
  • Zero-copy binary tokens in the code generator. float64array as an
    argument hands the Mojo function a Span[Float64] aliasing the JS
    Float64Array's own backing store; as a return type the function hands back
    a MojoFloat64Array whose allocation JavaScript adopts. No copy in either
    direction — the story Mojo exists for, previously reachable only from a
    hand-written callback. buffer gives a Span[Byte] view over a Node
    Buffer and is argument-only: there is no Mojo-owned Buffer type to hand
    back without copying, so returns = "buffer" is rejected with that reason
    rather than silently copying. TypeScript emits Float64Array and Buffer.
    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 init to 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.