rc is a standalone Carp library that provides macro-generated reference-counted
types with weak references:
- strong handles (
RcX) - weak handles (
RcXWeak) - structural sharing by pointer identity
- explicit ownership semantics through generated
copy/deletebehavior
The entrypoint is Rc.define, which generates a concrete pair of managed types
for one payload type.
Current status:
- feature complete for single-threaded reference counting
- includes strong + weak refs
- includes sanitizers and fuzz harnesses in
test/ - not atomic/thread-safe
- fail-fast runtime guards for refcount overflow/underflow and cross-thread use
- matches Rust-style
Rc/Weakcycle behavior (no automatic cycle collection)
Important semantic note:
- payload is dropped when the strong count reaches zero
- the control block remains while weak refs exist
- allocation free still happens when both strong and weak counts reach zero
This matches Rust Rc/Weak lifetime behavior.
Project structure:
rc.carp: library sourceREADME.md: usage and API docstest/fuzz_harness.carp: generic fuzz runner/env helperstest/rc.carp: functional + lifecycle teststest/rc_fuzz.carp: state-machine fuzz tests forRcStringtest/rc_fuzz_array_string.carp: state-machine fuzz tests forRcArrayStringtest/rc_fuzz_probe.carp: state-machine fuzz tests forRcProbedocs/design.md: implementation internals and invariantsdocs/testing.md: test matrix and fuzzing guidance
Load from a git source in Carp's standard library-loading style:
(load "git@github.com:carpentry-org/rc@0.1.0")Use release tags when possible for reproducible builds.
Instantiate one specialization:
(Rc.define RcString String)This generates:
- type
RcString - type
RcStringWeak - module
RcString - module
RcStringWeak - managed
copy/deleteimplementations for both types
Minimal example:
(Rc.define RcString String)
(let [a (RcString.new @"hello")
b (RcString.clone &a)]
(do
(IO.println &(RcString.str &a))
(IO.println &(str (RcString.get &b)))
(IO.println &(Long.str (RcString.strong-count &a)))))Weak example:
(Rc.define RcString String)
(let [a (RcString.new @"hello")
w (RcString.downgrade &a)]
(match (RcStringWeak.upgrade &w)
(Maybe.Just a2) (IO.println &(RcString.str &a2))
(Maybe.Nothing) (IO.println @"expired")))Each allocation stores:
strong : Longweak : Longvalue : (Ptr T)(cleared when strong reaches zero)owner-thread : Long(thread affinity guard for single-threaded usage)magic : Long(control-block integrity guard)
Behavior:
- cloning/copying strong handles increments
strong - downgrading increments
weak - upgrading weak to strong increments
strongonly ifstrong > 0 - strong deletion decrements
strong - weak deletion decrements
weak - payload drop happens at
strong=0 - final control-block free happens when both reach zero
This gives pointer-stable sharing and cheap cloning, with semantics explicit through value ownership.
Assume:
(Rc.define RcT T)Generated strong module: RcT
new : (Fn [T] RcT)- allocates a new cell with
strong=1, weak=0
- allocates a new cell with
copy : (Fn [(Ref RcT q)] RcT)- increments strong count and returns same pointer
- called implicitly by
@&xpatterns where needed
clone : (Fn [(Ref RcT q)] RcT)- alias of
copy
- alias of
delete : (Fn [RcT] ())- decrements strong count
- drops payload at
strong=0 - frees control block when resulting
strong=0andweak=0
strong-count : (Fn [(Ref RcT q)] Long)- returns current strong count (0 for null)
weak-count : (Fn [(Ref RcT q)] Long)- returns current weak count (0 for null)
unique? : (Fn [(Ref RcT q)] Bool)- true iff
strong-count == 1
- true iff
ptr-eq : (Fn [(Ref RcT q) (Ref RcT r)] Bool)- pointer identity comparison
get : (Fn [(Ref RcT q)] T)- returns
valueby copy semantics - may trigger payload copy for managed payloads
- returns
try-unwrap : (Fn [RcT] (Result T RcT))- success when uniquely owned
- on success, moves payload out without payload-delete
- error returns original
RcTunchanged when shared
unwrap : (Fn [RcT] T)- success path of
try-unwrap - moves payload out on success
- aborts on shared value
- success path of
unwrap-or-clone : (Fn [RcT] T)- unwraps when unique
- otherwise copies payload through
get
make-unique : (Fn [RcT] RcT)- returns input unchanged when unique
- otherwise detaches by cloning payload into a new cell and decrementing old
downgrade : (Fn [(Ref RcT q)] RcTWeak)- creates weak handle to same cell
str : (Fn [(Ref RcT q)] String)- diagnostic format with counts
Generated weak module: RcTWeak
new : (Fn [] RcTWeak)- creates an empty/expired weak handle (no allocation, no refcount change)
copy : (Fn [(Ref RcTWeak q)] RcTWeak)- increments weak count and returns same pointer
clone : (Fn [(Ref RcTWeak q)] RcTWeak)- alias of
copy
- alias of
delete : (Fn [RcTWeak] ())- decrements weak count
- frees control block if resulting
weak=0andstrong=0
strong-count : (Fn [(Ref RcTWeak q)] Long)- reads strong count via weak
weak-count : (Fn [(Ref RcTWeak q)] Long)- reads weak count
expired? : (Fn [(Ref RcTWeak q)] Bool)- true iff
strong-count == 0
- true iff
alive? : (Fn [(Ref RcTWeak q)] Bool)- true iff
strong-count > 0
- true iff
upgrade : (Fn [(Ref RcTWeak q)] (Maybe RcT))Maybe.Justwhenstrong > 0, incrementing strongMaybe.Nothingwhen expired
ptr-eq : (Fn [(Ref RcTWeak q) (Ref RcTWeak r)] Bool)- pointer identity comparison
str : (Fn [(Ref RcTWeak q)] String)- diagnostic format with counts
Rc has value-like handles, so these are important:
getreturns by value and may copy payload datatry-unwrap/unwrapmove payload out on the unique-success pathunwrap-or-clonemay copy payload when sharedmake-uniquedecrements old strong then allocates a new cell when sharedWeak.upgradereturns a fresh strong handle with incremented strong count
Null paths:
- internals defensively handle null pointers for counters and upgrade
- user code should still treat handles as valid managed values, not nullable raw pointers
Typical usage:
- persistent linked nodes as
Rc Node - trees with shared subtrees via
Rc - parent pointers as
Weakto avoid strong cycles in DAG-like structures
Example persistent node:
(deftype Node [value Int next (Maybe RcNode)])
(Rc.define RcNode Node)With this pattern, cloning head pointers is cheap and preserves sharing.
Current implementation is explicitly:
- non-atomic
- single-threaded
- not safe for concurrent shared mutation across threads
- guarded: using one control block from a different thread aborts immediately
Do not use these handles concurrently without an external synchronization and threading story in Carp runtime.
- alpha quality (
0.1.0): API and behavior may still change - non-atomic and single-threaded only (not thread-safe)
- no allocator pluggability yet
- weak API is intentionally minimal:
new, counts,expired?/alive?,upgrade, and pointer equality - forged handles created with
Unsafe.coerceare out of contract; public APIs abort on invalid control-block magic/thread-owner mismatches
Functional tests:
carp -x test/rc.carpFuzz tests:
carp -x test/rc_fuzz.carp
carp -x test/rc_fuzz_array_string.carp
carp -x test/rc_fuzz_probe.carpThe test harness enables:
-fsanitize=address-fsanitize=undefined-fno-sanitize-recover=all- memory-balance assertions via
Debug.memory-balance
See generated testing docs at docs/Testing.html and detailed markdown notes in
docs/testing.md.
Build/test flags can be controlled by environment variables:
RC_OPT_LEVELacceptsO0|O1|O2|O3(defaultO1)RC_SANITIZEaccepts1|true|yesor0|false|no(default enabled)
All fuzz suites support runtime knobs:
RC_FUZZ_RUNSdefault12RC_FUZZ_STEPSdefault400RC_FUZZ_RC_SLOTSdefault8RC_FUZZ_WEAK_SLOTSdefault8RC_FUZZ_BASE_SEEDdefault1001.0RC_FUZZ_SEED_STRIDEdefault9973.0RC_FUZZ_RANDOM_SEEDset to1|true|yesto seed fromSystem.nanotime
Example long soak:
RC_FUZZ_RUNS=200 RC_FUZZ_STEPS=2000 RC_FUZZ_RANDOM_SEED=1 \
carp -x test/rc_fuzz.carpExample array payload soak:
RC_FUZZ_RUNS=200 RC_FUZZ_STEPS=2000 RC_FUZZ_RANDOM_SEED=1 \
carp -x test/rc_fuzz_array_string.carpExample probe payload soak:
RC_FUZZ_RUNS=200 RC_FUZZ_STEPS=2000 RC_FUZZ_RANDOM_SEED=1 \
carp -x test/rc_fuzz_probe.carpRun the full validation matrix from doing/rc:
./scripts/validate.shvalidate.sh runs test/rc.carp plus all fuzz suites in each lane.
Matrix lanes:
- sanitized
O1 - unsanitized
O2 - optional unsanitized
O3(RC_VALIDATE_RUN_O3=0to disable)
Useful overrides:
RC_VALIDATE_PROFILE=quick|ci|soakRC_FUZZ_RUNS,RC_FUZZ_STEPS,RC_FUZZ_RC_SLOTS,RC_FUZZ_WEAK_SLOTSRC_VALIDATE_RANDOM_SEED=1for non-deterministic explorationCARP_BINto choose a specificcarpexecutableCARP_DIRto point at the Carp core directory (defaults to../../carp)
Example deterministic reproduction:
RC_FUZZ_RUNS=80 RC_FUZZ_STEPS=1500 RC_FUZZ_BASE_SEED=4242.0 RC_FUZZ_SEED_STRIDE=37.0 \
carp -x test/rc_fuzz.carpThe same fuzz env vars apply to test/rc_fuzz_array_string.carp and
test/rc_fuzz_probe.carp.
Generated docs:
- API module:
docs/Rc.html - design overview:
docs/Design.html - testing overview:
docs/Testing.html - docs index:
docs/rc_index.html
Extended markdown docs:
- implementation internals:
docs/design.md - testing and fuzzing details:
docs/testing.md
This library follows the standard Carp doc-generation workflow via
gendocs.carp:
CARP_DIR=/path/to/carp/compiler-repo carp -x gendocs.carpThat regenerates:
docs/Rc.htmldocs/Design.htmldocs/Testing.htmldocs/rc_index.html
Have fun!