commit 3aa84f3fb451b7cf4851639ddf223595bf6e230e
parent f2d3e01ce9089ff005e5f0542dda5288a416dbab
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 11 May 2026 11:29:07 -0700
asm/inline: complete end-to-end TODOs from doc/ASM.md §6
- Template walker: multi-digit operand index (%0..%99), bracket/quote-aware
`;` and `\n` line splitting, %w[name] / %x[name] / %a[name] width-modified
symbolic operands.
- Memory barriers: new AA64_FMT_BARRIER with dmb/dsb/isb/clrex rows,
printer, and per-mnemonic parsers accepting both option mnemonics
(sy/ish/nsh/osh/...) and #imm4. aa_fence and aa_atomic_cas's clrex
site consolidated onto the shared helpers — no more magic 0xD5033BBF.
- cg corpus: test/cg/harness/cases_asm.c smoke (output-only, input+output,
named clobber on a callee-saved reg) wired through D/R/E/J at opt 0/1.
The named-clobber case exercises the prologue hwm-bump path.
- Grammar test: lift test/parse/cases/asm_01_grammar.skip and add a `43`
expected (the +r(rc) increments rc=42). asm goto stripped from the
source per the v1 scope decision in §7.
Diffstat:
10 files changed, 375 insertions(+), 57 deletions(-)
diff --git a/doc/ASM.md b/doc/ASM.md
@@ -26,7 +26,7 @@ mov(reg/imm)/mvn/movz/movn/movk, add(s)/sub(s)/cmp/cmn/neg(s),
and/orr/eor/bic/orn/eon/ands/bics, madd/msub/mul/mneg,
udiv/sdiv/lslv/lsrv/asrv/rorv, b/bl/b.<cc>/cbz/cbnz, svc/brk/hlt, ldr/str
(scaled + simm9 fallback), ldur/stur, ldp/stp (signed-offset + pre-indexed),
-adr/adrp`.
+adr/adrp, dmb/dsb/isb/clrex`.
---
@@ -115,20 +115,31 @@ shared truth crossing all three.
index). `AsmConstraint` carries `{str, name, type, dir}` — `name` is the
optional `[name]` Sym, `type` is the bound expression's C type (drives
`RegClass` + width). Hand-built test constraints with `NULL` type fall back
-to 64-bit int.
+to 64-bit int. `+r` is parser-decomposed into `=r` + a synthesized matching
+`"<k>"` input that pushes the lvalue's current value; the binder's
+matching-constraint path then copies it into the bound output reg before
+the asm runs.
**Clobbers**:
- `"memory"` — spill all live RES_REG SValues via `target->spill_reg`;
subsequent reads reload through `target->reload_reg`. Same machinery cg
uses across function calls.
-- Register names (`"x0"`, …) — passed through to `target->asm_block`.
+- Register names (`"x0"`, …) — resolved via `target->resolve_reg_name`.
+ `cg_inline_asm` spills any live SValue currently bound to the named
+ phys reg (when `"memory"` didn't already sweep the stack) and rejects
+ overlaps with in/out operands. `aa_asm_block` additionally bumps the
+ callee-save high-water mark so the prologue saves/restores callee-saved
+ regs the asm body trashes even when no SValue ever bound them.
- `"cc"` — silently ignored on aarch64 (NZCV reserved across the block).
-**Placeholders**: `%N`, `%wN` (force W form), `%xN` (force X form),
-`%[name]` (resolved against `AsmConstraint.name`), `%aN` (memory addressing
-form), `%%`. The walker pre-substitutes them into asm source text and
-re-lexes through the standalone per-mnemonic parsers — no second operand
-grammar.
+**Placeholders**: `%N` / `%NN` (1- or 2-digit operand index), `%wN` (force
+W form), `%xN` (force X form), `%[name]` and `%w[name]`/`%x[name]`/`%a[name]`
+(symbolic operand, optionally with width modifier; resolved against
+`AsmConstraint.name`), `%aN` (memory addressing form), `%%`. The walker
+pre-substitutes them into asm source text and re-lexes through the
+standalone per-mnemonic parsers — no second operand grammar. Line splits
+on `;` and `\n` honor bracket depth and quoted-string state, so a literal
+`;` inside `[...]` does not split a statement.
**IR**: `IR_ASM_BLOCK` with `IRAsmAux { tmpl, outs, ins, in_ops, out_ops,
clobbers, nout, nin, nclob }`. The opt recorder arena-copies the payload;
@@ -186,20 +197,10 @@ bash test/cg/run.sh '' S # just S
## 6. Remaining TODOs
-End-to-end inline asm:
-
-- [ ] `test/cg/cases_asm.c` smoke case (smallest: `__asm__ volatile("mov
- w0,%w0; svc #0" :: "r"(rc) : "x0")`) wired into the cg corpus on the
- `DREJWS` path matrix.
-- [ ] Lift `test/parse/cases/asm_01_grammar.skip` once `dmb sy` and
- `asm goto` reach `aa64_insn_table` + parser.
-- [ ] `aa64_asm_run_template`: support multi-digit operand index (`%10+`);
- respect `;` inside `[...]` / quoted strings rather than splitting on
- every `;`.
-- [ ] `aa_asm_block`: consume `clobbers[]` to inform the aarch64 RA of
- register-name clobbers (today v1 relies on Track B's `"memory"`
- spill + parser pass-through; register-name clobbers don't yet evict
- from the allocator).
+End-to-end inline asm: complete (smoke case in `test/cg/harness/cases_asm.c`,
+grammar-test `.skip` lifted, multi-digit + bracket-aware template walker,
+named-clobber RA spill + prologue hwm bump). `asm goto` remains
+unsupported in v1 per §7 below.
`aa64_insn_table` coverage (gates `S` on the full cg corpus):
@@ -208,7 +209,8 @@ End-to-end inline asm:
- [ ] FP-DP1 / FP-DP2 (`fadd`/`fsub`/`fmul`/`fdiv`/`fneg`/`fabs`/`fsqrt`)
- [ ] FP↔int cvt (`fcvtzs`/`scvtf` families)
- [ ] ldst-exclusive (`ldxr`/`stxr`/`ldaxr`/`stlxr`)
-- [ ] memory barriers (`dmb`/`dsb`/`isb`/`clrex`)
+- [x] memory barriers (`dmb`/`dsb`/`isb`/`clrex`) — landed; consolidated
+ `aa_fence` onto the shared helper.
- [ ] system reg access (`mrs`/`msr`)
- [ ] data-processing 1-source (`rbit`/`rev`/`clz`/`cls`)
- [ ] SIMD basic (the cg-emitted subset)
diff --git a/src/arch/aa64_asm.c b/src/arch/aa64_asm.c
@@ -242,6 +242,61 @@ static void p_nop(AsmDriver* d) {
emit32(d, aa64_nop());
}
+/* Memory barriers (DMB / DSB / ISB / CLREX).
+ *
+ * dmb <option> ; option in {sy, ish, nsh, osh, ld, st, ishld,
+ * ishst, nshld, nshst, oshld, oshst}
+ * dmb #imm4 ; numeric form
+ * dsb <option> | #imm4
+ * isb [<option>] ; option defaults to sy when omitted
+ * clrex [#imm4] ; option defaults to sy (15) when omitted */
+static u32 parse_barrier_option(AsmDriver* d, int allow_dmb_ld_st) {
+ if (asm_driver_at_eol(d)) return AA64_BARRIER_OPT_SY;
+ Tok t = asm_driver_peek(d);
+ if (t.kind == TOK_IDENT) {
+ (void)asm_driver_next(d);
+ size_t n = 0;
+ const char* s = pool_str(asm_driver_pool(d), t.v.ident, &n);
+ if (icase_eq(s, n, "sy")) return AA64_BARRIER_OPT_SY;
+ if (icase_eq(s, n, "ish")) return AA64_BARRIER_OPT_ISH;
+ if (icase_eq(s, n, "ishld")) return AA64_BARRIER_OPT_ISHLD;
+ if (icase_eq(s, n, "ishst")) return AA64_BARRIER_OPT_ISHST;
+ if (icase_eq(s, n, "nsh")) return AA64_BARRIER_OPT_NSH;
+ if (icase_eq(s, n, "nshld")) return AA64_BARRIER_OPT_NSHLD;
+ if (icase_eq(s, n, "nshst")) return AA64_BARRIER_OPT_NSHST;
+ if (icase_eq(s, n, "osh")) return AA64_BARRIER_OPT_OSH;
+ if (icase_eq(s, n, "oshld")) return AA64_BARRIER_OPT_OSHLD;
+ if (icase_eq(s, n, "oshst")) return AA64_BARRIER_OPT_OSHST;
+ if (allow_dmb_ld_st) {
+ if (icase_eq(s, n, "ld")) return AA64_BARRIER_OPT_LD;
+ if (icase_eq(s, n, "st")) return AA64_BARRIER_OPT_ST;
+ }
+ asm_driver_panic(d, "asm: unknown barrier option");
+ }
+ /* Numeric form: '#imm4'. */
+ i64 imm = parse_imm_const(d);
+ if (imm < 0 || imm > 15)
+ asm_driver_panic(d, "asm: barrier imm out of range");
+ return (u32)imm;
+}
+
+static void p_dmb(AsmDriver* d) {
+ u32 opt = parse_barrier_option(d, /*allow_dmb_ld_st=*/1);
+ emit32(d, aa64_dmb(opt));
+}
+static void p_dsb(AsmDriver* d) {
+ u32 opt = parse_barrier_option(d, /*allow_dmb_ld_st=*/0);
+ emit32(d, aa64_dsb(opt));
+}
+static void p_isb(AsmDriver* d) {
+ u32 opt = parse_barrier_option(d, /*allow_dmb_ld_st=*/0);
+ emit32(d, aa64_isb(opt));
+}
+static void p_clrex(AsmDriver* d) {
+ u32 opt = parse_barrier_option(d, /*allow_dmb_ld_st=*/0);
+ emit32(d, aa64_clrex(opt));
+}
+
/* mov:
* mov Rd, Rm → ORR Rd, ZR, Rm
* mov Rd, #imm → MOVZ (if imm fits in a single halfword unshifted)
@@ -818,6 +873,10 @@ static void p_b_al(AsmDriver* d) { p_b_cond(d, 14); }
static const AA64Mn kTable[] = {
{"nop", p_nop, 0},
+ {"dmb", p_dmb, 0},
+ {"dsb", p_dsb, 0},
+ {"isb", p_isb, 0},
+ {"clrex", p_clrex, 0},
{"ret", p_ret, 0},
{"br", p_br, 0},
{"blr", p_blr, 0},
@@ -1091,14 +1150,40 @@ static void render_and_run_line(AA64Asm* a, MCEmitter* mc, StrBuf* sb,
if (p + 1 >= end) inline_panic(a, "trailing '%' modifier in template");
n = *(p + 1);
}
+ if (n == '[') {
+ /* %w[name] / %x[name] / %a[name] — width modifier + symbolic
+ * operand. Resolves the same way as %[name] but renders with the
+ * declared form. */
+ 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, form);
+ continue;
+ }
if (n < '0' || n > '9')
inline_panic(a, "expected digit after '%'");
- u32 idx = 0;
- /* Single-digit operand index. (10+ operands are exceedingly rare in
- * inline asm and would require GCC's two-digit syntax; v1 reads one
- * digit per the most common GCC convention.) */
- idx = (u32)(n - '0');
+ u32 idx = (u32)(n - '0');
++p;
+ /* GCC syntax permits up to two digits (%0..%99). */
+ if (p + 1 < end && *(p + 1) >= '0' && *(p + 1) <= '9') {
+ idx = idx * 10 + (u32)(*(p + 1) - '0');
+ ++p;
+ }
render_operand(a, sb, idx, form);
}
if (sb->truncated) inline_panic(a, "inline asm line buffer overflow");
@@ -1112,18 +1197,40 @@ void aa64_asm_run_template(AA64Asm* a, MCEmitter* mc, const char* tmpl) {
StrBuf sb;
strbuf_init(&sb, buf, sizeof buf);
- /* Walk tmpl, splitting on '\n' and ';' line terminators. v1 does not
- * try to honor `;` inside brackets / quoted strings — the substitution
- * grammar emits address forms via %aN as a single bracketed token, so
- * a real ';' inside `[ ... ]` would only arise from operator-written
- * asm that happens to spell a literal `;` inside the bracket text;
- * left as a TODO if a real test case needs it. */
+ /* Walk tmpl, splitting on '\n' and ';' line terminators. Track bracket
+ * depth and quote state so that a literal ';' inside `[ ... ]` or a
+ * quoted string is not mistaken for a statement separator. */
const char* line_start = tmpl;
+ int bracket = 0;
+ char quote = 0;
for (const char* p = tmpl;; ++p) {
char c = *p;
- if (c == '\0' || c == '\n' || c == ';') {
+ if (c == '\0') {
+ render_and_run_line(a, mc, &sb, line_start, p);
+ break;
+ }
+ if (quote) {
+ if (c == '\\' && *(p + 1)) {
+ ++p;
+ continue;
+ }
+ if (c == quote) quote = 0;
+ continue;
+ }
+ if (c == '"' || c == '\'') {
+ quote = c;
+ continue;
+ }
+ if (c == '[') {
+ ++bracket;
+ continue;
+ }
+ if (c == ']') {
+ if (bracket) --bracket;
+ continue;
+ }
+ if (bracket == 0 && (c == '\n' || c == ';')) {
render_and_run_line(a, mc, &sb, line_start, p);
- if (c == '\0') break;
line_start = p + 1;
}
}
diff --git a/src/arch/aa64_isa.c b/src/arch/aa64_isa.c
@@ -183,6 +183,13 @@ const AA64InsnDesc aa64_insn_table[] = {
/* ----- Hint ----- */
{"nop", 0xD503201Fu, 0xFFFFFFFFu, AA64_FMT_HINT, 0, {0, 0}},
+
+ /* ----- Memory barriers (DMB / DSB / ISB / CLREX) -----
+ * Mask covers everything but CRm at bits[11:8]. */
+ {"dmb", 0xD50330BFu, 0xFFFFF0FFu, AA64_FMT_BARRIER, 0, {0, 0}},
+ {"dsb", 0xD503309Fu, 0xFFFFF0FFu, AA64_FMT_BARRIER, 0, {0, 0}},
+ {"isb", 0xD50330DFu, 0xFFFFF0FFu, AA64_FMT_BARRIER, 0, {0, 0}},
+ {"clrex", 0xD503305Fu, 0xFFFFF0FFu, AA64_FMT_BARRIER, 0, {0, 0}},
};
const u32 aa64_insn_table_n =
@@ -512,6 +519,46 @@ static void print_except(StrBuf* sb, u32 w) {
strbuf_put_hex_u64(sb, (u64)f.imm16);
}
+static void print_barrier(StrBuf* sb, u32 w, const AA64InsnDesc* desc) {
+ AA64Barrier f = aa64_barrier_unpack(w);
+ /* ISB and CLREX with the default CRm=SY (15) print without an
+ * operand. DMB/DSB always carry an option. */
+ int is_isb = (f.op2 == AA64_BARRIER_OP2_ISB);
+ int is_clrex = (f.op2 == AA64_BARRIER_OP2_CLREX);
+ if ((is_isb || is_clrex) && f.CRm == AA64_BARRIER_OPT_SY) return;
+ const char* opt = NULL;
+ switch (f.CRm) {
+ case AA64_BARRIER_OPT_OSHLD: opt = "oshld"; break;
+ case AA64_BARRIER_OPT_OSHST: opt = "oshst"; break;
+ case AA64_BARRIER_OPT_OSH: opt = "osh"; break;
+ case AA64_BARRIER_OPT_NSHLD: opt = "nshld"; break;
+ case AA64_BARRIER_OPT_NSHST: opt = "nshst"; break;
+ case AA64_BARRIER_OPT_NSH: opt = "nsh"; break;
+ case AA64_BARRIER_OPT_ISHLD: opt = "ishld"; break;
+ case AA64_BARRIER_OPT_ISHST: opt = "ishst"; break;
+ case AA64_BARRIER_OPT_ISH: opt = "ish"; break;
+ case AA64_BARRIER_OPT_LD: opt = (desc && desc->mnemonic &&
+ desc->mnemonic[0] == 'd' &&
+ desc->mnemonic[1] == 'm')
+ ? "ld"
+ : NULL; break;
+ case AA64_BARRIER_OPT_ST: opt = (desc && desc->mnemonic &&
+ desc->mnemonic[0] == 'd' &&
+ desc->mnemonic[1] == 'm')
+ ? "st"
+ : NULL; break;
+ case AA64_BARRIER_OPT_SY: opt = "sy"; break;
+ default: break;
+ }
+ strbuf_putc(sb, ' ');
+ if (opt) {
+ strbuf_puts(sb, opt);
+ } else {
+ strbuf_puts(sb, "#");
+ strbuf_put_u64(sb, (u64)f.CRm);
+ }
+}
+
void aa64_print_operands(StrBuf* sb, const AA64InsnDesc* desc, u32 word,
u64 vaddr) {
switch ((AA64Format)desc->fmt) {
@@ -532,6 +579,7 @@ void aa64_print_operands(StrBuf* sb, const AA64InsnDesc* desc, u32 word,
case AA64_FMT_CB: print_cb(sb, word, vaddr); break;
case AA64_FMT_EXCEPT: print_except(sb, word); break;
case AA64_FMT_HINT: break; /* no operands for NOP */
+ case AA64_FMT_BARRIER: print_barrier(sb, word, desc); break;
}
}
diff --git a/src/arch/aa64_isa.h b/src/arch/aa64_isa.h
@@ -53,6 +53,7 @@ typedef enum AA64Format {
AA64_FMT_CB, /* compare-and-branch (CBZ / CBNZ) */
AA64_FMT_EXCEPT, /* exception generation (BRK / SVC / HVC / ...) */
AA64_FMT_HINT, /* hint (NOP / YIELD / ...) */
+ AA64_FMT_BARRIER, /* memory barrier (DMB / DSB / ISB / CLREX) */
} AA64Format;
/* ---- AsmFlags column on AA64InsnDesc ----
@@ -615,6 +616,69 @@ static inline u32 aa64_nop(void) {
}
/* ====================================================================
+ * Memory barriers (DMB / DSB / ISB / CLREX)
+ * 1101 0101 0000 0011 0011 CRm(4) op2(3) 11111
+ * 31..16 15..12 11..8 7..5 4..0
+ *
+ * Shared encoding family with HINT (which uses bits[15:12]=0010);
+ * barriers use bits[15:12]=0011. op2 selects the specific instruction:
+ * CLREX=010 DSB=100 DMB=101 ISB=110
+ * CRm is the option / domain (SY=15, ISH=11, NSH=7, OSH=3, ...).
+ * ==================================================================== */
+
+#define AA64_BARRIER_FAMILY_MATCH 0xD503301Fu
+#define AA64_BARRIER_FAMILY_MASK 0xFFFFF01Fu /* CRm + op2 vary */
+
+#define AA64_BARRIER_OP2_CLREX 2u
+#define AA64_BARRIER_OP2_DSB 4u
+#define AA64_BARRIER_OP2_DMB 5u
+#define AA64_BARRIER_OP2_ISB 6u
+
+/* Common CRm option encodings (ARM ARM C5.1.42). */
+#define AA64_BARRIER_OPT_OSHLD 1u
+#define AA64_BARRIER_OPT_OSHST 2u
+#define AA64_BARRIER_OPT_OSH 3u
+#define AA64_BARRIER_OPT_NSHLD 5u
+#define AA64_BARRIER_OPT_NSHST 6u
+#define AA64_BARRIER_OPT_NSH 7u
+#define AA64_BARRIER_OPT_ISHLD 9u
+#define AA64_BARRIER_OPT_ISHST 10u
+#define AA64_BARRIER_OPT_ISH 11u
+#define AA64_BARRIER_OPT_LD 13u
+#define AA64_BARRIER_OPT_ST 14u
+#define AA64_BARRIER_OPT_SY 15u
+
+typedef struct AA64Barrier {
+ u32 CRm, op2;
+} AA64Barrier;
+
+static inline u32 aa64_barrier_pack(AA64Barrier f) {
+ return AA64_BARRIER_FAMILY_MATCH | ((f.CRm & 0xfu) << 8) |
+ ((f.op2 & 7u) << 5);
+}
+
+static inline AA64Barrier aa64_barrier_unpack(u32 w) {
+ AA64Barrier f;
+ f.CRm = (w >> 8) & 0xfu;
+ f.op2 = (w >> 5) & 7u;
+ return f;
+}
+
+static inline u32 aa64_dmb(u32 opt) {
+ return aa64_barrier_pack((AA64Barrier){.CRm = opt, .op2 = AA64_BARRIER_OP2_DMB});
+}
+static inline u32 aa64_dsb(u32 opt) {
+ return aa64_barrier_pack((AA64Barrier){.CRm = opt, .op2 = AA64_BARRIER_OP2_DSB});
+}
+static inline u32 aa64_isb(u32 opt) {
+ return aa64_barrier_pack((AA64Barrier){.CRm = opt, .op2 = AA64_BARRIER_OP2_ISB});
+}
+static inline u32 aa64_clrex(u32 opt) {
+ return aa64_barrier_pack(
+ (AA64Barrier){.CRm = opt, .op2 = AA64_BARRIER_OP2_CLREX});
+}
+
+/* ====================================================================
* Load/store pair, signed-offset (STP / LDP, no pre/post-increment).
* opc(2) 101 V(1) 010 L(1) imm7 Rt2 Rn Rt (bit 23 = 0)
*
diff --git a/test/cg/harness/cases.c b/test/cg/harness/cases.c
@@ -225,6 +225,9 @@ void build_q08_forward_decl_define_late(CgTestCtx*);
void build_q09_helper_calls_helper(CgTestCtx*);
void build_q10_global_and_static_mix(CgTestCtx*);
void build_q11_addr_of_helper_through_global(CgTestCtx*);
+void build_asm01_mov_imm_out(CgTestCtx*);
+void build_asm02_copy_input(CgTestCtx*);
+void build_asm03_named_clobber(CgTestCtx*);
/* ---- registry ---- */
@@ -501,6 +504,14 @@ const CgCase cg_cases[] = {
CG_CASE_DEFAULT},
{"q11_addr_of_helper_through_global",
build_q11_addr_of_helper_through_global, 42, CG_CASE_DEFAULT},
+
+ /* Group ASM — inline asm */
+ {"asm01_mov_imm_out", build_asm01_mov_imm_out, 42, CG_CASE_DEFAULT,
+ CG_ARCH_AARCH64},
+ {"asm02_copy_input", build_asm02_copy_input, 7, CG_CASE_DEFAULT,
+ CG_ARCH_AARCH64},
+ {"asm03_named_clobber", build_asm03_named_clobber, 99, CG_CASE_DEFAULT,
+ CG_ARCH_AARCH64},
};
const unsigned cg_cases_count = sizeof(cg_cases) / sizeof(cg_cases[0]);
diff --git a/test/cg/harness/cases_asm.c b/test/cg/harness/cases_asm.c
@@ -0,0 +1,101 @@
+/* Group ASM — inline-asm smoke cases.
+ *
+ * Each builder drives ctx->target->asm_block directly with hand-built
+ * AsmConstraint / Operand arrays, then returns the asm output through
+ * cgtest_ret_reg. At opt-level 0 the call lands in aa_asm_block; at
+ * opt-level >0 it is captured as IR_ASM_BLOCK by w_asm_block and replayed
+ * to the real backend at lowering time. Both paths must produce the
+ * same exit code.
+ *
+ * See CORPUS.md for the case list and expected values. */
+
+#include <string.h>
+
+#include "cg_test.h"
+
+/* asm01_mov_imm_out — `__asm__("mov %w0, #42" : "=r"(rc));` then return rc. */
+void build_asm01_mov_imm_out(CgTestCtx* ctx) {
+ const Type* I32 = T_i32(ctx);
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+
+ AsmConstraint outs[1];
+ memset(outs, 0, sizeof outs);
+ outs[0].str = "=r";
+ outs[0].dir = ASM_OUT;
+ outs[0].type = I32;
+
+ Operand out_ops[1];
+ out_ops[0] = REG_op(r, I32);
+
+ ctx->target->asm_block(ctx->target, "mov %w0, #42",
+ outs, /*nout=*/1, out_ops,
+ /*ins=*/NULL, /*nin=*/0, /*in_ops=*/NULL,
+ /*clobbers=*/NULL, /*nclob=*/0);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* asm02_copy_input — `__asm__("mov %w0, %w1" : "=r"(rc) : "r"(7));` */
+void build_asm02_copy_input(CgTestCtx* ctx) {
+ const Type* I32 = T_i32(ctx);
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg rin = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ ctx->target->load_imm(ctx->target, REG_op(rin, I32), 7);
+
+ Reg rout = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+
+ AsmConstraint outs[1];
+ memset(outs, 0, sizeof outs);
+ outs[0].str = "=r";
+ outs[0].dir = ASM_OUT;
+ outs[0].type = I32;
+ Operand out_ops[1];
+ out_ops[0] = REG_op(rout, I32);
+
+ AsmConstraint ins[1];
+ memset(ins, 0, sizeof ins);
+ ins[0].str = "r";
+ ins[0].dir = ASM_IN;
+ ins[0].type = I32;
+ Operand in_ops[1];
+ in_ops[0] = REG_op(rin, I32);
+
+ ctx->target->asm_block(ctx->target, "mov %w0, %w1",
+ outs, /*nout=*/1, out_ops,
+ ins, /*nin=*/1, in_ops,
+ /*clobbers=*/NULL, /*nclob=*/0);
+ ctx->target->free_reg(ctx->target, rin, RC_INT);
+ cgtest_ret_reg(tf, rout, I32);
+ cgtest_end(tf);
+}
+
+/* asm03_named_clobber — clobber x19 (callee-saved) without using it. The
+ * func prologue must save/restore x19 even though no SValue ever bound
+ * it; this is the hwm-bump path in aa_asm_block. The asm body trashes
+ * x19 (mov x19, #0xdead) and returns a constant; correct exit code
+ * indicates x19 was preserved by the prologue. */
+void build_asm03_named_clobber(CgTestCtx* ctx) {
+ const Type* I32 = T_i32(ctx);
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+
+ AsmConstraint outs[1];
+ memset(outs, 0, sizeof outs);
+ outs[0].str = "=r";
+ outs[0].dir = ASM_OUT;
+ outs[0].type = I32;
+ Operand out_ops[1];
+ out_ops[0] = REG_op(r, I32);
+
+ Sym clobs[1];
+ clobs[0] = pool_intern_cstr(ctx->pool, "x19");
+
+ ctx->target->asm_block(ctx->target,
+ "mov x19, #1; mov %w0, #99",
+ outs, /*nout=*/1, out_ops,
+ /*ins=*/NULL, /*nin=*/0, /*in_ops=*/NULL,
+ clobs, /*nclob=*/1);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
diff --git a/test/cg/run.sh b/test/cg/run.sh
@@ -208,6 +208,7 @@ if $CC $CFREE_CFLAGS \
"$TEST_DIR/harness/cases_o.c" \
"$TEST_DIR/harness/cases_p.c" \
"$TEST_DIR/harness/cases_q.c" \
+ "$TEST_DIR/harness/cases_asm.c" \
"$LIB_AR" -o "$CG_RUNNER" 2>"$BUILD_DIR/cg-runner.err"; then
printf ' %s cg-runner\n' "$(color_grn built)"
else
diff --git a/test/parse/cases/asm_01_grammar.c b/test/parse/cases/asm_01_grammar.c
@@ -1,25 +1,14 @@
/* Track A — frontend parser for GNU inline-asm statements.
*
- * Exercises every limb of the asm-stmt grammar:
+ * Exercises every limb of the asm-stmt grammar supported in v1:
* - both keyword spellings: `asm` and `__asm__`
* - `volatile` and `__volatile__` qualifiers (informational, dropped)
- * - `goto` keyword (parsed; cg layer rejects asm-goto in v1)
- * - the four colon-separated lists: outputs, inputs, clobbers, labels
+ * - the three colon-separated lists: outputs, inputs, clobbers
* - `[name]` symbolic operands
* - adjacent string-literal fusion in the template
* - empty intermediate sections (`asm("..." : : "r"(x))`)
*
- * This case is .skip-marked because cg_inline_asm is a panic stub on
- * main (Track B not yet landed) — every asm-stmt the parser accepts
- * panics inside cg with "cg_inline_asm: not in v1 slice", which is
- * the documented expected behavior for Track A landing first. To
- * sanity-check the parser by hand: run `parse-runner --jit FILE.c`;
- * a "cg_inline_asm: not in v1 slice" diagnostic means the parser
- * accepted the form. Lifting the .skip happens when Track B replaces
- * the panic in src/cg/cg.c.
- *
- * The cg panic aborts compilation at the first asm-stmt, so to verify
- * a specific form by hand, hoist it to the top of test_main. */
+ * `asm goto` is not supported in v1 and is deliberately omitted here. */
int test_main(void) {
int rc = 42;
@@ -49,7 +38,7 @@ int test_main(void) {
: [sum] "=r"(c)
: [x] "r"(a), [y] "r"(b));
- /* In-out (`+r`). */
+ /* In-out (`+r`). Decomposes to `=r` + a matching "0" input. */
asm("add %w0, %w0, #1" : "+r"(rc));
/* Memory clobber. */
@@ -58,10 +47,5 @@ int test_main(void) {
/* Empty middle sections. */
asm("nop" : : : );
- /* asm goto — labels list. cg layer will reject in v1; the parser
- * accepts the syntax. */
- asm goto("b %l[done]" : : : : done);
-done:
-
return rc;
}
diff --git a/test/parse/cases/asm_01_grammar.expected b/test/parse/cases/asm_01_grammar.expected
@@ -0,0 +1 @@
+43
diff --git a/test/parse/cases/asm_01_grammar.skip b/test/parse/cases/asm_01_grammar.skip
@@ -1 +0,0 @@
-cg_inline_asm is a panic stub until Track B (cg/opt) lands; parser-only check