Skip to content

nanvm-lib: one source of truth for operator tests - #1489

Merged
sergey-shandar merged 18 commits into
mainfrom
claude/epic-fermi-amrbdm
Aug 12, 2026
Merged

nanvm-lib: one source of truth for operator tests#1489
sergey-shandar merged 18 commits into
mainfrom
claude/epic-fermi-amrbdm

Conversation

@sergey-shandar

Copy link
Copy Markdown
Contributor

Implements the P1 nanvm-lib/todo/single-source-of-truth-for-operator-tests.md
(deleted here), a prerequisite the MVP roadmap puts before completing the
basic operators.

Problem

Operator behaviour was written twice — tests/proof.f.ts against a JS engine,
tests/test.rs by hand against nanvm-lib — and the two drifted. The gaps were
tracked in a hand-maintained coverage table in tests/README.md, which is
exactly the kind of thing that goes stale: several rows marked "missing in Rust"
turned out to be cases nanvm-lib already handled and nobody had tested.

What changed

Operator behaviour is now described once, as data.

module.f.mjs ──> proof.f.mjs ─────────────────────────> a JS engine
   (data)    └─> rust/module.f.mjs ──> test/generated.rs ──> nanvm-lib
                    (printer)             (generated)
File Role
tests/types.ts The shape of the data: Value, Case, Group, Eq, Data.
tests/module.f.mjs Every operator case, once. eq, unaryPlus, unaryMinus, mul, stringCoercion.
tests/proof.f.mjs Runs each case through the native JavaScript operators.
tests/rust/module.f.mjs Pure printer: data → Rust.
tests/update/module.f.ts The effectful shell that writes the file.
tests/test/generated.rs Generated. One statement per case.
tests/test/harness.rs Hand-written value constructors and assertions the generated file calls.
tests/test/main.rs The former test.rs, reduced to tests with no JavaScript counterpart.

