mem: remove resolveType(null) → .s64 silent fallback
CLAUDE.md REJECTED PATTERNS forbids silent default returns where the
"reasonable-looking" value happens to match one common case (s64 = 8
bytes = pointer-sized on the host) and is silently wrong everywhere
else. `resolveType(null) → .s64` was exactly this shape: a top-level
`g_pi := 3.14;` was silently typed as `s64`, producing a wrong-typed
slot and the wrong runtime value.
`resolveType` now takes a non-optional `*const Node`. Twelve callers
were classified:
- Six were already guarded by `if (x.type_annotation != null)` blocks
— the null branch was unreachable. Cleaned up to optional-payload
syntax (`if (cd.type_annotation) |ta|`) so the always-non-null path
is obvious from the type.
- Two (`#objc_call` / `#jni_call` return types) pass `FfiIntrinsicCall.
return_type`, which is `*Node` (not optional) in the AST — the
silent fallback couldn't be reached there either.
- One (top-level `var_decl` at lower.zig:630) DID legitimately receive
null when the user omitted both annotation and initializer typing.
Now mirrors `lowerVarDecl`'s local-scope behavior: explicit
annotation → resolveType; no annotation → `inferExprType` from the
initializer; neither → diagnose with a real error message.
- One (`lowerComptimeGlobal`, fixed in commit 82e7b04 alongside
Phase 1.4) already infers from the comptime expression.
- Two (JNI super-call / JNI method return type) were already
hand-rolled with `if (rt) |t| resolveType(t) else .void`.
Regression at `examples/137-toplevel-var-type-inference.sx`: `g_count
:= 42;` / `g_pi := 3.14;` / `g_flag := true;` at module scope. Pre-fix
`g_pi` got silently typed as `s64` and printed `0` or garbage; now it
prints `3.140000`. 159/159 example tests + chess clean.
This commit is contained in:
@@ -574,10 +574,10 @@ pub const Lowering = struct {
|
||||
// comptime_expr handled in Pass 2
|
||||
|
||||
// Simple value constants with type annotation (e.g. AF_INET :s32: 2)
|
||||
if (cd.type_annotation != null) {
|
||||
if (cd.type_annotation) |ta| {
|
||||
switch (cd.value.data) {
|
||||
.int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal => {
|
||||
const ty = self.resolveType(cd.type_annotation);
|
||||
const ty = self.resolveType(ta);
|
||||
self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {};
|
||||
},
|
||||
else => {},
|
||||
@@ -630,8 +630,19 @@ pub const Lowering = struct {
|
||||
.var_decl => |vd| {
|
||||
// Top-level mutable global (e.g., `context : Context = ---;`)
|
||||
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
|
||||
// to their target type (not a synthetic empty struct).
|
||||
const var_ty = self.resolveType(vd.type_annotation);
|
||||
// to their target type (not a synthetic empty struct). When the
|
||||
// user omitted the annotation, infer from the initializer
|
||||
// expression; foreign globals with no annotation are diagnosed
|
||||
// because their type can't be inferred without an initializer.
|
||||
const var_ty: TypeId = if (vd.type_annotation) |ta|
|
||||
self.resolveType(ta)
|
||||
else if (vd.value) |val|
|
||||
self.inferExprType(val)
|
||||
else blk: {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, null, "top-level var '{s}' has no type annotation and no initializer to infer from", .{vd.name});
|
||||
break :blk .void;
|
||||
};
|
||||
// Foreign globals reference a symbol defined in libSystem etc.
|
||||
// (`_NSConcreteStackBlock : *void #foreign;`). The C symbol
|
||||
// name is the optional override or the sx name itself.
|
||||
@@ -1338,9 +1349,9 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void {
|
||||
if (vd.type_annotation != null) {
|
||||
if (vd.type_annotation) |ta| {
|
||||
// Explicit type annotation — resolve type first, then lower value
|
||||
const ty = self.resolveType(vd.type_annotation);
|
||||
const ty = self.resolveType(ta);
|
||||
const slot = self.builder.alloca(ty);
|
||||
if (vd.value) |val| {
|
||||
// = --- (undef_literal) on tuple types: zero-initialize
|
||||
@@ -1478,8 +1489,8 @@ pub const Lowering = struct {
|
||||
|
||||
const ref = self.lowerExpr(cd.value);
|
||||
// If there's an explicit type annotation, use it. Otherwise, infer from the expression.
|
||||
const ty = if (cd.type_annotation != null)
|
||||
self.resolveType(cd.type_annotation)
|
||||
const ty = if (cd.type_annotation) |ta|
|
||||
self.resolveType(ta)
|
||||
else
|
||||
self.builder.getRefType(ref);
|
||||
|
||||
@@ -8389,9 +8400,8 @@ pub const Lowering = struct {
|
||||
return elem_ty;
|
||||
}
|
||||
|
||||
fn resolveType(self: *Lowering, type_ann: ?*const Node) TypeId {
|
||||
if (type_ann) |n| return self.resolveTypeWithBindings(n);
|
||||
return .s64;
|
||||
fn resolveType(self: *Lowering, type_ann: *const Node) TypeId {
|
||||
return self.resolveTypeWithBindings(type_ann);
|
||||
}
|
||||
|
||||
/// Resolve a type node, checking type_bindings first for generic type params.
|
||||
|
||||
Reference in New Issue
Block a user