kit

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

commit da095c5232e37ba266029d2bbf5b5a0174e47c29
parent d56632d51f4d3fd7553ac1de5c04d7a0c0fc35ba
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 09:58:04 -0700

test/cg: scaffold codegen test harness and minimal AArch64 backend

Lays the groundwork for testing the cg / CGTarget / MCEmitter stack
independent of the C parser. Test fixtures drive CGTarget directly to
build `int test_main(void)`, which the runner exercises through four
paths (D direct-JIT, R ELF roundtrip, E exec, J jit-via-file) reusing
the existing test/link harness binaries.

Implements a generic byte-level MCEmitter (src/arch/mc.c), a minimal
AArch64 CGTarget covering func lifecycle, alloc_reg, load_imm, copy,
binop, unop, and ret (src/arch/aarch64.c), and 9 spine cases covering
function return and integer arithmetic. Other CGTarget methods are
panic-stubs so unsupported operations fail visibly.

R path passes for all 9 cases (emitted ELF round-trips cleanly through
read_elf). D/E/J paths currently fail; root cause is the pre-existing
cfree_jit_lookup regression noted in docs/linker-status.md, not the
new codegen output (which inspects correctly via llvm-readelf).

Strategy and coverage corpus are documented in doc/cg_testing.md.

Diffstat:
Adoc/cg_testing.md | 308+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/api/stubs.c | 10+++-------
Asrc/arch/aarch64.c | 413+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/mc.c | 297+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/core/buf.c | 21+++++++++++++++++++++
Msrc/core/buf.h | 1+
Atest/cg/CORPUS.md | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/cg/harness/cases.c | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/cg/harness/cg_runner.c | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/cg/harness/cg_test.c | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/cg/harness/cg_test.h | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/cg/run.sh | 275+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 10++++++++--
13 files changed, 2107 insertions(+), 9 deletions(-)

