Unsafe

Unsafe

The unsafe keyword appears in three distinct contexts in Kairo, each serving a different purpose:

ContextMeaning
unsafe { ... }Block that suspends AMT tracking
unsafe *TRaw pointer type with no compiler tracking
fn foo() unsafe -> TSeparate function overload namespace

These are independent mechanisms that share a keyword. An unsafe block does not make all pointers raw, and a raw pointer does not require an unsafe block to use.


Unsafe Blocks

An unsafe { ... } block suspends AMT for all operations within its scope. Inside an unsafe block:

  • AMT does not track pointer lifetimes or provenance
  • AMT does not insert automatic destructors or smart pointer promotions
  • forget!() is available to permanently drop pointers from AMT tracking
  • unsafe &x can create raw pointers from safe bindings
var x = std::create<i32>(42)   // AMT-tracked safe pointer

unsafe {
    forget!(x)                  // drop x from AMT tracking
    c_function(x as unsafe *i32)  // pass to C++ which will free it
}

// x is no longer tracked AMT will not auto-free it

When unsafe blocks are required

An unsafe block is required when calling a C/C++ function that takes or returns raw pointers:

ffi "c++" import "native.hh";

fn main() {
    var data = std::create<i32>(100)

    unsafe {
        forget!(data)
        native_take_ownership(data as unsafe *i32)
    }
}

When unsafe blocks are NOT required

Calling C/C++ functions that use safe parameter types does not require an unsafe block:

C++ parameter typeRequires unsafe block
Value types (int, float, structs by value)No
References (T&, const T&)No
Move references (T&&)No
std::unique_ptr<T>No
std::shared_ptr<T>No
std::weak_ptr<T>No
Raw pointers (T*, void*)Yes

The FFI layer reads C++ declaration signatures and maps C++ smart pointers to Kairo’s AMT-tracked equivalents automatically. std::unique_ptr<T> maps to std::Unique<*T>, std::shared_ptr<T> maps to std::Shared<*T>, std::weak_ptr<T> maps to std::Weak<*T>. A mismatch between AMT’s promotion and the C++ function’s expected smart pointer type is a compile error.

See C/C++ Interop for the full FFI model.


Forget

forget!() is a compiler intrinsic that permanently removes a pointer from AMT tracking. It is only valid inside an unsafe block:

var ptr = std::create<Config>(8080)

unsafe {
    forget!(ptr)   // AMT stops tracking ptr
    // ptr is now the caller's responsibility
}

// AMT will not auto-free ptr if nothing else frees it, this is a memory leak

The primary use case is transferring ownership to C++ code that will manage the pointer’s lifetime:

ffi "c++" import "engine.hh";

fn init_engine() {
    var cfg = std::create<EngineConfig>(defaults())

    unsafe {
        forget!(cfg)
        engine_init(cfg as unsafe *EngineConfig)
        // C++ engine now owns the allocation and will free it on shutdown
    }
}
Caution

forget!() does not free memory it tells AMT to stop tracking the pointer. If the pointer is not freed by other means (C++ code, manual std::free(), etc.), the memory leaks. Use forget!() only when transferring ownership across the FFI boundary.


Raw Pointers (unsafe *T)

unsafe *T declares a pointer with no AMT tracking, no null checks, and no bounds checks. It is the Kairo equivalent of a raw C/C++ pointer:

var raw: unsafe *i32 = unsafe &some_value
*raw = 42   // no null check UB if null

Raw pointers do not require an unsafe block to dereference or use. The unsafe is part of the type itself by declaring the pointer as unsafe *T, the programmer has already opted out of safety for that pointer.

var buf: unsafe *u8 = unsafe std::alloc<u8>(1024)
buf[0] = 0xFF      // no bounds check
buf[100] = 0x00    // no bounds check UB if out of allocation
unsafe std::free(buf)

See Pointers for the full pointer model.


Unsafe Function Overloads

The unsafe modifier on a function creates a separate overload in its own namespace. The caller explicitly selects the unsafe variant with the unsafe keyword:

fn sort(data: [i32]) -> [i32] {
    // safe: bounds-checked, stable sort
    return stable_sort(data)
}

