Make compiled JIT functions immutable

This commit is contained in:
Paul Masurel
2026-09-03 11:11:02 +02:00
parent fed69b5045
commit b5a5c6c0a0
43 changed files with 299 additions and 166 deletions
+5 -3
View File
@@ -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<dyn Error>> {
@@ -28,13 +29,14 @@ fn main() -> Result<(), Box<dyn Error>> {
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<CompiledFn> = 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(())
+1 -3
View File
@@ -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,
})
}
+93 -14
View File
@@ -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<TypedVariable>,
pub(crate) string_arena: StringArena,
// This AST owns the Arc-backed literals and regexes embedded in generated code.
pub(crate) _typed_expr: Box<TypedExpr>,
}
// `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<Self>) -> 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<CompiledFn>,
pub(crate) string_arena: StringArena,
}
impl CompiledFnCtx {
/// Creates an evaluation context for `compiled_fn`.
pub fn new(compiled_fn: Arc<CompiledFn>) -> 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<CompiledFn> {
&self.compiled_fn
}
}
impl From<Arc<CompiledFn>> for CompiledFnCtx {
fn from(compiled_fn: Arc<CompiledFn>) -> Self {
Self::new(compiled_fn)
}
}
impl Deref for CompiledFnCtx {
type Target = CompiledFn;
fn deref(&self) -> &Self::Target {
&self.compiled_fn
}
}
+40 -12
View File
@@ -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<CompiledFn, CompileError> {
) -> Result<Arc<CompiledFn>, 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<T: Send + Sync>() {}
assert_send_and_sync::<CompiledFn>();
}
#[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(&[]) };
+3 -2
View File
@@ -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()
+2 -2
View File
@@ -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
+12 -12
View File
@@ -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));
}
+2 -2
View File
@@ -120,7 +120,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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!(
+4 -2
View File
@@ -140,7 +140,7 @@ mod tests {
fn eval(expression: &str) -> Option<i64> {
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!(
+5 -5
View File
@@ -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<String> {
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
+4 -4
View File
@@ -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!(
+8 -8
View File
@@ -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<bool> {
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);
+4 -2
View File
@@ -140,7 +140,7 @@ mod tests {
fn eval(expression: &str) -> Option<i64> {
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!(
+3 -3
View File
@@ -82,7 +82,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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 {
+2 -2
View File
@@ -87,7 +87,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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 {
+3 -3
View File
@@ -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
+2 -2
View File
@@ -157,7 +157,7 @@ mod tests {
fn eval(expression: &str) -> Option<f64> {
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
+2 -2
View File
@@ -103,7 +103,7 @@ mod tests {
fn eval_without_args(expression: &str) -> (bool, Option<bool>) {
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<bool>) {
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]) };
+2 -2
View File
@@ -102,7 +102,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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!(
+4 -2
View File
@@ -133,7 +133,7 @@ mod tests {
fn eval(expression: &str) -> Option<String> {
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!(
+8 -8
View File
@@ -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);
{
+2 -2
View File
@@ -82,7 +82,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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 {
+2 -2
View File
@@ -87,7 +87,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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 {
+6 -5
View File
@@ -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
+6 -5
View File
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -117,7 +117,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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
+2 -2
View File
@@ -112,7 +112,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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!(
+2 -2
View File
@@ -120,7 +120,7 @@ mod tests {
fn eval(expression: &str) -> Option<bool> {
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!(
+2 -2
View File
@@ -152,7 +152,7 @@ mod tests {
fn eval(expression: &str) -> Option<f64> {
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
+9 -9
View File
@@ -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) };
+4 -2
View File
@@ -171,7 +171,7 @@ mod tests {
use crate::types::VariableValue;
fn eval(expr: &str) -> Option<bool> {
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)
+6 -4
View File
@@ -140,7 +140,7 @@ mod tests {
fn eval(expression: &str) -> Option<String> {
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("沢"));
}
}
+5 -3
View File
@@ -381,14 +381,14 @@ mod tests {
fn eval_i64(expression: &str) -> Option<i64> {
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<f64> {
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!(
+4 -2
View File
@@ -240,7 +240,7 @@ mod tests {
fn eval(expression: &str) -> Option<String> {
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!(
+4 -2
View File
@@ -239,7 +239,7 @@ mod tests {
fn eval(expression: &str) -> Option<String> {
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!(
+4 -2
View File
@@ -109,7 +109,7 @@ mod tests {
fn eval(expression: &str) -> Option<f64> {
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!(
+4 -2
View File
@@ -228,7 +228,7 @@ mod tests {
fn eval(expression: &str) -> Option<String> {
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")
+3 -2
View File
@@ -159,7 +159,7 @@ mod tests {
use crate::types::VariableValue;
fn eval(expr: &str) -> Option<i64> {
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
+7 -7
View File
@@ -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.
+2 -2
View File
@@ -82,7 +82,7 @@ mod tests {
fn eval(expression: &str) -> Option<String> {
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
+4 -2
View File
@@ -237,7 +237,7 @@ mod tests {
fn eval(expr: &str) -> Option<String> {
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")
+4 -4
View File
@@ -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() },