From 8364f9373d062a430f6e72a6973794de178895c6 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Thu, 21 May 2026 10:37:46 +0200 Subject: [PATCH] allow fn ref through c abi --- src/astgen.cpp | 30 +++++ src/jam_llvm.cpp | 16 +++ src/jam_llvm.h | 16 +++ src/jir.h | 15 +++ src/jir_codegen.cpp | 28 +++++ src/jir_verify.cpp | 13 +++ tests/cpp/test_codegen_errors.cpp | 176 ++++++++++++++++++++++++++++++ 7 files changed, 294 insertions(+) diff --git a/src/astgen.cpp b/src/astgen.cpp index 48a9023..f7c4429 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -740,6 +740,26 @@ static JirRef astgenVariable(AstGenCtx &gctx, const AstNode &n) { if (const auto *mc = gctx.ctx.getModuleConst(name)) { return astgenExpr(gctx, mc->initExpr, mc->declaredType); } + // Fn-name-as-value (Rust-style item coercion). The identifier + // resolves to a function symbol — surface its address as a u64 + // so it can be assigned to a u64 slot, written to a buffer, or + // (with `as *mut[] u8`) round-tripped to a typed pointer. Generic + // fns are rejected because no monomorphized body exists at this + // point in lowering. + if (const FunctionAST *fn = gctx.ctx.getFunctionAST(name)) { + if (fn->isGeneric()) { + return recoverHere(gctx, + "cannot take address of generic fn `" + name + + "`", + kNoType); + } + JirInst fnref{}; + fnref.tag = JirTag::FnRef; + fnref.a = static_cast( + gctx.ctx.getStringPool().intern(fn->Name)); + fnref.ty = BuiltinType::U64; + return emit(gctx, fnref); + } // Recoverable: emit a Poison so the rest of the function still // gets analyzed (and additional errors reported in the same pass). return recoverHere(gctx, "unknown variable `" + name + "`", kNoType); @@ -1586,6 +1606,16 @@ static JirRef astgenAsCast(AstGenCtx &gctx, const AstNode &n) { inst.ty = dstTy; return emit(gctx, inst); } + // Pointer ↔ integer cast — only u64 is wide enough to round-trip + // a pointer on every supported target, so restrict to that width. + // `myPtr as u64` or `addr as *mut[] u8`. Stays out of the int↔int + // path below because that one issues SExt/Trunc instead. + if (isPtr(src) && dst.kind == TypeKind::Int && dst.a == 64) { + return emitCast(JirTag::PtrToInt); + } + if (src.kind == TypeKind::Int && src.a == 64 && isPtr(dst)) { + return emitCast(JirTag::IntToPtr); + } if (src.kind == TypeKind::Int && dst.kind == TypeKind::Int) { uint32_t sw = src.a; uint32_t dw = dst.a; diff --git a/src/jam_llvm.cpp b/src/jam_llvm.cpp index a8070e4..42084b9 100644 --- a/src/jam_llvm.cpp +++ b/src/jam_llvm.cpp @@ -813,6 +813,22 @@ JamValueRef JamLLVMBuildIntCast(JamBuilderRef builder, JamValueRef val, UNWRAP_VALUE(val), UNWRAP_TYPE(destType), isSigned, name)); } +JamValueRef JamLLVMBuildPtrToInt(JamBuilderRef builder, JamValueRef val, + JamTypeRef destType, const char *name) { + return WRAP_VALUE(UNWRAP_BUILDER(builder)->CreatePtrToInt( + UNWRAP_VALUE(val), UNWRAP_TYPE(destType), name)); +} + +JamValueRef JamLLVMBuildIntToPtr(JamBuilderRef builder, JamValueRef val, + JamTypeRef destType, const char *name) { + return WRAP_VALUE(UNWRAP_BUILDER(builder)->CreateIntToPtr( + UNWRAP_VALUE(val), UNWRAP_TYPE(destType), name)); +} + +JamValueRef JamLLVMFunctionAsValue(JamFunctionRef func) { + return WRAP_VALUE(static_cast(UNWRAP_FUNCTION(func))); +} + JamValueRef JamLLVMBuildSIToFP(JamBuilderRef builder, JamValueRef val, JamTypeRef destType, const char *name) { return WRAP_VALUE(UNWRAP_BUILDER(builder)->CreateSIToFP( diff --git a/src/jam_llvm.h b/src/jam_llvm.h index eb030bb..a8fe88d 100644 --- a/src/jam_llvm.h +++ b/src/jam_llvm.h @@ -330,6 +330,22 @@ JAM_EXTERN_C JamValueRef JamLLVMBuildBitCast(JamBuilderRef builder, JamValueRef val, JamTypeRef destType, const char *name); +// Raw-address conversions used to lower Jam's `ptr as int` and +// `int as ptr` casts (plus the FnRef path which surfaces a function's +// address as a u64). Both lower to a single LLVM instruction. +JAM_EXTERN_C JamValueRef JamLLVMBuildPtrToInt(JamBuilderRef builder, + JamValueRef val, + JamTypeRef destType, + const char *name); +JAM_EXTERN_C JamValueRef JamLLVMBuildIntToPtr(JamBuilderRef builder, + JamValueRef val, + JamTypeRef destType, + const char *name); +// View a function as a generic Value pointer — needed because +// llvm::Function inherits from llvm::Value, and the rest of the +// codegen plumbing speaks in JamValueRef. Used by the FnRef lowering +// to feed the function into JamLLVMBuildPtrToInt. +JAM_EXTERN_C JamValueRef JamLLVMFunctionAsValue(JamFunctionRef func); JAM_EXTERN_C JamValueRef JamLLVMBuildIntCast(JamBuilderRef builder, JamValueRef val, JamTypeRef destType, bool isSigned, diff --git a/src/jir.h b/src/jir.h index b7cdc6c..441a264 100644 --- a/src/jir.h +++ b/src/jir.h @@ -130,6 +130,13 @@ enum class JirTag : uint8_t { FPExt, FPTrunc, BitCast, + // Pointer ↔ integer conversions. PtrToInt: `a` = ptr-typed value + // ref, `ty` = destination int type. IntToPtr: `a` = int-typed + // value ref, `ty` = destination ptr type. Used for raw-address + // round-trips (Rust-style `ptr as u64` / `u64 as *mut T`) and to + // surface function addresses as integers (paired with `FnRef`). + PtrToInt, + IntToPtr, // Control flow // Br: `a` = JirBlockRef target. No result. @@ -157,6 +164,14 @@ enum class JirTag : uint8_t { // `ty` = return type (kNoType for void). Call, + // Function reference (Rust-style item-as-value). Resolves a fn + // name to its address. `a` = StringIdx (function's LLVM symbol + // name); `ty` = u64. Generic functions are rejected at AstGen + // (no monomorphized body exists yet). Codegen lowers to + // `ptrtoint ptr @ to i64` so the result drops straight into + // a u64 slot — pair with IntToPtr if a typed pointer is needed. + FnRef, + // Function parameter access // Param: `a` = parameter index; `ty` = param type. Param, diff --git a/src/jir_codegen.cpp b/src/jir_codegen.cpp index 53a0f2d..52ba24f 100644 --- a/src/jir_codegen.cpp +++ b/src/jir_codegen.cpp @@ -362,6 +362,34 @@ static JamValueRef emitInstImpl(JirCodegenCtx &lctx, JirRef r) { JamTypeRef ty = lctx.ctx.getLLVMType(inst.ty); return JamLLVMBuildBitCast(lctx.ctx.getBuilder(), v, ty, "bitcast"); } + case JirTag::PtrToInt: { + JamValueRef v = emitInst(lctx, inst.a); + JamTypeRef ty = lctx.ctx.getLLVMType(inst.ty); + return JamLLVMBuildPtrToInt(lctx.ctx.getBuilder(), v, ty, "p2i"); + } + case JirTag::IntToPtr: { + JamValueRef v = emitInst(lctx, inst.a); + JamTypeRef ty = lctx.ctx.getLLVMType(inst.ty); + return JamLLVMBuildIntToPtr(lctx.ctx.getBuilder(), v, ty, "i2p"); + } + case JirTag::FnRef: { + // `inst.a` carries the StringIdx of the LLVM symbol name. We + // resolve to the LLVM Function (already a ptr-typed Value), + // then lower to ptrtoint to match the JIR's u64 result type. + // This mirrors Rust's `my_fn as u64` lowering. + StringIdx nameId = static_cast(inst.a); + const std::string &name = lctx.ctx.getStringPool().get(nameId); + JamFunctionRef f = + JamLLVMGetFunction(lctx.ctx.getModule(), name.c_str()); + if (!f) { + throw std::runtime_error("jirCodegen: unknown fn-ref `" + name + + "`"); + } + JamValueRef fnVal = JamLLVMFunctionAsValue(f); + JamTypeRef ty = lctx.ctx.getLLVMType(inst.ty); + return JamLLVMBuildPtrToInt(lctx.ctx.getBuilder(), fnVal, ty, + "fnref.u64"); + } // === Aggregates === case JirTag::StructLit: { JamTypeRef ty = lctx.ctx.getLLVMType(inst.ty); diff --git a/src/jir_verify.cpp b/src/jir_verify.cpp index 4717a0d..de3564a 100644 --- a/src/jir_verify.cpp +++ b/src/jir_verify.cpp @@ -133,6 +133,12 @@ const char *tagName(JirTag t) { return "FPTrunc"; case JirTag::BitCast: return "BitCast"; + case JirTag::PtrToInt: + return "PtrToInt"; + case JirTag::IntToPtr: + return "IntToPtr"; + case JirTag::FnRef: + return "FnRef"; case JirTag::Br: return "Br"; case JirTag::CondBr: @@ -343,12 +349,19 @@ struct Verifier { case JirTag::FPExt: case JirTag::FPTrunc: case JirTag::BitCast: + case JirTag::PtrToInt: + case JirTag::IntToPtr: case JirTag::FieldAccess: case JirTag::ExtractValue: case JirTag::FieldAddr: case JirTag::EnumPayload: checkRef(inst.a, false, r, "a"); return; + case JirTag::FnRef: + // `a` is a StringIdx, not a JirRef — nothing to check + // against `insts.size()`. Codegen will fail loudly if the + // referenced function symbol isn't in the module. + return; case JirTag::DropBinding: { checkRef(inst.a, false, r, "a"); // The `b` field is a StringIdx pointing at the LLVM diff --git a/tests/cpp/test_codegen_errors.cpp b/tests/cpp/test_codegen_errors.cpp index dcf753b..ab0c73a 100644 --- a/tests/cpp/test_codegen_errors.cpp +++ b/tests/cpp/test_codegen_errors.cpp @@ -57,6 +57,30 @@ bool stderrContains(const CompileResult &r, const std::string &substr) { return r.stderr_.find(substr) != std::string::npos; } +// `--emit-ir` variant. Skips the link step (so it doesn't try to drop +// the binary into `./output`, which is the build directory) and pipes +// LLVM IR back through the same stdout/stderr channel. Positive tests +// match on IR substrings; negative tests match on the diagnostic. +CompileResult compileSourceIR(const std::string &name, + const std::string &source) { + std::string path = "/tmp/" + name + ".jam"; + { + std::ofstream out(path); + out << source; + } + std::string cmd = "./jam.out --emit-ir " + path + " 2>&1"; + + std::string output; + FILE *pipe = popen(cmd.c_str(), "r"); + if (!pipe) { throw std::runtime_error("popen failed: " + cmd); } + char buf[256]; + while (fgets(buf, sizeof(buf), pipe) != nullptr) output += buf; + int status = pclose(pipe); + + int exitCode = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + return {exitCode, std::move(output)}; +} + // Multi-file variant: writes `main.jam` and `lib.jam` into a fresh // /tmp directory, then runs jam.out on main.jam. The module resolver // uses main.jam's directory as `baseDir`, so `import("lib")` from @@ -133,6 +157,27 @@ class CodegenErrorTests { framework.addTest( "Codegen - noreturn fn whose body may fall through is rejected", testNoreturnFallsThroughRejected); + // Fn-as-value (Rust-style item coercion) + ptr↔int casts. + framework.addTest("FnRef - bare fn name lowers to ptrtoint @fn", + testFnRefBareName); + framework.addTest("FnRef - explicit `fn as u64` lowers to ptrtoint", + testFnRefAsU64); + framework.addTest("FnRef - ptr ↔ u64 round-trips via ptrtoint/inttoptr", + testPtrU64RoundTrip); + framework.addTest("FnRef - extern fn name resolves to its address", + testFnRefExternFn); + framework.addTest( + "FnRef - generic fn rejected with `cannot take address` diagnostic", + testFnRefGenericRejected); + framework.addTest( + "FnRef - ptr as u32 (narrower than u64) is rejected", + testPtrAsNarrowIntRejected); + framework.addTest( + "FnRef - u32 as *mut[] u8 (narrower than u64) is rejected", + testNarrowIntAsPtrRejected); + framework.addTest( + "FnRef - truly unknown variable still errors (no fn fallback)", + testUnknownVariableStillErrors); } private: @@ -409,6 +454,137 @@ fn main() {} ASSERT_TRUE(r.exitCode != 0); ASSERT_TRUE(stderrContains(r, "Private")); } + + // === Fn-as-value (Rust-style item coercion) + ptr↔int casts ==== + // + // These exercise the `export fn` callback workflow needed for + // SDL_AudioSpec-style C-ABI callbacks. The bare-name form mirrors + // Rust's implicit fn-item coercion; the `as u64` form is the + // explicit cast. Both must lower to LLVM `ptrtoint`. + + // Bare fn name in expression position binds as a u64 — the + // "coercion" branch of the AsCast early-exits when src == dst so + // we should see a direct ptrtoint store with no extra cast IR. + static void testFnRefBareName() { + auto r = compileSourceIR("fnref_bare", R"( +export fn cb(ud: u64, s: *mut[] u8, len: i32) { s[0] = 1; } +fn main() { + var addr: u64 = cb; +} +)"); + ASSERT_TRUE(r.exitCode == 0); + // Function should be externally linked, C-ABI (no `internal`). + ASSERT_TRUE(stderrContains(r, "define void @cb")); + // The fn-ref must lower to ptrtoint of the named symbol. + ASSERT_TRUE(stderrContains(r, "ptrtoint (ptr @cb to i64)")); + } + + // Explicit `fn as u64` cast. With FnRef typed as u64 the cast is + // a no-op at the JIR level — same ptrtoint instruction. + static void testFnRefAsU64() { + auto r = compileSourceIR("fnref_as_u64", R"( +export fn cb(ud: u64, s: *mut[] u8, len: i32) { s[0] = 1; } +fn main() { + var addr: u64 = cb as u64; +} +)"); + ASSERT_TRUE(r.exitCode == 0); + ASSERT_TRUE(stderrContains(r, "ptrtoint (ptr @cb to i64)")); + } + + // Pointer ↔ u64 round-trip. Confirms both ptrtoint and inttoptr + // branches in astgenAsCast / jirCodegen exist and produce the + // matching LLVM instructions. + static void testPtrU64RoundTrip() { + auto r = compileSourceIR("ptr_u64_round_trip", R"( +extern fn malloc(size: u64) *mut[] u8; +fn main() { + var p: *mut[] u8 = malloc(16); + var a: u64 = p as u64; + var p2: *mut[] u8 = a as *mut[] u8; + p2[0] = 99; +} +)"); + ASSERT_TRUE(r.exitCode == 0); + ASSERT_TRUE(stderrContains(r, "ptrtoint ptr")); + ASSERT_TRUE(stderrContains(r, "inttoptr i64")); + } + + // Pure `extern fn` (no body) should still be referenceable by + // name — the symbol resolves to the LLVM `declare` placeholder + // and ptrtoint folds it just like a defined function. + static void testFnRefExternFn() { + auto r = compileSourceIR("fnref_extern", R"( +extern fn malloc(size: u64) *mut[] u8; +fn main() { + var addr: u64 = malloc; +} +)"); + ASSERT_TRUE(r.exitCode == 0); + ASSERT_TRUE(stderrContains(r, "ptrtoint (ptr @malloc to i64)")); + } + + // Taking the address of a generic fn is meaningless before + // monomorphization — no concrete LLVM symbol exists yet. AstGen + // surfaces a precise diagnostic naming both the action and the + // fn. + static void testFnRefGenericRejected() { + auto r = compileSource("fnref_generic", R"( +fn identity(T: type, x: T) T { return x; } +fn main() { + var a: u64 = identity as u64; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "cannot take address of generic fn")); + ASSERT_TRUE(stderrContains(r, "identity")); + } + + // Pointers are 64-bit on every target Jam supports. Casting to a + // narrower int width would silently truncate the upper bits and + // is rejected up front rather than letting LLVM emit a lossy + // truncate. The user can always do `(p as u64) as u32` if they + // really want the lower 32 bits. + static void testPtrAsNarrowIntRejected() { + auto r = compileSource("ptr_as_narrow_int", R"( +extern fn malloc(size: u64) *mut[] u8; +fn main() { + var p: *mut[] u8 = malloc(8); + var a: u32 = p as u32; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "unsupported `as` cast")); + } + + // Mirror of the above for the other direction: only u64 → ptr + // is accepted (a u32 can't carry a full target pointer). + static void testNarrowIntAsPtrRejected() { + auto r = compileSource("narrow_int_as_ptr", R"( +fn main() { + var n: u32 = 0xFF; + var p: *mut[] u8 = n as *mut[] u8; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "unsupported `as` cast")); + } + + // Regression guard: the fn-name fallback in astgenVariable must + // NOT swallow the existing "unknown variable" diagnostic. If a + // name is neither a local, a module const, nor a function, the + // error must still fire — otherwise downstream codegen will + // crash trying to resolve a non-existent symbol. + static void testUnknownVariableStillErrors() { + auto r = compileSource("fnref_unknown_var", R"( +fn main() { + var a: u64 = nonexistent_thing; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "unknown variable")); + ASSERT_TRUE(stderrContains(r, "nonexistent_thing")); + } }; int main() { -- 2.51.2