fix(ir): infer mixed int+float arithmetic as the promoted float [F0.7]

`ExprTyper.inferType`'s binary-op arm inferred every non-comparison op
from the LHS alone, so `M + 0.5` (s64 + f64) statically typed as s64
while `0.5 + M` typed as f64 — operand-order-dependent. The value path
(`lowerBinaryOp`) already promoted int×float → float, so static
inference disagreed with the value: `M + 0.5` formatted as a truncated
int and a typed const `BAD : s64 : M + 0.5` was accepted+truncated
(issue 0088 mixed-numeric escape).

Extract the value path's inline promotion into a shared
`Lowering.arithResultType(lhs, rhs)` and reuse it at both sites, so
arithmetic / bitwise / shift inference reports exactly the type the
lowered value carries — int LHS × float RHS → the float, order-
independent. The value-path behavior is unchanged (the block is moved
verbatim into the helper), so no IR shifts; the suite stays green. The
typed-const validation reuses `inferExprType`, so this auto-closes the
escape with no change to the validation logic.

- examples/1143: BAD/BAD2 (`s64 : M + 0.5`, `s64 : 0.5 + M`) rejected
  in both operand orders.
- examples/0162: MF/MFR (`f64 : M + 0.5`, `f64 : 0.5 + M`) fold to 2.5.
- examples/0163 (new): pins the inference fix in a value context
  (`print("{}", n + 0.5)` formats the float, both orders, +-*/, f32).
- expr_typer.test.zig: arithResultType + mixed-arithmetic inference.
- specs.md / readme.md: document the numeric-promotion rule.
- issues/0088: RESOLVED banner notes the inferExprType root fix.
This commit is contained in:
agra
2026-06-05 08:23:59 +03:00
parent 454ea06bd4
commit b69ec43ba3
14 changed files with 218 additions and 52 deletions

View File

@@ -15,6 +15,22 @@
> (`M :: 2; N : string : M + 2`, `V : string : -M`) are rejected — the validation
> is type-based, so a non-literal node kind can no longer escape it (attempt 2).
>
> **Mixed-numeric escape closed at the type-system root (attempt 3).** The
> type-based validation reuses `inferExprType`, which inferred a non-comparison
> binary op from its LHS alone — so `BAD : s64 : M + 0.5` (s64 + f64) inferred
> `s64` and was accepted+truncated, while `0.5 + M` inferred `f64` and was
> rejected: operand-order-dependent. The fix is in the binary-op arm of
> `ExprTyper.inferType` (`src/ir/expr_typer.zig`): arithmetic / bitwise / shift
> ops now infer the PROMOTED result of `(lhs, rhs)` via `Lowering.arithResultType`
> — the same int×float → float rule `lowerBinaryOp` already applied for the
> value (extracted from its inline block into a shared helper, so the two can't
> diverge). `M + 0.5` now infers `f64` in either operand order, so the typed-const
> validation rejects it against an `s64` annotation with no special-casing in the
> validation logic itself. This was a pre-existing inference bug broader than
> typed consts (it also mis-formatted `print("{}", M + 0.5)` as a truncated int);
> the typed-LOCAL `y : s64 = 1.5` → 1 narrowing is a SEPARATE assignment-coercion
> bug tracked as issue 0095.
>
> **Fix per file.**
> - `src/ir/lower.zig` — `registerTypedModuleConst` validates the initializer
> against the resolved annotation BY TYPE, covering literals AND
@@ -39,17 +55,25 @@
> updated.
>
> **Regression tests.**
> - `examples/1143-diagnostics-typed-module-const-mismatch.sx` — negative: six
> - `examples/1143-diagnostics-typed-module-const-mismatch.sx` — negative: eight
> mismatch shapes — four literal (`int→string`, `string→s64`, `bool→s64`,
> `float→s64`) and two const-expression (`M + 2 → string`, `-M → string`)
> each emit a `type mismatch` diagnostic, exit 1.
> `float→s64`), two const-expression (`M + 2 → string`, `-M → string`), and two
> mixed-numeric (`s64 : M + 0.5` and `s64 : 0.5 + M`, rejected in BOTH operand
> orders) — each emit a `type mismatch` diagnostic, exit 1.
> - `examples/0162-types-typed-module-const-roundtrip.sx` — positive: valid typed
> consts (`s64` as count + printed, `f32` from int, `f32` float, `string`,
> `*void` null, plus const-expression `s64 : M + 2` used as a count + printed
> and `f32 : M + 2`) compile, fold, and print correctly.
> `*void` null, const-expression `s64 : M + 2` used as a count + printed,
> `f32 : M + 2`, plus mixed-numeric `f64 : M + 0.5` and `f64 : 0.5 + M` folding
> to 2.5 in both orders) compile, fold, and print correctly.
> - `examples/0163-types-mixed-numeric-promotion.sx` — positive: pins the
> inferExprType promotion DIRECTLY in a value context (`print("{}", n + 0.5)`
> formats as the float `2.5`, both operand orders, across `+ - * /` and an f32
> operand; a pure-int expression stays an integer).
> - `src/ir/program_index.test.zig` — `moduleConstInt gates the fold on the
> declared type, not the initializer node` (covers both a literal and a
> binary_op value node declared with a non-numeric type).
> - `src/ir/expr_typer.test.zig` — `arithResultType promotes int×float to the
> float regardless of operand order` (the shared promotion helper).
# 0088 — Typed module const annotation mismatch is accepted