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

@@ -215,6 +215,8 @@ test "floatToIntExact accepts integral floats, rejects the rest" {
}
test "moduleConstInt folds expression-RHS consts and rejects cycles" {
var table = types.TypeTable.init(std.testing.allocator);
defer table.deinit();
var map = std.StringHashMap(pi.ModuleConstInfo).init(std.testing.allocator);
defer map.deinit();
@@ -236,12 +238,12 @@ test "moduleConstInt folds expression-RHS consts and rejects cycles" {
try map.put("F", .{ .value = &f_val, .ty = .f64 });
try map.put("G", .{ .value = &g_val, .ty = .f64 });
try std.testing.expectEqual(@as(?i64, 2), pi.moduleConstInt(&map, "M"));
try std.testing.expectEqual(@as(?i64, 3), pi.moduleConstInt(&map, "N"));
try std.testing.expectEqual(@as(?i64, 6), pi.moduleConstInt(&map, "P"));
try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, "F"));
try std.testing.expect(pi.moduleConstInt(&map, "G") == null);
try std.testing.expect(pi.moduleConstInt(&map, "absent") == null);
try std.testing.expectEqual(@as(?i64, 2), pi.moduleConstInt(&map, &table, "M"));
try std.testing.expectEqual(@as(?i64, 3), pi.moduleConstInt(&map, &table, "N"));
try std.testing.expectEqual(@as(?i64, 6), pi.moduleConstInt(&map, &table, "P"));
try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, &table, "F"));
try std.testing.expect(pi.moduleConstInt(&map, &table, "G") == null);
try std.testing.expect(pi.moduleConstInt(&map, &table, "absent") == null);
// A cyclic const has no compile-time integer value, and folding it must not
// recurse forever: mutual `A :: B + 0; B :: A + 0` and self `C :: C + 0` all
@@ -256,9 +258,29 @@ test "moduleConstInt folds expression-RHS consts and rejects cycles" {
try map.put("A", .{ .value = &a_val, .ty = .s64 });
try map.put("B", .{ .value = &b_val, .ty = .s64 });
try map.put("C", .{ .value = &c_val, .ty = .s64 });
try std.testing.expect(pi.moduleConstInt(&map, "A") == null);
try std.testing.expect(pi.moduleConstInt(&map, "B") == null);
try std.testing.expect(pi.moduleConstInt(&map, "C") == null);
try std.testing.expect(pi.moduleConstInt(&map, &table, "A") == null);
try std.testing.expect(pi.moduleConstInt(&map, &table, "B") == null);
try std.testing.expect(pi.moduleConstInt(&map, &table, "C") == null);
}
test "moduleConstInt gates the fold on the declared type, not the initializer node" {
var table = types.TypeTable.init(std.testing.allocator);
defer table.deinit();
var map = std.StringHashMap(pi.ModuleConstInfo).init(std.testing.allocator);
defer map.deinit();
// An `int_literal` value node folds to an integer ONLY when the declared
// type is numeric. A `string`/`bool`-typed const carrying an integer-looking
// initializer must never be folded into a count (issue 0088): the count path
// consults `ModuleConstInfo.ty`, not just the node shape.
var int_val = nLit(4);
try map.put("OK", .{ .value = &int_val, .ty = .s64 });
try map.put("STR", .{ .value = &int_val, .ty = .string });
try map.put("BOOLEAN", .{ .value = &int_val, .ty = .bool });
try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, &table, "OK"));
try std.testing.expect(pi.moduleConstInt(&map, &table, "STR") == null);
try std.testing.expect(pi.moduleConstInt(&map, &table, "BOOLEAN") == null);
}
test "evalConstIntExpr folds an integral float literal, halts on a fractional one" {