From 4614f78ea7b39fd40d37400ca02ae80c72b8c6a4 Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Thu, 14 May 2026 06:26:04 -0700 Subject: [PATCH 1/6] WIP --- backend/codegen/CodeGen.cpp | 7 +- backend/codegen/ops/ConstNode.cpp | 2 +- backend/codegen/ops/TheVarNode.cpp | 2 +- backend/codegen/ops/VarNode.cpp | 2 +- backend/jit/JITEngine.cpp | 1 + backend/main.cpp | 54 +-- backend/runtime/ExecutionContext.c | 6 +- backend/runtime/Namespace.c | 150 ++----- backend/runtime/Namespace.h | 2 +- backend/runtime/Symbol.c | 84 +++- backend/runtime/Var.c | 60 +-- backend/runtime/Var.h | 3 +- backend/runtime/tests/Var_test.c | 8 +- backend/state/ThreadsafeCompilerState.cpp | 407 ++++++++++++++---- backend/state/ThreadsafeCompilerState.h | 11 +- backend/tests/VarUAF_repro_test.cpp | 4 +- backend/tests/codegen/Concurrency_test.cpp | 2 +- .../tests/codegen/ContextPropagation_test.cpp | 2 +- backend/tests/codegen/DoubleAddRepro_test.cpp | 2 +- backend/tests/codegen/DynamicInvoke_test.cpp | 6 +- backend/tests/codegen/InstanceCallIC_test.cpp | 4 +- .../tests/codegen/InstanceCallNode_test.cpp | 2 +- backend/tests/codegen/InvokeNode_test.cpp | 6 +- backend/tests/codegen/MathIntrinsics_test.cpp | 4 +- backend/tests/codegen/NoThrow_test.cpp | 6 +- .../codegen/ProtocolInstanceCall_test.cpp | 2 +- backend/tests/codegen/VarCallIC_test.cpp | 2 +- 27 files changed, 509 insertions(+), 332 deletions(-) diff --git a/backend/codegen/CodeGen.cpp b/backend/codegen/CodeGen.cpp index a5c5ebca..7460d2b2 100644 --- a/backend/codegen/CodeGen.cpp +++ b/backend/codegen/CodeGen.cpp @@ -696,12 +696,7 @@ ObjectTypeSet CodeGen::getType(const Node &node, } Var *CodeGen::getOrCreateVar(std::string_view name) { - return compilerState.varRegistry.getOrCreate( - std::string(name).c_str(), [name]() { - RTValue keyword = - Keyword_create(String_createDynamicStr(std::string(name).c_str())); - return Var_create(keyword); - }); + return compilerState.getOrCreateVar(std::string(name).c_str()); } bool CodeGen::canThrow(const Node &node) { diff --git a/backend/codegen/ops/ConstNode.cpp b/backend/codegen/ops/ConstNode.cpp index 1e904540..687afa6e 100644 --- a/backend/codegen/ops/ConstNode.cpp +++ b/backend/codegen/ops/ConstNode.cpp @@ -93,7 +93,7 @@ TypedValue CodeGen::codegen(const Node &node, const ConstNode &subnode, break; case varType: { const std::string name = subnode.val(); - ScopedRef var(compilerState.varRegistry.getCurrent(name.c_str())); + ScopedRef var(compilerState.getCurrentVar(name.c_str())); if (!var) { throwCodeGenerationException( string("Unable to resolve var: ") + name + " in this context", node); diff --git a/backend/codegen/ops/TheVarNode.cpp b/backend/codegen/ops/TheVarNode.cpp index 2bf1273c..ad2655bc 100644 --- a/backend/codegen/ops/TheVarNode.cpp +++ b/backend/codegen/ops/TheVarNode.cpp @@ -10,7 +10,7 @@ namespace rt { TypedValue CodeGen::codegen(const Node &node, const TheVarNode &subnode, const ObjectTypeSet &typeRestrictions) { auto varName = subnode.var().substr(2); - ScopedRef var(compilerState.varRegistry.getCurrent(varName.c_str())); + ScopedRef var(compilerState.getOrCreateVar(varName.c_str())); if (!var) throwCodeGenerationException( "Unable to resolve var: " + varName + " in this context", node); diff --git a/backend/codegen/ops/VarNode.cpp b/backend/codegen/ops/VarNode.cpp index dc72f371..e40a6d3c 100644 --- a/backend/codegen/ops/VarNode.cpp +++ b/backend/codegen/ops/VarNode.cpp @@ -10,7 +10,7 @@ namespace rt { TypedValue CodeGen::codegen(const Node &node, const VarNode &subnode, const ObjectTypeSet &typeRestrictions) { auto varName = subnode.var().substr(2); - ScopedRef var(compilerState.varRegistry.getCurrent(varName.c_str())); + ScopedRef var(compilerState.getCurrentVar(varName.c_str())); if (!var) throwCodeGenerationException( diff --git a/backend/jit/JITEngine.cpp b/backend/jit/JITEngine.cpp index 7747a6e8..ab41431e 100644 --- a/backend/jit/JITEngine.cpp +++ b/backend/jit/JITEngine.cpp @@ -76,6 +76,7 @@ JITEngine::JITEngine(llvm::OptimizationLevel defaultOptLevel, // Initialise runtime. RuntimeInterface_initialise(); Ebr_enter_critical(); + threadsafeState.initializeDefaultNamespaces(); llvm::InitializeNativeTarget(); llvm::InitializeNativeTargetAsmPrinter(); diff --git a/backend/main.cpp b/backend/main.cpp index 204b8441..fa36b6f3 100644 --- a/backend/main.cpp +++ b/backend/main.cpp @@ -27,7 +27,11 @@ #include "jit/JITEngine.h" #include "runtime/Ebr.h" #include "runtime/ExecutionContext.h" +#include "runtime/Keyword.h" +#include "runtime/Namespace.h" +#include "runtime/RTValue.h" #include "runtime/String.h" +#include "runtime/Symbol.h" #include "runtime/Var.h" #include "tools/RTValueWrapper.h" #include @@ -130,47 +134,15 @@ int main(int argc, char *argv[]) { rt::ScopedRef ctx( ExecutionContext_create(RT_boxPtr(PersistentArrayMap_empty()))); - // Setup default Clojure namespaces and *ns* Var - { - // 1. Create "clojure.core" namespace - Symbol *coreSym = Symbol_create(String_create("clojure.core")); - Namespace *coreNs = Namespace_findOrCreate(coreSym); // coreNs has +1 refcount - - // 2. Intern "*ns*" Var in "clojure.core" - Symbol *nsSym = Symbol_create(String_create("*ns*")); - Ptr_retain(coreNs); // Retain coreNs because Namespace_intern consumes self - Var *nsVar = Namespace_intern(coreNs, nsSym); // nsVar has +1 refcount. nsSym is consumed. - Var_setDynamic(nsVar, true); - - // 3. Create "user" namespace - Symbol *userSym = Symbol_create(String_create("user")); - Namespace *userNs = Namespace_findOrCreate(userSym); // userNs has +1 refcount - RTValue userNsVal = RT_boxPtr(userNs); // userNsVal has +1 refcount - retain(userNsVal); // userNsVal has +2 refcount - - // 4. Bind clojure.core/*ns* Var root to the user namespace object - Ptr_retain(nsVar); // Retain nsVar because Var_bindRoot consumes self - Var_bindRoot(nsVar, userNsVal); // Consumes userNsVal (+2 -> +1) and nsVar - - // 5. Build clojure.core/*ns* keyword for ExecutionContext dynamic bindings map - RTValue nsVarKw = Keyword_create(String_create("clojure.core/*ns*")); - retain(nsVarKw); // nsVarKw has +2 refcount - - // Assoc clojure.core/*ns* keyword with userNsVal in ExecutionContext bindings map - ctx->bindingsMap = RT_boxPtr(PersistentArrayMap_assoc( - (PersistentArrayMap *)RT_unboxPtr(ctx->bindingsMap), nsVarKw, - userNsVal)); // assoc consumes nsVarKw (+2 -> +1) and userNsVal (+1 -> +0) - - // 6. Register in compiler state so JIT finds this exact Var object - engine.threadsafeState.varRegistry.registerObject("clojure.core/*ns*", - nsVar); // Registry takes ownership of nsVar (+1) - - // Release remaining local reference to clojure.core namespace object - Ptr_release(coreNs); - - // Release remaining keyword reference - release(nsVarKw); - } + Var *nsVar = engine.threadsafeState.getCurrentVar("clojure.core/*ns*"); + assert(nsVar != nullptr && "clojure.core/*ns* Var not found"); + + // Assoc clojure.core/*ns* Var pointer with user namespace in ExecutionContext + // bindings map (as we now perform dynamic bindings lookup by pointer) + ctx->bindingsMap = RT_boxPtr(PersistentArrayMap_assoc( + (PersistentArrayMap *)RT_unboxPtr(ctx->bindingsMap), + RT_boxPtr(nsVar), + RT_boxPtr(Namespace_findOrCreate(Symbol_create(String_create("user")))))); { ExecutionTimer t("Compiling and storing interfaces"); diff --git a/backend/runtime/ExecutionContext.c b/backend/runtime/ExecutionContext.c index ee304645..4cfb34c8 100644 --- a/backend/runtime/ExecutionContext.c +++ b/backend/runtime/ExecutionContext.c @@ -49,7 +49,7 @@ RT_bind(__attribute__((swift_context)) struct ExecutionContext *ctx, RTValue v, } RTValue nextMap = - RT_boxPtr(PersistentArrayMap_assoc(currentMap, var->keyword, val)); + RT_boxPtr(PersistentArrayMap_assoc(currentMap, v, val)); ExecutionContext *nextCtx = ExecutionContext_create(nextMap); return nextCtx; @@ -92,9 +92,9 @@ RT_bind_map(__attribute__((swift_context)) struct ExecutionContext *ctx, } // assoc consumes its arguments, so we must retain them from the source map - retain(var->keyword); + retain(v); retain(val); - currentMap = PersistentArrayMap_assoc(currentMap, var->keyword, val); + currentMap = PersistentArrayMap_assoc(currentMap, v, val); } release(bindingsMap); diff --git a/backend/runtime/Namespace.c b/backend/runtime/Namespace.c index 930d3472..708bfc2e 100644 --- a/backend/runtime/Namespace.c +++ b/backend/runtime/Namespace.c @@ -191,29 +191,20 @@ bool Namespace_isInternedMapping(Namespace *self, Symbol *sym, RTValue o) { * Validates whether an existing mapping (`oldVal`) can be safely replaced by a * new mapping (`neuVal`). Throws an IllegalStateException if an attempt is made * to overwrite a Var interned in this namespace. Consumes all arguments: - * `self`, `sym`, `oldVal`, and `neuVal`. + * `self`, `sym`, `oldVal`, and `newVal`. */ -bool Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, - RTValue neuVal) { - bool res = true; +void Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, + RTValue newVal) { if (getType(oldVal) == varType) { - Ptr_retain(self); - Ptr_retain(sym); - retain(oldVal); // for isInternedMapping which consumes if (Namespace_isInternedMapping(self, sym, oldVal)) { - Ptr_release(self); - Ptr_release(sym); - release(oldVal); - release(neuVal); + release(newVal); throwIllegalStateException_C("REJECTED: attempt to replace interned var"); - return false; } + release(newVal); + return; } - Ptr_release(self); - Ptr_release(sym); - release(oldVal); - release(neuVal); - return res; + + } /* @@ -248,88 +239,55 @@ Var *Namespace_intern(Namespace *self, Symbol *sym) { Ptr_retain(map); retain(symVal); RTValue o = PersistentArrayMap_get(map, symVal); + if (!RT_isNil(o)) { + retain(o); Ptr_retain(self); Ptr_retain(sym); - retain(o); if (Namespace_isInternedMapping(self, sym, o)) { - if (v) { + if (v != NULL) { release(RT_boxPtr((Object *)v)); + } else { + Ptr_release(self); + Ptr_release(sym); } - Ptr_release(self); - Ptr_release(sym); return (Var *)RT_unboxPtr(o); } + } - if (v == NULL) { - v = Var_create_interned(self, sym); // v has +1 refcount - } + if (v == NULL) { + v = Var_create_interned(self, sym); + } + RTValue newVal = RT_boxPtr((Object *)v); + if (!RT_isNil(o)) { Ptr_retain(self); - Ptr_retain(sym); + Ptr_retain(v->sym); retain(o); - retain(RT_boxPtr((Object *)v)); - if (Namespace_checkReplacement(self, sym, o, RT_boxPtr((Object *)v))) { - release(o); - RTValue newVal = RT_boxPtr((Object *)v); - Ptr_retain(map); - retain(symVal); - retain(newVal); - PersistentArrayMap *newMap = - PersistentArrayMap_assoc(map, symVal, newVal); - RTValue newMapVal = RT_boxPtr((Object *)newMap); - - if (atomic_compare_exchange_strong_explicit( - &self->mappings, &mapVal, newMapVal, memory_order_acq_rel, - memory_order_acquire)) { - autorelease(mapVal); - Ptr_release(self); - Ptr_release(sym); - // return v without releasing it, so caller owns the ref. Wait, assoc - // also retained it. So v has +2. We need to return it with +1 to - // caller. But wait, assoc retained newVal. v was created with +1. So - // returning v directly leaves it with +2. Wait, caller expects +1. We - // just drop our reference? If we drop our reference, we must - // `release(RT_boxPtr(v));` but wait! We can't release it AND return - // it. We just don't release it! Wait! If it has +2 and we give 1 to - // caller, we must release the other. But `assoc` took +1. We created - // it with +1. So +2. Caller takes +1. We don't need to retain. Wait! - // If `assoc` took +1, we have +1. We give +1 to caller. Perfect. - return v; - } else { - release(newMapVal); - } - } else { - if (v) { - release(RT_boxPtr((Object *)v)); - } - Ptr_release(self); - Ptr_release(sym); - return (Var *)RT_unboxPtr(o); - } - } else { - if (v == NULL) { - v = Var_create_interned(self, sym); - } - RTValue newVal = RT_boxPtr((Object *)v); - Ptr_retain(map); - retain(symVal); retain(newVal); - PersistentArrayMap *newMap = - PersistentArrayMap_assoc(map, symVal, newVal); - RTValue newMapVal = RT_boxPtr((Object *)newMap); - - if (atomic_compare_exchange_strong_explicit( - &self->mappings, &mapVal, newMapVal, memory_order_acq_rel, - memory_order_acquire)) { - autorelease(mapVal); + if (!Namespace_checkReplacement(self, sym, o, newVal)) { + release(newVal); Ptr_release(self); - Ptr_release(sym); - return v; - } else { - release(newMapVal); + return (Var *)RT_unboxPtr(o); } + release(o); + } + + Ptr_retain(map); + retain(symVal); + retain(newVal); + PersistentArrayMap *newMap = PersistentArrayMap_assoc(map, symVal, newVal); + RTValue newMapVal = RT_boxPtr((Object *)newMap); + + if (atomic_compare_exchange_strong_explicit(&self->mappings, &mapVal, + newMapVal, memory_order_acq_rel, + memory_order_acquire)) { + autorelease(mapVal); + Ptr_release(self); + return v; } + + release(newMapVal); } } @@ -368,24 +326,8 @@ RTValue Namespace_reference(Namespace *self, Symbol *sym, RTValue val) { Ptr_retain(self); Ptr_retain(sym); retain(o); - if (Namespace_isInternedMapping(self, sym, o)) { - Ptr_release(self); - Ptr_release(sym); - release(val); - release(o); - throwIllegalStateException_C( - "Cannot reference into an interned mapping"); - } - Ptr_retain(self); - Ptr_retain(sym); - retain(o); retain(val); - if (!Namespace_checkReplacement(self, sym, o, val)) { - Ptr_release(self); - Ptr_release(sym); - release(val); - return o; - } + Namespace_checkReplacement(self, sym, o, val); release(o); } @@ -401,14 +343,6 @@ RTValue Namespace_reference(Namespace *self, Symbol *sym, RTValue val) { autorelease(mapVal); Ptr_release(self); Ptr_release(sym); - // We don't release `val` because we return it to the caller! - // But wait! assoc retained `val`. So `val` has +2 refcount. - // If we return `val` to caller, caller takes +1. The map holds +1. - // So we MUST release `val` if we want to give caller the same `val`? No! - // If we received `val` with +1, we give `val` with +1 to caller. The map - // took +1. Wait! `assoc` called `retain(val)`. So `val` refcount is +2. - // We own +1 (from argument). We give +1 to caller. So it balances out. We - // do NOT release `val`. return val; } else { release(newMapVal); diff --git a/backend/runtime/Namespace.h b/backend/runtime/Namespace.h index b8425208..977d92c4 100644 --- a/backend/runtime/Namespace.h +++ b/backend/runtime/Namespace.h @@ -32,7 +32,7 @@ void Namespace_promoteToShared(Namespace *self, uword_t count); RTValue Namespace_getMappings(Namespace *self); RTValue Namespace_getAliases(Namespace *self); bool Namespace_isInternedMapping(Namespace *self, Symbol *sym, RTValue o); -bool Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, RTValue neuVal); +void Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, RTValue neuVal); Var *Namespace_intern(Namespace *self, Symbol *sym); RTValue Namespace_reference(Namespace *self, Symbol *sym, RTValue val); RTValue Namespace_referenceClass(Namespace *self, Symbol *sym, Class *val); diff --git a/backend/runtime/Symbol.c b/backend/runtime/Symbol.c index 1fe7f69c..f20c06e3 100644 --- a/backend/runtime/Symbol.c +++ b/backend/runtime/Symbol.c @@ -2,13 +2,47 @@ #include "Object.h" #include "RTValue.h" #include "String.h" +#include + +static void Symbol_init(Symbol *sym, String *string) { + bool split = false; + uword_t ns_len = 0; + if (string->count > 1) { + StringIterator it = String_iterator(string); + for (uword_t i = 0; i < string->count; i++) { + if (*String_iteratorGet(&it) == '/') { + ns_len = i; + split = true; + break; + } + String_iteratorNext(&it); + } + } + + if (split) { + Ptr_retain(string); + sym->ns = String_subs(string, 0, ns_len); + sym->name = String_subs(string, ns_len + 1, string->count); + } else { + sym->ns = NULL; + sym->name = string; + } + + // Assert that name never contains '/' if length > 1 + if (sym->name->count > 1) { + StringIterator it = String_iterator(sym->name); + for (uword_t i = 0; i < sym->name->count; i++) { + assert(*String_iteratorGet(&it) != '/' && "Symbol name (length > 1) cannot contain '/'"); + String_iteratorNext(&it); + } + } +} /* mem done - consumes string */ Symbol *Symbol_create(String *string) { Symbol *sym = (Symbol *)allocate(sizeof(Symbol)); Object_create(&sym->header, symbolType); - sym->name = string; - sym->ns = NULL; + Symbol_init(sym, string); sym->metadata = RT_boxNil(); return sym; @@ -23,16 +57,24 @@ Symbol *Symbol_withMeta(Symbol *self, RTValue meta) { } String *name = self->name; Ptr_retain(name); + String *ns = self->ns; + if (ns) { + Ptr_retain(ns); + } Ptr_release(self); - return Symbol_createWithMeta(name, meta); + + Symbol *sym = Symbol_createWithMeta(name, meta); + if (ns) { + sym->ns = ns; + } + return sym; } /* mem done - consumes string, consumes meta */ Symbol *Symbol_createWithMeta(String *string, RTValue meta) { Symbol *sym = (Symbol *)allocate(sizeof(Symbol)); Object_create(&sym->header, symbolType); - sym->name = string; - sym->ns = NULL; + Symbol_init(sym, string); sym->metadata = meta; return sym; @@ -62,16 +104,42 @@ void Symbol_destroy(Symbol *self) { bool Symbol_equals(Symbol *self, Symbol *other) { if (self == other) return true; + + if (self->ns == NULL && other->ns != NULL) + return false; + if (self->ns != NULL && other->ns == NULL) + return false; + if (self->ns != NULL && !String_equals(self->ns, other->ns)) + return false; + return String_equals(self->name, other->name); } -uword_t Symbol_hash(Symbol *self) { return String_hash(self->name); } +uword_t Symbol_hash(Symbol *self) { + uword_t h = String_hash(self->name); + if (self->ns) { + h ^= String_hash(self->ns); + } + return h; +} /* mem done */ String *Symbol_toString(RTValue self) { Symbol *sym = (Symbol *)RT_unboxSymbol(self); - String *res = sym->name; - Ptr_retain(res); + String *res = NULL; + if (sym->ns != NULL) { + String *ns = sym->ns; + Ptr_retain(ns); + String *slash = String_create("/"); + String *name = sym->name; + Ptr_retain(name); + + res = String_concat(ns, slash); + res = String_concat(res, name); + } else { + res = sym->name; + Ptr_retain(res); + } Ptr_release(sym); return res; } diff --git a/backend/runtime/Var.c b/backend/runtime/Var.c index b318a34b..adf2bb07 100644 --- a/backend/runtime/Var.c +++ b/backend/runtime/Var.c @@ -2,9 +2,9 @@ #include "Ebr.h" #include "Exceptions.h" #include "ExecutionContext.h" +#include "Namespace.h" #include "Object.h" #include "RTValue.h" -#include "Namespace.h" #include "String.h" #include "word.h" #include @@ -30,13 +30,13 @@ } while (0) #endif -Var *Var_create(RTValue keyword) { +Var *Var_create(struct Symbol *sym) { + assert(sym != NULL && "Must have sym to create a Var"); Var *self = (Var *)allocate(sizeof(Var)); atomic_store_explicit(&(self->root), RT_boxNull(), memory_order_relaxed); self->dynamic = false; - self->keyword = keyword; self->ns = NULL; - self->sym = NULL; + self->sym = sym; atomic_store_explicit(&self->metadata, RT_boxNil(), memory_order_relaxed); atomic_store_explicit(&self->rev, 0, memory_order_relaxed); Object_create((Object *)self, varType); @@ -52,21 +52,6 @@ Var *Var_create_interned(struct Namespace *ns, struct Symbol *sym) { self->dynamic = false; self->ns = ns; self->sym = sym; - if(sym) Ptr_retain(sym); - - - Ptr_retain(ns->name); - String *nsStr = Symbol_toString(RT_boxSymbol((Object *)ns->name)); - String *slashStr = String_create("/"); - Ptr_retain(sym); - String *symStr = Symbol_toString(RT_boxSymbol((Object *)sym)); - - - String *s1 = String_concat(nsStr, slashStr); - String *s2 = String_concat(s1, symStr); - - self->keyword = Keyword_create(s2); // keyword takes ownership - atomic_store_explicit(&self->metadata, RT_boxNil(), memory_order_relaxed); atomic_store_explicit(&self->rev, 0, memory_order_relaxed); Object_create((Object *)self, varType); @@ -79,13 +64,16 @@ bool Var_equals(Var *self, Var *other) { return false; // pointer equality in Object_equals }; -uword_t Var_hash(Var *self) { return hash(self->keyword); }; +uword_t Var_hash(Var *self) { + /* Vars are purely pointer based entities */ + return hash(RT_boxPtr(self)); +}; String *Var_toString(Var *self) { - String *retVal = String_create("#"); - retVal = String_concat(retVal, String_replace(toString(self->keyword), - String_create(":"), - String_create("'"))); + String *retVal = String_create("#'"); + Ptr_retain(self->sym); + retVal = + String_concat(retVal, Symbol_toString(RT_boxSymbol((Object *)self->sym))); Ptr_release(self); return retVal; }; @@ -95,14 +83,11 @@ void Var_destroy(Var *self) { if (oldRoot != RT_boxNull()) { autorelease(oldRoot); } - release(self->keyword); + Ptr_release(self->sym); RTValue oldMeta = atomic_load_explicit(&self->metadata, memory_order_relaxed); if (!RT_isNil(oldMeta)) { autorelease(oldMeta); } - if (self->sym) { - Ptr_release(self->sym); - } }; Var *Var_resetMeta(Var *self, RTValue meta) { @@ -150,9 +135,10 @@ RTValue Var_deref(__attribute__((swift_context)) struct ExecutionContext *ctx, !RT_isNil(ctx->bindingsMap)) { assert(getType(ctx->bindingsMap) == persistentArrayMapType && "Wrong type"); PersistentArrayMap *m = RT_unboxPtr(ctx->bindingsMap); - RTValue key = self->keyword; + RTValue key = RT_boxPtr(self); for (uword_t i = 0; i < m->count; i++) { - if (equals(key, m->keys[i])) { + // Pointer equality instead of "equals" for speed. + if (key == m->keys[i]) { RTValue val = m->values[i]; retain(val); Ptr_release(self); @@ -204,9 +190,10 @@ RTValue Var_peek(__attribute__((swift_context)) struct ExecutionContext *ctx, !RT_isNil(ctx->bindingsMap)) { assert(getType(ctx->bindingsMap) == persistentArrayMapType && "Wrong type"); PersistentArrayMap *m = RT_unboxPtr(ctx->bindingsMap); - RTValue key = self->keyword; + RTValue key = RT_boxPtr(self); for (uword_t i = 0; i < m->count; i++) { - if (equals(key, m->keys[i])) { + // Pointer equality instead of "equals" for speed. + if (key == m->keys[i]) { return m->values[i]; } } @@ -232,17 +219,16 @@ RTValue Var_set(__attribute__((swift_context)) struct ExecutionContext *ctx, // Check if the var has a thread-local binding (Clojure behavior) PersistentArrayMap *m = RT_unboxPtr(ctx->bindingsMap); Ptr_retain(m); - retain(self->keyword); - if (PersistentArrayMap_indexOf(m, self->keyword) == -1) { + Ptr_retain(self); + if (PersistentArrayMap_indexOf(m, RT_boxPtr(self)) == -1) { release(value); Ptr_release(self); throwIllegalStateException_C("Can't set!: Var has no thread-local binding"); } - retain(self->keyword); retain(value); - ctx->bindingsMap = RT_boxPtr(PersistentArrayMap_assoc( - RT_unboxPtr(ctx->bindingsMap), self->keyword, value)); + ctx->bindingsMap = + RT_boxPtr(PersistentArrayMap_assoc(m, RT_boxPtr(self), value)); Ptr_release(self); diff --git a/backend/runtime/Var.h b/backend/runtime/Var.h index 837d8dc8..f7ad7269 100644 --- a/backend/runtime/Var.h +++ b/backend/runtime/Var.h @@ -21,7 +21,6 @@ struct Var { bool dynamic; _Atomic(uword_t) rev; _Atomic(RTValue) root; - RTValue keyword; // TODO: split name and namespace - Marek - why? struct Namespace *ns; struct Symbol *sym; _Atomic(RTValue) metadata; @@ -33,7 +32,7 @@ typedef struct Var Var; // Class *UNIQUE_UnboundClass; -Var *Var_create(RTValue keyword); +Var *Var_create(struct Symbol *sym); Var *Var_create_interned(struct Namespace *ns, struct Symbol *sym); Var *Var_resetMeta(Var *self, RTValue meta); RTValue Var_getMeta(Var *self); diff --git a/backend/runtime/tests/Var_test.c b/backend/runtime/tests/Var_test.c index 1f838d03..2c05e0d9 100644 --- a/backend/runtime/tests/Var_test.c +++ b/backend/runtime/tests/Var_test.c @@ -10,7 +10,7 @@ static void test_var_basic_lifecycle(void **state) { (void)state; ASSERT_MEMORY_ALL_BALANCED({ - RTValue sym = Keyword_create(String_create("my-var")); + Symbol *sym = Symbol_create(String_create("my-var")); Var *v = Var_create(sym); assert_true(Var_isDynamic(v) == false); @@ -41,7 +41,7 @@ static void test_var_basic_lifecycle(void **state) { static void test_var_dynamic(void **state) { (void)state; ASSERT_MEMORY_ALL_BALANCED({ - RTValue sym = Keyword_create(String_create("dynamic-var")); + Symbol *sym = Symbol_create(String_create("dynamic-var")); Var *v = Var_create(sym); v = Var_setDynamic(v, true); @@ -59,7 +59,7 @@ static void test_var_tostring(void **state) { (void)state; ASSERT_MEMORY_ALL_BALANCED({ - RTValue sym = Keyword_create(String_create("foo")); + Symbol *sym = Symbol_create(String_create("foo")); Var *v = Var_create(sym); // Var_toString consumes self and may return a compound string @@ -75,7 +75,7 @@ static void test_var_tostring(void **state) { static void test_var_peek(void **state) { (void)state; ASSERT_MEMORY_ALL_BALANCED({ - RTValue sym = Keyword_create(String_create("peek-var")); + Symbol *sym = Symbol_create(String_create("peek-var")); Var *v = Var_create(sym); RTValue val = RT_boxPtr(String_create("peek-value")); diff --git a/backend/state/ThreadsafeCompilerState.cpp b/backend/state/ThreadsafeCompilerState.cpp index 1b888b6a..6a694f49 100644 --- a/backend/state/ThreadsafeCompilerState.cpp +++ b/backend/state/ThreadsafeCompilerState.cpp @@ -1,13 +1,43 @@ #include "ThreadsafeCompilerState.h" #include "../bridge/Exceptions.h" +#include "../runtime/Namespace.h" +#include "../runtime/Var.h" #include "../tools/EdnParser.h" #include "../tools/RTValueWrapper.h" - namespace rt { extern "C" void delete_class_description(void *ptr); extern "C" _Atomic(uword_t) globalMethodICEpoch; +ThreadsafeCompilerState::ThreadsafeCompilerState() + : classRegistry(true, 1000), protocolRegistry(true, 2000), + functionAstRegistry(false) {} + +void ThreadsafeCompilerState::initializeDefaultNamespaces() { + // Setup default Clojure namespaces and *ns* Var + + // 1. Create "clojure.core" namespace + Symbol *coreSym = Symbol_create(String_create("clojure.core")); + Namespace *coreNs = Namespace_findOrCreate(coreSym); // coreNs has +1 refcount + + // 2. Intern "*ns*" Var in "clojure.core" + Symbol *nsSym = Symbol_create(String_create("*ns*")); + Var *nsVar = Namespace_intern(coreNs, nsSym); + Var_setDynamic(nsVar, true); + + // 3. Create "user" namespace + Symbol *userSym = Symbol_create(String_create("user")); + Namespace *userNs = Namespace_findOrCreate(userSym); // userNs has +1 refcount + RTValue userNsVal = RT_boxPtr(userNs); + + // 4. Bind clojure.core/*ns* Var root to the user namespace object + Ptr_retain(nsVar); // Retain nsVar because Var_bindRoot consumes self + Var_bindRoot(nsVar, userNsVal); + + // 6. Register in compiler state so JIT finds this exact Var object + registerVar("clojure.core/*ns*", nsVar); +} + ThreadsafeCompilerState::~ThreadsafeCompilerState() {} void ThreadsafeCompilerState::storeInternalClasses(RTValue from) { @@ -29,80 +59,83 @@ void ThreadsafeCompilerState::storeInternalClasses(RTValue from) { c->compilerExtensionDestructor = delete_class_description; } - try { - // Phase 2: Link inheritance - for (auto const &pair : localMap) { - ::Class *c = pair.second; - ClassDescription *desc = - static_cast(c->compilerExtension); - - if (!desc->parentName.empty() || !desc->parentNames.empty()) { - // Populate runtime Class superclasses - int32_t count = (int32_t)desc->parentNames.size(); - ClassList *list = - (ClassList *)allocate(sizeof(ClassList) + sizeof(::Class *) * count); - list->count = count; - for (int32_t i = 0; i < count; i++) { - ::Class *pc = nullptr; - auto it2 = localMap.find(desc->parentNames[i]); - if (it2 != localMap.end()) { - pc = it2->second; - Ptr_retain(pc); - } else { - pc = classRegistry.getCurrent(desc->parentNames[i].c_str()); - if (!pc) { - pc = protocolRegistry.getCurrent(desc->parentNames[i].c_str()); - } + try { + // Phase 2: Link inheritance + for (auto const &pair : localMap) { + ::Class *c = pair.second; + ClassDescription *desc = + static_cast(c->compilerExtension); + + if (!desc->parentName.empty() || !desc->parentNames.empty()) { + // Populate runtime Class superclasses + int32_t count = (int32_t)desc->parentNames.size(); + ClassList *list = (ClassList *)allocate(sizeof(ClassList) + + sizeof(::Class *) * count); + list->count = count; + for (int32_t i = 0; i < count; i++) { + ::Class *pc = nullptr; + auto it2 = localMap.find(desc->parentNames[i]); + if (it2 != localMap.end()) { + pc = it2->second; + Ptr_retain(pc); + } else { + pc = classRegistry.getCurrent(desc->parentNames[i].c_str()); + if (!pc) { + pc = protocolRegistry.getCurrent(desc->parentNames[i].c_str()); } - if (pc) { - list->classes[i] = pc; - if (i == 0) { - desc->extends = pc; - Ptr_retain(pc); // Extra retain for desc->extends - } - } else { - throwInternalInconsistencyException( - "Parent class/protocol not found: " + desc->parentNames[i]); + } + if (pc) { + list->classes[i] = pc; + if (i == 0) { + desc->extends = pc; + Ptr_retain(pc); // Extra retain for desc->extends } + } else { + throwInternalInconsistencyException( + "Parent class/protocol not found: " + desc->parentNames[i]); } - // Atomic store to publish superclasses. - // Note: The initial empty list from Class_create is leaked here unless we destroy it. - // But Class_create was called with 0 superclasses, so it allocated an empty ClassList. - ClassList *oldList = atomic_load_explicit(&c->superclasses, memory_order_relaxed); - atomic_store_explicit(&c->superclasses, list, memory_order_relaxed); - if (oldList) deallocate(oldList); } + // Atomic store to publish superclasses. + // Note: The initial empty list from Class_create is leaked here unless + // we destroy it. But Class_create was called with 0 superclasses, so it + // allocated an empty ClassList. + ClassList *oldList = + atomic_load_explicit(&c->superclasses, memory_order_relaxed); + atomic_store_explicit(&c->superclasses, list, memory_order_relaxed); + if (oldList) + deallocate(oldList); } + } - // Phase 3: Register and cleanup local references - for (auto const &pair : localMap) { - ::Class *c = pair.second; - ClassDescription *desc = - static_cast(c->compilerExtension); - - Ptr_retain(c); // For name-based registry - classRegistry.registerObject(desc->name.c_str(), c); - - if (desc->type.isDetermined()) { - c->registerId = (int32_t)desc->type.determinedType(); - } + // Phase 3: Register and cleanup local references + for (auto const &pair : localMap) { + ::Class *c = pair.second; + ClassDescription *desc = + static_cast(c->compilerExtension); - Ptr_retain(c); // For ID-based registry - if (desc->type.isDetermined()) { - classRegistry.registerObject(c, c->registerId); - } else { - c->registerId = (int32_t)classRegistry.registerObject(c, -1); - } + Ptr_retain(c); // For name-based registry + classRegistry.registerObject(desc->name.c_str(), c); - // Release the initial reference from Phase 1 - Ptr_release(c); + if (desc->type.isDetermined()) { + c->registerId = (int32_t)desc->type.determinedType(); } - } catch (...) { - for (auto const &pair : localMap) { - Ptr_release(pair.second); + + Ptr_retain(c); // For ID-based registry + if (desc->type.isDetermined()) { + classRegistry.registerObject(c, c->registerId); + } else { + c->registerId = (int32_t)classRegistry.registerObject(c, -1); } - throw; + + // Release the initial reference from Phase 1 + Ptr_release(c); + } + } catch (...) { + for (auto const &pair : localMap) { + Ptr_release(pair.second); } + throw; + } // Phase 4: Link implemented protocols for (auto const &pair : localMap) { @@ -110,28 +143,30 @@ void ThreadsafeCompilerState::storeInternalClasses(RTValue from) { ClassDescription *desc = static_cast(c->compilerExtension); - if (!desc->implements.empty()) { - std::vector<::Class *> classImpls; - for (auto const &[protoName, protoMethods] : desc->implements) { - ::Class *proto = protocolRegistry.getCurrent(protoName.c_str()); - if (!proto) { - continue; - } - // proto was already retained by getCurrent. Ownership transferred to c->implementedProtocols. - classImpls.push_back(proto); + if (!desc->implements.empty()) { + std::vector<::Class *> classImpls; + for (auto const &[protoName, protoMethods] : desc->implements) { + ::Class *proto = protocolRegistry.getCurrent(protoName.c_str()); + if (!proto) { + continue; } + // proto was already retained by getCurrent. Ownership transferred to + // c->implementedProtocols. + classImpls.push_back(proto); + } - if (!classImpls.empty()) { - int32_t count = (int32_t)classImpls.size(); - ClassList *list = - (ClassList *)allocate(sizeof(ClassList) + sizeof(::Class *) * count); - list->count = count; - for (int i = 0; i < count; i++) { - list->classes[i] = classImpls[i]; - } - atomic_store_explicit(&c->implementedProtocols, list, memory_order_relaxed); + if (!classImpls.empty()) { + int32_t count = (int32_t)classImpls.size(); + ClassList *list = (ClassList *)allocate(sizeof(ClassList) + + sizeof(::Class *) * count); + list->count = count; + for (int i = 0; i < count; i++) { + list->classes[i] = classImpls[i]; } + atomic_store_explicit(&c->implementedProtocols, list, + memory_order_relaxed); } + } } try { @@ -175,8 +210,8 @@ void ThreadsafeCompilerState::storeInternalProtocols(RTValue from) { if (!desc->parentNames.empty()) { int32_t count = (int32_t)desc->parentNames.size(); - ClassList *list = - (ClassList *)allocate(sizeof(ClassList) + sizeof(::Class *) * count); + ClassList *list = (ClassList *)allocate(sizeof(ClassList) + + sizeof(::Class *) * count); list->count = count; for (int32_t i = 0; i < count; i++) { ::Class *pc = nullptr; @@ -196,16 +231,19 @@ void ThreadsafeCompilerState::storeInternalProtocols(RTValue from) { // here (the one from getCurrent/localMap is consumed here) if (i == 0) { desc->extends = pc; - Ptr_retain(pc); // Extra retain for desc->extends which is released in ~ClassDescription + Ptr_retain(pc); // Extra retain for desc->extends which is + // released in ~ClassDescription } } else { throwInternalInconsistencyException( "Extended protocol not found: " + desc->parentNames[i]); } } - ClassList *oldList = atomic_load_explicit(&c->superclasses, memory_order_relaxed); + ClassList *oldList = + atomic_load_explicit(&c->superclasses, memory_order_relaxed); atomic_store_explicit(&c->superclasses, list, memory_order_relaxed); - if (oldList) deallocate(oldList); + if (oldList) + deallocate(oldList); } } @@ -283,7 +321,8 @@ void ThreadsafeCompilerState::validateProtocolImplementations( } } // Also add parents of this protocol - ClassList *supers = atomic_load_explicit(&curr->superclasses, memory_order_acquire); + ClassList *supers = atomic_load_explicit(&curr->superclasses, + memory_order_acquire); if (supers) { for (int j = 0; j < supers->count; j++) { ::Class *parent = supers->classes[j]; @@ -296,9 +335,11 @@ void ThreadsafeCompilerState::validateProtocolImplementations( // Validate each required method for (auto const &[methodName, protoFns] : requiredMethods) { - // Find all implementations of this method across all protocol blocks of the class + // Find all implementations of this method across all protocol + // blocks of the class std::vector allImpls; - for (auto const &[anyProtoName, anyImplMethods] : desc->implements) { + for (auto const &[anyProtoName, anyImplMethods] : + desc->implements) { auto it = anyImplMethods.find(methodName); if (it != anyImplMethods.end()) { for (auto const &implFn : it->second) { @@ -402,7 +443,8 @@ void ThreadsafeCompilerState::extendInternalClasses(RTValue from) { auto descriptions = buildClasses(from); for (auto &desc : descriptions) { - if (!desc) continue; + if (!desc) + continue; string className = desc->name; ::Class *existing = classRegistry.getCurrent(className.c_str()); if (existing) { @@ -411,7 +453,7 @@ void ThreadsafeCompilerState::extendInternalClasses(RTValue from) { static_cast(existing->compilerExtension); if (!oldDesc) { throwInternalInconsistencyException("Class " + className + - " has no compiler metadata"); + " has no compiler metadata"); } // Use the new merge method for basic metadata merging @@ -422,11 +464,190 @@ void ThreadsafeCompilerState::extendInternalClasses(RTValue from) { ::Class *proto = protocolRegistry.getCurrent(protoName.c_str()); if (proto) { Class_addProtocol(existing, proto); - Ptr_release(proto); + Ptr_release(proto); + } + } + } + } +} + +Var *ThreadsafeCompilerState::getOrCreateVar(const char *name) { + std::string varName(name); + std::string nsName = "user"; + std::string symName = varName; + + size_t slashPos = varName.find('/'); + if (slashPos != std::string::npos) { + nsName = varName.substr(0, slashPos); + symName = varName.substr(slashPos + 1); + } + + // 1. Create Symbol for Namespace + Symbol *nsSym = Symbol_create(String_createDynamicStr(nsName.c_str())); + + // 2. Find or create the Namespace (consumes nsSym, returns +1 ref) + Namespace *ns = Namespace_findOrCreate(nsSym); + + // 3. Create Symbol for Var + Symbol *varSym = Symbol_create(String_createDynamicStr(symName.c_str())); + + // 4. Intern symbol inside Namespace (consumes ns and varSym, returns retained Var +1) + Var *var = Namespace_intern(ns, varSym); + return var; +} + +Var *ThreadsafeCompilerState::getCurrentVar(const char *name) { + std::string varName(name); + std::string nsName = "user"; + std::string symName = varName; + + size_t slashPos = varName.find('/'); + if (slashPos != std::string::npos) { + nsName = varName.substr(0, slashPos); + symName = varName.substr(slashPos + 1); + } + // 1. Szukamy Namespace + Symbol *nsSym = Symbol_create(String_createDynamicStr(nsName.c_str())); + Namespace *ns = Namespace_find(nsSym); + + if (!ns) { + // Jeśli brak namespace, a nazwa była niekwalifikowana, szukamy w clojure.core + if (slashPos == std::string::npos && nsName != "clojure.core") { + Symbol *coreSym = Symbol_create(String_create("clojure.core")); + Namespace *coreNs = Namespace_find(coreSym); + + if (coreNs) { + Symbol *coreVarSym = Symbol_create(String_createDynamicStr(symName.c_str())); + Ptr_retain(coreNs); + RTValue coreMapping = Namespace_getMapping(coreNs, coreVarSym); + Ptr_release(coreNs); + + if (!RT_isNil(coreMapping) && getType(coreMapping) == varType) { + Var *var = (Var *)RT_unboxPtr(coreMapping); + return var; + } + release(coreMapping); + } + } + return nullptr; + } + + // 2. Szukamy symbolu w mappings znalezionego namespace'u + Symbol *varSym = Symbol_create(String_createDynamicStr(symName.c_str())); + Ptr_retain(ns); + RTValue mapping = Namespace_getMapping(ns, varSym); + + if (RT_isNil(mapping)) { + release(mapping); + Ptr_release(ns); + + // Jeśli nazwa była niekwalifikowana i brak w user, szukamy w clojure.core + if (slashPos == std::string::npos && nsName != "clojure.core") { + Symbol *coreSym = Symbol_create(String_create("clojure.core")); + Namespace *coreNs = Namespace_find(coreSym); + + if (coreNs) { + Symbol *coreVarSym = Symbol_create(String_createDynamicStr(symName.c_str())); + Ptr_retain(coreNs); + RTValue coreMapping = Namespace_getMapping(coreNs, coreVarSym); + Ptr_release(coreNs); + + if (!RT_isNil(coreMapping) && getType(coreMapping) == varType) { + Var *var = (Var *)RT_unboxPtr(coreMapping); + return var; } + release(coreMapping); } } + return nullptr; } + + if (getType(mapping) != varType) { + release(mapping); + Ptr_release(ns); + return nullptr; + } + + Var *var = (Var *)RT_unboxPtr(mapping); + Ptr_release(ns); + return var; } +void ThreadsafeCompilerState::registerVar(const char *name, Var *var) { + std::string varName(name); + std::string nsName = "user"; + std::string symName = varName; + + size_t slashPos = varName.find('/'); + if (slashPos != std::string::npos) { + nsName = varName.substr(0, slashPos); + symName = varName.substr(slashPos + 1); + } + + // 1. Znajdź lub utwórz Namespace + Symbol *nsSym = Symbol_create(String_createDynamicStr(nsName.c_str())); + Namespace *ns = Namespace_findOrCreate(nsSym); // ns ma +1 refcount + + // 2. Utwórz Symbol dla Var + Symbol *varSym = Symbol_create(String_createDynamicStr(symName.c_str())); // varSym ma +1 refcount + + // 3. Uzupełnij pola ns i sym w strukturze Var, jeśli są puste + if (var->ns == nullptr) { + var->ns = ns; + Ptr_retain(ns); + } + if (var->sym == nullptr) { + var->sym = varSym; + Ptr_retain(varSym); + } + + // 4. Przypisz Var w Namespace za pomocą referencji + Ptr_retain(var); + Var *res = Namespace_refer(ns, varSym, var); + + // Zwalniamy nieużywany wskaźnik zwrócony przez refer (+1 refcount) + Ptr_release(res); +} + +// void registerObject(const char *name, T *newDef) { +// std::lock_guard lock(registryMutex); + +// if (manageRuntimeMemory) { +// promoteToShared(RT_boxPtr((void *)newDef)); +// } + +// std::string key(name); +// auto it = registry.find(key); + +// if (manageRuntimeMemory && it != registry.end()) { +// Ptr_release((void *)it->second); +// } + +// registry[key] = newDef; +// } + +// template T *getOrCreate(const char *name, F &&factory) { +// std::lock_guard lock(registryMutex); + +// std::string key(name); +// auto it = registry.find(key); + +// if (it != registry.end()) { +// if (manageRuntimeMemory) { +// Ptr_retain((void *)it->second); +// } +// return it->second; +// } + +// T *newDef = factory(); +// registry[key] = newDef; +// if (manageRuntimeMemory) { +// promoteToShared(RT_boxPtr(newDef)); +// Ptr_retain(newDef); // Still need to return a fresh reference if it's +// // being returned +// } + +// return newDef; +// } + } // namespace rt diff --git a/backend/state/ThreadsafeCompilerState.h b/backend/state/ThreadsafeCompilerState.h index 2b3dfc11..93c8d078 100644 --- a/backend/state/ThreadsafeCompilerState.h +++ b/backend/state/ThreadsafeCompilerState.h @@ -22,12 +22,9 @@ class ThreadsafeCompilerState { ThreadsafeRegistry<::Class> classRegistry; ThreadsafeRegistry<::Class> protocolRegistry; ThreadsafeRegistry functionAstRegistry; - ThreadsafeRegistry<::Var> varRegistry; - - ThreadsafeCompilerState() - : classRegistry(true, 1000), protocolRegistry(true, 2000), - functionAstRegistry(false), varRegistry(true) {} + ThreadsafeCompilerState(); + void initializeDefaultNamespaces(); ~ThreadsafeCompilerState(); void storeInternalClasses(RTValue from); @@ -36,6 +33,10 @@ class ThreadsafeCompilerState { void validateProtocolImplementations(const std::vector<::Class *> &classes); + Var *getOrCreateVar(const char *name); + Var *getCurrentVar(const char *name); + void registerVar(const char *name, Var *var); + private: }; } // namespace rt diff --git a/backend/tests/VarUAF_repro_test.cpp b/backend/tests/VarUAF_repro_test.cpp index d1fee821..9b8553df 100644 --- a/backend/tests/VarUAF_repro_test.cpp +++ b/backend/tests/VarUAF_repro_test.cpp @@ -87,12 +87,12 @@ static void test_double_release_uaf_repro(void **state_arg) { Var *v1 = Var_create(Keyword_create(String_create("user/v1"))); Ptr_retain(v1); // Retain so it survives bindRoot consumption Var_bindRoot(v1, s1); - compState.varRegistry.registerObject("user/v1", v1); + compState.registerVar("user/v1", v1); Var *v2 = Var_create(Keyword_create(String_create("user/v2"))); Ptr_retain(v2); Var_bindRoot(v2, s2); - compState.varRegistry.registerObject("user/v2", v2); + compState.registerVar("user/v2", v2); arg1->set_op(opVar); arg1->mutable_subnode()->mutable_var()->set_var("#'user/v1"); diff --git a/backend/tests/codegen/Concurrency_test.cpp b/backend/tests/codegen/Concurrency_test.cpp index d638739a..f215498d 100644 --- a/backend/tests/codegen/Concurrency_test.cpp +++ b/backend/tests/codegen/Concurrency_test.cpp @@ -122,7 +122,7 @@ static void test_concurrent_closures(void **state) { Var *v = Var_create(Keyword_create(String_create(varName))); Ptr_retain(v); // For Var_bindRoot (consumes 1) Var_bindRoot(v, funObj); - engine.threadsafeState.varRegistry.registerObject(varName, v); + engine.threadsafeState.registerVar(varName, v); // 2. Create the caller // (fn [] (user/shared-fn 5)) diff --git a/backend/tests/codegen/ContextPropagation_test.cpp b/backend/tests/codegen/ContextPropagation_test.cpp index 5520ea1c..ed5847ef 100644 --- a/backend/tests/codegen/ContextPropagation_test.cpp +++ b/backend/tests/codegen/ContextPropagation_test.cpp @@ -64,7 +64,7 @@ static void test_context_propagation_in_jit(void **state) { compState.classRegistry.registerObject("clojure.lang.Var", varCls); compState.classRegistry.registerObject(varCls, varType); - compState.varRegistry.registerObject("user/test-var", myVar); + compState.registerVar("user/test-var", myVar); Node callNode; callNode.set_op(opInstanceCall); diff --git a/backend/tests/codegen/DoubleAddRepro_test.cpp b/backend/tests/codegen/DoubleAddRepro_test.cpp index 6737e418..4d13160c 100644 --- a/backend/tests/codegen/DoubleAddRepro_test.cpp +++ b/backend/tests/codegen/DoubleAddRepro_test.cpp @@ -37,7 +37,7 @@ static void test_double_add_repro(void **state) { Ptr_retain(v); Var_bindRoot(v, str); - engine.threadsafeState.varRegistry.registerObject("a", v); + engine.threadsafeState.registerVar("a", v); } // (+ a a) diff --git a/backend/tests/codegen/DynamicInvoke_test.cpp b/backend/tests/codegen/DynamicInvoke_test.cpp index 7a25bd8c..5af8b31c 100644 --- a/backend/tests/codegen/DynamicInvoke_test.cpp +++ b/backend/tests/codegen/DynamicInvoke_test.cpp @@ -381,7 +381,7 @@ static void test_dynamic_invoke_arity_exception_erased(void **state) { Var *v = Var_create(kw); Ptr_retain(v); Ptr_retain(v); - compState.varRegistry.registerObject(varName, v); + compState.registerVar(varName, v); // fn = (fn [x] x) Node fnNode; @@ -439,7 +439,7 @@ static void test_dynamic_invoke_intermediate_throw(void **state) { Var *v = Var_create(kw); Ptr_retain(v); Ptr_retain(v); - compState.varRegistry.registerObject(varName, v); + compState.registerVar(varName, v); Node fnNode; create_identity_fn(fnNode); @@ -525,7 +525,7 @@ static void test_dynamic_invoke_nested_guidance(void **state) { Var *v = Var_create(kw); Ptr_retain(v); Ptr_retain(v); - compState.varRegistry.registerObject(varName, v); + compState.registerVar(varName, v); Node fnNode; create_identity_fn(fnNode); diff --git a/backend/tests/codegen/InstanceCallIC_test.cpp b/backend/tests/codegen/InstanceCallIC_test.cpp index a8c6b46b..d259c2e0 100644 --- a/backend/tests/codegen/InstanceCallIC_test.cpp +++ b/backend/tests/codegen/InstanceCallIC_test.cpp @@ -99,7 +99,7 @@ static void test_instance_call_ic_hit_miss(void **state) { // Register a Var "user/my-var" to hold our instance RTValue varKeyword = Keyword_create(String_create("user/my-var")); Var *myVar = Var_create(varKeyword); - compState.varRegistry.registerObject("user/my-var", myVar); + compState.registerVar("user/my-var", myVar); // AST: (.foo @my-var 10) Node callNode; @@ -180,7 +180,7 @@ static void test_instance_call_ic_atomicity(void **state) { // Register a Var "user/my-var" to hold our instance RTValue varKeyword = Keyword_create(String_create("user/my-var")); Var *myVar = Var_create(varKeyword); - compState.varRegistry.registerObject("user/my-var", myVar); + compState.registerVar("user/my-var", myVar); // AST: (.foo @my-var 10) Node callNode; diff --git a/backend/tests/codegen/InstanceCallNode_test.cpp b/backend/tests/codegen/InstanceCallNode_test.cpp index 28c66455..76804432 100644 --- a/backend/tests/codegen/InstanceCallNode_test.cpp +++ b/backend/tests/codegen/InstanceCallNode_test.cpp @@ -285,7 +285,7 @@ static void test_instance_call_fast_path_error(void **state) { RTValue vk = Keyword_create(String_create("user/my-vec")); Var *myVar = Var_create(vk); - compState.varRegistry.registerObject("user/my-vec", myVar); + compState.registerVar("user/my-vec", myVar); Node callNode; callNode.set_op(opInstanceCall); diff --git a/backend/tests/codegen/InvokeNode_test.cpp b/backend/tests/codegen/InvokeNode_test.cpp index 66a09ec3..4d2467f3 100644 --- a/backend/tests/codegen/InvokeNode_test.cpp +++ b/backend/tests/codegen/InvokeNode_test.cpp @@ -194,7 +194,7 @@ static void test_dynamic_invoke_arity_mismatch_leak(void **state) { Var *v = Var_create(kw); Ptr_retain(v); Ptr_retain(v); - compState.varRegistry.registerObject(varName, v); + compState.registerVar(varName, v); // fn = (fn [x] x) Node fnNode; @@ -299,7 +299,7 @@ static void test_var_call_unbound(void **state) { RTValue kw = Keyword_create(String_create(varName)); Var *v = Var_create(kw); Ptr_retain(v); - compState.varRegistry.registerObject(varName, v); + compState.registerVar(varName, v); // 2. Create an InvokeNode calling #'user/unbound-fn Node invokeNode; @@ -417,7 +417,7 @@ static void test_dynamic_keyword_invoke_on_number(void **state) { Ptr_retain(v); Var_bindRoot(v, kwVal); // Var_bindRoot releases v once, so we retained it above Ptr_retain(v); // Retain again so the registry and our manual release both have one - engine.threadsafeState.varRegistry.registerObject("v_kw", v); + engine.threadsafeState.registerVar("v_kw", v); Node root; root.set_op(opInvoke); diff --git a/backend/tests/codegen/MathIntrinsics_test.cpp b/backend/tests/codegen/MathIntrinsics_test.cpp index d2a181a6..6cb07732 100644 --- a/backend/tests/codegen/MathIntrinsics_test.cpp +++ b/backend/tests/codegen/MathIntrinsics_test.cpp @@ -189,7 +189,7 @@ static void test_math_sqrt_leak_repro(void **state) { Ptr_retain(v); // Var_bindRoot consumes v, so retain it Var_bindRoot(v, str); - compState.varRegistry.registerObject("a", v); + compState.registerVar("a", v); } // (Math/sqrt a) @@ -244,7 +244,7 @@ static void test_math_pow_var(void **state) { Ptr_retain(v); Var_bindRoot(v, v_a); - compState.varRegistry.registerObject("a", v); + compState.registerVar("a", v); } // (Math/pow 2.0 a) diff --git a/backend/tests/codegen/NoThrow_test.cpp b/backend/tests/codegen/NoThrow_test.cpp index 7b511a64..eb237f6d 100644 --- a/backend/tests/codegen/NoThrow_test.cpp +++ b/backend/tests/codegen/NoThrow_test.cpp @@ -61,9 +61,9 @@ static void test_hot_cold_separation(void **state) { // Define vars so they are found auto *nameX = String_createDynamicStr("x"); - compState.varRegistry.registerObject("x", Var_create(RT_boxPtr(nameX))); + compState.registerVar("x", Var_create(RT_boxPtr(nameX))); auto *nameY = String_createDynamicStr("y"); - compState.varRegistry.registerObject("y", Var_create(RT_boxPtr(nameY))); + compState.registerVar("y", Var_create(RT_boxPtr(nameY))); Node root; root.set_op(opVector); @@ -176,7 +176,7 @@ static void test_whitelist_works(void **state) { // Register the var manually so it's found auto *nameStr = String_createDynamicStr("clojure.core/*print-length*"); Var *v = Var_create(RT_boxPtr(nameStr)); - compState.varRegistry.registerObject("clojure.core/*print-length*", v); + compState.registerVar("clojure.core/*print-length*", v); Node root; root.set_op(opVar); diff --git a/backend/tests/codegen/ProtocolInstanceCall_test.cpp b/backend/tests/codegen/ProtocolInstanceCall_test.cpp index 333d4df8..3441923e 100644 --- a/backend/tests/codegen/ProtocolInstanceCall_test.cpp +++ b/backend/tests/codegen/ProtocolInstanceCall_test.cpp @@ -92,7 +92,7 @@ static void test_protocol_instance_call_lookup_and_invalidation(void **state) { // Register Var user/x RTValue varKeyword = Keyword_create(String_create("user/x")); Var *myVar = Var_create(varKeyword); - compState.varRegistry.registerObject("user/x", myVar); + compState.registerVar("user/x", myVar); // AST: (.m1 @user/x 10) Node callNode; diff --git a/backend/tests/codegen/VarCallIC_test.cpp b/backend/tests/codegen/VarCallIC_test.cpp index 21397a17..7c99cc4f 100644 --- a/backend/tests/codegen/VarCallIC_test.cpp +++ b/backend/tests/codegen/VarCallIC_test.cpp @@ -65,7 +65,7 @@ static void test_var_call_ic(void **state) { RTValue kw = Keyword_create(String_create("user/my-fn")); Var *v = Var_create(kw); Ptr_retain(v); - compState.varRegistry.registerObject("user/my-fn", v); + compState.registerVar("user/my-fn", v); // Function 1: Identity Node fn1Node = create_identity_fn(); From 40ec39c8d82d8ae62cae7da0529e889d42fbc5cd Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Thu, 28 May 2026 22:05:58 +0200 Subject: [PATCH 2/6] Implement namespaces. --- backend/runtime/Namespace.c | 32 +++++++++---------- backend/runtime/PersistentArrayMap.c | 1 + backend/runtime/Var.c | 9 ++++-- backend/runtime/tests/Metadata_test.c | 4 ++- backend/runtime/tests/Namespace_test.c | 5 ++- backend/runtime/tests/Var_Dynamic_test.c | 12 +++---- backend/runtime/tests/Var_Race_test.c | 10 +++--- backend/state/ThreadsafeCompilerState.cpp | 12 +++---- backend/tests/VarUAF_repro_test.cpp | 4 +-- backend/tests/codegen/Concurrency_test.cpp | 12 +++---- .../tests/codegen/ContextPropagation_test.cpp | 10 +++--- backend/tests/codegen/DoubleAddRepro_test.cpp | 2 +- backend/tests/codegen/DynamicInvoke_test.cpp | 21 ++++-------- backend/tests/codegen/InstanceCallIC_test.cpp | 4 +-- .../tests/codegen/InstanceCallNode_test.cpp | 2 +- backend/tests/codegen/InvokeNode_test.cpp | 20 +++++------- backend/tests/codegen/MathIntrinsics_test.cpp | 4 +-- backend/tests/codegen/NoThrow_test.cpp | 6 ++-- .../codegen/ProtocolInstanceCall_test.cpp | 2 +- backend/tests/codegen/VarCallIC_test.cpp | 12 +++---- 20 files changed, 86 insertions(+), 98 deletions(-) diff --git a/backend/runtime/Namespace.c b/backend/runtime/Namespace.c index 708bfc2e..2123a54c 100644 --- a/backend/runtime/Namespace.c +++ b/backend/runtime/Namespace.c @@ -238,39 +238,36 @@ Var *Namespace_intern(Namespace *self, Symbol *sym) { Ptr_retain(map); retain(symVal); - RTValue o = PersistentArrayMap_get(map, symVal); + RTValue existingVar = PersistentArrayMap_get(map, symVal); - if (!RT_isNil(o)) { - retain(o); + if (!RT_isNil(existingVar)) { Ptr_retain(self); Ptr_retain(sym); - if (Namespace_isInternedMapping(self, sym, o)) { + retain(existingVar); + if (Namespace_isInternedMapping(self, sym, existingVar)) { if (v != NULL) { release(RT_boxPtr((Object *)v)); - } else { - Ptr_release(self); - Ptr_release(sym); } - return (Var *)RT_unboxPtr(o); + Ptr_release(self); + Ptr_release(sym); + return (Var *)RT_unboxPtr(existingVar); } } if (v == NULL) { + Ptr_retain(self); + Ptr_retain(sym); v = Var_create_interned(self, sym); } RTValue newVal = RT_boxPtr((Object *)v); - if (!RT_isNil(o)) { + if (!RT_isNil(existingVar)) { Ptr_retain(self); - Ptr_retain(v->sym); - retain(o); + Ptr_retain(sym); + retain(existingVar); retain(newVal); - if (!Namespace_checkReplacement(self, sym, o, newVal)) { - release(newVal); - Ptr_release(self); - return (Var *)RT_unboxPtr(o); - } - release(o); + Namespace_checkReplacement(self, sym, existingVar, newVal); + release(existingVar); } Ptr_retain(map); @@ -284,6 +281,7 @@ Var *Namespace_intern(Namespace *self, Symbol *sym) { memory_order_acquire)) { autorelease(mapVal); Ptr_release(self); + Ptr_release(sym); return v; } diff --git a/backend/runtime/PersistentArrayMap.c b/backend/runtime/PersistentArrayMap.c index cd45e427..6aaff552 100644 --- a/backend/runtime/PersistentArrayMap.c +++ b/backend/runtime/PersistentArrayMap.c @@ -226,6 +226,7 @@ PersistentArrayMap *PersistentArrayMap_assoc(PersistentArrayMap *self, retVal->values[found] = value; if (!reusable) { PersistentArrayMap_retainChildren(retVal, found); + retain(retVal->keys[found]); Ptr_release(self); } release(key); diff --git a/backend/runtime/Var.c b/backend/runtime/Var.c index adf2bb07..2596154a 100644 --- a/backend/runtime/Var.c +++ b/backend/runtime/Var.c @@ -51,6 +51,7 @@ Var *Var_create_interned(struct Namespace *ns, struct Symbol *sym) { atomic_store_explicit(&(self->root), RT_boxNull(), memory_order_relaxed); self->dynamic = false; self->ns = ns; + Ptr_release(ns); // Consumes the refcount, but keeps it weak self->sym = sym; atomic_store_explicit(&self->metadata, RT_boxNil(), memory_order_relaxed); atomic_store_explicit(&self->rev, 0, memory_order_relaxed); @@ -218,8 +219,8 @@ RTValue Var_set(__attribute__((swift_context)) struct ExecutionContext *ctx, // Check if the var has a thread-local binding (Clojure behavior) PersistentArrayMap *m = RT_unboxPtr(ctx->bindingsMap); - Ptr_retain(m); - Ptr_retain(self); + Ptr_retain(m); // indexOf consumes map + Ptr_retain(self); // indexOf consumes key if (PersistentArrayMap_indexOf(m, RT_boxPtr(self)) == -1) { release(value); Ptr_release(self); @@ -227,8 +228,12 @@ RTValue Var_set(__attribute__((swift_context)) struct ExecutionContext *ctx, } retain(value); + RTValue oldMap = ctx->bindingsMap; + Ptr_retain(self); // assoc consumes key + Ptr_retain(m); // assoc consumes map ctx->bindingsMap = RT_boxPtr(PersistentArrayMap_assoc(m, RT_boxPtr(self), value)); + release(oldMap); Ptr_release(self); diff --git a/backend/runtime/tests/Metadata_test.c b/backend/runtime/tests/Metadata_test.c index a5c4e8e3..448dedc5 100644 --- a/backend/runtime/tests/Metadata_test.c +++ b/backend/runtime/tests/Metadata_test.c @@ -27,6 +27,7 @@ static void testSymbolMetadata(void **state) { assert_ptr_not_equal(sym, symWithMeta); + retain(RT_boxPtr(symWithMeta)); RTValue retrievedMeta = RT_meta(RT_boxPtr(symWithMeta)); // CONSUMES symWithMeta assert_true(equals(meta, retrievedMeta)); @@ -34,13 +35,14 @@ static void testSymbolMetadata(void **state) { release(retrievedMeta); release(meta); Ptr_release(sym); + Ptr_release(symWithMeta); }); } static void testVarMetadata(void **state) { (void)state; ASSERT_MEMORY_ALL_BALANCED({ - RTValue name = RT_boxPtr(String_create("my-var")); + Symbol *name = Symbol_create(String_create("my-var")); RTValue v = RT_boxPtr(Var_create(name)); RTValue meta = RT_boxPtr(PersistentArrayMap_create()); diff --git a/backend/runtime/tests/Namespace_test.c b/backend/runtime/tests/Namespace_test.c index a42f4301..88a83302 100644 --- a/backend/runtime/tests/Namespace_test.c +++ b/backend/runtime/tests/Namespace_test.c @@ -111,6 +111,8 @@ static void test_namespace_reference(void **state) { Namespace *ns = Namespace_create(nsSym); Symbol *sym = Symbol_create(String_create("inc")); + Ptr_retain(ns); + Ptr_retain(sym); Var *v = Var_create_interned(ns, sym); Ptr_retain(ns); Ptr_retain(sym); Ptr_retain(v); @@ -131,8 +133,9 @@ static void test_namespace_reference(void **state) { assert_true(RT_isNil(mappingAfter)); Ptr_release(v); - Ptr_release(ns); Ptr_release(sym); + Ptr_release(ns); + Ebr_force_reclaim(); }); } diff --git a/backend/runtime/tests/Var_Dynamic_test.c b/backend/runtime/tests/Var_Dynamic_test.c index d0efe638..7395be93 100644 --- a/backend/runtime/tests/Var_Dynamic_test.c +++ b/backend/runtime/tests/Var_Dynamic_test.c @@ -13,8 +13,7 @@ static void test_dynamic_var_binding(void **state) { (void)state; ASSERT_MEMORY_ALL_BALANCED({ - RTValue sym = Keyword_create(String_create("dynamic-var")); - retain(sym); + Symbol *sym = Symbol_create(String_create("dynamic-var")); Var *v = Var_create(sym); Var_setDynamic(v, true); @@ -24,8 +23,9 @@ static void test_dynamic_var_binding(void **state) { // ctx = {v: 10} // Note: PersistentArrayMap_createMany expects pairCount then key, value, // ... + Ptr_retain(v); PersistentArrayMap *m = PersistentArrayMap_createMany( - 1, sym, RT_boxPtr(BigInteger_createFromInt(10))); + 1, RT_boxPtr(v), RT_boxPtr(BigInteger_createFromInt(10))); ExecutionContext *ctx = ExecutionContext_create(RT_boxPtr(m)); // 1. Deref without context @@ -61,8 +61,7 @@ static void test_dynamic_var_binding(void **state) { static void test_dynamic_var_set(void **state) { (void)state; ASSERT_MEMORY_ALL_BALANCED({ - RTValue sym = Keyword_create(String_create("dynamic-var-set")); - retain(sym); + Symbol *sym = Symbol_create(String_create("dynamic-var-set")); Var *v = Var_create(sym); Var_setDynamic(v, true); @@ -70,8 +69,9 @@ static void test_dynamic_var_set(void **state) { Var_bindRoot(v, RT_boxPtr(BigInteger_createFromInt(1))); // Create initial context with v=10 + Ptr_retain(v); PersistentArrayMap *m = PersistentArrayMap_createMany( - 1, sym, RT_boxPtr(BigInteger_createFromInt(10))); + 1, RT_boxPtr(v), RT_boxPtr(BigInteger_createFromInt(10))); ExecutionContext *ctx = ExecutionContext_create(RT_boxPtr(m)); // 1. Verify initial state in context diff --git a/backend/runtime/tests/Var_Race_test.c b/backend/runtime/tests/Var_Race_test.c index 212a545a..8f020862 100644 --- a/backend/runtime/tests/Var_Race_test.c +++ b/backend/runtime/tests/Var_Race_test.c @@ -75,7 +75,7 @@ void *reader_thread(void *arg) { static void test_var_concurrent_bind_deref_race(void **state) { (void)state; atomic_store(&stop_threads, false); - RTValue sym = Keyword_create(String_create("race")); + Symbol *sym = Symbol_create(String_create("race")); Var *v = Var_create(sym); struct RaceArgs args = {v}; @@ -91,7 +91,7 @@ static void test_var_concurrent_bind_deref_race(void **state) { pthread_join(reader, NULL); Ptr_release(v); - release(sym); + Ebr_synchronize_and_reclaim(); Ebr_synchronize_and_reclaim(); } @@ -139,7 +139,7 @@ void *destruction_writer(void *arg) { // Reset for next iteration (re-init struct members that destroy cleared) // Note: This is an artificial test of Var internal state atomic_store(&v->root, RT_boxNull()); - v->keyword = Keyword_create(String_create("temp")); + v->sym = Symbol_create(String_create("temp")); // Wait for reader to finish its part of the iteration while (atomic_load(&args->reader_done) < i && !atomic_load(&stop_threads)) { @@ -194,7 +194,7 @@ void *destruction_reader(void *arg) { static void test_var_destruction_hazard_race_stable(void **state) { (void)state; atomic_store(&stop_threads, false); - RTValue sym = Keyword_create(String_create("stable")); + Symbol *sym = Symbol_create(String_create("stable")); Var *v = Var_create(sym); struct DestructionRaceArgs args = { .v = v, .iteration = 0, .reader_done = 0, .reader_entering = false}; @@ -207,7 +207,7 @@ static void test_var_destruction_hazard_race_stable(void **state) { pthread_join(reader, NULL); Ptr_release(v); - release(sym); + Ebr_synchronize_and_reclaim(); Ebr_synchronize_and_reclaim(); } diff --git a/backend/state/ThreadsafeCompilerState.cpp b/backend/state/ThreadsafeCompilerState.cpp index 6a694f49..d924f889 100644 --- a/backend/state/ThreadsafeCompilerState.cpp +++ b/backend/state/ThreadsafeCompilerState.cpp @@ -586,15 +586,14 @@ void ThreadsafeCompilerState::registerVar(const char *name, Var *var) { // 1. Znajdź lub utwórz Namespace Symbol *nsSym = Symbol_create(String_createDynamicStr(nsName.c_str())); - Namespace *ns = Namespace_findOrCreate(nsSym); // ns ma +1 refcount + Namespace *ns = Namespace_findOrCreate(nsSym); // 2. Utwórz Symbol dla Var - Symbol *varSym = Symbol_create(String_createDynamicStr(symName.c_str())); // varSym ma +1 refcount + Symbol *varSym = Symbol_create(String_createDynamicStr(symName.c_str())); // 3. Uzupełnij pola ns i sym w strukturze Var, jeśli są puste if (var->ns == nullptr) { var->ns = ns; - Ptr_retain(ns); } if (var->sym == nullptr) { var->sym = varSym; @@ -602,11 +601,8 @@ void ThreadsafeCompilerState::registerVar(const char *name, Var *var) { } // 4. Przypisz Var w Namespace za pomocą referencji - Ptr_retain(var); - Var *res = Namespace_refer(ns, varSym, var); - - // Zwalniamy nieużywany wskaźnik zwrócony przez refer (+1 refcount) - Ptr_release(res); + Var *referred = Namespace_refer(ns, varSym, var); + Ptr_release(referred); } // void registerObject(const char *name, T *newDef) { diff --git a/backend/tests/VarUAF_repro_test.cpp b/backend/tests/VarUAF_repro_test.cpp index 9b8553df..5fc016be 100644 --- a/backend/tests/VarUAF_repro_test.cpp +++ b/backend/tests/VarUAF_repro_test.cpp @@ -84,12 +84,12 @@ static void test_double_release_uaf_repro(void **state_arg) { // Actually, it's easier to use VarNode to pass them as if they are from // global vars. - Var *v1 = Var_create(Keyword_create(String_create("user/v1"))); + Var *v1 = Var_create(Symbol_create(String_create("user/v1"))); Ptr_retain(v1); // Retain so it survives bindRoot consumption Var_bindRoot(v1, s1); compState.registerVar("user/v1", v1); - Var *v2 = Var_create(Keyword_create(String_create("user/v2"))); + Var *v2 = Var_create(Symbol_create(String_create("user/v2"))); Ptr_retain(v2); Var_bindRoot(v2, s2); compState.registerVar("user/v2", v2); diff --git a/backend/tests/codegen/Concurrency_test.cpp b/backend/tests/codegen/Concurrency_test.cpp index f215498d..1ac85d4c 100644 --- a/backend/tests/codegen/Concurrency_test.cpp +++ b/backend/tests/codegen/Concurrency_test.cpp @@ -114,15 +114,11 @@ static void test_concurrent_closures(void **state) { engine.compileAST(fnDef, "closure_builder").get().address; RTValue funObj = builderAddr.toPtr()(); - // Promote the function object and its captures to shared! - promoteToShared(funObj); - - // Find/Create the Var. Var_create returns with +1 (shared). + // Find/Create the Var via ThreadsafeCompilerState so it is properly interned. const char *varName = "user/shared-fn"; - Var *v = Var_create(Keyword_create(String_create(varName))); - Ptr_retain(v); // For Var_bindRoot (consumes 1) + Var *v = engine.threadsafeState.getOrCreateVar(varName); // Returns +1 + Ptr_retain(v); // for Var_bindRoot Var_bindRoot(v, funObj); - engine.threadsafeState.registerVar(varName, v); // 2. Create the caller // (fn [] (user/shared-fn 5)) @@ -171,6 +167,8 @@ static void test_concurrent_closures(void **state) { start.store(true); for (auto &t : threads) t.join(); + + Var_unbindRoot(v); }); } diff --git a/backend/tests/codegen/ContextPropagation_test.cpp b/backend/tests/codegen/ContextPropagation_test.cpp index ed5847ef..7cda44a0 100644 --- a/backend/tests/codegen/ContextPropagation_test.cpp +++ b/backend/tests/codegen/ContextPropagation_test.cpp @@ -30,9 +30,7 @@ static void test_context_propagation_in_jit(void **state) { JITEngine engine; rt::ThreadsafeCompilerState &compState = engine.threadsafeState; - RTValue varKeyword = Keyword_create(String_create("user/test-var")); - - Var *myVar = Var_create(varKeyword); + Var *myVar = compState.getOrCreateVar("user/test-var"); Var_setDynamic(myVar, true); Ptr_retain(myVar); @@ -64,7 +62,6 @@ static void test_context_propagation_in_jit(void **state) { compState.classRegistry.registerObject("clojure.lang.Var", varCls); compState.classRegistry.registerObject(varCls, varType); - compState.registerVar("user/test-var", myVar); Node callNode; callNode.set_op(opInstanceCall); @@ -83,11 +80,14 @@ static void test_context_propagation_in_jit(void **state) { RTValue val100 = RT_boxInt32(100); PersistentArrayMap *m = - PersistentArrayMap_createMany(1, varKeyword, val100); + PersistentArrayMap_createMany(1, RT_boxPtr(myVar), val100); ExecutionContext *ctx = ExecutionContext_create(RT_boxPtr(m)); RTValue result = fn(ctx); + printf("Result Type: %d, Value: %ld\n", getType(result), + RT_isInt32(result) ? (long)RT_unboxInt32(result) : -1); + assert_true(RT_isInt32(result)); assert_int_equal(RT_unboxInt32(result), 100); diff --git a/backend/tests/codegen/DoubleAddRepro_test.cpp b/backend/tests/codegen/DoubleAddRepro_test.cpp index 4d13160c..90deade2 100644 --- a/backend/tests/codegen/DoubleAddRepro_test.cpp +++ b/backend/tests/codegen/DoubleAddRepro_test.cpp @@ -31,7 +31,7 @@ static void test_double_add_repro(void **state) { // (def a "aa") { - RTValue k = Keyword_create(String_create("a")); + Symbol *k = Symbol_create(String_create("a")); Var *v = Var_create(k); RTValue str = RT_boxPtr(String_create("aa")); diff --git a/backend/tests/codegen/DynamicInvoke_test.cpp b/backend/tests/codegen/DynamicInvoke_test.cpp index 5af8b31c..422123b6 100644 --- a/backend/tests/codegen/DynamicInvoke_test.cpp +++ b/backend/tests/codegen/DynamicInvoke_test.cpp @@ -377,11 +377,8 @@ static void test_dynamic_invoke_arity_exception_erased(void **state) { // 1. Create a Var and bind a 1-arity function to it const char *varName = "user/dynamic-arity-ex-erased"; - RTValue kw = Keyword_create(String_create(varName)); - Var *v = Var_create(kw); - Ptr_retain(v); - Ptr_retain(v); - compState.registerVar(varName, v); + Var *v = compState.getOrCreateVar(varName); + Ptr_retain(v); // for Var_bindRoot // fn = (fn [x] x) Node fnNode; @@ -435,11 +432,8 @@ static void test_dynamic_invoke_intermediate_throw(void **state) { // 1. Create a Var and bind the identity function const char *varName = "user/f-throw"; - RTValue kw = Keyword_create(String_create(varName)); - Var *v = Var_create(kw); - Ptr_retain(v); - Ptr_retain(v); - compState.registerVar(varName, v); + Var *v = compState.getOrCreateVar(varName); + Ptr_retain(v); // for Var_bindRoot Node fnNode; create_identity_fn(fnNode); @@ -521,11 +515,8 @@ static void test_dynamic_invoke_nested_guidance(void **state) { // 1. Create a Var and bind the identity function const char *varName = "user/f-nested"; - RTValue kw = Keyword_create(String_create(varName)); - Var *v = Var_create(kw); - Ptr_retain(v); - Ptr_retain(v); - compState.registerVar(varName, v); + Var *v = compState.getOrCreateVar(varName); + Ptr_retain(v); // for Var_bindRoot Node fnNode; create_identity_fn(fnNode); diff --git a/backend/tests/codegen/InstanceCallIC_test.cpp b/backend/tests/codegen/InstanceCallIC_test.cpp index d259c2e0..312314b5 100644 --- a/backend/tests/codegen/InstanceCallIC_test.cpp +++ b/backend/tests/codegen/InstanceCallIC_test.cpp @@ -97,7 +97,7 @@ static void test_instance_call_ic_hit_miss(void **state) { setup_ic_test_metadata(compState); // Register a Var "user/my-var" to hold our instance - RTValue varKeyword = Keyword_create(String_create("user/my-var")); + Symbol *varKeyword = Symbol_create(String_create("user/my-var")); Var *myVar = Var_create(varKeyword); compState.registerVar("user/my-var", myVar); @@ -178,7 +178,7 @@ static void test_instance_call_ic_atomicity(void **state) { setup_ic_test_metadata(compState); // Register a Var "user/my-var" to hold our instance - RTValue varKeyword = Keyword_create(String_create("user/my-var")); + Symbol *varKeyword = Symbol_create(String_create("user/my-var")); Var *myVar = Var_create(varKeyword); compState.registerVar("user/my-var", myVar); diff --git a/backend/tests/codegen/InstanceCallNode_test.cpp b/backend/tests/codegen/InstanceCallNode_test.cpp index 76804432..349ac1ff 100644 --- a/backend/tests/codegen/InstanceCallNode_test.cpp +++ b/backend/tests/codegen/InstanceCallNode_test.cpp @@ -283,7 +283,7 @@ static void test_instance_call_fast_path_error(void **state) { Ptr_release(cls); } - RTValue vk = Keyword_create(String_create("user/my-vec")); + Symbol *vk = Symbol_create(String_create("user/my-vec")); Var *myVar = Var_create(vk); compState.registerVar("user/my-vec", myVar); diff --git a/backend/tests/codegen/InvokeNode_test.cpp b/backend/tests/codegen/InvokeNode_test.cpp index 4d2467f3..4b3ee38a 100644 --- a/backend/tests/codegen/InvokeNode_test.cpp +++ b/backend/tests/codegen/InvokeNode_test.cpp @@ -2,6 +2,7 @@ #include #include "../../codegen/CodeGen.h" #include "../../jit/JITEngine.h" +#include "../../runtime/Keyword.h" #include "../../runtime/Object.h" #include "../../runtime/Function.h" @@ -190,11 +191,8 @@ static void test_dynamic_invoke_arity_mismatch_leak(void **state) { // 1. Create a Var and bind a 1-arity function to it const char *varName = "user/dynamic-arity-fn"; - RTValue kw = Keyword_create(String_create(varName)); - Var *v = Var_create(kw); - Ptr_retain(v); - Ptr_retain(v); - compState.registerVar(varName, v); + Var *v = compState.getOrCreateVar(varName); + Ptr_retain(v); // for Var_bindRoot // fn = (fn [x] x) Node fnNode; @@ -296,7 +294,7 @@ static void test_var_call_unbound(void **state) { // 1. Create a Var but do NOT bind it const char *varName = "user/unbound-fn"; - RTValue kw = Keyword_create(String_create(varName)); + Symbol *kw = Symbol_create(String_create(varName)); Var *v = Var_create(kw); Ptr_retain(v); compState.registerVar(varName, v); @@ -413,11 +411,9 @@ static void test_dynamic_keyword_invoke_on_number(void **state) { JITEngine engine; RTValue kwVal = Keyword_create(String_create("foo")); - Var *v = Var_create(kwVal); - Ptr_retain(v); - Var_bindRoot(v, kwVal); // Var_bindRoot releases v once, so we retained it above - Ptr_retain(v); // Retain again so the registry and our manual release both have one - engine.threadsafeState.registerVar("v_kw", v); + Var *v = engine.threadsafeState.getOrCreateVar("user/v_kw"); + Ptr_retain(v); // for Var_bindRoot + Var_bindRoot(v, kwVal); Node root; root.set_op(opInvoke); @@ -425,7 +421,7 @@ static void test_dynamic_keyword_invoke_on_number(void **state) { auto *fn = inv->mutable_fn(); fn->set_op(opVar); - fn->mutable_subnode()->mutable_var()->set_var("#'v_kw"); + fn->mutable_subnode()->mutable_var()->set_var("#'user/v_kw"); auto *arg = inv->add_args(); arg->set_op(opConst); diff --git a/backend/tests/codegen/MathIntrinsics_test.cpp b/backend/tests/codegen/MathIntrinsics_test.cpp index 6cb07732..36454fd3 100644 --- a/backend/tests/codegen/MathIntrinsics_test.cpp +++ b/backend/tests/codegen/MathIntrinsics_test.cpp @@ -182,7 +182,7 @@ static void test_math_sqrt_leak_repro(void **state) { // Create a String 'a' and bind it to a Var { - RTValue k = Keyword_create(String_create("a")); + Symbol *k = Symbol_create(String_create("a")); Var *v = Var_create(k); RTValue str = RT_boxPtr(String_create("aa")); @@ -239,7 +239,7 @@ static void test_math_pow_var(void **state) { // (def a 2) { RTValue v_a = RT_boxInt32(2); - RTValue k = Keyword_create(String_create("a")); + Symbol *k = Symbol_create(String_create("a")); Var *v = Var_create(k); Ptr_retain(v); diff --git a/backend/tests/codegen/NoThrow_test.cpp b/backend/tests/codegen/NoThrow_test.cpp index eb237f6d..a008fd67 100644 --- a/backend/tests/codegen/NoThrow_test.cpp +++ b/backend/tests/codegen/NoThrow_test.cpp @@ -61,9 +61,9 @@ static void test_hot_cold_separation(void **state) { // Define vars so they are found auto *nameX = String_createDynamicStr("x"); - compState.registerVar("x", Var_create(RT_boxPtr(nameX))); + compState.registerVar("x", Var_create(Symbol_create(nameX))); auto *nameY = String_createDynamicStr("y"); - compState.registerVar("y", Var_create(RT_boxPtr(nameY))); + compState.registerVar("y", Var_create(Symbol_create(nameY))); Node root; root.set_op(opVector); @@ -175,7 +175,7 @@ static void test_whitelist_works(void **state) { // Register the var manually so it's found auto *nameStr = String_createDynamicStr("clojure.core/*print-length*"); - Var *v = Var_create(RT_boxPtr(nameStr)); + Var *v = Var_create(Symbol_create(nameStr)); compState.registerVar("clojure.core/*print-length*", v); Node root; diff --git a/backend/tests/codegen/ProtocolInstanceCall_test.cpp b/backend/tests/codegen/ProtocolInstanceCall_test.cpp index 3441923e..22083802 100644 --- a/backend/tests/codegen/ProtocolInstanceCall_test.cpp +++ b/backend/tests/codegen/ProtocolInstanceCall_test.cpp @@ -90,7 +90,7 @@ static void test_protocol_instance_call_lookup_and_invalidation(void **state) { setupC1("mock_P1_m1_v1"); // Register Var user/x - RTValue varKeyword = Keyword_create(String_create("user/x")); + Symbol *varKeyword = Symbol_create(String_create("user/x")); Var *myVar = Var_create(varKeyword); compState.registerVar("user/x", myVar); diff --git a/backend/tests/codegen/VarCallIC_test.cpp b/backend/tests/codegen/VarCallIC_test.cpp index 7c99cc4f..77864abc 100644 --- a/backend/tests/codegen/VarCallIC_test.cpp +++ b/backend/tests/codegen/VarCallIC_test.cpp @@ -61,11 +61,9 @@ static void test_var_call_ic(void **state) { rt::JITEngine engine; auto &compState = engine.threadsafeState; - // 1. Create a Var and bind a function to it - RTValue kw = Keyword_create(String_create("user/my-fn")); - Var *v = Var_create(kw); - Ptr_retain(v); - compState.registerVar("user/my-fn", v); + Var *v = compState.getOrCreateVar("user/my-fn"); + printf("Var v from getOrCreateVar is %p\n", (void*)v); + Ptr_retain(v); // for Var_bindRoot // Function 1: Identity Node fn1Node = create_identity_fn(); @@ -74,7 +72,7 @@ static void test_var_call_ic(void **state) { .get(); RTValue (*fn1Ctor)() = fn1Res.address.toPtr(); RTValue fn1 = fn1Ctor(); - Ptr_retain(v); + Ptr_retain(v); // for Var_bindRoot Var_bindRoot(v, fn1); // 2. Create an InvokeNode calling #'user/my-fn @@ -115,7 +113,7 @@ static void test_var_call_ic(void **state) { RTValue (*fn2Ctor)() = fn2Res.address.toPtr(); RTValue fn2 = fn2Ctor(); - Ptr_retain(v); + Ptr_retain(v); // for Var_bindRoot Var_bindRoot(v, fn2); // 5. Call again: IC miss due to change, update IC, slow path From 95c6fc8213a6c18dfc8110661ae388c9a37adbbe Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Thu, 28 May 2026 22:08:41 +0200 Subject: [PATCH 3/6] Better soul. --- SOUL.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/SOUL.md b/SOUL.md index 86d582af..17fddd07 100644 --- a/SOUL.md +++ b/SOUL.md @@ -1,10 +1,15 @@ -We're building a clojure JIT compiler in LLVM/C++ API. We use reference counting with consuming semantics and NaN boxing. +We're building a clojure JIT compiler in LLVM/C++ API. -There's two components here - one is the frontend - a clojure program that creates AST, computes memory guidance (dropMemory for happy path and unwindMemory for landing pads) -and builds the binary proto. +There's two components here - one is the frontend - a clojure program that creates AST, computes memory guidance (dropMemory for happy path and unwindMemory for landing pads) and builds the binary proto. + + +In the backend we use reference counting with consuming semantics and NaN boxing. + +The entire memory architecture relies on consuming semantics. The only exceptions are runtime functions explicitly annotated with 'outside ref count', which operate on borrowing semantics. There is also a special case regarding the reference cycle between a namespace and a var—since a var is designed to hold a weak reference to its namespace, an additional release is required during its construction to properly satisfy the consuming semantics. The second - the backend which reads proto and compiles/executes. Our JIT currently compiles the proto forms using LLVMs ORC2 and links them, on IR level with runtime written in C - which allows for huge inlining optimisations. -When working with tests - always stop and ask for permission if you want to change non-test code. \ No newline at end of file +When working with tests - always stop and ask for permission if you want to change non-test code. + From 7e51f568344895f32f9c3fea7e6d8d735087a7e5 Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Fri, 29 May 2026 08:54:08 +0200 Subject: [PATCH 4/6] Interning fix. --- backend/runtime/Namespace.c | 74 +++++++++--- backend/runtime/Namespace.h | 2 +- backend/runtime/Var.c | 2 +- backend/runtime/tests/Namespace_test.c | 108 ++++++++++++++++++ backend/state/ThreadsafeCompilerState.cpp | 3 +- backend/tests/VarUAF_repro_test.cpp | 4 +- backend/tests/codegen/InstanceCallIC_test.cpp | 4 +- .../tests/codegen/InstanceCallNode_test.cpp | 2 +- backend/tests/codegen/InvokeNode_test.cpp | 4 +- .../codegen/ProtocolInstanceCall_test.cpp | 2 +- backend/tests/codegen/VarCallIC_test.cpp | 1 - 11 files changed, 180 insertions(+), 26 deletions(-) diff --git a/backend/runtime/Namespace.c b/backend/runtime/Namespace.c index 2123a54c..e152011c 100644 --- a/backend/runtime/Namespace.c +++ b/backend/runtime/Namespace.c @@ -193,18 +193,10 @@ bool Namespace_isInternedMapping(Namespace *self, Symbol *sym, RTValue o) { * to overwrite a Var interned in this namespace. Consumes all arguments: * `self`, `sym`, `oldVal`, and `newVal`. */ -void Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, +bool Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, RTValue newVal) { - if (getType(oldVal) == varType) { - if (Namespace_isInternedMapping(self, sym, oldVal)) { - release(newVal); - throwIllegalStateException_C("REJECTED: attempt to replace interned var"); - } - release(newVal); - return; - } - - + // In Clojure, once a mapping is established, it cannot be replaced with a different value/Var/Class. + return false; } /* @@ -266,7 +258,36 @@ Var *Namespace_intern(Namespace *self, Symbol *sym) { Ptr_retain(sym); retain(existingVar); retain(newVal); - Namespace_checkReplacement(self, sym, existingVar, newVal); + if (!Namespace_checkReplacement(self, sym, existingVar, newVal)) { + retain(existingVar); + String *oStr = toString(existingVar); + + Ptr_retain(oStr); + String *flatOStr = String_compactify(oStr); + + char buf[512]; + snprintf(buf, sizeof(buf), "%s already refers to: %s in namespace: %s", + String_c_str(sym->name), String_c_str(flatOStr), String_c_str(self->name->name)); + + Ptr_release(flatOStr); + Ptr_release(oStr); + + release(existingVar); + release(newVal); + Ptr_release(sym); + Ptr_release(self); + if (v != NULL) { + release(RT_boxPtr((Object *)v)); + } + release(existingVar); + Ptr_release(self); + Ptr_release(sym); + throwIllegalStateException_C(buf); + } + release(existingVar); + release(newVal); + Ptr_release(sym); + Ptr_release(self); release(existingVar); } @@ -325,7 +346,34 @@ RTValue Namespace_reference(Namespace *self, Symbol *sym, RTValue val) { Ptr_retain(sym); retain(o); retain(val); - Namespace_checkReplacement(self, sym, o, val); + if (!Namespace_checkReplacement(self, sym, o, val)) { + retain(o); + String *oStr = toString(o); + + Ptr_retain(oStr); + String *flatOStr = String_compactify(oStr); + + char buf[512]; + snprintf(buf, sizeof(buf), "%s already refers to: %s in namespace: %s", + String_c_str(sym->name), String_c_str(flatOStr), String_c_str(self->name->name)); + + Ptr_release(flatOStr); + Ptr_release(oStr); + + release(o); + release(val); + Ptr_release(sym); + Ptr_release(self); + release(o); + Ptr_release(self); + Ptr_release(sym); + release(val); + throwIllegalStateException_C(buf); + } + release(o); + release(val); + Ptr_release(sym); + Ptr_release(self); release(o); } diff --git a/backend/runtime/Namespace.h b/backend/runtime/Namespace.h index 977d92c4..b8425208 100644 --- a/backend/runtime/Namespace.h +++ b/backend/runtime/Namespace.h @@ -32,7 +32,7 @@ void Namespace_promoteToShared(Namespace *self, uword_t count); RTValue Namespace_getMappings(Namespace *self); RTValue Namespace_getAliases(Namespace *self); bool Namespace_isInternedMapping(Namespace *self, Symbol *sym, RTValue o); -void Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, RTValue neuVal); +bool Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, RTValue neuVal); Var *Namespace_intern(Namespace *self, Symbol *sym); RTValue Namespace_reference(Namespace *self, Symbol *sym, RTValue val); RTValue Namespace_referenceClass(Namespace *self, Symbol *sym, Class *val); diff --git a/backend/runtime/Var.c b/backend/runtime/Var.c index 2596154a..7759100e 100644 --- a/backend/runtime/Var.c +++ b/backend/runtime/Var.c @@ -67,7 +67,7 @@ bool Var_equals(Var *self, Var *other) { uword_t Var_hash(Var *self) { /* Vars are purely pointer based entities */ - return hash(RT_boxPtr(self)); + return avalanche((uword_t)self); }; String *Var_toString(Var *self) { diff --git a/backend/runtime/tests/Namespace_test.c b/backend/runtime/tests/Namespace_test.c index 88a83302..e2c5b2f1 100644 --- a/backend/runtime/tests/Namespace_test.c +++ b/backend/runtime/tests/Namespace_test.c @@ -207,6 +207,111 @@ static void test_namespace_var_circular_leak(void **state) { }); } +static void test_namespace_redefine_referred_var_should_throw(void **state) { + (void)state; + ASSERT_MEMORY_BALANCED_EXCEPT_STRINGS({ + Symbol *nsSym = Symbol_create(String_create("my.ns")); + Namespace *ns = Namespace_create(nsSym); + + Symbol *otherNsSym = Symbol_create(String_create("other.ns")); + Namespace *otherNs = Namespace_create(otherNsSym); + + Symbol *sym = Symbol_create(String_create("foo")); + + Ptr_retain(otherNs); Ptr_retain(sym); + Var *otherVar = Var_create_interned(otherNs, sym); + + // Refer otherVar in ns under "foo" + Ptr_retain(ns); Ptr_retain(sym); Ptr_retain(otherVar); + Var *ref = Namespace_refer(ns, sym, otherVar); + Ptr_release(ref); + + // Now try to intern "foo" in ns. This should throw IllegalStateException! + Ptr_retain(ns); Ptr_retain(sym); + ASSERT_THROWS("IllegalStateException", { + Var *v = Namespace_intern(ns, sym); + Ptr_release(v); + }); + + Ptr_release(otherVar); + Ptr_release(otherNs); + Ptr_release(ns); + Ptr_release(sym); + Ebr_force_reclaim(); + }); +} + +static void test_namespace_redefine_class_should_throw(void **state) { + (void)state; + ASSERT_MEMORY_BALANCED_EXCEPT_STRINGS({ + Symbol *nsSym = Symbol_create(String_create("my.ns")); + Namespace *ns = Namespace_create(nsSym); + + Symbol *sym = Symbol_create(String_create("foo")); + + // Map an arbitrary non-Var value, say a String, to sym + String *strVal = String_create("some class or other value"); + + Ptr_retain(ns); Ptr_retain(sym); retain(RT_boxPtr((Object *)strVal)); + RTValue ref = Namespace_reference(ns, sym, RT_boxPtr((Object *)strVal)); + release(ref); + + // Now try to intern "foo" in ns. This should throw IllegalStateException! + Ptr_retain(ns); Ptr_retain(sym); + ASSERT_THROWS("IllegalStateException", { + Var *v = Namespace_intern(ns, sym); + Ptr_release(v); + }); + + Ptr_release(ns); + Ptr_release(sym); + Ptr_release(strVal); + Ebr_force_reclaim(); + }); +} + +static void test_namespace_redefine_referred_var_with_other_reference_should_throw(void **state) { + (void)state; + ASSERT_MEMORY_BALANCED_EXCEPT_STRINGS({ + Symbol *nsSym = Symbol_create(String_create("my.ns")); + Namespace *ns = Namespace_create(nsSym); + + Symbol *otherNsSym = Symbol_create(String_create("other.ns")); + Namespace *otherNs = Namespace_create(otherNsSym); + + Symbol *thirdNsSym = Symbol_create(String_create("third.ns")); + Namespace *thirdNs = Namespace_create(thirdNsSym); + + Symbol *sym = Symbol_create(String_create("foo")); + + Ptr_retain(otherNs); Ptr_retain(sym); + Var *otherVar = Var_create_interned(otherNs, sym); + + Ptr_retain(thirdNs); Ptr_retain(sym); + Var *thirdVar = Var_create_interned(thirdNs, sym); + + // Refer otherVar in ns under "foo" + Ptr_retain(ns); Ptr_retain(sym); Ptr_retain(otherVar); + Var *ref1 = Namespace_refer(ns, sym, otherVar); + Ptr_release(ref1); + + // Now try to refer thirdVar in ns under "foo". This should throw IllegalStateException! + Ptr_retain(ns); Ptr_retain(sym); Ptr_retain(thirdVar); + ASSERT_THROWS("IllegalStateException", { + Var *ref2 = Namespace_refer(ns, sym, thirdVar); + Ptr_release(ref2); + }); + + Ptr_release(otherVar); + Ptr_release(thirdVar); + Ptr_release(otherNs); + Ptr_release(thirdNs); + Ptr_release(ns); + Ptr_release(sym); + Ebr_force_reclaim(); + }); +} + int main(void) { RuntimeInterface_initialise(); const struct CMUnitTest tests[] = { @@ -216,6 +321,9 @@ int main(void) { cmocka_unit_test(test_namespace_reference), cmocka_unit_test(test_namespace_global_registry), cmocka_unit_test(test_namespace_var_circular_leak), + cmocka_unit_test(test_namespace_redefine_referred_var_should_throw), + cmocka_unit_test(test_namespace_redefine_class_should_throw), + cmocka_unit_test(test_namespace_redefine_referred_var_with_other_reference_should_throw), }; int res = cmocka_run_group_tests(tests, NULL, NULL); RuntimeInterface_cleanup(); diff --git a/backend/state/ThreadsafeCompilerState.cpp b/backend/state/ThreadsafeCompilerState.cpp index d924f889..2d303bc5 100644 --- a/backend/state/ThreadsafeCompilerState.cpp +++ b/backend/state/ThreadsafeCompilerState.cpp @@ -1,6 +1,7 @@ #include "ThreadsafeCompilerState.h" #include "../bridge/Exceptions.h" #include "../runtime/Namespace.h" +#include "../runtime/Symbol.h" #include "../runtime/Var.h" #include "../tools/EdnParser.h" #include "../tools/RTValueWrapper.h" @@ -600,7 +601,7 @@ void ThreadsafeCompilerState::registerVar(const char *name, Var *var) { Ptr_retain(varSym); } - // 4. Przypisz Var w Namespace za pomocą referencji + // 4. Przypisz Var w Namespace za pomocą referencji (consumes var) Var *referred = Namespace_refer(ns, varSym, var); Ptr_release(referred); } diff --git a/backend/tests/VarUAF_repro_test.cpp b/backend/tests/VarUAF_repro_test.cpp index 5fc016be..0a76a97a 100644 --- a/backend/tests/VarUAF_repro_test.cpp +++ b/backend/tests/VarUAF_repro_test.cpp @@ -84,12 +84,12 @@ static void test_double_release_uaf_repro(void **state_arg) { // Actually, it's easier to use VarNode to pass them as if they are from // global vars. - Var *v1 = Var_create(Symbol_create(String_create("user/v1"))); + Var *v1 = Var_create(Symbol_create(String_create("v1"))); Ptr_retain(v1); // Retain so it survives bindRoot consumption Var_bindRoot(v1, s1); compState.registerVar("user/v1", v1); - Var *v2 = Var_create(Symbol_create(String_create("user/v2"))); + Var *v2 = Var_create(Symbol_create(String_create("v2"))); Ptr_retain(v2); Var_bindRoot(v2, s2); compState.registerVar("user/v2", v2); diff --git a/backend/tests/codegen/InstanceCallIC_test.cpp b/backend/tests/codegen/InstanceCallIC_test.cpp index 312314b5..93f8ca90 100644 --- a/backend/tests/codegen/InstanceCallIC_test.cpp +++ b/backend/tests/codegen/InstanceCallIC_test.cpp @@ -97,7 +97,7 @@ static void test_instance_call_ic_hit_miss(void **state) { setup_ic_test_metadata(compState); // Register a Var "user/my-var" to hold our instance - Symbol *varKeyword = Symbol_create(String_create("user/my-var")); + Symbol *varKeyword = Symbol_create(String_create("my-var")); Var *myVar = Var_create(varKeyword); compState.registerVar("user/my-var", myVar); @@ -178,7 +178,7 @@ static void test_instance_call_ic_atomicity(void **state) { setup_ic_test_metadata(compState); // Register a Var "user/my-var" to hold our instance - Symbol *varKeyword = Symbol_create(String_create("user/my-var")); + Symbol *varKeyword = Symbol_create(String_create("my-var")); Var *myVar = Var_create(varKeyword); compState.registerVar("user/my-var", myVar); diff --git a/backend/tests/codegen/InstanceCallNode_test.cpp b/backend/tests/codegen/InstanceCallNode_test.cpp index 349ac1ff..fd469ac6 100644 --- a/backend/tests/codegen/InstanceCallNode_test.cpp +++ b/backend/tests/codegen/InstanceCallNode_test.cpp @@ -283,7 +283,7 @@ static void test_instance_call_fast_path_error(void **state) { Ptr_release(cls); } - Symbol *vk = Symbol_create(String_create("user/my-vec")); + Symbol *vk = Symbol_create(String_create("my-vec")); Var *myVar = Var_create(vk); compState.registerVar("user/my-vec", myVar); diff --git a/backend/tests/codegen/InvokeNode_test.cpp b/backend/tests/codegen/InvokeNode_test.cpp index 4b3ee38a..f40c6a7f 100644 --- a/backend/tests/codegen/InvokeNode_test.cpp +++ b/backend/tests/codegen/InvokeNode_test.cpp @@ -294,9 +294,8 @@ static void test_var_call_unbound(void **state) { // 1. Create a Var but do NOT bind it const char *varName = "user/unbound-fn"; - Symbol *kw = Symbol_create(String_create(varName)); + Symbol *kw = Symbol_create(String_create("unbound-fn")); Var *v = Var_create(kw); - Ptr_retain(v); compState.registerVar(varName, v); // 2. Create an InvokeNode calling #'user/unbound-fn @@ -334,7 +333,6 @@ static void test_var_call_unbound(void **state) { assert_true(caught); // 4. Cleanup - Ptr_release(v); engine.retireModule("var_call_unbound_test"); }); } diff --git a/backend/tests/codegen/ProtocolInstanceCall_test.cpp b/backend/tests/codegen/ProtocolInstanceCall_test.cpp index 22083802..37fd35ac 100644 --- a/backend/tests/codegen/ProtocolInstanceCall_test.cpp +++ b/backend/tests/codegen/ProtocolInstanceCall_test.cpp @@ -90,7 +90,7 @@ static void test_protocol_instance_call_lookup_and_invalidation(void **state) { setupC1("mock_P1_m1_v1"); // Register Var user/x - Symbol *varKeyword = Symbol_create(String_create("user/x")); + Symbol *varKeyword = Symbol_create(String_create("x")); Var *myVar = Var_create(varKeyword); compState.registerVar("user/x", myVar); diff --git a/backend/tests/codegen/VarCallIC_test.cpp b/backend/tests/codegen/VarCallIC_test.cpp index 77864abc..e910a87c 100644 --- a/backend/tests/codegen/VarCallIC_test.cpp +++ b/backend/tests/codegen/VarCallIC_test.cpp @@ -63,7 +63,6 @@ static void test_var_call_ic(void **state) { Var *v = compState.getOrCreateVar("user/my-fn"); printf("Var v from getOrCreateVar is %p\n", (void*)v); - Ptr_retain(v); // for Var_bindRoot // Function 1: Identity Node fn1Node = create_identity_fn(); From 1c274e53e8c66a72844957feae5edaf045f29451 Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Fri, 29 May 2026 09:11:32 +0200 Subject: [PATCH 5/6] Fix some additional problems. --- backend/runtime/Namespace.c | 44 ++++++++++------------- backend/runtime/Namespace.h | 6 ++-- backend/runtime/tests/Keyword_test.c | 1 + backend/runtime/tests/Namespace_test.c | 30 ++++++++++------ backend/state/ThreadsafeCompilerState.cpp | 29 ++++++++++----- frontend/resources/rt-classes.edn | 6 ++-- 6 files changed, 65 insertions(+), 51 deletions(-) diff --git a/backend/runtime/Namespace.c b/backend/runtime/Namespace.c index e152011c..2e0c4e27 100644 --- a/backend/runtime/Namespace.c +++ b/backend/runtime/Namespace.c @@ -35,13 +35,9 @@ Namespace *Namespace_create(Symbol *name) { * Returns the retained Namespace (+1 refcount) if found, or NULL otherwise. * Consumes `name` argument. */ -Namespace *Namespace_find(Symbol *name) { +RTValue Namespace_find(Symbol *name) { RTValue symVal = RT_boxSymbol((Object *)name); - RTValue nsVal = ConcurrentHashMap_get_preservesSelf(namespaces, symVal); - if (RT_isNil(nsVal)) { - return NULL; - } - return (Namespace *)RT_unboxPtr(nsVal); + return ConcurrentHashMap_get_preservesSelf(namespaces, symVal); } /* @@ -52,9 +48,10 @@ Namespace *Namespace_find(Symbol *name) { */ Namespace *Namespace_findOrCreate(Symbol *name) { Ptr_retain(name); - Namespace *ns = Namespace_find(name); - if (ns != NULL) { + RTValue nsVal = Namespace_find(name); + if (!RT_isNil(nsVal)) { Ptr_release(name); // Consume argument + Namespace *ns = (Namespace *)RT_unboxPtr(nsVal); return ns; } @@ -195,7 +192,8 @@ bool Namespace_isInternedMapping(Namespace *self, Symbol *sym, RTValue o) { */ bool Namespace_checkReplacement(Namespace *self, Symbol *sym, RTValue oldVal, RTValue newVal) { - // In Clojure, once a mapping is established, it cannot be replaced with a different value/Var/Class. + // In Clojure, once a mapping is established, it cannot be replaced with a + // different value/Var/Class. return false; } @@ -267,7 +265,8 @@ Var *Namespace_intern(Namespace *self, Symbol *sym) { char buf[512]; snprintf(buf, sizeof(buf), "%s already refers to: %s in namespace: %s", - String_c_str(sym->name), String_c_str(flatOStr), String_c_str(self->name->name)); + String_c_str(sym->name), String_c_str(flatOStr), + String_c_str(self->name->name)); Ptr_release(flatOStr); Ptr_release(oStr); @@ -355,7 +354,8 @@ RTValue Namespace_reference(Namespace *self, Symbol *sym, RTValue val) { char buf[512]; snprintf(buf, sizeof(buf), "%s already refers to: %s in namespace: %s", - String_c_str(sym->name), String_c_str(flatOStr), String_c_str(self->name->name)); + String_c_str(sym->name), String_c_str(flatOStr), + String_c_str(self->name->name)); Ptr_release(flatOStr); Ptr_release(oStr); @@ -493,7 +493,7 @@ RTValue Namespace_getMapping(Namespace *self, Symbol *name) { * the given symbol. Returns the retained Var (+1 refcount) if found and owned * by this namespace, or NULL otherwise. Consumes `self` and `symbol` arguments. */ -Var *Namespace_findInternedVar(Namespace *self, Symbol *symbol) { +RTValue Namespace_findInternedVar(Namespace *self, Symbol *symbol) { Ptr_retain(self); Ptr_retain(symbol); RTValue o = Namespace_getMapping(self, symbol); @@ -503,33 +503,27 @@ Var *Namespace_findInternedVar(Namespace *self, Symbol *symbol) { Ptr_release(self); Ptr_release(symbol); // we already retained `o` inside getMapping. It returns +1 to caller. - return v; + return o; } } release(o); Ptr_release(self); Ptr_release(symbol); - return NULL; + return RT_boxNil(); } /* * Resolves an alias symbol to its corresponding Namespace object. - * Returns the mapped Namespace pointer with a retained (+1) refcount, or NULL - * if not found. Consumes `self` and `alias` arguments. + * Returns the mapped Namespace pointer as RTValue with a retained (+1) + * refcount, or RT_boxNil() if not found. Consumes `self` and `alias` arguments. */ -Namespace *Namespace_lookupAlias(Namespace *self, Symbol *alias) { +RTValue Namespace_lookupAlias(Namespace *self, Symbol *alias) { RTValue mapVal = atomic_load_explicit(&self->aliases, memory_order_acquire); Ptr_retain(RT_unboxPtr(mapVal)); - Ptr_retain(alias); + Ptr_release(self); RTValue res = PersistentArrayMap_get( (PersistentArrayMap *)RT_unboxPtr(mapVal), RT_boxSymbol((Object *)alias)); - Namespace *ns = NULL; - if (!RT_isNil(res)) { - ns = (Namespace *)RT_unboxPtr(res); - } - Ptr_release(self); - Ptr_release(alias); - return ns; + return res; } /* diff --git a/backend/runtime/Namespace.h b/backend/runtime/Namespace.h index b8425208..c7deeccc 100644 --- a/backend/runtime/Namespace.h +++ b/backend/runtime/Namespace.h @@ -20,7 +20,7 @@ struct Namespace { }; Namespace *Namespace_create(Symbol *name); -Namespace *Namespace_find(Symbol *name); +RTValue Namespace_find(Symbol *name); Namespace *Namespace_findOrCreate(Symbol *name); Symbol *Namespace_getName(Namespace *self); void Namespace_destroy(Namespace *self, bool deallocateChildren); @@ -40,8 +40,8 @@ void Namespace_unmap(Namespace *self, Symbol *sym); Class *Namespace_importClassSym(Namespace *self, Symbol *sym, Class *c); Var *Namespace_refer(Namespace *self, Symbol *sym, Var *var); RTValue Namespace_getMapping(Namespace *self, Symbol *name); -Var *Namespace_findInternedVar(Namespace *self, Symbol *symbol); -Namespace *Namespace_lookupAlias(Namespace *self, Symbol *alias); +RTValue Namespace_findInternedVar(Namespace *self, Symbol *symbol); +RTValue Namespace_lookupAlias(Namespace *self, Symbol *alias); void Namespace_addAlias(Namespace *self, Symbol *alias, Namespace *ns); void Namespace_removeAlias(Namespace *self, Symbol *alias); diff --git a/backend/runtime/tests/Keyword_test.c b/backend/runtime/tests/Keyword_test.c index 1fd9dd94..8f9c271d 100644 --- a/backend/runtime/tests/Keyword_test.c +++ b/backend/runtime/tests/Keyword_test.c @@ -74,6 +74,7 @@ static void test_concurrent_keyword_interning(void **state) { RT_unboxKeyword(expected)); } } + Ebr_force_reclaim(); }); } diff --git a/backend/runtime/tests/Namespace_test.c b/backend/runtime/tests/Namespace_test.c index e2c5b2f1..431c86d4 100644 --- a/backend/runtime/tests/Namespace_test.c +++ b/backend/runtime/tests/Namespace_test.c @@ -57,9 +57,11 @@ static void test_namespace_intern(void **state) { assert_ptr_equal(v->sym, varSym); Ptr_retain(ns); Ptr_retain(varSym); - Var *found = Namespace_findInternedVar(ns, varSym); + RTValue foundVal = Namespace_findInternedVar(ns, varSym); + assert_false(RT_isNil(foundVal)); + Var *found = (Var *)RT_unboxPtr(foundVal); assert_ptr_equal(found, v); - if(found) Ptr_release(found); + release(foundVal); Ptr_retain(ns); Ptr_retain(varSym); Namespace_unmap(ns, varSym); @@ -86,16 +88,19 @@ static void test_namespace_aliases(void **state) { Namespace_addAlias(userNs, aliasSym, coreNs); Ptr_retain(userNs); Ptr_retain(aliasSym); - Namespace *foundNs = Namespace_lookupAlias(userNs, aliasSym); + RTValue foundNsVal = Namespace_lookupAlias(userNs, aliasSym); + assert_false(RT_isNil(foundNsVal)); + Namespace *foundNs = (Namespace *)RT_unboxPtr(foundNsVal); assert_ptr_equal(foundNs, coreNs); - if(foundNs) Ptr_release(foundNs); + release(foundNsVal); Ptr_retain(userNs); Ptr_retain(aliasSym); Namespace_removeAlias(userNs, aliasSym); Ptr_retain(userNs); Ptr_retain(aliasSym); - Namespace *foundNsAfter = Namespace_lookupAlias(userNs, aliasSym); - assert_null(foundNsAfter); + RTValue foundNsAfterVal = Namespace_lookupAlias(userNs, aliasSym); + assert_true(RT_isNil(foundNsAfterVal)); + release(foundNsAfterVal); Ptr_release(coreNs); Ptr_release(userNs); @@ -148,10 +153,11 @@ static void test_namespace_global_registry(void **state) { // Create name symbol Symbol *nsSym1 = Symbol_create(String_create("my.global.ns")); - // Check that Namespace_find returns NULL before we register it + // Check that Namespace_find returns nil before we register it Ptr_retain(nsSym1); - Namespace *found1 = Namespace_find(nsSym1); - assert_null(found1); + RTValue found1Val = Namespace_find(nsSym1); + assert_true(RT_isNil(found1Val)); + release(found1Val); // Now call findOrCreate (should create and register a new one) Ptr_retain(nsSym1); @@ -169,9 +175,11 @@ static void test_namespace_global_registry(void **state) { // Call find (should find it) Ptr_retain(nsSym1); - Namespace *ns3 = Namespace_find(nsSym1); + RTValue ns3Val = Namespace_find(nsSym1); + assert_false(RT_isNil(ns3Val)); + Namespace *ns3 = (Namespace *)RT_unboxPtr(ns3Val); assert_ptr_equal(ns1, ns3); - Ptr_release(ns3); + release(ns3Val); // Now let's remove it from global map so we don't have global reference leak Ptr_retain(nsSym1); diff --git a/backend/state/ThreadsafeCompilerState.cpp b/backend/state/ThreadsafeCompilerState.cpp index 2d303bc5..033d7ab8 100644 --- a/backend/state/ThreadsafeCompilerState.cpp +++ b/backend/state/ThreadsafeCompilerState.cpp @@ -509,30 +509,37 @@ Var *ThreadsafeCompilerState::getCurrentVar(const char *name) { } // 1. Szukamy Namespace Symbol *nsSym = Symbol_create(String_createDynamicStr(nsName.c_str())); - Namespace *ns = Namespace_find(nsSym); + RTValue nsVal = Namespace_find(nsSym); - if (!ns) { + if (RT_isNil(nsVal)) { + release(nsVal); // Jeśli brak namespace, a nazwa była niekwalifikowana, szukamy w clojure.core if (slashPos == std::string::npos && nsName != "clojure.core") { Symbol *coreSym = Symbol_create(String_create("clojure.core")); - Namespace *coreNs = Namespace_find(coreSym); + RTValue coreNsVal = Namespace_find(coreSym); - if (coreNs) { + if (!RT_isNil(coreNsVal)) { + Namespace *coreNs = (Namespace *)RT_unboxPtr(coreNsVal); Symbol *coreVarSym = Symbol_create(String_createDynamicStr(symName.c_str())); Ptr_retain(coreNs); RTValue coreMapping = Namespace_getMapping(coreNs, coreVarSym); Ptr_release(coreNs); + release(coreNsVal); if (!RT_isNil(coreMapping) && getType(coreMapping) == varType) { Var *var = (Var *)RT_unboxPtr(coreMapping); return var; } release(coreMapping); + } else { + release(coreNsVal); } } return nullptr; } + Namespace *ns = (Namespace *)RT_unboxPtr(nsVal); + // 2. Szukamy symbolu w mappings znalezionego namespace'u Symbol *varSym = Symbol_create(String_createDynamicStr(symName.c_str())); Ptr_retain(ns); @@ -540,24 +547,28 @@ Var *ThreadsafeCompilerState::getCurrentVar(const char *name) { if (RT_isNil(mapping)) { release(mapping); - Ptr_release(ns); + release(nsVal); // Jeśli nazwa była niekwalifikowana i brak w user, szukamy w clojure.core if (slashPos == std::string::npos && nsName != "clojure.core") { Symbol *coreSym = Symbol_create(String_create("clojure.core")); - Namespace *coreNs = Namespace_find(coreSym); + RTValue coreNsVal = Namespace_find(coreSym); - if (coreNs) { + if (!RT_isNil(coreNsVal)) { + Namespace *coreNs = (Namespace *)RT_unboxPtr(coreNsVal); Symbol *coreVarSym = Symbol_create(String_createDynamicStr(symName.c_str())); Ptr_retain(coreNs); RTValue coreMapping = Namespace_getMapping(coreNs, coreVarSym); Ptr_release(coreNs); + release(coreNsVal); if (!RT_isNil(coreMapping) && getType(coreMapping) == varType) { Var *var = (Var *)RT_unboxPtr(coreMapping); return var; } release(coreMapping); + } else { + release(coreNsVal); } } return nullptr; @@ -565,12 +576,12 @@ Var *ThreadsafeCompilerState::getCurrentVar(const char *name) { if (getType(mapping) != varType) { release(mapping); - Ptr_release(ns); + release(nsVal); return nullptr; } Var *var = (Var *)RT_unboxPtr(mapping); - Ptr_release(ns); + release(nsVal); return var; } diff --git a/frontend/resources/rt-classes.edn b/frontend/resources/rt-classes.edn index 45ae8f5e..c504d827 100644 --- a/frontend/resources/rt-classes.edn +++ b/frontend/resources/rt-classes.edn @@ -97,11 +97,11 @@ unmap [{:args [:this clojure.lang.Symbol] :type :call :symbol "Namespace_unmap" :returns :nil}] addAlias [{:args [:this clojure.lang.Symbol clojure.lang.Namespace] :type :call :symbol "Namespace_addAlias" :returns :nil}] removeAlias [{:args [:this clojure.lang.Symbol] :type :call :symbol "Namespace_removeAlias" :returns :nil}] - lookupAlias [{:args [:this clojure.lang.Symbol] :type :call :symbol "Namespace_lookupAlias" :returns clojure.lang.Namespace}] - findInternedVar [{:args [:this clojure.lang.Symbol] :type :call :symbol "Namespace_findInternedVar" :returns clojure.lang.Var}] + lookupAlias [{:args [:this clojure.lang.Symbol] :type :call :symbol "Namespace_lookupAlias" :returns :any}] + findInternedVar [{:args [:this clojure.lang.Symbol] :type :call :symbol "Namespace_findInternedVar" :returns :any}] getMapping [{:args [:this clojure.lang.Symbol] :type :call :symbol "Namespace_getMapping" :returns :any}]} :static-fns - {find [{:args [clojure.lang.Symbol] :type :call :symbol "Namespace_find" :returns clojure.lang.Namespace}] + {find [{:args [clojure.lang.Symbol] :type :call :symbol "Namespace_find" :returns :any}] findOrCreate [{:args [clojure.lang.Symbol] :type :call :symbol "Namespace_findOrCreate" :returns clojure.lang.Namespace}]}} clojure.lang.Numbers From 08f12f12b5538cf4bc600bf41841bcb2fd3d87de Mon Sep 17 00:00:00 2001 From: Marek Lipert Date: Fri, 29 May 2026 09:14:28 +0200 Subject: [PATCH 6/6] Harden the EBR stress test. --- backend/runtime/tests/Keyword_test.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/runtime/tests/Keyword_test.c b/backend/runtime/tests/Keyword_test.c index 8f9c271d..11c51d34 100644 --- a/backend/runtime/tests/Keyword_test.c +++ b/backend/runtime/tests/Keyword_test.c @@ -30,8 +30,8 @@ static void test_keyword_interning(void **state) { } #include -#define THREAD_COUNT 4 -#define ITERATIONS 50 +#define THREAD_COUNT 16 +#define ITERATIONS 200 typedef struct KeywordThreadParams { const char *name;