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:
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 ---- */