Skip to main content

tidepool_codegen/
alloc.rs

1use cranelift_codegen::ir::{self, types, BlockArg, InstBuilder, MemFlags, Value};
2use cranelift_frontend::FunctionBuilder;
3
4use crate::layout::*;
5
6/// Emit the alloc fast-path as inline Cranelift IR.
7///
8/// This is a bump-pointer allocation:
9/// 1. Load alloc_ptr from VMContext
10/// 2. new_ptr = alloc_ptr + size (8-byte aligned)
11/// 3. If new_ptr > alloc_limit: call gc_trigger (cold path), then retry
12/// 4. Store new_ptr as alloc_ptr
13/// 5. Return old alloc_ptr (start of allocated region)
14///
15/// `vmctx_val` is the SSA value holding the VMContext pointer.
16/// `size` is the number of bytes to allocate (will be rounded up to 8-byte alignment).
17/// `gc_trigger_sig` is the signature reference for the gc_trigger call.
18///
19/// Returns the SSA value pointing to the start of the allocated memory.
20pub fn emit_alloc_fast_path(
21    builder: &mut FunctionBuilder,
22    vmctx_val: Value,
23    size: u64,
24    gc_trigger_sig: ir::SigRef,
25    oom_func: ir::FuncRef,
26) -> Value {
27    let aligned_size = (size + 7) & !7;
28    // SAFETY: We use `MemFlags::trusted()` because all VMContext accesses here are
29    // guaranteed safe by construction:
30    // - `vmctx_val` is the first parameter to this function and is always a valid
31    //   pointer to VMContext, provided by the embedding runtime under a fixed ABI.
32    // - VMContext is properly aligned, and the field offsets used below
33    //   (`VMCTX_ALLOC_PTR_OFFSET`, `VMCTX_ALLOC_LIMIT_OFFSET`,
34    //   `VMCTX_GC_TRIGGER_OFFSET`) correspond to a frozen layout that is
35    //   validated via const assertions.
36    // - All loads/stores are 64-bit and respect the alignment and bounds of these fields.
37    let flags = MemFlags::trusted();
38
39    // Load current alloc_ptr
40    let alloc_ptr = builder
41        .ins()
42        .load(types::I64, flags, vmctx_val, VMCTX_ALLOC_PTR_OFFSET);
43
44    // Compute new_ptr = alloc_ptr + aligned_size
45    let size_val = builder.ins().iconst(types::I64, aligned_size as i64);
46    let new_ptr = builder.ins().iadd(alloc_ptr, size_val);
47
48    // Load alloc_limit
49    let alloc_limit = builder
50        .ins()
51        .load(types::I64, flags, vmctx_val, VMCTX_ALLOC_LIMIT_OFFSET);
52
53    // Compare: new_ptr > alloc_limit
54    let overflow = builder.ins().icmp(
55        ir::condcodes::IntCC::UnsignedGreaterThan,
56        new_ptr,
57        alloc_limit,
58    );
59
60    let slow_block = builder.create_block();
61    let fast_store_block = builder.create_block();
62    let continue_block = builder.create_block();
63    builder.append_block_param(continue_block, types::I64); // result ptr
64
65    builder
66        .ins()
67        .brif(overflow, slow_block, &[], fast_store_block, &[]);
68
69    // --- Fast path: store new_ptr, jump to continue with old alloc_ptr ---
70    builder.switch_to_block(fast_store_block);
71    builder.seal_block(fast_store_block);
72    builder
73        .ins()
74        .store(flags, new_ptr, vmctx_val, VMCTX_ALLOC_PTR_OFFSET);
75    builder
76        .ins()
77        .jump(continue_block, &[BlockArg::Value(alloc_ptr)]);
78
79    // --- Slow path: call gc_trigger, retry alloc ---
80    builder.switch_to_block(slow_block);
81    builder.seal_block(slow_block);
82
83    let gc_trigger_ptr = builder
84        .ins()
85        .load(types::I64, flags, vmctx_val, VMCTX_GC_TRIGGER_OFFSET);
86    builder
87        .ins()
88        .call_indirect(gc_trigger_sig, gc_trigger_ptr, &[vmctx_val]);
89
90    // After GC: reload alloc_ptr and alloc_limit, bump, check, then store or trap.
91    let post_gc_ptr = builder
92        .ins()
93        .load(types::I64, flags, vmctx_val, VMCTX_ALLOC_PTR_OFFSET);
94    let post_gc_limit = builder
95        .ins()
96        .load(types::I64, flags, vmctx_val, VMCTX_ALLOC_LIMIT_OFFSET);
97    let post_gc_new = builder.ins().iadd(post_gc_ptr, size_val);
98    let post_gc_overflow = builder.ins().icmp(
99        ir::condcodes::IntCC::UnsignedGreaterThan,
100        post_gc_new,
101        post_gc_limit,
102    );
103
104    let slow_fail_block = builder.create_block();
105    let slow_store_block = builder.create_block();
106
107    builder.ins().brif(
108        post_gc_overflow,
109        slow_fail_block,
110        &[],
111        slow_store_block,
112        &[],
113    );
114
115    // Slow path success: store new alloc_ptr and continue.
116    builder.switch_to_block(slow_store_block);
117    builder.seal_block(slow_store_block);
118    builder
119        .ins()
120        .store(flags, post_gc_new, vmctx_val, VMCTX_ALLOC_PTR_OFFSET);
121    builder
122        .ins()
123        .jump(continue_block, &[BlockArg::Value(post_gc_ptr)]);
124
125    // Slow path failure: call runtime_oom instead of trapping
126    builder.switch_to_block(slow_fail_block);
127    builder.seal_block(slow_fail_block);
128    let oom_result = builder.ins().call(oom_func, &[]);
129    let poison_ptr = builder.inst_results(oom_result)[0];
130    builder
131        .ins()
132        .jump(continue_block, &[BlockArg::Value(poison_ptr)]);
133
134    // --- Continue: result is the old alloc_ptr from whichever path ---
135    builder.switch_to_block(continue_block);
136    builder.seal_block(continue_block);
137
138    builder.block_params(continue_block)[0]
139}