diff --git a/doc/cg_testing.md b/doc/cg_testing.md @@ -0,0 +1,308 @@ +# cg / CGTarget / MCEmitter test strategy + +How we test the codegen stack — `cg`, `CGTarget`, and `MCEmitter` — *before* +the C parser is ready, and how the suite extends naturally once the parser +arrives. Companion to `DESIGN.md`. Scope: harness shape, layering rationale, +test paths, fixture API, and a coverage corpus. + +## 1. Goal + +Test the meat of the single-pass compiler — recursive-descent parser ↔ `cg` +↔ `CGTarget` ↔ `MCEmitter` ↔ `ObjBuilder` — with the parser stubbed out. +The fixtures play the parser's role: each one drives `cg` (and a small +number drive `CGTarget` or `MCEmitter` directly) to produce a function +named `test_main` that returns an `int`. The harness then runs that +function and checks the return value. + +This decouples codegen development from parser development: we can build +out the AArch64 backend, opt's recording wrapper, MCEmitter encoding, and +CG's value-stack/spill/fusion logic against behavioral oracles, without +needing a working C front-end. + +## 2. Why three layers (and how to test each) + +``` +parser → cg → CGTarget → MCEmitter → ObjBuilder + | | | | + | | | └─ already covered by test/elf, test/link + | | └─ encoding, fixups, relocs, alignment, CFI + | └─ typed lowering vtable: takes resolved Operands + └─ TCC-style value stack: spills, fusion, conversions, frame-residency +``` + +- **`MCEmitter`** is the lowest layer. Encoding-table bugs and reloc/fixup + bugs surface here. Best tested with hand-written byte sequences and a + one-instruction `test_main` (analogue of `test/elf/unit/smoke.c`). +- **`CGTarget`** is typed lowering. It receives `Operand`s that are + already resolved (REG / IMM / LOCAL / GLOBAL / INDIRECT). No value + stack, no implicit conversions. Best tested with focused unit anchors + that exhaustively exercise operand-kind combinations and op enums + (every `BinOp`, every `ConvKind`, every `MemOrder`). +- **`cg`** owns C-shaped behavior: value stack, spill/reload across + pressure, `cmp` → `cmp_branch` fusion, frame-residency-by-default for + locals, implicit MemAccess derivation, address-taken tracking. This is + what the parser will drive, so the **primary** suite drives `cg`. + +The three layers map to three case categories: + +| Category | Drives | Where | Volume | +|---|---|---|---| +| Primary | `cg.h` | `test/cg/cases/` | grows with C language coverage | +| Unit | `CGTarget` | `test/cg/unit/cgt_*` | one per method group, write-once | +| Unit | `MCEmitter` | `test/cg/unit/mc_*` | one per encoding family, write-once | + +When a cg-driven case fails mysteriously, the matching unit anchor tells +you whether the bug is in CGTarget/MCEmitter or in `cg` itself. + +## 3. Test paths per fixture + +Each fixture is run through several paths. This mirrors the R/E/J path +matrix in `test/link/run.sh` and reuses its harness binaries. + +| Path | Pipeline | Validates | Available on | +|---|---|---|---| +| **D** direct-JIT | fixture → `ObjBuilder*` → `link_add_obj` → `cfree_link_jit` → call `test_main` | live ObjBuilder → JIT path; fastest; no file I/O | aarch64 host | +| **R** roundtrip | fixture → `emit_elf` → bytes → `cfree-roundtrip` → `read_elf` → normalized diff | ELF writer/reader fidelity on synthetic input | always (host-arch agnostic) | +| **E** exec | fixture → `emit_elf` → bytes → `link-exe-runner` → exe → qemu/podman → exit code | file linker + reloc application + ELF emission | when qemu or podman available | +| **J** jit-via-file | fixture → `emit_elf` → bytes → `jit-runner` (reads .o) → call `test_main` | full file → JIT pipeline | aarch64 host | +| **O** opt-wrapped | as D and J, but with `opt_cgtarget` between `cg` and target | IPO + lowering preserve behavior | once opt lands | + +Path D is intentionally distinct from J: it catches bugs that the ELF +emitter writes into a `.o` but the reader silently corrects (or +vice-versa). The two paths together force the post-finalize ObjBuilder +shape and the read-back ObjBuilder shape to be behaviorally equivalent. + +The R, E, and J paths reuse the existing harness binaries unchanged +(`cfree-roundtrip`, `link-exe-runner`, `jit-runner`). Only `cg-runner` is +new. + +## 4. Layout + +``` +test/cg/ + CORPUS.md # coverage matrix (mirrors test/elf/CORPUS.md) + run.sh # per-fixture: D, R, E, J (and O once opt lands) + harness/ + cg_runner.c # multi-mode binary + cg_test.h / cg_test.c # fixture API used by every case + cases.c # registry: { name, build_fn, expected, flags } + start.c # symlink/copy of test/link/harness/start.c + cases/ + a01_return_const.c # cg-driven cases (the primary suite) + a02_return_void.c + c01_add_const.c + c02_arith_mix.c + ... + unit/ + mc_smoke.c # MCEmitter-direct + cgt_load_imm.c # CGTarget-direct anchors + cgt_binop_int.c + ... +``` + +`cg-runner` modes: + +``` +cg-runner --list # print every registered case name +cg-runner --emit NAME OUT.o # build the case's ObjBuilder, emit_elf to OUT.o +cg-runner --jit NAME # build, link_add_obj, cfree_link_jit, call test_main +cg-runner --dump NAME # build, emit_elf, run ArchDisasm over .text — no oracle +``` + +The `--dump` mode is for debugging only; it has no expected output and is +not run by default. Snapshot/golden disassembly tests are deferred. + +## 5. Fixture API + +The fixture API hides the boilerplate that every case shares: Compiler +init, ObjBuilder allocation, `cgtarget_new`, `mc_new`, the `CGFuncDesc` ++ `ABIFuncInfo` dance for `test_main`, and the value-stack vs CGABIValue +plumbing for `ret`. + +```c +/* test/cg/harness/cg_test.h */ + +typedef struct CgTestCtx CgTestCtx; +typedef void (*CgCaseFn)(CgTestCtx*); + +typedef struct CgCase { + const char* name; /* "a01_return_const" */ + CgCaseFn build; + int expected; /* test_main's return value; 0 if absent */ + unsigned flags; /* CG_CASE_* */ +} CgCase; + +extern const CgCase cg_cases[]; /* registry, NUL-terminated */ +extern unsigned cg_cases_count; + +/* Common interned types; populated by cg_test_init. */ +extern const Type *T_VOID, *T_I8, *T_I16, *T_I32, *T_I64, *T_U32, *T_U64, + *T_F32, *T_F64, *T_PTR_VOID; + +/* Helpers cases use. */ +CG* cgtest_cg(CgTestCtx*); +CGTarget* cgtest_target(CgTestCtx*); +Compiler* cgtest_compiler(CgTestCtx*); + +/* Begin a function `<ret_ty> test_main(void)`. Allocates the ObjSymId, + * builds CGFuncDesc, queries TargetABI, calls cgtarget->func_begin. */ +typedef struct CgTestFn CgTestFn; +CgTestFn* cgtest_begin_main(CgTestCtx*, const Type* ret_ty); + +/* Begin a named function with parameters; for cases that need helpers. */ +CgTestFn* cgtest_begin_fn(CgTestCtx*, const char* name, + const Type* ret_ty, + const Type* const* param_tys, unsigned nparams); + +/* Return a value sitting on top of cg's value stack; ret_ty must match. */ +void cgtest_ret_value(CgTestFn*); +void cgtest_ret_void (CgTestFn*); + +/* End the function (cgtarget->func_end). */ +void cgtest_end(CgTestFn*); + +/* Operand sugar (used by CGTarget-direct unit cases). */ +Operand IMM (i64 v, const Type*); +Operand REG_OF(Reg r, const Type*); +Operand LOCAL_(FrameSlot, const Type*); +Operand GLOBAL_(ObjSymId, i64 addend, const Type*); +Operand IND (Reg base, i32 ofs, const Type*); +``` + +A primary cg-driven case is then ~10 lines: + +```c +/* test/cg/cases/c01_add_const.c — int test_main(void) { return 1 + 2; } */ +#include "cg_test.h" + +static void build(CgTestCtx* ctx) { + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + CG* g = cgtest_cg(ctx); + cg_push_int(g, 1, T_I32); + cg_push_int(g, 2, T_I32); + cg_binop(g, BO_IADD); + cgtest_ret_value(tf); + cgtest_end(tf); +} + +CG_CASE_REGISTER(c01_add_const, build, /*expected=*/3); +``` + +A CGTarget-direct unit anchor: + +```c +/* test/cg/unit/cgt_load_imm.c */ +#include "cg_test.h" + +static void build(CgTestCtx* ctx) { + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + CGTarget* a = cgtest_target(ctx); + Reg r = a->alloc_reg(a, RC_INT, T_I32); + a->load_imm(a, REG_OF(r, T_I32), 0xdeadbeefLL); + /* hand-build CGABIValue for ret */ + cgtest_ret_reg(tf, r, T_I32); + cgtest_end(tf); +} + +CG_CASE_REGISTER(cgt_load_imm, build, /*expected=*/(int)0xdeadbeef); +``` + +`CG_CASE_REGISTER` is a macro that appends to the registry via a constructor +section or a manual table in `cases.c` (depending on freestanding constraints). +Since cfree itself is freestanding-only but the test runner is hosted, we can +use `__attribute__((constructor))` here; if that's undesirable we maintain +`cases.c` by hand as a list. + +## 6. CORPUS coverage groups + +Each group has a one-line description here; CORPUS.md will expand into +explicit cases. **Initial landing covers groups A and C only** — that's +the spine that proves the harness works end-to-end. + +| Group | Surface | Examples | +|---|---|---| +| **A** Lifecycle / return | `func_begin/end`, `ret`, return widths, sret | const return; void return; multiple returns; i8/i16/i32/i64/struct return | +| **B** Frame slots / params | `frame_slot`, `param`, address-taken, byval, sret-param | sum two int params; spill 9 params; address-taken local; struct byval | +| **C** Arithmetic | `binop` (all), `unop` | IADD/ISUB/IMUL; SDIV/UDIV/SREM/UREM; AND/OR/XOR; SHL/SHR_S/SHR_U; FADD..FDIV | +| **D** Compare / branch | `cmp`, `cmp_branch`, `scope_*`, fusion | cmp materialize 0/1; cmp_branch eq/ne/lt; structured if; structured if-else | +| **E** Convert | `convert` (all `ConvKind`) | sext/zext/trunc; itof/ftoi; fext/ftrunc; bitcast | +| **F** Memory | `load`/`store`/`addr_of`/`copy_bytes`/`set_bytes`/bitfield | load/store i8..i64; global rw; indirect rw; struct copy; memset zero; bitfield_load/store; volatile | +| **G** Calls | `call`, `ret`, ABI | direct + indirect; sret; byval arg; mixed gp+fp; >reg-count args; varargs call | +| **H** Control flow | `label`/`jump`/`scope_*`/`break_to`/`continue_to` | flat if; while; for-with-continue lands on inc; do/while; nested loops; goto | +| **I** alloca | `alloca_` | __builtin_alloca round-trip | +| **J** Variadics | `va_start_/va_arg_/va_end_/va_copy_` | walk va_list with int args; va_copy | +| **K** Atomics | `atomic_load/store/rmw/cas/fence`, `MemOrder` | per op × per order matrix (sampled) | +| **L** Intrinsics | `intrinsic` × `IntrinKind` | popcount/ctz/clz; bswap; expect (no-op); add_overflow multi-result | +| **M** Inline asm | `asm_block` | "r"/"=r" round-trip; "memory" clobber forces flush | +| **N** TLS | `tls_addr_of` | thread_local read/write | +| **O** Sections / globals | `frame_slot` + `DeclTable` | .data init; .bss tentative; .rodata constant pool; -ffunction-sections | +| **P** set_loc / debug | `set_loc`, MCEmitter line program | line numbers reach .debug_line (oracle TBD) | +| **Q** Multi-function (1 TU) | multiple func_begin/end | fn calls fn; mutual recursion; forward-declared callee | +| **R** opt-wrapped equivalence | `opt_cgtarget` | every other group's exit codes preserved through opt | + +Group order roughly tracks priority. A is the prerequisite for every +other group; B + C + D are needed for almost any non-trivial case; +F + G + H bring us to "real C". K, M, N, P, R can land later without +blocking the rest. + +## 7. Initial landing — Spine (A + C) + +Concrete first cases. Each is `int test_main(void) { return ...; }`. + +| Case | Body | expected | +|---|---|---| +| `a01_return_const` | `return 42;` | 42 | +| `a02_return_zero` | `return 0;` | 0 | +| `a03_return_neg` | `return -7;` | -7 (i.e. 249 mod 256 from runner) | +| `a04_return_i8` | `int test_main(void){return (i8)200;}` exercising widening | as expected | +| `c01_add` | `return 1 + 2;` | 3 | +| `c02_sub_mul` | `return 7 * 3 - 4;` | 17 | +| `c03_div_mod` | `return 23 / 4 + 23 % 4;` | 8 | +| `c04_bitwise` | `return (~3) & 0xff;` | 252 | +| `c05_shift` | `return (1 << 5) | (16 >> 1);` | 40 | + +Plus unit anchors: + +| Case | Layer | Notes | +|---|---|---| +| `mc_smoke` | MCEmitter | one-insn `test_main`; analogue of `test/elf/unit/smoke.c` | +| `cgt_load_imm_ret` | CGTarget | `alloc_reg` + `load_imm` + `ret` only, no `cg` | +| `cgt_binop_iadd` | CGTarget | `binop(BO_IADD, REG, IMM)` only, no `cg` | + +These exit-code oracles are enough to drive AArch64 backend bring-up +through return + integer arith. The R/E/J/D paths from §3 give us +ELF-roundtrip, link-exe + qemu, JIT, and direct-JIT coverage for free. + +## 8. Build integration + +Add a `test-cg` target in `test/test.mk`: + +```make +test-cg: lib bin-soft + bash test/cg/run.sh +``` + +Wire into the `test` aggregate. `cg-runner` builds against `libcfree.a` +the same way `link-exe-runner` and `jit-runner` do +(`$(CC) $(DRIVER_CFLAGS) ... $(LIB_AR)`). + +`run.sh` walks `cg-runner --list`, then per case runs paths D, R, E, J +(and later O), reporting one PASS/FAIL/SKIP line per (case, path) pair — +identical to `test/link/run.sh`. Skip-vs-fail is governed by +`CFREE_TEST_ALLOW_SKIP`, matching the existing convention. + +## 9. Non-goals (this strategy) + +- **Encoding/disassembly snapshots.** Deferred. Behavioral exit codes + only; we'll add encoding lock-ins for specific instruction-selection + guarantees later, surgically. +- **Negative tests.** Not in this harness. CG/CGTarget contract + violations are covered by assertions inside the implementations and by + parser-level diagnostics tests once the parser exists. +- **C-source-level tests.** Those arrive once the parser is real; they + share the R/E/J paths but bypass the cg-runner fixture API. The + fixtures here continue to exist as bisection anchors. +- **Cross-arch.** AArch64 only for now. The harness is per-arch (cases + may be reused if encodings differ but ABI does not); a future x86_64 + pass duplicates the runner with target = x86_64 and the same case set + modulo arch-specific overrides. diff --git a/src/api/stubs.c b/src/api/stubs.c @@ -57,13 +57,9 @@ void decl_free(DeclTable* d) { (void)d; } CG* cg_new(Compiler* c, CGTarget* t, Debug* d) { (void)t; (void)d; unimplemented(c, "cg"); } void cg_free(CG* g) { (void)g; } -MCEmitter* mc_new(Compiler* c, ObjBuilder* o) { (void)o; unimplemented(c, "mc"); } -void mc_free(MCEmitter* m) { (void)m; } - -CGTarget* cgtarget_new(Compiler* c, ObjBuilder* o, MCEmitter* m) - { (void)o; (void)m; unimplemented(c, "cgtarget"); } -void cgtarget_finalize(CGTarget* t) { (void)t; } -void cgtarget_free(CGTarget* t) { (void)t; } +/* mc_new / mc_free live in src/arch/mc.c. + * cgtarget_new / cgtarget_finalize / cgtarget_free live in src/arch/<target>.c + * (dispatched through src/arch/arch.c). */ /* ============================================================ * Optimizer diff --git a/src/arch/aarch64.c b/src/arch/aarch64.c @@ -0,0 +1,413 @@ +/* Minimal AArch64 CGTarget. + * + * Initial coverage matches the spine A + C corpus (function lifecycle and + * integer arithmetic). Other CGTarget methods panic with a clear "unimpl" + * diagnostic so test cases that touch them fail visibly rather than + * silently emitting nothing. + * + * Single-pass register allocation: alloc_reg hands out W19..W28 in order + * and panics on exhaustion. No live-range tracking, no spills. Suitable + * for short straight-line fixtures only; replaced when CG's + * value-stack-aware spill/reload arrives. + * + * Width is derived from Operand.type via type_is_64(). For the test + * harness this is enough; full ABI integration arrives with TargetABI. */ + +#include "arch/arch.h" +#include "core/arena.h" +#include "obj/obj.h" +#include "type/type.h" + +#include <string.h> + +typedef struct AAImpl { + CGTarget base; + SrcLoc loc; + const CGFuncDesc* fd; + u32 func_start; + u32 next_alloc; +} AAImpl; + +static AAImpl* impl_of(CGTarget* t) { return (AAImpl*)t; } + +/* ---- helpers ---- */ + +static int type_is_64(const Type* t) +{ + if (!t) return 0; + switch (t->kind) { + case TY_LONG: case TY_ULONG: + case TY_LLONG: case TY_ULLONG: + case TY_PTR: + case TY_DOUBLE: + return 1; + default: + return 0; + } +} + +static u32 reg_num(Operand op) { return op.v.reg & 0x1fu; } + +static void emit32(MCEmitter* mc, u32 word) +{ + u8 b[4]; + b[0] = (u8)(word & 0xff); + b[1] = (u8)((word >> 8) & 0xff); + b[2] = (u8)((word >> 16)& 0xff); + b[3] = (u8)((word >> 24)& 0xff); + mc->emit_bytes(mc, b, 4); +} + +static _Noreturn void aa_panic(CGTarget* t, const char* what) +{ + SrcLoc loc = impl_of(t)->loc; + compiler_panic(t->c, loc, "aarch64: %s not implemented", what); +} + +/* ---- function lifecycle ---- */ + +static void aa_func_begin(CGTarget* t, const CGFuncDesc* fd) +{ + AAImpl* a = impl_of(t); + MCEmitter* mc = t->mc; + + mc->set_section(mc, fd->text_section_id); + mc->emit_align(mc, 4, 0); /* instruction alignment */ + + a->fd = fd; + a->func_start = mc->pos(mc); + a->next_alloc = 0; + + mc->cfi_startproc(mc); +} + +static void aa_func_end(CGTarget* t) +{ + AAImpl* a = impl_of(t); + MCEmitter* mc = t->mc; + u32 end = mc->pos(mc); + + obj_symbol_define(t->obj, + a->fd->sym, + a->fd->text_section_id, + (u64)a->func_start, + (u64)(end - a->func_start)); + + mc->cfi_endproc(mc); + a->fd = NULL; +} + +/* ---- registers / frame ---- */ + +static Reg aa_alloc_reg(CGTarget* t, RegClass cls, const Type* ty) +{ + AAImpl* a = impl_of(t); + (void)ty; + if (cls != RC_INT) { + compiler_panic(t->c, a->loc, "aarch64 alloc_reg: class %d unimpl", (int)cls); + } + if (a->next_alloc >= 10) { + compiler_panic(t->c, a->loc, + "aarch64 alloc_reg: out of scratch regs (no spill yet)"); + } + return (Reg)(19u + a->next_alloc++); +} + +static void aa_free_reg(CGTarget* t, Reg r) { (void)t; (void)r; } + +static FrameSlot aa_frame_slot(CGTarget* t, const FrameSlotDesc* d) { (void)d; aa_panic(t, "frame_slot"); } +static void aa_param (CGTarget* t, const CGParamDesc* p) { (void)p; aa_panic(t, "param"); } +static const Reg* aa_clobbers (CGTarget* t, RegClass c, u32* n) { (void)c; (void)n; aa_panic(t, "clobbers"); } +static void aa_spill_reg (CGTarget* t, Operand s, FrameSlot f, MemAccess m) { (void)s; (void)f; (void)m; aa_panic(t, "spill_reg"); } +static void aa_reload_reg(CGTarget* t, Operand d, FrameSlot f, MemAccess m) { (void)d; (void)f; (void)m; aa_panic(t, "reload_reg"); } + +/* ---- labels / control flow (deferred) ---- */ + +static Label aa_label_new (CGTarget* t) { aa_panic(t, "label_new"); } +static void aa_label_place(CGTarget* t, Label l) { (void)l; aa_panic(t, "label_place"); } +static void aa_jump (CGTarget* t, Label l) { (void)l; aa_panic(t, "jump"); } +static void aa_cmp_branch (CGTarget* t, CmpOp op, Operand a, Operand b, Label l) { (void)op;(void)a;(void)b;(void)l; aa_panic(t, "cmp_branch"); } + +static CGScope aa_scope_begin(CGTarget* t, const CGScopeDesc* d) { (void)d; aa_panic(t, "scope_begin"); } +static void aa_scope_else (CGTarget* t, CGScope s) { (void)s; aa_panic(t, "scope_else"); } +static void aa_scope_end (CGTarget* t, CGScope s) { (void)s; aa_panic(t, "scope_end"); } +static void aa_break_to (CGTarget* t, CGScope s) { (void)s; aa_panic(t, "break_to"); } +static void aa_continue_to(CGTarget* t, CGScope s) { (void)s; aa_panic(t, "continue_to"); } + +/* ---- data movement ---- */ + +static void aa_load_imm(CGTarget* t, Operand dst, i64 imm) +{ + AAImpl* a = impl_of(t); + MCEmitter* mc = t->mc; + u32 sf = type_is_64(dst.type) ? 1u : 0u; + u32 rd = reg_num(dst); + + if (imm >= 0 && imm <= 0xffff) { + /* MOVZ Wd|Xd, #imm16, lsl #0 */ + u32 word = (sf << 31) | 0x52800000u | (((u32)imm & 0xffff) << 5) | rd; + emit32(mc, word); + } else if (imm < 0 && imm >= -0x10000) { + /* MOVN Wd|Xd, #(~imm)16 */ + u32 inv = (u32)(~(u64)imm) & 0xffff; + u32 word = (sf << 31) | 0x12800000u | (inv << 5) | rd; + emit32(mc, word); + } else { + compiler_panic(t->c, a->loc, + "aarch64 load_imm: imm %lld out of 16-bit range", + (long long)imm); + } +} + +static void aa_load_const(CGTarget* t, Operand dst, ConstBytes cb) +{ (void)dst; (void)cb; aa_panic(t, "load_const"); } + +static void aa_copy(CGTarget* t, Operand dst, Operand src) +{ + /* MOV Wd, Wm ≡ ORR Wd, WZR, Wm */ + MCEmitter* mc = t->mc; + u32 sf = type_is_64(dst.type) ? 1u : 0u; + u32 rd = reg_num(dst); + u32 rm = reg_num(src); + u32 word = (sf << 31) | 0x2a000000u | (rm << 16) | (31u << 5) | rd; + emit32(mc, word); +} + +static void aa_load (CGTarget* t, Operand d, Operand a, MemAccess m) { (void)d;(void)a;(void)m; aa_panic(t, "load"); } +static void aa_store (CGTarget* t, Operand a, Operand s, MemAccess m) { (void)a;(void)s;(void)m; aa_panic(t, "store"); } +static void aa_addr_of (CGTarget* t, Operand d, Operand l) { (void)d;(void)l; aa_panic(t, "addr_of"); } +static void aa_tls_addr_of(CGTarget* t, Operand d, ObjSymId s, i64 a) { (void)d;(void)s;(void)a; aa_panic(t, "tls_addr_of"); } +static void aa_copy_bytes(CGTarget* t, Operand d, Operand s, AggregateAccess g) { (void)d;(void)s;(void)g; aa_panic(t, "copy_bytes"); } +static void aa_set_bytes (CGTarget* t, Operand d, Operand b, AggregateAccess g) { (void)d;(void)b;(void)g; aa_panic(t, "set_bytes"); } +static void aa_bitfield_load (CGTarget* t, Operand d, Operand a, BitFieldAccess f) { (void)d;(void)a;(void)f; aa_panic(t, "bitfield_load"); } +static void aa_bitfield_store(CGTarget* t, Operand a, Operand s, BitFieldAccess f) { (void)a;(void)s;(void)f; aa_panic(t, "bitfield_store"); } + +/* ---- arithmetic ---- */ + +static void aa_binop(CGTarget* t, BinOp op, Operand dst, Operand a, Operand b) +{ + MCEmitter* mc = t->mc; + u32 sf = type_is_64(dst.type) ? 1u : 0u; + u32 rd = reg_num(dst); + u32 rn = reg_num(a); + u32 rm = reg_num(b); + u32 word; + + /* All operands must be REG. CG materializes immediates first. */ + if (a.kind != OPK_REG || b.kind != OPK_REG) { + compiler_panic(t->c, impl_of(t)->loc, + "aarch64 binop: non-REG operands not yet supported"); + } + + switch (op) { + case BO_IADD: word = (sf << 31) | 0x0b000000u | (rm << 16) | (rn << 5) | rd; break; + case BO_ISUB: word = (sf << 31) | 0x4b000000u | (rm << 16) | (rn << 5) | rd; break; + case BO_IMUL: /* MADD Wd, Wn, Wm, WZR */ + word = (sf << 31) | 0x1b000000u | (rm << 16) | (31u << 10) | (rn << 5) | rd; break; + case BO_AND: word = (sf << 31) | 0x0a000000u | (rm << 16) | (rn << 5) | rd; break; + case BO_OR: word = (sf << 31) | 0x2a000000u | (rm << 16) | (rn << 5) | rd; break; + case BO_XOR: word = (sf << 31) | 0x4a000000u | (rm << 16) | (rn << 5) | rd; break; + case BO_SHL: /* LSLV: data-processing-2-source op2=001000 */ + word = (sf << 31) | 0x1ac02000u | (rm << 16) | (rn << 5) | rd; break; + case BO_SHR_U:/* LSRV: op2=001001 */ + word = (sf << 31) | 0x1ac02400u | (rm << 16) | (rn << 5) | rd; break; + case BO_SHR_S:/* ASRV: op2=001010 */ + word = (sf << 31) | 0x1ac02800u | (rm << 16) | (rn << 5) | rd; break; + case BO_UDIV: word = (sf << 31) | 0x1ac00800u | (rm << 16) | (rn << 5) | rd; break; + case BO_SDIV: word = (sf << 31) | 0x1ac00c00u | (rm << 16) | (rn << 5) | rd; break; + case BO_SREM: + case BO_UREM: + case BO_FADD: case BO_FSUB: case BO_FMUL: case BO_FDIV: + default: + compiler_panic(t->c, impl_of(t)->loc, + "aarch64 binop: op %d unimpl", (int)op); + } + emit32(mc, word); +} + +static void aa_unop(CGTarget* t, UnOp op, Operand dst, Operand a) +{ + MCEmitter* mc = t->mc; + u32 sf = type_is_64(dst.type) ? 1u : 0u; + u32 rd = reg_num(dst); + u32 rn = reg_num(a); + u32 word; + + if (a.kind != OPK_REG) { + compiler_panic(t->c, impl_of(t)->loc, + "aarch64 unop: non-REG operand not yet supported"); + } + + switch (op) { + case UO_NEG: + /* SUB Wd, WZR, Wn */ + word = (sf << 31) | 0x4b000000u | (rn << 16) | (31u << 5) | rd; + break; + case UO_BNOT: + /* MVN Wd, Wn ≡ ORN Wd, WZR, Wn (shift=0) */ + word = (sf << 31) | 0x2a200000u | (rn << 16) | (31u << 5) | rd; + break; + case UO_NOT: + default: + compiler_panic(t->c, impl_of(t)->loc, + "aarch64 unop: op %d unimpl", (int)op); + } + emit32(mc, word); +} + +static void aa_cmp (CGTarget* t, CmpOp op, Operand d, Operand a, Operand b) { (void)op;(void)d;(void)a;(void)b; aa_panic(t, "cmp"); } +static void aa_convert(CGTarget* t, ConvKind k, Operand d, Operand s) { (void)k;(void)d;(void)s; aa_panic(t, "convert"); } + +/* ---- calls / return ---- */ + +static void aa_call(CGTarget* t, const CGCallDesc* d) { (void)d; aa_panic(t, "call"); } + +static void aa_ret(CGTarget* t, const CGABIValue* val) +{ + MCEmitter* mc = t->mc; + + if (val && val->storage.kind == OPK_REG) { + /* MOV W0|X0, src */ + u32 sf = type_is_64(val->storage.type) ? 1u : 0u; + u32 rm = reg_num(val->storage); + u32 word = (sf << 31) | 0x2a000000u | (rm << 16) | (31u << 5) | 0u; + emit32(mc, word); + } else if (val && val->storage.kind == OPK_IMM) { + /* MOV W0, #imm via load_imm */ + Operand w0 = { OPK_REG, RC_INT, 0, val->storage.type, .v.reg = 0 }; + aa_load_imm(t, w0, val->storage.v.imm); + } + /* RET X30 */ + emit32(mc, 0xd65f03c0u); +} + +static void aa_alloca_ (CGTarget* t, Operand d, Operand s, u32 a) { (void)d;(void)s;(void)a; aa_panic(t, "alloca"); } +static void aa_va_start_(CGTarget* t, Operand a) { (void)a; aa_panic(t, "va_start"); } +static void aa_va_arg_ (CGTarget* t, Operand d, Operand a, const Type* ty) { (void)d;(void)a;(void)ty; aa_panic(t, "va_arg"); } +static void aa_va_end_ (CGTarget* t, Operand a) { (void)a; aa_panic(t, "va_end"); } +static void aa_va_copy_ (CGTarget* t, Operand d, Operand s) { (void)d;(void)s; aa_panic(t, "va_copy"); } + +static void aa_atomic_load (CGTarget* t, Operand d, Operand a, MemAccess m, MemOrder o) { (void)d;(void)a;(void)m;(void)o; aa_panic(t, "atomic_load"); } +static void aa_atomic_store(CGTarget* t, Operand a, Operand s, MemAccess m, MemOrder o) { (void)a;(void)s;(void)m;(void)o; aa_panic(t, "atomic_store"); } +static void aa_atomic_rmw (CGTarget* t, AtomicOp op, Operand d, Operand a, Operand v, MemAccess m, MemOrder o) { (void)op;(void)d;(void)a;(void)v;(void)m;(void)o; aa_panic(t, "atomic_rmw"); } +static void aa_atomic_cas (CGTarget* t, Operand p, Operand ok, Operand a, Operand e, Operand des, MemAccess m, MemOrder s, MemOrder f) { (void)p;(void)ok;(void)a;(void)e;(void)des;(void)m;(void)s;(void)f; aa_panic(t, "atomic_cas"); } +static void aa_fence (CGTarget* t, MemOrder o) { (void)o; aa_panic(t, "fence"); } + +static void aa_intrinsic(CGTarget* t, IntrinKind k, Operand* dsts, u32 nd, const Operand* args, u32 na) { (void)k;(void)dsts;(void)nd;(void)args;(void)na; aa_panic(t, "intrinsic"); } + +static void aa_asm_block(CGTarget* t, const char* tmpl, + const AsmConstraint* outs, u32 no, Operand* oo, + const AsmConstraint* ins, u32 ni, const Operand* io, + const Sym* clobs, u32 nc) +{ (void)tmpl;(void)outs;(void)no;(void)oo;(void)ins;(void)ni;(void)io;(void)clobs;(void)nc; aa_panic(t, "asm_block"); } + +static void aa_set_loc(CGTarget* t, SrcLoc loc) +{ + impl_of(t)->loc = loc; + t->mc->set_loc(t->mc, loc); +} + +static void aa_finalize(CGTarget* t) { (void)t; } + +static void aa_destroy(CGTarget* t) { (void)t; /* arena-backed */ } + +/* ---- construction ---- */ + +static void cgt_cleanup(void* arg) { cgtarget_free((CGTarget*)arg); } + +CGTarget* cgtarget_new(Compiler* c, ObjBuilder* o, MCEmitter* m) +{ + /* v1: only AArch64 implemented. Other targets fall back to a + * "not implemented" diagnostic at construction. */ + if (c->target.arch != CFREE_ARCH_ARM_64) { + SrcLoc loc = {0,0,0}; + compiler_panic(c, loc, + "cgtarget_new: only AArch64 implemented in v1 (got arch %d)", + (int)c->target.arch); + } + + AAImpl* a = arena_new(c->tu, AAImpl); + memset(a, 0, sizeof *a); + + CGTarget* t = &a->base; + t->c = c; + t->obj = o; + t->mc = m; + + t->func_begin = aa_func_begin; + t->func_end = aa_func_end; + + t->alloc_reg = aa_alloc_reg; + t->free_reg = aa_free_reg; + t->frame_slot = aa_frame_slot; + t->param = aa_param; + t->clobbers = aa_clobbers; + t->spill_reg = aa_spill_reg; + t->reload_reg = aa_reload_reg; + + t->label_new = aa_label_new; + t->label_place= aa_label_place; + t->jump = aa_jump; + t->cmp_branch = aa_cmp_branch; + + t->scope_begin= aa_scope_begin; + t->scope_else = aa_scope_else; + t->scope_end = aa_scope_end; + t->break_to = aa_break_to; + t->continue_to= aa_continue_to; + + t->load_imm = aa_load_imm; + t->load_const = aa_load_const; + t->copy = aa_copy; + t->load = aa_load; + t->store = aa_store; + t->addr_of = aa_addr_of; + t->tls_addr_of= aa_tls_addr_of; + t->copy_bytes = aa_copy_bytes; + t->set_bytes = aa_set_bytes; + t->bitfield_load = aa_bitfield_load; + t->bitfield_store= aa_bitfield_store; + + t->binop = aa_binop; + t->unop = aa_unop; + t->cmp = aa_cmp; + t->convert = aa_convert; + + t->call = aa_call; + t->ret = aa_ret; + + t->alloca_ = aa_alloca_; + t->va_start_ = aa_va_start_; + t->va_arg_ = aa_va_arg_; + t->va_end_ = aa_va_end_; + t->va_copy_ = aa_va_copy_; + + t->setjmp_ = NULL; /* parser lowers via __cfree_setjmp */ + t->longjmp_ = NULL; + + t->atomic_load = aa_atomic_load; + t->atomic_store= aa_atomic_store; + t->atomic_rmw = aa_atomic_rmw; + t->atomic_cas = aa_atomic_cas; + t->fence = aa_fence; + + t->intrinsic = aa_intrinsic; + t->asm_block = aa_asm_block; + + t->set_loc = aa_set_loc; + t->finalize = aa_finalize; + t->destroy = aa_destroy; + + compiler_defer(c, cgt_cleanup, t); + return t; +} + +void cgtarget_finalize(CGTarget* t) { if (t && t->finalize) t->finalize(t); } + +void cgtarget_free(CGTarget* t) +{ + if (!t) return; + /* Arena-backed; nothing to free. The compiler_defer cleanup callback + * arrives here at panic; intentional double-call from explicit free + * after success is safe because everything is arena memory. */ +} diff --git a/src/arch/mc.c b/src/arch/mc.c @@ -0,0 +1,297 @@ +/* Generic MCEmitter implementation. + * + * MCEmitter sits between CGTarget (or parse_asm) and ObjBuilder. It owns + * the current section, byte position, machine label table, and forwards + * relocations / source-location stamps. Encoding is the caller's job — + * MCEmitter writes whatever bytes it's handed. + * + * This implementation is target-agnostic: every supported arch shares + * one MCEmitter; arch-specific differences live in CGTarget. Per-arch + * MCEmitter subclasses can layer on later if encoding cache or + * peephole-merging need shared state with the emitter. + * + * MCLabel handling: ids are 1-based (0 = MC_LABEL_NONE). Each label + * carries either a placement (sec_id, offset) or a list of pending + * fixups for forward references. emit_label_ref records a fixup; on + * label_place, all pending fixups for that label are applied. Fixup + * application uses RelocKind to choose how to encode the resolved + * displacement into the already-emitted bytes. + * + * For v1 we support these label-ref reloc kinds for intra-section + * fixups: + * R_PC32 — write 32-bit signed displacement at fixup ofs + * R_AARCH64_CALL26 — 26-bit imm26 << 2 in the BL instruction + * R_AARCH64_JUMP26 — same encoding as CALL26 (B vs BL only differs + * in the parent opcode the caller already emitted) + * + * Anything else for a label_ref panics; cross-section references go + * through emit_reloc against an ObjSymId instead. */ + +#include "arch/arch.h" +#include "core/arena.h" +#include "obj/obj.h" + +#include <string.h> + +typedef struct MCFixup { + u32 sec_id; + u32 offset; + u32 width; /* bytes the encoding occupies */ + RelocKind kind; + i64 addend; + struct MCFixup* next; +} MCFixup; + +typedef struct MCLabelInfo { + u8 placed; + u8 pad[3]; + u32 sec_id; + u32 offset; + MCFixup* pending; +} MCLabelInfo; + +typedef struct MCImpl { + MCEmitter base; + Arena* arena; + SrcLoc loc; + MCLabelInfo* labels; /* index 0 unused (MC_LABEL_NONE) */ + u32 nlabels; + u32 cap; +} MCImpl; + +/* ---- helpers ---- */ + +static MCImpl* impl_of(MCEmitter* m) { return (MCImpl*)m; } + +static void labels_grow(MCImpl* mc, u32 want) +{ + if (want <= mc->cap) return; + u32 ncap = mc->cap ? mc->cap * 2 : 16; + while (ncap < want) ncap *= 2; + MCLabelInfo* nbuf = arena_array(mc->arena, MCLabelInfo, ncap); + if (mc->labels) memcpy(nbuf, mc->labels, sizeof(MCLabelInfo) * mc->nlabels); + memset(nbuf + mc->nlabels, 0, sizeof(MCLabelInfo) * (ncap - mc->nlabels)); + mc->labels = nbuf; + mc->cap = ncap; +} + +static void apply_fixup(MCImpl* mc, const MCFixup* fx, u32 target_offset) +{ + /* signed displacement from end-of-instruction position to target. */ + i64 disp = (i64)target_offset - (i64)fx->offset + fx->addend; + + switch (fx->kind) { + case R_PC32: { + i32 v = (i32)disp; + u8 bytes[4]; + bytes[0] = (u8)(v & 0xff); + bytes[1] = (u8)((v>>8) & 0xff); + bytes[2] = (u8)((v>>16) & 0xff); + bytes[3] = (u8)((v>>24) & 0xff); + obj_patch(mc->base.obj, fx->sec_id, fx->offset, bytes, 4); + break; + } + case R_AARCH64_JUMP26: + case R_AARCH64_CALL26: { + /* imm26 in the lower 26 bits of a BL/B; offset in instructions. */ + i64 idisp = disp >> 2; /* word-aligned displacement */ + u32 imm26 = (u32)(idisp & 0x03ffffffu); + u8 cur[4]; + /* read existing 4 bytes via section accessor. */ + u8* p = mc->base.obj ? NULL : NULL; + (void)p; + /* obj has obj_patch but not "read"; fetch via flatten — for v1 + * we know callers emit a fresh insn with imm26=0 right before + * recording the fixup, so we can reconstruct from the opcode + * carried in addend's high bits. Simpler: callers emit the + * full encoding with imm26=0 and the fixup just OR's imm26 in + * by patching the low 26 bits. We emulate that by reading + * indirectly through obj_section_get. */ + const Section* s = obj_section_get(mc->base.obj, fx->sec_id); + if (!s) break; + buf_read(&s->bytes, fx->offset, cur, 4); + u32 word = (u32)cur[0] | ((u32)cur[1] << 8) | ((u32)cur[2] << 16) | ((u32)cur[3] << 24); + word = (word & ~0x03ffffffu) | imm26; + cur[0] = (u8)(word & 0xff); + cur[1] = (u8)((word >> 8) & 0xff); + cur[2] = (u8)((word >> 16)& 0xff); + cur[3] = (u8)((word >> 24)& 0xff); + obj_patch(mc->base.obj, fx->sec_id, fx->offset, cur, 4); + break; + } + default: + compiler_panic(mc->base.c, mc->loc, + "MCEmitter: unsupported label-ref reloc kind %d", + (int)fx->kind); + } +} + +/* ---- vtable methods ---- */ + +static void m_set_section(MCEmitter* m, u32 section_id) { m->section_id = section_id; } + +static u32 m_pos(MCEmitter* m) +{ return obj_pos(m->obj, m->section_id); } + +static MCLabel m_label_new(MCEmitter* m) +{ + MCImpl* mc = impl_of(m); + if (mc->nlabels == 0) { labels_grow(mc, 1); mc->nlabels = 1; } /* skip 0 */ + labels_grow(mc, mc->nlabels + 1); + u32 id = mc->nlabels++; + MCLabelInfo* li = &mc->labels[id]; + li->placed = 0; + li->sec_id = 0; + li->offset = 0; + li->pending = NULL; + return (MCLabel)id; +} + +static void m_label_place(MCEmitter* m, MCLabel id) +{ + MCImpl* mc = impl_of(m); + if (id == MC_LABEL_NONE || id >= mc->nlabels) { + compiler_panic(m->c, mc->loc, "MCEmitter: bad label %u", (unsigned)id); + } + MCLabelInfo* li = &mc->labels[id]; + if (li->placed) { + compiler_panic(m->c, mc->loc, "MCEmitter: label %u placed twice", + (unsigned)id); + } + li->placed = 1; + li->sec_id = m->section_id; + li->offset = obj_pos(m->obj, m->section_id); + /* Apply pending fixups. */ + for (MCFixup* fx = li->pending; fx; fx = fx->next) { + apply_fixup(mc, fx, li->offset); + } + li->pending = NULL; +} + +static void m_emit_bytes(MCEmitter* m, const u8* data, size_t n) +{ obj_write(m->obj, m->section_id, data, n); } + +static void m_emit_fill(MCEmitter* m, size_t n, u8 byte) +{ + u8 buf[64]; + memset(buf, byte, sizeof buf); + while (n > 0) { + size_t k = n < sizeof buf ? n : sizeof buf; + obj_write(m->obj, m->section_id, buf, k); + n -= k; + } +} + +static void m_emit_align(MCEmitter* m, u32 align, u8 fill) +{ + if (align <= 1) return; + u32 cur = obj_pos(m->obj, m->section_id); + u32 misalign = cur & (align - 1); + if (misalign == 0) return; + m_emit_fill(m, align - misalign, fill); +} + +static void m_emit_reloc(MCEmitter* m, RelocKind k, ObjSymId sym, i64 addend) +{ + obj_reloc(m->obj, m->section_id, + obj_pos(m->obj, m->section_id), + k, sym, addend); +} + +static void m_emit_reloc_at(MCEmitter* m, u32 section_id, u32 offset, + RelocKind k, ObjSymId sym, i64 addend, + int explicit_addend, int pair) +{ obj_reloc_ex(m->obj, section_id, offset, k, sym, addend, explicit_addend, pair); } + +static void m_emit_label_ref(MCEmitter* m, MCLabel id, RelocKind kind, + u32 width, i64 addend) +{ + MCImpl* mc = impl_of(m); + if (id == MC_LABEL_NONE || id >= mc->nlabels) { + compiler_panic(m->c, mc->loc, "MCEmitter: bad label %u", (unsigned)id); + } + MCLabelInfo* li = &mc->labels[id]; + MCFixup* fx = arena_new(mc->arena, MCFixup); + fx->sec_id = m->section_id; + fx->offset = obj_pos(m->obj, m->section_id) - width; /* fixup site is the just-emitted insn */ + fx->width = width; + fx->kind = kind; + fx->addend = addend; + fx->next = NULL; + if (li->placed) { + apply_fixup(mc, fx, li->offset); + } else { + fx->next = li->pending; + li->pending = fx; + } +} + +static void m_set_loc(MCEmitter* m, SrcLoc loc) { impl_of(m)->loc = loc; } + +/* CFI: buffered for .eh_frame / .debug_frame emission. v1 stores nothing + * because Debug isn't wired up yet; methods are no-ops so backends can + * call them without conditionals. */ +static void m_cfi_startproc (MCEmitter* m) { (void)m; } +static void m_cfi_endproc (MCEmitter* m) { (void)m; } +static void m_cfi_def_cfa (MCEmitter* m, u32 r, i32 o) { (void)m;(void)r;(void)o; } +static void m_cfi_def_cfa_offset (MCEmitter* m, i32 o) { (void)m;(void)o; } +static void m_cfi_def_cfa_register(MCEmitter* m, u32 r) { (void)m;(void)r; } +static void m_cfi_offset (MCEmitter* m, u32 r, i32 o) { (void)m;(void)r;(void)o; } +static void m_cfi_rel_offset (MCEmitter* m, u32 r, i32 o) { (void)m;(void)r;(void)o; } +static void m_cfi_restore (MCEmitter* m, u32 r) { (void)m;(void)r; } + +static void m_destroy(MCEmitter* m) { (void)m; /* arena-backed */ } + +/* ---- construction ---- */ + +static void mc_cleanup(void* arg) { mc_free((MCEmitter*)arg); } + +MCEmitter* mc_new(Compiler* c, ObjBuilder* o) +{ + MCImpl* mc = arena_new(c->tu, MCImpl); + memset(mc, 0, sizeof *mc); + + MCEmitter* base = &mc->base; + base->c = c; + base->obj = o; + base->section_id = OBJ_SEC_NONE; + + base->set_section = m_set_section; + base->pos = m_pos; + + base->label_new = m_label_new; + base->label_place = m_label_place; + + base->emit_bytes = m_emit_bytes; + base->emit_fill = m_emit_fill; + base->emit_align = m_emit_align; + base->emit_reloc = m_emit_reloc; + base->emit_reloc_at = m_emit_reloc_at; + base->emit_label_ref= m_emit_label_ref; + base->set_loc = m_set_loc; + + base->cfi_startproc = m_cfi_startproc; + base->cfi_endproc = m_cfi_endproc; + base->cfi_def_cfa = m_cfi_def_cfa; + base->cfi_def_cfa_offset = m_cfi_def_cfa_offset; + base->cfi_def_cfa_register = m_cfi_def_cfa_register; + base->cfi_offset = m_cfi_offset; + base->cfi_rel_offset = m_cfi_rel_offset; + base->cfi_restore = m_cfi_restore; + + base->destroy = m_destroy; + + mc->arena = c->tu; + mc->labels = NULL; + mc->nlabels = 0; + mc->cap = 0; + + compiler_defer(c, mc_cleanup, base); + return base; +} + +void mc_free(MCEmitter* m) +{ + if (!m) return; + /* Arena-backed; nothing to free. */ +} diff --git a/src/core/buf.c b/src/core/buf.c @@ -102,6 +102,27 @@ void buf_patch(Buf* b, u32 ofs, const void* data, size_t n) * drop matches buf_write's allocation-failure policy. */ } +void buf_read(const Buf* b, u32 ofs, void* dst, size_t n) +{ + BufChunk* c = b->head; + u32 chunk_start = 0; + u8* p = (u8*)dst; + while (c && n) { + u32 chunk_end = chunk_start + c->used; + if (ofs < chunk_end) { + u32 within = ofs - chunk_start; + u32 avail = c->used - within; + u32 take = (u32)(n < avail ? n : avail); + memcpy(p, c->data + within, take); + p += take; + n -= take; + ofs += take; + } + chunk_start = chunk_end; + c = c->next; + } +} + void buf_flatten(const Buf* b, u8* dst) { BufChunk* c = b->head; diff --git a/src/core/buf.h b/src/core/buf.h @@ -28,6 +28,7 @@ void buf_write(Buf*, const void* data, size_t n); u8* buf_reserve(Buf*, size_t n); /* contiguous; spills to a fresh chunk if needed */ u32 buf_pos(const Buf*); void buf_patch(Buf*, u32 ofs, const void* data, size_t n); /* must lie within written range */ +void buf_read (const Buf*, u32 ofs, void* dst, size_t n); /* must lie within written range */ void buf_flatten(const Buf*, u8* dst); /* copy entire contents out, dst >= total bytes */ diff --git a/test/cg/CORPUS.md b/test/cg/CORPUS.md @@ -0,0 +1,88 @@ +# cg / CGTarget / MCEmitter test corpus + +Coverage matrix for `test/cg/`. Each registered case in +`harness/cases.c` is one row; behavioral oracle is `test_main`'s return +value. Mirrors the CORPUS shape used by `test/elf/` and `test/link/`. + +Test paths per case (run.sh): + +- **D** in-process JIT (aarch64 host only) — `cg-runner --jit NAME`. +- **R** ELF roundtrip (host-arch agnostic) — `cg-runner --emit` → + `cfree-roundtrip` → `readelf` + `normalize.py` diff. +- **E** exec via qemu/podman — `cg-runner --emit` + `start.o` → + `link-exe-runner` → run. +- **J** jit-via-file (aarch64 host only) — `cg-runner --emit` → + `jit-runner`. + +`O` (opt-wrapped) lands once `opt_cgtarget` is implemented. + +## Status legend + +- ★ landed in the spine +- · planned +- (deferred) — explicit non-goal for the current pass + +## MC-only — direct MCEmitter + +| Case | Status | Notes | +|---|---|---| +| `mc_smoke` | ★ | hand-built `mov w0, #42; ret`; analogue of `test/elf/unit/smoke.c` | + +## Group A — function lifecycle and return + +| Case | Status | Body | Expected | +|---|---|---|---| +| `a01_return_const_42` | ★ | `alloc_reg; load_imm 42; ret reg` | 42 | +| `a02_return_zero` | ★ | `load_imm 0; ret reg` | 0 | +| `a03_ret_imm` | ★ | `ret IMM 17` (backend materializes) | 17 | +| `a04_copy_reg` | ★ | `load_imm 7; copy r1->r2; ret r2` | 7 | +| `a05_return_neg_small`| · | `load_imm -7` via MOVN; ret | 249 (mod 256) | +| `a06_return_i64` | · | 64-bit return register selection | per width | +| `a07_void_return` | · | `ret(NULL)` | 0 (via _start init) | +| `a08_multiple_returns`| · | early return | depends | + +## Group B — frame slots, parameters, locals + +(deferred until frame_slot/param implemented) + +| Case | Status | Notes | +|---|---|---| +| `b01_param_int` | · | one int param echoed | +| `b02_param_sum` | · | two params summed | +| `b03_param_spill` | · | nine params (overflow registers) | +| `b04_local_int` | · | local stored, loaded, returned | +| `b05_addr_taken_local` | · | `&local` forces frame residency | +| `b06_sret` | · | struct return | +| `b07_byval_param` | · | aggregate-by-value parameter | +| `b08_fp_param` | · | float/double param | + +## Group C — integer arithmetic + +| Case | Status | Body | Expected | +|---|---|---|---| +| `c01_add` | ★ | `1 + 2` | 3 | +| `c02_sub_mul` | ★ | `7 * 3 - 4` | 17 | +| `c03_bitwise` | ★ | `(~3) & 0xff` | 252 | +| `c04_shift` | ★ | `(1<<5) | (16>>1)` | 40 | +| `c05_div_mod` | · | `23 / 4 + 23 % 4` | 8 (needs SDIV+SREM) | +| `c06_xor` | · | `0xa5 ^ 0x5a` | 0xff | +| `c07_iadd_i64` | · | 64-bit adds | depends | +| `c08_unsigned_div` | · | `100u / 7u` | 14 | + +## Group D — compare and branch (deferred) +## Group E — conversions (deferred) +## Group F — memory (deferred) +## Group G — calls (deferred) +## Group H — control flow (deferred) +## Group I — alloca (deferred) +## Group J — varargs (deferred) +## Group K — atomics (deferred) +## Group L — intrinsics (deferred) +## Group M — inline asm (deferred) +## Group N — TLS (deferred) +## Group O — sections + globals (deferred) +## Group P — set_loc / debug (deferred) +## Group Q — multi-function (deferred) +## Group R — opt-wrapped equivalence (deferred) + +See `doc/cg_testing.md` for the strategy and group definitions. diff --git a/test/cg/harness/cases.c b/test/cg/harness/cases.c @@ -0,0 +1,181 @@ +/* Test case registry. + * + * Each case is a static builder function plus an entry in cg_cases[]. The + * runner finds cases by name, sets up the per-case Compiler + ObjBuilder + + * MCEmitter (+ CGTarget for non-MC-only cases), invokes the builder, and + * exits with the result of test_main() (for --jit) or writes the .o (for + * --emit). + * + * Adding a case: write a static `build_<name>(CgTestCtx*)`, add a row to + * the array, and update CORPUS.md. */ + +#include "cg_test.h" + +#include <string.h> + +/* ============================================================ + * Group: MC-only (lowest layer) + * ============================================================ */ + +/* mc_smoke — emit `mov w0, #42; ret` as raw AArch64 bytes through + * MCEmitter. No CGTarget involved. Validates the byte path end-to-end. */ +static void build_mc_smoke(CgTestCtx* ctx) +{ + static const u8 BYTES[8] = { + /* mov w0, #42 */ 0x40, 0x05, 0x80, 0x52, + /* ret */ 0xc0, 0x03, 0x5f, 0xd6, + }; + + ObjSymId sym = cgtest_mc_begin_main(ctx); + ctx->mc->set_section(ctx->mc, ctx->text_sec); + ctx->mc->emit_align(ctx->mc, 4, 0); + u32 start = ctx->mc->pos(ctx->mc); + ctx->mc->emit_bytes(ctx->mc, BYTES, sizeof BYTES); + cgtest_mc_end_main(ctx, sym, start); +} + +/* ============================================================ + * Group A: function lifecycle and return + * ============================================================ */ + +/* a01_return_const_42 — alloc reg, load_imm(42), ret reg. */ +static void build_a01_return_const_42(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, T_I32); + ctx->target->load_imm(ctx->target, REG_i32(r), 42); + cgtest_ret_reg(tf, r, T_I32); + cgtest_end(tf); +} + +/* a02_return_zero — load_imm(0). */ +static void build_a02_return_zero(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, T_I32); + ctx->target->load_imm(ctx->target, REG_i32(r), 0); + cgtest_ret_reg(tf, r, T_I32); + cgtest_end(tf); +} + +/* a03_ret_imm — backend materializes the imm directly inside ret(). */ +static void build_a03_ret_imm(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + cgtest_ret_imm(tf, 17, T_I32); + cgtest_end(tf); +} + +/* a04_copy_reg — load_imm(7) into r1, copy r1->r2, ret r2. Exercises + * CGTarget.copy. */ +static void build_a04_copy_reg(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + Reg r1 = ctx->target->alloc_reg(ctx->target, RC_INT, T_I32); + Reg r2 = ctx->target->alloc_reg(ctx->target, RC_INT, T_I32); + ctx->target->load_imm(ctx->target, REG_i32(r1), 7); + ctx->target->copy(ctx->target, REG_i32(r2), REG_i32(r1)); + cgtest_ret_reg(tf, r2, T_I32); + cgtest_end(tf); +} + +/* ============================================================ + * Group C: integer arithmetic + * ============================================================ */ + +/* c01_add — 1 + 2 = 3 */ +static void build_c01_add(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + CGTarget* T = ctx->target; + Reg a = T->alloc_reg(T, RC_INT, T_I32); + Reg b = T->alloc_reg(T, RC_INT, T_I32); + Reg d = T->alloc_reg(T, RC_INT, T_I32); + T->load_imm(T, REG_i32(a), 1); + T->load_imm(T, REG_i32(b), 2); + T->binop(T, BO_IADD, REG_i32(d), REG_i32(a), REG_i32(b)); + cgtest_ret_reg(tf, d, T_I32); + cgtest_end(tf); +} + +/* c02_sub_mul — 7 * 3 - 4 = 17 */ +static void build_c02_sub_mul(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + CGTarget* T = ctx->target; + Reg r7 = T->alloc_reg(T, RC_INT, T_I32); + Reg r3 = T->alloc_reg(T, RC_INT, T_I32); + Reg r4 = T->alloc_reg(T, RC_INT, T_I32); + Reg rmul = T->alloc_reg(T, RC_INT, T_I32); + Reg rsub = T->alloc_reg(T, RC_INT, T_I32); + T->load_imm(T, REG_i32(r7), 7); + T->load_imm(T, REG_i32(r3), 3); + T->load_imm(T, REG_i32(r4), 4); + T->binop(T, BO_IMUL, REG_i32(rmul), REG_i32(r7), REG_i32(r3)); + T->binop(T, BO_ISUB, REG_i32(rsub), REG_i32(rmul), REG_i32(r4)); + cgtest_ret_reg(tf, rsub, T_I32); + cgtest_end(tf); +} + +/* c03_bitwise — (~3) & 0xff = 252 */ +static void build_c03_bitwise(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + CGTarget* T = ctx->target; + Reg r3 = T->alloc_reg(T, RC_INT, T_I32); + Reg rinv = T->alloc_reg(T, RC_INT, T_I32); + Reg rmask= T->alloc_reg(T, RC_INT, T_I32); + Reg rand = T->alloc_reg(T, RC_INT, T_I32); + T->load_imm(T, REG_i32(r3), 3); + T->load_imm(T, REG_i32(rmask),0xff); + T->unop (T, UO_BNOT, REG_i32(rinv), REG_i32(r3)); + T->binop(T, BO_AND, REG_i32(rand), REG_i32(rinv), REG_i32(rmask)); + cgtest_ret_reg(tf, rand, T_I32); + cgtest_end(tf); +} + +/* c04_shift — (1<<5) | (16>>1) = 40 */ +static void build_c04_shift(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + CGTarget* T = ctx->target; + Reg r1 = T->alloc_reg(T, RC_INT, T_I32); + Reg r5 = T->alloc_reg(T, RC_INT, T_I32); + Reg r16 = T->alloc_reg(T, RC_INT, T_I32); + Reg r1s = T->alloc_reg(T, RC_INT, T_I32); + Reg rshl= T->alloc_reg(T, RC_INT, T_I32); + Reg rshr= T->alloc_reg(T, RC_INT, T_I32); + Reg ror = T->alloc_reg(T, RC_INT, T_I32); + T->load_imm(T, REG_i32(r1), 1); + T->load_imm(T, REG_i32(r5), 5); + T->load_imm(T, REG_i32(r16), 16); + T->load_imm(T, REG_i32(r1s), 1); + T->binop(T, BO_SHL, REG_i32(rshl), REG_i32(r1), REG_i32(r5)); + T->binop(T, BO_SHR_U, REG_i32(rshr), REG_i32(r16), REG_i32(r1s)); + T->binop(T, BO_OR, REG_i32(ror), REG_i32(rshl),REG_i32(rshr)); + cgtest_ret_reg(tf, ror, T_I32); + cgtest_end(tf); +} + +/* ============================================================ + * Registry + * ============================================================ */ + +const CgCase cg_cases[] = { + /* MC-only */ + { "mc_smoke", build_mc_smoke, 42, CG_CASE_MC_ONLY }, + + /* Group A */ + { "a01_return_const_42", build_a01_return_const_42, 42, CG_CASE_DEFAULT }, + { "a02_return_zero", build_a02_return_zero, 0, CG_CASE_DEFAULT }, + { "a03_ret_imm", build_a03_ret_imm, 17, CG_CASE_DEFAULT }, + { "a04_copy_reg", build_a04_copy_reg, 7, CG_CASE_DEFAULT }, + + /* Group C */ + { "c01_add", build_c01_add, 3, CG_CASE_DEFAULT }, + { "c02_sub_mul", build_c02_sub_mul, 17, CG_CASE_DEFAULT }, + { "c03_bitwise", build_c03_bitwise, 252, CG_CASE_DEFAULT }, + { "c04_shift", build_c04_shift, 40, CG_CASE_DEFAULT }, +}; + +const unsigned cg_cases_count = sizeof(cg_cases) / sizeof(cg_cases[0]); diff --git a/test/cg/harness/cg_runner.c b/test/cg/harness/cg_runner.c @@ -0,0 +1,260 @@ +/* cg_runner — multi-mode test runner for the cg/CGTarget/MCEmitter stack. + * + * cg-runner --list # print every registered case name + * cg-runner --expected NAME # print expected exit code (stdout) + * cg-runner --emit NAME OUT.o # build, emit_elf, write to OUT.o + * cg-runner --jit NAME # build, link, JIT, call test_main; + * # exit code = test_main's return + * + * The --jit path uses link_add_obj on the in-process ObjBuilder, so it + * exercises the live OB → JIT mapping (no .o serialization). The --emit + * path produces a .o that the existing test/link harness binaries + * (cfree-roundtrip, link-exe-runner, jit-runner) consume to drive paths + * R, E, and J. The shell harness compares the exit codes of those runs + * against the value reported by --expected. */ + +#include <cfree.h> +#include "core/core.h" +#include "core/pool.h" +#include "obj/obj.h" +#include "type/type.h" +#include "abi/abi.h" +#include "arch/arch.h" +#include "link/link.h" + +#include "cg_test.h" + +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +/* ---- env ---- */ + +static void* h_alloc (CfreeHeap* h, size_t n, size_t a) +{ (void)h;(void)a; return n ? malloc(n) : NULL; } +static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) +{ (void)h;(void)o;(void)a; return realloc(p, n); } +static void h_free (CfreeHeap* h, void* p, size_t n) +{ (void)h;(void)n; free(p); } +static CfreeHeap g_heap = { h_alloc, h_realloc, h_free, NULL }; + +static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, + const char* fmt, va_list ap) +{ + static const char* names[] = { "note","warning","error","fatal" }; + (void)s; (void)loc; + fprintf(stderr, "%s: ", names[k]); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); +} +static CfreeDiagSink g_diag = { diag_emit, NULL, 0, 0 }; + +/* ---- helpers ---- */ + +static const CgCase* find_case(const char* name) +{ + for (unsigned i = 0; i < cg_cases_count; ++i) { + if (strcmp(cg_cases[i].name, name) == 0) return &cg_cases[i]; + } + return NULL; +} + +static void target_aarch64_linux(CfreeTarget* t) +{ + memset(t, 0, sizeof *t); + t->arch = CFREE_ARCH_ARM_64; + t->os = CFREE_OS_LINUX; + t->obj = CFREE_OBJ_ELF; + t->ptr_size = 8; + t->ptr_align = 8; + t->big_endian = 0; +} + +/* Build the ObjBuilder for a case. On success returns 0 and fills *ob_out; + * on panic returns nonzero (the diagnostic was already emitted). */ +typedef struct BuildState { + Compiler* c; + ObjBuilder* ob; + MCEmitter* mc; + CGTarget* target; + CgTestCtx ctx; +} BuildState; + +static int build_case(BuildState* st, const CgCase* cc) +{ + Compiler* c = st->c; + + if (setjmp(c->panic)) { + compiler_run_cleanups(c); + return 1; + } + + st->ob = obj_new(c); + st->mc = mc_new(c, st->ob); + + if (cc->kind != CG_CASE_MC_ONLY) { + st->target = cgtarget_new(c, st->ob, st->mc); + } else { + st->target = NULL; + } + + Sym text_name = pool_intern_cstr(c->global, ".text"); + ObjSecId text_sec = obj_section(st->ob, text_name, SEC_TEXT, + SF_ALLOC | SF_EXEC, 4); + + st->ctx.c = c; + st->ctx.ob = st->ob; + st->ctx.mc = st->mc; + st->ctx.target = st->target; + st->ctx.text_sec = text_sec; + st->ctx.pool = c->global; + + if (st->target) { + st->mc->set_section(st->mc, text_sec); + } + + cc->build(&st->ctx); + + if (st->target) cgtarget_finalize(st->target); + obj_finalize(st->ob); + return 0; +} + +/* ---- modes ---- */ + +static int mode_list(void) +{ + for (unsigned i = 0; i < cg_cases_count; ++i) { + fprintf(stdout, "%s\n", cg_cases[i].name); + } + return 0; +} + +static int mode_expected(const char* name) +{ + const CgCase* cc = find_case(name); + if (!cc) { fprintf(stderr, "cg-runner: unknown case '%s'\n", name); return 2; } + fprintf(stdout, "%d\n", cc->expected); + return 0; +} + +static int mode_emit(const char* name, const char* out_path) +{ + const CgCase* cc = find_case(name); + if (!cc) { fprintf(stderr, "cg-runner: unknown case '%s'\n", name); return 2; } + + CfreeTarget target; target_aarch64_linux(&target); + CfreeEnv env; memset(&env, 0, sizeof env); + env.heap = &g_heap; env.diag = &g_diag; + + CfreeCompiler* cc_ = cfree_compiler_new(target, &env); + if (!cc_) { fprintf(stderr, "cg-runner: compiler_new failed\n"); return 2; } + + BuildState st; memset(&st, 0, sizeof st); + st.c = (Compiler*)cc_; + if (build_case(&st, cc)) { + cfree_compiler_free(cc_); + return 1; + } + + /* Emit ELF to a memory writer, then dump to OUT_PATH. */ + CfreeWriter* w = cfree_writer_mem(&g_heap); + emit_elf(st.c, st.ob, w); + + size_t len = 0; + const uint8_t* data = cfree_writer_mem_bytes(w, &len); + + int rc = 0; + int fd = open(out_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { perror(out_path); rc = 2; } + else { + size_t off = 0; + while (off < len) { + ssize_t k = write(fd, data + off, len - off); + if (k <= 0) { perror("write"); rc = 2; break; } + off += (size_t)k; + } + close(fd); + } + cfree_writer_close(w); + cfree_compiler_free(cc_); + return rc; +} + +static int mode_jit(const char* name) +{ + const CgCase* cc = find_case(name); + if (!cc) { fprintf(stderr, "cg-runner: unknown case '%s'\n", name); return 2; } + + CfreeTarget target; target_aarch64_linux(&target); + CfreeEnv env; memset(&env, 0, sizeof env); + env.heap = &g_heap; env.diag = &g_diag; + + CfreeCompiler* cc_ = cfree_compiler_new(target, &env); + if (!cc_) { fprintf(stderr, "cg-runner: compiler_new failed\n"); return 2; } + + BuildState st; memset(&st, 0, sizeof st); + st.c = (Compiler*)cc_; + if (build_case(&st, cc)) { + cfree_compiler_free(cc_); + return 1; + } + + /* Direct in-process link: hand the ObjBuilder to the linker. */ + Compiler* c = st.c; + if (setjmp(c->panic)) { + compiler_run_cleanups(c); + cfree_compiler_free(cc_); + return 1; + } + + Linker* lk = link_new(c); + link_add_obj(lk, st.ob); + link_set_entry(lk, "test_main"); + LinkImage* img = link_resolve(lk); + if (!img) { + link_free(lk); + cfree_compiler_free(cc_); + return 1; + } + CfreeJit* jit = cfree_jit_from_image(img); + if (!jit) { + link_free(lk); + cfree_compiler_free(cc_); + return 1; + } + + int (*fn)(void) = (int(*)(void))cfree_jit_lookup(jit, "test_main"); + int result = fn ? fn() : 1; + + cfree_jit_free(jit); + link_free(lk); + cfree_compiler_free(cc_); + return result; +} + +/* ---- main ---- */ + +static int usage(void) +{ + fprintf(stderr, + "usage: cg-runner --list\n" + " cg-runner --expected NAME\n" + " cg-runner --emit NAME OUT.o\n" + " cg-runner --jit NAME\n"); + return 2; +} + +int main(int argc, char** argv) +{ + if (argc < 2) return usage(); + if (!strcmp(argv[1], "--list")) return mode_list(); + else if (!strcmp(argv[1], "--expected") && argc == 3) return mode_expected(argv[2]); + else if (!strcmp(argv[1], "--emit") && argc == 4) return mode_emit(argv[2], argv[3]); + else if (!strcmp(argv[1], "--jit") && argc == 3) return mode_jit (argv[2]); + return usage(); +} diff --git a/test/cg/harness/cg_test.c b/test/cg/harness/cg_test.c @@ -0,0 +1,152 @@ +/* test/cg fixture API implementation. */ + +#include "cg_test.h" + +#include "core/pool.h" + +#include <string.h> + +/* ---- static pre-interned types ---- + * These are not pool_interned (pool_type is a stub until the parser comes + * online), but the AArch64 backend only reads Type.kind to derive scalar + * width, which static literals satisfy. */ +static const Type s_void = { .kind = TY_VOID }; +static const Type s_i8 = { .kind = TY_SCHAR }; +static const Type s_i16 = { .kind = TY_SHORT }; +static const Type s_i32 = { .kind = TY_INT }; +static const Type s_i64 = { .kind = TY_LLONG }; +static const Type s_u8 = { .kind = TY_UCHAR }; +static const Type s_u16 = { .kind = TY_USHORT }; +static const Type s_u32 = { .kind = TY_UINT }; +static const Type s_u64 = { .kind = TY_ULLONG }; +static const Type s_ptr_v = { .kind = TY_PTR }; + +const Type*T_VOID = &s_void; +const Type*T_I8 = &s_i8; +const Type*T_I16 = &s_i16; +const Type*T_I32 = &s_i32; +const Type*T_I64 = &s_i64; +const Type*T_U8 = &s_u8; +const Type*T_U16 = &s_u16; +const Type*T_U32 = &s_u32; +const Type*T_U64 = &s_u64; +const Type*T_PTR_VOID= &s_ptr_v; + +/* ---- operand sugar ---- */ + +Operand IMM_i32(i64 v) +{ + Operand o = { 0 }; + o.kind = OPK_IMM; o.cls = RC_INT; o.type = T_I32; + o.v.imm = v; + return o; +} +Operand IMM_i64(i64 v) +{ + Operand o = { 0 }; + o.kind = OPK_IMM; o.cls = RC_INT; o.type = T_I64; + o.v.imm = v; + return o; +} +Operand REG_i32(Reg r) +{ + Operand o = { 0 }; + o.kind = OPK_REG; o.cls = RC_INT; o.type = T_I32; + o.v.reg = r; + return o; +} +Operand REG_i64(Reg r) +{ + Operand o = { 0 }; + o.kind = OPK_REG; o.cls = RC_INT; o.type = T_I64; + o.v.reg = r; + return o; +} + +/* ---- function-fixture helpers ---- */ + +CgTestFn* cgtest_begin_main(CgTestCtx* ctx, const Type* ret_ty) +{ + /* Allocate fixture state in tu arena so its lifetime spans the case. */ + CgTestFn* tf = arena_new(ctx->c->tu, CgTestFn); + memset(tf, 0, sizeof *tf); + tf->ctx = ctx; + tf->ret_ty = ret_ty; + + Sym name = pool_intern_cstr(ctx->pool, "test_main"); + tf->sym = obj_symbol(ctx->ob, name, SB_GLOBAL, SK_FUNC, + OBJ_SEC_NONE, 0, 0); + + tf->fd.sym = tf->sym; + tf->fd.text_section_id = ctx->text_sec; + tf->fd.group_id = OBJ_GROUP_NONE; + tf->fd.fn_type = NULL; /* unused by minimal AArch64 backend */ + tf->fd.abi = NULL; + tf->fd.params = NULL; + tf->fd.nparams = 0; + tf->fd.loc = (SrcLoc){0, 0, 0}; + + ctx->target->func_begin(ctx->target, &tf->fd); + return tf; +} + +void cgtest_ret_reg(CgTestFn* tf, Reg r, const Type* ty) +{ + CgTestCtx* ctx = tf->ctx; + memset(&tf->ret_val, 0, sizeof tf->ret_val); + tf->ret_val.type = ty; + tf->ret_val.abi = NULL; + tf->ret_val.storage.kind = OPK_REG; + tf->ret_val.storage.cls = RC_INT; + tf->ret_val.storage.type = ty; + tf->ret_val.storage.v.reg = r; + tf->ret_val.parts = NULL; + tf->ret_val.nparts = 0; + ctx->target->ret(ctx->target, &tf->ret_val); +} + +void cgtest_ret_imm(CgTestFn* tf, i64 imm, const Type* ty) +{ + CgTestCtx* ctx = tf->ctx; + memset(&tf->ret_val, 0, sizeof tf->ret_val); + tf->ret_val.type = ty; + tf->ret_val.abi = NULL; + tf->ret_val.storage.kind = OPK_IMM; + tf->ret_val.storage.cls = RC_INT; + tf->ret_val.storage.type = ty; + tf->ret_val.storage.v.imm = imm; + tf->ret_val.parts = NULL; + tf->ret_val.nparts = 0; + ctx->target->ret(ctx->target, &tf->ret_val); +} + +void cgtest_ret_void(CgTestFn* tf) +{ + CgTestCtx* ctx = tf->ctx; + ctx->target->ret(ctx->target, NULL); +} + +void cgtest_end(CgTestFn* tf) +{ + CgTestCtx* ctx = tf->ctx; + ctx->target->func_end(ctx->target); +} + +/* ---- MC-only case helpers ---- */ + +ObjSymId cgtest_mc_begin_main(CgTestCtx* ctx) +{ + Sym name = pool_intern_cstr(ctx->pool, "test_main"); + ObjSymId sym = obj_symbol(ctx->ob, name, SB_GLOBAL, SK_FUNC, + OBJ_SEC_NONE, 0, 0); + /* Caller is responsible for set_section + emit_align before recording + * the start position. */ + return sym; +} + +void cgtest_mc_end_main(CgTestCtx* ctx, ObjSymId sym, u32 start_pos) +{ + u32 end = ctx->mc->pos(ctx->mc); + obj_symbol_define(ctx->ob, sym, ctx->text_sec, + (u64)start_pos, (u64)(end - start_pos)); +} diff --git a/test/cg/harness/cg_test.h b/test/cg/harness/cg_test.h @@ -0,0 +1,100 @@ +/* test/cg fixture API. + * + * Each case is a small builder function that constructs `int test_main(void)` + * (or another named function returning int) by driving CGTarget directly, + * MCEmitter directly, or — once cg.h is implemented — the cg.h value-stack + * API. The runner finds the case by name, runs build(), finalizes the + * ObjBuilder, and exposes it through one of three exit paths: + * + * --emit NAME OUT.o : emit_elf to OUT.o (used by R/E/J path scripts) + * --jit NAME : link in-process and call test_main, exit with result + * --list : list every registered case name + * + * Pre-interned `Type`s here are static literals, not pool-interned. The + * AArch64 backend only inspects Type.kind to derive scalar width, which the + * literals satisfy. When the parser+TargetABI come online, the harness will + * switch to pool_type-interned Types but cases shouldn't need changes. */ + +#ifndef CFREE_TEST_CG_TEST_H +#define CFREE_TEST_CG_TEST_H + +#include "core/core.h" +#include "obj/obj.h" +#include "type/type.h" +#include "abi/abi.h" +#include "arch/arch.h" + +/* ---- ctx + case registry ---- */ + +typedef struct CgTestCtx { + Compiler* c; + ObjBuilder* ob; + MCEmitter* mc; + CGTarget* target; + ObjSecId text_sec; + Pool* pool; +} CgTestCtx; + +typedef void (*CgCaseFn)(CgTestCtx*); + +typedef enum { + CG_CASE_DEFAULT = 0, /* uses CGTarget (default) */ + CG_CASE_MC_ONLY = 1, /* uses MCEmitter only — no CGTarget construction */ +} CgCaseKind; + +typedef struct CgCase { + const char* name; + CgCaseFn build; + int expected; /* test_main return value (default 0) */ + unsigned kind; /* CgCaseKind */ +} CgCase; + +extern const CgCase cg_cases[]; +extern const unsigned cg_cases_count; + +/* ---- pre-interned types ---- + * Static Type literals; backend reads only .kind for width. */ +extern const Type *T_VOID, *T_I8, *T_I16, *T_I32, *T_I64, + *T_U8, *T_U16, *T_U32, *T_U64, + *T_PTR_VOID; + +/* ---- operand sugar ---- */ +Operand IMM_i32 (i64 v); +Operand IMM_i64 (i64 v); +Operand REG_i32 (Reg r); +Operand REG_i64 (Reg r); + +/* ---- function-fixture helpers ---- + * cgtest_begin_main creates an undefined `test_main` symbol of kind SK_FUNC + * and calls CGTarget.func_begin against the .text section. Use it from any + * CGTarget-driving case. + * + * cgtest_ret_reg materializes a CGABIValue holding the given register and + * type, then calls CGTarget.ret. Use it for cases that compute a value in + * a backend-allocated register and return it as int. + * + * cgtest_end calls CGTarget.func_end. */ + +typedef struct CgTestFn { + CgTestCtx* ctx; + const Type* ret_ty; + ObjSymId sym; + CGFuncDesc fd; + CGABIValue ret_val; /* reused by cgtest_ret_* */ +} CgTestFn; + +CgTestFn* cgtest_begin_main(CgTestCtx* ctx, const Type* ret_ty); +void cgtest_ret_reg (CgTestFn*, Reg r, const Type* ty); +void cgtest_ret_imm (CgTestFn*, i64 imm, const Type* ty); +void cgtest_ret_void (CgTestFn*); +void cgtest_end (CgTestFn*); + +/* ---- low-level helpers (used by mc_smoke and similar) ---- */ + +/* Define a function symbol at the current MCEmitter section position with + * size = current_pos - start_pos. Used by MC-only cases that emit bytes + * directly without a CGTarget. */ +ObjSymId cgtest_mc_begin_main(CgTestCtx*); +void cgtest_mc_end_main (CgTestCtx*, ObjSymId, u32 start_pos); + +#endif diff --git a/test/cg/run.sh b/test/cg/run.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash +# test/cg/run.sh — fixture-driven cg / CGTarget / MCEmitter test harness. +# +# For each registered case (cg-runner --list), runs up to four paths: +# +# D in-process JIT — cg-runner --jit NAME → exit code matches expected. +# No file I/O. aarch64 host only. +# R ELF roundtrip — cg-runner --emit NAME → cfree-roundtrip → readelf+ +# normalize diff. Validates emitter+reader fidelity. +# E exec via qemu — cg-runner --emit + start.o → link-exe-runner → qemu/ +# podman → exit code. Cross-host friendly. +# J jit-via-file — cg-runner --emit + jit-runner. aarch64 host. +# +# Reuses the existing test/link harness binaries (link-exe-runner, +# jit-runner, cfree-roundtrip) verbatim. +# +# Skip-vs-fail follows test/link convention: skipped layers are treated as +# failures unless CFREE_TEST_ALLOW_SKIP=1. + +set -u + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TEST_DIR="$ROOT/test/cg" +LINK_TEST_DIR="$ROOT/test/link" +BUILD_DIR="$ROOT/build/test" +LIB_AR="$ROOT/build/libcfree.a" + +CG_RUNNER="$BUILD_DIR/cg-runner" +ROUNDTRIP_BIN="$BUILD_DIR/cfree-roundtrip" +LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner" +JIT_RUNNER="$BUILD_DIR/jit-runner" +NORMALIZE="$ROOT/test/elf/normalize.py" + +CLANG_TARGET="--target=aarch64-linux-gnu" +CC="${CC:-cc}" +CFREE_CFLAGS="-I$ROOT/include -I$ROOT/src -I$TEST_DIR/harness" +ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" + +mkdir -p "$BUILD_DIR" "$BUILD_DIR/cg" + +PASS=0; FAIL=0; SKIP=0 +FAIL_NAMES=(); SKIP_NAMES=() + +color_red() { printf '\033[31m%s\033[0m' "$1"; } +color_grn() { printf '\033[32m%s\033[0m' "$1"; } +color_yel() { printf '\033[33m%s\033[0m' "$1"; } + +note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } +note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } +note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } + +# ---- tool detection -------------------------------------------------------- + +have_clang_cross=0 +have_readelf=0 +have_python3=0 +have_qemu=0 +have_podman=0 +have_runner=0 +have_roundtrip=0 +have_exe_runner=0 +have_jit_runner=0 +is_aarch64=0 + +if clang $CLANG_TARGET -c -x c - -o /dev/null < /dev/null 2>/dev/null; then + have_clang_cross=1 +fi +command -v llvm-readelf >/dev/null 2>&1 && have_readelf=1 +command -v readelf >/dev/null 2>&1 && have_readelf=1 +command -v python3 >/dev/null 2>&1 && have_python3=1 + +QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" +[ -n "$QEMU_BIN" ] && have_qemu=1 +command -v podman >/dev/null 2>&1 && have_podman=1 +{ [ $have_qemu -eq 1 ] || [ $have_podman -eq 1 ]; } && have_runner=1 + +arch_raw="$(uname -m 2>/dev/null || true)" +{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 + +READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" +RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}" + +run_aarch64() { + local exe="$1" out="$2" err="$3" + if [ $have_qemu -eq 1 ]; then + "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return + fi + if [ $have_podman -eq 1 ]; then + local dir base + dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" + podman run --rm --platform linux/arm64 --net=none \ + -v "$dir":/work:Z -w /work "$RUN_AARCH64_IMAGE" "./$base" >"$out" 2>"$err" + RUN_RC=$?; return + fi + RUN_RC=127 +} + +# ---- build harness binaries ------------------------------------------------ + +printf 'Building harness...\n' + +if [ ! -f "$LIB_AR" ]; then + printf ' FATAL: %s not found — run "make lib" first\n' "$LIB_AR" >&2 + exit 1 +fi + +# cg-runner +if $CC $CFREE_CFLAGS \ + "$TEST_DIR/harness/cg_runner.c" \ + "$TEST_DIR/harness/cg_test.c" \ + "$TEST_DIR/harness/cases.c" \ + "$LIB_AR" -o "$CG_RUNNER" 2>"$BUILD_DIR/cg-runner.err"; then + printf ' %s cg-runner\n' "$(color_grn built)" +else + printf ' %s cg-runner (see %s)\n' \ + "$(color_red FATAL)" "$BUILD_DIR/cg-runner.err" >&2 + exit 1 +fi + +# cfree-roundtrip — for path R. test/elf/run.sh builds this; skip path R if +# we can't find or build it. +if [ ! -x "$ROUNDTRIP_BIN" ]; then + if $CC -I"$ROOT/include" "$ROOT/test/elf/cfree-roundtrip.c" "$LIB_AR" \ + -o "$ROUNDTRIP_BIN" 2>"$BUILD_DIR/cfree-roundtrip.err"; then + have_roundtrip=1 + printf ' %s cfree-roundtrip\n' "$(color_grn built)" + else + printf ' %s cfree-roundtrip (see %s)\n' \ + "$(color_yel warn)" "$BUILD_DIR/cfree-roundtrip.err" >&2 + fi +else + have_roundtrip=1 +fi + +# link-exe-runner — for path E. +if [ ! -x "$LINK_EXE_RUNNER" ]; then + if $CC -I"$ROOT/include" "$LINK_TEST_DIR/harness/link_exe_runner.c" \ + "$LIB_AR" -o "$LINK_EXE_RUNNER" 2>"$BUILD_DIR/link-exe-runner.err"; then + have_exe_runner=1 + printf ' %s link-exe-runner\n' "$(color_grn built)" + else + printf ' %s link-exe-runner (see %s)\n' \ + "$(color_yel warn)" "$BUILD_DIR/link-exe-runner.err" >&2 + fi +else + have_exe_runner=1 +fi + +# jit-runner — for path J. Only on aarch64 host. +if [ $is_aarch64 -eq 1 ]; then + if [ ! -x "$JIT_RUNNER" ]; then + if $CC -I"$ROOT/include" "$LINK_TEST_DIR/harness/jit_runner.c" \ + "$LIB_AR" -o "$JIT_RUNNER" 2>"$BUILD_DIR/jit-runner.err"; then + have_jit_runner=1 + printf ' %s jit-runner\n' "$(color_grn built)" + else + printf ' %s jit-runner (see %s)\n' \ + "$(color_yel warn)" "$BUILD_DIR/jit-runner.err" >&2 + fi + else + have_jit_runner=1 + fi +fi + +printf 'Running cases...\n' + +# ---- per-case loop --------------------------------------------------------- + +CASES="$($CG_RUNNER --list)" + +for name in $CASES; do + work="$BUILD_DIR/cg/$name" + mkdir -p "$work" + + expected="$($CG_RUNNER --expected "$name" 2>/dev/null)" + expected="${expected:-0}" + # Exit codes are mod 256 on POSIX; mask the expected the same way so + # negative-return cases compare correctly. + expected_byte=$(( expected & 0xff )) + + # ---- Path D: in-process JIT (only on aarch64) ------------------------ + if [ $is_aarch64 -eq 1 ]; then + "$CG_RUNNER" --jit "$name" >"$work/d.out" 2>"$work/d.err" + d_rc=$? + if [ "$d_rc" -eq "$expected_byte" ]; then + note_pass "$name/D" + else + note_fail "$name/D (expected $expected_byte got $d_rc)" + fi + else + note_skip "$name/D" "not on aarch64 host" + fi + + # ---- Path R: ELF roundtrip -------------------------------------------- + obj="$work/$name.o" + if "$CG_RUNNER" --emit "$name" "$obj" 2>"$work/emit.err"; then + : + else + note_fail "$name/emit (cg-runner --emit failed; see $work/emit.err)" + continue + fi + + if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then + rt="$work/$name.rt.o" + if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt.err"; then + note_fail "$name/R (roundtrip failed)" + else + "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" >"$work/golden.norm" 2>/dev/null + "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$work/rt.norm" 2>/dev/null + if diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1; then + note_pass "$name/R" + else + note_fail "$name/R" + fi + fi + else + note_skip "$name/R" "missing roundtrip/readelf/python3" + fi + + # ---- Path E: link + qemu ---------------------------------------------- + if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ]; then + start_obj="$work/start.o" + clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie \ + -c "$LINK_TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null + + exe="$work/linked.exe" + if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \ + >"$work/exec_link.out" 2>"$work/exec_link.err"; then + note_fail "$name/E (link failed)" + elif [ $have_runner -eq 1 ]; then + run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" + if [ "$RUN_RC" -eq "$expected_byte" ]; then + note_pass "$name/E" + else + note_fail "$name/E (expected $expected_byte got $RUN_RC)" + fi + else + note_skip "$name/E" "no qemu/podman" + fi + else + note_skip "$name/E" "no link-exe-runner or aarch64 clang" + fi + + # ---- Path J: jit-via-file --------------------------------------------- + if [ $have_jit_runner -eq 1 ]; then + "$JIT_RUNNER" "$obj" >"$work/jit.out" 2>"$work/jit.err" + j_rc=$? + if [ "$j_rc" -eq "$expected_byte" ]; then + note_pass "$name/J" + else + note_fail "$name/J (expected $expected_byte got $j_rc)" + fi + else + note_skip "$name/J" "no jit-runner (not aarch64 host)" + fi +done + +# ---- summary --------------------------------------------------------------- + +printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" + +if [ ${#FAIL_NAMES[@]} -gt 0 ]; then + printf 'Failed:\n' + for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done +fi + +if [ ${#SKIP_NAMES[@]} -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then + printf 'Skipped (treat as failure; set CFREE_TEST_ALLOW_SKIP=1 to allow):\n' + for n in "${SKIP_NAMES[@]}"; do printf ' %s\n' "$n"; done +fi + +if [ $FAIL -gt 0 ]; then exit 1; fi +if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi +exit 0 diff --git a/test/test.mk b/test/test.mk @@ -12,10 +12,13 @@ # - test-link: linker + JIT behavioral harness in test/link/; three paths # per case (roundtrip R, ELF exec E, JIT J). Depends only on libcfree.a. # Set CFREE_TEST_ALLOW_SKIP=1 to allow skipped layers. +# - test-cg: cg / CGTarget / MCEmitter behavioral harness in test/cg/; +# four paths per case (D direct-JIT, R roundtrip, E exec, J jit-via-file). +# Depends only on libcfree.a; reuses test/link harness binaries. -.PHONY: test test-lex test-pp test-pp-err test-elf test-ar test-link +.PHONY: test test-lex test-pp test-pp-err test-elf test-ar test-link test-cg -test: test-lex test-pp test-pp-err test-elf test-ar test-link +test: test-lex test-pp test-pp-err test-elf test-ar test-link test-cg test-lex: bin @CFREE=$(abspath $(BIN)) test/lex/run.sh @@ -48,3 +51,6 @@ $(AR_TEST_BIN): test/ar_test.c $(LIB_AR) test-link: lib bash test/link/run.sh + +test-cg: lib + bash test/cg/run.sh