From b5a5c6c0a00a2d1ecf65aa1e7abe86aea932cf14 Mon Sep 17 00:00:00 2001 From: Paul Masurel Date: Wed, 19 Aug 2026 18:55:56 +0200 Subject: [PATCH] Make compiled JIT functions immutable --- jitexpr/examples/basic.rs | 8 +- jitexpr/src/compile/compile_fn_builder.rs | 4 +- jitexpr/src/compile/compiled_fn.rs | 107 +++++++++++++++++++--- jitexpr/src/compile/mod.rs | 52 ++++++++--- jitexpr/src/compile/string_arena.rs | 5 +- jitexpr/src/functions/abs.rs | 4 +- jitexpr/src/functions/add.rs | 24 ++--- jitexpr/src/functions/and.rs | 4 +- jitexpr/src/functions/ceil.rs | 6 +- jitexpr/src/functions/concat.rs | 10 +- jitexpr/src/functions/divide.rs | 8 +- jitexpr/src/functions/eq.rs | 16 ++-- jitexpr/src/functions/floor.rs | 6 +- jitexpr/src/functions/gt.rs | 6 +- jitexpr/src/functions/gt_eq.rs | 4 +- jitexpr/src/functions/if_fn.rs | 6 +- jitexpr/src/functions/int_mod.rs | 4 +- jitexpr/src/functions/is_not_null.rs | 4 +- jitexpr/src/functions/is_null.rs | 4 +- jitexpr/src/functions/left.rs | 6 +- jitexpr/src/functions/lower.rs | 16 ++-- jitexpr/src/functions/lt.rs | 4 +- jitexpr/src/functions/lt_eq.rs | 4 +- jitexpr/src/functions/max.rs | 11 ++- jitexpr/src/functions/min.rs | 11 ++- jitexpr/src/functions/multiply.rs | 12 +-- jitexpr/src/functions/neq.rs | 4 +- jitexpr/src/functions/not.rs | 4 +- jitexpr/src/functions/or.rs | 4 +- jitexpr/src/functions/pow.rs | 4 +- jitexpr/src/functions/regexp_extract.rs | 18 ++-- jitexpr/src/functions/regexp_like.rs | 6 +- jitexpr/src/functions/right.rs | 10 +- jitexpr/src/functions/round.rs | 8 +- jitexpr/src/functions/split_after.rs | 6 +- jitexpr/src/functions/split_before.rs | 6 +- jitexpr/src/functions/sqrt.rs | 6 +- jitexpr/src/functions/substring.rs | 6 +- jitexpr/src/functions/substring_count.rs | 5 +- jitexpr/src/functions/subtract.rs | 14 +-- jitexpr/src/functions/text_join.rs | 4 +- jitexpr/src/functions/trim.rs | 6 +- jitexpr/src/functions/upper.rs | 8 +- 43 files changed, 299 insertions(+), 166 deletions(-) diff --git a/jitexpr/examples/basic.rs b/jitexpr/examples/basic.rs index fc27e32e9..41923b4be 100644 --- a/jitexpr/examples/basic.rs +++ b/jitexpr/examples/basic.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use std::error::Error; +use std::sync::Arc; use jitexpr::ast::{Function, InferredTypeSet, UntypedExpr, infer_types}; -use jitexpr::compile::{CompiledFn, compile}; +use jitexpr::compile::{CompiledFn, CompiledFnCtx, compile}; use jitexpr::types::{VarType, VariableValue}; fn main() -> Result<(), Box> { @@ -28,13 +29,14 @@ fn main() -> Result<(), Box> { let variable_types: HashMap<&str, VarType> = std::iter::once(("my_col", VarType::F64)).collect(); - let mut compiled_fn: CompiledFn = compile(&untyped_expr, &variable_types)?; + let compiled_fn: Arc = compile(&untyped_expr, &variable_types)?; + let mut compiled_fn_ctx = CompiledFnCtx::new(compiled_fn); // We use a nullable wrapper around the value union to pass typed variables. // For present values, it is up to us to populate the correct union member. // Not doing so is UB. let input: Box<[VariableValue]> = vec![VariableValue::from(1.2f64)].into_boxed_slice(); - let output = unsafe { compiled_fn.call(&input[..]) }; + let output = unsafe { compiled_fn_ctx.call(&input[..]) }; assert_eq!(unsafe { output.as_f64() }, Some(1.2f64 + 1.0f64)); Ok(()) diff --git a/jitexpr/src/compile/compile_fn_builder.rs b/jitexpr/src/compile/compile_fn_builder.rs index 9a3c9ebc6..4f324c8a0 100644 --- a/jitexpr/src/compile/compile_fn_builder.rs +++ b/jitexpr/src/compile/compile_fn_builder.rs @@ -11,8 +11,7 @@ use cranelift_module::{FuncId, Module, ModuleError, default_libcall_names}; use super::compiled_fn::JitEntry; use super::{ - CompileError, CompiledFn, LoweringContext, StringArena, TypedExpr, TypedExprAst, TypedLiteral, - TypedVariable, + CompileError, CompiledFn, LoweringContext, TypedExpr, TypedExprAst, TypedLiteral, TypedVariable, }; use crate::ast::{InferredTypeSet, Literal, UntypedExpr}; use crate::functions::{declare_native_functions, register_jit_symbols}; @@ -316,7 +315,6 @@ impl LoweredFunction { entry, _module: module, inputs: input_vars, - string_arena: StringArena::new(), _typed_expr: expression, }) } diff --git a/jitexpr/src/compile/compiled_fn.rs b/jitexpr/src/compile/compiled_fn.rs index 6fb7cad78..0a47b68e3 100644 --- a/jitexpr/src/compile/compiled_fn.rs +++ b/jitexpr/src/compile/compiled_fn.rs @@ -1,3 +1,6 @@ +use std::ops::Deref; +use std::sync::Arc; + use cranelift_jit::JITModule; use super::{StringArena, TypedExpr, TypedVariable}; @@ -13,7 +16,7 @@ compile_error!( // On the supported targets, VariableValue's two eightbytes are returned in two // integer registers by the platform C ABI. The lifetime is selected by -// CompiledFn::call so that it is bounded by both possible sources of strings. +// CompiledFn::call so that it is bounded by all possible sources of strings. // This is a Rust-to-JIT boundary whose VariableValue layout is asserted in // types.rs, not an interface intended for C callers. #[allow(improper_ctypes_definitions)] @@ -29,18 +32,78 @@ pub struct CompiledFn { pub(crate) _module: JITModule, /// Input slots in the exact order expected by [`CompiledFn::call`]. pub inputs: Vec, - pub(crate) string_arena: StringArena, // This AST owns the Arc-backed literals and regexes embedded in generated code. pub(crate) _typed_expr: Box, } +// `JITModule` is not `Sync` because it supports lazily looking up symbols through +// interior mutability. A `CompiledFn` only retains a finalized module to keep its +// executable allocation alive and never invokes those mutable APIs. Its entry +// point and the immutable resources referenced by the generated code can be +// called concurrently when each caller supplies a distinct `StringArena`. +unsafe impl Sync for CompiledFn {} + impl CompiledFn { /// Returns the concrete result type selected during compilation. pub fn result_type(&self) -> VarType { self._typed_expr.return_type } - /// Evaluate the compiled expression. + /// Creates an evaluation context with a private string arena. + pub fn context(self: &Arc) -> CompiledFnCtx { + CompiledFnCtx::new(Arc::clone(self)) + } + + /// Evaluates the compiled expression using the supplied string arena. + /// + /// The mutable arena borrow prevents another evaluation from clearing the + /// arena while an arena-backed result from this call is still live. + /// + /// # Safety + /// + /// `args` must follow [`CompiledFn::inputs`] exactly: every present slot must + /// contain the union member corresponding to that variable's type. Absent + /// slots must use [`VariableValue::none`], and any borrowed strings must + /// remain alive for the duration of this call. + /// + /// The result cannot outlive the compiled function, the string arena, or + /// the passed arguments' lifetime. + #[inline(always)] + pub unsafe fn call<'args, 'compiled, 'arena, 'output>( + &'compiled self, + args: &[VariableValue<'args>], + string_arena: &'arena mut StringArena, + ) -> VariableValue<'output> + where + 'args: 'output, + 'compiled: 'output, + 'arena: 'output, + { + debug_assert_eq!(args.len(), self.inputs.len()); + let args: &[VariableValue<'output>] = args; + let string_arena = &raw mut *string_arena; + // SAFETY: Guaranteed by the caller. The input, compiled-function, and + // arena lifetimes outlive the lifetime selected for the returned value. + unsafe { (self.entry)(args.as_ptr(), string_arena) } + } +} + +/// Per-caller mutable state used to evaluate a shared [`CompiledFn`]. +pub struct CompiledFnCtx { + compiled_fn: Arc, + pub(crate) string_arena: StringArena, +} + +impl CompiledFnCtx { + /// Creates an evaluation context for `compiled_fn`. + pub fn new(compiled_fn: Arc) -> Self { + Self { + compiled_fn, + string_arena: StringArena::new(), + } + } + + /// Evaluates the compiled expression using this context's string arena. /// /// The mutable borrow prevents another evaluation from clearing the string /// arena while an arena-backed result from this call is still live. @@ -52,22 +115,38 @@ impl CompiledFn { /// slots must use [`VariableValue::none`], and any borrowed strings must /// remain alive for the duration of this call. /// - /// The result cannot outlive the compiled function nor the passed - /// arguments' lifetime. + /// The result cannot outlive this context nor the passed arguments' + /// lifetime. #[inline(always)] - pub unsafe fn call<'args, 'compiled, 'output>( - &'compiled mut self, + pub unsafe fn call<'args, 'ctx, 'output>( + &'ctx mut self, args: &[VariableValue<'args>], ) -> VariableValue<'output> where 'args: 'output, - 'compiled: 'output, + 'ctx: 'output, { - debug_assert_eq!(args.len(), self.inputs.len()); - let args: &[VariableValue<'output>] = args; - let string_arena = &raw mut self.string_arena; - // SAFETY: Guaranteed by the caller. Both the input and compiled-function - // lifetimes outlive the lifetime selected for the returned value. - unsafe { (self.entry)(args.as_ptr(), string_arena) } + // SAFETY: Guaranteed by the caller. The context owns both the compiled + // function and arena for the lifetime selected for the returned value. + unsafe { self.compiled_fn.call(args, &mut self.string_arena) } + } + + /// Returns the shared compiled expression owned by this context. + pub fn compiled_fn(&self) -> &Arc { + &self.compiled_fn + } +} + +impl From> for CompiledFnCtx { + fn from(compiled_fn: Arc) -> Self { + Self::new(compiled_fn) + } +} + +impl Deref for CompiledFnCtx { + type Target = CompiledFn; + + fn deref(&self) -> &Self::Target { + &self.compiled_fn } } diff --git a/jitexpr/src/compile/mod.rs b/jitexpr/src/compile/mod.rs index ef4cd89f1..de4e8cc44 100644 --- a/jitexpr/src/compile/mod.rs +++ b/jitexpr/src/compile/mod.rs @@ -6,9 +6,10 @@ mod typed_expr; mod typed_expr_serialize; use std::collections::HashMap; +use std::sync::Arc; pub(crate) use compile_fn_builder::CompileFnBuilder; -pub use compiled_fn::CompiledFn; +pub use compiled_fn::{CompiledFn, CompiledFnCtx}; use cranelift::codegen::ir::{ InstBuilder as _, MemFlagsData, Type, Value as CraneliftValue, types as cranelift_types, }; @@ -16,7 +17,7 @@ use cranelift::frontend::FunctionBuilder; pub use error::CompileError; #[cfg(test)] pub(crate) use string_arena::STRING_ARENA_CAPACITY; -pub(crate) use string_arena::StringArena; +pub use string_arena::StringArena; pub use typed_expr::TypedVariable; pub(crate) use typed_expr::{TypedExpr, TypedExprAst, TypedLiteral}; pub(crate) use typed_expr_serialize::{format_function_call, format_string_literal}; @@ -25,13 +26,14 @@ use crate::ast::UntypedExpr; use crate::functions::NativeFunctions; use crate::types::{VarType, VariablePrimitiveOpt, VariableValue}; +/// Compiles an expression into an immutable, shareable native function. pub fn compile( untyped_expr: &UntypedExpr, var_types: &HashMap<&str, VarType>, -) -> Result { +) -> Result, CompileError> { let mut builder = CompileFnBuilder::new(var_types); let typed_expr = builder.build_typed_expr(untyped_expr)?; - builder.compile_typed_expr(typed_expr) + builder.compile_typed_expr(typed_expr).map(Arc::new) } /// Applies concrete variable types and serializes the resulting typed expression. @@ -239,20 +241,46 @@ mod tests { fn test_compile_bool_variable() { let untyped_expr = UntypedExpr::variable("flag"); let variable_types = HashMap::from([("flag", VarType::Bool)]); - let mut compiled_fn = compile(&untyped_expr, &variable_types).unwrap(); - assert!(compiled_fn.string_arena.allocate(1).is_some()); + let compiled_fn = compile(&untyped_expr, &variable_types).unwrap(); + let mut string_arena = StringArena::new(); + assert!(string_arena.allocate(1).is_some()); let input = [VariableValue::some(true)]; - let output = unsafe { compiled_fn.call(&input) }; + let output = unsafe { compiled_fn.call(&input, &mut string_arena) }; assert_eq!(unsafe { output.as_bool() }, Some(true)); - assert_eq!(compiled_fn.string_arena.used_bytes(), 1); + assert_eq!(string_arena.used_bytes(), 1); + } + + #[test] + fn test_compiled_fn_is_send_and_sync() { + fn assert_send_and_sync() {} + + assert_send_and_sync::(); + } + + #[test] + fn test_contexts_have_independent_string_arenas() { + let untyped_expr = Function::Lower.call_untyped_expr(vec![UntypedExpr::variable("value")]); + let variable_types = HashMap::from([("value", VarType::Str)]); + let compiled_fn = compile(&untyped_expr, &variable_types).unwrap(); + let mut first_ctx = compiled_fn.context(); + let mut second_ctx = CompiledFnCtx::from(compiled_fn); + + let first = unsafe { first_ctx.call(&[VariableValue::some("FIRST")]) }; + assert_eq!(unsafe { first.as_str() }, Some("first")); + assert_eq!(first_ctx.string_arena.used_bytes(), 5); + + let second = unsafe { second_ctx.call(&[VariableValue::some("SECOND")]) }; + assert_eq!(unsafe { second.as_str() }, Some("second")); + assert_eq!(second_ctx.string_arena.used_bytes(), 6); + assert_eq!(first_ctx.string_arena.used_bytes(), 5); } #[test] fn test_compile_none_variable() { let untyped_expr = UntypedExpr::variable("value"); let variable_types = HashMap::from([("value", VarType::U64)]); - let mut compiled_fn = compile(&untyped_expr, &variable_types).unwrap(); + let mut compiled_fn = compile(&untyped_expr, &variable_types).unwrap().context(); let input = [VariableValue::none()]; assert_eq!(compiled_fn.result_type(), VarType::U64); let output = unsafe { compiled_fn.call(&input) }; @@ -263,7 +291,7 @@ mod tests { #[test] fn test_compile_string_literal_keeps_backing_data_alive() { let untyped_expr = UntypedExpr::literal("hello"); - let mut compiled_fn = compile(&untyped_expr, &HashMap::new()).unwrap(); + let mut compiled_fn = compile(&untyped_expr, &HashMap::new()).unwrap().context(); drop(untyped_expr); let output = unsafe { compiled_fn.call(&[]) }; @@ -274,7 +302,7 @@ mod tests { fn test_compile_returns_borrowed_string_variable_directly() { let untyped_expr = UntypedExpr::variable("value"); let variable_types = HashMap::from([("value", VarType::Str)]); - let mut compiled_fn = compile(&untyped_expr, &variable_types).unwrap(); + let mut compiled_fn = compile(&untyped_expr, &variable_types).unwrap().context(); let value = String::from("hello from an input"); let input = [VariableValue::some(value.as_str())]; let output = unsafe { compiled_fn.call(&input) }; @@ -287,7 +315,7 @@ mod tests { #[test] fn test_compile_none_literal_returns_absent_value() { let untyped_expr = UntypedExpr::literal(crate::ast::Literal::None); - let mut compiled_fn = compile(&untyped_expr, &HashMap::new()).unwrap(); + let mut compiled_fn = compile(&untyped_expr, &HashMap::new()).unwrap().context(); assert_eq!(compiled_fn.result_type(), VarType::None); let output = unsafe { compiled_fn.call(&[]) }; diff --git a/jitexpr/src/compile/string_arena.rs b/jitexpr/src/compile/string_arena.rs index 828ad8b28..bab3621d8 100644 --- a/jitexpr/src/compile/string_arena.rs +++ b/jitexpr/src/compile/string_arena.rs @@ -1,7 +1,7 @@ pub(crate) const STRING_ARENA_CAPACITY: usize = 262_144; /// Fixed-capacity storage for strings constructed while evaluating an expression. -pub(crate) struct StringArena { +pub struct StringArena { buffer: Box<[u8; STRING_ARENA_CAPACITY]>, cursor: usize, } @@ -9,7 +9,8 @@ pub(crate) struct StringArena { impl StringArena { pub(crate) const CURSOR_OFFSET: i32 = std::mem::offset_of!(StringArena, cursor) as i32; - pub(crate) fn new() -> Self { + /// Creates an empty string arena. + pub fn new() -> Self { let buffer = vec![0; STRING_ARENA_CAPACITY].into_boxed_slice(); let buffer = buffer .try_into() diff --git a/jitexpr/src/functions/abs.rs b/jitexpr/src/functions/abs.rs index 8371d23bd..2823df49e 100644 --- a/jitexpr/src/functions/abs.rs +++ b/jitexpr/src/functions/abs.rs @@ -192,7 +192,7 @@ mod tests { fn test_integer_edges_and_runtime_null() { let expression = deserialize("(ABS value)").unwrap(); let variable_types = HashMap::from([("value", VarType::I64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!( unsafe { compiled.call(&[VariableValue::some(-7i64)]).as_i64() }, Some(7) @@ -211,7 +211,7 @@ mod tests { fn test_float_preserves_negative_zero_and_nan_bits() { let expression = deserialize("(ABS value)").unwrap(); let variable_types = HashMap::from([("value", VarType::F64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let negative_zero = unsafe { compiled diff --git a/jitexpr/src/functions/add.rs b/jitexpr/src/functions/add.rs index ac13c3397..e9e78b509 100644 --- a/jitexpr/src/functions/add.rs +++ b/jitexpr/src/functions/add.rs @@ -294,7 +294,7 @@ mod tests { } ); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let output = unsafe { compiled.call(&[]) }; assert_eq!(unsafe { output.as_u64() }, Some(9223372036854775808u64)); @@ -325,7 +325,7 @@ mod tests { UntypedExpr::variable("myfield"), ]); let variable_types = HashMap::from([("myfield", VarType::I64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some(-8i64)]; let output = unsafe { compiled.call(&input) }; @@ -342,7 +342,7 @@ mod tests { for args in argument_orders { let expression = Function::Add.call_untyped_expr(args); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some(41u64)]; let output = unsafe { compiled.call(&input) }; assert_eq!(unsafe { output.as_u64() }, Some(42)); @@ -356,7 +356,7 @@ mod tests { let expression = Function::Add .call_untyped_expr(vec![UntypedExpr::variable("myfield"), nested_literals]); let variable_types = HashMap::from([("myfield", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some(39u64)]; let output = unsafe { compiled.call(&input) }; @@ -371,7 +371,7 @@ mod tests { UntypedExpr::literal(0.5f64), ]); let variable_types = HashMap::from([("myfield", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some(10u64)]; let output = unsafe { compiled.call(&input) }; @@ -383,7 +383,7 @@ mod tests { let expression = Function::Add .call_untyped_expr(vec![UntypedExpr::variable("x"), UntypedExpr::variable("y")]); let variable_types = HashMap::from([("x", VarType::U64), ("y", VarType::F64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some(10u64), VariableValue::some(0.5f64)]; let output = unsafe { compiled.call(&input) }; @@ -394,7 +394,7 @@ mod tests { fn test_compile_add_propagates_none_input() { let expression = crate::ast::deserialize(r#"(ADD x 0.5f64)"#).unwrap(); let variable_types = HashMap::from([("x", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::none()]; let output = unsafe { compiled.call(&input) }; assert_eq!(unsafe { output.as_f64() }, None); @@ -407,7 +407,7 @@ mod tests { UntypedExpr::literal(0.5f64), ]); let variable_types = HashMap::from([("x", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some(u64::MAX)]; let output = unsafe { compiled.call(&input) }; @@ -422,7 +422,7 @@ mod tests { UntypedExpr::literal(1u64), ]); let variable_types = HashMap::from([("x", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some(4i64)]; assert_eq!(compiled.inputs.len(), 1); assert_eq!(compiled.inputs[0].variable_name.as_ref(), "x"); @@ -441,7 +441,7 @@ mod tests { UntypedExpr::literal(1.2f64), ]); let variable_types = HashMap::from([("x", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some(4u64)]; assert_eq!(compiled.inputs.len(), 1); assert_eq!(compiled.inputs[0].variable_name.as_ref(), "x"); @@ -460,7 +460,7 @@ mod tests { assert_eq!(typed_expr.return_type, VarType::I64); let expression = Function::Add.call_untyped_expr(Vec::new()); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); let output = unsafe { compiled.call(&[]) }; assert_eq!(unsafe { output.as_i64() }, Some(0)); } @@ -472,7 +472,7 @@ mod tests { let typed_expr = crate::typed_expr_from_str("(ADD 1.2f64 1u64)", &variable_types); assert_eq!(typed_expr.return_type, VarType::F64); let expression = Function::Add.call_untyped_expr(args); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); let output = unsafe { compiled.call(&[]) }; assert_eq!(unsafe { output.as_f64() }, Some(2.2f64)); } diff --git a/jitexpr/src/functions/and.rs b/jitexpr/src/functions/and.rs index 884eb11d6..1e2fd8da5 100644 --- a/jitexpr/src/functions/and.rs +++ b/jitexpr/src/functions/and.rs @@ -120,7 +120,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no runtime inputs and return nullable booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -162,7 +162,7 @@ mod tests { fn test_runtime_null_propagation() { let expression = deserialize("(AND left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::Bool), ("right", VarType::Bool)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( diff --git a/jitexpr/src/functions/ceil.rs b/jitexpr/src/functions/ceil.rs index 95251d3f5..6e9e6d739 100644 --- a/jitexpr/src/functions/ceil.rs +++ b/jitexpr/src/functions/ceil.rs @@ -140,7 +140,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable i64 value. unsafe { compiled.call(&[]).as_i64() } } @@ -193,7 +193,9 @@ mod tests { #[test] fn test_runtime_null() { let expression = deserialize("(CEIL value)").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::F64)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::F64)])) + .unwrap() + .context(); // SAFETY: The compiled expression expects one nullable f64 argument. assert_eq!( diff --git a/jitexpr/src/functions/concat.rs b/jitexpr/src/functions/concat.rs index 1aaed5722..2745e67ec 100644 --- a/jitexpr/src/functions/concat.rs +++ b/jitexpr/src/functions/concat.rs @@ -10,7 +10,7 @@ //! written. Consequently `CONCAT(",", "false", "", "b")` is `"b"`, while the reversed values //! produce `"b,"`. Empty output is present, not null. //! -//! Constructed bytes live in `CompiledFn`'s fixed-capacity call arena; arena exhaustion returns +//! Constructed bytes live in the caller's fixed-capacity string arena; arena exhaustion returns //! null. use std::collections::HashMap; @@ -335,7 +335,7 @@ unsafe extern "C" fn string_concat( output_len = next_len; } - // SAFETY: CompiledFn exclusively owns and passes this arena for the duration of the call. + // SAFETY: The caller exclusively borrows and passes this arena for the duration of the call. let Some(output_ptr) = (unsafe { &mut *string_arena }).allocate(output_len) else { return RawStr::none(); }; @@ -384,7 +384,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable strings. unsafe { compiled.call(&[]).as_str().map(str::to_owned) } } @@ -434,7 +434,7 @@ mod tests { fn test_runtime_null_is_strict() { let expression = deserialize(r#"(CONCAT ":" "true" left right)"#).unwrap(); let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!( unsafe { @@ -450,7 +450,7 @@ mod tests { fn test_nested_values_are_stable_and_arena_exhaustion_is_null() { let expression = deserialize(r#"(CONCAT ":" "false" (UPPER left) (LOWER right))"#).unwrap(); let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/divide.rs b/jitexpr/src/functions/divide.rs index 69cad3f1c..bca9f3008 100644 --- a/jitexpr/src/functions/divide.rs +++ b/jitexpr/src/functions/divide.rs @@ -156,7 +156,7 @@ mod tests { #[test] fn test_integer_inputs_use_floating_point_division() { let expression = deserialize("(DIVIDE 5i64 2i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::F64); // SAFETY: The expression has no inputs and returns f64. @@ -167,7 +167,7 @@ mod tests { fn test_positive_and_negative_zero_divisors_return_none() { for expression in ["(DIVIDE 1f64 0f64)", "(DIVIDE 1f64 -0f64)"] { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns nullable f64. assert_eq!(unsafe { compiled.call(&[]).as_f64() }, None); } @@ -176,7 +176,7 @@ mod tests { #[test] fn test_nan_divisor_remains_present() { let expression = deserialize("(DIVIDE 1f64 nanf64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns f64. assert!(unsafe { compiled.call(&[]).as_f64() }.unwrap().is_nan()); @@ -186,7 +186,7 @@ mod tests { fn test_runtime_null_propagation() { let expression = deserialize("(DIVIDE left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::I64), ("right", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( diff --git a/jitexpr/src/functions/eq.rs b/jitexpr/src/functions/eq.rs index 2fc6d3103..87c70f10e 100644 --- a/jitexpr/src/functions/eq.rs +++ b/jitexpr/src/functions/eq.rs @@ -297,7 +297,7 @@ mod tests { fn eval(expression: &str) -> bool { let expression = ast::deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); let output = unsafe { compiled.call(&[]) }; unsafe { output.as_bool() }.unwrap() @@ -305,7 +305,7 @@ mod tests { fn eval_nullable(expression: &str) -> Option { let expression = ast::deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable boolean. unsafe { compiled.call(&[]).as_bool() } } @@ -352,7 +352,7 @@ mod tests { fn test_compile_signed_unsigned_comparison() { let expression = ast::deserialize("(EQ signed unsigned)").unwrap(); let variable_types = HashMap::from([("signed", VarType::I64), ("unsigned", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); for (signed, unsigned, expected) in [(7i64, 7u64, true), (-1, u64::MAX, false)] { let input = [VariableValue::some(signed), VariableValue::some(unsigned)]; @@ -365,7 +365,7 @@ mod tests { fn test_compile_float_integer_comparison_is_exact() { let expression = ast::deserialize("(EQ float integer)").unwrap(); let variable_types = HashMap::from([("float", VarType::F64), ("integer", VarType::I64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let cases = [ (1.0, 1, true), (1.2, 1, false), @@ -385,7 +385,7 @@ mod tests { fn test_compile_float_unsigned_comparison_is_exact() { let expression = ast::deserialize("(EQ float integer)").unwrap(); let variable_types = HashMap::from([("float", VarType::F64), ("integer", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let cases = [ (1.0, 1, true), (1.2, 1, false), @@ -405,7 +405,7 @@ mod tests { fn test_compile_string_equality_compares_contents() { let expression = ast::deserialize("(EQ left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let left_value = String::from("same contents"); let right_value = String::from("same contents"); let input = [ @@ -439,7 +439,7 @@ mod tests { fn test_compile_runtime_none_equality() { let expression = ast::deserialize("(EQ left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::U64), ("right", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let output = unsafe { compiled.call(&[VariableValue::none(), VariableValue::none()]) }; assert_eq!(unsafe { output.as_bool() }, None); @@ -451,7 +451,7 @@ mod tests { fn test_compile_none_literal_equals_absent_variable() { let expression = ast::deserialize("(EQ value none)").unwrap(); let variable_types = HashMap::from([("value", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let output = unsafe { compiled.call(&[VariableValue::none()]) }; assert_eq!(unsafe { output.as_bool() }, None); diff --git a/jitexpr/src/functions/floor.rs b/jitexpr/src/functions/floor.rs index 343370e97..bd7634339 100644 --- a/jitexpr/src/functions/floor.rs +++ b/jitexpr/src/functions/floor.rs @@ -140,7 +140,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable i64 value. unsafe { compiled.call(&[]).as_i64() } } @@ -193,7 +193,9 @@ mod tests { #[test] fn test_runtime_null() { let expression = deserialize("(FLOOR value)").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::F64)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::F64)])) + .unwrap() + .context(); // SAFETY: The compiled expression expects one nullable f64 argument. assert_eq!( diff --git a/jitexpr/src/functions/gt.rs b/jitexpr/src/functions/gt.rs index 4ff33bab0..f541ad391 100644 --- a/jitexpr/src/functions/gt.rs +++ b/jitexpr/src/functions/gt.rs @@ -82,7 +82,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -125,7 +125,7 @@ mod tests { let expression = deserialize("(GT float integer)").unwrap(); let variable_types = HashMap::from([("float", VarType::F64), ("integer", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( unsafe { @@ -146,7 +146,7 @@ mod tests { let expression = deserialize("(GT left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( unsafe { diff --git a/jitexpr/src/functions/gt_eq.rs b/jitexpr/src/functions/gt_eq.rs index d05bfdb96..b04c7a8d5 100644 --- a/jitexpr/src/functions/gt_eq.rs +++ b/jitexpr/src/functions/gt_eq.rs @@ -87,7 +87,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -129,7 +129,7 @@ mod tests { fn test_runtime_null_propagates() { let expression = deserialize("(GT_EQ left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( unsafe { diff --git a/jitexpr/src/functions/if_fn.rs b/jitexpr/src/functions/if_fn.rs index 5b425d63c..abdbd49c9 100644 --- a/jitexpr/src/functions/if_fn.rs +++ b/jitexpr/src/functions/if_fn.rs @@ -159,11 +159,11 @@ mod tests { )); for (expr, expected) in [("(IF true 7i64 9i64)", 7), ("(IF false 7i64 9i64)", 9)] { let expr = deserialize(expr).unwrap(); - let mut compiled = compile(&expr, &HashMap::new()).unwrap(); + let mut compiled = compile(&expr, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_i64() }, Some(expected)); } let expr = deserialize("(IF true \"yes\" \"no\")").unwrap(); - let mut compiled = compile(&expr, &HashMap::new()).unwrap(); + let mut compiled = compile(&expr, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_str() }, Some("yes")); } @@ -175,7 +175,7 @@ mod tests { ("yes", VarType::I64), ("no", VarType::I64), ]); - let mut compiled = compile(&expr, &types).unwrap(); + let mut compiled = compile(&expr, &types).unwrap().context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/int_mod.rs b/jitexpr/src/functions/int_mod.rs index f9645df55..bca5f672c 100644 --- a/jitexpr/src/functions/int_mod.rs +++ b/jitexpr/src/functions/int_mod.rs @@ -157,7 +157,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable f64 values. unsafe { compiled.call(&[]).as_f64() } } @@ -209,7 +209,7 @@ mod tests { fn test_runtime_null_propagates() { let expression = deserialize("(INT_MOD value modulus)").unwrap(); let variable_types = HashMap::from([("value", VarType::I64), ("modulus", VarType::F64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/is_not_null.rs b/jitexpr/src/functions/is_not_null.rs index aa9adc504..c263fdebf 100644 --- a/jitexpr/src/functions/is_not_null.rs +++ b/jitexpr/src/functions/is_not_null.rs @@ -103,7 +103,7 @@ mod tests { fn eval_without_args(expression: &str) -> (bool, Option) { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert!(compiled.inputs.is_empty()); // SAFETY: The compiled expression has no inputs. let output = unsafe { compiled.call(&[]) }; @@ -115,7 +115,7 @@ mod tests { fn eval_variable(var_type: VarType, input: VariableValue<'_>) -> (bool, Option) { let expression = deserialize("(IS_NOT_NULL value)").unwrap(); let variable_types = HashMap::from([("value", var_type)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: `input` is constructed with the union member matching `var_type` // at each call site, or is absent and therefore has no active payload. let output = unsafe { compiled.call(&[input]) }; diff --git a/jitexpr/src/functions/is_null.rs b/jitexpr/src/functions/is_null.rs index 1332621a6..71d5e570d 100644 --- a/jitexpr/src/functions/is_null.rs +++ b/jitexpr/src/functions/is_null.rs @@ -102,7 +102,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no runtime inputs and return booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -157,7 +157,7 @@ mod tests { fn test_runtime_absent_variable_is_true_and_present_variable_is_false() { let expression = deserialize("(IS_NULL value)").unwrap(); let variable_types = HashMap::from([("value", VarType::I64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( diff --git a/jitexpr/src/functions/left.rs b/jitexpr/src/functions/left.rs index a0b2d01d7..02750b1d9 100644 --- a/jitexpr/src/functions/left.rs +++ b/jitexpr/src/functions/left.rs @@ -133,7 +133,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable string. unsafe { compiled.call(&[]).as_str().map(str::to_owned) } } @@ -167,7 +167,9 @@ mod tests { #[test] fn test_runtime_null() { let expression = deserialize("(LEFT value 2i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])) + .unwrap() + .context(); // SAFETY: The compiled expression expects one nullable string argument. assert_eq!( diff --git a/jitexpr/src/functions/lower.rs b/jitexpr/src/functions/lower.rs index 4c6db127a..8c8b27eaf 100644 --- a/jitexpr/src/functions/lower.rs +++ b/jitexpr/src/functions/lower.rs @@ -5,7 +5,7 @@ //! than expanding to `i` followed by a combining dot. Null input returns null, while empty input //! remains present. //! -//! Constructed bytes live in `CompiledFn`'s fixed-capacity call arena; arena exhaustion returns +//! Constructed bytes live in the caller's fixed-capacity string arena; arena exhaustion returns //! null. use std::collections::HashMap; @@ -173,7 +173,7 @@ unsafe extern "C" fn string_lowercase( // The input reference above is no longer live. This matters for nested // LOWER calls whose input points into an earlier, disjoint arena allocation. - // SAFETY: CompiledFn exclusively owns and passes this arena for the call. + // SAFETY: The caller exclusively borrows and passes this arena for the call. let Some(output_ptr) = (unsafe { &mut *string_arena }).allocate(output_len) else { return RawStr::none(); }; @@ -248,7 +248,7 @@ mod tests { fn test_compile_lowercases_unicode_without_mutating_input() { let expression = deserialize("(LOWER value)").unwrap(); let variable_types = HashMap::from([("value", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input_string = String::from("CAFÉ İSTANBUL"); let input = [VariableValue::some(input_string.as_str())]; @@ -262,7 +262,7 @@ mod tests { fn test_compile_propagates_none_and_preserves_empty_string() { let expression = deserialize("(LOWER value)").unwrap(); let variable_types = HashMap::from([("value", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let none = unsafe { compiled.call(&[VariableValue::none()]) }; assert_eq!(unsafe { none.as_str() }, None); @@ -275,7 +275,7 @@ mod tests { fn test_nested_lower_uses_stable_arena_allocations() { let expression = deserialize("(LOWER (LOWER value))").unwrap(); let variable_types = HashMap::from([("value", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some("HeLLo")]; assert_eq!(unsafe { compiled.call(&input).as_str() }, Some("hello")); @@ -285,7 +285,7 @@ mod tests { fn test_multiple_lower_calls_keep_previous_allocations_valid() { let expression = deserialize("(EQ (LOWER left) (LOWER right))").unwrap(); let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some("FiRsT"), VariableValue::some("SeCoNd")]; let output = unsafe { compiled.call(&input) }; @@ -298,7 +298,7 @@ mod tests { fn test_arena_exhaustion_returns_none_without_advancing_cursor() { let expression = deserialize("(LOWER value)").unwrap(); let variable_types = HashMap::from([("value", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let too_large = "A".repeat(STRING_ARENA_CAPACITY + 1); let input = [VariableValue::some(too_large.as_str())]; @@ -312,7 +312,7 @@ mod tests { fn test_arena_cursor_is_cleared_before_each_call() { let expression = deserialize("(LOWER value)").unwrap(); let variable_types = HashMap::from([("value", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let full = "A".repeat(STRING_ARENA_CAPACITY); { diff --git a/jitexpr/src/functions/lt.rs b/jitexpr/src/functions/lt.rs index 67779bcb4..f78f6dd49 100644 --- a/jitexpr/src/functions/lt.rs +++ b/jitexpr/src/functions/lt.rs @@ -82,7 +82,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -130,7 +130,7 @@ mod tests { fn test_runtime_null_propagates() { let expression = deserialize("(LT left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::I64), ("right", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( unsafe { diff --git a/jitexpr/src/functions/lt_eq.rs b/jitexpr/src/functions/lt_eq.rs index 1698ac873..45dc52f64 100644 --- a/jitexpr/src/functions/lt_eq.rs +++ b/jitexpr/src/functions/lt_eq.rs @@ -87,7 +87,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -129,7 +129,7 @@ mod tests { fn test_runtime_null_propagates() { let expression = deserialize("(LT_EQ left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::F64), ("right", VarType::I64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( unsafe { diff --git a/jitexpr/src/functions/max.rs b/jitexpr/src/functions/max.rs index cc2bbe03d..7e56fb42e 100644 --- a/jitexpr/src/functions/max.rs +++ b/jitexpr/src/functions/max.rs @@ -171,20 +171,20 @@ mod tests { }) )); let expression = deserialize("(MAX 7i64 -3i64 12i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_i64() }, Some(12)); let expression = deserialize("(MAX 7u64 13u64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_u64() }, Some(13)); } #[test] fn test_float_nan_and_null_behavior() { let expression = deserialize("(MAX nanf64 3f64 -2f64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_f64() }, Some(3.0)); let expression = deserialize("(MAX nanf64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!( unsafe { compiled.call(&[]).as_f64() }, Some(f64::NEG_INFINITY) @@ -194,7 +194,8 @@ mod tests { &expression, &HashMap::from([("left", VarType::F64), ("right", VarType::F64)]), ) - .unwrap(); + .unwrap() + .context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/min.rs b/jitexpr/src/functions/min.rs index ae0885068..80fc0fb54 100644 --- a/jitexpr/src/functions/min.rs +++ b/jitexpr/src/functions/min.rs @@ -170,20 +170,20 @@ mod tests { )); let expression = deserialize("(MIN 7i64 -3i64 2i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_i64() }, Some(-3)); let expression = deserialize("(MIN 7u64 3u64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_u64() }, Some(3)); } #[test] fn test_float_nan_and_null_behavior() { let expression = deserialize("(MIN nanf64 3f64 -2f64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_f64() }, Some(-2.0)); let expression = deserialize("(MIN nanf64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_f64() }, Some(f64::INFINITY)); let expression = deserialize("(MIN left right)").unwrap(); @@ -191,7 +191,8 @@ mod tests { &expression, &HashMap::from([("left", VarType::I64), ("right", VarType::I64)]), ) - .unwrap(); + .unwrap() + .context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/multiply.rs b/jitexpr/src/functions/multiply.rs index 8e79142d6..b98dcd15c 100644 --- a/jitexpr/src/functions/multiply.rs +++ b/jitexpr/src/functions/multiply.rs @@ -176,18 +176,18 @@ mod tests { #[test] fn test_signed_unsigned_and_float_multiplication() { let expression = deserialize("(MULTIPLY -7i64 3i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns i64. assert_eq!(unsafe { compiled.call(&[]).as_i64() }, Some(-21)); let expression = deserialize("(MULTIPLY 9223372036854775808u64 2u64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::U64); // SAFETY: The expression has no inputs and returns u64; multiplication wraps. assert_eq!(unsafe { compiled.call(&[]).as_u64() }, Some(0)); let expression = deserialize("(MULTIPLY 1.5f64 2f64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns f64. assert_eq!(unsafe { compiled.call(&[]).as_f64() }, Some(3.0)); } @@ -195,7 +195,7 @@ mod tests { #[test] fn test_nan_is_preserved() { let expression = deserialize("(MULTIPLY nanf64 2f64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns f64. assert!(unsafe { compiled.call(&[]).as_f64() }.unwrap().is_nan()); @@ -205,7 +205,7 @@ mod tests { fn test_runtime_null_propagation_and_integer_coercion() { let expression = deserialize("(MULTIPLY value 3i64)").unwrap(); let variable_types = HashMap::from([("value", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!(compiled.result_type(), VarType::U64); // SAFETY: The input and output types match the compiled signature. @@ -223,7 +223,7 @@ mod tests { #[test] fn test_compile_time_none_propagates() { let expression = deserialize("(MULTIPLY none 2i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::None); // SAFETY: The expression has no inputs and returns an absent value. diff --git a/jitexpr/src/functions/neq.rs b/jitexpr/src/functions/neq.rs index 6cfcee528..b735f56b1 100644 --- a/jitexpr/src/functions/neq.rs +++ b/jitexpr/src/functions/neq.rs @@ -117,7 +117,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -157,7 +157,7 @@ mod tests { let expression = deserialize("(NEQ left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/not.rs b/jitexpr/src/functions/not.rs index 0e63a7a49..7253c4d23 100644 --- a/jitexpr/src/functions/not.rs +++ b/jitexpr/src/functions/not.rs @@ -112,7 +112,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no runtime inputs and return booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -157,7 +157,7 @@ mod tests { let expression = deserialize("(NOT value)").unwrap(); let variable_types = HashMap::from([("value", VarType::Bool)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( diff --git a/jitexpr/src/functions/or.rs b/jitexpr/src/functions/or.rs index 9d5bca0f4..652004d65 100644 --- a/jitexpr/src/functions/or.rs +++ b/jitexpr/src/functions/or.rs @@ -120,7 +120,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no runtime inputs and return nullable booleans. unsafe { compiled.call(&[]).as_bool() } } @@ -163,7 +163,7 @@ mod tests { fn test_runtime_null_handling() { let expression = deserialize("(OR left right)").unwrap(); let variable_types = HashMap::from([("left", VarType::Bool), ("right", VarType::Bool)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( diff --git a/jitexpr/src/functions/pow.rs b/jitexpr/src/functions/pow.rs index 7e3785897..e414e001d 100644 --- a/jitexpr/src/functions/pow.rs +++ b/jitexpr/src/functions/pow.rs @@ -152,7 +152,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable f64 values. unsafe { compiled.call(&[]).as_f64() } } @@ -196,7 +196,7 @@ mod tests { fn test_runtime_null_propagates() { let expression = deserialize("(POW base exponent)").unwrap(); let variable_types = HashMap::from([("base", VarType::I64), ("exponent", VarType::F64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/regexp_extract.rs b/jitexpr/src/functions/regexp_extract.rs index 376f81cf0..ca8e9fe3d 100644 --- a/jitexpr/src/functions/regexp_extract.rs +++ b/jitexpr/src/functions/regexp_extract.rs @@ -296,7 +296,7 @@ mod tests { let expression = ast::deserialize(r#"(REGEXP_EXTRACT message "([a-z]+)-(\\d+)" 1u64)"#).unwrap(); let variable_types = HashMap::from([("message", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let haystack = "prefix user-123 suffix"; let input = [VariableValue::some(haystack)]; let output = unsafe { compiled.call(&input) }; @@ -311,7 +311,7 @@ mod tests { let expression = ast::deserialize(r#"(REGEXP_EXTRACT message "([a-z]+)-(\\d+)" 2u64)"#).unwrap(); let variable_types = HashMap::from([("message", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some("user-123")]; let output = unsafe { compiled.call(&input) }; @@ -323,7 +323,7 @@ mod tests { let expression = ast::deserialize(r#"(REGEXP_EXTRACT message "([a-z]+)-(\\d+)" 0u64)"#).unwrap(); let variable_types = HashMap::from([("message", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some("no digits here")]; let output = unsafe { compiled.call(&input) }; @@ -334,7 +334,7 @@ mod tests { fn test_compile_propagates_none_haystack() { let expression = ast::deserialize(r#"(REGEXP_EXTRACT message "([a-z]+)" 0u64)"#).unwrap(); let variable_types = HashMap::from([("message", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::none()]; let output = unsafe { compiled.call(&input) }; @@ -344,7 +344,7 @@ mod tests { #[test] fn test_compile_propagates_compile_time_none_haystack() { let expression = ast::deserialize(r#"(REGEXP_EXTRACT missing "([a-z]+)" 0u64)"#).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::None); let output = unsafe { compiled.call(&[]) }; @@ -354,7 +354,7 @@ mod tests { #[test] fn test_compile_distinguishes_empty_capture_from_none() { let expression = ast::deserialize(r#"(REGEXP_EXTRACT "b" "(a*)b" 1u64)"#).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); let output = unsafe { compiled.call(&[]) }; assert_eq!(unsafe { output.as_str() }, Some("")); @@ -364,7 +364,7 @@ mod tests { fn test_compile_omitted_group_defaults_to_full_match() { let expression = ast::deserialize(r#"(REGEXP_EXTRACT message "[a-z]+-\\d+")"#).unwrap(); let variable_types = HashMap::from([("message", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some("prefix user-123 suffix")]; let output = unsafe { compiled.call(&input) }; @@ -376,7 +376,7 @@ mod tests { let expression = ast::deserialize(r#"(REGEXP_EXTRACT message "[a-z]+-\\d+" 0u64)"#).unwrap(); let variable_types = HashMap::from([("message", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some("prefix user-123 suffix")]; let output = unsafe { compiled.call(&input) }; @@ -393,7 +393,7 @@ mod tests { ) .unwrap(); let variable_types = HashMap::from([("message", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input = [VariableValue::some("id=user-123!")]; let output = unsafe { compiled.call(&input) }; diff --git a/jitexpr/src/functions/regexp_like.rs b/jitexpr/src/functions/regexp_like.rs index 47d2275f4..38c61d567 100644 --- a/jitexpr/src/functions/regexp_like.rs +++ b/jitexpr/src/functions/regexp_like.rs @@ -171,7 +171,7 @@ mod tests { use crate::types::VariableValue; fn eval(expr: &str) -> Option { let expr = deserialize(expr).unwrap(); - let mut compiled = compile(&expr, &HashMap::new()).unwrap(); + let mut compiled = compile(&expr, &HashMap::new()).unwrap().context(); unsafe { compiled.call(&[]).as_bool() } } #[test] @@ -193,7 +193,9 @@ mod tests { assert_eq!(eval("(REGEXP_LIKE none \"x\")"), Some(false)); assert_eq!(eval("(REGEXP_LIKE 123i64 \"123\")"), Some(false)); let expr = deserialize("(REGEXP_LIKE value \"x\")").unwrap(); - let mut compiled = compile(&expr, &HashMap::from([("value", VarType::Str)])).unwrap(); + let mut compiled = compile(&expr, &HashMap::from([("value", VarType::Str)])) + .unwrap() + .context(); assert_eq!( unsafe { compiled.call(&[VariableValue::none()]).as_bool() }, Some(false) diff --git a/jitexpr/src/functions/right.rs b/jitexpr/src/functions/right.rs index 2a8ef1024..53ef6743d 100644 --- a/jitexpr/src/functions/right.rs +++ b/jitexpr/src/functions/right.rs @@ -140,7 +140,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable string. unsafe { compiled.call(&[]).as_str().map(str::to_owned) } } @@ -174,7 +174,9 @@ mod tests { #[test] fn test_runtime_null() { let expression = deserialize("(RIGHT value 2i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])) + .unwrap() + .context(); assert_eq!( unsafe { compiled.call(&[VariableValue::some("abc")]).as_str() }, Some("bc") @@ -188,10 +190,10 @@ mod tests { #[test] fn test_utf8_boundary_break_returns_null() { let expression = deserialize(r#"(RIGHT "下北沢" 1i64)"#).unwrap(); - let mut compiled = compile(&expression, &HashMap::default()).unwrap(); + let mut compiled = compile(&expression, &HashMap::default()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_str() }, None); let expression = deserialize(r#"(RIGHT "下北沢" 3i64)"#).unwrap(); - let mut compiled = compile(&expression, &HashMap::default()).unwrap(); + let mut compiled = compile(&expression, &HashMap::default()).unwrap().context(); assert_eq!(unsafe { compiled.call(&[]).as_str() }, Some("沢")); } } diff --git a/jitexpr/src/functions/round.rs b/jitexpr/src/functions/round.rs index f3baabb90..00bc36cf8 100644 --- a/jitexpr/src/functions/round.rs +++ b/jitexpr/src/functions/round.rs @@ -381,14 +381,14 @@ mod tests { fn eval_i64(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable i64 value. unsafe { compiled.call(&[]).as_i64() } } fn eval_f64(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable f64 value. unsafe { compiled.call(&[]).as_f64() } } @@ -467,7 +467,9 @@ mod tests { #[test] fn test_runtime_null_and_integer_rounding() { let expression = deserialize("(ROUND value -1i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::I64)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::I64)])) + .unwrap() + .context(); // SAFETY: The compiled expression expects one nullable i64 argument. assert_eq!( diff --git a/jitexpr/src/functions/split_after.rs b/jitexpr/src/functions/split_after.rs index 43f66b1a8..4a393fe09 100644 --- a/jitexpr/src/functions/split_after.rs +++ b/jitexpr/src/functions/split_after.rs @@ -240,7 +240,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable string. unsafe { compiled.call(&[]).as_str().map(str::to_owned) } } @@ -295,7 +295,9 @@ mod tests { #[test] fn test_runtime_null() { let expression = deserialize("(SPLIT_AFTER value \".\")").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])) + .unwrap() + .context(); // SAFETY: The compiled expression expects one nullable string argument. assert_eq!( diff --git a/jitexpr/src/functions/split_before.rs b/jitexpr/src/functions/split_before.rs index a26efe128..6b913bba7 100644 --- a/jitexpr/src/functions/split_before.rs +++ b/jitexpr/src/functions/split_before.rs @@ -239,7 +239,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable string. unsafe { compiled.call(&[]).as_str().map(str::to_owned) } } @@ -306,7 +306,9 @@ mod tests { #[test] fn test_runtime_null() { let expression = deserialize("(SPLIT_BEFORE value \".\")").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])) + .unwrap() + .context(); // SAFETY: The compiled expression expects one nullable string argument. assert_eq!( diff --git a/jitexpr/src/functions/sqrt.rs b/jitexpr/src/functions/sqrt.rs index 7f4e4aba0..8663ff0e3 100644 --- a/jitexpr/src/functions/sqrt.rs +++ b/jitexpr/src/functions/sqrt.rs @@ -109,7 +109,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable f64 value. unsafe { compiled.call(&[]).as_f64() } } @@ -160,7 +160,9 @@ mod tests { #[test] fn test_runtime_null_and_negative_input() { let expression = deserialize("(SQRT value)").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::I64)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::I64)])) + .unwrap() + .context(); // SAFETY: The compiled expression expects one nullable i64 argument. assert_eq!( diff --git a/jitexpr/src/functions/substring.rs b/jitexpr/src/functions/substring.rs index 082d2e794..0f8c8b328 100644 --- a/jitexpr/src/functions/substring.rs +++ b/jitexpr/src/functions/substring.rs @@ -228,7 +228,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: The expression has no inputs and returns a nullable string. unsafe { compiled.call(&[]).as_str().map(str::to_owned) } } @@ -271,7 +271,9 @@ mod tests { assert_eq!(eval("(SUBSTRING \"abc\" -1i64 1i64)"), None); let expression = deserialize("(SUBSTRING value 0i64 2i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])).unwrap(); + let mut compiled = compile(&expression, &HashMap::from([("value", VarType::Str)])) + .unwrap() + .context(); assert_eq!( unsafe { compiled.call(&[VariableValue::some("abc")]).as_str() }, Some("ab") diff --git a/jitexpr/src/functions/substring_count.rs b/jitexpr/src/functions/substring_count.rs index 174508d92..fddf617e1 100644 --- a/jitexpr/src/functions/substring_count.rs +++ b/jitexpr/src/functions/substring_count.rs @@ -159,7 +159,7 @@ mod tests { use crate::types::VariableValue; fn eval(expr: &str) -> Option { let expr = deserialize(expr).unwrap(); - let mut compiled = compile(&expr, &HashMap::new()).unwrap(); + let mut compiled = compile(&expr, &HashMap::new()).unwrap().context(); unsafe { compiled.call(&[]).as_i64() } } #[test] @@ -183,7 +183,8 @@ mod tests { &expr, &HashMap::from([("text", VarType::Str), ("needle", VarType::Str)]), ) - .unwrap(); + .unwrap() + .context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/subtract.rs b/jitexpr/src/functions/subtract.rs index 86770f214..e0062cfd0 100644 --- a/jitexpr/src/functions/subtract.rs +++ b/jitexpr/src/functions/subtract.rs @@ -177,13 +177,13 @@ mod tests { #[test] fn test_signed_unsigned_and_float_subtraction() { let expression = deserialize("(SUBTRACT 7i64 10i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::I64); // SAFETY: The expression has no inputs and returns i64. assert_eq!(unsafe { compiled.call(&[]).as_i64() }, Some(-3)); let expression = deserialize("(SUBTRACT 9223372036854775810u64 1u64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::U64); // SAFETY: The expression has no inputs and returns u64. assert_eq!( @@ -192,7 +192,7 @@ mod tests { ); let expression = deserialize("(SUBTRACT 7.5f64 2f64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::F64); // SAFETY: The expression has no inputs and returns f64. assert_eq!(unsafe { compiled.call(&[]).as_f64() }, Some(5.5)); @@ -202,7 +202,7 @@ mod tests { fn test_unsigned_subtraction_wraps() { let expression = deserialize("(SUBTRACT value 1u64)").unwrap(); let variable_types = HashMap::from([("value", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); // SAFETY: The input and output types match the compiled signature. assert_eq!( @@ -215,7 +215,7 @@ mod tests { fn test_runtime_null_propagation_and_mixed_literal_coercion() { let expression = deserialize("(SUBTRACT value 1i64)").unwrap(); let variable_types = HashMap::from([("value", VarType::U64)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!(compiled.result_type(), VarType::U64); // SAFETY: The input and output types match the compiled signature. @@ -233,7 +233,7 @@ mod tests { #[test] fn test_compile_time_none_propagates() { let expression = deserialize("(SUBTRACT none 1i64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::None); // SAFETY: The expression has no inputs and returns an absent value. @@ -243,7 +243,7 @@ mod tests { #[test] fn test_compile_time_i64() { let expression = deserialize("(SUBTRACT 1u64 10u64)").unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); assert_eq!(compiled.result_type(), VarType::I64); // SAFETY: The expression has no inputs and returns an absent value. diff --git a/jitexpr/src/functions/text_join.rs b/jitexpr/src/functions/text_join.rs index 2fb36498b..c1e88af23 100644 --- a/jitexpr/src/functions/text_join.rs +++ b/jitexpr/src/functions/text_join.rs @@ -82,7 +82,7 @@ mod tests { fn eval(expression: &str) -> Option { let expression = deserialize(expression).unwrap(); - let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap().context(); // SAFETY: These expressions have no inputs and return nullable strings. unsafe { compiled.call(&[]).as_str().map(str::to_owned) } } @@ -125,7 +125,7 @@ mod tests { fn test_runtime_null_is_strict() { let expression = deserialize(r#"(TEXT_JOIN "" "false" left right)"#).unwrap(); let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!( unsafe { compiled diff --git a/jitexpr/src/functions/trim.rs b/jitexpr/src/functions/trim.rs index e91179086..ecbbf4f20 100644 --- a/jitexpr/src/functions/trim.rs +++ b/jitexpr/src/functions/trim.rs @@ -237,7 +237,7 @@ mod tests { fn eval(expr: &str) -> Option { let expr = deserialize(expr).unwrap(); - let mut compiled = compile(&expr, &HashMap::new()).unwrap(); + let mut compiled = compile(&expr, &HashMap::new()).unwrap().context(); unsafe { compiled.call(&[]).as_str().map(str::to_owned) } } @@ -270,7 +270,9 @@ mod tests { #[test] fn test_runtime_null_and_unicode() { let expr = deserialize("(TRIM value \"é\" \"both\")").unwrap(); - let mut compiled = compile(&expr, &HashMap::from([("value", VarType::Str)])).unwrap(); + let mut compiled = compile(&expr, &HashMap::from([("value", VarType::Str)])) + .unwrap() + .context(); assert_eq!( unsafe { compiled.call(&[VariableValue::some("éhelloé")]).as_str() }, Some("hello") diff --git a/jitexpr/src/functions/upper.rs b/jitexpr/src/functions/upper.rs index e1d300e9e..944cc1824 100644 --- a/jitexpr/src/functions/upper.rs +++ b/jitexpr/src/functions/upper.rs @@ -5,7 +5,7 @@ //! into several (for example `ß` to `SS`) are not applied. Null input returns null, while an empty //! input returns a present empty string. //! -//! Constructed bytes live in `CompiledFn`'s fixed-capacity call arena; arena exhaustion returns +//! Constructed bytes live in the caller's fixed-capacity string arena; arena exhaustion returns //! null. use std::collections::HashMap; @@ -171,7 +171,7 @@ unsafe extern "C" fn string_uppercase( }; // The input borrow has ended. Nested calls may pass an earlier, disjoint arena allocation. - // SAFETY: CompiledFn exclusively owns and passes this arena for the duration of the call. + // SAFETY: The caller exclusively borrows and passes this arena for the duration of the call. let Some(output_ptr) = (unsafe { &mut *string_arena }).allocate(output_len) else { return RawStr::none(); }; @@ -245,7 +245,7 @@ mod tests { fn test_uppercases_unicode_without_full_case_expansion_or_mutation() { let expression = deserialize("(UPPER value)").unwrap(); let variable_types = HashMap::from([("value", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); let input_string = String::from("café Straße ı"); let output = unsafe { compiled.call(&[VariableValue::some(input_string.as_str())]) }; @@ -258,7 +258,7 @@ mod tests { fn test_null_empty_nested_and_arena_exhaustion() { let expression = deserialize("(UPPER (UPPER value))").unwrap(); let variable_types = HashMap::from([("value", VarType::Str)]); - let mut compiled = compile(&expression, &variable_types).unwrap(); + let mut compiled = compile(&expression, &variable_types).unwrap().context(); assert_eq!( unsafe { compiled.call(&[VariableValue::none()]).as_str() },