checker: scalar * value with a user Multiply on the number type lowers as the builtin product #440
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Found by the scratch probe of the
std/tensordesign record (#438, step 1 of #82;docs/design/TENSOR.md, "Operators"). A scalar on the left of an operator whose implementation is a userextendof the number type is accepted by the checker and then lowered as the builtin operation.Reproduction
torb checkanswers1 file, no problems;torb runanswersinternal error: t_probe2 b0:multiply.f64works onFloat64, and %0 isRecord(T_probe2_Tensor__Float64)``. The operator form picks the primitivemultiply.f64of the left operand and never looks for the user's implementation (the method form3.0.multiply(a)was not probed). Probed with the nightly of 2026-10-23.Goal
scalar * valuewith a user-declaredMultiply<Value, Value>on the number type resolves to that implementation in the IR lowering (or the checker refuses it with a clear message if the language decides it must not exist; LINEAR section 6 says coherence forbids it forVector2, and the probe shows the checker does not).Acceptance criteria
[1.0, 2.0]in the VM and natively, ortorb checkrefuses it with a diagnostic that says why.Float64,Intand a user type on the right.Seed
Touches the seed: probably (the lowering of an operator,
compiler/src/ir/lower); the two-commit rule applies if the fix changes what the compiler reads. The seed refresh stays with the owner.Out of scope
std/tensorslices: they usetensor * scalarand do not wait for this.std/tensoron the CPU (step 1 of #82) #438[loop:torbscript] intake: in scope (author orion, label loop). claim refine by session:loop-torbscript-20261024 until 2026-10-24T00:00Z
Multiply<Scalar>andMultiplyon one generic type are refused as overlapping #441[loop:torbscript] refine: ready, next: implement
Claim released. Refined against
docs/ROADMAP.md(thestd/tensorstep of the linear-algebra line),docs/design/TENSOR.mdsection 7 (decision 14, finding 3),docs/design/LINEAR.mdsection 6, CONCEPT.md (orphan rule) and the code atmain(fe8c4b8b).Findings
checkOrphan(compiler/src/semantics/checker/implementation.trb,ownsTraitArgument) deliberately lets the package that ownsTensorwriteextend Float64 with Multiply<Tensor<Float64>, Tensor<Float64>>; CONCEPT records that rule ("counting a type named as an argument of the trait as owning it"). So the second option of the issue goal (refuse in the checker) would contradict a decided rule. LINEAR section 6 ("coherence forbids it:std/linearowns neitherFloat64norMultiply") predates that rule and is stale.operatorBoundpicks the user'sMultiply<Tensor<Float64>, ...>from the right operand's type and records the resolution.lowerArithmetic(compiler/src/ir/lower/expression.trb) takes the intrinsic as soon as the left slot is a built-in number (numericKindOf(..., first)) and never looks at the right operand. The intrinsic is only the right answer when the right operand has the same built-in number type; otherwise the operator is the user's member and belongs tolowerOperator(compiler/src/ir/lower/call.trb), which already reads the checker's recorded resolution.decided by the loop: the operator form is supported (fix the lowering), not refused. Why: the orphan rule decided in CONCEPT permits exactly this implementation and the checker already resolves it; refusing would need a new coherence rule, a breaking change to a decided contract, and would leave
Float64 with From<Celsius>-style implementations as the odd one out. Fixing the lowering is the smaller change (one guard) and no published contract changes. Not an owner question (internal technical design, per the contract).Goal
scalar <op> value, where the left operand is a built-in number and the right operand has a type other than that number, is lowered as a call of the implementation the checker recorded, in the VM and natively.scalar <op> scalarstays the one intrinsic instruction it is today.Acceptance criteria
[1.0, 2.0]withtorb run(VM) and natively;torb checkstays clean.tests/conformance/scalar-left-operator.trb(with.expected,.exit,.allocationsas its neighbours) runs natively and in the VM and pinsscalar * valueforFloat64, forIntand for a user type (a generic one, as in the issue, and a plain record) on the right; it also pins thatFloat64 * Float64andInt * Intin the same program are unchanged..expectedor.allocations(the guard must not turn a plain number product into a call); the IR print of1.0 * 2.0is still onemultiply.f64.3.0.multiply(a)is probed in the same scratch run. If it fails, it is recorded in a new issue (after a keyword search for a duplicate) and is not part of this PR.docs/design/LINEAR.mdsection 6 gives the real reasonscalar * vectoris not instd/linear(a library choice: it needs anextend Float64in the vector's package, and the spelling isvector * scalar), no longer "coherence forbids it".docs/design/TENSOR.mdsection 7 (finding 3) links this fix as closed once it lands; the first-release decision (noscalar * tensor) is unchanged.skills/regenerated,pdisc.py checkok.Design point: TENSOR.md decision 14 / section 7 (finding 3, blocks none of the slices) and LINEAR.md section 6; ROADMAP: the
std/tensorline (step 1 of #82 found it, #438). Related: #441 (the overlap finding of the same probe, refined separately; no dependency in either direction).Seed: no. The change is a guard inside the lowering of
compiler/src; no syntax, no new native, no driver flag and no std name looked up by string, andcompiler/andstd/do not use a scalar-left operator on a foreign right operand, so the seed compiles the new source unchanged. Two-commit rule not needed; seed refresh stays with the owner.Size: one PR, two Conventional Commits:
fix(ir): lower a built-in number's operator with a foreign right operand as the recorded member(the guard inlowerArithmetic, plus the conformance program and expected files),docs(linear): state the real reason scalar * vector is not provided(LINEAR section 6, TENSOR section 7 pointer, regeneratedskills/).Implementation hints (data for the implementer, not a contract): in
lowerArithmetic, take the intrinsic only if the type ofsecondequals the type offirst(or both resolve to the samenumericKindOf); shifts (Int << UInt8) keep their intrinsic because their right operand is a built-in number, so test the guard on "right operand is a built-in number" rather than strict type equality. Check the checker leaves a resolution at the expression span for this case; if it only records one for non-built-in left operands, record it there. Verify in forge CI only (bootstrap and gates), no local gates.Token usage of this refine worker: about 75k.
[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T04:30Z
[loop:torbscript] implement: checkpoint, PR #446 is open (guard in lowerArithmetic, conformance program, docs commit); waiting for the forge CI gates. Method form 3.0.multiply(a) probed with the 0.1.0 toolchain: works, only the operator form was broken. The conformance allocation budget is a first guess and is set from the CI count.
[loop:torbscript] release implement: the implement worker died on a usage limit (weekly limit of its account) after opening PR #446; forge CI is still running. Left: set the conformance allocation budget from the CI count, then fix rounds. next: waiting (CI running on PR #446)
[loop:torbscript] close: merged via PR (main CI green on the landed commit
bc504c24), issue closed. next: donestd/tensor,Tensor<Scalar>over aBuffer(step 2 of #82) #691std/tensoron the CPU #82