wcc: array static-init let/def DATA emit via SSoT helper (#129 A.3)

Extract emit_array_data + emit_array_lit_bytes helpers (both stages,
mirrored) for module-level let/def with N_ARRLIT initializer or no-rhs
zero-init. Two-pass validate-then-emit: validate pass walks elements
and fails atomically on any non-foldable element (no partial-byte
emit on failure); emit pass writes element bytes after success.
Element-kind dispatch: integer via fold_int_literal byte-for-byte
preserved from pre-A.3 inline arm (bootstrap NEUTRAL — 6 live consumers
in lib/os/bufio/strings/encoding-utf8/strconv-stof_data), float via
inline bitcast + sign-XOR byte-loop (A.1 shape, no INT64_MIN — sibling
#144), struct via recursion into emit_struct_lit_bytes (A.2 helper).
Out-of-scope element kinds (ptr-elem, nested-array) rule-7 fatal.

emit_struct_lit_bytes gains TY_ARRAY field arm calling emit_array_lit_
bytes recursively — closes A.2 parked shape-15 (array-in-struct
`def D: dt = dt{tag=42, buf=[1u8,2u8,3u8,4u8]};`).

LOAD-side widened symmetric to A.2 precedent: cstage cgindex N_INDEX
direct-ident isglobal gate widened via new DefArray registry
(def_isarraydef populated in let_collect parallel to DefStruct);
wwstage cgindex N_INDEX falls through to defvartnode on letvartnode nil
(reads defent.dtnode field added in A.2). Both stages materialise
array-def via LEAQ name(SB) same as array-let.

Mid-impl rule-7 stop: refactor initially routed only rhs==N_ARRLIT
through emitarraydata, leaving nil-rhs zero-init arrays (e.g.
`let f64tos_buf: [64]u8;` in lib/strconv) silently SKIPPED → undef-ref
at link of wwstage-rebuilt selfhost binaries. Caught on first gate run
via bootstrap 994/995 RED. Fixed by adding nil-rhs branch to
emitarraydata (zero-fills arrt.size bytes) + widening wwstage caller
to route both N_ARRLIT and nil through helper. Same-class-lower-stratum
pattern (recurring across A.1 N_UN-peel, A.2 sz==8-short-circuit, A.3
nil-rhs-drop); banked as feedback memory.

Test 919 (11 rows: int-elem 1B/4B/8B + signed-N_UN-peel + float-elem
f64/f32 + def-int / def-float / struct-with-array-field shape-15 +
explicit-zero + single-elem-regression) registered. Make test:
182/182 incl. 990-997 byte-id + combined_ww_fresh.

Followups filed:
- #43 — wwstage emitletdataw str/slice-size arms lack !isarr guards;
  hypothetical no-rhs [16/24]u8 triple-emits (NOT A.3-introduced;
  no live consumer; 2-line parity fix)
This commit is contained in:
2026-05-27 05:56:24 +09:00
parent 0ed0b3933c
commit 9e3bc4ea37
7 changed files with 1562 additions and 242 deletions

View File

@@ -711,6 +711,17 @@ struct DefStruct {
};
static DefStruct *defstructs;
/* #129 A.3: array-typed defs now have DATA storage and need the same
* LEAQ name(SB) + indexed-load shape as array-typed lets at cgindex
* and N_DOT base-resolution sites. Mirrors DefStruct (A.2). */
typedef struct DefArray DefArray;
struct DefArray {
const char *name;
Type *type;
DefArray *next;
};
static DefArray *defarrays;
/* Slot size for a top-level `let` of type t, or 0 if the type isn't
* supported as a writable global yet. Tagged unions are deferred.
* enums route through their storage type.
@@ -914,6 +925,7 @@ let_collect(Cg *c, Node *file)
{
letvars = NULL;
defstructs = NULL;
defarrays = NULL;
if (file == NULL) return;
for (Node *d = file->list; d; d = d->next) {
if (d->kind == N_LET) {
@@ -927,18 +939,32 @@ let_collect(Cg *c, Node *file)
continue;
}
if (d->kind == N_DEF) {
if (d->str == NULL || d->str[0] == '\0') continue;
/* #129 A.2: struct-typed defs now have DATA storage
* (emit_defs struct arm); register them so the N_DOT
* struct-let LEAQ-and-offset shape widens to cover
* them too. Other def kinds (int / float / str)
* stay on their existing load paths. */
if (d->str == NULL || d->str[0] == '\0') continue;
if (!let_isstruct(d->type)) continue;
DefStruct *ds = amalloc(c->a, sizeof *ds);
ds->name = d->str;
ds->type = d->type;
ds->next = defstructs;
defstructs = ds;
if (let_isstruct(d->type)) {
DefStruct *ds = amalloc(c->a, sizeof *ds);
ds->name = d->str;
ds->type = d->type;
ds->next = defstructs;
defstructs = ds;
continue;
}
/* #129 A.3: array-typed defs now have DATA storage
* (emit_defs array arm); register them so cgindex's
* `let_islet`-gated LEAQ name(SB) base-load widens
* to defs too (LOAD-side twin of the struct-def
* registry). */
if (let_isarray(d->type)) {
DefArray *da = amalloc(c->a, sizeof *da);
da->name = d->str;
da->type = d->type;
da->next = defarrays;
defarrays = da;
}
}
}
}
@@ -952,6 +978,15 @@ def_isstructdef(const char *name)
return 0;
}
static int
def_isarraydef(const char *name)
{
if (name == NULL) return 0;
for (DefArray *da = defarrays; da; da = da->next)
if (strcmp(da->name, name) == 0) return 1;
return 0;
}
static int
let_islet(const char *name)
{
@@ -6797,7 +6832,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (n->lhs->kind == N_IDENT && u) {
int off = localfind(locals, n->lhs->str);
int isglobal = (off == 0) && let_islet(n->lhs->str);
/* #129 A.3: array-typed defs now have DATA storage; the
* LEAQ name(SB) base-load must fire for them too, not
* just let_islet. Parallel to A.2's def_isstructdef
* gate at the N_DOT direct-struct-ident arm. */
int isglobal = (off == 0) && (let_islet(n->lhs->str)
|| def_isarraydef(n->lhs->str));
cgexpr(c, n->rhs, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
@@ -8523,6 +8563,12 @@ emit_floatlit_data(FILE *out, Cg *c, const char *directive,
return 1;
}
/* Forward declaration: emit_struct_lit_bytes recurses into
* emit_array_lit_bytes for nested array fields (#129 A.3 closes the
* A.2 shape-15 park). Defined further down. */
static int emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs,
int emit_phase);
/* emit_struct_lit_bytes — emit the byte sequence for a struct-typed
* top-level let/def whose rhs is an N_STRUCTLIT (or NULL for bare
* no-rhs). Walks Tfield list in declaration order, zero-fills padding
@@ -8585,6 +8631,24 @@ emit_struct_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, u64 base)
pos = fstart + (u64)fsz;
continue;
}
/* #129 A.3: array-typed field with N_ARRLIT rhs (the shape
* parked in A.2). Calls emit_array_lit_bytes which dispatches
* by element kind (int/float/struct). Returns 0 if the rhs
* shape can't reduce — fatal here per rule-7 since the field
* is declared array-typed and a non-reducible inner rhs is
* a real bug surface, not a fall-through. */
if (fu && fu->kind == TY_ARRAY) {
if (vr == NULL || vr->kind != N_ARRLIT)
fatal("emit_struct_lit_bytes: array field "
"'%s' rhs is not N_ARRLIT (#129 A.3)",
f->name ? f->name : "?");
if (!emit_array_lit_bytes(out, c, f->type, vr, 1))
fatal("emit_struct_lit_bytes: array field "
"'%s' rhs has non-reducible elements "
"(#129 A.3)", f->name ? f->name : "?");
pos = fstart + (u64)fsz;
continue;
}
if (type_isfloat(f->type)) {
int isf32 = type_isf32(f->type);
u64 fv = 0;
@@ -8654,6 +8718,210 @@ emit_struct_data(FILE *out, Cg *c, const char *directive,
return 1;
}
/* emit_array_lit_bytes — emit alen * esz bytes for an [N]T top-level
* let/def with N_ARRLIT rhs. Per-element dispatch:
* - int element (covers bool/rune/typed-int/N_UN-int): fold_int_literal
* per element, emit LE bytes. Existing pre-#129-A.3 emit_lets array
* arm logic preserved byte-for-byte so the bootstrap consumers in
* lib/os, lib/bufio, lib/strings, lib/encoding/utf8, lib/strconv/
* stof_data don't shift.
* - float element (f32/f64): peel N_CAST/N_UN(±), bitcast magnitude
* via union (mirrors emit_floatlit_data), sign-XOR top byte of each
* element inline. NO 2^63 immediate.
* - struct element: per element call emit_struct_lit_bytes (#129 A.2
* helper).
* - other element kinds (str/slice/ptr-with-address/nested-array):
* return 0 — caller falls through to zero-init (str/slice accepts
* no-rhs already).
*
* Trailing `...` repeat marker fills remaining slots with the last
* value (mirrors the scalar repeat path). Returns 1 on emit, 0 if the
* rhs shape can't reduce to a foldable literal — caller MUST then
* fall back to zero-init / skip path; the caller opens the DATA/DATAW
* directive AFTER a successful validate-only call. Two-call pattern
* keeps emit-on-failure from emitting partial bytes.
*
* `emit_phase = 0` runs validate-only (returns 1 if ok); `emit_phase
* = 1` actually emits. */
static int
emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase)
{
Type *u = type_unwrap(t);
if (u == NULL || u->kind != TY_ARRAY) return 0;
Type *etype = u->sub;
Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype;
int esz = etype ? (int)etype->size : 1;
int alen = (int)u->alen;
if (eu && eu->kind == TY_STRUCT) {
/* Validate: every element must be N_STRUCTLIT (after N_CAST
* peel). */
int idx = 0;
Node *last_ev = NULL;
int repeat = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev == NULL || ev->kind != N_STRUCTLIT) return 0;
last_ev = ev;
idx++;
}
if (!emit_phase) return 1;
idx = 0;
repeat = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
emit_struct_lit_bytes(out, c, etype, ev, 0);
idx++;
}
while (idx < alen) {
if (repeat && last_ev != NULL)
emit_struct_lit_bytes(out, c, etype, last_ev, 0);
else
for (int b = 0; b < esz; b++)
emit_data_byte(out, 0);
idx++;
}
return 1;
}
if (type_isfloat(etype)) {
int isf32 = type_isf32(etype);
/* Validate: every element must be N_FLOATLIT (after N_CAST
* + optional N_UN(±) peel). */
int idx = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) break;
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev != NULL && ev->kind == N_UN
&& (ev->op == TK_MINUS || ev->op == TK_PLUS)) {
ev = ev->lhs;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
}
if (ev == NULL || ev->kind != N_FLOATLIT) return 0;
idx++;
}
if (!emit_phase) return 1;
idx = 0;
u64 last_bits = 0;
int last_neg = 0;
int repeat = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
int neg = 0;
if (ev != NULL && ev->kind == N_UN
&& (ev->op == TK_MINUS || ev->op == TK_PLUS)) {
if (ev->op == TK_MINUS) neg = 1;
ev = ev->lhs;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
}
u64 bits = 0;
if (isf32) {
union { float f; u32 u; } x;
x.f = (float)ev->fval;
bits = (u64)x.u;
} else {
union { double d; u64 u; } x;
x.d = ev->fval;
bits = x.u;
}
for (int b = 0; b < esz; b++) {
u8 byt = (u8)((bits >> (b * 8)) & 0xff);
if (neg && b == esz - 1) byt = (u8)(byt ^ 0x80);
emit_data_byte(out, byt);
}
last_bits = bits;
last_neg = neg;
idx++;
}
while (idx < alen) {
if (repeat) {
for (int b = 0; b < esz; b++) {
u8 byt = (u8)((last_bits >> (b * 8)) & 0xff);
if (last_neg && b == esz - 1)
byt = (u8)(byt ^ 0x80);
emit_data_byte(out, byt);
}
} else {
for (int b = 0; b < esz; b++)
emit_data_byte(out, 0);
}
idx++;
}
return 1;
}
/* Int-element path — preserved BYTE-FOR-BYTE from the pre-A.3
* emit_lets in-place array arm so the bootstrap consumers (u8 /
* i8 / u16 arrays in lib/os, lib/bufio, lib/strings, lib/
* encoding/utf8, lib/strconv/stof_data) don't shift. */
u64 *vals = amalloc(c->a, sizeof(u64) * (size_t)alen);
int idx = 0;
int ok = 1;
u64 last = 0;
int repeat = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev == NULL) { ok = 0; break; }
if (!fold_int_literal(ev, &last)) { ok = 0; break; }
vals[idx++] = last;
}
if (!ok) return 0;
if (!emit_phase) return 1;
if (repeat) {
while (idx < alen) vals[idx++] = last;
} else {
while (idx < alen) vals[idx++] = 0;
}
for (int i = 0; i < alen; i++) {
u64 v = vals[i];
for (int b = 0; b < esz; b++)
emit_data_byte(out, (u8)((v >> (b * 8)) & 0xff));
}
return 1;
}
/* emit_array_data — opens DATA/DATAW prefix on validate success, then
* emits payload. Two-pass keeps emit-on-failure from emitting partial
* bytes (would corrupt the asm if rhs reduces partway through). */
static int
emit_array_data(FILE *out, Cg *c, const char *directive,
const char *name, Type *t, Node *rhs)
{
Type *u = type_unwrap(t);
if (u == NULL || u->kind != TY_ARRAY) return 0;
if (!emit_array_lit_bytes(out, c, t, rhs, 0)) return 0;
fprintf(out, "%s %s(SB),\"", directive, mod_mangle(c, name));
emit_array_lit_bytes(out, c, t, rhs, 1);
fputs("\"\n", out);
return 1;
}
static void
emit_lets(Cg *c, FILE *out, Node *file)
{
@@ -8718,59 +8986,17 @@ emit_lets(Cg *c, FILE *out, Node *file)
fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, lab);
continue;
}
/* Array literal init: `let xs: [N]T = [v0, v1, ...];`. Walk
* elements in declaration order; each must reduce to an
* integer literal (casts are stripped). The trailing `...`
* repeat marker fills remaining slots with the last value.
* Falls through to zero-init if any element isn't a
* constant we can evaluate at emit time. */
/* Array literal init: `let xs: [N]T = [v0, v1, ...];`. The
* helper dispatches per element kind (int/float/struct).
* Int-element path preserved BYTE-FOR-BYTE from pre-A.3 so
* bootstrap consumers (lib/os, lib/bufio, lib/strings, lib/
* encoding/utf8, lib/strconv/stof_data) don't shift. Float
* + struct elements gain emit; ptr / nested-array fall
* through to zero-init (existing path below). */
if (r != NULL && r->kind == N_ARRLIT && let_isarray(d->type)) {
Type *u = type_unwrap(d->type);
int esz = (u && u->sub) ? (int)u->sub->size : 1;
int alen = (u) ? (int)u->alen : 0;
u64 *vals = amalloc(c->a, sizeof(u64) * (size_t)alen);
int idx = 0;
int ok = 1;
u64 last = 0;
int repeat = 0;
for (Node *e = r->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str &&
strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev == NULL) { ok = 0; break; }
/* Same helper as the scalar arm above —
* fold_int_literal covers N_UN over a leaf
* so signed-negative array elements (`-1i8`)
* encode as their two's-complement bytes
* once truncated to esz below. */
if (!fold_int_literal(ev, &last)) {
ok = 0;
break;
}
vals[idx++] = last;
}
if (ok) {
if (repeat) {
while (idx < alen) vals[idx++] = last;
} else {
while (idx < alen) vals[idx++] = 0;
}
fprintf(out, "DATAW %s(SB),\"",
mod_mangle(c, d->str));
for (int i = 0; i < alen; i++) {
u64 v = vals[i];
for (int b = 0; b < esz; b++) {
emit_data_byte(out,
(u8)((v >> (b * 8)) & 0xff));
}
}
fputs("\"\n", out);
if (emit_array_data(out, c, "DATAW", d->str,
d->type, r))
continue;
}
/* fall through to zero-init */
}
/* Otherwise: zero-init. str accepts nil / ""; struct
@@ -8846,6 +9072,15 @@ emit_defs(Cg *c, FILE *out, Node *file)
d->str, d->type, d->rhs);
continue;
}
/* #129 A.3: array-typed def with N_ARRLIT rhs. Parallel to
* emit_lets's array arm; uses DATA (read-only). LOAD-side
* widening at cgindex/cgdot resolves the def's address via
* LEAQ name(SB). */
if (let_isarray(d->type) && d->rhs->kind == N_ARRLIT) {
(void)emit_array_data(out, c, "DATA",
d->str, d->type, d->rhs);
continue;
}
}
(void)c;
}