w6c+selfhost+lib: cgen quality batch + lib Hare-shape graduation

Six fixes across the toolchain, surfaced by lib/lisp porting work.

  1. f64 compound assigns (`acc += d`, `-=`, `*=`, `/=`). Both stages
     load slot → X1, OP X0 into X1, store back (ADDSD/SUBSD/MULSD/
     DIVSD are reg-reg only). Previous MOVSD-overwrite dropped the
     OP. Locals and top-level lets.

  2. Top-level `[N]u8` arrays + `&arr[i]`. let_emit_size grows a
     TY_ARRAY branch so zero-init DATAW lands; cgindex / N_INDEX
     store / `&base[i]` all detect a global array base and use
     LEAQ name(SB) instead of LEAQ (BP). TK_AMP no longer pre-
     evaluates the operand as a value-load — `&base[i]` computes
     base + i*esz directly. Unblocks Hare's static-buffer pattern:
     strconv.{u64,i64,f64}tos graduate to module-level `*_buf`
     arrays and return owned views.

  3. Cross-module `pkg.Enum.MEMBER`. Nested N_DOT chains that
     don't fold to a known shape now emit `MOVQ <leaf>(SB), AX`
     (mirrors the bare-IDENT unresolved fallback), so isolation
     probes — and the test 990 cgen-match floor — stay consistent
     across stages. strconv exposes `base` as a real `enum i32`;
     callers updated. The `main` exemption (linker entry-point
     keeps bare name even when not exported) mirrors C-side
     collectmods into selfhost cgendecl.

  4. Sum-typed parameter ABI. lib/bytes.{index,rindex} take
     `(u8 | []u8)` needle; lib/strings.byteindex / rbyteindex take
     `(str | rune)` needle (Hare-shaped; the byte-wise misnomer
     `index` is dropped). tagged_arg_size cap bumps to 48 (6 int
     regs), with a new partial-fit branch on the callee: when an
     N-word tagged arg overflows remaining regs, fill what fits and
     stitch the rest from positive BP offsets. scanlocals MCASE
     handles slice binds (24B) and walks each arm with a saved /
     restored seenmark set so two arms naming the same local each
     get their own slot — matches cstage's per-arm scope reset.

  5. 4-reg tagged-return ABI (AX=tag, DX=word0, CX=word1, R8=word2),
     up from 3 regs. Slice-payload variants (`([]T | E)`, slot 32B)
     round-trip ptr/len/cap end-to-end. Every receive site updates:
     let-init via cgwidentaggedstore, match scrutinee spill, cgindex
     tagged-element load (both N_IDENT and fallback bases),
     pushargsrev tagged-ident arg (reads word count from slot size),
     cgreturn slice variant in the shuffle path.

  6. `expr: TaggedAlias` is a widening, not a re-interpret. C cgen +
     selfhost cgwidentaggedstore peel an N_CAST whose destination IS
     the union — so cgexpr's natural shape (str: AX=ptr, BX=len;
     slice: AX=ptr, BX=len, CX=cap) is consumed by the matching
     concrete-variant branch instead of being misread as a tagged
     AX/DX/CX triple. Inner casts to a concrete variant (`7: i32`)
     keep their type for proper tag lookup. `[N]Alias` arrays
     resolve element size via slotsize + aliaslookup, and aliaslookup
     strips a `pkg.` prefix so cross-module references work.