Design points worth a look:

  • rust reasons instead of a coverage table. A case nanvm-lib cannot pass
    yet carries a reason string; the generated file keeps it as a commented-out
    TODO and the JavaScript proof still runs it. Divergence is a property of a
    case, so it cannot go stale — deleting the reason and regenerating is what
    closes the gap.
  • Reference identity in eq. [] === [] is false in both languages, so
    equality cases name values in a shared list and refer to them with
    ['ref', name]; that is the only way a case can say "the same object".
  • #[rustfmt::skip] on every generated function. cargo fmt -- --check
    runs in CI, and one statement per case is the readable layout; the alternative
    is reproducing rustfmt's wrapping in the printer.
  • tests/test/main.rs, not tests/test.rs. Cargo makes every tests/*.rs
    its own target, so the generated file and the harness have to be in a
    subdirectory, whose entry point is main.rs.
  • ci-update chains the new generator, so the existing CI drift check
    (git add -A && git diff --cached --exit-code) rejects a stale committed copy
    with no CI changes.

old_eq in test.rs is gone — the tests/README.md note asking whether to
remove it as redundant is answered by the generated eq cases covering it.

What this found

String(123n) returns "0x7Bn" in nanvm-lib and "123" in JavaScript:
StringCoercion::bigint formats through Debug, which prints hexadecimal. The
two cases carry a rust reason and the fix is filed as
nanvm-lib/todo/bigint-decimal-string-coercion.md, with those cases as its
acceptance test. Everything else the old coverage table listed as missing on the
Rust side passes — the tests simply hadn't been written.

Verification

  • npx tsc — clean.
  • fjs test — 2481 pass, 0 fail (was 2356; the shared corpus is 179 JS cases).
  • npm run cov — 100% lines/branches/functions on all three new FunctionalScript modules.
  • cargo test, cargo clippy -- -D warnings, cargo fmt -- --check — clean.
  • npm run ci-update is idempotent: rerunning leaves the tree unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FyMg2KPgfJeQ5FXGXjyFmd


Generated by Claude Code

Operator behaviour was written twice — once in `tests/proof.f.ts` against a
JS engine, once by hand in `tests/test.rs` against nanvm-lib — and the two
drifted, with the gaps tracked in a coverage table that went stale.

It is now data. `tests/module.f.mjs` names every case once; `tests/proof.f.mjs`
runs each through the native JavaScript operators, and `tests/rust/module.f.mjs`
prints them as `tests/test/generated.rs`, which `ci-update` regenerates so the
CI drift check catches a stale copy. A case nanvm-lib cannot pass yet carries a
`rust` reason and is emitted as a commented-out TODO, so divergence lives in the
data instead of prose.

The generated tests found one: `String(123n)` returns `"0x7Bn"` instead of
`"123"`, filed as bigint-decimal-string-coercion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyMg2KPgfJeQ5FXGXjyFmd
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
functionalscript 0148edb Commit Preview URL

Branch Preview URL
Aug 12 2026, 03:23 PM

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyMg2KPgfJeQ5FXGXjyFmd

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Reviewed at bcef2af against main (03126e4). CI is green across all platforms.

This is a nicer shape than the coverage table it replaces, and the part that makes it work is that the generator is checkable. So that is what I checked hardest.

The generated file is what the generator produces

I deleted nanvm-lib/tests/test/generated.rs outright and regenerated from scratch:

$ rm nanvm-lib/tests/test/generated.rs
$ npm run ci-update
$ diff generated.committed.rs nanvm-lib/tests/test/generated.rs
BYTE-IDENTICAL
$ git status --porcelain
(empty)

So the committed copy is reproducible, and the CI drift check (git add -A && git diff --cached --exit-code) has something real to bite on now that ci-update chains the generator.

The generated cases actually run, and name themselves when they fail

Worth confirming rather than assuming, because cargo test reports the whole integration target as one test:

Running unittests src/lib.rs   → 110 passed
Running tests/test/main.rs     → 1 passed
Doc-tests nanvm_lib            → 5 passed

That single test is gen_test, and generated::all::<A>() is its first call — so all 179 cases execute inside it. The design only holds up if a failure is diagnosable, and it is: check_eq asserts with the case name as the message (assert_eq!(a == b, expected, "{case}")), including a "{case} reversed" for the symmetric direction.

The divergence mechanism does what the description says

This is the claim I most wanted to see hold, since it is what replaces the stale table. Traced end to end for the bigint coercion gap:

  • module.f.mjs carries the two cases with rust: hexadecimalBigint.
  • generated.rs emits them commented out, each above a // TODO: naming nanvm-lib/todo/bigint-decimal-string-coercion.md.
  • proof.f.mjs has no reference to rust at all, so it does not skip them — and the suite shows both running and passing against the JS engine:
proof.stringCoercion.bigint(): ok
proof.stringCoercion.negativeBigint(): ok
  • The todo file exists with a real problem statement pointing at StringCoercion::bigint.

Divergence really is a property of the case, and deleting the reason plus regenerating really is what closes it.

Everything else

  • npx tsc clean. fjs test2481 pass / 0 fail, matching the claimed number exactly (2356 before).
  • cargo fmt -- --check clean. cargo clippy -- -D warnings — the command as stated, and the one fjs/ci/rust/module.f.mjs generates — passes.
  • Markdown links improve: 144 broken on main, 141 here. The three fixed are the deleted todo's own links, and nothing new broke. No dangling references to the removed file.
  • .gitattributes marking generated.rs as linguist-generated=true is a good touch — it collapses the file in diffs and keeps it out of language stats.
  • CHANGELOG entry is present and correctly not marked breaking, since this is test infrastructure.

old_eq being dropped is justified: the generated eq group covers it.

Two notes on my own verification, for the record

  • I could not check npm run cov. On this machine (Node v23.11.0) node --test discovers zero test files, so a bare npm run cov prints tests 0 and then a vacuous 100.00 — and it does the identical thing on origin/main, so it is my Node version, not anything here. AGENTS.md §1.4 lists Node 22+ and CI runs 22/24/26. Flagging only so nobody reads my approval as having confirmed that particular line.
  • cargo clippy --all-targets (stricter than the stated command or CI) surfaces clippy::eq_op at nanvm-lib/src/vm/mod.rs:40assert_eq!(x, x). That is pre-existing on main and this PR does not touch nanvm-lib/src, so it is out of scope here; mentioning it only in case it is worth its own cleanup someday.

@sergey-shandar

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: bcef2af8ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sergey-shandar
sergey-shandar marked this pull request as draft August 11, 2026 13:44
Comment thread fjs/nanvm/update/proof.f.mjs Fixed

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at 2b8c739. My earlier approval was against bcef2af, and the head has since picked up two main merges (bringing in #1488 and #1490) plus the go commit.

go is the adaptation #1488 forced: fjs/effects/node migrated to .f.mjs on main, so tests/update/module.f.ts and proof.f.ts now take runtime values from effects/node/module.f.mjs and types from effects/node/types.ts, split into separate import statements. Exactly the mechanical change that migration requires, and nothing in this PR's own design moved.

Since update/module.f.ts is the effectful shell that writes the generated file, I re-ran the check that matters rather than assuming the merge was inert:

$ rm nanvm-lib/tests/test/generated.rs
$ npm run ci-update
$ diff gen.committed.rs nanvm-lib/tests/test/generated.rs
BYTE-IDENTICAL
$ git status --porcelain
(empty)

Still reproducible, still clean for the CI drift check.

Everything else re-verified at this head:

  • npx tsc clean; 2482 pass / 0 fail — one more than the 2481 I saw before, which is #1490's new btree case arriving via main.
  • cargo fmt -- --check, cargo clippy -- -D warnings clean; cargo test 110 unit + 1 integration + 5 doctests, all passing.
  • Markdown links 141 vs main's 144 — still three better, none added, 0 label/target mismatches.
  • MERGEABLE, CI green.

The two caveats from my first review still stand as written: I can't exercise npm run cov on this machine (Node 23 discovers no test files, identically on main), and the clippy::eq_op that --all-targets surfaces in nanvm-lib/src/vm/mod.rs is pre-existing and untouched here.

Comment thread nanvm-lib/tests/types.ts Outdated
* would also lose the distinction the printer depends on — `undefined` versus
* a missing property, and `function` versus anything else.
*/
export type Value =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend using non-wrapped primitives, objects, and arrays. And use functions to describe other things (including functions). See the fjs/rtti module as an example. Actually, we may use a subset of RTTI to describe a constant value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bd7228e. Operands and expectations are plain JavaScript now, with the RTTI convention that a constant is its own description:

