TYCHO Search

Search documentation

    ↑ ↓ to move · Enter to open · Esc to close

    From malloc to implicit arenas

    If you write C, you already know Tycho's memory model — you just have to remove one thing from it. If you don't, skip the grey C boxes: each one ends with a plain-English sentence that carries the point. This is the idea in five steps, starting from code you've written a hundred times. For the rigorous version with benchmarks, see thesis.md; this is the on-ramp.

    Step 1 — the C you know

    char* greet(const char* name) {
        size_t n = strlen(name) + 7;
        char* out = malloc(n);
        snprintf(out, n, "hello %s", name);
        return out;          // caller must free() this. miss it -> leak. free twice -> crash.
    }
    

    Every allocation is a promise to free, and the compiler doesn't hold you to it. That's the whole tax of manual memory management: the knowledge of *when* to free lives in your head, not in the code.

    Step 2 — arenas, an old and fast fix

    An arena (or region, or bump allocator) replaces malloc/free with something simpler: hand out memory by moving a pointer forward, and free *everything at once* when the arena's job is done.

    Arena a = arena_new();
    char* x = arena_alloc(&a, 32);   // no individual free
    char* y = arena_alloc(&a, 64);
    // ... use x, y ...
    arena_free(&a);                  // one free reclaims both
    

    Allocation is a pointer bump (nearly free), there's no per-object bookkeeping, and you can't leak an individual object. Game engines, compilers, and web servers have used arenas for decades. So why isn't every program written this way?

    Step 3 — the one hard question

    Because of *escape*. If a value allocated in an arena is still referenced after the arena is freed, you have a dangling pointer:

    Arena a = arena_new();
    char* p = arena_alloc(&a, 32);
    stash_globally(p);      // p escapes the arena's lifetime...
    arena_free(&a);         // ...and is now a use-after-free.
    

    Knowing whether a value escapes, in a language with pointers, requires whole-program alias analysis — following every pointer everywhere to prove nothing outlives its arena. That's the hard part, and it's why arenas stay a manual, expert-only tool.

    Step 4 — Tycho's move: delete the question

    Tycho removes escape analysis by removing the thing it analyzes: there is no reference type. You cannot store or return a pointer into another value's memory, and assignment copies:

    a := [1, 2, 3]
    b := a                                  # an independent copy, not an alias
    b[0] = 9
    println(str(a[0]) + " " + str(b[0]))    # 1 9 -- writing b left a alone
    

    Now a value can leave a scope in exactly two ways, and *both are written in the source*:

    There's no third way, because there are no references to smuggle one out. So the compiler can see every value's lifetime from the syntax alone. Here's the C function from Step 1, in Tycho:

    fn greet(name: string) -> string:
        return "hello " + name
    

    No malloc, no free, no arena handle. Each scope — every function call, every loop iteration, every if block — owns an arena. The "hello " + name string is built in greet's arena; because it's *returned*, the compiler allocates it in the caller's arena instead, so it's still alive after greet returns and is freed when the caller's scope ends. The compiler placed every allocation and every free itself, from the shape of the code. No garbage collector runs; no annotations were written.

    Tycho transpiles to C, so you can read exactly the arena calls it emits. Here is that greet, run through ./tychoc greet.ty --emit-c:

    char *h_greet(Arena *_parent, char *h_name) {
        { char *_ret = tycho_str_concat(_parent, TYCHO_LIT("hello "), h_name); return _ret; }
    }
    

    _parent is the caller's arena. The string is built there directly, so there is nothing to free on the way out and no scope of its own to create.

    Now change one thing, so the string no longer escapes:

    fn greet_len(name: string) -> int:
        msg := "hello " + name
        return len(msg)
    

    Same shape, one line different, and the compiler reaches a different decision:

    tycho_int h_greet_len(Arena *_parent, char *h_name) {
        Arena _scope = arena_child(_parent); _scope.name = "greet_len";
        char *h_msg = tycho_str_concat(&_scope, TYCHO_LIT("hello "), h_name);
        { tycho_int _ret = tycho_str_len(h_msg); arena_free(&_scope); return _ret; }
    }
    

    A child arena, the string built inside it, and arena_free on the way out — placed without an annotation, a lifetime, or a runtime check. That is the whole mechanism, and the diff between the two C functions is the diff between return msg and return len(msg).

    Two things worth noticing:

    Step 5 — the cost

    Value semantics has real costs:

    What you get for it

    No garbage collector and its pauses. No manual free and its leaks and use-after-frees. No borrow checker and its annotations. Just lexical scope — the same blocks you already write — deciding when memory is freed, with the compiler doing the placement. The thesis has the measurements so far.

    Next: the language reference for what each feature does, and quiet results before the first real program.