From 1b6596220e16376ea1568cd3a567f61d18845288 Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Sun, 12 Apr 2026 00:08:57 +0200 Subject: [PATCH 1/3] Fn Node. --- backend-v2/CMakeLists.txt | 3 +- backend-v2/codegen/CodeGen.cpp | 159 +++++++++++++++++++++++- backend-v2/codegen/CodeGen.h | 27 ++++ backend-v2/codegen/LLVMTypes.cpp | 112 ++++++++++------- backend-v2/codegen/LLVMTypes.h | 22 ++-- backend-v2/codegen/MemoryManagement.cpp | 25 ++++ backend-v2/codegen/MemoryManagement.h | 16 +++ backend-v2/codegen/ops/FnNode.cpp | 121 ++++++++++++++++++ backend-v2/main.cpp | 2 +- backend-v2/runtime/Ebr.c | 4 +- backend-v2/runtime/Function.c | 27 ++-- backend-v2/runtime/Function.h | 22 ++-- backend-v2/runtime/Object.h | 11 +- backend-v2/types/ConstantFunction.h | 52 ++++++-- 14 files changed, 520 insertions(+), 83 deletions(-) create mode 100644 backend-v2/codegen/ops/FnNode.cpp diff --git a/backend-v2/CMakeLists.txt b/backend-v2/CMakeLists.txt index a70c01f6..70774935 100644 --- a/backend-v2/CMakeLists.txt +++ b/backend-v2/CMakeLists.txt @@ -144,7 +144,8 @@ set(BACKEND_SOURCES codegen/ops/HostInteropNode.cpp codegen/ops/NewNode.cpp codegen/ops/IsInstanceNode.cpp - codegen/ops/ThrowNode.cpp) + codegen/ops/ThrowNode.cpp + codegen/ops/FnNode.cpp) add_subdirectory(runtime) diff --git a/backend-v2/codegen/CodeGen.cpp b/backend-v2/codegen/CodeGen.cpp index 18f70ce9..70987ac9 100644 --- a/backend-v2/codegen/CodeGen.cpp +++ b/backend-v2/codegen/CodeGen.cpp @@ -97,9 +97,9 @@ std::string CodeGen::generateInstanceCallBridge( std::stringstream ss; ss << std::hex << dist(gen); - std::string funcName = - moduleName + "_" + std::to_string(instanceType.determinedType()) + "_" + - ss.str(); + std::string funcName = moduleName + "_" + + std::to_string(instanceType.determinedType()) + "_" + + ss.str(); // 1. Define Signature: (Instance, Arg1, ...) -> RTValue // Specialized arguments use natural LLVM types for unboxed primitives std::vector llvmArgTypes; @@ -167,6 +167,155 @@ std::string CodeGen::generateInstanceCallBridge( return funcName; } +llvm::Function *CodeGen::generateBaselineMethod( + const FnMethodNode &method, + const std::vector> &captureInfo) { + CLJ_ASSERT(TSContext != nullptr, "Codegen was moved"); + static thread_local std::mt19937 gen(std::random_device{}()); + std::uniform_int_distribution dist; + std::stringstream ss; + ss << std::hex << dist(gen); + + std::string funcName = "fn_" + ss.str(); + + // Create the function with baseline signature: (Frame*, Arg0, Arg1, Arg2, + // Arg3, Arg4) -> RTValue + Function *F = + Function::Create(types.baselineFunctionTy, Function::ExternalLinkage, + funcName, *TheModule); + + // Use the guard to save current IR state and memory management context + FunctionScopeGuard scopeGuard(*this, F); + + // Enforce frame pointers for reliable stack traces + F->addFnAttr("frame-pointer", "all"); + + // Create entry block + BasicBlock *BB = BasicBlock::Create(TheContext, "entry", F); + Builder.SetInsertPoint(BB); + + // Unpack arguments from LLVM function + auto arg_it = F->arg_begin(); + Value *framePtr = &*arg_it++; // Arg 0: Frame* + Value *regArgs[5]; // Arg 1-5: RTValue registers + for (int i = 0; i < 5; ++i) { + regArgs[i] = &*arg_it++; + } + + // Set personality function for exception handling + FunctionType *personalityFnTy = FunctionType::get(types.i32Ty, true); + personalityFn = + TheModule->getOrInsertFunction("__gxx_personality_v0", personalityFnTy); + F->setPersonalityFn(cast(personalityFn.getCallee())); + + // Prepare environment for body codegen + variableBindingStack.push(); + variableTypesBindingsStack.push(); + + int isVariadic = method.isvariadic(); + int numParams = method.params_size(); + Value *nilValue = valueEncoder.boxNil().value; + + // 1. Unpack parameters + for (int i = 0; i < numParams; ++i) { + const auto ¶mNode = method.params(i); + std::string paramName = paramNode.subnode().binding().name(); + + Value *val = nullptr; + + if (isVariadic && i == numParams - 1) { + // It's the variadic parameter. Unpack from frame->variadicSeq (index 2) + Value *variadicSeqPtr = Builder.CreateStructGEP(types.frameTy, framePtr, + 2, "variadicSeq_ptr"); + val = Builder.CreateLoad(types.RT_valueTy, variadicSeqPtr, "variadicSeq"); + } else { + // Regular parameter + if (i < 5) { + // From registers + val = regArgs[i]; + } else { + // From frame->locals[i-5] (index 5) + Value *localsBase = + Builder.CreateStructGEP(types.frameTy, framePtr, 5, "locals_base"); + // Accessing element i-5 in the flexible array + Value *argPtr = Builder.CreateInBoundsGEP( + types.RT_valueTy, localsBase, + {Builder.getInt32(0), Builder.getInt32(i - 5)}, "arg_ptr"); + val = Builder.CreateLoad(types.RT_valueTy, argPtr, "arg_val"); + } + } + + if (!val) + val = nilValue; + + TypedValue paramTV(ObjectTypeSet::dynamicType(), val); + variableBindingStack.set(paramName, paramTV); + variableTypesBindingsStack.set(paramName, ObjectTypeSet::dynamicType()); + } + + // 2. Unpack closed overs (captures) with type propagation + if (!captureInfo.empty()) { + // a. Get method pointer from frame + Value *methodPtrPtr = + Builder.CreateStructGEP(types.frameTy, framePtr, 1, "method_ptr_ptr"); + Value *methodPtr = + Builder.CreateLoad(types.ptrTy, methodPtrPtr, "method_ptr"); + + // b. Load closedOvers array pointer from method + Value *closedOversPtrPtr = Builder.CreateStructGEP( + types.methodTy, methodPtr, 6, "closedOvers_ptr_ptr"); + Value *closedOversPtr = + Builder.CreateLoad(types.ptrTy, closedOversPtrPtr, "closedOvers_ptr"); + + // c. Unpack each capture + for (int i = 0; i < (int)captureInfo.size(); ++i) { + const std::string &name = captureInfo[i].first; + const ObjectTypeSet &type = captureInfo[i].second; + + Value *valRawPtr = Builder.CreateInBoundsGEP( + types.RT_valueTy, closedOversPtr, Builder.getInt64(i), "capture_ptr"); + Value *valRaw = + Builder.CreateLoad(types.RT_valueTy, valRawPtr, "capture_raw"); + + Value *val = valRaw; + if (!type.isBoxedType()) { + if (type.isUnboxedType(integerType)) { + val = valueEncoder.unboxInt32(TypedValue(type.boxed(), valRaw)).value; + } else if (type.isUnboxedType(doubleType)) { + val = + valueEncoder.unboxDouble(TypedValue(type.boxed(), valRaw)).value; + } else if (type.isUnboxedType(booleanType)) { + val = valueEncoder.unboxBool(TypedValue(type.boxed(), valRaw)).value; + } + } + + variableBindingStack.set(name, TypedValue(type, val)); + variableTypesBindingsStack.set(name, type); + } + } + + // Generate body + TypedValue result = codegen(method.body(), ObjectTypeSet::all()); + + // TODO - it is not yet clear if this should always happen. + // Previous function calls cound introduce the flush as well, we need to + // carefully analyse the memory model for closed overs. + // + // llvm::FunctionType *flushTy = + // llvm::FunctionType::get(types.voidTy, {}, false); + // invokeManager.invokeRaw("Ebr_flush_critical", flushTy, {}); + + // Box result and return + Builder.CreateRet(valueEncoder.box(result).value); + + // Clean up bindings + variableBindingStack.pop(); + variableTypesBindingsStack.pop(); + + verifyFunction(*F); + return F; +} + TypedValue CodeGen::codegen(const Node &node, const ObjectTypeSet &typeRestrictions) { CLJ_ASSERT(TSContext != nullptr, "Codegen was moved"); @@ -283,6 +432,8 @@ TypedValue CodeGen::codegen(const Node &node, // return codegen(node, node.subnode().try_(), typeRestrictions); case opVar: return codegen(node, node.subnode().var(), typeRestrictions); + case opFn: + return codegen(node, node.subnode().fn(), typeRestrictions); case opWithMeta: return codegen(node, node.subnode().withmeta(), typeRestrictions); default: { @@ -308,6 +459,8 @@ ObjectTypeSet CodeGen::getType(const Node &node, return getType(node, node.subnode().vector(), typeRestrictions); case opMap: return getType(node, node.subnode().map(), typeRestrictions); + case opFn: + return getType(node, node.subnode().fn(), typeRestrictions); case opStaticCall: return getType(node, node.subnode().staticcall(), typeRestrictions); diff --git a/backend-v2/codegen/CodeGen.h b/backend-v2/codegen/CodeGen.h index ad695f2a..bb25993e 100644 --- a/backend-v2/codegen/CodeGen.h +++ b/backend-v2/codegen/CodeGen.h @@ -105,6 +105,10 @@ class CodeGen { const ObjectTypeSet &instanceType, const std::vector &argTypes); + llvm::Function *generateBaselineMethod( + const FnMethodNode &method, + const std::vector> &captureInfo); + TypedValue codegen(const Node &node, const ObjectTypeSet &typeRestrictions); TypedValue codegen(const Node &node, const ConstNode &subnode, @@ -129,6 +133,8 @@ class CodeGen { const ObjectTypeSet &typeRestrictions); TypedValue codegen(const Node &node, const WithMetaNode &subnode, const ObjectTypeSet &typeRestrictions); + TypedValue codegen(const Node &node, const FnNode &subnode, + const ObjectTypeSet &typeRestrictions); TypedValue codegen(const Node &node, const StaticFieldNode &subnode, const ObjectTypeSet &typeRestrictions); TypedValue codegen(const Node &node, const LetNode &subnode, @@ -171,6 +177,8 @@ class CodeGen { const ObjectTypeSet &typeRestrictions); ObjectTypeSet getType(const Node &node, const WithMetaNode &subnode, const ObjectTypeSet &typeRestrictions); + ObjectTypeSet getType(const Node &node, const FnNode &subnode, + const ObjectTypeSet &typeRestrictions); ObjectTypeSet getType(const Node &node, const StaticFieldNode &subnode, const ObjectTypeSet &typeRestrictions); ObjectTypeSet getType(const Node &node, const LetNode &subnode, @@ -204,6 +212,25 @@ class CodeGen { MemoryManagement &getMemoryManagement() { return memoryManagement; } DynamicConstructor &getDynamicConstructor() { return dynamicConstructor; } + class FunctionScopeGuard { + CodeGen &cg; + llvm::IRBuilder<>::InsertPointGuard ipGuard; + size_t prevLexicalBlocksSize; + + public: + FunctionScopeGuard(CodeGen &cg, llvm::Function *newFunction) + : cg(cg), ipGuard(cg.Builder), + prevLexicalBlocksSize(cg.LexicalBlocks.size()) { + cg.memoryManagement.pushState(newFunction); + } + ~FunctionScopeGuard() { + cg.memoryManagement.popState(); + while (cg.LexicalBlocks.size() > prevLexicalBlocksSize) { + cg.LexicalBlocks.pop_back(); + } + } + }; + private: std::map formMap; }; diff --git a/backend-v2/codegen/LLVMTypes.cpp b/backend-v2/codegen/LLVMTypes.cpp index 3afd852e..c93a617a 100644 --- a/backend-v2/codegen/LLVMTypes.cpp +++ b/backend-v2/codegen/LLVMTypes.cpp @@ -1,54 +1,78 @@ #include "LLVMTypes.h" #include "../RuntimeHeaders.h" #include "../bridge/Exceptions.h" +#include using namespace llvm; namespace rt { - - LLVMTypes::LLVMTypes(LLVMContext& context) { - i64Ty = Type::getInt64Ty(context); - i32Ty = Type::getInt32Ty(context); - i1Ty = Type::getInt1Ty(context); - doubleTy = Type::getDoubleTy(context); - ptrTy = PointerType::getUnqual(context); // Opaque pointer in LLVM 15+ - wordTy = K_WORD_SIZE == 32 ? i32Ty : i64Ty; - voidTy = Type::getVoidTy(context); - RT_valueTy = i64Ty; - - RT_objectTy = StructType::create(context, - { - wordTy, // atomicRefCount - i32Ty, // type (enum) + +LLVMTypes::LLVMTypes(LLVMContext &context) { + i64Ty = Type::getInt64Ty(context); + i32Ty = Type::getInt32Ty(context); + i8Ty = Type::getInt8Ty(context); + i1Ty = Type::getInt1Ty(context); + doubleTy = Type::getDoubleTy(context); + ptrTy = PointerType::getUnqual(context); // Opaque pointer in LLVM 15+ + wordTy = K_WORD_SIZE == 32 ? i32Ty : i64Ty; + voidTy = Type::getVoidTy(context); + RT_valueTy = i64Ty; + + RT_objectTy = StructType::create(context, + { + wordTy, // atomicRefCount + i32Ty, // type (enum) #ifdef USE_MEMORY_BANKS - Type::getInt8Ty(Ctx) // bankId + Type::getInt8Ty(Ctx) // bankId #endif - }, - "Clojure_Object"); - } - - llvm::Type *LLVMTypes::typeForType(const ObjectTypeSet &type) { - if (type.isBoxedType()) - return i64Ty; - if (type.isUnboxedPointer()) - return ptrTy; - - if (type.isUnboxedType(integerType)) - return i32Ty; - if (type.isUnboxedType(doubleType)) - return doubleTy; - if (type.isUnboxedType(keywordType)) - return i32Ty; - if (type.isUnboxedType(symbolType)) - return i32Ty; - if (type.isUnboxedType(booleanType)) - return i1Ty; - - throwInternalInconsistencyException("Type not supported: " + - type.toString()); - - return voidTy; - } - -} // namespace rt + }, + "Clojure_Object"); + + frameTy = StructType::create(context, + {ptrTy, // leafFrame + ptrTy, // method + RT_valueTy, // variadicSeq + i32Ty, // bailoutEntryIndex + i32Ty, // localsCount + ArrayType::get(i64Ty, 0)}, // locals + "Clojure_Frame"); + + methodTy = StructType::create(context, + {i8Ty, // index + i8Ty, // isVariadic + i8Ty, // fixedArity + i8Ty, // closedOversCount + ptrTy, // baselineImplementation + ptrTy, // loopId + ptrTy}, // closedOvers + "Clojure_FunctionMethod"); + /* Usually 6 args fit in registers */ + baselineFunctionTy = FunctionType::get( + RT_valueTy, + {ptrTy, RT_valueTy, RT_valueTy, RT_valueTy, RT_valueTy, RT_valueTy}, + false); +} +llvm::Type *LLVMTypes::typeForType(const ObjectTypeSet &type) { + if (type.isBoxedType()) + return i64Ty; + if (type.isUnboxedPointer()) + return ptrTy; + + if (type.isUnboxedType(integerType)) + return i32Ty; + if (type.isUnboxedType(doubleType)) + return doubleTy; + if (type.isUnboxedType(keywordType)) + return i32Ty; + if (type.isUnboxedType(symbolType)) + return i32Ty; + if (type.isUnboxedType(booleanType)) + return i1Ty; + + throwInternalInconsistencyException("Type not supported: " + type.toString()); + + return voidTy; +} + +} // namespace rt diff --git a/backend-v2/codegen/LLVMTypes.h b/backend-v2/codegen/LLVMTypes.h index cdc5fa84..9c3d5ee4 100644 --- a/backend-v2/codegen/LLVMTypes.h +++ b/backend-v2/codegen/LLVMTypes.h @@ -1,25 +1,29 @@ #ifndef LLVM_TYPES_H #define LLVM_TYPES_H +#include "../types/ObjectTypeSet.h" #include #include #include -#include "../types/ObjectTypeSet.h" namespace rt { -struct LLVMTypes { +struct LLVMTypes { // LLVM Types (Cached for speed) - llvm::Type* wordTy; - llvm::Type* i64Ty; - llvm::Type* i32Ty; - llvm::Type* i1Ty; - llvm::Type* doubleTy; - llvm::Type* ptrTy; + llvm::Type *wordTy; + llvm::Type *i64Ty; + llvm::Type *i32Ty; + llvm::Type *i8Ty; + llvm::Type *i1Ty; + llvm::Type *doubleTy; + llvm::Type *ptrTy; llvm::Type *voidTy; - + llvm::Type *RT_valueTy; llvm::StructType *RT_objectTy; + llvm::StructType *frameTy; + llvm::StructType *methodTy; + llvm::FunctionType *baselineFunctionTy; explicit LLVMTypes(llvm::LLVMContext &ctx); diff --git a/backend-v2/codegen/MemoryManagement.cpp b/backend-v2/codegen/MemoryManagement.cpp index 509081b8..fcbcec91 100644 --- a/backend-v2/codegen/MemoryManagement.cpp +++ b/backend-v2/codegen/MemoryManagement.cpp @@ -36,6 +36,31 @@ void MemoryManagement::initFunction(llvm::Function *F) { clear(); } +void MemoryManagement::pushState(llvm::Function *F) { + stateStack.push_back({exceptionSlot, terminalResumeBB, std::move(cleanupStack), + std::move(activeResources), totalPushedResources, + resourcesWithCleanup, std::move(lpadCache), + activeUnwindGuidance}); + initFunction(F); +} + +void MemoryManagement::popState() { + if (stateStack.empty()) { + throwInternalInconsistencyException( + "MemoryManagement::popState called on empty stack"); + } + FunctionState &s = stateStack.back(); + exceptionSlot = s.exceptionSlot; + terminalResumeBB = s.terminalResumeBB; + cleanupStack = std::move(s.cleanupStack); + activeResources = std::move(s.activeResources); + totalPushedResources = s.totalPushedResources; + resourcesWithCleanup = s.resourcesWithCleanup; + lpadCache = std::move(s.lpadCache); + activeUnwindGuidance = s.activeUnwindGuidance; + stateStack.pop_back(); +} + void MemoryManagement::ensureExceptionInfrastructure(llvm::Function *F) { if (exceptionSlot) return; diff --git a/backend-v2/codegen/MemoryManagement.h b/backend-v2/codegen/MemoryManagement.h index 99aa8482..4b4f4638 100644 --- a/backend-v2/codegen/MemoryManagement.h +++ b/backend-v2/codegen/MemoryManagement.h @@ -37,6 +37,21 @@ class MemoryManagement { bool hasPushedResources() const { return !activeResources.empty(); } void clear(); + struct FunctionState { + llvm::Value *exceptionSlot; + llvm::BasicBlock *terminalResumeBB; + std::vector cleanupStack; + std::vector activeResources; + size_t totalPushedResources; + size_t resourcesWithCleanup; + std::map lpadCache; + const google::protobuf::RepeatedPtrField + *activeUnwindGuidance; + }; + + void pushState(llvm::Function *F); + void popState(); + const google::protobuf::RepeatedPtrField * getActiveUnwindGuidance() const { return activeUnwindGuidance; @@ -87,6 +102,7 @@ class MemoryManagement { const google::protobuf::RepeatedPtrField *activeUnwindGuidance = nullptr; + std::vector stateStack; void *jitEnginePtr = nullptr; void ensureExceptionInfrastructure(llvm::Function *F); diff --git a/backend-v2/codegen/ops/FnNode.cpp b/backend-v2/codegen/ops/FnNode.cpp new file mode 100644 index 00000000..3aaadcbe --- /dev/null +++ b/backend-v2/codegen/ops/FnNode.cpp @@ -0,0 +1,121 @@ +#include "../CodeGen.h" +#include +#include + +namespace rt { + +ObjectTypeSet CodeGen::getType(const Node &node, const FnNode &subnode, + const ObjectTypeSet &typeRestrictions) { + // getType returns the boxed function type without a constant, + // as implementations are not yet generated. + return ObjectTypeSet(functionType, false).restriction(typeRestrictions); +} + +TypedValue CodeGen::codegen(const Node &node, const FnNode &subnode, + const ObjectTypeSet &typeRestrictions) { + // 1. Runtime Creation + // Function_create(methodCount, maxFixedArity, once) + std::vector createArgs = { + llvm::ConstantInt::get(types.wordTy, subnode.methods_size()), + llvm::ConstantInt::get(types.wordTy, subnode.maxfixedarity()), + Builder.getInt1(subnode.once())}; + + llvm::FunctionType *createTy = llvm::FunctionType::get( + types.ptrTy, {types.wordTy, types.wordTy, types.i1Ty}, false); + + llvm::Value *funObj = + invokeManager.invokeRaw("Function_create", createTy, createArgs); + + // 2. Sort methods (descending arity, variadic last) + struct MethodInfo { + const FnMethodNode *node; + int originalIndex; + }; + std::vector methods; + for (int i = 0; i < subnode.methods_size(); ++i) { + methods.push_back({&subnode.methods(i).subnode().fnmethod(), i}); + } + + std::sort(methods.begin(), methods.end(), + [](const MethodInfo &a, const MethodInfo &b) { + if (a.node->isvariadic() != b.node->isvariadic()) { + return !a.node->isvariadic(); // non-variadic first + } + return a.node->fixedarity() > + b.node->fixedarity(); // descending arity + }); + + // 3. Generate baseline implementations and register them + std::vector constantMethods; + + for (int i = 0; i < (int)methods.size(); ++i) { + const FnMethodNode &m = *methods[i].node; + + // Calculate types and names for captures (closed-overs) + std::vector> captureInfo; + for (const auto &node : m.closedovers()) { + // Calculate type in outer scope. + // Note: getType() should work correctly here. + ObjectTypeSet type = getType(node, ObjectTypeSet::all()); + + // Determine name + std::string name = "unknown"; + if (node.subnode().has_local()) { + name = node.subnode().local().name(); + } else if (node.subnode().has_binding()) { + name = node.subnode().binding().name(); + } + captureInfo.push_back({name, type}); + } + + // Generate the IR for the method + llvm::Function *baselineF = generateBaselineMethod(m, captureInfo); + + // Handle closed overs (captures) for the fillMethod call + // (these will be boxed RTValues in the runtime) + std::vector fillArgs; + + // Fixed arguments for Function_fillMethod + // (self, position, index, fixedArity, isVariadic, implementation, loopId, + // closedCount, ...captures) + fillArgs.push_back(funObj); + fillArgs.push_back(llvm::ConstantInt::get(types.wordTy, i)); + fillArgs.push_back( + llvm::ConstantInt::get(types.wordTy, methods[i].originalIndex)); + fillArgs.push_back(llvm::ConstantInt::get(types.wordTy, m.fixedarity())); + fillArgs.push_back(Builder.getInt1(m.isvariadic())); + fillArgs.push_back(baselineF); + fillArgs.push_back( + Builder.CreateGlobalString(m.loopid())); // loopId (string) + fillArgs.push_back(llvm::ConstantInt::get(types.wordTy, m.closedovers_size())); + + // Codegen and box each capture + for (int j = 0; j < m.closedovers_size(); ++j) { + TypedValue capture = codegen(m.closedovers(j), ObjectTypeSet::all()); + TypedValue boxed = valueEncoder.box(capture); + fillArgs.push_back(boxed.value); + } + + // Prepare variadic signature for Function_fillMethod + std::vector fillParamTypes = { + types.ptrTy, types.wordTy, types.wordTy, types.wordTy, + types.i1Ty, types.ptrTy, types.ptrTy, types.wordTy}; + + // 'true' indicates a variadic function (...) + llvm::FunctionType *fillTy = + llvm::FunctionType::get(types.voidTy, fillParamTypes, true); + invokeManager.invokeRaw("Function_fillMethod", fillTy, fillArgs); + + // Record for ConstantFunction + constantMethods.push_back({m.fixedarity(), m.isvariadic(), baselineF}); + } + + // 4. Return unboxed function object + ObjectTypeSet enrichedType( + functionType, false, + new ConstantFunction(constantMethods, subnode.once())); + + return TypedValue(enrichedType.restriction(typeRestrictions), funObj); +} + +} // namespace rt diff --git a/backend-v2/main.cpp b/backend-v2/main.cpp index 95c78676..f8c084d6 100644 --- a/backend-v2/main.cpp +++ b/backend-v2/main.cpp @@ -50,7 +50,7 @@ int main(int argc, char *argv[]) { } std::string filename = argv[1]; - llvm::OptimizationLevel optLevel = llvm::OptimizationLevel::O3; + llvm::OptimizationLevel optLevel = llvm::OptimizationLevel::O0; int retVal = -1; diff --git a/backend-v2/runtime/Ebr.c b/backend-v2/runtime/Ebr.c index b855309a..030caf49 100644 --- a/backend-v2/runtime/Ebr.c +++ b/backend-v2/runtime/Ebr.c @@ -200,8 +200,10 @@ void Ebr_flush_critical() { atomic_load_explicit(&global_epoch, memory_order_relaxed); EBR_LOG("Flushing critical section (state %p, global_epoch: %u).", (void *)thread_state, current); + /* We need release barrier because we want to make sure all + the reads completed before the barrier. */ atomic_store_explicit(&thread_state->local_epoch, current, - memory_order_relaxed); + memory_order_release); } void Ebr_leave_critical() { diff --git a/backend-v2/runtime/Function.c b/backend-v2/runtime/Function.c index 2addcace..9f905768 100644 --- a/backend-v2/runtime/Function.c +++ b/backend-v2/runtime/Function.c @@ -8,25 +8,24 @@ /* mem done */ -ClojureFunction* Function_create(uword_t methodCount, uword_t uniqueId, uword_t maxArity, bool once) { +ClojureFunction* Function_create(uword_t methodCount, uword_t maxArity, bool once) { size_t size = sizeof(ClojureFunction) + methodCount * sizeof(FunctionMethod); ClojureFunction *self = (ClojureFunction *)allocate(size); memset(self, 0, size); self->methodCount = methodCount; self->maxArity = maxArity; - self->uniqueId = uniqueId; self->once = once; self->executed = false; Object_create((Object *)self, functionType); - // TODO - ^:once meta. When present return self; } /* outside refcount system (w.r.t. self) and closedOvers (they need to be retained already when this fun is called) */ -void Function_fillMethod(ClojureFunction *self, uword_t position, uword_t index, uword_t fixedArity, bool isVariadic, char *loopId, word_t closedOversCount, ...) { +void Function_fillMethod(ClojureFunction *self, uword_t position, uword_t index, uword_t fixedArity, bool isVariadic, void *implementation, char *loopId, word_t closedOversCount, ...) { FunctionMethod * method = self->methods + position; method->fixedArity = fixedArity; method->isVariadic = isVariadic; + method->baselineImplementation = implementation; method->loopId = loopId; method->index = index; method->closedOversCount = closedOversCount; @@ -60,20 +59,32 @@ bool Function_validCallWithArgCount(ClojureFunction *self, uword_t argCount) { /* outside refcount system */ bool Function_equals(ClojureFunction *self, ClojureFunction *other) { - return self->uniqueId == other->uniqueId; + return self == other; } /* outside refcount system */ uword_t Function_hash(ClojureFunction *self) { - return avalanche(self->uniqueId); + return avalanche((uword_t)self); } /* mem done */ String *Function_toString(ClojureFunction *self) { - String *retVal = String_concat(String_createStatic("fn_"), toString(RT_boxInt32(self->uniqueId))); + char buf[2048]; + int pos = snprintf(buf, sizeof(buf), "fn_%p {", (void *)self); + for (uword_t i = 0; i < self->methodCount; ++i) { + FunctionMethod *m = &self->methods[i]; + pos += snprintf(buf + pos, sizeof(buf) - pos, + "%s[arity %d, %s, closed-overs %d, impl %p]", (i > 0 ? ", " : ""), + (int)m->fixedArity, (m->isVariadic ? "var" : "fixed"), + (int)m->closedOversCount, m->baselineImplementation); + if (pos >= (int)sizeof(buf) - 64) + break; // Avoid overflow or very long strings + } + snprintf(buf + pos, sizeof(buf) - pos, "}"); + String *retVal = String_createDynamicStr(buf); Ptr_release(self); return retVal; -} +} void Function_cleanupOnce(ClojureFunction *self) { assert(!self->executed && "Function with :once meta executed more than once"); diff --git a/backend-v2/runtime/Function.h b/backend-v2/runtime/Function.h index eea0fc33..4480a015 100644 --- a/backend-v2/runtime/Function.h +++ b/backend-v2/runtime/Function.h @@ -15,14 +15,23 @@ typedef struct InvokationCache InvokationCache; struct FunctionMethod { unsigned char index; - uword_t fixedArity; - uword_t isVariadic; + unsigned char isVariadic; + unsigned char fixedArity; + unsigned char closedOversCount; void *baselineImplementation; char *loopId; - uword_t closedOversCount; RTValue *closedOvers; }; +typedef struct Frame { + struct Frame *leafFrame; + struct FunctionMethod *method; + RTValue variadicSeq; + int32_t bailoutEntryIndex; + int32_t localsCount; + RTValue locals[]; +} Frame; + typedef struct FunctionMethod FunctionMethod; typedef struct Object Object; @@ -31,7 +40,6 @@ struct ClojureFunction { Object super; bool once; bool executed; - uword_t uniqueId; uword_t methodCount; uword_t maxArity; struct FunctionMethod methods[]; @@ -39,12 +47,12 @@ struct ClojureFunction { typedef struct ClojureFunction ClojureFunction; -struct ClojureFunction *Function_create(uword_t methodCount, uword_t uniqueId, - uword_t maxArity, bool once); +struct ClojureFunction *Function_create(uword_t methodCount, uword_t maxArity, bool once); void Function_fillMethod(struct ClojureFunction *self, uword_t position, uword_t index, uword_t fixedArity, bool isVariadic, - char *loopId, word_t closedOversCount, ...); + void *implementation, char *loopId, + word_t closedOversCount, ...); bool Function_validCallWithArgCount(ClojureFunction *self, uword_t argCount); bool Function_equals(ClojureFunction *self, ClojureFunction *other); diff --git a/backend-v2/runtime/Object.h b/backend-v2/runtime/Object.h index f984d6be..362c6240 100644 --- a/backend-v2/runtime/Object.h +++ b/backend-v2/runtime/Object.h @@ -107,6 +107,7 @@ inline bool Ptr_isReusable(void *ptr); #include "BigInteger.h" #include "Boolean.h" +#include "BridgedObject.h" #include "Class.h" #include "ConcurrentHashMap.h" #include "Double.h" @@ -124,7 +125,6 @@ inline bool Ptr_isReusable(void *ptr); #include "String.h" #include "Symbol.h" #include "Var.h" -#include "BridgedObject.h" inline void *allocate(size_t size) { #ifndef USE_MEMORY_BANKS @@ -252,6 +252,15 @@ inline void Object_retain(Object *restrict self) { #endif } +/* TODO: an improvement could be that for collections if we detect that it + should be destroyed, we do retain; autorelease; and ignore destroy. This would + free the current thread from the burden of deallocating them. + + Difficulties: + - how does it interact with shared bit in refcount? + - how to avoid endless loop (the cleanup thread needs to do a real + destroy) + */ inline void Object_destroy(Object *restrict self, bool deallocateChildren) { // printf("--> Deallocating type %d addres %lld\n", self->type, (uword_t)); // printReferenceCounts(); diff --git a/backend-v2/types/ConstantFunction.h b/backend-v2/types/ConstantFunction.h index 469a90f7..79f60b0a 100644 --- a/backend-v2/types/ConstantFunction.h +++ b/backend-v2/types/ConstantFunction.h @@ -2,18 +2,54 @@ #define CONSTANT_FUNCTION_H #include "ObjectTypeConstant.h" +#include +#include namespace rt { -class ConstantFunction: public ObjectTypeConstant { - public: - uword_t value; - ConstantFunction(uword_t val) : ObjectTypeConstant(functionType), value(val) {} - virtual ObjectTypeConstant *copy() { return static_cast (new ConstantFunction(value)); } - virtual std::string toString() { return std::string("fn_") + std::to_string(value); } +struct ConstantMethod { + int fixedArity; + bool isVariadic; + llvm::Function *implementation; + + bool operator==(const ConstantMethod &other) const { + return fixedArity == other.fixedArity && isVariadic == other.isVariadic && + implementation == other.implementation; + } +}; + +class ConstantFunction : public ObjectTypeConstant { +public: + std::vector methods; + bool once; + + ConstantFunction(const std::vector &methods, bool once = false) + : ObjectTypeConstant(functionType), methods(methods), once(once) {} + + virtual ObjectTypeConstant *copy() { + return static_cast(new ConstantFunction(methods, once)); + } + + virtual std::string toString() { + std::string res = "fn["; + if (once) + res += "once,"; + for (size_t i = 0; i < methods.size(); ++i) { + res += std::to_string(methods[i].fixedArity) + + (methods[i].isVariadic ? "+" : ""); + if (methods[i].implementation) { + res += ":" + methods[i].implementation->getName().str(); + } + if (i < methods.size() - 1) + res += ","; + } + res += "]"; + return res; + } + virtual bool equals(ObjectTypeConstant *other) { - if(ConstantFunction *i = dynamic_cast(other)) { - return i->value == value; + if (ConstantFunction *i = dynamic_cast(other)) { + return i->once == once && i->methods == methods; } return false; } From 24ed1b4b621ed8fee2f557a5c9c8af1f4b9cf7d6 Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Sun, 12 Apr 2026 00:29:27 +0200 Subject: [PATCH 2/3] Add tests --- backend-v2/tests/CMakeLists.txt | 1 + backend-v2/tests/codegen/FnNode_test.cpp | 173 +++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 backend-v2/tests/codegen/FnNode_test.cpp diff --git a/backend-v2/tests/CMakeLists.txt b/backend-v2/tests/CMakeLists.txt index 0ab60d4c..bdb79d62 100644 --- a/backend-v2/tests/CMakeLists.txt +++ b/backend-v2/tests/CMakeLists.txt @@ -29,6 +29,7 @@ set(TEST_MODULES codegen/NewNode_test codegen/IsInstanceNode_test codegen/ThrowNode_test + codegen/FnNode_test state/ClassRegistration_test VarUAF_repro_test JITSafety_test diff --git a/backend-v2/tests/codegen/FnNode_test.cpp b/backend-v2/tests/codegen/FnNode_test.cpp new file mode 100644 index 00000000..9c320b1b --- /dev/null +++ b/backend-v2/tests/codegen/FnNode_test.cpp @@ -0,0 +1,173 @@ +#include "../../codegen/CodeGen.h" +#include "../../state/ThreadsafeCompilerState.h" +#include "../../tools/EdnParser.h" +#include "../../types/ConstantInteger.h" +#include "bytecode.pb.h" + +#include + +#include "../../jit/JITEngine.h" +#include "../../runtime/Object.h" +#include "../../runtime/RTValue.h" +#include "../../runtime/RuntimeInterface.h" +#include "../../runtime/defines.h" + +extern "C" { +#include "../../runtime/tests/TestTools.h" +#include +} + +using namespace std; +using namespace rt; +using namespace clojure::rt::protobuf::bytecode; + +static void setup_mock_runtime(rt::ThreadsafeCompilerState &compState) { + // Add "Add" intrinsic to clojure.lang.Numbers + String *nameStr = String_createDynamicStr("clojure.lang.Numbers"); + Ptr_retain(nameStr); + Class *cls = Class_create(nameStr, nameStr, 0, nullptr); + + ClassDescription *ext = new ClassDescription(); + ext->name = "clojure.lang.Numbers"; + + IntrinsicDescription addId; + addId.symbol = "Add"; + addId.type = CallType::Intrinsic; + addId.argTypes = {ObjectTypeSet(integerType, false), + ObjectTypeSet(integerType, false)}; + addId.returnType = ObjectTypeSet(integerType, false); + ext->staticFns["add"].push_back(addId); + + cls->compilerExtension = ext; + cls->compilerExtensionDestructor = rt::delete_class_description; + compState.classRegistry.registerObject("clojure.lang.Numbers", cls); +} + +// (fn [x] x) +static void test_simple_fn(void **state) { + (void)state; + ASSERT_MEMORY_ALL_BALANCED({ + rt::JITEngine engine; + + Node fnNode; + fnNode.set_op(opFn); + auto *fn = fnNode.mutable_subnode()->mutable_fn(); + fn->set_once(false); + fn->set_maxfixedarity(1); + + auto *m = fn->add_methods(); + auto *mn = m->mutable_subnode()->mutable_fnmethod(); + mn->set_fixedarity(1); + mn->set_isvariadic(false); + + auto *param = mn->add_params(); + param->set_op(opBinding); + param->mutable_subnode()->mutable_binding()->set_name("x"); + + auto *body = mn->mutable_body(); + body->set_op(opLocal); + body->mutable_subnode()->mutable_local()->set_name("x"); + + auto res = + engine + .compileAST(fnNode, "simple_fn", llvm::OptimizationLevel::O0, true) + .get(); + + // Execute: returns a ClojureFunction object + RTValue funObj = res.address.toPtr()(); + assert_true(RT_isPtr(funObj)); + assert_int_equal(functionType, ::getType(funObj)); + + ClojureFunction *f = (ClojureFunction *)RT_unboxPtr(funObj); + assert_int_equal(1, f->methodCount); + + // We can't easily call the closure from here without a frame setup, + // but the fact it compiled and returned a function object is a good start. + + release(funObj); + }); +} + +// (let [u 10] (fn [x] (+ x u))) +static void test_fn_capture_unboxing_int(void **state) { + (void)state; + ASSERT_MEMORY_ALL_BALANCED({ + rt::JITEngine engine; + setup_mock_runtime(engine.threadsafeState); + + Node letNode; + letNode.set_op(opLet); + auto *let = letNode.mutable_subnode()->mutable_let(); + + auto *b = let->add_bindings(); + b->set_op(opBinding); + auto *bn = b->mutable_subnode()->mutable_binding(); + bn->set_name("u"); + auto *val = bn->mutable_init(); + val->set_op(opConst); + val->mutable_subnode()->mutable_const_()->set_type( + ConstNode_ConstType_constTypeNumber); + val->mutable_subnode()->mutable_const_()->set_val("10"); + val->set_tag("long"); // Ensure it's unboxed long + + auto *bodyFn = let->mutable_body(); + bodyFn->set_op(opFn); + auto *fn = bodyFn->mutable_subnode()->mutable_fn(); + fn->set_maxfixedarity(1); + + auto *m = fn->add_methods(); + auto *mn = m->mutable_subnode()->mutable_fnmethod(); + mn->set_fixedarity(1); + + auto *p1 = mn->add_params(); + p1->set_op(opBinding); + p1->mutable_subnode()->mutable_binding()->set_name("x"); + + // Captured 'u' + auto *co = mn->add_closedovers(); + co->set_op(opLocal); + co->mutable_subnode()->mutable_local()->set_name("u"); + + // (+ x u) + auto *body = mn->mutable_body(); + body->set_op(opStaticCall); + auto *sc = body->mutable_subnode()->mutable_staticcall(); + sc->set_class_("clojure.lang.Numbers"); + sc->set_method("add"); + + auto *arg1 = sc->add_args(); + arg1->set_op(opLocal); + arg1->mutable_subnode()->mutable_local()->set_name("x"); + + auto *arg2 = sc->add_args(); + arg2->set_op(opLocal); + arg2->mutable_subnode()->mutable_local()->set_name("u"); + + cout << "=== Fn Capture Unboxing Test IR ===" << endl; + auto res = engine + .compileAST(letNode, "fn_capture_int", + llvm::OptimizationLevel::O0, true) + .get(); + cout << "====================================" << endl; + + RTValue funObj = res.address.toPtr()(); + assert_true(RT_isPtr(funObj)); + + ClojureFunction *f = (ClojureFunction *)RT_unboxPtr(funObj); + assert_int_equal(1, f->methods[0].closedOversCount); + + // The IR dump (if viewed) should show unbox_int for %u% + + release(funObj); + }); +} + +int main(void) { + initialise_memory(); + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_simple_fn), + cmocka_unit_test(test_fn_capture_unboxing_int), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} From 2a6e69cc9365b0cb63901f15e350c8e1216ecbf6 Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Sun, 12 Apr 2026 00:33:00 +0200 Subject: [PATCH 3/3] tests --- backend-v2/codegen/ops/FnNode.cpp | 4 +- backend-v2/tests/codegen/FnNode_test.cpp | 56 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/backend-v2/codegen/ops/FnNode.cpp b/backend-v2/codegen/ops/FnNode.cpp index 3aaadcbe..a558b68b 100644 --- a/backend-v2/codegen/ops/FnNode.cpp +++ b/backend-v2/codegen/ops/FnNode.cpp @@ -41,8 +41,8 @@ TypedValue CodeGen::codegen(const Node &node, const FnNode &subnode, if (a.node->isvariadic() != b.node->isvariadic()) { return !a.node->isvariadic(); // non-variadic first } - return a.node->fixedarity() > - b.node->fixedarity(); // descending arity + return a.node->fixedarity() < + b.node->fixedarity(); // ascending arity }); // 3. Generate baseline implementations and register them diff --git a/backend-v2/tests/codegen/FnNode_test.cpp b/backend-v2/tests/codegen/FnNode_test.cpp index 9c320b1b..a1175540 100644 --- a/backend-v2/tests/codegen/FnNode_test.cpp +++ b/backend-v2/tests/codegen/FnNode_test.cpp @@ -162,11 +162,67 @@ static void test_fn_capture_unboxing_int(void **state) { }); } +// (fn ([x] x) ([x y] y)) +static void test_multi_arity_fn(void **state) { + (void)state; + ASSERT_MEMORY_ALL_BALANCED({ + rt::JITEngine engine; + + Node fnNode; + fnNode.set_op(opFn); + auto *fn = fnNode.mutable_subnode()->mutable_fn(); + fn->set_maxfixedarity(2); + + // Arity 1 + { + auto *m = fn->add_methods(); + auto *mn = m->mutable_subnode()->mutable_fnmethod(); + mn->set_fixedarity(1); + auto *p = mn->add_params(); + p->set_op(opBinding); + p->mutable_subnode()->mutable_binding()->set_name("x"); + auto *body = mn->mutable_body(); + body->set_op(opLocal); + body->mutable_subnode()->mutable_local()->set_name("x"); + } + + // Arity 2 + { + auto *m = fn->add_methods(); + auto *mn = m->mutable_subnode()->mutable_fnmethod(); + mn->set_fixedarity(2); + auto *p1 = mn->add_params(); + p1->set_op(opBinding); + p1->mutable_subnode()->mutable_binding()->set_name("x"); + auto *p2 = mn->add_params(); + p2->set_op(opBinding); + p2->mutable_subnode()->mutable_binding()->set_name("y"); + auto *body = mn->mutable_body(); + body->set_op(opLocal); + body->mutable_subnode()->mutable_local()->set_name("y"); + } + + auto res = engine.compileAST(fnNode, "multi_arity_fn", llvm::OptimizationLevel::O0, true).get(); + + RTValue funObj = res.address.toPtr()(); + assert_true(RT_isPtr(funObj)); + assert_int_equal(functionType, ::getType(funObj)); + + ClojureFunction *f = (ClojureFunction*)RT_unboxPtr(funObj); + assert_int_equal(2, f->methodCount); + assert_int_equal(1, f->methods[0].fixedArity); + assert_int_equal(2, f->methods[1].fixedArity); + + release(funObj); + }); +} + int main(void) { initialise_memory(); const struct CMUnitTest tests[] = { cmocka_unit_test(test_simple_fn), cmocka_unit_test(test_fn_capture_unboxing_int), + cmocka_unit_test(test_multi_arity_fn), }; return cmocka_run_group_tests(tests, NULL, NULL);