///|
/// C Heap wrapper for GC objects
/// This wraps the C-managed GC heap that can be used by both
/// the interpreter and JIT-compiled code.
// ============================================================
// Error Types
// ============================================================
///|
/// CHeap error types for runtime errors
pub(all) suberror CHeapError {
OutOfBoundsArrayAccess
NullReference
OutOfMemory
}
// ============================================================
// Value Encoding Constants
// ============================================================
// Object kinds (must match GC_KIND_* in gc_heap.h)
///|
const GC_KIND_STRUCT : Int = 1
///|
const GC_KIND_ARRAY : Int = 2
// ============================================================
// CHeap Type
// ============================================================
///|
/// C-managed GC Heap
/// The heap is automatically freed when this object is garbage collected.
pub struct CHeap {
ptr : Int64 // C GcHeap* pointer
} derive(Debug)
///|
/// Create a new C heap with the given initial capacity
pub fn CHeap::CHeap(capacity? : Int = @types.ONE_MIB_BYTES) -> CHeap {
let ptr = c_gc_heap_new(capacity.to_int64())
{ ptr, }
}
///|
/// Free the C heap (called explicitly if needed before GC)
pub fn CHeap::free(self : CHeap) -> Unit {
c_gc_heap_free(self.ptr)
}
///|
/// Get the raw C pointer (for JIT)
pub fn CHeap::get_ptr(self : CHeap) -> Int64 {
self.ptr
}
///|
/// Return an owner-bound action that reclaims later allocations unless they
/// remain reachable when the action runs. Retained object-table indices are
/// never reused.
pub fn CHeap::make_allocation_rollback(
self : CHeap,
roots : () -> Array[@types.Value],
) -> () -> Unit {
let object_count = self.get_object_count()
fn() {
let current_roots = roots()
let num_roots = current_roots.length()
let raw_roots = FixedArray::makei(num_roots, fn(i) {
value_to_i64(current_roots[i])
})
c_gc_heap_rollback_allocations(self.ptr, object_count, raw_roots, num_roots)
|> ignore
}
}
// ============================================================
// Value Encoding/Decoding
// ============================================================
///|
/// One field or element as the C heap stores it.
///
/// `lo` is the runtime word. Every value kind except v128 is encoded entirely
/// into it, and it is the only word the collector scans for references, so a
/// reference must never be written to `hi`.
pub struct GcSlot {
lo : Int64
hi : Int64
} derive(Eq, Debug)
///|
/// Encode a Value into a C heap slot.
///
/// Total, and that is the point: a slot is 16 bytes, so there is no value
/// this cannot represent and no case left to abort on.
///
/// Encoding rules for `lo`:
/// - i32: sign-extended to i64
/// - i64: as-is
/// - f32: lower 32 bits (IEEE 754 bits)
/// - f64: as-is (IEEE 754 bits)
/// - v128: low 8 bytes, with the high 8 in `hi`
/// - structref/arrayref: (gc_ref) << 1, where gc_ref = idx + 1 (even, low bit = 0)
/// - funcref: function index + 1 (0 = null)
/// - externref: extern index + 1 (0 = null)
/// - exnref: exception index + 1 (0 = null)
/// - i31: (value << 1) | 1 (tagged, low bit = 1)
/// - null: 0
///
/// GC reference detection in gc_heap_mark uses: (lo & 1) == 0 && lo > 0
pub fn value_to_slot(value : @types.Value) -> GcSlot {
match value {
I32(n) => { lo: n.to_int64(), hi: 0L }
I64(n) => { lo: n, hi: 0L }
F32(f) => { lo: f.reinterpret_as_int().to_int64(), hi: 0L }
F64(d) => { lo: d.reinterpret_as_int64(), hi: 0L }
V128(bytes) =>
{
lo: @types.bytes_to_int64_le(bytes, 0),
hi: @types.bytes_to_int64_le(bytes, 8),
}
// gc_ref is 1-based: StructRef(0) -> gc_ref=1 -> encoded=2
StructRef(idx) => { lo: (idx + 1).to_int64() << 1, hi: 0L }
ArrayRef(idx) => { lo: (idx + 1).to_int64() << 1, hi: 0L }
FuncRef(idx) => { lo: (idx + 1).to_int64(), hi: 0L }
ExternRef(idx) => { lo: (idx + 1).to_int64(), hi: 0L }
ExnRef(idx) => { lo: (idx + 1).to_int64(), hi: 0L }
I31(n) => { lo: (n.to_int64() << 1) | 1L, hi: 0L }
Null => { lo: @wasm_milkir.NULL_REF, hi: 0L }
}
}
///|
/// Encode a Value as a bare runtime word, for the root array the collector
/// scans. A root's high word would never be read, so dropping it is not a
/// loss of information here.
pub fn value_to_i64(value : @types.Value) -> Int64 {
value_to_slot(value).lo
}
///|
/// Decode a runtime word to a Value based on expected type.
///
/// A word carries no high half, so a `V128` request yields a vector whose
/// upper 8 bytes are zero. Callers reading a stored field want
/// `slot_to_value`, which has the other half.
pub fn i64_to_value(raw : Int64, ty : @types.ValueType) -> @types.Value {
slot_to_value({ lo: raw, hi: 0L }, ty)
}
///|
/// Decode a C heap slot to a Value based on expected type.
///
/// Total, and inverse to `value_to_slot`: every `ValueType` names a value
/// this can produce, so no case has to fall back to a value of some other
/// type the way `V128 => I64(raw)` once did.
pub fn slot_to_value(slot : GcSlot, ty : @types.ValueType) -> @types.Value {
let raw = slot.lo
match ty {
I32 => I32(raw.to_int())
I64 => I64(raw)
F32 => F32(Float::reinterpret_from_int(raw.to_int()))
F64 => F64(raw.reinterpret_as_double())
RefStruct(_) | RefNullStruct(_) | StructRef | RefStructAbs =>
if raw == @wasm_milkir.NULL_REF {
Null
} else {
// Decode: gc_ref = raw >> 1, idx = gc_ref - 1
StructRef(((raw >> 1) - 1L).to_int())
}
RefArray(_) | RefNullArray(_) | ArrayRef | RefArrayAbs =>
if raw == @wasm_milkir.NULL_REF {
Null
} else {
// Decode: gc_ref = raw >> 1, idx = gc_ref - 1
ArrayRef(((raw >> 1) - 1L).to_int())
}
FuncRef | RefFunc | RefFuncTyped(_) | RefNullFuncTyped(_) =>
if raw == @wasm_milkir.NULL_REF {
Null
} else {
FuncRef((raw - 1L).to_int())
}
ExternRef | RefExtern =>
if raw == @wasm_milkir.NULL_REF {
Null
} else {
ExternRef((raw - 1L).to_int())
}
ExnRef =>
if raw == @wasm_milkir.NULL_REF {
Null
} else {
ExnRef((raw - 1L).to_int())
}
RefI31 | RefNullI31 =>
if raw == @wasm_milkir.NULL_REF {
Null
} else {
I31((raw >> 1).to_int())
}
RefEq | RefNullEq | AnyRef | RefAny =>
// For abstract types, we need to decode based on the actual value
if raw == @wasm_milkir.NULL_REF {
Null
} else if (raw & 1L) != 0L {
// Tagged i31 (odd)
I31((raw >> 1).to_int())
} else {
// GC reference (even, non-zero)
// Decode: gc_ref = raw >> 1, idx = gc_ref - 1
// We don't know if struct or array without querying the heap
StructRef(((raw >> 1) - 1L).to_int())
}
NullRef | NullFuncRef | NullExnRef | NullExternRef | RefNone => Null
V128 => V128(@types.int64_pair_to_v128_le(slot.lo, slot.hi))
}
}
///|
/// Flatten values into the [lo, hi] word pairs the wide C entry points take.
fn slots_to_words(values : Array[@types.Value]) -> FixedArray[Int64] {
FixedArray::makei(values.length() * 2, fn(i) {
let slot = value_to_slot(values[i / 2])
if i % 2 == 0 {
slot.lo
} else {
slot.hi
}
})
}
// ============================================================
// Struct Operations
// ============================================================
///|
/// Allocate a new struct
/// Returns gc_ref (0-based index for external use)
pub fn CHeap::alloc_struct(
self : CHeap,
type_idx : Int,
fields : Array[@types.Value],
) -> Int raise CHeapError {
let num_fields = fields.length()
let raw_fields = slots_to_words(fields)
let gc_ref = c_gc_heap_alloc_struct_wide(
self.ptr,
type_idx,
raw_fields,
num_fields,
)
if gc_ref <= 0 {
raise OutOfMemory
}
// C heap returns 1-based gc_ref, convert to 0-based for external use
gc_ref - 1
}
///|
/// Get a struct field value
pub fn CHeap::struct_get(
self : CHeap,
struct_idx : Int,
field_idx : Int,
field_type : @types.ValueType,
) -> @types.Value {
// Convert 0-based to 1-based for C heap
let gc_ref = struct_idx + 1
let out = FixedArray::make(2, 0L)
c_gc_heap_struct_get_wide(self.ptr, gc_ref, field_idx, out)
slot_to_value({ lo: out[0], hi: out[1] }, field_type)
}
///|
/// Set a struct field value
pub fn CHeap::struct_set(
self : CHeap,
struct_idx : Int,
field_idx : Int,
value : @types.Value,
) -> Unit {
let gc_ref = struct_idx + 1
let slot = value_to_slot(value)
c_gc_heap_struct_set_wide(self.ptr, gc_ref, field_idx, slot.lo, slot.hi)
}
// ============================================================
// Array Operations
// ============================================================
///|
/// Allocate a new array with initial value
/// Returns gc_ref (0-based index for external use)
pub fn CHeap::alloc_array(
self : CHeap,
type_idx : Int,
len : Int,
init_value : @types.Value,
) -> Int raise CHeapError {
let slot = value_to_slot(init_value)
let gc_ref = c_gc_heap_alloc_array_wide(
self.ptr,
type_idx,
len,
slot.lo,
slot.hi,
)
if gc_ref <= 0 {
raise OutOfMemory
}
gc_ref - 1
}
///|
/// Allocate a new array from existing values
/// Returns gc_ref (0-based index for external use)
pub fn CHeap::alloc_array_from_values(
self : CHeap,
type_idx : Int,
elements : Array[@types.Value],
) -> Int raise CHeapError {
let len = elements.length()
let raw_values = slots_to_words(elements)
let gc_ref = c_gc_heap_alloc_array_from_slots(
self.ptr,
type_idx,
raw_values,
len,
)
if gc_ref <= 0 {
raise OutOfMemory
}
gc_ref - 1
}
///|
/// Get array length
pub fn CHeap::array_len(self : CHeap, array_idx : Int) -> Int {
let gc_ref = array_idx + 1
c_gc_heap_array_len(self.ptr, gc_ref)
}
///|
/// Get an array element
pub fn CHeap::array_get(
self : CHeap,
array_idx : Int,
elem_idx : Int,
elem_type : @types.ValueType,
) -> @types.Value raise CHeapError {
let gc_ref = array_idx + 1
// Check bounds before accessing
let len = c_gc_heap_array_len(self.ptr, gc_ref)
if elem_idx < 0 || elem_idx >= len {
raise OutOfBoundsArrayAccess
}
let out = FixedArray::make(2, 0L)
c_gc_heap_array_get_wide(self.ptr, gc_ref, elem_idx, out)
slot_to_value({ lo: out[0], hi: out[1] }, elem_type)
}
///|
/// Set an array element
pub fn CHeap::array_set(
self : CHeap,
array_idx : Int,
elem_idx : Int,
value : @types.Value,
) -> Unit raise CHeapError {
let gc_ref = array_idx + 1
// Check bounds before accessing
let len = c_gc_heap_array_len(self.ptr, gc_ref)
if elem_idx < 0 || elem_idx >= len {
raise OutOfBoundsArrayAccess
}
let slot = value_to_slot(value)
c_gc_heap_array_set_wide(self.ptr, gc_ref, elem_idx, slot.lo, slot.hi)
}
///|
/// Fill array elements with a value
pub fn CHeap::array_fill(
self : CHeap,
array_idx : Int,
offset : Int,
value : @types.Value,
count : Int,
) -> Unit raise CHeapError {
let gc_ref = array_idx + 1
// Check bounds before filling
let len = c_gc_heap_array_len(self.ptr, gc_ref)
if offset < 0 || count < 0 || offset + count > len {
raise OutOfBoundsArrayAccess
}
let slot = value_to_slot(value)
c_gc_heap_array_fill_wide(self.ptr, gc_ref, offset, slot.lo, slot.hi, count)
}
///|
/// Copy array elements
pub fn CHeap::array_copy(
self : CHeap,
dst_idx : Int,
dst_offset : Int,
src_idx : Int,
src_offset : Int,
count : Int,
) -> Unit raise CHeapError {
let dst_ref = dst_idx + 1
let src_ref = src_idx + 1
// Check bounds before copying
let dst_len = c_gc_heap_array_len(self.ptr, dst_ref)
let src_len = c_gc_heap_array_len(self.ptr, src_ref)
if dst_offset < 0 ||
src_offset < 0 ||
count < 0 ||
dst_offset + count > dst_len ||
src_offset + count > src_len {
raise OutOfBoundsArrayAccess
}
c_gc_heap_array_copy(
self.ptr,
dst_ref,
dst_offset,
src_ref,
src_offset,
count,
)
}
// ============================================================
// Type Information
// ============================================================
///|
/// Get the type index of an object
pub fn CHeap::get_type_idx(self : CHeap, idx : Int) -> Int {
let gc_ref = idx + 1
c_gc_heap_get_type_idx(self.ptr, gc_ref)
}
///|
/// Get the kind of an object (1=struct, 2=array)
pub fn CHeap::get_kind(self : CHeap, idx : Int) -> Int {
let gc_ref = idx + 1
c_gc_heap_get_kind(self.ptr, gc_ref)
}
///|
/// Check if this is a struct
pub fn CHeap::is_struct(self : CHeap, idx : Int) -> Bool {
self.get_kind(idx) == GC_KIND_STRUCT
}
///|
/// Check if this is an array
pub fn CHeap::is_array(self : CHeap, idx : Int) -> Bool {
self.get_kind(idx) == GC_KIND_ARRAY
}
///|
/// Check if an object reference is valid
pub fn CHeap::is_valid(self : CHeap, idx : Int) -> Bool {
let gc_ref = idx + 1
c_gc_heap_is_valid(self.ptr, gc_ref) != 0
}
// ============================================================
// GC Operations
// ============================================================
///|
/// Perform garbage collection with given roots
/// Returns the number of objects collected
pub fn CHeap::collect(self : CHeap, roots : Array[@types.Value]) -> Int {
let num_roots = roots.length()
let raw_roots = FixedArray::makei(num_roots, fn(i) { value_to_i64(roots[i]) })
c_gc_heap_collect(self.ptr, raw_roots, num_roots)
}
///|
/// Verify heap invariants (for debugging)
pub fn CHeap::verify(self : CHeap, verbose? : Bool = false) -> Bool {
c_gc_heap_verify(self.ptr, if verbose { 1 } else { 0 }) != 0
}
// ============================================================
// JIT Utilities
// ============================================================
///|
/// Get heap base pointer (for JIT inline access)
pub fn CHeap::get_base(self : CHeap) -> Int64 {
c_gc_heap_get_base(self.ptr)
}
///|
/// Get object offset in heap (for JIT inline access)
pub fn CHeap::get_offset(self : CHeap, idx : Int) -> Int {
let gc_ref = idx + 1
c_gc_heap_get_offset(self.ptr, gc_ref)
}
// ============================================================
// GC Statistics
// ============================================================
///|
/// Get current heap size (bytes used)
pub fn CHeap::get_size(self : CHeap) -> Int64 {
c_gc_heap_get_size(self.ptr)
}
///|
/// Get heap capacity (total allocated bytes)
pub fn CHeap::get_capacity(self : CHeap) -> Int64 {
c_gc_heap_get_capacity(self.ptr)
}
///|
/// Get heap usage ratio (0.0 to 1.0)
pub fn CHeap::get_usage_ratio(self : CHeap) -> Double {
let size = self.get_size()
let capacity = self.get_capacity()
if capacity == 0L {
0.0
} else {
size.to_double() / capacity.to_double()
}
}
///|
/// Get number of objects in heap
pub fn CHeap::get_object_count(self : CHeap) -> Int {
c_gc_heap_get_object_count(self.ptr)
}
///|
/// Get total number of write-barrier calls recorded
pub fn CHeap::get_barrier_writes(self : CHeap) -> Int {
c_gc_heap_get_barrier_writes(self.ptr)
}
///|
/// Get total number of allocations since heap creation
pub fn CHeap::get_total_allocations(self : CHeap) -> Int {
c_gc_heap_get_total_allocations(self.ptr)
}
///|
/// Get total number of GC cycles performed
pub fn CHeap::get_total_collections(self : CHeap) -> Int {
c_gc_heap_get_total_collections(self.ptr)
}
///|
/// Check if GC should be triggered based on heap usage
/// Default threshold is 75% of capacity
pub fn CHeap::should_collect(self : CHeap, threshold? : Double = 0.75) -> Bool {
self.get_usage_ratio() >= threshold
}
///|
/// Configure allocation fault injection for debugging
pub fn gc_debug_set_fail_alloc(fail_at : Int, fail_every? : Int = 0) -> Unit {
c_gc_heap_debug_set_fail_alloc(fail_at, fail_every)
}