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:
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/: