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:
10
examples/1113-diagnostics-unknown-type-local-var-rejected.sx
Normal file
10
examples/1113-diagnostics-unknown-type-local-var-rejected.sx
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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;
|
||||
| ^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user