diff --git a/examples/1113-diagnostics-unknown-type-local-var-rejected.sx b/examples/1113-diagnostics-unknown-type-local-var-rejected.sx new file mode 100644 index 0000000..3a79aaf --- /dev/null +++ b/examples/1113-diagnostics-unknown-type-local-var-rejected.sx @@ -0,0 +1,10 @@ +// An identifier used in a LOCAL variable's type annotation that names no +// declared type is rejected, exactly like a signature or struct-field use. +// Previously a body-level annotation bypassed the check: the type resolver's +// empty-struct stub silently gave the local a 0-field type, so `v: Coordnate +// = 5` compiled and ran (the `5` dropped) with no diagnostic. Regression +// (issue 0064, body-level positions). Expected: error at the annotation; exit 1. +main :: () -> s32 { + v: Coordnate = 5; + return 0; +} diff --git a/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.exit b/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.stderr b/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.stderr new file mode 100644 index 0000000..8e13b80 --- /dev/null +++ b/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.stderr @@ -0,0 +1,5 @@ +error: unknown type 'Coordnate' + --> /Users/agra/projects/sx/examples/1113-diagnostics-unknown-type-local-var-rejected.sx:8:8 + | + 8 | v: Coordnate = 5; + | ^^^^^^^^^ diff --git a/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.stdout b/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1113-diagnostics-unknown-type-local-var-rejected.stdout @@ -0,0 +1 @@ + diff --git a/issues/0064-nondollar-type-param-silent-empty-struct.md b/issues/0064-nondollar-type-param-silent-empty-struct.md index 6263e18..04d2591 100644 --- a/issues/0064-nondollar-type-param-silent-empty-struct.md +++ b/issues/0064-nondollar-type-param-silent-empty-struct.md @@ -17,9 +17,19 @@ > types are recognized via the type table (`findByName`), so cross-module > references aren't false-flagged; inline compound spellings (`[:0]u8`), arbitrary- > width ints (`u1`/`u2`), and `$`-introduced generics (`-> $R`) are all exempted. +> The pass also walks function **bodies** (`checkBodyTypes` + `collectBodyDeclNames`): +> local `var` / `const` type annotations — including inside `if` / loop / `match` / +> `push` / `defer` / `onfail` blocks and decl-value blocks — are checked with the +> enclosing function's generic params in scope, and body-local `T :: struct/enum/ +> union` declarations are collected so they aren't false-flagged. This closes the +> silent body-level hole where `v: Coordnate = 5` (a non-existent type) compiled and +> ran with the value dropped. Nested function / closure bodies are their own scope +> and are not descended (safe under-coverage); explicit `cast(T)` already has its +> own `unresolved` diagnostic and is left to it. > Regression tests: `examples/1111-diagnostics-nondollar-type-param-rejected.sx` -> (tailored hint, exit 1) and `examples/1112-diagnostics-unknown-type-name-rejected.sx` -> (typo'd field type, exit 1). Suite: 347 passed, 0 failed. +> (tailored hint), `examples/1112-diagnostics-unknown-type-name-rejected.sx` +> (typo'd field type), and `examples/1113-diagnostics-unknown-type-local-var-rejected.sx` +> (body-level local annotation) — all exit 1. Suite: 348 passed, 0 failed. ## Symptom diff --git a/src/ir/lower.zig b/src/ir/lower.zig index bb7326e..7e59fd5 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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.