Files
sx/issues/0088-typed-module-const-annotation-mismatch.md
agra b69ec43ba3 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.
2026-06-05 08:23:59 +03:00

7.0 KiB
Raw Blame History

RESOLVED (F0.7) — A typed module-level constant whose initializer does not match its annotation is now rejected at the declaration with a clear type mismatch diagnostic, killing both symptoms (the print(N) segfault and the [N]s64 → 4 fold).

Root cause. registerTypedModuleConst (src/ir/lower.zig) stored the annotation type on the const but never checked the initializer literal against it, so N : string : 4 registered as {value = int 4, ty = string}. emitModuleConst then stamped the int_literal with the string type (a bogus pointer → segfault at the use site), and program_index.moduleConstInt folded the const into an integer COUNT by inspecting the int_literal node alone, ignoring ModuleConstInfo.ty (so [N]s64 folded to 4).

Both LITERAL initializers (N : string : 4) and const-EXPRESSION initializers (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.zigregisterTypedModuleConst validates the initializer against the resolved annotation BY TYPE, covering literals AND const-expressions (binary_op / unary_op) uniformly. typedConstInitFits keeps the literal arms (int → int/float, float → float, bool → bool, string → string, null → pointer/optional, --- → any) and routes any non-literal through constExprInitFits, which compares the initializer's INFERRED type (inferExprType, the existing type-inference facility — no second const evaluator) to the annotation with the same integer/float compatibility. A mismatch emits type mismatch: constant '<n>' is declared '<ty>' but its initializer is <desc> at the initializer span (a literal names its kind; a const-expression is described by its inferred type, e.g. "an integer expression"), and does NOT register the const — it evicts the pass-0 placeholder so a count use can't still fold it. On a MATCH the const is registered at its resolved annotation type (the same put the literal path always did), so a const-expression folds and emits at its declared type.
  • src/ir/program_index.zigmoduleConstInt / moduleConstIntFramed take the TypeTable and gate the fold on isCountableConstType(ci.ty) (integer of any width, or a float), so a non-numeric typed const can never be folded into a count off its initializer node — whether that node is a literal or a foldable integer expression. Callers in lower.zig and type_bridge.zig updated.

Regression tests.

  • examples/1143-diagnostics-typed-module-const-mismatch.sx — negative: eight mismatch shapes — four literal (int→string, string→s64, bool→s64, 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, 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.zigmoduleConstInt 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.zigarithResultType promotes int×float to the float regardless of operand order (the shared promotion helper).

0088 — Typed module const annotation mismatch is accepted

Symptom

A module-level typed constant whose initializer does not match its annotation is accepted. Observed: N : string : 4 compiles; printing N segfaults, and using N as an array dimension folds it as 4. Expected: the const declaration emits a type-mismatch diagnostic and no downstream use treats it as a valid string or integer count.

Reproduction

#import "modules/std.sx";

N : string : 4;

main :: () {
    print("N={}\n", N);
}

Related count-surface manifestation:

#import "modules/std.sx";

N : string : 4;

main :: () {
    a : [N]s64 = ---;
    print("{}\n", a.len);
}

Observed on flow/sx-foundation/F0.4 attempt 10: the first repro segfaults in the generated program; the second prints 4.

Investigation prompt

Fix issue 0088: typed module constants must validate/coerce their initializer against the explicit annotation before being registered or used. Suspected area: src/ir/lower.zig, especially registerTypedModuleConst, lowerExpr's module-const identifier path, and any const-declaration lowering that stores ProgramIndex.module_const_map entries. src/ir/program_index.zig's moduleConstInt currently folds by inspecting the initializer node and ignores ModuleConstInfo.ty; after the declaration is diagnosed or represented correctly, a non-integer typed const such as N : string : 4 must not become a valid count. Likely fix: add a typed-const validation path that emits a clear diagnostic for incompatible initializer/annotation pairs, and ensure the module-const count lookup only accepts constants whose declared/inferred type is numeric and integral-compatible. Verify by running the two repros above: expect a non-zero compile with a type-mismatch diagnostic for N : string : 4, no runtime segfault, and no [N] length of 4.