Compiler Intrinsics

Compiler Intrinsics

Compiler intrinsics are low-level functions built directly into the Sun compiler. They provide direct access to memory operations, type information, and other primitives that cannot be expressed in regular Sun code.

⚠️

Intrinsics are unsafe operations intended for implementing standard library primitives like Vec<T> and Map<K,V>. Most user code should use the standard library instead.

Generic Intrinsics

These intrinsics require a type parameter <T>.

_sizeof<T>()

Returns the byte size of type T as i64.

var size = _sizeof<i32>();   // 4
var size2 = _sizeof<i64>();  // 8
 
class Point { x: f64; y: f64; }
var pointSize = _sizeof<Point>();  // 16 (two 8-byte floats)

_init<T>(ptr, args...)

Constructs an instance of type T at the memory location pointed to by ptr, forwarding args to the constructor.

// Allocate raw memory for a Point
var mem = _malloc(_sizeof<Point>());
 
// Construct Point at that location
_init<Point>(mem, 3.0, 4.0);
 
// mem now points to an initialized Point

For non-class types, _init is a no-op since primitives don't have constructors.

_load<T>(ptr, index)

Loads an element of type T from ptr at the given element index. Equivalent to ptr[index] in C.

// Assuming data points to an array of i32
var value = _load<i32>(data, 5);  // Load data[5]

_store<T>(ptr, index, value)

Stores value of type T at ptr[index].

// Store 42 at data[5]
_store<i32>(data, 5, 42);

_ptr_as_raw<T>(ptr<T>)

Converts an owning ptr<T> to a non-owning raw_ptr<T> without transferring ownership. Similar to C++'s unique_ptr::get().

var owned: ptr<Point> = new Point(1.0, 2.0);
var raw: raw_ptr<Point> = _ptr_as_raw<Point>(owned);
// raw points to the same memory, but owned still manages lifetime
⚠️

Be careful: the raw pointer becomes invalid if the owning pointer is freed or goes out of scope.

_is<T>(value)

Compile-time type check that returns true or false based on whether the type of value matches T. This intrinsic is always resolved at compile time — no runtime overhead.

T can be:

  • A concrete type (e.g., i32, f64, Point) — exact type match
  • A type trait — checks type category (see below)
  • An interface — checks if a class implements the interface
function example(x: i32, y: f64) {
    _is<i32>(x);      // true - exact type match
    _is<i64>(x);      // false - i32 is not i64
    _is<_Integer>(x); // true - i32 is an integer
    _is<_Float>(y);   // true - f64 is a float
}

Type Traits

Type traits are pseudo-types that categorize primitives:

TraitMatches
_Integeri8, i16, i32, i64, u8, u16, u32, u64
_Signedi8, i16, i32, i64
_Unsignedu8, u16, u32, u64
_Floatf32, f64
_NumericAll integers and floats
_PrimitiveAll numeric types plus bool

Use in Generic Code

_is<T> is particularly useful in generic functions to branch based on type:

function processValue<T>(x: T) i32 {
    if (_is<_Integer>(x)) {
        // Integer-specific logic
        return 1;
    }
    if (_is<_Float>(x)) {
        // Float-specific logic
        return 2;
    }
    return 0;
}
 
function main() i32 {
    var a = processValue<i32>(42);    // Returns 1
    var b = processValue<f64>(3.14);  // Returns 2
    return a + b;  // 3
}

Since Sun uses monomorphization, each instantiation of processValue compiles to code with the dead branches eliminated by LLVM.

Interface Checks

For class types, _is<T> can check interface implementation:

interface IHashable {
    function hash() i64;
}
 
class MyKey implements IHashable {
    function hash() i64 { return 42; }
}
 
function example(key: MyKey) bool {
    return _is<IHashable>(key);  // true
}

_convert<T>(value)

Converts a numeric value to another numeric type T, unchecked. Integers truncate or extend (sign-extending from a signed source, zero-extending from an unsigned one); integer-to-float and float-to-integer convert by value, dropping any fraction; f32/f64 convert between each other. This is the only way to narrow an integer — an assignment never does it on its own.

