Files
ww/cmd/w6a/parse.c
Hojun-Cho cbcc0167ae w6c+w6a+selfhost+lib: cgen+asm bugs surfaced by hash modules
Seven fixes across the toolchain, plus three new lib/hash modules
(adler32, crc16, crc32) that surfaced them.

  1. `~x` on u8/u16/u32 left the upper bits set: NOTQ inverts the
     whole 64-bit register and nothing trimmed it back to type
     width, so a returned `u16` would compare 64-bit against a
     typed literal and disagree. Both stages now mask after NOTQ
     for narrow unsigned: AND $0xFF/0xFFFF for u8/u16, MOVL r,r for
     u32 (ANDQ $0xFFFFFFFF sign-extends imm32 and is a no-op).
     Signed narrows stay sign-extended and need no fix-up. See
     cmd/w6c/cgen.c N_UN TK_TILDE and selfhost cgenexpr.ww cgun
     TK_TILDE with new nodeprimwidth helper.

  2. w6a had no D_CONST immediate path for ANDQ / ORQ. cgen would
     emit `ANDQ $65535, AX` and the rr encoder silently wrote
     `21 /r` with garbage reg fields — the mask never happened.
     Added `81 /4` (AND) and `81 /1` (OR) imm32 paths in both
     cstage and selfhost w6a. The ~width fix above depends on this.

  3. `s: []u8` cast as a direct fn argument produced a 0-length
     slice. cgexpr for N_CAST left (AX=ptr, BX=len) from the str
     source but never set CX (cap), and the arg-push fallback only
     pushed AX. cgcast now synthesises CX=BX when target is slice
     and source is str; node_isslice / arg-push recognise
     cast-to-slice and emit the full (cap, len, ptr) triple. Both
     stages.

  4. `*[N]T` element-store used 8-byte stride + MOVQ regardless of
     T's width. Indexing `buf: *[4]u16` would step 8 bytes and
     write 8 bytes per element. Added idx_eff (drills *[N]T → T)
     in cstage and the matching pointer-array drill in selfhost
     elemsizeof. Also added MOVW / MOVZWQ / MOVSWQ to w6c, w6a,
     and selfhost mirrors so 2-byte element stores/loads use the
     right opcode (was falling through to MOVQ and trailing 6 bytes
     into the next slot).

  5. Slicing a top-level fixed array (`g[0:n]` where `g: [N]T` is
     a global) computed the base from BP instead of the symbol —
     localfind returned 0 and the cgen treated it as a local at
     offset 0. Both N_SLICE-as-expression (cgslice) and N_SLICE-
     as-call-arg paths now check let_islet / letvartnode and emit
     LEAQ name(SB) when the base is a global array (or MOVQ
     name(SB) for a global slice/pointer base). Both stages.

  6. Top-level `let arr: [N]T = [v0, v1, ...]` link-failed on
     cstage — emit_lets bailed when it saw N_ARRLIT init on an
     array type, and the sz==8 scalar path then misemitted any
     8-byte-sized array (e.g. [4]u16, [8]u8) as a single quad.
     emit_lets now walks N_ARRLIT, evaluates each element as an
     int/rune/bool/nil literal, packs per-element bytes
     little-endian, and honours the trailing `...` repeat marker.
     Selfhost already handled the literal-init path; fixed the
     parallel sz==8 duplicate-DATAW emit on its side (the array
     and the scalar paths both fired, last write winning at link
     but the duplicate broke cross-stage byte-identicality on user
     code with this shape).

  7. w6a's per-line input buffer was a 1KB stack `char buf[1024]`.
     A `DATAW` for a [256]u16 emits ~2080 bytes on one line, which
     truncated mid-escape; the assembler then re-parsed the
     remaining tail as garbage opcodes ("unknown opcode"). Bumped
     cstage w6a to a 32K static buffer (selfhost w6a already
     allocated per-line via amalloc).

  lib: lib/hash/adler32, lib/hash/crc16, lib/hash/crc32 — pure
  buffer-subset shape (matching lib/hash/fnv), with per-module
  *_test.ww runnable via `ww test lib/hash/<name>`. Adler-32 plus
  CRC-16 (CCITT/CMDA2000/DECT/ANSI) and CRC-32 (IEEE/Castagnoli/
  Koopman) cover Hare's reference vectors bit-for-bit. Wired into
  test/wcc/900_stdlib.c. .gitignore: lib/**/*.s,*.o so `ww test`
  droppings stay untracked.

`make test` (26/26), `make bootstrap` (ww2≡ww3≡ww4), and per-module
`ww test` all pass. cgen output is byte-identical across cstage and
selfhost for every repro that previously diverged.
2026-05-13 14:26:18 +09:00

425 lines
11 KiB
C

