2.13. marker - Clone / Copy / Drop
Duplicating values and cleaning up resources that are no longer needed are unavoidable concerns in any language. The rok::core::marker module expresses these two concerns with three traits. Clone defines how to make an independent copy of a value; Copy is a marker stating that a plain bit copy suffices for a type; and Drop defines cleanup code that must run when a value leaves scope. The module also holds NoneSentinel, the zero-sized marker type of the none literal (2.13.8).
rok's use of a GC directly affects the meaning of these traits. The GC is responsible for reclaiming memory, so Drop is not tied to memory deallocation. Drop is limited to finalization work that cleans up resources living outside the language, such as file handles and sockets. This places rok's marker traits apart from both Rust and Java/Python.
2.13.1. Memory model - Clone/Drop in a language with a GC
In rok's runtime, the Immix + Sticky Mark-Bit Generational GC manages the lifetime of heap objects automatically. The user does not decide when an object disappears from memory. Cleanup of external resources, however, must happen deterministically, so rok's Drop operates as a mechanism separate from the GC. Compared with Rust:
| Aspect | Rust (no GC) | rok (GC) |
|---|---|---|
| Memory reclamation | Drop is responsible | GC automatically - Drop does not |
Meaning of Drop::drop | release resource + free memory | external resource cleanup only (file handle, socket, ...) |
| When called | deterministic (scope exit) | only on leaving a using block (unrelated to GC collection) |
| Effect if omitted | resource leak + memory leak | resource leak only (the GC eventually reclaims memory) |
Note
Ordinary bindings have no automatic drop at scope exit. Drop::drop is called only for bindings bound with using (refs-lang 3.17 using). The GC does not guarantee finalization of other objects and never calls drop.
Because memory reclamation and resource cleanup are separate, ordinary objects need not care about Drop - the GC reclaims them - and only types that handle external resources implement Drop to obtain a deterministic cleanup point.
2.13.2. Clone - deep copy
Clone::clone is the method called when an independent copy of a value is needed. Its signature is simple:
#[lang("clone_trait")]
trait Clone {
fn clone(self) -> Self;
}
clone performs a deep copy. Even if the value holds references internally, the data they point to is copied too, so the two sides can be modified independently afterwards. Shared data that the GC guarantees to be immutable may still be shared as is.
Meaning of Clone
The following example shows Clone in its most direct form.
let a: List<i32> = [1, 2, 3];
let b = a.clone(); // new List, with a new internal buffer
b.push(4); // a is unaffected
a; //→ [1, 2, 3]
b; //→ [1, 2, 3, 4]
"Deep" here means precisely this: Clone preserves logical equality while guaranteeing mutation independence between the values. Two properties follow. Immediately after let b = a.clone();, a == b holds if PartialEq is defined. After that, no change to a is reflected in b, and vice versa.
No automatic calls - clone_trait lang slot
In some languages, writing let y = x; makes the compiler call clone automatically. rok chooses the explicit side here.
let xs: List<i32> = [1, 2, 3];
let ys = xs; // GC heap object → reference shared (no clone)
let y = x; shares the reference for a GC heap object and performs a bit copy for a Copy type. Clone::clone is therefore never dispatched automatically; it runs only when the user calls it explicitly. For this reason Clone is registered not as a method slot but as the trait slot clone_trait, and the compiler looks this slot up only in the #[derive(Clone)] expansion.
Default bodies
The clone of a primitive type amounts to a bit copy, so it maps directly to an intrinsic; the clone of a collection allocates a new internal buffer and clones the elements recursively.
impl Clone for i32 {
#[intrinsic("rok.clone.copy")] // bitwise copy
fn clone(self) -> i32;
}
impl<T: Clone> Clone for List<T> {
fn clone(self) -> List<T> {
let r: List<T> = [];
for x in self {
r.push(x.clone());
}
r
}
}
Cloning a GC object always allocates a new buffer and clones the elements recursively.
#[derive(...)]
For most user-defined structs whose fields are all Clone, there is no reason to write the body by hand. #[derive(Clone)] does it instead.
#[derive(Clone)]
struct Config {
name: str;
timeout: u32;
retries: u32;
}
let c1 = Config { name: "default", timeout: 30, retries: 3 };
let c2 = c1.clone(); // every field cloned recursively
The derived body calls Clone::clone on every field and returns the results in the same structure. Every field must therefore implement Clone, recursively.
2.13.3. Copy - marker
trait Copy: Clone {
// marker - no methods
}
In rok every value is copied by value. For collections and GC objects the handle is copied and points to the same object; primitive values, str (an immutable (ptr, len)), structs, enums and tuples are copied themselves (refs-lang 2.3.8). Copy is therefore only a marker and does not change the meaning of assignment or argument passing. Copy is used only in generic bounds (T: Copy) to select types for which "a bit copy is an independent copy".
impl policy
| Type | Clone | Copy | Notes |
|---|---|---|---|
i*, u*, f* | ✓ | ✓ | primitive numbers |
bool, char, c_char | ✓ | ✓ | |
str | ✓ | ✓ | immutable (ptr, len) value |
int, float | ✓ | ✓ | immutable value - a heap BigInt is only shared |
List<T>, Dict<K,V>, Set<T> | ✓ (T:Clone) | ✗ | copying the handle shares - not an independent copy |
| tuples | ✓ (all) | ✓ (if all Copy) | per component |
Range<T> | ✓ (T:Clone) | ✓ (T:Copy) | when both start/end are Copy |
| user struct / enum | derive | derive | when every field implements the same trait |
A type that implements Drop cannot be Copy.
#[derive(Clone, Copy)]
struct Vec3 { x: f64; y: f64; z: f64; }
Important
Deriving Copy is allowed only when Clone is implemented as well, and only when every field is Copy.
2.13.4. Drop - cleanup on using release
A file that was opened must be closed; a mutex that was locked must be unlocked. Such external resource cleanup cannot be left to the GC. Holding a resource until an unpredictable sweep could exhaust file descriptors or leave other code stalled on a lock that was never released. The Drop trait and the using expression solve this.
trait Drop {
#[lang("drop_method")]
fn drop(ref self);
}
When called - only for using bindings
rok is a GC language, so drop is not called automatically when an ordinary binding leaves scope. drop is called only for bindings bound with using, on leaving the block (refs-lang 3.17 using).
- It is called on normal completion,
return,breakand error propagation alike. - Multiple bindings are dropped in reverse declaration order (LIFO).
- A panic ends the process, so
dropis not called. - Drop glue: after the value's own
drop, fields that haveDropare cleaned up recursively in reverse declaration order. The same applies to tuple elements and to the payload currently held by an enum / sum type.
fn process(path: str) throws (IoError) {
using lock = MUTEX.lock(), file = try File::open(path) {
do_work(file);
} // ← on normal exit or error: file.drop() then lock.drop()
}
{
let f = try! File::open("data.txt");
} // ← no automatic drop - the GC reclaims f but File::drop is not called
Binding a type that does not implement Drop with using is a compile error stating that Drop is required.
Relation to the GC
The GC never calls drop, and there are no finalizers. A resource that needs cleanup must be bound with using or closed directly with a method such as close(); the unclosed_resource lint warns otherwise (refs-lang 6.2.9).
Drop implementation example
The most typical implementation is a wrapper around an OS resource.
struct File {
handle: i32;
}
impl Drop for File {
fn drop(ref self) {
rok::bare::close_fd(self.handle); // OS file descriptor close
}
}
Restrictions on Drop
Drop comes with several restrictions. First, a type that implements Drop cannot be Copy: multiplying copies freely by bit copy while guaranteeing deterministic cleanup is a contradiction.
#[derive(Copy)] // ERROR - conflicts with Drop
struct File { handle: i32; }
impl Drop for File { ... }
Second, calling drop directly is a compile error. Correctness is guaranteed only if it is called exactly once, at using release.
let f = File::open(path);
f.drop(); // ERROR - calling Drop::drop directly is forbidden
To close a resource early, expose a separate method such as File::close().
The only exception is unsafe code inside rok::bare. For example, unsafe fn drop_in_place (1.3 mem) is a function for manual memory management. It runs the drop glue: the type's own drop, then its fields' in reverse declaration order, as above.
Also, drop has no throws signature, so its body cannot throw an error. A panic inside drop ends the process like any other panic.
impl Drop for X {
fn drop(ref self) {
if some_cond { throw "oops" } // ERROR - drop is not throws
}
}
derive policy
Unlike Clone, Drop cannot be derived. After the drop of a value bound with using finishes, the compiler calls drop on that struct's Drop fields in reverse declaration order. A user-written Drop body therefore handles only "what needs cleaning up beyond the fields".
struct Container {
a: File;
b: Socket;
}
// when Container is bound with using, release does b.drop() → a.drop()
// (because the fields themselves implement Drop)
impl Drop for Container {
fn drop(ref self) {
log("Container released");
// field drops are called automatically by the compiler _after this method returns_
}
}
2.13.5. lang slot mapping
This module connects to the compiler through five lang slots. The automatic call points follow the policies described above.
| slot | Attached to | Automatic call point |
|---|---|---|
clone_trait | Clone trait | none - used in the #[derive(Clone)] expansion |
drop_method | Drop::drop method | release of a using binding (and drop glue) |
none_type | NoneSentinel type (2.13.8) | lowering of the none literal |
send_trait | Send trait (2.13.9) | implemented automatically, thread-boundary checks |
sync_trait | Sync trait (2.13.9) | implemented automatically, thread-boundary checks |
For details see sections 0.4.2 / 0.4.3 / 0.4.5 of the 0.4 lang slot table.
2.13.6. Trait implementation matrix
How the main standard library types relate to the three traits:
| Type | Clone | Copy | Drop | Behavior |
|---|---|---|---|---|
i*, u*, f* | ✓ | ✓ | ✗ | rok.clone.copy (bitwise) |
bool, char, c_char | ✓ | ✓ | ✗ | bitwise |
str | ✓ | ✓ | ✗ | copies the immutable (ptr, len) value |
List<T>, Dict<K,V>, Set<T> | ✓ (T:Clone) | ✗ | ✗ | recursive clone |
| tuples | ✓ (all) | ✓ (all Copy) | ✗ | per component |
Range<T> | ✓ (T:Clone) | ✓ (T:Copy) | ✗ | derived automatically |
File | ✗ | ✗ | ✓ | rok::bare::close_fd |
MutexGuard<T> | ✗ | ✗ | ✓ (on using release) | rok::bare::unlock |
Channel::Sender<T> | ✓ | ✗ | ✗ (closed with close()) | (same as above) |
The principle behind the table is simple. Every primitive and internal GC type implements Clone, and only those for which a bit copy is semantically correct are Copy. The collections are Clone but not Copy, because a bit copy of a handle does not preserve the meaning of a deep copy. str is Copy: it is immutable, so copying its (ptr, len) is an independent copy. Drop is implemented only by wrappers around external resources such as File, Socket and MutexGuard. These types usually do not implement Clone, to prevent duplicate ownership of the resource.
2.13.7. Comparison with other languages
Comparing rok's model with existing languages makes the design intent clearer.
| Aspect | Rust | Java/Python (GC) | rok (GC + Drop) |
|---|---|---|---|
| Memory reclamation | Drop is responsible | GC | GC |
| Deterministic finalize | Drop (compiler-guaranteed) | none (finalizers nondeterministic) | Drop (scope-bound) |
| Meaning of Copy | bitwise + !Drop enforced | none (every object is a ref) | bitwise + !Drop enforced |
| Meaning of Clone | deep copy | clone() (Object.clone) | deep copy |
| derive | #[derive(Clone, Copy)] | Cloneable interface | #[derive(Clone, Copy)] |
What sets rok apart is the coexistence of a GC with a deterministic Drop. Because the GC handles memory reclamation, Drop does not mean memory deallocation, so ordinary objects never need to implement Drop. Only types that need external resource cleanup implement Drop, and users bind them with using.
This model avoids both the nondeterminism of Java/Python finalizers and the costs Rust incurs from having no GC.
2.13.8. NoneSentinel - the type of the none literal
The none literal of refs-lang 1.9.2 lowers to NoneSentinel, a zero-sized type with a single value. This type is defined in rok::core::marker and attached to the none_type lang slot.
#[lang("none_type")]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct NoneSentinel;
Users rarely write this name directly. In sum type notation such as T | none (refs-lang 2.6), the none position is the NoneSentinel variant; both the none literal and the none type notation resolve to this same type through the slot.
2.13.9. Send / Sync - thread-boundary markers
#[lang("send_trait")]
trait Send { } // safe to move to another thread
#[lang("sync_trait")]
trait Sync { } // safe to read from multiple threads through shared references
Both traits are automatic marker traits with no methods; the compiler implements them structurally.
Primitive numbers,
bool,char,c_char,strandnoneareSend + Sync.A
struct,enum,union, tuple, array or sum type isSendif all its fields (components) areSend, andSyncif all areSync.Sendfor a generic container followsTby the same rule.Collections (
List,Dict,Set,StrBuf,Deque,BinaryHeap,OrderedDict,OrderedSet,Slice) areSendand!Sync: they share a mutable handle. Across threads they are shared throughMutex/RwLock/ channels.rok has no move semantics. Handing a value over (
Mutex::new(xs),RwLock::new(xs),tx.send(xs)/try_send/send_async,Handoff::new(xs)) leaves the sender's handle pointing at the object the new owner now shares with other threads. A handle taken out of a guard (get), given to it (set) or reached byupdate's closure still points at the guarded object after the guard'susingblock ends. Using such a value afterwards, on any path, is a compile error (E202, refs-lang 9.9.4). The value is followed through:- copies (
let ys = xs, a struct field or tuple element, a field assignment); - the fields it is passed by (
Mutex::new(s.items)); - the objects that hold it (
all.push(xs), a closure that captures it), both ways: handing overxsleavesallunusable too; - the elements of a collection. What is stored into a collection and what is read out of it (
xs[i],get, iteration, a slice of it) are one value, so handing over either leaves the other unusable (let x = xs[0]; tx.send(xs); x.len(),tx.send(xs[0]); xs[1].len()). Onlylen,is_empty,capacity,clear,truncate,reserve,shrink_to_fit,pushandinsert(andDeque'spush_back/push_front) stay allowed on a collection whose elements were handed over;clearempties it, so it is fully usable again. A collection copied from another (extend,+=,collect,par::filter) shares the source's elements. An element taken out withpoporremove(List,Deque,Dict,OrderedDict,BinaryHeap) has left the collection: handing it over leaves the collection usable, unless the collection may hold that object twice - one value stored twice, two stored values that share an object, or elements of one collection stored into another in a loop (elements are not told apart by position); - the arguments of a direct call that hands something over: passing one object through two arguments, when the callee hands it over through one and uses it through the other (
f(tx, a, a),h.go(tx, h.items)wheregousesself.items, a list it pops from and sends plus one of its elements), is a compile error. Two elements of one collection are taken to be distinct (f(tx, rows[0], rows[1])is accepted); - a
refparameter. When the callee hands a part of it over and then gives that part a new value on every path that hands it over (self.buf = []aftertx.send(self.buf)), the caller's variable gets the new value back and stays usable; other handles to the old value do not. A by-value parameter is a copy: giving its field a new value does not reach the caller, which still holds what was handed over; - calls. A function that hands a parameter over, returns it or a part of it, stores it into another parameter's object or a
refparameter, captures it into the closure it returns, or returns what a guard parameter holds, does so for its caller too, also across a prebuilt library (refs-lang 9.4.7).
A call through a function value is taken to reach every function used as a value with its parameter types. Conditions are followed where they test a value the compiler knows on each path: a
boolor a sum or enum variant set by a constant or copied (let sent = false; if c { tx.send(xs); sent = true; } if !sent { xs.len() }), and how a call ended. A function that hands a parameter over only when it returns without an error, or a value other thannone/false, does not hand it over on its caller'scatch,try?,??,elseorifpath for the other outcome; the same holds for thecatchoftry awaiton anasync fn. Other conditions are not followed: a value handed over on one path is gone where that path joins others.To keep using a value, hand over a
clone()(an independent value), or give the variable or field a new value first (for a struct copy, only that field); asetreleases what an earliergetreturned. Uses ofSyncvalues, reading a value's tag (x is none,match x), and uses insideunsafe(refs-lang 4.15.2) are exempt. AHandoffprotocol ends inside the function that makes it: its callers do not see that hand-off. Captures of a spawned closure need no check: they must beSend + Sync.task::spawnhands its future over: the task's frame gets copies of what the future captured, and the future and what it captured are gone for the spawner, as after a send (2.18.1).Also compile errors: handing over a value read out of a
Syncobject that is not itselfSync(a field of a user'sunsafe impl Synctype, athread::local's value), which other code reaches without the hand-off - hand over aclone(); values a channel'srecv, a task's result or a lock's guard give out are the receiver's and are not covered by this rule; handing over what a guard'sgetreturned while the guard still holds it (untilsetgives the lock a new value); returning it from inside theusingblock, yielding it there (yield g.get()in a generator), making it the block's own value (let x = using g = m.lock() { g.get() },f(using g = m.lock() { g.get() })), or giving it to a coroutine made inside the block and run after it (anasync fn's future, a generator:fut = grow(g.get()[0])): the block's end releases the lock before the value is used. A guard is good only inside itsusingblock: a copy of it (pushed into a collection, captured by a closure, stored by a callee, bound by a secondusing, held by a coroutine: anasync fn's or generator's argument, anasyncblock's capture) used after that block ends is a compile error; guards are!Sync(2.16.3).par::for_each,par::mapandpar::filtergive each item to a worker, so two items must not reach one collection that is notSync. Each call checks this at run time (2.19.1): if two items reach one collection, the items run one at a time on the calling thread, with a warning once per call site. Slices only read, so items may share a list through slices of it, as long as no item holds the list itself.What remains outside these rules: code inside
unsafe; a call through a function value or a closure, which skips the argument check above; and a generator that yields one object more than once, collected into a collection whose elements are then handed out one by one - such a collection is not seen as holding the object twice.At run time every hand-off also copies, so on the routes above leave out (and in code inside
unsafe) no collection the handed value reaches by value ends up shared between the sender and the new owner (the exceptions are listed below).Mutex::new,RwLock::new, a guard'ssetandupdate,send/try_send/send_async, and the result of a task, a thread or aparscope job (and atry_scopeerror) give the new owner a copy of every collection the value reaches: through the parts of structs, tuples, arrays, enums and sums and through the elements of collections, to any depth, with a work list rather than stack per level. A collection reached more than once is copied once, so the copy keeps the value's sharing and its cycles. Copied areList,Dict,Deque,StrBuf,Sliceand the collections built on them, andGcPtr(1.6.3). A coroutine future (AsyncFuture,TryAsyncFuture) gets a frame of its own, holding copies of what it captured made in the same copy; one that has started or finished, or whose frame (or a captured future's) has a[ref x]capture, cannot be handed over, and the hand-off panics (2.18.1). ATaskis not copied, but once it is handed over eachawaitof it gets its own copy of the result (2.18.2). A list and every view of its buffer keep sharing one copied buffer; a view of astr's bytes copies only those bytes (they cannot change).updatekeeps what the guard already holds: only what the closure brings in is copied. The sender's handles are left as they were: one the rules above missed names an object no other thread uses. A send that throws carries back an equal value (the caller's or its copy). The copy takes time and memory in proportion to what it copies; a run of plain values (no GC reference) is one memory copy.rok::core::marker::hand_over(x)returns the same copy. Left shared:Syncparts, values of types with anunsafe impl Send(they manage their own sharing: tasks, files, sockets), and generators and streams (notSend: they are never handed over).Handoff::newandMutex::new_unshared(2.16.3) do not copy. A handle taken out of a guard (get,update's closure argument) is not copied at run time: the rules above follow it, and the guard itself, out of theusingblock through every copy, areturn, ayield, the block's value and the frames of coroutines made inside it.- copies (
The following are excluded:
Cell<T>, raw pointers (Ptr<T>,ConstPtr<T>,CPtr<T>), bareanyandany Trwhose bound lacksSend/Sync(anany TrisSendonly when its bound has bothSendandSync: copies of ananyshare its box, as a closure's captures are shared, so no collection may sit inside one that crosses threads), function types without a bound, and closures with a[ref]capture (refs-lang 4.6.9). Other exceptions are specified in each type's chapter.A function type has these traits only when the bound is written, as in
fn(..) -> R + Send(refs-lang 4.4).
Types for which the automatic implementation is wrong declare it directly. A type that holds a raw pointer internally but wraps it thread-safely opts in with unsafe impl (safety is the author's responsibility); a type that is structurally auto-implemented but not thread-safe opts out with impl !Send. The exclusion of Cell<T> is declared this way.
struct Handle { raw: Ptr<u8>; }
unsafe impl Send for Handle { }
unsafe impl Sync for Handle { }
impl<T> !Send for Cell<T> { }
impl<T> !Sync for Cell<T> { }
impl<T> !Send for Ptr<T> { }
impl<T> !Sync for Ptr<T> { }
impl<T> !Send for CPtr<T> { }
impl<T> !Sync for CPtr<T> { }
APIs that pass values to threads or tasks (thread::spawn, task::spawn, par::*, channel) constrain their arguments with these two traits. The policy for synchronization primitives follows 2.16 sync.
2.13.10. Default - default values
trait Default {
fn default() -> Self;
}
| Type | default() |
|---|---|
integers, int | 0 |
floating-point, float | 0.0 |
bool | false |
char | '\0' |
str | "" |
collections such as List, Dict, Set | empty collection |
T | none | none |
Default can be derived; it builds the value from every field's default().
2.13.11. Derivable traits
The built-in traits that #[derive(...)] (refs-lang 6.2.1) can produce are only these:
| Trait | Generated implementation | Requirement |
|---|---|---|
Clone | per-field clone | every field Clone |
Copy | marker | Clone also derived/implemented, not Drop |
Debug | field names and values in f"{x:?}" form | every field Debug |
Default | per-field default() | every field Default |
PartialEq | AND of per-field == | every field PartialEq |
Eq | marker | PartialEq also derived/implemented |
PartialOrd | lexicographic comparison in field declaration order | every field PartialOrd, PartialEq |
Ord | lexicographic comparison in field declaration order | Eq, PartialOrd also derived/implemented |
Hash | combination of per-field hashes (combining function unspecified) | every field Hash |
Displaycannot be derived (2.10).- Deriving
Eq/Ordwithout its supertrait is a compile error. - Applying
#[derive]to aunionis a compile error.