kit

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

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:
Mdoc/ASM.md | 299+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Asrc/api/arch_regs.c | 36++++++++++++++++++++++++++++++++++++
Asrc/api/disasm.c | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/api/stubs.c | 60+++++++-----------------------------------------------------
Asrc/arch/aa64_asm.c | 861+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/aa64_asm.h | 35+++++++++++++++++++++++++++++++++++
Asrc/arch/aa64_disasm.c | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/aa64_disasm.h | 14++++++++++++++
Asrc/arch/aa64_regs.c | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/aa64_regs.h | 12++++++++++++
Msrc/arch/arch.h | 5+++++
Asrc/arch/disasm.c | 32++++++++++++++++++++++++++++++++
Msrc/obj/obj.c | 2++
Msrc/obj/obj.h | 5+++++
Asrc/parse/parse_asm.c | 935+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/parse/parse_asm_helpers.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/asm/decode/nop_ret.skip | 1-
Dtest/asm/encode/exit_zero.skip | 1-
Mtest/asm/harness/asm_runner.c | 9+++++----
Dtest/asm/listing/nop_ret.skip | 1-
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)