var ms: i64 = 200;
var t: i32 = _convert<i32>(ms);     // 200
var f: f64 = _convert<f64>(t);      // 200.0
var b: u8 = _convert<u8>(300);      // 44 — the high bits are dropped

Use the stdlib's safe_convert<T>(value) when the value may not fit; it throws ConversionError instead of wrapping (see Standard Library).

_bitcast<T>(value)

Reinterprets the bits of a numeric value as another numeric type of the same size — f32u32/i32, f64u64/i64. The sizes must match, which is checked at compile time. Used by binary wire formats to read and write floats.

var bits: u64 = _bitcast<u64>(1.0);   // 0x3FF0000000000000
var back: f64 = _bitcast<f64>(bits);  // 1.0

Non-Generic Intrinsics

These intrinsics operate on specific, fixed types.

_malloc(size)

Allocates size bytes of heap memory and returns a raw_ptr<i8>. This is a direct wrapper around the C library's malloc.

var mem = _malloc(1024);  // Allocate 1024 bytes
🚫

Prefer using allocators. Direct _malloc calls bypass Sun's memory management. Use HeapAllocator.alloc_raw(size) in standard library code.

_free(ptr)

Frees memory previously allocated with _malloc. Direct wrapper around C's free.

var mem = _malloc(1024);
// ... use memory ...
_free(mem);

_address_of<T>(ref T)

Returns a raw_ptr<T> pointing to the memory location of a reference. Useful for interfacing with low-level operations that require a pointer (e.g., atomics).

var x: i32 = 42;
var ptr = _address_of<i32>(x);  // raw_ptr<i32> to x's memory
⚠️

The returned pointer is only valid while the referenced variable is in scope. Do not store it beyond the variable's lifetime.

_to_ref<T>(raw_ptr<T>)

The reverse of _address_of: turns a raw_ptr<T> into a ref T so the pointed-to value can be read, borrowed or assigned through like any reference. Must be used inside unsafe { }. The borrow checker gives the reference the lifetime of the pointer, so the pointer must stay valid for as long as the reference is used.

var x: i32 = 1;
var p: raw_ptr<i32> = _address_of<i32>(x);
var r: ref i32 = unsafe { _to_ref<i32>(p); };
r = 2;   // x is now 2

_load_i64(ptr, index)

Loads an i64 from ptr at element offset index. A non-generic version of _load<i64>.

var value = _load_i64(data, 0);  // Load first i64

_store_i64(ptr, index, value)

Stores an i64 value at ptr[index]. A non-generic version of _store<i64>.

_store_i64(data, 0, 42);  // Store 42 at first position

Print Intrinsics

These intrinsics provide low-level output capabilities. They are used internally by the standard library's print.sun module.

_print_i32(value)

Prints an i32 value to stdout (no newline).

_print_i64(value)

Prints an i64 value to stdout (no newline).

_print_f64(value)

Prints an f64 value to stdout (no newline).

_print_newline()

Prints a newline character to stdout.

_print_bytes(ptr, len)

Prints len bytes from the memory at ptr to stdout.

var s: static_ptr<u8> = "hello";
_print_bytes(s.raw(), s.length());  // Prints "hello"

The pointer and length of a static_ptr<T> come from its raw() and length() methods, not from an intrinsic (see Builtin Types).

_println_str(str)

Prints a static_ptr<u8> string followed by a newline.

_println_str("Hello, world!");

Atomic Intrinsics

Atomic intrinsics provide lock-free thread-safe operations. Used internally by Mutex and other synchronization primitives.

_atomic_cmpxchg_i32(ptr, expected, desired)

Atomic compare-and-exchange on an i32. If the value at ptr equals expected, it is replaced with desired. Returns the old value at ptr.

var state: i32 = 0;
var old = _atomic_cmpxchg_i32(_address_of<i32>(state), 0, 1);
// If state was 0, it's now 1. old == 0

_atomic_store_i32(ptr, value)

Atomically stores value at the memory location ptr.

_atomic_store_i32(_address_of<i32>(state), 0);  // Reset to 0

