Unsafe
The unsafe keyword appears in three distinct contexts in Kairo, each serving a different purpose:
| Context | Meaning |
|---|---|
unsafe { ... } | Block that suspends Tether tracking |
unsafe *T | Raw pointer type with no compiler tracking |
fn foo() unsafe -> T | Separate 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 Tether for all operations within its scope. Inside an unsafe block:
- Tether does not track pointer lifetimes or provenance
- Tether does not insert automatic destructors or smart pointer promotions
forget!()is available to permanently drop pointers from Tether trackingunsafe &xcan create raw pointers from safe bindings
var *x = @create i32(42) // Tether-tracked safe pointer
unsafe {
forget!(x) // drop x from Tether tracking
c_function(x as unsafe *i32) // pass to C++ which will free it
}
// x is no longer tracked Tether 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 = @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 type | Requires 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 Tether-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 Tether'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 Tether tracking. It is only valid inside an unsafe block:
var *ptr = @create Config(8080)
unsafe {
forget!(ptr) // Tether stops tracking ptr
// ptr is now the caller's responsibility
}
// Tether 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 = @create EngineConfig(defaults())
unsafe {
forget!(cfg)
engine_init(cfg as unsafe *EngineConfig)
// C++ engine now owns the allocation and will free it on shutdown
}
}
forget!() does not free memory it tells Tether 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 Tether 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." Tether 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 combines with every other function modifier except eval:
| Combination | Valid |
|---|---|
unsafe + const | Yes |
unsafe + eval | No |
unsafe + async | Yes |
unsafe + panic | Yes |
unsafe + inline | Yes |
unsafe + volatile | Yes |
unsafe is an overload qualifier: the other modifiers apply to the safe and unsafe versions alike. See Functions for the full modifier compatibility table.
The Safety Boundary
Kairo's safety model has a clear boundary:
Inside normal code: Tether tracks all pointer lifetimes and proves every safe dereference sound. *T is non-null by construction, so its dereference needs no check; a *T? must be unwrapped or proven non-null. Where a proof is missing, the response depends on the clause (see the Residual Table): bounds and epoch residuals get a runtime check in both modes, while nullability and ownership residuals are transformed in debug (null guard, smart pointer promotion) and are hard errors in release. Everything else is a compile error. Memory safety is the compiler's responsibility.
Inside unsafe { } blocks: Tether 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 Tether. Only semantic invariants beyond memory safety are relaxed.
Memory safe? Tether tracked? Who manages lifetime?
Normal code Yes Yes Compiler (Tether)
unsafe { } Programmer No Programmer
unsafe *T Programmer No Programmer
fn foo() unsafe Yes Yes Compiler (Tether)
Common Patterns
FFI ownership transfer
ffi "c++" import "lib.hh";
fn send_to_native(data: Config) {
var *ptr = @create Config(data)
unsafe {
forget!(ptr)
native_take_ownership(ptr as unsafe *Config)
}
}
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) {
unsafe {
var unsafe *reg = addr as unsafe *u32 // integer-to-pointer cast fabricates provenance
*reg = value
}
}
Unsafe overload for performance
fn bounds_check(arr: [i32], index: i32) -> i32 {
assert index >= 0 && index < arr.length(), "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 Tether
var *ptr = @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 Tether
var *data = @create Buffer(1024)
unsafe {
forget!(data)
// data is no longer tracked manual management required
}