use std::sync::Arc;
use cranelift_codegen::ir::{condcodes::IntCC, types, AbiParam, InstBuilder, MemFlags, Signature};
use cranelift_codegen::isa::CallConv;
use cranelift_codegen::settings::{self, Configurable};
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::{Linkage, Module};
use super::filter_compiler::JITModuleOwner;
const FNV_OFFSET_BASIS: i64 = -3_750_763_034_362_895_579_i64;
const FNV_PRIME: i64 = 1_099_511_628_211_i64;
#[derive(Debug, Clone, Copy)]
pub struct DistinctKeySpec {
pub col_idx: usize,
}
type DistinctHashFn = unsafe extern "C" fn(*const f64, usize) -> i64;
pub struct CompiledDistinct {
fn_ptr: DistinctHashFn,
specs: Vec<DistinctKeySpec>,
_module_owner: Arc<JITModuleOwner>,
}
unsafe impl Send for CompiledDistinct {}
unsafe impl Sync for CompiledDistinct {}
impl std::fmt::Debug for CompiledDistinct {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompiledDistinct")
.field("key_count", &self.specs.len())
.finish_non_exhaustive()
}
}
impl CompiledDistinct {
pub fn hash_key(&self, row: &[f64]) -> Option<i64> {
let result = unsafe { (self.fn_ptr)(row.as_ptr(), row.len()) };
if self.specs.is_empty() {
return Some(result);
}
let max_col = self.specs.iter().map(|s| s.col_idx).max().unwrap_or(0);
if row.len() <= max_col {
return None;
}
Some(result)
}
pub fn key_count(&self) -> usize {
self.specs.len()
}
}
#[derive(Debug, thiserror::Error)]
pub enum DistinctCompilerError {
#[error("JIT codegen error: {0}")]
CodegenError(String),
#[error("JIT ISA init error: {0}")]
IsaInitError(String),
#[error("JIT linkage error: {0}")]
LinkageError(String),
}
pub struct DistinctCompiler;
impl Default for DistinctCompiler {
fn default() -> Self {
DistinctCompiler
}
}
impl DistinctCompiler {
pub fn new() -> Self {
DistinctCompiler
}
pub fn compile(
&mut self,
specs: &[DistinctKeySpec],
) -> Result<CompiledDistinct, DistinctCompilerError> {
let module = build_jit_module()?;
let (fn_ptr, module) = compile_distinct_fn(module, specs)?;
let owner = Arc::new(JITModuleOwner::new(module));
Ok(CompiledDistinct {
fn_ptr,
specs: specs.to_vec(),
_module_owner: owner,
})
}
}
fn build_jit_module() -> Result<JITModule, DistinctCompilerError> {
let mut flag_builder = settings::builder();
flag_builder
.set("use_colocated_libcalls", "false")
.map_err(|e| DistinctCompilerError::CodegenError(e.to_string()))?;
flag_builder
.set("is_pic", "false")
.map_err(|e| DistinctCompilerError::CodegenError(e.to_string()))?;
flag_builder
.set("opt_level", "speed")
.map_err(|e| DistinctCompilerError::CodegenError(e.to_string()))?;
let flags = settings::Flags::new(flag_builder);
let isa = cranelift_native::builder()
.map_err(|e| DistinctCompilerError::IsaInitError(e.to_string()))?
.finish(flags)
.map_err(|e| DistinctCompilerError::IsaInitError(e.to_string()))?;
let builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
Ok(JITModule::new(builder))
}
fn compile_distinct_fn(
mut module: JITModule,
specs: &[DistinctKeySpec],
) -> Result<(DistinctHashFn, JITModule), DistinctCompilerError> {
let ptr_type = module.isa().pointer_type();
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(ptr_type)); sig.params.push(AbiParam::new(ptr_type)); sig.returns.push(AbiParam::new(types::I64));
let func_id = module
.declare_function("distinct_hash_fn", Linkage::Local, &sig)
.map_err(|e| DistinctCompilerError::LinkageError(e.to_string()))?;
{
let mut ctx = module.make_context();
ctx.func.signature = sig.clone();
let mut fn_builder_ctx = FunctionBuilderContext::new();
let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fn_builder_ctx);
if specs.is_empty() {
emit_constant_hash(&mut builder, ptr_type, FNV_OFFSET_BASIS);
} else {
emit_distinct_body(&mut builder, specs, ptr_type)?;
}
builder.finalize();
module
.define_function(func_id, &mut ctx)
.map_err(|e| DistinctCompilerError::CodegenError(format!("{e:?}")))?;
}
module
.finalize_definitions()
.map_err(|e| DistinctCompilerError::CodegenError(format!("finalize_definitions: {e:?}")))?;
let raw_ptr = module.get_finalized_function(func_id);
let fn_ptr: DistinctHashFn = unsafe { std::mem::transmute(raw_ptr) };
Ok((fn_ptr, module))
}
fn emit_constant_hash(builder: &mut FunctionBuilder<'_>, ptr_type: types::Type, constant: i64) {
let entry_block = builder.create_block();
builder.append_block_params_for_function_params(entry_block);
builder.switch_to_block(entry_block);
builder.seal_block(entry_block);
let _ptr: cranelift_codegen::ir::Value = builder.block_params(entry_block)[0];
let _len: cranelift_codegen::ir::Value = builder.block_params(entry_block)[1];
let _ = ptr_type;
let val = builder.ins().iconst(types::I64, constant);
builder.ins().return_(&[val]);
}
fn emit_distinct_body(
builder: &mut FunctionBuilder<'_>,
specs: &[DistinctKeySpec],
ptr_type: types::Type,
) -> Result<(), DistinctCompilerError> {
let max_col = specs.iter().map(|s| s.col_idx).max().unwrap_or(0);
let entry_block = builder.create_block();
let bounds_fail_block = builder.create_block();
let body_block = builder.create_block();
builder.append_block_params_for_function_params(entry_block);
builder.switch_to_block(entry_block);
builder.seal_block(entry_block);
let ptr = builder.block_params(entry_block)[0];
let len = builder.block_params(entry_block)[1];
let max_col_val = builder.ins().iconst(ptr_type, max_col as i64);
let out_of_bounds = builder
.ins()
.icmp(IntCC::UnsignedLessThanOrEqual, len, max_col_val);
builder
.ins()
.brif(out_of_bounds, bounds_fail_block, &[], body_block, &[]);
builder.switch_to_block(bounds_fail_block);
builder.seal_block(bounds_fail_block);
let zero = builder.ins().iconst(types::I64, 0);
builder.ins().return_(&[zero]);
builder.switch_to_block(body_block);
builder.seal_block(body_block);
let mut hash = builder.ins().iconst(types::I64, FNV_OFFSET_BASIS);
for spec in specs {
let offset = builder
.ins()
.iconst(ptr_type, (spec.col_idx * std::mem::size_of::<f64>()) as i64);
let addr = builder.ins().iadd(ptr, offset);
let f64_val = builder.ins().load(types::F64, MemFlags::new(), addr, 0);
let bits = builder.ins().bitcast(types::I64, MemFlags::new(), f64_val);
hash = builder.ins().bxor(hash, bits);
let prime = builder.ins().iconst(types::I64, FNV_PRIME);
hash = builder.ins().imul(hash, prime);
}
builder.ins().return_(&[hash]);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn compiler() -> DistinctCompiler {
DistinctCompiler::new()
}
#[test]
fn test_empty_specs_returns_fnv_basis() {
let cd = compiler().compile(&[]).expect("compile ok");
let row = [1.0f64, 2.0];
let h = cd.hash_key(&row).expect("should return Some");
assert_eq!(h, FNV_OFFSET_BASIS);
}
#[test]
fn test_same_row_same_hash() {
let specs = [
DistinctKeySpec { col_idx: 0 },
DistinctKeySpec { col_idx: 1 },
];
let cd = compiler().compile(&specs).expect("compile ok");
let row = [1.0f64, 2.0];
let h1 = cd.hash_key(&row).expect("ok");
let h2 = cd.hash_key(&row).expect("ok");
assert_eq!(h1, h2);
}
#[test]
fn test_different_rows_different_hash() {
let specs = [DistinctKeySpec { col_idx: 0 }];
let cd = compiler().compile(&specs).expect("compile ok");
let h1 = cd.hash_key(&[1.0f64]).expect("ok");
let h2 = cd.hash_key(&[2.0f64]).expect("ok");
assert_ne!(
h1, h2,
"distinct f64 values should (almost certainly) hash differently"
);
}
#[test]
fn test_bounds_check_fails() {
let specs = [DistinctKeySpec { col_idx: 5 }];
let cd = compiler().compile(&specs).expect("compile ok");
let row = [1.0f64, 2.0]; assert!(cd.hash_key(&row).is_none());
}
}