fix(ir): reject typed module const whose initializer mismatches annotation [F0.7]

A typed module-level constant whose initializer did not match its
annotation was silently accepted: `N : string : 4` compiled, then
`print(N)` segfaulted (an integer emitted as a `string` const → a bogus
pointer) and `[N]s64` folded `N` to 4 as an integer count. Issue 0088.

Root cause: `registerTypedModuleConst` stored the annotation type but never
validated the initializer literal against it, and
`program_index.moduleConstInt` folded a const into a count by inspecting
the initializer node alone, ignoring `ModuleConstInfo.ty`.

Fix at the declaration (kills both symptoms):
- lower.zig: `registerTypedModuleConst` now validates the initializer via
  `typedConstInitFits` (arms mirror `emitModuleConst`'s faithful-emit
  precondition: int→int/float, float→float, bool→bool, string→string,
  null→pointer/optional, `---`→any). A mismatch emits a `type mismatch`
  diagnostic at the initializer span and does not register the const (also
  evicting the pass-0 placeholder). Not routed through
  `coercionResolver().classify`: that runtime-coercion planner is unsound
  here (null's natural type is void → false-rejects `*T`; bool is 1 bit →
  false-accepts s64).
- program_index.zig: `moduleConstInt` now takes the `TypeTable` and gates
  the fold on `isCountableConstType(ci.ty)` (integer of any width, or a
  float), so a non-numeric typed const can never fold into a count off its
  initializer node. Callers in lower.zig and type_bridge.zig updated.

Regression:
- examples/1143-diagnostics-typed-module-const-mismatch.sx (negative, exit 1)
- examples/0162-types-typed-module-const-roundtrip.sx (positive)
- program_index.test.zig: gate-on-declared-type unit test

Docs: specs.md §3 Constant Binding + readme.md note the compatibility rule.
This commit is contained in:
agra
2026-06-05 07:17:20 +03:00
parent 3edb60762d
commit 156edf8e28
15 changed files with 326 additions and 16 deletions

View File

@@ -0,0 +1,93 @@
> **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).
>
> **Fix per file.**
> - `src/ir/lower.zig` — `registerTypedModuleConst` now validates the
> initializer against the resolved annotation via the new `typedConstInitFits`
> (arms mirror `emitModuleConst`'s faithful-emit precondition: int → int/float,
> float → float, bool → bool, string → string, null → pointer/optional,
> `---` → any). A mismatch emits `type mismatch: constant '<n>' is declared
> '<ty>' but its initializer is <kind>` at the initializer span and does NOT
> register the const (it also evicts the pass-0 placeholder so a count use
> can't still fold it). `literalKindName` names the literal kind for the
> message.
> - `src/ir/program_index.zig` — `moduleConstInt` / `moduleConstIntFramed` now
> 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. Callers in `lower.zig` and
> `type_bridge.zig` updated.
>
> **Regression tests.**
> - `examples/1143-diagnostics-typed-module-const-mismatch.sx` — negative: four
> mismatch shapes (`int→string`, `string→s64`, `bool→s64`, `float→s64`) 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) compile, fold, and print correctly.
> - `src/ir/program_index.test.zig` — `moduleConstInt gates the fold on the
> declared type, not the initializer node`.
# 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`.