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

@@ -1465,6 +1465,10 @@ boolean `bool`, a string `string`, `null` a pointer or optional, and `---` any
type. The check is type-based, so it applies equally to a literal and to a
constant expression: both `N : string : 4` and `N : string : M + 2` (with
`M :: 2`) are rejected at the declaration — neither registers a usable constant.
A constant expression's type is its promoted result type (see
[Arithmetic](#arithmetic)), so a mixed int+float initializer is a float in either
operand order: `C : s64 : M + 0.5` and `C : s64 : 0.5 + M` are both rejected, and
`F : f64 : M + 0.5` is accepted and folds to `2.5`.
### Variable Binding (mutable)
@@ -1758,6 +1762,15 @@ x * x
x + 2
```
**Numeric promotion.** When the two operands of an arithmetic op have different
numeric types, the result is the promoted type: an integer operand combined with
a floating-point operand yields the **float**, regardless of operand order
(`n + 0.5` and `0.5 + n` both produce an `f64`). This holds for the expression's
static type as well as its value, so `print("{}", n + 0.5)` formats a float and a
typed binding `x : f64 = n + 0.5` is exact (not truncated). A mixed-numeric
expression therefore does not satisfy an integer annotation — `C : s64 : n + 0.5`
is a `type mismatch` in either operand order.
### Chained Comparisons
Comparison operators can be chained. Each operand is evaluated exactly once.
```sx