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