_atomic_load_i32(ptr)

Atomically loads the i32 value at ptr.

var current = _atomic_load_i32(_address_of<i32>(state));

Bit Intrinsics

Wide multiplication and zero counts, the building blocks of multi-limb integer arithmetic (BigUint) and table-driven float conversion.

_mul_hi_u64(a, b)

The high 64 bits of the 128-bit product a * b (the low 64 bits are just a * b).

var lo: u64 = a * b;
var hi: u64 = _mul_hi_u64(a, b);

_ctlz_u64(x) / _cttz_u64(x)

Number of leading / trailing zero bits in a u64; both return 64 for 0.

var bits: u64 = 64 - _ctlz_u64(x);   // bit length of x

Futex Intrinsics

Linux-specific thread synchronization primitives. Used by Mutex for efficient blocking.

_futex_wait(ptr, expected)

Blocks the calling thread if the value at ptr equals expected. The thread sleeps until woken by _futex_wake.

_futex_wait(_address_of<i32>(state), 2);  // Sleep if state == 2

_futex_wake(ptr)

Wakes one thread blocked on _futex_wait at the given address.

_futex_wake(_address_of<i32>(state));  // Wake one waiter
⚠️

Atomic and futex intrinsics are platform-specific (Linux) and intended for implementing synchronization primitives. Use Mutex from the standard library instead.

Usage in Standard Library

The standard library uses intrinsics to implement generic containers. Here's a simplified example of how Vec<T> might use them:

class Vec<T> {
    data: raw_ptr<i8>;
    len: i64;
    cap: i64;
    alloc: HeapAllocator;
 
    function get(index: i64) T {
        return _load<T>(this.data, index);
    }
 
    function set(index: i64, value: T) void {
        _store<T>(this.data, index, value);
    }
 
    function push(value: T) void {
        if (this.len >= this.cap) {
            this.grow();
        }
        _store<T>(this.data, this.len, value);
        this.len = this.len + 1;
    }
 
    function grow() void {
        var newCap = this.cap * 2;
        var newData = this.alloc.alloc_raw(newCap * _sizeof<T>());
        // Copy old data to new buffer...
        _free(this.data);
        this.data = newData;
        this.cap = newCap;
    }
}

Summary Table

IntrinsicParametersReturnsDescription
_sizeof<T>nonei64Byte size of type T
_init<T>ptr, args...voidConstruct T at ptr
_load<T>ptr, indexTLoad element at index
_store<T>ptr, index, valuevoidStore element at index
_ptr_as_raw<T>ptr<T>raw_ptr<T>Get raw pointer without ownership transfer
_is<T>valueboolCompile-time type check
_convert<T>valueTUnchecked numeric conversion (truncate, extend, int ↔ float)
_bitcast<T>valueTReinterpret bits as a same-size numeric type
_address_of<T>ref Traw_ptr<T>Get pointer to referenced memory
_to_ref<T>raw_ptr<T>ref TReference the pointed-to value (unsafe)
_mallocsize: i64raw_ptr<i8>Allocate heap memory
_freeptrvoidFree heap memory
_load_i64ptr, indexi64Load i64 at index
_store_i64ptr, index, valuevoidStore i64 at index
_print_i32value: i32voidPrint i32 to stdout
_print_i64value: i64voidPrint i64 to stdout
_print_f64value: f64voidPrint f64 to stdout
_print_newlinenonevoidPrint newline
_print_bytesptr, lenvoidPrint bytes to stdout
_println_strstatic_ptr<u8>voidPrint string with newline
_atomic_cmpxchg_i32ptr, expected, desiredi32Atomic compare-and-exchange
_atomic_store_i32ptr, valuevoidAtomic store
_atomic_load_i32ptri32Atomic load
_mul_hi_u64a: u64, b: u64u64High 64 bits of the 128-bit product
_ctlz_u64x: u64u64Leading zero bits (64 for 0)
_cttz_u64x: u64u64Trailing zero bits (64 for 0)
_futex_waitptr, expectedvoidBlock until woken
_futex_wakeptrvoidWake one blocked thread