fix(types): extend unknown-type check into function bodies (issue 0064)

The signature/field check missed body-level type positions: a local
annotation naming a non-existent type flowed through the empty-struct stub
untouched, so `v: Coordnate = 5` silently compiled and ran (the value
dropped) — an invalid program accepted with no diagnostic.

`checkUnknownTypeNames` now also walks each main-file function body
(`checkBodyTypes`): local var/const type annotations — including inside
if / loop / match / push / defer / onfail blocks and decl-value blocks — are
validated with the enclosing function's generic params in scope, and
body-local `T :: struct/enum/union` declarations are collected first
(`collectBodyDeclNames`) so legitimate locals aren't false-flagged. Nested
function/closure bodies are their own scope and are not descended (safe
under-coverage); explicit `cast(T)` already surfaces its own `unresolved`
diagnostic and is left to it.

Regression: examples/1113 (local annotation of a non-existent type, exit 1).
This commit is contained in:
agra
2026-06-02 10:41:29 +03:00
parent c490ffcfe9
commit 63b512a182
6 changed files with 106 additions and 3 deletions

View File

@@ -564,8 +564,12 @@ pub const Lowering = struct {
fn collectDeclaredTypeNames(self: *Lowering, decls: []const *const Node, out: *std.StringHashMap(void)) void {
for (decls) |decl| {
switch (decl.data) {
.const_decl => |cd| out.put(cd.name, {}) catch {},
.const_decl => |cd| {
out.put(cd.name, {}) catch {};
if (cd.value.data == .fn_decl) self.collectBodyDeclNames(cd.value.data.fn_decl.body, out);
},
.struct_decl => |sd| out.put(sd.name, {}) catch {},
.fn_decl => |fd| self.collectBodyDeclNames(fd.body, out),
else => {},
}
}
@@ -581,6 +585,77 @@ pub const Lowering = struct {
while (it_al.next()) |k| out.put(k.*, {}) catch {};
}
/// Collect names declared inside a function body — local `T :: struct/enum/
/// union` types and named consts — so a body-level type annotation
/// referencing one isn't flagged. Recurses control-flow bodies but stops at
/// nested function / closure boundaries (those have their own scope and are
/// not body-checked).
fn collectBodyDeclNames(self: *Lowering, node: *const Node, out: *std.StringHashMap(void)) void {
switch (node.data) {
.block => |b| for (b.stmts) |s| self.collectBodyDeclNames(s, out),
.if_expr => |ie| {
self.collectBodyDeclNames(ie.then_branch, out);
if (ie.else_branch) |e| self.collectBodyDeclNames(e, out);
},
.while_expr => |we| self.collectBodyDeclNames(we.body, out),
.for_expr => |fe| self.collectBodyDeclNames(fe.body, out),
.match_expr => |me| for (me.arms) |arm| self.collectBodyDeclNames(arm.body, out),
.push_stmt => |ps| self.collectBodyDeclNames(ps.body, out),
.defer_stmt => |ds| self.collectBodyDeclNames(ds.expr, out),
.onfail_stmt => |os| self.collectBodyDeclNames(os.body, out),
.const_decl => |cd| {
out.put(cd.name, {}) catch {};
self.collectBodyDeclNames(cd.value, out);
},
.var_decl => |vd| if (vd.value) |v| self.collectBodyDeclNames(v, out),
.struct_decl => |sd| out.put(sd.name, {}) catch {},
.enum_decl => |ed| out.put(ed.name, {}) catch {},
.union_decl => |ud| out.put(ud.name, {}) catch {},
else => {},
}
}
/// Walk a function body checking type annotations on local var / const
/// declarations (and body-local struct fields). `in_scope` and `type_vals`
/// carry the enclosing function's generic params / value-`Type` params.
/// Recurses control-flow and decl-value blocks (so `x := if c { a: T; a }`
/// is reached) but not nested function / closure bodies.
fn checkBodyTypes(
self: *Lowering,
node: *const Node,
declared: *std.StringHashMap(void),
in_scope: []const ast.StructTypeParam,
type_vals: []const []const u8,
) void {
switch (node.data) {
.block => |b| for (b.stmts) |s| self.checkBodyTypes(s, declared, in_scope, type_vals),
.if_expr => |ie| {
self.checkBodyTypes(ie.then_branch, declared, in_scope, type_vals);
if (ie.else_branch) |e| self.checkBodyTypes(e, declared, in_scope, type_vals);
},
.while_expr => |we| self.checkBodyTypes(we.body, declared, in_scope, type_vals),
.for_expr => |fe| self.checkBodyTypes(fe.body, declared, in_scope, type_vals),
.match_expr => |me| for (me.arms) |arm| self.checkBodyTypes(arm.body, declared, in_scope, type_vals),
.push_stmt => |ps| self.checkBodyTypes(ps.body, declared, in_scope, type_vals),
.defer_stmt => |ds| self.checkBodyTypes(ds.expr, declared, in_scope, type_vals),
.onfail_stmt => |os| self.checkBodyTypes(os.body, declared, in_scope, type_vals),
.var_decl => |vd| {
if (vd.type_annotation) |ta| self.checkTypeNodeForUnknown(ta, declared, in_scope, type_vals);
if (vd.value) |v| self.checkBodyTypes(v, declared, in_scope, type_vals);
},
.const_decl => |cd| {
if (cd.type_annotation) |ta| self.checkTypeNodeForUnknown(ta, declared, in_scope, type_vals);
self.checkBodyTypes(cd.value, declared, in_scope, type_vals);
},
.assignment => |a| self.checkBodyTypes(a.value, declared, in_scope, type_vals),
.return_stmt => |r| if (r.value) |v| self.checkBodyTypes(v, declared, in_scope, type_vals),
.struct_decl => |sd| if (sd.type_params.len == 0) {
for (sd.field_types) |ft| self.checkTypeNodeForUnknown(ft, declared, in_scope, type_vals);
},
else => {},
}
}
fn checkStructFieldTypes(self: *Lowering, sd: *const ast.StructDecl, declared: *std.StringHashMap(void)) void {
// Generic struct fields reference the struct's own type params ($T) —
// resolved at instantiation, not here.
@@ -603,6 +678,7 @@ pub const Lowering = struct {
}
for (fd.params) |p| self.checkTypeNodeForUnknown(p.type_expr, declared, fd.type_params, type_vals.items);
if (fd.return_type) |rt| self.checkTypeNodeForUnknown(rt, declared, fd.type_params, type_vals.items);
self.checkBodyTypes(fd.body, declared, fd.type_params, type_vals.items);
}
/// Recurse a type-annotation node to its leaf names, reporting any unknown.