Status: This is the authoritative specification of firescript's ownership-based memory model. Some described behaviors (e.g., last-use drop insertion, borrow checking) may not yet be enforced in current builds. If another document conflicts with this page, this page takes precedence.
firescript uses a deterministic ownership model rather than a tracing garbage collector. Values are categorized as either copyable or owned. Owned values move by default; borrowing (&T) provides a temporary, read‑only view only for Owned types. Copyable types are always passed and assigned by value (bitwise copy); they cannot be borrowed. The compiler (planned) inserts destruction (“drop”) at the last proven use or at scope end. Ordinary owned values incur no runtime reference counting.
This page is layered: first core concepts, then detailed rules. Other documentation pages should link here instead of redefining terminology.
- Categorize each value: copyable or owned
- Moving an Owned value transfers ownership; the previous binding becomes invalid
- Borrowing (
&T) is only allowed for Owned types; it grants a read-only, non-owning view tied to the original owner’s lifetime - Copyable types are simply copied; “move” is indistinguishable from copy; borrowing does not exist for Copyable types
- The compiler performs last-use analysis to place deterministic drop points (Owned types only)
- Scope exit drops any remaining owned values
- Cloning is explicit (
.clone()/clone(x)) for Owned values
Core terms (Copyable, Owned, Move, Borrow, Clone, Drop) are defined centrally in the glossary. This document applies those definitions normatively and adds the constraint:
- Borrow (
&T) is only defined for Owned types. Attempting to borrow a Copyable type (e.g.,&int) is a compile error.
- Scoping: Scope exit triggers drops of any still-owned values (see Scoping).
- Functions: Owned parameters move; borrowed parameters (
&T) (Owned types only) do not transfer ownership (see Functions). - Future Closures: Captures of Owned variables default to move; borrow captures are allowed only if the closure cannot escape.
- Copyable: Fixed-size scalars with no destructor (e.g.,
intN,floatN,bool,char). These values are stored on the stack. Copy is bitwise; there is no drop. Borrowing a Copyable type is disallowed (&intis invalid). - Owned: Heap-backed or resource-managing values (e.g., user-defined objects, closures, arrays, strings). These values are stored on the heap with pointers on the stack. Ownership is unique; assignment/pass/return moves ownership. Destruction runs at drop points.
Cloning Owned values is explicit via .clone() or clone(x).
- Each lexical scope defines a region. An owned value lives in the region of its current owner.
- On scope exit, all owned values remaining in the region are dropped (reverse creation order).
- The compiler performs last-use analysis and inserts earlier drops when safe.
- Copyable values have no lifetime actions; they are ignored by drop logic.
Moving a value transfers it to the destination region:
- Passing to a function that takes
Tmoves into the callee's region. - Returning
Tmoves into the caller’s region. - Assigning to a variable moves into that variable’s region.
Parameters may be:
T: (T is Owned) callee receives ownership; caller loses it unless returned. All parameters default to move semantics.&T: (T is Owned) borrowed, read-only; callee cannot retain or return it in a longer-lived form. Borrowing must be explicit.
For Copyable types:
- There is only pass-by-value (copy). No borrow syntax is permitted.
intN,bool, etc. are always copied; moves do not invalidate the source.
Returns:
- Returning
T(Owned) transfers ownership to the caller. - Returning
&T(Owned) only allowed if the referenced value is owned by the caller (non-escaping). Borrow returns of Copyable types are invalid (they cannot be borrowed).
Notes:
- For owned types,
Tmeans move semantics. If you want to borrow, you must use&T. - There is no implicit borrowing - borrowing must be explicitly requested with the
&marker. - At call sites, passing an owned value to a function expecting
Tmoves ownership; passing to&Tborrows. - Attempting to pass a Copyable value where
&Tis required is a type error (cannot borrow Copyable). - Generic Parameters Exception: For generic type parameters (e.g.,
&Tin a generic function), Copyable values are implicitly copied rather than borrowed. This allows generic functions to accept both Copyable and Owned types. For concrete (non-generic) Owned types, the borrow restriction still applies.
- Borrowing is only defined for Owned types.
- A borrowed
&OwnedTypeis a non-owning, read-only view. - A borrow cannot be stored in any owned field or global location that outlives the borrow expression/call.
- Mutability of Owned values occurs via methods on an owned receiver or a consuming pattern (no mutable borrow form yet).
- If a function must retain or store a value, it must take ownership (
owned T) or clone inside the borrow’s scope.
TL;DRL Borrowing passes a read-only pointer.
- Capturing by move transfers ownership into the closure’s region (Owned types only).
- Capturing by borrow is only allowed for non-escaping closures and only for Owned types.
- Copyable values captured are copied; no borrow form exists. (Copyable capture has no ownership effect.)
- Escaping closures cannot have borrowed captures.
- Owned (Owned) types may define
drop(this). - Drops occur deterministically:
- At last use (if provable).
- At scope exit.
- Along all control-flow exits (return, break, exception).
- Moves prevent duplicate drops (previous binding invalidated).
- Copyable values never have drops.
Example:
File f = File.open("log.txt");
f.write("hello");
f.flush();
// f dropped here even on early return/exception; File.drop closes the descriptor.
Method receiver kinds apply only to Owned types (Copyable methods implicitly copy the receiver):
- Borrowed receiver (default for Owned): signature form
name(...) ReturnTypeimplicitly receives&OwnedType this(read-only). - Consuming receiver:
name(this, ...) ReturnTypetakes ownership; caller's binding is invalid unless the method returns it.
For Copyable types:
- Methods always receive the value by copy; “consuming” semantics do not apply.
Consuming example:
Account upgrade(this) {
// ... mutate internal state
return this;
}
acct = acct.upgrade(); // rebind because ownership moved and was returned
- Conditional joins: if both branches use a value, drop inserted after merge; otherwise at last branch that uses it.
- Loops: Values used across iterations drop after loop. Iteration-temporary owned values drop at iteration end.
- Copyable values ignored by last-use logic (always trivially copyable).
- To create multiple independent owners of an Owned value:
.clone(). - Opt-in reference-counted or shared abstractions may be provided (e.g.,
Rc<T>,Arc<T>, arenas). These layer explicit runtime or bulk management semantics on top of the ownership core. - Copyable values never require sharing constructs.
- Native: Drops become direct destructor calls.
- JavaScript: Destruction logic executes at specified points; underlying JS GC handles memory backing where applicable; resource closures are deterministic.
- Use-after-move (Owned) is an error: primary note at move, secondary at invalid use.
- Illegal borrow of a Copyable type (e.g.,
&int) is an error: “Cannot borrow trivially copyable type ‘int’; pass by value.” - Borrow escape detection flags returning or storing a borrow beyond its allowed lifetime.
- Deterministic destruction order may be visualized (tooling).
A class may be annotated copyable to become Copyable if it satisfies:
- All fields are Copyable.
- No
dropdefined. - Fixed-size representation.
- No disallowed interior references.
Example:
copyable class Vec2 {
float32 x; // float32 is Copyable
float32 y;
}
Vec2 values copy bitwise; moves do not invalidate the source.
The following examples illustrate planned semantics. Some features (e.g., full last-use optimization) may not yet be implemented.
Object o1 = Object(); // owned
Object o2 = o1; // move; o1 invalid afterward
// print(o1); // error: moved value
int8 x = 42;
int8 y = x; // copy (Copyable); x still valid
print(x); // OK
string s1 = "fire";
string s2 = s1.clone(); // independent
print(s1);
print(s2);
int length(&string s) {
return s.length;
}
string name = "firescript";
int8 n = length(name); // borrow; name still valid
int8 id = 10;
printId(&id); // ERROR: cannot borrow Copyable type 'int'; remove '&'
// Correct version:
void printId(int8 v) {
print(v);
}
printId(id); // copies 'id'
print(id); // still valid
void addUser(string username); // Plain 'string' means owned/move
string u = "alice";
addUser(u); // move; u invalid afterward
// print(u); // ERROR: use after move
Or equivalently with explicit owned:
void addUser(owned string username); // Explicit owned
string u = "alice";
addUser(u); // move; u invalid afterward
&string head(string[] xs) {
return xs[0]; // OK: element owned by caller's array
}
Account activate(owned this) {
// mutate state
return this;
}
acct = acct.activate(); // rebind with returned owned value
| Kind | Copyable Types | Owned Types |
|---|---|---|
Borrow (&T) |
Not allowed (error) | Allowed (read-only, non-owning) |
| Move | Degenerate (copy) | Transfers ownership; source invalid |
| Clone | Not needed | Explicit, creates new owner |
| Drop | Not applicable | Invokes destructor deterministically |
Borrowing is intentionally restricted to Owned types to:
- Preserve simplicity (no redundant alias form for scalars).
- Keep ownership-focused diagnostics clear.
- Avoid needless syntactic noise for trivially cheap copies.
If a future revision broadens Copyable to include large POD aggregates, this restriction may be reconsidered; until then it is normative.