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

@@ -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;
}

View File

@@ -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;
| ^^^^^^^^^

View File

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

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.