Memory Management
Memory reclamation is an implementation detail. Osprey source cannot select
allocation sites, release values, or observe when a value is reclaimed. Native
builds default to tracing GC (gc); arc selects Perceus reference counting. WebAssembly, Apple and Android archives, and Python-hosted libraries default to ARC.
Unmanaged allocation requires explicit --memory=none. The old
--memory=default spelling is rejected.
Collection Is Unobservable [MEM-OPAQUE]
Osprey has no finalizers or destructors. Source code cannot inspect addresses, collector state, reference counts, collection order, or collection timing.
External resources such as files, sockets, processes, and foreign handles must therefore be closed through their explicit APIs; losing the Osprey value does not release the external resource.
The Managed Value Heap Is Acyclic [MEM-ACYCLIC]
Managed Osprey values are immutable after construction. mut rebinds a name or
cell to a new value; it does not mutate an existing list, map, record, union, or
closure environment to point at a newer managed value. Consequently, the
managed heap cannot contain reference cycles and ARC does not need a cycle
collector. Raw pointers obtained through FFI are outside the managed heap.
Ownership Lowering [MEM-OWNERSHIP]
Code generation tracks managed values in an ownership ledger:
- A fresh managed result enters its current region with one owned reference.
- Binding transfers a fresh owner or retains a borrowed value.
- Storing a managed value retains it unless a fresh owner can be moved into the destination.
- Region exits and proved last uses release their remaining owners.
- A returned owner transfers its reference; a returned borrow is retained.
The generated calls are identical for every backend. General retain/release
calls are no-ops under the unmanaged none and tracing-GC runtimes and active under ARC.
For a proved-unique value, codegen emits the paired osp_release_unique hook
with LLVM allocator/free attributes, allowing -O2 to remove a non-escaping
allocation and release. The unmanaged none runtime also frees that unique value when
the pair survives optimization; tracing GC leaves it for the collector.
Fiber Boundary Ownership [MEM-FIBER-ISOLATION]
spawn allocates a capture cell for that spawn and transfers one reference to
the runtime. The fiber releases the cell after its thunk returns. A managed
result is retained as a runtime root after the thunk returns. Every await
retains a separate reference for that receiving side, so repeated awaits are
safe. Once all spawned computations are quiescent, normal program teardown
releases runtime roots after language-owned values have dropped; teardown does
not invalidate cached results while another fiber can still await them. A
managed value boxed into a channel is likewise retained for the receiving side.
The runtime may therefore co-own managed allocations across fiber threads.
Before creating the first pthread-backed fiber it calls
osp_mem_notify_multithreaded: ARC switches from its single-threaded fast path
to synchronized retain/release operations, while the conservative GC disables
collection after a second allocator thread appears. Deterministic fiber mode
runs the same ownership protocol without creating a thread.
Backend Selection [MEM-BACKENDS]
Why GC is the native default
GC makes ordinary osp_retain and osp_release calls no-ops, reclaiming unreachable values during collection instead. ARC updates reference counts at ownership changes and releases an allocation when its last owner is dropped. Osprey defaults to GC to avoid that per-ownership-change bookkeeping as the starting policy for native programs; allocation tracking and collection still cost time and memory. This is a policy choice, not a claim that GC is faster than ARC on every workload.
The tradeoff is delayed reclamation, conservative retention and collection pauses. Choose --memory=arc when prompt reclamation or avoiding tracing pauses matters, then measure the application. Both backends are automatic and require no ownership annotations. Files, sockets and other external resources still need explicit cleanup under either backend.
The current GC is supported on Apple and glibc-Linux, not Windows, and stops collecting after a second allocator thread appears. Use ARC on Windows and for native programs that need reclamation across allocating fiber threads. WebAssembly, mobile and Python-library targets default to ARC and reject explicit tracing GC. Their managed runtime selection never falls back to unmanaged allocation. Exported host-library globals remain alive for later calls; --entry-only programs release globals on return, and native, WASM and mobile differential suites require zero live ARC allocations.
All code-generated heap allocation sites use the same IR symbols, so native backend selection only changes the runtime archive passed to the linker:
- Explicit
--memory=noneusescompiler/runtime/memory_runtime.c. gcusescompiler/runtime/memory_gc.c.arcusescompiler/runtime/memory_arc.c.
The C runtime memory tests exercise each backend directly. The differential conformance targets exercise compiled Osprey programs and compare their output; the ARC target additionally checks its live-allocation counter at process exit.
Container Element Ownership [MEM-BACKENDS-ELEMENTS]
Persistent container slots use an erased int64 ABI, so the runtime cannot
infer whether a slot is a managed pointer from its bits. Lists carry an
elem_managed flag. Maps derive key ownership from the key type and carry a
separate managed-value flag. Codegen supplies these flags from static types.
- Insertion gives the container an owned key/value reference.
- Path copying and builders retain every managed key, value, or child they continue to share.
- Lookup, iteration, loop elements, and list-pattern heads borrow from the container; they do not create an owned reference by themselves.
Backend Hook ABI [MEM-BACKENDS-CUSTOM]
The runtime archives implement the same internal C hooks: allocation, tagged allocation, retain, release, proved-unique release, layout stamping, finalizer registration, multithread notification, and collection. Backends that do not use a hook implement it as a no-op. This ABI keeps emitted IR independent of the selected native backend.
Conservative Tracing GC [GC-TRACE-CONSERVATIVE]
The native GC records managed allocation base addresses and conservatively scans the native stack, flushed registers, and data/BSS ranges. A word is a root only when it equals a registered allocation base. A false positive can retain an otherwise dead object but cannot make the collector free a reachable one. The collector is non-moving.
Apple and glibc-Linux targets provide stack and executable data/BSS bounds.
Windows reads the stack's high bound from the TEB, covered by the caller-stack
root regression in the Windows workflow. Windows executable data/BSS ranges
are still not scanned, so the GC backend is not part of the supported
Windows contract until global-root discovery and its required Windows
regression are implemented. Windows native callers should select --memory=arc explicitly.
Collection is restricted to the initial allocator thread. The first allocation from another thread permanently disables collection, while allocation-table access remains synchronized.
Perceus ARC [GC-ARC-PERCEUS]
The ownership and dup/drop discipline follows Perceus (Reinking, Xie, de Moura, Leijen), implemented from the extended technical report MSR-TR-2020-42 rather than the PLDI 2021 paper, because the report carries the full λ₁ calculus and insertion algorithm.
The ARC backend stores a 16-byte header before each managed body: a layout word, a signed reference count, and the body size. Retain/release first probe the live allocation registry, so literals, foreign pointers, and other unmanaged pointers are safe no-ops.
The layout word identifies raw blocks, pointer masks, list headers, pointer
arrays, and finalized list headers: a header whose body owns a resource beyond
the heap (a GPU buffer's copy on the device, GPU-BUFFER-RESIDENT)
passes through the one finalizer the owning runtime unit registers
(osp_mem_set_finalizer) before its data is released; the tracing collector
runs the same finalizer at the sweep that reclaims the header, and the unmanaged none
backend, which reclaims no general allocation, never runs it. Releases walk
managed child slots non-recursively. Persistent list and
map nodes retain shared structure and release the portion no longer referenced
by any live version. ARC operations start lock-free and become mutex-protected
before a pthread-backed fiber is spawned.