kit

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

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:
Mdoc/ASM.md | 48+++++++++++++++++++++++++-----------------------
Msrc/arch/aa64_asm.c | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/arch/aa64_isa.c | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/aa64_isa.h | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/cg/harness/cases.c | 11+++++++++++
Atest/cg/harness/cases_asm.c | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/cg/run.sh | 1+
Mtest/parse/cases/asm_01_grammar.c | 24++++--------------------
Atest/parse/cases/asm_01_grammar.expected | 1+
Dtest/parse/cases/asm_01_grammar.skip | 1-
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