package manager: keep the modules of two majors apart in lowering and back ends #325

Closed
opened 2026-10-12 08:24:57 +00:00 by orion · 6 comments
Collaborator

Context

Slice 3 of 3 of #28. After #324 (PR #360, landed) a workspace holds two majors of one package as two Package entries and the checker keeps their modules and types apart (Workspace.dependencyOf, Workspace.labelOf). Lowering and the back ends still derive symbol and path names from package.name, so two majors would collide in the output. #324 therefore added a guard at the entry of lowerWorkspace (compiler/src/ir/lower/lower.trb, packageInTwoMajors) that refuses such a program with acme/json is in the program in two majors, which build and run cannot keep apart yet (#325). This issue removes the guard and makes the names major-aware.

Design

docs/design/PROJECT.md section 6a, "Two majors in the checker, not yet in a binary".

Scope

  • One identity string per package for names and paths: a new method Workspace.identityOf(package: Package): String next to labelOf: package.name where the workspace holds one package of that name, name@major (acme/json@2) where it holds several (major from package.manifest.version; an unparsable version falls back to the bare name, like labelOf). Single-major workspaces get exactly today's strings.
  • Every place that derives a name or path from package.name goes through it:
    • ir/instantiate.trb symbolPathOf (SymbolPath package.name ...), which feeds ir/mangle.trb and with it both back ends (they see mangled names only, no package lookups of their own);
    • ir/lower/function.trb scriptNameOf and entryNameOf;
    • ir/lower/lower.trb stablePathOf (the module paths of diagnostics, #line/TORB_LOCATION, the VM manifest and debugger);
    • ir/lower/encoding.trb qualifiedNameOf;
    • reference/model.trb packageNameOf (torb doc and the site must not merge the two majors into one package).
  • Left alone on purpose: the string lookups of the toolchain by name (path.package == "std/fs", std/number, startsWith("std/"), packageNamed("std/encoding") and friends). std/* keep one version, so these stay bare and the seed path does not change.
  • Remove the guard: packageInTwoMajors and its call in lowerWorkspace, and the imports (LoweredProblem, Span, Diagnostic) if nothing else in lower.trb uses them. lowerScripts and lowerLibrary never had it; they must work on a two-major graph too through the same identity.
  • Docs: rewrite the "Two majors in the checker, not yet in a binary" paragraph of PROJECT.md 6a to the built state (naming rule, stable paths); drop the "building two majors of one package ... lowering and back ends open" item from the not-built list in RELEASE.md (line ~1573) and mark it built; the torb add page (docs/tooling/torb-add.md) gets one sentence that a graph may hold two majors and build/run keep them apart; ROADMAP "package manager" bullet notes two majors built. Regenerate skills/ only if a page that feeds it changed (docs skill --check).

Acceptance criteria

  • Lowering test on the majorsWorkspace fixture of compiler/tests/check.test.trb (move or share the helper): a program where x and y both define public fn describe(): String in acme/json 1 and 2 and the app calls both lowers (isLowered true, no problems) and the IR/mangled names of the two differ (t_acme_x2f_json_x40_1_... style: package component acme/json@1 / acme/json@2, escaped). A test asserts the emitted function, type layout and static names of both majors are present and distinct.
  • The old guard test (... cannot keep apart yet (#325)) is replaced by the positive test above; the message is gone from the tree (grep '#325' compiler docs finds nothing).
  • Bytecode and C: the same fixture emits bytecode (chunks of both majors, as bytecode.test.trb does) and C (functions and TORB_LOCATION paths of both, as emit-c.test.trb does); the module paths read acme/json@1/src/lib.trb and acme/json@2/src/lib.trb.
  • Run through the VM like coverage.test.trb/debugger.test.trb run a program (fixture written to a temp directory with the locked cache): torb run equivalent prints a value from each major; if the harness cannot place a cache, the criterion is met by the bytecode and C tests above plus the torb lock --check of the same fixture.
  • torb doc model of the fixture holds two packages named acme/json@1 and acme/json@2.
  • Single-major output is byte-identical: no existing golden, IR snapshot, bytecode or C test changes; the bootstrap fixpoint and same C on every host stay green (they compile the single-major compiler workspace).
  • Docs as listed under Scope; tools/check-commits.sh clean; ROADMAP bullet for #28 marked built; #28 closes when this lands.

Seed impact

None. Only compiler sources written in existing TorbScript change (no syntax, no native, no driver flag); the std/* lookups by name stay bare, and a single-major workspace gets the same strings as before, so the seed's output does not move. State "seed impact: none" in the PR; if a golden file changes, that is a bug in the identity rule, not an expected diff.

Dependencies

#324 (landed, PR #360). Size: one PR, one or two Conventional Commits: feat(ir): keep modules of two majors apart (identity, name derivation, guard removal, tests) and, if the diff gets large, docs: two majors of one package build and run (docs only).

## Context Slice 3 of 3 of #28. After #324 (PR #360, landed) a workspace holds two majors of one package as two `Package` entries and the checker keeps their modules and types apart (`Workspace.dependencyOf`, `Workspace.labelOf`). Lowering and the back ends still derive symbol and path names from `package.name`, so two majors would collide in the output. #324 therefore added a guard at the entry of `lowerWorkspace` (`compiler/src/ir/lower/lower.trb`, `packageInTwoMajors`) that refuses such a program with `acme/json is in the program in two majors, which build and run cannot keep apart yet (#325)`. This issue removes the guard and makes the names major-aware. ## Design [docs/design/PROJECT.md section 6a](https://git.torb.dev/torbscript/language/src/branch/main/docs/design/PROJECT.md#6a-requirements-and-resolution), "Two majors in the checker, not yet in a binary". ## Scope - One identity string per package for names and paths: a new method `Workspace.identityOf(package: Package): String` next to `labelOf`: `package.name` where the workspace holds one package of that name, `name@major` (`acme/json@2`) where it holds several (major from `package.manifest.version`; an unparsable version falls back to the bare name, like `labelOf`). Single-major workspaces get exactly today's strings. - Every place that derives a name or path from `package.name` goes through it: - `ir/instantiate.trb` `symbolPathOf` (`SymbolPath package.name ...`), which feeds `ir/mangle.trb` and with it both back ends (they see mangled names only, no package lookups of their own); - `ir/lower/function.trb` `scriptNameOf` and `entryNameOf`; - `ir/lower/lower.trb` `stablePathOf` (the module paths of diagnostics, `#line`/`TORB_LOCATION`, the VM manifest and debugger); - `ir/lower/encoding.trb` `qualifiedNameOf`; - `reference/model.trb` `packageNameOf` (`torb doc` and the site must not merge the two majors into one package). - Left alone on purpose: the string lookups of the toolchain by name (`path.package == "std/fs"`, `std/number`, `startsWith("std/")`, `packageNamed("std/encoding")` and friends). `std/*` keep one version, so these stay bare and the seed path does not change. - Remove the guard: `packageInTwoMajors` and its call in `lowerWorkspace`, and the imports (`LoweredProblem`, `Span`, `Diagnostic`) if nothing else in `lower.trb` uses them. `lowerScripts` and `lowerLibrary` never had it; they must work on a two-major graph too through the same identity. - Docs: rewrite the "Two majors in the checker, not yet in a binary" paragraph of PROJECT.md 6a to the built state (naming rule, stable paths); drop the "building two majors of one package ... lowering and back ends open" item from the not-built list in RELEASE.md (line ~1573) and mark it built; the `torb add` page (`docs/tooling/torb-add.md`) gets one sentence that a graph may hold two majors and `build`/`run` keep them apart; ROADMAP "package manager" bullet notes two majors built. Regenerate `skills/` only if a page that feeds it changed (`docs skill --check`). ## Acceptance criteria - [ ] Lowering test on the `majorsWorkspace` fixture of `compiler/tests/check.test.trb` (move or share the helper): a program where `x` and `y` both define `public fn describe(): String` in `acme/json` 1 and 2 and the app calls both lowers (`isLowered` true, no problems) and the IR/mangled names of the two differ (`t_acme_x2f_json_x40_1_...` style: package component `acme/json@1` / `acme/json@2`, escaped). A test asserts the emitted function, type layout and static names of both majors are present and distinct. - [ ] The old guard test (`... cannot keep apart yet (#325)`) is replaced by the positive test above; the message is gone from the tree (`grep '#325' compiler docs` finds nothing). - [ ] Bytecode and C: the same fixture emits bytecode (chunks of both majors, as `bytecode.test.trb` does) and C (functions and `TORB_LOCATION` paths of both, as `emit-c.test.trb` does); the module paths read `acme/json@1/src/lib.trb` and `acme/json@2/src/lib.trb`. - [ ] Run through the VM like `coverage.test.trb`/`debugger.test.trb` run a program (fixture written to a temp directory with the locked cache): `torb run` equivalent prints a value from each major; if the harness cannot place a cache, the criterion is met by the bytecode and C tests above plus the `torb lock --check` of the same fixture. - [ ] `torb doc` model of the fixture holds two packages named `acme/json@1` and `acme/json@2`. - [ ] Single-major output is byte-identical: no existing golden, IR snapshot, bytecode or C test changes; the bootstrap fixpoint and `same C on every host` stay green (they compile the single-major compiler workspace). - [ ] Docs as listed under Scope; `tools/check-commits.sh` clean; ROADMAP bullet for #28 marked built; #28 closes when this lands. ## Seed impact None. Only compiler sources written in existing TorbScript change (no syntax, no native, no driver flag); the `std/*` lookups by name stay bare, and a single-major workspace gets the same strings as before, so the seed's output does not move. State "seed impact: none" in the PR; if a golden file changes, that is a bug in the identity rule, not an expected diff. ## Dependencies #324 (landed, PR #360). Size: one PR, one or two Conventional Commits: `feat(ir): keep modules of two majors apart` (identity, name derivation, guard removal, tests) and, if the diff gets large, `docs: two majors of one package build and run` (docs only).
Author
Collaborator

[loop:torbscript] refine: slice 3 of 3 of #28, specified; next: waiting (dependency #324)

[loop:torbscript] refine: slice 3 of 3 of #28, specified; next: waiting (dependency #324)
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261013 until 2026-10-13T12:36Z

[loop:torbscript] claim refine by session:loop-torbscript-20261013 until 2026-10-13T12:36Z
Author
Collaborator

[loop:torbscript] decided by the loop: scope of #325 after #324 (PR #360, read on main 3468a08). No owner question: this is the internal naming of symbols and paths in the toolchain; the language, the lock and the published contract do not change, and single-major output stays byte-identical. The issue body is rewritten; the points below are the decisions.

  1. Guard to remove: packageInTwoMajors and its call at the top of lowerWorkspace (ir/lower/lower.trb), with its test in check.test.trb. lowerScripts and lowerLibrary have no guard and need no removal, only the same identity.
  2. One identity method, not scattered code: Workspace.identityOf(package) next to labelOf (design rule: a method, no free wrapper). Bare name for a name held once, name@major for each of several. @ cannot occur in a package name, so the mangled component (_x40_ after escaping) cannot collide with any other package, and both majors get the suffix (not only the second), so a name does not depend on which major is listed first.
  3. Why name@major and not labelOf's acme/json 2.x: labelOf is message text with a space; names and paths (#line, TORB_LOCATION, VM manifest, torb doc) want a token with no space. labelOf stays for messages.
  4. Granularity: the workspace, not the program. A name gets its suffix when the workspace (the lock) holds several packages of it, even if one program reaches only one. Taking it per program would need the program set in symbolPathOf (which only has the checker) and would make a name depend on what is built. The cost is that adding a second major to the lock renames the symbols of the first; nothing persists those names (the fixpoint compares builds of one workspace), so this is harmless. Single-major workspaces, including the compiler's own, are unchanged: seed impact none.
  5. Call sites found by reading main: ir/instantiate.trb symbolPathOf (feeds ir/mangle.trb and both back ends; backend/c and backend/bytecode have no package lookup of their own), ir/lower/function.trb scriptNameOf/entryNameOf, ir/lower/lower.trb stablePathOf (via pathsOfModules for diagnostics, C #line, VM manifest), ir/lower/encoding.trb qualifiedNameOf, reference/model.trb packageNameOf (torb doc). Not changed on purpose: the by-name toolchain lookups of std/* (path.package == "std/fs", std/number, startsWith("std/"), packageNamed("std/encoding")); std/* keep one version.
  6. Tests: at the lowering, bytecode and C level on the existing majorsWorkspace fixture, plus a VM run if the harness can place the cache; the VM and C back ends need no change of their own beyond the names. Docs and skills/ as in the issue.
  7. Size: one PR, one or two commits; implementer sonnet/high (IR and lowering, per the contract), no seed work.
[loop:torbscript] decided by the loop: scope of #325 after #324 (PR #360, read on `main` 3468a08). No owner question: this is the internal naming of symbols and paths in the toolchain; the language, the lock and the published contract do not change, and single-major output stays byte-identical. The issue body is rewritten; the points below are the decisions. 1. **Guard to remove:** `packageInTwoMajors` and its call at the top of `lowerWorkspace` (`ir/lower/lower.trb`), with its test in `check.test.trb`. `lowerScripts` and `lowerLibrary` have no guard and need no removal, only the same identity. 2. **One identity method, not scattered code:** `Workspace.identityOf(package)` next to `labelOf` (design rule: a method, no free wrapper). Bare `name` for a name held once, `name@major` for each of several. `@` cannot occur in a package name, so the mangled component (`_x40_` after escaping) cannot collide with any other package, and both majors get the suffix (not only the second), so a name does not depend on which major is listed first. 3. **Why `name@major` and not `labelOf`'s `acme/json 2.x`:** `labelOf` is message text with a space; names and paths (`#line`, `TORB_LOCATION`, VM manifest, `torb doc`) want a token with no space. `labelOf` stays for messages. 4. **Granularity: the workspace, not the program.** A name gets its suffix when the workspace (the lock) holds several packages of it, even if one program reaches only one. Taking it per program would need the program set in `symbolPathOf` (which only has the checker) and would make a name depend on what is built. The cost is that adding a second major to the lock renames the symbols of the first; nothing persists those names (the fixpoint compares builds of one workspace), so this is harmless. Single-major workspaces, including the compiler's own, are unchanged: seed impact none. 5. **Call sites found by reading `main`:** `ir/instantiate.trb symbolPathOf` (feeds `ir/mangle.trb` and both back ends; `backend/c` and `backend/bytecode` have no package lookup of their own), `ir/lower/function.trb scriptNameOf/entryNameOf`, `ir/lower/lower.trb stablePathOf` (via `pathsOfModules` for diagnostics, C `#line`, VM manifest), `ir/lower/encoding.trb qualifiedNameOf`, `reference/model.trb packageNameOf` (`torb doc`). Not changed on purpose: the by-name toolchain lookups of `std/*` (`path.package == "std/fs"`, `std/number`, `startsWith("std/")`, `packageNamed("std/encoding")`); `std/*` keep one version. 6. **Tests:** at the lowering, bytecode and C level on the existing `majorsWorkspace` fixture, plus a VM run if the harness can place the cache; the VM and C back ends need no change of their own beyond the names. Docs and `skills/` as in the issue. 7. **Size:** one PR, one or two commits; implementer sonnet/high (IR and lowering, per the contract), no seed work.
Author
Collaborator

[loop:torbscript] refine: read #324/PR #360 and main, rewrote the scope and acceptance criteria (guard packageInTwoMajors in lowerWorkspace to remove; new Workspace.identityOf = name or name@major, used by ir/instantiate symbolPathOf, scriptNameOf/entryNameOf, stablePathOf, qualifiedNameOf, torb doc; std/* lookups stay bare; back ends need only the names), decided the design points (see the decided-by-the-loop comment), one PR in one or two Conventional Commits, seed impact none, no owner question. Label ready. Tokens: ~95k, next: implement

[loop:torbscript] refine: read #324/PR #360 and `main`, rewrote the scope and acceptance criteria (guard `packageInTwoMajors` in `lowerWorkspace` to remove; new `Workspace.identityOf` = `name` or `name@major`, used by `ir/instantiate` `symbolPathOf`, `scriptNameOf`/`entryNameOf`, `stablePathOf`, `qualifiedNameOf`, `torb doc`; `std/*` lookups stay bare; back ends need only the names), decided the design points (see the decided-by-the-loop comment), one PR in one or two Conventional Commits, seed impact none, no owner question. Label `ready`. Tokens: ~95k, next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261014 until 2026-10-14T04:02Z

[loop:torbscript] claim implement by session:loop-torbscript-20261014 until 2026-10-14T04:02Z
Author
Collaborator

[loop:torbscript] implement: PR #361 opened for #325 (Workspace.identityOf, all name/path derivations routed through it, guard removed, new two-majors.test.trb, docs and skills/ updated). CI green on d8e9b575 (gates, bootstrap linux-x64, tier A/B, commit messages, pdisc; the other targets are skipped by the workflow). One red run on the first push (the VM-run test's program had no entry statement) was fixed on the same branch and squashed into the two commits. seed impact: none. Token usage: not reported by the runtime. next: review

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/361 opened for #325 (`Workspace.identityOf`, all name/path derivations routed through it, guard removed, new `two-majors.test.trb`, docs and `skills/` updated). CI green on d8e9b575 (gates, bootstrap linux-x64, tier A/B, commit messages, pdisc; the other targets are skipped by the workflow). One red run on the first push (the VM-run test's program had no entry statement) was fixed on the same branch and squashed into the two commits. seed impact: none. Token usage: not reported by the runtime. next: review
argus closed this issue 2026-10-13 22:38:29 +00:00
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#325
No description provided.