{ name: 'arrayNumber', args: [[2.3]], expected: 2.3 },
{ name: 'emptyObjectByOne', args: [{}, 1], expected: NaN },
{ name: 'bigint', args: [0n], expected: throws },

Only three things a literal cannot express stay as thunks — so a function in the data is always a description, never a value that happens to be a function:

Thunk Info Means
functionValue ['function'] a function value
ref(name) ['ref', name] one of the eq shared values, so the same object reaches both sides
throws ['throw'] the case must throw; valid only as expected

Value = Const \| Special where Special = () => Info, mirroring RTTI's Const/Thunk split. Both consumers already had to walk the value, and typeof + Array.isArray recovers everything the tags carried, so nothing was lost — the generated Rust is byte-identical apart from the source path in its header comment.

One real bug fell out of the rewrite: resolving a ref by rebuilding from shared would have handed each side of a comparison its own object, which is the one thing a ref exists to prevent. build now takes a resolver returning the already-built object, and the shared values are built exactly once.


Generated by Claude Code

Comment thread nanvm-lib/tests/rust/module.f.mjs Outdated
@@ -0,0 +1,216 @@
/**

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move the module (or some parts of it) to fjs/media/rust?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — "some parts of it", split on whether the code is about Rust or about nanvm-lib. Done in bd7228e.

fjs/media/rust (new, sibling of fjs/media/nix) owns literal syntax: stringLiteral, f64Literal, i64Literal, snakeCase. None of it knows anything about this generator, and the roadmap's fjs compile <input>.rs backend will need exactly the same layer — the f64 edge cases in particular are worth writing once (-0 prints as 0 from toString; NaN/±Infinity have no literal form; a whole number needs the f64 suffix or it lexes as an integer).

fjs/nanvm/rust keeps what targets the nanvm-lib API: Nullish::Null.to_any(), check::<A>(…), the #[rustfmt::skip] layout. That is not media-type knowledge, it is knowledge of one crate's surface.

I stopped short of putting an expression/item layer in fjs/media/rust — there is only one consumer today, so the right shape for it isn't visible yet, and the code generator will be the one to reveal it.


Generated by Claude Code

@sergey-shandar

Copy link
Copy Markdown
Contributor Author

IMHO, we should move most of the JavaScript/TypeScript files to fjs/....

…vm-lib

Three changes requested on the PR:

- Test-data operands and expectations are now ordinary JavaScript values
  rather than tagged tuples, following `fjs/types/rtti`'s convention that a
  constant is its own description. `args: [[2.3]], expected: 2.3` instead of a
  tree of constructor calls. Only the three things a literal cannot express —
  a function value, a `ref` to a shared object, and `throws` — stay as thunks.

- Rust literal syntax (string escaping, `f64`/`i64` spelling, `snake_case`)
  moves to `fjs/media/rust`, a sibling of `fjs/media/nix`. It is not specific
  to this generator: the roadmap's `fjs compile <input>.rs` backend needs the
  same layer. The `nanvm-lib` API the statements target stays in the printer.

- The FunctionalScript sources move out of the Rust crate into `fjs/nanvm`.
  `nanvm-lib/tests` keeps only Rust: the generated file, the harness, and the
  hand-written tests.

Generated output is byte-identical apart from the source path in its header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyMg2KPgfJeQ5FXGXjyFmd

Copy link
Copy Markdown
Contributor Author

IMHO, we should move most of the JavaScript/TypeScript files to fjs/....

Done in bd7228e. nanvm-lib/ is now Rust only:

fjs/nanvm/                     the FunctionalScript side of NaNVM
  types.ts, module.f.mjs       the shared operator test data
  proof.f.mjs                  runs it against a JS engine
  rust/                        prints it against the nanvm-lib API
  update/                      writes the generated file
fjs/media/rust/                Rust literal syntax, reusable
nanvm-lib/tests/test/          main.rs, harness.rs, generated.rs

fjs/nanvm is also where the roadmap's .rs output branch of fjs compile would go, so the directory earns its name beyond the test data.

The three changes are in one commit because they interlock — moving the files changed the same import lines the value rewrite touched, and the fjs/media/rust split determined what fjs/nanvm/rust was left holding. Each is described separately in the commit message.

Worth noting as evidence the value rewrite was behaviour-preserving: nanvm-lib/tests/test/generated.rs came out byte-identical apart from the source path in its header comment.

Verified at bd7228e: npx tsc clean; fjs test 2484 pass / 0 fail; cargo test 110 unit + 1 integration + 5 doctests; cargo fmt -- --check and cargo clippy -- -D warnings clean; npm run cov 100% lines/branches/functions on all four new modules; npm run ci-update idempotent.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at bd7228e. Address review is a real restructure, not a touch-up, so I re-ran everything rather than trusting the earlier pass.

The move is the right call and it is complete

FunctionalScript sources leave the Rust crate: nanvm-lib/tests/{module.f.mjs, types.ts, rust/, update/, proof.f.mjs} become fjs/nanvm/…, with the generic Rust printer split out to fjs/media/rust/. nanvm-lib/ is now only Rust plus the generated file and its harness, which is the boundary you would want.

I checked the move left nothing behind: no reference anywhere — markdown, JSON, .mjs, .ts, or .rs — still points at the old nanvm-lib/tests/ FunctionalScript paths. ci-update follows to ./fjs/nanvm/update/module.f.ts.

The generator still round-trips from its new home

The thing most likely to break in a move like this:

$ rm nanvm-lib/tests/test/generated.rs
$ npm run ci-update
$ diff gen.committed.rs nanvm-lib/tests/test/generated.rs
BYTE-IDENTICAL
$ git status --porcelain
(empty)

And the diff on generated.rs and harness.rs between the two heads is only the provenance comment (nanvm-lib/tests/module.f.mjsfjs/nanvm/module.f.mjs). Not one generated statement changed, so the restructure is genuinely a relocation rather than a rewrite of what gets tested.

Re-verified at this head

  • npx tsc clean; 2484 pass / 0 fail — up from 2482, the two new proofs being fjs/media/rust and fjs/nanvm/rust. 195 cases now run across the fjs/nanvm and fjs/media/rust proofs.
  • cargo fmt -- --check and cargo clippy -- -D warnings clean; cargo test 110 unit + 1 integration + 5 doctests, all passing.
  • Links 141 against main's 144 — still three better, none added, 0 label/target mismatches.
  • MERGEABLE, CI green.

Both caveats from my first review are unchanged: npm run cov stays unverifiable on this machine (Node 23 discovers no test files, identically on main), and the clippy::eq_op in nanvm-lib/src/vm/mod.rs that --all-targets surfaces is pre-existing and still untouched here.

@sergey-shandar

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: cb4f230f53

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at cb4f230. The only change since bd7228e is the main merge that brings in #1491 — no new work of its own — so this pass was about confirming the merge didn't break anything rather than re-reviewing the design.

The merge touched package.json from both sides and resolved correctly, which is the one thing here that could have gone quietly wrong: #1489 repoints ci-update at the relocated generator while #1491 repoints dev-update and index-html at migrated modules. All three survived:

ci-update:  node ./fjs/module.ts ci && node ./fjs/module.ts r ./fjs/nanvm/update/module.f.ts
dev-update: node ./fjs/module.ts r ./fjs/dev/update/module.f.mjs
index-html: node ./fjs/module.ts r ./fjs/website/module.f.mjs

A wrong resolution here type-checks and tests clean, so I ran them: dev-update and index-html both execute, and ci-update still round-trips the generator byte-identical with a clean tree.

Re-verified at this head:

  • npx tsc clean; 2484 pass / 0 fail.
  • Generator: rm generated.rs && npm run ci-update reproduces it byte-for-byte; git status empty.
  • cargo fmt -- --check and cargo clippy -- -D warnings clean; cargo test 110 + 1 + 5 passing.
  • Links 141 against main's 144 — still three better, none added, 0 label/target mismatches.
  • MERGEABLE, CI green.

Same two caveats as before, unchanged: npm run cov is unverifiable on this machine (Node 23, identical behaviour on main), and the pre-existing clippy::eq_op in nanvm-lib/src/vm/mod.rs is untouched here.

#1491 migrated fjs/dev/update — the module this one is modelled on — so the
writer's whole runtime closure is now .f.mjs and nothing blocks it. Shipping a
new authored .f.ts implementation would work against the stage-1 migration.

The proof stays .f.ts: it needs `virtual` from effects/node/virtual, which is
still TypeScript. Same split fjs/dev/update has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyMg2KPgfJeQ5FXGXjyFmd
Comment thread fjs/nanvm/update/proof.f.mjs Fixed

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at e48f98c. One commit: the generated-tests writer itself becomes .f.mjs, with ci-update repointed to match.

That is the one file in this PR whose migration could break the whole mechanism, so the round-trip is the check that matters:

$ rm nanvm-lib/tests/test/generated.rs
$ npm run ci-update
$ diff gen.committed.rs nanvm-lib/tests/test/generated.rs
BYTE-IDENTICAL
$ git status --porcelain
(empty)

The writer runs from its new extension and reproduces the committed file exactly, so the CI drift check still has teeth.

  • npx tsc clean; 2484 pass / 0 fail.
  • cargo fmt -- --check clean; cargo test 110 + 1 + 5 passing.
  • package.json's ci-update follows to ./fjs/nanvm/update/module.f.mjs; dev-update and index-html are untouched and still resolve.
  • MERGEABLE.

Caveats unchanged: npm run cov remains unverifiable here (Node 23, same on main), and the pre-existing clippy::eq_op in nanvm-lib/src/vm/mod.rs is untouched.

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at 348bf4c. Two main merges since e48f98c (pulling in #1492 and #1493) and no new work of its own, so this was a check that the merges left the mechanism intact.

  • Generator still round-trips: rm generated.rs && npm run ci-update reproduces it byte-identical, git status empty.
  • npx tsc clean; 2484 pass / 0 fail.
  • cargo test — all three targets green.
  • MERGEABLE.

#1492 landed the EOF = -1 documents and #1493 the fjs/ci migrations; neither disturbs anything here.

sergey-shandar and others added 2 commits August 11, 2026 21:09
#1494 migrated fjs/effects/node/virtual, so the writer's proof could no longer
resolve its `virtual` import and every job failed on tsc. The proof's whole
runtime closure is now .f.mjs, so it moves rather than just repointing —
fjs/nanvm holds no authored TypeScript beyond types.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyMg2KPgfJeQ5FXGXjyFmd
assertEq(result, generate(data))
},
main: () => {
const [, result] = virtual(emptyState)(main(defaultNodeProgramOptions))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same finding as #discussion_r3761689209, re-raised because the file was renamed to .f.mjs. Answered there: NodeProgram requires the argument, so dropping it would be a type error, and fjs/dev/update has the identical shape. Still not acting on it.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 2fa156a25ee74d5d2d8ccb97fbb58f292dfb6648. My earlier approval was against
348bf4c, and the rebase onto the post-#1494 main moved enough that I re-ran the whole
battery rather than carrying it forward. Everything holds. Approving again at this head.

JavaScript side. npx tsc --noEmit exits 0 and npm run prepack exits 0 with 0 errors
from a clean tree. npm test gives 2484 pass / 0 fail against main's 2357 — the +127 being
the operator cases. Branch is level with origin/main and GitHub reports MERGEABLE CLEAN.

Generator round-trip. Deleted nanvm-lib/tests/test/generated.rs, ran npm run ci-update, then the repository's own drift gate — git add -A && git diff --cached --exit-code — came back clean. Regeneration is byte-identical, so CI's drift check will pass
and the checked-in file genuinely is the generator's output.

Rust side, using CI's exact invocations: cargo fmt -- --check clean, cargo test green
(110 + 1 + 5), cargo clippy -- -D warnings clean, cargo clippy --release -- -D warnings
clean.

The two things I most wanted to confirm are behavioural rather than structural, so I tested
both rather than reading them.

The single Rust test isn't vacuous. #[test] fn test() running the whole generated suite
through one entry point could in principle pass while exercising nothing. Flipping one
expectation in generated.rszeroByNegativeZero from true to false — fails, and
names the case:

assertion `left == right` failed: zeroByNegativeZero
test result: FAILED. 0 passed; 1 failed

So harness.rs's "{case}" message does the work a per-case #[test] would have, which was
the thing that made collapsing to one test acceptable.

The single source of truth is real. Renaming one case in fjs/nanvm/module.f.mjs propagates
to both consumers after npm run ci-update — the new name appears in the generated Rust and
in the JavaScript proof run. One data edit, two consumers, no manual synchronisation, which
is exactly what the PR title claims.

One observation that is explicitly not this PR's problem, recorded only so it isn't
rediscovered later: cargo clippy --all-targets -- -D warnings fails on
nanvm-lib/src/vm/mod.rs:40 with clippy::eq_op for assert_eq!(x, x). That is pre-existing
origin/main fails identically, this branch touches no files under nanvm-lib/src, and CI
doesn't pass --all-targets, so nothing here regresses. Worth a small cleanup PR of its own
sometime.

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at 537896b07d3a69cfecd93b3cfe4f4c59fa0f56d3. The only change since the head I
approved is 537896b, a merge of main bringing in #1498 — the PR's own content is
untouched — but the merge moves the tree, so I re-ran the gates rather than assuming.

All green: npx tsc --noEmit 0, npm run prepack 0 errors from a clean tree, and npm test
2495 pass / 0 fail. That count is worth one sanity note: it's up from 2484 at the previous
head by exactly 11, which is #1498's new fjs/media/json/rtti proof arriving via the merge
rather than anything changing here.

Generator round-trip still holds — deleting nanvm-lib/tests/test/generated.rs, running npm run ci-update, and applying the repo's own git add -A && git diff --cached --exit-code
gate comes back clean, so regeneration is byte-identical.

Rust, on CI's exact invocations: cargo fmt -- --check clean, cargo test green (110 + 1 +
5), cargo clippy -- -D warnings and cargo clippy --release -- -D warnings both clean.
GitHub now reports MERGEABLE CLEAN.

I didn't repeat the two behavioural probes from my last pass — that the single #[test] names
its failing case, and that one edit to fjs/nanvm/module.f.mjs propagates to both the
generated Rust and the JavaScript proof — since neither the data, the generator, nor the
harness changed in this merge.

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at b6e5d24ca99e057e41cc38a9f27e52bef875106a. Same situation as last time: the
only new commit is b6e5d24, a merge of main picking up #1496, whose entire content is a
33-line AGENTS.md addition. The PR's own content is untouched and the branch is now level
with origin/main.

Re-ran the gates anyway since the tree moved: npx tsc --noEmit 0, npm test 2495 pass / 0
fail, generator round-trip still byte-identical under git add -A && git diff --cached --exit-code, cargo test green across all three suites, cargo clippy -- -D warnings clean.

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at f30a0538c838560a5a465b940168e54f114253fe. Same pattern as the last two
passes: the only new commit is f30a053, a merge of main picking up #1499 and #1500. The
PR's own delta is unchanged at 25 files and the branch is level with origin/main.

Gates re-run on the merged tree: npx tsc --noEmit 0, npm test 2495 pass / 0 fail, generator
round-trip byte-identical under git add -A && git diff --cached --exit-code, cargo fmt -- --check clean, cargo test green across all three suites, cargo clippy -- -D warnings clean.

sergey-shandar and others added 3 commits August 12, 2026 08:15
#1502 documents it in AGENTS.md §4: module-level `@import` tags belong in the
same JSDoc block as `@module`, not in separate comments, and a blank line
separates that block from the first import. These modules predate the rule.

Moves 6 `@import` tags into their headers across 5 files — two of them were
interleaved between runtime imports — and adds the required blank line to the
two headers that carry no tags.

Generated output is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyMg2KPgfJeQ5FXGXjyFmd

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 0148edb, previously approved at f30a053. Short, because the delta is small.

What actually changed since f30a053: two merges of main (b23693f, 0148edb, picking
up #1502 and #1501) plus one real commit, a0a8985 — a comment-only change that moves six
@import tags into the @module header block and adds the required blank line to the two
headers that had none, across fjs/media/rust/proof.f.mjs, fjs/nanvm/{module,proof}.f.mjs,
fjs/nanvm/rust/{module,proof}.f.mjs and fjs/nanvm/update/{module,proof}.f.mjs. No runtime
statement changed. Since main had moved a long way, I re-ran the gates on the merged tree
rather than assuming.

Gates, all from a freshly cleaned tree at 0148edb:

  • npx tsc --noEmit — exit 0.
  • npm run prepack — exit 0 (both passes).
  • npm test — 2495/2495 pass, 0 fail. origin/main at ede2a96 is 2368/2368; +127 is the
    new fjs/nanvm proof surface replacing the deleted nanvm-lib/tests/proof.f.ts, which is
    what this PR is for.
  • Rust, with CI's exact invocations: cargo fmt -- --check 0, cargo test 0 (incl. 5
    doc-tests), cargo clippy -- -D warnings 0, cargo clippy --release -- -D warnings 0.
  • Generator round-trip: deleted nanvm-lib/tests/test/generated.rs, ran npm run ci-update,
    then the repo's own drift gate git add -A && git diff --cached --exit-code — clean. The
    committed generated file is exactly what the generator produces.
  • §4 module header, which is the point of a0a8985: grep -c '@module' on each emitted
    declaration is 1 for all seven touched modules plus fjs/nanvm/types.d.ts, so no header was
    folded into import trivia and lost. No elided and no bare : any anywhere in the emitted
    fjs/nanvm/** or fjs/media/rust/** declarations.
  • Link check: broken-link set comparison against main, not just the count — 141 vs main's
    144, and the diff is one-directional. The three that disappear are the dangling
    tests/proof.f.ts, tests/README.md and tests/test.rs references in the now-deleted
    nanvm-lib/todo/single-source-of-truth-for-operator-tests.md. Nothing new is stranded.
  • CHANGELOG: single ## Unreleased entry linking only /pull/1489, no issue or todo/ link,
    released sections untouched.

Not verified: coverage. npm run cov reports a vacuous 100.00 over 0 tests in this Node
v23 environment, and does the same on main, so it says nothing about this PR either way.

Nothing to flag. Draft status is not a blocker per your standing instruction. Approving.

@sergey-shandar
sergey-shandar marked this pull request as ready for review August 12, 2026 16:32
@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit b76018c Aug 12, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the claude/epic-fermi-amrbdm branch August 12, 2026 16:35
Comment thread fjs/nanvm/types.ts
| readonly ['throw']

/** The operators covered by the shared data. */
export type Op = 'unaryPlus' | 'unaryMinus' | 'mul' | 'stringCoercion'

@sergey-shandar sergey-shandar Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we use the operators themselves, like '+' | '-' | ...? Also, we may have two groups of operations: unary and binary. For example, a generic operation is { readonly name: string, readonly argsN: number }

Comment thread fjs/nanvm/types.ts
* `commutative` additionally checks every case with its arguments swapped,
* which is what the hand-written tests did for `*`.
*/
export type Group = {

@sergey-shandar sergey-shandar Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can associate each operation with a number of arguments and then cases: readonly Case<Op.argsN>

check_throws::<A>("numberByBigintSwapped", bigint_any(1) * (1f64).to_any());
}

#[rustfmt::skip]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it global for the module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants