kit

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

commit 3334513345b6bc6fa45adec0119683b7951256b8
parent 4f4f5f3154004c2d4041df033a93a896d88d8228
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sun, 10 May 2026 10:55:03 -0700

arch/abi/obj: MULTIARCH Phase 2 — rv64 stubs

Mirror the x64 phase-2 scaffolding for riscv64: arch/rv64.{h,c}
panics-everywhere CGTarget, abi_rv64.c indirect-everything classifier,
elf_reloc_riscv64.c RelocKind <-> ELF type mapping, and dispatch wiring
in cgtarget_new / abi_init / emit_elf / link_emit_elf. Adds rv64 to
test/lib/exec_target.sh (linux/riscv64 platform, qemu-riscv64 fallback)
and a test/smoke/rv64.sh counterpart to test/smoke/x64.sh.

Diffstat:
Msrc/abi/abi.c | 2++
Msrc/abi/abi_internal.h | 1+
Asrc/abi/abi_rv64.c | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/cgtarget.c | 9++++++---
Asrc/arch/rv64.c | 387+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/rv64.h | 8++++++++
Msrc/link/link_elf.c | 3+++
Msrc/obj/elf.h | 24++++++++++++++++++++++++
Msrc/obj/elf_emit.c | 4++++
Asrc/obj/elf_reloc_riscv64.c | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/lib/exec_target.sh | 15++++++++++++++-
Atest/smoke/rv64.sh | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 12+++++++++++-
13 files changed, 730 insertions(+), 5 deletions(-)

diff --git a/src/abi/abi.c b/src/abi/abi.c @@ -289,6 +289,8 @@ static const ABIVtable* select_vtable(Compiler* c) { return &aapcs64_vtable; case CFREE_ARCH_X86_64: return &sysv_x64_vtable; + case CFREE_ARCH_RV64: + return &rv64_vtable; default: { SrcLoc loc = {0, 0, 0}; compiler_panic(c, loc, "abi_init: unsupported target arch %d", diff --git a/src/abi/abi_internal.h b/src/abi/abi_internal.h @@ -20,6 +20,7 @@ typedef struct ABIVtable { /* Per-ABI vtables exposed by their TUs. */ extern const ABIVtable aapcs64_vtable; extern const ABIVtable sysv_x64_vtable; +extern const ABIVtable rv64_vtable; /* Shared TargetABI internals. The struct definition is here so each ABI * TU can reach into the per-TU caches via TargetABI*. abi.c owns the diff --git a/src/abi/abi_rv64.c b/src/abi/abi_rv64.c @@ -0,0 +1,65 @@ +/* RISC-V LP64D ABI — phase-2 stub. + * + * Initial classifier returns ABI_ARG_INDIRECT for everything: correct + * (every value passes through memory), slow, but unblocks bring-up of + * the rv64 codegen path. A future phase replaces this with the real + * RISC-V calling convention (a0..a7 for ints, fa0..fa7 for floats, + * 2*XLEN aggregate flattening, etc.). */ + +#include <string.h> + +#include "abi/abi_internal.h" +#include "core/arena.h" +#include "core/core.h" +#include "core/pool.h" + +static void classify_indirect(TargetABI* a, const Type* t, ABIArgInfo* out, + int is_return) { + if (!t || t->kind == TY_VOID) { + memset(out, 0, sizeof *out); + out->kind = ABI_ARG_IGNORE; + return; + } + ABITypeInfo ti = abi_internal_type_info(a, t); + out->kind = ABI_ARG_INDIRECT; + out->flags = is_return ? ABI_AF_SRET : ABI_AF_BYVAL; + out->indirect_align = ti.align ? ti.align : 8; + out->parts = NULL; + out->nparts = 0; +} + +static ABIFuncInfo* rv64_compute_func_info(TargetABI* a, const Type* fn) { + ABIFuncInfo* info = arena_new(a->c->tu, ABIFuncInfo); + memset(info, 0, sizeof *info); + + classify_indirect(a, fn->fn.ret, &info->ret, /*is_return=*/1); + info->has_sret = (info->ret.kind == ABI_ARG_INDIRECT) ? 1 : 0; + info->variadic = fn->fn.variadic; + + info->nparams = fn->fn.nparams; + if (fn->fn.nparams) { + ABIArgInfo* arr = arena_array(a->c->tu, ABIArgInfo, fn->fn.nparams); + memset(arr, 0, sizeof(ABIArgInfo) * fn->fn.nparams); + for (u16 i = 0; i < fn->fn.nparams; ++i) { + classify_indirect(a, fn->fn.params[i], &arr[i], /*is_return=*/0); + } + info->params = arr; + } else { + info->params = NULL; + } + return info; +} + +static const Type* rv64_va_list_type(TargetABI* a, Pool* p) { + /* RISC-V LP64: va_list is `void *` (one pointer to the next argument + * in memory). Modeled as a typedef-shaped void* — the macro semantics + * resolve through abi_va_list_type and the (future) backend's + * va_start/va_arg lowering. */ + (void)a; + return type_ptr(p, type_void(p)); +} + +const ABIVtable rv64_vtable = { + .compute_func_info = rv64_compute_func_info, + .va_list_type = rv64_va_list_type, +}; diff --git a/src/arch/cgtarget.c b/src/arch/cgtarget.c @@ -1,11 +1,12 @@ /* Public CGTarget constructor — dispatches by Compiler.target.arch. * - * Per-arch constructors live in their own files (aa64.c, x64.c). The - * lifecycle helpers (cgtarget_finalize, cgtarget_free) are arch-agnostic - * shims over the vtable. */ + * Per-arch constructors live in their own files (aa64.c, x64.c, rv64.c). + * The lifecycle helpers (cgtarget_finalize, cgtarget_free) are arch- + * agnostic shims over the vtable. */ #include "arch/aa64.h" #include "arch/arch.h" +#include "arch/rv64.h" #include "arch/x64.h" CGTarget* cgtarget_new(Compiler* c, ObjBuilder* o, MCEmitter* m) { @@ -14,6 +15,8 @@ CGTarget* cgtarget_new(Compiler* c, ObjBuilder* o, MCEmitter* m) { return aa64_cgtarget_new(c, o, m); case CFREE_ARCH_X86_64: return x64_cgtarget_new(c, o, m); + case CFREE_ARCH_RV64: + return rv64_cgtarget_new(c, o, m); default: { SrcLoc loc = {0, 0, 0}; compiler_panic(c, loc, diff --git a/src/arch/rv64.c b/src/arch/rv64.c @@ -0,0 +1,387 @@ +/* riscv64 CGTarget skeleton. + * + * Phase-2 placeholder: the vtable is wired up but every method panics. + * This proves the cgtarget_new dispatch reaches an rv64-shaped target. + * A future phase fills in real codegen — see doc/MULTIARCH.md §4. */ + +#include <string.h> + +#include "arch/arch.h" +#include "arch/rv64.h" +#include "core/arena.h" + +typedef struct RImpl { + CGTarget base; + SrcLoc loc; +} RImpl; + +static SrcLoc rv_loc(void) { return (SrcLoc){0, 0, 0}; } + +_Noreturn static void rv_panic(CGTarget* t, const char* what) { + compiler_panic(t->c, rv_loc(), "rv64: %s not implemented", what); +} + +static void rv_func_begin(CGTarget* t, const CGFuncDesc* d) { + (void)d; + rv_panic(t, "func_begin"); +} +static void rv_func_end(CGTarget* t) { rv_panic(t, "func_end"); } + +static Reg rv_alloc_reg(CGTarget* t, RegClass cls, const Type* ty) { + (void)cls; + (void)ty; + rv_panic(t, "alloc_reg"); +} +static void rv_free_reg(CGTarget* t, Reg r) { + (void)r; + rv_panic(t, "free_reg"); +} +static FrameSlot rv_frame_slot(CGTarget* t, const FrameSlotDesc* d) { + (void)d; + rv_panic(t, "frame_slot"); +} +static void rv_param(CGTarget* t, const CGParamDesc* d) { + (void)d; + rv_panic(t, "param"); +} +static const Reg* rv_clobbers(CGTarget* t, RegClass cls, u32* nregs) { + (void)cls; + (void)nregs; + rv_panic(t, "clobbers"); +} +static void rv_spill_reg(CGTarget* t, Operand a, FrameSlot s, MemAccess m) { + (void)a; + (void)s; + (void)m; + rv_panic(t, "spill_reg"); +} +static void rv_reload_reg(CGTarget* t, Operand a, FrameSlot s, MemAccess m) { + (void)a; + (void)s; + (void)m; + rv_panic(t, "reload_reg"); +} + +static Label rv_label_new(CGTarget* t) { rv_panic(t, "label_new"); } +static void rv_label_place(CGTarget* t, Label l) { + (void)l; + rv_panic(t, "label_place"); +} +static void rv_jump(CGTarget* t, Label l) { + (void)l; + rv_panic(t, "jump"); +} +static void rv_cmp_branch(CGTarget* t, CmpOp op, Operand a, Operand b, + Label l) { + (void)op; + (void)a; + (void)b; + (void)l; + rv_panic(t, "cmp_branch"); +} + +static CGScope rv_scope_begin(CGTarget* t, const CGScopeDesc* d) { + (void)d; + rv_panic(t, "scope_begin"); +} +static void rv_scope_else(CGTarget* t, CGScope s) { + (void)s; + rv_panic(t, "scope_else"); +} +static void rv_scope_end(CGTarget* t, CGScope s) { + (void)s; + rv_panic(t, "scope_end"); +} +static void rv_break_to(CGTarget* t, CGScope s) { + (void)s; + rv_panic(t, "break_to"); +} +static void rv_continue_to(CGTarget* t, CGScope s) { + (void)s; + rv_panic(t, "continue_to"); +} + +static void rv_load_imm(CGTarget* t, Operand d, i64 i) { + (void)d; + (void)i; + rv_panic(t, "load_imm"); +} +static void rv_load_const(CGTarget* t, Operand d, ConstBytes b) { + (void)d; + (void)b; + rv_panic(t, "load_const"); +} +static void rv_copy(CGTarget* t, Operand d, Operand s) { + (void)d; + (void)s; + rv_panic(t, "copy"); +} +static void rv_load(CGTarget* t, Operand d, Operand a, MemAccess m) { + (void)d; + (void)a; + (void)m; + rv_panic(t, "load"); +} +static void rv_store(CGTarget* t, Operand a, Operand s, MemAccess m) { + (void)a; + (void)s; + (void)m; + rv_panic(t, "store"); +} +static void rv_addr_of(CGTarget* t, Operand d, Operand l) { + (void)d; + (void)l; + rv_panic(t, "addr_of"); +} +static void rv_tls_addr_of(CGTarget* t, Operand d, ObjSymId s, i64 a) { + (void)d; + (void)s; + (void)a; + rv_panic(t, "tls_addr_of"); +} +static void rv_copy_bytes(CGTarget* t, Operand da, Operand sa, + AggregateAccess g) { + (void)da; + (void)sa; + (void)g; + rv_panic(t, "copy_bytes"); +} +static void rv_set_bytes(CGTarget* t, Operand da, Operand bv, + AggregateAccess g) { + (void)da; + (void)bv; + (void)g; + rv_panic(t, "set_bytes"); +} +static void rv_bitfield_load(CGTarget* t, Operand d, Operand ra, + BitFieldAccess b) { + (void)d; + (void)ra; + (void)b; + rv_panic(t, "bitfield_load"); +} +static void rv_bitfield_store(CGTarget* t, Operand ra, Operand s, + BitFieldAccess b) { + (void)ra; + (void)s; + (void)b; + rv_panic(t, "bitfield_store"); +} + +static void rv_binop(CGTarget* t, BinOp op, Operand d, Operand a, Operand b) { + (void)op; + (void)d; + (void)a; + (void)b; + rv_panic(t, "binop"); +} +static void rv_unop(CGTarget* t, UnOp op, Operand d, Operand a) { + (void)op; + (void)d; + (void)a; + rv_panic(t, "unop"); +} +static void rv_cmp(CGTarget* t, CmpOp op, Operand d, Operand a, Operand b) { + (void)op; + (void)d; + (void)a; + (void)b; + rv_panic(t, "cmp"); +} +static void rv_convert(CGTarget* t, ConvKind k, Operand d, Operand s) { + (void)k; + (void)d; + (void)s; + rv_panic(t, "convert"); +} + +static void rv_call(CGTarget* t, const CGCallDesc* d) { + (void)d; + rv_panic(t, "call"); +} +static void rv_ret(CGTarget* t, const CGABIValue* v) { + (void)v; + rv_panic(t, "ret"); +} + +static void rv_alloca_(CGTarget* t, Operand d, Operand s, u32 a) { + (void)d; + (void)s; + (void)a; + rv_panic(t, "alloca"); +} +static void rv_va_start_(CGTarget* t, Operand a) { + (void)a; + rv_panic(t, "va_start"); +} +static void rv_va_arg_(CGTarget* t, Operand d, Operand a, const Type* ty) { + (void)d; + (void)a; + (void)ty; + rv_panic(t, "va_arg"); +} +static void rv_va_end_(CGTarget* t, Operand a) { + (void)a; + rv_panic(t, "va_end"); +} +static void rv_va_copy_(CGTarget* t, Operand d, Operand s) { + (void)d; + (void)s; + rv_panic(t, "va_copy"); +} + +static void rv_atomic_load(CGTarget* t, Operand d, Operand a, MemAccess m, + MemOrder o) { + (void)d; + (void)a; + (void)m; + (void)o; + rv_panic(t, "atomic_load"); +} +static void rv_atomic_store(CGTarget* t, Operand a, Operand s, MemAccess m, + MemOrder o) { + (void)a; + (void)s; + (void)m; + (void)o; + rv_panic(t, "atomic_store"); +} +static void rv_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; + rv_panic(t, "atomic_rmw"); +} +static void rv_atomic_cas(CGTarget* t, Operand p, Operand ok, Operand a, + Operand e, Operand des, MemAccess m, MemOrder so, + MemOrder fo) { + (void)p; + (void)ok; + (void)a; + (void)e; + (void)des; + (void)m; + (void)so; + (void)fo; + rv_panic(t, "atomic_cas"); +} +static void rv_fence(CGTarget* t, MemOrder o) { + (void)o; + rv_panic(t, "fence"); +} + +static void rv_intrinsic(CGTarget* t, IntrinKind k, Operand* d, u32 nd, + const Operand* a, u32 na) { + (void)k; + (void)d; + (void)nd; + (void)a; + (void)na; + rv_panic(t, "intrinsic"); +} +static void rv_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; + rv_panic(t, "asm_block"); +} + +static void rv_set_loc(CGTarget* t, SrcLoc l) { + ((RImpl*)t)->loc = l; + if (t->mc) t->mc->set_loc(t->mc, l); +} + +static void rv_finalize(CGTarget* t) { (void)t; } +static void rv_destroy(CGTarget* t) { (void)t; } + +static void cgt_cleanup(void* arg) { cgtarget_free((CGTarget*)arg); } + +CGTarget* rv64_cgtarget_new(Compiler* c, ObjBuilder* o, MCEmitter* m) { + RImpl* x = arena_new(c->tu, RImpl); + memset(x, 0, sizeof *x); + + CGTarget* t = &x->base; + t->c = c; + t->obj = o; + t->mc = m; + + t->func_begin = rv_func_begin; + t->func_end = rv_func_end; + + t->alloc_reg = rv_alloc_reg; + t->free_reg = rv_free_reg; + t->frame_slot = rv_frame_slot; + t->param = rv_param; + t->clobbers = rv_clobbers; + t->spill_reg = rv_spill_reg; + t->reload_reg = rv_reload_reg; + + t->label_new = rv_label_new; + t->label_place = rv_label_place; + t->jump = rv_jump; + t->cmp_branch = rv_cmp_branch; + + t->scope_begin = rv_scope_begin; + t->scope_else = rv_scope_else; + t->scope_end = rv_scope_end; + t->break_to = rv_break_to; + t->continue_to = rv_continue_to; + + t->load_imm = rv_load_imm; + t->load_const = rv_load_const; + t->copy = rv_copy; + t->load = rv_load; + t->store = rv_store; + t->addr_of = rv_addr_of; + t->tls_addr_of = rv_tls_addr_of; + t->copy_bytes = rv_copy_bytes; + t->set_bytes = rv_set_bytes; + t->bitfield_load = rv_bitfield_load; + t->bitfield_store = rv_bitfield_store; + + t->binop = rv_binop; + t->unop = rv_unop; + t->cmp = rv_cmp; + t->convert = rv_convert; + + t->call = rv_call; + t->ret = rv_ret; + + t->alloca_ = rv_alloca_; + t->va_start_ = rv_va_start_; + t->va_arg_ = rv_va_arg_; + t->va_end_ = rv_va_end_; + t->va_copy_ = rv_va_copy_; + + t->setjmp_ = NULL; + t->longjmp_ = NULL; + + t->atomic_load = rv_atomic_load; + t->atomic_store = rv_atomic_store; + t->atomic_rmw = rv_atomic_rmw; + t->atomic_cas = rv_atomic_cas; + t->fence = rv_fence; + + t->intrinsic = rv_intrinsic; + t->asm_block = rv_asm_block; + + t->set_loc = rv_set_loc; + t->finalize = rv_finalize; + t->destroy = rv_destroy; + + compiler_defer(c, cgt_cleanup, t); + return t; +} diff --git a/src/arch/rv64.h b/src/arch/rv64.h @@ -0,0 +1,8 @@ +#ifndef CFREE_ARCH_RV64_H +#define CFREE_ARCH_RV64_H + +#include "arch/arch.h" + +CGTarget* rv64_cgtarget_new(Compiler*, ObjBuilder*, MCEmitter*); + +#endif diff --git a/src/link/link_elf.c b/src/link/link_elf.c @@ -584,6 +584,9 @@ void link_emit_elf(LinkImage* img, Writer* w) { case CFREE_ARCH_X86_64: e_machine = EM_X86_64; break; + case CFREE_ARCH_RV64: + e_machine = EM_RISCV; + break; default: compiler_panic(c, no_loc(), "link_emit_elf: unsupported target arch %u", diff --git a/src/obj/elf.h b/src/obj/elf.h @@ -50,6 +50,7 @@ /* ---- e_machine ---- */ #define EM_X86_64 0x3E #define EM_AARCH64 0xB7 +#define EM_RISCV 0xF3 /* ---- header sizes (also literal e_*size fields) ---- */ #define ELF64_EHDR_SIZE 64 @@ -263,6 +264,29 @@ u32 elf_aarch64_reloc_from(u32 elf_type); u32 elf_x86_64_reloc_to(u32 kind /* RelocKind */); u32 elf_x86_64_reloc_from(u32 elf_type); +/* ---- RISC-V relocation types ---- + * + * Subset matching the cfree-canonical RelocKind R_RV_* entries. The + * full RISC-V ELF psABI has many more (TLS, GOT variants, compressed + * forms, ...) — only the ones the codegen and linker actually need + * today are represented here. */ +#define ELF_R_RISCV_NONE 0 +#define ELF_R_RISCV_32 1 +#define ELF_R_RISCV_64 2 +#define ELF_R_RISCV_RELATIVE 3 +#define ELF_R_RISCV_COPY 4 +#define ELF_R_RISCV_JUMP_SLOT 5 +#define ELF_R_RISCV_BRANCH 16 +#define ELF_R_RISCV_JAL 17 +#define ELF_R_RISCV_CALL 18 +#define ELF_R_RISCV_CALL_PLT 19 +#define ELF_R_RISCV_HI20 26 +#define ELF_R_RISCV_LO12_I 27 +#define ELF_R_RISCV_LO12_S 28 + +u32 elf_riscv64_reloc_to(u32 kind /* RelocKind */); +u32 elf_riscv64_reloc_from(u32 elf_type); + /* ---- little-endian byte writers/readers (Writer-based) ---- * Reads use rd_u*_le from core/bytes.h directly; only writes need the * Writer-bridging wrappers below. */ diff --git a/src/obj/elf_emit.c b/src/obj/elf_emit.c @@ -256,6 +256,10 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { e_machine = EM_X86_64; reloc_to = elf_x86_64_reloc_to; break; + case CFREE_ARCH_RV64: + e_machine = EM_RISCV; + reloc_to = elf_riscv64_reloc_to; + break; default: compiler_panic(c, no_loc(), "emit_elf: unsupported target arch %u", diff --git a/src/obj/elf_reloc_riscv64.c b/src/obj/elf_reloc_riscv64.c @@ -0,0 +1,66 @@ +/* RelocKind <-> RISC-V ELF reloc-type mapping. + * + * Mirror of elf_reloc_x86_64.c for the RISC-V LP64 ABI. The arch- + * agnostic R_ABS / R_PC RelocKind entries fan out to the native + * RISC-V codes; the RISC-V-specific encodings (HI20/LO12, BRANCH, + * JAL, CALL, dynamic-only entries) live in the lower band as + * R_RV_*. + * + * Returning ELF_R_RISCV_NONE for an unsupported kind is the signal + * to the caller to either panic (emit) or panic (read with diagnostic). */ + +#include "obj/elf.h" + +u32 elf_riscv64_reloc_to(u32 kind /* RelocKind */) { + switch (kind) { + case R_NONE: + return ELF_R_RISCV_NONE; + case R_ABS64: + return ELF_R_RISCV_64; + case R_ABS32: + return ELF_R_RISCV_32; + case R_RV_HI20: + return ELF_R_RISCV_HI20; + case R_RV_LO12_I: + return ELF_R_RISCV_LO12_I; + case R_RV_LO12_S: + return ELF_R_RISCV_LO12_S; + case R_RV_BRANCH: + return ELF_R_RISCV_BRANCH; + case R_RV_JAL: + return ELF_R_RISCV_JAL; + case R_RV_CALL: + return ELF_R_RISCV_CALL; + case R_PLT32: + return ELF_R_RISCV_CALL_PLT; + default: + return ELF_R_RISCV_NONE; + } +} + +u32 elf_riscv64_reloc_from(u32 elf_type) { + switch (elf_type) { + case ELF_R_RISCV_NONE: + return R_NONE; + case ELF_R_RISCV_64: + return R_ABS64; + case ELF_R_RISCV_32: + return R_ABS32; + case ELF_R_RISCV_HI20: + return R_RV_HI20; + case ELF_R_RISCV_LO12_I: + return R_RV_LO12_I; + case ELF_R_RISCV_LO12_S: + return R_RV_LO12_S; + case ELF_R_RISCV_BRANCH: + return R_RV_BRANCH; + case ELF_R_RISCV_JAL: + return R_RV_JAL; + case ELF_R_RISCV_CALL: + return R_RV_CALL; + case ELF_R_RISCV_CALL_PLT: + return R_PLT32; + default: + return (u32)-1; /* sentinel */ + } +} diff --git a/test/lib/exec_target.sh b/test/lib/exec_target.sh @@ -24,7 +24,7 @@ # exec_target_supported <arch> # Returns 0 if some runner is available for arch on this host. # -# Recognized arches: aarch64, x64. Each maps to a podman --platform +# Recognized arches: aarch64, x64, rv64. Each maps to a podman --platform # string and an optional user-mode qemu binary detected on the host. # # Caller contract: @@ -61,6 +61,7 @@ _exec_target_platform() { case "$1" in aarch64) echo "linux/arm64" ;; x64) echo "linux/amd64" ;; + rv64) echo "linux/riscv64" ;; *) echo "" ;; esac } @@ -69,6 +70,7 @@ _exec_target_image() { case "$1" in aarch64) echo "${RUN_AARCH64_IMAGE:-alpine:latest}" ;; x64) echo "${RUN_X64_IMAGE:-alpine:latest}" ;; + rv64) echo "${RUN_RV64_IMAGE:-alpine:latest}" ;; *) echo "alpine:latest" ;; esac } @@ -89,6 +91,7 @@ _exec_target_podman_native() { aarch64) [ "${is_aarch64:-0}" -eq 1 ] ;; x64) [ "$(uname -m 2>/dev/null)" = "x86_64" ] || \ [ "$(uname -m 2>/dev/null)" = "amd64" ] ;; + rv64) [ "$(uname -m 2>/dev/null)" = "riscv64" ] ;; *) return 1 ;; esac } @@ -98,6 +101,16 @@ _exec_target_qemu() { aarch64) [ "${have_qemu:-0}" -eq 1 ] && echo "${QEMU_BIN:-}" ;; x64) # No qemu-user fallback for x64 in current harnesses. echo "" ;; + rv64) # qemu-riscv64 user-mode is the easiest way to exec + # rv64 ELFs on a non-rv64 host without podman. + if [ -n "${QEMU_RV64_BIN:-}" ]; then + echo "${QEMU_RV64_BIN}" + elif command -v qemu-riscv64 >/dev/null 2>&1; then + command -v qemu-riscv64 + else + echo "" + fi + ;; *) echo "" ;; esac } diff --git a/test/smoke/rv64.sh b/test/smoke/rv64.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# test/smoke/rv64.sh — end-to-end smoke test for the rv64 podman/qemu path. +# +# Phase-2 of doc/MULTIARCH.md: prove the test/lib/exec_target.sh helper +# can build, queue, and run a riscv64-linux ELF before any cfree-emitted +# rv64 bytes exist. Builds a tiny freestanding static executable with +# clang --target=riscv64-linux-gnu and pushes it through +# exec_target_run / exec_target_queue+flush, asserting the expected +# exit code on both paths. +# +# Skipped if clang lacks the riscv64-linux-gnu target or no runner +# (podman or qemu-riscv64) is available. Mirrors test/cg's skip-vs-fail +# convention: skip is treated as failure unless CFREE_TEST_ALLOW_SKIP=1. + +set -u + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BUILD_DIR="$ROOT/build/test/smoke-rv64" +mkdir -p "$BUILD_DIR" + +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"; } + +ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" + +# ---- detect prerequisites -------------------------------------------------- + +CLANG_TARGET="--target=riscv64-linux-gnu" +have_clang_rv64=0 +if clang $CLANG_TARGET -march=rv64gc -c -x c - -o /dev/null < /dev/null 2>/dev/null; then + have_clang_rv64=1 +fi + +# Cross-link wants an ELF-aware ld. On macOS the host /usr/bin/ld is +# Mach-O only; insist on lld. On a Linux host the default host linker +# typically can't produce rv64 ELF either unless cross-tooling is +# installed, so lld is the simplest portable choice. +have_lld=0 +command -v ld.lld >/dev/null 2>&1 && have_lld=1 + +# Variables expected by exec_target.sh. The aarch64 helper expects +# these names regardless of target arch — they describe the host +# detection rather than the target. For rv64-only smoke we don't need +# QEMU_BIN (that's the aarch64 user-mode qemu); rv64 picks up +# qemu-riscv64 automatically inside _exec_target_qemu. +have_qemu=0 +QEMU_BIN="" +have_podman=0 +command -v podman >/dev/null 2>&1 && have_podman=1 +arch_raw="$(uname -m 2>/dev/null || true)" +is_aarch64=0 +{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 +export have_qemu QEMU_BIN have_podman is_aarch64 + +EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" +# shellcheck source=../lib/exec_target.sh +source "$ROOT/test/lib/exec_target.sh" + +PASS=0; FAIL=0; SKIP=0 +note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } +note_fail() { FAIL=$((FAIL+1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } +note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } + +if [ $have_clang_rv64 -eq 0 ]; then + note_skip "build" "clang --target=riscv64-linux-gnu unavailable" + printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" + if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi + exit 1 +fi +if [ $have_lld -eq 0 ]; then + note_skip "build" "ld.lld unavailable (needed for ELF cross-link)" + printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" + if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi + exit 1 +fi +if ! exec_target_supported rv64; then + note_skip "exec" "no runner for rv64 (podman or qemu-riscv64)" + printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" + if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi + exit 1 +fi + +# ---- build a tiny freestanding riscv64 ELF ---------------------------------- + +# Direct syscall in _start: SYS_exit_group on rv64 is 94 (a7), exit +# code 42 (a0). No libc, no relocations, no PIE. The point is to +# exercise the harness pipeline (clang cross-compile → podman/qemu +# run → recorded rc), not to build a complete program. +SRC="$BUILD_DIR/smoke.c" +cat >"$SRC" <<'EOF' +__attribute__((noreturn)) void _start(void) { + register long a7 __asm__("a7") = 94; /* sys_exit_group */ + register long a0 __asm__("a0") = 42; + __asm__ volatile("ecall" : : "r"(a7), "r"(a0) : "memory"); + __builtin_unreachable(); +} +EOF + +EXE="$BUILD_DIR/smoke.exe" +if ! clang $CLANG_TARGET -march=rv64gc -fuse-ld=lld \ + -O1 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie -nostdlib -static \ + -Wl,-e,_start \ + "$SRC" -o "$EXE" 2>"$BUILD_DIR/build.err"; then + note_fail "build (see $BUILD_DIR/build.err)" + printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" + exit 1 +fi + +# ---- exec_target_run --------------------------------------------------------- + +exec_target_run rv64 "$EXE" "$BUILD_DIR/run.out" "$BUILD_DIR/run.err" +if [ "$RUN_RC" -eq 42 ]; then + note_pass "exec_target_run rv64 (rc=42)" +else + note_fail "exec_target_run rv64 (expected 42 got $RUN_RC; see $BUILD_DIR/run.err)" +fi + +# ---- exec_target_queue + flush ---------------------------------------------- + +exec_target_queue rv64 smoke "$EXE" \ + "$BUILD_DIR/q.out" "$BUILD_DIR/q.err" "$BUILD_DIR/q.rc" +exec_target_flush +if [ ! -f "$BUILD_DIR/q.rc" ]; then + note_fail "exec_target_flush rv64 (no rc file produced)" +else + Q_RC="$(cat "$BUILD_DIR/q.rc")" + if [ "$Q_RC" -eq 42 ]; then + note_pass "exec_target_queue+flush rv64 (rc=42)" + else + note_fail "exec_target_queue+flush rv64 (expected 42 got $Q_RC; see $BUILD_DIR/q.err)" + fi +fi + +printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" +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 @@ -24,7 +24,7 @@ # against the public cfree.h surface; reuses cfree-roundtrip, # link-exe-runner, and jit-runner. -.PHONY: test test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg test-dwarf test-debug test-parse test-parse-err test-musl test-glibc test-lib-deps test-smoke-x64 +.PHONY: test test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg test-dwarf test-debug test-parse test-parse-err test-musl test-glibc test-lib-deps test-smoke-x64 test-smoke-rv64 test: test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg test-dwarf test-debug test-parse test-parse-err test-lib-deps @@ -139,6 +139,16 @@ test-parse-err: lib $(PARSE_RUNNER) test-smoke-x64: bash test/smoke/x64.sh +# test-smoke-rv64: phase-2 counterpart of test-smoke-x64. Builds a +# tiny freestanding riscv64 ELF with clang --target=riscv64-linux-gnu +# and runs it through test/lib/exec_target.sh, proving the rv64 lane +# of the harness end-to-end before any cfree-emitted rv64 bytes +# exist. Excluded from the default `test` target because it needs +# qemu-riscv64 (or podman with riscv64 emulation) + lld; opt-in via +# `make test-smoke-rv64`. +test-smoke-rv64: + bash test/smoke/rv64.sh + # test-musl / test-glibc: end-to-end static + dynamic libc link/run on # aarch64. Each variant pulls its own pinned sysroot (podman, ~30s on # first run) and shares the same case files under test/libc/cases/: