Casting

Casting

All explicit type conversions in Kairo use the as keyword. There are no separate cast operators like C++‘s static_cast, dynamic_cast, reinterpret_cast, and const_cast as handles all conversion categories, with safety determined by the source and target types.

For implicit conversions (numeric widening, T to T?, derived-to-base pointers), see Type System.


Numeric Casts

Widening

Integer and float widening is implicit no as required:

var a: i32 = 42
var b: i64 = a       // implicit

as is permitted but redundant for widening conversions.

Narrowing (truncation)

Narrowing conversions require an explicit as. The cast truncates by keeping the low bits of the source value:

var x: i64 = 1000
var y: i8 = x as i8    // truncates to low 8 bits: -24

var big: u32 = 0xDEADBEEF
var small: u8 = big as u8   // 0xEF

Truncation never panics. The as keyword is the programmer explicitly accepting potential data loss.

Float to integer

Float-to-integer casts truncate toward zero, matching C++ behavior. Out-of-range values saturate instead of producing undefined behavior:

var f: f64 = 3.9
var i: i32 = f as i32      // 3 (truncates toward zero)

var neg: f64 = -2.7
var n: i32 = neg as i32    // -2

var huge: f64 = 1.0e18
var s: i32 = huge as i32   // i32 max (2147483647) saturates

Integer to float

Integer-to-float casts may lose precision for large values but never fail:

var x: i64 = 9007199254740993   // 2^53 + 1
var f: f64 = x as f64           // rounded f64 cannot represent this exactly

Signed/unsigned conversion

Casting between signed and unsigned integers of the same width reinterprets the bit pattern:

var s: i8 = -1
var u: u8 = s as u8    // 255 (same bits, different interpretation)

Pointer Casts

Upcasting (derived to base)

Derived-to-base pointer conversion is implicit no as required:

class Animal { ... }
class Dog derives Animal { ... }

var dog = Dog("Rex", "Labrador")
var animal: *Animal = &dog   // implicit upcast

Downcasting (base to derived)

Base-to-derived casts come in two forms:

Asserting downcast panics if the runtime type does not match. The function must have the panic specifier or the cast must be inside a try block:

fn process(animal: *Animal) panic {
    var dog = animal as *Dog   // panics if animal is not a Dog
    dog->fetch()
}

Checked downcast returns a nullable pointer. &null if the runtime type does not match:

fn process(animal: *Animal) {
    var dog = animal as *Dog?   // null if animal is not a Dog
    if dog != &null {
        dog->fetch()
    }
}

Both forms perform a runtime type check using the vtable (the class must have at least one virtual method). Downcasting a non-polymorphic class is a compile error.

Raw pointer cast

Casting to unsafe *T reinterprets the pointer with no type checking equivalent to C++‘s reinterpret_cast. The compiler performs no validation.

Casting a safe pointer to a raw pointer *T as unsafe *U requires an unsafe block. This is the point where AMT loses provenance: after the cast the compiler can no longer relate the pointer to the allocation it came from, so the loss is made visible at the source:

var ptr: *i32 = &some_value

unsafe {
    var raw = ptr as unsafe *void    // provenance loss: safe pointer to raw
    var back = raw as unsafe *i32    // reinterpret caller must ensure correctness
}

Casting between unsafe *T types needs no unsafe block there is no provenance left to lose. The result is the same pointer value with a different type no runtime check, no adjustment.

Caution

Raw pointer casts bypass AMT’s safety guarantees. Casting to unsafe *void erases type information permanently the compiler cannot verify the correctness of a subsequent cast back. Use only for C/C++ interop, custom allocators, and other low-level scenarios.

Pointer to integer

Casting a pointer to an integer extracts the numeric address. This is safe and requires no unsafe block reading an address cannot violate memory safety on its own, and the resulting integer carries no provenance:

var ptr: *i32 = &some_value
var addr = ptr as usize    // ok: numeric address

usize is the only permitted target it is the one integer type guaranteed to hold a full address. Casting a pointer directly to a narrower integer is a compile error, not a warning: silently discarding the high bits of an address is never what the programmer meant.

// var truncated = ptr as u8       // compile error: cannot cast pointer to u8

var low = ptr as usize as u8       // ok: address, then explicit truncation

If you genuinely want the low byte of an address (tag bits, alignment checks), spell it as two casts. The second cast is an ordinary numeric narrowing and reads as deliberate.

Integer to pointer

Casting an integer to a pointer fabricates a pointer from a numeric address. The result must be an unsafe pointer safe pointers require provenance tracking that an integer cannot provide and the cast requires an unsafe block. The integer carries no provenance, so the cast invents one that AMT has no way to verify:

var addr: usize = 0x7FFE_0000_1000

unsafe {
    var ptr = addr as unsafe *i32   // ok: fabricating provenance
}

// var bad = addr as *i32           // compile error: cannot create safe pointer from integer

This is the inverse of ptr as usize, and the asymmetry is deliberate: discarding provenance is harmless, inventing it is not.

Pointer cast rules

Every pointer cast is classified by what it does to provenance the compiler’s knowledge of which allocation a pointer belongs to. Casts that discard provenance are safe; casts that create or fabricate it are not:

