Memory
Values and objects, the memory mode each project chooses, destructors and delete, regions, references and the borrow checker, arrays, unsafe code and pointers, fixed buffers and sizeof.
On this page
Values and objects
Every type is either a value type or an object type:
| Kind | Types | Where it lives | Assigning and passing |
|---|---|---|---|
| Values | numbers, bool, enums, value and record value types, tuples, func values, pointers |
In the variable, field or element that holds it. | Copies it. |
| Objects | class, record class, str, arrays, any |
On the heap; variables hold a reference. | Copies the reference: both names see the same object. |
A value type that holds objects (a value with a str field, a tuple (i32, str)) copies the references it holds, not the objects.
What gives heap memory back is chosen per project, not per type: the same source compiles in every mode.
Memory modes
The gc: key of the BUILD chunk in the project's .queen file chooses the mode (see Project files):
chunk BUILD:
gc: refcount
gc: |
How objects are freed | When destroy() runs |
|---|---|---|
default (also tracing) |
A tracing collector finds the objects nothing reaches and frees them. | When the collector frees the object; perhaps never, if the program ends first. |
refcount |
Each object counts the references to it, and is freed when the count reaches zero. Cycles are never freed: break them yourself, for example by setting a field to null. |
Right after the last use of the last reference. |
arena |
Objects made inside a region { } are freed together when the region ends. Objects made outside any region live until the program ends. |
When the region ends. |
manual |
Each object is freed with delete. |
On delete. |
custom |
A collector written by the project. |
Without a gc: key the project uses your machine's default (larva config gc, see The larva command), and that is default unless you changed it.
Warning
Not yet available: gc: custom with gc.collector: builds, but System.Core has no collector interface for a project to implement, so the program runs with the tracing collector.
The same program in each mode
import System;
into namespace Modes
{
public class Note
{
str text;
public:
new(str text) { this.text = text; }
Show() { Console.WriteLine("note " + text); }
destroy() { Console.WriteLine("destroy " + text); }
}
Make()
{
Note n = new Note("local");
n.Show();
}
Main()
{
Make();
Note kept = new Note("first");
kept = new Note("second");
kept.Show();
Console.WriteLine("end of Main");
}
}
With gc: refcount, every object is destroyed as soon as nothing uses it any more: local after its last use, first when kept is assigned another note, second after its last use.
larva run
note local
destroy local
destroy first
note second
destroy second
end of Main
With gc: default, gc: arena and gc: manual this program destroys nothing: the tracing collector had no reason to run before the end, no region was used, and nothing was deleted.
larva run
note local
note second
end of Main
Libraries built for one mode can be used from a project built for another; Larva links each as its own domain. Numbers, bools, enums and pointers to numbers cross between domains; strings and bytes go through System.Exchange. Using an object of a library built for another gc: is error AN1079.
Destructors
A class declares one destructor, destroy(): no parameters, no result, not static (AN1078). Values, interfaces and enums have none (AN1077).
- It runs once per object.
- A derived class's destructor runs first, then its base's.
- When it runs depends on the memory mode (see the table above).
delete
With gc: manual, delete x; runs the object's destructors and frees it. Freeing by hand is unsafe, so delete goes in unsafe code; outside it the build fails with error[UnsafeRequired].
import System;
into namespace Manual
{
public base class Resource
{
public:
Name() : str;
destroy() { Console.WriteLine("destroy Resource"); }
}
public class File : Resource
{
public:
override Name() : str => "file";
destroy() { Console.WriteLine("destroy File"); }
}
Main()
{
Resource r = new File();
Console.WriteLine(r.Name());
unsafe { delete r; }
Console.WriteLine("end");
}
}
larva run
file
destroy File
destroy Resource
end
delete through a base class runs the destructors of the object's real class. The operand is a class-typed expression: delete on an interface-typed variable is error AN2012 (delete frees an object); cast it to its class first.
Using an object after deleting it is undefined: nothing checks it.
Regions
region { … } is a block whose objects are freed together when it ends, however it ends (normally, or by break, continue, return or an exception). It frees objects in projects with gc: arena; in the other modes it is an ordinary block.
import System;
into namespace Regions
{
public class Buffer
{
i32 id;
public:
new(i32 id) { this.id = id; }
destroy() { Console.WriteLine("free " + id); }
}
Main()
{
loop (i32 i = 0..2)
{
region
{
Buffer a = new Buffer(i * 10);
Buffer b = new Buffer(i * 10 + 1);
Console.WriteLine("turn " + i);
}
}
Buffer outside = new Buffer(99);
Console.WriteLine("end");
}
}
With gc: arena:
larva run
turn 0
free 1
free 0
turn 1
free 11
free 10
end
Objects are freed in the reverse order they were made. outside was made outside any region, so it lives until the program ends.
An object made in a region cannot outlive it. Larva checks this when it builds the program: returning such an object, or storing it where something outside the region can reach it, is a RegionEscape error.
Make() : Buffer
{
region
{
Buffer b = new Buffer();
return b;
}
}
larva build
larva: error[RegionEscape]: `return` holds a value from region r0 and is still used after region_exit(r0)
--> Escape.$Module.Make bb1[4]
The collector
System.GC talks to the tracing collector:
| Member | What it does |
|---|---|
GC.Collect() |
Asks for a collection, which happens at the next safepoint. |
GC.CollectionCount() |
How many collections ran (an i64). |
GC.ObjectCount() |
How many objects are alive, or not collected yet. |
import System;
into namespace Tracing
{
public class Temp
{
public:
destroy() { Console.WriteLine("finalized"); }
}
Churn()
{
loop (i32 i = 0..3)
{
Temp t = new Temp();
}
}
Main()
{
Churn();
GC.Collect();
Console.WriteLine("collections: " + GC.CollectionCount());
Console.WriteLine("end");
}
}
With gc: default, one run printed:
larva run
finalized
finalized
collections: 1
end
The collector scans the stack conservatively, so which unreachable objects one collection finds can vary; a program must not depend on it. In refcount, arena and manual there is no collector, and these functions have nothing to do.
Arrays
An array is an object: assigning an array, or passing it, shares it.
import System;
Main()
{
i32[] a = [1, 2, 3];
i32[] b = a;
b[0] = 100;
Console.WriteLine(a[0]);
}
larva run
100
To have a separate array, make a new one and copy the elements (List<T> has ToArray(), and Marshal.Copy copies numbers to and from native memory).
Every index is checked. An index outside the array stops the program at once with exit code 101; it is not an exception, and catch does not see it:
larva run
larva panic: index 3 out of bounds for length 1
A new array starts zeroed, so its element type needs a zero value: new str[3] is error AN2022; use new str?[3], an array literal, or a List<str>. See Types for arrays of several dimensions.
References and the borrow checker
&T is a shared reference to a T: it reads, and many can exist at once. &mut T is an exclusive reference: it reads and writes, and while it lives nothing else uses the same place.
| Syntax | What it is |
|---|---|
&T, &mut T |
Reference types, for parameters and locals. |
&x, &mut x |
Borrows a place: a local, a parameter, a field or an element (AN2145 for anything else). |
r.field, r.Method(), r[i] |
Members, methods, indexing and operators see through a reference. |
&mut Counter r = &mut c; |
A reference local. |
public value Counter
{
public:
i32 n;
}
Bump(&mut Counter c)
{
c.n = c.n + 1;
}
Read(&Counter c) : i32 => c.n;
Main()
{
Counter c;
Bump(&mut c);
Bump(&mut c);
Console.WriteLine(Read(&c)); // 2
&mut Counter r = &mut c;
r.n = 7;
Console.WriteLine(c.n); // 7
}
References exist only when the project's borrow checker is on. It has three settings, chosen with borrowcheck: in BUILD, with --borrowcheck on larva build and larva run, or for your machine with larva config borrowcheck:
| Setting | What happens |
|---|---|
off |
The default. Reference types are error AN1092 and &x / &mut x are error AN2144: pass the value itself, or turn the checker on. |
warn |
Borrow errors are reported as warnings, and the program is built. |
enforce |
Borrow errors stop the build. |
The checker runs on every build, and in the editor. A reference lives until its last use. The rules:
| Rule | Error |
|---|---|
| While an exclusive borrow lives, the place is not used, assigned or borrowed again. | ConflictingBorrow |
Two &mut of one place do not live at once. |
ConflictingBorrow |
Nothing is written through a &T. |
MutationThroughSharedReference |
| A function does not return a reference to its own local. | ReturnsLocalReference |
Counter c;
&mut Counter r = &mut c;
c.n = 1; // c is borrowed by r, which is used on the next line
r.n = 7;
larva build --check --borrowcheck enforce
app.ant(7,2): error[ConflictingBorrow]: cannot assign to `c.0` while it is borrowed (mutable borrow of `c` on line 6)
larva build --check --borrowcheck warn
app.ant(7,2): warning[ConflictingBorrow]: cannot assign to `c.0` while it is borrowed (mutable borrow of `c` on line 6)
In unsafe code, &x is the address of x (a pointer) unless a reference is expected.
Warning
Not yet available:
*rto read the whole referred value is error AN2067 outsideunsafecode; read its members instead (r.n).- A reference to an array (
&i32[]) cannot be indexed, looped over or asked itsLength(error[CopyOfNonCopy]). - References are meant to be locals and parameters only, but a reference field in a class, or a reference as a type argument (
List<&i32>), is not reported yet. Borrowing a static field gives an internal error.
Unsafe code
Pointers, delete, calls through func* and pointer casts are allowed only in unsafe code:
| Form | Example |
|---|---|
| An unsafe function or method | unsafe Fill(i32* p, i32 count) { … } |
| An unsafe type: every member is unsafe | public unsafe class Cursor { … } |
| An unsafe block | unsafe { … } |
Calling an unsafe function needs no unsafe block; what needs it is using a pointer, so a caller that passes &data[0] is in unsafe code anyway.
A project can forbid unsafe code with unsafe: deny in BUILD. Then every unsafe type, function or block, and every pointer type, in the project's own files is error AN1104. Libraries and dependencies are not affected, and their unsafe functions can still be called.
larva build --check
Program.ant(3,13): error AN1104: a pointer type: this project denies unsafe code (BUILD `unsafe: deny`): no `unsafe` types, functions or blocks, and no pointers; allow it with `unsafe: allow`
Pointers
| Syntax | What it does |
|---|---|
T* |
A pointer to a T: a number, bool, enum, value, pointer or func*. |
void* |
A pointer to bytes. |
&x |
The address of a local, field or element (AN2066 outside unsafe code). |
*p |
The value p points at, to read or write (AN2067 outside unsafe code). |
p[i] |
The element i places after p: *(p + i). |
p->field |
A field of the value p points at: (*p).field. |
p + n, p - n, p += n |
Moves by n elements (bytes for void*). |
p - q |
The distance in elements. |
p == q, p < q… |
Comparisons; p == null tests for no address. |
(i32*)raw, (u64)p, (void*)p |
Casts between pointer types and to and from integers. |
func*<A, B> : R |
The address of a C-style function (see Interop). |
Multiplying or dividing a pointer is error AN2122.
import System;
public value Point
{
public:
i32 X;
i32 Y;
}
unsafe Fill(i32* p, i32 count, i32 value)
{
loop (i32 i = 0..count)
p[i] = value;
}
public unsafe class Cursor
{
u8* at;
public:
new(u8* start) { at = start; }
Next() : u8 { u8 b = *at; at += 1; return b; }
}
Main()
{
i32[] data = new i32[4];
Point pt;
unsafe
{
Fill(&data[0], data.Length, 9);
i32* second = &data[0] + 1;
*second = 1;
i32* last = &data[3];
Console.WriteLine(last - second);
Console.WriteLine(second < last);
Point* pp = &pt;
pp->X = 3;
(*pp).Y = 4;
void* raw = (void*)pp;
i32* asInts = (i32*)raw;
Console.WriteLine(asInts[1]);
i32* nothing = null;
Console.WriteLine(nothing == null);
u8[] bytes = [65, 66];
Cursor c = new Cursor(&bytes[0]);
Console.WriteLine(c.Next() + c.Next());
}
Console.WriteLine(data[0] + data[1] + data[2] + data[3]);
Console.WriteLine(pt.X + pt.Y);
}
larva run
2
true
4
true
131
28
7
A pointer to an element of an array or a field of an object stays valid only while the object is alive and does not move; keep such pointers short-lived. Pointers into memory that is not the program's come from C, or from Marshal.AllocHGlobal and NativeMemory.Alloc (see Interop).
Fixed buffers
fixed T[N] name; keeps N elements inside the value or class, with no separate allocation. N is a positive constant, sizeof(T) or N * sizeof(T) (AN1073). The field is indexed like an array and has a Length. A fixed buffer belongs to each object or value, so it is not static (AN1074).
Arrays kept in the object
A final class can keep one array of any length inside each object, as its last field: fixed T[] name;, with no length and no initializer. Object and elements are allocated together, in one piece.
- Each constructor says how many elements the object holds with
: fixed(n), which is its whole header (no: base(…)or: (…)with it). The expression is evaluated before the object exists, so it uses only the parameters. - The elements are numbers,
boolor a type parameter. - The field is read and written by index and has a
Length, which never changes; indexes are checked. It is not an array value: it cannot be assigned, passed or returned (AN1099). Copy it to have an array.
import System;
public value Header
{
public:
u32 Tag;
fixed u8[8] Name;
fixed u16[sizeof(u32)] Wide;
}
public final class Samples
{
str label;
fixed i32[] items;
public:
new(i32 count) : fixed(count)
{
label = "squares";
loop (i32 i = 0..count)
items[i] = i * i;
}
new(i32[] from) : fixed(from.Length)
{
label = "copy";
loop (i32 i = 0..from.Length)
items[i] = from[i];
}
i32 Count { get => items.Length; }
At(i32 i) : i32 => items[i];
}
Main()
{
Header h;
h.Tag = 7;
h.Name[0] = 65;
Console.WriteLine(h.Name.Length);
Console.WriteLine(sizeof(Header));
Samples s = new Samples(5);
Console.WriteLine(s.Count + " " + s.At(4));
Samples t = new Samples([3, 1, 2]);
Console.WriteLine(t.At(0));
}
larva run
8
20
5 16
3
| Code | When |
|---|---|
| AN1097 | fixed T[] is not the last instance field of a final class that is not a record, or has an initializer. |
| AN1098 | A constructor does not say : fixed(n), or combines it with : base(…) or : (…). |
| AN1099 | The field is used as an array value instead of by index. |
sizeof
sizeof(T) is the size of a T in bytes, as a u64 constant: sizeof(i128) is 16, and sizeof(Header) above is 20 (a u32, 8 bytes and 4 u16s, aligned to 4). For a class it is the size of a reference. Inside a generic, sizeof(T) is known for each instance. Marshal.SizeOf<T>() gives the same number as an i32.
Edit this page on GitHub Report an issue