Memory safety and type system

You probably have heard of the recent Bun Rust rewrite debacle. Long story short, Bun’s recent Rust rewrite is not about Rust being better than Zig; it is about which kind of type system you need when a tracing garbage collector and a more manual allocator share a process. The boundary between JavaScriptCore’s tracing GC and Zig’s manual allocator is where ownership invariants have to be enforced by hand. With a linear type system, you can automatically enforce the invariance.

Example failure at programming

Bun calls into JavaScriptCore through the C API.

  1. JSValueMakeString and JSObjectMake return a value with a +1 retain; you must release it or let it be collected.
  2. JSValueProtect and JSValueUnprotect must be called in matched pairs; unprotecting too many times is undefined behavior.
  3. The conservative stack scanner can find JSValueRefs on the stack during a JSC call, but it cannot see inside Zig heap allocations or globals, so anything stored there requires JSValueProtect.

Here’s an example bug that could have been caught with a more capable type system.

const NativeCache = struct {
    ctx: c.JSContextRef,
    cached_js_value: c.JSValueRef,

    pub fn init(ctx: c.JSContextRef, js_object: c.JSObjectRef, prop_name: []const u8) NativeCache {
        const name_str = c.JSStringCreateWithUTF8CString(prop_name.ptr);
        defer c.JSStringRelease(name_str);
        const val = c.JSObjectGetProperty(ctx, js_object, name_str, null);
        return .{ .ctx = ctx, .cached_js_value = val };
    }

    pub fn getCached(self: *NativeCache) c.JSValueRef {
        return self.cached_js_value;
    }
};

JSObjectGetProperty returns a borrowed reference. The NativeCache itself lives on the Zig heap, invisible to the JSC stack scanner. When the next GC cycle runs, the borrowed value may be collected, and getCached returns a dangling pointer. The compiler does not notice because the type JSValueRef carries no information about scope, protect count, or ownership.

The manual fix is to add JSValueProtect1 in init and JSValueUnprotect in deinit. But the compiler still does not check that the calls are matched, that the destructor always runs, or that the value is not used after being unprotected. You are simply expected to get it right.

The languages

Rust:

Rust uses linear ownership plus borrow checking. You can’t prove arbitrary theorems in it, but it’s good enough for solving programming tasks with challenging memory safety.

I won’t explain how linear type works here. Look it up.

Lean:

Lean doesn’t have linear types, but it allows you to prove theorems.

Idris:

Idris 2 has a C backend that allows you to compile to C with GC. Having GC is bad though in Bun’s case, since TK.

Runtime engineering

It is different when you own a whole runtime. A Wren implementation, for example, has no foreign GC boundary; the Rust code can design the collector to cooperate with ownership from the start. safe_wren is a pure Rust implementation of Wren, and projects like piccolo and rust-gc explore the same direction for Lua and general GCs. In that world, the unsafe surface is the GC itself, and everything else sits in a safe, ownership-checked core. Bun cannot take that approach unless it replaces JavaScriptCore. Or maybe it can, since Anthropic’s model seems to have conscious thought about achieving AGI2.

Closing thoughts

There are too few choices out there these days when it comes to programming languages. Yes, ATS does not count as an engineering choice to me. So choose the right one for your use case!


  1. Apple docs↩︎

  2. not my advertisement↩︎