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

@@ -5634,6 +5634,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) {