wcc-ww: assert/abort builtins — checker tag + cgen rt_abort lowering (#58)

wwstage had no EXPR_ASSERT-family intercept: the checker left bare
assert/abort calls untyped (asserttyped gate 4 skipped them by design)
and cgcall fell through to the regular call path, emitting
CALL assert(SB) for a symbol that exists nowhere — link-fail. cstage
was already correct (tag ty_err at check.c:1536-1572, lower inline via
rt_abort at cgen.c:6618-6663).

Mirror the same tag-then-lower pair: exprtype N_CALL stamps the call
void and the callee TY_ERR behind the scopelookupprefer no-shadow gate
(the isassertfam predicate), with the cstage arg diagnostics (cond must
be bool, msg must be str, arity caps); cgcall keys on the TY_ERR tag
and emits the identical CMPQ/JNE/rt_abort sequence. A user-shadowed
assert/abort (same-module or cross-module, the #45 shape, task #14)
stays untagged on the regular call path — byte-id for all existing lib
code preserved.

The cond check does NOT alias-peel: cstage compares ty_bool by
identity (check.c:1560), so `type myb = bool` is rejected there;
wwstage aligns down per rule 10 (a resolvealias here was accepting it
— cs/ww accept-reject divergence). Widening both stages together
belongs to the alias-peel choke-point arc (task #5, #47/#68).

The resolvewalk N_IDENT resolution counter learns the builtin shape:
an unshadowed abort/assert ident binds no sym BY DESIGN, so wwdump
-r's zero-unresolved gate (990 probe 4) counts it resolved instead of
failing builtin-using units.

test 957: 13 rows — pass/fail/msg/bare abort (run exit + rt_abort
stderr content; no-msg rows pin EMPTY stderr = the (NULL,0) shape),
assert in an imported module, same-module + cross-module shadow
controls, 5 checker rejects pinned on diagnostic CONTENT (shared
substring; cstage prefixes pos, wwstage cerr is bare) incl. the
alias-of-bool cond row pinning the rule-10 down-alignment; each
positive row pins cstage run exit + cs==ww byte-id. On pre-fix master
11/13 rows trip (survivors = the two shadow controls).

Residual (separate root, deferred diagnostic class): zero-arg assert()
is not intercepted by either stage; cstage rejects via the generic
undefined-ident path, wwstage's undefined-callee diagnostic is the
class deferred behind wiring checkfile into w6c_ww.
This commit is contained in:
2026-06-04 22:07:13 +09:00
parent 33ec0fb1ac
commit e091dfbdbe
6 changed files with 720 additions and 15 deletions

View File

@@ -10657,11 +10657,20 @@ fn resolvewalk(c: *checker, n: *node) void = {
if (nm.len > 0) {
let s: *sym = scopelookupprefer(c.cur, c.curmod, nm);
if (s == nil) {
c.nunresolved += 1;
if (c.verbose != 0) {
cerr(" unresolved id: ");
cerr(nm);
cerr("\n");
// Unshadowed abort/assert binds no sym BY
// DESIGN (the EXPR_ASSERT family has no callee
// object — see isassertfam); count it resolved
// so wwdump -r's zero-unresolved gate holds
// over builtin-using lib code (#58 respell).
if (isassertfam(c, n)) {
c.nresolved += 1;
} else {
c.nunresolved += 1;
if (c.verbose != 0) {
cerr(" unresolved id: ");
cerr(nm);
cerr("\n");
};
};
} else { c.nresolved += 1; };
};
@@ -12915,6 +12924,74 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// Hare-faithful path. This is the intercept-shim minimum to
// unblock #15 (A.6.2.1e assertion enable).
if (callee.kind == nkind.N_IDENT) {
// abort([msg]) / assert(cond[, msg]) — the EXPR_ASSERT
// family (harec ref/harec/src/check.c:877,893), lowered
// to rt_abort. Builtin only when no user symbol shadows
// the name — the same scopelookupprefer gate as cstage
// cmd/wcc/check.c:1536-1572 and isassertfam; a shadowed
// call stays on the regular path (#58; the #45
// flat-scope root is filed as task #14).
// The TY_ERR stamp on the callee is cgen's routing key
// (cstage spells it `n->lhs->type = ty_err`).
if (streq(callee.str, "abort")
&& scopelookupprefer(c.cur, c.curmod, "abort") == nil) {
if (e.list != nil) {
let mt: *node = exprtype(c, e.list, nil);
let conf: bool = false;
if (!isassignable(c, mktname(c, "str"), mt, &conf)) {
cerr("abort: message must be str\n");
c.errs += 1;
};
if (e.list.next != nil) {
cerr("abort: at most one arg\n");
c.errs += 1;
};
};
let tn: *node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
callee.type_ = c.tc.tyerr: *void;
return tn;
};
if (streq(callee.str, "assert") && e.list != nil
&& scopelookupprefer(c.cur, c.curmod, "assert") == nil) {
let ct: *node = exprtype(c, e.list, nil);
if (ct != nil) {
// No alias peel: cstage compares ty_bool by
// IDENTITY (cmd/wcc/check.c:1560), so a
// `type myb = bool` cond is rejected there —
// align down (rule 10). Widening belongs to
// the alias-peel choke-point arc (task #5,
// #47/#68), both stages together.
let cu: *node = unwrapbang(ct);
let condok: bool = false;
if (cu.kind == nkind.N_TNAME) {
if (streq(cu.str, "bool")
|| streq(cu.str, "untyped_bool")) {
condok = true;
};
};
if (!condok) {
cerr("assert: cond must be bool\n");
c.errs += 1;
};
};
if (e.list.next != nil) {
let mt: *node = exprtype(c, e.list.next, nil);
let conf: bool = false;
if (!isassignable(c, mktname(c, "str"), mt, &conf)) {
cerr("assert: message must be str\n");
c.errs += 1;
};
if (e.list.next.next != nil) {
cerr("assert: at most two args\n");
c.errs += 1;
};
};
let tn: *node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
callee.type_ = c.tc.tyerr: *void;
return tn;
};
if (streq(callee.str, "len")) {
let tn: *node = mktname(c, "i32");
e.type_ = tinfofornode(c, tn): *void;
@@ -26006,6 +26083,46 @@ fn cgcall(c: *cgen, n: *node) void = {
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
// abort([msg]) / assert(cond[, msg]) — checker-tagged
// builtins (callee TY_ERR is the routing key, cstage's
// `lhs->type == ty_err` at cmd/w6c/cgen.c:6618,6645);
// lower to rt_abort. A user-shadowed abort/assert is
// untagged and stays on the regular call path (#58).
let bti: *tinfo = callee.type_: *tinfo;
if (bti != nil && bti.kind == tykind.TY_ERR) {
if (streq(callee.str, "abort")) {
if (n.list != nil) {
cgexpr(c, n.list);
emitline("\tMOVQ\tAX, DI\n");
emitline("\tMOVQ\tBX, SI\n");
} else {
emitline("\tMOVQ\t$0, DI\n");
emitline("\tMOVQ\t$0, SI\n");
};
emitline("\tCALL\trt_abort(SB)\n");
return;
};
if (streq(callee.str, "assert") && n.list != nil) {
cgexpr(c, n.list);
let skip: str = mklabel(c, "as");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(skip);
emitline("\n");
let msg: *node = n.list.next;
if (msg != nil) {
cgexpr(c, msg);
emitline("\tMOVQ\tAX, DI\n");
emitline("\tMOVQ\tBX, SI\n");
} else {
emitline("\tMOVQ\t$0, DI\n");
emitline("\tMOVQ\t$0, SI\n");
};
emitline("\tCALL\trt_abort(SB)\n");
emitlabel(skip);
return;
};
};
if (streq(callee.str, "append")) {
if (n.list != nil) {
if (n.list.next != nil) {