language: the rule for a prelude name that collides with an imported name #50

Closed
opened 2026-10-05 21:07:14 +00:00 by orion · 8 comments
Collaborator

Context

If an imported name and a prelude name collide, adding to the prelude breaks programs. The answer decides whether the prelude may grow in a minor release. A 1.0 blocker.

Source

Acceptance criteria

  • The rule decided and written into the language reference (the owner signs off the design)
  • The checker implements it
  • Tests for a collision

Dependencies

None known.

Moved from the planning lists of the docs; the design record stays the specification.

## Context If an imported name and a prelude name collide, adding to the prelude breaks programs. The answer decides whether the prelude may grow in a minor release. A 1.0 blocker. ## Source - [RELEASE.md 1.0 list](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/RELEASE.md#L119) ## Acceptance criteria - [ ] The rule decided and written into the language reference (the owner signs off the design) - [ ] The checker implements it - [ ] Tests for a collision ## Dependencies None known. _Moved from the planning lists of the docs; the design record stays the specification._
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop), source RELEASE.md 1.0 list, not a duplicate. It decides a language rule (prelude name vs imported name), so refine has to read CONCEPT and the language reference first; the loop decides the technical design itself and escalates only if the rule changes a published contract. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z

[loop:torbscript] intake: in scope (author orion, label loop), source RELEASE.md 1.0 list, not a duplicate. It decides a language rule (prelude name vs imported name), so refine has to read CONCEPT and the language reference first; the loop decides the technical design itself and escalates only if the rule changes a published contract. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261012 until 2026-10-12T18:08Z

[loop:torbscript] claim refine by session:loop-torbscript-20261012 until 2026-10-12T18:08Z
Author
Collaborator

[loop:torbscript] decided by the loop: the prelude ranks below everything a file wrote or imported, in every lookup. A new prelude name, member or trait can then never break or silently change a program, so the prelude may grow in a minor release (additions only, as RELEASE.md section 3 already promises for std).

What the checker does today (read from main d679af0e)

  • Names (types, functions, constants, traits): already file-first. Binder.addPreludes (compiler/src/semantics/scope.trb) adds a prelude name only if the file has none of that name; TYPECHECKER 3.1 puts "file scope" (own declarations and imports) before "prelude", and check.test.trb has "a declaration of the file shadows the prelude". So a prelude that gains Slug never touches a file that declares or imports its own Slug. Bare cases are ranked the same way (CLOSED-TRAITS decision 6.2: "the prelude comes after the cases").
  • Implicit closure parameters: no longer an error (closures.test.trb ~211); the bare name never reads the function type. TYPECHECKER 3.1 still says it is an error, which is stale.
  • Members (the open hole): the prelude re-exports members (public use Int64.seconds from "std/time") and addPreludes merges them into every file's member imports. extensionMember (checker/member.trb) then treats a prelude-reached extension and a file-imported one as equals: the same member name for the same type is `shout` comes from `a` and from `b` at the use, and a more specific extension wins silently. So (1) if the prelude gains String.shout and the file imports String.shout from "acme/text", the program stops compiling, and (2) if the prelude gains a List-specific member while the file uses its own extend<Item> Iterate<Item> member of that name, the call silently changes meaning. The same tie exists for the members of a trait the prelude names and one the file names.

The rule

  1. A name the file declares or imports shadows a prelude name (already true; stays, and is now specified).
  2. For a member m of a type, candidates come in two classes: written or named by the file (an extension of the receiver's own package, an extension reached through the file's own use of a module or a member import, a trait the file names) and reached only through the prelude (a prelude re-export of a foreign-type extension, a trait that only the prelude names). A candidate of the first class always beats one of the second, whatever their specificity. Inside a class the current rules stay: the more specific extension wins, otherwise the ambiguity error at the use with as as the fix.
  3. The only ways a file's meaning can change with a prelude release are removals and signature changes of existing prelude entries, which are breaking changes of std and wait for a major (RELEASE.md section 3).