CastStatusProvenanceWhy
ptr as usizeAllowed, no unsafeDiscardedReading an address cannot cause UB on its own
ptr as u8 (any narrower int)Compile errorNarrowing an address is never intended write ptr as usize as u8
n as unsafe *TAllowed, requires unsafe blockFabricatedAMT cannot verify an address that came from an integer
n as *TCompile errorSafe pointers require provenance an integer cannot supply
*T as unsafe *UAllowed, requires unsafe blockLostThe provenance-loss point keep it visible
unsafe *T as unsafe *UAllowed, no unsafeAlready absentNothing left to lose
*Derived as *BaseImplicitPreservedSame allocation, adjusted offset
*Base as *DerivedAllowed, runtime checkPreservedVtable check, panics or yields null

The two error rows are deliberate refusals rather than warnings. ptr as u8 has an honest spelling (ptr as usize as u8) that says the same thing in two steps, and n as *T has no honest spelling at all a safe pointer’s guarantees cannot be reconstructed from a number.

unsafe blocks appear on exactly the rows where AMT stops being able to reason about the pointer. See Unsafe for the boundary model and AMT for what provenance tracking buys.


Enum Casts

Plain enums

Plain enums can be cast to their underlying integer type and back:

enum Direction derives u8 {
    North = 0,
    East = 1,
    South = 2,
    West = 3,
}

var raw = Direction::North as u8   // 0
var dir = 2u8 as Direction         // Direction::South

Casting an integer to an enum that has no matching discriminant is undefined behavior. The compiler does not insert a runtime check.

The cast target must match the underlying type. Direction::North as i32 requires the enum to be backed by i32, or an intermediate cast: Direction::North as u8 as i32.

ADT enums

ADT enums cannot be cast to integers. The discriminant tag is an internal implementation detail. If you need the tag value, expose it through an extend method:

enum <T> ParseResult {
    Success { value: T, consumed: i32 },
    Error   { message: string },
    EndOfInput,
}

// ParseResult::Success { ... } as u32   // compile error: ADT enums cannot be cast to integers

extend <T> ParseResult<T> {
    fn tag(const self) -> u32 {
        match self {
            case .Success { 0 }
            case .Error   { 1 }
            case .EndOfInput { 2 }
        }
    }
}

Nullable to Non-Nullable

Casting a nullable value T? to its underlying type T is a compile error:

var x: i32? = null
// var y = x as i32    // compile error: cannot cast i32? to i32
//                     // use `x ?? <default>` or `unwrap!(x)`

There is no collapsing cast in Kairo. A cast that quietly produced a default-constructed value on null would fabricate a value the program never computed which is precisely what the nullable system exists to prevent. The null case has to be answered, not erased, and the two honest answers already have syntax:

IntentWriteOn null
Supply a fallbackx ?? 0Yields the fallback
Assert non-nullunwrap!(x)Panics (requires panic / try)

Both read at the call site as a decision about null. x as i32 reads as a type conversion and hides one.

var count: i32? = lookup_count()

var n = count ?? 0            // explicit: absent means zero
var m = unwrap!(count)        // explicit: absent is a bug, panic

See Variables for the rest of the nullable operations (?., ??, unwrap!(), null checking).


User-Defined Conversions (op as)

Types can define custom conversions by overloading the op as operator:

class Temperature {
    var celsius: f64

    fn Temperature(self, c: f64) { self.celsius = c }

    fn op as(self) -> f64 {
        return self.celsius
    }

    fn op as(self) -> string {
        return f"{self.celsius}C"
    }
}

var temp = Temperature(100.0)
var f = temp as f64      // 100.0
var s = temp as string   // "100.0C"

op as can be overloaded for multiple target types. The compiler selects the overload based on the target type in the as expression. op as must take only self as a parameter and return the target type.

See Operators for the full operator overloading reference.


Cast Summary

CastSyntaxSafetyBehavior on failure
Numeric wideningImplicitSafeN/A
Numeric narrowingx as i8TruncatesLow bits kept
Float to intx as i32Truncates/saturatesSaturates on overflow
Int to floatx as f64May lose precisionRounded
Derived-to-base ptrImplicitSafeN/A
Base-to-derived ptr (asserting)ptr as *DerivedRuntime checkPanics
Base-to-derived ptr (checked)ptr as *Derived?Runtime checkReturns &null
Safe to raw pointerptr as unsafe *TRequires unsafe blockProvenance loss
Raw to raw pointerraw as unsafe *TNo checkReinterpret
Pointer to usizeptr as usizeSafeAddress value
Pointer to narrower intptr as u8Compile errorUse ptr as usize as u8
Integer to pointern as unsafe *TRequires unsafe blockFabricated provenance
Plain enum to inte as u8SafeDiscriminant value
Int to plain enumn as DirectionUB if no matchNo runtime check
ADT enum to intN/ACompile errorUse extend method
User-definedx as TargetTypeDepends on op asCalls user code
T to T?ImplicitSafeN/A
T? to TNot a castCompile errorUse ?? or unwrap!()

Casts Not in Kairo

C++ CastKairo Equivalent
static_cast<T>(x)x as T
dynamic_cast<T*>(p)p as *T? (checked) or p as *T (asserting)
reinterpret_cast<T*>(p)p as unsafe *T
const_cast<T*>(p)Not supported const cannot be stripped at runtime