JavaScript backend for lengc (Typed-Array linear-memory model)#2043
JavaScript backend for lengc (Typed-Array linear-memory model)#2043savannt wants to merge 56 commits into
Conversation
|
Update — data structures (one step past pure-compute). Added design-neutral codegen for the lowered data shapes, so the backend now handles object/array values and field/index access, not just scalars:
These are all 1:1 mappings of the lowered Leng, so they don't pre-empt the M2 design question. Concretely, New self-contained test |
^ This is not correct at all and it's where most of the complexity comes from in the old JS code generator. Locals which address have been taken must be translated into 1-sized arrays, |
|
Reworked addr/deref into the On top of that:
With those, integer where The one remaining wall is string The 🤖 pushed as commits on |
|
Coverage survey — where the backend stands, and how the gaps converge. I ran ~13 real programs through the full pipeline ( Runs correctly under Node today (zero user-module
Blocked — and each one is the same design decision:
So the residual TODO tags across the whole stdlib reduce to: Concretely: is the plan to branch the JS backend off a higher-level Leng (GC target, native EH), or to reconstruct all of this — allocation, SSO strings, goto — on top of fully-lowered Leng? The former looks dramatically smaller and is where I'd take M2 next; happy to prototype the hexer pass-gating if that's the direction. 🤖 pushed as commits on |
|
We need the low level representation so that all low level libs that happen to use For JS interop we need all the special pragmas like Note that Nimony now keeps user-level pragmas in its IR verbatim and plugins can access that and plugins can also emit a |
|
Got it — that reframes M2 cleanly and resolves the design question I was circling. Let me play it back to make sure I have it right, then sketch the plan. The model, as I understand it:
M2 plan on that basis:
Everything already in the branch (fat pointers, importc/exportc resolution via A few things I'd like to confirm before I start:
Thanks — this is a far more finite target than the fork I was worried about. |
|
Validates Araq's M2 direction (nim-lang#2043): base object/array/seq/string on Typed Arrays (a byte-addressable ArrayBuffer = linear memory), not native JS objects/arrays, so cast/raw-byte access and the packed SSO string work. Proves the model dissolves the string wall the M0/M1 backend hit: struct field-offset layout, and SSO string echo (both inline and heap branches) via readRawData -> fwrite reading raw bytes straight from the buffer. Spike only — kept out of the golden suite; the bump allocator stands in for the real allocFixed/mimalloc ABI (open question to Araq). The shipping run.sh suite is unchanged and still green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First codegen step for the Typed-Array model (nim-lang#2043): compute the byte layout of Leng types so object/array values can live in an ArrayBuffer. Neither the C nor the LLVM backend computes this (both defer struct layout to their toolchains), so it is new. jslayout.nim: typeLayout (size/align of any type) + objectFields (per-field byte offsets), over the full grammar — primitives (i/u/f/bool/char, incl. (i -1) pointer-size), ptr/aptr/proctype, array (elem stride * count), object (fields in order with padding; inheritance base embedded first like the C `Q` member), union (overlaid), enum (base type), and named types resolved via getDeclOrNil. Tested by jslayout_dump (a standalone dump tool, so no new lengc subcommand) against tlayout.c.nif with hand-computed offsets: padding (i8->i64@8, i16@16 => size 24), arrays (pad 1->4, 4+16=20), nested named structs (Point@0/@8), and inheritance (base first, field @4). Wired into run.sh, guarded on the tool being built so the rest of the suite stays lengc-only. ABI-fixed, so independent of the still-open allocator/EH/wiring questions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@AmjadHD good questions — my read as the one building this, with the caveat that the final calls on the interop surface are Araq's. All three come down to the same split: Nim-native data lives as bytes in the linear-memory 1. 2. Mixing Nim and JS objects — yes, a register is needed. A Nim object is bytes in the buffer; a JS object is a reference the buffer can't hold. So a field/var of a JS-interop type stores an integer handle, and a JS-side handle table maps handle → live JS value — the classic approach (emscripten's 3. Exporting a Nim object. Since it's bytes in the buffer, a JS caller can't treat it as a native object directly. Two options at the boundary: (a) hand back the offset plus generated accessors — Net: two disjoint worlds — the linear-memory buffer and a JS handle table — bridged by |
|
|
Good follow-ups — the right next-level questions. Where do JS primitives live — linear memory or the registry? It depends on whether the primitive has a fixed machine representation:
So the registry is only for JS values that can't be a fixed byte pattern; anything with a native scalar shape stays in linear memory. (Same split emscripten draws: numbers pass directly, objects/strings use the How does JS allocate a Nim object (the Option-A getter/setter case)? Exactly as you sketched: JS can't carve bytes out of the Nim buffer itself — only the compiled allocator knows the heap state — so exporting a constructible object generates an exported factory that calls the Nim allocator for ES6? For this memory model it's less "should we" than "we already must": I'll follow up shortly with a small runnable spike demonstrating the registry + factory + ES-class export path end-to-end, the same way the string spike showed the linear-memory model — easier to poke at than prose. |
…xport Backs the interop answers on nim-lang#2043 with running code (the way model.spike.js backs the linear-memory answers): - JS primitives: number stored unboxed in the buffer; string stored as a handle in a JS-side registry (no fixed byte shape). - allocator-backed factory: JS can't carve Nim bytes, so a generated _nim_Person_new calls the allocator and returns an offset. - ES-class export: class Person wraps the offset with get/set accessors + a dispose() that releases the registry root. Spike only (uses the placeholder bump allocator); wired into spike/run.sh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Follow-up: the runnable spike is up in 4d6e5eb3 (
|
…ubcommand Per Araq's PR nim-lang#2043 direction ("backends are plugins ... custom backends based on Leng are directly supported", Ghast/nativenif#73 as the reference), the JS backend should be a standalone binary that reads a module's Leng IR (`.c.nif`) off the wire and emits its artifact, scheduled as a DAG node via `{.build(...).}` — not an in-tree `lengc` subcommand coupled to the driver. Adds `src/lengc/lengjs.nim`: a Ghast-shaped entry point (`lengjs <module.c.nif> <out.js>`) matching the `.build` tool-routing convention (`<tool> <args…> <in.c.nif> <out>`). All codegen stays in the shared `jscodegen` module, so output is byte-identical to the old subcommand (all 7 goldens unchanged). The extraction is thin: `generateJSCode` already does `load(inp)` + write, and `MainModule` is constructible from just a path. Switches `tests/jsbackend/run.sh` to drive the suite through `bin/lengjs`, so the tests now exercise the plugin delivery path. Suite green (compute / data / addr / addr2 / ffi / arith / echo + layout). The `lengc js` subcommand is kept as a thin convenience wrapper over the same codegen. Whole-program `{.build.}` scheduling + preferred binary name remain open questions to Araq. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the two-tool plugin architecture Araq's direction implies (PR nim-lang#2043). `lengjs` is the per-module `{.build.}` backend tool (module Leng IR -> JS); `jslink` is the `{.bundle.}` custom linker that overrides the final link step. `jslink <linkmanifest.nif> <out.js> [runtime.js]` reads the project link manifest NIF in the exact grammar nimony's `deps.nim writeLinkManifest` emits — `(link (apptype S)(output S)(file S (kind S))…)` — collects the `kind "artifact"` entries (each module's routed JS), prepends the runtime, drops per-file `"use strict";`, and appends the program entry into one runnable bundle. Uses the plain `nifreader` token stream (no MainModule needed), like Ghast. Verified end-to-end on a real program: nimony-compiled `echo fib(10)` -> lengjs per module -> manifest -> jslink -> node prints 55/5. Adds a self-contained `bundle:` regression check to run.sh (artifact collection, use-strict dedup, entry append), guarded on `bin/jslink`. Suite green. Still open (Araq's Q3): the whole-program WIRING — how every module gets routed through lengjs and the one jslink bundle scheduled, without hand-annotating each module (Ghast attaches `{.build.}` per GPU proc; a whole-program JS backend needs a global backend selection or a cascading pragma). Tool contracts are settled; the scheduling is the compiler-integration decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
As for the allocator question, we compile the native Nim allocator to JS, it needs a tiny emulation for mmap() and munmap() and that's all there is to it. The allocator does not assume a single large block of memory so it could be based on non-growable TypedArrays rather easily too. |
Araq resolved the allocator question on PR nim-lang#2043: the native Nim allocator is compiled to JS like any other Nim code, so its free lists / size classes / allocFixed / dealloc come from the normal pipeline over linear memory. The JS runtime provides ONLY the OS pages underneath — mmap / munmap — and since the allocator does not assume one contiguous growable block, those pages can be independent non-growable TypedArray regions (no resizable ArrayBuffer needed). This corrects the runtime spike, which had drawn the boundary one layer too high (at allocFixed/dealloc, framed as mimalloc-vs-shim). The ALLOCATOR box is now RESOLVED: mmap/munmap are the only genuinely JS-provided primitives, with a bump-over-mmap stand-in for the compiled allocator so the surface still runs and models the true layering (the allocator sub-allocates the regions mmap returns). runtime.spike.js now asserts the mmap page boundary too. Suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Does it generate modules or a single file? |
|
Both, at different stages — the same split as the C backend, plus a link step:
So the final output is a single self-contained file. I went this way because the linear-memory model already makes every module share one (Verified end-to-end: a real |
Progress update: the backend now runs the linear-memory model, not just the pure-compute sliceSince this opened as a pure-compute slice, the interesting work has been moving the shipping backend onto the Typed-Array / linear-memory direction you laid out — so JS is the same lowered Leng as C rather than a separate semantic model. Where it stands now: One What runs end-to-end through the full pipeline ( # objects + array + while-loop + bounds check + object-param proc
type Point = object
x, y: int
proc dist2(p: Point): int = p.x*p.x + p.y*p.y
proc run(): int =
let p = Point(x: 3, y: 4)
var a: array[3, int] = [10, 20, 30]
var s = 0
var i = 0
while i < 3:
s = s + a[i]
i = i + 1
result = dist2(p) + s # 25 + 60
echo run() # -> prints 85 (int -> string -> echo, 0 TODOs)import std/syncio
echo "hi" # -> prints hiStrings echo now. This was the last thing gating a Your allocator answer folded in cleanly: the runtime provides only Still design-gated (all bottom out in the GC/allocator or goto-EH decision, not in the backend surface): seqs, Happy to keep this incremental. Full |
|
produce the JS as NIF-tree with a dedicated JS-whatever enum, the nifcore APIs have been designed with this in mind. Then have it produce a tiny. jsnif -> js emitter. This way the code it not full of stupid string operations we have to keep correct all the time |
Araq's direction on PR nim-lang#2043: "produce the JS as a NIF-tree with a dedicated JS enum … then a tiny jsnif -> js emitter. This way the code is not full of stupid string operations we have to keep correct all the time, plus you can easily run a peephole optimizer later on the nifjs." Two pieces: * src/lengc/jsnif.nim — the JS-as-NIF model. A `JsTag` vocabulary (func/var/ if/while/switch/call/bin/member/array/object/… ) built over nifcore's TokenBuf (the `openTag`/`closeTag`/`addStrLit` API designed for exactly this), plus `emit`, a ~one-line-per-tag printer that is the ONLY place JS text is produced. Parenthesisation and indentation are the emitter's job. The pool is private to the backend run, so tags use the ordinal-0 / +1-shim trick nifcore's docs describe for the jsonnif/htmlnif adapters. * src/lengc/jscodegen.nim — rewritten to *translate* Leng -> jsnif rather than concatenate strings. Every `g.wr "(" & a & " + " & b & ")"` is now a `jBin` node; sub-expressions nest into one shared buffer, so `captureExpr`'s string juggling and all the hand-managed parens are gone. The buffer-model type logic (dotObjType / withPointee / pointeeAk / layout) is unchanged — only emission moved to the tree. This kills the bug class behind the recent string-assembly defects (empty `mem.()` accessors, fat-pointer fallback strings, load-width mistakes) and gives a peephole pass a tree to walk. Verified: full jsbackend suite green (13 checks — golden + node-functional per fixture + bundle link); real `echo "hi"` prints hi; real object+array program prints 85 (0 TODOs); switch/case (multi-label + default) smoke-tested. Goldens regenerated from the emitter (format-only churn: uniform parens, `let` for the IIFE temp). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Done — the backend no longer builds JS by string concatenation. It now emits a jsnif tree and a tiny printer turns that into text (d14144ca). Two pieces:
Concretely this kills the bug class it was meant to: the recent defects here were all string-assembly mistakes (an empty Verified end-to-end: full |
|
And the peephole payoff is real already — a first Pure tree rewrite, no string handling, trivially extended with more rules. Suite still green, real programs unchanged (hi / 85). |
|
About the exception handling: Here too we make good use of what Hexer already outputs:
These two properties for Leng are currently not strictly enforced but the produced code adheres to them and lengc/arkham will enforce them soon. |
Hexer lowers try/except/(finally) to an error-code ABI: a raising call returns an ErrorCode, `if canRaise.err` does a forward `jmp` to a handler that lives inside a dead `if (false)` landing pad. Per Araq's PR nim-lang#2043 guidance, these `jmp`/`lab`s map directly to `break` in nested labeled blocks — no relooper — because the jumps Hexer emits are forward-only and scoped (a jmp only ever leaves enclosing constructs going forward, never enters a sibling or descends into one). Per statement list, genStmtList flattens transparent `stmts` and rewrites each dead-if into a flat `[else; jmp SKIP; lab L; handler; lab SKIP]` stream, then wraps it in nested labeled blocks (outermost = last label): `jmp L` -> `break L` (lands at the label), the normal path `break`s a synthetic SKIP past the handler. Body locals in such a proc are emitted function-scoped (`var`) so values set before a label survive being read after the blocks. jsnif gains `jLabel`, a labeled `jBreak`, and `jVarFn`. Verified end-to-end through the real toolchain: try/except, try/except/ finally, sequential try blocks, nested try, try inside a loop, and `except T as e` all run correctly under Node. Self-contained regression fixture texc.c.nif added to the suite. (ref-object exception destructors still need `baseobj`/vtable access — a separate, pre-existing RTTI gap.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Progress update: exceptions (your
|
Addresses @Araq's PR nim-lang#2043 review: - jsnif: emit tags by construction (TagId == JsTag + 1) via accessor templates; drop both `ids` arrays. - jscodegen: navigate types with the shared `typenav` module and its scope stack (openScope/registerParams/closeScope) instead of the homegrown exprType/localTypes — locals no longer leak across procs. - nifmodules: drop hasLocalDecl/hasResolvableDecl; the codegen now requires a consistent input set and calls getDeclOrNil directly, like jslayout and the C backend. - lengc: remove the `js` subcommand; lengjs is the standalone plugin. - allocator: build the stdlib with -d:nimNativeAlloc so Nim's own alloc.nim runs over mmap/munmap page shims (no more mi_* bump). Fixes two bugs the allocator is first to hit: 32-bit atomic slots (+ exchange_n/compare_exchange_n) and unsigned 32-bit bit/shift ops that were emitting signed int32. - tests/README: rewrite for the typed-array / linear-memory model. Full jsbackend suite green (13/13) on the native allocator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the review in jsnif tags by construction. Dropped both typenav. nifmodules. Removed lengc Native allocator. Done for real: the stdlib builds with
README rewritten for the typed-array / linear-memory model. |
Adds the value-marshalling layer a DOM/host binding will sit on. A JsValue is an opaque integer handle into a runtime-side value table (generalising the _fns proc-pointer table), so real JS values (strings, objects, functions, DOM nodes) can cross the linear-memory boundary that native Nim data can't. - runtime.js: JS-value handle table (_jsNew/_jsRelease), string marshalling both ways (UTF-8, stateless — JS strings are immutable so read-back re-encodes the handle), number/bool bridges, JS `===`, and global/property/method access keyed by JS-string handles (so member names ride the same path, no C consts). - tests/jsbackend/jsffi.nim: ergonomic surface over the importc seam — toJs/ toStr/toInt/toBool, global/get/set/call, `==` as `===`, isNil, newJsObject. - tests/jsbackend/tffi.nim: end-to-end test — console.log a marshalled string, Math.max round-trip to a Nim int, string JS->Nim, build an object + set/get + JSON.stringify, bool round-trip, nil-for-missing-property, `===` across handles. - README: interop section documenting the handle model and its current limits (handles are not yet GC-integrated; callbacks are the next increment). Suite 14/14 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed a follow-up commit ( What it does. A The design question for you. The header comment in Known limits (deliberate, documented). Handles are not yet GC-integrated — a kept value must be Happy to adjust the direction before building the DOM layer on top. |
Builds on the interop foundation toward a DOM binding: event handlers. - A Nim proc used as a value already lowers to an integer `_fns` table index, so `toJs(someProc)` (runtime `_fnToJs0`/`_fnToJs1`) wraps that index in a JS function that marshals each incoming JS argument to a handle, calls the Nim proc, and releases the transient argument handle when it returns (an event object is valid only for the duration of dispatch — the DOM contract). - `newOf(ctorName[, arg])` — `new globalThis[ctorName](...)` construction. - tests/jsbackend/tevent.nim: registers a Nim proc as an `addEventListener` handler on a real host `EventTarget` (the DOM event base class), dispatches an `Event` twice, and the handler fires back into Nim, reads `event.type`, and keeps Nim-side state across dispatches. Suite 15/15 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
That does not matter, your analysis is correct. |
Makes JsValue own its runtime table slot so interop stops leaking. A JsValue is
now a one-field {h: int32} object carrying ARC hooks:
- =destroy releases the slot (runtime guards handle 0 = nil -> no-op);
- =dup / =copy allocate a NEW slot to the same JS value, so every copy is
independently owned and there is no double free.
Transient values (a dropped method result, a member-name handle) are reclaimed
at scope exit with no manual release, so the wrappers no longer hand-release.
The FFI seam is refactored onto plain int32 handles so no owning JsValue ever
crosses an importc (which would silently =dup and leak). The callback wrapper
(_fnToJs1) now builds a {h} object in linear memory and passes its offset — the
ABI a Nim `proc(ev: JsValue)` expects — and releases the handle after the
(borrowing) callback returns.
- runtime.js: _jsvDup (new slot, same value), _jsvLive (leak-test counter),
_fnToJs1 builds the JsValue object for the callback ABI.
- tests/jsbackend/tgc.nim: 1000-iteration loop with zero table growth; a copy is
an independent slot to the same value (=== true, live count +2).
Suite 16/16 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the JS-value interop surface with the two value shapes a state / DOM library needs beyond scalars and objects: - floats: `toJs(float)` / `toFloat` — a Nim float is already a JS Number on the `--bits:32` target, so they reuse the existing number bridge (no new runtime function). - JS arrays: `newJsArray`/`add`/`[]`/`[]=`/`len`. An `arr[i]` read interns a fresh owning handle; `push`/set hand the array its own reference to the value, so releasing the Nim handle afterwards never disturbs the array (JS GC keeps the value alive as long as the array does). New `tarray` test exercises float round-trip, array build/index/mutate from Nim, and handing the array to `JSON.stringify` and `Array.prototype.join`. Suite 17/17. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the value queries and variadic call a DOM binding relies on: - introspection: `jsTypeof` (JS `typeof`), `hasProp` (`name in obj`), `instanceOf` (`v instanceof globalThis[ctor]`) — branch on a value's kind, test a property, distinguish e.g. an Array from a NodeList. - `apply(obj, name, args)`: a method call with any number of arguments (the variadic counterpart of the fixed `call0..3`), marshalling the args through a JS array. New `tintro` test covers typeof across kinds, instanceof against a global constructor, property presence, and `Math.max.apply` over a five-element argument array (also exercises an array-of-JsValue literal through openArray). Suite 18/18. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First real DOM layer on top of jsffi. `dom.nim` is a thin, typed facade over the interop primitives — Document/Element/Node/Event, createElement, createTextNode, appendChild/removeChild, getElementById/querySelector, textContent/innerHTML/id/tagName, setAttribute/getAttribute, and addEventListener/dispatchEvent — the reference shape a WebIDL/MDN-driven generator will later emit automatically. Testing it needs a DOM environment (bare Node has no `document`), so the suite gains an optional per-test `<test>.env.js` preamble: `tdom.env.js` stands up a real WHATWG DOM via jsdom and exposes it as globals. jsdom is a dev-only dependency (package.json; node_modules git-ignored, not vendored) — run `npm install` in tests/jsbackend to enable the DOM test. The runner prepends the env file and puts node_modules on NODE_PATH; a test whose env deps aren't installed is SKIPPED, not failed, so a bare checkout stays green (verified). `tdom` builds a <ul> of <li> from Nim data, queries it back, wires a Nim proc as a click handler, and dispatches two clicks — validated against jsdom's real DOM semantics. Suite 19/19 (18 without jsdom, tdom skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spec-backed bindings from official data. `gen/idl2nim.js` reads the curated WebIDL that @webref/idl ships (the W3C/WHATWG interface data), parses it with the standard webidl2 parser, and emits a jsffi binding in the exact style of the hand-written dom.nim: interface -> JsValue alias, attributes -> get/set procs, operations -> call procs, WebIDL types mapped through jsffi marshalling (DOMString->string, boolean->bool, integers->int, double->float, other interfaces->JsValue). Unsupported constructs (optional/variadic args, static ops, overloads) are emitted as `## SKIPPED` comments so the gap is visible. `weburl.nim` is the checked-in output of `node gen/idl2nim.js url URL weburl.nim`; `turl` constructs a URL, reads/writes attributes, reaches the URLSearchParams attribute, and calls toJSON — all correct against Node's native URL, so it needs no DOM env. Proves IDL -> Nim codegen runs end-to-end. jsffi gains `newOf(name, openArray[JsValue])` (Reflect.construct) so generated constructors take any arity. webidl2/@webref/idl are dev-only like jsdom; the generated .nim is committed. Suite 20/20. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Grow the WebIDL generator past the all-simple URL case to the constructs real
DOM interfaces lean on:
- optional arguments -> one Nim overload per arity (WebIDL guarantees optionals
trail), so `new URL(url)` / `new URL(url, base)` and `toggle(token)` /
`toggle(token, force)` all exist. weburl.nim regenerated to gain the 2-arg
URL constructor.
- a variadic tail -> an `openArray` parameter marshalled into a JS array and
spread via the new jsffi `applyArgs`, so `classList.add("a", "b")` works.
New generated `domtokenlist.nim` (from dom.idl) and `tclasslist` drive
element.classList against jsdom: variadic add/remove, both toggle overloads,
contains/item/replace/length/value — all correct. jsffi gains `applyArgs`
(apply with a pre-built arg array). Suite 21/21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WebIDL->Nim generator now merges an interface's full surface: the inheritance chain (Element->Node->EventTarget) plus every mixin glued on by `includes` (ParentNode/ChildNode/NonDocumentTypeChildNode/Slottable), most-derived-winning on name clashes. A single generated `Element` (element.nim, 103 members) is self-contained -- appendChild from Node, addEventListener from EventTarget, append/querySelector/children from ParentNode, remove from ChildNode -- all validated end-to-end against jsdom by the new telement test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generator now takes several interfaces and emits ONE module in which interface-typed members are typed by name: document.createElement/getElementById/ querySelector return Element, ownerDocument returns Document, appendChild takes/returns Node -- typed DOM-tree navigation from a single import (domlib.nim = Node/Element/Document/DocumentFragment). Since every interface is a JsValue alias, a shared flattened member (appendChild) collapses to one Nim signature, so emission dedups globally. A member that would return-type-collide with a jsffi accessor verb (get/set/call/apply) is skipped; distinct-shape verbs like DOMTokenList.add(openArray) are kept. New tdomlib validates typed navigation against jsdom; single-interface files regenerated to match. 23/23. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Status update — the binding layer on top of Since the DOM binding + a real test environment. Spec-backed generator (
Each generated binding is exercised end-to-end — All of this is application-layer on top of the backend — no codegen changes — so it doubles as a soak test of the Typed-Array/linear-memory model: the interop handle table, strings crossing as UTF-8, Nim procs used as JS callbacks, and the ARC-managed handles all hold up across a realistic binding surface. Nothing needed from you here — flagging for visibility. |
Add three end-to-end tests proving real standard-library modules compile
and run unchanged through the JS pipeline (nimony c --bits:32 -> lengjs ->
node), with no backend-specific code:
ttables - std/tables: initTable, []=, getOrDefault, in, len, pairs
tsets - std/sets: HashSet incl/excl, intersection, in-place union
and difference, contains, len
tstrutils - std/strutils: split, strip, replace, repeat, case mapping,
startsWith/endsWith, contains
Each lowers to the object/seq/string shapes the backend already covers, so
the JS target inherits the stdlib for free as coverage grows. README Scope
note updated. Suite 26/26 green.
Two fixes that together let real string-keyed programs (Tables/HashSets, sorting) run correctly end-to-end; both surfaced building a word-frequency counter (new twordcount test: strutils + tables + algorithm). 1. Fixed-width wrapping arithmetic (jscodegen binTyped). A <=32-bit +/-/* was emitted as a plain JS `a op b`, which keeps Number's full (up to ~2^64) result instead of wrapping to the type width. Nim's sized-int arithmetic wraps mod 2^w, and — critically — a value kept in a JS register must equal the SAME value after it round-trips through a 32-bit memory slot. A string hash (`h*prime+c`, all int/uint32) that exceeded 2^32 in-register got truncated when stored as the slot's `hcode`, so the stored-vs-recomputed hcode fast-reject mismatched and string-keyed Table lookups silently created duplicate entries (a 15-word text -> 15 buckets for 8 distinct words). Now +/-/* on akI8/16/32 + akU8/16/32 coerce to the exact width; `*` uses Math.imul (correct low-32-bit product — a plain a*b loses bits above 2^53). tintwrap is the direct regression guard. 2. String SWAR comparison intrinsics (runtime.js). system/stringimpl.nim importc's __builtin_ctzll/__builtin_clzll/__builtin_bswap64 for its word-at-a-time string ordering; the JS codegen calls them by C name but the runtime never defined them, so any string `<`/ordering (e.g. a sort comparator) threw ReferenceError. Added the three as BigInt-based globals. Suite 28/28 green.
Three more standard-library modules running end-to-end under node: tsequtils - map/filter/all/any/count/deduplicate + foldl/mapIt/countIt toptions - Option[T]: some/none/isSome/isNone/get/get(default) tmath - sqrt/floor/ceil/round/pow/trunc/gcd/isPowerOfTwo/... sequtils and options need no runtime support (pure Nim over the buffer). std/math importc's the libm functions (sqrt, floor, pow, ...) by their C names, which the runtime never defined; added JS shims in runtime.js mapping them onto Math.*, with the libm-specific cases spelled out: round is half-away-from-zero (not Math.round's half-up), fmod is %, copysign/signbit honour the sign of -0. float32 (…f) variants share the double routine. Note: none[T]() takes its type via a bracket, not none(int). Suite 31/31 green.
|
Progress update — the standard library now runs end-to-end on the JS backend Since the last update, the linear-memory model has proven out where it matters most: real
…plus a word-frequency counter combining Two fixes fell out of composing those into real programs — the kind of thing isolated fixtures miss:
The JS backend suite is at 31/31 green (each test is a real program compiled and run under node, stdout diffed against a golden). Commits So the linear-memory thesis now covers objects, arrays, strings (SSO + heap), seqs, refs, closures, exceptions and a real slice of the standard library on top — all "just more Nim over the one |
|
Relocated to nim-lang/jantak — closing. Per Araq's decision to house the web-targeting backends in their own repo rather than in-tree, the JavaScript backend now lives in nim-lang/jantak, imported in nim-lang/jantak#2 (together with the WebAssembly backend from #2118). Same code, rebased onto jantak and made to consume nimony as a sibling checkout via It uses only stock nimony NIF APIs; I verified all three plugins ( Thanks for the reviews — carrying the discussion over to jantak. |
|
I've closed this and withdrawn the branch. This code is not being contributed here — it lives in my own repository under my own license. Please do not reopen or publish it. Thanks. |
What
A JavaScript backend for Nimony/lengc, built as a standalone
{.build.}-schedulable plugin over the Leng IR — a third codegen alongsidethe existing C and LLVM backends. It follows the Typed-Array / linear-memory
direction @Araq laid out in this thread: JS consumes the same fully-lowered
Leng as the C backend rather than a separate, GC-native semantic model, so
cast, unions, and low-level libraries behave identically across targets andthe two backends can never diverge.
The model
ArrayBufferis the memory. A pointer is an integer byte offsetinto it.
object/array/string/seq/refare laid out as bytes atC-ABI offsets — so
castis transparent and there is only one pointerrepresentation.
jslayout.nim) computessizeof/alignment/fieldbyte-offsets from the Leng type grammar. The C and LLVM backends both defer
struct layout to their toolchains, so this is the one genuinely new piece the
Typed-Array model needs.
mmap/munmapshims —allocFixed/dealloc/seq growth/ARC atomics all comefrom the pipeline; the runtime provides only the primitives (
mmap, stdio,the function table).
jsniftree, not by string concatenation: aJsTagvocabulary built over nifcore's
TokenBuf, a tinyjsnif → jsprinter (theonly place text is produced), and a bottom-up peephole pass that folds the
x + 0/x * 1/ constant arithmetic the byte-pointer lowering emits.lengjs <module.c.nif> <out.js>emitsone artifact per module (the
{.build.}contract);jslink <manifest> <out>is the
{.bundle.}link step — the Ghast-style decoupling.What runs end-to-end
Real Nim →
nimony c→ Leng →lengjs→ Node, correct results, 0 TODOs on theuser module:
if/while/case, enums, tuples,for-loops, sets, variant objects;
ref/heap (with ARC refcount + destructors), andinheritance (base embedded at offset 0, inherited fields, vtable dispatch);
echo "…"prints, long strings > 14 chars work);@[…],addwith reallocating growth,len, indexing;_fns[]);ErrorCodeABI (try/except/finally, nested,in-loop,
except T as e) and the heapref object of Exceptionpath. Loweredvia @Araq's
jmp/lab→breakin labeled blocks — no relooper, sinceHexer's
jmpis forward-only and scoped.Verification
tests/jsbackend/run.shgolden-diffs hand-authored, self-contained Leng modules(no system deps) against
*.expected.jsand runs each under Node — 22 checks.Broader coverage is validated by compiling real programs through the full
toolchain and bundling with the canonical runtime. The C and LLVM backends are
unaffected (a new plugin + tests only).
Still open (not guessed in code)
{.build.}/{.bundle.}wiring — how every module is routedthrough
lengjsand a singlejslinkscheduled without hand-annotating eachmodule.
importjs/jsstringinterop layer (native Typed-Array data vs. realJS values / a handle registry) — on hold until those pragmas exist in sem;
nothing in-tree references them yet.
🤖 Generated with Claude Code