#![allow(unsafe_code)]
#![allow(clippy::similar_names)]
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use cranelift_codegen::Context;
use cranelift_codegen::ir::condcodes::{FloatCC, IntCC};
use cranelift_codegen::ir::{AbiParam, BlockArg, InstBuilder, MemFlags, Signature, Value, types};
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::{Linkage, Module};
use crate::ast::projection::{AstNode, AstProjection};
use crate::dag::symbol::{FnId, OpKind, SymbolKind};
use crate::jit::analysis::{NodeAnalysis, PowExpansion, analyze};
use crate::jit::passes;
pub type CompiledExprFn = extern "C" fn(*const f64) -> f64;
pub type CustomFn1 = extern "C" fn(f64) -> f64;
pub type CustomFn2 = extern "C" fn(f64, f64) -> f64;
pub type CustomFn3 = extern "C" fn(f64, f64, f64) -> f64;
pub type CompiledBatchFn =
extern "C" fn(vars_cols: *const *const f64, n_rows: usize, out: *mut f64);
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct OptConfig {
pub max_int_pow: u32,
pub expand_sqrt: bool,
pub allow_reciprocal_math: bool,
pub elide_nan_guard: bool,
pub enable_cse: bool,
}
impl Default for OptConfig {
fn default() -> Self {
Self {
max_int_pow: 16,
expand_sqrt: true,
allow_reciprocal_math: false,
elide_nan_guard: true,
enable_cse: true,
}
}
}
extern "C" fn jit_powf(base: f64, exp: f64) -> f64 {
base.powf(exp)
}
extern "C" fn jit_fmod(lhs: f64, rhs: f64) -> f64 {
lhs % rhs
}
#[derive(Clone, Copy)]
struct CustomFnEntry {
ptr: usize,
arity: u8,
}
type CustomFnRegistry = Arc<RwLock<HashMap<u32, CustomFnEntry>>>;
static JIT_FUNC_COUNTER: AtomicU64 = AtomicU64::new(0);
pub struct JitCompiler {
module: JITModule,
builder_ctx: FunctionBuilderContext,
custom_fns: CustomFnRegistry,
work_stack: Vec<Frame>,
work_values: Vec<Value>,
custom_op_registry: Option<Arc<crate::custom::descriptor::CustomOpRegistry>>,
}
impl std::fmt::Debug for JitCompiler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JitCompiler").finish_non_exhaustive()
}
}
impl Default for JitCompiler {
fn default() -> Self {
Self::new()
}
}
impl JitCompiler {
pub fn try_new() -> Result<Self, crate::error::JitError> {
let mut isa_builder =
cranelift_native::builder().map_err(|_| crate::error::JitError::InitFailed)?;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
for flag in &[
"has_sse3",
"has_ssse3",
"has_sse41",
"has_sse42",
"has_avx",
"has_avx2",
"has_bmi1",
"has_bmi2",
"has_lzcnt",
"has_popcnt",
] {
let _ = cranelift_codegen::settings::Configurable::enable(&mut isa_builder, flag);
}
}
#[cfg(target_arch = "aarch64")]
{
for flag in &["has_lse", "has_pauth", "has_sve"] {
let _ = cranelift_codegen::settings::Configurable::enable(&mut isa_builder, flag);
}
}
let mut flag_builder = cranelift_codegen::settings::builder();
cranelift_codegen::settings::Configurable::set(&mut flag_builder, "opt_level", "speed")
.map_err(|_| crate::error::JitError::InitFailed)?;
let _ = cranelift_codegen::settings::Configurable::set(
&mut flag_builder,
"enable_verifier",
"false",
);
let _ = cranelift_codegen::settings::Configurable::set(
&mut flag_builder,
"enable_probestack",
"false",
);
let isa = isa_builder
.finish(cranelift_codegen::settings::Flags::new(flag_builder))
.map_err(|_| crate::error::JitError::InitFailed)?;
let mut builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
builder.symbol("powf", jit_powf as *const u8);
builder.symbol("fmod", jit_fmod as *const u8);
let custom_fns: CustomFnRegistry = Arc::new(RwLock::new(HashMap::new()));
let lookup_registry = Arc::clone(&custom_fns);
builder.symbol_lookup_fn(Box::new(move |name: &str| -> Option<*const u8> {
let id_str = name.strip_prefix("rssn_custom_fn_")?;
let id: u32 = id_str.parse().ok()?;
let guard = lookup_registry
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.get(&id).map(|entry| entry.ptr as *const u8)
}));
let module = JITModule::new(builder);
Ok(Self {
module,
builder_ctx: FunctionBuilderContext::new(),
custom_fns,
work_stack: Vec::with_capacity(64),
work_values: Vec::with_capacity(64),
custom_op_registry: None,
})
}
#[must_use]
pub fn new() -> Self {
Self::try_new().expect("JIT compiler initialization failed")
}
pub fn register_custom_function(&self, fn_id: FnId, func: CustomFn1) {
let mut guard = self
.custom_fns
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.insert(
fn_id.0,
CustomFnEntry {
ptr: func as usize,
arity: 1,
},
);
}
pub fn register_custom_function_2(&self, fn_id: FnId, func: CustomFn2) {
let mut guard = self
.custom_fns
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.insert(
fn_id.0,
CustomFnEntry {
ptr: func as usize,
arity: 2,
},
);
}
pub fn register_custom_function_3(&self, fn_id: FnId, func: CustomFn3) {
let mut guard = self
.custom_fns
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.insert(
fn_id.0,
CustomFnEntry {
ptr: func as usize,
arity: 3,
},
);
}
pub fn set_custom_op_registry(
&mut self,
registry: Arc<crate::custom::descriptor::CustomOpRegistry>,
) {
let mut guard = self
.custom_fns
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for desc in registry.ops_iter() {
let (ptr, arity) = match desc.eval_fn {
crate::custom::descriptor::EvalFn::Arity1(f) => (f as usize, 1u8),
crate::custom::descriptor::EvalFn::Arity2(f) => (f as usize, 2u8),
crate::custom::descriptor::EvalFn::Arity3(f) => (f as usize, 3u8),
};
guard.insert(desc.fn_id.0, CustomFnEntry { ptr, arity });
}
drop(guard);
self.custom_op_registry = Some(registry);
}
pub fn compile(
&mut self,
ast: &AstProjection,
) -> Result<CompiledExprFn, crate::error::JitError> {
self.compile_with_opts(ast, &OptConfig::default())
}
pub fn compile_with_opts(
&mut self,
ast: &AstProjection,
opts: &OptConfig,
) -> Result<CompiledExprFn, crate::error::JitError> {
if ast.is_empty() {
return crate::error::cold_jit_error_malformed_node();
}
let analysis = analyze(ast);
let mut ctx = Context::new();
ctx.func.signature.call_conv = self.module.target_config().default_call_conv;
let ptr_type = self.module.target_config().pointer_type();
ctx.func.signature.params.push(AbiParam::new(ptr_type));
ctx.func.signature.returns.push(AbiParam::new(types::F64));
let mut func_builder = FunctionBuilder::new(&mut ctx.func, &mut self.builder_ctx);
let entry_block = func_builder.create_block();
func_builder.append_block_params_for_function_params(entry_block);
func_builder.switch_to_block(entry_block);
func_builder.seal_block(entry_block);
let vars_ptr = func_builder.block_params(entry_block)[0];
let mut powf_sig = Signature::new(self.module.target_config().default_call_conv);
powf_sig.params.push(AbiParam::new(types::F64));
powf_sig.params.push(AbiParam::new(types::F64));
powf_sig.returns.push(AbiParam::new(types::F64));
let powf_name = self
.module
.declare_function("powf", Linkage::Import, &powf_sig)
.map_err(|_| crate::error::JitError::InitFailed)?;
let powf_func_ref = self
.module
.declare_func_in_func(powf_name, func_builder.func);
let fmod_name = self
.module
.declare_function("fmod", Linkage::Import, &powf_sig)
.map_err(|_| crate::error::JitError::InitFailed)?;
let fmod_func_ref = self
.module
.declare_func_in_func(fmod_name, func_builder.func);
let mut custom_refs: HashMap<u32, cranelift_codegen::ir::FuncRef> = HashMap::new();
let registered_entries: HashMap<u32, u8> = {
let guard = self
.custom_fns
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.iter().map(|(&id, e)| (id, e.arity)).collect()
};
let default_call_conv = self.module.target_config().default_call_conv;
let make_fn_sig = |arity: u8| -> Signature {
let mut sig = Signature::new(default_call_conv);
for _ in 0..arity {
sig.params.push(AbiParam::new(types::F64));
}
sig.returns.push(AbiParam::new(types::F64));
sig
};
for node in &ast.nodes {
if let SymbolKind::Function(fn_id) = node.kind {
if custom_refs.contains_key(&fn_id.0) {
continue;
}
let arity = registered_entries
.get(&fn_id.0)
.copied()
.ok_or(crate::error::JitError::UnknownFunction)?;
let sig = make_fn_sig(arity);
let sym = format!("rssn_custom_fn_{}", fn_id.0);
let fid = self
.module
.declare_function(&sym, Linkage::Import, &sig)
.map_err(|_| crate::error::JitError::InitFailed)?;
let fr = self.module.declare_func_in_func(fid, func_builder.func);
custom_refs.insert(fn_id.0, fr);
}
}
self.work_stack.clear();
self.work_values.clear();
let root_val = compile_ast_iterative(
ast,
&analysis,
opts,
&mut func_builder,
vars_ptr,
powf_func_ref,
fmod_func_ref,
&custom_refs,
&mut self.work_stack,
&mut self.work_values,
)?;
func_builder.ins().return_(&[root_val]);
func_builder.finalize();
let fn_name = format!("expr_{}", JIT_FUNC_COUNTER.fetch_add(1, Ordering::Relaxed));
let func_id = self
.module
.declare_function(&fn_name, Linkage::Export, &ctx.func.signature)
.map_err(|_| crate::error::JitError::VerifierRejected)?;
self.module
.define_function(func_id, &mut ctx)
.map_err(|_| crate::error::JitError::VerifierRejected)?;
self.module.clear_context(&mut ctx);
self.module
.finalize_definitions()
.map_err(|_| crate::error::JitError::VerifierRejected)?;
let code_ptr = self.module.get_finalized_function(func_id);
let compiled_fn: CompiledExprFn = unsafe { std::mem::transmute(code_ptr) };
Ok(compiled_fn)
}
pub fn compile_batch_f64x2(
&mut self,
ast: &AstProjection,
) -> Result<Option<CompiledBatchFn>, crate::error::JitError> {
if ast.is_empty() {
return crate::error::cold_jit_error_malformed_node().map(Some);
}
let analysis = analyze(ast);
let opts = OptConfig::default();
if !is_vectorizable_ast(ast, &analysis, self.custom_op_registry.as_deref()) {
return Ok(None);
}
let sym_ids: Vec<u32> = {
let mut seen: HashSet<u32> = HashSet::new();
let mut ordered: Vec<u32> = Vec::new();
for node in &ast.nodes {
if let SymbolKind::Variable(sid) = node.kind
&& seen.insert(sid.0)
{
ordered.push(sid.0);
}
}
ordered
};
let mut ctx = Context::new();
ctx.func.signature.call_conv = self.module.target_config().default_call_conv;
let ptr_type = self.module.target_config().pointer_type();
ctx.func.signature.params.push(AbiParam::new(ptr_type)); ctx.func.signature.params.push(AbiParam::new(ptr_type)); ctx.func.signature.params.push(AbiParam::new(ptr_type));
let mut func_builder = FunctionBuilder::new(&mut ctx.func, &mut self.builder_ctx);
let mut powf_sig = Signature::new(self.module.target_config().default_call_conv);
powf_sig.params.push(AbiParam::new(types::F64));
powf_sig.params.push(AbiParam::new(types::F64));
powf_sig.returns.push(AbiParam::new(types::F64));
let powf_name = self
.module
.declare_function("powf", Linkage::Import, &powf_sig)
.map_err(|_| crate::error::JitError::InitFailed)?;
let entry_block = func_builder.create_block();
let loop_check = func_builder.create_block();
let vec_body = func_builder.create_block();
let scalar_check = func_builder.create_block();
let scalar_body = func_builder.create_block();
let ret_block = func_builder.create_block();
func_builder.append_block_params_for_function_params(entry_block);
func_builder.append_block_param(loop_check, ptr_type); func_builder.append_block_param(vec_body, ptr_type); func_builder.append_block_param(scalar_check, ptr_type); func_builder.append_block_param(scalar_body, ptr_type);
func_builder.switch_to_block(entry_block);
let params = func_builder.block_params(entry_block);
let vars_cols_val = params[0];
let n_rows_val = params[1];
let out_ptr_val = params[2];
let ptr_size = i64::from(ptr_type.bytes());
let mut var_col_ptrs = HashMap::new();
for &sid in &sym_ids {
let col_offset = func_builder
.ins()
.iconst(ptr_type, i64::from(sid).wrapping_mul(ptr_size));
let col_ptr_addr = func_builder.ins().iadd(vars_cols_val, col_offset);
let col_ptr = func_builder
.ins()
.load(ptr_type, MemFlags::new(), col_ptr_addr, 0);
var_col_ptrs.insert(sid, col_ptr);
}
let zero_i = func_builder.ins().iconst(ptr_type, 0);
let zero_i_ba = BlockArg::Value(zero_i);
func_builder.ins().jump(loop_check, &[zero_i_ba]);
func_builder.seal_block(entry_block);
func_builder.switch_to_block(loop_check);
let i_lc = func_builder.block_params(loop_check)[0];
let remaining = func_builder.ins().isub(n_rows_val, i_lc);
let two_i = func_builder.ins().iconst(ptr_type, 2);
let can_vec = func_builder
.ins()
.icmp(IntCC::UnsignedGreaterThanOrEqual, remaining, two_i);
let i_lc_ba = BlockArg::Value(i_lc);
func_builder
.ins()
.brif(can_vec, vec_body, &[i_lc_ba], scalar_check, &[i_lc_ba]);
func_builder.switch_to_block(vec_body);
let i_vb = func_builder.block_params(vec_body)[0];
let byte_off_vec = func_builder.ins().ishl_imm(i_vb, 3);
let mut var_vals_vec: HashMap<u32, Value> = HashMap::new();
for &sid in &sym_ids {
let col_ptr = *var_col_ptrs
.get(&sid)
.ok_or(crate::error::JitError::MalformedNode)?;
let elem_addr = func_builder.ins().iadd(col_ptr, byte_off_vec);
let vec_val = func_builder
.ins()
.load(types::F64X2, MemFlags::new(), elem_addr, 0);
var_vals_vec.insert(sid, vec_val);
}
let powf_func_ref_vb = self
.module
.declare_func_in_func(powf_name, func_builder.func);
let result_vec = emit_ast_simd_f64x2(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals_vec,
powf_func_ref_vb,
)?;
let out_addr_vec = func_builder.ins().iadd(out_ptr_val, byte_off_vec);
func_builder
.ins()
.store(MemFlags::new(), result_vec, out_addr_vec, 0);
let i_vb_next = func_builder.ins().iadd_imm(i_vb, 2);
let i_vb_next_ba = BlockArg::Value(i_vb_next);
func_builder.ins().jump(loop_check, &[i_vb_next_ba]);
func_builder.seal_block(loop_check);
func_builder.seal_block(vec_body);
func_builder.switch_to_block(scalar_check);
let i_scheck = func_builder.block_params(scalar_check)[0];
let done = func_builder
.ins()
.icmp(IntCC::UnsignedGreaterThanOrEqual, i_scheck, n_rows_val);
let i_scheck_ba = BlockArg::Value(i_scheck);
func_builder.ins().brif(
done,
ret_block,
&[] as &[BlockArg],
scalar_body,
&[i_scheck_ba],
);
func_builder.switch_to_block(scalar_body);
let i_sbody = func_builder.block_params(scalar_body)[0];
let byte_off_sb = func_builder.ins().ishl_imm(i_sbody, 3);
let mut var_vals_sb: HashMap<u32, Value> = HashMap::new();
for &sid in &sym_ids {
let col_ptr = *var_col_ptrs
.get(&sid)
.ok_or(crate::error::JitError::MalformedNode)?;
let addr = func_builder.ins().iadd(col_ptr, byte_off_sb);
let v = func_builder
.ins()
.load(types::F64, MemFlags::new(), addr, 0);
var_vals_sb.insert(sid, v);
}
let powf_ref_sbody = self
.module
.declare_func_in_func(powf_name, func_builder.func);
let mut work_stack_sb: Vec<Frame> = Vec::with_capacity(32);
let mut work_vals_sb: Vec<Value> = Vec::with_capacity(32);
let res_sb = emit_ast_scalar_with_vars(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals_sb,
powf_ref_sbody,
&HashMap::new(),
&mut work_stack_sb,
&mut work_vals_sb,
)?;
let out_addr_sb = func_builder.ins().iadd(out_ptr_val, byte_off_sb);
func_builder
.ins()
.store(MemFlags::new(), res_sb, out_addr_sb, 0);
let i_sbody_next = func_builder.ins().iadd_imm(i_sbody, 1);
let i_sbody_next_ba = BlockArg::Value(i_sbody_next);
func_builder.ins().jump(scalar_check, &[i_sbody_next_ba]);
func_builder.seal_block(scalar_check);
func_builder.seal_block(scalar_body);
func_builder.switch_to_block(ret_block);
func_builder.ins().return_(&[]);
func_builder.seal_block(ret_block);
func_builder.finalize();
let fn_name = format!(
"batch_expr_{}",
JIT_FUNC_COUNTER.fetch_add(1, Ordering::Relaxed)
);
let func_id = self
.module
.declare_function(&fn_name, Linkage::Export, &ctx.func.signature)
.map_err(|_| crate::error::JitError::VerifierRejected)?;
self.module
.define_function(func_id, &mut ctx)
.map_err(|_| crate::error::JitError::VerifierRejected)?;
self.module.clear_context(&mut ctx);
self.module
.finalize_definitions()
.map_err(|_| crate::error::JitError::VerifierRejected)?;
let code_ptr = self.module.get_finalized_function(func_id);
let batch_fn: CompiledBatchFn = unsafe { std::mem::transmute(code_ptr) };
Ok(Some(batch_fn))
}
pub fn compile_batch_f64x4(
&mut self,
ast: &AstProjection,
) -> Result<Option<CompiledBatchFn>, crate::error::JitError> {
if ast.is_empty() {
return crate::error::cold_jit_error_malformed_node().map(Some);
}
let analysis = analyze(ast);
let opts = OptConfig::default();
if !is_vectorizable_ast(ast, &analysis, self.custom_op_registry.as_deref()) {
return Ok(None);
}
let sym_ids: Vec<u32> = {
let mut seen: HashSet<u32> = HashSet::new();
let mut ordered: Vec<u32> = Vec::new();
for node in &ast.nodes {
if let SymbolKind::Variable(sid) = node.kind
&& seen.insert(sid.0)
{
ordered.push(sid.0);
}
}
ordered
};
let mut ctx = Context::new();
ctx.func.signature.call_conv = self.module.target_config().default_call_conv;
let ptr_type = self.module.target_config().pointer_type();
ctx.func.signature.params.push(AbiParam::new(ptr_type)); ctx.func.signature.params.push(AbiParam::new(ptr_type)); ctx.func.signature.params.push(AbiParam::new(ptr_type));
let mut func_builder = FunctionBuilder::new(&mut ctx.func, &mut self.builder_ctx);
let mut powf_sig = Signature::new(self.module.target_config().default_call_conv);
powf_sig.params.push(AbiParam::new(types::F64));
powf_sig.params.push(AbiParam::new(types::F64));
powf_sig.returns.push(AbiParam::new(types::F64));
let powf_name = self
.module
.declare_function("powf", Linkage::Import, &powf_sig)
.map_err(|_| crate::error::JitError::InitFailed)?;
let entry_block = func_builder.create_block();
let loop_check = func_builder.create_block();
let vec_body = func_builder.create_block();
let scalar_check = func_builder.create_block();
let scalar_body = func_builder.create_block();
let ret_block = func_builder.create_block();
func_builder.append_block_params_for_function_params(entry_block);
func_builder.append_block_param(loop_check, ptr_type); func_builder.append_block_param(vec_body, ptr_type); func_builder.append_block_param(scalar_check, ptr_type); func_builder.append_block_param(scalar_body, ptr_type);
func_builder.switch_to_block(entry_block);
let params = func_builder.block_params(entry_block);
let vars_cols_val = params[0];
let n_rows_val = params[1];
let out_ptr_val = params[2];
let ptr_size = i64::from(ptr_type.bytes());
let mut var_col_ptrs = HashMap::new();
for &sid in &sym_ids {
let col_offset = func_builder
.ins()
.iconst(ptr_type, i64::from(sid).wrapping_mul(ptr_size));
let col_ptr_addr = func_builder.ins().iadd(vars_cols_val, col_offset);
let col_ptr = func_builder
.ins()
.load(ptr_type, MemFlags::new(), col_ptr_addr, 0);
var_col_ptrs.insert(sid, col_ptr);
}
let zero_i = func_builder.ins().iconst(ptr_type, 0);
let zero_i_ba = BlockArg::Value(zero_i);
func_builder.ins().jump(loop_check, &[zero_i_ba]);
func_builder.seal_block(entry_block);
func_builder.switch_to_block(loop_check);
let i_lc = func_builder.block_params(loop_check)[0];
let remaining = func_builder.ins().isub(n_rows_val, i_lc);
let four_i = func_builder.ins().iconst(ptr_type, 4);
let can_vec = func_builder
.ins()
.icmp(IntCC::UnsignedGreaterThanOrEqual, remaining, four_i);
let i_lc_ba = BlockArg::Value(i_lc);
func_builder
.ins()
.brif(can_vec, vec_body, &[i_lc_ba], scalar_check, &[i_lc_ba]);
func_builder.switch_to_block(vec_body);
let i_vb = func_builder.block_params(vec_body)[0];
let byte_off_vec = func_builder.ins().ishl_imm(i_vb, 3);
let mut var_vals_vec1: HashMap<u32, Value> = HashMap::new();
let mut var_vals_vec2: HashMap<u32, Value> = HashMap::new();
for &sid in &sym_ids {
let col_ptr = *var_col_ptrs
.get(&sid)
.ok_or(crate::error::JitError::MalformedNode)?;
let elem_addr1 = func_builder.ins().iadd(col_ptr, byte_off_vec);
let vec_val1 = func_builder
.ins()
.load(types::F64X2, MemFlags::new(), elem_addr1, 0);
var_vals_vec1.insert(sid, vec_val1);
let elem_addr2 = func_builder.ins().iadd_imm(elem_addr1, 16); let vec_val2 = func_builder
.ins()
.load(types::F64X2, MemFlags::new(), elem_addr2, 0);
var_vals_vec2.insert(sid, vec_val2);
}
let powf_func_ref_vb = self
.module
.declare_func_in_func(powf_name, func_builder.func);
let result_vec1 = emit_ast_simd_f64x2(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals_vec1,
powf_func_ref_vb,
)?;
let result_vec2 = emit_ast_simd_f64x2(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals_vec2,
powf_func_ref_vb,
)?;
let out_addr1 = func_builder.ins().iadd(out_ptr_val, byte_off_vec);
func_builder
.ins()
.store(MemFlags::new(), result_vec1, out_addr1, 0);
let out_addr2 = func_builder.ins().iadd_imm(out_addr1, 16);
func_builder
.ins()
.store(MemFlags::new(), result_vec2, out_addr2, 0);
let i_vb_next = func_builder.ins().iadd_imm(i_vb, 4);
let i_vb_next_ba = BlockArg::Value(i_vb_next);
func_builder.ins().jump(loop_check, &[i_vb_next_ba]);
func_builder.seal_block(loop_check);
func_builder.seal_block(vec_body);
func_builder.switch_to_block(scalar_check);
let i_scheck = func_builder.block_params(scalar_check)[0];
let done = func_builder
.ins()
.icmp(IntCC::UnsignedGreaterThanOrEqual, i_scheck, n_rows_val);
let i_scheck_ba = BlockArg::Value(i_scheck);
func_builder.ins().brif(
done,
ret_block,
&[] as &[BlockArg],
scalar_body,
&[i_scheck_ba],
);
func_builder.switch_to_block(scalar_body);
let i_sbody = func_builder.block_params(scalar_body)[0];
let byte_off_sb = func_builder.ins().ishl_imm(i_sbody, 3);
let mut var_vals_sb: HashMap<u32, Value> = HashMap::new();
for &sid in &sym_ids {
let col_ptr = *var_col_ptrs
.get(&sid)
.ok_or(crate::error::JitError::MalformedNode)?;
let addr = func_builder.ins().iadd(col_ptr, byte_off_sb);
let v = func_builder
.ins()
.load(types::F64, MemFlags::new(), addr, 0);
var_vals_sb.insert(sid, v);
}
let powf_ref_sbody = self
.module
.declare_func_in_func(powf_name, func_builder.func);
let mut work_stack_sb: Vec<Frame> = Vec::with_capacity(32);
let mut work_vals_sb: Vec<Value> = Vec::with_capacity(32);
let res_sb = emit_ast_scalar_with_vars(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals_sb,
powf_ref_sbody,
&HashMap::new(),
&mut work_stack_sb,
&mut work_vals_sb,
)?;
let out_addr_sb = func_builder.ins().iadd(out_ptr_val, byte_off_sb);
func_builder
.ins()
.store(MemFlags::new(), res_sb, out_addr_sb, 0);
let i_sbody_next = func_builder.ins().iadd_imm(i_sbody, 1);
let i_sbody_next_ba = BlockArg::Value(i_sbody_next);
func_builder.ins().jump(scalar_check, &[i_sbody_next_ba]);
func_builder.seal_block(scalar_check);
func_builder.seal_block(scalar_body);
func_builder.switch_to_block(ret_block);
func_builder.ins().return_(&[]);
func_builder.seal_block(ret_block);
func_builder.finalize();
let fn_name = format!(
"batch_expr_{}",
JIT_FUNC_COUNTER.fetch_add(1, Ordering::Relaxed)
);
let func_id = self
.module
.declare_function(&fn_name, Linkage::Export, &ctx.func.signature)
.map_err(|_| crate::error::JitError::VerifierRejected)?;
self.module
.define_function(func_id, &mut ctx)
.map_err(|_| crate::error::JitError::VerifierRejected)?;
self.module.clear_context(&mut ctx);
self.module
.finalize_definitions()
.map_err(|_| crate::error::JitError::VerifierRejected)?;
let code_ptr = self.module.get_finalized_function(func_id);
let batch_fn: CompiledBatchFn = unsafe { std::mem::transmute(code_ptr) };
Ok(Some(batch_fn))
}
pub fn compile_batch_f64x8(
&mut self,
ast: &AstProjection,
) -> Result<Option<CompiledBatchFn>, crate::error::JitError> {
if ast.is_empty() {
return crate::error::cold_jit_error_malformed_node().map(Some);
}
let analysis = analyze(ast);
let opts = OptConfig::default();
if !is_vectorizable_ast(ast, &analysis, self.custom_op_registry.as_deref()) {
return Ok(None);
}
let sym_ids: Vec<u32> = {
let mut seen: HashSet<u32> = HashSet::new();
let mut ordered: Vec<u32> = Vec::new();
for node in &ast.nodes {
if let SymbolKind::Variable(sid) = node.kind
&& seen.insert(sid.0)
{
ordered.push(sid.0);
}
}
ordered
};
let mut ctx = Context::new();
ctx.func.signature.call_conv = self.module.target_config().default_call_conv;
let ptr_type = self.module.target_config().pointer_type();
ctx.func.signature.params.push(AbiParam::new(ptr_type)); ctx.func.signature.params.push(AbiParam::new(ptr_type)); ctx.func.signature.params.push(AbiParam::new(ptr_type));
let mut func_builder = FunctionBuilder::new(&mut ctx.func, &mut self.builder_ctx);
let mut powf_sig = Signature::new(self.module.target_config().default_call_conv);
powf_sig.params.push(AbiParam::new(types::F64));
powf_sig.params.push(AbiParam::new(types::F64));
powf_sig.returns.push(AbiParam::new(types::F64));
let powf_name = self
.module
.declare_function("powf", Linkage::Import, &powf_sig)
.map_err(|_| crate::error::JitError::InitFailed)?;
let entry_block = func_builder.create_block();
let loop_check = func_builder.create_block();
let vec_body = func_builder.create_block();
let scalar_check = func_builder.create_block();
let scalar_body = func_builder.create_block();
let ret_block = func_builder.create_block();
func_builder.append_block_params_for_function_params(entry_block);
func_builder.append_block_param(loop_check, ptr_type);
func_builder.append_block_param(vec_body, ptr_type);
func_builder.append_block_param(scalar_check, ptr_type);
func_builder.append_block_param(scalar_body, ptr_type);
func_builder.switch_to_block(entry_block);
let params = func_builder.block_params(entry_block);
let vars_cols_val = params[0];
let n_rows_val = params[1];
let out_ptr_val = params[2];
let ptr_size = i64::from(ptr_type.bytes());
let mut var_col_ptrs: HashMap<u32, Value> = HashMap::new();
for &sid in &sym_ids {
let col_offset = func_builder
.ins()
.iconst(ptr_type, i64::from(sid).wrapping_mul(ptr_size));
let col_ptr_addr = func_builder.ins().iadd(vars_cols_val, col_offset);
let col_ptr = func_builder
.ins()
.load(ptr_type, MemFlags::new(), col_ptr_addr, 0);
var_col_ptrs.insert(sid, col_ptr);
}
let zero_i = func_builder.ins().iconst(ptr_type, 0);
func_builder
.ins()
.jump(loop_check, &[BlockArg::Value(zero_i)]);
func_builder.seal_block(entry_block);
func_builder.switch_to_block(loop_check);
let i_lc = func_builder.block_params(loop_check)[0];
let remaining = func_builder.ins().isub(n_rows_val, i_lc);
let eight_i = func_builder.ins().iconst(ptr_type, 8);
let can_vec =
func_builder
.ins()
.icmp(IntCC::UnsignedGreaterThanOrEqual, remaining, eight_i);
func_builder.ins().brif(
can_vec,
vec_body,
&[BlockArg::Value(i_lc)],
scalar_check,
&[BlockArg::Value(i_lc)],
);
func_builder.switch_to_block(vec_body);
let i_vb = func_builder.block_params(vec_body)[0];
let byte_off = func_builder.ins().ishl_imm(i_vb, 3);
let offsets: [i64; 4] = [0, 16, 32, 48]; let mut var_vals: [HashMap<u32, Value>; 4] = [
HashMap::new(),
HashMap::new(),
HashMap::new(),
HashMap::new(),
];
for &sid in &sym_ids {
let col_ptr = *var_col_ptrs
.get(&sid)
.ok_or(crate::error::JitError::MalformedNode)?;
let base = func_builder.ins().iadd(col_ptr, byte_off);
for (k, &off) in offsets.iter().enumerate() {
let addr = if off == 0 {
base
} else {
func_builder.ins().iadd_imm(base, off)
};
let v = func_builder
.ins()
.load(types::F64X2, MemFlags::new(), addr, 0);
var_vals[k].insert(sid, v);
}
}
let powf_ref_vb = self
.module
.declare_func_in_func(powf_name, func_builder.func);
let results: [Value; 4] = [
emit_ast_simd_f64x2(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals[0],
powf_ref_vb,
)?,
emit_ast_simd_f64x2(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals[1],
powf_ref_vb,
)?,
emit_ast_simd_f64x2(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals[2],
powf_ref_vb,
)?,
emit_ast_simd_f64x2(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals[3],
powf_ref_vb,
)?,
];
let out_base = func_builder.ins().iadd(out_ptr_val, byte_off);
for (k, &off) in offsets.iter().enumerate() {
let addr = if off == 0 {
out_base
} else {
func_builder.ins().iadd_imm(out_base, off)
};
func_builder
.ins()
.store(MemFlags::new(), results[k], addr, 0);
}
let i_next = func_builder.ins().iadd_imm(i_vb, 8);
func_builder
.ins()
.jump(loop_check, &[BlockArg::Value(i_next)]);
func_builder.seal_block(loop_check);
func_builder.seal_block(vec_body);
func_builder.switch_to_block(scalar_check);
let i_sc = func_builder.block_params(scalar_check)[0];
let done = func_builder
.ins()
.icmp(IntCC::UnsignedGreaterThanOrEqual, i_sc, n_rows_val);
func_builder.ins().brif(
done,
ret_block,
&[] as &[BlockArg],
scalar_body,
&[BlockArg::Value(i_sc)],
);
func_builder.switch_to_block(scalar_body);
let i_sb = func_builder.block_params(scalar_body)[0];
let byte_off_sb = func_builder.ins().ishl_imm(i_sb, 3);
let mut var_vals_sb: HashMap<u32, Value> = HashMap::new();
for &sid in &sym_ids {
let col_ptr = *var_col_ptrs
.get(&sid)
.ok_or(crate::error::JitError::MalformedNode)?;
let addr = func_builder.ins().iadd(col_ptr, byte_off_sb);
let v = func_builder
.ins()
.load(types::F64, MemFlags::new(), addr, 0);
var_vals_sb.insert(sid, v);
}
let powf_ref_sb = self
.module
.declare_func_in_func(powf_name, func_builder.func);
let mut wstack: Vec<Frame> = Vec::with_capacity(32);
let mut wvals: Vec<Value> = Vec::with_capacity(32);
let res_sb = emit_ast_scalar_with_vars(
ast,
&analysis,
&opts,
&mut func_builder,
&var_vals_sb,
powf_ref_sb,
&HashMap::new(),
&mut wstack,
&mut wvals,
)?;
let out_addr_sb = func_builder.ins().iadd(out_ptr_val, byte_off_sb);
func_builder
.ins()
.store(MemFlags::new(), res_sb, out_addr_sb, 0);
let i_sb_next = func_builder.ins().iadd_imm(i_sb, 1);
func_builder
.ins()
.jump(scalar_check, &[BlockArg::Value(i_sb_next)]);
func_builder.seal_block(scalar_check);
func_builder.seal_block(scalar_body);
func_builder.switch_to_block(ret_block);
func_builder.ins().return_(&[]);
func_builder.seal_block(ret_block);
func_builder.finalize();
let fn_name = format!(
"batch8_expr_{}",
JIT_FUNC_COUNTER.fetch_add(1, Ordering::Relaxed)
);
let func_id = self
.module
.declare_function(&fn_name, Linkage::Export, &ctx.func.signature)
.map_err(|_| crate::error::JitError::VerifierRejected)?;
if let Err(e) = self.module.define_function(func_id, &mut ctx) {
eprintln!("JIT f64x8 define_function failed: {e:?}");
return Err(crate::error::JitError::VerifierRejected);
}
self.module.clear_context(&mut ctx);
if let Err(e) = self.module.finalize_definitions() {
eprintln!("JIT f64x8 finalize_definitions failed: {e:?}");
return Err(crate::error::JitError::VerifierRejected);
}
let code_ptr = self.module.get_finalized_function(func_id);
let batch_fn: CompiledBatchFn = unsafe { std::mem::transmute(code_ptr) };
Ok(Some(batch_fn))
}
pub fn compile_dag(
&mut self,
arena: &crate::dag::arena::DagArena,
root: crate::dag::node::DagNodeId,
) -> Result<CompiledExprFn, crate::error::JitError> {
let ast = crate::ast::convert::dag_to_ast(arena, root);
self.compile(&ast)
}
}
struct Frame {
idx: usize,
arity: usize,
cursor: usize,
}
#[allow(clippy::too_many_arguments)] fn compile_ast_iterative(
ast: &AstProjection,
analysis: &[NodeAnalysis],
opts: &OptConfig,
builder: &mut FunctionBuilder<'_>,
vars_ptr: Value,
powf_func_ref: cranelift_codegen::ir::FuncRef,
fmod_func_ref: cranelift_codegen::ir::FuncRef,
custom_refs: &HashMap<u32, cranelift_codegen::ir::FuncRef>,
stack: &mut Vec<Frame>,
values: &mut Vec<Value>,
) -> Result<Value, crate::error::JitError> {
let mut mul_factors: HashMap<Value, (Value, Value)> = HashMap::new();
let duplicate_dag_ids: HashSet<u32> = if opts.enable_cse {
let mut dag_id_count: HashMap<u32, u32> = HashMap::new();
for node in &ast.nodes {
*dag_id_count.entry(node.dag_id.0).or_insert(0) = dag_id_count
.get(&node.dag_id.0)
.copied()
.unwrap_or(0)
.saturating_add(1);
}
dag_id_count
.into_iter()
.filter(|&(_, c)| c > 1)
.map(|(id, _)| id)
.collect()
} else {
HashSet::new()
};
let mut cse_map: HashMap<u32, Value> = HashMap::new();
let root_node = ast
.nodes
.first()
.ok_or(crate::error::JitError::MalformedNode)?;
stack.push(Frame {
idx: 0,
arity: root_node.children.len(),
cursor: 0,
});
while let Some(top) = stack.last_mut() {
if opts.enable_cse && top.cursor == 0 && !duplicate_dag_ids.is_empty() {
let dag_id = ast.nodes.get(top.idx).map_or(u32::MAX, |n| n.dag_id.0);
if duplicate_dag_ids.contains(&dag_id)
&& let Some(&cached) = cse_map.get(&dag_id)
{
stack.pop();
values.push(cached);
continue;
}
}
let action = if top.cursor < top.arity {
let Some(node) = ast.nodes.get(top.idx) else {
return Err(crate::error::JitError::MalformedNode);
};
let Some(&child_ptr) = node.children.as_slice().get(top.cursor) else {
return Err(crate::error::JitError::MalformedNode);
};
let child_idx = child_ptr
.resolve(top.idx)
.ok_or(crate::error::JitError::MalformedNode)?;
top.cursor += 1;
Action::Descend(child_idx)
} else {
Action::Emit(top.idx, top.arity)
};
match action {
Action::Descend(child_idx) => {
let Some(child_node) = ast.nodes.get(child_idx) else {
return Err(crate::error::JitError::MalformedNode);
};
stack.push(Frame {
idx: child_idx,
arity: child_node.children.len(),
cursor: 0,
});
}
Action::Emit(idx, arity) => {
stack.pop();
emit_one_node(
ast,
analysis,
opts,
idx,
arity,
builder,
vars_ptr,
powf_func_ref,
fmod_func_ref,
custom_refs,
values,
&mut mul_factors,
)?;
if opts.enable_cse
&& !duplicate_dag_ids.is_empty()
&& let Some(node) = ast.nodes.get(idx)
{
let dag_id = node.dag_id.0;
if duplicate_dag_ids.contains(&dag_id)
&& let Some(&v) = values.last()
{
cse_map.insert(dag_id, v);
}
}
}
}
}
let result = values.pop().ok_or(crate::error::JitError::MalformedNode)?;
if !values.is_empty() {
return Err(crate::error::JitError::VerifierRejected);
}
Ok(result)
}
enum Action {
Descend(usize),
Emit(usize, usize),
}
#[allow(clippy::too_many_arguments)]
fn emit_one_node(
ast: &AstProjection,
analysis: &[NodeAnalysis],
opts: &OptConfig,
idx: usize,
arity: usize,
builder: &mut FunctionBuilder<'_>,
vars_ptr: Value,
powf_func_ref: cranelift_codegen::ir::FuncRef,
fmod_func_ref: cranelift_codegen::ir::FuncRef,
custom_refs: &HashMap<u32, cranelift_codegen::ir::FuncRef>,
values: &mut Vec<Value>,
mul_factors: &mut HashMap<Value, (Value, Value)>,
) -> Result<(), crate::error::JitError> {
let node = ast
.nodes
.get(idx)
.ok_or(crate::error::JitError::MalformedNode)?;
match node.kind {
SymbolKind::Constant(_) => {
values.push(builder.ins().f64const(node.value)); }
SymbolKind::Variable(sym_id) => {
let val = emit_variable_load(builder, vars_ptr, sym_id.0);
values.push(val);
}
SymbolKind::Operator(op) => {
let split_at = values
.len()
.checked_sub(arity)
.ok_or(crate::error::JitError::MalformedNode)?;
let child_vals: Vec<Value> = values.drain(split_at..).collect();
let children = node.children.as_slice_with_pool(&ast.children_pool);
let mut child_analyses: Vec<Option<&NodeAnalysis>> = children
.iter()
.map(|ptr| ptr.resolve(idx).and_then(|ci| analysis.get(ci)))
.collect();
child_analyses.push(analysis.get(idx));
let result = emit_operator(
builder,
op,
&child_vals,
powf_func_ref,
fmod_func_ref,
node,
&child_analyses,
opts,
mul_factors,
)?;
values.push(result);
}
SymbolKind::Function(fn_id) => {
let func_ref = custom_refs
.get(&fn_id.0)
.copied()
.ok_or(crate::error::JitError::UnknownFunction)?;
let split_at = values
.len()
.checked_sub(arity)
.ok_or(crate::error::JitError::MalformedNode)?;
let child_vals: Vec<Value> = values.drain(split_at..).collect();
if child_vals.is_empty() || child_vals.len() > 3 {
return Err(crate::error::JitError::MalformedNode);
}
let call = builder.ins().call(func_ref, &child_vals);
let result = builder
.inst_results(call)
.first()
.copied()
.ok_or(crate::error::JitError::VerifierRejected)?;
values.push(result);
}
}
Ok(())
}
fn emit_variable_load(builder: &mut FunctionBuilder<'_>, vars_ptr: Value, sym_idx: u32) -> Value {
let offset = i64::from(sym_idx).wrapping_mul(8);
let addr = builder.ins().iadd_imm(vars_ptr, offset);
builder.ins().load(types::F64, MemFlags::new(), addr, 0)
}
#[allow(clippy::too_many_arguments)]
fn emit_operator(
builder: &mut FunctionBuilder<'_>,
op: OpKind,
child_vals: &[Value],
powf_func_ref: cranelift_codegen::ir::FuncRef,
fmod_func_ref: cranelift_codegen::ir::FuncRef,
ast_node: &AstNode,
child_analyses: &[Option<&NodeAnalysis>],
opts: &OptConfig,
mul_factors: &mut HashMap<Value, (Value, Value)>,
) -> Result<Value, crate::error::JitError> {
use crate::jit::primitives::{simplify_add, simplify_mul};
let constants: Vec<Option<f64>> = child_vals
.iter()
.map(|v| constant_behind(builder, *v))
.collect();
let arity = child_vals.len();
let _ = ast_node;
match op {
OpKind::Add => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
match (constants[0], constants[1]) {
(Some(l), Some(r)) => {
let folded = simplify_add(l, r).unwrap_or(l + r);
Ok(builder.ins().f64const(folded))
}
(Some(0.0), _) => Ok(child_vals[1]),
(_, Some(0.0)) => Ok(child_vals[0]),
_ => {
if let Some(&(a, b)) = mul_factors.get(&child_vals[0]) {
Ok(builder.ins().fma(a, b, child_vals[1]))
} else if let Some(&(a, b)) = mul_factors.get(&child_vals[1]) {
Ok(builder.ins().fma(a, b, child_vals[0]))
} else {
Ok(builder.ins().fadd(child_vals[0], child_vals[1]))
}
}
}
}
OpKind::Sub => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
if child_vals[0] == child_vals[1] {
return Ok(builder.ins().f64const(0.0));
}
match (constants[0], constants[1]) {
(Some(l), Some(r)) => Ok(builder.ins().f64const(l - r)),
(_, Some(0.0)) => Ok(child_vals[0]),
(Some(0.0), _) => Ok(builder.ins().fneg(child_vals[1])),
_ => {
if let Some(&(a, b)) = mul_factors.get(&child_vals[0]) {
let neg_c = builder.ins().fneg(child_vals[1]);
return Ok(builder.ins().fma(a, b, neg_c));
}
Ok(builder.ins().fsub(child_vals[0], child_vals[1]))
}
}
}
OpKind::Mul => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
match (constants[0], constants[1]) {
(Some(l), Some(r)) => {
let folded = simplify_mul(l, r).unwrap_or(l * r);
Ok(builder.ins().f64const(folded))
}
(Some(0.0), _) | (_, Some(0.0)) => Ok(builder.ins().f64const(0.0)),
(Some(1.0), _) => Ok(child_vals[1]),
(_, Some(1.0)) => Ok(child_vals[0]),
(Some(-1.0), _) => Ok(builder.ins().fneg(child_vals[1])),
(_, Some(-1.0)) => Ok(builder.ins().fneg(child_vals[0])),
(Some(2.0), _) => Ok(builder.ins().fadd(child_vals[1], child_vals[1])),
(_, Some(2.0)) => Ok(builder.ins().fadd(child_vals[0], child_vals[0])),
_ => {
let result = builder.ins().fmul(child_vals[0], child_vals[1]);
mul_factors.insert(result, (child_vals[0], child_vals[1]));
Ok(result)
}
}
}
OpKind::Div => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let lhs = child_vals[0];
let rhs = child_vals[1];
if constants[0] == Some(0.0) {
let rhs_nonzero = child_analyses
.get(1)
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
let rhs_const_nonzero = constants[1].is_some_and(|c| c != 0.0 && !c.is_nan());
if rhs_nonzero || rhs_const_nonzero {
return Ok(builder.ins().f64const(0.0));
}
}
if lhs == rhs {
let lhs_nonzero = child_analyses
.first()
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if lhs_nonzero {
return Ok(builder.ins().f64const(1.0));
}
}
if let (Some(lval), Some(rval)) = (constants[0], constants[1]) {
let result = if rval == 0.0 { f64::NAN } else { lval / rval };
return Ok(builder.ins().f64const(result));
}
if constants[1] == Some(0.0) {
return Ok(builder.ins().f64const(f64::NAN));
}
if opts.allow_reciprocal_math
&& let Some(c) = constants[1]
&& c != 0.0
{
let recip = builder.ins().f64const(1.0 / c);
return Ok(builder.ins().fmul(lhs, recip));
}
let rhs_is_const_nonzero = constants[1].is_some_and(|c| c != 0.0 && !c.is_nan());
let rhs_nonzero = child_analyses
.get(1)
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
let skip_guard = opts.elide_nan_guard && (rhs_nonzero || rhs_is_const_nonzero);
if skip_guard {
Ok(builder.ins().fdiv(lhs, rhs))
} else {
let zero = builder.ins().f64const(0.0);
let nan_val = builder.ins().f64const(f64::NAN);
let is_zero = builder.ins().fcmp(FloatCC::Equal, rhs, zero);
let div_result = builder.ins().fdiv(lhs, rhs);
Ok(builder.ins().select(is_zero, nan_val, div_result))
}
}
OpKind::Pow => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
match (constants[0], constants[1]) {
(Some(l), Some(r)) => return Ok(builder.ins().f64const(l.powf(r))),
(_, Some(0.0)) => return Ok(builder.ins().f64const(1.0)),
(_, Some(1.0)) => return Ok(child_vals[0]),
_ => {}
}
if constants[0] == Some(1.0) {
return Ok(builder.ins().f64const(1.0));
}
if constants[0] == Some(0.0) {
let exp_positive = child_analyses
.get(1)
.and_then(|a| *a)
.is_some_and(|a| a.is_positive);
let exp_const_positive = constants[1].is_some_and(|e| e > 0.0 && !e.is_nan());
if exp_positive || exp_const_positive {
return Ok(builder.ins().f64const(0.0));
}
}
let node_an: Option<&NodeAnalysis> = child_analyses.get(2).and_then(|a| *a);
let expansion = node_an.map(|a| &a.pow_expansion);
match expansion {
Some(PowExpansion::Sqrt) if opts.expand_sqrt => {
Ok(passes::emit_sqrt(builder, child_vals[0]))
}
Some(PowExpansion::IntPow(n)) if *n >= 2 && *n <= opts.max_int_pow => {
Ok(passes::emit_int_pow(builder, child_vals[0], *n))
}
Some(PowExpansion::NegIntPow(n)) => {
let n = *n;
let base = child_vals[0];
let x_n = if n == 1 {
base
} else {
passes::emit_int_pow(builder, base, n)
};
let one = builder.ins().f64const(1.0);
let base_nonzero = child_analyses
.first()
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if opts.elide_nan_guard && base_nonzero {
Ok(builder.ins().fdiv(one, x_n))
} else {
let zero = builder.ins().f64const(0.0);
let nan_val = builder.ins().f64const(f64::NAN);
let is_zero = builder.ins().fcmp(FloatCC::Equal, x_n, zero);
let div_result = builder.ins().fdiv(one, x_n);
Ok(builder.ins().select(is_zero, nan_val, div_result))
}
}
_ => {
if let Some(exp) = constants[1] {
if opts.expand_sqrt && (exp - 0.5_f64).abs() < f64::EPSILON {
return Ok(passes::emit_sqrt(builder, child_vals[0]));
}
let n = exp as u32;
if exp.to_bits() == f64::from(n).to_bits()
&& n >= 2
&& n <= opts.max_int_pow
{
return Ok(passes::emit_int_pow(builder, child_vals[0], n));
}
}
let call = builder.ins().call(powf_func_ref, child_vals);
Ok(builder.inst_results(call)[0])
}
}
}
OpKind::Mod => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let lhs = child_vals[0];
let rhs = child_vals[1];
if let (Some(lval), Some(rval)) = (constants[0], constants[1]) {
let result = if rval == 0.0 { f64::NAN } else { lval % rval };
return Ok(builder.ins().f64const(result));
}
if constants[1] == Some(0.0) {
return Ok(builder.ins().f64const(f64::NAN));
}
let rhs_is_const_nonzero = constants[1].is_some_and(|c| c != 0.0 && !c.is_nan());
let rhs_nonzero = child_analyses
.get(1)
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if opts.elide_nan_guard && (rhs_nonzero || rhs_is_const_nonzero) {
let call = builder.ins().call(fmod_func_ref, &[lhs, rhs]);
Ok(builder.inst_results(call)[0])
} else {
let zero = builder.ins().f64const(0.0);
let nan_val = builder.ins().f64const(f64::NAN);
let is_zero = builder.ins().fcmp(FloatCC::Equal, rhs, zero);
let call = builder.ins().call(fmod_func_ref, &[lhs, rhs]);
let rem_result = builder.inst_results(call)[0];
Ok(builder.ins().select(is_zero, nan_val, rem_result))
}
}
OpKind::Neg => {
if arity != 1 {
return Err(crate::error::JitError::MalformedNode);
}
if let Some(c) = constants[0] {
return Ok(builder.ins().f64const(-c));
}
Ok(builder.ins().fneg(child_vals[0]))
}
}
}
fn f64x2_const(builder: &mut FunctionBuilder<'_>, v: f64) -> Value {
use cranelift_codegen::ir::ConstantData;
let bits = v.to_bits().to_le_bytes();
let data: [u8; 16] = [
bits[0], bits[1], bits[2], bits[3], bits[4], bits[5], bits[6], bits[7], bits[0], bits[1],
bits[2], bits[3], bits[4], bits[5], bits[6], bits[7],
];
let constant_handle = builder
.func
.dfg
.constants
.insert(ConstantData::from(&data[..]));
builder.ins().vconst(types::F64X2, constant_handle)
}
fn emit_ast_simd_f64x2(
ast: &AstProjection,
analysis: &[NodeAnalysis],
opts: &OptConfig,
builder: &mut FunctionBuilder<'_>,
var_vals_vec: &HashMap<u32, Value>, _powf_func_ref: cranelift_codegen::ir::FuncRef,
) -> Result<Value, crate::error::JitError> {
let mut stack: Vec<Frame> = Vec::with_capacity(64);
let mut values: Vec<Value> = Vec::with_capacity(64);
let mut mul_factors: HashMap<Value, (Value, Value)> = HashMap::new();
let mut const_cache: HashMap<u64, Value> = HashMap::new();
let root_node = ast
.nodes
.first()
.ok_or(crate::error::JitError::MalformedNode)?;
stack.push(Frame {
idx: 0,
arity: root_node.children.len(),
cursor: 0,
});
while let Some(top) = stack.last_mut() {
let action = if top.cursor < top.arity {
let Some(node) = ast.nodes.get(top.idx) else {
return Err(crate::error::JitError::MalformedNode);
};
let Some(&child_ptr) = node.children.as_slice().get(top.cursor) else {
return Err(crate::error::JitError::MalformedNode);
};
let child_idx = child_ptr
.resolve(top.idx)
.ok_or(crate::error::JitError::MalformedNode)?;
top.cursor += 1;
Action::Descend(child_idx)
} else {
Action::Emit(top.idx, top.arity)
};
match action {
Action::Descend(child_idx) => {
let Some(child_node) = ast.nodes.get(child_idx) else {
return Err(crate::error::JitError::MalformedNode);
};
stack.push(Frame {
idx: child_idx,
arity: child_node.children.len(),
cursor: 0,
});
}
Action::Emit(idx, arity) => {
stack.pop();
let node = ast
.nodes
.get(idx)
.ok_or(crate::error::JitError::MalformedNode)?;
match node.kind {
SymbolKind::Constant(_) => {
let bits = node.value.to_bits();
let v = *const_cache
.entry(bits)
.or_insert_with(|| f64x2_const(builder, node.value));
values.push(v);
}
SymbolKind::Variable(sym_id) => {
let v = var_vals_vec
.get(&sym_id.0)
.copied()
.ok_or(crate::error::JitError::MalformedNode)?;
values.push(v);
}
SymbolKind::Operator(op) => {
let split_at = values
.len()
.checked_sub(arity)
.ok_or(crate::error::JitError::MalformedNode)?;
let child_v: Vec<Value> = values.drain(split_at..).collect();
let children = node.children.as_slice_with_pool(&ast.children_pool);
let mut child_analyses: Vec<Option<&NodeAnalysis>> = children
.iter()
.map(|ptr| ptr.resolve(idx).and_then(|ci| analysis.get(ci)))
.collect();
child_analyses.push(analysis.get(idx));
let result = emit_operator_simd_f64x2(
builder,
op,
&child_v,
node,
&child_analyses,
opts,
&mut mul_factors,
)?;
values.push(result);
}
SymbolKind::Function(_) => {
return Err(crate::error::JitError::MalformedNode);
}
}
}
}
}
values.pop().ok_or(crate::error::JitError::MalformedNode)
}
#[allow(clippy::too_many_arguments)]
fn emit_operator_simd_f64x2(
builder: &mut FunctionBuilder<'_>,
op: OpKind,
child_vals: &[Value],
ast_node: &AstNode,
child_analyses: &[Option<&NodeAnalysis>],
opts: &OptConfig,
mul_factors: &mut HashMap<Value, (Value, Value)>,
) -> Result<Value, crate::error::JitError> {
let _ = ast_node;
let arity = child_vals.len();
match op {
OpKind::Add => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[0]) {
return Ok(builder.ins().fma(a, b, child_vals[1]));
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[1]) {
return Ok(builder.ins().fma(a, b, child_vals[0]));
}
Ok(builder.ins().fadd(child_vals[0], child_vals[1]))
}
OpKind::Sub => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
if child_vals[0] == child_vals[1] {
return Ok(f64x2_const(builder, 0.0));
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[0]) {
let neg_c = builder.ins().fneg(child_vals[1]);
return Ok(builder.ins().fma(a, b, neg_c));
}
Ok(builder.ins().fsub(child_vals[0], child_vals[1]))
}
OpKind::Mul => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let result = builder.ins().fmul(child_vals[0], child_vals[1]);
mul_factors.insert(result, (child_vals[0], child_vals[1]));
Ok(result)
}
OpKind::Div => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let lhs = child_vals[0];
let rhs = child_vals[1];
let rhs_nonzero = child_analyses
.get(1)
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if opts.elide_nan_guard && rhs_nonzero {
Ok(builder.ins().fdiv(lhs, rhs))
} else {
let zero_vec = f64x2_const(builder, 0.0);
let nan_vec = f64x2_const(builder, f64::NAN);
let div_result = builder.ins().fdiv(lhs, rhs);
let is_zero_bv = builder.ins().fcmp(FloatCC::Equal, rhs, zero_vec);
let is_zero_mask = builder
.ins()
.bitcast(types::F64X2, MemFlags::new(), is_zero_bv);
Ok(builder.ins().bitselect(is_zero_mask, nan_vec, div_result))
}
}
OpKind::Pow => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let node_an: Option<&NodeAnalysis> = child_analyses.get(2).and_then(|a| *a);
let expansion = node_an.map(|a| &a.pow_expansion);
match expansion {
Some(PowExpansion::Sqrt) if opts.expand_sqrt => {
Ok(builder.ins().sqrt(child_vals[0]))
}
Some(PowExpansion::IntPow(n)) if *n >= 2 && *n <= opts.max_int_pow => {
Ok(passes::emit_int_pow(builder, child_vals[0], *n))
}
Some(PowExpansion::NegIntPow(n)) => {
let n = *n;
let base_vec = child_vals[0];
let x_n = if n == 1 {
base_vec
} else {
passes::emit_int_pow(builder, base_vec, n)
};
let one_vec = f64x2_const(builder, 1.0);
let base_nonzero = child_analyses
.first()
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if opts.elide_nan_guard && base_nonzero {
Ok(builder.ins().fdiv(one_vec, x_n))
} else {
let zero_vec = f64x2_const(builder, 0.0);
let nan_vec = f64x2_const(builder, f64::NAN);
let div_result = builder.ins().fdiv(one_vec, x_n);
let is_zero_bv = builder.ins().fcmp(FloatCC::Equal, x_n, zero_vec);
let is_zero_mask =
builder
.ins()
.bitcast(types::F64X2, MemFlags::new(), is_zero_bv);
Ok(builder.ins().bitselect(is_zero_mask, nan_vec, div_result))
}
}
_ => {
Err(crate::error::JitError::MalformedNode)
}
}
}
OpKind::Mod => {
Err(crate::error::JitError::MalformedNode)
}
OpKind::Neg => {
if arity != 1 {
return Err(crate::error::JitError::MalformedNode);
}
Ok(builder.ins().fneg(child_vals[0]))
}
}
}
#[allow(dead_code)]
fn f64x4_const(builder: &mut FunctionBuilder<'_>, v: f64) -> Value {
use cranelift_codegen::ir::ConstantData;
let bits = v.to_bits().to_le_bytes();
let mut data = [0u8; 32];
for i in 0..4 {
data[i * 8..(i + 1) * 8].copy_from_slice(&bits);
}
let constant_handle = builder
.func
.dfg
.constants
.insert(ConstantData::from(&data[..]));
builder.ins().vconst(types::F64X4, constant_handle)
}
#[allow(dead_code)]
fn emit_ast_simd_f64x4(
ast: &AstProjection,
analysis: &[NodeAnalysis],
opts: &OptConfig,
builder: &mut FunctionBuilder<'_>,
var_vals_vec: &HashMap<u32, Value>, _powf_func_ref: cranelift_codegen::ir::FuncRef,
) -> Result<Value, crate::error::JitError> {
let mut stack: Vec<Frame> = Vec::with_capacity(64);
let mut values: Vec<Value> = Vec::with_capacity(64);
let mut mul_factors: HashMap<Value, (Value, Value)> = HashMap::new();
let mut const_cache: HashMap<u64, Value> = HashMap::new();
let root_node = ast
.nodes
.first()
.ok_or(crate::error::JitError::MalformedNode)?;
stack.push(Frame {
idx: 0,
arity: root_node.children.len(),
cursor: 0,
});
while let Some(top) = stack.last_mut() {
let action = if top.cursor < top.arity {
let Some(node) = ast.nodes.get(top.idx) else {
return Err(crate::error::JitError::MalformedNode);
};
let Some(&child_ptr) = node.children.as_slice().get(top.cursor) else {
return Err(crate::error::JitError::MalformedNode);
};
let child_idx = child_ptr
.resolve(top.idx)
.ok_or(crate::error::JitError::MalformedNode)?;
top.cursor += 1;
Action::Descend(child_idx)
} else {
Action::Emit(top.idx, top.arity)
};
match action {
Action::Descend(child_idx) => {
let Some(child_node) = ast.nodes.get(child_idx) else {
return Err(crate::error::JitError::MalformedNode);
};
stack.push(Frame {
idx: child_idx,
arity: child_node.children.len(),
cursor: 0,
});
}
Action::Emit(idx, arity) => {
stack.pop();
let node = ast
.nodes
.get(idx)
.ok_or(crate::error::JitError::MalformedNode)?;
match node.kind {
SymbolKind::Constant(_) => {
let bits = node.value.to_bits();
let v = *const_cache
.entry(bits)
.or_insert_with(|| f64x4_const(builder, node.value));
values.push(v);
}
SymbolKind::Variable(sym_id) => {
let v = var_vals_vec
.get(&sym_id.0)
.copied()
.ok_or(crate::error::JitError::MalformedNode)?;
values.push(v);
}
SymbolKind::Operator(op) => {
let split_at = values
.len()
.checked_sub(arity)
.ok_or(crate::error::JitError::MalformedNode)?;
let child_v: Vec<Value> = values.drain(split_at..).collect();
let children = node.children.as_slice_with_pool(&ast.children_pool);
let mut child_analyses: Vec<Option<&NodeAnalysis>> = children
.iter()
.map(|ptr| ptr.resolve(idx).and_then(|ci| analysis.get(ci)))
.collect();
child_analyses.push(analysis.get(idx));
let result = emit_operator_simd_f64x4(
builder,
op,
&child_v,
node,
&child_analyses,
opts,
&mut mul_factors,
)?;
values.push(result);
}
SymbolKind::Function(_) => {
return Err(crate::error::JitError::MalformedNode);
}
}
}
}
}
values.pop().ok_or(crate::error::JitError::MalformedNode)
}
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
fn emit_operator_simd_f64x4(
builder: &mut FunctionBuilder<'_>,
op: OpKind,
child_vals: &[Value],
ast_node: &AstNode,
child_analyses: &[Option<&NodeAnalysis>],
opts: &OptConfig,
mul_factors: &mut HashMap<Value, (Value, Value)>,
) -> Result<Value, crate::error::JitError> {
let _ = ast_node;
let arity = child_vals.len();
match op {
OpKind::Add => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[0]) {
return Ok(builder.ins().fma(a, b, child_vals[1]));
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[1]) {
return Ok(builder.ins().fma(a, b, child_vals[0]));
}
Ok(builder.ins().fadd(child_vals[0], child_vals[1]))
}
OpKind::Sub => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
if child_vals[0] == child_vals[1] {
return Ok(f64x4_const(builder, 0.0));
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[0]) {
let neg_c = builder.ins().fneg(child_vals[1]);
return Ok(builder.ins().fma(a, b, neg_c));
}
Ok(builder.ins().fsub(child_vals[0], child_vals[1]))
}
OpKind::Mul => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let result = builder.ins().fmul(child_vals[0], child_vals[1]);
mul_factors.insert(result, (child_vals[0], child_vals[1]));
Ok(result)
}
OpKind::Div => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let lhs = child_vals[0];
let rhs = child_vals[1];
let rhs_nonzero = child_analyses
.get(1)
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if opts.elide_nan_guard && rhs_nonzero {
Ok(builder.ins().fdiv(lhs, rhs))
} else {
let zero_vec = f64x4_const(builder, 0.0);
let nan_vec = f64x4_const(builder, f64::NAN);
let div_result = builder.ins().fdiv(lhs, rhs);
let is_zero_bv = builder.ins().fcmp(FloatCC::Equal, rhs, zero_vec);
let is_zero_mask = builder
.ins()
.bitcast(types::F64X4, MemFlags::new(), is_zero_bv);
Ok(builder.ins().bitselect(is_zero_mask, nan_vec, div_result))
}
}
OpKind::Pow => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let node_an: Option<&NodeAnalysis> = child_analyses.get(2).and_then(|a| *a);
let expansion = node_an.map(|a| &a.pow_expansion);
match expansion {
Some(PowExpansion::Sqrt) if opts.expand_sqrt => {
Ok(builder.ins().sqrt(child_vals[0]))
}
Some(PowExpansion::IntPow(n)) if *n >= 2 && *n <= opts.max_int_pow => {
Ok(passes::emit_int_pow(builder, child_vals[0], *n))
}
Some(PowExpansion::NegIntPow(n)) => {
let n = *n;
let base_vec = child_vals[0];
let x_n = if n == 1 {
base_vec
} else {
passes::emit_int_pow(builder, base_vec, n)
};
let one_vec = f64x4_const(builder, 1.0);
let base_nonzero = child_analyses
.first()
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if opts.elide_nan_guard && base_nonzero {
Ok(builder.ins().fdiv(one_vec, x_n))
} else {
let zero_vec = f64x4_const(builder, 0.0);
let nan_vec = f64x4_const(builder, f64::NAN);
let div_result = builder.ins().fdiv(one_vec, x_n);
let is_zero_bv = builder.ins().fcmp(FloatCC::Equal, x_n, zero_vec);
let is_zero_mask =
builder
.ins()
.bitcast(types::F64X4, MemFlags::new(), is_zero_bv);
Ok(builder.ins().bitselect(is_zero_mask, nan_vec, div_result))
}
}
_ => {
Err(crate::error::JitError::MalformedNode)
}
}
}
OpKind::Mod => {
Err(crate::error::JitError::MalformedNode)
}
OpKind::Neg => {
if arity != 1 {
return Err(crate::error::JitError::MalformedNode);
}
Ok(builder.ins().fneg(child_vals[0]))
}
}
}
#[allow(dead_code)]
fn f64x8_const(builder: &mut FunctionBuilder<'_>, v: f64) -> Value {
use cranelift_codegen::ir::ConstantData;
let bits = v.to_bits().to_le_bytes();
let mut data = [0u8; 64];
for i in 0..8 {
data[i * 8..(i + 1) * 8].copy_from_slice(&bits);
}
let constant_handle = builder
.func
.dfg
.constants
.insert(ConstantData::from(&data[..]));
builder.ins().vconst(types::F64X8, constant_handle)
}
#[allow(dead_code)]
fn emit_ast_simd_f64x8(
ast: &AstProjection,
analysis: &[NodeAnalysis],
opts: &OptConfig,
builder: &mut FunctionBuilder<'_>,
var_vals_vec: &HashMap<u32, Value>, _powf_func_ref: cranelift_codegen::ir::FuncRef,
) -> Result<Value, crate::error::JitError> {
let mut stack: Vec<Frame> = Vec::with_capacity(64);
let mut values: Vec<Value> = Vec::with_capacity(64);
let mut mul_factors: HashMap<Value, (Value, Value)> = HashMap::new();
let mut const_cache: HashMap<u64, Value> = HashMap::new();
let root_node = ast
.nodes
.first()
.ok_or(crate::error::JitError::MalformedNode)?;
stack.push(Frame {
idx: 0,
arity: root_node.children.len(),
cursor: 0,
});
while let Some(top) = stack.last_mut() {
let action = if top.cursor < top.arity {
let Some(node) = ast.nodes.get(top.idx) else {
return Err(crate::error::JitError::MalformedNode);
};
let Some(&child_ptr) = node.children.as_slice().get(top.cursor) else {
return Err(crate::error::JitError::MalformedNode);
};
let child_idx = child_ptr
.resolve(top.idx)
.ok_or(crate::error::JitError::MalformedNode)?;
top.cursor += 1;
Action::Descend(child_idx)
} else {
Action::Emit(top.idx, top.arity)
};
match action {
Action::Descend(child_idx) => {
let Some(child_node) = ast.nodes.get(child_idx) else {
return Err(crate::error::JitError::MalformedNode);
};
stack.push(Frame {
idx: child_idx,
arity: child_node.children.len(),
cursor: 0,
});
}
Action::Emit(idx, arity) => {
stack.pop();
let node = ast
.nodes
.get(idx)
.ok_or(crate::error::JitError::MalformedNode)?;
match node.kind {
SymbolKind::Constant(_) => {
let bits = node.value.to_bits();
let v = *const_cache
.entry(bits)
.or_insert_with(|| f64x8_const(builder, node.value));
values.push(v);
}
SymbolKind::Variable(sym_id) => {
let v = var_vals_vec
.get(&sym_id.0)
.copied()
.ok_or(crate::error::JitError::MalformedNode)?;
values.push(v);
}
SymbolKind::Operator(op) => {
let split_at = values
.len()
.checked_sub(arity)
.ok_or(crate::error::JitError::MalformedNode)?;
let child_v: Vec<Value> = values.drain(split_at..).collect();
let children = node.children.as_slice_with_pool(&ast.children_pool);
let mut child_analyses: Vec<Option<&NodeAnalysis>> = children
.iter()
.map(|ptr| ptr.resolve(idx).and_then(|ci| analysis.get(ci)))
.collect();
child_analyses.push(analysis.get(idx));
let result = emit_operator_simd_f64x8(
builder,
op,
&child_v,
node,
&child_analyses,
opts,
&mut mul_factors,
)?;
values.push(result);
}
SymbolKind::Function(_) => {
return Err(crate::error::JitError::MalformedNode);
}
}
}
}
}
values.pop().ok_or(crate::error::JitError::MalformedNode)
}
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
fn emit_operator_simd_f64x8(
builder: &mut FunctionBuilder<'_>,
op: OpKind,
child_vals: &[Value],
ast_node: &AstNode,
child_analyses: &[Option<&NodeAnalysis>],
opts: &OptConfig,
mul_factors: &mut HashMap<Value, (Value, Value)>,
) -> Result<Value, crate::error::JitError> {
let _ = ast_node;
let arity = child_vals.len();
match op {
OpKind::Add => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[0]) {
return Ok(builder.ins().fma(a, b, child_vals[1]));
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[1]) {
return Ok(builder.ins().fma(a, b, child_vals[0]));
}
Ok(builder.ins().fadd(child_vals[0], child_vals[1]))
}
OpKind::Sub => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
if child_vals[0] == child_vals[1] {
return Ok(f64x8_const(builder, 0.0));
}
if let Some(&(a, b)) = mul_factors.get(&child_vals[0]) {
let neg_c = builder.ins().fneg(child_vals[1]);
return Ok(builder.ins().fma(a, b, neg_c));
}
Ok(builder.ins().fsub(child_vals[0], child_vals[1]))
}
OpKind::Mul => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let result = builder.ins().fmul(child_vals[0], child_vals[1]);
mul_factors.insert(result, (child_vals[0], child_vals[1]));
Ok(result)
}
OpKind::Div => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let lhs = child_vals[0];
let rhs = child_vals[1];
let rhs_nonzero = child_analyses
.get(1)
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if opts.elide_nan_guard && rhs_nonzero {
Ok(builder.ins().fdiv(lhs, rhs))
} else {
let zero_vec = f64x8_const(builder, 0.0);
let nan_vec = f64x8_const(builder, f64::NAN);
let div_result = builder.ins().fdiv(lhs, rhs);
let is_zero_bv = builder.ins().fcmp(FloatCC::Equal, rhs, zero_vec);
let is_zero_mask = builder
.ins()
.bitcast(types::F64X8, MemFlags::new(), is_zero_bv);
Ok(builder.ins().bitselect(is_zero_mask, nan_vec, div_result))
}
}
OpKind::Pow => {
if arity != 2 {
return Err(crate::error::JitError::MalformedNode);
}
let node_an: Option<&NodeAnalysis> = child_analyses.get(2).and_then(|a| *a);
let expansion = node_an.map(|a| &a.pow_expansion);
match expansion {
Some(PowExpansion::Sqrt) if opts.expand_sqrt => {
Ok(builder.ins().sqrt(child_vals[0]))
}
Some(PowExpansion::IntPow(n)) if *n >= 2 && *n <= opts.max_int_pow => {
Ok(passes::emit_int_pow(builder, child_vals[0], *n))
}
Some(PowExpansion::NegIntPow(n)) => {
let n = *n;
let base_vec = child_vals[0];
let x_n = if n == 1 {
base_vec
} else {
passes::emit_int_pow(builder, base_vec, n)
};
let one_vec = f64x8_const(builder, 1.0);
let base_nonzero = child_analyses
.first()
.and_then(|a| *a)
.is_some_and(super::analysis::NodeAnalysis::is_nonzero);
if opts.elide_nan_guard && base_nonzero {
Ok(builder.ins().fdiv(one_vec, x_n))
} else {
let zero_vec = f64x8_const(builder, 0.0);
let nan_vec = f64x8_const(builder, f64::NAN);
let div_result = builder.ins().fdiv(one_vec, x_n);
let is_zero_bv = builder.ins().fcmp(FloatCC::Equal, x_n, zero_vec);
let is_zero_mask =
builder
.ins()
.bitcast(types::F64X8, MemFlags::new(), is_zero_bv);
Ok(builder.ins().bitselect(is_zero_mask, nan_vec, div_result))
}
}
_ => Err(crate::error::JitError::MalformedNode),
}
}
OpKind::Mod => Err(crate::error::JitError::MalformedNode),
OpKind::Neg => {
if arity != 1 {
return Err(crate::error::JitError::MalformedNode);
}
Ok(builder.ins().fneg(child_vals[0]))
}
}
}
fn constant_behind(builder: &FunctionBuilder<'_>, v: Value) -> Option<f64> {
use cranelift_codegen::ir::{InstructionData, ValueDef};
let dfg = &builder.func.dfg;
let ValueDef::Result(inst, _) = dfg.value_def(v) else {
return None;
};
if let InstructionData::UnaryIeee64 { imm, .. } = dfg.insts[inst] {
Some(f64::from_bits(imm.bits()))
} else {
None
}
}
fn is_vectorizable_ast(
ast: &AstProjection,
analysis: &[NodeAnalysis],
custom_ops: Option<&crate::custom::descriptor::CustomOpRegistry>,
) -> bool {
for (node, an) in ast.nodes.iter().zip(analysis.iter()) {
match node.kind {
SymbolKind::Function(fn_id) => {
if custom_ops.is_some_and(|r| r.is_vectorizable(fn_id)) {
continue;
}
return false;
}
SymbolKind::Operator(OpKind::Mod) => return false,
SymbolKind::Operator(OpKind::Pow) => {
if matches!(an.pow_expansion, PowExpansion::None) {
return false;
}
}
_ => {}
}
}
true
}
#[allow(clippy::too_many_arguments)]
fn emit_ast_scalar_with_vars(
ast: &AstProjection,
analysis: &[NodeAnalysis],
opts: &OptConfig,
builder: &mut FunctionBuilder<'_>,
var_vals: &HashMap<u32, Value>,
powf_func_ref: cranelift_codegen::ir::FuncRef,
custom_refs: &HashMap<u32, cranelift_codegen::ir::FuncRef>,
stack: &mut Vec<Frame>,
values: &mut Vec<Value>,
) -> Result<Value, crate::error::JitError> {
stack.clear();
values.clear();
let mut mul_factors: HashMap<Value, (Value, Value)> = HashMap::new();
let fmod_dummy = powf_func_ref;
let root_node = ast
.nodes
.first()
.ok_or(crate::error::JitError::MalformedNode)?;
stack.push(Frame {
idx: 0,
arity: root_node.children.len(),
cursor: 0,
});
while let Some(top) = stack.last_mut() {
let action = if top.cursor < top.arity {
let Some(node) = ast.nodes.get(top.idx) else {
return Err(crate::error::JitError::MalformedNode);
};
let Some(&child_ptr) = node.children.as_slice().get(top.cursor) else {
return Err(crate::error::JitError::MalformedNode);
};
let child_idx = child_ptr
.resolve(top.idx)
.ok_or(crate::error::JitError::MalformedNode)?;
top.cursor += 1;
Action::Descend(child_idx)
} else {
Action::Emit(top.idx, top.arity)
};
match action {
Action::Descend(child_idx) => {
let Some(child_node) = ast.nodes.get(child_idx) else {
return Err(crate::error::JitError::MalformedNode);
};
stack.push(Frame {
idx: child_idx,
arity: child_node.children.len(),
cursor: 0,
});
}
Action::Emit(idx, arity) => {
stack.pop();
let node = ast
.nodes
.get(idx)
.ok_or(crate::error::JitError::MalformedNode)?;
match node.kind {
SymbolKind::Constant(_) => {
values.push(builder.ins().f64const(node.value));
}
SymbolKind::Variable(sym_id) => {
let v = var_vals
.get(&sym_id.0)
.copied()
.ok_or(crate::error::JitError::MalformedNode)?;
values.push(v);
}
SymbolKind::Operator(op) => {
let split_at = values
.len()
.checked_sub(arity)
.ok_or(crate::error::JitError::MalformedNode)?;
let child_v: Vec<Value> = values.drain(split_at..).collect();
let children = node.children.as_slice_with_pool(&ast.children_pool);
let mut child_analyses: Vec<Option<&NodeAnalysis>> = children
.iter()
.map(|ptr| ptr.resolve(idx).and_then(|ci| analysis.get(ci)))
.collect();
child_analyses.push(analysis.get(idx));
let result = emit_operator(
builder,
op,
&child_v,
powf_func_ref,
fmod_dummy,
node,
&child_analyses,
opts,
&mut mul_factors,
)?;
values.push(result);
}
SymbolKind::Function(fn_id) => {
let func_ref = custom_refs
.get(&fn_id.0)
.copied()
.ok_or(crate::error::JitError::UnknownFunction)?;
let split_at = values
.len()
.checked_sub(arity)
.ok_or(crate::error::JitError::MalformedNode)?;
let child_v: Vec<Value> = values.drain(split_at..).collect();
if child_v.is_empty() || child_v.len() > 3 {
return Err(crate::error::JitError::MalformedNode);
}
let call = builder.ins().call(func_ref, &child_v);
let result = builder
.inst_results(call)
.first()
.copied()
.ok_or(crate::error::JitError::VerifierRejected)?;
values.push(result);
}
}
}
}
}
let result = values.pop().ok_or(crate::error::JitError::MalformedNode)?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::convert::dag_to_ast;
use crate::dag::builder::DagBuilder;
use crate::parser::expr::parse_expression;
#[test]
fn test_jit_compile_and_execute() {
let mut builder = DagBuilder::new();
let id = parse_expression("x * 2.5 + y ^ 2.0", &mut builder).unwrap();
let ast = dag_to_ast(builder.arena(), id);
let mut compiler = JitCompiler::new();
let compiled_fn = compiler.compile(&ast).unwrap();
let vars = vec![3.0, 4.0];
let result = compiled_fn(vars.as_ptr());
let expected = 3.0 * 2.5 + 4.0_f64.powf(2.0);
assert!((result - expected).abs() < f64::EPSILON);
}
#[test]
fn test_jit_divide_by_zero_yields_nan() {
let mut builder = DagBuilder::new();
let id = parse_expression("x / y", &mut builder).unwrap();
let ast = dag_to_ast(builder.arena(), id);
let mut compiler = JitCompiler::new();
let compiled_fn = compiler.compile(&ast).unwrap();
let safe_vars = vec![10.0, 2.0];
let safe_res = compiled_fn(safe_vars.as_ptr());
assert!((safe_res - 5.0).abs() < f64::EPSILON);
let zero_vars = vec![10.0, 0.0];
let nan_res = compiled_fn(zero_vars.as_ptr());
assert!(nan_res.is_nan(), "x/0 must be NaN; got {nan_res}");
}
#[test]
fn test_jit_deep_chain_no_stack_overflow() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let mut acc = x;
for _ in 0..2500 {
acc = b.add(acc, x);
}
let ast = dag_to_ast(b.arena(), acc);
let mut compiler = JitCompiler::new();
let compiled_fn = compiler.compile(&ast).unwrap();
let vars = vec![1.0];
let result = compiled_fn(vars.as_ptr());
let expected = 2501.0;
assert!((result - expected).abs() < f64::EPSILON);
}
#[test]
fn test_peephole_x_plus_zero() {
let mut b = DagBuilder::new();
let id = parse_expression("x + 0", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
assert!((f([3.5_f64].as_ptr()) - 3.5).abs() < f64::EPSILON);
}
#[test]
fn test_peephole_x_times_zero() {
let mut b = DagBuilder::new();
let id = parse_expression("x * 0", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
assert!(f([42.0_f64].as_ptr()).abs() < f64::EPSILON);
}
#[test]
fn test_peephole_constant_fold() {
let mut b = DagBuilder::new();
let id = parse_expression("3 + 4", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
let r = f([].as_ptr());
assert!((r - 7.0).abs() < f64::EPSILON);
}
extern "C" fn rssn_test_double(x: f64) -> f64 {
x * 2.0
}
#[test]
fn test_custom_function_jit_round_trip() {
use crate::dag::metadata::NodeFlags;
use crate::dag::symbol::{FnId, SymbolKind as SK};
let mut b = DagBuilder::new();
let x = b.variable("x");
let fn_id = FnId(42);
let expr = b.operator(SK::Function(fn_id), &[x], NodeFlags::EMPTY);
let ast = dag_to_ast(b.arena(), expr);
let mut compiler = JitCompiler::new();
compiler.register_custom_function(fn_id, rssn_test_double);
let f = compiler.compile(&ast).expect("compile with custom fn");
let vars = vec![3.5_f64];
let r = f(vars.as_ptr());
assert!((r - 7.0).abs() < f64::EPSILON, "expected 3.5 * 2 = 7.0");
}
#[test]
fn test_custom_function_unregistered_fails_cleanly() {
use crate::dag::metadata::NodeFlags;
use crate::dag::symbol::{FnId, SymbolKind as SK};
let mut b = DagBuilder::new();
let x = b.variable("x");
let expr = b.operator(SK::Function(FnId(99)), &[x], NodeFlags::EMPTY);
let ast = dag_to_ast(b.arena(), expr);
let mut compiler = JitCompiler::new();
let err = compiler.compile(&ast).expect_err("must error");
assert_eq!(
err,
crate::error::JitError::UnknownFunction,
"unregistered fn id must yield UnknownFunction; got: {err:?}"
);
}
#[test]
fn test_power_expansion_x_squared_no_powf() {
let mut b = DagBuilder::new();
let id = parse_expression("x ^ 2", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
let result = f([3.0_f64].as_ptr());
assert!(
(result - 9.0).abs() < f64::EPSILON,
"3^2 should be 9; got {result}"
);
}
#[test]
fn test_power_expansion_sqrt() {
let mut b = DagBuilder::new();
let id = parse_expression("x ^ 0.5", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
let result = f([4.0_f64].as_ptr());
assert!(
(result - 2.0).abs() < 1e-10,
"4^0.5 should be 2; got {result}"
);
}
#[test]
fn test_nan_guard_elision_constant_denominator() {
let mut b = DagBuilder::new();
let id = parse_expression("x / 3", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
let result = f([9.0_f64].as_ptr());
assert!(
(result - 3.0).abs() < f64::EPSILON,
"9/3 should be 3; got {result}"
);
}
#[test]
fn test_batch_f64x2_correctness() {
let mut b = DagBuilder::new();
let id = parse_expression("x + y", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let batch_fn = compiler
.compile_batch_f64x2(&ast)
.expect("compile ok")
.expect("should be vectorizable");
let x_col = vec![1.0_f64, 3.0, 5.0, 7.0];
let y_col = vec![2.0_f64, 4.0, 6.0, 8.0];
let cols: Vec<*const f64> = vec![x_col.as_ptr(), y_col.as_ptr()];
let mut out = vec![0.0_f64; 4];
batch_fn(cols.as_ptr(), 4, out.as_mut_ptr());
let expected = [3.0_f64, 7.0, 11.0, 15.0];
for (i, (&got, &exp)) in out.iter().zip(expected.iter()).enumerate() {
assert!(
(got - exp).abs() < f64::EPSILON,
"row {i}: expected {exp}, got {got}"
);
}
}
#[test]
fn test_neg_int_pow_x_inv() {
let mut b = DagBuilder::new();
let id = parse_expression("x ^ -1", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
let result = f([3.0_f64].as_ptr());
let expected = 1.0_f64 / 3.0;
assert!(
(result - expected).abs() < 1e-14,
"x^(-1) for x=3 should be ~{expected}; got {result}"
);
let nan_result = f([0.0_f64].as_ptr());
assert!(
nan_result.is_nan(),
"x^(-1) for x=0 should be NaN; got {nan_result}"
);
}
#[test]
fn test_analysis_x_squared_plus_1_is_positive() {
let mut b = DagBuilder::new();
let id = parse_expression("x ^ 2 + 1", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let analysis = crate::jit::analysis::analyze(&ast);
let root_an = &analysis[0];
assert!(
root_an.is_positive,
"x^2 + 1 should be provably positive; got {root_an:?}"
);
assert!(
root_an.is_nonnegative,
"x^2 + 1 should be provably nonneg; got {root_an:?}"
);
}
#[test]
fn test_nan_guard_elision_x_sq_plus_1_denominator() {
let mut b = DagBuilder::new();
let id = parse_expression("x / (x ^ 2 + 1)", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
let test_cases: &[(f64, f64)] = &[
(0.0, 0.0), (1.0, 0.5), (2.0, 0.4), (-1.0, -0.5), (3.0, 3.0 / 10.0), ];
for &(x, expected) in test_cases {
let result = f([x].as_ptr());
assert!(
(result - expected).abs() < 1e-14,
"x / (x^2+1) for x={x}: expected {expected}, got {result}"
);
}
}
#[test]
fn test_batch_f64x2_true_simd() {
let mut b = DagBuilder::new();
let id = parse_expression("x * x + 1", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let batch_fn = compiler
.compile_batch_f64x2(&ast)
.expect("compile ok")
.expect("should be vectorizable");
let x_col = vec![0.0_f64, 1.0, 2.0, 3.0, 4.0];
let cols: Vec<*const f64> = vec![x_col.as_ptr()];
let mut out = vec![0.0_f64; 5];
batch_fn(cols.as_ptr(), 5, out.as_mut_ptr());
let expected = [1.0_f64, 2.0, 5.0, 10.0, 17.0]; for (i, (&got, &exp)) in out.iter().zip(expected.iter()).enumerate() {
assert!(
(got - exp).abs() < f64::EPSILON,
"row {i}: expected {exp}, got {got}"
);
}
}
#[test]
fn test_zero_minus_x_is_fneg() {
let mut b = DagBuilder::new();
let id = parse_expression("0 - x", &mut b).unwrap();
let ast = dag_to_ast(b.arena(), id);
let mut compiler = JitCompiler::new();
let f = compiler.compile(&ast).unwrap();
let result = f([3.0_f64].as_ptr());
assert!(
(result - (-3.0)).abs() < f64::EPSILON,
"0 - 3 should be -3; got {result}"
);
let result2 = f([0.0_f64].as_ptr());
assert!(
result2 == 0.0 || result2 == -0.0,
"0 - 0 should be ±0; got {result2}"
);
}
}