From 53051e04a3a2d4a5bd5cf368d6c8335076fa9f03 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Tue, 22 Jul 2025 00:37:41 +0200 Subject: [PATCH] Add `thread::fence` (from $$fence builtin). Ref and RefCounted types. --- lib/std/core/refcount.c3 | 132 +++++++++++++++++++++++++++ lib/std/threads/thread.c3 | 5 + releasenotes.md | 2 + src/build/build.h | 1 + src/compiler/enums.h | 1 + src/compiler/llvm_codegen_builtins.c | 9 ++ src/compiler/sema_builtins.c | 17 ++++ src/compiler/symtab.c | 1 + 8 files changed, 168 insertions(+) create mode 100644 lib/std/core/refcount.c3 diff --git a/lib/std/core/refcount.c3 b/lib/std/core/refcount.c3 new file mode 100644 index 000000000..136c07e8e --- /dev/null +++ b/lib/std/core/refcount.c3 @@ -0,0 +1,132 @@ +<* + Ref provides a general *external* ref counted wrapper for a pointer. For convenience, a ref count of 0 + means the reference is still valid. + + When the rc drops to -1, it will first run the dealloc function on the underlying pointer (if it exists), + then free the pointer and the atomic variable assuming that they are allocated using the Allocator in the Ref. + + @require !$defined(Type.dealloc) ||| $defined(Type.dealloc(&&(Type){})) : "'dealloc' must only take a pointer to the underlying type" + @require !$defined(Type.dealloc) ||| @typeis((Type){}.dealloc(), void) : "'dealloc' must return 'void'" +*> +module std::core::mem::ref { Type }; +import std::thread, std::atomic; + +const OVERALIGNED @private = Type.alignof > mem::DEFAULT_MEM_ALIGNMENT; + +alias DeallocFn = fn void(void*); + +fn Ref wrap(Type* ptr, Allocator allocator = mem) +{ + return { .refcount = allocator::new(allocator, Atomic{int}), .ptr = ptr, .allocator = allocator }; +} +<* + @require $vacount < 2 : "Too many arguments." + @require $vacount == 0 ||| @assignable_to($vaexpr[0], Type) : "The first argument must be an initializer for the type" +*> +macro Ref new(..., Allocator allocator = mem) +{ + + $switch: + $case OVERALIGNED && !$vacount: + Type* ptr = allocator::calloc_aligned(allocator, Type.sizeof, Type.alignof)!!; + $case OVERALIGNED: + Type* ptr = allocator::malloc_aligned(allocator, Type.sizeof, Type.alignof)!!; + *ptr = $vaexpr[0]; + $case !$vacount: + Type* ptr = allocator::calloc(allocator, Type.sizeof); + $default: + Type* ptr = allocator::malloc(allocator, Type.sizeof); + *ptr = $vaexpr[0]; + $endswitch + return { .refcount = allocator::new(allocator, Atomic{int}), + .ptr = ptr, + .allocator = allocator }; +} + +struct Ref +{ + Atomic{int}* refcount; + Type* ptr; + Allocator allocator; +} + +fn Ref* Ref.retain(&self) +{ + assert(self.refcount != null, "Reference already released"); + assert(self.refcount.load(RELAXED) >= 0, "Retaining zombie"); + self.refcount.add(1, RELAXED); + return self; +} + +fn void Ref.release(&self) +{ + assert(self.refcount != null, "Reference already released"); + assert(self.refcount.load(RELAXED) >= 0, "Overrelease of refcount"); + if (self.refcount.sub(1, RELAXED) == 0) + { + thread::fence(ACQUIRE); + $if $defined(Type.dealloc): + self.ptr.dealloc(); + $endif + $if OVERALIGNED: + allocator::free_aligned(self.allocator, self.ptr); + $else + allocator::free(self.allocator, self.ptr); + $endif + allocator::free(self.allocator, self.refcount); + *self = {}; + } +} + +module std::core::mem::rc; +import std::thread, std::atomic; + +<* + A RefCounted struct should be an inline base of a struct. + If a `dealloc` is defined, then it will be called rather than `free` + + For convenience, a ref count of 0 is still valid, and the struct is + only freed when when ref count drops to -1. + + The macros rc::retain and rc::release must be used on the full pointer, + not on the RefCounted substruct. + + So `Foo* f = ...; RefCounted* rc = f; rc::release(rc);` will not do the right thing. +*> +struct RefCounted +{ + Atomic{int} refcount; +} + +<* + @require @assignable_to(refcounted, RefCounted*) : "Expected a ref counted value" +*> +macro retain(refcounted) +{ + if (refcounted) + { + assert(refcounted.refcount.load(RELAXED) >= 0, "Retaining zombie"); + refcounted.refcount.add(1, RELAXED); + } + return refcounted; +} + +<* + @require @assignable_to(refcounted, RefCounted*) : "Expected a ref counted value" + @require !$defined(refcounted.dealloc()) ||| @typeis(refcounted.dealloc(), void) + : "Expected refcounted type to have a valid dealloc" +*> +macro void release(refcounted) +{ + if (!refcounted) return; + assert(refcounted.refcount.load(RELAXED) >= 0, "Overrelease of refcount"); + if (refcounted.refcount.sub(1, RELAXED) == 0) + { + thread::fence(ACQUIRE); + $if $defined(refcounted.dealloc): + refcounted.dealloc(); + $else + free(refcounted); + $endif + } +} \ No newline at end of file diff --git a/lib/std/threads/thread.c3 b/lib/std/threads/thread.c3 index a6193f1eb..013b8cff3 100644 --- a/lib/std/threads/thread.c3 +++ b/lib/std/threads/thread.c3 @@ -48,6 +48,11 @@ macro void? TimedMutex.lock_timeout(&mutex, ulong ms) => NativeTimedMutex.lock_t macro bool TimedMutex.try_lock(&mutex) => NativeTimedMutex.try_lock((NativeTimedMutex*)mutex); macro void? TimedMutex.unlock(&mutex) => NativeTimedMutex.unlock((NativeTimedMutex*)mutex); +macro void fence(AtomicOrdering $ordering) @safemacro +{ + $$fence($ordering.ordinal); +} + macro void Mutex.@in_lock(&mutex; @body) { (void)mutex.lock(); diff --git a/releasenotes.md b/releasenotes.md index eb70e5c1a..6abd3f6db 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -29,6 +29,7 @@ - Add komihash, a5hash, metrohash64, metrohash128, and wyhash2 variants with tests/benchmark. #2293 - '$assignable' is deprecated. - Deprecate allocator::heap() and allocator::temp() +- Add `thread::fence` providing a thread fence. ### Fixes - mkdir/rmdir would not work properly with substring paths on non-windows platforms. @@ -83,6 +84,7 @@ - Added `@addr` macro. - Add `ConditionVariable.wait_until` and `ConditionVariable.wait_for` - Added readline_to_stream that takes a stream. +- Added `Ref` and `RefCounted` experimental functionality. ## 0.7.3 Change list diff --git a/src/build/build.h b/src/build/build.h index 00aa1438e..66142f04b 100644 --- a/src/build/build.h +++ b/src/build/build.h @@ -698,6 +698,7 @@ typedef struct bool print_stats; bool old_slice_copy; bool old_enums; + bool single_threaded; int build_threads; TrustLevel trust_level; OptimizationSetting optsetting; diff --git a/src/compiler/enums.h b/src/compiler/enums.h index d71e41742..457b42526 100644 --- a/src/compiler/enums.h +++ b/src/compiler/enums.h @@ -451,6 +451,7 @@ typedef enum BUILTIN_EXP2, BUILTIN_EXPECT, BUILTIN_EXPECT_WITH_PROBABILITY, + BUILTIN_FENCE, BUILTIN_FLOOR, BUILTIN_FMA, BUILTIN_FMULADD, diff --git a/src/compiler/llvm_codegen_builtins.c b/src/compiler/llvm_codegen_builtins.c index d90d6e3ee..d61e1562a 100644 --- a/src/compiler/llvm_codegen_builtins.c +++ b/src/compiler/llvm_codegen_builtins.c @@ -157,6 +157,12 @@ INLINE void llvm_emit_atomic_store(GenContext *c, BEValue *result_value, Expr *e } } +INLINE void llvm_emit_fence(GenContext *c, BEValue *result_value, Expr *expr) +{ + LLVMValueRef value = LLVMBuildFence(c->builder, llvm_atomic_ordering(expr->call_expr.arguments[0]->const_expr.ixx.i.low), compiler.build.single_threaded, ""); + llvm_value_set(result_value, value, type_void); +} + INLINE void llvm_emit_unaligned_store(GenContext *c, BEValue *result_value, Expr *expr) { bool emit_check = c->emitting_load_store_check; @@ -773,6 +779,9 @@ void llvm_emit_builtin_call(GenContext *c, BEValue *result_value, Expr *expr) case BUILTIN_VOLATILE_LOAD: llvm_emit_volatile_load(c, result_value, expr); return; + case BUILTIN_FENCE: + llvm_emit_fence(c, result_value, expr); + return; case BUILTIN_ATOMIC_STORE: llvm_emit_atomic_store(c, result_value, expr); return; diff --git a/src/compiler/sema_builtins.c b/src/compiler/sema_builtins.c index 5fb54a74b..122410162 100644 --- a/src/compiler/sema_builtins.c +++ b/src/compiler/sema_builtins.c @@ -1006,6 +1006,22 @@ bool sema_expr_analyse_builtin_call(SemaContext *context, Expr *expr) if (!sema_check_builtin_args_match(context, args, 3)) return false; rtype = args[0]->type; break; + case BUILTIN_FENCE: + ASSERT(arg_count == 1); + if (!sema_check_builtin_args(context, args, (BuiltinArg[]) {BA_INTEGER}, 1)) return false; + if (!sema_cast_const(args[0])) RETURN_SEMA_ERROR(args[0], "Ordering must be a compile time constant."); + if (!is_valid_atomicity(context, args[0])) return false; + switch (args[0]->const_expr.ixx.i.low) + { + case ATOMIC_NONE: + case ATOMIC_RELAXED: + case ATOMIC_UNORDERED: + RETURN_SEMA_ERROR(args[0], "'none', 'relaxed' and 'unordered' are not valid for fence."); + default: + break; + } + rtype = type_void; + break; case BUILTIN_ATOMIC_LOAD: { ASSERT(arg_count == 3); @@ -1228,6 +1244,7 @@ static inline int builtin_expected_args(BuiltinFunction func) case BUILTIN_EXACT_NEG: case BUILTIN_EXP2: case BUILTIN_EXP: + case BUILTIN_FENCE: case BUILTIN_FLOOR: case BUILTIN_FRAMEADDRESS: case BUILTIN_LLRINT: diff --git a/src/compiler/symtab.c b/src/compiler/symtab.c index 5fa856a9c..5f4f68e49 100644 --- a/src/compiler/symtab.c +++ b/src/compiler/symtab.c @@ -224,6 +224,7 @@ void symtab_init(uint32_t capacity) builtin_list[BUILTIN_EXP2] = KW_DEF("exp2"); builtin_list[BUILTIN_EXPECT] = KW_DEF("expect"); builtin_list[BUILTIN_EXPECT_WITH_PROBABILITY] = KW_DEF("expect_with_probability"); + builtin_list[BUILTIN_FENCE] = KW_DEF("fence"); builtin_list[BUILTIN_FLOOR] = KW_DEF("floor"); builtin_list[BUILTIN_FMA] = KW_DEF("fma"); builtin_list[BUILTIN_FMULADD] = KW_DEF("fmuladd");