checker: From<Scalar> on a generic type is refused as overlapping the blanket From<Never> #455

Closed
opened 2026-10-24 00:34:27 +00:00 by orion · 8 comments
Collaborator

Context

Found while implementing #83 (std/gradient, PR #454). public type Dual<Scalar: Real = Float> with ..., From<Scalar> (or extend<Scalar: Real> Dual<Scalar> with From<Scalar>) is rejected:

error: `Dual<Scalar>` already implements `From<Scalar>` (in std/core/src/convert.trb)
  = Two implementations of one trait may not overlap: only different target types tell them apart

The other implementation is extend<Target> Target with From<Never> in std/core/src/convert.trb. Same family as #441 (overlap check of impls with a generic parameter), but the cause is a different one (see Cause).

Cause (refine, against main after #441 / PR #448)

reportOverlap in compiler/src/semantics/checker/implementation.trb unifies the two targets and the two trait applications over one shared substitution (unifyTerms, with occurs check, from #441). For this pair that gives Target = Dual<Scalar> and Scalar = Never and no conflict, so it reports the overlap. The overlap is real in the checker's terms: Dual<Never> has two From<Never>.

The first version of this issue assumed the bound Scalar: Real rules Never out. It does not: resolveUncached deliberately answers every bound on Never as holding (a bound on Never holds whatever the trait is, needed for Source<Item, Never>). A fix that consults bounds in the overlap check would therefore not work.

What makes the overlap harmless is the other side: From<Never>::from(value: Never) can never be called, because no value of Never exists, so two implementations that meet only at a Never source cannot be told apart by any program. Everything about the blanket is already special-cased through checker.wellKnown.from and checker.neverType (fitsType refuses it for any argument but a Never).

Fix location

reportOverlap (compiler/src/semantics/checker/implementation.trb), after the two unifyTerms calls and before reportBlanketOverlap: skip the report when one implementation is the From<Never> blanket and the other implementation's single From argument is a bare generic parameter. The solutions of that pair are exactly parameter = Never.

  • New helper (e.g. isNeverSourceBlanket): targetShapeOf(target) is .Blanket, capabilitySymbolOf(trait) == checker.wellKnown.from, and the one trait argument is checker.neverType. No new name looked up by string.
  • The exemption applies when the other trait argument is a Parameter (walk it with walkTerm). extend Foo with From<Never> written by hand stays a real overlap with the blanket; From<Int> beside From<Scalar> on one generic type stays one too (the solution Scalar = Int is not Never).
  • Only From is exempt. Other traits keep the plain rule: their members can return or use the argument type, so a Never instance is not provably unobservable.

Acceptance criteria

  • extend<Scalar> Wrapper<Scalar> with From<Scalar> (a generic type that takes its own parameter as From source, bounded or unbounded) is accepted by torb check beside the std blanket From<Never>; type Wrapper<Scalar: Real> with From<Scalar> too (the with list form).
  • compiler/tests/implementations.test.trb pins, next to the #441 tests: (a) the two forms above are accepted (problems(source).isEmpty(); the test workspace contains std/*, so the blanket is present); (b) extend Wrapper<Int> with From<Never> beside it is still refused with the unchanged message; (c) From<Scalar> plus From<Int> on one generic type is still refused with the unchanged message; (d) the existing overlap and blanket-message tests stay green unmodified.
  • A program in tests/language/ (with .expected) declares such a type, calls Wrapper.from(x) / .into() with a Scalar value and gets the right implementation under torb run and torb run --native. If call resolution reports ambiguity with the blanket for a concrete Scalar, fixing that is part of this issue; fitsType already refuses Never for any other argument.
  • Dual in std/gradient (std/gradient/src/dual.trb) gets From<Scalar> (a constant, rate zero, same as Dual.constant) with a test in std/gradient/tests; docs/design/COMPUTE.md section 4 drops the pitfall bullet ("Dual<Scalar> has no From<Scalar> ...") and names the conversion; skills/ regenerated (docs skill).
  • sh tools/gates.sh a green in forge CI (not run locally).

Commit plan (one PR)

  1. fix(checker): let a generic type take its own parameter as the source of From beside the Never blanket - the exemption, the compiler tests, the tests/language/ program.
  2. feat(std/gradient): add From<Scalar> to Dual - dual.trb, its test, the COMPUTE.md bullet, regenerated skills/.

Seed

Does not touch the seed: checker change only (no syntax, no new native, no driver flag, no name looked up by string). Commit 2 changes std/gradient, which the seed does not use. Dependencies: #441 / PR #448 (merged) and #83 / PR #454 (merged); none open.

## Context Found while implementing #83 (`std/gradient`, PR #454). `public type Dual<Scalar: Real = Float> with ..., From<Scalar>` (or `extend<Scalar: Real> Dual<Scalar> with From<Scalar>`) is rejected: ``` error: `Dual<Scalar>` already implements `From<Scalar>` (in std/core/src/convert.trb) = Two implementations of one trait may not overlap: only different target types tell them apart ``` The other implementation is `extend<Target> Target with From<Never>` in `std/core/src/convert.trb`. Same family as #441 (overlap check of impls with a generic parameter), but the cause is a different one (see Cause). ## Cause (refine, against `main` after #441 / PR #448) `reportOverlap` in `compiler/src/semantics/checker/implementation.trb` unifies the two targets and the two trait applications over one shared substitution (`unifyTerms`, with occurs check, from #441). For this pair that gives `Target = Dual<Scalar>` and `Scalar = Never` and no conflict, so it reports the overlap. The overlap is real in the checker's terms: `Dual<Never>` has two `From<Never>`. The first version of this issue assumed the bound `Scalar: Real` rules `Never` out. It does not: `resolveUncached` deliberately answers every bound on `Never` as holding (a bound on `Never` holds whatever the trait is, needed for `Source<Item, Never>`). A fix that consults bounds in the overlap check would therefore not work. What makes the overlap harmless is the other side: `From<Never>::from(value: Never)` can never be called, because no value of `Never` exists, so two implementations that meet only at a `Never` source cannot be told apart by any program. Everything about the blanket is already special-cased through `checker.wellKnown.from` and `checker.neverType` (`fitsType` refuses it for any argument but a `Never`). ## Fix location `reportOverlap` (`compiler/src/semantics/checker/implementation.trb`), after the two `unifyTerms` calls and before `reportBlanketOverlap`: skip the report when one implementation is the `From<Never>` blanket and the other implementation's single `From` argument is a bare generic parameter. The solutions of that pair are exactly `parameter = Never`. - New helper (e.g. `isNeverSourceBlanket`): `targetShapeOf(target)` is `.Blanket`, `capabilitySymbolOf(trait) == checker.wellKnown.from`, and the one trait argument is `checker.neverType`. No new name looked up by string. - The exemption applies when the *other* trait argument is a `Parameter` (walk it with `walkTerm`). `extend Foo with From<Never>` written by hand stays a real overlap with the blanket; `From<Int>` beside `From<Scalar>` on one generic type stays one too (the solution `Scalar = Int` is not `Never`). - Only `From` is exempt. Other traits keep the plain rule: their members can return or use the argument type, so a `Never` instance is not provably unobservable. ## Acceptance criteria - [ ] `extend<Scalar> Wrapper<Scalar> with From<Scalar>` (a generic type that takes its own parameter as `From` source, bounded or unbounded) is accepted by `torb check` beside the std blanket `From<Never>`; `type Wrapper<Scalar: Real> with From<Scalar>` too (the `with` list form). - [ ] `compiler/tests/implementations.test.trb` pins, next to the #441 tests: (a) the two forms above are accepted (`problems(source).isEmpty()`; the test workspace contains `std/*`, so the blanket is present); (b) `extend Wrapper<Int> with From<Never>` beside it is still refused with the unchanged message; (c) `From<Scalar>` plus `From<Int>` on one generic type is still refused with the unchanged message; (d) the existing overlap and blanket-message tests stay green unmodified. - [ ] A program in `tests/language/` (with `.expected`) declares such a type, calls `Wrapper.from(x)` / `.into()` with a `Scalar` value and gets the right implementation under `torb run` and `torb run --native`. If call resolution reports ambiguity with the blanket for a concrete `Scalar`, fixing that is part of this issue; `fitsType` already refuses `Never` for any other argument. - [ ] `Dual` in `std/gradient` (`std/gradient/src/dual.trb`) gets `From<Scalar>` (a constant, rate zero, same as `Dual.constant`) with a test in `std/gradient/tests`; `docs/design/COMPUTE.md` section 4 drops the pitfall bullet ("`Dual<Scalar>` has no `From<Scalar>` ...") and names the conversion; `skills/` regenerated (`docs skill`). - [ ] `sh tools/gates.sh a` green in forge CI (not run locally). ## Commit plan (one PR) 1. `fix(checker): let a generic type take its own parameter as the source of From beside the Never blanket` - the exemption, the compiler tests, the `tests/language/` program. 2. `feat(std/gradient): add From<Scalar> to Dual` - `dual.trb`, its test, the `COMPUTE.md` bullet, regenerated `skills/`. ## Seed Does not touch the seed: checker change only (no syntax, no new native, no driver flag, no name looked up by string). Commit 2 changes `std/gradient`, which the seed does not use. Dependencies: #441 / PR #448 (merged) and #83 / PR #454 (merged); none open.
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261024 until 2026-10-24T04:24:35Z

[loop:torbscript] claim refine by session:loop-torbscript-20261024 until 2026-10-24T04:24:35Z
Author
Collaborator

[loop:torbscript] refine: ready, claim released, next: implement

Definition of ready

  • Goal / design point: docs/design/COMPUTE.md section 4 (the Dual pitfall bullet) and CONCEPT.md "Coherence". The rule "two implementations of one trait may not overlap" stays; one provably unobservable overlap is exempted.
  • Seed: none (checker change plus a std/gradient addition; no syntax, native, driver flag or string-looked-up name).
  • Size: one PR, two Conventional Commits (plan in the issue body).
  • Body rewritten with cause, fix location and testable criteria.

Finding that changes the premise: the issue assumed Scalar: Real excludes Scalar = Never. It does not, because a bound on Never holds whatever the trait is (resolveUncached, needed for Source<Item, Never>). So bound-aware disjointness is the wrong fix.

decided by the loop:

  • Exempt the overlap with the From<Never> blanket when the other From takes a bare generic parameter, in reportOverlap after the shared-substitution unification. Why: the only common instance is parameter = Never, and from(value: Never) can never be called, so no program can tell the two apart. The checker already special-cases this blanket through wellKnown.from and neverType.
  • Only From, not every trait. Why: other traits can return or use the argument type, so an instance at Never is not provably unobservable; keeping the rule narrow loses nothing the issue needs.
  • Not the bounds route and not removing the blanket. Why: bounds on Never always hold, and removing the blanket breaks ? on Result<_, Never> (a published std behaviour). Technical checker design, no owner question.

No owner question: none of the contract's categories applies.

[loop:torbscript] refine: ready, claim released, next: implement **Definition of ready** - **Goal / design point:** `docs/design/COMPUTE.md` section 4 (the `Dual` pitfall bullet) and `CONCEPT.md` "Coherence". The rule "two implementations of one trait may not overlap" stays; one provably unobservable overlap is exempted. - **Seed:** none (checker change plus a `std/gradient` addition; no syntax, native, driver flag or string-looked-up name). - **Size:** one PR, two Conventional Commits (plan in the issue body). - **Body rewritten** with cause, fix location and testable criteria. **Finding that changes the premise:** the issue assumed `Scalar: Real` excludes `Scalar = Never`. It does not, because a bound on `Never` holds whatever the trait is (`resolveUncached`, needed for `Source<Item, Never>`). So bound-aware disjointness is the wrong fix. **decided by the loop:** - *Exempt the overlap with the `From<Never>` blanket when the other `From` takes a bare generic parameter, in `reportOverlap` after the shared-substitution unification.* Why: the only common instance is `parameter = Never`, and `from(value: Never)` can never be called, so no program can tell the two apart. The checker already special-cases this blanket through `wellKnown.from` and `neverType`. - *Only `From`, not every trait.* Why: other traits can return or use the argument type, so an instance at `Never` is not provably unobservable; keeping the rule narrow loses nothing the issue needs. - *Not the bounds route and not removing the blanket.* Why: bounds on `Never` always hold, and removing the blanket breaks `?` on `Result<_, Never>` (a published std behaviour). Technical checker design, no owner question. No owner question: none of the contract's categories applies.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T04:32:11Z

[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T04:32:11Z
Author
Collaborator

[loop:torbscript] implement: checkpoint (context limit reached, not finished), next: continue implement

Done (uncommitted in worktree code/wt/torbscript-language-455-from-never-overlap, branch loop/455-from-never-overlap, nothing pushed yet):

  • compiler/src/semantics/checker/implementation.trb: reportOverlap skips the report via new meetsOnlyAtNeverSource / isNeverSourceBlanket (blanket = .Blanket target, trait From by wellKnown.from, single argument checker.neverType; other side = single From argument that is a bare .Parameter). Not yet compiled or format-checked (no local torb binary found; use CI or the repo build).

Left:

  1. Commit 1 fix(checker): ...: tests in compiler/tests/implementations.test.trb next to the #441 tests (accepted: extend<Scalar> Wrapper<Scalar> with From<Scalar> bounded and unbounded, and the type Wrapper<Scalar: Real> with From<Scalar> list form; refused unchanged: extend Wrapper<Int> with From<Never>, From<Scalar> plus From<Int>), plus a tests/language/ program with .expected.
  2. Commit 2 feat(std/gradient): From<Scalar> on Dual, test, drop the COMPUTE.md pitfall bullet, docs skill regenerate, pdisc stamp+check.
  3. Push, open PR with "Closes #455", torb format --check on touched files.
[loop:torbscript] implement: checkpoint (context limit reached, not finished), next: continue implement **Done (uncommitted in worktree `code/wt/torbscript-language-455-from-never-overlap`, branch `loop/455-from-never-overlap`, nothing pushed yet):** - `compiler/src/semantics/checker/implementation.trb`: `reportOverlap` skips the report via new `meetsOnlyAtNeverSource` / `isNeverSourceBlanket` (blanket = `.Blanket` target, trait `From` by `wellKnown.from`, single argument `checker.neverType`; other side = single `From` argument that is a bare `.Parameter`). Not yet compiled or format-checked (no local `torb` binary found; use CI or the repo build). **Left:** 1. Commit 1 `fix(checker): ...`: tests in `compiler/tests/implementations.test.trb` next to the #441 tests (accepted: `extend<Scalar> Wrapper<Scalar> with From<Scalar>` bounded and unbounded, and the `type Wrapper<Scalar: Real> with From<Scalar>` list form; refused unchanged: `extend Wrapper<Int> with From<Never>`, `From<Scalar>` plus `From<Int>`), plus a `tests/language/` program with `.expected`. 2. Commit 2 `feat(std/gradient)`: `From<Scalar>` on `Dual`, test, drop the COMPUTE.md pitfall bullet, `docs skill` regenerate, pdisc stamp+check. 3. Push, open PR with "Closes #455", `torb format --check` on touched files.
Author
Collaborator

[loop:torbscript] implement: PR #461 open (2 commits, rebased on main, commit-message gate fixed: subject was 102 chars). Both commits written; code never compiled locally, waiting for CI. Next: fix from CI output, final implement comment, remove worktree.

[loop:torbscript] implement: PR #461 open (2 commits, rebased on main, commit-message gate fixed: subject was 102 chars). Both commits written; code never compiled locally, waiting for CI. Next: fix from CI output, final `implement` comment, remove worktree.
Author
Collaborator

[loop:torbscript] release implement: the continue worker timed out at 02:33Z; its work landed as PR #461 (see the 03:11Z comment). next: waiting (forge CI of PR #461)

[loop:torbscript] release implement: the continue worker timed out at 02:33Z; its work landed as PR #461 (see the 03:11Z comment). next: waiting (forge CI of PR #461)
Author
Collaborator

[loop:torbscript] claim fix by session:loop-torbscript-20261024 until 2026-10-24T07:26:45Z

[loop:torbscript] claim fix by session:loop-torbscript-20261024 until 2026-10-24T07:26:45Z
Author
Collaborator

[loop:torbscript] fix: bootstrap (linux-x64) of run 1569 failed in step 1 with the seed: Cannot find holder here (compiler/tests/implementations.test.trb:274). takesScalar called the sibling group helper holder, and a fn declared in a group body does not see its siblings; folded holder into takesScalar (commit 6ececbd3, test-only). No local compiler on this box, so the checker code and the other new tests are untested beyond a read-through (names and forms checked against existing call sites). next: waiting (forge CI of 6ececbd3)

[loop:torbscript] fix: bootstrap (linux-x64) of run 1569 failed in step 1 with the seed: `Cannot find `holder` here` (compiler/tests/implementations.test.trb:274). `takesScalar` called the sibling group helper `holder`, and a fn declared in a `group` body does not see its siblings; folded `holder` into `takesScalar` (commit 6ececbd3, test-only). No local compiler on this box, so the checker code and the other new tests are untested beyond a read-through (names and forms checked against existing call sites). next: waiting (forge CI of 6ececbd3)
argus closed this issue 2026-10-24 12:46:40 +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#455
No description provided.