Why this and not the alternatives: an error for every collision would make every prelude addition a breaking change (the exact problem of #50); "prelude wins" would take away the names a user wrote; keeping specificity first leaves case (2) as a silent behaviour change, which is worse than an error. The rule is a loosening: a program that compiled today means the same afterwards, except where a file-written member now beats a more specific prelude-only one; nothing in std/ or compiler/ is allowed to change meaning (the gates show it). No published contract with real users is broken (0.x), and no design record reserves this for the owner, so the loop decides; the PR review stays the veto point.

Definition of ready

  • Goal: implement the rule above and write it down; close the 1.0 item "whether a new prelude name can break a program".
  • Design points: RELEASE.md section 2 (1.0 list, the row of this issue) and section 3 (the std row: "The prelude rule of section 2 decides how far additions only reaches for the prelude"); CLOSED-TRAITS 6.2; CONCEPT "Imports" (the extend-visibility bullets and the prelude bullet).
  • Acceptance criteria (testable):
    • extensionMember and traitMembers (compiler/src/semantics/checker/member.trb) rank file-written/named candidates above prelude-only ones, then apply specificity, then the ambiguity error. The provenance of a candidate is derived in checker/implementation.trb (visibleModulesOf, which today merges the prelude's modules and the imported ones; a module reached both ways counts as the file's).
    • Tests in compiler/tests/implementations.test.trb ("The visibility of an extend") and check.test.trb: (a) a prelude gaining String.shout and a file importing String.shout from another package: no error, the file's member is used; (b) a prelude List-specific member and a file's Iterate extension of the same name: the file's is used, and the answer does not depend on whether the prelude has the member; (c) two prelude-only candidates and two file-class candidates still report the ambiguity; (d) names: a file declaring or importing X while the prelude gains X is unchanged (declaration, import, as rename, bare case position); (e) a trait named by the file and one named only by the prelude with the same member.
    • The language reference says it in one place and the other pages point at it: CONCEPT (a "The prelude and collisions" bullet in Imports), TYPECHECKER 3.1 (step order plus the member rule; remove the stale implicit-parameter sentence), RELEASE.md section 2 (mark the row resolved with the date) and section 3's std row, the ROADMAP row of #50 (remove from "designed and not built", milestone 8 text), and any generated skills/ (torb docs skill docs skills, --check is a tier A gate).
    • torb lint: the existing rule warning about a declaration that shadows a prelude name is not extended here (follow-up only if one is missing; not part of this PR).
    • Tier A (and B if the lowering of members changes, expected not) green in forge CI; no change in the meaning of std/, compiler/ or examples/ (the suite and the fixpoint compare show it). If some std code does depend on specificity beating provenance, stop and record the case in the PR instead of special-casing it.
  • Seed impact: none. It is checker behaviour on already existing syntax; no syntax change, no new native or driver flag, no std name looked up by string.
  • Size: one PR, two Conventional Commits: feat(checker): rank file members above prelude members (code and tests) and docs: the prelude rule for collisions (reference, RELEASE, ROADMAP, regenerated skills).
  • Dependencies: none.
[loop:torbscript] decided by the loop: **the prelude ranks below everything a file wrote or imported, in every lookup. A new prelude name, member or trait can then never break or silently change a program, so the prelude may grow in a minor release (additions only, as RELEASE.md section 3 already promises for `std`).** ## What the checker does today (read from `main` d679af0e) - **Names** (types, functions, constants, traits): already file-first. `Binder.addPreludes` (`compiler/src/semantics/scope.trb`) adds a prelude name only if the file has none of that name; TYPECHECKER 3.1 puts "file scope" (own declarations and imports) before "prelude", and `check.test.trb` has "a declaration of the file shadows the prelude". So a prelude that gains `Slug` never touches a file that declares or imports its own `Slug`. Bare cases are ranked the same way (CLOSED-TRAITS decision 6.2: "the prelude comes after the cases"). - **Implicit closure parameters**: no longer an error (`closures.test.trb` ~211); the bare name never reads the function type. TYPECHECKER 3.1 still says it is an error, which is stale. - **Members** (the open hole): the prelude re-exports members (`public use Int64.seconds from "std/time"`) and `addPreludes` merges them into every file's member imports. `extensionMember` (`checker/member.trb`) then treats a prelude-reached extension and a file-imported one as equals: the same member name for the same type is `` `shout` comes from `a` and from `b` `` at the use, and a more specific extension wins silently. So (1) if the prelude gains `String.shout` and the file imports `String.shout from "acme/text"`, the program stops compiling, and (2) if the prelude gains a `List`-specific member while the file uses its own `extend<Item> Iterate<Item>` member of that name, the call silently changes meaning. The same tie exists for the members of a trait the prelude names and one the file names. ## The rule 1. A name the file declares or imports shadows a prelude name (already true; stays, and is now specified). 2. For a member `m` of a type, candidates come in two classes: **written or named by the file** (an extension of the receiver's own package, an extension reached through the file's own `use` of a module or a member import, a trait the file names) and **reached only through the prelude** (a prelude re-export of a foreign-type extension, a trait that only the prelude names). A candidate of the first class always beats one of the second, whatever their specificity. Inside a class the current rules stay: the more specific extension wins, otherwise the ambiguity error at the use with `as` as the fix. 3. The only ways a file's meaning can change with a prelude release are removals and signature changes of existing prelude entries, which are breaking changes of `std` and wait for a major (RELEASE.md section 3). Why this and not the alternatives: an error for every collision would make every prelude addition a breaking change (the exact problem of #50); "prelude wins" would take away the names a user wrote; keeping specificity first leaves case (2) as a silent behaviour change, which is worse than an error. The rule is a loosening: a program that compiled today means the same afterwards, except where a file-written member now beats a more specific prelude-only one; nothing in `std/` or `compiler/` is allowed to change meaning (the gates show it). No published contract with real users is broken (0.x), and no design record reserves this for the owner, so the loop decides; the PR review stays the veto point. ## Definition of ready - **Goal**: implement the rule above and write it down; close the 1.0 item "whether a new prelude name can break a program". - **Design points**: RELEASE.md section 2 (1.0 list, the row of this issue) and section 3 (the `std` row: "The prelude rule of section 2 decides how far additions only reaches for the prelude"); CLOSED-TRAITS 6.2; CONCEPT "Imports" (the extend-visibility bullets and the prelude bullet). - **Acceptance criteria** (testable): - [ ] `extensionMember` and `traitMembers` (`compiler/src/semantics/checker/member.trb`) rank file-written/named candidates above prelude-only ones, then apply specificity, then the ambiguity error. The provenance of a candidate is derived in `checker/implementation.trb` (`visibleModulesOf`, which today merges the prelude's modules and the imported ones; a module reached both ways counts as the file's). - [ ] Tests in `compiler/tests/implementations.test.trb` ("The visibility of an `extend`") and `check.test.trb`: (a) a prelude gaining `String.shout` and a file importing `String.shout` from another package: no error, the file's member is used; (b) a prelude `List`-specific member and a file's `Iterate` extension of the same name: the file's is used, and the answer does not depend on whether the prelude has the member; (c) two prelude-only candidates and two file-class candidates still report the ambiguity; (d) names: a file declaring or importing `X` while the prelude gains `X` is unchanged (declaration, import, `as` rename, bare case position); (e) a trait named by the file and one named only by the prelude with the same member. - [ ] The language reference says it in one place and the other pages point at it: CONCEPT (a "The prelude and collisions" bullet in Imports), TYPECHECKER 3.1 (step order plus the member rule; remove the stale implicit-parameter sentence), RELEASE.md section 2 (mark the row resolved with the date) and section 3's `std` row, the ROADMAP row of #50 (remove from "designed and not built", milestone 8 text), and any generated `skills/` (`torb docs skill docs skills`, `--check` is a tier A gate). - [ ] `torb lint`: the existing rule warning about a declaration that shadows a prelude name is not extended here (follow-up only if one is missing; not part of this PR). - [ ] Tier A (and B if the lowering of members changes, expected not) green in forge CI; no change in the meaning of `std/`, `compiler/` or `examples/` (the suite and the fixpoint compare show it). If some std code does depend on specificity beating provenance, stop and record the case in the PR instead of special-casing it. - **Seed impact**: none. It is checker behaviour on already existing syntax; no syntax change, no new native or driver flag, no std name looked up by string. - **Size**: one PR, two Conventional Commits: `feat(checker): rank file members above prelude members` (code and tests) and `docs: the prelude rule for collisions` (reference, RELEASE, ROADMAP, regenerated skills). - **Dependencies**: none.
Author
Collaborator

[loop:torbscript] refine: rule decided (the prelude ranks below everything a file wrote or imported, for names and members), acceptance criteria, tests and docs listed, no seed impact, one PR; label ready set. Tokens: about 130k. next: implement

[loop:torbscript] refine: rule decided (the prelude ranks below everything a file wrote or imported, for names and members), acceptance criteria, tests and docs listed, no seed impact, one PR; label `ready` set. Tokens: about 130k. next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261012 until 2026-10-12T22:14Z

[loop:torbscript] claim implement by session:loop-torbscript-20261012 until 2026-10-12T22:14Z
Author
Collaborator

[loop:torbscript] release implement: the worker was terminated by a gateway restart before pushing anything; nothing to keep, restarting implementation.

[loop:torbscript] release implement: the worker was terminated by a gateway restart before pushing anything; nothing to keep, restarting implementation.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261012 until 2026-10-12T22:22Z

[loop:torbscript] claim implement by session:loop-torbscript-20261012 until 2026-10-12T22:22Z
Author
Collaborator

[loop:torbscript] implement: PR #346 is open and forge CI is green (gates, tier A); the worker ended on a usage limit after pushing. next: waiting (merge gate review of PR #346)

[loop:torbscript] implement: PR #346 is open and forge CI is green (gates, tier A); the worker ended on a usage limit after pushing. next: waiting (merge gate review of PR #346)
argus closed this issue 2026-10-12 21:03:52 +00:00
Sign in to join this conversation.
No labels
loop
ready
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#50
No description provided.