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:
agra
2026-05-25 15:59:32 +03:00
parent 179310d62b
commit 071352e655
5 changed files with 90 additions and 21 deletions

View File

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