kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit cc72c49f15d87bd56dbed760c9a310e12de541a9
parent 40c33a8064ee77a7d0d61bad473978394e834f2e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 11 May 2026 09:58:28 -0700

asm/inline: integration — wire AsmConstraint.type through binder + walker

Phase 4b tracks A/B/C each landed against typed seams without knowing
each other's structs. This commit threads the missing piece: the C type
of each bound expression flows from parser → binder → arch backend.

- arch.h: extend AsmConstraint with `const Type* type`. NULL falls back
  to a 64-bit int default (used by hand-built unit-test constraints);
  the parser always populates it.
- parse.c (Track A site): populate `c.type` from `cg_top_type` on both
  outputs (lvalue's resolved type, captured before `cg_addr`) and inputs
  (rvalue's type, captured after `to_rvalue`).
- cg.c (Track B binder): drop the `default_out_ty` TODO. Each freshly-
  allocated output reg now uses `outs[i].type` + `type_class(...)`, so
  FP outputs land in RC_FP and pointer outputs keep their pointer type.
- aa64_asm.c (Track C walker): enable `%[name]` resolution against
  AsmConstraint.name via a linear search over outs then ins; the
  Track-C placeholder rejection diagnostic is gone.
- test/cg/binder_test.c: convert positional AsmConstraint initializers
  to designated form (struct gained two fields since Track B was
  authored). Add `test_output_type_drives_regclass` covering FP and
  pointer outputs — asserts the binder routes RC_FP/RC_INT correctly
  and preserves the bound Type through `op_reg`.

test-cg-binder: 32 cases / 0 failures.
test-aa64-inline: ok.
test-isa: 37 cases ok.
test-asm: 5 pass, 0 fail, 1 skip (E-runner unavailable on host).
Full `make test` matches Track A's baseline counts: 1126 pass / 18 fail
(pre-existing attr_p2_*) / 2 skip (pre-existing long_double + Track A's
asm_01_grammar.skip — the latter stays skipped pending dmb-sy/asm-goto
support, neither in v1 scope).

Diffstat:
Msrc/arch/aa64_asm.c | 27++++++++++++++++++++++++---
Msrc/arch/arch.h | 10+++++++---
Msrc/cg/cg.c | 31+++++++++++++++++++------------
Msrc/parse/parse.c | 5+++++
Mtest/arch/aa64_inline_test.c | 2+-
Mtest/cg/binder_test.c | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mtest/debug/roundtrip_unit.c | 2+-
7 files changed, 111 insertions(+), 32 deletions(-)

diff --git a/src/arch/aa64_asm.c b/src/arch/aa64_asm.c @@ -1059,9 +1059,30 @@ static void render_and_run_line(AA64Asm* a, MCEmitter* mc, StrBuf* sb, continue; } if (n == '[') { - /* %[name] — Track A ships the AsmConstraint name field; v1 has - * no name carrier yet, so this rejects with a clear message. */ - inline_panic(a, "%[name] requires AsmConstraint.name (Track A pending)"); + /* %[name] — scan to the closing ']' and resolve against + * AsmConstraint.name on the combined outs+ins list. Match by + * comparing the named-bracket contents against the interned name + * Sym stored on each constraint. */ + const char* nbeg = p + 2; + const char* nend = nbeg; + while (nend < end && *nend != ']') ++nend; + if (nend == end) inline_panic(a, "unterminated %[name]"); + size_t nlen = (size_t)(nend - nbeg); + Sym needle = pool_intern(a->c->global, nbeg, nlen); + u32 idx = (u32)-1; + for (u32 k = 0; k < a->nout; ++k) { + if (a->outs[k].name == needle) { idx = k; break; } + } + if (idx == (u32)-1) { + for (u32 k = 0; k < a->nin; ++k) { + if (a->ins[k].name == needle) { idx = a->nout + k; break; } + } + } + if (idx == (u32)-1) + inline_panic(a, "%[name] does not match any constraint"); + p = nend; /* loop's ++p steps past the ']' */ + render_operand(a, sb, idx, 0); + continue; } int form = 0; /* 0=default, 1=w, 2=x, 3=a */ if (n == 'w' || n == 'x' || n == 'a') { diff --git a/src/arch/arch.h b/src/arch/arch.h @@ -377,9 +377,13 @@ typedef struct CGScopeDesc { typedef enum AsmDir { ASM_IN, ASM_OUT, ASM_INOUT } AsmDir; typedef struct AsmConstraint { - const char* str; /* GCC-style: "r", "=&r", "+m", "i", "0" ... */ - Sym name; /* GCC `[name]` symbolic operand; 0 if absent */ - u8 dir; /* AsmDir */ + const char* str; /* GCC-style: "r", "=&r", "+m", "i", "0" ... */ + Sym name; /* GCC `[name]` symbolic operand; 0 if absent */ + const Type* type; /* C type of the bound expression (output lvalue or + input rvalue). Drives RegClass + width for the + binder. NULL only for hand-built test constraints + (binder falls back to a 64-bit int default). */ + u8 dir; /* AsmDir */ u8 pad[3]; } AsmConstraint; diff --git a/src/cg/cg.c b/src/cg/cg.c @@ -1520,11 +1520,11 @@ void cg_set_loc(CG* g, SrcLoc loc) { * * The parser pushed `nin` input SValues onto the value stack in declaration * order (the Nth input is at the top). Outputs come back as fresh SValues - * that the parser assigns to its declared lvalues. The signature does not - * carry per-output Type info today; outputs that need a fresh register get - * an arch-default 64-bit int type. TODO(track-A): when the parser starts - * carrying output types alongside AsmConstraint, route them through here so - * RegClass and width are correct for FP/short outputs. + * that the parser assigns to its declared lvalues. AsmConstraint.type + * carries the bound expression's C type (parser-populated); the binder + * routes it through alloc_reg + type_class so FP outputs land in RC_FP, + * pointer outputs keep their pointer type, and narrow types get the right + * width. Hand-built test constraints (NULL type) fall back to 64-bit int. * * Constraints handled: * inputs : "r" (force into REG), "i" (must be IMM), @@ -1571,8 +1571,10 @@ void cg_inline_asm(CG* g, const char* tmpl, const AsmConstraint* outs, u32 nout, u32 nclob) { CGTarget* T = g->target; Heap* h = g->c->env->heap; - /* Default output type for the v1 binder. RC_INT, 64-bit. */ - const Type* default_out_ty = type_prim(g->pool, TY_LLONG); + /* Fallback for hand-built test constraints that don't carry a type. The + * parser always populates AsmConstraint.type from the bound expression's + * C type; only unit-test constraints leave it NULL. */ + const Type* fallback_ty = type_prim(g->pool, TY_LLONG); /* ---- pop inputs in reverse, store in declaration order ---- */ SValue* in_svs = NULL; @@ -1611,8 +1613,10 @@ void cg_inline_asm(CG* g, const char* tmpl, const AsmConstraint* outs, u32 nout, const char* body = asm_constraint_body(outs[i].str); if (asm_is_early_clobber(outs[i].str)) continue; if (body[0] == 'r') { - Reg r = alloc_reg_or_spill(g, RC_INT, default_out_ty); - out_ops[i] = op_reg(r, default_out_ty); + const Type* oty = outs[i].type ? outs[i].type : fallback_ty; + u8 cls = type_class(oty); + Reg r = alloc_reg_or_spill(g, cls, oty); + out_ops[i] = op_reg(r, oty); out_reg_owned[i] = 1; } else { compiler_panic(g->c, g->cur_loc, @@ -1714,7 +1718,9 @@ void cg_inline_asm(CG* g, const char* tmpl, const AsmConstraint* outs, u32 nout, "cg_inline_asm: unsupported early-clobber constraint '%s'", outs[i].str); } - Reg r = alloc_reg_or_spill(g, RC_INT, default_out_ty); + const Type* oty = outs[i].type ? outs[i].type : fallback_ty; + u8 cls = type_class(oty); + Reg r = alloc_reg_or_spill(g, cls, oty); /* Validate disjoint: walk inputs, collide-check. The pool guarantees * uniqueness against currently-allocated regs, so this is belt-and- * suspenders, but the panic gives a meaningful diagnostic if any @@ -1731,7 +1737,7 @@ void cg_inline_asm(CG* g, const char* tmpl, const AsmConstraint* outs, u32 nout, "input INDIRECT base (binder bug)"); } } - out_ops[i] = op_reg(r, default_out_ty); + out_ops[i] = op_reg(r, oty); out_reg_owned[i] = 1; } @@ -1778,7 +1784,8 @@ void cg_inline_asm(CG* g, const char* tmpl, const AsmConstraint* outs, u32 nout, * Each pushed SValue owns the freshly-allocated reg (RES_REG), so the * parser's eventual cg_store on it will release the reg after consuming. */ for (u32 i = 0; i < nout; ++i) { - SValue sv = make_sv(out_ops[i], default_out_ty); + const Type* oty = outs[i].type ? outs[i].type : fallback_ty; + SValue sv = make_sv(out_ops[i], oty); /* If the target overwrote out_ops[i] with a different kind (e.g. a * memory location), make_sv already classified residency correctly. */ if (!out_reg_owned[i] && sv.res == RES_REG) { diff --git a/src/parse/parse.c b/src/parse/parse.c @@ -5831,6 +5831,7 @@ static void parse_asm_stmt(Parser* p) { parse_assign_expr(p); val_ty = cg_top_type(p->cg); if (!val_ty) perr(p, "asm output: cannot determine lvalue type"); + c.type = val_ty; cg_addr(p->cg); ptr_ty = cg_top_type(p->cg); if (!ptr_ty) perr(p, "asm output: cannot take address"); @@ -5883,6 +5884,10 @@ static void parse_asm_stmt(Parser* p) { * cg_inline_asm consumes them per its docstring. */ parse_assign_expr(p); to_rvalue(p); + /* Capture the rvalue's C type for the binder. cg_top_type + * is valid after to_rvalue while the value is still on top + * of the CG stack. */ + c.type = cg_top_type(p->cg); expect_punct(p, ')', "')' after asm input expression"); if (nin == cap_in) { u32 nc = cap_in * 2; diff --git a/test/arch/aa64_inline_test.c b/test/arch/aa64_inline_test.c @@ -63,7 +63,7 @@ static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, fputc('\n', stderr); } static CfreeDiagSink g_sink = {diag_emit, 0, 0, 0}; -static CfreeEnv g_env = {&g_heap, NULL, &g_sink, NULL, 0}; +static CfreeEnv g_env = {.heap = &g_heap, .diag = &g_sink, .now = -1}; static int g_fail = 0; #define EXPECT(cond, ...) \ diff --git a/test/cg/binder_test.c b/test/cg/binder_test.c @@ -296,7 +296,7 @@ static void test_r_in(void) { tc_init(&tc); /* asm("nop" :: "r"(42)) */ cg_push_int(tc.g, 42, tc.i64_ty); - AsmConstraint ins[1] = {{"r", ASM_IN, {0,0,0}}}; + AsmConstraint ins[1] = {{.str="r", .dir=ASM_IN}}; cg_inline_asm(tc.g, "nop", NULL, 0, ins, 1, NULL, 0); EXPECT(tc.mt.asm_called, "asm_block was not invoked"); EXPECT(tc.mt.nin == 1, "nin=%u", tc.mt.nin); @@ -312,7 +312,7 @@ static void test_eq_r_out(void) { TestCtx tc; tc_init(&tc); /* asm("mov %0, #1" : "=r"(x)) — pushes an output SValue back. */ - AsmConstraint outs[1] = {{"=r", ASM_OUT, {0,0,0}}}; + AsmConstraint outs[1] = {{.str="=r", .dir=ASM_OUT}}; cg_inline_asm(tc.g, "mov %0, #1", outs, 1, NULL, 0, NULL, 0); EXPECT(tc.mt.asm_called, "asm_block was not invoked"); EXPECT(tc.mt.nout == 1, "nout=%u", tc.mt.nout); @@ -329,8 +329,8 @@ static void test_plus_r_inout(void) { * convention this binder honors: emit one output with =r-style behavior * and one matching input "0" with the input value to seed the reg. */ cg_push_int(tc.g, 7, tc.i64_ty); - AsmConstraint outs[1] = {{"+r", ASM_INOUT, {0,0,0}}}; - AsmConstraint ins[1] = {{"0", ASM_IN, {0,0,0}}}; + AsmConstraint outs[1] = {{.str="+r", .dir=ASM_INOUT}}; + AsmConstraint ins[1] = {{.str="0", .dir=ASM_IN}}; cg_inline_asm(tc.g, "add %0, %0, #1", outs, 1, ins, 1, NULL, 0); EXPECT(tc.mt.nout == 1 && tc.mt.nin == 1, "nout/nin"); EXPECT(tc.mt.out_ops[0].kind == OPK_REG, "out reg"); @@ -346,8 +346,8 @@ static void test_eq_amp_r_early_clobber(void) { tc_init(&tc); /* asm("..." : "=&r"(x) : "r"(y)) — output reg must differ from input reg. */ cg_push_int(tc.g, 5, tc.i64_ty); - AsmConstraint outs[1] = {{"=&r", ASM_OUT, {0,0,0}}}; - AsmConstraint ins[1] = {{"r", ASM_IN, {0,0,0}}}; + AsmConstraint outs[1] = {{.str="=&r", .dir=ASM_OUT}}; + AsmConstraint ins[1] = {{.str="r", .dir=ASM_IN}}; cg_inline_asm(tc.g, "tmpl", outs, 1, ins, 1, NULL, 0); EXPECT(tc.mt.out_ops[0].kind == OPK_REG && tc.mt.in_ops[0].kind == OPK_REG, "REGs expected"); @@ -361,7 +361,7 @@ static void test_i_constant(void) { TestCtx tc; tc_init(&tc); cg_push_int(tc.g, 99, tc.i64_ty); - AsmConstraint ins[1] = {{"i", ASM_IN, {0,0,0}}}; + AsmConstraint ins[1] = {{.str="i", .dir=ASM_IN}}; cg_inline_asm(tc.g, "tmpl", NULL, 0, ins, 1, NULL, 0); EXPECT(tc.mt.in_ops[0].kind == OPK_IMM, "in kind=%u", tc.mt.in_ops[0].kind); EXPECT(tc.mt.in_ops[0].v.imm == 99, "in imm=%lld", @@ -386,7 +386,7 @@ static void test_m_memory_lvalue(void) { /* Use the type-aware push path. We declare it via prototype: */ void cg_push_local_typed(CG*, FrameSlot, const Type*); cg_push_local_typed(tc.g, s, tc.i64_ty); - AsmConstraint ins[1] = {{"m", ASM_IN, {0,0,0}}}; + AsmConstraint ins[1] = {{.str="m", .dir=ASM_IN}}; cg_inline_asm(tc.g, "ldr w0, %0", NULL, 0, ins, 1, NULL, 0); EXPECT(tc.mt.in_ops[0].kind == OPK_INDIRECT, "in kind=%u", tc.mt.in_ops[0].kind); @@ -400,8 +400,8 @@ static void test_matching_input(void) { tc_init(&tc); /* Output =r at index 0; input "0" should bind to its reg. */ cg_push_int(tc.g, 11, tc.i64_ty); - AsmConstraint outs[1] = {{"=r", ASM_OUT, {0,0,0}}}; - AsmConstraint ins[1] = {{"0", ASM_IN, {0,0,0}}}; + AsmConstraint outs[1] = {{.str="=r", .dir=ASM_OUT}}; + AsmConstraint ins[1] = {{.str="0", .dir=ASM_IN}}; cg_inline_asm(tc.g, "tmpl", outs, 1, ins, 1, NULL, 0); EXPECT(tc.mt.out_ops[0].v.reg == tc.mt.in_ops[0].v.reg, "matching '0' input should reuse out reg (out=%u in=%u)", @@ -433,7 +433,7 @@ static void test_memory_clobber_spills_live_regs(void) { * promoting via cg_inline_asm itself — easier: skip this complexity and * directly observe spill via a real reg-resident value built by =r * output being pushed back. */ - AsmConstraint outs1[1] = {{"=r", ASM_OUT, {0,0,0}}}; + AsmConstraint outs1[1] = {{.str="=r", .dir=ASM_OUT}}; cg_inline_asm(tc.g, "produce", outs1, 1, NULL, 0, NULL, 0); /* Now the stack has a REG-resident SValue from the produced output. * Reset the log before the second call so we can scan for spill_reg @@ -468,7 +468,7 @@ static void test_cc_clobber_silent(void) { Sym cc_sym = pool_intern_cstr(tc.c->global, "cc"); Sym clobs[1] = {cc_sym}; /* Arrange a live REG-resident SValue first; verify it is NOT spilled. */ - AsmConstraint outs1[1] = {{"=r", ASM_OUT, {0,0,0}}}; + AsmConstraint outs1[1] = {{.str="=r", .dir=ASM_OUT}}; cg_inline_asm(tc.g, "produce", outs1, 1, NULL, 0, NULL, 0); tc.mt.log_len = 0; tc.mt.log[0] = '\0'; @@ -479,6 +479,47 @@ static void test_cc_clobber_silent(void) { tc_fini(&tc); } +/* AsmConstraint.type drives RegClass for fresh output regs. An FP-typed + * output must land in RC_FP; a pointer-typed output stays in RC_INT. + * Hand-built (NULL-type) constraints fall back to int / 64-bit (covered + * by every other case in this file). */ +static void test_output_type_drives_regclass(void) { + TestCtx tc; + tc_init(&tc); + + /* asm("..." : "=r"(double_var)) → RC_FP allocation. */ + const Type* dbl_ty = type_prim(tc.c->global, TY_DOUBLE); + AsmConstraint outs_fp[1] = {{.str="=r", .type=dbl_ty, .dir=ASM_OUT}}; + cg_inline_asm(tc.g, "fmov %0, #1.0", outs_fp, 1, NULL, 0, NULL, 0); + EXPECT(tc.mt.nout == 1, "nout=%u", tc.mt.nout); + EXPECT(tc.mt.out_ops[0].kind == OPK_REG, "fp out kind=%u", + tc.mt.out_ops[0].kind); + EXPECT(tc.mt.out_ops[0].cls == RC_FP, + "fp out cls=%u (expected RC_FP=%u)", tc.mt.out_ops[0].cls, RC_FP); + EXPECT(tc.mt.out_ops[0].type == dbl_ty, + "fp out type lost through binder"); + + /* Drop the SValue the previous call pushed back so the next call + * starts from a clean stack. */ + cg_drop(tc.g); + tc.mt.asm_called = 0; + tc.mt.nout = 0; + + /* asm("..." : "=r"(int_ptr)) → RC_INT, pointer type preserved. */ + const Type* ptr_ty = type_ptr(tc.c->global, type_prim(tc.c->global, TY_INT)); + AsmConstraint outs_p[1] = {{.str="=r", .type=ptr_ty, .dir=ASM_OUT}}; + cg_inline_asm(tc.g, "mov %0, sp", outs_p, 1, NULL, 0, NULL, 0); + EXPECT(tc.mt.out_ops[0].kind == OPK_REG, "ptr out kind=%u", + tc.mt.out_ops[0].kind); + EXPECT(tc.mt.out_ops[0].cls == RC_INT, + "ptr out cls=%u (expected RC_INT=%u)", tc.mt.out_ops[0].cls, RC_INT); + EXPECT(tc.mt.out_ops[0].type == ptr_ty, + "ptr out type lost through binder"); + + cg_drop(tc.g); + tc_fini(&tc); +} + int main(void) { test_r_in(); test_eq_r_out(); @@ -490,6 +531,7 @@ int main(void) { test_memory_clobber_spills_live_regs(); test_register_clobber_passthrough(); test_cc_clobber_silent(); + test_output_type_drives_regclass(); fprintf(stderr, "binder_test: %d cases, %d failures\n", g_cases, g_fails); return g_fails ? 1 : 0; diff --git a/test/debug/roundtrip_unit.c b/test/debug/roundtrip_unit.c @@ -55,7 +55,7 @@ static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, fputc('\n', stderr); } static CfreeDiagSink g_sink = {diag_emit, 0, 0, 0}; -static CfreeEnv g_env = {&g_heap, NULL, &g_sink, NULL, 0}; +static CfreeEnv g_env = {.heap = &g_heap, .diag = &g_sink, .now = -1}; /* ---- fail counters ---- */