use std::collections::HashMap;
use inkwell::builder::Builder;
use inkwell::context::Context;
use inkwell::module::{Linkage, Module as LlvmModule};
use inkwell::types::{BasicMetadataTypeEnum, BasicTypeEnum};
use inkwell::values::{BasicValue, BasicValueEnum, FunctionValue, IntValue, PointerValue};
use inkwell::{AddressSpace, IntPredicate};
use relon_ir::ir::{Func, IrType, Module as IrModule, Op, TaggedOp};
use crate::error::LlvmError;
use crate::state::{ARENA_STATE_OFFSET_BASE, ARENA_STATE_OFFSET_TAIL_CURSOR};
mod arith;
mod call;
mod closure;
mod collections;
mod control;
mod mem;
mod schema;
mod string;
mod unicode;
use arith::BinOp;
use mem::{AbsLoad, AbsStore};
pub(crate) const ENTRY_SYMBOL: &str = "relon_llvm_entry";
fn mark_invariant_load(ctx: &Context, loaded: BasicValueEnum<'_>) {
if let Some(inst) = loaded.as_instruction_value() {
let kind_id = ctx.get_kind_id("invariant.load");
let empty = ctx.metadata_node(&[]);
let _ = inst.set_metadata(empty, kind_id);
}
}
pub(crate) const ENTRY_SYMBOL_FAST: &str = "relon_llvm_entry_fast";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EntryShape {
LegacyI64,
Buffer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WorldMode {
#[default]
OpenWorld,
ClosedWorld,
}
#[derive(Debug, Clone)]
pub(crate) struct FastPathProfile {
pub(crate) arg_offsets: Vec<u32>,
pub(crate) ret_offset: u32,
}
#[derive(Debug, Default, Clone)]
pub struct ConstPool {
pub string_offsets: std::collections::HashMap<u32, u32>,
pub list_int_offsets: std::collections::HashMap<u32, u32>,
pub list_float_offsets: std::collections::HashMap<u32, u32>,
pub list_bool_offsets: std::collections::HashMap<u32, u32>,
pub list_string_offsets: std::collections::HashMap<u32, u32>,
pub dict_offsets: std::collections::HashMap<u32, u32>,
pub(crate) unicode_table_offsets: std::collections::HashMap<unicode::UnicodeTable, u32>,
pub bytes: Vec<u8>,
}
impl ConstPool {
pub fn from_module(module: &IrModule) -> Result<Self, LlvmError> {
let mut pool = ConstPool::default();
for func in &module.funcs {
pool.collect_body(&func.body)?;
}
Ok(pool)
}
fn collect_body(&mut self, body: &[TaggedOp]) -> Result<(), LlvmError> {
for tagged in body {
self.collect_op(&tagged.op)?;
}
Ok(())
}
fn collect_op(&mut self, op: &Op) -> Result<(), LlvmError> {
match op {
Op::ConstString { idx, value } => self.add_string(*idx, value),
Op::ConstListInt { idx, elements } => self.add_list_int(*idx, elements),
Op::ConstListFloat { idx, elements } => self.add_list_float(*idx, elements),
Op::ConstListBool { idx, elements } => self.add_list_bool(*idx, elements),
Op::ConstListString { idx, elements } => self.add_list_string(*idx, elements),
Op::ConstDict { idx, entries } => self.add_dict(*idx, entries),
Op::Block { body, .. } | Op::Loop { body, .. } => self.collect_body(body),
Op::If {
then_body,
else_body,
..
} => {
self.collect_body(then_body)?;
self.collect_body(else_body)
}
Op::Call { fn_index, .. } => {
let stdlib = relon_ir::stdlib::builtin_stdlib();
if let Some(callee) = stdlib.get(*fn_index as usize) {
let body = callee.body_owned();
self.collect_body(&body)?;
}
Ok(())
}
other => {
if let Some(table) = unicode::UnicodeTable::from_op(other) {
self.add_unicode_table(table)?;
}
Ok(())
}
}
}
fn add_unicode_table(&mut self, table: unicode::UnicodeTable) -> Result<(), LlvmError> {
if self.unicode_table_offsets.contains_key(&table) {
return Ok(());
}
self.align_to(4);
let off = u32::try_from(self.bytes.len())
.map_err(|_| LlvmError::Codegen("llvm const pool exceeds u32 range".into()))?;
let bytes = table.encode_bytes();
self.bytes.extend_from_slice(&bytes);
self.unicode_table_offsets.insert(table, off);
Ok(())
}
fn add_string(&mut self, idx: u32, value: &str) -> Result<(), LlvmError> {
if self.string_offsets.contains_key(&idx) {
return Ok(());
}
self.align_to(4);
let off = u32::try_from(self.bytes.len())
.map_err(|_| LlvmError::Codegen("llvm const pool exceeds u32 range".into()))?;
let len = u32::try_from(value.len())
.map_err(|_| LlvmError::Codegen("ConstString length exceeds u32 range".into()))?;
self.bytes.extend_from_slice(&len.to_le_bytes());
self.bytes.extend_from_slice(value.as_bytes());
self.string_offsets.insert(idx, off);
Ok(())
}
fn align_to(&mut self, align: usize) {
let rem = self.bytes.len() % align;
if rem != 0 {
self.bytes.resize(self.bytes.len() + (align - rem), 0);
}
}
fn add_list_int(&mut self, idx: u32, elements: &[i64]) -> Result<(), LlvmError> {
if self.list_int_offsets.contains_key(&idx) {
return Ok(());
}
self.align_to(8);
let off = u32::try_from(self.bytes.len())
.map_err(|_| LlvmError::Codegen("llvm const pool exceeds u32 range".into()))?;
let len = u32::try_from(elements.len())
.map_err(|_| LlvmError::Codegen("ConstListInt length exceeds u32 range".into()))?;
self.bytes.extend_from_slice(&len.to_le_bytes());
self.bytes.extend_from_slice(&[0u8; 4]); for e in elements {
self.bytes.extend_from_slice(&e.to_le_bytes());
}
self.list_int_offsets.insert(idx, off);
Ok(())
}
fn add_list_float(&mut self, idx: u32, elements: &[u64]) -> Result<(), LlvmError> {
if self.list_float_offsets.contains_key(&idx) {
return Ok(());
}
self.align_to(8);
let off = u32::try_from(self.bytes.len())
.map_err(|_| LlvmError::Codegen("llvm const pool exceeds u32 range".into()))?;
let len = u32::try_from(elements.len())
.map_err(|_| LlvmError::Codegen("ConstListFloat length exceeds u32 range".into()))?;
self.bytes.extend_from_slice(&len.to_le_bytes());
self.bytes.extend_from_slice(&[0u8; 4]); for e in elements {
self.bytes.extend_from_slice(&e.to_le_bytes());
}
self.list_float_offsets.insert(idx, off);
Ok(())
}
fn add_list_bool(&mut self, idx: u32, elements: &[bool]) -> Result<(), LlvmError> {
if self.list_bool_offsets.contains_key(&idx) {
return Ok(());
}
self.align_to(4);
let off = u32::try_from(self.bytes.len())
.map_err(|_| LlvmError::Codegen("llvm const pool exceeds u32 range".into()))?;
let len = u32::try_from(elements.len())
.map_err(|_| LlvmError::Codegen("ConstListBool length exceeds u32 range".into()))?;
self.bytes.extend_from_slice(&len.to_le_bytes());
for e in elements {
self.bytes.push(if *e { 1 } else { 0 });
}
self.list_bool_offsets.insert(idx, off);
Ok(())
}
fn add_list_string(&mut self, idx: u32, elements: &[String]) -> Result<(), LlvmError> {
if self.list_string_offsets.contains_key(&idx) {
return Ok(());
}
self.align_to(4);
let mut str_offsets: Vec<u32> = Vec::with_capacity(elements.len());
for s in elements {
self.align_to(4);
let s_off = u32::try_from(self.bytes.len()).map_err(|_| {
LlvmError::Codegen("ConstListString string offset exceeds u32".into())
})?;
let slen = u32::try_from(s.len()).map_err(|_| {
LlvmError::Codegen("ConstListString element length exceeds u32".into())
})?;
self.bytes.extend_from_slice(&slen.to_le_bytes());
self.bytes.extend_from_slice(s.as_bytes());
str_offsets.push(s_off);
}
self.align_to(4);
let header_off = u32::try_from(self.bytes.len())
.map_err(|_| LlvmError::Codegen("ConstListString header offset exceeds u32".into()))?;
let len = u32::try_from(elements.len())
.map_err(|_| LlvmError::Codegen("ConstListString length exceeds u32".into()))?;
self.bytes.extend_from_slice(&len.to_le_bytes());
for off in &str_offsets {
self.bytes.extend_from_slice(&off.to_le_bytes());
}
self.list_string_offsets.insert(idx, header_off);
Ok(())
}
fn add_dict(&mut self, idx: u32, entries: &[(String, i64)]) -> Result<(), LlvmError> {
if self.dict_offsets.contains_key(&idx) {
return Ok(());
}
self.align_to(8);
let off = u32::try_from(self.bytes.len())
.map_err(|_| LlvmError::Codegen("llvm const pool exceeds u32 range".into()))?;
let mut sorted: Vec<&(String, i64)> = entries.iter().collect();
sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
let entry_count = u32::try_from(sorted.len())
.map_err(|_| LlvmError::Codegen("ConstDict entry count exceeds u32".into()))?;
let shape_hash =
relon_ir::shape_hash::shape_hash_for_keys(sorted.iter().map(|(k, _)| k.as_str()));
self.bytes.extend_from_slice(&entry_count.to_le_bytes());
self.bytes.extend_from_slice(&[0u8; 4]); self.bytes.extend_from_slice(&shape_hash.to_le_bytes());
const HEADER_BYTES: u32 = 16;
const ENTRY_BYTES: u32 = 16;
let table_bytes = entry_count
.checked_mul(ENTRY_BYTES)
.ok_or_else(|| LlvmError::Codegen("ConstDict table size overflow".into()))?;
let key_payload_base = HEADER_BYTES
.checked_add(table_bytes)
.ok_or_else(|| LlvmError::Codegen("ConstDict key base overflow".into()))?;
let mut running_key_off = key_payload_base;
for (key, value) in &sorted {
let key_len = u32::try_from(key.len())
.map_err(|_| LlvmError::Codegen("ConstDict key length exceeds u32".into()))?;
self.bytes.extend_from_slice(&running_key_off.to_le_bytes());
self.bytes.extend_from_slice(&key_len.to_le_bytes());
self.bytes.extend_from_slice(&value.to_le_bytes());
running_key_off = running_key_off
.checked_add(key_len)
.ok_or_else(|| LlvmError::Codegen("ConstDict key offset overflow".into()))?;
}
for (key, _) in &sorted {
self.bytes.extend_from_slice(key.as_bytes());
}
self.dict_offsets.insert(idx, off);
Ok(())
}
}
pub(crate) fn is_buffer_protocol_signature(params: &[IrType], ret: IrType) -> bool {
matches!(
params,
[
IrType::I32,
IrType::I32,
IrType::I32,
IrType::I32,
IrType::I64
]
) && matches!(ret, IrType::I32)
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn emit_module_funcs<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
entry: &Func,
buffer_return_size: u32,
const_pool: &ConstPool,
helpers: &[&Func],
helper_ir_indices: Option<&[u32]>,
lambdas: &[&Func],
closure_table: &[u32],
imports: &[relon_ir::ir::NativeImport],
) -> Result<
(
FunctionValue<'ctx>,
EntryShape,
HashMap<u32, FunctionValue<'ctx>>,
Vec<FunctionValue<'ctx>>,
),
LlvmError,
> {
emit_module_funcs_impl(
ctx,
module,
entry,
buffer_return_size,
const_pool,
helpers,
helper_ir_indices,
lambdas,
closure_table,
imports,
WorldMode::OpenWorld,
crate::CodegenTarget::Native,
&[],
)
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn emit_module_funcs_wasm<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
entry: &Func,
buffer_return_size: u32,
const_pool: &ConstPool,
helpers: &[&Func],
helper_ir_indices: Option<&[u32]>,
lambdas: &[&Func],
closure_table: &[u32],
imports: &[relon_ir::ir::NativeImport],
) -> Result<
(
FunctionValue<'ctx>,
EntryShape,
HashMap<u32, FunctionValue<'ctx>>,
Vec<FunctionValue<'ctx>>,
),
LlvmError,
> {
emit_module_funcs_impl(
ctx,
module,
entry,
buffer_return_size,
const_pool,
helpers,
helper_ir_indices,
lambdas,
closure_table,
imports,
WorldMode::OpenWorld,
crate::CodegenTarget::Wasm32,
&[],
)
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn emit_module_funcs_closed_world<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
entry: &Func,
buffer_return_size: u32,
const_pool: &ConstPool,
helpers: &[&Func],
helper_ir_indices: Option<&[u32]>,
lambdas: &[&Func],
closure_table: &[u32],
imports: &[relon_ir::ir::NativeImport],
) -> Result<
(
FunctionValue<'ctx>,
EntryShape,
HashMap<u32, FunctionValue<'ctx>>,
Vec<FunctionValue<'ctx>>,
),
LlvmError,
> {
emit_module_funcs_impl(
ctx,
module,
entry,
buffer_return_size,
const_pool,
helpers,
helper_ir_indices,
lambdas,
closure_table,
imports,
WorldMode::ClosedWorld,
crate::CodegenTarget::Native,
&[],
)
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn emit_module_funcs_closed_world_wasm<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
entry: &Func,
buffer_return_size: u32,
const_pool: &ConstPool,
helpers: &[&Func],
helper_ir_indices: Option<&[u32]>,
lambdas: &[&Func],
closure_table: &[u32],
imports: &[relon_ir::ir::NativeImport],
effectful_imports: &[bool],
) -> Result<
(
FunctionValue<'ctx>,
EntryShape,
HashMap<u32, FunctionValue<'ctx>>,
Vec<FunctionValue<'ctx>>,
),
LlvmError,
> {
emit_module_funcs_impl(
ctx,
module,
entry,
buffer_return_size,
const_pool,
helpers,
helper_ir_indices,
lambdas,
closure_table,
imports,
WorldMode::ClosedWorld,
crate::CodegenTarget::Wasm32,
effectful_imports,
)
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn emit_module_funcs_impl<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
entry: &Func,
buffer_return_size: u32,
const_pool: &ConstPool,
helpers: &[&Func],
helper_ir_indices: Option<&[u32]>,
lambdas: &[&Func],
closure_table: &[u32],
imports: &[relon_ir::ir::NativeImport],
world_mode: WorldMode,
target: crate::CodegenTarget,
effectful_imports: &[bool],
) -> Result<
(
FunctionValue<'ctx>,
EntryShape,
HashMap<u32, FunctionValue<'ctx>>,
Vec<FunctionValue<'ctx>>,
),
LlvmError,
> {
declare_llvm_trap(ctx, module);
let mut helper_table: HashMap<u32, FunctionValue<'ctx>> = HashMap::new();
if let Some(ir_indices) = helper_ir_indices {
if ir_indices.len() != helpers.len() {
return Err(LlvmError::Codegen(format!(
"emit_module_funcs: helpers.len()={} but helper_ir_indices.len()={}",
helpers.len(),
ir_indices.len()
)));
}
}
for (i, helper) in helpers.iter().enumerate() {
let fv = declare_helper_function(ctx, module, helper, i)?;
let ir_idx = helper_ir_indices.map(|v| v[i]).unwrap_or(i as u32);
helper_table.insert(ir_idx, fv);
}
let mut closure_fn_table: Vec<FunctionValue<'ctx>> = Vec::with_capacity(closure_table.len());
if lambdas.len() != closure_table.len() {
return Err(LlvmError::Codegen(format!(
"emit_module_funcs: lambdas.len()={} but closure_table.len()={}",
lambdas.len(),
closure_table.len()
)));
}
for (slot, lambda) in lambdas.iter().enumerate() {
let fv = declare_lambda_function(ctx, module, lambda, slot)?;
closure_fn_table.push(fv);
}
let (entry_fn, shape) = if is_buffer_protocol_signature(&entry.params, entry.ret) {
let fv = emit_buffer_entry_with_helpers_and_closures(
ctx,
module,
entry,
buffer_return_size,
const_pool,
&helper_table,
&closure_fn_table,
imports,
world_mode,
target,
effectful_imports,
)?;
(fv, EntryShape::Buffer)
} else {
let fv =
emit_legacy_entry_with_helpers(ctx, module, entry, &helper_table, imports, world_mode)?;
(fv, EntryShape::LegacyI64)
};
for helper in helpers.iter() {
let helper_fn = helper_table
.values()
.find(|fv| {
let expected = format!("relon_helper_{}", helper.name);
fv.get_name().to_string_lossy() == expected
})
.copied()
.ok_or_else(|| {
LlvmError::Codegen(format!(
"emit_module_funcs: helper `{}` declared but FunctionValue missing",
helper.name
))
})?;
emit_helper_body(ctx, module, helper, helper_fn, const_pool, &helper_table)?;
}
let self_capture_table = build_self_capture_table(entry, helpers, lambdas);
let known_capture_table = build_known_capture_table(entry, helpers, lambdas);
for (slot, lambda) in lambdas.iter().enumerate() {
let lambda_fn = closure_fn_table[slot];
let slot_u32 = slot as u32;
let offsets = self_capture_table
.get(&slot_u32)
.cloned()
.unwrap_or_default();
let known_offsets = known_capture_table
.get(&slot_u32)
.cloned()
.unwrap_or_default();
emit_lambda_body(
ctx,
module,
lambda,
lambda_fn,
const_pool,
&helper_table,
&closure_fn_table,
&offsets,
&known_offsets,
)?;
}
Ok((entry_fn, shape, helper_table, closure_fn_table))
}
fn build_self_capture_table(
entry: &Func,
helpers: &[&Func],
lambdas: &[&Func],
) -> HashMap<u32, Vec<(u32, u32)>> {
let mut table: HashMap<u32, Vec<(u32, u32)>> = HashMap::new();
let scan = |func: &Func, table: &mut HashMap<u32, Vec<(u32, u32)>>| {
let ops = &func.body;
for (i, tagged) in ops.iter().enumerate() {
let Op::MakeClosure {
fn_table_idx,
ref captures,
..
} = tagged.op
else {
continue;
};
let Some(next) = ops.get(i + 1) else {
continue;
};
let Op::LetSet {
idx,
ty: relon_ir::ir::IrType::Closure,
} = next.op
else {
continue;
};
for cap in captures {
if cap.let_idx == idx && matches!(cap.ty, relon_ir::ir::IrType::Closure) {
table
.entry(fn_table_idx)
.or_default()
.push((cap.offset, fn_table_idx));
}
}
}
};
scan(entry, &mut table);
for h in helpers {
scan(h, &mut table);
}
for l in lambdas {
scan(l, &mut table);
}
table
}
fn build_known_capture_table(
entry: &Func,
helpers: &[&Func],
lambdas: &[&Func],
) -> HashMap<u32, Vec<(u32, u32)>> {
use relon_ir::ir::IrType as Irt;
let mut table: HashMap<u32, Vec<(u32, u32)>> = HashMap::new();
let scan = |func: &Func, table: &mut HashMap<u32, Vec<(u32, u32)>>| {
let ops = &func.body;
let mut known_slots: HashMap<u32, u32> = HashMap::new();
for (i, tagged) in ops.iter().enumerate() {
if let Op::LetSet {
idx,
ty: Irt::Closure,
} = tagged.op
{
if let Some(Op::MakeClosure { fn_table_idx, .. }) =
i.checked_sub(1).and_then(|p| ops.get(p)).map(|t| &t.op)
{
known_slots.insert(idx, *fn_table_idx);
} else {
known_slots.remove(&idx);
}
continue;
}
if let Op::MakeClosure {
fn_table_idx: l_idx,
ref captures,
..
} = tagged.op
{
for cap in captures {
if !matches!(cap.ty, Irt::Closure) {
continue;
}
if let Some(&k_idx) = known_slots.get(&cap.let_idx) {
if k_idx != l_idx {
table.entry(l_idx).or_default().push((cap.offset, k_idx));
}
}
}
}
}
};
scan(entry, &mut table);
for h in helpers {
scan(h, &mut table);
}
for l in lambdas {
scan(l, &mut table);
}
table
}
fn collect_closure_letset_slots(body: &[TaggedOp], out: &mut Vec<u32>) {
for t in body {
match &t.op {
Op::LetSet {
idx,
ty: relon_ir::ir::IrType::Closure,
} => out.push(*idx),
Op::If {
then_body,
else_body,
..
} => {
collect_closure_letset_slots(then_body, out);
collect_closure_letset_slots(else_body, out);
}
Op::Block { body, .. } | Op::Loop { body, .. } => {
collect_closure_letset_slots(body, out);
}
_ => {}
}
}
}
fn declare_helper_function<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
slot: usize,
) -> Result<FunctionValue<'ctx>, LlvmError> {
let mut param_types: Vec<BasicMetadataTypeEnum<'ctx>> = Vec::with_capacity(func.params.len());
for (i, p) in func.params.iter().enumerate() {
let bt = ir_ty_to_llvm_abi(ctx, *p).ok_or_else(|| {
LlvmError::UnsupportedSignature(format!(
"llvm-aot: helper `{}` param #{i} type {p:?} unsupported",
func.name
))
})?;
param_types.push(basic_to_metadata(bt));
}
let ret_bt = ir_ty_to_llvm_abi(ctx, func.ret).ok_or_else(|| {
LlvmError::UnsupportedSignature(format!(
"llvm-aot: helper `{}` return type {:?} unsupported",
func.name, func.ret
))
})?;
let fn_type = match ret_bt {
BasicTypeEnum::IntType(t) => t.fn_type(¶m_types, false),
BasicTypeEnum::FloatType(t) => t.fn_type(¶m_types, false),
BasicTypeEnum::PointerType(t) => t.fn_type(¶m_types, false),
other => {
return Err(LlvmError::Codegen(format!(
"llvm-aot: helper `{}` ret BasicType {other:?} unsupported",
func.name
)));
}
};
let _ = slot;
let llvm_name = format!("relon_helper_{}", func.name);
let fv = module.add_function(&llvm_name, fn_type, Some(Linkage::Internal));
Ok(fv)
}
fn declare_lambda_function<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
slot: usize,
) -> Result<FunctionValue<'ctx>, LlvmError> {
let ptr_t = ctx.ptr_type(AddressSpace::default());
let mut param_types: Vec<BasicMetadataTypeEnum<'ctx>> =
Vec::with_capacity(1 + func.params.len());
param_types.push(ptr_t.into());
for (i, p) in func.params.iter().enumerate() {
let bt = ir_ty_to_llvm_abi(ctx, *p).ok_or_else(|| {
LlvmError::UnsupportedSignature(format!(
"llvm-aot: lambda `{}` param #{i} type {p:?} unsupported",
func.name
))
})?;
param_types.push(basic_to_metadata(bt));
}
let ret_bt = ir_ty_to_llvm_abi(ctx, func.ret).ok_or_else(|| {
LlvmError::UnsupportedSignature(format!(
"llvm-aot: lambda `{}` return type {:?} unsupported",
func.name, func.ret
))
})?;
let fn_type = match ret_bt {
BasicTypeEnum::IntType(t) => t.fn_type(¶m_types, false),
BasicTypeEnum::FloatType(t) => t.fn_type(¶m_types, false),
BasicTypeEnum::PointerType(t) => t.fn_type(¶m_types, false),
other => {
return Err(LlvmError::Codegen(format!(
"llvm-aot: lambda `{}` ret BasicType {other:?} unsupported",
func.name
)));
}
};
let llvm_name = format!("relon_lambda_{}_{}", slot, func.name);
let fv = module.add_function(&llvm_name, fn_type, Some(Linkage::Internal));
Ok(fv)
}
fn declare_llvm_trap<'ctx>(ctx: &'ctx Context, module: &LlvmModule<'ctx>) -> FunctionValue<'ctx> {
if let Some(f) = module.get_function("llvm.trap") {
return f;
}
let void_t = ctx.void_type();
let fn_ty = void_t.fn_type(&[], false);
module.add_function("llvm.trap", fn_ty, None)
}
fn declare_call_native<'ctx>(ctx: &'ctx Context, module: &LlvmModule<'ctx>) -> FunctionValue<'ctx> {
if let Some(f) = module.get_function(crate::state::RELON_LLVM_CALL_NATIVE_SYMBOL) {
return f;
}
let i64_t = ctx.i64_type();
let i32_t = ctx.i32_type();
let ptr_t = ctx.ptr_type(AddressSpace::default());
let fn_ty = i64_t.fn_type(
&[ptr_t.into(), i32_t.into(), ptr_t.into(), i32_t.into()],
false,
);
module.add_function(
crate::state::RELON_LLVM_CALL_NATIVE_SYMBOL,
fn_ty,
Some(Linkage::External),
)
}
fn declare_host_fn_direct<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
import: &relon_ir::ir::NativeImport,
) -> FunctionValue<'ctx> {
if let Some(f) = module.get_function(&import.name) {
return f;
}
let i64_t = ctx.i64_type();
let params: Vec<BasicMetadataTypeEnum<'ctx>> =
import.param_tys.iter().map(|_| i64_t.into()).collect();
let fn_ty = match import.ret_ty {
IrType::Unit => ctx.void_type().fn_type(¶ms, false),
_ => i64_t.fn_type(¶ms, false),
};
module.add_function(&import.name, fn_ty, Some(Linkage::External))
}
fn ir_ty_to_llvm_abi<'ctx>(ctx: &'ctx Context, ty: IrType) -> Option<BasicTypeEnum<'ctx>> {
match ty {
IrType::I64 | IrType::F64 => Some(ctx.i64_type().into()),
IrType::I32 | IrType::Bool | IrType::Unit => Some(ctx.i32_type().into()),
IrType::String
| IrType::ListInt
| IrType::ListFloat
| IrType::ListBool
| IrType::ListString
| IrType::ListSchema
| IrType::ListList
| IrType::Closure
| IrType::Dict => Some(ctx.i32_type().into()),
}
}
fn basic_to_metadata(bt: BasicTypeEnum<'_>) -> BasicMetadataTypeEnum<'_> {
match bt {
BasicTypeEnum::IntType(t) => t.into(),
BasicTypeEnum::FloatType(t) => t.into(),
BasicTypeEnum::PointerType(t) => t.into(),
BasicTypeEnum::ArrayType(t) => t.into(),
BasicTypeEnum::StructType(t) => t.into(),
BasicTypeEnum::VectorType(t) => t.into(),
BasicTypeEnum::ScalableVectorType(t) => t.into(),
}
}
fn emit_helper_body<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
llvm_fn: FunctionValue<'ctx>,
const_pool: &ConstPool,
helper_table: &HashMap<u32, FunctionValue<'ctx>>,
) -> Result<(), LlvmError> {
let entry_bb = ctx.append_basic_block(llvm_fn, "entry");
let builder = ctx.create_builder();
builder.position_at_end(entry_bb);
let mut emit = Emit::new(
ctx,
&builder,
module,
llvm_fn,
EntryShape::LegacyI64,
None,
None,
0,
const_pool,
);
emit.param_base = 0;
emit.helper_table = Some(helper_table.clone());
emit.helper_ret_ty = Some(func.ret);
emit.llvm_trap_fn = Some(declare_llvm_trap(ctx, module));
emit.let_floor = relon_ir::ir::body_let_watermark(&func.body);
emit.lower_body(&func.body)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn emit_lambda_body<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
llvm_fn: FunctionValue<'ctx>,
const_pool: &ConstPool,
helper_table: &HashMap<u32, FunctionValue<'ctx>>,
closure_fn_table: &[FunctionValue<'ctx>],
self_capture_offsets: &[(u32, u32)],
known_capture_offsets: &[(u32, u32)],
) -> Result<(), LlvmError> {
let entry_bb = ctx.append_basic_block(llvm_fn, "entry");
let builder = ctx.create_builder();
builder.position_at_end(entry_bb);
let i32_t = ctx.i32_type();
let i64_t = ctx.i64_type();
let i8_t = ctx.i8_type();
let ptr_t = ctx.ptr_type(AddressSpace::default());
let state_param = llvm_fn
.get_nth_param(0)
.ok_or_else(|| LlvmError::Codegen(format!("lambda `{}` missing state param", func.name)))?
.into_pointer_value();
let arena_base_gep = unsafe {
builder
.build_in_bounds_gep(
i8_t,
state_param,
&[i32_t.const_int(ARENA_STATE_OFFSET_BASE as u64, false)],
"lambda_arena_base_gep",
)
.map_err(|e| LlvmError::Codegen(format!("lambda arena_base GEP: {e}")))?
};
let arena_base_load = builder
.build_load(i64_t, arena_base_gep, "lambda_arena_base")
.map_err(|e| LlvmError::Codegen(format!("lambda arena_base load: {e}")))?;
mark_invariant_load(ctx, arena_base_load);
let arena_base_int = arena_base_load.into_int_value();
let arena_base_ptr = builder
.build_int_to_ptr(arena_base_int, ptr_t, "lambda_arena_base_ptr")
.map_err(|e| LlvmError::Codegen(format!("lambda arena_base inttoptr: {e}")))?;
let captures_ptr_param = llvm_fn
.get_nth_param(1)
.ok_or_else(|| {
LlvmError::Codegen(format!("lambda `{}` missing captures_ptr param", func.name))
})?
.into_int_value();
let mut emit = Emit::new(
ctx,
&builder,
module,
llvm_fn,
EntryShape::LegacyI64,
Some(arena_base_ptr),
Some(state_param),
0,
const_pool,
);
emit.param_base = 1;
emit.helper_table = Some(helper_table.clone());
emit.closure_fn_table = closure_fn_table.to_vec();
emit.helper_ret_ty = Some(func.ret);
emit.llvm_trap_fn = Some(declare_llvm_trap(ctx, module));
emit.self_capture_offsets = self_capture_offsets.to_vec();
emit.known_capture_offsets = known_capture_offsets.to_vec();
emit.captures_ptr_param = Some(captures_ptr_param);
emit.let_floor = relon_ir::ir::body_let_watermark(&func.body);
emit.lower_body(&func.body)?;
Ok(())
}
pub(crate) fn emit_fast_entry<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
profile: &FastPathProfile,
helper_table: &HashMap<u32, FunctionValue<'ctx>>,
closure_fn_table: &[FunctionValue<'ctx>],
) -> Result<FunctionValue<'ctx>, LlvmError> {
if !is_buffer_protocol_signature(&func.params, func.ret) {
return Err(LlvmError::UnsupportedSignature(
"fast-path entry requires buffer-protocol IR".into(),
));
}
let arity = profile.arg_offsets.len();
if arity > 8 {
return Err(LlvmError::UnsupportedSignature(format!(
"fast-path entry: arity {arity} exceeds cap of 8"
)));
}
let i64_t = ctx.i64_type();
let param_types: Vec<BasicMetadataTypeEnum<'ctx>> = (0..arity).map(|_| i64_t.into()).collect();
let fn_type = i64_t.fn_type(¶m_types, false);
let llvm_fn = module.add_function(ENTRY_SYMBOL_FAST, fn_type, None);
let entry_bb = ctx.append_basic_block(llvm_fn, "fast_entry");
let builder = ctx.create_builder();
builder.position_at_end(entry_bb);
let ret_slot = builder
.build_alloca(i64_t, "fast_ret_slot")
.map_err(|e| LlvmError::Codegen(format!("fast ret_slot alloca: {e}")))?;
builder
.build_store(ret_slot, i64_t.const_zero())
.map_err(|e| LlvmError::Codegen(format!("fast ret_slot init: {e}")))?;
let empty_pool = ConstPool::default();
let mut emit = Emit::new(
ctx,
&builder,
module,
llvm_fn,
EntryShape::LegacyI64,
None,
None,
0,
&empty_pool,
);
emit.fast_path = Some(FastEmit {
profile: profile.clone(),
ret_slot,
});
emit.param_base = 0;
emit.llvm_trap_fn = Some(declare_llvm_trap(ctx, module));
emit.helper_table = Some(helper_table.clone());
emit.closure_fn_table = closure_fn_table.to_vec();
emit.let_floor = relon_ir::ir::body_let_watermark(&func.body);
emit.lower_body(&func.body)?;
if let Some(cur) = builder.get_insert_block() {
if cur.get_terminator().is_none() {
let v = builder
.build_load(i64_t, ret_slot, "fast_ret_load")
.map_err(|e| LlvmError::Codegen(format!("fast trailing load: {e}")))?
.into_int_value();
builder
.build_return(Some(&v))
.map_err(|e| LlvmError::Codegen(format!("fast trailing ret: {e}")))?;
}
}
Ok(llvm_fn)
}
fn emit_legacy_entry_with_helpers<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
helper_table: &HashMap<u32, FunctionValue<'ctx>>,
imports: &[relon_ir::ir::NativeImport],
world_mode: WorldMode,
) -> Result<FunctionValue<'ctx>, LlvmError> {
emit_legacy_entry_impl(ctx, module, func, Some(helper_table), imports, world_mode)
}
fn emit_legacy_entry_impl<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
helper_table: Option<&HashMap<u32, FunctionValue<'ctx>>>,
imports: &[relon_ir::ir::NativeImport],
world_mode: WorldMode,
) -> Result<FunctionValue<'ctx>, LlvmError> {
for (i, p) in func.params.iter().enumerate() {
if *p != IrType::I64 {
return Err(LlvmError::UnsupportedSignature(format!(
"llvm-aot: legacy-i64 envelope expects I64 param at #{i}, got {p:?}"
)));
}
}
if func.ret != IrType::I64 {
return Err(LlvmError::UnsupportedSignature(format!(
"llvm-aot: legacy-i64 envelope expects I64 return, got {:?}",
func.ret
)));
}
let i64_t = ctx.i64_type();
let param_types: Vec<BasicMetadataTypeEnum<'ctx>> =
(0..func.params.len()).map(|_| i64_t.into()).collect();
let fn_type = i64_t.fn_type(¶m_types, false);
let llvm_fn = module.add_function(ENTRY_SYMBOL, fn_type, None);
let entry_bb = ctx.append_basic_block(llvm_fn, "entry");
let builder = ctx.create_builder();
builder.position_at_end(entry_bb);
let empty_pool = ConstPool::default();
let mut emit = Emit::new(
ctx,
&builder,
module,
llvm_fn,
EntryShape::LegacyI64,
None,
None,
0,
&empty_pool,
);
emit.param_base = 0;
if let Some(table) = helper_table {
emit.helper_table = Some(table.clone());
}
emit.llvm_trap_fn = Some(declare_llvm_trap(ctx, module));
emit.imports = imports;
emit.world_mode = world_mode;
if matches!(world_mode, WorldMode::ClosedWorld) {
for import in imports {
declare_host_fn_direct(ctx, module, import);
}
}
emit.let_floor = relon_ir::ir::body_let_watermark(&func.body);
emit.lower_body(&func.body)?;
Ok(llvm_fn)
}
#[allow(dead_code)]
fn emit_buffer_entry_with_helpers<'ctx>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
buffer_return_size: u32,
const_pool: &ConstPool,
helper_table: &HashMap<u32, FunctionValue<'ctx>>,
) -> Result<FunctionValue<'ctx>, LlvmError> {
emit_buffer_entry_impl(
ctx,
module,
func,
buffer_return_size,
const_pool,
Some(helper_table),
&[],
&[],
WorldMode::OpenWorld,
crate::CodegenTarget::Native,
&[],
)
}
#[allow(clippy::too_many_arguments)]
fn emit_buffer_entry_with_helpers_and_closures<'ctx, 'cp>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
buffer_return_size: u32,
const_pool: &'cp ConstPool,
helper_table: &HashMap<u32, FunctionValue<'ctx>>,
closure_fn_table: &[FunctionValue<'ctx>],
imports: &'cp [relon_ir::ir::NativeImport],
world_mode: WorldMode,
target: crate::CodegenTarget,
effectful_imports: &'cp [bool],
) -> Result<FunctionValue<'ctx>, LlvmError> {
emit_buffer_entry_impl(
ctx,
module,
func,
buffer_return_size,
const_pool,
Some(helper_table),
closure_fn_table,
imports,
world_mode,
target,
effectful_imports,
)
}
#[allow(clippy::too_many_arguments)]
fn emit_buffer_entry_impl<'ctx, 'cp>(
ctx: &'ctx Context,
module: &LlvmModule<'ctx>,
func: &Func,
buffer_return_size: u32,
const_pool: &'cp ConstPool,
helper_table: Option<&HashMap<u32, FunctionValue<'ctx>>>,
closure_fn_table: &[FunctionValue<'ctx>],
imports: &'cp [relon_ir::ir::NativeImport],
world_mode: WorldMode,
target: crate::CodegenTarget,
effectful_imports: &'cp [bool],
) -> Result<FunctionValue<'ctx>, LlvmError> {
let i32_t = ctx.i32_type();
let i64_t = ctx.i64_type();
let ptr_t = ctx.ptr_type(AddressSpace::default());
let param_types: Vec<BasicMetadataTypeEnum<'ctx>> = vec![
ptr_t.into(),
i32_t.into(),
i32_t.into(),
i32_t.into(),
i32_t.into(),
i64_t.into(),
];
let fn_type = i32_t.fn_type(¶m_types, false);
let llvm_fn = module.add_function(ENTRY_SYMBOL, fn_type, None);
let entry_bb = ctx.append_basic_block(llvm_fn, "entry");
let builder = ctx.create_builder();
builder.position_at_end(entry_bb);
let state_param = llvm_fn
.get_nth_param(0)
.ok_or_else(|| LlvmError::Codegen("buffer entry missing state param".into()))?
.into_pointer_value();
let i8_t = ctx.i8_type();
let arena_base_gep = unsafe {
builder
.build_in_bounds_gep(
i8_t,
state_param,
&[i32_t.const_int(ARENA_STATE_OFFSET_BASE as u64, false)],
"arena_base_gep",
)
.map_err(|e| LlvmError::Codegen(format!("arena_base GEP: {e}")))?
};
let arena_base_load = builder
.build_load(i64_t, arena_base_gep, "arena_base")
.map_err(|e| LlvmError::Codegen(format!("arena_base load: {e}")))?;
mark_invariant_load(ctx, arena_base_load);
let arena_base_int = arena_base_load.into_int_value();
let arena_base_ptr = builder
.build_int_to_ptr(arena_base_int, ptr_t, "arena_base_ptr")
.map_err(|e| LlvmError::Codegen(format!("arena_base inttoptr: {e}")))?;
let tail_init_gep = unsafe {
builder
.build_in_bounds_gep(
i8_t,
state_param,
&[i32_t.const_int(u64::from(ARENA_STATE_OFFSET_TAIL_CURSOR), false)],
"tail_cursor_init_gep",
)
.map_err(|e| LlvmError::Codegen(format!("tail_cursor init GEP: {e}")))?
};
let tail_init = i32_t.const_int(u64::from(buffer_return_size), false);
builder
.build_store(tail_init_gep, tail_init)
.map_err(|e| LlvmError::Codegen(format!("tail_cursor init store: {e}")))?;
let mut emit = Emit::new(
ctx,
&builder,
module,
llvm_fn,
EntryShape::Buffer,
Some(arena_base_ptr),
Some(state_param),
buffer_return_size,
const_pool,
);
emit.param_base = 1;
if let Some(table) = helper_table {
emit.helper_table = Some(table.clone());
}
emit.closure_fn_table = closure_fn_table.to_vec();
emit.llvm_trap_fn = Some(declare_llvm_trap(ctx, module));
emit.imports = imports;
emit.world_mode = world_mode;
emit.target = target;
emit.effectful_imports = effectful_imports;
match world_mode {
WorldMode::OpenWorld if matches!(target, crate::CodegenTarget::Wasm32) => {
emit.call_native_fn = None;
}
WorldMode::OpenWorld => {
emit.call_native_fn = Some(declare_call_native(ctx, module));
}
WorldMode::ClosedWorld => {
emit.call_native_fn = None;
for (idx, import) in imports.iter().enumerate() {
let effectful = effectful_imports.get(idx).copied().unwrap_or(false);
if !effectful {
declare_host_fn_direct(ctx, module, import);
}
}
}
}
emit.let_floor = relon_ir::ir::body_let_watermark(&func.body);
emit.emit_step_budget_check("entry")?;
emit.lower_body(&func.body)?;
Ok(llvm_fn)
}
pub(crate) struct Emit<'ctx, 'b, 'cp> {
pub(crate) ctx: &'ctx Context,
pub(crate) builder: &'b Builder<'ctx>,
pub(crate) func: FunctionValue<'ctx>,
pub(crate) module: &'b LlvmModule<'ctx>,
pub(crate) shape: EntryShape,
pub(crate) arena_base_ptr: Option<PointerValue<'ctx>>,
pub(crate) state_ptr: Option<PointerValue<'ctx>>,
pub(crate) stack: Vec<TypedValue<'ctx>>,
pub(crate) let_slots: std::collections::HashMap<u32, (PointerValue<'ctx>, IrType)>,
pub(crate) let_floor: u32,
pub(crate) param_base: u32,
pub(crate) label_stack: Vec<LabelFrame<'ctx>>,
pub(crate) name_seq: u32,
pub(crate) buffer_return_size: u32,
pub(crate) fast_path: Option<FastEmit<'ctx>>,
pub(crate) helper_table: Option<HashMap<u32, FunctionValue<'ctx>>>,
pub(crate) helper_ret_ty: Option<IrType>,
pub(crate) llvm_trap_fn: Option<FunctionValue<'ctx>>,
pub(crate) const_pool: &'cp ConstPool,
pub(crate) inline_frames: Vec<InlineFrame<'ctx>>,
pub(crate) needs_tail_cursor: bool,
pub(crate) inplace_return_root: Option<IntValue<'ctx>>,
pub(crate) closure_fn_table: Vec<FunctionValue<'ctx>>,
pub(crate) record_locals: std::collections::HashMap<u32, PointerValue<'ctx>>,
pub(crate) last_const_string: Option<Vec<u8>>,
pub(crate) self_capture_offsets: Vec<(u32, u32)>,
pub(crate) self_capture_let_slots: std::collections::HashMap<u32, (u32, u32)>,
pub(crate) captures_ptr_param: Option<IntValue<'ctx>>,
pub(crate) fast_path_closure_let_slots: std::collections::HashMap<u32, u32>,
pub(crate) const_string_let_slots: std::collections::HashMap<u32, (u32, Option<u8>)>,
pub(crate) known_closure_let_slots: std::collections::HashMap<u32, u32>,
pub(crate) known_capture_offsets: Vec<(u32, u32)>,
pub(crate) imports: &'cp [relon_ir::ir::NativeImport],
pub(crate) call_native_fn: Option<FunctionValue<'ctx>>,
pub(crate) world_mode: WorldMode,
pub(crate) target: crate::CodegenTarget,
pub(crate) effectful_imports: &'cp [bool],
}
pub(crate) struct InlineFrame<'ctx> {
pub(crate) params: Vec<TypedValue<'ctx>>,
pub(crate) let_offset: u32,
pub(crate) ret_slot: PointerValue<'ctx>,
pub(crate) ret_ty: IrType,
pub(crate) exit_bb: inkwell::basic_block::BasicBlock<'ctx>,
}
#[derive(Clone)]
pub(crate) struct FastEmit<'ctx> {
pub(crate) profile: FastPathProfile,
pub(crate) ret_slot: PointerValue<'ctx>,
}
#[derive(Clone, Copy)]
pub(crate) struct TypedValue<'ctx> {
pub(crate) val: IntValue<'ctx>,
#[allow(dead_code)]
pub(crate) ty: IrType,
pub(crate) prov: Provenance,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Provenance {
None,
OwnCapturesPtr,
OwnCaptureHandle {
#[allow(dead_code)]
offset: u32,
self_fn_table_idx: u32,
},
FastPathClosure {
fn_table_idx: u32,
},
KnownClosure {
fn_table_idx: u32,
},
ConstString {
len: u32,
first_byte: Option<u8>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LabelKind {
Block,
Loop,
}
#[derive(Clone, Copy)]
pub(crate) struct LabelFrame<'ctx> {
pub(crate) header_bb: inkwell::basic_block::BasicBlock<'ctx>,
pub(crate) tail_bb: inkwell::basic_block::BasicBlock<'ctx>,
pub(crate) kind: LabelKind,
}
impl<'ctx, 'b, 'cp> Emit<'ctx, 'b, 'cp> {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
ctx: &'ctx Context,
builder: &'b Builder<'ctx>,
module: &'b LlvmModule<'ctx>,
func: FunctionValue<'ctx>,
shape: EntryShape,
arena_base_ptr: Option<PointerValue<'ctx>>,
state_ptr: Option<PointerValue<'ctx>>,
buffer_return_size: u32,
const_pool: &'cp ConstPool,
) -> Self {
Self {
ctx,
builder,
func,
module,
shape,
arena_base_ptr,
state_ptr,
stack: Vec::with_capacity(8),
let_slots: std::collections::HashMap::new(),
let_floor: 0,
param_base: 0,
label_stack: Vec::new(),
name_seq: 0,
buffer_return_size,
fast_path: None,
helper_table: None,
helper_ret_ty: None,
llvm_trap_fn: None,
const_pool,
inline_frames: Vec::new(),
needs_tail_cursor: false,
inplace_return_root: None,
last_const_string: None,
closure_fn_table: Vec::new(),
record_locals: std::collections::HashMap::new(),
self_capture_offsets: Vec::new(),
self_capture_let_slots: std::collections::HashMap::new(),
captures_ptr_param: None,
fast_path_closure_let_slots: std::collections::HashMap::new(),
const_string_let_slots: std::collections::HashMap::new(),
known_closure_let_slots: std::collections::HashMap::new(),
known_capture_offsets: Vec::new(),
imports: &[],
call_native_fn: None,
world_mode: WorldMode::OpenWorld,
target: crate::CodegenTarget::Native,
effectful_imports: &[],
}
}
pub(crate) fn next_name(&mut self, hint: &str) -> String {
self.name_seq += 1;
format!("{hint}_{}", self.name_seq)
}
pub(crate) fn push(&mut self, v: IntValue<'ctx>, ty: IrType) {
self.stack.push(TypedValue {
val: v,
ty,
prov: Provenance::None,
});
}
pub(crate) fn push_with_prov(&mut self, v: IntValue<'ctx>, ty: IrType, prov: Provenance) {
self.stack.push(TypedValue { val: v, ty, prov });
}
pub(crate) fn peek_self_capture_provenance(&self, offset: u32) -> Option<Provenance> {
let top = self.stack.last()?;
if !matches!(top.prov, Provenance::OwnCapturesPtr) {
return None;
}
for (cap_offset, self_fn_table_idx) in &self.self_capture_offsets {
if *cap_offset == offset {
return Some(Provenance::OwnCaptureHandle {
offset,
self_fn_table_idx: *self_fn_table_idx,
});
}
}
for (cap_offset, captured_fn_table_idx) in &self.known_capture_offsets {
if *cap_offset == offset {
return Some(Provenance::KnownClosure {
fn_table_idx: *captured_fn_table_idx,
});
}
}
None
}
pub(crate) fn pop(&mut self, ip_hint: &str) -> Result<TypedValue<'ctx>, LlvmError> {
self.stack.pop().ok_or_else(|| {
LlvmError::Codegen(format!(
"operand stack underflow at {ip_hint}: producer emitted an Op with no matching push"
))
})
}
pub(crate) fn pop_int(&mut self, ip_hint: &str) -> Result<IntValue<'ctx>, LlvmError> {
self.pop(ip_hint).map(|tv| tv.val)
}
pub(crate) fn lookup_param(&self, idx: u32) -> Result<IntValue<'ctx>, LlvmError> {
let llvm_idx = self
.param_base
.checked_add(idx)
.ok_or_else(|| LlvmError::Codegen(format!("LocalGet({idx}): param idx overflow")))?;
let p = self.func.get_nth_param(llvm_idx).ok_or_else(|| {
LlvmError::Codegen(format!(
"LocalGet({idx}) -> llvm param #{llvm_idx} out of range; function has {} param(s)",
self.func.count_params()
))
})?;
match p {
BasicValueEnum::IntValue(v) => Ok(v),
other => Err(LlvmError::Codegen(format!(
"LocalGet({idx}) llvm param #{llvm_idx} is {other:?}, expected IntValue"
))),
}
}
pub(crate) fn ensure_let_slot(
&mut self,
idx: u32,
ty: IrType,
) -> Result<PointerValue<'ctx>, LlvmError> {
if let Some((ptr, existing_ty)) = self.let_slots.get(&idx) {
if *existing_ty != ty {
return Err(LlvmError::Codegen(format!(
"let-slot {idx} aliased: previous type {existing_ty:?}, new type {ty:?}"
)));
}
return Ok(*ptr);
}
let entry_bb = self.func.get_first_basic_block().ok_or_else(|| {
LlvmError::Codegen("ensure_let_slot: function has no entry block".into())
})?;
let cur = self.builder.get_insert_block();
if let Some(first_instr) = entry_bb.get_first_instruction() {
self.builder.position_before(&first_instr);
} else {
self.builder.position_at_end(entry_bb);
}
let llvm_ty: inkwell::types::BasicTypeEnum<'ctx> = match ty {
IrType::I64 | IrType::F64 => self.ctx.i64_type().into(),
IrType::I32
| IrType::Bool
| IrType::Unit
| IrType::String
| IrType::ListInt
| IrType::ListFloat
| IrType::ListBool
| IrType::ListString
| IrType::ListSchema
| IrType::ListList
| IrType::Closure
| IrType::Dict => self.ctx.i32_type().into(),
};
let name = format!("let_{idx}");
let ptr = self
.builder
.build_alloca(llvm_ty, &name)
.map_err(|e| LlvmError::Codegen(format!("let-slot {idx} alloca: {e}")))?;
if let Some(bb) = cur {
self.builder.position_at_end(bb);
}
self.let_slots.insert(idx, (ptr, ty));
Ok(ptr)
}
pub(crate) fn lower_body(&mut self, body: &[TaggedOp]) -> Result<(), LlvmError> {
for (ip, tagged) in body.iter().enumerate() {
self.lower_op(ip, tagged)?;
}
if let Some(cur) = self.builder.get_insert_block() {
if cur.get_terminator().is_none() {
self.builder
.build_unreachable()
.map_err(|e| LlvmError::Codegen(format!("trailing unreachable: {e}")))?;
}
}
Ok(())
}
pub(crate) fn lower_op(&mut self, ip: usize, tagged: &TaggedOp) -> Result<(), LlvmError> {
let ip_hint = format!("ip={ip} op={:?}", tagged.op);
let prev_const_string = self.last_const_string.take();
match &tagged.op {
Op::ConstI64(v) => {
let c = self.ctx.i64_type().const_int(*v as u64, true);
self.push(c, IrType::I64);
}
Op::ConstI32(v) => {
let c = self.ctx.i32_type().const_int(*v as u32 as u64, false);
self.push(c, IrType::I32);
}
Op::ConstBool(b) => {
let c = self.ctx.i32_type().const_int(u64::from(*b), false);
self.push(c, IrType::Bool);
}
Op::ConstF64(v) => {
let f = self.ctx.f64_type().const_float(v.into_inner());
let bits = self
.builder
.build_bit_cast(f, self.ctx.i64_type(), &self.next_name("constf64_bits"))
.map_err(|e| LlvmError::Codegen(format!("ConstF64 bitcast: {e}")))?
.into_int_value();
self.push(bits, IrType::F64);
}
Op::LocalGet(idx) => {
if let Some(frame) = self.inline_frames.last() {
let i = *idx as usize;
let tv = frame.params.get(i).ok_or_else(|| {
LlvmError::Codegen(format!(
"inline LocalGet({idx}) out of range — callee has {} params",
frame.params.len()
))
})?;
let (val, prov) = (tv.val, tv.prov);
match prov {
Provenance::KnownClosure { .. } => {
self.push_with_prov(val, tv.ty, prov);
}
_ => self.push(val, tv.ty),
}
} else {
let p = self.lookup_param(*idx)?;
let width = p.get_type().get_bit_width();
let ty = if width == 32 {
IrType::I32
} else {
IrType::I64
};
if *idx == 0 && self.captures_ptr_param.is_some() {
self.push_with_prov(p, ty, Provenance::OwnCapturesPtr);
} else {
self.push(p, ty);
}
}
}
Op::LetSet { idx, ty } => {
let v = self.pop(&ip_hint)?;
let mapped = self.remap_let_idx(*idx);
let slot = self.ensure_let_slot(mapped, *ty)?;
let stored = self.coerce_to_let_ty(v, *ty)?;
self.builder
.build_store(slot, stored)
.map_err(|e| LlvmError::Codegen(format!("LetSet store: {e}")))?;
if let Provenance::OwnCaptureHandle {
offset,
self_fn_table_idx,
} = v.prov
{
if matches!(*ty, IrType::Closure) {
self.self_capture_let_slots
.insert(mapped, (offset, self_fn_table_idx));
}
}
if let Provenance::FastPathClosure { fn_table_idx } = v.prov {
if matches!(*ty, IrType::Closure) {
self.fast_path_closure_let_slots
.insert(mapped, fn_table_idx);
}
}
match (v.prov, *ty) {
(Provenance::KnownClosure { fn_table_idx }, IrType::Closure) => {
self.known_closure_let_slots.insert(mapped, fn_table_idx);
}
(_, IrType::Closure) => {
self.known_closure_let_slots.remove(&mapped);
}
_ => {}
}
match (v.prov, *ty) {
(Provenance::ConstString { len, first_byte }, IrType::String) => {
self.const_string_let_slots
.insert(mapped, (len, first_byte));
}
(_, IrType::String) => {
self.const_string_let_slots.remove(&mapped);
}
_ => {}
}
}
Op::LetGet { idx, ty } => {
let mapped = self.remap_let_idx(*idx);
let slot = self.ensure_let_slot(mapped, *ty)?;
let llvm_ty: inkwell::types::BasicTypeEnum<'ctx> = match *ty {
IrType::I64 | IrType::F64 => self.ctx.i64_type().into(),
IrType::I32
| IrType::Bool
| IrType::Unit
| IrType::String
| IrType::ListInt
| IrType::ListFloat
| IrType::ListBool
| IrType::ListString
| IrType::ListSchema
| IrType::ListList
| IrType::Closure
| IrType::Dict => self.ctx.i32_type().into(),
};
let name = self.next_name("letget");
let v = self
.builder
.build_load(llvm_ty, slot, &name)
.map_err(|e| LlvmError::Codegen(format!("LetGet load: {e}")))?
.into_int_value();
if matches!(*ty, IrType::Closure) {
if let Some(&(offset, self_fn_table_idx)) =
self.self_capture_let_slots.get(&mapped)
{
self.push_with_prov(
v,
*ty,
Provenance::OwnCaptureHandle {
offset,
self_fn_table_idx,
},
);
} else if let Some(&fn_table_idx) =
self.fast_path_closure_let_slots.get(&mapped)
{
self.push_with_prov(v, *ty, Provenance::FastPathClosure { fn_table_idx });
} else if let Some(&fn_table_idx) = self.known_closure_let_slots.get(&mapped) {
self.push_with_prov(v, *ty, Provenance::KnownClosure { fn_table_idx });
} else {
self.push(v, *ty);
}
} else if matches!(*ty, IrType::String) {
if let Some(&(len, first_byte)) = self.const_string_let_slots.get(&mapped) {
self.push_with_prov(v, *ty, Provenance::ConstString { len, first_byte });
} else {
self.push(v, *ty);
}
} else {
self.push(v, *ty);
}
}
Op::Add(ty) => match ty {
IrType::String => self.emit_str_add_inplace_or_concat(&ip_hint)?,
_ => self.emit_binop(&ip_hint, *ty, BinOp::Add)?,
},
Op::Sub(ty) => self.emit_binop(&ip_hint, *ty, BinOp::Sub)?,
Op::Mul(ty) => self.emit_binop(&ip_hint, *ty, BinOp::Mul)?,
Op::Div(ty) => self.emit_binop(&ip_hint, *ty, BinOp::Div)?,
Op::Mod(ty) => self.emit_binop(&ip_hint, *ty, BinOp::Mod)?,
Op::BitAnd(ty) => self.emit_binop(&ip_hint, *ty, BinOp::BitAnd)?,
Op::ConvertI64ToF64 => self.emit_convert_i64_to_f64(&ip_hint)?,
Op::F64ToI64Sat => self.emit_f64_to_i64_sat(&ip_hint)?,
Op::F64Unary(op) => self.emit_f64_unary(&ip_hint, *op)?,
Op::F64Pow => self.emit_f64_pow(&ip_hint)?,
Op::Eq(ty) => self.emit_cmp(&ip_hint, *ty, IntPredicate::EQ)?,
Op::Ne(ty) => self.emit_cmp(&ip_hint, *ty, IntPredicate::NE)?,
Op::Lt(ty) => self.emit_cmp(&ip_hint, *ty, IntPredicate::SLT)?,
Op::Le(ty) => self.emit_cmp(&ip_hint, *ty, IntPredicate::SLE)?,
Op::Gt(ty) => self.emit_cmp(&ip_hint, *ty, IntPredicate::SGT)?,
Op::Ge(ty) => self.emit_cmp(&ip_hint, *ty, IntPredicate::SGE)?,
Op::LoadField { offset, ty } => self.emit_load_field(*offset, *ty)?,
Op::StoreField {
offset,
ty,
inplace,
} => self.emit_store_field(&ip_hint, *offset, *ty, *inplace)?,
Op::LoadStringPtr { offset } => {
self.emit_load_pointer_indirect_param(*offset, IrType::String)?
}
Op::LoadListIntPtr { offset } => {
self.emit_load_pointer_indirect_param(*offset, IrType::ListInt)?
}
Op::LoadListFloatPtr { offset } => {
self.emit_load_pointer_indirect_param(*offset, IrType::ListFloat)?
}
Op::LoadListBoolPtr { offset } => {
self.emit_load_pointer_indirect_param(*offset, IrType::ListBool)?
}
Op::LoadListStringPtr { offset } => {
self.emit_load_pointer_indirect_param(*offset, IrType::ListString)?
}
Op::LoadListSchemaPtr { offset } => {
self.emit_load_pointer_indirect_param(*offset, IrType::ListSchema)?
}
Op::LoadListListPtr { offset } => {
self.emit_load_pointer_indirect_param(*offset, IrType::ListList)?
}
Op::ReadStringLen => self.emit_read_string_len(&ip_hint)?,
Op::Block { result_ty, body } => self.emit_block(*result_ty, body)?,
Op::Loop { result_ty, body } => self.emit_loop(*result_ty, body)?,
Op::Br { label_depth } => self.emit_br(*label_depth)?,
Op::BrIf { label_depth } => self.emit_br_if(&ip_hint, *label_depth)?,
Op::If {
result_ty,
then_body,
else_body,
} => self.emit_if(&ip_hint, *result_ty, then_body, else_body)?,
Op::Return => self.emit_return(&ip_hint)?,
Op::ConstString { idx, value } => {
let off = self
.const_pool
.string_offsets
.get(idx)
.copied()
.ok_or_else(|| {
LlvmError::Codegen(format!(
"Op::ConstString {{ idx: {idx} }}: missing const-pool entry — \
did the host forget to lay out the pool blob before dispatch?"
))
})?;
let c = self.ctx.i32_type().const_int(u64::from(off), false);
let bytes = value.as_bytes();
let len_u32 = u32::try_from(bytes.len()).map_err(|_| {
LlvmError::Codegen("ConstString length exceeds u32 range".into())
})?;
let first_byte = if bytes.len() == 1 {
Some(bytes[0])
} else {
None
};
self.push_with_prov(
c,
IrType::String,
Provenance::ConstString {
len: len_u32,
first_byte,
},
);
self.last_const_string = Some(bytes.to_vec());
}
Op::LoadI32AtAbsolute { offset } => {
let prov_hint = self.peek_self_capture_provenance(*offset);
self.emit_load_at_absolute(&ip_hint, *offset, AbsLoad::I32)?;
if let Some(prov) = prov_hint {
if let Some(top) = self.stack.last_mut() {
top.prov = prov;
}
}
}
Op::LoadI64AtAbsolute { offset } => {
self.emit_load_at_absolute(&ip_hint, *offset, AbsLoad::I64)?
}
Op::LoadI8UAtAbsolute { offset } => {
self.emit_load_at_absolute(&ip_hint, *offset, AbsLoad::I8U)?
}
Op::LoadF64AtAbsolute { offset } => {
self.emit_load_at_absolute(&ip_hint, *offset, AbsLoad::F64)?
}
Op::StoreI32AtAbsolute { offset } => {
self.emit_store_at_absolute(&ip_hint, *offset, AbsStore::I32)?
}
Op::StoreI64AtAbsolute { offset } => {
self.emit_store_at_absolute(&ip_hint, *offset, AbsStore::I64)?
}
Op::StoreI8AtAbsolute { offset } => {
self.emit_store_at_absolute(&ip_hint, *offset, AbsStore::I8)?
}
Op::StoreF64AtAbsolute { offset } => {
self.emit_store_at_absolute(&ip_hint, *offset, AbsStore::F64)?
}
Op::MemcpyAtAbsolute => self.emit_memcpy_at_absolute(&ip_hint)?,
Op::AllocScratch { size_bytes } => self.emit_alloc_scratch_static(*size_bytes)?,
Op::AllocScratchDyn => self.emit_alloc_scratch_dyn(&ip_hint)?,
Op::StrConcatN { operand_count } => self.emit_str_concat_n(&ip_hint, *operand_count)?,
Op::IntToStr => self.emit_int_to_str(&ip_hint)?,
Op::FloatToStr => self.emit_float_to_str(&ip_hint)?,
Op::Call {
fn_index,
arg_count,
param_tys,
ret_ty,
} => {
let stdlib_count = relon_ir::stdlib::stdlib_function_count();
if *fn_index < stdlib_count
&& relon_ir::stdlib::stdlib_function_index("contains") == Some(*fn_index)
&& *arg_count == 2
&& param_tys == &[IrType::String, IrType::String]
&& *ret_ty == IrType::Bool
{
if let Some(needle_bytes) = prev_const_string.as_deref() {
self.emit_str_contains_const_needle(&ip_hint, needle_bytes)?;
} else {
self.emit_str_contains_extern(&ip_hint)?;
}
} else if *fn_index < stdlib_count {
self.emit_call_stdlib(&ip_hint, *fn_index, *arg_count, param_tys, *ret_ty)?
} else {
self.emit_call(&ip_hint, *fn_index, *arg_count, param_tys, *ret_ty)?
}
}
Op::AllocRootRecord { record_local_idx } => {
self.emit_alloc_root_record(*record_local_idx)?
}
Op::StoreFieldAtRecord {
record_local_idx,
offset,
ty,
} => self.emit_store_field_at_record(&ip_hint, *record_local_idx, *offset, *ty)?,
Op::MakeClosure {
fn_table_idx,
captures,
captures_size,
} => self.emit_make_closure(&ip_hint, *fn_table_idx, captures, *captures_size)?,
Op::CallClosure { param_tys, ret_ty } => {
self.emit_call_closure(&ip_hint, param_tys, *ret_ty)?
}
Op::ConstListInt { .. }
| Op::ConstListFloat { .. }
| Op::ConstListBool { .. }
| Op::ConstListString { .. }
| Op::ConstDict { .. }
| Op::DictGetByStringKey { .. }
| Op::ListGetByIntIdx { .. }
| Op::AllocSubRecord { .. }
| Op::AllocScratchRecord { .. }
| Op::PushRecordBase { .. }
| Op::PushRecordBaseAbsolute { .. }
| Op::StoreFieldAtRecordAbsolute { .. }
| Op::EmitTailRecordFromAbsoluteAddr { .. }
| Op::BuildVariantRecord { .. }
| Op::BuildVariantRecordScratch { .. }
| Op::BuildPointerList { .. } => {
self.lower_collections_rest(ip, &ip_hint, &tagged.op)?
}
Op::Select { .. } | Op::BrTable { .. } => {
self.lower_control_rest(ip, &ip_hint, &tagged.op)?
}
Op::LoadFieldAtAbsolute { .. } => self.lower_mem_rest(ip, &ip_hint, &tagged.op)?,
Op::CallNative { .. } | Op::CheckCap { .. } | Op::Trap { .. } => {
self.lower_call_rest(ip, &ip_hint, &tagged.op)?
}
Op::LoadSchemaPtr { .. } => self.lower_schema_rest(ip, &ip_hint, &tagged.op)?,
Op::CaseFoldTableAddr { .. }
| Op::CombiningMarkRangesAddr
| Op::WhitespaceRangesAddr
| Op::DecompTableAddr { .. }
| Op::CccTableAddr
| Op::CompositionTableAddr
| Op::FullCaseFoldTableAddr { .. }
| Op::CasedRangesAddr
| Op::CaseIgnorableRangesAddr
| Op::TurkishCaseFoldTableAddr { .. } => {
self.lower_unicode_rest(ip, &ip_hint, &tagged.op)?
}
}
Ok(())
}
pub(crate) fn remap_let_idx(&self, idx: u32) -> u32 {
match self.inline_frames.last() {
Some(frame) => frame.let_offset.saturating_add(idx),
None => idx,
}
}
pub(crate) fn coerce_to_let_ty(
&self,
tv: TypedValue<'ctx>,
target: IrType,
) -> Result<BasicValueEnum<'ctx>, LlvmError> {
let want_width = match target {
IrType::I64 | IrType::F64 => 64,
IrType::I32
| IrType::Bool
| IrType::Unit
| IrType::String
| IrType::ListInt
| IrType::ListFloat
| IrType::ListBool
| IrType::ListString
| IrType::ListSchema
| IrType::ListList
| IrType::Closure
| IrType::Dict => 32,
};
let have_width = tv.val.get_type().get_bit_width();
if have_width == want_width {
return Ok(tv.val.into());
}
let target_ty = if want_width == 64 {
self.ctx.i64_type()
} else {
self.ctx.i32_type()
};
if have_width < want_width {
self.builder
.build_int_z_extend(tv.val, target_ty, "let_zext")
.map(|v| v.as_basic_value_enum())
.map_err(|e| LlvmError::Codegen(format!("let zext: {e}")))
} else {
self.builder
.build_int_truncate(tv.val, target_ty, "let_trunc")
.map(|v| v.as_basic_value_enum())
.map_err(|e| LlvmError::Codegen(format!("let trunc: {e}")))
}
}
}
impl<'ctx, 'b, 'cp> Emit<'ctx, 'b, 'cp> {}
impl<'ctx, 'b, 'cp> Emit<'ctx, 'b, 'cp> {
pub(crate) fn ir_ty_to_llvm_int(
&self,
ty: IrType,
) -> Result<inkwell::types::IntType<'ctx>, LlvmError> {
match ty {
IrType::I64 | IrType::F64 => Ok(self.ctx.i64_type()),
IrType::I32
| IrType::Bool
| IrType::Unit
| IrType::String
| IrType::ListInt
| IrType::ListFloat
| IrType::ListBool
| IrType::ListString
| IrType::ListSchema
| IrType::ListList
| IrType::Closure
| IrType::Dict => Ok(self.ctx.i32_type()),
}
}
}
#[cfg(test)]
mod const_pool_tests {
use super::*;
use relon_ir::ir::{Func, Op, TaggedOp};
use relon_parser::TokenRange;
fn tagged(op: Op) -> TaggedOp {
TaggedOp {
op,
range: TokenRange::default(),
}
}
fn synth_module(body: Vec<TaggedOp>) -> IrModule {
IrModule {
funcs: vec![Func {
name: "run_main".into(),
params: vec![],
ret: IrType::I64,
body,
range: TokenRange::default(),
}],
entry_func_index: Some(0),
imports: vec![],
closure_table: vec![],
}
}
#[test]
fn const_list_int_byte_layout() {
let pool = ConstPool::from_module(&synth_module(vec![tagged(Op::ConstListInt {
idx: 0,
elements: vec![10, 20, 30],
})]))
.unwrap();
assert_eq!(pool.list_int_offsets.get(&0).copied(), Some(0));
assert_eq!(&pool.bytes[0..4], &3u32.to_le_bytes());
assert_eq!(&pool.bytes[4..8], &[0u8; 4]);
assert_eq!(&pool.bytes[8..16], &10i64.to_le_bytes());
assert_eq!(&pool.bytes[16..24], &20i64.to_le_bytes());
assert_eq!(&pool.bytes[24..32], &30i64.to_le_bytes());
assert_eq!(pool.bytes.len(), 32);
}
#[test]
fn const_list_float_byte_layout() {
let f0 = 1.5f64.to_bits();
let f1 = (-2.0f64).to_bits();
let pool = ConstPool::from_module(&synth_module(vec![tagged(Op::ConstListFloat {
idx: 0,
elements: vec![f0, f1],
})]))
.unwrap();
assert_eq!(pool.list_float_offsets.get(&0).copied(), Some(0));
assert_eq!(&pool.bytes[0..4], &2u32.to_le_bytes());
assert_eq!(&pool.bytes[4..8], &[0u8; 4]);
assert_eq!(&pool.bytes[8..16], &f0.to_le_bytes());
assert_eq!(&pool.bytes[16..24], &f1.to_le_bytes());
assert_eq!(pool.bytes.len(), 24);
}
#[test]
fn const_list_bool_byte_layout() {
let pool = ConstPool::from_module(&synth_module(vec![tagged(Op::ConstListBool {
idx: 0,
elements: vec![true, false, true],
})]))
.unwrap();
assert_eq!(pool.list_bool_offsets.get(&0).copied(), Some(0));
assert_eq!(&pool.bytes[0..4], &3u32.to_le_bytes());
assert_eq!(&pool.bytes[4..7], &[1u8, 0, 1]);
assert_eq!(pool.bytes.len(), 7);
}
#[test]
fn const_list_alignment_across_records() {
let pool = ConstPool::from_module(&synth_module(vec![
tagged(Op::ConstListBool {
idx: 0,
elements: vec![true, false, true],
}),
tagged(Op::ConstListInt {
idx: 1,
elements: vec![42],
}),
]))
.unwrap();
assert_eq!(pool.list_bool_offsets.get(&0).copied(), Some(0));
assert_eq!(pool.list_int_offsets.get(&1).copied(), Some(8));
assert_eq!(&pool.bytes[8..12], &1u32.to_le_bytes());
assert_eq!(&pool.bytes[16..24], &42i64.to_le_bytes());
}
#[test]
fn const_list_string_byte_layout() {
let pool = ConstPool::from_module(&synth_module(vec![tagged(Op::ConstListString {
idx: 0,
elements: vec!["a".into(), "bb".into(), "ccc".into()],
})]))
.unwrap();
assert_eq!(&pool.bytes[0..4], &1u32.to_le_bytes());
assert_eq!(&pool.bytes[4..5], b"a");
assert_eq!(&pool.bytes[8..12], &2u32.to_le_bytes());
assert_eq!(&pool.bytes[12..14], b"bb");
assert_eq!(&pool.bytes[16..20], &3u32.to_le_bytes());
assert_eq!(&pool.bytes[20..23], b"ccc");
let h = pool.list_string_offsets.get(&0).copied();
assert_eq!(h, Some(24));
assert_eq!(&pool.bytes[24..28], &3u32.to_le_bytes());
assert_eq!(&pool.bytes[28..32], &0u32.to_le_bytes());
assert_eq!(&pool.bytes[32..36], &8u32.to_le_bytes());
assert_eq!(&pool.bytes[36..40], &16u32.to_le_bytes());
assert_eq!(pool.bytes.len(), 40);
}
#[test]
fn duplicate_const_list_idx_is_noop() {
let pool = ConstPool::from_module(&synth_module(vec![
tagged(Op::ConstListInt {
idx: 0,
elements: vec![1, 2],
}),
tagged(Op::ConstListInt {
idx: 0,
elements: vec![1, 2],
}),
]))
.unwrap();
assert_eq!(pool.bytes.len(), 24);
}
}
#[cfg(test)]
mod devirt_tests {
use super::*;
use relon_ir::ir::{ClosureCapture, Func, IrType, Op, TaggedOp};
use relon_parser::TokenRange;
fn op(o: Op) -> TaggedOp {
TaggedOp {
op: o,
range: TokenRange::default(),
}
}
fn make_closure(fn_table_idx: u32, captures: Vec<ClosureCapture>) -> Op {
let captures_size = captures.iter().map(|c| c.offset + 8).max().unwrap_or(0);
Op::MakeClosure {
fn_table_idx,
captures,
captures_size,
}
}
fn cap(let_idx: u32, offset: u32) -> ClosureCapture {
ClosureCapture {
let_idx,
ty: IrType::Closure,
offset,
}
}
fn entry_with_body(body: Vec<TaggedOp>) -> Func {
Func {
name: "run_main".into(),
params: vec![IrType::I32],
ret: IrType::I32,
body,
range: TokenRange::default(),
}
}
#[test]
fn records_known_non_self_capture() {
let body = vec![
op(make_closure(0, vec![cap(0, 0)])), op(Op::LetSet {
idx: 0,
ty: IrType::Closure,
}),
op(make_closure(1, vec![cap(0, 0)])), op(Op::Call {
fn_index: 14,
arg_count: 2,
param_tys: vec![IrType::ListInt, IrType::Closure],
ret_ty: IrType::ListInt,
}),
];
let entry = entry_with_body(body);
let table = build_known_capture_table(&entry, &[], &[]);
assert_eq!(
table.get(&1).map(Vec::as_slice),
Some(&[(0u32, 0u32)][..]),
"predicate (L=1) must record its is_prime (K=0) capture as known"
);
assert!(
!table.contains_key(&0),
"self-capture (K==L) must be excluded from the known-capture table"
);
}
#[test]
fn drops_reassigned_dynamic_closure_slot() {
let body = vec![
op(make_closure(0, vec![cap(0, 0)])),
op(Op::LetSet {
idx: 0,
ty: IrType::Closure,
}),
op(Op::LetGet {
idx: 5,
ty: IrType::Closure,
}),
op(Op::LetSet {
idx: 0,
ty: IrType::Closure,
}),
op(make_closure(2, vec![cap(0, 0)])),
op(Op::LetSet {
idx: 9,
ty: IrType::Closure,
}),
];
let entry = entry_with_body(body);
let table = build_known_capture_table(&entry, &[], &[]);
assert!(
!table.contains_key(&2),
"a capture of a reassigned (dynamic) closure slot must NOT be \
recorded — the call must keep the runtime switch"
);
}
#[test]
fn binding_letset_does_not_clear_its_own_slot() {
let body = vec![
op(make_closure(3, vec![])),
op(Op::LetSet {
idx: 7,
ty: IrType::Closure,
}),
op(make_closure(4, vec![cap(7, 0)])),
op(Op::LetSet {
idx: 8,
ty: IrType::Closure,
}),
];
let entry = entry_with_body(body);
let table = build_known_capture_table(&entry, &[], &[]);
assert_eq!(
table.get(&4).map(Vec::as_slice),
Some(&[(0u32, 3u32)][..]),
"L=4 must record its capture of known closure K=3 at offset 0"
);
}
}