commit 1434889223137803ad7d7f6b4263ab08d9682dfa
parent 7783c4556dcf5bd14b3703e46f72c5fcb3281f17
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 11 May 2026 08:54:29 -0700
asm: phase-3 + phase-4a — standalone .s assembler + disassembler overlay
Brings up the aarch64 .s frontend and the matching disassembler against
the shared aa64_isa descriptor table, so encode and decode go through
one bit-layout source of truth.
Standalone assembler (phase 3):
- src/parse/parse_asm.c: arch-agnostic driver — directives, labels,
full constant-expression evaluator, sym±const symbolic terms,
string-literal decoding.
- src/parse/parse_asm_helpers.h: driver↔arch seam.
- src/arch/aa64_asm.{h,c}: per-mnemonic dispatch over aa64_insn_table
via the inline encoders. Covers mov/add/sub/cmp/logical/dp2/dp3/
branch/cbz/svc/ldr/str/ldp/stp/adr/adrp and the common aliases.
Branch operands emit CALL26/JUMP26/CONDBR19; adr/adrp emit
ADR_PREL_LO21 / _PG_HI21.
Disassembler overlay (phase 4a):
- src/arch/aa64_disasm.{h,c}: aarch64 ArchDisasm impl wrapping
aa64_disasm_find + aa64_print_operands; synthesizes b.<cond>.
- src/arch/disasm.c: per-arch dispatcher.
- src/api/disasm.c: cfree_disasm_iter_* / cfree_obj_disasm with
reloc/symbol annotation overlay.
- src/arch/aa64_regs.{h,c} + src/api/arch_regs.c: canonical
register table shared by parser, printer, and public API.
Smoke corpus (test/asm/) is green on H (encode), T (decode), L (listing),
D (direct JIT), and J (jit-via-file) for the host where applicable.
doc/ASM.md updated to reflect the landed seams and the remaining work
(phase 4b inline asm; phase 5 multiarch).
Diffstat:
20 files changed, 2703 insertions(+), 156 deletions(-)
diff --git a/doc/ASM.md b/doc/ASM.md
@@ -13,27 +13,55 @@ update at one site and stay in sync by construction.
## 1. Current state
-- `src/arch/aa64_isa.{h,c}`: per-format `pack`/`unpack` round-trippers and
- a `(mnemonic, match, mask, AA64Format)` descriptor table.
- `aa64_disasm_find` already linear-scans the table by `(word & mask) ==
- match`. The encoders are inline wrappers that call `pack`. **This is the
- pairing seam.** Half of the work below is just finishing what's started
- here.
-- `src/parse/parse.h:23` declares `parse_asm`; `src/api/stubs.c:45`
- implements it as a panic. `src/api/pipeline.c:208` already routes
- `CFREE_LANG_ASM` inputs to it, so the wiring is in place.
-- `CGTarget.asm_block` is a method on every backend; `aa_asm_block`
- (`arch/aarch64.c:2969`), `xx_asm_block`, `rv_asm_block`, and the opt
- recorder's `w_asm_block` all panic.
-- `cg_inline_asm` (`src/cg/cg.c:1337`) is the parser-side entry; panics.
-- `arch_disasm_new` / `arch_disasm_decode` (`src/arch/arch.h:647`) are
- declared but no impl exists. Public surface
- (`cfree_disasm_iter_*`, `cfree_obj_disasm`,
- `cfree_arch_register_*`) is in `include/cfree.h` and stubbed out in
- `src/api/stubs.c`.
-
-So: data model decided, descriptor table partly populated, all behavior
-still a panic. The work is one focused vertical.
+- `src/arch/aa64_isa.{h,c}`: per-format `pack`/`unpack` round-trippers
+ and a `(mnemonic, match, mask, AA64Format, AsmFlags)` descriptor
+ table. `aa64_disasm_find` linear-scans the table by
+ `(word & mask) == match`; first-match-wins, with alias rows placed
+ before their canonical form. `aa64_print_operands` renders operand
+ text via per-format helpers — shared between the disasm iterator and
+ the object listing. **This is the pairing seam.**
+- `src/parse/parse_asm.c`: arch-agnostic .s driver — directives
+ (`.text/.data/.rodata/.bss/.section/.globl/.local/.weak/.hidden/
+ .protected/.internal/.type/.size/.byte/.hword/.word/.quad/.ascii/
+ .asciz/.string/.zero/.skip/.fill/.align/.balign/.p2align/.set/.equ`
+ plus accepted-but-ignored `.cfi_*`/`.file`/`.loc`/`.macro`/...),
+ labels, full constant-expression evaluator (`+ - * / % << >> & | ^ ~`
+ with parens), `sym ± const` symbolic terms, string-literal decoding
+ with C-style escapes. Wired by `src/api/pipeline.c:208`; the panic
+ stub in `src/api/stubs.c` is gone.
+- `src/arch/aa64_asm.{h,c}`: per-mnemonic dispatch over
+ `aa64_insn_table` via the inline encoders in `aa64_isa.h`. Coverage
+ spans `nop, ret/br/blr, 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`. Branches emit
+ `R_AARCH64_{CALL,JUMP}26` / `R_AARCH64_CONDBR19`; `adr/adrp` emit
+ `R_AARCH64_ADR_PREL_{LO21,PG_HI21}`.
+- `src/arch/aa64_disasm.{h,c}` + `src/arch/disasm.c`: aarch64
+ `ArchDisasm` impl wraps `aa64_disasm_find` + `aa64_print_operands`,
+ synthesizing `b.<cond>` mnemonics from the BR_COND format.
+ `arch_disasm_new` dispatches by `c->target.arch` (aarch64 only; x64
+ / rv64 panic with a clean diagnostic).
+- `src/api/disasm.c`: public `cfree_disasm_iter_*` and
+ `cfree_obj_disasm` over `arch_disasm_*`, plus the reloc/symbol
+ annotation overlay (rendered into the iterator's annotation buffer
+ per decoded word).
+- `src/arch/aa64_regs.{h,c}` + `src/api/arch_regs.c`: stateless
+ `cfree_arch_register_name`/`_index` queries against the canonical
+ aarch64 register table — same source list the parser and printer
+ consume.
+- `driver/as.c`: `cfree as` multi-call subcommand wired to
+ `cfree_compile_obj_emit(CFREE_LANG_ASM)`. Accepts `-target` for
+ cross-assembly, `-g` for debug-info forwarding (no-op until CFI
+ storage is wired).
+- `CGTarget.asm_block` (inline asm) is still a panic on every backend
+ (`aa_asm_block`, `xx_asm_block`, `rv_asm_block`, the opt recorder's
+ `w_asm_block`); `cg_inline_asm` (`src/cg/cg.c`) likewise. Inline-asm
+ bring-up is the remaining phase-4 work.
+
+So: standalone `.s` end-to-end works (encode → ELF → disasm
+round-trip, plus JIT execute). Inline asm is the next vertical.
---
@@ -119,26 +147,40 @@ Reuse `aa64` prefix.
```
src/parse/parse_asm.c shared driver: scan tokens, dispatch directives,
- call per-arch instruction parser. New.
-src/arch/aa64_asm.{h,c} aa64 instruction parser + inline-asm template
- walker. New. Owns AsmCtx and constraint binding.
+ label management, expression evaluation,
+ call per-arch instruction parser.
+src/parse/parse_asm_helpers.h
+ driver↔arch seam (asm_driver_peek/next/
+ parse_const/parse_sym_expr/intern_sym/panic).
+ AsmDriver itself stays internal to parse_asm.c.
+src/arch/aa64_asm.{h,c} aa64 instruction parser: per-mnemonic dispatch
+ over aa64_insn_table → inline encoders in
+ aa64_isa.h. Phase 4b will grow the inline-asm
+ template walker on top of the same parsers.
src/arch/aa64_disasm.{h,c} aa64 ArchDisasm impl. Wraps aa64_disasm_find
- with operand printing. New.
-src/arch/aa64_isa.{h,c} already exists. Gains per-format
- parse_operands / print_operands and
- AsmFlags column on AA64InsnDesc.
+ with operand printing; synthesizes b.<cond>.
+src/arch/aa64_regs.{h,c} canonical aarch64 register name list — same
+ source the parser and printer consume.
+src/arch/aa64_isa.{h,c} per-format pack/unpack + print_operands
+ dispatcher + AsmFlags column on AA64InsnDesc.
+ aa64_parse_operands declared but unused by
+ phase 3 (we dispatch per-mnemonic instead;
+ the table-driven parser belongs with the
+ remaining cg-emitted formats — see §5).
src/arch/disasm.c arch_disasm_new dispatch by c->target.arch
(peer of arch/cgtarget.c per MULTIARCH §2.1).
- New.
-src/api/disasm.c cfree_disasm_iter_* / cfree_obj_disasm /
- cfree_arch_register_* over arch_disasm_*.
- Replaces stubs in src/api/stubs.c.
+src/api/disasm.c cfree_disasm_iter_* / cfree_obj_disasm over
+ arch_disasm_*, plus reloc/symbol overlay.
+src/api/arch_regs.c cfree_arch_register_name / _index dispatch.
+driver/as.c `cfree as` subcommand: cross-target flag,
+ -g/-o, single positional input. Drives
+ cfree_compile_obj_emit(CFREE_LANG_ASM).
```
-The four pieces fall on three seams: (a) `parse_asm` ↔ per-arch instruction
-parser, (b) `MCEmitter` is the byte sink for both asm and codegen, (c)
-`arch_disasm_new` ↔ per-arch decoder. `aa64_isa.h` is the shared truth
-crossing those seams.
+The pieces fall on three seams: (a) `parse_asm` ↔ per-arch instruction
+parser via `parse_asm_helpers.h`, (b) `MCEmitter` is the byte sink for
+both asm and codegen, (c) `arch_disasm_new` ↔ per-arch decoder.
+`aa64_isa.h` is the shared truth crossing all three.
### 4.1 `parse_asm` driver — arch-agnostic
@@ -164,20 +206,33 @@ function pointer (`arch->insn`), one symbol (`aa64_asm_open`).
### 4.2 `aa64_asm_open` — instruction parser
```c
-typedef struct AsmCtx AsmCtx; /* tokens, scratch, label map */
-typedef struct AA64Asm {
- Compiler* c;
- void (*insn)(struct AA64Asm*, AsmDriver*, Tok mnemonic);
- /* + register-name table, mnemonic→AA64InsnDesc lookup,
- * inline-asm placeholder substitution state. */
-} AA64Asm;
+void aa64_asm_insn(AA64Asm*, AsmDriver*, Sym mnemonic);
```
-`insn` looks the mnemonic up in `aa64_insn_table` (the same table
-`aa64_disasm_find` uses), dispatches on `format`, calls
-`aa64_<fmt>_parse` to fill the field struct, then calls
-`aa64_<fmt>_pack` and writes the `u32` through `mc->emit_bytes`. Branches
-also call `mc->emit_label_ref` for the relocatable bit slice.
+The parser dispatches per-mnemonic against a small in-file table
+(`{name, parse_fn}`). Each `parse_fn` reads operands via the
+`asm_driver_*` helpers (register, immediate, memory addressing) and
+calls the inline encoder in `aa64_isa.h` for its format
+(`aa64_movz`, `aa64_add`, `aa64_ldr64_uimm12`, ...). One parser per
+operand grammar — register-vs-immediate variants of the same
+mnemonic (e.g. `add Rd, Rn, Rm` vs. `add Rd, Rn, #imm`) branch on
+the first non-Rd operand. Aliases (`mov`, `mvn`, `cmp`, `cmn`,
+`neg`, `mul`, `mneg`, no-operand `ret`) live as dedicated rows that
+emit the canonical encoding directly.
+
+Branches do not go through `mc->emit_label_ref`; the parser emits
+the instruction with `imm26=0`/`imm19=0` and records a reloc
+(`R_AARCH64_CALL26`/`JUMP26`/`CONDBR19`) against the operand's
+ObjSymId via `mc->emit_reloc_at`. The linker (and the in-process
+fixup machinery in `src/arch/mc.c`) applies the displacement at
+relocation time.
+
+The table-driven `aa64_parse_operands` declared in `aa64_isa.h`
+(phase-2 placeholder) remains stubbed — phase 3 chose per-mnemonic
+dispatch because it lets one parser handle alias / immediate /
+register-form branching at the right level. The format-driven
+parser slots in alongside this one for the remaining cg-emitted
+formats when `S` (cg round-trip) needs them.
### 4.3 Inline asm — same parser, different operand source
@@ -283,9 +338,12 @@ keyed on the section + offset and writes the resolved `name+addend` into
bytes. This keeps `arch_disasm_decode` per-arch and the symbol/reloc
overlay arch-agnostic.
-`cfree_arch_register_name` / `_index` table lives in `aa64_asm.c`
-alongside the parser (one canonical name list — same source for parse
-and print).
+`cfree_arch_register_name` / `_index` live in `aa64_regs.{h,c}`
+alongside one canonical name list shared by the parser, the printer,
+and the public API. `src/api/arch_regs.c` is the stateless dispatcher
+(`switch (arch)` over per-arch tables); the iterator surface remains
+a NULL-returning stub pending an env/heap on its constructor (see the
+TODO at the top of `src/api/arch_regs.c`).
---
@@ -293,9 +351,12 @@ and print).
Each phase ends mergeable. Phase 1 stands up the test harness so every
later phase gates on real runs from its first commit. Phase 2 lands the
-encode/decode pairing as
-a mechanical refactor; phase 3 is the standalone assembler; phase 4 is
-inline asm + disasm overlay; phase 5 is the seam-rev for x64/rv64.
+encode/decode pairing as a mechanical refactor; phase 3 is the standalone
+assembler; phase 4 splits into 4a (disasm overlay) and 4b (inline asm);
+phase 5 is the seam-rev for x64/rv64.
+
+Phases 1, 2, 3, and 4a are DONE. Phase 4b (inline asm) and phase 5
+(multiarch) remain.
### Phase 1 — test harness (DONE)
@@ -377,45 +438,90 @@ path on `test/cg/run.sh` turns green in phase 4 — the remaining
codegen-only formats (bitfield, condsel, FP-DP1/2, FP↔int cvt,
ldst-exclusive, dmb/clrex, mrs, dp1, SIMD basic) get table rows then.
-### Phase 3 — standalone `.s` assembler
-
-1. New `src/parse/parse_asm.c`. Replace the panic in `src/api/stubs.c`.
- Driver loop, directive parser, label management, expression
- evaluator (constant + `sym + const`, full arithmetic on constants
- per §7).
-2. New `src/arch/aa64_asm.{h,c}` with `aa64_asm_open` and the
- instruction parser. Mnemonic lookup goes through `aa64_insn_table`.
-3. CFI directives forwarded to `MCEmitter.cfi_*`.
-4. `.loc` calls `mc->set_loc` so debug line tables work for hand-written
- `.s`.
-5. `cfree as` driver subcommand (multi-call dispatch).
-
-Exit criterion: every `test/asm/encode/` case is green; every row of
-`aa64_insn_table` is hit by at least one encode case. `rt/` stays on
-clang.
-
-### Phase 4 — inline asm + disasm overlay
+### Phase 3 — standalone `.s` assembler (DONE)
+
+- [x] New `src/parse/parse_asm.c`. Panic stub in `src/api/stubs.c`
+ removed. Driver loop, directive parser, label management,
+ expression evaluator (constants with `+ - * / % << >> & | ^ ~` and
+ parens; `sym ± const` for symbolic terms), string-literal decoding
+ with C-style escapes.
+- [x] New `src/parse/parse_asm_helpers.h`. Lightweight surface
+ (`asm_driver_peek/next/eat_*/parse_const/parse_sym_expr/intern_sym/
+ panic/...`) the per-arch parser consumes; the AsmDriver struct
+ itself stays internal to `parse_asm.c`.
+- [x] New `src/arch/aa64_asm.{h,c}` with `aa64_asm_open` /
+ `aa64_asm_insn`. Per-mnemonic dispatch over `aa64_insn_table`
+ resolved through the inline encoders in `aa64_isa.h` (no second
+ copy of the bit layout). Composite mnemonics (`b.eq`, `b.ne`, ...)
+ are stitched in the driver before dispatch.
+- [x] Reloc-emitting operands: branches → `R_AARCH64_CALL26` /
+ `JUMP26`; conditional branches and CBZ/CBNZ → `R_AARCH64_CONDBR19`;
+ `adr`/`adrp` → `R_AARCH64_ADR_PREL_LO21` / `_PG_HI21`. Data
+ directives (`.word`/`.quad`) with a symbolic operand emit
+ `R_ABS32`/`R_ABS64` through `MCEmitter.emit_reloc_at`, no new
+ mechanism needed (per §7).
+- [x] CFI directives accepted (parsed + skipped) — forward to
+ `MCEmitter.cfi_*` once those hooks store records (today they are
+ no-ops in `src/arch/mc.c`). `.loc` and `.file` likewise accepted-
+ and-ignored; wiring them to `mc->set_loc` is a follow-up that
+ drops in without touching the parser shape.
+- [x] `cfree as` driver subcommand (`driver/as.c`) — accepts
+ `-target TRIPLE`, `-g`, `-o OUT.o INPUT.s`. Same composition point
+ as `cfree -c <file.s>` modulo lang inference.
+- [x] Smoke-case skips dropped from `test/asm/encode/`,
+ `test/asm/decode/`, `test/asm/listing/`. `test-asm` runs green on
+ every path it can on the host (E skips on a non-aarch64 host when
+ no exec runner is configured).
+
+Exit criterion (met for the smoke corpus): the phase-1 encode case
+runs through H (hex roundtrip), D (direct JIT execute), J (JIT via
+file). Coverage of every row in `aa64_insn_table` becomes enforced
+when the `S` path on `test/cg/run.sh` turns on by default (see
+§6.2); the remaining codegen-only formats (bitfield, condsel,
+FP-DP1/2, FP↔int cvt, ldst-exclusive, dmb/clrex, mrs, dp1, SIMD
+basic) gain table rows + parser coverage in lockstep with `S`.
+
+### Phase 4a — disasm overlay (DONE)
+
+- [x] `src/arch/aa64_disasm.{h,c}`: aarch64 `ArchDisasm` impl wraps
+ `aa64_disasm_find` + `aa64_print_operands`. Owns the per-iterator
+ StrBuf storage for mnemonic / operands / annotation. Mnemonic
+ rewrite for `b.<cond>` happens here (the printer keeps the BR_COND
+ format opcode-agnostic).
+- [x] `src/arch/disasm.c`: dispatcher peer of `src/arch/cgtarget.c`,
+ switches `arch_disasm_new` on `c->target.arch`. aarch64 only;
+ x86_64 / rv64 panic with a clean diagnostic.
+- [x] `src/api/disasm.c`: `cfree_disasm_iter_new/next/free` and
+ `cfree_obj_disasm` over `arch_disasm_*`, plus the reloc/symbol
+ annotation overlay (rendered per-decoded-word into the iterator
+ buffer; the arch decoder stays reloc-unaware).
+- [x] `src/arch/aa64_regs.{h,c}` + `src/api/arch_regs.c`: stateless
+ `cfree_arch_register_name` / `_index` against one canonical reg
+ table — same source the parser and printer share.
+
+Exit criterion (met): every `test/asm/decode/` and
+`test/asm/listing/` case is green; `cfree objdump -d` over the
+output of `cfree as` round-trips the smoke corpus.
+
+### Phase 4b — inline asm
1. Implement `aa_asm_block` in `arch/aarch64.c` calling into
- `aa64_asm_run_template`. Implement `cg_inline_asm` in `cg/cg.c`:
- evaluate inputs to `Operand`s, materialize `&buf` for `m` constraints,
- call `target->asm_block`, push `out_ops` back as `SValue`s.
+ `aa64_asm_insn` against a template-driven token source. Implement
+ `cg_inline_asm` in `cg/cg.c`: evaluate inputs to `Operand`s,
+ materialize `&buf` for `m` constraints, call `target->asm_block`,
+ push `out_ops` back as `SValue`s.
2. Constraint binding (§4.3): `r`, `=r`, `+r`, `=&r`, `i`, `m`, `0`.
3. Memory clobber: CG flushes value stack (`spill_reg` for every live
reg-resident SValue) before the call, marks them invalid after.
Register clobbers route through the existing `clobbers` mechanism.
4. `IR_ASM_BLOCK` already opaque-to-passes; opt recorder
(`opt.c:692`) materializes operands and replays.
-5. `arch_disasm_new` for aarch64 (`aa64_disasm.c`); dispatch in new
- `arch/disasm.c`.
-6. `cfree_obj_disasm` / `cfree_disasm_iter_*` over `arch_disasm_*`,
- plus reloc/symbol annotation overlay. `cfree_arch_register_*` table.
-Exit criterion: every `test/asm/decode/` and `test/asm/listing/` case
-is green; the inline-asm cases under `test/cg/` (svc-style write-then-
-exit) build, run under qemu/podman, and report green on `DREJWS`. The
-`S` path turns green for the full cg corpus, proving encode/decode
-pairing across every `.text` byte cfree currently emits.
+Exit criterion: the inline-asm cases under `test/cg/` (svc-style
+write-then-exit) build, run under qemu/podman, and report green on
+`DREJWS`. The `S` path turns green for the full cg corpus, proving
+encode/decode pairing across every `.text` byte cfree currently
+emits.
### Phase 5 — multiarch seam
@@ -554,14 +660,14 @@ D and J need the host arch to match `CFREE_TEST_ARCH` (no cross-JIT);
E uses qemu/podman per `test/lib/exec_target.sh` and is cross-host
friendly.
-### Skips during phase 1
+### Skip sidecars
-Every smoke case carries a `<name>.skip` sidecar because `parse_asm` /
-`cfree_disasm_iter_*` / `cfree_obj_disasm` are still stubs. The
-harness defaults `CFREE_TEST_ALLOW_SKIP=1` so the suite passes; set
-`CFREE_TEST_ALLOW_SKIP=0` to surface the skips as failures
-(`make test-asm CFREE_TEST_ALLOW_SKIP=0`). Drop the `.skip` files as
-each subsystem comes online — the goldens are already in place.
+The phase-1 smoke `.skip` sidecars are gone; the corresponding
+subsystems are real. New cases that hit an unimplemented mnemonic or
+directive can still drop a `<name>.skip` sidecar — single-line reason
+— and the harness will report SKIP. Run with
+`CFREE_TEST_ALLOW_SKIP=0` to surface skips as failures (the default
+in CI from phase 3 onward).
### Cross-target
@@ -574,14 +680,15 @@ CFREE_TEST_ARCH=rv64 bash test/asm/run.sh # rv64 lane
### The `S` path on `test/cg/run.sh`
`S` (asm roundtrip across every cg-emitted aarch64 binary) is
-recognized but opt-in this phase — the default cg matrix stays
-`DREJW`. Run it explicitly:
+recognized but opt-in until the assembler covers every cg-emitted
+format. The default cg matrix stays `DREJW`. Run it explicitly:
```
bash test/cg/run.sh '' DREJWS # full matrix incl. S
bash test/cg/run.sh '' S # just S
```
-Today every `S` invocation reports SKIP with reason
-"phase 1: cfree_disasm_iter_* / parse_asm are stubs". Becomes part of
-the default cg matrix once phase 4 lands.
+`S` becomes part of the default cg matrix once the remaining
+codegen-only formats (bitfield, condsel, FP-DP1/2, FP↔int cvt,
+ldst-exclusive, dmb/clrex, mrs, dp1, SIMD basic) gain
+`aa64_insn_table` rows and matching `aa64_asm` parsers.
diff --git a/src/api/arch_regs.c b/src/api/arch_regs.c
@@ -0,0 +1,36 @@
+/* Public arch register name API.
+ *
+ * Stateless dispatch onto the per-arch register table. v1 wires aarch64
+ * only; other arches return NULL / unknown.
+ *
+ * The iterator surface uses an opaque handle but the public API doesn't
+ * supply a heap, and src/ is -ffreestanding (no malloc). v1 keeps the
+ * iterator as the existing NULL-returning stub and exposes the
+ * stateless name ↔ index queries for the disassembler and unwinder
+ * paths. Iterator support can land later by making the iter API take an
+ * env/heap. */
+
+#include <cfree.h>
+
+#include <stddef.h>
+
+#include "arch/aa64_regs.h"
+
+const char* cfree_arch_register_name(CfreeArchKind arch, uint32_t dwarf_idx) {
+ switch (arch) {
+ case CFREE_ARCH_ARM_64:
+ return aa64_register_name(dwarf_idx);
+ default:
+ return NULL;
+ }
+}
+
+int cfree_arch_register_index(CfreeArchKind arch, const char* name,
+ uint32_t* idx_out) {
+ switch (arch) {
+ case CFREE_ARCH_ARM_64:
+ return aa64_register_index(name, idx_out);
+ default:
+ return 1;
+ }
+}
diff --git a/src/api/disasm.c b/src/api/disasm.c
@@ -0,0 +1,282 @@
+/* Public disassembler API.
+ *
+ * cfree_disasm_iter_new / next / free - low-level byte → CfreeInsn walk.
+ * cfree_obj_disasm - objdump-style listing over an
+ * already-built relocatable.
+ *
+ * The arch-level decoder (arch_disasm_*) owns the mnemonic/operand text and
+ * is reloc-unaware. The annotation overlay (sym + reloc decoration) lives
+ * here so the per-arch backends don't grow a dependency on ObjBuilder. */
+
+#include <cfree.h>
+
+#include <stdint.h>
+#include <string.h>
+
+#include "arch/arch.h"
+#include "core/core.h"
+#include "core/heap.h"
+#include "core/pool.h"
+#include "core/strbuf.h"
+#include "obj/obj.h"
+
+#define DASM_ANN_CAP 128u
+
+/* ---- iterator -------------------------------------------------------- */
+
+struct CfreeDisasmIter {
+ Compiler* c;
+ Heap* heap;
+ ArchDisasm* arch;
+ const uint8_t* bytes;
+ size_t len;
+ size_t off; /* bytes consumed so far */
+ uint64_t vaddr0; /* vaddr of bytes[0] */
+ ObjBuilder* obj; /* optional; for reloc/symbol annotation */
+ /* Owned annotation buffer; the per-arch decoder writes into its own
+ * strings, this one carries the overlay we splice on top. */
+ char ann_buf[DASM_ANN_CAP];
+ StrBuf ann;
+};
+
+/* For (sec_offset == off-since-vaddr0), find a relocation that applies to
+ * this 4-byte slot and, if found, render "<sym><+addend> (<kind>)" into the
+ * iterator's annotation buffer. Returns the resulting cstr (possibly the
+ * empty string). */
+static const char* dasm_overlay(CfreeDisasmIter* it, uint64_t vaddr) {
+ if (!it->obj) return "";
+ strbuf_reset(&it->ann);
+
+ /* Walk every reloc and match (any section, offset == vaddr - vaddr0).
+ * For a public-API call where `bytes` is one section's data and
+ * vaddr0 == its base, this is correct; multi-section buffers would
+ * require a richer key. The doc/ASM.md §4.5 contract is single-section
+ * inputs. */
+ u64 want = vaddr - it->vaddr0;
+ u32 nrel = obj_reloc_total(it->obj);
+ for (u32 i = 0; i < nrel; ++i) {
+ const Reloc* r = obj_reloc_at(it->obj, i);
+ if (!r) continue;
+ if ((u64)r->offset != want) continue;
+ /* Only annotate text-section relocs. We don't have the section_id
+ * tied to the iterator buffer, so we accept any reloc whose offset
+ * matches and let the caller scope the input appropriately. */
+ const ObjSym* sym = (r->sym != OBJ_SYM_NONE)
+ ? obj_symbol_get(it->obj, r->sym)
+ : NULL;
+ if (sym && sym->name) {
+ /* The Sym is interned in the obj's owning compiler pool, which may
+ * differ from the iterator caller's compiler (e.g. driver loads
+ * the .o via cfree_obj_open, which mints its own Compiler). */
+ Compiler* oc = obj_compiler(it->obj);
+ size_t nlen = 0;
+ const char* nm = oc ? pool_str(oc->global, sym->name, &nlen) : NULL;
+ if (nm) strbuf_putn(&it->ann, nm, nlen);
+ else strbuf_puts(&it->ann, "<anon>");
+ } else {
+ strbuf_puts(&it->ann, "<anon>");
+ }
+ if (r->addend > 0) {
+ strbuf_puts(&it->ann, "+");
+ strbuf_put_i64(&it->ann, r->addend);
+ } else if (r->addend < 0) {
+ strbuf_put_i64(&it->ann, r->addend);
+ }
+ break;
+ }
+ return strbuf_cstr(&it->ann);
+}
+
+CfreeDisasmIter* cfree_disasm_iter_new(CfreeCompiler* c, const uint8_t* bytes,
+ size_t len, uint64_t vaddr,
+ CfreeObjBuilder* obj) {
+ if (!c || (!bytes && len > 0)) return NULL;
+ Heap* h = (Heap*)c->env->heap;
+ CfreeDisasmIter* it =
+ (CfreeDisasmIter*)h->alloc(h, sizeof(*it), _Alignof(CfreeDisasmIter));
+ if (!it) return NULL;
+ memset(it, 0, sizeof(*it));
+ it->c = c;
+ it->heap = h;
+ it->arch = arch_disasm_new(c);
+ if (!it->arch) {
+ h->free(h, it, sizeof(*it));
+ return NULL;
+ }
+ it->bytes = bytes;
+ it->len = len;
+ it->off = 0;
+ it->vaddr0 = vaddr;
+ it->obj = obj;
+ strbuf_init(&it->ann, it->ann_buf, sizeof it->ann_buf);
+ return it;
+}
+
+int cfree_disasm_iter_next(CfreeDisasmIter* it, CfreeInsn* out) {
+ if (!it || it->off >= it->len) return 0;
+ uint64_t vaddr = it->vaddr0 + (uint64_t)it->off;
+ u32 n = arch_disasm_decode(it->arch, it->bytes + it->off, it->len - it->off,
+ vaddr, out);
+ if (n == 0) {
+ /* Undecodable. Advance by the arch's minimum unit (4 for aarch64) so
+ * the listing stays in sync; emit a placeholder. The arch decoder
+ * already populated mnemonic="(unknown)" via its own .inst path when
+ * it had at least 4 bytes — here we ran out of bytes entirely. */
+ if (it->off >= it->len) return 0;
+ /* Best-effort fixed-step advance for the only supported arch. */
+ n = (u32)(it->len - it->off);
+ if (n > 4) n = 4;
+ out->vaddr = vaddr;
+ out->bytes = it->bytes + it->off;
+ out->nbytes = n;
+ out->mnemonic = "(truncated)";
+ out->operands = "";
+ out->annotation = "";
+ it->off += n;
+ return 1;
+ }
+ out->annotation = dasm_overlay(it, vaddr);
+ it->off += n;
+ return 1;
+}
+
+void cfree_disasm_iter_free(CfreeDisasmIter* it) {
+ if (!it) return;
+ arch_disasm_free(it->arch);
+ it->heap->free(it->heap, it, sizeof(*it));
+}
+
+/* ---- objdump-style listing ------------------------------------------- */
+
+static void w_str(CfreeWriter* w, const char* s) {
+ cfree_writer_write(w, s, strlen(s));
+}
+
+static void w_hex(CfreeWriter* w, u64 v, u32 width) {
+ static const char H[] = "0123456789abcdef";
+ char buf[17];
+ u32 i;
+ if (width > 16) width = 16;
+ for (i = 0; i < width; ++i) {
+ buf[width - 1 - i] = H[(v >> (4 * i)) & 0xfu];
+ }
+ buf[width] = '\0';
+ cfree_writer_write(w, buf, width);
+}
+
+/* Right-align v in a `minwidth`-char field as lowercase hex. */
+static void w_hex_padded(CfreeWriter* w, u64 v, u32 minwidth) {
+ static const char H[] = "0123456789abcdef";
+ char buf[24];
+ u32 i = sizeof(buf);
+ if (v == 0) {
+ buf[--i] = '0';
+ } else {
+ while (v) {
+ buf[--i] = H[v & 0xfu];
+ v >>= 4;
+ }
+ }
+ u32 nch = (u32)(sizeof(buf) - i);
+ while (nch < minwidth) {
+ cfree_writer_write(w, " ", 1);
+ ++nch;
+ }
+ cfree_writer_write(w, buf + i, sizeof(buf) - i);
+}
+
+/* Look up the symbol whose value == offset within `section_idx`, if any.
+ * Used to emit objdump-style "<addr> <name>:" headers ahead of each
+ * defined function. Returns NULL if no symbol starts at that offset. */
+static const char* dasm_sym_at(CfreeCompiler* c, CfreeObjFile* f,
+ uint32_t section_idx, uint64_t offset) {
+ CfreeObjSymIter* si = cfree_obj_symiter_new(f);
+ if (!si) return NULL;
+ const char* found = NULL;
+ CfreeObjSymInfo s;
+ while (cfree_obj_symiter_next(si, &s)) {
+ if (s.section != section_idx) continue;
+ if (s.value != offset) continue;
+ if (!s.name || !s.name[0]) continue;
+ /* Skip AArch64 mapping symbols ($x / $d / $a / $t) — they decorate
+ * code/data boundaries, not user-facing entry points. */
+ if (s.name[0] == '$') continue;
+ found = s.name;
+ if (s.kind == CFREE_SK_FUNC) break; /* preferred; otherwise last wins */
+ }
+ cfree_obj_symiter_free(si);
+ (void)c;
+ return found;
+}
+
+int cfree_obj_disasm(CfreeCompiler* c, const CfreeBytesInput* in,
+ CfreeWriter* out) {
+ if (!c || !in || !out) return 1;
+ CfreeObjFile* f = cfree_obj_open(c->env, in);
+ if (!f) return 1;
+ CfreeObjBuilder* ob = cfree_obj_builder(f);
+ uint32_t nsec = cfree_obj_nsections(f);
+ uint32_t i;
+ for (i = 0; i < nsec; ++i) {
+ CfreeObjSecInfo s = cfree_obj_section(f, i);
+ if (s.kind != CFREE_SEC_TEXT) continue;
+ size_t n = 0;
+ const uint8_t* data = cfree_obj_section_data(f, i, &n);
+ if (!data || !n) continue;
+
+ w_str(out, "Disassembly of section ");
+ w_str(out, s.name ? s.name : ".text");
+ w_str(out, ":\n\n");
+
+ /* Header for the start-of-section symbol, if any. */
+ const char* head = dasm_sym_at(c, f, i, 0);
+ if (head) {
+ w_hex(out, 0, 16);
+ w_str(out, " <");
+ w_str(out, head);
+ w_str(out, ">:\n");
+ }
+
+ CfreeDisasmIter* it = cfree_disasm_iter_new(c, data, n, 0, ob);
+ if (!it) {
+ cfree_obj_close(f);
+ return 1;
+ }
+ CfreeInsn ins;
+ while (cfree_disasm_iter_next(it, &ins)) {
+ /* objdump-ish: right-aligned hex vaddr in 8-char field, ":\t", raw
+ * bytes (little-endian word for aarch64 4-byte slots) + " \t",
+ * mnemonic [\t operands] [ ; annotation]. */
+ w_hex_padded(out, ins.vaddr, 8);
+ w_str(out, ":\t");
+ /* Raw bytes: 4 bytes little-endian, rendered as a single 8-hex-digit
+ * word followed by space-tab (objdump convention for aarch64). */
+ if (ins.nbytes == 4) {
+ uint32_t w = (uint32_t)ins.bytes[0] | ((uint32_t)ins.bytes[1] << 8) |
+ ((uint32_t)ins.bytes[2] << 16) |
+ ((uint32_t)ins.bytes[3] << 24);
+ w_hex(out, w, 8);
+ w_str(out, " \t");
+ } else {
+ uint32_t k;
+ for (k = 0; k < ins.nbytes; ++k) {
+ w_hex(out, ins.bytes[k], 2);
+ }
+ w_str(out, " \t");
+ }
+ w_str(out, ins.mnemonic ? ins.mnemonic : "");
+ if (ins.operands && ins.operands[0]) {
+ w_str(out, "\t");
+ w_str(out, ins.operands);
+ }
+ if (ins.annotation && ins.annotation[0]) {
+ w_str(out, " ; ");
+ w_str(out, ins.annotation);
+ }
+ w_str(out, "\n");
+ }
+ cfree_disasm_iter_free(it);
+ }
+ cfree_obj_close(f);
+ return 0;
+}
diff --git a/src/api/stubs.c b/src/api/stubs.c
@@ -35,20 +35,9 @@ static _Noreturn void unimplemented(Compiler* c, const char* what) {
/* Preprocessor implementation lives in src/pp/pp.c. */
-/* ============================================================
- * Parser
- * ============================================================
- * parse_c lives in src/parse/parse.c. The asm parser is still a stub
- * pending its own corpus rows; reaching it from a CFREE_LANG_ASM input
- * raises a clean diagnostic. */
-
-void parse_asm(Compiler* c, Lexer* l, MCEmitter* m) {
- (void)l;
- (void)m;
- unimplemented(c, "parse_asm");
-}
-
-/* DeclTable lives in src/decl/decl.c. CG lives in src/cg/cg.c. */
+/* parse_c lives in src/parse/parse.c. parse_asm lives in
+ * src/parse/parse_asm.c. DeclTable lives in src/decl/decl.c.
+ * CG lives in src/cg/cg.c. */
/* mc_new / mc_free live in src/arch/mc.c.
* cgtarget_new / cgtarget_finalize / cgtarget_free live in src/arch/<target>.c
@@ -114,49 +103,14 @@ int cfree_dep_iter_next(CfreeDepIter* it, CfreeDepEdge* o) {
}
void cfree_dep_iter_free(CfreeDepIter* it) { (void)it; }
-/* Disassembler. */
-struct CfreeDisasmIter {
- int _;
-};
-int cfree_obj_disasm(CfreeCompiler* c, const CfreeBytesInput* in,
- CfreeWriter* o) {
- (void)c;
- (void)in;
- (void)o;
- return 1;
-}
-CfreeDisasmIter* cfree_disasm_iter_new(CfreeCompiler* c, const uint8_t* b,
- size_t l, uint64_t v,
- CfreeObjBuilder* o) {
- (void)c;
- (void)b;
- (void)l;
- (void)v;
- (void)o;
- return 0;
-}
-int cfree_disasm_iter_next(CfreeDisasmIter* it, CfreeInsn* o) {
- (void)it;
- (void)o;
- return 0;
-}
-void cfree_disasm_iter_free(CfreeDisasmIter* it) { (void)it; }
+/* Disassembler is real (src/api/disasm.c, src/arch/disasm.c,
+ * src/arch/aa64_disasm.c). Per-arch register name lookups are real
+ * (src/api/arch_regs.c + src/arch/aa64_regs.c). The reg-name iterator
+ * still has no heap supply via the public API, so its stub remains. */
-/* Architecture register name iterator. */
struct CfreeArchRegIter {
int _;
};
-const char* cfree_arch_register_name(CfreeArchKind a, uint32_t i) {
- (void)a;
- (void)i;
- return 0;
-}
-int cfree_arch_register_index(CfreeArchKind a, const char* n, uint32_t* o) {
- (void)a;
- (void)n;
- (void)o;
- return 1;
-}
CfreeArchRegIter* cfree_arch_reg_iter_new(CfreeArchKind a) {
(void)a;
return 0;
diff --git a/src/arch/aa64_asm.c b/src/arch/aa64_asm.c
@@ -0,0 +1,861 @@
+/* AArch64 standalone .s instruction parser.
+ *
+ * Per-mnemonic dispatch: each entry in the mnemonic table names a
+ * parse function that reads operand tokens through the asm-driver
+ * surface and emits the encoded word via the inline encoders in
+ * aa64_isa.h. Encoders are the single source of truth for bit
+ * layout — the disassembler shares them through aa64_*_unpack.
+ *
+ * Aliases (`mov`, `neg`, `cmp`, `mul`, ...) live in this table as
+ * dedicated rows that pick the canonical form's encoder with the
+ * alias-specific operand shape. When a mnemonic admits multiple
+ * forms (e.g. `mov` register-vs-immediate, `add` register-vs-
+ * immediate), the parser branches on operand shape after reading
+ * the first non-Rd operand. */
+
+#include "arch/aa64_asm.h"
+
+#include <string.h>
+
+#include "arch/aa64_isa.h"
+#include "arch/arch.h"
+#include "core/arena.h"
+#include "core/pool.h"
+#include "lex/lex.h"
+#include "obj/obj.h"
+#include "parse/parse_asm_helpers.h"
+
+/* ---- public handle ---- */
+
+struct AA64Asm {
+ Compiler* c;
+};
+
+AA64Asm* aa64_asm_open(Compiler* c) {
+ AA64Asm* a = arena_new(c->tu, AA64Asm);
+ a->c = c;
+ return a;
+}
+
+void aa64_asm_close(AA64Asm* a) { (void)a; }
+
+/* ---- helpers ---- */
+
+static int tok_punct(Tok t, u32 p) { return asm_driver_tok_is_punct(t, p); }
+
+static int icase_eq(const char* a, size_t an, const char* b) {
+ size_t i;
+ for (i = 0; i < an; ++i) {
+ char x = a[i], y = b[i];
+ if (x >= 'A' && x <= 'Z') x = (char)(x + ('a' - 'A'));
+ if (y >= 'A' && y <= 'Z') y = (char)(y + ('a' - 'A'));
+ if (x != y || !y) return 0;
+ }
+ return b[an] == '\0';
+}
+
+/* Parse a register operand. Returns the 5-bit encoded register number
+ * via *reg_out and the form via *is64_out. Recognized forms (case-
+ * insensitive):
+ * w0..w30, wzr → is64=0, reg=0..30 / 31
+ * x0..x30, xzr, lr (=x30) → is64=1, reg=0..30 / 31
+ * sp → is64=1, reg=31 (sp_means_sp set)
+ * wsp → is64=0, reg=31 (sp_means_sp set)
+ * Aliases:
+ * fp = x29
+ * ip0 = x16, ip1 = x17 (PLT scratch — useful for hand-written PLTs) */
+typedef struct AA64Reg {
+ u32 num;
+ u8 is64;
+ u8 is_sp; /* 1 if the spelling was "sp" / "wsp" */
+ u8 pad[2];
+} AA64Reg;
+
+static int parse_reg_from_ident(AsmDriver* d, Sym ident, AA64Reg* out) {
+ size_t n = 0;
+ const char* p = pool_str(asm_driver_pool(d), ident, &n);
+ if (!p || !n) return 0;
+ /* "sp" */
+ if (icase_eq(p, n, "sp")) {
+ out->num = 31;
+ out->is64 = 1;
+ out->is_sp = 1;
+ return 1;
+ }
+ if (icase_eq(p, n, "wsp")) {
+ out->num = 31;
+ out->is64 = 0;
+ out->is_sp = 1;
+ return 1;
+ }
+ if (icase_eq(p, n, "lr")) {
+ out->num = 30;
+ out->is64 = 1;
+ out->is_sp = 0;
+ return 1;
+ }
+ if (icase_eq(p, n, "fp")) {
+ out->num = 29;
+ out->is64 = 1;
+ out->is_sp = 0;
+ return 1;
+ }
+ if (icase_eq(p, n, "ip0")) {
+ out->num = 16;
+ out->is64 = 1;
+ out->is_sp = 0;
+ return 1;
+ }
+ if (icase_eq(p, n, "ip1")) {
+ out->num = 17;
+ out->is64 = 1;
+ out->is_sp = 0;
+ return 1;
+ }
+ if (icase_eq(p, n, "xzr")) {
+ out->num = 31;
+ out->is64 = 1;
+ out->is_sp = 0;
+ return 1;
+ }
+ if (icase_eq(p, n, "wzr")) {
+ out->num = 31;
+ out->is64 = 0;
+ out->is_sp = 0;
+ return 1;
+ }
+ /* W/X<num> */
+ if ((p[0] == 'w' || p[0] == 'W' || p[0] == 'x' || p[0] == 'X') && n >= 2) {
+ u32 r = 0;
+ size_t i;
+ for (i = 1; i < n; ++i) {
+ char c = p[i];
+ if (c < '0' || c > '9') return 0;
+ r = r * 10 + (u32)(c - '0');
+ if (r > 31) return 0;
+ }
+ out->num = r;
+ out->is64 = (p[0] == 'x' || p[0] == 'X') ? 1 : 0;
+ out->is_sp = 0;
+ return 1;
+ }
+ return 0;
+}
+
+static AA64Reg parse_reg(AsmDriver* d) {
+ Tok t = asm_driver_next(d);
+ AA64Reg r;
+ memset(&r, 0, sizeof r);
+ if (t.kind != TOK_IDENT || !parse_reg_from_ident(d, t.v.ident, &r))
+ asm_driver_panic(d, "asm: expected register");
+ return r;
+}
+
+/* Parse "#imm" (with optional + / -) or a bare expression — GNU as is
+ * lenient about the leading hash. Returns an i64. */
+static i64 parse_imm_const(AsmDriver* d) {
+ (void)asm_driver_eat_punct(d, '#');
+ return asm_driver_parse_const(d);
+}
+
+/* Parse a possibly-symbolic operand prefixed by '#'. */
+static void parse_imm_sym(AsmDriver* d, ObjSymId* sym_out, i64* val_out) {
+ (void)asm_driver_eat_punct(d, '#');
+ asm_driver_parse_sym_expr(d, sym_out, val_out);
+}
+
+static void emit32(AsmDriver* d, u32 word) {
+ MCEmitter* mc = asm_driver_mc(d);
+ (void)asm_driver_cur_section(d);
+ u8 buf[4];
+ buf[0] = (u8)(word & 0xff);
+ buf[1] = (u8)((word >> 8) & 0xff);
+ buf[2] = (u8)((word >> 16) & 0xff);
+ buf[3] = (u8)((word >> 24) & 0xff);
+ mc->emit_bytes(mc, buf, 4);
+}
+
+static void expect_comma(AsmDriver* d, const char* what) {
+ if (!asm_driver_eat_comma(d))
+ asm_driver_panic(d, "asm: expected ',' (%s)", what);
+}
+
+/* ---- per-mnemonic parsers ---- */
+
+/* ret [Xn] — Xn defaults to x30. */
+static void p_ret(AsmDriver* d) {
+ if (asm_driver_at_eol(d)) {
+ emit32(d, aa64_ret(30));
+ return;
+ }
+ AA64Reg r = parse_reg(d);
+ if (!r.is64) asm_driver_panic(d, "asm: ret: 64-bit register expected");
+ emit32(d, aa64_ret(r.num));
+}
+
+static void p_br(AsmDriver* d) {
+ AA64Reg r = parse_reg(d);
+ if (!r.is64) asm_driver_panic(d, "asm: br: 64-bit register expected");
+ emit32(d, aa64_br(r.num));
+}
+
+static void p_blr(AsmDriver* d) {
+ AA64Reg r = parse_reg(d);
+ if (!r.is64) asm_driver_panic(d, "asm: blr: 64-bit register expected");
+ emit32(d, aa64_blr(r.num));
+}
+
+static void p_nop(AsmDriver* d) {
+ (void)d;
+ emit32(d, aa64_nop());
+}
+
+/* mov:
+ * mov Rd, Rm → ORR Rd, ZR, Rm
+ * mov Rd, #imm → MOVZ (if imm fits in a single halfword unshifted)
+ * MOVN (if ~imm fits)
+ * otherwise: panic (multi-step expansion deferred). */
+static void p_mov(AsmDriver* d) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "mov");
+ Tok t = asm_driver_peek(d);
+ if (t.kind == TOK_IDENT) {
+ AA64Reg src;
+ memset(&src, 0, sizeof src);
+ if (parse_reg_from_ident(d, t.v.ident, &src)) {
+ (void)asm_driver_next(d);
+ if (src.is64 != rd.is64)
+ asm_driver_panic(d, "asm: mov: register width mismatch");
+ /* mov involving SP encodes as `ADD Rd, Rsp, #0` per AArch64;
+ * approximate with that exact form. */
+ if (rd.is_sp || src.is_sp) {
+ emit32(d, aa64_add_imm(rd.is64, rd.num, src.num, 0, 0));
+ return;
+ }
+ emit32(d, aa64_mov_reg(rd.is64, rd.num, src.num));
+ return;
+ }
+ /* fall through: identifier that is not a register → treat as
+ * symbol/equate via expression below. */
+ }
+ /* Immediate. */
+ i64 imm = parse_imm_const(d);
+ if (rd.is_sp) asm_driver_panic(d, "asm: mov: cannot move imm into SP");
+ u64 uv = (u64)imm;
+ u64 mask = rd.is64 ? ~0ull : 0xffffffffull;
+ uv &= mask;
+ /* Try MOVZ with one of four halfwords. */
+ for (u32 hw = 0; hw < (rd.is64 ? 4u : 2u); ++hw) {
+ u64 shift = (u64)hw * 16;
+ u64 hwmask = 0xffffull << shift;
+ if ((uv & ~hwmask) == 0) {
+ u32 v = (u32)((uv >> shift) & 0xffff);
+ emit32(d, aa64_movz(rd.is64, rd.num, v, hw));
+ return;
+ }
+ }
+ /* Try MOVN with one halfword (encodes ~imm in that halfword). */
+ u64 nv = (~uv) & mask;
+ for (u32 hw = 0; hw < (rd.is64 ? 4u : 2u); ++hw) {
+ u64 shift = (u64)hw * 16;
+ u64 hwmask = 0xffffull << shift;
+ if ((nv & ~hwmask) == 0) {
+ u32 v = (u32)((nv >> shift) & 0xffff);
+ emit32(d, aa64_movn(rd.is64, rd.num, v, hw));
+ return;
+ }
+ }
+ asm_driver_panic(d, "asm: mov: immediate cannot be encoded in one insn");
+}
+
+/* mvn Rd, Rm */
+static void p_mvn(AsmDriver* d) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "mvn");
+ AA64Reg rm = parse_reg(d);
+ if (rd.is64 != rm.is64) asm_driver_panic(d, "asm: mvn: width mismatch");
+ emit32(d, aa64_mvn(rd.is64, rd.num, rm.num));
+}
+
+/* movz / movn / movk Rd, #imm[, lsl #shift] */
+static void p_movwide(AsmDriver* d, u32 opc) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "movz/n/k");
+ i64 imm = parse_imm_const(d);
+ u32 hw = 0;
+ if (asm_driver_eat_comma(d)) {
+ /* lsl #N (N is 0/16/32/48). */
+ Tok lid = asm_driver_next(d);
+ if (lid.kind != TOK_IDENT)
+ asm_driver_panic(d, "asm: expected 'lsl'");
+ size_t ln = 0;
+ const char* lp = pool_str(asm_driver_pool(d), lid.v.ident, &ln);
+ if (!lp || !icase_eq(lp, ln, "lsl"))
+ asm_driver_panic(d, "asm: expected 'lsl'");
+ i64 sh = parse_imm_const(d);
+ if (sh % 16 != 0 || sh < 0 || sh > 48)
+ asm_driver_panic(d, "asm: movz/n/k: bad lsl shift");
+ hw = (u32)(sh / 16);
+ }
+ u32 word = ((rd.is64 & 1u) << 31) | ((opc & 3u) << 29) |
+ AA64_MOVEWIDE_FAMILY_MATCH | ((hw & 3u) << 21) |
+ (((u32)imm & 0xffffu) << 5) | (rd.num & 0x1fu);
+ emit32(d, word);
+}
+
+/* svc / brk / hlt #imm */
+static void p_except(AsmDriver* d, u32 form) {
+ i64 imm = parse_imm_const(d);
+ switch (form) {
+ case 0: emit32(d, aa64_svc((u32)imm)); break;
+ case 1: emit32(d, aa64_brk((u32)imm)); break;
+ case 2: {
+ /* HLT */
+ u32 word = AA64_EXCEPT_FAMILY_MATCH | ((u32)2 << 21) |
+ (((u32)imm & 0xffffu) << 5);
+ emit32(d, word);
+ break;
+ }
+ default: asm_driver_panic(d, "asm: bad exception form");
+ }
+}
+
+/* Read optional `, lsl|lsr|asr|ror #imm` shift modifier. Returns 1 if
+ * present. */
+static int parse_shift_mod(AsmDriver* d, u32* shift_out, u32* imm6_out) {
+ Tok t = asm_driver_peek(d);
+ if (t.kind != TOK_IDENT) return 0;
+ size_t n = 0;
+ const char* p = pool_str(asm_driver_pool(d), t.v.ident, &n);
+ u32 sh;
+ if (icase_eq(p, n, "lsl")) sh = 0;
+ else if (icase_eq(p, n, "lsr")) sh = 1;
+ else if (icase_eq(p, n, "asr")) sh = 2;
+ else if (icase_eq(p, n, "ror")) sh = 3;
+ else return 0;
+ (void)asm_driver_next(d);
+ i64 imm = parse_imm_const(d);
+ if (imm < 0 || imm > 63)
+ asm_driver_panic(d, "asm: shift amount out of range");
+ *shift_out = sh;
+ *imm6_out = (u32)imm;
+ return 1;
+}
+
+/* add / sub family.
+ * Forms:
+ * add Rd, Rn, Rm[, lsl #s] shifted-register
+ * add Rd, Rn, #imm immediate
+ * add Rd, Rn, #imm, lsl #12 immediate w/ shift
+ * S-suffixed (adds/subs) sets flags. */
+static void p_addsub(AsmDriver* d, int is_sub, int set_flags) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "add/sub");
+ AA64Reg rn = parse_reg(d);
+ expect_comma(d, "add/sub");
+ Tok t = asm_driver_peek(d);
+ if (tok_punct(t, '#') || t.kind == TOK_NUM || tok_punct(t, '-') ||
+ tok_punct(t, '+')) {
+ /* immediate form */
+ i64 imm = parse_imm_const(d);
+ u32 sh = 0;
+ if (asm_driver_eat_comma(d)) {
+ Tok lid = asm_driver_next(d);
+ if (lid.kind != TOK_IDENT)
+ asm_driver_panic(d, "asm: expected 'lsl #12'");
+ size_t ln = 0;
+ const char* lp = pool_str(asm_driver_pool(d), lid.v.ident, &ln);
+ if (!lp || !icase_eq(lp, ln, "lsl"))
+ asm_driver_panic(d, "asm: expected 'lsl'");
+ i64 s = parse_imm_const(d);
+ if (s == 12) sh = 1;
+ else if (s == 0) sh = 0;
+ else asm_driver_panic(d, "asm: add/sub imm: lsl must be 0 or 12");
+ }
+ if (imm < 0 || imm > 0xfff)
+ asm_driver_panic(d, "asm: add/sub imm out of range");
+ u32 word = aa64_addsubimm_pack((AA64AddSubImm){
+ .sf = rd.is64, .op = (u32)is_sub, .S = (u32)set_flags, .sh = sh,
+ .imm12 = (u32)imm, .Rn = rn.num, .Rd = rd.num});
+ emit32(d, word);
+ return;
+ }
+ /* register form */
+ AA64Reg rm = parse_reg(d);
+ if (rd.is64 != rm.is64 || rd.is64 != rn.is64)
+ asm_driver_panic(d, "asm: add/sub reg: width mismatch");
+ u32 shift = 0, imm6 = 0;
+ if (asm_driver_eat_comma(d)) {
+ if (!parse_shift_mod(d, &shift, &imm6))
+ asm_driver_panic(d, "asm: add/sub reg: expected shift modifier");
+ }
+ u32 word = aa64_addsubsr_pack((AA64AddSubSR){
+ .sf = rd.is64, .op = (u32)is_sub, .S = (u32)set_flags,
+ .shift = shift, .Rm = rm.num, .imm6 = imm6, .Rn = rn.num,
+ .Rd = rd.num});
+ emit32(d, word);
+}
+
+/* cmp Rn, Rm | cmp Rn, #imm → SUBS ZR, Rn, ... */
+static void p_cmp(AsmDriver* d, int is_neg /* cmn flips op */) {
+ AA64Reg rn = parse_reg(d);
+ expect_comma(d, "cmp");
+ Tok t = asm_driver_peek(d);
+ if (tok_punct(t, '#') || t.kind == TOK_NUM || tok_punct(t, '-') ||
+ tok_punct(t, '+')) {
+ i64 imm = parse_imm_const(d);
+ u32 sh = 0;
+ if (asm_driver_eat_comma(d)) {
+ Tok lid = asm_driver_next(d);
+ size_t ln = 0;
+ const char* lp =
+ (lid.kind == TOK_IDENT)
+ ? pool_str(asm_driver_pool(d), lid.v.ident, &ln)
+ : NULL;
+ if (!lp || !icase_eq(lp, ln, "lsl"))
+ asm_driver_panic(d, "asm: cmp imm: expected 'lsl'");
+ i64 s = parse_imm_const(d);
+ if (s == 12) sh = 1;
+ else if (s != 0)
+ asm_driver_panic(d, "asm: cmp imm: lsl must be 0 or 12");
+ }
+ if (imm < 0 || imm > 0xfff)
+ asm_driver_panic(d, "asm: cmp imm out of range");
+ u32 word = aa64_addsubimm_pack(
+ (AA64AddSubImm){.sf = rn.is64, .op = (u32)(!is_neg), .S = 1,
+ .sh = sh, .imm12 = (u32)imm, .Rn = rn.num,
+ .Rd = AA64_ZR});
+ emit32(d, word);
+ return;
+ }
+ AA64Reg rm = parse_reg(d);
+ if (rm.is64 != rn.is64) asm_driver_panic(d, "asm: cmp: width mismatch");
+ u32 shift = 0, imm6 = 0;
+ if (asm_driver_eat_comma(d)) parse_shift_mod(d, &shift, &imm6);
+ u32 word = aa64_addsubsr_pack((AA64AddSubSR){
+ .sf = rn.is64, .op = (u32)(!is_neg), .S = 1, .shift = shift,
+ .Rm = rm.num, .imm6 = imm6, .Rn = rn.num, .Rd = AA64_ZR});
+ emit32(d, word);
+}
+
+/* neg / negs Rd, Rm → SUB / SUBS Rd, ZR, Rm */
+static void p_neg(AsmDriver* d, int set_flags) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "neg");
+ AA64Reg rm = parse_reg(d);
+ if (rd.is64 != rm.is64) asm_driver_panic(d, "asm: neg: width mismatch");
+ u32 shift = 0, imm6 = 0;
+ if (asm_driver_eat_comma(d)) parse_shift_mod(d, &shift, &imm6);
+ u32 word = aa64_addsubsr_pack((AA64AddSubSR){
+ .sf = rd.is64, .op = 1, .S = (u32)set_flags, .shift = shift,
+ .Rm = rm.num, .imm6 = imm6, .Rn = AA64_ZR, .Rd = rd.num});
+ emit32(d, word);
+}
+
+/* Logical shifted-register family. */
+static void p_log_sr(AsmDriver* d, u32 opc, u32 N) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "logical");
+ AA64Reg rn = parse_reg(d);
+ expect_comma(d, "logical");
+ AA64Reg rm = parse_reg(d);
+ if (rd.is64 != rn.is64 || rd.is64 != rm.is64)
+ asm_driver_panic(d, "asm: logical: width mismatch");
+ u32 shift = 0, imm6 = 0;
+ if (asm_driver_eat_comma(d)) parse_shift_mod(d, &shift, &imm6);
+ u32 word = aa64_logsr_pack((AA64LogSR){
+ .sf = rd.is64, .opc = opc, .shift = shift, .N = N, .Rm = rm.num,
+ .imm6 = imm6, .Rn = rn.num, .Rd = rd.num});
+ emit32(d, word);
+}
+
+/* Data-processing 3-source: madd/msub Rd, Rn, Rm, Ra. */
+static void p_dp3(AsmDriver* d, u32 o0) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "dp3");
+ AA64Reg rn = parse_reg(d);
+ expect_comma(d, "dp3");
+ AA64Reg rm = parse_reg(d);
+ expect_comma(d, "dp3");
+ AA64Reg ra = parse_reg(d);
+ if (rd.is64 != rn.is64 || rd.is64 != rm.is64 || rd.is64 != ra.is64)
+ asm_driver_panic(d, "asm: dp3: width mismatch");
+ u32 word = aa64_dp3_pack((AA64DP3){
+ .sf = rd.is64, .op31 = 0, .o0 = o0, .Rm = rm.num, .Ra = ra.num,
+ .Rn = rn.num, .Rd = rd.num});
+ emit32(d, word);
+}
+
+/* mul Rd, Rn, Rm → MADD Rd, Rn, Rm, ZR */
+static void p_mul(AsmDriver* d, u32 o0) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "mul");
+ AA64Reg rn = parse_reg(d);
+ expect_comma(d, "mul");
+ AA64Reg rm = parse_reg(d);
+ if (rd.is64 != rn.is64 || rd.is64 != rm.is64)
+ asm_driver_panic(d, "asm: mul: width mismatch");
+ u32 word = aa64_dp3_pack((AA64DP3){
+ .sf = rd.is64, .op31 = 0, .o0 = o0, .Rm = rm.num, .Ra = AA64_ZR,
+ .Rn = rn.num, .Rd = rd.num});
+ emit32(d, word);
+}
+
+/* DP2: udiv/sdiv/lslv/lsrv/asrv/rorv Rd, Rn, Rm. */
+static void p_dp2(AsmDriver* d, u32 opcode) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "dp2");
+ AA64Reg rn = parse_reg(d);
+ expect_comma(d, "dp2");
+ AA64Reg rm = parse_reg(d);
+ if (rd.is64 != rn.is64 || rd.is64 != rm.is64)
+ asm_driver_panic(d, "asm: dp2: width mismatch");
+ u32 word = aa64_dp2_pack((AA64DP2){.sf = rd.is64, .opcode = opcode,
+ .Rm = rm.num, .Rn = rn.num,
+ .Rd = rd.num});
+ emit32(d, word);
+}
+
+/* Branch immediate / conditional / compare-and-branch. */
+
+static void emit_branch_imm(AsmDriver* d, u32 op_bl, ObjSymId target,
+ i64 addend, i64 const_disp) {
+ MCEmitter* mc = asm_driver_mc(d);
+ /* Emit a B/BL with imm26 = 0; record a CALL26/JUMP26 reloc against
+ * either the symbol or the constant displacement. */
+ u32 word = aa64_brimm_pack((AA64BrImm){.op = op_bl, .imm26 = 0});
+ emit32(d, word);
+ u32 ofs = mc->pos(mc) - 4;
+ RelocKind k = op_bl ? R_AARCH64_CALL26 : R_AARCH64_JUMP26;
+ if (target != OBJ_SYM_NONE) {
+ mc->emit_reloc_at(mc, asm_driver_cur_section(d), ofs, k, target,
+ addend, 1, 0);
+ } else {
+ /* Pure constant displacement is rare in real .s; reject it now.
+ * The recommended form is to use a label and let the assembler
+ * compute the displacement. */
+ (void)const_disp;
+ asm_driver_panic(d, "asm: branch with pure constant disp not supported");
+ }
+}
+
+static void p_b(AsmDriver* d, u32 op_bl) {
+ ObjSymId sym = OBJ_SYM_NONE;
+ i64 off = 0;
+ /* GNU as accepts `b sym`, `bl sym+8`, etc. */
+ parse_imm_sym(d, &sym, &off);
+ if (sym == OBJ_SYM_NONE)
+ asm_driver_panic(d, "asm: b/bl: symbolic target required");
+ emit_branch_imm(d, op_bl, sym, off, 0);
+}
+
+static void p_b_cond(AsmDriver* d, u32 cond) {
+ ObjSymId sym = OBJ_SYM_NONE;
+ i64 off = 0;
+ parse_imm_sym(d, &sym, &off);
+ if (sym == OBJ_SYM_NONE)
+ asm_driver_panic(d, "asm: b.cond: symbolic target required");
+ /* Emit the instruction with imm19=0 + R_AARCH64_CONDBR19 reloc. */
+ u32 word = aa64_brcond_pack((AA64BrCond){.imm19 = 0, .cond = cond});
+ emit32(d, word);
+ MCEmitter* mc = asm_driver_mc(d);
+ u32 ofs = mc->pos(mc) - 4;
+ mc->emit_reloc_at(mc, asm_driver_cur_section(d), ofs,
+ R_AARCH64_CONDBR19, sym, off, 1, 0);
+}
+
+static void p_cbz(AsmDriver* d, u32 op) {
+ AA64Reg rt = parse_reg(d);
+ expect_comma(d, "cbz");
+ ObjSymId sym = OBJ_SYM_NONE;
+ i64 off = 0;
+ parse_imm_sym(d, &sym, &off);
+ if (sym == OBJ_SYM_NONE)
+ asm_driver_panic(d, "asm: cbz: symbolic target required");
+ u32 word = aa64_cb_pack((AA64CB){.sf = rt.is64, .op = op, .imm19 = 0,
+ .Rt = rt.num});
+ emit32(d, word);
+ MCEmitter* mc = asm_driver_mc(d);
+ u32 ofs = mc->pos(mc) - 4;
+ mc->emit_reloc_at(mc, asm_driver_cur_section(d), ofs,
+ R_AARCH64_CONDBR19, sym, off, 1, 0);
+}
+
+/* Memory-operand parser for [Xn], [Xn, #imm], [Xn, #imm]!.
+ *
+ * pre_index_out is 1 when the closing `]!` appeared (pre-indexed).
+ * imm is the literal byte offset (no scaling). */
+typedef struct AA64Mem {
+ AA64Reg base;
+ i64 imm; /* byte offset (literal as written) */
+ u8 pre_index;
+ u8 has_offset;
+ u8 pad[2];
+} AA64Mem;
+
+static AA64Mem parse_mem(AsmDriver* d) {
+ AA64Mem m;
+ memset(&m, 0, sizeof m);
+ if (!asm_driver_eat_punct(d, '['))
+ asm_driver_panic(d, "asm: expected '['");
+ m.base = parse_reg(d);
+ if (!m.base.is64)
+ asm_driver_panic(d, "asm: ldr/str: base register must be 64-bit");
+ if (asm_driver_eat_comma(d)) {
+ m.imm = parse_imm_const(d);
+ m.has_offset = 1;
+ }
+ if (!asm_driver_eat_punct(d, ']'))
+ asm_driver_panic(d, "asm: expected ']'");
+ if (asm_driver_eat_punct(d, '!')) m.pre_index = 1;
+ return m;
+}
+
+/* ldr/str Rt, [Xn, #imm] — chooses scaled or unscaled form based on
+ * alignment of imm. */
+static void p_ldr_str(AsmDriver* d, int is_load) {
+ AA64Reg rt = parse_reg(d);
+ expect_comma(d, "ldr/str");
+ AA64Mem m = parse_mem(d);
+ u32 size = rt.is64 ? 3u : 2u;
+ u32 opc = is_load ? AA64_LDST_OPC_LDR : AA64_LDST_OPC_STR;
+ if (!m.pre_index) {
+ /* Try scaled unsigned-imm12 first. */
+ u32 scale = 1u << size;
+ if (m.imm >= 0 && (i64)((u64)m.imm % scale) == 0 &&
+ (u64)m.imm / scale <= 0xfff) {
+ u32 imm12 = (u32)((u64)m.imm / scale);
+ u32 word = aa64_ldst_uimm_pack((AA64LdStUimm){
+ .size = size, .V = 0, .opc = opc, .imm12 = imm12,
+ .Rn = m.base.num, .Rt = rt.num});
+ emit32(d, word);
+ return;
+ }
+ /* Fall back to unscaled signed-imm9 (LDUR/STUR). */
+ if (m.imm >= -256 && m.imm <= 255) {
+ u32 imm9 = (u32)((u64)m.imm & 0x1ffu);
+ u32 word = aa64_ldst_simm9_pack((AA64LdStSimm9){
+ .size = size, .V = 0, .opc = opc, .imm9 = imm9,
+ .Rn = m.base.num, .Rt = rt.num});
+ emit32(d, word);
+ return;
+ }
+ asm_driver_panic(d, "asm: ldr/str: immediate out of range");
+ }
+ asm_driver_panic(d, "asm: ldr/str: pre-indexed form not yet supported");
+}
+
+/* ldur/stur — unscaled signed-imm9. */
+static void p_ldur_stur(AsmDriver* d, int is_load) {
+ AA64Reg rt = parse_reg(d);
+ expect_comma(d, "ldur/stur");
+ AA64Mem m = parse_mem(d);
+ u32 size = rt.is64 ? 3u : 2u;
+ if (m.imm < -256 || m.imm > 255)
+ asm_driver_panic(d, "asm: ldur/stur: imm9 out of range");
+ u32 imm9 = (u32)((u64)m.imm & 0x1ffu);
+ u32 word = aa64_ldst_simm9_pack((AA64LdStSimm9){
+ .size = size, .V = 0,
+ .opc = is_load ? AA64_LDST_OPC_LDR : AA64_LDST_OPC_STR,
+ .imm9 = imm9, .Rn = m.base.num, .Rt = rt.num});
+ emit32(d, word);
+}
+
+/* ldp / stp Rt, Rt2, [Xn, #imm] or [Xn, #imm]! */
+static void p_ldp_stp(AsmDriver* d, int is_load) {
+ AA64Reg rt = parse_reg(d);
+ expect_comma(d, "ldp/stp");
+ AA64Reg rt2 = parse_reg(d);
+ expect_comma(d, "ldp/stp");
+ if (rt.is64 != rt2.is64)
+ asm_driver_panic(d, "asm: ldp/stp: width mismatch");
+ AA64Mem m = parse_mem(d);
+ u32 scale = rt.is64 ? 8u : 4u;
+ if ((i64)((u64)m.imm % scale) != 0)
+ asm_driver_panic(d, "asm: ldp/stp: imm not scale-aligned");
+ i64 imm7 = m.imm / (i64)scale;
+ if (imm7 < -64 || imm7 > 63)
+ asm_driver_panic(d, "asm: ldp/stp: imm7 out of range");
+ AA64LdStPPre f = {.opc = rt.is64 ? 2u : 0u,
+ .V = 0,
+ .L = is_load ? 1u : 0u,
+ .imm7 = (u32)imm7 & 0x7fu,
+ .Rt2 = rt2.num,
+ .Rn = m.base.num,
+ .Rt = rt.num};
+ if (m.pre_index)
+ emit32(d, aa64_ldstp_pre_pack(f));
+ else
+ emit32(d, aa64_ldstp_soff_pack(f));
+}
+
+/* adr / adrp Rd, sym */
+static void p_adr(AsmDriver* d, int is_adrp) {
+ AA64Reg rd = parse_reg(d);
+ expect_comma(d, "adr");
+ ObjSymId sym = OBJ_SYM_NONE;
+ i64 off = 0;
+ parse_imm_sym(d, &sym, &off);
+ if (sym == OBJ_SYM_NONE)
+ asm_driver_panic(d, "asm: adr/adrp: symbol required");
+ AA64PCRelAdr f = {.op = is_adrp ? AA64_ADR_OP_ADRP : AA64_ADR_OP_ADR,
+ .immlo = 0, .immhi = 0, .Rd = rd.num};
+ emit32(d, aa64_pcrel_adr_pack(f));
+ MCEmitter* mc = asm_driver_mc(d);
+ u32 ofs = mc->pos(mc) - 4;
+ RelocKind k = is_adrp ? R_AARCH64_ADR_PREL_PG_HI21 : R_AARCH64_ADR_PREL_LO21;
+ mc->emit_reloc_at(mc, asm_driver_cur_section(d), ofs, k, sym, off, 1, 0);
+}
+
+/* ---- mnemonic dispatch table ---- */
+
+typedef void (*P_Fn)(AsmDriver*);
+
+typedef struct AA64Mn {
+ const char* name;
+ P_Fn fn;
+ u32 arg; /* per-fn discriminator (alias parameter) */
+} AA64Mn;
+
+/* Wrapper functions for the discriminator-taking parsers, since the
+ * table holds a uniform P_Fn pointer. Each wraps a single (fn, arg)
+ * tuple. */
+static void p_addsub_add(AsmDriver* d) { p_addsub(d, /*is_sub=*/0, 0); }
+static void p_addsub_adds(AsmDriver* d) { p_addsub(d, 0, 1); }
+static void p_addsub_sub(AsmDriver* d) { p_addsub(d, 1, 0); }
+static void p_addsub_subs(AsmDriver* d) { p_addsub(d, 1, 1); }
+static void p_cmp_w(AsmDriver* d) { p_cmp(d, 0); }
+static void p_cmn_w(AsmDriver* d) { p_cmp(d, 1); }
+static void p_neg_w(AsmDriver* d) { p_neg(d, 0); }
+static void p_negs_w(AsmDriver* d) { p_neg(d, 1); }
+static void p_and_w(AsmDriver* d) { p_log_sr(d, AA64_LOG_AND_OPC, 0); }
+static void p_bic_w(AsmDriver* d) { p_log_sr(d, AA64_LOG_AND_OPC, 1); }
+static void p_orr_w(AsmDriver* d) { p_log_sr(d, AA64_LOG_ORR_OPC, 0); }
+static void p_orn_w(AsmDriver* d) { p_log_sr(d, AA64_LOG_ORR_OPC, 1); }
+static void p_eor_w(AsmDriver* d) { p_log_sr(d, AA64_LOG_EOR_OPC, 0); }
+static void p_eon_w(AsmDriver* d) { p_log_sr(d, AA64_LOG_EOR_OPC, 1); }
+static void p_ands_w(AsmDriver* d) { p_log_sr(d, AA64_LOG_ANDS_OPC, 0); }
+static void p_bics_w(AsmDriver* d) { p_log_sr(d, AA64_LOG_ANDS_OPC, 1); }
+static void p_madd(AsmDriver* d) { p_dp3(d, 0); }
+static void p_msub(AsmDriver* d) { p_dp3(d, 1); }
+static void p_mul_w(AsmDriver* d) { p_mul(d, 0); }
+static void p_mneg_w(AsmDriver* d) { p_mul(d, 1); }
+static void p_udiv_w(AsmDriver* d) { p_dp2(d, AA64_DP2_UDIV_OP); }
+static void p_sdiv_w(AsmDriver* d) { p_dp2(d, AA64_DP2_SDIV_OP); }
+static void p_lslv_w(AsmDriver* d) { p_dp2(d, AA64_DP2_LSLV_OP); }
+static void p_lsrv_w(AsmDriver* d) { p_dp2(d, AA64_DP2_LSRV_OP); }
+static void p_asrv_w(AsmDriver* d) { p_dp2(d, AA64_DP2_ASRV_OP); }
+static void p_rorv_w(AsmDriver* d) { p_dp2(d, AA64_DP2_RORV_OP); }
+static void p_b_(AsmDriver* d) { p_b(d, 0); }
+static void p_bl_(AsmDriver* d) { p_b(d, 1); }
+static void p_cbz_(AsmDriver* d) { p_cbz(d, 0); }
+static void p_cbnz_(AsmDriver* d) { p_cbz(d, 1); }
+static void p_movz_(AsmDriver* d) { p_movwide(d, AA64_MOVZ_OPC); }
+static void p_movn_(AsmDriver* d) { p_movwide(d, AA64_MOVN_OPC); }
+static void p_movk_(AsmDriver* d) { p_movwide(d, AA64_MOVK_OPC); }
+static void p_svc_(AsmDriver* d) { p_except(d, 0); }
+static void p_brk_(AsmDriver* d) { p_except(d, 1); }
+static void p_hlt_(AsmDriver* d) { p_except(d, 2); }
+static void p_ldr_(AsmDriver* d) { p_ldr_str(d, 1); }
+static void p_str_(AsmDriver* d) { p_ldr_str(d, 0); }
+static void p_ldur_(AsmDriver* d) { p_ldur_stur(d, 1); }
+static void p_stur_(AsmDriver* d) { p_ldur_stur(d, 0); }
+static void p_ldp_(AsmDriver* d) { p_ldp_stp(d, 1); }
+static void p_stp_(AsmDriver* d) { p_ldp_stp(d, 0); }
+static void p_adr_(AsmDriver* d) { p_adr(d, 0); }
+static void p_adrp_(AsmDriver* d) { p_adr(d, 1); }
+
+/* b.cond family. cond codes follow the standard ARMv8 numbering. */
+static void p_b_eq(AsmDriver* d) { p_b_cond(d, 0); }
+static void p_b_ne(AsmDriver* d) { p_b_cond(d, 1); }
+static void p_b_cs(AsmDriver* d) { p_b_cond(d, 2); }
+static void p_b_hs(AsmDriver* d) { p_b_cond(d, 2); }
+static void p_b_cc(AsmDriver* d) { p_b_cond(d, 3); }
+static void p_b_lo(AsmDriver* d) { p_b_cond(d, 3); }
+static void p_b_mi(AsmDriver* d) { p_b_cond(d, 4); }
+static void p_b_pl(AsmDriver* d) { p_b_cond(d, 5); }
+static void p_b_vs(AsmDriver* d) { p_b_cond(d, 6); }
+static void p_b_vc(AsmDriver* d) { p_b_cond(d, 7); }
+static void p_b_hi(AsmDriver* d) { p_b_cond(d, 8); }
+static void p_b_ls(AsmDriver* d) { p_b_cond(d, 9); }
+static void p_b_ge(AsmDriver* d) { p_b_cond(d, 10); }
+static void p_b_lt(AsmDriver* d) { p_b_cond(d, 11); }
+static void p_b_gt(AsmDriver* d) { p_b_cond(d, 12); }
+static void p_b_le(AsmDriver* d) { p_b_cond(d, 13); }
+static void p_b_al(AsmDriver* d) { p_b_cond(d, 14); }
+
+static const AA64Mn kTable[] = {
+ {"nop", p_nop, 0},
+ {"ret", p_ret, 0},
+ {"br", p_br, 0},
+ {"blr", p_blr, 0},
+ {"mov", p_mov, 0},
+ {"mvn", p_mvn, 0},
+ {"movz", p_movz_, 0},
+ {"movn", p_movn_, 0},
+ {"movk", p_movk_, 0},
+ {"add", p_addsub_add, 0},
+ {"adds", p_addsub_adds, 0},
+ {"sub", p_addsub_sub, 0},
+ {"subs", p_addsub_subs, 0},
+ {"cmp", p_cmp_w, 0},
+ {"cmn", p_cmn_w, 0},
+ {"neg", p_neg_w, 0},
+ {"negs", p_negs_w, 0},
+ {"and", p_and_w, 0},
+ {"bic", p_bic_w, 0},
+ {"orr", p_orr_w, 0},
+ {"orn", p_orn_w, 0},
+ {"eor", p_eor_w, 0},
+ {"eon", p_eon_w, 0},
+ {"ands", p_ands_w, 0},
+ {"bics", p_bics_w, 0},
+ {"madd", p_madd, 0},
+ {"msub", p_msub, 0},
+ {"mul", p_mul_w, 0},
+ {"mneg", p_mneg_w, 0},
+ {"udiv", p_udiv_w, 0},
+ {"sdiv", p_sdiv_w, 0},
+ {"lslv", p_lslv_w, 0},
+ {"lsrv", p_lsrv_w, 0},
+ {"asrv", p_asrv_w, 0},
+ {"rorv", p_rorv_w, 0},
+ {"b", p_b_, 0},
+ {"bl", p_bl_, 0},
+ {"cbz", p_cbz_, 0},
+ {"cbnz", p_cbnz_, 0},
+ {"svc", p_svc_, 0},
+ {"brk", p_brk_, 0},
+ {"hlt", p_hlt_, 0},
+ {"ldr", p_ldr_, 0},
+ {"str", p_str_, 0},
+ {"ldur", p_ldur_, 0},
+ {"stur", p_stur_, 0},
+ {"ldp", p_ldp_, 0},
+ {"stp", p_stp_, 0},
+ {"adr", p_adr_, 0},
+ {"adrp", p_adrp_, 0},
+ {"b.eq", p_b_eq, 0}, {"b.ne", p_b_ne, 0},
+ {"b.cs", p_b_cs, 0}, {"b.hs", p_b_hs, 0},
+ {"b.cc", p_b_cc, 0}, {"b.lo", p_b_lo, 0},
+ {"b.mi", p_b_mi, 0}, {"b.pl", p_b_pl, 0},
+ {"b.vs", p_b_vs, 0}, {"b.vc", p_b_vc, 0},
+ {"b.hi", p_b_hi, 0}, {"b.ls", p_b_ls, 0},
+ {"b.ge", p_b_ge, 0}, {"b.lt", p_b_lt, 0},
+ {"b.gt", p_b_gt, 0}, {"b.le", p_b_le, 0},
+ {"b.al", p_b_al, 0},
+ {NULL, NULL, 0},
+};
+
+void aa64_asm_insn(AA64Asm* a, AsmDriver* d, Sym mnemonic) {
+ (void)a;
+ size_t mn = 0;
+ const char* mp = pool_str(asm_driver_pool(d), mnemonic, &mn);
+ for (const AA64Mn* row = kTable; row->name; ++row) {
+ if (icase_eq(mp, mn, row->name)) {
+ row->fn(d);
+ return;
+ }
+ }
+ asm_driver_panic(d, "asm: unknown mnemonic");
+}
diff --git a/src/arch/aa64_asm.h b/src/arch/aa64_asm.h
@@ -0,0 +1,35 @@
+#ifndef CFREE_ARCH_AA64_ASM_H
+#define CFREE_ARCH_AA64_ASM_H
+
+/* AArch64 standalone .s instruction parser.
+ *
+ * Owns the per-mnemonic operand grammar (registers, immediates, shift /
+ * extend modifiers, memory addressing). Reads tokens from the lexer
+ * the asm driver hands it, emits encoded words through MCEmitter, and
+ * issues relocations against ObjSymIds for symbolic operands.
+ *
+ * The driver exposes a tiny per-arch handle (AA64Asm) plus a single
+ * entry point (aa64_asm_insn) called for each mnemonic line. Symbol
+ * resolution and label management live on the driver side. */
+
+#include "core/core.h"
+#include "lex/lex.h"
+
+typedef struct AsmDriver AsmDriver;
+
+typedef struct AA64Asm AA64Asm;
+
+/* Construct/destroy. Pure: no allocations beyond the AA64Asm struct
+ * itself (which lives on the compiler's TU arena). */
+AA64Asm* aa64_asm_open(Compiler* c);
+void aa64_asm_close(AA64Asm*);
+
+/* Parse one mnemonic line. `mnemonic` is the first identifier on the
+ * line (or "b.cond" composite). The driver has already consumed the
+ * mnemonic identifier and any trailing dot-suffix. This function
+ * consumes operands up to (but not including) the next TOK_NEWLINE or
+ * TOK_EOF, and writes the encoded instruction(s) through the driver's
+ * MCEmitter. Diagnostics on parse failure go through compiler_panic. */
+void aa64_asm_insn(AA64Asm*, AsmDriver*, Sym mnemonic);
+
+#endif
diff --git a/src/arch/aa64_disasm.c b/src/arch/aa64_disasm.c
@@ -0,0 +1,133 @@
+/* AArch64 disassembler implementation.
+ *
+ * Decodes one 4-byte instruction word per call into a CfreeInsn whose
+ * string fields point into iterator-owned StrBufs. The decoder shares
+ * the aa64_isa.{h,c} descriptor table with the encoder: aa64_disasm_find
+ * matches the word; aa64_print_operands renders operand text via the
+ * format's unpack + per-format pretty-printer. Mnemonic rewriting (the
+ * one bit the printer can't own, because b.cond rolls cond into the
+ * "operand" text) happens here. */
+
+#include "arch/aa64_disasm.h"
+
+#include <string.h>
+
+#include "arch/aa64_isa.h"
+#include "core/heap.h"
+#include "core/strbuf.h"
+
+/* Enough for any aarch64 mnemonic-with-suffix ("b.cond" → "b.le", etc.). */
+#define AA64_DASM_MNEM_CAP 16u
+/* Operand text. The widest cases (LDP X, X, [SP, #-imm]!) fit easily. */
+#define AA64_DASM_OPS_CAP 96u
+/* Annotation overlay (symbol + addend). */
+#define AA64_DASM_ANN_CAP 96u
+
+typedef struct AA64Disasm {
+ ArchDisasm base;
+ Compiler* c;
+ Heap* heap;
+ char mnem_buf[AA64_DASM_MNEM_CAP];
+ char ops_buf[AA64_DASM_OPS_CAP];
+ char ann_buf[AA64_DASM_ANN_CAP];
+ StrBuf mnem;
+ StrBuf ops;
+ StrBuf ann;
+} AA64Disasm;
+
+static const char* aa64_cond_names[16] = {
+ "eq", "ne", "cs", "cc", "mi", "pl", "vs", "vc",
+ "hi", "ls", "ge", "lt", "gt", "le", "al", "nv",
+};
+
+static void aa64_write_mnemonic(AA64Disasm* d, const AA64InsnDesc* desc,
+ u32 word) {
+ strbuf_reset(&d->mnem);
+ if (desc->fmt == AA64_FMT_BR_COND) {
+ /* Synthesize "b.<cond>" so the operands buffer can hold just the
+ * target. Matches GNU as / objdump conventions. */
+ u32 cond = word & 0xfu;
+ strbuf_puts(&d->mnem, "b.");
+ strbuf_puts(&d->mnem, aa64_cond_names[cond]);
+ return;
+ }
+ strbuf_puts(&d->mnem, desc->mnemonic);
+}
+
+static void aa64_write_operands(AA64Disasm* d, const AA64InsnDesc* desc,
+ u32 word, u64 vaddr) {
+ strbuf_reset(&d->ops);
+ if (desc->fmt == AA64_FMT_BR_COND) {
+ /* aa64_print_operands prints "<cond> <target>"; we already lifted
+ * the cond into the mnemonic, so skip the dispatcher and inline
+ * just the target. */
+ AA64BrCond f = aa64_brcond_unpack(word);
+ i64 ofs = (i64)((u64)f.imm19 & 0x7ffffu);
+ /* sign-extend 19 bits */
+ if (ofs & 0x40000) ofs |= ~(i64)0x7ffff;
+ ofs *= 4;
+ if (vaddr) {
+ strbuf_put_hex_u64(&d->ops, vaddr + (u64)ofs);
+ } else {
+ strbuf_puts(&d->ops, "#");
+ strbuf_put_i64(&d->ops, ofs);
+ }
+ return;
+ }
+ aa64_print_operands(&d->ops, desc, word, vaddr);
+}
+
+static u32 aa64_read_u32_le(const u8* b) {
+ return (u32)b[0] | ((u32)b[1] << 8) | ((u32)b[2] << 16) | ((u32)b[3] << 24);
+}
+
+static void aa64_write_unknown(AA64Disasm* d, u32 word) {
+ strbuf_reset(&d->mnem);
+ strbuf_puts(&d->mnem, ".inst");
+ strbuf_reset(&d->ops);
+ strbuf_put_hex_u64(&d->ops, (u64)word);
+}
+
+static u32 aa64_decode(ArchDisasm* base, const u8* bytes, size_t len, u64 vaddr,
+ CfreeInsn* out) {
+ AA64Disasm* d = (AA64Disasm*)base;
+ if (len < 4u) return 0;
+ u32 word = aa64_read_u32_le(bytes);
+ const AA64InsnDesc* desc = aa64_disasm_find(word);
+ if (desc) {
+ aa64_write_mnemonic(d, desc, word);
+ aa64_write_operands(d, desc, word, vaddr);
+ } else {
+ aa64_write_unknown(d, word);
+ }
+ /* Annotation overlay is owned by the public iterator (cfree_disasm_iter_*).
+ * The arch-level decoder leaves it empty. */
+ strbuf_reset(&d->ann);
+ out->vaddr = vaddr;
+ out->bytes = bytes;
+ out->nbytes = 4;
+ out->mnemonic = strbuf_cstr(&d->mnem);
+ out->operands = strbuf_cstr(&d->ops);
+ out->annotation = strbuf_cstr(&d->ann);
+ return 4;
+}
+
+static void aa64_destroy(ArchDisasm* base) {
+ AA64Disasm* d = (AA64Disasm*)base;
+ d->heap->free(d->heap, d, sizeof(*d));
+}
+
+ArchDisasm* aa64_disasm_new(Compiler* c) {
+ Heap* h = (Heap*)c->env->heap;
+ AA64Disasm* d = (AA64Disasm*)h->alloc(h, sizeof(*d), _Alignof(AA64Disasm));
+ if (!d) return NULL;
+ memset(d, 0, sizeof(*d));
+ d->c = c;
+ d->heap = h;
+ d->base.decode = aa64_decode;
+ d->base.destroy = aa64_destroy;
+ strbuf_init(&d->mnem, d->mnem_buf, sizeof d->mnem_buf);
+ strbuf_init(&d->ops, d->ops_buf, sizeof d->ops_buf);
+ strbuf_init(&d->ann, d->ann_buf, sizeof d->ann_buf);
+ return &d->base;
+}
diff --git a/src/arch/aa64_disasm.h b/src/arch/aa64_disasm.h
@@ -0,0 +1,14 @@
+#ifndef CFREE_ARCH_AA64_DISASM_H
+#define CFREE_ARCH_AA64_DISASM_H
+
+/* AArch64 disassembler — ArchDisasm implementation.
+ *
+ * Wraps aa64_disasm_find + aa64_print_operands (src/arch/aa64_isa.{h,c}).
+ * The dispatcher in src/arch/disasm.c constructs one of these when the
+ * compiler target is CFREE_ARCH_ARM_64. */
+
+#include "arch/arch.h"
+
+ArchDisasm* aa64_disasm_new(Compiler*);
+
+#endif
diff --git a/src/arch/aa64_regs.c b/src/arch/aa64_regs.c
@@ -0,0 +1,88 @@
+/* AArch64 register name table — DWARF index ↔ assembler name.
+ *
+ * DWARF register numbering for AArch64 (per the AAPCS64 ABI supplement):
+ * 0..30 X0..X30 (also W0..W30; same DWARF index)
+ * 31 SP (X31 / WSP)
+ * 32 PC
+ * 33 ELR (mode dependent; unused here)
+ * 64..95 V0..V31 (also B/H/S/D forms; same index)
+ *
+ * The canonical assembler spelling for v1 is the 64-bit form (Xn / Vn);
+ * disassembler output picks W/B/H/S/D based on instruction width
+ * separately. */
+
+#include <stdint.h>
+#include <string.h>
+
+#include "arch/aa64_regs.h"
+#include "core/core.h"
+
+typedef struct AA64Reg {
+ uint32_t dwarf_idx;
+ const char* name;
+} AA64Reg;
+
+static const AA64Reg AA64_REGS[] = {
+ {0, "x0"}, {1, "x1"}, {2, "x2"}, {3, "x3"}, {4, "x4"},
+ {5, "x5"}, {6, "x6"}, {7, "x7"}, {8, "x8"}, {9, "x9"},
+ {10, "x10"}, {11, "x11"}, {12, "x12"}, {13, "x13"}, {14, "x14"},
+ {15, "x15"}, {16, "x16"}, {17, "x17"}, {18, "x18"}, {19, "x19"},
+ {20, "x20"}, {21, "x21"}, {22, "x22"}, {23, "x23"}, {24, "x24"},
+ {25, "x25"}, {26, "x26"}, {27, "x27"}, {28, "x28"}, {29, "x29"},
+ {30, "x30"}, {31, "sp"}, {32, "pc"},
+ {64, "v0"}, {65, "v1"}, {66, "v2"}, {67, "v3"}, {68, "v4"},
+ {69, "v5"}, {70, "v6"}, {71, "v7"}, {72, "v8"}, {73, "v9"},
+ {74, "v10"}, {75, "v11"}, {76, "v12"}, {77, "v13"}, {78, "v14"},
+ {79, "v15"}, {80, "v16"}, {81, "v17"}, {82, "v18"}, {83, "v19"},
+ {84, "v20"}, {85, "v21"}, {86, "v22"}, {87, "v23"}, {88, "v24"},
+ {89, "v25"}, {90, "v26"}, {91, "v27"}, {92, "v28"}, {93, "v29"},
+ {94, "v30"}, {95, "v31"},
+};
+
+static const uint32_t AA64_REGS_N = (uint32_t)(sizeof AA64_REGS /
+ sizeof AA64_REGS[0]);
+
+const char* aa64_register_name(uint32_t dwarf_idx) {
+ uint32_t i;
+ for (i = 0; i < AA64_REGS_N; ++i) {
+ if (AA64_REGS[i].dwarf_idx == dwarf_idx) return AA64_REGS[i].name;
+ }
+ return NULL;
+}
+
+int aa64_register_index(const char* name, uint32_t* idx_out) {
+ uint32_t i;
+ if (!name) return 1;
+ for (i = 0; i < AA64_REGS_N; ++i) {
+ if (!strcmp(AA64_REGS[i].name, name)) {
+ if (idx_out) *idx_out = AA64_REGS[i].dwarf_idx;
+ return 0;
+ }
+ }
+ /* Accept Wn alias for Xn (same DWARF index). */
+ if (name[0] == 'w' && name[1] != '\0') {
+ char buf[8];
+ size_t n = strlen(name);
+ if (n < sizeof buf) {
+ buf[0] = 'x';
+ memcpy(buf + 1, name + 1, n);
+ return aa64_register_index(buf, idx_out);
+ }
+ }
+ /* wzr / xzr aliases. */
+ if (!strcmp(name, "wzr") || !strcmp(name, "xzr")) {
+ if (idx_out) *idx_out = 31u; /* shares SP encoding slot; v1 picks SP */
+ return 0;
+ }
+ return 1;
+}
+
+uint32_t aa64_register_iter_size(void) { return AA64_REGS_N; }
+
+int aa64_register_iter_get(uint32_t i, uint32_t* dwarf_out,
+ const char** name_out) {
+ if (i >= AA64_REGS_N) return 1;
+ if (dwarf_out) *dwarf_out = AA64_REGS[i].dwarf_idx;
+ if (name_out) *name_out = AA64_REGS[i].name;
+ return 0;
+}
diff --git a/src/arch/aa64_regs.h b/src/arch/aa64_regs.h
@@ -0,0 +1,12 @@
+#ifndef CFREE_ARCH_AA64_REGS_H
+#define CFREE_ARCH_AA64_REGS_H
+
+#include <stdint.h>
+
+const char* aa64_register_name(uint32_t dwarf_idx);
+int aa64_register_index(const char* name, uint32_t* idx_out);
+uint32_t aa64_register_iter_size(void);
+int aa64_register_iter_get(uint32_t i, uint32_t* dwarf_out,
+ const char** name_out);
+
+#endif
diff --git a/src/arch/arch.h b/src/arch/arch.h
@@ -643,6 +643,11 @@ void cgtarget_free(CGTarget*);
* annotation string buffers placed into *out; they are valid until the
* next decode or arch_disasm_free, whichever comes first. */
typedef struct ArchDisasm ArchDisasm;
+struct ArchDisasm {
+ u32 (*decode)(ArchDisasm*, const u8* bytes, size_t len, u64 vaddr,
+ CfreeInsn* out);
+ void (*destroy)(ArchDisasm*);
+};
ArchDisasm* arch_disasm_new(Compiler*);
u32 arch_disasm_decode(ArchDisasm*, const u8* bytes, size_t len, u64 vaddr,
diff --git a/src/arch/disasm.c b/src/arch/disasm.c
@@ -0,0 +1,32 @@
+/* Disassembler dispatcher — peer of src/arch/cgtarget.c.
+ *
+ * Per-arch ArchDisasm constructors live in their own files. v1 wires
+ * aarch64 only; x86_64 / rv64 panic with a clean diagnostic so a build
+ * that asks for those targets dies loudly instead of returning NULL
+ * silently. arch_disasm_decode / arch_disasm_free are vtable thunks. */
+
+#include "arch/aa64_disasm.h"
+#include "arch/arch.h"
+
+ArchDisasm* arch_disasm_new(Compiler* c) {
+ switch (c->target.arch) {
+ case CFREE_ARCH_ARM_64:
+ return aa64_disasm_new(c);
+ default: {
+ SrcLoc loc = {0, 0, 0};
+ compiler_panic(c, loc,
+ "arch_disasm_new: unsupported target arch %d",
+ (int)c->target.arch);
+ }
+ }
+}
+
+u32 arch_disasm_decode(ArchDisasm* d, const u8* bytes, size_t len, u64 vaddr,
+ CfreeInsn* out) {
+ return d->decode(d, bytes, len, vaddr, out);
+}
+
+void arch_disasm_free(ArchDisasm* d) {
+ if (!d) return;
+ if (d->destroy) d->destroy(d);
+}
diff --git a/src/obj/obj.c b/src/obj/obj.c
@@ -64,6 +64,8 @@ ObjBuilder* obj_new(Compiler* c) {
return ob;
}
+Compiler* obj_compiler(const ObjBuilder* ob) { return ob ? ob->c : NULL; }
+
void obj_free(ObjBuilder* ob) {
u32 i, n;
if (!ob) return;
diff --git a/src/obj/obj.h b/src/obj/obj.h
@@ -303,6 +303,11 @@ typedef struct ObjGroup {
ObjBuilder* obj_new(Compiler*);
void obj_free(ObjBuilder*);
+/* The owning Compiler; needed by consumers (e.g. cfree_disasm_iter_new)
+ * that take a bare ObjBuilder and still must pool_str() symbol names
+ * against the right pool. */
+Compiler* obj_compiler(const ObjBuilder*);
+
/* ---- write side (MCEmitter/CGTarget and .o readers) ---- */
ObjSecId obj_section(ObjBuilder*, Sym name, SecKind, u16 flags, u32 align);
ObjSecId obj_section_ex(ObjBuilder*, Sym name, SecKind, SecSem, u16 flags,
diff --git a/src/parse/parse_asm.c b/src/parse/parse_asm.c
@@ -0,0 +1,935 @@
+/* GNU-as compatible assembler driver — arch-agnostic.
+ *
+ * Reads tokens from a Lexer, dispatches directives, manages labels and
+ * section state, and forwards mnemonic lines to the per-arch instruction
+ * parser. Output goes through MCEmitter against an ObjBuilder.
+ *
+ * Lexer quirks worked around here:
+ * - `#` is the immediate marker in asm but TOK_PP_HASH in the C lexer.
+ * `#` at BOL is a cpp linemarker → skip to next newline; elsewhere
+ * the per-arch parser treats it as the immediate prefix.
+ * - composite mnemonics (`b.eq`, `b.ne`, ...) arrive as IDENT '.' IDENT
+ * and are reassembled before dispatch.
+ * - `.text` etc. arrive as PUNCT('.') + IDENT and are stitched here.
+ *
+ * Symbol bookkeeping: a Sym→ObjSymId map records the symbols introduced
+ * by labels, `.globl`, and operand references so a forward reference
+ * (`b foo` before `foo:`) shares one symbol with its later definition.
+ * A second Sym→AsmEqu map carries `.set`/`.equ` constants. */
+
+#include "parse/parse.h"
+
+#include <stdarg.h>
+#include <string.h>
+
+#include "arch/aa64_asm.h"
+#include "arch/arch.h"
+#include "core/arena.h"
+#include "core/hashmap.h"
+#include "core/heap.h"
+#include "core/pool.h"
+#include "lex/lex.h"
+#include "obj/obj.h"
+#include "parse/parse_asm_helpers.h"
+
+HASHMAP_DEFINE(SymSecMap, Sym, ObjSecId, hash_u32);
+HASHMAP_DEFINE(SymSymMap, Sym, ObjSymId, hash_u32);
+
+typedef struct AsmEqu {
+ i64 value;
+ ObjSymId sym; /* nonzero when value is `sym + offset` */
+ u8 has_sym;
+ u8 pad[3];
+} AsmEqu;
+HASHMAP_DEFINE(SymEquMap, Sym, AsmEqu, hash_u32);
+
+struct AsmDriver {
+ Compiler* c;
+ Lexer* lex;
+ MCEmitter* mc;
+ ObjBuilder* ob;
+ Pool* pool;
+ Heap* heap;
+
+ Tok cur;
+ int has_cur;
+
+ /* OBJ_SEC_NONE until first emit / explicit `.text` etc. */
+ ObjSecId cur_sec;
+
+ SymSecMap sec_map;
+ SymSymMap sym_map;
+ SymEquMap equ_map;
+
+ Sym n_text, n_data, n_rodata, n_bss;
+
+ /* Per-arch handle. Phase-3 ships aa64 only; phase-5 adds dispatch. */
+ AA64Asm* aa64;
+};
+
+/* ---- token plumbing ---- */
+
+static Tok d_peek(AsmDriver* d) {
+ if (!d->has_cur) {
+ d->cur = lex_next(d->lex);
+ d->has_cur = 1;
+ }
+ return d->cur;
+}
+
+static Tok d_next(AsmDriver* d) {
+ Tok t = d_peek(d);
+ d->has_cur = 0;
+ return t;
+}
+
+static int d_is_eol(AsmDriver* d) {
+ Tok t = d_peek(d);
+ return t.kind == TOK_NEWLINE || t.kind == TOK_EOF;
+}
+
+static void d_skip_to_eol(AsmDriver* d) {
+ while (!d_is_eol(d)) (void)d_next(d);
+}
+
+static void d_eat_eol(AsmDriver* d) {
+ Tok t = d_peek(d);
+ if (t.kind == TOK_NEWLINE) (void)d_next(d);
+}
+
+static SrcLoc d_loc(AsmDriver* d) {
+ if (d->has_cur) return d->cur.loc;
+ return lex_loc(d->lex);
+}
+
+_Noreturn static void d_panicf(AsmDriver* d, const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ compiler_panicv(d->c, d_loc(d), fmt, ap);
+ /* unreachable; va_end omitted because compiler_panicv is _Noreturn */
+}
+
+/* ---- spelling helpers ---- */
+
+static const char* asm_str(AsmDriver* d, Sym s, size_t* nout) {
+ return pool_str(d->pool, s, nout);
+}
+
+static int sym_eq(AsmDriver* d, Sym s, const char* lit) {
+ size_t n = 0;
+ const char* p = asm_str(d, s, &n);
+ size_t i;
+ if (!p) return 0;
+ for (i = 0; i < n; ++i) {
+ if (!lit[i] || p[i] != lit[i]) return 0;
+ }
+ return lit[n] == '\0';
+}
+
+static int starts_with(AsmDriver* d, Sym s, const char* prefix) {
+ size_t n = 0;
+ const char* p = asm_str(d, s, &n);
+ size_t i;
+ if (!p) return 0;
+ for (i = 0; prefix[i]; ++i) {
+ if (i >= n || p[i] != prefix[i]) return 0;
+ }
+ return 1;
+}
+
+/* ---- section management ---- */
+
+static ObjSecId ensure_section(AsmDriver* d, Sym name, SecKind kind,
+ u16 flags, u32 align) {
+ ObjSecId* hit = SymSecMap_get(&d->sec_map, name);
+ if (hit) return *hit;
+ ObjSecId id = obj_section(d->ob, name, kind, flags, align);
+ SymSecMap_set(&d->sec_map, name, id);
+ return id;
+}
+
+static void set_section(AsmDriver* d, Sym name, SecKind kind, u16 flags,
+ u32 align) {
+ ObjSecId id = ensure_section(d, name, kind, flags, align);
+ d->cur_sec = id;
+ d->mc->set_section(d->mc, id);
+}
+
+/* ---- symbol management ---- */
+
+static ObjSymId intern_sym(AsmDriver* d, Sym name) {
+ ObjSymId* hit = SymSymMap_get(&d->sym_map, name);
+ if (hit) return *hit;
+ ObjSymId id = obj_symbol_ex(d->ob, name, SB_LOCAL, SV_DEFAULT, SK_NOTYPE,
+ OBJ_SEC_NONE, 0, 0, 0);
+ SymSymMap_set(&d->sym_map, name, id);
+ return id;
+}
+
+static ObjSym* sym_mut(AsmDriver* d, ObjSymId id) {
+ /* obj.h gives us a const view via obj_symbol_get; the underlying
+ * record lives in the builder's arena and is safe to mutate
+ * pre-finalize. Wrapping the cast keeps the const-stripping in
+ * one place. */
+ return (ObjSym*)obj_symbol_get(d->ob, id);
+}
+
+/* ---- expression evaluator (constants + sym ± const) ---- */
+
+typedef struct AsmExpr {
+ ObjSymId sym;
+ i64 value;
+} AsmExpr;
+
+static AsmExpr expr_c(i64 v) { AsmExpr e = {OBJ_SYM_NONE, v}; return e; }
+static AsmExpr expr_s(ObjSymId s, i64 v) { AsmExpr e = {s, v}; return e; }
+
+static int tok_is_punct(Tok t, u32 p) {
+ return t.kind == TOK_PUNCT && t.v.punct == p;
+}
+
+static i64 lit_to_i64(AsmDriver* d, Sym spelling) {
+ size_t n = 0;
+ const char* p = asm_str(d, spelling, &n);
+ u64 v = 0;
+ int base = 10;
+ size_t i = 0;
+ if (!p || !n) return 0;
+ if (n >= 2 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) {
+ base = 16; i = 2;
+ } else if (n >= 2 && p[0] == '0' && (p[1] == 'b' || p[1] == 'B')) {
+ base = 2; i = 2;
+ } else if (n >= 1 && p[0] == '0') {
+ base = 8; i = 1;
+ }
+ for (; i < n; ++i) {
+ char c = p[i];
+ u32 dv;
+ if (c == 'u' || c == 'U' || c == 'l' || c == 'L') break;
+ if (c >= '0' && c <= '9') dv = (u32)(c - '0');
+ else if (c >= 'a' && c <= 'f') dv = 10 + (u32)(c - 'a');
+ else if (c >= 'A' && c <= 'F') dv = 10 + (u32)(c - 'A');
+ else d_panicf(d, "asm: bad digit in integer literal");
+ if (dv >= (u32)base) d_panicf(d, "asm: digit out of base");
+ v = v * (u64)base + dv;
+ }
+ return (i64)v;
+}
+
+static AsmExpr parse_expr(AsmDriver*);
+static AsmExpr parse_unary(AsmDriver*);
+
+static AsmExpr parse_primary(AsmDriver* d) {
+ Tok t = d_peek(d);
+ if (t.kind == TOK_NUM) {
+ (void)d_next(d);
+ return expr_c(lit_to_i64(d, t.spelling));
+ }
+ if (t.kind == TOK_IDENT) {
+ (void)d_next(d);
+ AsmEqu* eq = SymEquMap_get(&d->equ_map, t.v.ident);
+ if (eq) {
+ if (eq->has_sym) return expr_s(eq->sym, eq->value);
+ return expr_c(eq->value);
+ }
+ return expr_s(intern_sym(d, t.v.ident), 0);
+ }
+ if (tok_is_punct(t, '(')) {
+ (void)d_next(d);
+ AsmExpr e = parse_expr(d);
+ Tok cl = d_peek(d);
+ if (!tok_is_punct(cl, ')')) d_panicf(d, "asm: expected ')'");
+ (void)d_next(d);
+ return e;
+ }
+ d_panicf(d, "asm: expected expression");
+}
+
+static AsmExpr parse_unary(AsmDriver* d) {
+ Tok t = d_peek(d);
+ if (tok_is_punct(t, '-')) {
+ (void)d_next(d);
+ AsmExpr e = parse_unary(d);
+ if (e.sym) d_panicf(d, "asm: unary '-' on symbol");
+ return expr_c(-e.value);
+ }
+ if (tok_is_punct(t, '+')) {
+ (void)d_next(d);
+ return parse_unary(d);
+ }
+ if (tok_is_punct(t, '~')) {
+ (void)d_next(d);
+ AsmExpr e = parse_unary(d);
+ if (e.sym) d_panicf(d, "asm: unary '~' on symbol");
+ return expr_c(~e.value);
+ }
+ return parse_primary(d);
+}
+
+static AsmExpr parse_mul(AsmDriver* d) {
+ AsmExpr a = parse_unary(d);
+ for (;;) {
+ Tok t = d_peek(d);
+ if (!tok_is_punct(t, '*') && !tok_is_punct(t, '/') &&
+ !tok_is_punct(t, '%')) return a;
+ u32 op = t.v.punct;
+ (void)d_next(d);
+ AsmExpr b = parse_unary(d);
+ if (a.sym || b.sym) d_panicf(d, "asm: '*/%%' on symbolic operand");
+ if (op == '*') a.value *= b.value;
+ else if (op == '/') {
+ if (!b.value) d_panicf(d, "asm: division by zero");
+ a.value /= b.value;
+ } else {
+ if (!b.value) d_panicf(d, "asm: modulo by zero");
+ a.value %= b.value;
+ }
+ }
+}
+
+static AsmExpr parse_add(AsmDriver* d) {
+ AsmExpr a = parse_mul(d);
+ for (;;) {
+ Tok t = d_peek(d);
+ if (!tok_is_punct(t, '+') && !tok_is_punct(t, '-')) return a;
+ u32 op = t.v.punct;
+ (void)d_next(d);
+ AsmExpr b = parse_mul(d);
+ if (op == '+') {
+ if (a.sym && b.sym) d_panicf(d, "asm: cannot add two symbols");
+ if (b.sym) { a.sym = b.sym; a.value += b.value; }
+ else a.value += b.value;
+ } else {
+ if (b.sym) d_panicf(d, "asm: cannot subtract symbol from constant");
+ a.value -= b.value;
+ }
+ }
+}
+
+static AsmExpr parse_shift(AsmDriver* d) {
+ AsmExpr a = parse_add(d);
+ for (;;) {
+ Tok t = d_peek(d);
+ if (!tok_is_punct(t, P_SHL) && !tok_is_punct(t, P_SHR)) return a;
+ u32 op = t.v.punct;
+ (void)d_next(d);
+ AsmExpr b = parse_add(d);
+ if (a.sym || b.sym) d_panicf(d, "asm: shift on symbolic operand");
+ if (op == P_SHL) a.value = (i64)((u64)a.value << (b.value & 63));
+ else a.value = a.value >> (b.value & 63);
+ }
+}
+
+static AsmExpr parse_band(AsmDriver* d) {
+ AsmExpr a = parse_shift(d);
+ for (;;) {
+ Tok t = d_peek(d);
+ if (!tok_is_punct(t, '&')) return a;
+ (void)d_next(d);
+ AsmExpr b = parse_shift(d);
+ if (a.sym || b.sym) d_panicf(d, "asm: '&' on symbolic operand");
+ a.value &= b.value;
+ }
+}
+
+static AsmExpr parse_bxor(AsmDriver* d) {
+ AsmExpr a = parse_band(d);
+ for (;;) {
+ Tok t = d_peek(d);
+ if (!tok_is_punct(t, '^')) return a;
+ (void)d_next(d);
+ AsmExpr b = parse_band(d);
+ if (a.sym || b.sym) d_panicf(d, "asm: '^' on symbolic operand");
+ a.value ^= b.value;
+ }
+}
+
+static AsmExpr parse_bor(AsmDriver* d) {
+ AsmExpr a = parse_bxor(d);
+ for (;;) {
+ Tok t = d_peek(d);
+ if (!tok_is_punct(t, '|')) return a;
+ (void)d_next(d);
+ AsmExpr b = parse_bxor(d);
+ if (a.sym || b.sym) d_panicf(d, "asm: '|' on symbolic operand");
+ a.value |= b.value;
+ }
+}
+
+static AsmExpr parse_expr(AsmDriver* d) { return parse_bor(d); }
+
+/* ---- public helpers exposed to per-arch parser ---- */
+
+Tok asm_driver_peek(AsmDriver* d) { return d_peek(d); }
+Tok asm_driver_next(AsmDriver* d) { return d_next(d); }
+int asm_driver_at_eol(AsmDriver* d) { return d_is_eol(d); }
+SrcLoc asm_driver_loc(AsmDriver* d) { return d_loc(d); }
+MCEmitter* asm_driver_mc(AsmDriver* d) { return d->mc; }
+ObjBuilder* asm_driver_ob(AsmDriver* d) { return d->ob; }
+Compiler* asm_driver_compiler(AsmDriver* d) { return d->c; }
+Pool* asm_driver_pool(AsmDriver* d) { return d->pool; }
+
+_Noreturn void asm_driver_panic(AsmDriver* d, const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ compiler_panicv(d->c, d_loc(d), fmt, ap);
+}
+
+ObjSymId asm_driver_intern_sym(AsmDriver* d, Sym name) {
+ return intern_sym(d, name);
+}
+
+ObjSecId asm_driver_cur_section(AsmDriver* d) {
+ if (d->cur_sec == OBJ_SEC_NONE) {
+ if (!d->n_text) d->n_text = pool_intern_cstr(d->pool, ".text");
+ d->cur_sec = ensure_section(d, d->n_text, SEC_TEXT,
+ (u16)(SF_ALLOC | SF_EXEC), 4);
+ d->mc->set_section(d->mc, d->cur_sec);
+ }
+ return d->cur_sec;
+}
+
+int asm_driver_eat_comma(AsmDriver* d) {
+ Tok t = d_peek(d);
+ if (tok_is_punct(t, ',')) {
+ (void)d_next(d);
+ return 1;
+ }
+ return 0;
+}
+
+int asm_driver_eat_punct(AsmDriver* d, u32 p) {
+ Tok t = d_peek(d);
+ if (tok_is_punct(t, p)) {
+ (void)d_next(d);
+ return 1;
+ }
+ /* `#` arrives as TOK_PP_HASH from the C lexer; accept it as the
+ * immediate-prefix punctuator here. */
+ if (p == '#' && t.kind == TOK_PP_HASH) {
+ (void)d_next(d);
+ return 1;
+ }
+ return 0;
+}
+
+void asm_driver_expect_punct(AsmDriver* d, u32 p, const char* what) {
+ if (!asm_driver_eat_punct(d, p))
+ d_panicf(d, "asm: expected '%s' (%s)", "punct", what);
+}
+
+i64 asm_driver_parse_const(AsmDriver* d) {
+ AsmExpr e = parse_expr(d);
+ if (e.sym) d_panicf(d, "asm: constant expression expected");
+ return e.value;
+}
+
+void asm_driver_parse_sym_expr(AsmDriver* d, ObjSymId* sym_out,
+ i64* off_out) {
+ AsmExpr e = parse_expr(d);
+ *sym_out = e.sym;
+ *off_out = e.value;
+}
+
+int asm_driver_tok_is_punct(Tok t, u32 p) {
+ if (tok_is_punct(t, p)) return 1;
+ /* `#` arrives as TOK_PP_HASH from the C lexer. */
+ if (p == '#' && t.kind == TOK_PP_HASH) return 1;
+ return 0;
+}
+
+/* ---- string-literal decoding ---- */
+
+static void decode_string(AsmDriver* d, Sym spelling, u8** out, u32* nout) {
+ size_t n = 0;
+ const char* p = asm_str(d, spelling, &n);
+ /* Skip any encoding prefix (L/u/u8/U). */
+ while (n && (*p == 'L' || *p == 'u' || *p == 'U' || *p == '8')) {
+ ++p;
+ --n;
+ }
+ if (n < 2 || p[0] != '"' || p[n - 1] != '"')
+ d_panicf(d, "asm: malformed string literal");
+ size_t cap = n;
+ u8* buf = (u8*)d->heap->alloc(d->heap, cap ? cap : 1, 1);
+ u32 k = 0;
+ for (size_t i = 1; i + 1 < n; ++i) {
+ char c = p[i];
+ if (c != '\\') {
+ buf[k++] = (u8)c;
+ continue;
+ }
+ ++i;
+ if (i + 1 >= n) break;
+ char e = p[i];
+ switch (e) {
+ case 'n': buf[k++] = '\n'; break;
+ case 't': buf[k++] = '\t'; break;
+ case 'r': buf[k++] = '\r'; break;
+ case '\\': buf[k++] = '\\'; break;
+ case '"': buf[k++] = '"'; break;
+ case '\'': buf[k++] = '\''; break;
+ case '0': buf[k++] = 0; break;
+ case 'b': buf[k++] = 8; break;
+ case 'f': buf[k++] = 12; break;
+ case 'v': buf[k++] = 11; break;
+ case 'a': buf[k++] = 7; break;
+ case 'x': {
+ u32 v = 0;
+ int dn = 0;
+ while (i + 2 < n) {
+ char h = p[i + 1];
+ int dv;
+ if (h >= '0' && h <= '9') dv = h - '0';
+ else if (h >= 'a' && h <= 'f') dv = 10 + (h - 'a');
+ else if (h >= 'A' && h <= 'F') dv = 10 + (h - 'A');
+ else break;
+ v = v * 16 + (u32)dv;
+ ++i;
+ if (++dn >= 2) break;
+ }
+ buf[k++] = (u8)v;
+ break;
+ }
+ default:
+ if (e >= '0' && e <= '7') {
+ u32 v = (u32)(e - '0');
+ int dn = 1;
+ while (dn < 3 && i + 2 < n) {
+ char h = p[i + 1];
+ if (h < '0' || h > '7') break;
+ v = v * 8 + (u32)(h - '0');
+ ++i;
+ ++dn;
+ }
+ buf[k++] = (u8)v;
+ } else {
+ buf[k++] = (u8)e;
+ }
+ break;
+ }
+ }
+ *out = buf;
+ *nout = k;
+}
+
+/* ---- directives ---- */
+
+static Sym expect_ident(AsmDriver* d, const char* what) {
+ Tok t = d_peek(d);
+ if (t.kind != TOK_IDENT) d_panicf(d, "asm: %s: expected identifier", what);
+ (void)d_next(d);
+ return t.v.ident;
+}
+
+static void emit_le(AsmDriver* d, u64 v, u32 width) {
+ u8 buf[8];
+ for (u32 i = 0; i < width; ++i) buf[i] = (u8)(v >> (8 * i));
+ (void)asm_driver_cur_section(d);
+ d->mc->emit_bytes(d->mc, buf, width);
+}
+
+static void emit_int_directive(AsmDriver* d, u32 width) {
+ for (;;) {
+ AsmExpr e = parse_expr(d);
+ if (e.sym) {
+ RelocKind k;
+ if (width == 4) k = R_ABS32;
+ else if (width == 8) k = R_ABS64;
+ else d_panicf(d, "asm: symbolic .byte/.hword not supported");
+ (void)asm_driver_cur_section(d);
+ u32 ofs = d->mc->pos(d->mc);
+ u8 zero[8] = {0};
+ d->mc->emit_bytes(d->mc, zero, width);
+ d->mc->emit_reloc_at(d->mc, d->cur_sec, ofs, k, e.sym, e.value, 1, 0);
+ } else {
+ emit_le(d, (u64)e.value, width);
+ }
+ if (!asm_driver_eat_comma(d)) break;
+ }
+}
+
+static void do_directive(AsmDriver* d, Sym name) {
+ if (sym_eq(d, name, "text")) {
+ if (!d->n_text) d->n_text = pool_intern_cstr(d->pool, ".text");
+ set_section(d, d->n_text, SEC_TEXT, (u16)(SF_ALLOC | SF_EXEC), 4);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "data")) {
+ if (!d->n_data) d->n_data = pool_intern_cstr(d->pool, ".data");
+ set_section(d, d->n_data, SEC_DATA, (u16)(SF_ALLOC | SF_WRITE), 8);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "rodata")) {
+ if (!d->n_rodata) d->n_rodata = pool_intern_cstr(d->pool, ".rodata");
+ set_section(d, d->n_rodata, SEC_RODATA, (u16)SF_ALLOC, 8);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "bss")) {
+ if (!d->n_bss) d->n_bss = pool_intern_cstr(d->pool, ".bss");
+ set_section(d, d->n_bss, SEC_BSS, (u16)(SF_ALLOC | SF_WRITE), 8);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "section")) {
+ Sym sname = 0;
+ Tok t = d_peek(d);
+ if (t.kind == TOK_IDENT) {
+ sname = t.v.ident;
+ (void)d_next(d);
+ } else if (t.kind == TOK_STR) {
+ size_t n = 0;
+ const char* p = asm_str(d, t.spelling, &n);
+ if (n >= 2 && p[0] == '"') sname = pool_intern(d->pool, p + 1, n - 2);
+ (void)d_next(d);
+ } else if (tok_is_punct(t, '.')) {
+ (void)d_next(d);
+ Tok id = d_next(d);
+ if (id.kind != TOK_IDENT) d_panicf(d, "asm: .section: bad name");
+ size_t ni = 0;
+ const char* nm = asm_str(d, id.v.ident, &ni);
+ char buf[128];
+ if (ni + 1 >= sizeof buf) d_panicf(d, "asm: .section: name too long");
+ buf[0] = '.';
+ for (size_t i = 0; i < ni; ++i) buf[i + 1] = nm[i];
+ sname = pool_intern(d->pool, buf, ni + 1);
+ } else {
+ d_panicf(d, "asm: .section: expected name");
+ }
+ SecKind kind = SEC_OTHER;
+ u16 flags = 0;
+ {
+ size_t nn = 0;
+ const char* p = asm_str(d, sname, &nn);
+ if (p) {
+ if (nn >= 5 && memcmp(p, ".text", 5) == 0) {
+ kind = SEC_TEXT;
+ flags = (u16)(SF_ALLOC | SF_EXEC);
+ } else if (nn >= 7 && memcmp(p, ".rodata", 7) == 0) {
+ kind = SEC_RODATA;
+ flags = (u16)SF_ALLOC;
+ } else if (nn >= 5 && memcmp(p, ".data", 5) == 0) {
+ kind = SEC_DATA;
+ flags = (u16)(SF_ALLOC | SF_WRITE);
+ } else if (nn >= 4 && memcmp(p, ".bss", 4) == 0) {
+ kind = SEC_BSS;
+ flags = (u16)(SF_ALLOC | SF_WRITE);
+ }
+ }
+ }
+ /* Skip optional remainder: flags string, type tag, etc. */
+ d_skip_to_eol(d);
+ set_section(d, sname, kind, flags, 1);
+ return;
+ }
+ if (sym_eq(d, name, "globl") || sym_eq(d, name, "global")) {
+ Sym n = expect_ident(d, ".globl");
+ sym_mut(d, intern_sym(d, n))->bind = (u16)SB_GLOBAL;
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "local")) {
+ Sym n = expect_ident(d, ".local");
+ sym_mut(d, intern_sym(d, n))->bind = (u16)SB_LOCAL;
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "weak")) {
+ Sym n = expect_ident(d, ".weak");
+ sym_mut(d, intern_sym(d, n))->bind = (u16)SB_WEAK;
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "hidden")) {
+ Sym n = expect_ident(d, ".hidden");
+ sym_mut(d, intern_sym(d, n))->vis = (u8)SV_HIDDEN;
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "protected")) {
+ Sym n = expect_ident(d, ".protected");
+ sym_mut(d, intern_sym(d, n))->vis = (u8)SV_PROTECTED;
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "internal")) {
+ Sym n = expect_ident(d, ".internal");
+ sym_mut(d, intern_sym(d, n))->vis = (u8)SV_INTERNAL;
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "type")) {
+ Sym n = expect_ident(d, ".type");
+ ObjSymId id = intern_sym(d, n);
+ if (!asm_driver_eat_comma(d)) d_panicf(d, "asm: .type: expected ','");
+ Tok t = d_next(d);
+ Sym tag = 0;
+ if (tok_is_punct(t, '@') || tok_is_punct(t, '%')) {
+ Tok ti = d_next(d);
+ if (ti.kind != TOK_IDENT) d_panicf(d, "asm: .type: tag");
+ tag = ti.v.ident;
+ } else if (t.kind == TOK_IDENT) {
+ tag = t.v.ident;
+ } else if (t.kind == TOK_STR) {
+ size_t sn = 0;
+ const char* sp = asm_str(d, t.spelling, &sn);
+ if (sn >= 2 && sp[0] == '"' && sp[sn - 1] == '"')
+ tag = pool_intern(d->pool, sp + 1, sn - 2);
+ } else {
+ d_panicf(d, "asm: .type: tag");
+ }
+ if (tag && sym_eq(d, tag, "function"))
+ sym_mut(d, id)->kind = (u16)SK_FUNC;
+ else if (tag && sym_eq(d, tag, "object"))
+ sym_mut(d, id)->kind = (u16)SK_OBJ;
+ else if (tag && sym_eq(d, tag, "tls_object"))
+ sym_mut(d, id)->kind = (u16)SK_TLS;
+ else if (tag && sym_eq(d, tag, "gnu_indirect_function"))
+ sym_mut(d, id)->kind = (u16)SK_IFUNC;
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "size")) {
+ Sym n = expect_ident(d, ".size");
+ ObjSymId id = intern_sym(d, n);
+ if (!asm_driver_eat_comma(d)) d_panicf(d, "asm: .size: expected ','");
+ /* Recognize `. - NAME`. */
+ Tok t = d_peek(d);
+ i64 sz = 0;
+ if (tok_is_punct(t, '.')) {
+ (void)d_next(d);
+ if (tok_is_punct(d_peek(d), '-')) {
+ (void)d_next(d);
+ Tok rid = d_peek(d);
+ if (rid.kind == TOK_IDENT && rid.v.ident == n) {
+ (void)d_next(d);
+ const ObjSym* os = obj_symbol_get(d->ob, id);
+ if (os && os->section_id == d->cur_sec)
+ sz = (i64)d->mc->pos(d->mc) - (i64)os->value;
+ }
+ }
+ } else {
+ AsmExpr e = parse_expr(d);
+ if (!e.sym) sz = e.value;
+ }
+ if (sz < 0) sz = 0;
+ sym_mut(d, id)->size = (u64)sz;
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "byte")) {
+ emit_int_directive(d, 1);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "hword") || sym_eq(d, name, "short") ||
+ sym_eq(d, name, "2byte")) {
+ emit_int_directive(d, 2);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "word") || sym_eq(d, name, "long") ||
+ sym_eq(d, name, "int") || sym_eq(d, name, "4byte")) {
+ emit_int_directive(d, 4);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "quad") || sym_eq(d, name, "8byte") ||
+ sym_eq(d, name, "dword") || sym_eq(d, name, "xword")) {
+ emit_int_directive(d, 8);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "ascii") || sym_eq(d, name, "asciz") ||
+ sym_eq(d, name, "string")) {
+ int term = !sym_eq(d, name, "ascii");
+ for (;;) {
+ Tok t = d_peek(d);
+ if (t.kind != TOK_STR)
+ d_panicf(d, "asm: .ascii/.string: expected string");
+ (void)d_next(d);
+ u8* buf = NULL;
+ u32 n = 0;
+ decode_string(d, t.spelling, &buf, &n);
+ (void)asm_driver_cur_section(d);
+ d->mc->emit_bytes(d->mc, buf, n);
+ if (term) emit_le(d, 0, 1);
+ d->heap->free(d->heap, buf, n);
+ if (!asm_driver_eat_comma(d)) break;
+ }
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "zero") || sym_eq(d, name, "skip") ||
+ sym_eq(d, name, "space")) {
+ i64 n = asm_driver_parse_const(d);
+ i64 fill = 0;
+ if (asm_driver_eat_comma(d)) fill = asm_driver_parse_const(d);
+ if (n > 0) {
+ (void)asm_driver_cur_section(d);
+ d->mc->emit_fill(d->mc, (size_t)n, (u8)fill);
+ }
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "fill")) {
+ i64 n = asm_driver_parse_const(d);
+ i64 size = 1, val = 0;
+ if (asm_driver_eat_comma(d)) size = asm_driver_parse_const(d);
+ if (asm_driver_eat_comma(d)) val = asm_driver_parse_const(d);
+ if (size < 1 || size > 8) d_panicf(d, "asm: .fill: size out of range");
+ (void)asm_driver_cur_section(d);
+ for (i64 i = 0; i < n; ++i) emit_le(d, (u64)val, (u32)size);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "align") || sym_eq(d, name, "balign")) {
+ i64 a = asm_driver_parse_const(d);
+ i64 fill = 0;
+ if (asm_driver_eat_comma(d)) fill = asm_driver_parse_const(d);
+ if (a <= 0 || (a & (a - 1))) d_panicf(d, "asm: .align: not a power of 2");
+ (void)asm_driver_cur_section(d);
+ d->mc->emit_align(d->mc, (u32)a, (u8)fill);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "p2align")) {
+ i64 lg = asm_driver_parse_const(d);
+ i64 fill = 0;
+ if (asm_driver_eat_comma(d)) fill = asm_driver_parse_const(d);
+ if (lg < 0 || lg > 16) d_panicf(d, "asm: .p2align: out of range");
+ (void)asm_driver_cur_section(d);
+ d->mc->emit_align(d->mc, 1u << (u32)lg, (u8)fill);
+ d_skip_to_eol(d);
+ return;
+ }
+ if (sym_eq(d, name, "set") || sym_eq(d, name, "equ")) {
+ Sym n = expect_ident(d, ".set");
+ if (!asm_driver_eat_comma(d)) d_panicf(d, "asm: .set: expected ','");
+ AsmExpr e = parse_expr(d);
+ AsmEqu eq;
+ eq.value = e.value;
+ eq.sym = e.sym;
+ eq.has_sym = e.sym ? 1 : 0;
+ eq.pad[0] = eq.pad[1] = eq.pad[2] = 0;
+ SymEquMap_set(&d->equ_map, n, eq);
+ d_skip_to_eol(d);
+ return;
+ }
+
+ /* CFI block + accepted-but-ignored directives. Keep parser
+ * forward-progress without aborting the whole TU. */
+ if (starts_with(d, name, "cfi_") ||
+ sym_eq(d, name, "file") || sym_eq(d, name, "loc") ||
+ sym_eq(d, name, "ident") || sym_eq(d, name, "popsection") ||
+ sym_eq(d, name, "pushsection") || sym_eq(d, name, "previous") ||
+ sym_eq(d, name, "subsections_via_symbols") ||
+ sym_eq(d, name, "comm") || sym_eq(d, name, "lcomm") ||
+ sym_eq(d, name, "uleb128") || sym_eq(d, name, "sleb128") ||
+ sym_eq(d, name, "macro") || sym_eq(d, name, "endm") ||
+ sym_eq(d, name, "if") || sym_eq(d, name, "endif") ||
+ sym_eq(d, name, "else") || sym_eq(d, name, "include")) {
+ d_skip_to_eol(d);
+ return;
+ }
+
+ /* Unknown directive — recover. */
+ d_skip_to_eol(d);
+}
+
+/* ---- driver loop ---- */
+
+static void process_label(AsmDriver* d, Sym name) {
+ ObjSymId id = intern_sym(d, name);
+ (void)asm_driver_cur_section(d);
+ const ObjSym* os = obj_symbol_get(d->ob, id);
+ if (os && os->section_id != OBJ_SEC_NONE)
+ d_panicf(d, "asm: symbol defined twice");
+ obj_symbol_define(d->ob, id, d->cur_sec, (u64)d->mc->pos(d->mc), 0);
+ /* Promote SK_UNDEF (forward ref via reloc) to SK_NOTYPE so it's a
+ * real defined symbol; explicit `.type SYM, @function` will refine. */
+ if (os && os->kind == SK_UNDEF) sym_mut(d, id)->kind = (u16)SK_NOTYPE;
+}
+
+static Sym maybe_compose_mnemonic(AsmDriver* d, Sym head) {
+ Tok t = d_peek(d);
+ if (!tok_is_punct(t, '.')) return head;
+ if (t.flags & TF_HAS_SPACE) return head;
+ (void)d_next(d);
+ Tok rest = d_next(d);
+ if (rest.kind != TOK_IDENT)
+ d_panicf(d, "asm: composite mnemonic: expected ident");
+ size_t hn = 0, rn = 0;
+ const char* hp = asm_str(d, head, &hn);
+ const char* rp = asm_str(d, rest.v.ident, &rn);
+ size_t n = hn + 1 + rn;
+ if (n >= 64) d_panicf(d, "asm: mnemonic too long");
+ char buf[64];
+ for (size_t i = 0; i < hn; ++i) buf[i] = hp[i];
+ buf[hn] = '.';
+ for (size_t i = 0; i < rn; ++i) buf[hn + 1 + i] = rp[i];
+ return pool_intern(d->pool, buf, n);
+}
+
+void parse_asm(Compiler* c, Lexer* l, MCEmitter* mc) {
+ AsmDriver d;
+ memset(&d, 0, sizeof d);
+ d.c = c;
+ d.lex = l;
+ d.mc = mc;
+ d.ob = mc->obj;
+ d.pool = c->global;
+ d.heap = (Heap*)c->env->heap;
+ d.cur_sec = OBJ_SEC_NONE;
+ SymSecMap_init(&d.sec_map, d.heap);
+ SymSymMap_init(&d.sym_map, d.heap);
+ SymEquMap_init(&d.equ_map, d.heap);
+ d.aa64 = aa64_asm_open(c);
+
+ for (;;) {
+ Tok t = d_peek(&d);
+ if (t.kind == TOK_EOF) break;
+ if (t.kind == TOK_NEWLINE) {
+ (void)d_next(&d);
+ continue;
+ }
+ if (t.kind == TOK_PP_HASH) {
+ /* cpp-style linemarker; skip the whole line. */
+ d_skip_to_eol(&d);
+ continue;
+ }
+ if (tok_is_punct(t, '.')) {
+ (void)d_next(&d);
+ Tok id = d_next(&d);
+ if (id.kind != TOK_IDENT)
+ d_panicf(&d, "asm: expected directive name after '.'");
+ do_directive(&d, id.v.ident);
+ d_eat_eol(&d);
+ continue;
+ }
+ if (t.kind == TOK_IDENT) {
+ Sym head = t.v.ident;
+ (void)d_next(&d);
+ Tok nxt = d_peek(&d);
+ if (tok_is_punct(nxt, ':')) {
+ (void)d_next(&d);
+ process_label(&d, head);
+ continue;
+ }
+ Sym mnemonic = maybe_compose_mnemonic(&d, head);
+ aa64_asm_insn(d.aa64, &d, mnemonic);
+ d_skip_to_eol(&d);
+ continue;
+ }
+ /* Anything else: recover by skipping the line. */
+ d_skip_to_eol(&d);
+ }
+
+ aa64_asm_close(d.aa64);
+ SymSecMap_fini(&d.sec_map);
+ SymSymMap_fini(&d.sym_map);
+ SymEquMap_fini(&d.equ_map);
+}
diff --git a/src/parse/parse_asm_helpers.h b/src/parse/parse_asm_helpers.h
@@ -0,0 +1,48 @@
+#ifndef CFREE_PARSE_ASM_HELPERS_H
+#define CFREE_PARSE_ASM_HELPERS_H
+
+/* Lightweight asm-driver surface consumed by per-arch instruction
+ * parsers. The driver itself is opaque to per-arch code; these helpers
+ * are the only seam. Implementations live in src/parse/parse_asm.c. */
+
+#include "arch/arch.h"
+#include "core/core.h"
+#include "lex/lex.h"
+#include "obj/obj.h"
+
+typedef struct AsmDriver AsmDriver;
+
+/* ---- token plumbing ---- */
+Tok asm_driver_peek(AsmDriver*);
+Tok asm_driver_next(AsmDriver*);
+int asm_driver_at_eol(AsmDriver*);
+int asm_driver_tok_is_punct(Tok t, u32 p);
+int asm_driver_eat_comma(AsmDriver*);
+int asm_driver_eat_punct(AsmDriver*, u32 punct);
+void asm_driver_expect_punct(AsmDriver*, u32 punct, const char* what);
+
+/* Source position for diagnostics. */
+SrcLoc asm_driver_loc(AsmDriver*);
+
+/* Owning subsystems. */
+MCEmitter* asm_driver_mc(AsmDriver*);
+ObjBuilder* asm_driver_ob(AsmDriver*);
+Compiler* asm_driver_compiler(AsmDriver*);
+Pool* asm_driver_pool(AsmDriver*);
+ObjSecId asm_driver_cur_section(AsmDriver*);
+
+/* Diagnostics: emits then longjmps via Compiler.panic. No return. */
+_Noreturn void asm_driver_panic(AsmDriver*, const char* fmt, ...);
+
+/* ---- symbol + expression parsing ---- */
+ObjSymId asm_driver_intern_sym(AsmDriver*, Sym name);
+
+/* Parse a constant integer expression. Panics if the expression
+ * references a symbol. */
+i64 asm_driver_parse_const(AsmDriver*);
+
+/* Parse a `sym ± const` expression. Both outputs valid: pure constants
+ * leave *sym_out == OBJ_SYM_NONE. */
+void asm_driver_parse_sym_expr(AsmDriver*, ObjSymId* sym_out, i64* off_out);
+
+#endif
diff --git a/test/asm/decode/nop_ret.skip b/test/asm/decode/nop_ret.skip
@@ -1 +0,0 @@
-phase 1: cfree_disasm_iter_* is still a stub (src/api/stubs.c)
diff --git a/test/asm/encode/exit_zero.skip b/test/asm/encode/exit_zero.skip
@@ -1 +0,0 @@
-phase 1: parse_asm is still a stub (src/api/stubs.c)
diff --git a/test/asm/harness/asm_runner.c b/test/asm/harness/asm_runner.c
@@ -488,10 +488,11 @@ static int mode_decode(const char* in_path, const char* out_path) {
}
while (cfree_disasm_iter_next(it, &ins)) {
- fprintf(out, "%llx:\t%s\t%s",
- (unsigned long long)ins.vaddr,
- ins.mnemonic ? ins.mnemonic : "",
- ins.operands ? ins.operands : "");
+ fprintf(out, "%llx:\t%s", (unsigned long long)ins.vaddr,
+ ins.mnemonic ? ins.mnemonic : "");
+ if (ins.operands && ins.operands[0]) {
+ fprintf(out, "\t%s", ins.operands);
+ }
if (ins.annotation && ins.annotation[0]) {
fprintf(out, " ; %s", ins.annotation);
}
diff --git a/test/asm/listing/nop_ret.skip b/test/asm/listing/nop_ret.skip
@@ -1 +0,0 @@
-phase 1: cfree_obj_disasm is still a stub (src/api/stubs.c)