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:
@@ -920,12 +920,75 @@ pub const Lowering = struct {
|
||||
switch (cd.value.data) {
|
||||
.int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal => {
|
||||
const ty = self.resolveType(ta);
|
||||
// An unresolvable annotation is already diagnosed by the type
|
||||
// resolver; don't pile a bogus type-mismatch on top, and don't
|
||||
// leave the pass-0 placeholder (registered off the initializer
|
||||
// literal) behind as a usable const.
|
||||
if (ty == .unresolved) {
|
||||
_ = self.program_index.module_const_map.remove(cd.name);
|
||||
return;
|
||||
}
|
||||
// Validate the initializer literal against the explicit
|
||||
// annotation. A mismatch (`N : string : 4`) is a type error, not
|
||||
// a silently-accepted const — registering it would let
|
||||
// `emitModuleConst` stamp the literal with the wrong IR type
|
||||
// (an int emitted as a `string` const → a bogus pointer that
|
||||
// segfaults at the use site) and let the count path fold it
|
||||
// (`[N]s64` → 4). Issue 0088.
|
||||
if (!self.typedConstInitFits(cd.value, ty)) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, cd.value.span, "type mismatch: constant '{s}' is declared '{s}' but its initializer is {s}", .{
|
||||
cd.name, self.formatTypeName(ty), literalKindName(cd.value),
|
||||
});
|
||||
}
|
||||
// Evict the pass-0 placeholder (`N : string : 4` was
|
||||
// pre-registered as `.s64` off its `int_literal` in
|
||||
// scanDecls pass 0); leaving it would let a count use still
|
||||
// fold `N` to 4.
|
||||
_ = self.program_index.module_const_map.remove(cd.name);
|
||||
return;
|
||||
}
|
||||
self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {};
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// True iff a literal initializer of `value`'s kind is faithfully
|
||||
/// representable at the declared `dst_ty` — the precondition
|
||||
/// `emitModuleConst` relies on when it materialises the constant. The arms
|
||||
/// match `emitModuleConst`'s arms exactly, using the same type-kind
|
||||
/// predicates (`isIntEx` / `isFloat` / the `module.types.get` tag) the rest
|
||||
/// of lowering uses.
|
||||
///
|
||||
/// Deliberately NOT routed through `coercionResolver().classify`
|
||||
/// (conversions.zig): that planner judges RUNTIME value coercions and is
|
||||
/// unsound as a compile-time literal-representability oracle here — a `null`
|
||||
/// literal's natural type is `.void`, so `classify(.void, *T)` yields `.none`
|
||||
/// and would reject the valid `P : *void : null`; `bool` is 1 bit wide, so
|
||||
/// `classify(.bool, s64)` yields `.widen` and would accept the bogus
|
||||
/// `B : s64 : true`.
|
||||
fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool {
|
||||
return switch (value.data) {
|
||||
// `---` zero-inits at any type.
|
||||
.undef_literal => true,
|
||||
// Integer literal → any integer (incl. custom widths) or float
|
||||
// (`WIDTH : f32 : 800`).
|
||||
.int_literal => self.isIntEx(dst_ty) or isFloat(dst_ty),
|
||||
// Float literal → a float type only (the float arm emits `constFloat`).
|
||||
.float_literal => isFloat(dst_ty),
|
||||
.bool_literal => dst_ty == .bool,
|
||||
.string_literal => dst_ty == .string,
|
||||
// `null` → a pointer or optional.
|
||||
.null_literal => !dst_ty.isBuiltin() and switch (self.module.types.get(dst_ty)) {
|
||||
.pointer, .many_pointer, .optional => true,
|
||||
else => false,
|
||||
},
|
||||
// Only the literal kinds the caller's switch admits reach here.
|
||||
else => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Register a top-level mutable global (e.g., `context : Context = ---;`).
|
||||
/// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias
|
||||
/// in the type annotation (`A :: B; B :: s32; g : A = 7;`) resolves to its
|
||||
@@ -11876,7 +11939,7 @@ pub const Lowering = struct {
|
||||
// The module-const branch is shared verbatim with the stateless
|
||||
// registration-time resolver (`type_bridge`) so a `[N]T` dimension
|
||||
// resolves to the same length on both paths (issue 0083).
|
||||
return program_index_mod.moduleConstInt(&self.program_index.module_const_map, name);
|
||||
return program_index_mod.moduleConstInt(&self.program_index.module_const_map, &self.module.types, name);
|
||||
}
|
||||
|
||||
/// Resolve a type node, checking type_bindings first for generic type params.
|
||||
@@ -15519,6 +15582,20 @@ pub const Lowering = struct {
|
||||
return self.closureShapeKey(params, self.returnValuePart(ret));
|
||||
}
|
||||
|
||||
/// Human-readable name for a literal initializer kind, used in the typed
|
||||
/// module-const type-mismatch diagnostic.
|
||||
fn literalKindName(node: *const Node) []const u8 {
|
||||
return switch (node.data) {
|
||||
.int_literal => "an integer literal",
|
||||
.float_literal => "a float literal",
|
||||
.bool_literal => "a boolean literal",
|
||||
.string_literal => "a string literal",
|
||||
.null_literal => "null",
|
||||
.undef_literal => "'---'",
|
||||
else => "a value",
|
||||
};
|
||||
}
|
||||
|
||||
fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
|
||||
return switch (op) {
|
||||
.add => "+",
|
||||
|
||||
Reference in New Issue
Block a user