diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 85c03735..6190aa37 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2064,6 +2064,35 @@ cgexpr(Cg *c, Node *n, Local *locals) Node *args[16] = {0}; for (Node *a = n->list; a; a = a->next) if (argcount < 16) args[argcount++] = a; + /* Resolve callee fn-type so we can match each arg against + * its declared parameter type — needed to detect implicit + * widening of a concrete variant into a tagged-union slot. */ + Type *callee_t = n->lhs ? n->lhs->type : NULL; + Type *cu = (callee_t && callee_t->kind == TY_NAMED) ? + callee_t->under : callee_t; + Tparam *callee_params = (cu && cu->kind == TY_FN) ? + cu->params : NULL; + /* widen[i]: param is tagged, arg is a concrete variant. + * widen_sz[i]: param's tagged slot size (8/16/24). + * widen_param[i]: param type (for tag-index lookup). */ + int widen[16] = {0}; + int widen_sz[16] = {0}; + Type *widen_param[16] = {0}; + { + Tparam *p = callee_params; + for (int i = 0; i < argcount; i++) { + if (p == NULL) break; + Type *at = args[i] ? args[i]->type : NULL; + int psz = tagged_arg_size(p->type); + int arg_tagged = tagged_arg_size(at) > 0; + if (psz > 0 && !arg_tagged) { + widen[i] = 1; + widen_sz[i] = psz; + widen_param[i] = p->type; + } + p = p->next; + } + } /* eval right-to-left, push to stack */ for (int i = argcount - 1; i >= 0; i--) { if (node_isslice(args[i]) && args[i]->kind == N_IDENT) { @@ -2150,6 +2179,46 @@ cgexpr(Cg *c, Node *n, Local *locals) } continue; } + if (widen[i]) { + /* Concrete → tagged-union widening at the call + * site. Mirrors the let/assign/return widening: + * synthesise tag from the arg's static variant + * type, evaluate the arg, lay it out as the + * parameter's tagged slot, then push high→low so + * pop drains tag first. */ + int tag = cg_tag_for_variant(widen_param[i], + args[i]->type); + if (tag < 0) tag = 0; + int sz = widen_sz[i]; + if (sz == 8) { + /* Nullable fold: the pointer value IS the + * discriminator — no separate tag word. */ + cgexpr(c, args[i], locals); + ins1(c, A_PUSHQ, areg(D_AX)); + continue; + } + cgexpr(c, args[i], locals); + if (node_isstr(args[i])) { + /* slot 24: [+0]=tag,[+8]=ptr,[+16]=len */ + ins1(c, A_PUSHQ, areg(D_BX)); + ins1(c, A_PUSHQ, areg(D_AX)); + ins2(c, A_MOVQ, aimm(tag), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } else { + /* Scalar variant: single value word at +8. + * Pad a zero high word when the slot is 24B + * (some other variant of the union is 16B). */ + if (sz > 16) { + ins2(c, A_XORQ, areg(D_DX), + areg(D_DX)); + ins1(c, A_PUSHQ, areg(D_DX)); + } + ins1(c, A_PUSHQ, areg(D_AX)); + ins2(c, A_MOVQ, aimm(tag), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } + continue; + } cgexpr(c, args[i], locals); if (node_isfloat(args[i])) { ins2(c, A_SUBQ, aimm(8), areg(D_SP)); @@ -2179,6 +2248,20 @@ cgexpr(Cg *c, Node *n, Local *locals) * responsible for cleaning them up after CALL. */ int ii = 0, fi = 0, stackslots = 0; for (int i = 0; i < argcount; i++) { + if (widen[i]) { + /* Pop widened tagged slot into arg-register + * class — sized by the parameter's tagged slot, + * not the arg's static type. */ + int eb = widen_sz[i] / 8; + for (int k = 0; k < eb; k++) { + if (ii < 6) + ins1(c, A_POPQ, + areg(sysv_argregs[ii++])); + else + stackslots++; + } + continue; + } if (node_isfloat(args[i])) { if (fi < 8) { ins2(c, A_MOVSD, amem(D_SP, 0), @@ -2232,9 +2315,6 @@ cgexpr(Cg *c, Node *n, Local *locals) /* SysV: variadic callees require AL to hold the count of * XMM regs used in the variable portion. We don't pass * floats yet, so AL=0 covers every case we emit. */ - Type *callee_t = n->lhs ? n->lhs->type : NULL; - Type *cu = (callee_t && callee_t->kind == TY_NAMED) ? - callee_t->under : callee_t; if (cu && cu->kind == TY_FN && cu->variadic) ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); if (n->lhs->kind == N_IDENT) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index c4af8a90..3c810829 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -5561,9 +5561,77 @@ use strconv; // the order on the stack so a left-to-right pop into argregs lands // (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the // pop sequence then yields AX, then BX. -fn pushargsrev(c: *cgen, arg: *node) i32 = { +// +// `param` is the corresponding declared parameter for `arg` (N_PARAM +// node from the callee's signature) or nil. When param's type is a +// tagged union and `arg`'s surface type is a concrete variant of it, +// we materialise (tag, value-words, pad) for the parameter slot before +// pushing — mirrors cmd/w6c/cgen.c's call-arg widening. +fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { if (arg == nil) { return 0; }; - let rest: i32 = pushargsrev(c, arg.next); + let nextparam: *node = nil; + if (param != nil) { nextparam = param.next; }; + let rest: i32 = pushargsrev(c, arg.next, nextparam); + // Implicit widening from a concrete variant to a tagged-union + // parameter slot. Skips when the arg is already a tagged local + // (line 121's slice-or-tagged shortcut handles that). + let widensz: i32 = 0; + let widentag: i32 = 0; + if (param != nil) { + if (param.kind == nkind.N_PARAM) { + let ptype: *node = param.lhs; + if (istaggedtype(c, ptype)) { + let aistagged: bool = false; + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + aistagged = istaggedtype(c, lc.tnode); + }; + }; + if (!aistagged) { + widensz = slotsize(c, ptype); + let tagged: *node = resolvetagged(c, ptype); + let t: i32 = taggedvariantindex(c, tagged, arg); + if (t < 0) { t = 0; }; + widentag = t; + }; + }; + }; + }; + if (widensz == 8) { + // Nullable fold: pointer value IS the discriminator. No + // separate tag word. + cgexpr(c, arg); + emitline("\tPUSHQ\tAX\n"); + return rest + 1; + }; + if (widensz > 0) { + cgexpr(c, arg); + if (nodeisstr(c, arg)) { + // slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low + // so pop drains tag first into arg-reg[0]. + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t$"); + emitint(widentag: i64); + emitline(", AX\n"); + emitline("\tPUSHQ\tAX\n"); + } else { + // Scalar variant: single value word at +8. Pad a zero + // high word when slot is 24B (some other variant of + // the union is 16B-shaped). + if (widensz > 16) { + emitline("\tXORQ\tDX, DX\n"); + emitline("\tPUSHQ\tDX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t$"); + emitint(widentag: i64); + emitline(", AX\n"); + emitline("\tPUSHQ\tAX\n"); + }; + return rest + widensz / 8; + }; // nkind.N_SLICE expression as arg: `buf[lo:hi]` builds a slice header // on the stack matching C cgen's sequence — push base, push hi, // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). @@ -8880,7 +8948,17 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; - let nargs: i32 = pushargsrev(c, n.list); + // Look up the callee's declared params for tagged-union widening. + // fn-pointer calls (callee is a local) don't get widening — the + // user must build the tagged value explicitly. Matches the most + // common case (direct named calls). + let calleeparams: *node = nil; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { + calleeparams = fnparamslookup(c, callee.str); + }; + }; + let nargs: i32 = pushargsrev(c, n.list, calleeparams); // Pop forward. Float args were pushed as 8 bytes from X0 via // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else // pops into the int stream (DI..R9) per the SysV ABI. Walk the @@ -12777,6 +12855,7 @@ fn emitdatasection(c: *cgen) void = { type fnret = struct { fname: str, rtype: *node, + params: *node, frnext: *fnret, }; @@ -12785,9 +12864,10 @@ fn collectfnrets(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { - let f: *fnret = amalloc(c.a, 32u64): *fnret; + let f: *fnret = amalloc(c.a, 48u64): *fnret; f.fname = d.str; f.rtype = d.lhs; + f.params = d.list; f.frnext = c.fnrets; c.fnrets = f; }; @@ -12805,6 +12885,19 @@ fn fnretlookup(c: *cgen, name: str) *node = { return nil; }; +// fnparamslookup — head of the declared param-list for a fn, or nil +// if the name isn't a registered fn. Used by cgcall / pushargsrev to +// detect implicit widening from a concrete variant into a tagged-union +// parameter slot. +fn fnparamslookup(c: *cgen, name: str) *node = { + let f: *fnret = c.fnrets; + for (f != nil) { + if (streq(f.fname, name)) { return f.params; }; + f = f.frnext; + }; + return nil; +}; + // ---- def-constant registry ------------------------------------------ // // `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index eba1de70..bc05c1d9 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1315,6 +1315,7 @@ fn emitdatasection(c: *cgen) void = { type fnret = struct { fname: str, rtype: *node, + params: *node, frnext: *fnret, }; @@ -1323,9 +1324,10 @@ fn collectfnrets(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { - let f: *fnret = amalloc(c.a, 32u64): *fnret; + let f: *fnret = amalloc(c.a, 48u64): *fnret; f.fname = d.str; f.rtype = d.lhs; + f.params = d.list; f.frnext = c.fnrets; c.fnrets = f; }; @@ -1343,6 +1345,19 @@ fn fnretlookup(c: *cgen, name: str) *node = { return nil; }; +// fnparamslookup — head of the declared param-list for a fn, or nil +// if the name isn't a registered fn. Used by cgcall / pushargsrev to +// detect implicit widening from a concrete variant into a tagged-union +// parameter slot. +fn fnparamslookup(c: *cgen, name: str) *node = { + let f: *fnret = c.fnrets; + for (f != nil) { + if (streq(f.fname, name)) { return f.params; }; + f = f.frnext; + }; + return nil; +}; + // ---- def-constant registry ------------------------------------------ // // `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 07154f37..8a22611c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1961,7 +1961,17 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; - let nargs: i32 = pushargsrev(c, n.list); + // Look up the callee's declared params for tagged-union widening. + // fn-pointer calls (callee is a local) don't get widening — the + // user must build the tagged value explicitly. Matches the most + // common case (direct named calls). + let calleeparams: *node = nil; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { + calleeparams = fnparamslookup(c, callee.str); + }; + }; + let nargs: i32 = pushargsrev(c, n.list, calleeparams); // Pop forward. Float args were pushed as 8 bytes from X0 via // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else // pops into the int stream (DI..R9) per the SysV ABI. Walk the diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 73db176c..bb8d15be 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -27,9 +27,77 @@ use strconv; // the order on the stack so a left-to-right pop into argregs lands // (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the // pop sequence then yields AX, then BX. -fn pushargsrev(c: *cgen, arg: *node) i32 = { +// +// `param` is the corresponding declared parameter for `arg` (N_PARAM +// node from the callee's signature) or nil. When param's type is a +// tagged union and `arg`'s surface type is a concrete variant of it, +// we materialise (tag, value-words, pad) for the parameter slot before +// pushing — mirrors cmd/w6c/cgen.c's call-arg widening. +fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { if (arg == nil) { return 0; }; - let rest: i32 = pushargsrev(c, arg.next); + let nextparam: *node = nil; + if (param != nil) { nextparam = param.next; }; + let rest: i32 = pushargsrev(c, arg.next, nextparam); + // Implicit widening from a concrete variant to a tagged-union + // parameter slot. Skips when the arg is already a tagged local + // (line 121's slice-or-tagged shortcut handles that). + let widensz: i32 = 0; + let widentag: i32 = 0; + if (param != nil) { + if (param.kind == nkind.N_PARAM) { + let ptype: *node = param.lhs; + if (istaggedtype(c, ptype)) { + let aistagged: bool = false; + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + aistagged = istaggedtype(c, lc.tnode); + }; + }; + if (!aistagged) { + widensz = slotsize(c, ptype); + let tagged: *node = resolvetagged(c, ptype); + let t: i32 = taggedvariantindex(c, tagged, arg); + if (t < 0) { t = 0; }; + widentag = t; + }; + }; + }; + }; + if (widensz == 8) { + // Nullable fold: pointer value IS the discriminator. No + // separate tag word. + cgexpr(c, arg); + emitline("\tPUSHQ\tAX\n"); + return rest + 1; + }; + if (widensz > 0) { + cgexpr(c, arg); + if (nodeisstr(c, arg)) { + // slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low + // so pop drains tag first into arg-reg[0]. + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t$"); + emitint(widentag: i64); + emitline(", AX\n"); + emitline("\tPUSHQ\tAX\n"); + } else { + // Scalar variant: single value word at +8. Pad a zero + // high word when slot is 24B (some other variant of + // the union is 16B-shaped). + if (widensz > 16) { + emitline("\tXORQ\tDX, DX\n"); + emitline("\tPUSHQ\tDX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t$"); + emitint(widentag: i64); + emitline(", AX\n"); + emitline("\tPUSHQ\tAX\n"); + }; + return rest + widensz / 8; + }; // nkind.N_SLICE expression as arg: `buf[lo:hi]` builds a slice header // on the stack matching C cgen's sequence — push base, push hi, // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 67cb53be..98ca125c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -5561,9 +5561,77 @@ use strconv; // the order on the stack so a left-to-right pop into argregs lands // (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the // pop sequence then yields AX, then BX. -fn pushargsrev(c: *cgen, arg: *node) i32 = { +// +// `param` is the corresponding declared parameter for `arg` (N_PARAM +// node from the callee's signature) or nil. When param's type is a +// tagged union and `arg`'s surface type is a concrete variant of it, +// we materialise (tag, value-words, pad) for the parameter slot before +// pushing — mirrors cmd/w6c/cgen.c's call-arg widening. +fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { if (arg == nil) { return 0; }; - let rest: i32 = pushargsrev(c, arg.next); + let nextparam: *node = nil; + if (param != nil) { nextparam = param.next; }; + let rest: i32 = pushargsrev(c, arg.next, nextparam); + // Implicit widening from a concrete variant to a tagged-union + // parameter slot. Skips when the arg is already a tagged local + // (line 121's slice-or-tagged shortcut handles that). + let widensz: i32 = 0; + let widentag: i32 = 0; + if (param != nil) { + if (param.kind == nkind.N_PARAM) { + let ptype: *node = param.lhs; + if (istaggedtype(c, ptype)) { + let aistagged: bool = false; + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + aistagged = istaggedtype(c, lc.tnode); + }; + }; + if (!aistagged) { + widensz = slotsize(c, ptype); + let tagged: *node = resolvetagged(c, ptype); + let t: i32 = taggedvariantindex(c, tagged, arg); + if (t < 0) { t = 0; }; + widentag = t; + }; + }; + }; + }; + if (widensz == 8) { + // Nullable fold: pointer value IS the discriminator. No + // separate tag word. + cgexpr(c, arg); + emitline("\tPUSHQ\tAX\n"); + return rest + 1; + }; + if (widensz > 0) { + cgexpr(c, arg); + if (nodeisstr(c, arg)) { + // slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low + // so pop drains tag first into arg-reg[0]. + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t$"); + emitint(widentag: i64); + emitline(", AX\n"); + emitline("\tPUSHQ\tAX\n"); + } else { + // Scalar variant: single value word at +8. Pad a zero + // high word when slot is 24B (some other variant of + // the union is 16B-shaped). + if (widensz > 16) { + emitline("\tXORQ\tDX, DX\n"); + emitline("\tPUSHQ\tDX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t$"); + emitint(widentag: i64); + emitline(", AX\n"); + emitline("\tPUSHQ\tAX\n"); + }; + return rest + widensz / 8; + }; // nkind.N_SLICE expression as arg: `buf[lo:hi]` builds a slice header // on the stack matching C cgen's sequence — push base, push hi, // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). @@ -8880,7 +8948,17 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; - let nargs: i32 = pushargsrev(c, n.list); + // Look up the callee's declared params for tagged-union widening. + // fn-pointer calls (callee is a local) don't get widening — the + // user must build the tagged value explicitly. Matches the most + // common case (direct named calls). + let calleeparams: *node = nil; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { + calleeparams = fnparamslookup(c, callee.str); + }; + }; + let nargs: i32 = pushargsrev(c, n.list, calleeparams); // Pop forward. Float args were pushed as 8 bytes from X0 via // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else // pops into the int stream (DI..R9) per the SysV ABI. Walk the @@ -12777,6 +12855,7 @@ fn emitdatasection(c: *cgen) void = { type fnret = struct { fname: str, rtype: *node, + params: *node, frnext: *fnret, }; @@ -12785,9 +12864,10 @@ fn collectfnrets(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { - let f: *fnret = amalloc(c.a, 32u64): *fnret; + let f: *fnret = amalloc(c.a, 48u64): *fnret; f.fname = d.str; f.rtype = d.lhs; + f.params = d.list; f.frnext = c.fnrets; c.fnrets = f; }; @@ -12805,6 +12885,19 @@ fn fnretlookup(c: *cgen, name: str) *node = { return nil; }; +// fnparamslookup — head of the declared param-list for a fn, or nil +// if the name isn't a registered fn. Used by cgcall / pushargsrev to +// detect implicit widening from a concrete variant into a tagged-union +// parameter slot. +fn fnparamslookup(c: *cgen, name: str) *node = { + let f: *fnret = c.fnrets; + for (f != nil) { + if (streq(f.fname, name)) { return f.params; }; + f = f.frnext; + }; + return nil; +}; + // ---- def-constant registry ------------------------------------------ // // `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an