> **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.zig` — `registerTypedModuleConst` 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.zig` — `moduleConstInt` / `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.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

## 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

```sx
#import "modules/std.sx";

N : string : 4;

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

Related count-surface manifestation:

```sx
#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`.
