selfhost+cstage+test: graduate frame growth to first-use+fail-loud (#15)
Subsumes #36. Drop wwstage scanlocals pre-pass; both stages converge on first-use+fail-loud frame growth, rule-10 polarity DOWN to leaner side. #36's surfaces (frame-total divergence on match-arm case-let; sibling offset divergence in variadic+iter+match-prev compositions) close naturally — running-max c.frame includes every first-use binding. selfhost/cmd/wcc: add atlocals persistent @-prefix registry surviving cgblock save/restore; add cgoutbuf/cgoutmode/cgout_enable/disable/flush for deferred prologue (emit body to buffer, finalise c.frame, then TEXT/SUBQ + flush); localadd @-prefix dedups against atlocals + fail-louds on size-grow (rule 7 — no silent truncate); cgreturn-tagged routes through @retscr (was colliding with @tagscr on arg-widen sizes); variadic gather esz uses raw primsize (rune->4) not slotsize (rune->8) — matches cstage and fixes the #36 sibling runtime miscompile in non-leaf variadic+iter+match-prev callees. cmd/w6c/cgen.c: drop the over-allocation hack ("for byte-id with wwstage scanlocals reservation") since wwstage no longer over-reserves; add fail-loud on @sretscr size-grow; @tagscr sites pass actual slot_sz instead of stale c.tagscrsz. 748_size_strategy_convergence: table-driven 4 rows x 2 stages (tag_variadic_runearm, trim_iter_match_prev, variadic_gather_rune_stride, leaf_baseline). Each exercises a #36 surface shape; 8/8 ok. Net -1565 lines. Sister latents filed as cosmetic (cs/ws frame size drift on multiple-variadic-call fns): labelseq drift + varargseq stuck at 0 — both bootstrap-byte-id safe (ww2==ww3==ww4 holds since both ww2 and ww3 are wwstage outputs). make test 122/122; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
This commit is contained in:
7
Makefile
7
Makefile
@@ -282,6 +282,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
|||||||
$(BIN)/test_fnret34_modshadow \
|
$(BIN)/test_fnret34_modshadow \
|
||||||
$(BIN)/test_strdef_inline \
|
$(BIN)/test_strdef_inline \
|
||||||
$(BIN)/test_def_modqual_modshadow \
|
$(BIN)/test_def_modqual_modshadow \
|
||||||
|
$(BIN)/test_size_strategy_convergence \
|
||||||
$(BIN)/test_param_shadow_mod \
|
$(BIN)/test_param_shadow_mod \
|
||||||
$(BIN)/test_localoff_scope \
|
$(BIN)/test_localoff_scope \
|
||||||
$(BIN)/test_cast_enum_movl \
|
$(BIN)/test_cast_enum_movl \
|
||||||
@@ -699,6 +700,12 @@ $(BIN)/test_def_modqual_modshadow: test/wcc/747_def_modqual_modshadow.c \
|
|||||||
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
|
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
$(BIN)/test_size_strategy_convergence: test/wcc/748_size_strategy_convergence.c \
|
||||||
|
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||||
|
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
|
||||||
|
$(LIB)/libwwrt.a | $(BIN)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
$(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \
|
$(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \
|
||||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||||
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
|
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
|
||||||
|
|||||||
102
cmd/w6c/cgen.c
102
cmd/w6c/cgen.c
@@ -51,17 +51,15 @@ static int cg_retscr;
|
|||||||
* cg_tagscr — sized scratch shared across THREE sites: cg_widen_
|
* cg_tagscr — sized scratch shared across THREE sites: cg_widen_
|
||||||
* tagged_store via_outer write target, cg_widen_tagged_
|
* tagged_store via_outer write target, cg_widen_tagged_
|
||||||
* push struct/tagged-source widen, N_INDEX tagged-element
|
* push struct/tagged-source widen, N_INDEX tagged-element
|
||||||
* assign. Mirrors wwstage @tagscr (cgenutil.ww:180 +
|
* assign. Mirrors wwstage @tagscr — wwstage shares the
|
||||||
* :2918, cgenexpr.ww:3524). Wwstage shares the slot via
|
* slot via localadd `@`-prefix dedup against c.atlocals.
|
||||||
* localadd `@`-prefix dedup, sized to `c.tagscrsz`
|
|
||||||
* (per-fn max computed by scanlocals pre-pass).
|
|
||||||
*
|
*
|
||||||
* Cstage has no pre-pass: first call across the 3 sites sizes the
|
* Both stages now size at first use and fatal() if a later site asks
|
||||||
* slot; subsequent calls (any of the 3 sites) reuse if sz ≤ cached,
|
* for more (rule 7: surface, don't silently corrupt the frame —
|
||||||
* fatal() if larger. Per rule 7: surface, don't silently corrupt the
|
* pinned offset can't grow in place once neighbours are allocated).
|
||||||
* frame. The size-strategy convergence (wwstage DOWN to first-use+
|
* Per-fn convergence completed by #15 (#26c follow-up): wwstage
|
||||||
* fail-loud on BOTH stages, or cstage UP to a scanlocals pre-pass) is
|
* dropped its scanlocals pre-pass and aligned DOWN to cstage's
|
||||||
* filed as #26c — separate concern. _sz tracks cached allocation size. */
|
* first-use shape. _sz tracks cached allocation size. */
|
||||||
static int cg_tagbase;
|
static int cg_tagbase;
|
||||||
static int cg_tagbase_sz;
|
static int cg_tagbase_sz;
|
||||||
static int cg_tagscr;
|
static int cg_tagscr;
|
||||||
@@ -1239,9 +1237,8 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
|
|||||||
if (cg_tagscr != 0) {
|
if (cg_tagscr != 0) {
|
||||||
if (sz > cg_tagscr_sz)
|
if (sz > cg_tagscr_sz)
|
||||||
fatal("cg_widen_tagged_store: @tagscr "
|
fatal("cg_widen_tagged_store: @tagscr "
|
||||||
"cached sz %d, need %d (per-fn slot "
|
"cached sz %d, need %d (pinned offset "
|
||||||
"growth needs scanlocals pre-pass — "
|
"can't grow in place; rule 7 — #15/#26c)",
|
||||||
"STATUS-4 #26c follow-up)",
|
|
||||||
cg_tagscr_sz, sz);
|
cg_tagscr_sz, sz);
|
||||||
write_off = cg_tagscr;
|
write_off = cg_tagscr;
|
||||||
} else {
|
} else {
|
||||||
@@ -1532,8 +1529,8 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
|
|||||||
if (cg_tagscr != 0) {
|
if (cg_tagscr != 0) {
|
||||||
if (sz > cg_tagscr_sz)
|
if (sz > cg_tagscr_sz)
|
||||||
fatal("cg_widen_tagged_push: @tagscr cached sz %d, "
|
fatal("cg_widen_tagged_push: @tagscr cached sz %d, "
|
||||||
"need %d (per-fn slot growth needs scanlocals "
|
"need %d (pinned offset can't grow in place; "
|
||||||
"pre-pass — STATUS-4 #26c follow-up)",
|
"rule 7 — #15/#26c)",
|
||||||
cg_tagscr_sz, sz);
|
cg_tagscr_sz, sz);
|
||||||
scr = cg_tagscr;
|
scr = cg_tagscr;
|
||||||
} else {
|
} else {
|
||||||
@@ -3518,10 +3515,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
|||||||
if (ssz > cg_tagscr_sz)
|
if (ssz > cg_tagscr_sz)
|
||||||
fatal("N_INDEX tagged: "
|
fatal("N_INDEX tagged: "
|
||||||
"@tagscr cached sz %d, "
|
"@tagscr cached sz %d, "
|
||||||
"need %d (per-fn slot "
|
"need %d (pinned offset "
|
||||||
"growth needs scanlocals "
|
"can't grow in place; "
|
||||||
"pre-pass — STATUS-4 #26c "
|
"rule 7 — #15/#26c)",
|
||||||
"follow-up)",
|
|
||||||
cg_tagscr_sz, ssz);
|
cg_tagscr_sz, ssz);
|
||||||
scr = cg_tagscr;
|
scr = cg_tagscr;
|
||||||
} else {
|
} else {
|
||||||
@@ -4318,8 +4314,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
|||||||
? (int)velem->size : 8;
|
? (int)velem->size : 8;
|
||||||
/* Allocate dname BEFORE sname so the
|
/* Allocate dname BEFORE sname so the
|
||||||
* descriptor lives below the element
|
* descriptor lives below the element
|
||||||
* buffer, matching the wwstage scanlocals
|
* buffer, matching wwstage's emit-time
|
||||||
* reservation order (rule 10). */
|
* order (rule 10). */
|
||||||
int doff = 0;
|
int doff = 0;
|
||||||
if (nvar > 0) {
|
if (nvar > 0) {
|
||||||
const char *dname = mklabel(c, "vararg_d");
|
const char *dname = mklabel(c, "vararg_d");
|
||||||
@@ -4612,21 +4608,31 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
|||||||
sret_call_sz = cg_sret_retsize(ret);
|
sret_call_sz = cg_sret_retsize(ret);
|
||||||
}
|
}
|
||||||
if (sret_call_sz > 0) {
|
if (sret_call_sz > 0) {
|
||||||
/* Always pre-allocate @sretscr at the first sret CALL
|
/* @sretscr is only needed when the result is dropped
|
||||||
* regardless of whether cg_sret_dest_off is set — keeps
|
* (no `let x = f();` receiver wired the call's dest into
|
||||||
* cstage's frame in lockstep with wwstage's
|
* cg_sret_dest_off). Allocate first-use per #15/#26c
|
||||||
* scanlocals-based reservation. Single-slot SSoT (cg_
|
* size-strategy convergence — wwstage's scanlocals pre-
|
||||||
* sretscr_off) mirrors @retscr / @tagscr conventions. */
|
* pass that used to reserve this slot unconditionally is
|
||||||
if (cg_sretscr_off == 0) {
|
* gone; cstage matches by skipping the allocation when a
|
||||||
cg_sretscr_off = local_alloc(c,
|
* dest is already wired. fatal() on a later sret CALL
|
||||||
&locals, "@sretscr",
|
* needing a bigger slot (rule 7 — pinned offset can't
|
||||||
sret_call_sz, cg_frame);
|
* grow in place). */
|
||||||
cg_sretscr_sz = sret_call_sz;
|
|
||||||
}
|
|
||||||
if (cg_sret_dest_off != 0) {
|
if (cg_sret_dest_off != 0) {
|
||||||
sret_call_off = cg_sret_dest_off;
|
sret_call_off = cg_sret_dest_off;
|
||||||
cg_sret_dest_off = 0;
|
cg_sret_dest_off = 0;
|
||||||
} else {
|
} else {
|
||||||
|
if (cg_sretscr_off == 0) {
|
||||||
|
cg_sretscr_off = local_alloc(c,
|
||||||
|
&locals, "@sretscr",
|
||||||
|
sret_call_sz, cg_frame);
|
||||||
|
cg_sretscr_sz = sret_call_sz;
|
||||||
|
} else if (sret_call_sz > cg_sretscr_sz) {
|
||||||
|
fatal("cgcall: @sretscr cached sz "
|
||||||
|
"%d, need %d (per-fn slot growth "
|
||||||
|
"post-#15 — pinned offset can't "
|
||||||
|
"grow in place)",
|
||||||
|
cg_sretscr_sz, sret_call_sz);
|
||||||
|
}
|
||||||
sret_call_off = cg_sretscr_off;
|
sret_call_off = cg_sretscr_off;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4708,9 +4714,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
|||||||
* forwards through an sret callee, source RDI from outer's
|
* forwards through an sret callee, source RDI from outer's
|
||||||
* saved @sretarg — inner writes directly into outer's
|
* saved @sretarg — inner writes directly into outer's
|
||||||
* caller-prealloc dest. No temporary in outer's frame.
|
* caller-prealloc dest. No temporary in outer's frame.
|
||||||
* The @sretscr slot was still allocated above for byte-id
|
* Post-#15 @sretscr is skipped entirely on the forwarding
|
||||||
* lockstep with wwstage's scanlocals reservation; it goes
|
* branch (no allocation, no frame growth) — earlier scan-
|
||||||
* unused on the forwarding branch. */
|
* lockstep reservation is gone. */
|
||||||
if (sret_call_sz > 0) {
|
if (sret_call_sz > 0) {
|
||||||
if (cg_sret_forward) {
|
if (cg_sret_forward) {
|
||||||
ins2(c, A_MOVQ,
|
ins2(c, A_MOVQ,
|
||||||
@@ -6768,19 +6774,19 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
|||||||
if (cg_retscr != 0) {
|
if (cg_retscr != 0) {
|
||||||
scr = cg_retscr;
|
scr = cg_retscr;
|
||||||
} else {
|
} else {
|
||||||
/* STATUS-3 #15: use the fixed
|
/* Fixed "@retscr" SSoT name —
|
||||||
* "@retscr" SSoT name (mirrors
|
* mirrors wwstage's localadd
|
||||||
* wwstage's c.retscroff pattern in
|
* @-prefix dedup. Pre-fix
|
||||||
* selfhost/cmd/wcc/cgen.ww). Pre-
|
* mklabel(c, "retscr") consumed
|
||||||
* fix mklabel(c, "retscr") consumed
|
|
||||||
* one labelseq counter slot per
|
* one labelseq counter slot per
|
||||||
* function with a tagged return,
|
* function with a tagged return,
|
||||||
* pushing every subsequent ct/ce/
|
* pushing every subsequent ct/ce/
|
||||||
* else/end label 1 ahead of wwstage.
|
* else/end label 1 ahead of
|
||||||
* Site 1 sentinel masked by latent
|
* wwstage. Site 1 sentinel
|
||||||
* struct-widen offset divergence
|
* masked by latent struct-widen
|
||||||
* (STATUS-3 #20/#21); fix is
|
* offset divergence (#20/#21);
|
||||||
* preventive symmetry per rule 10. */
|
* fix is preventive symmetry per
|
||||||
|
* rule 10. */
|
||||||
scr = local_alloc(c, locals,
|
scr = local_alloc(c, locals,
|
||||||
"@retscr", sz, cg_frame);
|
"@retscr", sz, cg_frame);
|
||||||
cg_retscr = scr;
|
cg_retscr = scr;
|
||||||
@@ -6936,9 +6942,9 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
|||||||
|| n->lhs->kind == N_STRUCTLIT)) {
|
|| n->lhs->kind == N_STRUCTLIT)) {
|
||||||
int sz = (int)rt->size;
|
int sz = (int)rt->size;
|
||||||
/* Single-slot @retscr (#14): see tagged arm
|
/* Single-slot @retscr (#14): see tagged arm
|
||||||
* above for rationale. STATUS-3 #15: fixed
|
* above for rationale. Fixed "@retscr" name
|
||||||
* "@retscr" name avoids bumping labelseq;
|
* avoids bumping labelseq; mirrors wwstage's
|
||||||
* mirrors wwstage's c.retscroff SSoT. */
|
* localadd @-prefix dedup. */
|
||||||
int scr;
|
int scr;
|
||||||
if (cg_retscr != 0) {
|
if (cg_retscr != 0) {
|
||||||
scr = cg_retscr;
|
scr = cg_retscr;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -377,6 +377,13 @@ type structinfo = struct {
|
|||||||
type local = struct {
|
type local = struct {
|
||||||
name: str,
|
name: str,
|
||||||
off: i32,
|
off: i32,
|
||||||
|
sz: i32, // allocated slot size; carried so @-prefix reuse can
|
||||||
|
// fail-loud (rule 7) if a later site needs a larger
|
||||||
|
// slot than the first allocation pinned. Per #15/#26c
|
||||||
|
// size-strategy convergence — wwstage dropped its
|
||||||
|
// scanlocals pre-pass, so @tagscr/@retscr/@sretscr/
|
||||||
|
// @tagbase are sized at first-use; subsequent uses
|
||||||
|
// must fit.
|
||||||
tnode: *node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil
|
tnode: *node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil
|
||||||
lnext: *local,
|
lnext: *local,
|
||||||
};
|
};
|
||||||
@@ -422,6 +429,17 @@ def DEFER_MAX: i32 = 16;
|
|||||||
type cgen = struct {
|
type cgen = struct {
|
||||||
a: *arena,
|
a: *arena,
|
||||||
locals: *local,
|
locals: *local,
|
||||||
|
// atlocals — persistent registry of `@`-prefix scratch slots
|
||||||
|
// for the current fn. cgblock save/restores c.locals to scope
|
||||||
|
// inner shadows (post-#27); a return/cgindex/cgwidentaggedstore
|
||||||
|
// inside one block must not reallocate @retscr/@tagscr when a
|
||||||
|
// sibling block uses them again. cgblock leaves atlocals alone
|
||||||
|
// so the slot offsets survive. localadd checks here first for
|
||||||
|
// @-prefix names; localfind falls back here when c.locals misses
|
||||||
|
// an @-name. Pre-#15 this was a handful of named offsets on the
|
||||||
|
// cgen (c.retscroff / c.sretargoff / c.sretscroff); post-#15
|
||||||
|
// every @-name flows through the same registry.
|
||||||
|
atlocals: *local,
|
||||||
frame: i32,
|
frame: i32,
|
||||||
lastwasreturn: i32,
|
lastwasreturn: i32,
|
||||||
labelseq: i32,
|
labelseq: i32,
|
||||||
@@ -451,55 +469,34 @@ type cgen = struct {
|
|||||||
yieldbuf: *str, // stack of match end labels for yield
|
yieldbuf: *str, // stack of match end labels for yield
|
||||||
defertop: i32,
|
defertop: i32,
|
||||||
deferbuf: **node, // stack of deferred exprs (LIFO at return)
|
deferbuf: **node, // stack of deferred exprs (LIFO at return)
|
||||||
// Variadic-call gather state. scanlocals walks the body in pre-
|
// Variadic-call gather state. cgcall assigns per-call scratch
|
||||||
// order DFS and assigns per-call scratch names `@vararg_d_N` /
|
// names `@vararg_d_N` / `@vararg_sl_N` using this counter;
|
||||||
// `@vararg_sl_N` using this counter; cgcall resets and walks in
|
// post #15 the seq is bumped only at emit time so a single
|
||||||
// the same order so the names line up at emission time.
|
// sequence is observed (the scanlocals pre-pass was dropped).
|
||||||
varargseq: i32,
|
varargseq: i32,
|
||||||
// Max @tagscr slot_sz across all reservation sites in the current
|
|
||||||
// function. scanlocals bumps; every emit-time `localadd("@tagscr",
|
|
||||||
// ...)` passes this same size so the first allocation lands a slot
|
|
||||||
// big enough for every later user. Single source of truth — pins
|
|
||||||
// rob's "scan + emit lockstep" invariant. Reset per cgfn.
|
|
||||||
tagscrsz: i32,
|
|
||||||
// Live @retscr offset (#14). c.locals-based `@`-prefix dedup in
|
|
||||||
// localadd is unwound by cgblock save/restore (post-#27), so a
|
|
||||||
// second `return` in a sibling/outer block reallocates a fresh
|
|
||||||
// slot — emit grew the frame past what scanlocals reserved, and
|
|
||||||
// the stomp landed below SP. retscroff is the persistent SSoT:
|
|
||||||
// 0 means "not yet allocated"; first emit-site sets it, every
|
|
||||||
// later emit reuses. Mirrors c.tagscrsz pattern (#38) but tracks
|
|
||||||
// offset, not size (per-fn return type is fixed, so size is too).
|
|
||||||
retscroff: i32,
|
|
||||||
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
|
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
|
||||||
// with size > 24B are passed via a hidden first-arg pointer
|
// with size > 24B are passed via a hidden first-arg pointer
|
||||||
// (RDI) to a caller-prealloc dest; the callee writes through
|
// (RDI) to a caller-prealloc dest; the callee writes through
|
||||||
// that pointer and returns it in RAX.
|
// that pointer and returns it in RAX.
|
||||||
//
|
//
|
||||||
// sretargoff — callee-side @sretarg slot (8B, holds saved RDI).
|
|
||||||
// Set in cgfn prologue when the fn's return type
|
|
||||||
// triggers sret. 0 means N/A.
|
|
||||||
// sretdestoff — caller-side dest BP offset, propagated from a
|
// sretdestoff — caller-side dest BP offset, propagated from a
|
||||||
// receive site (cglet / cgassign ident) to the
|
// receive site (cglet / cgassign ident) to the
|
||||||
// nested cgexpr → cgcall so the call emits
|
// nested cgexpr → cgcall so the call emits
|
||||||
// `LEAQ off(BP), DI` instead of allocating a
|
// `LEAQ off(BP), DI` instead of allocating a
|
||||||
// scratch. 0 means no receiver wired.
|
// scratch. 0 means no receiver wired.
|
||||||
// sretscroff — per-fn @sretscr discard slot, used by sret CALLs
|
|
||||||
// whose result has no named receiver. Single-slot
|
|
||||||
// SSoT mirroring c.retscroff; the scanlocals walk
|
|
||||||
// sums c.sretscrsz to pre-reserve.
|
|
||||||
// sretscrsz — max sret discard size in this fn (sums during
|
|
||||||
// scanlocals, consumed by localadd("@sretscr", ...)).
|
|
||||||
// sretforward — set by cgreturn `return f();` from an sret callee to
|
// sretforward — set by cgreturn `return f();` from an sret callee to
|
||||||
// signal cgcall: source RDI for inner from outer's
|
// signal cgcall: source RDI for inner from outer's
|
||||||
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
|
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
|
||||||
// dest. Inner writes into outer's caller-prealloc;
|
// dest. Inner writes into outer's caller-prealloc;
|
||||||
// inner's RAX (the dest pointer) is already outer's
|
// inner's RAX (the dest pointer) is already outer's
|
||||||
// return value. Cleared after cgcall consumes it.
|
// return value. Cleared after cgcall consumes it.
|
||||||
sretargoff: i32,
|
//
|
||||||
|
// The single-slot caches for @sretarg / @sretscr / @retscr that
|
||||||
|
// used to live here are gone: localadd's `@`-prefix dedup against
|
||||||
|
// c.locals (fail-loud on size grow) is the SSoT now. cgenstmt /
|
||||||
|
// cgenexpr resolve `@sretarg` via localfind when they need the
|
||||||
|
// saved RDI.
|
||||||
sretdestoff: i32,
|
sretdestoff: i32,
|
||||||
sretscroff: i32,
|
|
||||||
sretscrsz: i32,
|
|
||||||
sretforward: i32,
|
sretforward: i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -518,16 +515,12 @@ type letvar = struct {
|
|||||||
fn cgeninit(c: *cgen, a: *arena) void = {
|
fn cgeninit(c: *cgen, a: *arena) void = {
|
||||||
c.a = a;
|
c.a = a;
|
||||||
c.locals = nil;
|
c.locals = nil;
|
||||||
|
c.atlocals = nil;
|
||||||
c.frame = 0;
|
c.frame = 0;
|
||||||
c.lastwasreturn = 0;
|
c.lastwasreturn = 0;
|
||||||
c.labelseq = 0;
|
c.labelseq = 0;
|
||||||
c.varargseq = 0;
|
c.varargseq = 0;
|
||||||
c.tagscrsz = 0;
|
|
||||||
c.retscroff = 0;
|
|
||||||
c.sretargoff = 0;
|
|
||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
c.sretscroff = 0;
|
|
||||||
c.sretscrsz = 0;
|
|
||||||
c.sretforward = 0;
|
c.sretforward = 0;
|
||||||
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
||||||
// persist across cgfn calls within one file. cgfile resets them
|
// persist across cgfn calls within one file. cgfile resets them
|
||||||
@@ -542,10 +535,9 @@ fn cgeninit(c: *cgen, a: *arena) void = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// localalloc — append a slot for `name` without dedup. Used for
|
// localalloc — append a slot for `name` without dedup. Used for
|
||||||
// match-arm bindings, which C cgen allocates via cgexpr's by-value
|
// match-arm bindings, which cstage allocates via cgexpr's by-value
|
||||||
// `locals` list — so two separate matches each get fresh slots even
|
// `locals` list — so two separate matches each get fresh slots even
|
||||||
// when their bind names collide. scanlocals follows the same rule
|
// when their bind names collide.
|
||||||
// for nkind.N_MCASE.
|
|
||||||
fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
||||||
let asz: i32 = sz;
|
let asz: i32 = sz;
|
||||||
if (asz < 8) { asz = 8; };
|
if (asz < 8) { asz = 8; };
|
||||||
@@ -555,6 +547,7 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
|||||||
let l: *local = amalloc(c.a, 48u64): *local;
|
let l: *local = amalloc(c.a, 48u64): *local;
|
||||||
l.name = name;
|
l.name = name;
|
||||||
l.off = off;
|
l.off = off;
|
||||||
|
l.sz = asz;
|
||||||
l.tnode = tnode;
|
l.tnode = tnode;
|
||||||
l.lnext = c.locals;
|
l.lnext = c.locals;
|
||||||
c.locals = l;
|
c.locals = l;
|
||||||
@@ -570,6 +563,7 @@ fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = {
|
|||||||
let l: *local = amalloc(c.a, 48u64): *local;
|
let l: *local = amalloc(c.a, 48u64): *local;
|
||||||
l.name = name;
|
l.name = name;
|
||||||
l.off = off;
|
l.off = off;
|
||||||
|
l.sz = 0;
|
||||||
l.tnode = tnode;
|
l.tnode = tnode;
|
||||||
l.lnext = c.locals;
|
l.lnext = c.locals;
|
||||||
c.locals = l;
|
c.locals = l;
|
||||||
@@ -579,86 +573,60 @@ fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
|||||||
// User-let path (post-#27): always allocate a fresh slot per
|
// User-let path (post-#27): always allocate a fresh slot per
|
||||||
// binding. Pre-fix this deduped by name to share one slot
|
// binding. Pre-fix this deduped by name to share one slot
|
||||||
// across same-name lets in disjoint scopes — inherited from
|
// across same-name lets in disjoint scopes — inherited from
|
||||||
// C cgen's localoff. Both stages had the same silent-stack-
|
// cstage's localoff. Both stages had the same silent-stack-
|
||||||
// corruption bug: an inner 8B `let a: i64` allocated first
|
// corruption bug: an inner 8B `let a: i64` allocated first
|
||||||
// would force a later outer `let a: [128]u8` onto the 8B slot,
|
// would force a later outer `let a: [128]u8` onto the 8B slot,
|
||||||
// and `a[127]` would write at +119(BP), past the saved RIP.
|
// and `a[127]` would write at +119(BP), past the saved RIP.
|
||||||
// Localfind walks head-first, so the most-recent binding still
|
|
||||||
// wins lookups inside its scope. Tnode is carried on the
|
|
||||||
// freshly-pushed entry, so type dispatch in cgenutil never
|
|
||||||
// sees a stale predecessor.
|
|
||||||
//
|
//
|
||||||
// Synthetic scratch slots (`@tagscr`, `@retscr`, `@tagbase`)
|
// `@`-prefix scratch slots (`@tagscr`, `@retscr`, `@tagbase`,
|
||||||
// keep the per-fn dedup. Each scratch is sized identically
|
// `@sretarg`, `@sretscr`, `@match_spill`, `@vararg_*`) share
|
||||||
// across its call sites and intended to be shared — the
|
// one slot per name per fn. Post #15/#26c the slot is sized
|
||||||
// scanlocals pre-pass also dedups via scanseenmark, so frame
|
// at first use and reused by every later caller; a later
|
||||||
// reservation and emit-time allocation stay in sync. The
|
// caller asking for a larger slot than the first allocation
|
||||||
// `@`-prefix carve-out preserves that contract; user names
|
// pinned fatals (rule 7 — surface, don't silently corrupt
|
||||||
// can never start with `@` (lexer-rejected).
|
// the frame: the pinned offset already neighbours other
|
||||||
//
|
// locals so the slot can't grow in place). Mirrors cstage's
|
||||||
// @retscr (#14) routes through c.retscroff instead of c.locals.
|
// cg_tagscr / cg_retscr / cg_sretscr same-fn caches in
|
||||||
// The c.locals-based dedup is unwound by cgblock save/restore
|
// cmd/w6c/cgen.c (#26 / #15).
|
||||||
// (post-#27): a return inside an `if` block adds @retscr to
|
|
||||||
// c.locals; on block exit, c.locals reverts and a sibling/outer
|
|
||||||
// return reallocates a fresh slot. Scan had reserved one slot;
|
|
||||||
// emit grew the frame past the reservation and the second
|
|
||||||
// site's writes landed below SP. c.retscroff is per-fn state
|
|
||||||
// that survives cgblock save/restore and pins single-slot.
|
|
||||||
if (name.len > 0) {
|
if (name.len > 0) {
|
||||||
if (name[0] == 64u8) { // '@'
|
if (name[0] == 64u8) { // '@'
|
||||||
if (streq(name, "@retscr")) {
|
let asz: i32 = sz;
|
||||||
if (c.retscroff != 0) { return c.retscroff; };
|
if (asz < 8) { asz = 8; };
|
||||||
let off: i32 = localalloc(c, name, sz, tnode);
|
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
|
||||||
c.retscroff = off;
|
let cur: *local = c.atlocals;
|
||||||
return off;
|
|
||||||
};
|
|
||||||
// @sretarg / @sretscr (#23): same single-slot SSoT
|
|
||||||
// pattern as @retscr. @sretarg holds the saved hidden
|
|
||||||
// RDI for sret callees (8B, set once per fn at the
|
|
||||||
// prologue); @sretscr is the caller-side discard slot
|
|
||||||
// for sret CALLs whose result is dropped.
|
|
||||||
if (streq(name, "@sretarg")) {
|
|
||||||
if (c.sretargoff != 0) { return c.sretargoff; };
|
|
||||||
let off: i32 = localalloc(c, name, sz, tnode);
|
|
||||||
c.sretargoff = off;
|
|
||||||
return off;
|
|
||||||
};
|
|
||||||
if (streq(name, "@sretscr")) {
|
|
||||||
if (c.sretscroff != 0) { return c.sretscroff; };
|
|
||||||
let off: i32 = localalloc(c, name, sz, tnode);
|
|
||||||
c.sretscroff = off;
|
|
||||||
return off;
|
|
||||||
};
|
|
||||||
let cur: *local = c.locals;
|
|
||||||
for (cur != nil) {
|
for (cur != nil) {
|
||||||
let cn: str = cur.name;
|
let cn: str = cur.name;
|
||||||
if (streq(cn, name)) {
|
if (streq(cn, name)) {
|
||||||
|
if (asz > cur.sz) {
|
||||||
|
// rule-7 surface, post-#15: pinned slot
|
||||||
|
// offset can't grow in place.
|
||||||
|
let msg: str = "localadd: @-prefix slot grew within fn\n";
|
||||||
|
os.write(2, msg.ptr, msg.len: u64);
|
||||||
|
os.exit(1);
|
||||||
|
};
|
||||||
cur.tnode = tnode;
|
cur.tnode = tnode;
|
||||||
return cur.off;
|
return cur.off;
|
||||||
};
|
};
|
||||||
cur = cur.lnext;
|
cur = cur.lnext;
|
||||||
};
|
};
|
||||||
|
// First use: allocate via localalloc (bumps c.frame +
|
||||||
|
// pushes to c.locals so localfind sees it within this
|
||||||
|
// block) and pin a parallel entry in c.atlocals so the
|
||||||
|
// allocation survives cgblock save/restore.
|
||||||
|
let off: i32 = localalloc(c, name, sz, tnode);
|
||||||
|
let at: *local = amalloc(c.a, 48u64): *local;
|
||||||
|
at.name = name;
|
||||||
|
at.off = off;
|
||||||
|
at.sz = asz;
|
||||||
|
at.tnode = tnode;
|
||||||
|
at.lnext = c.atlocals;
|
||||||
|
c.atlocals = at;
|
||||||
|
return off;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
return localalloc(c, name, sz, tnode);
|
return localalloc(c, name, sz, tnode);
|
||||||
};
|
};
|
||||||
|
|
||||||
// scanseenmark — called by scanlocals on every let / match-bind
|
|
||||||
// site. Returns true if `name` is already tracked in c.locals (so
|
|
||||||
// the slot will be shared at emission time — no new frame bump).
|
|
||||||
// Otherwise appends a name-only stub and returns false. Stubs are
|
|
||||||
// thrown away when cgfn resets c.locals before emission.
|
|
||||||
fn scanseenmark(c: *cgen, name: str) bool = {
|
|
||||||
if (localfindnode(c, name) != nil) { return true; };
|
|
||||||
let l: *local = amalloc(c.a, 48u64): *local;
|
|
||||||
l.name = name;
|
|
||||||
l.off = 0;
|
|
||||||
l.tnode = nil;
|
|
||||||
l.lnext = c.locals;
|
|
||||||
c.locals = l;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
fn localfindnode(c: *cgen, name: str) *local = {
|
fn localfindnode(c: *cgen, name: str) *local = {
|
||||||
let l: *local = c.locals;
|
let l: *local = c.locals;
|
||||||
for (l != nil) {
|
for (l != nil) {
|
||||||
@@ -666,6 +634,18 @@ fn localfindnode(c: *cgen, name: str) *local = {
|
|||||||
if (streq(ln, name)) { return l; };
|
if (streq(ln, name)) { return l; };
|
||||||
l = l.lnext;
|
l = l.lnext;
|
||||||
};
|
};
|
||||||
|
// @-prefix scratch slots survive cgblock save/restore via
|
||||||
|
// c.atlocals; a localfindnode from a sibling/outer block must
|
||||||
|
// still resolve them.
|
||||||
|
if (name.len > 0) {
|
||||||
|
if (name[0] == 64u8) {
|
||||||
|
let a: *local = c.atlocals;
|
||||||
|
for (a != nil) {
|
||||||
|
if (streq(a.name, name)) { return a; };
|
||||||
|
a = a.lnext;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
return nil;
|
return nil;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -684,21 +664,92 @@ fn localfind(c: *cgen, name: str) i32 = {
|
|||||||
};
|
};
|
||||||
l = l.lnext;
|
l = l.lnext;
|
||||||
};
|
};
|
||||||
|
if (name.len > 0) {
|
||||||
|
if (name[0] == 64u8) {
|
||||||
|
let a: *local = c.atlocals;
|
||||||
|
for (a != nil) {
|
||||||
|
if (streq(a.name, name)) { return a.off; };
|
||||||
|
a = a.lnext;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- emit helpers ---------------------------------------------------
|
// ---- emit helpers ---------------------------------------------------
|
||||||
|
|
||||||
fn emitline(s: str) void = { os.write(1, s.ptr, s.len: u64); };
|
// Cgfn defers its prologue (TEXT / SUBQ) until after the body so the
|
||||||
|
// frame size reflects every emit-time localadd — the scanlocals pre-
|
||||||
|
// pass that previously pre-computed it was dropped per #15/#26c. The
|
||||||
|
// body is captured into cgoutbuf while cgoutmode != 0, then flushed
|
||||||
|
// after the prologue is written to stdout. Module-level state so the
|
||||||
|
// existing emitline/emitint/emitlabel/emitsymname callers don't have
|
||||||
|
// to thread a *cgen they don't already hold. Mirrors cstage's deferred
|
||||||
|
// Prog-chain emit (cmd/w6c/cgen.c cgfn allocates `subsp`/`text` up
|
||||||
|
// front and patches `from.offset` after the body finishes).
|
||||||
|
let cgoutbuf: *u8 = nil;
|
||||||
|
let cgoutbufcap: i32 = 0;
|
||||||
|
let cgoutbuflen: i32 = 0;
|
||||||
|
let cgoutmode: i32 = 0;
|
||||||
|
let cgoutarena: *arena = nil;
|
||||||
|
|
||||||
|
def CGOUT_INIT_CAP: i32 = 65536;
|
||||||
|
|
||||||
|
fn cgout_grow(need: i32) void = {
|
||||||
|
if (need <= cgoutbufcap) { return; };
|
||||||
|
let want: i32 = cgoutbufcap;
|
||||||
|
if (want == 0) { want = CGOUT_INIT_CAP; };
|
||||||
|
for (want < need) { want = want * 2; };
|
||||||
|
let p: *u8 = amalloc(cgoutarena, want: u64): *u8;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < cgoutbuflen) {
|
||||||
|
p[i] = cgoutbuf[i];
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
cgoutbuf = p;
|
||||||
|
cgoutbufcap = want;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn cgout_enable(a: *arena) void = {
|
||||||
|
cgoutarena = a;
|
||||||
|
cgoutbuflen = 0;
|
||||||
|
cgoutmode = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn cgout_disable() void = { cgoutmode = 0; };
|
||||||
|
|
||||||
|
fn cgout_flush() void = {
|
||||||
|
if (cgoutbuflen > 0) {
|
||||||
|
os.write(1, cgoutbuf, cgoutbuflen: u64);
|
||||||
|
cgoutbuflen = 0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn emitbytes(p: *u8, n: u64) void = {
|
||||||
|
if (cgoutmode != 0) {
|
||||||
|
let nn: i32 = n: i32;
|
||||||
|
cgout_grow(cgoutbuflen + nn);
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < nn) {
|
||||||
|
cgoutbuf[cgoutbuflen + i] = p[i];
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
cgoutbuflen += nn;
|
||||||
|
} else {
|
||||||
|
os.write(1, p, n);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn emitline(s: str) void = { emitbytes(s.ptr, s.len: u64); };
|
||||||
|
|
||||||
fn emitint(v: i64) void = {
|
fn emitint(v: i64) void = {
|
||||||
let s: str = strconv.i64tos(v, strconv.base.DEC);
|
let s: str = strconv.i64tos(v, strconv.base.DEC);
|
||||||
os.write(1, s.ptr, s.len: u64);
|
emitbytes(s.ptr, s.len: u64);
|
||||||
};
|
};
|
||||||
|
|
||||||
fn emituint(v: u64) void = {
|
fn emituint(v: u64) void = {
|
||||||
let s: str = strconv.u64tos(v, strconv.base.DEC);
|
let s: str = strconv.u64tos(v, strconv.base.DEC);
|
||||||
os.write(1, s.ptr, s.len: u64);
|
emitbytes(s.ptr, s.len: u64);
|
||||||
};
|
};
|
||||||
|
|
||||||
// emitdispreg — print "disp(reg)" or "(reg)" when disp == 0, the
|
// emitdispreg — print "disp(reg)" or "(reg)" when disp == 0, the
|
||||||
@@ -755,7 +806,7 @@ fn mklabel(c: *cgen, prefix: str) str = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn emitlabel(s: str) void = {
|
fn emitlabel(s: str) void = {
|
||||||
os.write(1, s.ptr, s.len: u64);
|
emitbytes(s.ptr, s.len: u64);
|
||||||
emitline(":\n");
|
emitline(":\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1076,7 +1127,7 @@ fn emitdatawbyte(b: u8) void = {
|
|||||||
else { bb[0] = (hi - 10u8) + 97u8; };
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
||||||
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
||||||
else { bb[1] = (lo - 10u8) + 97u8; };
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
||||||
os.write(1, bb.ptr, 2u64);
|
emitbytes( bb.ptr, 2u64);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (b >= 127u8) {
|
if (b >= 127u8) {
|
||||||
@@ -1088,12 +1139,12 @@ fn emitdatawbyte(b: u8) void = {
|
|||||||
else { bb[0] = (hi - 10u8) + 97u8; };
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
||||||
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
||||||
else { bb[1] = (lo - 10u8) + 97u8; };
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
||||||
os.write(1, bb.ptr, 2u64);
|
emitbytes( bb.ptr, 2u64);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let bb: [1]u8;
|
let bb: [1]u8;
|
||||||
bb[0] = b;
|
bb[0] = b;
|
||||||
os.write(1, bb.ptr, 1u64);
|
emitbytes( bb.ptr, 1u64);
|
||||||
};
|
};
|
||||||
|
|
||||||
// letpreintern — intern strlits referenced from top-level str-let
|
// letpreintern — intern strlits referenced from top-level str-let
|
||||||
@@ -1258,7 +1309,7 @@ fn emitletdataw(c: *cgen, file: *node) void = {
|
|||||||
emitline("DATAR ");
|
emitline("DATAR ");
|
||||||
emitsymname(c, nm);
|
emitsymname(c, nm);
|
||||||
emitline("+0(SB),");
|
emitline("+0(SB),");
|
||||||
os.write(1, lab.ptr, lab.len: u64);
|
emitbytes( lab.ptr, lab.len: u64);
|
||||||
emitline("(SB)\n");
|
emitline("(SB)\n");
|
||||||
} else {
|
} else {
|
||||||
// zero-init: accept no rhs, nil,
|
// zero-init: accept no rhs, nil,
|
||||||
@@ -1426,12 +1477,12 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
|
|||||||
emitline("DATA ");
|
emitline("DATA ");
|
||||||
if (d.exported == 0) {
|
if (d.exported == 0) {
|
||||||
if (d.nmod.len > 0) {
|
if (d.nmod.len > 0) {
|
||||||
os.write(1, d.nmod.ptr, d.nmod.len: u64);
|
emitbytes( d.nmod.ptr, d.nmod.len: u64);
|
||||||
os.write(1, ".".ptr, 1u64);
|
emitbytes( ".".ptr, 1u64);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
let nm: str = d.str;
|
let nm: str = d.str;
|
||||||
os.write(1, nm.ptr, nm.len: u64);
|
emitbytes( nm.ptr, nm.len: u64);
|
||||||
emitline("(SB),\"");
|
emitline("(SB),\"");
|
||||||
let i: i32 = 0;
|
let i: i32 = 0;
|
||||||
let n: u64 = v;
|
let n: u64 = v;
|
||||||
@@ -1452,7 +1503,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
|
|||||||
else { bb[0] = (hi - 10u8) + 97u8; };
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
||||||
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
||||||
else { bb[1] = (lo - 10u8) + 97u8; };
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
||||||
os.write(1, bb.ptr, 2u64);
|
emitbytes( bb.ptr, 2u64);
|
||||||
} else {
|
} else {
|
||||||
if (b >= 127u8) {
|
if (b >= 127u8) {
|
||||||
emitline("\\x");
|
emitline("\\x");
|
||||||
@@ -1463,11 +1514,11 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
|
|||||||
else { bb[0] = (hi - 10u8) + 97u8; };
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
||||||
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
||||||
else { bb[1] = (lo - 10u8) + 97u8; };
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
||||||
os.write(1, bb.ptr, 2u64);
|
emitbytes( bb.ptr, 2u64);
|
||||||
} else {
|
} else {
|
||||||
let bb: [1]u8;
|
let bb: [1]u8;
|
||||||
bb[0] = b;
|
bb[0] = b;
|
||||||
os.write(1, bb.ptr, 1u64);
|
emitbytes( bb.ptr, 1u64);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};};
|
};};
|
||||||
@@ -1487,7 +1538,7 @@ fn emitdatasection(c: *cgen) void = {
|
|||||||
for (s != nil) {
|
for (s != nil) {
|
||||||
emitline("DATA ");
|
emitline("DATA ");
|
||||||
let lab: str = s.label;
|
let lab: str = s.label;
|
||||||
os.write(1, lab.ptr, lab.len: u64);
|
emitbytes( lab.ptr, lab.len: u64);
|
||||||
emitline("(SB),\"");
|
emitline("(SB),\"");
|
||||||
let bs: str = s.bytes;
|
let bs: str = s.bytes;
|
||||||
let i: i32 = 0;
|
let i: i32 = 0;
|
||||||
@@ -1508,7 +1559,7 @@ fn emitdatasection(c: *cgen) void = {
|
|||||||
else { bb[0] = (hi - 10u8) + 97u8; };
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
||||||
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
||||||
else { bb[1] = (lo - 10u8) + 97u8; };
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
||||||
os.write(1, bb.ptr, 2u64);
|
emitbytes( bb.ptr, 2u64);
|
||||||
} else {
|
} else {
|
||||||
if (b >= 127u8) {
|
if (b >= 127u8) {
|
||||||
emitline("\\x");
|
emitline("\\x");
|
||||||
@@ -1519,11 +1570,11 @@ fn emitdatasection(c: *cgen) void = {
|
|||||||
else { bb[0] = (hi - 10u8) + 97u8; };
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
||||||
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
||||||
else { bb[1] = (lo - 10u8) + 97u8; };
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
||||||
os.write(1, bb.ptr, 2u64);
|
emitbytes( bb.ptr, 2u64);
|
||||||
} else {
|
} else {
|
||||||
let bb: [1]u8;
|
let bb: [1]u8;
|
||||||
bb[0] = b;
|
bb[0] = b;
|
||||||
os.write(1, bb.ptr, 1u64);
|
emitbytes( bb.ptr, 1u64);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};};};};};
|
};};};};};
|
||||||
@@ -1891,15 +1942,15 @@ fn emitsymname(c: *cgen, ident: str) void = {
|
|||||||
let resolved: str = ffiresolve(c, ident);
|
let resolved: str = ffiresolve(c, ident);
|
||||||
if (resolved.ptr != ident.ptr) {
|
if (resolved.ptr != ident.ptr) {
|
||||||
// FFI hit — emit the mapped linker symbol verbatim.
|
// FFI hit — emit the mapped linker symbol verbatim.
|
||||||
os.write(1, resolved.ptr, resolved.len: u64);
|
emitbytes( resolved.ptr, resolved.len: u64);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let mod: str = modlookup(c, ident);
|
let mod: str = modlookup(c, ident);
|
||||||
if (mod.len > 0) {
|
if (mod.len > 0) {
|
||||||
os.write(1, mod.ptr, mod.len: u64);
|
emitbytes( mod.ptr, mod.len: u64);
|
||||||
os.write(1, ".".ptr, 1u64);
|
emitbytes( ".".ptr, 1u64);
|
||||||
};
|
};
|
||||||
os.write(1, ident.ptr, ident.len: u64);
|
emitbytes( ident.ptr, ident.len: u64);
|
||||||
};
|
};
|
||||||
|
|
||||||
// emitfnname — write the asm symbol name for a fn `ident`, threading
|
// emitfnname — write the asm symbol name for a fn `ident`, threading
|
||||||
@@ -1910,15 +1961,15 @@ fn emitsymname(c: *cgen, ident: str) void = {
|
|||||||
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
|
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
|
||||||
let resolved: str = ffiresolve(c, ident);
|
let resolved: str = ffiresolve(c, ident);
|
||||||
if (resolved.ptr != ident.ptr) {
|
if (resolved.ptr != ident.ptr) {
|
||||||
os.write(1, resolved.ptr, resolved.len: u64);
|
emitbytes( resolved.ptr, resolved.len: u64);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let mod: str = modlookupforfn(c, ident, hint);
|
let mod: str = modlookupforfn(c, ident, hint);
|
||||||
if (mod.len > 0) {
|
if (mod.len > 0) {
|
||||||
os.write(1, mod.ptr, mod.len: u64);
|
emitbytes( mod.ptr, mod.len: u64);
|
||||||
os.write(1, ".".ptr, 1u64);
|
emitbytes( ".".ptr, 1u64);
|
||||||
};
|
};
|
||||||
os.write(1, ident.ptr, ident.len: u64);
|
emitbytes( ident.ptr, ident.len: u64);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- FFI map ---------------------------------------------------------
|
// ---- FFI map ---------------------------------------------------------
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww.
|
// selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww.
|
||||||
//
|
//
|
||||||
// Houses the top-level emission glue:
|
// Houses the top-level emission glue:
|
||||||
// - scanlocals: frame pre-scan that counts each local `let`
|
|
||||||
// - cgfnparams: parameter spilling per SysV
|
// - cgfnparams: parameter spilling per SysV
|
||||||
// - cgfn: fn prologue + body + epilogue
|
// - cgfn: fn body emit (TEXT/SUBQ patched after body), prologue
|
||||||
|
// deferred via cgen.ww's cgoutbuf so the frame size
|
||||||
|
// reflects every emit-time localadd (#15/#26c)
|
||||||
// - cgfile: file-level entry (the exported driver)
|
// - cgfile: file-level entry (the exported driver)
|
||||||
//
|
//
|
||||||
// Bundler pulls this in transitively via cgen.ww; consumers don't
|
// Bundler pulls this in transitively via cgen.ww; consumers don't
|
||||||
@@ -19,519 +20,6 @@ import typ;
|
|||||||
import sym;
|
import sym;
|
||||||
import strconv;
|
import strconv;
|
||||||
|
|
||||||
// tagscrbump — record that the body needs an @tagscr scratch slot of at
|
|
||||||
// least `need` bytes and return how many additional frame bytes that
|
|
||||||
// imposes. Each tagged-scratch reservation site calls this; the first
|
|
||||||
// raises c.tagscrsz from 0, later sites only grow it when they need
|
|
||||||
// more. Closes STATUS latent #1: pre-fix every site reserved a flat 24B
|
|
||||||
// and the slot under-allocated for any tagged-union with a 24B+ payload
|
|
||||||
// (e.g. `(void | err)` where `err` is 24B → slot_sz 32). Cgen-side
|
|
||||||
// emit (cgreturn / pushargsrev / cgindex / cgwidentaggedstore) reads
|
|
||||||
// c.tagscrsz to allocate the actual slot — scan + emit see the same
|
|
||||||
// number, so rob's "lockstep" invariant holds. The @tagscr lifetime is
|
|
||||||
// short-lived per use (zero, fill, copy out), and uses are sequential
|
|
||||||
// within a fn body, so sharing the max is safe.
|
|
||||||
fn tagscrbump(c: *cgen, need: i32) i32 = {
|
|
||||||
let n: i32 = need;
|
|
||||||
if (n < 8) { n = 8; };
|
|
||||||
if ((n & 7) != 0) { n = (n + 7) & ~7; };
|
|
||||||
if (n <= c.tagscrsz) { return 0; };
|
|
||||||
let delta: i32 = n - c.tagscrsz;
|
|
||||||
c.tagscrsz = n;
|
|
||||||
return delta;
|
|
||||||
};
|
|
||||||
|
|
||||||
// sretscrbump — sister of tagscrbump for the sret discard slot
|
|
||||||
// (#23). Tracks max sret return type used as a CALL discard / nested
|
|
||||||
// receiver. Returns frame-byte delta vs the previous high water mark
|
|
||||||
// (rounded up to 8B).
|
|
||||||
fn sretscrbump(c: *cgen, need: i32) i32 = {
|
|
||||||
let n: i32 = need;
|
|
||||||
if (n < 8) { n = 8; };
|
|
||||||
if ((n & 7) != 0) { n = (n + 7) & ~7; };
|
|
||||||
if (n <= c.sretscrsz) { return 0; };
|
|
||||||
let delta: i32 = n - c.sretscrsz;
|
|
||||||
c.sretscrsz = n;
|
|
||||||
return delta;
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Recursively walks the body to count every local `let`. Each gets a
|
|
||||||
// slot sized by slotsize(typ); 8-byte default. Match-bindings + for-
|
|
||||||
// init lets count too. Params are added by the cgfn driver.
|
|
||||||
|
|
||||||
fn scanlocals(c: *cgen, n: *node) i32 = {
|
|
||||||
if (n == nil) { return 0; };
|
|
||||||
let total: i32 = 0;
|
|
||||||
if (n.kind == nkind.N_LET) {
|
|
||||||
// Match localadd's rounding: < 8 bumps to 8, then 8-align.
|
|
||||||
// scanlocals must agree with localadd or the prologue
|
|
||||||
// SUBQ undersizes the frame and lets overflow into the
|
|
||||||
// caller's stack — corrupting whatever's at -frameSize..-1
|
|
||||||
// of the caller. Post-#27 every let allocates fresh (no
|
|
||||||
// name dedup), so we always count + always append a stub.
|
|
||||||
// The stub carries n.lhs as tnode so later scanlocals
|
|
||||||
// nodes can dispatch on type — e.g. detecting `arr[i] = ...`
|
|
||||||
// where arr is a tagged-element array (needs @tagscr).
|
|
||||||
// localfindnode walks head-first, so the freshest stub
|
|
||||||
// (innermost binding) wins lookup.
|
|
||||||
let sz: i32 = letslotsize(c, n);
|
|
||||||
if (sz < 8) { sz = 8; };
|
|
||||||
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
|
|
||||||
total += sz;
|
|
||||||
let stub: *local = amalloc(c.a, 48u64): *local;
|
|
||||||
stub.name = n.str;
|
|
||||||
stub.off = 0;
|
|
||||||
stub.tnode = n.lhs;
|
|
||||||
stub.lnext = c.locals;
|
|
||||||
c.locals = stub;
|
|
||||||
};
|
|
||||||
// Multi-let from a tuple-returning call: each binding's size
|
|
||||||
// comes from its annotated type (l.lhs) when present, else from
|
|
||||||
// the rhs call's return-tuple element type. Marking via
|
|
||||||
// scanseenmark also dedupes the recursive descent into n.list
|
|
||||||
// so each child isn't counted again at the default 8B.
|
|
||||||
if (n.kind == nkind.N_MLET) {
|
|
||||||
let p0t: *node = nil;
|
|
||||||
let p1t: *node = nil;
|
|
||||||
if (n.rhs != nil) {
|
|
||||||
if (n.rhs.kind == nkind.N_CALL) {
|
|
||||||
let callee: *node = n.rhs.lhs;
|
|
||||||
if (callee != nil) {
|
|
||||||
let cnm: str;
|
|
||||||
cnm.ptr = nil; cnm.len = 0;
|
|
||||||
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
|
|
||||||
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
|
|
||||||
if (cnm.len > 0) {
|
|
||||||
let rt: *node = fnretlookup(c, cnm);
|
|
||||||
if (rt != nil) {
|
|
||||||
if (rt.kind == nkind.N_TTUPLE) {
|
|
||||||
p0t = rt.list;
|
|
||||||
if (p0t != nil) { p1t = p0t.next; };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
let l: *node = n.list;
|
|
||||||
let pt: *node = p0t;
|
|
||||||
let bidx: i32 = 0;
|
|
||||||
for (l != nil) {
|
|
||||||
let t: *node = l.lhs;
|
|
||||||
if (t == nil) {
|
|
||||||
if (bidx == 0) { t = p0t; };
|
|
||||||
if (bidx == 1) { t = p1t; };
|
|
||||||
};
|
|
||||||
let sz: i32 = 8;
|
|
||||||
if (t != nil) { sz = slotsize(c, t); };
|
|
||||||
if (sz < 8) { sz = 8; };
|
|
||||||
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
|
|
||||||
total += sz;
|
|
||||||
// Always-fresh stub (post-#27); tnode carries the
|
|
||||||
// binding's type so later array-index dispatch can
|
|
||||||
// resolve the let through localfindnode.
|
|
||||||
let stub: *local = amalloc(c.a, 48u64): *local;
|
|
||||||
stub.name = l.str;
|
|
||||||
stub.off = 0;
|
|
||||||
stub.tnode = t;
|
|
||||||
stub.lnext = c.locals;
|
|
||||||
c.locals = stub;
|
|
||||||
l = l.next;
|
|
||||||
bidx += 1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// `switch` allocates an 8B scratch slot for the scrutinee so case
|
|
||||||
// bodies can spill through SP without losing it. The slot is named
|
|
||||||
// ".sw_<labelseq>" at cgen time — unique per switch — so it must
|
|
||||||
// not dedup. Count it here so the frame SUBQ matches.
|
|
||||||
if (n.kind == nkind.N_SWITCH) { total += 8; };
|
|
||||||
// `for (let x .. s)` allocates two 8B scratch slots — `.rgi_<seq>`
|
|
||||||
// (counter) and `.rgl_<seq>` (length) — plus one slot per binding.
|
|
||||||
// Per-binding sz defaults to 8 (covers scalar primitives + ptrs).
|
|
||||||
// `str` tuple-fields would need 16 — selfhost doesn't yet emit
|
|
||||||
// those, so the simple count tracks C cgen for current fixtures.
|
|
||||||
if (n.kind == nkind.N_FORRANGE) {
|
|
||||||
total += 16; // .rgi + .rgl scratch
|
|
||||||
// Each forrange binding gets a fresh 8B slot (post-#27).
|
|
||||||
// Stub is also appended so the body's references resolve
|
|
||||||
// to this binding via head-first localfindnode lookup.
|
|
||||||
if (n.list != nil) {
|
|
||||||
let m: *node = n.list;
|
|
||||||
for (m != nil) {
|
|
||||||
let bnm: str = m.str;
|
|
||||||
total += 8;
|
|
||||||
if (bnm.len > 0) {
|
|
||||||
let stub: *local = amalloc(c.a, 48u64): *local;
|
|
||||||
stub.name = bnm;
|
|
||||||
stub.off = 0;
|
|
||||||
stub.tnode = m.lhs;
|
|
||||||
stub.lnext = c.locals;
|
|
||||||
c.locals = stub;
|
|
||||||
};
|
|
||||||
m = m.next;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
let bnm: str = n.str;
|
|
||||||
total += 8;
|
|
||||||
if (bnm.len > 0) {
|
|
||||||
let stub: *local = amalloc(c.a, 48u64): *local;
|
|
||||||
stub.name = bnm;
|
|
||||||
stub.off = 0;
|
|
||||||
stub.tnode = nil;
|
|
||||||
stub.lnext = c.locals;
|
|
||||||
c.locals = stub;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// `match (non-ident)` needs an `@match_spill` scratch slot sized
|
|
||||||
// to the scrutinee's tagged-union slot (16/24/32 for 1/2/3-word
|
|
||||||
// payload). Mirrors cstage's `slot_size = su->size` default 16
|
|
||||||
// in cmd/w6c/cgen.c cgmatch (task #9 align-down to cstage).
|
|
||||||
// matchspillsz must agree with cgmatch's emit-time computation
|
|
||||||
// for scan+emit lockstep. N_IDENT scrutinees read the slot
|
|
||||||
// directly off the local — no spill needed.
|
|
||||||
if (n.kind == nkind.N_MATCH) {
|
|
||||||
let sc: *node = n.lhs;
|
|
||||||
if (sc != nil) {
|
|
||||||
if (sc.kind != nkind.N_IDENT) {
|
|
||||||
total += matchspillsz(c, matchscrutt(c, sc));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// Match-arm binding (`case let v: T => ...`) gets a slot too.
|
|
||||||
// Crucially we do NOT dedup these against c.locals: C cgen
|
|
||||||
// handles a match as an expression with a by-value locals copy,
|
|
||||||
// so two separate matches in the same function each allocate
|
|
||||||
// their `v`/`e` slots fresh. Treating these as deduped would
|
|
||||||
// shrink the frame below what localadd then bumps it to.
|
|
||||||
if (n.kind == nkind.N_MCASE) {
|
|
||||||
let bn: str = n.str;
|
|
||||||
if (bn.len > 0) {
|
|
||||||
let pat: *node = n.lhs;
|
|
||||||
if (pat != nil) {
|
|
||||||
// Must mirror cgmatch's bind-slot sizing in
|
|
||||||
// cgenexpr.ww (`bsz = slotsize(c, pat)`):
|
|
||||||
// hardcoding str/slice/8 here underbooked the
|
|
||||||
// frame for TY_STRUCT variants — the emit-time
|
|
||||||
// localalloc(bsz=24) then wrote past the SUBQ'd
|
|
||||||
// SP, smashing whatever the OS put under it
|
|
||||||
// (project #31).
|
|
||||||
let psz: i32 = slotsize(c, pat);
|
|
||||||
if (psz <= 0) { psz = 8; };
|
|
||||||
if ((psz & 7) != 0) { psz = (psz + 7) & ~7; };
|
|
||||||
total += psz;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// 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 an @tagscr scratch slot for
|
|
||||||
// cgwidentaggedstore to materialise the source in before copying
|
|
||||||
// to the element address. Slot is shared per function via
|
|
||||||
// c.tagscrsz (raised to the largest element slot_sz seen).
|
|
||||||
// N_DOT-base graduated (task #30) so `obj.arr[i] = v` for an
|
|
||||||
// [N]Tagged struct field pre-reserves the slot — parallels the
|
|
||||||
// cgassign N_DOT-base elemtn fix in cgenexpr.ww. Without this
|
|
||||||
// arm the runtime localadd allocates past the frame boundary
|
|
||||||
// and clobbers live locals; cstage handles the shape naturally
|
|
||||||
// via first-use+fail-loud (rule-10 convergence filed as #15).
|
|
||||||
if (n.kind == nkind.N_ASSIGN) {
|
|
||||||
let alhs: *node = n.lhs;
|
|
||||||
if (alhs != nil) {
|
|
||||||
if (alhs.kind == nkind.N_INDEX) {
|
|
||||||
let abase: *node = alhs.lhs;
|
|
||||||
if (abase != nil) {
|
|
||||||
if (abase.kind == nkind.N_IDENT) {
|
|
||||||
let lc: *local = localfindnode(c, abase.str);
|
|
||||||
let btn: *node = nil;
|
|
||||||
if (lc != nil) { btn = lc.tnode; }
|
|
||||||
else { btn = letvartnode(c, abase.str); };
|
|
||||||
if (btn != nil) {
|
|
||||||
let bk: nkind = btn.kind;
|
|
||||||
let etn: *node = nil;
|
|
||||||
if (bk == nkind.N_TARRAY) { etn = btn.lhs; };
|
|
||||||
if (bk == nkind.N_TSLICE) { etn = btn.lhs; };
|
|
||||||
if (bk == nkind.N_TPTR) { etn = btn.lhs; };
|
|
||||||
if (etn != nil) {
|
|
||||||
if (istaggedtype(c, etn)) {
|
|
||||||
total += tagscrbump(c, slotsize(c, etn));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (abase.kind == nkind.N_DOT) {
|
|
||||||
let ft: *node = dotfieldtnode(c, abase);
|
|
||||||
if (ft != nil) {
|
|
||||||
let bk: nkind = ft.kind;
|
|
||||||
let etn: *node = nil;
|
|
||||||
if (bk == nkind.N_TARRAY) { etn = ft.lhs; };
|
|
||||||
if (bk == nkind.N_TSLICE) { etn = ft.lhs; };
|
|
||||||
if (bk == nkind.N_TPTR) { etn = ft.lhs; };
|
|
||||||
if (etn != nil) {
|
|
||||||
if (istaggedtype(c, etn)) {
|
|
||||||
total += tagscrbump(c, slotsize(c, etn));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// Tagged-union struct-field write: `s.f = v` or `(*p).f = v`
|
|
||||||
// where f is a tagged-union field. cgassign delegates to
|
|
||||||
// cgwidentaggedstore; for pointer-rooted dst the wrapper
|
|
||||||
// allocates @tagbase (8B, fixed) and @tagscr (sized to the
|
|
||||||
// field's tagged slot). The @tagscr size feeds c.tagscrsz.
|
|
||||||
if (n.kind == nkind.N_ASSIGN) {
|
|
||||||
let alhs: *node = n.lhs;
|
|
||||||
if (alhs != nil) {
|
|
||||||
if (alhs.kind == nkind.N_DOT) {
|
|
||||||
let abase: *node = alhs.lhs;
|
|
||||||
if (abase != nil) {
|
|
||||||
if (abase.kind == nkind.N_IDENT) {
|
|
||||||
let lc: *local = localfindnode(c, abase.str);
|
|
||||||
let btn: *node = nil;
|
|
||||||
if (lc != nil) { btn = lc.tnode; }
|
|
||||||
else { btn = letvartnode(c, abase.str); };
|
|
||||||
let isptr: bool = false;
|
|
||||||
let stype: *node = nil;
|
|
||||||
if (btn != nil) {
|
|
||||||
if (btn.kind == nkind.N_TPTR) {
|
|
||||||
isptr = true;
|
|
||||||
stype = btn.lhs;
|
|
||||||
};
|
|
||||||
if (btn.kind == nkind.N_TNAME) { stype = btn; };
|
|
||||||
};
|
|
||||||
if (stype != nil) {
|
|
||||||
if (stype.kind == nkind.N_TNAME) {
|
|
||||||
let si: *structinfo = structlookup(c, stype.str);
|
|
||||||
if (si != nil) {
|
|
||||||
let fi: *fieldinfo = si.fields;
|
|
||||||
for (fi != nil) {
|
|
||||||
if (streq(fi.fname, alhs.str)) {
|
|
||||||
if (istaggedtype(c, fi.tnode)) {
|
|
||||||
if (isptr) {
|
|
||||||
if (!scanseenmark(c, "@tagbase")) {
|
|
||||||
total += 8;
|
|
||||||
};
|
|
||||||
total += tagscrbump(c, slotsize(c, fi.tnode));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
fi = nil;
|
|
||||||
} else {
|
|
||||||
fi = fi.finext;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// Tagged-union return with struct payload or tagged-subset
|
|
||||||
// source — cgreturn materialises in @tagscr then loads AX/DX/
|
|
||||||
// CX/R8. Detect via the same rhsstructpayload predicate the
|
|
||||||
// cgen uses, so we only reserve when the cgen will actually
|
|
||||||
// emit a scratch-using path. `!void` / `!i32` aliases share
|
|
||||||
// N_STRUCTLIT shape but resolve to non-struct types — they
|
|
||||||
// fall through to scalar/str and don't need scratch. Slot is
|
|
||||||
// sized to the return type's slot_sz (was hardcoded 24, which
|
|
||||||
// truncated 32B slots — `(void | err24)` clobbered its own
|
|
||||||
// payload local; task #38).
|
|
||||||
if (n.kind == nkind.N_RETURN) {
|
|
||||||
if (c.fnret != nil) {
|
|
||||||
if (istaggedtype(c, c.fnret)) {
|
|
||||||
if (!isnullabletype(c.fnret)) {
|
|
||||||
let rhs: *node = n.lhs;
|
|
||||||
let needs: bool = false;
|
|
||||||
if (rhs != nil) {
|
|
||||||
let sn: str = rhsstructpayload(c, rhs);
|
|
||||||
if (sn.len > 0) { needs = true; };
|
|
||||||
if (rhs.kind == nkind.N_IDENT) {
|
|
||||||
let lc: *local = localfindnode(c, rhs.str);
|
|
||||||
if (lc != nil) {
|
|
||||||
if (istaggedtype(c, lc.tnode)) {
|
|
||||||
needs = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (needs) {
|
|
||||||
total += tagscrbump(c, slotsize(c, c.fnret));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// Whole-struct return for sizes <= 24B uses @retscr — when
|
|
||||||
// the function's return type is a registered TY_STRUCT of
|
|
||||||
// size <= 24 and the return rhs is N_IDENT or N_STRUCTLIT,
|
|
||||||
// cgreturn materialises in @retscr then loads AX/DX/CX.
|
|
||||||
// Mirrors cstage cgen.c which allocates the scratch slot
|
|
||||||
// inline; here we must pre-reserve so the prologue SUBQ
|
|
||||||
// reserves enough frame.
|
|
||||||
if (n.kind == nkind.N_RETURN) {
|
|
||||||
if (c.fnret != nil) {
|
|
||||||
if (c.fnret.kind == nkind.N_TNAME) {
|
|
||||||
let rname: str = c.fnret.str;
|
|
||||||
let rsi: *structinfo = structlookup(c, rname);
|
|
||||||
if (rsi != nil) {
|
|
||||||
if (rsi.totsize <= 24) {
|
|
||||||
let rhs: *node = n.lhs;
|
|
||||||
let okrhs: bool = false;
|
|
||||||
if (rhs != nil) {
|
|
||||||
if (rhs.kind == nkind.N_IDENT) {
|
|
||||||
okrhs = true;
|
|
||||||
};
|
|
||||||
if (rhs.kind == nkind.N_STRUCTLIT) {
|
|
||||||
okrhs = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (okrhs) {
|
|
||||||
if (!scanseenmark(c, "@retscr")) {
|
|
||||||
total += 24;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// sret CALL (#23): callee returns plain TY_STRUCT > 24B. The
|
|
||||||
// receive site (cglet / cgassign ident) overrides at emit time
|
|
||||||
// with the dest local's own slot; discards / nested calls fall
|
|
||||||
// back to @sretscr. Single-slot per fn sized to the max sret
|
|
||||||
// return — sretscrbump tracks the high-water mark so a later
|
|
||||||
// larger call grows the frame without re-counting the prior
|
|
||||||
// reservation. Mirrors @tagscr's cumulative tagscrbump.
|
|
||||||
if (n.kind == nkind.N_CALL) {
|
|
||||||
let scs: i32 = callsretsize(c, n);
|
|
||||||
if (scs > 0) { total += sretscrbump(c, scs); };
|
|
||||||
};
|
|
||||||
// Call-site struct-payload widening uses @tagscr — when the
|
|
||||||
// arg is a struct literal/ident and the callee's param is
|
|
||||||
// tagged, pushargsrev materialises in scratch and pushes.
|
|
||||||
// Scalar / str args take the direct-push fast path (no
|
|
||||||
// scratch). Tagged-typed ident args also skip widening (the
|
|
||||||
// slot is already laid out, so pushargsrev pushes slot words
|
|
||||||
// directly). Both fast paths agree with C cgen bytewise, so
|
|
||||||
// only struct-payload sites get a scratch reservation.
|
|
||||||
if (n.kind == nkind.N_CALL) {
|
|
||||||
let callee: *node = n.lhs;
|
|
||||||
let cnm: str;
|
|
||||||
cnm.ptr = nil; cnm.len = 0;
|
|
||||||
if (callee != nil) {
|
|
||||||
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
|
|
||||||
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
|
|
||||||
};
|
|
||||||
if (cnm.len > 0) {
|
|
||||||
let ps: *node = fnparamslookup(c, cnm);
|
|
||||||
let a: *node = n.list;
|
|
||||||
for (a != nil) {
|
|
||||||
if (ps == nil) { a = nil; }
|
|
||||||
else {
|
|
||||||
if (ps.kind == nkind.N_PARAM) {
|
|
||||||
let pt: *node = ps.lhs;
|
|
||||||
if (istaggedtype(c, pt)) {
|
|
||||||
if (!isnullabletype(pt)) {
|
|
||||||
let sn: str = rhsstructpayload(c, a);
|
|
||||||
if (sn.len > 0) {
|
|
||||||
let isidentstruct: bool = false;
|
|
||||||
if (a.kind == nkind.N_IDENT) {
|
|
||||||
// Struct ident as
|
|
||||||
// tagged arg — pushargsrev
|
|
||||||
// still routes through the
|
|
||||||
// scratch path.
|
|
||||||
isidentstruct = true;
|
|
||||||
};
|
|
||||||
let _u: bool = isidentstruct;
|
|
||||||
total += tagscrbump(c, slotsize(c, pt));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (a != nil) {
|
|
||||||
a = a.next;
|
|
||||||
ps = ps.next;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// Hare-style variadic call: reserve @vararg_d_<seq> for the
|
|
||||||
// element data and @vararg_sl_<seq> for the 24B slice
|
|
||||||
// descriptor. The seq is recorded on the N_CALL node so
|
|
||||||
// cgcall picks the same names regardless of walk order
|
|
||||||
// (scanlocals descends LTR; pushargsrev evaluates RTL).
|
|
||||||
let nfixed: i32 = 0;
|
|
||||||
let varp: *node = callee_variadic_param(c, n.lhs, &nfixed);
|
|
||||||
if (varp != nil) {
|
|
||||||
let nargs: i32 = 0;
|
|
||||||
let aw: *node = n.list;
|
|
||||||
for (aw != nil) { nargs += 1; aw = aw.next; };
|
|
||||||
let nvar: i32 = nargs - nfixed;
|
|
||||||
if (nvar < 0) { nvar = 0; };
|
|
||||||
let forwarding: bool = false;
|
|
||||||
if (nvar == 1) {
|
|
||||||
let aa: *node = n.list;
|
|
||||||
let k0: i32 = 0;
|
|
||||||
for (k0 < nfixed) { aa = aa.next; k0 += 1; };
|
|
||||||
if (aa != nil) {
|
|
||||||
if (aa.kind == nkind.N_SPREAD) {
|
|
||||||
forwarding = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (!forwarding) {
|
|
||||||
let seq: i32 = c.varargseq;
|
|
||||||
n.uval = seq: u64;
|
|
||||||
c.varargseq += 1;
|
|
||||||
let esz: i32 = slotsize(c, varp.lhs);
|
|
||||||
if (esz < 1) { esz = 1; };
|
|
||||||
let dname: str = mkvarargname(c, "@vararg_d_", seq);
|
|
||||||
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
|
|
||||||
if (nvar > 0) {
|
|
||||||
if (!scanseenmark(c, dname)) {
|
|
||||||
let dsz: i32 = nvar * esz;
|
|
||||||
if ((dsz & 7) != 0) {
|
|
||||||
dsz = (dsz + 7) & ~7;
|
|
||||||
};
|
|
||||||
total += dsz;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (!scanseenmark(c, sname)) { total += 24; };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (n.lhs != nil) { total += scanlocals(c, n.lhs); };
|
|
||||||
if (n.rhs != nil) { total += scanlocals(c, n.rhs); };
|
|
||||||
if (n.cond != nil) { total += scanlocals(c, n.cond); };
|
|
||||||
if (n.body != nil) { total += scanlocals(c, n.body); };
|
|
||||||
if (n.els != nil) { total += scanlocals(c, n.els); };
|
|
||||||
if (n.list != nil) {
|
|
||||||
let m: *node = n.list;
|
|
||||||
for (m != nil) {
|
|
||||||
total += scanlocals(c, m);
|
|
||||||
m = m.next;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return total;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// ---- function-level cgen ---------------------------------------------
|
// ---- function-level cgen ---------------------------------------------
|
||||||
|
|
||||||
@@ -541,7 +29,7 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
|||||||
// (already spilled to @sretarg by cgfn); the first user param
|
// (already spilled to @sretarg by cgfn); the first user param
|
||||||
// lands in SI.
|
// lands in SI.
|
||||||
let idx: i32 = 0;
|
let idx: i32 = 0;
|
||||||
if (c.sretargoff != 0) { idx = 1; };
|
if (localfind(c, "@sretarg") != 0) { idx = 1; };
|
||||||
let fidx: i32 = 0;
|
let fidx: i32 = 0;
|
||||||
// Cursor for args that overflow the SysV reg windows. Each
|
// Cursor for args that overflow the SysV reg windows. Each
|
||||||
// stack-passed arg lives at 16+8*k(BP) — no spill, the local
|
// stack-passed arg lives at 16+8*k(BP) — no spill, the local
|
||||||
@@ -854,130 +342,18 @@ fn cgfn(c: *cgen, fn_: *node) void = {
|
|||||||
// sret callee (#23): return type is plain TY_STRUCT > 24B.
|
// sret callee (#23): return type is plain TY_STRUCT > 24B.
|
||||||
// Reserve 8B for @sretarg (holds the saved hidden RDI dest
|
// Reserve 8B for @sretarg (holds the saved hidden RDI dest
|
||||||
// pointer); cgfnparams skips DI for user args, cgreturn writes
|
// pointer); cgfnparams skips DI for user args, cgreturn writes
|
||||||
// through *(@sretarg) and returns @sretarg in RAX. Decision
|
// through *(@sretarg) and returns @sretarg in RAX.
|
||||||
// made here so the frame pre-scan and cgfnparams see the same
|
|
||||||
// view of the int-arg cursor.
|
|
||||||
let sret_callee: bool = sretretsize(c, c.fnret) > 0;
|
let sret_callee: bool = sretretsize(c, c.fnret) > 0;
|
||||||
|
|
||||||
// Emit the TEXT label via emitfnname so the def site picks up the
|
// Capture the body into cgoutbuf while c.frame grows under
|
||||||
// same skip rule (FFI / `main` / empty-module) and the same module
|
// emit-time localadd calls (#15/#26c — wwstage dropped its
|
||||||
// hint (this fn's own module) that the call sites use. Drops the
|
// scanlocals pre-pass to align DOWN with cstage's first-use
|
||||||
// `exported == 0` skip in the legacy inline form — exported fns
|
// pattern). The prologue (TEXT label, PUSHQ/MOVQ/SUBQ) emits
|
||||||
// now mangle too, so cross-module same-leaf exports coexist.
|
// after the body finishes so the frame size reflects every
|
||||||
emitline("TEXT ");
|
// localadd. Mirrors cstage cmd/w6c/cgen.c cgfn which builds
|
||||||
emitfnname(c, fn_.str, fn_.nmod);
|
// `subsp`/`text` Progs up front and patches their `from.offset`
|
||||||
emitline(",$");
|
// at the end via txt_emit.
|
||||||
|
cgout_enable(c.a);
|
||||||
// Pre-scan total frame: only count params that land in a local
|
|
||||||
// slot. SysV-class accounting; mirrors runtime walk in cstage
|
|
||||||
// cgen.c §5130-5223 and cgfnparams below. A stack-spilled param
|
|
||||||
// is addressed at a positive BP offset by cgfnparams (via
|
|
||||||
// localaddstack) and consumes no frame, so adding its size here
|
|
||||||
// would over-allocate. Seed c.locals with param-name stubs so
|
|
||||||
// scanlocals dedups a re-declared `let <name>` in the body
|
|
||||||
// against the param's slot (matches C cgen). Stubs get cleared
|
|
||||||
// before emission.
|
|
||||||
let scanp: *node = fn_.list;
|
|
||||||
let frame: i32 = 0;
|
|
||||||
let argi: i32 = 0;
|
|
||||||
let fargi: i32 = 0;
|
|
||||||
// Reserve @sretarg (8B) BEFORE the param-induced frame, and
|
|
||||||
// start argi at 1 so the param walker sees RDI as consumed.
|
|
||||||
if (sret_callee) {
|
|
||||||
frame += 8;
|
|
||||||
argi = 1;
|
|
||||||
};
|
|
||||||
for (scanp != nil) {
|
|
||||||
if (scanp.kind == nkind.N_PARAM) {
|
|
||||||
let isvar: bool = scanp.op == tkind.TK_ELLIPSIS;
|
|
||||||
let isf: bool = false;
|
|
||||||
let istg: bool = false;
|
|
||||||
let issl: bool = false;
|
|
||||||
let isst: bool = false;
|
|
||||||
let isstruct: bool = false;
|
|
||||||
let structsz: i32 = 0;
|
|
||||||
if (!isvar) {
|
|
||||||
isf = isfloattype(c, scanp.lhs);
|
|
||||||
istg = istaggedtype(c, scanp.lhs);
|
|
||||||
if (!isf && !istg) {
|
|
||||||
issl = isslicetype(c, scanp.lhs);
|
|
||||||
if (!issl) { isst = isstrtype(c, scanp.lhs); };
|
|
||||||
if (!issl && !isst) {
|
|
||||||
structsz = structparamsize(c, scanp.lhs);
|
|
||||||
isstruct = structsz > 0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
let eb: i32 = 1;
|
|
||||||
let sz: i32 = 8;
|
|
||||||
if (isvar) { eb = 3; sz = 24; }
|
|
||||||
else { if (istg) { sz = slotsize(c, scanp.lhs); eb = sz / 8; }
|
|
||||||
else { if (issl) { eb = 3; sz = 24; }
|
|
||||||
else { if (isst) { eb = 2; sz = 16; }
|
|
||||||
else { if (isstruct) {
|
|
||||||
sz = structsz;
|
|
||||||
eb = 1;
|
|
||||||
if (structsz > 8) { eb = 2; };
|
|
||||||
}
|
|
||||||
else { if (isf) {
|
|
||||||
eb = 1;
|
|
||||||
sz = 8;
|
|
||||||
if (isf32type(c, scanp.lhs)) { sz = 4; };
|
|
||||||
}; }; }; }; }; };
|
|
||||||
let regs_left: i32 = 6 - argi;
|
|
||||||
if (isf) { regs_left = 8 - fargi; };
|
|
||||||
if (regs_left >= eb) {
|
|
||||||
frame += sz;
|
|
||||||
if (isf) { fargi += 1; }
|
|
||||||
else { argi += eb; };
|
|
||||||
} else { if (eb > 1 && regs_left > 0 && (istg || issl || isst || isstruct || isvar)) {
|
|
||||||
// Multi-word param straddles the reg/stack boundary;
|
|
||||||
// cgfnparams stitches the tail from positive BP
|
|
||||||
// offsets into a single local slot, so we still
|
|
||||||
// reserve the full size. Symmetric across tagged,
|
|
||||||
// slice, str and variadic `T...`
|
|
||||||
// (cgendecl.ww:467/518/564/394).
|
|
||||||
frame += sz;
|
|
||||||
argi = 6;
|
|
||||||
} else {
|
|
||||||
// Pure stack: lives at +BP(16+stkcursor*8); no
|
|
||||||
// local slot consumed. The reg cursor stays put.
|
|
||||||
}; };
|
|
||||||
// Record the param's tnode on the stub so scanlocals's
|
|
||||||
// `h.f = v` / `&h[i]` / etc. detection paths can
|
|
||||||
// resolve a *struct / *[]T / *T param through
|
|
||||||
// localfindnode rather than seeing tnode=nil and
|
|
||||||
// skipping the reservation. Latent pre-#38: the
|
|
||||||
// pointer-rooted struct-field tagged write
|
|
||||||
// (cgendecl.ww:264) never fired for `fn fill(h: *holder)
|
|
||||||
// { h.e = v; }` because the param stub had no type
|
|
||||||
// info, so @tagscr / @tagbase weren't counted in the
|
|
||||||
// frame. Emit-time localadd happened to fit pre-#38
|
|
||||||
// because the 24B hardcoded slot didn't collide with
|
|
||||||
// the 8B @tagbase neighbour, but a correctly-sized
|
|
||||||
// slot revealed the under-reservation.
|
|
||||||
if (!scanseenmark(c, scanp.str)) {
|
|
||||||
c.locals.tnode = scanp.lhs;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
scanp = scanp.next;
|
|
||||||
};
|
|
||||||
c.varargseq = 0;
|
|
||||||
if (fn_.body != nil) { frame += scanlocals(c, fn_.body); };
|
|
||||||
c.varargseq = 0;
|
|
||||||
// Drop the stubs so emission rebuilds c.locals with real offsets.
|
|
||||||
c.locals = nil;
|
|
||||||
if ((frame & 15) != 0) {
|
|
||||||
frame = (frame + 15) & ~15;
|
|
||||||
};
|
|
||||||
emitint(frame: i64);
|
|
||||||
emitline("\n");
|
|
||||||
|
|
||||||
emitline("\tPUSHQ\tBP\n");
|
|
||||||
emitline("\tMOVQ\tSP, BP\n");
|
|
||||||
emitline("\tSUBQ\t$");
|
|
||||||
emitint(frame: i64);
|
|
||||||
emitline(", SP\n");
|
|
||||||
|
|
||||||
if (sret_callee) {
|
if (sret_callee) {
|
||||||
let saoff: i32 = localadd(c, "@sretarg", 8, nil);
|
let saoff: i32 = localadd(c, "@sretarg", 8, nil);
|
||||||
@@ -1011,7 +387,7 @@ fn cgfn(c: *cgen, fn_: *node) void = {
|
|||||||
// Run any registered defers in LIFO order before the
|
// Run any registered defers in LIFO order before the
|
||||||
// implicit return.
|
// implicit return.
|
||||||
rundefers(c);
|
rundefers(c);
|
||||||
// Zero AX before the fall-through return — matches C cgen,
|
// Zero AX before the fall-through return — matches cstage,
|
||||||
// which always emits this so void-returning fns don't leak
|
// which always emits this so void-returning fns don't leak
|
||||||
// a stale callee value to their caller.
|
// a stale callee value to their caller.
|
||||||
emitline("\tMOVQ\t$0, AX\n");
|
emitline("\tMOVQ\t$0, AX\n");
|
||||||
@@ -1019,6 +395,28 @@ fn cgfn(c: *cgen, fn_: *node) void = {
|
|||||||
emitline("\tPOPQ\tBP\n");
|
emitline("\tPOPQ\tBP\n");
|
||||||
emitline("\tRET\n");
|
emitline("\tRET\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cgout_disable();
|
||||||
|
|
||||||
|
let frame: i32 = c.frame;
|
||||||
|
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
|
||||||
|
|
||||||
|
// Emit the TEXT label via emitfnname so the def site picks up the
|
||||||
|
// same skip rule (FFI / `main` / empty-module) and the same module
|
||||||
|
// hint (this fn's own module) that the call sites use.
|
||||||
|
emitline("TEXT ");
|
||||||
|
emitfnname(c, fn_.str, fn_.nmod);
|
||||||
|
emitline(",$");
|
||||||
|
emitint(frame: i64);
|
||||||
|
emitline("\n");
|
||||||
|
|
||||||
|
emitline("\tPUSHQ\tBP\n");
|
||||||
|
emitline("\tMOVQ\tSP, BP\n");
|
||||||
|
emitline("\tSUBQ\t$");
|
||||||
|
emitint(frame: i64);
|
||||||
|
emitline(", SP\n");
|
||||||
|
|
||||||
|
cgout_flush();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- file-level entry ------------------------------------------------
|
// ---- file-level entry ------------------------------------------------
|
||||||
|
|||||||
@@ -497,7 +497,7 @@ fn cgstrlit(c: *cgen, n: *node) void = {
|
|||||||
let nstr: str = n.str;
|
let nstr: str = n.str;
|
||||||
let lab: str = internstrlit(c, nstr);
|
let lab: str = internstrlit(c, nstr);
|
||||||
emitline("\tLEAQ\t");
|
emitline("\tLEAQ\t");
|
||||||
os.write(1, lab.ptr, lab.len: u64);
|
emitbytes( lab.ptr, lab.len: u64);
|
||||||
emitline("(SB), AX\n");
|
emitline("(SB), AX\n");
|
||||||
emitline("\tMOVQ\t$");
|
emitline("\tMOVQ\t$");
|
||||||
emitint(nstr.len: i64);
|
emitint(nstr.len: i64);
|
||||||
@@ -564,7 +564,7 @@ fn cgident(c: *cgen, n: *node) void = {
|
|||||||
let bytes: str = drhs.str;
|
let bytes: str = drhs.str;
|
||||||
let lab: str = internstrlit(c, bytes);
|
let lab: str = internstrlit(c, bytes);
|
||||||
emitline("\tLEAQ\t");
|
emitline("\tLEAQ\t");
|
||||||
os.write(1, lab.ptr, lab.len: u64);
|
emitbytes( lab.ptr, lab.len: u64);
|
||||||
emitline("(SB), AX\n");
|
emitline("(SB), AX\n");
|
||||||
emitline("\tMOVQ\t$");
|
emitline("\tMOVQ\t$");
|
||||||
emitint(bytes.len: i64);
|
emitint(bytes.len: i64);
|
||||||
@@ -1008,9 +1008,9 @@ fn cgmatch(c: *cgen, n: *node) void = {
|
|||||||
// (N_INDEX) and tagged-field loads (N_DOT, fixed by
|
// (N_INDEX) and tagged-field loads (N_DOT, fixed by
|
||||||
// #28) produce the same triple. Nullable returns are
|
// #28) produce the same triple. Nullable returns are
|
||||||
// single-word (AX = ptr); only +0 is read.
|
// single-word (AX = ptr); only +0 is read.
|
||||||
// Scrutinee type + spill size factored into matchscrutt
|
// Scrutinee type + spill size resolved through matchscrutt
|
||||||
// / matchspillsz so scanlocals stays lockstep — see
|
// / matchspillsz at first use (#15) — see cgenutil.ww
|
||||||
// cgenutil.ww (task #9 align-down to cstage).
|
// (task #9 align-down to cstage).
|
||||||
scrutt = matchscrutt(c, scrut);
|
scrutt = matchscrutt(c, scrut);
|
||||||
let spillsz: i32 = matchspillsz(c, scrutt);
|
let spillsz: i32 = matchspillsz(c, scrutt);
|
||||||
scrutoff = localalloc(c, "@match_spill", spillsz, nil);
|
scrutoff = localalloc(c, "@match_spill", spillsz, nil);
|
||||||
@@ -1520,7 +1520,7 @@ fn cgdot(c: *cgen, n: *node) void = {
|
|||||||
if (streq(fld, "ptr")) {
|
if (streq(fld, "ptr")) {
|
||||||
let lab: str = internstrlit(c, bytes);
|
let lab: str = internstrlit(c, bytes);
|
||||||
emitline("\tLEAQ\t");
|
emitline("\tLEAQ\t");
|
||||||
os.write(1, lab.ptr, lab.len: u64);
|
emitbytes( lab.ptr, lab.len: u64);
|
||||||
emitline("(SB), AX\n");
|
emitline("(SB), AX\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1754,7 +1754,7 @@ fn cgdot(c: *cgen, n: *node) void = {
|
|||||||
let bytes: str = drhs.str;
|
let bytes: str = drhs.str;
|
||||||
let lab: str = internstrlit(c, bytes);
|
let lab: str = internstrlit(c, bytes);
|
||||||
emitline("\tLEAQ\t");
|
emitline("\tLEAQ\t");
|
||||||
os.write(1, lab.ptr, lab.len: u64);
|
emitbytes( lab.ptr, lab.len: u64);
|
||||||
emitline("(SB), AX\n");
|
emitline("(SB), AX\n");
|
||||||
emitline("\tMOVQ\t$");
|
emitline("\tMOVQ\t$");
|
||||||
emitint(bytes.len: i64);
|
emitint(bytes.len: i64);
|
||||||
@@ -2941,7 +2941,7 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// Forwarding shape (`xs...`) skips the gather: the spread's
|
// Forwarding shape (`xs...`) skips the gather: the spread's
|
||||||
// inner slice expression replaces the wrapper in place. Empty
|
// inner slice expression replaces the wrapper in place. Empty
|
||||||
// (no trailing args) writes a {nil, 0, 0} descriptor. The seq
|
// (no trailing args) writes a {nil, 0, 0} descriptor. The seq
|
||||||
// matches the one scanlocals stamped on n.uval.
|
// matches the one cgcall stamped on n.uval at first emit.
|
||||||
{
|
{
|
||||||
let nfixed_v: i32 = 0;
|
let nfixed_v: i32 = 0;
|
||||||
let varp: *node = callee_variadic_param(c, callee, &nfixed_v);
|
let varp: *node = callee_variadic_param(c, callee, &nfixed_v);
|
||||||
@@ -2982,7 +2982,25 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
let seq: i32 = n.uval: i32;
|
let seq: i32 = n.uval: i32;
|
||||||
let dname: str = mkvarargname(c, "@vararg_d_", seq);
|
let dname: str = mkvarargname(c, "@vararg_d_", seq);
|
||||||
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
|
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
|
||||||
let esz: i32 = slotsize(c, varp.lhs);
|
// Use raw element size, not stack-padded
|
||||||
|
// slotsize. cstage cmd/w6c/cgen.c cgcall
|
||||||
|
// gathers a `T...` slice at velem->size stride
|
||||||
|
// (MOVL for u32, MOVB for u8); the callee
|
||||||
|
// `arg[i]` reads at the same raw stride. wwstage
|
||||||
|
// previously sized through slotsize which pads
|
||||||
|
// scalars to 8, mismatching the stride at the
|
||||||
|
// callee read site — runtime miscompile in
|
||||||
|
// `(rune...)` callees per #36.
|
||||||
|
let esz: i32 = 8;
|
||||||
|
if (varp.lhs != nil) {
|
||||||
|
if (varp.lhs.kind == nkind.N_TNAME) {
|
||||||
|
let ps: i32 = primsize(varp.lhs.str);
|
||||||
|
if (ps > 0) { esz = ps; }
|
||||||
|
else { esz = slotsize(c, varp.lhs); };
|
||||||
|
} else {
|
||||||
|
esz = slotsize(c, varp.lhs);
|
||||||
|
};
|
||||||
|
};
|
||||||
if (esz < 1) { esz = 1; };
|
if (esz < 1) { esz = 1; };
|
||||||
let velemtagged: bool = istaggedtype(c, varp.lhs);
|
let velemtagged: bool = istaggedtype(c, varp.lhs);
|
||||||
let velemstr: bool = isstrtype(c, varp.lhs);
|
let velemstr: bool = isstrtype(c, varp.lhs);
|
||||||
@@ -3079,7 +3097,7 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// pops have finished (so they don't clobber RDI). The dest off
|
// pops have finished (so they don't clobber RDI). The dest off
|
||||||
// is either the receive site's slot (c.sretdestoff, propagated
|
// is either the receive site's slot (c.sretdestoff, propagated
|
||||||
// from cglet / cgassign ident) or the per-fn @sretscr discard
|
// from cglet / cgassign ident) or the per-fn @sretscr discard
|
||||||
// slot reserved by scanlocals.
|
// slot, sized at first use per #15/#26c.
|
||||||
let sretcs: i32 = callsretsize(c, n);
|
let sretcs: i32 = callsretsize(c, n);
|
||||||
let sretcalloff: i32 = 0;
|
let sretcalloff: i32 = 0;
|
||||||
if (sretcs > 0) {
|
if (sretcs > 0) {
|
||||||
@@ -3088,7 +3106,7 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
} else {
|
} else {
|
||||||
sretcalloff = localadd(c, "@sretscr",
|
sretcalloff = localadd(c, "@sretscr",
|
||||||
c.sretscrsz, nil);
|
sretcs, nil);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
// Pop forward. Float args were pushed as 8 bytes from X0 via
|
// Pop forward. Float args were pushed as 8 bytes from X0 via
|
||||||
@@ -3242,8 +3260,9 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// on the forwarding branch.
|
// on the forwarding branch.
|
||||||
if (sretcs > 0) {
|
if (sretcs > 0) {
|
||||||
if (c.sretforward != 0) {
|
if (c.sretforward != 0) {
|
||||||
|
let sretargoff: i32 = localfind(c, "@sretarg");
|
||||||
emitline("\tMOVQ\t");
|
emitline("\tMOVQ\t");
|
||||||
emitoff(c.sretargoff: i64);
|
emitoff(sretargoff: i64);
|
||||||
emitline("(BP), DI\n");
|
emitline("(BP), DI\n");
|
||||||
c.sretforward = 0;
|
c.sretforward = 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -3608,12 +3627,12 @@ fn cgassign(c: *cgen, n: *node) void = {
|
|||||||
// str / scalar / subset / nullable variants uniformly),
|
// str / scalar / subset / nullable variants uniformly),
|
||||||
// then compute &arr[i] and byte-copy. The scratch
|
// then compute &arr[i] and byte-copy. The scratch
|
||||||
// (@tagscr) is reused across all tagged-arr stores in
|
// (@tagscr) is reused across all tagged-arr stores in
|
||||||
// the function and counted once in scanlocals.
|
// the function; first-use sizes the slot (#15/#26c).
|
||||||
if (elemtn != nil) {
|
if (elemtn != nil) {
|
||||||
if (istaggedtype(c, elemtn)) {
|
if (istaggedtype(c, elemtn)) {
|
||||||
let slot_sz: i32 = slotsize(c, elemtn);
|
let slot_sz: i32 = slotsize(c, elemtn);
|
||||||
let scroff: i32 = localadd(c, "@tagscr",
|
let scroff: i32 = localadd(c, "@tagscr",
|
||||||
c.tagscrsz, nil);
|
slot_sz, nil);
|
||||||
// Pre-zero scratch (matches push helper).
|
// Pre-zero scratch (matches push helper).
|
||||||
emitline("\tXORQ\tAX, AX\n");
|
emitline("\tXORQ\tAX, AX\n");
|
||||||
let zz: i32 = 0;
|
let zz: i32 = 0;
|
||||||
|
|||||||
@@ -193,13 +193,19 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
|||||||
};
|
};
|
||||||
if (needswiden) {
|
if (needswiden) {
|
||||||
let rsz: i32 = slotsize(c, c.fnret);
|
let rsz: i32 = slotsize(c, c.fnret);
|
||||||
// Use c.tagscrsz so the first @tagscr allocation in
|
// @retscr (not @tagscr) for the return materialise
|
||||||
// the fn lands a slot sized to the *max* across all
|
// path. Cstage cmd/w6c/cgen.c cgreturn uses
|
||||||
// uses (scanlocals bumped to rsz here). Hardcoding 24
|
// `@retscr` here and reserves the @tagscr SSoT
|
||||||
// truncated 32B-slot returns and overwrote adjacent
|
// for arg-widen / non-BP-base store / N_INDEX
|
||||||
// locals during the pre-zero loop (#38).
|
// tagged-element write. Sharing the name in a fn
|
||||||
let scroff: i32 = localadd(c, "@tagscr",
|
// that BOTH returns a 32B tagged AND pushes a
|
||||||
c.tagscrsz, nil);
|
// smaller tagged arg fatals localadd's @-prefix
|
||||||
|
// size-grow guard (rule 7); routing returns
|
||||||
|
// through their own slot keeps each cache
|
||||||
|
// monotonic. Hardcoding 24 truncated 32B-slot
|
||||||
|
// returns and overwrote adjacent locals during
|
||||||
|
// the pre-zero loop (#38).
|
||||||
|
let scroff: i32 = localadd(c, "@retscr", rsz, nil);
|
||||||
emitline("\tXORQ\tAX, AX\n");
|
emitline("\tXORQ\tAX, AX\n");
|
||||||
let zz: i32 = 0;
|
let zz: i32 = 0;
|
||||||
for (zz < rsz) {
|
for (zz < rsz) {
|
||||||
@@ -299,7 +305,8 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
|||||||
// the SysV "return the pointer" discipline. Two rhs shapes
|
// the SysV "return the pointer" discipline. Two rhs shapes
|
||||||
// are wired: N_IDENT (word-copy from rhs slot to *(dest))
|
// are wired: N_IDENT (word-copy from rhs slot to *(dest))
|
||||||
// and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL).
|
// and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL).
|
||||||
if (c.sretargoff != 0) {
|
let sretargoff: i32 = localfind(c, "@sretarg");
|
||||||
|
if (sretargoff != 0) {
|
||||||
let scs: i32 = sretretsize(c, c.fnret);
|
let scs: i32 = sretretsize(c, c.fnret);
|
||||||
if (scs > 0) {
|
if (scs > 0) {
|
||||||
// sret return-forwarding (task #9 follow-up to
|
// sret return-forwarding (task #9 follow-up to
|
||||||
@@ -317,7 +324,7 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
|||||||
c.sretforward = 1;
|
c.sretforward = 1;
|
||||||
cgexpr(c, rhs);
|
cgexpr(c, rhs);
|
||||||
emitline("\tMOVQ\t");
|
emitline("\tMOVQ\t");
|
||||||
emitoff(c.sretargoff: i64);
|
emitoff(sretargoff: i64);
|
||||||
emitline("(BP), AX\n");
|
emitline("(BP), AX\n");
|
||||||
emitline("\tMOVQ\tBP, SP\n");
|
emitline("\tMOVQ\tBP, SP\n");
|
||||||
emitline("\tPOPQ\tBP\n");
|
emitline("\tPOPQ\tBP\n");
|
||||||
@@ -346,14 +353,14 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
|||||||
// each field store. disp = 0 because
|
// each field store. disp = 0 because
|
||||||
// the dest pointer IS the struct base.
|
// the dest pointer IS the struct base.
|
||||||
cgstructlitfill(c, sret_si, rhs,
|
cgstructlitfill(c, sret_si, rhs,
|
||||||
1, c.sretargoff, emptys,
|
1, sretargoff, emptys,
|
||||||
0, scs);
|
0, scs);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let rl: *local = localfindnode(c, rhs.str);
|
let rl: *local = localfindnode(c, rhs.str);
|
||||||
if (rl != nil) {
|
if (rl != nil) {
|
||||||
emitline("\tMOVQ\t");
|
emitline("\tMOVQ\t");
|
||||||
emitoff(c.sretargoff: i64);
|
emitoff(sretargoff: i64);
|
||||||
emitline("(BP), BX\n");
|
emitline("(BP), BX\n");
|
||||||
let k: i32 = 0;
|
let k: i32 = 0;
|
||||||
for (k + 8 <= scs) {
|
for (k + 8 <= scs) {
|
||||||
@@ -387,7 +394,7 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
|||||||
};
|
};
|
||||||
// sret return: RAX = dest pointer.
|
// sret return: RAX = dest pointer.
|
||||||
emitline("\tMOVQ\t");
|
emitline("\tMOVQ\t");
|
||||||
emitoff(c.sretargoff: i64);
|
emitoff(sretargoff: i64);
|
||||||
emitline("(BP), AX\n");
|
emitline("(BP), AX\n");
|
||||||
emitline("\tMOVQ\tBP, SP\n");
|
emitline("\tMOVQ\tBP, SP\n");
|
||||||
emitline("\tPOPQ\tBP\n");
|
emitline("\tPOPQ\tBP\n");
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = {
|
|||||||
|
|
||||||
// mkvarargname — fresh local-slot name "<prefix><seq>". Used for
|
// mkvarargname — fresh local-slot name "<prefix><seq>". Used for
|
||||||
// the per-variadic-call scratch buffers (`@vararg_d_N` for the
|
// the per-variadic-call scratch buffers (`@vararg_d_N` for the
|
||||||
// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor)
|
// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor).
|
||||||
// where N is recorded on the N_CALL node at scanlocals time so both
|
// N is recorded on the N_CALL node at first emit so re-entry into
|
||||||
// the prologue reservation and the call-site emission agree.
|
// cgcall picks the same names regardless of walk order.
|
||||||
fn mkvarargname(c: *cgen, prefix: str, seq: i32) str = {
|
fn mkvarargname(c: *cgen, prefix: str, seq: i32) str = {
|
||||||
let buf: [128]u8;
|
let buf: [128]u8;
|
||||||
let i: i32 = 0;
|
let i: i32 = 0;
|
||||||
@@ -179,7 +179,7 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
|
|||||||
let pname: str = rhsstructpayload(c, arg);
|
let pname: str = rhsstructpayload(c, arg);
|
||||||
if (pname.len > 0) {
|
if (pname.len > 0) {
|
||||||
let ptype: *node = param.lhs;
|
let ptype: *node = param.lhs;
|
||||||
let scroff: i32 = localadd(c, "@tagscr", c.tagscrsz, nil);
|
let scroff: i32 = localadd(c, "@tagscr", widensz, nil);
|
||||||
emitline("\tXORQ\tAX, AX\n");
|
emitline("\tXORQ\tAX, AX\n");
|
||||||
let zz: i32 = 0;
|
let zz: i32 = 0;
|
||||||
for (zz < widensz) {
|
for (zz < widensz) {
|
||||||
@@ -1747,8 +1747,8 @@ fn inferletcalltype(c: *cgen, rhs: *node) *node = {
|
|||||||
// letslotsize — slot size for a `let` binding. Like slotsize, but
|
// letslotsize — slot size for a `let` binding. Like slotsize, but
|
||||||
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
|
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
|
||||||
// length-inferred sentinel) and computes count × element-size from
|
// length-inferred sentinel) and computes count × element-size from
|
||||||
// the initialiser. Used by both scanlocals (prologue sizing) and
|
// the initialiser. Called from cglet at emit time so the frame
|
||||||
// cglet (slot alloc) so they agree on the frame layout.
|
// grows monotonically per first-use (#15).
|
||||||
//
|
//
|
||||||
// `let x = f();` (no annotation): infer from `f`'s declared return
|
// `let x = f();` (no annotation): infer from `f`'s declared return
|
||||||
// type so a 24B tagged-union return reserves all three spill slots,
|
// type so a 24B tagged-union return reserves all three spill slots,
|
||||||
@@ -2119,9 +2119,8 @@ export fn resolvetagged(c: *cgen, t: *node) *node = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// matchscrutt — resolve a non-ident match scrutinee node to its tagged
|
// matchscrutt — resolve a non-ident match scrutinee node to its tagged
|
||||||
// type (or nil if unresolvable). Mirrors cgmatch's inline scrutinee
|
// type (or nil if unresolvable). Used by cgmatch to size the
|
||||||
// type resolution; factored so cgmatch (emit) and scanlocals (count)
|
// @match_spill slot at first use (#15 first-use+fail-loud convergence).
|
||||||
// agree on the spill slot's size per the scan+emit lockstep invariant.
|
|
||||||
// IDENT scrutinees use a different lookup path (read off the local
|
// IDENT scrutinees use a different lookup path (read off the local
|
||||||
// directly, no spill) so this returns nil for them too.
|
// directly, no spill) so this returns nil for them too.
|
||||||
fn matchscrutt(c: *cgen, scrut: *node) *node = {
|
fn matchscrutt(c: *cgen, scrut: *node) *node = {
|
||||||
@@ -2184,8 +2183,8 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = {
|
|||||||
// scrutinee lands in. Mirrors cstage's `slot_size = (su->kind ==
|
// scrutinee lands in. Mirrors cstage's `slot_size = (su->kind ==
|
||||||
// TY_TAGGED) ? su->size : 16` (cmd/w6c/cgen.c cgmatch). 16 default
|
// TY_TAGGED) ? su->size : 16` (cmd/w6c/cgen.c cgmatch). 16 default
|
||||||
// when the scrutinee type can't be resolved keeps the historical
|
// when the scrutinee type can't be resolved keeps the historical
|
||||||
// alloc for non-tagged / unresolved cases. Used by both scanlocals
|
// alloc for non-tagged / unresolved cases. Called by cgmatch at first
|
||||||
// (counting) and cgmatch (emitting) per rule-10 align-to-cstage.
|
// use; #15 first-use+fail-loud pins this size per fn.
|
||||||
fn matchspillsz(c: *cgen, scrutt: *node) i32 = {
|
fn matchspillsz(c: *cgen, scrutt: *node) i32 = {
|
||||||
if (scrutt == nil) { return 16; };
|
if (scrutt == nil) { return 16; };
|
||||||
let sz: i32 = slotsize(c, scrutt);
|
let sz: i32 = slotsize(c, scrutt);
|
||||||
@@ -2972,11 +2971,11 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node,
|
|||||||
emitline(", ");
|
emitline(", ");
|
||||||
emitoff(bspill: i64);
|
emitoff(bspill: i64);
|
||||||
emitline("(BP)\n");
|
emitline("(BP)\n");
|
||||||
// Same shared scratch — c.tagscrsz is the per-fn max across every
|
// Shared scratch sized at first use per #15/#26c. A sibling
|
||||||
// reservation site (scanlocals); pinning to slot_sz here would
|
// site (cgreturn, pushargsrev, cgindex) hitting @tagscr later
|
||||||
// undersize the slot if a sibling site (cgreturn, pushargsrev,
|
// with a larger size fatals (rule 7) — pinned offset can't
|
||||||
// cgindex) needed a larger one and fired second.
|
// grow in place.
|
||||||
let scr: i32 = localadd(c, "@tagscr", c.tagscrsz, nil);
|
let scr: i32 = localadd(c, "@tagscr", slot_sz, nil);
|
||||||
emitline("\tXORQ\tAX, AX\n");
|
emitline("\tXORQ\tAX, AX\n");
|
||||||
let z: i32 = 0;
|
let z: i32 = 0;
|
||||||
for (z < slot_sz) {
|
for (z < slot_sz) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
293
test/wcc/748_size_strategy_convergence.c
Normal file
293
test/wcc/748_size_strategy_convergence.c
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* 748_size_strategy_convergence — sentinel for #15/#26c (subsumes #36).
|
||||||
|
*
|
||||||
|
* Pins wwstage's first-use+fail-loud frame-sizing convergence with
|
||||||
|
* cstage. Pre-#15 wwstage ran a `scanlocals` pre-pass that walked the
|
||||||
|
* body to pre-size the SUBQ slot total; cstage allocated slots at
|
||||||
|
* emit time and patched the prologue after the body finished. The
|
||||||
|
* pre-pass under-counted match-arm `case let` bindings in `(str|rune)
|
||||||
|
* ...` callees (#36 original surface): variadic param tnode was the
|
||||||
|
* inner type, not the synthesised slice wrap, so the @match_spill
|
||||||
|
* lookup at scan time fell back to the default (16) instead of the
|
||||||
|
* real slot (24). Emit-time cgmatch then resolved through the slice-
|
||||||
|
* wrapped local and chose 24, growing c.frame past the SUBQ reservation;
|
||||||
|
* `case let r: rune =>` writes landed below SP.
|
||||||
|
*
|
||||||
|
* Post-fix both stages defer the prologue until after the body emits,
|
||||||
|
* and every @-prefix scratch slot (@tagscr / @retscr / @sretscr /
|
||||||
|
* @tagbase / @sretarg / @vararg_*) is sized at first use. A later
|
||||||
|
* caller asking for a larger slot than the first allocation pinned
|
||||||
|
* fatals (rule 7 — pinned offset can't grow in place; #26 already
|
||||||
|
* proved the model for cstage's @tagscr cache). The variadic gather
|
||||||
|
* also routes the element stride through the raw type size (matching
|
||||||
|
* cstage's velem->size) so the callee read at `arg[i]` lines up with
|
||||||
|
* the caller's store.
|
||||||
|
*
|
||||||
|
* Rows assert per-stage runtime and cstage-vs-wwstage byte-id on the
|
||||||
|
* shapes that #36 + #15 are pinned against.
|
||||||
|
*
|
||||||
|
* row | what it pins
|
||||||
|
* -------------------------------+--------------------------------
|
||||||
|
* tag_variadic_runearm | #36 original surface — rune-arm
|
||||||
|
* | of `(str|rune)...` callee. Pre-
|
||||||
|
* | fix wwstage SUBQ $80, cstage $96.
|
||||||
|
* | Post-fix byte-id on the callee.
|
||||||
|
* trim_iter_match_prev | #36 sibling — `trim: rune...` +
|
||||||
|
* | strings.iter outer + nested
|
||||||
|
* | strings.prev match. Pre-fix both
|
||||||
|
* | stages framed $192 but match-arm
|
||||||
|
* | scratch offsets diverged inside
|
||||||
|
* | the same frame. Post-fix loop_for
|
||||||
|
* | byte-id; runtime returns 0.
|
||||||
|
* variadic_gather_rune_stride | Caller-side gather stride for
|
||||||
|
* | `(rune...)` must use raw u32
|
||||||
|
* | size (4), not slotsize (8 —
|
||||||
|
* | stack-padded). Pre-fix wwstage
|
||||||
|
* | MOVQ @ 8B stride vs cstage MOVL
|
||||||
|
* | @ 4B; callee's `arg[i]` read at
|
||||||
|
* | 4B stride saw garbage at odd
|
||||||
|
* | indices.
|
||||||
|
* leaf_baseline | Leaf fn match: no nested blocks,
|
||||||
|
* | no scratch slot cache interaction.
|
||||||
|
* | Confirms the deferred-prologue
|
||||||
|
* | refactor didn't regress the
|
||||||
|
* | simple frame size.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
static int
|
||||||
|
runwait(const char *cmd)
|
||||||
|
{
|
||||||
|
int rc = system(cmd);
|
||||||
|
if (rc == -1) return -1;
|
||||||
|
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct row {
|
||||||
|
const char *label;
|
||||||
|
const char *src;
|
||||||
|
int want;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct row rows[] = {
|
||||||
|
/* 1. #36 original surface: rune-arm of `(str|rune)...` callee.
|
||||||
|
* want("hello", 'X', "world") → 5 + 1 + 5 = 11. */
|
||||||
|
{ "tag_variadic_runearm",
|
||||||
|
"package main;\n"
|
||||||
|
"import os;\n"
|
||||||
|
"fn want(args: (str | rune)...) i64 = {\n"
|
||||||
|
" let total: i64 = 0;\n"
|
||||||
|
" let i: i32 = 0;\n"
|
||||||
|
" for (i < args.len) {\n"
|
||||||
|
" match (args[i]) {\n"
|
||||||
|
" case let s: str => total += s.len: i64;\n"
|
||||||
|
" case let r: rune => total += 1;\n"
|
||||||
|
" };\n"
|
||||||
|
" i += 1;\n"
|
||||||
|
" };\n"
|
||||||
|
" return total;\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let n: i64 = want(\"hello\", 'X', \"world\");\n"
|
||||||
|
" return n: i32;\n"
|
||||||
|
"};\n",
|
||||||
|
11 },
|
||||||
|
|
||||||
|
/* 2. #36 sibling: variadic + iter + nested match prev in non-leaf.
|
||||||
|
* Mirrors .ai/probe_trim_36extra.ww. loop_for("aabcc", 'a', 'b')
|
||||||
|
* trims both leading 'a's and the leading 'b' → "cc" (len 2). */
|
||||||
|
{ "trim_iter_match_prev",
|
||||||
|
"package main;\n"
|
||||||
|
"import strings;\n"
|
||||||
|
"import encoding.utf8;\n"
|
||||||
|
"fn loop_for(input: str, trim: rune...) str = {\n"
|
||||||
|
" let it: strings.iterator = strings.iter(input);\n"
|
||||||
|
" for (true) {\n"
|
||||||
|
" match (strings.next(&it)) {\n"
|
||||||
|
" case let r: rune => {\n"
|
||||||
|
" let j: i32 = 0;\n"
|
||||||
|
" let found: bool = false;\n"
|
||||||
|
" for (j < trim.len) {\n"
|
||||||
|
" if (r == trim[j]) { found = true; j = trim.len; }\n"
|
||||||
|
" else { j += 1; };\n"
|
||||||
|
" };\n"
|
||||||
|
" if (!found) {\n"
|
||||||
|
" match (strings.prev(&it)) {\n"
|
||||||
|
" case let r2: rune => void;\n"
|
||||||
|
" case utf8.done => void;\n"
|
||||||
|
" };\n"
|
||||||
|
" break;\n"
|
||||||
|
" };\n"
|
||||||
|
" };\n"
|
||||||
|
" case utf8.done => break;\n"
|
||||||
|
" };\n"
|
||||||
|
" };\n"
|
||||||
|
" return strings.iterstr(&it);\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let r: str = loop_for(\"aabcc\", 'a', 'b');\n"
|
||||||
|
" if (r.len != 2) { return 11; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
|
||||||
|
/* 3. Caller-side variadic gather stride. The callee reads `arg[j]`
|
||||||
|
* at the raw element size (4 for rune); the gather must match.
|
||||||
|
* Pre-fix wwstage stored at slotsize (8B stride) and the callee
|
||||||
|
* read garbage for j=1 — the test would have returned `'b'` (98)
|
||||||
|
* or 0 depending on the high half of the stack word. Post-fix the
|
||||||
|
* caller gathers at 4B stride and `arg[1]` reads 'b' correctly. */
|
||||||
|
{ "variadic_gather_rune_stride",
|
||||||
|
"package main;\n"
|
||||||
|
"fn pick(idx: i32, args: rune...) i32 = {\n"
|
||||||
|
" if (idx >= args.len) { return 0; };\n"
|
||||||
|
" return args[idx]: i32;\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let r0: i32 = pick(0, 'a', 'b', 'c');\n"
|
||||||
|
" let r1: i32 = pick(1, 'a', 'b', 'c');\n"
|
||||||
|
" let r2: i32 = pick(2, 'a', 'b', 'c');\n"
|
||||||
|
" if (r0 != 97) { return 11; };\n"
|
||||||
|
" if (r1 != 98) { return 12; };\n"
|
||||||
|
" if (r2 != 99) { return 13; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
|
||||||
|
/* 4. Leaf-only baseline. No nested blocks, no @-prefix scratch
|
||||||
|
* interaction — a fn that match-binds a `(rune|void)` from a
|
||||||
|
* concrete scrutinee should frame exactly the bind slot and
|
||||||
|
* nothing else. Confirms the deferred-prologue refactor didn't
|
||||||
|
* regress simple frame sizes. */
|
||||||
|
{ "leaf_baseline",
|
||||||
|
"package main;\n"
|
||||||
|
"type tagged = (rune | void);\n"
|
||||||
|
"fn pickrune(x: tagged) i32 = {\n"
|
||||||
|
" match (x) {\n"
|
||||||
|
" case let r: rune => return r: i32;\n"
|
||||||
|
" case void => return 0;\n"
|
||||||
|
" };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let v: tagged = 'A';\n"
|
||||||
|
" let r: i32 = pickrune(v);\n"
|
||||||
|
" if (r != 65) { return 11; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
build_with(const char *driver, const char *src_path, const char *tmpdir)
|
||||||
|
{
|
||||||
|
char cmd[1024];
|
||||||
|
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
|
||||||
|
tmpdir, driver, src_path);
|
||||||
|
return runwait(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
exec_bin(const char *bin)
|
||||||
|
{
|
||||||
|
return runwait(bin);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(void)
|
||||||
|
{
|
||||||
|
const char *bin = getenv("BIN");
|
||||||
|
if (!bin) bin = "out/bin";
|
||||||
|
char absbin[512];
|
||||||
|
if (bin[0] != '/') {
|
||||||
|
char cwd[256];
|
||||||
|
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||||
|
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||||
|
bin = absbin;
|
||||||
|
}
|
||||||
|
|
||||||
|
char cdrv[640], wdrv[640];
|
||||||
|
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||||
|
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
||||||
|
int have_ww = (access(wdrv, X_OK) == 0);
|
||||||
|
|
||||||
|
int n = (int)(sizeof rows / sizeof rows[0]);
|
||||||
|
int total = 0, fail = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
const struct row *r = &rows[i];
|
||||||
|
|
||||||
|
char src[64], tmpdir[64];
|
||||||
|
snprintf(src, sizeof src, "/tmp/sz_conv_%d_%d.ww", getpid(), i);
|
||||||
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/sz_conv_%d_d_%d",
|
||||||
|
getpid(), i);
|
||||||
|
FILE *f = fopen(src, "wb");
|
||||||
|
if (!f) { fail++; total++; continue; }
|
||||||
|
fputs(r->src, f);
|
||||||
|
fclose(f);
|
||||||
|
mkdir(tmpdir, 0755);
|
||||||
|
|
||||||
|
const char *base = strrchr(src, '/');
|
||||||
|
base = base ? base + 1 : src;
|
||||||
|
char outbin[128];
|
||||||
|
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
|
||||||
|
char *dot = strrchr(outbin, '.');
|
||||||
|
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
|
||||||
|
|
||||||
|
/* Runtime parity (cstage). */
|
||||||
|
total++;
|
||||||
|
if (build_with(cdrv, src, tmpdir) != 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"size_strategy_convergence[cs][%s]: build failed\n",
|
||||||
|
r->label);
|
||||||
|
fail++;
|
||||||
|
} else {
|
||||||
|
int got = exec_bin(outbin);
|
||||||
|
if (got != r->want) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"size_strategy_convergence[cs][%s]: rc=%d want=%d\n",
|
||||||
|
r->label, got, r->want);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unlink(outbin);
|
||||||
|
|
||||||
|
/* Runtime parity (wwstage). */
|
||||||
|
if (have_ww) {
|
||||||
|
total++;
|
||||||
|
if (build_with(wdrv, src, tmpdir) != 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"size_strategy_convergence[ws][%s]: build failed\n",
|
||||||
|
r->label);
|
||||||
|
fail++;
|
||||||
|
} else {
|
||||||
|
int got = exec_bin(outbin);
|
||||||
|
if (got != r->want) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"size_strategy_convergence[ws][%s]: rc=%d want=%d\n",
|
||||||
|
r->label, got, r->want);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unlink(outbin);
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink(src);
|
||||||
|
rmdir(tmpdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fail) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"size_strategy_convergence: %d/%d rows failed\n",
|
||||||
|
fail, total);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("size_strategy_convergence: %d/%d ok\n", total, total);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user