/*
* parse.c — line-oriented parser for the asm subset emitted by w6c.
*
* Grammar:
* line := blank | comment | label | text | instr
* blank := /^\s*$/
* comment := /^\s*\/\/.*$/
* label := /^IDENT:$/
* text := TEXT name,$framesize
* instr := \tMNEM\t[OP1[, OP2]]
* OP := $NUM | REG | NUM(REG) | (REG) | name(SB) | label
*
* Identifiers may include '.' and '_'. Whitespace inside operands
* (between '$' and a number, etc.) is rejected for sanity.
*/
#include "a.h"
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
extern int a_isidstart(int);
extern int a_isidcont(int);
extern i64 a_parsenum(const char *, char **);
void
a_init(Asm *a, const char *file, const char *src, u64 len)
{
memset(a, 0, sizeof *a);
a->file = file;
a->src = src;
a->srclen = len;
a->line = 1;
}
static void
err(Asm *a, const char *msg)
{
fprintf(stderr, "w6a: %s:%d: %s\n", a->file, a->line, msg);
a->errs++;
}
Asym *
a_intern(Asm *a, const char *name)
{
for (Asym *s = a->syms; s; s = s->next)
if (strcmp(s->name, name) == 0) return s;
Asym *s = calloc(1, sizeof *s);
s->name = strdup(name);
s->next = a->syms;
a->syms = s;
return s;
}
/* ------------------------------------------------------------------ */
/* line iterator: returns the next line as a NUL-terminated buffer in
* line/llen pointers, advances pos. Returns 0 on EOF.
*/
static int
nextline(Asm *a, char **line, size_t *llen, char *buf, size_t bufsz)
{
if (a->pos >= a->srclen) return 0;
size_t n = 0;
while (a->pos < a->srclen && a->src[a->pos] != '\n' && n + 1 < bufsz)
buf[n++] = a->src[a->pos++];
buf[n] = '\0';
if (a->pos < a->srclen && a->src[a->pos] == '\n') a->pos++;
*line = buf;
*llen = n;
return 1;
}
/* skip leading whitespace */
static const char *
skipws(const char *p)
{
while (*p == ' ' || *p == '\t') p++;
return p;
}
static int
opcode_lookup(const char *m)
{
struct { const char *m; int op; } tab[] = {
{ "MOVQ", A_MOVQ }, { "MOVL", A_MOVL },
{ "MOVW", A_MOVW }, { "MOVB", A_MOVB },
{ "MOVZBQ", A_MOVZBQ }, { "MOVZWQ", A_MOVZWQ },
{ "MOVSXD", A_MOVSXD }, { "MOVSWQ", A_MOVSWQ },
{ "MOVSD", A_MOVSD },
{ "ADDSD", A_ADDSD },{ "SUBSD", A_SUBSD },
{ "MULSD", A_MULSD },{ "DIVSD", A_DIVSD },
{ "UCOMISD", A_UCOMISD },
{ "CVTTSD2SI", A_CVTTSD2SI },
{ "CVTSI2SD", A_CVTSI2SD },
{ "MOVSS", A_MOVSS },
{ "ADDSS", A_ADDSS },{ "SUBSS", A_SUBSS },
{ "MULSS", A_MULSS },{ "DIVSS", A_DIVSS },
{ "UCOMISS", A_UCOMISS },
{ "CVTTSS2SI", A_CVTTSS2SI },
{ "CVTSI2SS", A_CVTSI2SS },
{ "CVTSD2SS", A_CVTSD2SS },
{ "CVTSS2SD", A_CVTSS2SD },
{ "ADDQ", A_ADDQ }, { "SUBQ", A_SUBQ },
{ "IMULQ",A_IMULQ},{ "IDIVQ",A_IDIVQ},
{ "DIVQ", A_DIVQ },
{ "NEGQ", A_NEGQ },{ "NOTQ", A_NOTQ },
{ "ANDQ", A_ANDQ },{ "ORQ", A_ORQ },
{ "XORQ", A_XORQ },
{ "SHLQ", A_SHLQ },{ "SHRQ", A_SHRQ },
{ "CMPQ", A_CMPQ },
{ "PUSHQ",A_PUSHQ},{ "POPQ", A_POPQ },
{ "LEAQ", A_LEAQ },
{ "CALL", A_CALL },{ "RET", A_RET },
{ "JMP", A_JMP },
{ "JE", A_JE },{ "JNE", A_JNE },
{ "JL", A_JL },{ "JLE", A_JLE },
{ "JG", A_JG },{ "JGE", A_JGE },
{ "JB", A_JB },{ "JBE", A_JBE },
{ "JA", A_JA },{ "JAE", A_JAE },
{ "JZ", A_JZ },{ "JNZ", A_JNZ },
{ "SYSCALL", A_SYSCALL },
{ "TEXT", A_TEXT },
{ "DATA", A_DATA },
{ "DATAW", A_DATAW },
{ "DATAR", A_DATAR },
{ NULL, 0 }
};
for (int i = 0; tab[i].m; i++)
if (strcmp(tab[i].m, m) == 0) return tab[i].op;
return 0;
}
static int
reg_lookup(const char *r)
{
struct { const char *m; int reg; } tab[] = {
{ "AX", D_AX }, { "BX", D_BX }, { "CX", D_CX }, { "DX", D_DX },
{ "SP", D_SP }, { "BP", D_BP }, { "SI", D_SI }, { "DI", D_DI },
{ "R8", D_R8 }, { "R9", D_R9 }, { "R10", D_R10 },
{ "R11", D_R11 }, { "R12", D_R12 }, { "R13", D_R13 },
{ "R14", D_R14 }, { "R15", D_R15 },
{ "X0", D_X0 }, { "X1", D_X1 }, { "X2", D_X2 }, { "X3", D_X3 },
{ "X4", D_X4 }, { "X5", D_X5 }, { "X6", D_X6 }, { "X7", D_X7 },
{ "X8", D_X8 }, { "X9", D_X9 }, { "X10", D_X10 },
{ "X11", D_X11 }, { "X12", D_X12 }, { "X13", D_X13 },
{ "X14", D_X14 }, { "X15", D_X15 },
{ "SB", D_PSB }, { "FP", D_PFP },
{ NULL, 0 }
};
for (int i = 0; tab[i].m; i++)
if (strcmp(tab[i].m, r) == 0) return tab[i].reg;
return 0;
}
static int
parse_operand(Asm *a, const char *s, Aoperand *out)
{
while (*s == ' ' || *s == '\t') s++;
if (*s == '\0') { out->type = D_NONE; return 0; }
if (*s == '$') {
s++;
char *end;
out->type = D_CONST;
out->offset = a_parsenum(s, &end);
return 0;
}
/* (REG) form */
if (*s == '(') {
s++;
char rbuf[8] = {0};
int n = 0;
while (*s && *s != ')' && n < 7) rbuf[n++] = *s++;
if (*s != ')') { err(a, "missing ')' in indirect"); return -1; }
int r = reg_lookup(rbuf);
if (r == 0) { err(a, "bad register in indirect"); return -1; }
out->type = D_INDIR;
out->reg = r;
out->offset = 0;
return 0;
}
/* number(REG) form, or label form, or REG */
const char *p = s;
int sign = 1;
if (*p == '-') { sign = -1; p++; }
if (isdigit((unsigned char)*p)) {
char *end;
i64 off = a_parsenum(s, &end);
if (*end == '(') {
char rbuf[8] = {0};
int n = 0;
end++;
while (*end && *end != ')' && n < 7) rbuf[n++] = *end++;
if (*end != ')') { err(a, "missing ')'"); return -1; }
int r = reg_lookup(rbuf);
if (r == 0) { err(a, "bad register"); return -1; }
out->type = D_INDIR;
out->reg = r;
out->offset = off;
return 0;
}
out->type = D_CONST;
out->offset = off * sign;
return 0;
}
/* IDENT — register or symbol-or-label */
if (a_isidstart((unsigned char)*s)) {
char buf[64] = {0};
int n = 0;
while (a_isidcont((unsigned char)*s) && n < 63) buf[n++] = *s++;
buf[n] = 0;
/* Optional `+disp` between the ident and `(SB)`. Used by
* DATAR to address bytes inside an existing .data slot
* (e.g. `DATAR s+8(SB),...`). */
i64 sym_disp = 0;
if (*s == '+') {
s++;
char *end;
sym_disp = a_parsenum(s, &end);
s = end;
}
/* ID(SB) means external symbol */
if (*s == '(') {
char rbuf[8] = {0};
int rn = 0;
s++;
while (*s && *s != ')' && rn < 7) rbuf[rn++] = *s++;
if (*s != ')') { err(a, "missing ')'"); return -1; }
s++;
int r = reg_lookup(rbuf);
if (r == D_PSB) {
out->type = D_EXTERN;
out->sym = strdup(buf);
out->offset = sym_disp;
return 0;
}
out->type = D_INDIR;
out->reg = r;
out->offset = 0;
/* unusual case: name(REG) with named offset; not used */
return 0;
}
int r = reg_lookup(buf);
if (r != 0) {
out->type = r;
return 0;
}
/* otherwise it's a branch target */
out->type = D_BRANCH;
out->sym = strdup(buf);
return 0;
}
err(a, "unrecognised operand");
return -1;
}
int
a_parse(Asm *a)
{
/* Big enough for a DATAW emitting a [256]u32 table (1024 bytes
* → ~4100 chars of `\xNN` escapes plus directive boilerplate).
* Selfhost w6a allocates per-line; this is the cstage equivalent. */
static char buf[32768];
char *line;
size_t len;
const char *pending_label = NULL;
while (nextline(a, &line, &len, buf, sizeof buf)) {
const char *p = skipws(line);
if (*p == '\0' || (*p == '/' && p[1] == '/')) {
a->line++;
continue;
}
/* label? */
if (a_isidstart((unsigned char)*p) && line[0] != '\t') {
const char *q = p;
while (a_isidcont((unsigned char)*q)) q++;
if (*q == ':') {
size_t nl = q - p;
char *name = malloc(nl + 1);
memcpy(name, p, nl);
name[nl] = '\0';
/* If a label is already pending we'd lose it
* by overwriting; flush it onto a NOP prog so
* each label still pins to a real address. */
if (pending_label) {
Aprog *prg = calloc(1, sizeof *prg);
prg->as = A_NOP;
prg->label = pending_label;
prg->line = a->line;
if (a->head == NULL) a->head = prg;
else a->tail->link = prg;
a->tail = prg;
}
pending_label = name;
a->line++;
continue;
}
}
/* TEXT or instruction */
const char *m = p;
char mnem[16] = {0};
int n = 0;
while (*m && *m != ' ' && *m != '\t' && n < 15) mnem[n++] = *m++;
mnem[n] = '\0';
int op = opcode_lookup(mnem);
if (op == 0) {
err(a, "unknown opcode");
a->line++;
continue;
}
Aprog *prg = calloc(1, sizeof *prg);
prg->as = op;
prg->line = a->line;
prg->label = pending_label;
pending_label = NULL;
while (*m == ' ' || *m == '\t') m++;
const char *rest = m;
if (op == A_TEXT) {
/* TEXT name,$framesize */
char nbuf[64] = {0};
int nn = 0;
while (*m && *m != ',' && nn < 63) nbuf[nn++] = *m++;
prg->to.type = D_EXTERN;
prg->to.sym = strdup(nbuf);
if (*m == ',') {
m++;
while (*m == ' ' || *m == '$') m++;
prg->from.type = D_CONST;
prg->from.offset = a_parsenum(m, NULL);
}
} else if (op == A_DATA || op == A_DATAW) {
/* DATA name(SB),"escaped bytes" — read-only in .text
* DATAW name(SB),"escaped bytes" — writable in .data */
char nbuf[128] = {0};
int nn = 0;
while (*m && *m != '(' && nn < 127) nbuf[nn++] = *m++;
prg->to.type = D_EXTERN;
prg->to.sym = strdup(nbuf);
if (*m == '(') {
while (*m && *m != ')') m++;
if (*m == ')') m++;
}
while (*m == ' ' || *m == ',' || *m == '\t') m++;
if (*m != '"') {
err(a, "DATA expects \"...\"");
prg->bytes = NULL;
prg->nbytes = 0;
} else {
m++;
/* parse escapes into a fresh buffer */
size_t cap = 32, len = 0;
u8 *buf = malloc(cap);
while (*m && *m != '"') {
int c = (unsigned char)*m++;
if (c == '\\' && *m) {
int e = (unsigned char)*m++;
switch (e) {
case 'n': c = '\n'; break;
case 't': c = '\t'; break;
case 'r': c = '\r'; break;
case '\\': c = '\\'; break;
case '"': c = '"'; break;
case '0': c = 0; break;
case 'x': {
int hi = (unsigned char)*m++;
int lo = (unsigned char)*m++;
int h = (hi<='9'?hi-'0':(hi|0x20)-'a'+10);
int l = (lo<='9'?lo-'0':(lo|0x20)-'a'+10);
c = (h << 4) | l;
break;
}
default: c = e; break;
}
}
if (len + 1 > cap) {
cap *= 2;
buf = realloc(buf, cap);
}
buf[len++] = (u8)c;
}
prg->bytes = buf;
prg->nbytes = len;
}
} else {
/* split rest at top-level comma */
const char *comma = NULL;
for (const char *q = rest; *q; q++)
if (*q == ',' && comma == NULL) comma = q;
if (comma) {
char op1[256], op2[256];
size_t l1 = comma - rest;
if (l1 >= sizeof op1) l1 = sizeof op1 - 1;
memcpy(op1, rest, l1); op1[l1] = '\0';
size_t l2 = strlen(comma + 1);
if (l2 >= sizeof op2) l2 = sizeof op2 - 1;
memcpy(op2, comma + 1, l2); op2[l2] = '\0';
parse_operand(a, op1, &prg->from);
parse_operand(a, op2, &prg->to);
} else if (*rest) {
parse_operand(a, rest, &prg->to);
}
}
if (a->head == NULL) a->head = prg;
else a->tail->link = prg;
a->tail = prg;
a->line++;
}
return a->errs;
}