fn sort(data: [i32]) unsafe -> [i32] {
    // unsafe: unstable sort, may reorder equal elements
    return quick_sort(data)
}

var a = sort(my_data)           // calls the safe version
var b = unsafe sort(my_data)    // calls the unsafe version

What unsafe means on a function

unsafe on a function does not mean “unsafe memory.” AMT still guarantees memory safety in both safe and unsafe overloads. The unsafe qualifier signals that the function may not uphold semantic invariants that the safe version does stability, ordering, precision, idempotency, or any other contract beyond memory safety.

The caller writing unsafe sort(...) is explicitly acknowledging: “I know this version has weaker guarantees and I accept the trade-off.”

Overload resolution

Safe and unsafe overloads live in separate namespaces. They can have identical parameter types because the dispatch is determined by the presence or absence of the unsafe keyword at the call site:

fn process(x: i32) -> i32 { ... }        // safe
fn process(x: i32) unsafe -> i32 { ... }  // unsafe different namespace

process(42)          // calls safe version
unsafe process(42)   // calls unsafe version

Modifier restrictions

unsafe cannot be combined with other function modifiers:

CombinationValid
unsafe + constYes
unsafe + evalNo
unsafe + asyncYes
unsafe + panicYes
unsafe + inlineYes
unsafe + volatileYes

unsafe stands alone as an overload qualifier. See Functions for the full modifier compatibility table.


The Safety Boundary

Kairo’s safety model has a clear boundary:

Inside normal code: AMT tracks all pointer lifetimes, inserts null checks on safe pointer dereference, auto-promotes to smart pointers, and emits compile errors when safety cannot be guaranteed. Memory safety is the compiler’s responsibility.

Inside unsafe { } blocks: AMT is suspended. The programmer is responsible for pointer lifetimes, null safety, and deallocation. The compiler trusts the programmer.

With unsafe *T pointers: No tracking regardless of whether the code is inside an unsafe block. The pointer is permanently untracked by its type.

With unsafe function overloads: Memory safety is still guaranteed by AMT. Only semantic invariants beyond memory safety are relaxed.

                    Memory safe?    AMT tracked?    Who manages lifetime?
Normal code         Yes             Yes             Compiler (AMT)
unsafe { }          Programmer      No              Programmer
unsafe *T           Programmer      No              Programmer
fn foo() unsafe     Yes             Yes             Compiler (AMT)

Common Patterns

FFI ownership transfer

ffi "c++" import "lib.hh";

fn send_to_native(data: Config) {
    var ptr = std::create<Config>(data)

    unsafe {
        forget!(ptr)
        native_take_ownership(ptr as unsafe *i32)
    }
}

Custom allocator

fn allocate_aligned(size: usize, align: usize) -> unsafe *void {
    var raw: unsafe *void = unsafe std::aligned_alloc(align, size)
    return raw
}

Interfacing with hardware registers

fn write_register(addr: usize, value: u32) {
    var reg = addr as unsafe *u32
    *reg = value
}

Unsafe overload for performance

fn bounds_check(arr: [i32], index: i32) -> i32 {
    assert index >= 0 && index < arr.len(), "out of bounds"
    return arr[index]
}

fn bounds_check(arr: [i32], index: i32) unsafe -> i32 {
    // caller guarantees index is valid skip the check
    return arr[index]
}

Summary

// Unsafe block suspends AMT
var ptr = std::create<i32>(42)
unsafe {
    forget!(ptr)
    c_function(ptr as unsafe *i32)
}

// Raw pointer no tracking by type
var raw: unsafe *i32 = unsafe &some_value
*raw = 100   // no null check

// Unsafe overload separate dispatch namespace
fn compute(x: f64) -> f64 { /* precise */ }
fn compute(x: f64) unsafe -> f64 { /* fast approximation */ }

var precise = compute(3.14)
var fast = unsafe compute(3.14)

// forget!() drop pointer from AMT
var data = std::create<Buffer>(1024)
unsafe {
    forget!(data)
    // data is no longer tracked manual management required
}