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:
@@ -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" {
|
||||
|
||||
Reference in New Issue
Block a user