wcc/ww: cgtryprop/cgtryunw gain the nullable arm

Try-propagation on a nullable value fell to the tagged path and
compared the pointer to a tag — the null check came out inverted.
Mirror ww's own cgtypetest nullable fold (cgenexpr.ww:652) and the
cstage twin (cgen.c:10366/10504). Review item #15.
This commit is contained in:
2026-06-12 19:19:26 +09:00
parent 87367ae332
commit 6f3c896fea
5 changed files with 346 additions and 0 deletions

View File

@@ -23500,6 +23500,22 @@ fn cgtryprop(c: *cgen, n: *node) void = {
// cursor here (was a silent word0 unwrap); call sources keep
// the plain cgexpr emission byte-for-byte.
cgtryunwcursor(c, n, "?");
// Nullable `(*T | void)`: AX IS the pointer, not a tag. Non-null
// = success (any *T variant), null = error → propagate (RET with
// AX=0). The pre-fix path compared AX against successtag and so
// treated null as success (inverted). Mirrors cstage cgen.c
// N_TRYPROP nullable arm; ww's own cgtypetest carries the twin
// (cgenexpr.ww nullable fold). (task #15/F4)
if (isnullabletype(n.lhs)) {
let nl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(nl);
emitline("\n");
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(nl);
return;
};
// AX = tag. If not the success tag, this is an error; pop frame and
// RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an
@@ -23638,6 +23654,21 @@ fn cgtryunw(c: *cgen, n: *node) void = {
};
// Family C (#35/#46): see the cgtryprop twin.
cgtryunwcursor(c, n, "!");
// Nullable `(*T | void)`: AX IS the pointer. Non-null = success
// (leave AX as-is), null = error → abort exit(1). The pre-fix
// path compared AX against successtag, treating null as success
// (inverted). Mirrors cstage cgen.c N_TRYUNW nullable arm; ww's
// own cgtypetest carries the twin. (task #15/F4)
if (isnullabletype(n.lhs)) {
let nl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(nl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(nl);
return;
};
let cl: str = mklabel(c, "tryunw_ok");
// Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an

View File

@@ -349,6 +349,22 @@ fn cgtryprop(c: *cgen, n: *node) void = {
// cursor here (was a silent word0 unwrap); call sources keep
// the plain cgexpr emission byte-for-byte.
cgtryunwcursor(c, n, "?");
// Nullable `(*T | void)`: AX IS the pointer, not a tag. Non-null
// = success (any *T variant), null = error → propagate (RET with
// AX=0). The pre-fix path compared AX against successtag and so
// treated null as success (inverted). Mirrors cstage cgen.c
// N_TRYPROP nullable arm; ww's own cgtypetest carries the twin
// (cgenexpr.ww nullable fold). (task #15/F4)
if (isnullabletype(n.lhs)) {
let nl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(nl);
emitline("\n");
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(nl);
return;
};
// AX = tag. If not the success tag, this is an error; pop frame and
// RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an
@@ -487,6 +503,21 @@ fn cgtryunw(c: *cgen, n: *node) void = {
};
// Family C (#35/#46): see the cgtryprop twin.
cgtryunwcursor(c, n, "!");
// Nullable `(*T | void)`: AX IS the pointer. Non-null = success
// (leave AX as-is), null = error → abort exit(1). The pre-fix
// path compared AX against successtag, treating null as success
// (inverted). Mirrors cstage cgen.c N_TRYUNW nullable arm; ww's
// own cgtypetest carries the twin. (task #15/F4)
if (isnullabletype(n.lhs)) {
let nl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(nl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(nl);
return;
};
let cl: str = mklabel(c, "tryunw_ok");
// Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an

View File

@@ -23500,6 +23500,22 @@ fn cgtryprop(c: *cgen, n: *node) void = {
// cursor here (was a silent word0 unwrap); call sources keep
// the plain cgexpr emission byte-for-byte.
cgtryunwcursor(c, n, "?");
// Nullable `(*T | void)`: AX IS the pointer, not a tag. Non-null
// = success (any *T variant), null = error → propagate (RET with
// AX=0). The pre-fix path compared AX against successtag and so
// treated null as success (inverted). Mirrors cstage cgen.c
// N_TRYPROP nullable arm; ww's own cgtypetest carries the twin
// (cgenexpr.ww nullable fold). (task #15/F4)
if (isnullabletype(n.lhs)) {
let nl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(nl);
emitline("\n");
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(nl);
return;
};
// AX = tag. If not the success tag, this is an error; pop frame and
// RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an
@@ -23638,6 +23654,21 @@ fn cgtryunw(c: *cgen, n: *node) void = {
};
// Family C (#35/#46): see the cgtryprop twin.
cgtryunwcursor(c, n, "!");
// Nullable `(*T | void)`: AX IS the pointer. Non-null = success
// (leave AX as-is), null = error → abort exit(1). The pre-fix
// path compared AX against successtag, treating null as success
// (inverted). Mirrors cstage cgen.c N_TRYUNW nullable arm; ww's
// own cgtypetest carries the twin. (task #15/F4)
if (isnullabletype(n.lhs)) {
let nl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(nl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(nl);
return;
};
let cl: str = mklabel(c, "tryunw_ok");
// Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an