w6a, wcc: add A_SARQ for signed arithmetic right-shift (#136)

Add SAR/SARQ to both assemblers' opcode tables (cstage cmd/w6a +
wwstage selfhost/cmd/w6a) — REX.W + D3 /7, parallel to SHR's D3 /5.
Encoding is the unary-on-CL form (SAR r/m64, CL), the only variant
the cgen emits today. cstage cgen + wwstage cgen sweep all 12 SHRQ
emission sites (6 per stage) so signed RSHIFT and signed RSHIFTEQ
route through SARQ (arithmetic, sign-extends MSB) instead of SHRQ
(logical, zero-fill). Pre-fix `let i: i32 = -200; i >>= 2;`
produced 0x3FFFFFCE (1073741774) instead of -50; cs==ww held because
BOTH stages emitted SHRQ, so the 990-997 byte-id gates were
gate-blind to this silent miscompile.

Sites covered (per stage 6, same shape in both):
  - plain TK_RSHIFT (cgbin / N_BIN ordered binop) — derives unsignd
    from operand types via type_isunsigned / nodeisunsigned, picks
    SHRQ vs SARQ at emit
  - chained-ptr-field compound RSHIFTEQ (cgen.c:3281-3317 area)
  - N_INDEX-lhs compound RSHIFTEQ (#133-expanded N_INDEX site)
  - deref-target compound RSHIFTEQ
  - top-level let compound RSHIFTEQ
  - IDENT-local compound RSHIFTEQ
All sites reuse the in-scope unsignd variable from the surrounding
SLASHEQ block (or derive one locally when not available). LSHIFTEQ
unchanged — SHL == SAL at the encoder, no signedness dispatch needed.

912_sar_shr_run: 5 rows. i32_neg_rshifteq (lead's repro, was wrong
1073741774 → now -50), i64_neg_rshifteq (wider type), i32_pos_
rshifteq (positive control, SARQ ≡ SHRQ on positives, no regression),
u32_rshifteq (unsigned control, still SHRQ), i32_neg_rshift_binop
(plain >> not compound, cgbin TK_RSHIFT site). Exit codes use small
absolute values with u8 wrap (-50 = 206) per Unix 8-bit exit.

Bootstrap-NEUTRAL — `grep -rE '>>=|>>\b'` in lib/+selfhost/ (excl.
combined.ww) returned zero callers of signed RSHIFT today; the only
asm shifts are on previously-broken paths. 990-997 + combined_ww_
fresh stay green. Closes the silent-misbehavior class on signed
right-shift across all 12 cgen emission paths in one fold per
rule-11. Foundation for Eisel-Lemire (strconv fold-4) big-int signed
shifts.
This commit is contained in:
2026-05-27 01:29:24 +09:00
parent 90d31c5b41
commit 13441c5e2e
14 changed files with 471 additions and 81 deletions

221
test/wcc/912_sar_shr_run.c Normal file
View File

@@ -0,0 +1,221 @@
/*
* 912_sar_shr_run — runtime + byte-id net for #136: signed right-shift
* (both plain `>>` and `>>=`) must use SAR (arithmetic, sign-extends
* MSB), not SHR (logical, zero-fill). Pre-fix BOTH stages emitted SHRQ
* for signed RSHIFT because A_SARQ wasn't in the w6a opcode table, so
* `let i: i32 = -200; i >>= 2;` produced i = 0x3FFFFFCE (1073741774)
* instead of -50. cs==ww held → gate-blind. #133-expanded shipped with
* this pre-existing concern documented (parity with cgen.c:4145 deref-
* lvalue compound site); #136 closes it.
*
* Fix: add A_SARQ to w6a + w6c + w6a_ww opcode tables (encoding REX.W
* D3 /7, parallel to SHR's D3 /5), then sweep cgen sites in both
* stages to dispatch SARQ vs SHRQ on the operand signedness. Sites
* covered (per-stage 6 each):
* - plain `>>` binop (cgbin TK_RSHIFT)
* - chained-ptr-field compound RSHIFTEQ (#133-expanded site 1)
* - N_INDEX-lhs compound RSHIFTEQ (#133-expanded site 2)
* - deref-target compound RSHIFTEQ (pre-existing)
* - top-level let compound RSHIFTEQ
* - IDENT-local compound RSHIFTEQ
*
* The bootstrap audit at design time showed zero current signed-RSHIFT
* callers in lib/+selfhost/, so 990-997 byte-id stays GREEN; the only
* shifts are on the previously-broken paths.
*
* Rows cover the operator + signedness × storage matrix:
* - i32_neg_rshifteq: lead's repro pattern, -200 >>= 2 = -50 (was
* 1073741774 pre-fix on SHRQ)
* - i64_neg_rshifteq: same shape, i64
* - i32_pos_rshifteq: control, SARQ and SHRQ produce same result on
* positives; asserts no regression
* - u32_rshifteq: unsigned control, must still use SHRQ (no change)
* - i32_neg_rshift_binop: plain `>>` (not compound) on negative i32,
* covers the cgen 2596 / 3176 TK_RSHIFT N_BIN site
*
* Each row carries (a) cstage `ww build` + run asserting the exit
* code and (b) w6c vs w6c_ww `.s` cmp (rule-10 byte-id). Exit codes
* use small absolute values + 256 to encode signed expectations:
* -50 → 206 = 256-50 (Unix exit is 8-bit unsigned). i64 row clamped
* similarly.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.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;
}
struct row { const char *label; const char *src; int want_exit; };
static const struct row rows[] = {
/* The #136 repro: signed i32 >>=. Pre-fix produced wrong large
* positive; post-fix produces -50. Exit code = -50 as u8 = 206. */
{ "i32_neg_rshifteq",
"package main;\n"
"export fn main() i32 = {\n"
" let i: i32 = -200;\n"
" i >>= 2;\n"
" return i;\n"
"};\n", 206 },
/* signed i64 >>=. Same shape, wider type. Result -50 as exit u8
* = 206. */
{ "i64_neg_rshifteq",
"package main;\n"
"export fn main() i32 = {\n"
" let i: i64 = -200i64;\n"
" i >>= 2i64;\n"
" return i: i32;\n"
"};\n", 206 },
/* Positive control: SARQ and SHRQ agree. 200 >> 2 = 50. */
{ "i32_pos_rshifteq",
"package main;\n"
"export fn main() i32 = {\n"
" let i: i32 = 200;\n"
" i >>= 2;\n"
" return i;\n"
"};\n", 50 },
/* Unsigned control: SHRQ unchanged. 200u32 >> 2 = 50. */
{ "u32_rshifteq",
"package main;\n"
"export fn main() i32 = {\n"
" let i: u32 = 200u32;\n"
" i >>= 2u32;\n"
" return i: i32;\n"
"};\n", 50 },
/* Plain `>>` (not compound) on signed negative: covers cgbin
* TK_RSHIFT site (cgen.c:2596 / cgenexpr.ww:3176). */
{ "i32_neg_rshift_binop",
"package main;\n"
"export fn main() i32 = {\n"
" let i: i32 = -200;\n"
" let r: i32 = i >> 2;\n"
" return r;\n"
"};\n", 206 },
{ NULL, NULL, 0 }
};
static int
slurp_eq(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
FILE *fb = fopen(b, "rb");
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
int rc = 0;
for (;;) {
int ca = fgetc(fa);
int cb = fgetc(fb);
if (ca != cb) { rc = -1; break; }
if (ca == EOF) break;
}
fclose(fa); fclose(fb);
return rc;
}
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 w6c[1100], w6c_ww[1100];
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
if (access(w6c_ww, X_OK) != 0) {
fprintf(stderr, "sarshr: w6c_ww missing — cannot run the "
"cs==ww byte-id gate (the whole point of this test)\n");
return 1;
}
int n = 0, fail = 0;
for (int i = 0; rows[i].src; i++, n++) {
char src[64];
snprintf(src, sizeof src, "/tmp/wwsar_%d_%d.ww", getpid(), i);
FILE *f = fopen(src, "wb");
if (f == NULL) { fail++; continue; }
fputs(rows[i].src, f);
fclose(f);
char tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwsar_%d_d_%d",
getpid(), i);
mkdir(tmpdir, 0755);
char cmd[2048];
snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s",
tmpdir, bin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: cstage build failed\n",
rows[i].label);
fail++;
unlink(src); rmdir(tmpdir);
continue;
}
char outbin[128];
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
if (got != rows[i].want_exit) {
fprintf(stderr, "row[%s]: cstage exit %d, want %d\n",
rows[i].label, got, rows[i].want_exit);
fail++;
}
unlink(outbin); rmdir(tmpdir);
char cs_s[64], ws_s[64];
snprintf(cs_s, sizeof cs_s, "/tmp/wwsar_%d_%d_cs.s",
getpid(), i);
snprintf(ws_s, sizeof ws_s, "/tmp/wwsar_%d_%d_ww.s",
getpid(), i);
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
w6c, cs_s, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
fail++; unlink(src); continue;
}
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
w6c_ww, ws_s, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww failed\n",
rows[i].label);
fail++; unlink(src); unlink(cs_s); continue;
}
if (slurp_eq(cs_s, ws_s) != 0) {
fprintf(stderr,
"row[%s]: cstage/wwstage .s DIFFER (rule-10 "
"byte-id violation)\n", rows[i].label);
fail++;
}
unlink(src); unlink(cs_s); unlink(ws_s);
}
if (fail) {
fprintf(stderr, "%d/%d sar/shr tests failed\n", fail, n);
return 1;
}
printf("sarshr: %d/%d ok (cstage run + cs==ww byte-id)\n",
n, n);
return 0;
}