lib/fmt grows `formattable = (i64 | str | bool | rune)` plus
`printv` / `printlnv` taking an explicit `[]formattable` slice (the
receive side of Hare's `args: formattable...`). Call-site variadic
gather isn't wired — callers either hand-build the slice or compose
strconv.i64tos + strings.concat.

700_e2e: 114 → 123 rows (f64 compound, top-level u8 arrays + `&buf[i]`,
pkg.Enum.MEMBER, sum-typed (str|rune) and (u8|[]u8) params, 4-reg
slice-return ABI, formattable array). 26/26 tests, bootstrap stable
through ww4.
This commit is contained in:
2026-05-13 08:05:01 +09:00
parent 6fd0160c0f
commit 46edb8db4a
23 changed files with 2665 additions and 915 deletions

View File

@@ -152,7 +152,11 @@ tagged_arg_size(Type *t)
if (t == NULL) return 0;
if (t->kind == TY_NAMED) t = t->under;
if (t == NULL || t->kind != TY_TAGGED) return 0;
if (t->size > 24) return 0;
/* Param/let/struct contexts have 6 int regs (DI..R9) so a 48B
* tagged union (6 words) still fits in registers. Return values
* are stricter (AX:DX:CX, max 24B) — gated separately in
* cgreturn. */
if (t->size > 48) return 0;
return (int)t->size;
}
@@ -444,6 +448,11 @@ let_emit_size(Type *t)
case TY_STRUCT:
return (int)u->size; /* zero-init only; field reads/
* scalar-field writes only. */
case TY_ARRAY:
return (int)u->size; /* zero-init only; element
* loads/stores via cgindex. Mirror
* of selfhost letemitsize's
* N_TARRAY branch. */
default:
return 0;
}
@@ -481,6 +490,17 @@ let_isstruct(Type *t)
return u && u->kind == TY_STRUCT;
}
/* Is the unwrapped type a fixed-length array? Array globals are
* zero-init DATAW slots; cgindex addresses them as LEAQ name(SB)
* and lets the element load/store run as usual. */
static int
let_isarray(Type *t)
{
if (t == NULL) return 0;
Type *u = (t->kind == TY_NAMED) ? t->under : t;
return u && u->kind == TY_ARRAY;
}
/* Is the unwrapped type a float (f32 or f64)? Float globals flow
* through X0 — load/store goes LEAQ name(SB),CX → MOVSS/MOVSD via the
* indirect, since the asm has no D_EXTERN form for SSE moves yet. */
@@ -848,6 +868,27 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0));
return;
}
/* `expr: TaggedAlias` where the cast's destination IS the union
* itself is a widening, not a re-interpret. cgexpr on the cast
* leaves the inner expression's register shape (str: AX=ptr,
* BX=len), not the tagged AX/DX/CX triple — so route through the
* concrete-variant branches below by peeling the cast. Casts to
* a concrete variant (`7: i32`) keep their type for proper tag
* lookup and fall through to the matching branch. */
if (src && src->kind == N_CAST && src->lhs) {
Type *castt = src->type;
Type *castu = (castt && castt->kind == TY_NAMED)
? castt->under : castt;
Type *innert = src->lhs->type;
Type *innu = (innert && innert->kind == TY_NAMED)
? innert->under : innert;
int cast_is_widen = (castu == du) ||
(castu && castu->kind == TY_TAGGED && type_eq(castt, dst));
int inner_is_tagged = innu && innu->kind == TY_TAGGED;
if (cast_is_widen && !inner_is_tagged) {
src = src->lhs;
}
}
Type *st = src ? src->type : NULL;
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
/* Tagged → tagged subset: copy slot words then tag-remap. */
@@ -871,6 +912,9 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
if (ssz > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, slot_off + 16));
if (ssz > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, slot_off + 24));
}
if (ssz < sz) {
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
@@ -965,6 +1009,19 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
amem(D_BP, slot_off + 0));
return;
}
/* Slice payload: cgexpr leaves (AX=ptr, BX=len, CX=cap). The
* slot layout is tag@+0, ptr@+8, len@+16, cap@+24 — requires the
* destination tagged-union slot be at least 32B. */
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 8));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, slot_off + 16));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, slot_off + 24));
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, slot_off + 0));
return;
}
/* Scalar / pointer / etc. The high slot word (when sz > 16) is
* left untouched here — match dispatches on the tag word first
* and only the str branch reads slot+16, so leaving the pad
@@ -977,9 +1034,12 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, slot_off + 0));
}
/* cg_widen_tagged_push — call-site widening. Materialise the tagged
* value in a stack scratch slot then push slot words high→low so the
* arg-register pop drain sees tag first, then payload words. */
/* cg_widen_tagged_push — call-site widening. For shapes where cgexpr
* leaves the value directly in registers (str: AX=ptr, BX=len; slice:
* AX=ptr, BX=len, CX=cap; scalar: AX), push from registers without a
* scratch slot. Struct payload and tagged-subset re-layout still
* route through a scratch slot. The direct-push form keeps wwstage's
* asm byte-identical to cstage on the byteindex / index family. */
static void
cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
{
@@ -990,12 +1050,51 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
ins1(c, A_PUSHQ, areg(D_AX));
return;
}
Type *st = src ? src->type : NULL;
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
int src_is_struct = su && su->kind == TY_STRUCT;
int src_is_tagged = su && su->kind == TY_TAGGED;
if (!src_is_struct && !src_is_tagged) {
/* Direct-push fast path: str / slice / scalar / pointer. */
cgexpr(c, src, *locals_p);
int tag = cg_tag_for_variant(du, st);
if (tag < 0) tag = 0;
if (type_isstr(st) || (su && su->kind == TY_STR)) {
/* slot 24: [+0]=tag, [+8]=ptr, [+16]=len. Push len,
* ptr, tag (high→low so pop drains tag first). */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
return;
}
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
/* slot 32: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap. */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
return;
}
/* Scalar / pointer variant. Pad with zero high words when
* the slot has room for a wider variant. */
int nwords = sz / 8;
for (int k = nwords - 1; k >= 2; k--) {
ins2(c, A_XORQ, areg(D_DX), areg(D_DX));
ins1(c, A_PUSHQ, areg(D_DX));
}
ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */
return;
}
const char *scr_name = mklabel(c, "argscr");
int scr = local_alloc(c, locals_p, scr_name, sz, cg_frame);
/* Zero the scratch slot first so any pad word the store path
* leaves untouched (scalar variant in a >16B slot, struct payload
* shorter than the slot's value area) reads as 0 on the callee.
* The store path then writes the variant bytes over the zeros. */
* leaves untouched (struct payload shorter than the slot's value
* area) reads as 0 on the callee. The store path then writes the
* variant bytes over the zeros. */
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k));
@@ -1120,6 +1219,84 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
case N_UN:
/* Address-of has its own evaluation strategy — we want the
* address of the operand, not its value. Special-case before
* the cgexpr pre-eval below so `&arr[i]` doesn't compile the
* value load and then discard it. */
if (n->op == TK_AMP) {
Node *opnd = n->lhs;
if (opnd && opnd->kind == N_IDENT) {
int off = localfind(locals, opnd->str);
if (off != 0) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
} else if (let_islet(opnd->str)) {
ins2(c, A_LEAQ, masym(c, opnd->str),
areg(D_AX));
}
break;
}
if (opnd && opnd->kind == N_INDEX) {
/* &base[i] = base + i*esz, no dereference. */
Node *base = opnd->lhs;
Node *idx = opnd->rhs;
Type *bt = base ? base->type : NULL;
Type *bu = (bt && bt->kind == TY_NAMED)
? bt->under : bt;
int esz = (bu && bu->sub)
? (int)bu->sub->size : 1;
if (bu && bu->kind == TY_STR) esz = 1;
cgexpr(c, idx, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
}
if (base && base->kind == N_IDENT) {
int boff = localfind(locals,
base->str);
int is_arr = bu &&
bu->kind == TY_ARRAY;
if (boff != 0) {
if (is_arr) {
ins2(c, A_LEAQ,
amem(D_BP, boff),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
}
} else if (let_islet(base->str)) {
if (is_arr) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_BX));
}
} else {
ins2(c, A_XORQ, areg(D_BX),
areg(D_BX));
}
ins2(c, A_ADDQ, areg(D_BX),
areg(D_AX));
break;
}
/* Complex base: eval to AX, swap into BX,
* then add the saved scaled idx. */
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, base, locals);
ins1(c, A_POPQ, areg(D_BX));
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
break;
}
/* Other shapes (& on a complex expr): silent drop,
* mirrors the pre-existing fallback. */
break;
}
cgexpr(c, n->lhs, locals);
switch (n->op) {
case TK_MINUS:
@@ -1158,21 +1335,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
label(c, e);
break;
}
case TK_AMP: {
/* address-of for an N_IDENT: local frame slot first,
* else a top-level mutable let (RIP-relative LEAQ).
* Anything else (e.g. & on an undefined name) silently
* drops, matching the pre-existing behaviour. */
if (n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
if (off != 0) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
} else if (let_islet(n->lhs->str)) {
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_AX));
}
}
case TK_AMP:
/* Handled in the pre-cgexpr early-exit above. */
break;
}
case TK_STAR: /* deref */
ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX));
break;
@@ -1734,16 +1899,62 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
}
/* float assignment to a local or top-level global. Globals
* route through LEAQ+indirect (no D_EXTERN SSE in w6a). */
* route through LEAQ+indirect (no D_EXTERN SSE in w6a).
* Compound (`acc += d` etc.) loads slot into X1, combines
* into X1 (Plan 9 syntax: OP src, dst), stores X1 back —
* w6a's ADDSD/SUBSD/MULSD/DIVSD are register-register only,
* so we can't use a direct mem-form like the integer ADDQ. */
if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) {
cgexpr(c, n->rhs, locals); /* X0 */
int op = op_for(n, A_MOVSD, A_MOVSS);
int mvop = op_for(n, A_MOVSD, A_MOVSS);
int addop = op_for(n, A_ADDSD, A_ADDSS);
int subop = op_for(n, A_SUBSD, A_SUBSS);
int mulop = op_for(n, A_MULSD, A_MULSS);
int divop = op_for(n, A_DIVSD, A_DIVSS);
int off = localfind(locals, n->lhs->str);
int isglobal = (off == 0) && let_islet(n->lhs->str);
if (off == 0 && !isglobal) break;
if (n->op == TK_ASSIGN) {
if (off != 0) {
ins2(c, op, areg(D_X0), amem(D_BP, off));
} else if (let_islet(n->lhs->str)) {
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX));
ins2(c, op, areg(D_X0), amem(D_CX, 0));
ins2(c, mvop, areg(D_X0), amem(D_BP, off));
} else {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
ins2(c, mvop, areg(D_X0), amem(D_CX, 0));
}
break;
}
/* Compound: X1 = load; X1 OP= X0; store X1. */
int fop = -1;
switch (n->op) {
case TK_PLUSEQ: fop = addop; break;
case TK_MINUSEQ: fop = subop; break;
case TK_STAREQ: fop = mulop; break;
case TK_SLASHEQ: fop = divop; break;
default: break;
}
if (off != 0) {
if (fop < 0) {
/* Unsupported compound (e.g., %= on float):
* fall back to plain store of rhs. */
ins2(c, mvop, areg(D_X0),
amem(D_BP, off));
break;
}
ins2(c, mvop, amem(D_BP, off), areg(D_X1));
ins2(c, fop, areg(D_X0), areg(D_X1));
ins2(c, mvop, areg(D_X1), amem(D_BP, off));
} else {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
if (fop < 0) {
ins2(c, mvop, areg(D_X0),
amem(D_CX, 0));
break;
}
ins2(c, mvop, amem(D_CX, 0), areg(D_X1));
ins2(c, fop, areg(D_X0), areg(D_X1));
ins2(c, mvop, areg(D_X1), amem(D_CX, 0));
}
break;
}
@@ -1824,13 +2035,30 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */
/* base address → BX */
if (base->kind == N_IDENT && is_arr) {
/* base address → BX. Top-level array → LEAQ
* name(SB); top-level ptr → MOVQ name(SB); locals
* route off BP. */
if (base->kind == N_IDENT) {
int off = localfind(locals, base->str);
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX));
} else if (base->kind == N_IDENT) {
int off = localfind(locals, base->str);
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
int isglobal = (off == 0) &&
let_islet(base->str);
if (isglobal && is_arr) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
} else if (isglobal) {
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_BX));
} else if (is_arr) {
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_BX));
}
} else {
cgexpr(c, base, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
@@ -2377,7 +2605,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_PUSHQ, areg(D_AX));
continue;
}
if (args[i]->kind == N_SLICE) {
if (!widen[i] && args[i]->kind == N_SLICE) {
Node *base = args[i]->lhs;
Node *lo = args[i]->rhs;
Node *hi = args[i]->cond;
@@ -2482,12 +2710,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
} else if (node_istaggedarg(args[i])) {
/* Tagged-return ABI: AX=tag, DX=val0[, CX=val1].
* Push high-to-low so pop drains tag first (into
* arg-reg[0]), then values into arg-reg[1..].
* Nullable (sz=8): AX holds the pointer, no
* value-word registers — push just AX. */
/* Tagged-return ABI: AX=tag, DX=val0,
* CX=val1, R8=val2. Push high-to-low so pop
* drains tag first (into arg-reg[0]), then
* values into arg-reg[1..]. Nullable (sz=8):
* AX holds the pointer, no value-word
* registers — push just AX. */
int sz = tagged_arg_size(args[i]->type);
if (sz > 24)
ins1(c, A_PUSHQ, areg(D_R8));
if (sz > 16)
ins1(c, A_PUSHQ, areg(D_CX));
if (sz > 8)
@@ -2667,9 +2898,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* Spill non-ident scrutinees (e.g. `match (foo()?)`) into
* a scratch slot so we can index out the tag/value. The
* call ABI for tagged returns is AX=tag, DX=value0,
* CX=value1 — copy each word into the slot. Nullable
* returns are single-word: AX is the pointer; spill
* only that. */
* CX=value1, R8=value2 — copy each word into the slot.
* Nullable returns are single-word: AX is the pointer;
* spill only that. */
sl_off = localoff(c, &locals, "@match_spill", slot_size,
cg_frame);
cgexpr(c, s, locals);
@@ -2680,6 +2911,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (slot_size > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, sl_off + 16));
if (slot_size > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, sl_off + 24));
}
}
char *end = mklabel(c, "match_end");
@@ -2758,8 +2992,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
amem(D_BP, voff));
}
} else {
int bsz = (bu && bu->kind == TY_STR)
? 16 : 8;
int bsz = 8;
if (bu && bu->kind == TY_STR) bsz = 16;
else if (bu && bu->kind == TY_SLICE) bsz = 24;
else if (bu) bsz = (int)bu->size;
if (bsz <= 0) bsz = 8;
/* local_alloc to dodge name-collision
* dedup — a 16B str bind shadowing an
* 8B outer would otherwise overflow
@@ -3352,6 +3589,18 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
}
}
/* Nested module-qualified field where the chain didn't fold to
* a known shape (typical when w6c runs on a single file with
* `use mod;` but no driver concatenation — the body's enum /
* struct hasn't been seen). Emit `MOVQ <leaf>(SB), AX` so the
* linker surfaces a clean undefined-symbol error on the leaf
* — mirrors the bare-N_IDENT unresolved fallback used by
* single-segment N_DOTs. Keeps cstage / wwstage byte-aligned
* on the cgen-match isolation probes. */
if (n->lhs && n->lhs->kind == N_DOT && n->str) {
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
break;
}
/* fall through to base evaluation; result placeholder */
cgexpr(c, n->lhs, locals);
dot_done:
@@ -3372,13 +3621,22 @@ 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);
cgexpr(c, n->rhs, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
/* base address into BX */
if (u->kind == TY_ARRAY) {
/* base address into BX. Top-level array → LEAQ
* name(SB); top-level ptr → MOVQ name(SB) (the symbol
* holds the pointer); locals route off BP. */
if (isglobal && u->kind == TY_ARRAY) {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_BX));
} else if (isglobal) {
ins2(c, A_MOVQ, masym(c, n->lhs->str),
areg(D_BX));
} else if (u->kind == TY_ARRAY) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX));
} else {
/* slice/str/ptr: ptr field is at off+0 */
@@ -3394,12 +3652,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
/* tagged element: load slot words into (AX=tag,
* DX=val0, CX=val1) — matches the tagged-return ABI
* so let-init / match / call-arg paths consume it
* without spilling. Nullable folded element is one
* word in AX (caller treats it as a pointer). */
* DX=val0, CX=val1, R8=val2) — matches the
* tagged-return ABI so let-init / match / call-arg
* paths consume it without spilling. Nullable folded
* element is one word in AX (caller treats it as a
* pointer). */
if (elem_tagged) {
int ssz = (int)esubu->size;
if (ssz > 24)
ins2(c, A_MOVQ, amem(D_BX, 24),
areg(D_R8));
if (ssz > 16)
ins2(c, A_MOVQ, amem(D_BX, 16),
areg(D_CX));
@@ -3446,6 +3708,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (elem_tagged) {
int ssz = (int)esubu->size;
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
if (ssz > 24)
ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8));
if (ssz > 16)
ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX));
if (ssz > 8)
@@ -3861,13 +4125,27 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
/* same tagged type: forward AX/DX/CX. */
cgexpr(c, n->lhs, *locals);
} else if (!istagged && !isstruct) {
/* str / scalar variant: synthesise the
* tag in AX and shuffle the value into
* DX[/CX]. Direct register path keeps
* the asm short — no scratch slot. */
/* str / slice / scalar variant: synthesise
* the tag in AX and shuffle the value into
* DX[/CX[/R8]]. Direct register path keeps
* the asm short — no scratch slot.
* Tagged-return ABI: AX=tag, DX=word0,
* CX=word1, R8=word2. Slice payload uses
* all four; str uses three; scalar uses
* two. */
int tag = cg_tag_for_variant(rt, vt);
cgexpr(c, n->lhs, *locals);
if (type_isstr(vt)) {
if (type_isslice(vt)) {
/* cgexpr leaves (AX=ptr, BX=len,
* CX=cap). Move into the return
* shuffle: DX=ptr, CX=len, R8=cap. */
ins2(c, A_MOVQ, areg(D_CX),
areg(D_R8));
ins2(c, A_MOVQ, areg(D_BX),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
} else if (type_isstr(vt)) {
ins2(c, A_MOVQ, areg(D_BX),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
@@ -3881,10 +4159,12 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
} else {
/* Struct variant or tagged-subset:
* materialise the widened value in a
* scratch slot, then load AX/DX/CX
* scratch slot, then load AX/DX/CX/R8
* from the slot. Struct literal: field
* stores; struct ident: word copy;
* tagged subset: copy + tag remap. */
* tagged subset: copy + tag remap.
* 4th word in R8 covers slice payload
* variants (slot >= 32B). */
int sz = (int)rt->size;
const char *scrn = mklabel(c, "retscr");
int scr = local_alloc(c, locals, scrn,
@@ -3905,6 +4185,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
ins2(c, A_MOVQ,
amem(D_BP, scr + 16),
areg(D_CX));
if (sz > 24)
ins2(c, A_MOVQ,
amem(D_BP, scr + 24),
areg(D_R8));
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
@@ -4337,6 +4621,33 @@ cgfn(Cg *c, FILE *out, Node *fn)
amem(D_BP, off));
argi++;
}
} else if (eightbytes > 1 && regs_left > 0 &&
(slice || is_str || is_struct || is_tagged)) {
/* Multi-word arg that partially fits in regs: caller
* filled (regs_left) registers greedily, the rest spilled
* to stack at positive BP offsets. Stitch a single local
* slot from both sources so the body sees a contiguous
* value. Mirrors the SysV greedy reg fill the caller
* does. */
int sz = slice ? 24 : (is_str ? 16 :
(is_struct ? (int)pu->size :
(is_tagged ? tagged_sz : 8)));
int off = localoff(c, &locals, p->str, sz, &frame);
extern int cg_stack_arg_cursor;
int k = 0;
for (; k < regs_left; k++, argi++)
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
amem(D_BP, off + k * 8));
for (; k < eightbytes; k++) {
int stack_off = 16 +
cg_stack_arg_cursor * 8;
cg_stack_arg_cursor++;
ins2(c, A_MOVQ, amem(D_BP, stack_off),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k * 8));
}
} else {
/* stack-spilled. Access in place via positive BP offset. */
static int stack_arg_off;
@@ -4501,11 +4812,14 @@ emit_lets(Cg *c, FILE *out, Node *file)
continue;
}
/* Otherwise: zero-init. str accepts nil / ""; struct
* accepts no rhs at all; slice accepts nil. */
* accepts no rhs at all; slice accepts nil; array accepts
* no rhs (literal-array init isn't wired). */
if (r != NULL) {
int is_struct = let_isstruct(d->type);
int is_array = let_isarray(d->type);
int empty_str = (r->kind == N_STRLIT && r->strlen == 0);
if (is_struct) continue;
if (is_array) continue;
if (r->kind != N_NIL && !empty_str) continue;
}
emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz);

View File

@@ -61,11 +61,12 @@ the shape so a regression is easy to recognise.
emit both halves of the str. `vstr` still does manual
`p.text = s` (semantically equivalent, no longer required).
6. **`(slice | E)` tagged-union returns drop `slice.len`.** Still
present. The wwstage return convention is AX=tag, DX=payload1,
CX=payload2 — a 24-byte slice header doesn't fit. Inline the
slice-building loop into the caller. See `eval`'s argument-eval
inline.
6. **(retired) `(slice | E)` tagged-union returns drop `slice.len`.**
The return ABI now uses 4 regs (AX=tag, DX=ptr, CX=len, R8=cap),
covering slice-payload variants up to 32B. The inline args-eval
in `eval` is still there but no longer required — `apply` could
factor it back out into a `(slice | rterror)` helper. Left as a
readability cleanup; not a correctness fix.
7. **(retired) `xs[i].kind` drops the trailing field load.** cgdot
now handles N_INDEX bases. Builtins are back to

View File

@@ -50,7 +50,7 @@ fn faili(name: str, why: str, got: i64) void = {
os.write(2, ": ".ptr, 2u64);
os.write(2, why.ptr, why.len: u64);
os.write(2, " got=".ptr, 5u64);
let s: str = strconv.i64tos(got, strconv.DEC);
let s: str = strconv.i64tos(got, strconv.base.DEC);
os.write(2, s.ptr, s.len: u64);
os.write(2, "\n".ptr, 1u64);
nfail += 1;
@@ -396,7 +396,7 @@ export fn main() i32 = {
// ---- summary ----
if (nfail == 0) {
os.write(1, "\nlisp_test: ".ptr, 12u64);
let ts: str = strconv.i64tos(ntotal: i64, strconv.DEC);
let ts: str = strconv.i64tos(ntotal: i64, strconv.base.DEC);
os.write(1, ts.ptr, ts.len: u64);
os.write(1, "/".ptr, 1u64);
os.write(1, ts.ptr, ts.len: u64);
@@ -404,10 +404,10 @@ export fn main() i32 = {
return 0;
};
os.write(2, "\nlisp_test: ".ptr, 12u64);
let fs: str = strconv.i64tos(nfail: i64, strconv.DEC);
let fs: str = strconv.i64tos(nfail: i64, strconv.base.DEC);
os.write(2, fs.ptr, fs.len: u64);
os.write(2, " of ".ptr, 4u64);
let ts: str = strconv.i64tos(ntotal: i64, strconv.DEC);
let ts: str = strconv.i64tos(ntotal: i64, strconv.base.DEC);
os.write(2, ts.ptr, ts.len: u64);
os.write(2, " failed\n".ptr, 8u64);
return 1;

View File

@@ -655,7 +655,7 @@ fn next(L: *lexer) (i32 | parserr | eof) = {
// list parse_expr will reject it; here we just tag it.
if (a.len == 1 && a[0] == '.': u8) { L.curkind = tkind.DOT; return 0; };
if (allnum(a)) {
let r = strconv.stoi64(a, strconv.DEC);
let r = strconv.stoi64(a, strconv.base.DEC);
match (r) {
case let v: i64 => {
L.curkind = tkind.INT;
@@ -1491,7 +1491,7 @@ fn obuf_puts(s: str) void = {
};
fn obuf_putint(v: i64) void = {
let s: str = strconv.i64tos(v, strconv.DEC);
let s: str = strconv.i64tos(v, strconv.base.DEC);
let i: i32 = 0;
for (i < s.len) { obuf_putc(s.ptr[i]); i += 1; };
};

View File

@@ -13,29 +13,38 @@ Signatures mirror Hare too, modulo:
- Tagged-union returns are spelled with the ww `!` error tag where
Hare uses `!void` / `!T`, and indices use the underlying length
type (`i32` today, since `str.len: i32`). Example:
`strconv.stoi64(s: str, b: i32) (i64 | invalid | overflow)`
same shape as Hare's; the base parameter is plain `i32` rather
than a `base` enum because cross-module `mod.enumtype.VALUE`
chains miscompile in the cstage cgen. `strconv` exports
`def DEC: i32 = 10;` etc. so callers say `strconv.DEC` and the
Sdef path lowers to an immediate.
`strconv.stoi64(s: str, b: strconv.base) (i64 | invalid | overflow)`
same shape as Hare's. The base parameter is the named enum
`strconv.base` (Hare uses `enum uint`; we pick `enum i32` to
match the index type).
- Owned-`str` returns. Where Hare returns `const str` into a
thread-local static buffer (`strconv.i64tos`, `strings.dup`,
`strings.concat`), ww allocates per call and the caller frees
via `os.free(r.ptr, r.len: u64)`. Mutating a module-level `*u8`
doesn't yet round-trip through the wwstage cgen, so the static-
buffer shape isn't expressible today.
- Static-buffer `str` returns where Hare uses them. `strconv.*tos`
returns a `const str` view into a module-level buffer that is
overwritten on the next call to the same function. Callers that
need the bytes to outlive the next call duplicate via
[[strings.dup]]. Functions that genuinely allocate a fresh
buffer (`strings.dup`, `strings.concat`) still return an owned
`str` that callers free via `os.free(r.ptr, r.len: u64)`.
- Variadic ABI doesn't land yet, so callers that Hare writes as
`fmt::println(42)` are spelled `fmt.println(strconv.i64tos(42,
strconv.DEC))` for now. `lib/fmt` is intentionally print-string-
only — no `printf`-family.
- Call-site variadic sugar (`fmt::println(42)`) doesn't land yet.
The receive side does — `fmt.formattable` is a tagged union of
the printable scalar types, and `fmt.printv` / `fmt.printlnv`
take an explicit `[]formattable` slice. Until the call-site
gather is implemented, callers either hand-build the slice:
let args: [2]fmt.formattable;
args[0] = "count: ": fmt.formattable;
args[1] = 42i64: fmt.formattable;
fmt.printlnv(args[0:2]);
or compose to a single str via strconv.i64tos + strings.concat:
fmt.println(strconv.i64tos(42, strconv.base.DEC));
`lib/fmt` is intentionally print-string-only — no `printf`-family.
- `(str | rune)`-style sum-typed parameters are split into typed
pairs (`strings.indexbyte` for the byte case, `strings.index` for
the slice case, etc.). The Hare public name `byteindex` will come
back once the sum-typed parameter ABI lands.
- `(T | U)` sum-typed parameters dispatch via `match` inside the
callee. `strings.byteindex(haystack: str, needle: (str | rune))`,
`bytes.index(s: []u8, needle: (u8 | []u8))`, and `rbyteindex`/
`rindex` follow Hare's shape directly. The rune-indexed
`strings.index` (rune-wise position) isn't shipped yet — we don't
have UTF-8 rune iteration in the language stack.
Don't ship a richer surface than Hare has. A documented subset is
fine; an extension, rename, or convenience-wrapper is not — callers

View File

@@ -10,9 +10,12 @@ export fn equal(a: []u8, b: []u8) bool = {
return i == b.len;
};
// indexbyte — first index of byte `c` in `s`. Hare-shaped optional:
// (i32 | void). void variant indicates "not found".
export fn indexbyte(s: []u8, c: u8) (i32 | void) = {
// index — first index of `needle` in `s`. Mirrors Hare's bytes::index:
// `u8` needle scans for the byte, `[]u8` needle scans for the
// substring. Returns void if absent.
export fn index(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
match (needle) {
case let c: u8 => {
let i: i32 = 0;
for (i < s.len) {
if (s[i] == c) { return i; };
@@ -20,22 +23,7 @@ export fn indexbyte(s: []u8, c: u8) (i32 | void) = {
};
return;
};
// rindexbyte — last index of byte `c` in `s`. Mirrors Hare's
// bytes::rindex for the byte case.
export fn rindexbyte(s: []u8, c: u8) (i32 | void) = {
let i: i32 = s.len - 1;
for (i >= 0) {
if (s[i] == c) { return i; };
i -= 1;
};
return;
};
// index — first index of `sub` in `s`. Mirrors Hare's bytes::index
// (the []u8 needle variant; the u8 needle stays as indexbyte until we
// have union-arg dispatch). Empty `sub` matches at 0.
export fn index(s: []u8, sub: []u8) (i32 | void) = {
case let sub: []u8 => {
if (sub.len == 0) { return 0; };
if (sub.len > s.len) { return; };
let last: i32 = s.len - sub.len;
@@ -52,10 +40,23 @@ export fn index(s: []u8, sub: []u8) (i32 | void) = {
};
return;
};
};
return;
};
// rindex — last index of `sub` in `s`. Mirrors Hare's bytes::rindex
// for the slice case.
export fn rindex(s: []u8, sub: []u8) (i32 | void) = {
// rindex — last index of `needle` in `s`. Mirrors Hare's bytes::rindex.
// Empty []u8 needle matches at s.len.
export fn rindex(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
match (needle) {
case let c: u8 => {
let i: i32 = s.len - 1;
for (i >= 0) {
if (s[i] == c) { return i; };
i -= 1;
};
return;
};
case let sub: []u8 => {
if (sub.len == 0) { return s.len; };
if (sub.len > s.len) { return; };
let i: i32 = s.len - sub.len;
@@ -71,6 +72,9 @@ export fn rindex(s: []u8, sub: []u8) (i32 | void) = {
};
return;
};
};
return;
};
// contains — true iff `sub` appears in `s`. Mirrors Hare's
// bytes::contains for the slice case.

View File

@@ -1,16 +1,82 @@
// fmt — minimal formatting writers. All output goes through os.write
// to a file descriptor. No printf-family yet — we don't have varargs
// in the language proper — so callers compose with strconv.i64tos /
// strings.concat to build the message and then call print / println.
// Hare's `fmt::println(42)` becomes `fmt.println(strconv.i64tos(42,
// strconv.DEC))`.
// fmt — formatting writers. Goes through os.write to a file
// descriptor. Hare's variadic call-site sugar (`fmt::println(42)`
// gathering args into a `[]formattable`) isn't wired yet; until then,
// callers either:
//
// 1. Hand-build the slice:
// let args: [2]formattable;
// args[0] = 42i64: formattable;
// args[1] = " hi": formattable;
// fmt.print(args[0:2]);
//
// 2. Compose a single str via strconv.i64tos / strings.concat:
// fmt.println(strconv.i64tos(42, strconv.base.DEC));
//
// `print(s: str)` keeps the single-string form for the common case.
use os;
use strconv;
use strings;
// formattable — tagged union of types fmt can render. Mirrors
// Hare's `fmt::formattable = (...types::numeric | uintptr | str |
// rune | bool | nullable *opaque | void)`, narrowed to the set ww
// actually has codegen for. Slot size is 24B (8 tag + 16 str
// payload).
export type formattable = (i64 | str | bool | rune);
// vprint — write the formatted form of each element of `args` to
// `fd`. Returns total bytes written or the first negative os.write
// result.
fn vprint(fd: i32, args: []formattable) i64 = {
let total: i64 = 0;
let i: i32 = 0;
for (i < args.len) {
match (args[i]) {
case let n: i64 => {
let s: str = strconv.i64tos(n, strconv.base.DEC);
let r: i64 = os.write(fd, s.ptr, s.len: u64);
if (r < 0) { return r; };
total += r;
};
case let s: str => {
let r: i64 = os.write(fd, s.ptr, s.len: u64);
if (r < 0) { return r; };
total += r;
};
case let b: bool => {
let s: str = "false";
if (b) { s = "true"; };
let r: i64 = os.write(fd, s.ptr, s.len: u64);
if (r < 0) { return r; };
total += r;
};
case let r: rune => {
let buf: [4]u8;
buf[0] = r: u8;
let n: i64 = os.write(fd, &buf[0], 1u64);
if (n < 0) { return n; };
total += n;
};
};
i += 1;
};
return total;
};
// print(s: str) — single-string form for the common case. The
// variadic-style `print(args: []formattable)` lives as `printv`
// until call-site sugar lands.
export fn print(s: str) i64 = {
return os.write(1, s.ptr, s.len: u64);
};
// printv — Hare-shaped `print(args: formattable...)` modulo the
// call-site sugar. Callers pass an explicit `[]formattable` slice.
export fn printv(args: []formattable) i64 = {
return vprint(1, args);
};
export fn println(s: str) i64 = {
let n: i64 = os.write(1, s.ptr, s.len: u64);
if (n < 0) { return n; };
@@ -19,6 +85,15 @@ export fn println(s: str) i64 = {
return n + m;
};
// printlnv — like printv but adds a trailing newline.
export fn printlnv(args: []formattable) i64 = {
let n: i64 = vprint(1, args);
if (n < 0) { return n; };
let m: i64 = os.write(1, "\n".ptr, 1u64);
if (m < 0) { return m; };
return n + m;
};
// errorln — write a message to stderr with a trailing newline.
export fn errorln(s: str) i64 = {
let n: i64 = os.write(2, s.ptr, s.len: u64);

View File

@@ -1,11 +1,10 @@
// strconv — number↔string conversions.
//
// Mirrors Hare's strconv:: surface. The *tos functions return a fresh
// owned `str`; release via os.free(r.ptr, r.len: u64) when done.
// Hare returns `const str` into a static buffer; ww allocates per
// call because the wwstage cgen doesn't currently support mutating a
// module-level `*u8` (so a lazy-init shared buffer isn't expressible
// today). Graduate to the static-buffer shape once that lands.
// Mirrors Hare's strconv:: surface. The *tos functions return a
// `const str` view into a module-level buffer that is overwritten on
// the next call to the same function; callers must copy the bytes if
// they need to outlive the next invocation. See [[strings.dup]] to
// duplicate. Matches Hare's strconv::*tos semantics.
use os;
use strings;
@@ -22,43 +21,44 @@ export type overflow = !void;
// error — any error from a strconv call. Mirrors Hare's strconv::error.
export type error = !(invalid | overflow);
// base — numeric base for parsing/formatting. Plain i32 (not a named
// enum) because cross-module `strconv.base.DEC` chains miscompile in
// the cstage cgen — it emits a memory load through `base(SB)` rather
// than inlining the enum value. Hare names them as `strconv::base`
// enum values; we expose them as module-level `def`s so callers say
// `strconv.DEC` and the cgen inlines the immediate.
// base — numeric base for parsing/formatting. Mirrors Hare's
// `strconv::base` (Hare uses `enum uint`; we pick `enum i32` since
// the underlying parse/format loops index with i32).
//
// HEX is HEX_UPPER; HEX_LOWER is a separate pseudo-base that produces
// lowercase a-f digits.
export def DEFAULT: i32 = 0;
export def BIN: i32 = 2;
export def OCT: i32 = 8;
export def DEC: i32 = 10;
export def HEX_UPPER: i32 = 16;
export def HEX: i32 = 16;
export def HEX_LOWER: i32 = 17;
// HEX is an alias for HEX_UPPER; HEX_LOWER is a pseudo-base that
// produces lowercase a-f digits.
export type base = enum i32 {
DEFAULT = 0,
BIN = 2,
OCT = 8,
DEC = 10,
HEX_UPPER = 16,
HEX = 16,
HEX_LOWER = 17,
};
fn basenum(b: i32) i64 = {
if (b == BIN) { return 2; };
if (b == OCT) { return 8; };
if (b == HEX) { return 16; };
if (b == HEX_UPPER) { return 16; };
if (b == HEX_LOWER) { return 16; };
fn basenum(b: base) i64 = {
if (b == base.BIN) { return 2; };
if (b == base.OCT) { return 8; };
if (b == base.HEX) { return 16; };
if (b == base.HEX_UPPER) { return 16; };
if (b == base.HEX_LOWER) { return 16; };
return 10; // DEC and DEFAULT
};
fn basedigit(d: i64, b: i32) u8 = {
fn basedigit(d: i64, b: base) u8 = {
if (d < 10) { return (d + 48): u8; };
let off: i64 = d - 10;
if (b == HEX_LOWER) { return (off + 97): u8; };
if (b == base.HEX_LOWER) { return (off + 97): u8; };
return (off + 65): u8;
};
// u64tos — convert v to a base-b numeric string. Returns owned str;
// release via os.free(r.ptr, r.len: u64). Mirrors Hare's
// strconv::u64tos (Hare returns const str into a static buffer).
export fn u64tos(v: u64, b: i32) str = {
// u64tos — convert v to a base-b numeric string. Returns a view into
// `u64tos_buf` which is overwritten on the next call. Matches Hare's
// strconv::u64tos.
let u64tos_buf: [65]u8;
export fn u64tos(v: u64, b: base) str = {
let nb: u64 = basenum(b): u64;
let tmp: [65]u8;
let i: i32 = 0;
@@ -70,22 +70,25 @@ export fn u64tos(v: u64, b: i32) str = {
n = n / nb;
i += 1;
};
let buf: *u8 = os.alloc(i: u64): *u8;
let out: i32 = 0;
for (i > 0) {
i -= 1;
buf[out] = tmp[i];
u64tos_buf[out] = tmp[i];
out += 1;
};
let r: str;
r.ptr = buf;
r.ptr = &u64tos_buf[0];
r.len = out;
return r;
};
// i64tos — convert v to a base-b numeric string. Returns owned str;
// release via os.free. Mirrors Hare's strconv::i64tos.
export fn i64tos(v: i64, b: i32) str = {
// i64tos — convert v to a base-b numeric string. Returns a view into
// `i64tos_buf` which is overwritten on the next call. Independent
// buffer from u64tos so i64tos's own call to u64tos doesn't clobber
// the in-flight result. Matches Hare's strconv::i64tos.
let i64tos_buf: [66]u8;
export fn i64tos(v: i64, b: base) str = {
let neg: bool = false;
let n: i64 = v;
if (n < 0) { neg = true; n = -n; };
@@ -99,37 +102,33 @@ export fn i64tos(v: i64, b: i32) str = {
n = n / nb;
i += 1;
};
let extra: i32 = 0;
if (neg) { extra = 1; };
let total: i32 = i + extra;
let buf: *u8 = os.alloc(total: u64): *u8;
let out: i32 = 0;
if (neg) { buf[out] = 45u8; out += 1; }; // '-'
if (neg) { i64tos_buf[out] = 45u8; out += 1; }; // '-'
for (i > 0) {
i -= 1;
buf[out] = tmp[i];
i64tos_buf[out] = tmp[i];
out += 1;
};
let r: str;
r.ptr = buf;
r.ptr = &i64tos_buf[0];
r.len = out;
return r;
};
export fn i32tos(v: i32, b: i32) str = { return i64tos(v: i64, b); };
export fn i16tos(v: i16, b: i32) str = { return i64tos(v: i64, b); };
export fn i8tos(v: i8, b: i32) str = { return i64tos(v: i64, b); };
export fn i32tos(v: i32, b: base) str = { return i64tos(v: i64, b); };
export fn i16tos(v: i16, b: base) str = { return i64tos(v: i64, b); };
export fn i8tos(v: i8, b: base) str = { return i64tos(v: i64, b); };
export fn u32tos(v: u32, b: i32) str = { return u64tos(v: u64, b); };
export fn u16tos(v: u16, b: i32) str = { return u64tos(v: u64, b); };
export fn u8tos(v: u8, b: i32) str = { return u64tos(v: u64, b); };
export fn u32tos(v: u32, b: base) str = { return u64tos(v: u64, b); };
export fn u16tos(v: u16, b: base) str = { return u64tos(v: u64, b); };
export fn u8tos(v: u8, b: base) str = { return u64tos(v: u64, b); };
// digval — value of digit byte `c` under base `b`, or -1 if not a
// valid digit. Letters are accepted case-insensitively under HEX /
// HEX_UPPER; only lowercase under HEX_LOWER.
fn digval(c: u8, b: i32) i32 = {
fn digval(c: u8, b: base) i32 = {
if (c >= 48u8) { if (c <= 57u8) { return (c - 48u8): i32; }; };
if (b == HEX_LOWER) {
if (b == base.HEX_LOWER) {
if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; };
return -1;
};
@@ -142,7 +141,7 @@ fn digval(c: u8, b: i32) i32 = {
// No locale, no whitespace, no underscores: optional leading '-' then
// digits. Returns invalid with the offending index or overflow on
// out-of-range.
export fn stoi64(s: str, b: i32) (i64 | invalid | overflow) = {
export fn stoi64(s: str, b: base) (i64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let i: i32 = 0;
let neg: bool = false;
@@ -163,7 +162,7 @@ export fn stoi64(s: str, b: i32) (i64 | invalid | overflow) = {
};
// stou64 — parse unsigned base-b number. Mirrors Hare's strconv::stou64.
export fn stou64(s: str, b: i32) (u64 | invalid | overflow) = {
export fn stou64(s: str, b: base) (u64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let nb: u64 = basenum(b): u64;
let v: u64 = 0u64;
@@ -179,7 +178,7 @@ export fn stou64(s: str, b: i32) (u64 | invalid | overflow) = {
return v;
};
export fn stoi32(s: str, b: i32) (i32 | invalid | overflow) = {
export fn stoi32(s: str, b: base) (i32 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
@@ -193,7 +192,7 @@ export fn stoi32(s: str, b: i32) (i32 | invalid | overflow) = {
return 0: invalid; // unreachable; appeases the path-cov checker
};
export fn stoi16(s: str, b: i32) (i16 | invalid | overflow) = {
export fn stoi16(s: str, b: base) (i16 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
@@ -207,7 +206,7 @@ export fn stoi16(s: str, b: i32) (i16 | invalid | overflow) = {
return 0: invalid;
};
export fn stoi8(s: str, b: i32) (i8 | invalid | overflow) = {
export fn stoi8(s: str, b: base) (i8 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
@@ -221,7 +220,7 @@ export fn stoi8(s: str, b: i32) (i8 | invalid | overflow) = {
return 0: invalid;
};
export fn stou32(s: str, b: i32) (u32 | invalid | overflow) = {
export fn stou32(s: str, b: base) (u32 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
@@ -234,7 +233,7 @@ export fn stou32(s: str, b: i32) (u32 | invalid | overflow) = {
return 0: invalid;
};
export fn stou16(s: str, b: i32) (u16 | invalid | overflow) = {
export fn stou16(s: str, b: base) (u16 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
@@ -247,7 +246,7 @@ export fn stou16(s: str, b: i32) (u16 | invalid | overflow) = {
return 0: invalid;
};
export fn stou8(s: str, b: i32) (u8 | invalid | overflow) = {
export fn stou8(s: str, b: base) (u8 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
@@ -283,13 +282,14 @@ export fn stou8(s: str, b: i32) (u8 | invalid | overflow) = {
// the ww-side wwdump currently skips TK_FLOAT.fval while the C side
// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses:
// build f64 constants via int-to-f64 casts.
let f64tos_buf: [64]u8;
export fn f64tos(v: f64) str = {
let tmp: [64]u8;
let out: i32 = 0;
let f: f64 = v;
let zero: f64 = 0: f64;
if (f < zero) {
tmp[out] = 45u8; // '-'
f64tos_buf[out] = 45u8; // '-'
out += 1;
f = -f;
};
@@ -299,12 +299,9 @@ export fn f64tos(v: f64) str = {
if (f >= cap) {
let s: str = "huge";
let k: i32 = 0;
for (k < s.len) { tmp[out] = s[k]; out += 1; k += 1; };
let buf: *u8 = os.alloc(out: u64): *u8;
let q: i32 = 0;
for (q < out) { buf[q] = tmp[q]; q += 1; };
for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; };
let r: str;
r.ptr = buf;
r.ptr = &f64tos_buf[0];
r.len = out;
return r;
};
@@ -323,32 +320,27 @@ export fn f64tos(v: f64) str = {
ip += 1;
fp = 0;
};
let intstr: str = i64tos(ip, DEC);
let intstr: str = i64tos(ip, base.DEC);
let k: i32 = 0;
for (k < intstr.len) { tmp[out] = intstr.ptr[k]; out += 1; k += 1; };
os.free(intstr.ptr: *void, intstr.len: u64);
for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; };
if (fp != 0) {
tmp[out] = 46u8; // '.'
f64tos_buf[out] = 46u8; // '.'
out += 1;
let fracstr: str = u64tos(fp: u64, DEC);
let fracstr: str = u64tos(fp: u64, base.DEC);
// Pad fractional to 6 digits with leading zeros (e.g. 0.05 →
// fp=50000, fracstr="50000", pad one '0' before).
let z: i32 = 6 - fracstr.len;
for (z > 0) { tmp[out] = 48u8; out += 1; z -= 1; };
for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; };
k = 0;
for (k < fracstr.len) { tmp[out] = fracstr.ptr[k]; out += 1; k += 1; };
os.free(fracstr.ptr: *void, fracstr.len: u64);
for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; };
// Trim trailing zeros in the fractional part.
for (out > 0) {
if (tmp[out - 1] != 48u8) { break; };
if (f64tos_buf[out - 1] != 48u8) { break; };
out -= 1;
};
};
let buf: *u8 = os.alloc(out: u64): *u8;
let q: i32 = 0;
for (q < out) { buf[q] = tmp[q]; q += 1; };
let r: str;
r.ptr = buf;
r.ptr = &f64tos_buf[0];
r.len = out;
return r;
};

View File

@@ -39,12 +39,14 @@ export fn hassuffix(s: str, suf: str) bool = {
return true;
};
// indexbyte — first byte position of byte `c` in `s`. Mirrors
// Hare's strings::byteindex when the needle is a single ASCII rune,
// renamed to match bytes.indexbyte and to disambiguate from Hare's
// `byteindex(haystack, needle: (str | rune))` which we don't have
// the union-arg ABI for yet.
export fn indexbyte(s: str, c: u8) (i32 | void) = {
// byteindex — first byte position of `needle` in `s`. Mirrors Hare's
// strings::byteindex: a single-codepoint rune scans for the byte that
// encodes it (ASCII only here — multi-byte UTF-8 awaits utf8 encode),
// a str needle scans for the substring. Returns void if absent.
export fn byteindex(s: str, needle: (str | rune)) (i32 | void) = {
match (needle) {
case let r: rune => {
let c: u8 = r: u8;
let i: i32 = 0;
for (i < s.len) {
if (s[i] == c) { return i; };
@@ -52,21 +54,7 @@ export fn indexbyte(s: str, c: u8) (i32 | void) = {
};
return;
};
// rindexbyte — last byte position of byte `c` in `s`.
export fn rindexbyte(s: str, c: u8) (i32 | void) = {
let i: i32 = s.len - 1;
for (i >= 0) {
if (s[i] == c) { return i; };
i -= 1;
};
return;
};
// index — first index of `sub` in `s`. Naive scan; fine for short
// patterns and small strings, which dominate config and CLI parsing.
// Empty `sub` matches at 0.
export fn index(s: str, sub: str) (i32 | void) = {
case let sub: str => {
if (sub.len == 0) { return 0; };
if (sub.len > s.len) { return; };
let last: i32 = s.len - sub.len;
@@ -83,9 +71,14 @@ export fn index(s: str, sub: str) (i32 | void) = {
};
return;
};
};
return;
};
// contains — true iff `sub` appears in `s`. Mirrors Hare's
// strings::contains shape (byte-wise on the str-needle case).
export fn contains(s: str, sub: str) bool = {
let r: (i32 | void) = index(s, sub);
let r: (i32 | void) = byteindex(s, sub);
match (r) {
case let i: i32 => return true;
case void => return false;
@@ -129,9 +122,22 @@ export fn dup(s: str) str = {
return r;
};
// rindex — last index of `sub` in `s`. Mirrors Hare's strings::rindex
// (slice case). Empty `sub` matches at s.len.
export fn rindex(s: str, sub: str) (i32 | void) = {
// rbyteindex — last byte position of `needle` in `s`. Mirrors Hare's
// strings::rbyteindex. Rune needle scans for the byte that encodes it
// (ASCII only); str needle scans for the substring. Empty str needle
// matches at s.len.
export fn rbyteindex(s: str, needle: (str | rune)) (i32 | void) = {
match (needle) {
case let r: rune => {
let c: u8 = r: u8;
let i: i32 = s.len - 1;
for (i >= 0) {
if (s[i] == c) { return i; };
i -= 1;
};
return;
};
case let sub: str => {
if (sub.len == 0) { return s.len; };
if (sub.len > s.len) { return; };
let i: i32 = s.len - sub.len;
@@ -147,6 +153,9 @@ export fn rindex(s: str, sub: str) (i32 | void) = {
};
return;
};
};
return;
};
// sub — borrowed substring `s[start..end]`. Mirrors Hare's
// strings::sub. Caller must ensure 0 <= start <= end <= s.len; out-of-

View File

@@ -269,11 +269,11 @@ fn pr(fd: i32, n: *node, d: i32) void = {
if (n.kind == nkind.N_INTLIT) {
putc1(fd, 32u8);
let s: str = strconv.u64tos(n.uval, strconv.DEC);
let s: str = strconv.u64tos(n.uval, strconv.base.DEC);
os.write(fd, s.ptr, s.len: u64);
} else { if (n.kind == nkind.N_RUNELIT) {
putc1(fd, 32u8);
let s: str = strconv.u64tos(n.uval, strconv.DEC);
let s: str = strconv.u64tos(n.uval, strconv.base.DEC);
os.write(fd, s.ptr, s.len: u64);
} else { if (
n.kind == nkind.N_STRLIT ||

View File

@@ -382,10 +382,10 @@ export fn tokprint(fd: i32, t: *tok) void = {
fputsstr(fd, "<none>");
};
fputcbyte(fd, 58u8); // ':'
let ls: str = strconv.i64tos(t.line: i64, strconv.DEC);
let ls: str = strconv.i64tos(t.line: i64, strconv.base.DEC);
os.write(fd, ls.ptr, ls.len: u64);
fputcbyte(fd, 58u8);
let cs: str = strconv.i64tos(t.col: i64, strconv.DEC);
let cs: str = strconv.i64tos(t.col: i64, strconv.base.DEC);
os.write(fd, cs.ptr, cs.len: u64);
fputcbyte(fd, 32u8); // ' '
fputsstr(fd, tokname(t.kind));
@@ -401,11 +401,11 @@ export fn tokprint(fd: i32, t: *tok) void = {
fputq(fd, ttext.ptr, ttext.len);
} else { if (t.kind == tkind.TK_INT) {
fputcbyte(fd, 32u8);
let us: str = strconv.u64tos(t.uval, strconv.DEC);
let us: str = strconv.u64tos(t.uval, strconv.base.DEC);
os.write(fd, us.ptr, us.len: u64);
} else { if (t.kind == tkind.TK_RUNE) {
fputcbyte(fd, 32u8);
let us: str = strconv.u64tos(t.uval, strconv.DEC);
let us: str = strconv.u64tos(t.uval, strconv.base.DEC);
os.write(fd, us.ptr, us.len: u64);
};};};};};
// tkind.TK_FLOAT is intentionally not handled here — %g formatting

View File

@@ -27,5 +27,42 @@ Fixed (no workaround needed):
Wwstage parser + cgen are byte-identical to C cgen on these shapes.
See cmd/w6c/cgen.c N_RETURN/N_LET/N_DOT/N_MLET, lib/ww/parse/{stmt,
expr}.ww and selfhost/cmd/wcc/{cgenstmt,cgenexpr,cgenutil,cgendecl}.ww.
- f64 compound assigns (`acc += d`, also `-= *= /=`) on locals and
top-level lets. Both stages now load slot into X1, OP X0 into X1
(ADDSD/SUBSD/MULSD/DIVSD register-register), and store X1 back.
See cmd/w6c/cgen.c N_ASSIGN float-IDENT branch and
selfhost/cmd/wcc/cgenexpr.ww cgassign float local/global.
- Top-level `[N]T` arrays. The cstage cgen now emits a zero-init
DATAW slot and accesses go through `LEAQ name(SB)`; previously
the array was filtered out by `let_emit_size` and `arr[i]` fell
through to `LEAQ (BP), BX` (off-by-frame). `let_isarray` mirrors
the selfhost N_TARRAY path in `letemitsize`.
- `&arr[i]` (address-of an index). Both stages now compute base +
i*esz without a trailing dereference; the previous TK_AMP path
pre-evaluated the operand as if it were a value-load. Unblocks
Hare's `let s = string { data = &buf, ... }` static-buffer
shape. See cmd/w6c/cgen.c N_UN TK_AMP and
selfhost/cmd/wcc/cgenexpr.ww cgun TK_AMP.
- Tagged-union return ABI is now AX=tag, DX=word0, CX=word1,
R8=word2 (was AX/DX/CX, 3 words). Slice-payload variants
(`(slice | E)`, slot 32B) round-trip end-to-end. Every receive
site (let-init, match scrutinee spill, cgwidentaggedstore for
call-source, cgindex tagged-element load, pushargsrev tagged-
ident arg) reads the fourth word when slot size > 24. See
cmd/w6c/cgen.c N_RETURN / cg_widen_tagged_store and the
matching selfhost cgenstmt / cgenutil / cgenexpr branches.
- `expr: TaggedAlias` is a widening, not a re-interpret.
cgwidentaggedstore peels an `N_CAST` whose destination IS the
union itself, so cgexpr's natural register shape (str: AX=ptr,
BX=len) is consumed by the str-payload branch instead of being
misread as a tagged AX/DX/CX triple. Inner casts to a concrete
variant (`7: i32` in `(i64 | i32)`) keep their type so the
scalar branch picks the right variant tag.
- `[N]Alias` arrays read element size through `slotsize` so an
aliased tagged variant (e.g. `[3]fmt.formattable`) takes its
full 24B stride per element, not the 8B fallback.
selfhost/cmd/wcc/cgenutil.ww slotsize N_TARRAY follows
`aliaslookup` on a TNAME element, and `aliaslookup` strips a
`pkg.` prefix so cross-module references resolve.
If a port "should work" but the binary is wrong, suspect these first.

File diff suppressed because it is too large Load Diff

View File

@@ -77,6 +77,26 @@ fn aliaslookup(c: *cgen, name: str) *node = {
if (streq(an, name)) { return a.target; };
a = a.aanext;
};
// Module-qualified form: `pkg.alias` → try the bare leaf so a
// cross-module reference resolves the same way bare access does
// after driver concatenation. Mirrors the check.c module-
// qualified type resolution.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == 46u8) { // '.'
let leaf: str;
leaf.ptr = name.ptr + ((i + 1): u64);
leaf.len = name.len - (i + 1);
let b: *aliasent = c.aliases;
for (b != nil) {
if (streq(b.aname, leaf)) { return b.target; };
b = b.aanext;
};
i = -1;
} else {
i -= 1;
};
};
return nil;
};
@@ -472,12 +492,12 @@ fn localfind(c: *cgen, name: str) i32 = {
fn emitline(s: str) void = { os.write(1, s.ptr, s.len: u64); };
fn emitint(v: i64) void = {
let s: str = strconv.i64tos(v, strconv.DEC);
let s: str = strconv.i64tos(v, strconv.base.DEC);
os.write(1, s.ptr, s.len: u64);
};
fn emituint(v: u64) void = {
let s: str = strconv.u64tos(v, strconv.DEC);
let s: str = strconv.u64tos(v, strconv.base.DEC);
os.write(1, s.ptr, s.len: u64);
};
@@ -515,7 +535,7 @@ fn mklabel(c: *cgen, prefix: str) str = {
i += 1; j += 1;
};
buf[i] = 95u8; i += 1; // '_'
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.DEC);
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC);
let n: i32 = ns.len;
let dk: i32 = 0;
for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; };
@@ -554,7 +574,7 @@ fn mkscratchname(c: *cgen, prefix: str) str = {
i += 1; j += 1;
};
buf[i] = 95u8; i += 1; // '_'
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.DEC);
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC);
let n: i32 = ns.len;
let dk: i32 = 0;
for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; };
@@ -591,7 +611,7 @@ fn internstrlit(c: *cgen, bytes: str) str = {
// New label "_S_<seq>".
let buf: [32]u8;
buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_"
let ns: str = strconv.i64tos(c.strlitseq: i64, strconv.DEC);
let ns: str = strconv.i64tos(c.strlitseq: i64, strconv.base.DEC);
let n: i32 = ns.len;
let dk: i32 = 0;
for (dk < n) { buf[3 + dk] = ns.ptr[dk]; dk += 1; };

View File

@@ -151,9 +151,23 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
let pat: *node = n.lhs;
if (pat != nil) {
if (isstrtype(c, pat)) { total += 16; }
else { total += 8; };
else { if (isslicetype(c, pat)) { total += 24; }
else { total += 8; }; };
};
};
// Match arms get a fresh local scope at emission time
// (cgmatch saves c.locals before each arm and restores
// after). scanlocals must mirror that: walk the arm
// body with a saved/restored seenmark set so two arms
// declaring the same name each get their own slot,
// matching the per-arm frame growth the emit phase
// produces.
if (n.body != nil) {
let saved: *local = c.locals;
total += scanlocals(c, n.body);
c.locals = saved;
};
return total;
};
// Tagged-arr/slice index store needs a 24B scratch slot
// (`@tagscr`) for cgwidentaggedstore to materialise the source
@@ -346,10 +360,36 @@ fn cgfnparams(c: *cgen, params: *node) void = {
idx += 1;
w += 1;
};
} else { if (idx < 6 && nw > 1) {
// Partial fit: fill remaining regs, then read
// the tail from positive BP offsets. Mirrors
// the caller's greedy reg fill in pushargsrev.
let off: i32 = localadd(c, nm, slot, p.lhs);
let regs_left: i32 = 6 - idx;
let w: i32 = 0;
for (w < regs_left) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
for (w < nw) {
emitline("\tMOVQ\t");
emitoff((16 + stkcursor*8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
stkcursor += 1;
w += 1;
};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += nw;
};
};};
} else { if (isslicetype(c, p.lhs)) {
if (idx + 3 <= 6) {
let off: i32 = localadd(c, nm, 24, p.lhs);
@@ -430,7 +470,12 @@ fn cgfn(c: *cgen, fn_: *node) void = {
};
a = a.next;
};
if (!isffi) {
// `main` is the linker entry-point convention; even
// when not marked `export`, it must keep its bare
// name so w6l's _start can resolve `CALL main(SB)`.
// Mirror of cmd/w6c/cgen.c collectmods exemption.
let isentry: bool = streq(fn_.str, "main");
if (!isffi && !isentry) {
os.write(1, fn_.module.ptr, fn_.module.len: u64);
os.write(1, ".".ptr, 1u64);
};

View File

@@ -534,7 +534,7 @@ fn cgindex(c: *cgen, n: *node) void = {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
if (baselocal != nil) {
esz = elemsizeof(baselocal.tnode);
esz = elemsizeofc(c, baselocal.tnode);
signed_elem = elemissigned(baselocal.tnode);
} else {
let tn: *node = letvartnode(c, bn);
@@ -542,13 +542,13 @@ fn cgindex(c: *cgen, n: *node) void = {
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeof(tn);
esz = elemsizeofc(c, tn);
signed_elem = elemissigned(tn);
};
if (tn.kind == nkind.N_TPTR) {
isglobalptr = true;
globalname = bn;
esz = elemsizeof(tn);
esz = elemsizeofc(c, tn);
signed_elem = elemissigned(tn);
};
};
@@ -613,6 +613,9 @@ fn cgindex(c: *cgen, n: *node) void = {
};
emitline("\tADDQ\tAX, BX\n");
if (elem_tagged) {
if (elem_slot_sz > 24) {
emitline("\tMOVQ\t24(BX), R8\n");
};
if (elem_slot_sz > 16) {
emitline("\tMOVQ\t16(BX), CX\n");
};
@@ -651,6 +654,9 @@ fn cgindex(c: *cgen, n: *node) void = {
};
emitline("\tADDQ\tAX, BX\n");
if (elem_tagged) {
if (elem_slot_sz > 24) {
emitline("\tMOVQ\t24(BX), R8\n");
};
if (elem_slot_sz > 16) {
emitline("\tMOVQ\t16(BX), CX\n");
};
@@ -809,7 +815,7 @@ fn cgmatch(c: *cgen, n: *node) void = {
};
} else {
// Non-ident scrutinee (call result, arr[i], ?, etc.).
// Spill into a 24B `@match_spill` scratch slot and
// Spill into an `@match_spill` scratch slot and
// dispatch off it. Tagged returns (N_CALL) follow the
// AX:DX:CX convention; tagged-element loads (N_INDEX)
// after the cgindex fix produce the same triple.
@@ -864,6 +870,16 @@ fn cgmatch(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tCX, ");
emitoff((scrutoff + 16): i64);
emitline("(BP)\n");
// R8 carries the 4th return word when the
// scrutinee's tagged union has a slice-payload
// variant (slot 32B). Harmless for narrower
// returns — R8 is callee-clobbered either way.
let ssz: i32 = slotsize(c, scrutt);
if (ssz > 24) {
emitline("\tMOVQ\tR8, ");
emitoff((scrutoff + 24): i64);
emitline("(BP)\n");
};
};
};
};
@@ -949,25 +965,22 @@ fn cgmatch(c: *cgen, n: *node) void = {
};
} else {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
if (isstrtype(c, pat)) { bsz = 16; }
else { if (isslicetype(c, pat)) { bsz = 24; }; };
// localalloc (not localadd): match-arm
// binds don't dedup with same-named binds
// in *other* matches, since C's cgexpr
// allocates a fresh slot per match expr.
let voff: i32 = localalloc(c, bn, bsz, pat);
let bw: i32 = 0;
for (bw < bsz) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 8): i64);
emitoff((scrutoff + 8 + bw): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitoff((voff + bw): i64);
emitline("(BP)\n");
bw += 8;
};
};
};
@@ -1589,6 +1602,20 @@ fn cgdot(c: *cgen, n: *node) void = {
};};
};
};
// Nested module-qualified field where the chain didn't fold to a
// known shape (raw w6c on a single file with `use mod;` but no
// driver concatenation — the inner enum / struct hasn't been
// seen). Emit `MOVQ <leaf>(SB), AX` so the linker surfaces a
// clean undefined-symbol error on the leaf. Mirror of
// cmd/w6c/cgen.c N_DOT nested fallback.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
emitline("\tMOVQ\t");
emitsymname(c, fld);
emitline("(SB), AX\n");
return;
};
};
return;
};
@@ -1617,10 +1644,9 @@ fn cgun(c: *cgen, n: *node) void = {
emitline("\t"); emitline(sub); emitline("\tX1, X0\n");
return;
};
cgexpr(c, n.lhs);
if (n.op == tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; };
if (n.op == tkind.TK_TILDE) { emitline("\tNOTQ\tAX\n"); return; };
if (n.op == tkind.TK_STAR) { emitline("\tMOVQ\t(AX), AX\n"); return; };
// Address-of has its own evaluation strategy — we want the address
// of the operand, not its value. Special-case here so `&arr[i]`
// doesn't compile the value load and then discard it.
if (n.op == tkind.TK_AMP) {
let opnd: *node = n.lhs;
if (opnd != nil) {
@@ -1633,17 +1659,94 @@ fn cgun(c: *cgen, n: *node) void = {
emitline("(BP), AX\n");
return;
};
// Top-level mutable let — RIP-relative LEAQ.
if (isletvar(c, nm)) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
return;
};
return;
};
if (opnd.kind == nkind.N_INDEX) {
// &base[i] = base + i*esz, no dereference.
let base: *node = opnd.lhs;
let idx: *node = opnd.rhs;
let esz: i32 = 8;
let isglobalarr: bool = false;
let isglobalptr: bool = false;
let globalname: str;
globalname.ptr = nil; globalname.len = 0;
let baselocal: *local = nil;
let isarr: bool = false;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
baselocal = localfindnode(c, base.str);
if (baselocal != nil) {
esz = elemsizeofc(c, baselocal.tnode);
let tn: *node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) { isarr = true; };
};
} else {
let tn: *node = letvartnode(c, base.str);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = base.str;
esz = elemsizeofc(c, tn);
};
if (tn.kind == nkind.N_TPTR) {
isglobalptr = true;
globalname = base.str;
esz = elemsizeofc(c, tn);
};
};
};
};
};
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (isglobalarr) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), BX\n");
} else { if (isglobalptr) {
emitline("\tMOVQ\t");
emitsymname(c, globalname);
emitline("(SB), BX\n");
} else { if (baselocal != nil) {
if (isarr) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else {
// Complex base: spill scaled idx, eval
// base to AX, move to BX, restore idx.
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n");
};};};
emitline("\tADDQ\tBX, AX\n");
return;
};
};
return;
};
cgexpr(c, n.lhs);
if (n.op == tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; };
if (n.op == tkind.TK_TILDE) { emitline("\tNOTQ\tAX\n"); return; };
if (n.op == tkind.TK_STAR) { emitline("\tMOVQ\t(AX), AX\n"); return; };
if (n.op == tkind.TK_NOT) {
let t: str = mklabel(c, "tt");
let e: str = mklabel(c, "te");
@@ -2349,7 +2452,7 @@ fn cgassign(c: *cgen, n: *node) void = {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
if (baselocal != nil) {
esz = elemsizeof(baselocal.tnode);
esz = elemsizeofc(c, baselocal.tnode);
let btn: *node = baselocal.tnode;
if (btn != nil) {
let bk: nkind = btn.kind;
@@ -2363,13 +2466,13 @@ fn cgassign(c: *cgen, n: *node) void = {
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeof(tn);
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
};
if (tn.kind == nkind.N_TPTR) {
isglobalptr = true;
globalname = bn;
esz = elemsizeof(tn);
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
};
};
@@ -3063,18 +3166,57 @@ fn cgassign(c: *cgen, n: *node) void = {
lvf = lvf.lvnext;
};
};
if (isfg && n.op == tkind.TK_ASSIGN) {
if (isfg) {
cgexpr(c, n.rhs);
let mov: str = "MOVSD";
if (isf32g) { mov = "MOVSS"; };
let addf: str = "ADDSD";
let subf: str = "SUBSD";
let mulf: str = "MULSD";
let divf: str = "DIVSD";
if (isf32g) {
mov = "MOVSS";
addf = "ADDSS";
subf = "SUBSS";
mulf = "MULSS";
divf = "DIVSS";
};
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
if (n.op == tkind.TK_ASSIGN) {
emitline("\t");
emitline(mov);
emitline("\tX0, (CX)\n");
return;
};
// Compound: X1 = load; X1 OP= X0; store X1.
// ADDSD/SUBSD/MULSD/DIVSD are register-register
// only, so we can't combine direct to memory.
let fop: str;
fop.ptr = nil; fop.len = 0;
if (n.op == tkind.TK_PLUSEQ) { fop = addf; };
if (n.op == tkind.TK_MINUSEQ) { fop = subf; };
if (n.op == tkind.TK_STAREQ) { fop = mulf; };
if (n.op == tkind.TK_SLASHEQ) { fop = divf; };
if (fop.len == 0) {
// Unsupported (e.g., %= on float):
// fall back to plain store of rhs.
emitline("\t");
emitline(mov);
emitline("\tX0, (CX)\n");
return;
};
emitline("\t");
emitline(mov);
emitline("\t(CX), X1\n");
emitline("\t");
emitline(fop);
emitline("\tX0, X1\n");
emitline("\t");
emitline(mov);
emitline("\tX1, (CX)\n");
return;
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
if (letvarisstr(c, nm)) {
@@ -3151,12 +3293,24 @@ fn cgassign(c: *cgen, n: *node) void = {
lcf32 = isf32type(c, lcn.tnode);
};
// Float-typed local: rhs lands in X0; store via MOVSD/
// MOVSS, no AX shuffle. Only plain `=` is wired; compound
// float-assign isn't.
if (lcf && n.op == tkind.TK_ASSIGN) {
// MOVSS, no AX shuffle. Compound (+= -= *= /=) loads
// slot into X1, combines into X1, stores X1 back —
// ADDSD/SUBSD/MULSD/DIVSD are register-register only.
if (lcf) {
cgexpr(c, n.rhs);
let mov: str = "MOVSD";
if (lcf32) { mov = "MOVSS"; };
let addf: str = "ADDSD";
let subf: str = "SUBSD";
let mulf: str = "MULSD";
let divf: str = "DIVSD";
if (lcf32) {
mov = "MOVSS";
addf = "ADDSS";
subf = "SUBSS";
mulf = "MULSS";
divf = "DIVSS";
};
if (n.op == tkind.TK_ASSIGN) {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
@@ -3164,6 +3318,35 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("(BP)\n");
return;
};
let fop: str;
fop.ptr = nil; fop.len = 0;
if (n.op == tkind.TK_PLUSEQ) { fop = addf; };
if (n.op == tkind.TK_MINUSEQ) { fop = subf; };
if (n.op == tkind.TK_STAREQ) { fop = mulf; };
if (n.op == tkind.TK_SLASHEQ) { fop = divf; };
if (fop.len == 0) {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
emitline("\t");
emitline(mov);
emitline("\t");
emitoff(off: i64);
emitline("(BP), X1\n");
emitline("\t");
emitline(fop);
emitline("\tX0, X1\n");
emitline("\t");
emitline(mov);
emitline("\tX1, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
emitline("\tMOVQ\tAX, ");

View File

@@ -203,6 +203,11 @@ fn cgreturn(c: *cgen, n: *node) void = {
emitoff((scroff + 16): i64);
emitline("(BP), CX\n");
};
if (rsz > 24) {
emitline("\tMOVQ\t");
emitoff((scroff + 24): i64);
emitline("(BP), R8\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
@@ -225,12 +230,19 @@ fn cgreturn(c: *cgen, n: *node) void = {
return;
};
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
if (nodeisstr(c, rhs)) {
if (nodeisslice(c, rhs)) {
// cgexpr leaves (AX=ptr, BX=len, CX=cap).
// Shuffle into return ABI: DX=ptr, CX=len,
// R8=cap.
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
} else { if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
} else {
emitline("\tMOVQ\tAX, DX\n");
};
};};
emitline("\tMOVQ\t$");
if (idx < 0) { idx = 0; };
emitint(idx: i64);

View File

@@ -101,7 +101,21 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
return rest + widensz / 8;
};
cgexpr(c, arg);
if (nodeisstr(c, arg)) {
if (nodeisslice(c, arg)) {
// Slice payload (24B): cgexpr leaves (AX=ptr, BX=len,
// CX=cap). Slot layout: [+0]=tag, [+8]=ptr, [+16]=len,
// [+24]=cap. Push high→low so pop drains tag first.
// Requires widensz >= 32; a smaller slot would mean the
// destination union doesn't list slice as a variant
// (caller should have flagged a type error).
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t$");
emitint(widentag: i64);
emitline(", AX\n");
emitline("\tPUSHQ\tAX\n");
} else { if (nodeisstr(c, arg)) {
// slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low
// so pop drains tag first into arg-reg[0].
emitline("\tPUSHQ\tBX\n");
@@ -114,16 +128,18 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
// Scalar variant: single value word at +8. Pad a zero
// high word when slot is 24B (some other variant of
// the union is 16B-shaped).
if (widensz > 16) {
let pp: i32 = widensz - 8;
for (pp > 8) {
emitline("\tXORQ\tDX, DX\n");
emitline("\tPUSHQ\tDX\n");
pp -= 8;
};
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t$");
emitint(widentag: i64);
emitline(", AX\n");
emitline("\tPUSHQ\tAX\n");
};
};};
return rest + widensz / 8;
};
// nkind.N_SLICE expression as arg: `buf[lo:hi]` builds a slice header
@@ -209,25 +225,28 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
// Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in
// reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop
// into argregs lands the canonical (ptr/tag, len/v0, cap/v1).
// For tagged ident with a >24B slot (slice-payload variant),
// push a fourth word from off+24.
if (arg.kind == nkind.N_IDENT) {
let nm: str = arg.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
let off: i32 = lc.off;
if (isslicetype(c, lc.tnode) || istaggedtype(c, lc.tnode)) {
let nwords: i32 = 3;
if (istaggedtype(c, lc.tnode)) {
let ssz: i32 = slotsize(c, lc.tnode);
nwords = ssz / 8;
};
let w: i32 = nwords - 1;
for (w >= 0) {
emitline("\tMOVQ\t");
emitoff((off + 16): i64);
emitoff((off + w*8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
w -= 1;
};
return rest + nwords;
};
};
};
@@ -610,6 +629,9 @@ fn dotinnerstructptr(c: *cgen, n: *node) *node = {
// elemsizeof — given the type node of an indexable (`*T`, `[]T`,
// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/
// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback).
// For aliased element types (e.g. `[N]formattable`), callers that
// need the resolved slot size should use elemsizeofc(c, t) which
// follows aliases via slotsize.
fn elemsizeof(t: *node) i32 = {
if (t == nil) { return 1; };
let k: nkind = t.kind;
@@ -636,6 +658,27 @@ fn elemsizeof(t: *node) i32 = {
return 8;
};
// elemsizeofc — like elemsizeof but resolves aliased element types
// (struct / tagged / `type foo = bar;`) via slotsize. Used where
// cgindex / cgassign need a correct stride for `[N]Alias` arrays
// whose Alias resolves to a tagged union (e.g. `[N]formattable`).
fn elemsizeofc(c: *cgen, t: *node) i32 = {
if (t == nil) { return 1; };
let direct: i32 = elemsizeof(t);
if (direct != 8) { return direct; };
let k: nkind = t.kind;
let elem: *node = nil;
if (k == nkind.N_TPTR) { elem = t.lhs; };
if (k == nkind.N_TSLICE) { elem = t.lhs; };
if (k == nkind.N_TARRAY) { elem = t.lhs; };
if (elem == nil) { return direct; };
if (elem.kind == nkind.N_TNAME) {
let ps: i32 = primsize(elem.str);
if (ps > 0) { return ps; };
};
return slotsize(c, elem);
};
// nodeisunsigned — best-effort cgen-time inference from the AST. We
// don't have a typed AST yet, so we walk surface nodes:
// nkind.N_INTLIT — never marked unsigned (no tsuffix plumbing yet)
@@ -986,9 +1029,20 @@ fn slotsize(c: *cgen, typn: *node) i32 = {
if (ps > 0) { esz = ps; }
else {
// Named struct / aliased type: size off
// the structinfo if present.
// the structinfo if present, else follow
// the alias via aliaslookup so
// `[N]formattable` reads the resolved
// tagged slot (e.g. 24B for
// `(i64|str|bool)`), not the fall-
// through 8B.
let si: *structinfo = structlookup(c, en);
if (si != nil) { esz = si.totsize; };
if (si != nil) { esz = si.totsize; }
else { if (c != nil) {
let al: *node = aliaslookup(c, en);
if (al != nil) {
esz = slotsize(c, al);
};
}; };
};
} else { if (elemn.kind == nkind.N_TTAGGED) {
// Tagged-union element: full slot (8 tag +
@@ -1734,6 +1788,53 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz:
emitline("(BP)\n");
return;
};
// `expr: TaggedAlias` where the cast's destination IS the union
// itself is a widening, not a re-interpret. cgexpr on a CAST
// produces the inner's register shape (str: AX=ptr, BX=len), not
// the tagged AX/DX/CX triple — so peel to the inner and route
// through the matching concrete-variant branch below. A cast to
// a concrete variant (`7: i32`) is left intact so the existing
// scalar / str / slice branches pick the right variant tag.
if (src != nil) {
if (src.kind == nkind.N_CAST) {
if (src.lhs != nil) {
let inner: *node = src.lhs;
let inneristagged: bool = false;
if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) {
inneristagged = istaggedtype(c, lc.tnode);
};
};
if (rhstaggedabicall(c, inner)) {
inneristagged = true;
};
// Cast's destination = the dst tagged union
// itself? The rhs of N_CAST holds the target
// type. Compare nominally via str match on
// the tagged-alias name.
let castisdst: bool = false;
let castrhs: *node = src.rhs;
if (castrhs != nil) {
if (castrhs.kind == nkind.N_TTAGGED) {
castisdst = true;
};
if (castrhs.kind == nkind.N_TNAME) {
if (dst != nil) {
if (dst.kind == nkind.N_TNAME) {
if (streq(castrhs.str, dst.str)) {
castisdst = true;
};
};
};
};
};
if (castisdst && !inneristagged) {
src = inner;
};
};
};
};
// Tagged source ident: byte-copy slot words then tag-remap.
let st: *node = rhstaggedident(c, src);
if (st != nil) {
@@ -1763,8 +1864,9 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz:
cgwidentagremap(c, dt, st, slot_off);
return;
};
// Tagged source via AX/DX/CX register ABI (N_CALL, N_INDEX of
// tagged element).
// Tagged source via AX/DX/CX/R8 register ABI (N_CALL, N_INDEX
// of tagged element). R8 carries the 4th word for slice-payload
// variants (slot 32B).
if (rhstaggedabicall(c, src)) {
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
@@ -1780,6 +1882,11 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz:
emitoff((slot_off + 16): i64);
emitline("(BP)\n");
};
if (slot_sz > 24) {
emitline("\tMOVQ\tR8, ");
emitoff((slot_off + 24): i64);
emitline("(BP)\n");
};
return;
};
// Struct payload (literal or ident).
@@ -1898,6 +2005,28 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz:
emitline("(BP)\n");
return;
};
// Slice payload (24B): cgexpr leaves (AX=ptr, BX=len, CX=cap).
// Slot layout: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap.
if (nodeisslice(c, src)) {
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((slot_off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((slot_off + 24): i64);
emitline("(BP)\n");
let tag: i32 = taggedvariantindex(c, dt, src);
if (tag < 0) { tag = 0; };
emitline("\tMOVQ\t$");
emitint(tag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
// Scalar payload.
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");

File diff suppressed because it is too large Load Diff

View File

@@ -145,11 +145,11 @@ export fn main(argc: i32, argv: **u8) i32 = {
// "<file>: <resolved>/<resolved+unresolved> resolved"
os.write(1, argstr(path).ptr, argstrlen(path): u64);
os.write(1, ": ".ptr, 2u64);
let rs: str = strconv.i64tos(ck.nresolved: i64, strconv.DEC);
let rs: str = strconv.i64tos(ck.nresolved: i64, strconv.base.DEC);
os.write(1, rs.ptr, rs.len: u64);
os.write(1, "/".ptr, 1u64);
let total: i32 = ck.nresolved + ck.nunresolved;
let ts: str = strconv.i64tos(total: i64, strconv.DEC);
let ts: str = strconv.i64tos(total: i64, strconv.base.DEC);
os.write(1, ts.ptr, ts.len: u64);
os.write(1, " resolved\n".ptr, 10u64);
if (ck.nunresolved > 0) { return 1; };

View File

@@ -269,12 +269,14 @@ export fn hassuffix(s: str, suf: str) bool = {
return true;
};
// indexbyte — first byte position of byte `c` in `s`. Mirrors
// Hare's strings::byteindex when the needle is a single ASCII rune,
// renamed to match bytes.indexbyte and to disambiguate from Hare's
// `byteindex(haystack, needle: (str | rune))` which we don't have
// the union-arg ABI for yet.
export fn indexbyte(s: str, c: u8) (i32 | void) = {
// byteindex — first byte position of `needle` in `s`. Mirrors Hare's
// strings::byteindex: a single-codepoint rune scans for the byte that
// encodes it (ASCII only here — multi-byte UTF-8 awaits utf8 encode),
// a str needle scans for the substring. Returns void if absent.
export fn byteindex(s: str, needle: (str | rune)) (i32 | void) = {
match (needle) {
case let r: rune => {
let c: u8 = r: u8;
let i: i32 = 0;
for (i < s.len) {
if (s[i] == c) { return i; };
@@ -282,21 +284,7 @@ export fn indexbyte(s: str, c: u8) (i32 | void) = {
};
return;
};
// rindexbyte — last byte position of byte `c` in `s`.
export fn rindexbyte(s: str, c: u8) (i32 | void) = {
let i: i32 = s.len - 1;
for (i >= 0) {
if (s[i] == c) { return i; };
i -= 1;
};
return;
};
// index — first index of `sub` in `s`. Naive scan; fine for short
// patterns and small strings, which dominate config and CLI parsing.
// Empty `sub` matches at 0.
export fn index(s: str, sub: str) (i32 | void) = {
case let sub: str => {
if (sub.len == 0) { return 0; };
if (sub.len > s.len) { return; };
let last: i32 = s.len - sub.len;
@@ -313,9 +301,14 @@ export fn index(s: str, sub: str) (i32 | void) = {
};
return;
};
};
return;
};
// contains — true iff `sub` appears in `s`. Mirrors Hare's
// strings::contains shape (byte-wise on the str-needle case).
export fn contains(s: str, sub: str) bool = {
let r: (i32 | void) = index(s, sub);
let r: (i32 | void) = byteindex(s, sub);
match (r) {
case let i: i32 => return true;
case void => return false;
@@ -359,9 +352,22 @@ export fn dup(s: str) str = {
return r;
};
// rindex — last index of `sub` in `s`. Mirrors Hare's strings::rindex
// (slice case). Empty `sub` matches at s.len.
export fn rindex(s: str, sub: str) (i32 | void) = {
// rbyteindex — last byte position of `needle` in `s`. Mirrors Hare's
// strings::rbyteindex. Rune needle scans for the byte that encodes it
// (ASCII only); str needle scans for the substring. Empty str needle
// matches at s.len.
export fn rbyteindex(s: str, needle: (str | rune)) (i32 | void) = {
match (needle) {
case let r: rune => {
let c: u8 = r: u8;
let i: i32 = s.len - 1;
for (i >= 0) {
if (s[i] == c) { return i; };
i -= 1;
};
return;
};
case let sub: str => {
if (sub.len == 0) { return s.len; };
if (sub.len > s.len) { return; };
let i: i32 = s.len - sub.len;
@@ -377,6 +383,9 @@ export fn rindex(s: str, sub: str) (i32 | void) = {
};
return;
};
};
return;
};
// sub — borrowed substring `s[start..end]`. Mirrors Hare's
// strings::sub. Caller must ensure 0 <= start <= end <= s.len; out-of-
@@ -450,12 +459,11 @@ export fn trimbyte(s: str, c: u8) str = {
// MODULE: strconv
// strconv — number↔string conversions.
//
// Mirrors Hare's strconv:: surface. The *tos functions return a fresh
// owned `str`; release via os.free(r.ptr, r.len: u64) when done.
// Hare returns `const str` into a static buffer; ww allocates per
// call because the wwstage cgen doesn't currently support mutating a
// module-level `*u8` (so a lazy-init shared buffer isn't expressible
// today). Graduate to the static-buffer shape once that lands.
// Mirrors Hare's strconv:: surface. The *tos functions return a
// `const str` view into a module-level buffer that is overwritten on
// the next call to the same function; callers must copy the bytes if
// they need to outlive the next invocation. See [[strings.dup]] to
// duplicate. Matches Hare's strconv::*tos semantics.
use os;
use strings;
@@ -472,43 +480,44 @@ export type overflow = !void;
// error — any error from a strconv call. Mirrors Hare's strconv::error.
export type error = !(invalid | overflow);
// base — numeric base for parsing/formatting. Plain i32 (not a named
// enum) because cross-module `strconv.base.DEC` chains miscompile in
// the cstage cgen — it emits a memory load through `base(SB)` rather
// than inlining the enum value. Hare names them as `strconv::base`
// enum values; we expose them as module-level `def`s so callers say
// `strconv.DEC` and the cgen inlines the immediate.
// base — numeric base for parsing/formatting. Mirrors Hare's
// `strconv::base` (Hare uses `enum uint`; we pick `enum i32` since
// the underlying parse/format loops index with i32).
//
// HEX is HEX_UPPER; HEX_LOWER is a separate pseudo-base that produces
// lowercase a-f digits.
export def DEFAULT: i32 = 0;
export def BIN: i32 = 2;
export def OCT: i32 = 8;
export def DEC: i32 = 10;
export def HEX_UPPER: i32 = 16;
export def HEX: i32 = 16;
export def HEX_LOWER: i32 = 17;
// HEX is an alias for HEX_UPPER; HEX_LOWER is a pseudo-base that
// produces lowercase a-f digits.
export type base = enum i32 {
DEFAULT = 0,
BIN = 2,
OCT = 8,
DEC = 10,
HEX_UPPER = 16,
HEX = 16,
HEX_LOWER = 17,
};
fn basenum(b: i32) i64 = {
if (b == BIN) { return 2; };
if (b == OCT) { return 8; };
if (b == HEX) { return 16; };
if (b == HEX_UPPER) { return 16; };
if (b == HEX_LOWER) { return 16; };
fn basenum(b: base) i64 = {
if (b == base.BIN) { return 2; };
if (b == base.OCT) { return 8; };
if (b == base.HEX) { return 16; };
if (b == base.HEX_UPPER) { return 16; };
if (b == base.HEX_LOWER) { return 16; };
return 10; // DEC and DEFAULT
};
fn basedigit(d: i64, b: i32) u8 = {
fn basedigit(d: i64, b: base) u8 = {
if (d < 10) { return (d + 48): u8; };
let off: i64 = d - 10;
if (b == HEX_LOWER) { return (off + 97): u8; };
if (b == base.HEX_LOWER) { return (off + 97): u8; };
return (off + 65): u8;
};
// u64tos — convert v to a base-b numeric string. Returns owned str;
// release via os.free(r.ptr, r.len: u64). Mirrors Hare's
// strconv::u64tos (Hare returns const str into a static buffer).
export fn u64tos(v: u64, b: i32) str = {
// u64tos — convert v to a base-b numeric string. Returns a view into
// `u64tos_buf` which is overwritten on the next call. Matches Hare's
// strconv::u64tos.
let u64tos_buf: [65]u8;
export fn u64tos(v: u64, b: base) str = {
let nb: u64 = basenum(b): u64;
let tmp: [65]u8;
let i: i32 = 0;
@@ -520,22 +529,25 @@ export fn u64tos(v: u64, b: i32) str = {
n = n / nb;
i += 1;
};
let buf: *u8 = os.alloc(i: u64): *u8;
let out: i32 = 0;
for (i > 0) {
i -= 1;
buf[out] = tmp[i];
u64tos_buf[out] = tmp[i];
out += 1;
};
let r: str;
r.ptr = buf;
r.ptr = &u64tos_buf[0];
r.len = out;
return r;
};
// i64tos — convert v to a base-b numeric string. Returns owned str;
// release via os.free. Mirrors Hare's strconv::i64tos.
export fn i64tos(v: i64, b: i32) str = {
// i64tos — convert v to a base-b numeric string. Returns a view into
// `i64tos_buf` which is overwritten on the next call. Independent
// buffer from u64tos so i64tos's own call to u64tos doesn't clobber
// the in-flight result. Matches Hare's strconv::i64tos.
let i64tos_buf: [66]u8;
export fn i64tos(v: i64, b: base) str = {
let neg: bool = false;
let n: i64 = v;
if (n < 0) { neg = true; n = -n; };
@@ -549,37 +561,33 @@ export fn i64tos(v: i64, b: i32) str = {
n = n / nb;
i += 1;
};
let extra: i32 = 0;
if (neg) { extra = 1; };
let total: i32 = i + extra;
let buf: *u8 = os.alloc(total: u64): *u8;
let out: i32 = 0;
if (neg) { buf[out] = 45u8; out += 1; }; // '-'
if (neg) { i64tos_buf[out] = 45u8; out += 1; }; // '-'
for (i > 0) {
i -= 1;
buf[out] = tmp[i];
i64tos_buf[out] = tmp[i];
out += 1;
};
let r: str;
r.ptr = buf;
r.ptr = &i64tos_buf[0];
r.len = out;
return r;
};
export fn i32tos(v: i32, b: i32) str = { return i64tos(v: i64, b); };
export fn i16tos(v: i16, b: i32) str = { return i64tos(v: i64, b); };
export fn i8tos(v: i8, b: i32) str = { return i64tos(v: i64, b); };
export fn i32tos(v: i32, b: base) str = { return i64tos(v: i64, b); };
export fn i16tos(v: i16, b: base) str = { return i64tos(v: i64, b); };
export fn i8tos(v: i8, b: base) str = { return i64tos(v: i64, b); };
export fn u32tos(v: u32, b: i32) str = { return u64tos(v: u64, b); };
export fn u16tos(v: u16, b: i32) str = { return u64tos(v: u64, b); };
export fn u8tos(v: u8, b: i32) str = { return u64tos(v: u64, b); };
export fn u32tos(v: u32, b: base) str = { return u64tos(v: u64, b); };
export fn u16tos(v: u16, b: base) str = { return u64tos(v: u64, b); };
export fn u8tos(v: u8, b: base) str = { return u64tos(v: u64, b); };
// digval — value of digit byte `c` under base `b`, or -1 if not a
// valid digit. Letters are accepted case-insensitively under HEX /
// HEX_UPPER; only lowercase under HEX_LOWER.
fn digval(c: u8, b: i32) i32 = {
fn digval(c: u8, b: base) i32 = {
if (c >= 48u8) { if (c <= 57u8) { return (c - 48u8): i32; }; };
if (b == HEX_LOWER) {
if (b == base.HEX_LOWER) {
if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; };
return -1;
};
@@ -592,7 +600,7 @@ fn digval(c: u8, b: i32) i32 = {
// No locale, no whitespace, no underscores: optional leading '-' then
// digits. Returns invalid with the offending index or overflow on
// out-of-range.
export fn stoi64(s: str, b: i32) (i64 | invalid | overflow) = {
export fn stoi64(s: str, b: base) (i64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let i: i32 = 0;
let neg: bool = false;
@@ -613,7 +621,7 @@ export fn stoi64(s: str, b: i32) (i64 | invalid | overflow) = {
};
// stou64 — parse unsigned base-b number. Mirrors Hare's strconv::stou64.
export fn stou64(s: str, b: i32) (u64 | invalid | overflow) = {
export fn stou64(s: str, b: base) (u64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let nb: u64 = basenum(b): u64;
let v: u64 = 0u64;
@@ -629,7 +637,7 @@ export fn stou64(s: str, b: i32) (u64 | invalid | overflow) = {
return v;
};
export fn stoi32(s: str, b: i32) (i32 | invalid | overflow) = {
export fn stoi32(s: str, b: base) (i32 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
@@ -643,7 +651,7 @@ export fn stoi32(s: str, b: i32) (i32 | invalid | overflow) = {
return 0: invalid; // unreachable; appeases the path-cov checker
};
export fn stoi16(s: str, b: i32) (i16 | invalid | overflow) = {
export fn stoi16(s: str, b: base) (i16 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
@@ -657,7 +665,7 @@ export fn stoi16(s: str, b: i32) (i16 | invalid | overflow) = {
return 0: invalid;
};
export fn stoi8(s: str, b: i32) (i8 | invalid | overflow) = {
export fn stoi8(s: str, b: base) (i8 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
@@ -671,7 +679,7 @@ export fn stoi8(s: str, b: i32) (i8 | invalid | overflow) = {
return 0: invalid;
};
export fn stou32(s: str, b: i32) (u32 | invalid | overflow) = {
export fn stou32(s: str, b: base) (u32 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
@@ -684,7 +692,7 @@ export fn stou32(s: str, b: i32) (u32 | invalid | overflow) = {
return 0: invalid;
};
export fn stou16(s: str, b: i32) (u16 | invalid | overflow) = {
export fn stou16(s: str, b: base) (u16 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
@@ -697,7 +705,7 @@ export fn stou16(s: str, b: i32) (u16 | invalid | overflow) = {
return 0: invalid;
};
export fn stou8(s: str, b: i32) (u8 | invalid | overflow) = {
export fn stou8(s: str, b: base) (u8 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
@@ -733,13 +741,14 @@ export fn stou8(s: str, b: i32) (u8 | invalid | overflow) = {
// the ww-side wwdump currently skips TK_FLOAT.fval while the C side
// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses:
// build f64 constants via int-to-f64 casts.
let f64tos_buf: [64]u8;
export fn f64tos(v: f64) str = {
let tmp: [64]u8;
let out: i32 = 0;
let f: f64 = v;
let zero: f64 = 0: f64;
if (f < zero) {
tmp[out] = 45u8; // '-'
f64tos_buf[out] = 45u8; // '-'
out += 1;
f = -f;
};
@@ -749,12 +758,9 @@ export fn f64tos(v: f64) str = {
if (f >= cap) {
let s: str = "huge";
let k: i32 = 0;
for (k < s.len) { tmp[out] = s[k]; out += 1; k += 1; };
let buf: *u8 = os.alloc(out: u64): *u8;
let q: i32 = 0;
for (q < out) { buf[q] = tmp[q]; q += 1; };
for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; };
let r: str;
r.ptr = buf;
r.ptr = &f64tos_buf[0];
r.len = out;
return r;
};
@@ -773,32 +779,27 @@ export fn f64tos(v: f64) str = {
ip += 1;
fp = 0;
};
let intstr: str = i64tos(ip, DEC);
let intstr: str = i64tos(ip, base.DEC);
let k: i32 = 0;
for (k < intstr.len) { tmp[out] = intstr.ptr[k]; out += 1; k += 1; };
os.free(intstr.ptr: *void, intstr.len: u64);
for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; };
if (fp != 0) {
tmp[out] = 46u8; // '.'
f64tos_buf[out] = 46u8; // '.'
out += 1;
let fracstr: str = u64tos(fp: u64, DEC);
let fracstr: str = u64tos(fp: u64, base.DEC);
// Pad fractional to 6 digits with leading zeros (e.g. 0.05 →
// fp=50000, fracstr="50000", pad one '0' before).
let z: i32 = 6 - fracstr.len;
for (z > 0) { tmp[out] = 48u8; out += 1; z -= 1; };
for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; };
k = 0;
for (k < fracstr.len) { tmp[out] = fracstr.ptr[k]; out += 1; k += 1; };
os.free(fracstr.ptr: *void, fracstr.len: u64);
for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; };
// Trim trailing zeros in the fractional part.
for (out > 0) {
if (tmp[out - 1] != 48u8) { break; };
if (f64tos_buf[out - 1] != 48u8) { break; };
out -= 1;
};
};
let buf: *u8 = os.alloc(out: u64): *u8;
let q: i32 = 0;
for (q < out) { buf[q] = tmp[q]; q += 1; };
let r: str;
r.ptr = buf;
r.ptr = &f64tos_buf[0];
r.len = out;
return r;
};
@@ -1083,7 +1084,7 @@ export fn main() i32 = {
if (dn != 3) { return 10; };
// Probe 5 — strconv round-trip via the real stdlib.
let s: str = strconv.i64tos(4242i64, strconv.DEC);
let s: str = strconv.i64tos(4242i64, strconv.base.DEC);
if (s.len != 4) { return 11; };
if (s.ptr[0] != 52u8) { return 12; }; // '4'
if (s.ptr[3] != 50u8) { return 13; }; // '2'

View File

@@ -130,7 +130,7 @@ export fn main() i32 = {
if (dn != 3) { return 10; };
// Probe 5 — strconv round-trip via the real stdlib.
let s: str = strconv.i64tos(4242i64, strconv.DEC);
let s: str = strconv.i64tos(4242i64, strconv.base.DEC);
if (s.len != 4) { return 11; };
if (s.ptr[0] != 52u8) { return 12; }; // '4'
if (s.ptr[3] != 50u8) { return 13; }; // '2'

View File

@@ -144,7 +144,7 @@ static const struct row rows[] = {
{ "use os;\n"
"use strconv;\n"
"fn main() i32 = {\n"
" let s: str = strconv.i64tos(12345, strconv.DEC);\n"
" let s: str = strconv.i64tos(12345, strconv.base.DEC);\n"
" os.write(1, s.ptr, s.len: u64);\n"
" os.write(1, \"\\n\".ptr, 1u64);\n"
" return s.len;\n"
@@ -199,8 +199,8 @@ static const struct row rows[] = {
"use strconv;\n"
"fn main() i32 = {\n"
" fmt.println(\"ww\");\n"
" fmt.println(strconv.i64tos(42, strconv.DEC));\n"
" fmt.println(strconv.i64tos(-7, strconv.DEC));\n"
" fmt.println(strconv.i64tos(42, strconv.base.DEC));\n"
" fmt.println(strconv.i64tos(-7, strconv.base.DEC));\n"
" return 0;\n"
"};", 0 },
/* struct with i32 fields: MOVL/MOVSXD avoids clobbering neighbors */
@@ -1178,9 +1178,9 @@ static const struct row rows[] = {
{ "use strconv;\n"
"type r_t = (i64 | strconv.invalid | strconv.overflow);\n"
"fn main() i32 = {\n"
" let r1: r_t = strconv.stoi64(\"42\", strconv.DEC);\n"
" let r2: r_t = strconv.stoi64(\"-7\", strconv.DEC);\n"
" let r3: r_t = strconv.stoi64(\"abc\", strconv.DEC);\n"
" let r1: r_t = strconv.stoi64(\"42\", strconv.base.DEC);\n"
" let r2: r_t = strconv.stoi64(\"-7\", strconv.base.DEC);\n"
" let r3: r_t = strconv.stoi64(\"abc\", strconv.base.DEC);\n"
" let acc: i32 = 0;\n"
" match (r1) {\n"
" case let v: i64 => acc += v: i32;\n"
@@ -1204,8 +1204,8 @@ static const struct row rows[] = {
{ "use strconv;\n"
"type r_t = (u64 | strconv.invalid | strconv.overflow);\n"
"fn main() i32 = {\n"
" let r1: r_t = strconv.stou64(\"123\", strconv.DEC);\n"
" let r2: r_t = strconv.stou64(\"-1\", strconv.DEC);\n"
" let r1: r_t = strconv.stou64(\"123\", strconv.base.DEC);\n"
" let r2: r_t = strconv.stou64(\"-1\", strconv.base.DEC);\n"
" let acc: i32 = 0;\n"
" match (r1) {\n"
" case let v: u64 => acc += v: i32;\n"
@@ -1219,7 +1219,7 @@ static const struct row rows[] = {
" };\n"
" return acc;\n"
"};", 123 }, /* 123 + 0 (invalid at index 0 in \"-1\") */
/* strings.indexbyte and strings.index: now (i32 | void). */
/* strings.byteindex with (str | rune) needle: returns (i32 | void). */
{ "use strings;\n"
"fn pick(r: (i32 | void), miss: i32) i32 = {\n"
" match (r) {\n"
@@ -1230,10 +1230,10 @@ static const struct row rows[] = {
"};\n"
"fn main() i32 = {\n"
" let s: str = \"hello, world\";\n"
" let i1: i32 = pick(strings.indexbyte(s, 44u8), -1);\n"
" let i2: i32 = pick(strings.indexbyte(s, 122u8), -1);\n"
" let i3: i32 = pick(strings.index(s, \"world\"), -1);\n"
" let i4: i32 = pick(strings.index(s, \"nope\"), -1);\n"
" let i1: i32 = pick(strings.byteindex(s, ','), -1);\n"
" let i2: i32 = pick(strings.byteindex(s, 'z'), -1);\n"
" let i3: i32 = pick(strings.byteindex(s, \"world\"), -1);\n"
" let i4: i32 = pick(strings.byteindex(s, \"nope\"), -1);\n"
" return i1 + i2 + i3 + i4;\n"
"};", 10 }, /* 5 + (-1) + 7 + (-1) */
/* bytes.index: substring search over []u8, (i32 | void). */
@@ -1419,6 +1419,145 @@ static const struct row rows[] = {
"// MODULE: main\n"
"use pkg;\n"
"fn main() i32 = { return pkg.dir.SOUTH as i32; };", 1 },
/* f64 compound assigns on local: += -= *= /= each modify in place
* (ADDSD/SUBSD/MULSD/DIVSD load-modify-store, not a plain MOVSD that
* would overwrite). 1.5 + 0.5 = 2.0 → 2.0 - 1.0 = 1.0 → 1.0 * 4.0 =
* 4.0 → 4.0 / 2.0 = 2.0 → return 2. */
{ "fn main() i32 = {\n"
" let a: f64 = 1.5;\n"
" a += 0.5;\n"
" a -= 1.0;\n"
" a *= 4.0;\n"
" a /= 2.0;\n"
" return a: i32;\n"
"};", 2 },
/* f64 compound on a top-level global: LEAQ name(SB), then MOVSD
* load → ADDSD → MOVSD store. 10.0 + 5.0 = 15.0 → 15.0 * 2.0 =
* 30.0 → 30.0 - 20.0 = 10.0 → 10.0 / 5.0 = 2.0. */
{ "let G: f64 = 10.0;\n"
"fn main() i32 = {\n"
" G += 5.0;\n"
" G *= 2.0;\n"
" G -= 20.0;\n"
" G /= 5.0;\n"
" return G: i32;\n"
"};", 2 },
/* Top-level `[N]u8` array: zero-init DATAW slot + LEAQ name(SB)
* addressing for index and address-of. `buf[i] = c` narrows to
* MOVB; reading back roundtrips through MOVZBQ. */
{ "let buf: [4]u8;\n"
"fn main() i32 = {\n"
" buf[0] = 7u8;\n"
" buf[1] = 35u8;\n"
" return (buf[0] + buf[1]): i32;\n"
"};", 42 },
/* `&arr[i]` for a top-level array: TK_AMP must compute the
* address, not the value. Then `*p = c` for *u8 stores 1 byte.
* Drives Hare's static-buffer pattern (strconv.*tos). */
{ "let buf: [4]u8;\n"
"fn main() i32 = {\n"
" let p: *u8 = &buf[0];\n"
" *p = 41u8;\n"
" let q: *u8 = &buf[1];\n"
" *q = 1u8;\n"
" return (buf[0] + buf[1]): i32;\n"
"};", 42 },
/* Cross-module enum member access: `pkg.Enum.MEMBER`. Inner
* N_DOT resolves through SK_USE → SK_TYPE; outer N_DOT folds to
* the member's integer literal. Validates the strconv.base.DEC
* shape that the *tos / sto* signatures now use. */
{ "// MODULE: pkg\n"
"export type base = enum i32 { DEC = 10, HEX = 16 };\n"
"// MODULE: main\n"
"use pkg;\n"
"fn pick(b: pkg.base) i32 = { return b as i32; };\n"
"fn main() i32 = {\n"
" let a: i32 = pick(pkg.base.DEC);\n"
" let b: i32 = pick(pkg.base.HEX);\n"
" return a + b + 16;\n"
"};", 42 },
/* Sum-typed parameter (str | rune): match-dispatch on a tagged
* union arg widened from a concrete variant at the call site.
* Mirrors lib/strings.byteindex's needle parameter. */
{ "fn pick(n: (str | rune)) i32 = {\n"
" match (n) {\n"
" case let s: str => return s.len + 100;\n"
" case let r: rune => return r: i32;\n"
" };\n"
"};\n"
"fn main() i32 = {\n"
" let a: i32 = pick(\"hi\");\n"
" let b: i32 = pick('?');\n"
" return a + b - 123;\n"
"};", 42 }, /* (2+100) + 63 - 123 = 42 */
/* Sum-typed (u8 | []u8): 32B slot exceeds the old 24B cap on
* tagged_arg_size. Param fills 4 reg words; the callee must
* read slot+24 (cap) for the slice variant to round-trip. */
{ "use bytes;\n"
"fn main() i32 = {\n"
" let buf: [4]u8;\n"
" buf[0] = 1u8; buf[1] = 2u8; buf[2] = 3u8; buf[3] = 4u8;\n"
" let needle: [2]u8;\n"
" needle[0] = 3u8; needle[1] = 4u8;\n"
" let r1: (i32 | void) = bytes.index(buf[0:4], 3u8);\n"
" let r2: (i32 | void) = bytes.index(buf[0:4], needle[0:2]);\n"
" let a: i32 = 99;\n"
" let b: i32 = 99;\n"
" match (r1) {\n"
" case let i: i32 => a = i;\n"
" case void => a = -1;\n"
" };\n"
" match (r2) {\n"
" case let i: i32 => b = i;\n"
" case void => b = -1;\n"
" };\n"
" return a * 10 + b + 18;\n" /* 2*10 + 2 + 18 = 40, off-by-2 → 42 */
"};", 40 },
/* Slice-payload tagged return ([]u8 | E), slot 32B. The 4-reg
* return ABI (AX=tag, DX=ptr, CX=len, R8=cap) lets the callee
* forward all 4 words. Before the bump, slice.len was dropped
* because only 3 regs were used. */
{ "type rterr = !str;\n"
"fn build(n: i32) ([]u8 | rterr) = {\n"
" if (n < 0) { return \"bad\": rterr; };\n"
" let buf: [4]u8;\n"
" buf[0] = 10u8; buf[1] = 20u8; buf[2] = 30u8; buf[3] = 40u8;\n"
" return buf[0:n];\n"
"};\n"
"fn main() i32 = {\n"
" let r: ([]u8 | rterr) = build(3);\n"
" match (r) {\n"
" case let xs: []u8 => {\n"
" if (xs.len != 3) { return 100; };\n"
" return (xs[0] + xs[1] + xs[2]): i32;\n"
" };\n"
" case let e: rterr => return -1;\n"
" };\n"
" return 0;\n"
"};", 60 }, /* 10 + 20 + 30 = 60 */
/* `[N]TaggedAlias` array: each element is a 24B tagged slot,
* and `arr[i] = literal: TaggedAlias` widens through the
* cgwidentaggedstore path. Validates the cast-peel for
* `expr: TaggedAlias` (which is a widening, not a re-interpret)
* and the alias-resolving element-size lookup. */
{ "type formattable = (i64 | str | bool);\n"
"fn main() i32 = {\n"
" let args: [3]formattable;\n"
" args[0] = 1i64: formattable;\n"
" args[1] = \"hi\": formattable;\n"
" args[2] = true: formattable;\n"
" let s: i32 = 0;\n"
" let i: i32 = 0;\n"
" for (i < args.len) {\n"
" match (args[i]) {\n"
" case let n: i64 => s += n: i32;\n"
" case let v: str => s += v.len;\n"
" case let b: bool => { if (b) { s += 39; }; };\n"
" };\n"
" i += 1;\n"
" };\n"
" return s;\n"
"};", 42 }, /* 1 + 2 + 39 = 42 */
{ NULL, 0 }
};