fix(ir): lower a built-in number's operator with a foreign right operand as the recorded member #446

Merged
argus merged 2 commits from loop/440-scalar-left-operator into main 2026-10-23 23:20:45 +00:00
Collaborator

Closes #440

What

lowerArithmetic took the intrinsic as soon as the left operand was a built-in number, so 3.0 * series with a user extend Float64 with Multiply<Series<Float64>, Series<Float64>> (accepted by the orphan rule, resolved by the checker from the right operand) reached the VM as multiply.f64 on a record. The intrinsic now also needs a built-in number on the right (shifts such as Int << UInt8 keep it); anything else goes through lowerOperator, which reads the checker's recorded resolution.

The method form 3.0.multiply(a) was probed with the 0.1.0 toolchain and works; only the operator form was broken. Decision to fix the lowering instead of refusing in the checker: see the refine comment on the issue.

Commits

  1. fix(ir): ...: the guard in compiler/src/ir/lower/expression.trb plus the conformance program tests/conformance/scalar-left-operator.trb (Float64 and Int on the left, a generic type and a plain record on the right, operator and method form side by side, plain Float64 * Float64 / Int * Int unchanged).
  2. docs(linear): ...: LINEAR section 6 gives the real reason scalar * vector is not provided; TENSOR section 7 records finding 3 as closed by this fix. No skills/ change: these are design records, not in the skills.

Seed

No. A guard inside the lowering; no syntax, native, driver flag or std name looked up by string, and compiler/ and std/ use no scalar-left operator on a foreign right operand.

Test

Locally only: torb format --check on the touched .trb files (clean), torb check of the conformance program (clean), and its expected output taken from the method-form variant under the 0.1.0 toolchain. The operator form, tier A and tier B gates run in forge CI. scalar-left-operator.allocations is a first guess; the budget is set from the CI count.

Closes #440 ## What `lowerArithmetic` took the intrinsic as soon as the **left** operand was a built-in number, so `3.0 * series` with a user `extend Float64 with Multiply<Series<Float64>, Series<Float64>>` (accepted by the orphan rule, resolved by the checker from the right operand) reached the VM as `multiply.f64` on a record. The intrinsic now also needs a built-in number on the right (shifts such as `Int << UInt8` keep it); anything else goes through `lowerOperator`, which reads the checker's recorded resolution. The method form `3.0.multiply(a)` was probed with the 0.1.0 toolchain and works; only the operator form was broken. Decision to fix the lowering instead of refusing in the checker: see the refine comment on the issue. ## Commits 1. `fix(ir): ...`: the guard in `compiler/src/ir/lower/expression.trb` plus the conformance program `tests/conformance/scalar-left-operator.trb` (`Float64` and `Int` on the left, a generic type and a plain record on the right, operator and method form side by side, plain `Float64 * Float64` / `Int * Int` unchanged). 2. `docs(linear): ...`: LINEAR section 6 gives the real reason `scalar * vector` is not provided; TENSOR section 7 records finding 3 as closed by this fix. No `skills/` change: these are design records, not in the skills. ## Seed No. A guard inside the lowering; no syntax, native, driver flag or std name looked up by string, and `compiler/` and `std/` use no scalar-left operator on a foreign right operand. ## Test Locally only: `torb format --check` on the touched `.trb` files (clean), `torb check` of the conformance program (clean), and its expected output taken from the method-form variant under the 0.1.0 toolchain. The operator form, tier A and tier B gates run in forge CI. `scalar-left-operator.allocations` is a first guess; the budget is set from the CI count.
A built-in number on the left took the intrinsic whatever the right operand was, so a user's extend Float64 with Multiply<Series<Float64>, ...> reached the VM as multiply.f64 on a record. The intrinsic now needs a built-in number on the right too (shifts keep it); anything else is the member the checker recorded.
docs(linear): state the real reason scalar * vector is not provided
All checks were successful
ci / commit messages (pull_request) Successful in 5s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / what the change touches (pull_request) Successful in 5s
ci / the VS Code extension (editors/vscode) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 9s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap (linux-x64) (pull_request) Successful in 2m35s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 24m35s
ci / tier B (linux-x64) (pull_request) Successful in 12m42s
ci / pack the seed (pull_request) Has been skipped
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m27s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 2m8s
ci / every host emits the same C (pull_request) Successful in 5m8s
ci / archives (pull_request) Has been skipped
ci / gates (pull_request) Successful in 0s
0d9676786e
The orphan rule allows extend Float64 with Multiply<Vector2<Float64>, ...> in the package that owns Vector2; leaving it out is a library choice. TENSOR section 7 records scalar * tensor as closed by #440.
argus approved these changes 2026-10-23 23:20:44 +00:00
argus left a comment

Approved. Checklist for head 0d96767:

  • Tests: ok. The new conformance program scalar-left-operator covers Float64 and Int on the left, a generic type and a plain record on the right, operator and method form side by side, and plain Float64Float64 / IntInt unchanged. CI is green for this head (tier A, tier B, gates, pdisc).
  • Correctness: ok. lowerArithmetic takes the intrinsic only when the right operand is also a built-in number, so shifts like Int << UInt8 keep it. Otherwise it falls through to lowerOperator, which reads the checker's recorded resolution. Text + and the final fall-through are unchanged.
  • Scope: ok. It matches #440 and the refine comment there, with one guard, the conformance program and the two design-record updates.
  • Security, data, seed: ok. No secrets, no format changes, and the seed note is plausible.
  • Docs: ok. LINEAR section 6 and TENSOR section 7 now match the behaviour. The pdisc check is green.
  • Release/deploy: none.

Non-blocking:

  1. scalar-left-operator.allocations is a first-guess budget of 100. Lower it to the real CI count with --update-budgets in a follow-up, so a regression still fails.
  2. A built-in left operand with a different built-in right operand and a user Multiply<OtherNumber, ...> still takes the intrinsic. This is an edge case, not part of #440.
Approved. Checklist for head 0d96767: - Tests: ok. The new conformance program `scalar-left-operator` covers Float64 and Int on the left, a generic type and a plain record on the right, operator and method form side by side, and plain Float64*Float64 / Int*Int unchanged. CI is green for this head (tier A, tier B, gates, pdisc). - Correctness: ok. `lowerArithmetic` takes the intrinsic only when the right operand is also a built-in number, so shifts like `Int << UInt8` keep it. Otherwise it falls through to `lowerOperator`, which reads the checker's recorded resolution. Text `+` and the final fall-through are unchanged. - Scope: ok. It matches #440 and the refine comment there, with one guard, the conformance program and the two design-record updates. - Security, data, seed: ok. No secrets, no format changes, and the seed note is plausible. - Docs: ok. LINEAR section 6 and TENSOR section 7 now match the behaviour. The pdisc check is green. - Release/deploy: none. Non-blocking: 1. `scalar-left-operator.allocations` is a first-guess budget of 100. Lower it to the real CI count with `--update-budgets` in a follow-up, so a regression still fails. 2. A built-in left operand with a different built-in right operand and a user `Multiply<OtherNumber, ...>` still takes the intrinsic. This is an edge case, not part of #440.
argus merged commit 6b24a2536f into main 2026-10-23 23:20:45 +00:00
argus deleted branch loop/440-scalar-left-operator 2026-10-23 23:20:45 +00:00
Sign in to join this conversation.
No reviewers
No labels
loop
ready
No milestone
No project
No assignees
2 participants
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!446
No description provided.