Files
sx/issues/0088-typed-module-const-annotation-mismatch.md
agra 156edf8e28 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.
2026-06-05 07:17:20 +03:00

4.2 KiB

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.zigregisterTypedModuleConst 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.zigmoduleConstInt / 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.zigmoduleConstInt 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

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