Skip to content

fix(compiler)!: stabilize JS constructor tags across modules - #1488

Open
oxura wants to merge 2 commits into
bendlang:mainfrom
oxura:fix/1105-stable-js-constructor-tags-20261011
Open

oxura wants to merge 2 commits into
bendlang:mainfrom
oxura:fix/1105-stable-js-constructor-tags-20261011

Conversation

@oxura

@oxura oxura commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A constructor's JS tag currently contains the namespace assigned by the loading book. A value exported by a.bend has tag Counter; a module importing the same type expects a.Counter. Passing the first module's value to the second rejects at the boundary instead of returning 4n.

Fixes #1105.

Change

  • One compiler-local constructor-tag contract: the constructor's name inside its declaring file, without the loading file's namespace prefix.
  • Apply it consistently to JS constructor emission, type-local matches, constructor CID(...) expansion and Nat/ADT marshalling. Foreign-def effect identities and public def export keys retain their full namespaces.
  • Keep qualified pure-main display names independently of the internal tag, so JS and native C still print a.Counter{5n}.
  • Add an actual cross-library consumer regression: a Nat record, both Nat sum arms, both non-Nat enum arms, and rejection of an unknown tag. It builds two real modules and uses one module's values in the other.
  • Restore tests/io/marshal_tag_unknown with a real effect-result fail-stop assertion: a valid Box is consumed and prints 6, then an unknown tag must terminate with status 1 before a second consumer output. A semantic wrapper removes the old diagnostic-wording pin rather than re-pinning the changed sentence.
  • Remove the GUIDE edit as requested; document the public tag cutover in the changelog instead.

This is a clean cutover: manually manufactured old book-qualified JS tags are not compatibility aliases. Constructor tags are file-local; constructor names explicitly qualified within a source file remain qualified. No edit to the protected language checker, gates, allow list or caps.

Evidence

Linux x64: Bun 1.3.13, Node 22.23.1, clang 22.1.8.

  • Current main baseline, emitted .mjs in Node: the producer value and short host tag both fail in B.dec; only manually adding a.Counter returns 4n.
  • This branch, real emitted modules in Node: producer value and short tag both return 4n; the obsolete qualified tag is rejected.
  • Permanent semantic regression fails on main and prints 0 on this branch. It covers cross-module record/sum/enum behavior and unknown-tag refusal. Its Node execution also prints 0; compilation uses the actual Bun compiler rather than asking Node's strip-only TypeScript mode to execute the CLI.
  • 20 source-CLI boundary/namespace consumers matched their existing expected outputs/statuses, including deep values, closures, erased fields/binders, native forms, BigInt conversion and scalar validation.
  • Standalone Bun/Node namespace-capture and imported-nullary-constructor cases retain None / 6 and off / on respectively.
  • Pure imported-record display matches in actual standalone Bun, Node and native C: a.Counter{5n}.
  • Repository gate 54/54, permanent caps unchanged.
  • Maintainer follow-up: both permanent tests now execute successfully in the source interpreter, emitted standalone Bun/Node programs, and the installed compiled Bend CLI. Bun interpreter/JS and compiled CLI runs use an empty PATH and still print 0; they invoke their actual running executable, not a named bun. Node retains its existing requirement that Bun be available to compile Bend source.

Limits

Mini-cluster and GPU gates were not run. Native code generation is unchanged; the native smoke checks display agreement, not a GPU qualification. No claim that constructor tags are globally unique across different datatypes: matching remains type-local, while effect identities remain namespace-qualified.

@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

Thanks @oxura. A few notes on this one:

The GUIDE is supposed to be written by hand, so please drop the GUIDE.md edit. The wording there could improve, but we'll do that ourselves.

Two problems with the tests:

  1. The new test, tests/io/constructor_interchange, fails on our cluster test gate (check, interp and js lanes: "Error: null", exit 1). It spawns bun by name, and bun is not on PATH there.
  2. Please keep tests/io/marshal_tag_unknown instead of deleting it. It still covers the effect-result side of the seam, the message and the exit code. Only its expected line needs to change.

We'll discuss the tag change itself with @nicolas-abril before deciding on this PR.

@oxura

oxura commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 059c4ce:

  • The GUIDE edit is removed entirely; the file matches main again. The public cutover note is in the changelog instead.
  • In the Bun gate lanes, constructor_interchange now invokes process.execPath, not bun from PATH. The installed compiled Bend CLI invokes itself without a TypeScript entry argument. Node keeps its existing source-compilation requirement that Bun be available.
  • tests/io/marshal_tag_unknown and its original valid/invalid effect producers are restored. It exercises the effect-result unmarshalling seam, not just the JS module-argument seam: the valid result prints 6; the invalid tag must stop with status 1 before another consumer output. I kept this as a semantic status wrapper rather than pinning the changed human-readable diagnostic sentence; the old wording expectation is not restored.

Actual local verification: both fixtures print 0 in interpreter, standalone Bun JS, Node JS and installed compiled-CLI runs. Interpreter/Bun JS/compiled-CLI runs pass with empty PATH. The actual namespaced --checkup lane also prints 0 for both with empty PATH. Repository gate: 54/54. I have not rerun the mini-cluster/GPU gates.

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

Labels

None yet

Projects

None yet

2 participants