diff --git a/Makefile b/Makefile index 4bcd0779..5e42629f 100644 --- a/Makefile +++ b/Makefile @@ -230,7 +230,8 @@ $(BIN) $(LIB) $(OBJ)/wcc $(OBJ)/ww $(OBJ)/wwdump $(OBJ)/w6c $(OBJ)/w6a $(OBJ)/w6 # ---- tests ------------------------------------------------------------- # Each phase adds a $(BIN)/test_ target; the runner walks them. TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ - $(BIN)/test_w6c $(BIN)/test_w6a $(BIN)/test_dataw $(BIN)/test_datar \ + $(BIN)/test_w6c $(BIN)/test_w6a $(BIN)/test_w6a_parsenum \ + $(BIN)/test_dataw $(BIN)/test_datar \ $(BIN)/test_w6l $(BIN)/test_data_link \ $(BIN)/test_arch \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ @@ -614,6 +615,13 @@ $(BIN)/test_w6a: test/wcc/500_w6a.c $(BIN)/w6c $(BIN)/w6a | $(BIN) $(BIN)/test_dataw: test/wcc/510_dataw.c $(BIN)/w6a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 530_w6a_parsenum (F14 #62): w6a vs w6a_ww must agree on strtoll-edge +# immediate shapes ($ 5 / $08) and both reject a bare -(BP). Needs both +# assembler twins. +$(BIN)/test_w6a_parsenum: test/wcc/530_w6a_parsenum.c $(BIN)/w6a $(BIN)/w6a_ww \ + | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_datar: test/wcc/520_datar.c $(BIN)/w6a $(BIN)/w6l | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/selfhost/cmd/w6a/lex.ww b/selfhost/cmd/w6a/lex.ww index 462e3352..c10190d8 100644 --- a/selfhost/cmd/w6a/lex.ww +++ b/selfhost/cmd/w6a/lex.ww @@ -25,20 +25,29 @@ export fn isidcont(c: i32) bool = { // Plain Plan 9-style: $123 / $0x1f / $-7. Decimal default; 0x prefix // for hex; 0 prefix for octal when followed by a digit (else just 0). export fn parsenum(p: *u8, n: u64) (i64, u64) = { + // strtoll(s, end, 0) semantics, matching the C twin cmd/w6a/lex.c:30: + // skip leading whitespace, optional sign, base-0 prefix detection + // (0x -> hex, leading 0 -> octal, else decimal). w6c never emits the + // `$ 5` / `$08` edge shapes; this aligns the hand-written-asm path + // with cstage so the two assemblers agree byte-for-byte (#62). let i: u64 = 0u64; + for (i < n) { + if (p[i] != 32u8) { if (p[i] != 9u8) { break; }; }; + i += 1u64; + }; let neg: bool = false; if (i < n) { if (p[i] == 45u8) { neg = true; i += 1u64; } else { if (p[i] == 43u8) { i += 1u64; }; }; }; let base: i64 = 10i64; - if (i + 1u64 < n) { - if (p[i] == 48u8) { - if (p[i + 1u64] == 120u8) { base = 16i64; i += 2u64; } - else { if (p[i + 1u64] == 88u8) { base = 16i64; i += 2u64; } - else { if (p[i + 1u64] >= 48u8) { if (p[i + 1u64] <= 55u8) { - base = 8i64; i += 1u64; - };};};}; + if (i < n) { + if (p[i] == 48u8) { // leading '0' -> octal, unless '0x'/'0X' + if (i + 1u64 < n) { + if (p[i + 1u64] == 120u8) { base = 16i64; i += 2u64; } + else { if (p[i + 1u64] == 88u8) { base = 16i64; i += 2u64; } + else { base = 8i64; i += 1u64; }; }; + } else { base = 8i64; i += 1u64; }; }; }; let v: i64 = 0i64; diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww index 15b88f80..70782b03 100644 --- a/selfhost/cmd/w6a/main.combined.ww +++ b/selfhost/cmd/w6a/main.combined.ww @@ -3113,20 +3113,29 @@ export fn isidcont(c: i32) bool = { // Plain Plan 9-style: $123 / $0x1f / $-7. Decimal default; 0x prefix // for hex; 0 prefix for octal when followed by a digit (else just 0). export fn parsenum(p: *u8, n: u64) (i64, u64) = { + // strtoll(s, end, 0) semantics, matching the C twin cmd/w6a/lex.c:30: + // skip leading whitespace, optional sign, base-0 prefix detection + // (0x -> hex, leading 0 -> octal, else decimal). w6c never emits the + // `$ 5` / `$08` edge shapes; this aligns the hand-written-asm path + // with cstage so the two assemblers agree byte-for-byte (#62). let i: u64 = 0u64; + for (i < n) { + if (p[i] != 32u8) { if (p[i] != 9u8) { break; }; }; + i += 1u64; + }; let neg: bool = false; if (i < n) { if (p[i] == 45u8) { neg = true; i += 1u64; } else { if (p[i] == 43u8) { i += 1u64; }; }; }; let base: i64 = 10i64; - if (i + 1u64 < n) { - if (p[i] == 48u8) { - if (p[i + 1u64] == 120u8) { base = 16i64; i += 2u64; } - else { if (p[i + 1u64] == 88u8) { base = 16i64; i += 2u64; } - else { if (p[i + 1u64] >= 48u8) { if (p[i + 1u64] <= 55u8) { - base = 8i64; i += 1u64; - };};};}; + if (i < n) { + if (p[i] == 48u8) { // leading '0' -> octal, unless '0x'/'0X' + if (i + 1u64 < n) { + if (p[i + 1u64] == 120u8) { base = 16i64; i += 2u64; } + else { if (p[i + 1u64] == 88u8) { base = 16i64; i += 2u64; } + else { base = 8i64; i += 1u64; }; }; + } else { base = 8i64; i += 1u64; }; }; }; let v: i64 = 0i64; @@ -3430,7 +3439,17 @@ fn parseoperand(a: *asm_, p: *u8, offin: u64, n: u64, out: *aoperand) u64 = { let cur: u64 = off; let isnum: bool = false; if (cur < n) { - if (p[cur] == '-') { isnum = true; } + // cstage parse.c:189-190 strips '-' then requires isdigit(*p); + // a bare `-(BP)` is NOT a number operand and falls through to a + // loud reject. ww treated '-' alone as a number → silent 0(BP). + // Require a following digit to match (#62). + if (p[cur] == '-') { + if (cur + 1u64 < n) { + if (p[cur + 1u64] >= '0') { if (p[cur + 1u64] <= '9') { + isnum = true; + }; }; + }; + } else { if (p[cur] >= '0') { if (p[cur] <= '9') { isnum = true; }; }; }; }; if (isnum) { diff --git a/selfhost/cmd/w6a/parse.ww b/selfhost/cmd/w6a/parse.ww index a44b0c09..5fab678a 100644 --- a/selfhost/cmd/w6a/parse.ww +++ b/selfhost/cmd/w6a/parse.ww @@ -273,7 +273,17 @@ fn parseoperand(a: *asm_, p: *u8, offin: u64, n: u64, out: *aoperand) u64 = { let cur: u64 = off; let isnum: bool = false; if (cur < n) { - if (p[cur] == '-') { isnum = true; } + // cstage parse.c:189-190 strips '-' then requires isdigit(*p); + // a bare `-(BP)` is NOT a number operand and falls through to a + // loud reject. ww treated '-' alone as a number → silent 0(BP). + // Require a following digit to match (#62). + if (p[cur] == '-') { + if (cur + 1u64 < n) { + if (p[cur + 1u64] >= '0') { if (p[cur + 1u64] <= '9') { + isnum = true; + }; }; + }; + } else { if (p[cur] >= '0') { if (p[cur] <= '9') { isnum = true; }; }; }; }; if (isnum) { diff --git a/test/wcc/530_w6a_parsenum.c b/test/wcc/530_w6a_parsenum.c new file mode 100644 index 00000000..5f402b04 --- /dev/null +++ b/test/wcc/530_w6a_parsenum.c @@ -0,0 +1,159 @@ +/* + * 530_w6a_parsenum — F14 #62: w6a's parsenum diverged from the C twin's + * strtoll(s,end,0) (cmd/w6a/lex.c:30) on three hand-written-asm edge + * shapes, all gate-blind (w6c emits the canonical $5 / $8 / -8(BP), never + * these). The fix aligns wwstage's assembler to strtoll semantics: + * (a) `$ 5` — leading whitespace after $: strtoll skips it (→5); ww + * had no skip and silently encoded imm 0. + * (b) `$08` — strtoll base-0 reads a leading 0 as octal and STOPS at + * '8' (→0); ww parsed it as decimal 8. + * (c) `-(BP)` — strtoll/cstage require a digit after the sign, so a bare + * `-(` is `unrecognised operand` (loud reject); ww silently + * accepted it as 0(BP). + * + * (a)/(b): both assemblers must emit a byte-identical .o. + * (c): both assemblers must FAIL (loud reject). + * Controls: the canonical $5 / $8 / -8(BP) shapes that w6c DOES emit must + * stay byte-identical (no regression). + * + * Drives w6a (cstage) and w6a_ww (wwstage) directly; gated on w6a_ww. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +/* assemble `asm` with `tool` into `obj`; returns the build rc. */ +static int +assemble(const char *tool, const char *asmtext, const char *obj, int i) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/w6apn_%d_%d.s", getpid(), i); + FILE *f = fopen(src, "wb"); + if (!f) return -2; + fputs(asmtext, f); + fclose(f); + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", tool, obj, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +static int +files_equal(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + int eq = (fa && fb); + if (fa && fb) { + for (;;) { + int ca = fgetc(fa), cb = fgetc(fb); + if (ca != cb) { eq = 0; break; } + if (ca == EOF) break; + } + } + if (fa) fclose(fa); + if (fb) fclose(fb); + return eq; +} + +struct idrow { const char *label; const char *asmtext; }; + +/* shapes that must assemble byte-identically across the two assemblers. */ +static const struct idrow idrows[] = { + { "ws_after_dollar", "TEXT main,$16\n\tMOVQ\t$ 5, AX\n\tRET\n" }, + { "leading_zero", "TEXT main,$16\n\tMOVQ\t$08, AX\n\tRET\n" }, + /* controls — the canonical shapes w6c actually emits. */ + { "plain_imm", "TEXT main,$16\n\tMOVQ\t$5, AX\n\tRET\n" }, + { "plain_imm_eight", "TEXT main,$16\n\tMOVQ\t$8, AX\n\tRET\n" }, + { "hex_imm", "TEXT main,$16\n\tMOVQ\t$0x1f, AX\n\tRET\n" }, + { "zero_imm", "TEXT main,$16\n\tMOVQ\t$0, AX\n\tRET\n" }, + { "neg_indir", "TEXT main,$16\n\tMOVQ\t-8(BP), AX\n\tRET\n" }, + { "pos_indir", "TEXT main,$16\n\tMOVQ\t8(BP), AX\n\tRET\n" }, +}; + +/* shapes both assemblers must reject. */ +static const struct idrow rejrows[] = { + { "bare_minus_indir", "TEXT main,$16\n\tMOVQ\t-(BP), AX\n\tRET\n" }, +}; + +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 ctool[1024], wtool[1024]; + snprintf(ctool, sizeof ctool, "%s/w6a", bin); + snprintf(wtool, sizeof wtool, "%s/w6a_ww", bin); + + if (access(wtool, X_OK) != 0) { + fprintf(stderr, "w6a_parsenum: skip (no %s)\n", wtool); + printf("w6a_parsenum: skipped\n"); + return 0; + } + + int nid = (int)(sizeof idrows / sizeof idrows[0]); + int nrej = (int)(sizeof rejrows / sizeof rejrows[0]); + int fail = 0; + + for (int i = 0; i < nid; i++) { + char co[64], wo[64]; + snprintf(co, sizeof co, "/tmp/w6apn_%d_%d_c.o", getpid(), i); + snprintf(wo, sizeof wo, "/tmp/w6apn_%d_%d_w.o", getpid(), i); + int cr = assemble(ctool, idrows[i].asmtext, co, i); + int wr = assemble(wtool, idrows[i].asmtext, wo, 1000 + i); + if (cr != 0 || wr != 0) { + fprintf(stderr, "w6a_parsenum[%s]: assemble failed " + "(c=%d w=%d)\n", idrows[i].label, cr, wr); + fail++; + } else if (!files_equal(co, wo)) { + fprintf(stderr, "w6a_parsenum[%s]: .o differ across " + "assemblers\n", idrows[i].label); + fail++; + } + unlink(co); unlink(wo); + } + + for (int i = 0; i < nrej; i++) { + char co[64], wo[64]; + snprintf(co, sizeof co, "/tmp/w6apn_%d_r%d_c.o", getpid(), i); + snprintf(wo, sizeof wo, "/tmp/w6apn_%d_r%d_w.o", getpid(), i); + int cr = assemble(ctool, rejrows[i].asmtext, co, 2000 + i); + int wr = assemble(wtool, rejrows[i].asmtext, wo, 3000 + i); + if (cr == 0) { + fprintf(stderr, "w6a_parsenum[%s]: cstage accepted, " + "expected reject\n", rejrows[i].label); + fail++; + } + if (wr == 0) { + fprintf(stderr, "w6a_parsenum[%s]: wwstage accepted, " + "expected reject\n", rejrows[i].label); + fail++; + } + unlink(co); unlink(wo); + } + + if (fail) { + fprintf(stderr, "w6a_parsenum: %d fixtures failed\n", fail); + return 1; + } + printf("w6a_parsenum: %d/%d ok\n", nid + nrej, nid + nrej); + return 0; +}