js: witnesses, trait values, shared objects and the counts under Node (slice 3 of #63) #701

Closed
opened 2026-10-30 18:22:07 +00:00 by orion · 6 comments
Collaborator

Context

Step 4 of epic #63 (JavaScript back end), slice 3 of docs/design/JAVASCRIPT-BACKEND.md section 6: the rest of the language, part 2. Slices 1 (#678, PR #679) and 2 (#684, PR #694, landed as d515ae9d) are on main: target browser-js, compiler/src/backend/javascript/{emit,body,type,natives,writer}.trb, runtime/javascript/{core,int64}.mjs, tools/conformance.sh --js, tests/conformance/js.allow, CI job conformance-js.

Source

  • docs/design/JAVASCRIPT-BACKEND.md section 6 row 3, section 2.6 (generics, traits, dynamic dispatch) and section 4 (memory: Close, copy on write, identity).
  • docs/design/DESTRUCTORS.md section 2a (when exactly a release happens), docs/design/DESTRUCTORS.md section 10 (R1, R3).
  • The C back end is the reference for what the IR means: compiler/src/backend/c/body.trb (CallWitness, TraitValue, Retain, Release, MakeUnique, releaseStatement, dropFunctionOf) and compiler/src/ir/ir.trb (WitnessTable, WitnessSource, Instruction).

Goal

The emitter, which today reports these as unsupported findings, handles:

  • Witnesses. A function whose signature.witnesses is not empty takes the tables as leading arguments (parametersOf, emitCall); Call with witnesses passes them; CallWitness is w.m<k>(self, ...) on a WitnessSource (root and steps, the nested tables of a supertrait).
  • Witness tables. One module-level const object per WitnessTable, created once, with the same property names in the same order for every table of a trait (m0, m1, ... for members, one property per slot; a slot of FunctionId(-1) is null), plus nested and drop.
  • TraitValue and Object(bounds). The pair of the value and one table per bound.
  • SharedObject. A class per shared type layout whose identity is the JavaScript object.
  • Counts of section 4. Retain, Release, EndOfScope, Clear and MakeUnique (today all printed as nothing), and the drop function that runs close().

Compiler only. Everything the program does not use stays absent from the output (a program without Close, without a MakeUnique/Write of a counted type, or without traits emits none of this).

Decided by the loop

Each follows the record or the C back end; none needs the owner.

  • Table layout. members become properties m0 ... mN in table order; nested is an array of tables (null for None); drop is the drop function of the target (or null when the target holds nothing counted). All tables of one trait are written by one generated constructor function so they share a hidden class, which makes w.m3 one inline cache (section 2.6).
  • Object representation. {v, w0, w1, ...}, one w<i> per bound in bound order, the same shape as the C back end's data plus witnessMemberNameOf(i); a call through bound i is o.w<i>.m<k>(o.v, ...). The record writes {v, w} for the single-bound case, which is w0; this issue records the generalisation. Narrowing to a supertrait is o.w0.nested[j].
  • Counts. A type gets an rc field, initialised to 1 by its constructor, if and only if it is (a) the base of a MakeUnique or of a Write anywhere in the program, or (b) a type for which ir/closing.trb answers Closing.holding (it can hold a Close object). It is one whole-program flag computed by one walk in emit.trb, passed to body.trb and type.trb; Retain is x.rc++, Release is release$T(x) which decrements and at zero runs the drop. A Retain or Release of any other type prints nothing, as today. MakeUnique of a counted base clones when rc > 1 and decrements the original, using the same generated clone() the Copy of slice 2 already uses.
  • Drop and Close order. release$T closes the held objects in the order the C drop function does (fields in reverse declaration order, elements of a FixedArray in reverse, then the object's own close() first for a shared type); read dropFunctionOf and DESTRUCTORS.md section 2a and copy the order, not the mechanism. A shared type's close() is called at most once (the drop clears the field).
  • No try/finally. A panic runs no close() (section 2.7); the drop is plain generated code.
  • Frames of tasks are not touched. Release of a task frame and cancellation are slice 7.

Files

  • compiler/src/backend/javascript/body.trb: emitCall, parametersOf, emitInstruction (CallWitness, TraitValue, Retain, Release, EndOfScope, Clear, MakeUnique).
  • compiler/src/backend/javascript/type.trb: classes for shared objects, the rc field, clone() for counted aggregates, release$T and drop functions.
  • compiler/src/backend/javascript/emit.trb: witness-table constants, the whole-program rc flag, ordering of module-level constants.
  • runtime/javascript/core.mjs: only if a small helper (a shared release of a bare counted object) is needed.
  • tests/conformance/js-witnesses-close.trb (new) with .expected and .exit, and tests/conformance/js.allow.
  • docs/design/JAVASCRIPT-BACKEND.md: row 3 marked done, "What slice 3 left out" paragraph, docs/ regenerated as torb docs requires.

Tests

  1. New program tests/conformance/js-witnesses-close.trb, printing only text and Int (no list, Show or map, which need slice 4): a generic function with a trait bound called with two types (CallWitness), a trait-typed value built from two types and called through (TraitValue, Object), a supertrait narrowing, a shared type with close() that prints its name, closed in reverse declaration order at scope end, on an early return, when held by a record field and by an array, and a copy-on-write case (a record kept by two names, one written in place; the other keeps its value). Passes on the C, VM and JavaScript legs with the same .expected and .exit files.
  2. Existing programs. Every program below that builds and runs under Node with the same .expected, .stderr and .exit files joins tests/conformance/js.allow:
    traits, trait-values, bound-methods, plain-bound, generics, generic-trait-members, trait-constants, source-trait-value, trait-typed-literals, implementation-trait-arguments, entry-cell-trait, shared-top-level-const, destructor-scope-end, destructor-early-exits, destructor-fields-and-lists, destructor-patterns-and-temporaries, destructor-conditional-temporaries, destructor-using, destructor-slice-concrete, slice-close-generic.
    A program of that list that still fails because of a construct of a later slice (a list or map native, Show, a task) stays off the list; the PR body lists each such program with the finding or the line of output that names the later slice. Programs with .allocations files are leak-style: their allocation counts are a C-leg check and are not compared on the JavaScript leg, but the close() lines they print must match.
  3. No program of js.allow is removed; the existing JS programs still pass.

Acceptance criteria

  • js-witnesses-close.trb passes on the C, VM and JavaScript legs; the close order and the copy-on-write result match the C leg byte for byte
  • js.allow grows by every program of the list above that passes, at least traits or trait-values, and at least one of destructor-scope-end or destructor-early-exits; each program left off is listed in the PR with its reason
  • A program without traits, Close or counted writes produces the same JavaScript as before (no rc, no tables): pinned by an unchanged output of javascript_first.trb
  • The design record's slice table row 3 is marked done and the "left out" paragraph says what moved on; docs regenerated; torb format --check and torb docs check green
  • tools/check-commits.sh clean; sh tools/gates.sh a and the conformance-js job green in forge CI (CI only, no local bootstrap or gates)

Seed

Compiler only: no syntax change, no new native, no std name the compiler looks up, no change of the manifest, no Architecture case. seed/ is untouched. One PR; one Conventional Commit per logical change (feat(js): ..., test(js): ...), expect two or three.

Out of scope

Slices 4 to 8: ListStorage, isSame and the kernel natives (4), floats and libm (5), structured control flow (6), tasks, Suspend, frame release and cancellation (7), .nojs markers and torb test (8). A minifier, source maps.

Dependencies

#684 (slice 2, landed as PR #694, d515ae9d).

Part of #63 (epic; do not close it).

## Context Step 4 of epic #63 (JavaScript back end), slice 3 of `docs/design/JAVASCRIPT-BACKEND.md` section 6: the rest of the language, part 2. Slices 1 (#678, PR #679) and 2 (#684, PR #694, landed as d515ae9d) are on `main`: target `browser-js`, `compiler/src/backend/javascript/{emit,body,type,natives,writer}.trb`, `runtime/javascript/{core,int64}.mjs`, `tools/conformance.sh --js`, `tests/conformance/js.allow`, CI job `conformance-js`. ## Source - `docs/design/JAVASCRIPT-BACKEND.md` section 6 row 3, section 2.6 (generics, traits, dynamic dispatch) and section 4 (memory: `Close`, copy on write, identity). - `docs/design/DESTRUCTORS.md` section 2a (when exactly a release happens), `docs/design/DESTRUCTORS.md` section 10 (R1, R3). - The C back end is the reference for what the IR means: `compiler/src/backend/c/body.trb` (`CallWitness`, `TraitValue`, `Retain`, `Release`, `MakeUnique`, `releaseStatement`, `dropFunctionOf`) and `compiler/src/ir/ir.trb` (`WitnessTable`, `WitnessSource`, `Instruction`). ## Goal The emitter, which today reports these as unsupported findings, handles: - **Witnesses.** A function whose `signature.witnesses` is not empty takes the tables as leading arguments (`parametersOf`, `emitCall`); `Call` with `witnesses` passes them; `CallWitness` is `w.m<k>(self, ...)` on a `WitnessSource` (`root` and `steps`, the `nested` tables of a supertrait). - **Witness tables.** One module-level `const` object per `WitnessTable`, created once, with the same property names in the same order for every table of a trait (`m0`, `m1`, ... for `members`, one property per slot; a slot of `FunctionId(-1)` is `null`), plus `nested` and `drop`. - **`TraitValue` and `Object(bounds)`.** The pair of the value and one table per bound. - **`SharedObject`.** A class per `shared type` layout whose identity is the JavaScript object. - **Counts of section 4.** `Retain`, `Release`, `EndOfScope`, `Clear` and `MakeUnique` (today all printed as nothing), and the drop function that runs `close()`. Compiler only. Everything the program does not use stays absent from the output (a program without `Close`, without a `MakeUnique`/`Write` of a counted type, or without traits emits none of this). ## Decided by the loop Each follows the record or the C back end; none needs the owner. - **Table layout.** `members` become properties `m0 ... mN` in table order; `nested` is an array of tables (`null` for `None`); `drop` is the drop function of the target (or `null` when the target holds nothing counted). All tables of one trait are written by one generated constructor function so they share a hidden class, which makes `w.m3` one inline cache (section 2.6). - **Object representation.** `{v, w0, w1, ...}`, one `w<i>` per bound in bound order, the same shape as the C back end's `data` plus `witnessMemberNameOf(i)`; a call through bound `i` is `o.w<i>.m<k>(o.v, ...)`. The record writes `{v, w}` for the single-bound case, which is `w0`; this issue records the generalisation. Narrowing to a supertrait is `o.w0.nested[j]`. - **Counts.** A type gets an `rc` field, initialised to 1 by its constructor, if and only if it is (a) the base of a `MakeUnique` or of a `Write` anywhere in the program, or (b) a type for which `ir/closing.trb` answers `Closing.holding` (it can hold a `Close` object). It is one whole-program flag computed by one walk in `emit.trb`, passed to `body.trb` and `type.trb`; `Retain` is `x.rc++`, `Release` is `release$T(x)` which decrements and at zero runs the drop. A `Retain` or `Release` of any other type prints nothing, as today. `MakeUnique` of a counted base clones when `rc > 1` and decrements the original, using the same generated `clone()` the `Copy` of slice 2 already uses. - **Drop and `Close` order.** `release$T` closes the held objects in the order the C drop function does (fields in reverse declaration order, elements of a `FixedArray` in reverse, then the object's own `close()` first for a `shared type`); read `dropFunctionOf` and DESTRUCTORS.md section 2a and copy the order, not the mechanism. A `shared type`'s `close()` is called at most once (the drop clears the field). - **No `try`/`finally`.** A panic runs no `close()` (section 2.7); the drop is plain generated code. - **Frames of tasks are not touched.** Release of a task frame and cancellation are slice 7. ## Files - `compiler/src/backend/javascript/body.trb`: `emitCall`, `parametersOf`, `emitInstruction` (`CallWitness`, `TraitValue`, `Retain`, `Release`, `EndOfScope`, `Clear`, `MakeUnique`). - `compiler/src/backend/javascript/type.trb`: classes for shared objects, the `rc` field, `clone()` for counted aggregates, `release$T` and drop functions. - `compiler/src/backend/javascript/emit.trb`: witness-table constants, the whole-program `rc` flag, ordering of module-level constants. - `runtime/javascript/core.mjs`: only if a small helper (a shared `release` of a bare counted object) is needed. - `tests/conformance/js-witnesses-close.trb` (new) with `.expected` and `.exit`, and `tests/conformance/js.allow`. - `docs/design/JAVASCRIPT-BACKEND.md`: row 3 marked done, "What slice 3 left out" paragraph, `docs/` regenerated as `torb docs` requires. ## Tests 1. **New program `tests/conformance/js-witnesses-close.trb`**, printing only text and `Int` (no list, `Show` or map, which need slice 4): a generic function with a trait bound called with two types (`CallWitness`), a trait-typed value built from two types and called through (`TraitValue`, `Object`), a supertrait narrowing, a `shared type` with `close()` that prints its name, closed in reverse declaration order at scope end, on an early `return`, when held by a record field and by an array, and a copy-on-write case (a record kept by two names, one written in place; the other keeps its value). Passes on the C, VM and JavaScript legs with the same `.expected` and `.exit` files. 2. **Existing programs.** Every program below that builds and runs under Node with the same `.expected`, `.stderr` and `.exit` files joins `tests/conformance/js.allow`: `traits`, `trait-values`, `bound-methods`, `plain-bound`, `generics`, `generic-trait-members`, `trait-constants`, `source-trait-value`, `trait-typed-literals`, `implementation-trait-arguments`, `entry-cell-trait`, `shared-top-level-const`, `destructor-scope-end`, `destructor-early-exits`, `destructor-fields-and-lists`, `destructor-patterns-and-temporaries`, `destructor-conditional-temporaries`, `destructor-using`, `destructor-slice-concrete`, `slice-close-generic`. A program of that list that still fails because of a construct of a later slice (a list or map native, `Show`, a task) stays off the list; the PR body lists each such program with the finding or the line of output that names the later slice. Programs with `.allocations` files are leak-style: their allocation counts are a C-leg check and are not compared on the JavaScript leg, but the `close()` lines they print must match. 3. No program of `js.allow` is removed; the existing JS programs still pass. ## Acceptance criteria - [ ] `js-witnesses-close.trb` passes on the C, VM and JavaScript legs; the close order and the copy-on-write result match the C leg byte for byte - [ ] `js.allow` grows by every program of the list above that passes, at least `traits` or `trait-values`, and at least one of `destructor-scope-end` or `destructor-early-exits`; each program left off is listed in the PR with its reason - [ ] A program without traits, `Close` or counted writes produces the same JavaScript as before (no `rc`, no tables): pinned by an unchanged output of `javascript_first.trb` - [ ] The design record's slice table row 3 is marked done and the "left out" paragraph says what moved on; docs regenerated; `torb format --check` and `torb docs check` green - [ ] `tools/check-commits.sh` clean; `sh tools/gates.sh a` and the `conformance-js` job green in forge CI (CI only, no local bootstrap or gates) ## Seed Compiler only: no syntax change, no new native, no std name the compiler looks up, no change of the manifest, no `Architecture` case. `seed/` is untouched. One PR; one Conventional Commit per logical change (`feat(js): ...`, `test(js): ...`), expect two or three. ## Out of scope Slices 4 to 8: `ListStorage`, `isSame` and the kernel natives (4), floats and `libm` (5), structured control flow (6), tasks, `Suspend`, frame release and cancellation (7), `.nojs` markers and `torb test` (8). A minifier, source maps. ## Dependencies #684 (slice 2, landed as PR #694, d515ae9d). Part of #63 (epic; do not close it).
Author
Collaborator

[loop:torbscript] ready: filed just in time from the design record's slice table (row 3) after slice 2 landed. Technical choices are recorded in the issue under "Decided by the loop".
next: ready

[loop:torbscript] ready: filed just in time from the design record's slice table (row 3) after slice 2 landed. Technical choices are recorded in the issue under "Decided by the loop". next: ready
Author
Collaborator

[loop:torbscript] claim implementation by session:loop-torbscript-20261030 @anthropic:tkoehn until 2026-10-31T01:31Z

[loop:torbscript] claim implementation by session:loop-torbscript-20261030 @anthropic:tkoehn until 2026-10-31T01:31Z
Author
Collaborator

[loop:torbscript] release implementation by session:loop-torbscript-20261030 @anthropic:tkoehn: the worker run ended without a result (the worker runtime refused its oversized output log); no branch, commit or PR was pushed, so nothing is left to pick up. The item is still ready; the next worker starts from main and keeps its command output small.
next: implementation

[loop:torbscript] release implementation by session:loop-torbscript-20261030 @anthropic:tkoehn: the worker run ended without a result (the worker runtime refused its oversized output log); no branch, commit or PR was pushed, so nothing is left to pick up. The item is still `ready`; the next worker starts from main and keeps its command output small. next: implementation
Author
Collaborator

[loop:torbscript] claim implementation by session:loop-torbscript-20261030 @anthropic:tkoehn until 2026-10-31T02:20Z

[loop:torbscript] claim implementation by session:loop-torbscript-20261030 @anthropic:tkoehn until 2026-10-31T02:20Z
Author
Collaborator

[loop:torbscript] implementation by session:loop-torbscript-20261030 @anthropic:tkoehn: worker interrupted by a gateway restart after opening PR #703 (head 950140cd, CI success, mergeable, argus requested). Release of the claim.
next: review

[loop:torbscript] implementation by session:loop-torbscript-20261030 @anthropic:tkoehn: worker interrupted by a gateway restart after opening PR #703 (head 950140cd, CI success, mergeable, argus requested). Release of the claim. next: review
argus closed this issue 2026-10-30 22:10:42 +00:00
Author
Collaborator

[orion] PR #703 merged 2026-10-30 22:10Z (argus). Slice 3 is in main; no after-merge steps or deploy in the PR. Removed the stale ready label; the issue is closed.

[orion] PR #703 merged 2026-10-30 22:10Z (argus). Slice 3 is in main; no after-merge steps or deploy in the PR. Removed the stale ready label; the issue is closed.
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#701
No description provided.