On this page
article
Chapter 9: Memory and Ownership
Allocators
Every allocation in Violet is explicit. You choose the allocator:
const alloc_mod = import("std/alloc");
fn main(ctx: process.Context): !void {
-- general purpose allocator
var gpa = alloc_mod.GPA::new();
defer gpa.deinit();
var alloc = gpa.allocator();
-- allocate memory
var buf = try alloc.alloc(u8, 1024);
defer alloc.free(buf);
-- allocate a single value
var ptr = try alloc.create(i32);
defer alloc.destroy(ptr);
ptr.* := 42;
}
using — Implicit Allocator Threading
When a function uses the same allocator everywhere, using reduces noise:
fn buildReport(alloc: Allocator, data: []const u8): !Report {
using alloc; -- alloc is now implicit below
var tokens = try tokenize(data); -- alloc threaded implicitly
errdefer tokens.deinit();
var ast = try parse(tokens.asSlice());
errdefer ast.deinit();
return try analyze(ast);
}
Functions that accept implicit allocators declare ctx alloc:
fn tokenize(input: []const u8, ctx alloc: Allocator): ![]Token { ... }
Ownership and Move Semantics
Non-Copy types move when assigned:
var list = List[i32]::new();
var other = list; -- list is MOVED into other
-- list is gone here, using it is a compile error
use(other); -- valid: other owns the list
-- consume the value
other.deinit(alloc); -- explicitly consumed
Copy types copy silently:
var x: i32 = 5;
var y = x; -- x is copied, both x and y are valid
debug.print("{} {}\n", .{x, y});
Mark your type Copy:
type Point = struct { x: f32, y: f32 } derive [Copy];
var p1 = Point { .x = 1.0, .y = 2.0 };
var p2 = p1; -- copied, p1 still valid
Linear Types — The Destroy Interface
Types that own resources implement Destroy and must be explicitly cleaned up:
interface Destroy {
type Args;
fn deinit(*self, args: Self::Args): void;
}
type Connection = struct {
fd: i32,
host: []const u8,
};
provide Destroy for Connection {
type Args = void;
fn deinit(*self, _: void) {
os.close(self.fd);
}
}
-- the compiler enforces cleanup:
fn example(): !void {
var conn = try connect("example.com:80");
-- error if conn is not consumed before function exits
defer conn.deinit(); -- satisfies the compiler
try send(&conn, "GET / HTTP/1.0\r\n");
}
Scratch Allocation
For temporary memory within a function:
fn processRequest(alloc: Allocator, req: Request): !Response {
scratch(alloc, 4096) {
-- temporary arena, freed when this block exits
var headers = try parseHeaders(req.raw);
var validated = try validate(headers);
-- all temporaries freed here
}
-- build the real response with the outer allocator
return try buildResponse(alloc, req);
}
Smart Pointers
For shared ownership:
const sync = import("std/sync");
-- Rc: single-threaded reference counting
var shared = Rc[Config]::new(alloc, config);
var copy = shared.clone(alloc); -- increments refcount
-- config freed when last Rc drops
-- Arc: thread-safe reference counting
var arc1 = Arc[Config]::new(alloc, config);
var arc2 = arc1.clone(alloc); -- safe to send to another thread