wcc/cgen: #154 str==-global ident operand — name(SB) base in cbinop, not (BP) garbage (cstage)

The str==/!= arm of cbinop had an N_IDENT fast-path that assumed the operand
was a local: localfind returns 0 for a module-global str, so it loaded
(BP)/8(BP) — saved-BP/retaddr garbage — into rt_streq. `p == sepstr` silently
compared garbage (returned wrong). Mirror #148's global branch at both sub-sites
(rhs/lhs): off==0 && let_islet -> LEAQ name(SB) base, load ptr/len. Distinct
per-site fast-path, not a shared choke (the by-value-global-arg family
#148/#150/#151 closes separately). cstage-only; the wwstage str== twin is #146
(-> #125 batch).

Pin test/wcc/989_strglobeq (table-driven: const+let globals, rhs+lhs ident,
==/!=, unequal + len>1 rows; teeth-proven). Surfaced by the lib/path c3
buffer-ops gate-1 oracle.
This commit is contained in:
2026-06-08 13:18:59 +09:00
parent feae910a9b
commit 3f6b68cbf2
4 changed files with 155 additions and 8 deletions

View File

@@ -449,6 +449,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_hex_run $(BIN)/test_utf8_run $(BIN)/test_bytes_run \
$(BIN)/test_path_run \
$(BIN)/test_letshadow_run \
$(BIN)/test_strglobeq_run \
$(BIN)/test_decimal_run $(BIN)/test_strconv_int_run \
$(BIN)/test_stof_run $(BIN)/test_ftos_run \
$(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
@@ -2002,6 +2003,10 @@ $(BIN)/test_letshadow_run: test/wcc/989_letshadow_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_strglobeq_run: test/wcc/989_strglobeq_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_ascii_run: test/wcc/904_ascii_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<

View File

@@ -4568,10 +4568,23 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* Push rhs (len, then ptr top) */
if (n->rhs->kind == N_IDENT) {
int off = localfind(locals, n->rhs->str);
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
/* #154: localfind→0 for a module global, but the
* str header lives at name(SB), not BP+0. Mirror
* #148's slice global branch (cgen.c:9082): LEAQ
* the symbol into a base reg, push len then ptr. */
if (off == 0 && let_islet(n->rhs->str)) {
ins2(c, A_LEAQ, masym(c, n->rhs->str),
areg(D_BX));
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
} else {
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
} else {
cgexpr(c, n->rhs, locals); /* AX=ptr, BX=len */
ins1(c, A_PUSHQ, areg(D_BX));
@@ -4580,10 +4593,21 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* Push lhs */
if (n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
/* #154: see the rhs branch above — a module-global
* str ident lives at name(SB), not BP+0. */
if (off == 0 && let_islet(n->lhs->str)) {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_BX));
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
} else {
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
} else {
cgexpr(c, n->lhs, locals);
ins1(c, A_PUSHQ, areg(D_BX));

67
test/wcc/989_strglobeq.ww Normal file
View File

@@ -0,0 +1,67 @@
// strglobeq — #154 regression pin. `str == str` / `str != str` delegate to
// rt_streq, and cgen's str-compare arm had an N_IDENT fast-path that ALWAYS
// read the header off BP+localfind(name). For a module-GLOBAL str ident
// localfind→0, so it loaded saved-BP/retaddr garbage instead of name(SB) —
// a silent cstage miscompile (the header lives at name(SB), not the frame).
// Fix mirrors #148's slice global branch: LEAQ name(SB), load ptr/len off it.
//
// Table-driven: each row is {input, expected}; the loop feeds every input
// through the five compile-time-distinct comparison shapes (the bug is per
// SHAPE in cgen, so the shapes are separate fns the rows drive). Shapes cover
// const-global AND let-global operands, ident on RHS AND on LHS, == AND !=,
// and a length>1 global ("/usr") so the LEN word — not just the ptr — is read
// off name(SB) on both sub-sites. signalled = row*10+shape pinpoints failures.
//
// Run with `out/bin/ww run test/wcc/989_strglobeq.ww`; exit 0 = all pass.
package main;
import os;
const csep: str = "/";
let lsep: str = "/";
const longsep: str = "/usr";
// rhs-ident global (p == g): const, let, len>1, and !=.
fn eqr_const(p: str) bool = { return p == csep; };
fn eqr_let(p: str) bool = { return p == lsep; };
fn eqr_long(p: str) bool = { return p == longsep; };
fn ner_const(p: str) bool = { return p != csep; };
// lhs-ident global (g == p): const and len>1.
fn eql_const(p: str) bool = { return csep == p; };
fn eql_long(p: str) bool = { return longsep == p; };
type row = struct {
in: str,
eqsep: bool, // in == "/"
eqlong: bool, // in == "/usr"
};
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
export fn main() i32 = {
let rows: [_]row = [
row { in = "/", eqsep = true, eqlong = false },
row { in = "foo", eqsep = false, eqlong = false },
row { in = "/usr", eqsep = false, eqlong = true },
];
// len() stamps i32 on cstage (#26); the index must match for the bound.
for (let i: i32 = 0; i < len(rows); i += 1) {
let r = rows[i];
signalled = i * 10 + 1;
if (eqr_const(r.in) != r.eqsep) { fail(); };
signalled = i * 10 + 2;
if (eqr_let(r.in) != r.eqsep) { fail(); };
signalled = i * 10 + 3;
if (eql_const(r.in) != r.eqsep) { fail(); };
signalled = i * 10 + 4;
if (ner_const(r.in) != !r.eqsep) { fail(); };
signalled = i * 10 + 5;
if (eqr_long(r.in) != r.eqlong) { fail(); };
signalled = i * 10 + 6;
if (eql_long(r.in) != r.eqlong) { fail(); };
};
return 0;
};

View File

@@ -0,0 +1,51 @@
/*
* 989_strglobeq_run — #154 regression pin. Compile + run the strglobeq
* fixture under the C-side `ww run` driver (cstage w6c) and assert exit 0.
*
* The bug (str== fast-path read the global str header off BP+0 instead of
* name(SB)) is a CSTAGE silent miscompile; byte-id 990-997 can't see a
* runtime-value miscompile, so only this value check catches it. Same
* thin-wrapper shape as 989_letshadow_run.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 1;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
const char *src = "test/wcc/989_strglobeq.ww";
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "strglobeq_run FAIL: %s exited %d "
"(row %d miscompiled — #154)\n", src, rc, rc - 10);
return 1;
}
printf("strglobeq_run: %s ok\n", src);
return 0;
}