diff --git a/Makefile b/Makefile index 0d8212b2..e5b3797e 100644 --- a/Makefile +++ b/Makefile @@ -215,7 +215,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_at_test \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ - $(BIN)/test_dyn_ww + $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_smoke: test/wcc/000_smoke.c $(LIB)/libwcc.a | $(BIN) $(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwcc @@ -283,6 +283,9 @@ $(BIN)/test_self_rebuild: test/wcc/995_self_rebuild.c $(BIN)/ww_ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_selfcheck: test/wcc/950_selfcheck.c $(BIN)/wwdump_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_dyn_ww: test/wcc/996_dyn_ww.c $(BIN)/ww $(BIN)/w6l $(BIN)/w6l_ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 23a89952..329515b9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -3747,6 +3747,7 @@ type checker = struct { nunresolved: i32, errs: i32, verbose: i32, // when non-zero, log each unresolved name + fnret: *node, // enclosing fn's return type AST (for `?`) }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -3799,10 +3800,17 @@ fn installdecl(c: *checker, d: *node) void = { // their init/type expressions have been walked (forward use of a let // before its declaration would resolve to nothing — same semantics as // the C checker's collect-then-resolve flow within a function). +// Also runs the typed checks (match exhaustiveness, ? subset) in +// the same pass — they need the same scope state. fn resolvewalk(c: *checker, n: *node) void = { if (n == nil) { return; }; let k: i32 = n.kind; + // Typed checks fire on the way down so the scrutinee/operand + // is examined before the arm bodies install new bindings. + if (k == N_MATCH) { check_match_exhaustive(c, n); }; + if (k == N_TRYPROP) { check_tryprop(c, n); }; + // `use IDENT;` — name is a module label, not a free ident. if (k == N_USE) { return; }; @@ -3914,6 +3922,275 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; +// ---- type-level helpers (AST-level, no resolved tinfo) -------------- +// +// The selfhost check operates on AST type expressions rather than +// resolved Type structs. These helpers mirror what cmd/wcc/check.c +// does with tinfo, but only on the subset of cases this checker +// needs to enforce: tagged-union exhaustiveness, ? subset +// propagation, and !-flag semantics. + +// unwrapbang — strip an N_TBANG wrapper; leaves other nodes alone. +fn unwrapbang(n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind == N_TBANG) { return n.lhs; }; + return n; +}; + +// resolvealias — if n is an N_TNAME pointing at a typedecl, return +// the typedecl's body (possibly recursively). Pass-through for any +// other node. The chain stops once we hit a non-N_TNAME node or a +// name we can't resolve. +fn resolvealias(c: *checker, n: *node) *node = { + let cur: *node = n; + for (cur != nil) { + if (cur.kind != N_TNAME) { return cur; }; + let s: *sym = scopelookup(c.cur, cur.str); + if (s == nil) { return cur; }; + if (s.skind != SK_TYPE) { return cur; }; + let body: *node = nil; + if (s.decl != nil) { body = s.decl.lhs; }; + if (body == nil) { return cur; }; + cur = unwrapbang(body); + }; + return n; +}; + +// is_tagged_type — true if `n` (after alias resolution) is an +// N_TTAGGED type expression. +fn is_tagged_type(c: *checker, n: *node) bool = { + let u: *node = resolvealias(c, unwrapbang(n)); + if (u == nil) { return false; }; + return u.kind == N_TTAGGED; +}; + +// type_eq_ast — structural equality on AST type expressions, mod +// the `!` wrapper. Mirrors variant_match in cgen + check.c: NAMED +// types compare by string (the closest stand-in for pointer +// identity at the AST level); other nodes recurse by kind. +fn type_eq_ast(a: *node, b: *node) bool = { + let aa: *node = unwrapbang(a); + let bb: *node = unwrapbang(b); + if (aa == nil) { return bb == nil; }; + if (bb == nil) { return false; }; + if (aa.kind != bb.kind) { return false; }; + let k: i32 = aa.kind; + if (k == N_TNAME) { return streq(aa.str, bb.str); }; + if (k == N_TPTR) { return type_eq_ast(aa.lhs, bb.lhs); }; + if (k == N_TSLICE){ return type_eq_ast(aa.lhs, bb.lhs); }; + if (k == N_TCHAN) { return type_eq_ast(aa.lhs, bb.lhs); }; + // Conservative: anything else (struct/fn/tagged/tuple/array) + // fails the cheap check. Selfhost code doesn't currently rely + // on equality at these shapes for the targeted checks. + return false; +}; + +// variant_is_error — does this variant carry the `!` mark? Either +// the variant itself is N_TBANG or it's an alias whose typedecl +// body is `!T`. Mirrors C check.c's iserror-after-NAMED rule. +fn variant_is_error(c: *checker, v: *node) bool = { + if (v == nil) { return false; }; + if (v.kind == N_TBANG) { return true; }; + if (v.kind == N_TNAME) { + let s: *sym = scopelookup(c.cur, v.str); + if (s != nil) { + if (s.skind == SK_TYPE) { + if (s.decl != nil) { + if (s.decl.lhs != nil) { + if (s.decl.lhs.kind == N_TBANG) { + return true; + }; + }; + }; + }; + }; + }; + return false; +}; + +// tagged_has_errflag — true iff any variant of `n` (assumed +// N_TTAGGED) is `!`-marked. Picks the explicit-flag semantics over +// the legacy "first variant = success" rule. +fn tagged_has_errflag(c: *checker, n: *node) bool = { + let v: *node = n.list; + for (v != nil) { + if (variant_is_error(c, v)) { return true; }; + v = v.next; + }; + return false; +}; + +// is_error_variant — under flag-aware mode (any !-marked variant), +// returns true iff `v` is `!`-marked. Under legacy mode (no flags), +// returns true iff `v` is not the first variant of `tagged`. +fn is_error_variant(c: *checker, tagged: *node, v: *node) bool = { + if (tagged_has_errflag(c, tagged)) { + return variant_is_error(c, v); + }; + // Legacy: first variant of the union is success. + if (tagged.list == v) { return false; }; + return true; +}; + +// scrutinee_type — resolve the type expression for a match's +// scrutinee. Handles N_IDENT (look up local/param's declared +// type) and N_DOT (struct-field access). Returns nil if we +// can't statically determine the type. Used by exhaustiveness. +fn scrutinee_type(c: *checker, e: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind == N_IDENT) { + let s: *sym = scopelookup(c.cur, e.str); + if (s == nil) { return nil; }; + if (s.decl == nil) { return nil; }; + // For N_LET / N_PARAM: declared type is decl.lhs. + return s.decl.lhs; + }; + return nil; +}; + +// ---- match exhaustiveness -------------------------------------------- +// +// For every match arm, verify that every variant of the scrutinee's +// tagged-union type is handled by some case (or a default arm +// exists). Multi-pattern `case A | B =>` covers all alts. + +fn case_covers(c: *checker, cs: *node, want: *node) bool = { + if (cs.lhs != nil) { + if (type_eq_ast(cs.lhs, want)) { return true; }; + }; + let alt: *node = cs.list; + for (alt != nil) { + if (type_eq_ast(alt, want)) { return true; }; + alt = alt.next; + }; + return false; +}; + +fn err_match_variant(c: *checker, n: *node, vname: *node) void = { + os.write(2, "match: variant not handled".ptr, 26u64); + if (vname != nil) { + if (vname.kind == N_TNAME) { + os.write(2, " (".ptr, 2u64); + os.write(2, vname.str.ptr, vname.str.len: u64); + os.write(2, ")".ptr, 1u64); + }; + }; + os.write(2, "\n".ptr, 1u64); + c.errs += 1; +}; + +fn check_match_exhaustive(c: *checker, n: *node) void = { + if (n == nil) { return; }; + if (n.lhs == nil) { return; }; + let st: *node = scrutinee_type(c, n.lhs); + let u: *node = resolvealias(c, unwrapbang(st)); + if (u == nil) { return; }; + if (u.kind != N_TTAGGED) { return; }; + // Default arm absorbs anything; skip. + let cs: *node = n.list; + for (cs != nil) { + if (cs.lhs == nil) { return; }; // default + cs = cs.next; + }; + // For each variant of u, look for a covering case. + let v: *node = u.list; + for (v != nil) { + let covered: bool = false; + let cs2: *node = n.list; + for (cs2 != nil) { + if (case_covers(c, cs2, v)) { + covered = true; + cs2 = nil; + } else { + cs2 = cs2.next; + }; + }; + if (!covered) { err_match_variant(c, n, v); }; + v = v.next; + }; +}; + +// ---- ? subset propagation -------------------------------------------- +// +// For `expr?`, the operand's error subset must be a subset of the +// enclosing fn's return-type variants. Mirrors C check.c. Operand +// is N_TRYPROP; its lhs is the value-bearing expr; we look at the +// expr's *declared* type for N_IDENT/N_CALL cases. + +fn expr_type_for_tryprop(c: *checker, e: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind == N_IDENT) { + let s: *sym = scopelookup(c.cur, e.str); + if (s == nil) { return nil; }; + if (s.decl == nil) { return nil; }; + return s.decl.lhs; + }; + if (e.kind == N_CALL) { + // callee return type lookup: callee is e.lhs (N_IDENT or + // N_DOT). We need the fn-decl's lhs (return-type AST). + let callee: *node = e.lhs; + if (callee == nil) { return nil; }; + let nm: str; + nm.ptr = nil; nm.len = 0; + if (callee.kind == N_IDENT) { nm = callee.str; }; + if (callee.kind == N_DOT) { nm = callee.str; }; + if (nm.len == 0) { return nil; }; + let s: *sym = scopelookup(c.cur, nm); + if (s == nil) { return nil; }; + if (s.skind != SK_FN) { return nil; }; + if (s.decl == nil) { return nil; }; + return s.decl.lhs; + }; + return nil; +}; + +fn check_tryprop(c: *checker, n: *node) void = { + if (n == nil) { return; }; + let t: *node = expr_type_for_tryprop(c, n.lhs); + let u: *node = resolvealias(c, unwrapbang(t)); + if (u == nil) { return; }; + if (u.kind != N_TTAGGED) { return; }; + // Does the operand have any error variants? + let has_err: bool = false; + let v: *node = u.list; + for (v != nil) { + if (is_error_variant(c, u, v)) { has_err = true; }; + v = v.next; + }; + if (!has_err) { return; }; + // Enclosing fn must return a tagged union with each operand + // error variant present. + let r: *node = resolvealias(c, unwrapbang(c.fnret)); + if (r == nil) { + os.write(2, "?: enclosing fn has no tagged-union return\n".ptr, 43u64); + c.errs += 1; + return; + }; + if (r.kind != N_TTAGGED) { + os.write(2, "?: enclosing fn return is not tagged\n".ptr, 37u64); + c.errs += 1; + return; + }; + let ev: *node = u.list; + for (ev != nil) { + if (is_error_variant(c, u, ev)) { + let found: bool = false; + let rv: *node = r.list; + for (rv != nil) { + if (type_eq_ast(rv, ev)) { + found = true; + rv = nil; + } else { rv = rv.next; }; + }; + if (!found) { + os.write(2, "?: error variant not in enclosing return\n".ptr, 41u64); + c.errs += 1; + }; + }; + ev = ev.next; + }; +}; + // install_param — when entering a fn body, define its params in a // fresh local scope. fn installparams(c: *checker, params: *node) void = { @@ -3937,9 +4214,12 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = { let outer: *scope = c.cur; c.cur = newscope(c.a, c.cur); installparams(c, fnnode.list); + let prevret: *node = c.fnret; + c.fnret = fnnode.lhs; // return type AST, used by `?` check if (fnnode.body != nil) { resolvewalk(c, fnnode.body); }; + c.fnret = prevret; c.cur = outer; }; @@ -3952,6 +4232,7 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = { c.nunresolved = 0; c.errs = 0; c.verbose = 0; + c.fnret = nil; seedprimitives(c); }; @@ -3984,6 +4265,7 @@ export fn checkfile(c: *checker, file: *node) void = { };};};}; d = d.next; }; + }; // MODULE: wcc diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 1a6e74a7..92bd67ed 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -30,6 +30,7 @@ type checker = struct { nunresolved: i32, errs: i32, verbose: i32, // when non-zero, log each unresolved name + fnret: *node, // enclosing fn's return type AST (for `?`) }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -82,10 +83,17 @@ fn installdecl(c: *checker, d: *node) void = { // their init/type expressions have been walked (forward use of a let // before its declaration would resolve to nothing — same semantics as // the C checker's collect-then-resolve flow within a function). +// Also runs the typed checks (match exhaustiveness, ? subset) in +// the same pass — they need the same scope state. fn resolvewalk(c: *checker, n: *node) void = { if (n == nil) { return; }; let k: i32 = n.kind; + // Typed checks fire on the way down so the scrutinee/operand + // is examined before the arm bodies install new bindings. + if (k == N_MATCH) { check_match_exhaustive(c, n); }; + if (k == N_TRYPROP) { check_tryprop(c, n); }; + // `use IDENT;` — name is a module label, not a free ident. if (k == N_USE) { return; }; @@ -197,6 +205,275 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; +// ---- type-level helpers (AST-level, no resolved tinfo) -------------- +// +// The selfhost check operates on AST type expressions rather than +// resolved Type structs. These helpers mirror what cmd/wcc/check.c +// does with tinfo, but only on the subset of cases this checker +// needs to enforce: tagged-union exhaustiveness, ? subset +// propagation, and !-flag semantics. + +// unwrapbang — strip an N_TBANG wrapper; leaves other nodes alone. +fn unwrapbang(n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind == N_TBANG) { return n.lhs; }; + return n; +}; + +// resolvealias — if n is an N_TNAME pointing at a typedecl, return +// the typedecl's body (possibly recursively). Pass-through for any +// other node. The chain stops once we hit a non-N_TNAME node or a +// name we can't resolve. +fn resolvealias(c: *checker, n: *node) *node = { + let cur: *node = n; + for (cur != nil) { + if (cur.kind != N_TNAME) { return cur; }; + let s: *sym = scopelookup(c.cur, cur.str); + if (s == nil) { return cur; }; + if (s.skind != SK_TYPE) { return cur; }; + let body: *node = nil; + if (s.decl != nil) { body = s.decl.lhs; }; + if (body == nil) { return cur; }; + cur = unwrapbang(body); + }; + return n; +}; + +// is_tagged_type — true if `n` (after alias resolution) is an +// N_TTAGGED type expression. +fn is_tagged_type(c: *checker, n: *node) bool = { + let u: *node = resolvealias(c, unwrapbang(n)); + if (u == nil) { return false; }; + return u.kind == N_TTAGGED; +}; + +// type_eq_ast — structural equality on AST type expressions, mod +// the `!` wrapper. Mirrors variant_match in cgen + check.c: NAMED +// types compare by string (the closest stand-in for pointer +// identity at the AST level); other nodes recurse by kind. +fn type_eq_ast(a: *node, b: *node) bool = { + let aa: *node = unwrapbang(a); + let bb: *node = unwrapbang(b); + if (aa == nil) { return bb == nil; }; + if (bb == nil) { return false; }; + if (aa.kind != bb.kind) { return false; }; + let k: i32 = aa.kind; + if (k == N_TNAME) { return streq(aa.str, bb.str); }; + if (k == N_TPTR) { return type_eq_ast(aa.lhs, bb.lhs); }; + if (k == N_TSLICE){ return type_eq_ast(aa.lhs, bb.lhs); }; + if (k == N_TCHAN) { return type_eq_ast(aa.lhs, bb.lhs); }; + // Conservative: anything else (struct/fn/tagged/tuple/array) + // fails the cheap check. Selfhost code doesn't currently rely + // on equality at these shapes for the targeted checks. + return false; +}; + +// variant_is_error — does this variant carry the `!` mark? Either +// the variant itself is N_TBANG or it's an alias whose typedecl +// body is `!T`. Mirrors C check.c's iserror-after-NAMED rule. +fn variant_is_error(c: *checker, v: *node) bool = { + if (v == nil) { return false; }; + if (v.kind == N_TBANG) { return true; }; + if (v.kind == N_TNAME) { + let s: *sym = scopelookup(c.cur, v.str); + if (s != nil) { + if (s.skind == SK_TYPE) { + if (s.decl != nil) { + if (s.decl.lhs != nil) { + if (s.decl.lhs.kind == N_TBANG) { + return true; + }; + }; + }; + }; + }; + }; + return false; +}; + +// tagged_has_errflag — true iff any variant of `n` (assumed +// N_TTAGGED) is `!`-marked. Picks the explicit-flag semantics over +// the legacy "first variant = success" rule. +fn tagged_has_errflag(c: *checker, n: *node) bool = { + let v: *node = n.list; + for (v != nil) { + if (variant_is_error(c, v)) { return true; }; + v = v.next; + }; + return false; +}; + +// is_error_variant — under flag-aware mode (any !-marked variant), +// returns true iff `v` is `!`-marked. Under legacy mode (no flags), +// returns true iff `v` is not the first variant of `tagged`. +fn is_error_variant(c: *checker, tagged: *node, v: *node) bool = { + if (tagged_has_errflag(c, tagged)) { + return variant_is_error(c, v); + }; + // Legacy: first variant of the union is success. + if (tagged.list == v) { return false; }; + return true; +}; + +// scrutinee_type — resolve the type expression for a match's +// scrutinee. Handles N_IDENT (look up local/param's declared +// type) and N_DOT (struct-field access). Returns nil if we +// can't statically determine the type. Used by exhaustiveness. +fn scrutinee_type(c: *checker, e: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind == N_IDENT) { + let s: *sym = scopelookup(c.cur, e.str); + if (s == nil) { return nil; }; + if (s.decl == nil) { return nil; }; + // For N_LET / N_PARAM: declared type is decl.lhs. + return s.decl.lhs; + }; + return nil; +}; + +// ---- match exhaustiveness -------------------------------------------- +// +// For every match arm, verify that every variant of the scrutinee's +// tagged-union type is handled by some case (or a default arm +// exists). Multi-pattern `case A | B =>` covers all alts. + +fn case_covers(c: *checker, cs: *node, want: *node) bool = { + if (cs.lhs != nil) { + if (type_eq_ast(cs.lhs, want)) { return true; }; + }; + let alt: *node = cs.list; + for (alt != nil) { + if (type_eq_ast(alt, want)) { return true; }; + alt = alt.next; + }; + return false; +}; + +fn err_match_variant(c: *checker, n: *node, vname: *node) void = { + os.write(2, "match: variant not handled".ptr, 26u64); + if (vname != nil) { + if (vname.kind == N_TNAME) { + os.write(2, " (".ptr, 2u64); + os.write(2, vname.str.ptr, vname.str.len: u64); + os.write(2, ")".ptr, 1u64); + }; + }; + os.write(2, "\n".ptr, 1u64); + c.errs += 1; +}; + +fn check_match_exhaustive(c: *checker, n: *node) void = { + if (n == nil) { return; }; + if (n.lhs == nil) { return; }; + let st: *node = scrutinee_type(c, n.lhs); + let u: *node = resolvealias(c, unwrapbang(st)); + if (u == nil) { return; }; + if (u.kind != N_TTAGGED) { return; }; + // Default arm absorbs anything; skip. + let cs: *node = n.list; + for (cs != nil) { + if (cs.lhs == nil) { return; }; // default + cs = cs.next; + }; + // For each variant of u, look for a covering case. + let v: *node = u.list; + for (v != nil) { + let covered: bool = false; + let cs2: *node = n.list; + for (cs2 != nil) { + if (case_covers(c, cs2, v)) { + covered = true; + cs2 = nil; + } else { + cs2 = cs2.next; + }; + }; + if (!covered) { err_match_variant(c, n, v); }; + v = v.next; + }; +}; + +// ---- ? subset propagation -------------------------------------------- +// +// For `expr?`, the operand's error subset must be a subset of the +// enclosing fn's return-type variants. Mirrors C check.c. Operand +// is N_TRYPROP; its lhs is the value-bearing expr; we look at the +// expr's *declared* type for N_IDENT/N_CALL cases. + +fn expr_type_for_tryprop(c: *checker, e: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind == N_IDENT) { + let s: *sym = scopelookup(c.cur, e.str); + if (s == nil) { return nil; }; + if (s.decl == nil) { return nil; }; + return s.decl.lhs; + }; + if (e.kind == N_CALL) { + // callee return type lookup: callee is e.lhs (N_IDENT or + // N_DOT). We need the fn-decl's lhs (return-type AST). + let callee: *node = e.lhs; + if (callee == nil) { return nil; }; + let nm: str; + nm.ptr = nil; nm.len = 0; + if (callee.kind == N_IDENT) { nm = callee.str; }; + if (callee.kind == N_DOT) { nm = callee.str; }; + if (nm.len == 0) { return nil; }; + let s: *sym = scopelookup(c.cur, nm); + if (s == nil) { return nil; }; + if (s.skind != SK_FN) { return nil; }; + if (s.decl == nil) { return nil; }; + return s.decl.lhs; + }; + return nil; +}; + +fn check_tryprop(c: *checker, n: *node) void = { + if (n == nil) { return; }; + let t: *node = expr_type_for_tryprop(c, n.lhs); + let u: *node = resolvealias(c, unwrapbang(t)); + if (u == nil) { return; }; + if (u.kind != N_TTAGGED) { return; }; + // Does the operand have any error variants? + let has_err: bool = false; + let v: *node = u.list; + for (v != nil) { + if (is_error_variant(c, u, v)) { has_err = true; }; + v = v.next; + }; + if (!has_err) { return; }; + // Enclosing fn must return a tagged union with each operand + // error variant present. + let r: *node = resolvealias(c, unwrapbang(c.fnret)); + if (r == nil) { + os.write(2, "?: enclosing fn has no tagged-union return\n".ptr, 43u64); + c.errs += 1; + return; + }; + if (r.kind != N_TTAGGED) { + os.write(2, "?: enclosing fn return is not tagged\n".ptr, 37u64); + c.errs += 1; + return; + }; + let ev: *node = u.list; + for (ev != nil) { + if (is_error_variant(c, u, ev)) { + let found: bool = false; + let rv: *node = r.list; + for (rv != nil) { + if (type_eq_ast(rv, ev)) { + found = true; + rv = nil; + } else { rv = rv.next; }; + }; + if (!found) { + os.write(2, "?: error variant not in enclosing return\n".ptr, 41u64); + c.errs += 1; + }; + }; + ev = ev.next; + }; +}; + // install_param — when entering a fn body, define its params in a // fresh local scope. fn installparams(c: *checker, params: *node) void = { @@ -220,9 +497,12 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = { let outer: *scope = c.cur; c.cur = newscope(c.a, c.cur); installparams(c, fnnode.list); + let prevret: *node = c.fnret; + c.fnret = fnnode.lhs; // return type AST, used by `?` check if (fnnode.body != nil) { resolvewalk(c, fnnode.body); }; + c.fnret = prevret; c.cur = outer; }; @@ -235,6 +515,7 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = { c.nunresolved = 0; c.errs = 0; c.verbose = 0; + c.fnret = nil; seedprimitives(c); }; @@ -267,4 +548,5 @@ export fn checkfile(c: *checker, file: *node) void = { };};};}; d = d.next; }; + }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 26ce53f6..68feb3ab 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -3747,6 +3747,7 @@ type checker = struct { nunresolved: i32, errs: i32, verbose: i32, // when non-zero, log each unresolved name + fnret: *node, // enclosing fn's return type AST (for `?`) }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -3799,10 +3800,17 @@ fn installdecl(c: *checker, d: *node) void = { // their init/type expressions have been walked (forward use of a let // before its declaration would resolve to nothing — same semantics as // the C checker's collect-then-resolve flow within a function). +// Also runs the typed checks (match exhaustiveness, ? subset) in +// the same pass — they need the same scope state. fn resolvewalk(c: *checker, n: *node) void = { if (n == nil) { return; }; let k: i32 = n.kind; + // Typed checks fire on the way down so the scrutinee/operand + // is examined before the arm bodies install new bindings. + if (k == N_MATCH) { check_match_exhaustive(c, n); }; + if (k == N_TRYPROP) { check_tryprop(c, n); }; + // `use IDENT;` — name is a module label, not a free ident. if (k == N_USE) { return; }; @@ -3914,6 +3922,275 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; +// ---- type-level helpers (AST-level, no resolved tinfo) -------------- +// +// The selfhost check operates on AST type expressions rather than +// resolved Type structs. These helpers mirror what cmd/wcc/check.c +// does with tinfo, but only on the subset of cases this checker +// needs to enforce: tagged-union exhaustiveness, ? subset +// propagation, and !-flag semantics. + +// unwrapbang — strip an N_TBANG wrapper; leaves other nodes alone. +fn unwrapbang(n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind == N_TBANG) { return n.lhs; }; + return n; +}; + +// resolvealias — if n is an N_TNAME pointing at a typedecl, return +// the typedecl's body (possibly recursively). Pass-through for any +// other node. The chain stops once we hit a non-N_TNAME node or a +// name we can't resolve. +fn resolvealias(c: *checker, n: *node) *node = { + let cur: *node = n; + for (cur != nil) { + if (cur.kind != N_TNAME) { return cur; }; + let s: *sym = scopelookup(c.cur, cur.str); + if (s == nil) { return cur; }; + if (s.skind != SK_TYPE) { return cur; }; + let body: *node = nil; + if (s.decl != nil) { body = s.decl.lhs; }; + if (body == nil) { return cur; }; + cur = unwrapbang(body); + }; + return n; +}; + +// is_tagged_type — true if `n` (after alias resolution) is an +// N_TTAGGED type expression. +fn is_tagged_type(c: *checker, n: *node) bool = { + let u: *node = resolvealias(c, unwrapbang(n)); + if (u == nil) { return false; }; + return u.kind == N_TTAGGED; +}; + +// type_eq_ast — structural equality on AST type expressions, mod +// the `!` wrapper. Mirrors variant_match in cgen + check.c: NAMED +// types compare by string (the closest stand-in for pointer +// identity at the AST level); other nodes recurse by kind. +fn type_eq_ast(a: *node, b: *node) bool = { + let aa: *node = unwrapbang(a); + let bb: *node = unwrapbang(b); + if (aa == nil) { return bb == nil; }; + if (bb == nil) { return false; }; + if (aa.kind != bb.kind) { return false; }; + let k: i32 = aa.kind; + if (k == N_TNAME) { return streq(aa.str, bb.str); }; + if (k == N_TPTR) { return type_eq_ast(aa.lhs, bb.lhs); }; + if (k == N_TSLICE){ return type_eq_ast(aa.lhs, bb.lhs); }; + if (k == N_TCHAN) { return type_eq_ast(aa.lhs, bb.lhs); }; + // Conservative: anything else (struct/fn/tagged/tuple/array) + // fails the cheap check. Selfhost code doesn't currently rely + // on equality at these shapes for the targeted checks. + return false; +}; + +// variant_is_error — does this variant carry the `!` mark? Either +// the variant itself is N_TBANG or it's an alias whose typedecl +// body is `!T`. Mirrors C check.c's iserror-after-NAMED rule. +fn variant_is_error(c: *checker, v: *node) bool = { + if (v == nil) { return false; }; + if (v.kind == N_TBANG) { return true; }; + if (v.kind == N_TNAME) { + let s: *sym = scopelookup(c.cur, v.str); + if (s != nil) { + if (s.skind == SK_TYPE) { + if (s.decl != nil) { + if (s.decl.lhs != nil) { + if (s.decl.lhs.kind == N_TBANG) { + return true; + }; + }; + }; + }; + }; + }; + return false; +}; + +// tagged_has_errflag — true iff any variant of `n` (assumed +// N_TTAGGED) is `!`-marked. Picks the explicit-flag semantics over +// the legacy "first variant = success" rule. +fn tagged_has_errflag(c: *checker, n: *node) bool = { + let v: *node = n.list; + for (v != nil) { + if (variant_is_error(c, v)) { return true; }; + v = v.next; + }; + return false; +}; + +// is_error_variant — under flag-aware mode (any !-marked variant), +// returns true iff `v` is `!`-marked. Under legacy mode (no flags), +// returns true iff `v` is not the first variant of `tagged`. +fn is_error_variant(c: *checker, tagged: *node, v: *node) bool = { + if (tagged_has_errflag(c, tagged)) { + return variant_is_error(c, v); + }; + // Legacy: first variant of the union is success. + if (tagged.list == v) { return false; }; + return true; +}; + +// scrutinee_type — resolve the type expression for a match's +// scrutinee. Handles N_IDENT (look up local/param's declared +// type) and N_DOT (struct-field access). Returns nil if we +// can't statically determine the type. Used by exhaustiveness. +fn scrutinee_type(c: *checker, e: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind == N_IDENT) { + let s: *sym = scopelookup(c.cur, e.str); + if (s == nil) { return nil; }; + if (s.decl == nil) { return nil; }; + // For N_LET / N_PARAM: declared type is decl.lhs. + return s.decl.lhs; + }; + return nil; +}; + +// ---- match exhaustiveness -------------------------------------------- +// +// For every match arm, verify that every variant of the scrutinee's +// tagged-union type is handled by some case (or a default arm +// exists). Multi-pattern `case A | B =>` covers all alts. + +fn case_covers(c: *checker, cs: *node, want: *node) bool = { + if (cs.lhs != nil) { + if (type_eq_ast(cs.lhs, want)) { return true; }; + }; + let alt: *node = cs.list; + for (alt != nil) { + if (type_eq_ast(alt, want)) { return true; }; + alt = alt.next; + }; + return false; +}; + +fn err_match_variant(c: *checker, n: *node, vname: *node) void = { + os.write(2, "match: variant not handled".ptr, 26u64); + if (vname != nil) { + if (vname.kind == N_TNAME) { + os.write(2, " (".ptr, 2u64); + os.write(2, vname.str.ptr, vname.str.len: u64); + os.write(2, ")".ptr, 1u64); + }; + }; + os.write(2, "\n".ptr, 1u64); + c.errs += 1; +}; + +fn check_match_exhaustive(c: *checker, n: *node) void = { + if (n == nil) { return; }; + if (n.lhs == nil) { return; }; + let st: *node = scrutinee_type(c, n.lhs); + let u: *node = resolvealias(c, unwrapbang(st)); + if (u == nil) { return; }; + if (u.kind != N_TTAGGED) { return; }; + // Default arm absorbs anything; skip. + let cs: *node = n.list; + for (cs != nil) { + if (cs.lhs == nil) { return; }; // default + cs = cs.next; + }; + // For each variant of u, look for a covering case. + let v: *node = u.list; + for (v != nil) { + let covered: bool = false; + let cs2: *node = n.list; + for (cs2 != nil) { + if (case_covers(c, cs2, v)) { + covered = true; + cs2 = nil; + } else { + cs2 = cs2.next; + }; + }; + if (!covered) { err_match_variant(c, n, v); }; + v = v.next; + }; +}; + +// ---- ? subset propagation -------------------------------------------- +// +// For `expr?`, the operand's error subset must be a subset of the +// enclosing fn's return-type variants. Mirrors C check.c. Operand +// is N_TRYPROP; its lhs is the value-bearing expr; we look at the +// expr's *declared* type for N_IDENT/N_CALL cases. + +fn expr_type_for_tryprop(c: *checker, e: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind == N_IDENT) { + let s: *sym = scopelookup(c.cur, e.str); + if (s == nil) { return nil; }; + if (s.decl == nil) { return nil; }; + return s.decl.lhs; + }; + if (e.kind == N_CALL) { + // callee return type lookup: callee is e.lhs (N_IDENT or + // N_DOT). We need the fn-decl's lhs (return-type AST). + let callee: *node = e.lhs; + if (callee == nil) { return nil; }; + let nm: str; + nm.ptr = nil; nm.len = 0; + if (callee.kind == N_IDENT) { nm = callee.str; }; + if (callee.kind == N_DOT) { nm = callee.str; }; + if (nm.len == 0) { return nil; }; + let s: *sym = scopelookup(c.cur, nm); + if (s == nil) { return nil; }; + if (s.skind != SK_FN) { return nil; }; + if (s.decl == nil) { return nil; }; + return s.decl.lhs; + }; + return nil; +}; + +fn check_tryprop(c: *checker, n: *node) void = { + if (n == nil) { return; }; + let t: *node = expr_type_for_tryprop(c, n.lhs); + let u: *node = resolvealias(c, unwrapbang(t)); + if (u == nil) { return; }; + if (u.kind != N_TTAGGED) { return; }; + // Does the operand have any error variants? + let has_err: bool = false; + let v: *node = u.list; + for (v != nil) { + if (is_error_variant(c, u, v)) { has_err = true; }; + v = v.next; + }; + if (!has_err) { return; }; + // Enclosing fn must return a tagged union with each operand + // error variant present. + let r: *node = resolvealias(c, unwrapbang(c.fnret)); + if (r == nil) { + os.write(2, "?: enclosing fn has no tagged-union return\n".ptr, 43u64); + c.errs += 1; + return; + }; + if (r.kind != N_TTAGGED) { + os.write(2, "?: enclosing fn return is not tagged\n".ptr, 37u64); + c.errs += 1; + return; + }; + let ev: *node = u.list; + for (ev != nil) { + if (is_error_variant(c, u, ev)) { + let found: bool = false; + let rv: *node = r.list; + for (rv != nil) { + if (type_eq_ast(rv, ev)) { + found = true; + rv = nil; + } else { rv = rv.next; }; + }; + if (!found) { + os.write(2, "?: error variant not in enclosing return\n".ptr, 41u64); + c.errs += 1; + }; + }; + ev = ev.next; + }; +}; + // install_param — when entering a fn body, define its params in a // fresh local scope. fn installparams(c: *checker, params: *node) void = { @@ -3937,9 +4214,12 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = { let outer: *scope = c.cur; c.cur = newscope(c.a, c.cur); installparams(c, fnnode.list); + let prevret: *node = c.fnret; + c.fnret = fnnode.lhs; // return type AST, used by `?` check if (fnnode.body != nil) { resolvewalk(c, fnnode.body); }; + c.fnret = prevret; c.cur = outer; }; @@ -3952,6 +4232,7 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = { c.nunresolved = 0; c.errs = 0; c.verbose = 0; + c.fnret = nil; seedprimitives(c); }; @@ -3984,6 +4265,7 @@ export fn checkfile(c: *checker, file: *node) void = { };};};}; d = d.next; }; + }; // MODULE: wcc diff --git a/test/wcc/950_selfcheck.c b/test/wcc/950_selfcheck.c new file mode 100644 index 00000000..fd4efa2f --- /dev/null +++ b/test/wcc/950_selfcheck.c @@ -0,0 +1,144 @@ +/* + * 950_selfcheck — selfhost typechecker emits the same errors the + * C-side checker does for tagged-union misuse. + * + * Each row is a .ww fixture + an expected stderr substring. We run + * `wwdump_ww -r` (which invokes the selfhost checker's resolve + + * typed passes) and grep for the substring. + */ +#include +#include +#include +#include +#include + +static int +runcap(const char *cmd, char **outerr, size_t *outlen) +{ + char tmp[256]; + snprintf(tmp, sizeof tmp, "/tmp/wwd_sc_%d", getpid()); + char full[2048]; + snprintf(full, sizeof full, "%s 2>%s 1>/dev/null", cmd, tmp); + int rc = system(full); + FILE *f = fopen(tmp, "rb"); + if (!f) { unlink(tmp); return -1; } + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); unlink(tmp); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { + free(b); fclose(f); unlink(tmp); return -1; + } + b[n] = '\0'; + fclose(f); unlink(tmp); + *outerr = b; *outlen = (size_t)n; + (void)rc; + return 0; +} + +static int +writefile(const char *path, const char *src) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + return 0; +} + +struct row { const char *src; const char *expect; }; + +static const struct row rows[] = { + /* match: missing variant */ + { "fn pick() (i32 | str | bool) = { return 1; };\n" + "fn caller() void = {\n" + " let v: (i32 | str | bool) = pick();\n" + " match (v) {\n" + " case let x: i32 => { };\n" + " case let x: str => { };\n" + " };\n" + "};\n", + "match: variant not handled" }, + /* ?: enclosing not tagged */ + { "fn inner() (i32 | str) = { return 1; };\n" + "fn caller() i32 = {\n" + " let v: i32 = inner()?;\n" + " return v;\n" + "};\n", + "?: enclosing fn return is not tagged" }, + /* ?: error variant not in enclosing return */ + { "fn inner() (i32 | str | bool) = { return 1; };\n" + "fn caller() (i32 | str) = {\n" + " let v: i32 = inner()?;\n" + " return v;\n" + "};\n", + "?: error variant not in enclosing return" }, + /* !-flag aware: legal case (one success, two errors propagated) */ + { "type invalid = !i32;\n" + "type overflow = !void;\n" + "fn parse() (i64 | invalid | overflow) = { return 0i64; };\n" + "fn caller() (i64 | invalid | overflow) = {\n" + " let v: i64 = parse()?;\n" + " return v + 1;\n" + "};\n", + NULL }, /* expect no error */ + /* !-flag aware: missing one error variant in enclosing return */ + { "type invalid = !i32;\n" + "type overflow = !void;\n" + "fn parse() (i64 | invalid | overflow) = { return 0i64; };\n" + "fn caller() (i64 | invalid) = {\n" + " let v: i64 = parse()?;\n" + " return v + 1;\n" + "};\n", + "?: error variant not in enclosing return" }, +}; + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char wwdump[2048]; + snprintf(wwdump, sizeof wwdump, "%s/wwdump_ww", bin); + + int n = sizeof rows / sizeof rows[0]; + int fail = 0; + for (int i = 0; i < n; i++) { + char path[64]; + snprintf(path, sizeof path, "/tmp/wwd_sc_%d_%d.ww", getpid(), i); + if (writefile(path, rows[i].src) != 0) { fail++; continue; } + char cmd[2048]; + snprintf(cmd, sizeof cmd, "%s -r %s", wwdump, path); + char *err = NULL; size_t elen = 0; + runcap(cmd, &err, &elen); + const char *want = rows[i].expect; + int got_match = (err && want && strstr(err, want) != NULL); + int expected_no_err = (want == NULL); + int err_present = err && strstr(err, + "match: variant not handled") != NULL; + err_present = err_present || (err && strstr(err, + "?: error variant not in enclosing return") != NULL); + err_present = err_present || (err && strstr(err, + "?: enclosing fn return is not tagged") != NULL); + err_present = err_present || (err && strstr(err, + "?: enclosing fn has no tagged-union return") != NULL); + int ok; + if (expected_no_err) ok = !err_present; + else ok = got_match; + if (!ok) { + fprintf(stderr, + "row %d failed: expected %s, got stderr:\n%s\n", + i, want ? want : "(no error)", err ? err : "(empty)"); + fail++; + } + free(err); + unlink(path); + } + if (fail) { + fprintf(stderr, "%d/%d selfcheck rows failed\n", fail, n); + return 1; + } + printf("selfcheck: %d/%d ok\n", n, n); + return 0; +}