w6c+wcc/check: infer [_]T array length from initializer element count (fix #7)

`[_]T = [...]` (canonical Hare array-length inference) silently
miscompiled to a zero-length array: the parser already left the array
type's length child nil as the infer sentinel — distinct from an
explicit [N] — but neither checker stamped the real count, so `len(x)`
returned 0 with no diagnostic (rule-7 silent miscompile). Module-level
was worse on wwstage, where `x.len` on ANY global array (even an
explicit [N]) fell to the SB fallback and mis-emitted `MOVQ len(SB), AX`
(linker: undefined reference to len).

The length lives in the stamped TYPE and cgen already keys stride /
length / data-emission off it, so stamping the inferred count at the one
checker inference point closes it permanently (rob's #7 ruling):

  - check.c clet + module-level N_LET pass-2: count the initializer's
    elements and patch the array type's length (the Sym too, so a later
    x.len reads the inferred alen). No-init / non-array init can't infer
    -> loud error, never a silent zero-length array.
  - check.ww inferarraylen: the wwstage twin — stamp a synthesized
    N_INTLIT length child before resolvewalk caches the array tinfo;
    same loud-error rule. Idempotent for the module-level double-call.
  - cgenexpr.ww cgdot: the missing wwstage arm for a top-level [N]T
    global's .len / .ptr (cstage cgen.c:8011 already had it).
  - cgenutil.ww letslotsize: drop the now-redundant [_] slot-size
    intercept — a workaround for this very bug; the stamped length flows
    through the general slotsize path (rule 7).

Both stages converge byte-identical; new table-driven test 684 covers
[_]int/[_]str/[_]u8 local + module-level, len + element read-back,
dual-stage runtime + asm byte-id, plus three negative no-infer rows.
This commit is contained in:
2026-06-03 18:44:39 +09:00
parent 63cb39e45b
commit 7ca32432b1
8 changed files with 653 additions and 136 deletions

View File

@@ -14313,8 +14313,48 @@ fn checkassign(c: *checker, n: *node) void = {
n.rhs = desugararrayslice(c, ltn, rtn, n.rhs);
};
// inferarraylen — `let xs: [_]T = arrlit;` length inference (#7). The
// parser leaves a `[_]` array's length child nil as the infer sentinel
// (parse.ww, mirror cstage parse.c:186). Count the array-literal's
// elements (skipping the `...` repeat marker, same walk as the N_ARRLIT
// exprtype at L2905) and stamp a synthesized N_INTLIT length node so
// tinfofornode / cgen / `.len` all read the real count — the wwstage
// analogue of cstage clet's `declared = type_array(.., iu->alen)` patch.
// A `[_]T` with no array-literal initialiser can't infer: loud error,
// never a silent zero-length array (rule 7). Idempotent (skips once the
// length child is set), so the module-level double-call (checkfile's
// pre-resolvewalk call + checkletassign here) raises at most one error.
fn inferarraylen(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
if (n.lhs.kind != nkind.N_TARRAY) { return; };
if (n.lhs.rhs != nil) { return; }; // explicit [N] or already inferred
if (n.rhs == nil || n.rhs.kind != nkind.N_ARRLIT) {
cerr("error: [_]T needs an array-literal initialiser\n");
c.errs += 1;
let z: *node = newnode(nkind.N_INTLIT, "", 0, 0);
z.uval = 0u64;
n.lhs.rhs = z; // sentinel: idempotent, error already raised
return;
};
let cnt: u64 = 0u64;
let it: *node = n.rhs.list;
for (it != nil) {
let skip: bool = false;
if (it.kind == nkind.N_FIELD) {
if (streq(it.str, "...")) { skip = true; };
};
if (!skip) { cnt += 1u64; };
it = it.next;
};
let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0);
cn.uval = cnt;
n.lhs.rhs = cn;
};
fn checkletassign(c: *checker, n: *node) void = {
if (n == nil) { return; };
inferarraylen(c, n); // #7: must run before the n.rhs==nil bail
if (n.rhs == nil) { return; }; // no init
// hint = nil for A.6.0; A.6.1 will pass n.lhs once STRUCTLIT/ARRLIT
// arms consume it. Plumbing-only at this point.
@@ -14870,6 +14910,12 @@ export fn checkfile(c: *checker, file: *node) void = {
case nkind.N_TYPEDECL:
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
case nkind.N_LET:
// #7: a module-level `let xs: [_]T = arrlit;` must infer
// its length BEFORE resolvewalk stamps d.lhs's tinfo —
// otherwise the array tinfo caches the alen=0 sentinel and
// the patched length child never reaches the size/data
// reads. Idempotent with the checkletassign call below.
inferarraylen(c, d);
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
if (d.rhs != nil) { resolvewalk(c, d.rhs); };
// #130: top-level let assignability — the subtree
@@ -17396,50 +17442,10 @@ fn inferletcalltype(c: *cgen, rhs: *node) *node = {
// tagged-init branch writes past the local and tramples the next
// slot.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
// Composite primitive: `str` is 16B
// (ptr+len) — primsize returns 0 for
// it, so it'd slot 8B without this.
if (streq(elemn.str, "str")) {
esz = primtypesize("str"): i32;
} else {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
// `[_]T = arrlit;` inferred-length arrays no longer need a slot-size
// intercept here: the checker (inferarraylen, check.ww) stamps the
// real element count onto the array type's length child before cgen
// runs, so slotsize reads it like any explicit `[N]T` (#7).
if (n.lhs != nil) { return slotsize(c, n.lhs); };
// Annotation-less init: defer to the call's return type if we
// can infer it. Tagged-union returns need 24B; everything else
@@ -22165,6 +22171,41 @@ fn cgdot(c: *cgen, n: *node) void = {
};
};
};
// Top-level [N]T global pseudo-fields (#7): `.len` is the static
// element count (immediate from the array type node's length child);
// `.ptr` is the array's base address (LEAQ name(SB)). Without this a
// module-level array's `x.len` falls to the module-qualified SB
// fallback below and mis-emits `MOVQ len(SB), AX` (linker: undefined
// reference to len). Mirror of the local-array arm above and cstage
// cg_base_cap's `aimm(bu->alen)` immediate (cgen.c:1692).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let gtn: *node = letvartnode(c, lhs.str);
if (gtn != nil) {
if (gtn.kind == nkind.N_TARRAY) {
if (streq(fld, "ptr")) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), AX\n");
return;
};
if (streq(fld, "len")) {
let lenn: *node = gtn.rhs;
let alen: i64 = 0i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) {
alen = lenn.uval: i64;
};
};
emitline("\tMOVQ\t$");
emitint(alen);
emitline(", AX\n");
return;
};
};
};
};
};
// Top-level struct global field read — LEAQ name(SB), CX then
// load at fi.foff(CX). Mirrors the local "Direct struct local"
// branch above, swapping the BP frame slot for the global VA.