wcc: nullable-global storage is a loud reject pending #15, both stages

A module-level nullable `(*T | void)` GLOBAL has no storage path in
either stage: let_emit_size / letemitsize returned 0 for the nullable
TY_TAGGED, so let_collect skipped registration and emit_lets skipped
DATA. The three READ paths then miscompiled SILENTLY and identically-
wrong (a #263-class both-wrong gap, not a wwstage align-up): match read
0(BP) = saved BP via the let_islet-gated #87 arm falling to localfind;
`g is *T` / `g as *T` emitted MOVQ name(SB) for a symbol with no DATA →
w6l undefined-reference. cstage's #87 match arm was itself `!is_nullable`-
gated, so both stages were wrong.

This is the silent→loud bridge: die loud at the size/storage layer the
instant a nullable global is declared, so all three read paths hit one
diagnostic instead of a silent miscompile. A silent gap here is exactly
what "stable before CSP" forbids — CSP's process/handle/chan singletons
(`let c: *Chan | void`) are THE canonical nullable-global consumer. The
full storage + read-class arc (real DATA, nil/void/address-of init, let-
registration, the three SB-resolution read arms) is deferred to task #15
(CSP-prereq); the `&`-init sub-problem additionally couples to the #48
static address-of relocation gap (which already bites a plain `*T` global
init the same way).

Diagnostic core text is identical both stages ("nullable-global storage
unimplemented (task #15)"); cstage's fatal() adds the harness-wide "ww: "
err.c prefix err.ww does not, the same per-stage asymmetry every existing
both-stage reject carries. Byte-id-neutral: the corpus declares zero
nullable globals (grep-verified), so the loud path is unreached in self-
compile and the emitted asm is zero-move; the embedded w6c/wwdump
combined.ww amalgamations are regenerated for the cgen.ww source change.

New 989_nullableglobal_reject: 6 reject rows (match/is/as on a &gv init,
plus nil-init and void-init match, plus an inline non-aliased nullable
form) prove rc!=0 + the shared diagnostic on both stages, init- and
form-invariant; 2 controls (non-nullable tagged global, plain nil-init
*T global) prove the reject is keyed on the nullable TY_TAGGED and the
#87 storage path is untouched.
This commit is contained in:
2026-06-13 18:27:53 +09:00
parent dd24de1134
commit 728d86518e
6 changed files with 349 additions and 16 deletions

View File

@@ -41821,11 +41821,22 @@ fn letemitsize(c: *cgen, d: *node) i32 = {
return tsum;
};
// #87: non-nullable tagged-union global — box size (tag word +
// max payload, mirror of the runtime local). Nullable stays 0 so
// the (*T|void) one-word fold keeps the 8B scalar arm in
// emitletdataw. Mirrors cstage let_emit_size TY_TAGGED.
// max payload, mirror of the runtime local). Mirrors cstage
// let_emit_size TY_TAGGED.
if (t.kind == nkind.N_TTAGGED) {
if (isnullabletype(t)) { return 0; };
// #45 (silent→loud bridge, task #15): a nullable (*T|void)
// GLOBAL has no storage path. Returning 0 here made
// letcollect + emitletdataw silently skip the decl (no DATA,
// no let-registration), so a later match/is/as resolved
// 0(BP) or an undefined symbol — a silent miscompile in the
// CSP handle-singleton substrate. Die loud at the size/
// storage layer so all three read paths hit one diagnostic;
// the full storage + read-class arc is task #15 (CSP-prereq).
if (isnullabletype(t)) {
let mng: str = "nullable-global storage unimplemented (task #15)\n";
os.write(2, mng.ptr, mng.len: u64);
os.exit(1);
};
return slotsize(c, t);
};
if (t.kind != nkind.N_TNAME) { return 0; };

View File

@@ -1047,11 +1047,22 @@ fn letemitsize(c: *cgen, d: *node) i32 = {
return tsum;
};
// #87: non-nullable tagged-union global — box size (tag word +
// max payload, mirror of the runtime local). Nullable stays 0 so
// the (*T|void) one-word fold keeps the 8B scalar arm in
// emitletdataw. Mirrors cstage let_emit_size TY_TAGGED.
// max payload, mirror of the runtime local). Mirrors cstage
// let_emit_size TY_TAGGED.
if (t.kind == nkind.N_TTAGGED) {
if (isnullabletype(t)) { return 0; };
// #45 (silent→loud bridge, task #15): a nullable (*T|void)
// GLOBAL has no storage path. Returning 0 here made
// letcollect + emitletdataw silently skip the decl (no DATA,
// no let-registration), so a later match/is/as resolved
// 0(BP) or an undefined symbol — a silent miscompile in the
// CSP handle-singleton substrate. Die loud at the size/
// storage layer so all three read paths hit one diagnostic;
// the full storage + read-class arc is task #15 (CSP-prereq).
if (isnullabletype(t)) {
let mng: str = "nullable-global storage unimplemented (task #15)\n";
os.write(2, mng.ptr, mng.len: u64);
os.exit(1);
};
return slotsize(c, t);
};
if (t.kind != nkind.N_TNAME) { return 0; };

View File

@@ -41821,11 +41821,22 @@ fn letemitsize(c: *cgen, d: *node) i32 = {
return tsum;
};
// #87: non-nullable tagged-union global — box size (tag word +
// max payload, mirror of the runtime local). Nullable stays 0 so
// the (*T|void) one-word fold keeps the 8B scalar arm in
// emitletdataw. Mirrors cstage let_emit_size TY_TAGGED.
// max payload, mirror of the runtime local). Mirrors cstage
// let_emit_size TY_TAGGED.
if (t.kind == nkind.N_TTAGGED) {
if (isnullabletype(t)) { return 0; };
// #45 (silent→loud bridge, task #15): a nullable (*T|void)
// GLOBAL has no storage path. Returning 0 here made
// letcollect + emitletdataw silently skip the decl (no DATA,
// no let-registration), so a later match/is/as resolved
// 0(BP) or an undefined symbol — a silent miscompile in the
// CSP handle-singleton substrate. Die loud at the size/
// storage layer so all three read paths hit one diagnostic;
// the full storage + read-class arc is task #15 (CSP-prereq).
if (isnullabletype(t)) {
let mng: str = "nullable-global storage unimplemented (task #15)\n";
os.write(2, mng.ptr, mng.len: u64);
os.exit(1);
};
return slotsize(c, t);
};
if (t.kind != nkind.N_TNAME) { return 0; };