From f6fedef60e792a031fdbc07ee0f65ac74f711f9c Mon Sep 17 00:00:00 2001 From: Paul Masurel Date: Tue, 18 Aug 2026 15:00:30 +0200 Subject: [PATCH] Add GT_EQ function --- jitexpr/docs/FUNCTIONS.md | 4 +- jitexpr/src/ast/serialize.rs | 2 + jitexpr/src/functions/gt_eq.rs | 141 +++++++++++++++++++++++++++++++++ jitexpr/src/functions/mod.rs | 13 +++ 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 jitexpr/src/functions/gt_eq.rs diff --git a/jitexpr/docs/FUNCTIONS.md b/jitexpr/docs/FUNCTIONS.md index 20bae06b2..aea427db3 100644 --- a/jitexpr/docs/FUNCTIONS.md +++ b/jitexpr/docs/FUNCTIONS.md @@ -30,7 +30,7 @@ excluded from this pass. | 8 | `EQ` | done | | 9 | `GT` | done | | 10 | `LT` | done | -| 11 | `GT_EQ` | pending | +| 11 | `GT_EQ` | done | | 12 | `LT_EQ` | pending | | 13 | `IS_NULL` | done | | 14 | `IS_NOT_NULL` | done | @@ -76,4 +76,4 @@ excluded from this pass. | 79 | `SUBSTRING_COUNT` | pending | | 80 | `REGEXP_LIKE` | pending | -Progress: **14 / 48 in-scope** functions implemented; **7** functions are out-of-scope. +Progress: **15 / 48 in-scope** functions implemented; **7** functions are out-of-scope. diff --git a/jitexpr/src/ast/serialize.rs b/jitexpr/src/ast/serialize.rs index 1af7b8131..1811097c7 100644 --- a/jitexpr/src/ast/serialize.rs +++ b/jitexpr/src/ast/serialize.rs @@ -123,6 +123,7 @@ fn function_name(function: Function) -> &'static str { Function::Divide => "DIVIDE", Function::Eq => "EQ", Function::Gt => "GT", + Function::GtEq => "GT_EQ", Function::Lt => "LT", Function::IsNull => "IS_NULL", Function::IsNotNull => "IS_NOT_NULL", @@ -142,6 +143,7 @@ fn parse_function(name: &str, offset: usize) -> Result Ok(Function::Divide), "EQ" => Ok(Function::Eq), "GT" => Ok(Function::Gt), + "GT_EQ" => Ok(Function::GtEq), "LT" => Ok(Function::Lt), "IS_NULL" => Ok(Function::IsNull), "IS_NOT_NULL" => Ok(Function::IsNotNull), diff --git a/jitexpr/src/functions/gt_eq.rs b/jitexpr/src/functions/gt_eq.rs new file mode 100644 index 000000000..df50a9346 --- /dev/null +++ b/jitexpr/src/functions/gt_eq.rs @@ -0,0 +1,141 @@ +//! `GT_EQ` tests whether one scalar value is greater than or equal to another. +//! +//! It accepts exactly two operands. Ordered production operands are strings or numbers; booleans +//! are rejected. Strings use lexicographic UTF-8 ordering. Numeric comparisons support `i64`, +//! `u64`, and `f64` combinations without converting large integers through a lossy `f64`. IEEE +//! unordered comparisons involving NaN return `false`, including `NaN >= NaN`. +//! +//! Null propagation is strict: if either operand is absent, the result is absent. This follows +//! dd-go's predicate path (`arrayCOMPARE` followed by `propagateNullsPredicateLazy`). Multivalued +//! comparison uses existential semantics in dd-go but is outside jitexpr's scalar type model. + +use std::collections::HashMap; + +use cranelift::frontend::FunctionBuilder; + +use super::comparison::{self, OrderedComparison}; +use crate::ast::{Function, InferredTypeSet, TypeError, UntypedExpr}; +use crate::compile::{ + CompileError, CompileFnBuilder, LoweredValue, LoweringContext, TypedExpr, TypedExprAst, +}; +use crate::functions::{FnCall, FnCallEnum}; +use crate::types::VarType; + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct GtEqFnCall { + pub(crate) args: Box<[TypedExpr]>, +} + +impl FnCall for GtEqFnCall { + fn infer_types<'a>( + args: &'a [UntypedExpr], + target_type: InferredTypeSet, + inferred_types: &mut HashMap<&'a str, InferredTypeSet>, + ) -> Result { + comparison::infer_types(Function::GtEq, args, target_type, inferred_types) + } + + fn call_with_types( + args: &[UntypedExpr], + target_type_set: InferredTypeSet, + context: &mut CompileFnBuilder<'_, '_>, + ) -> Result { + assert_eq!(args.len(), 2, "expected 2 args for GT_EQ"); + debug_assert!(target_type_set.contains(VarType::Bool)); + Ok(TypedExpr { + return_type: VarType::Bool, + ast: TypedExprAst::from_call(GtEqFnCall { + args: comparison::apply_types(args, context)?, + }), + }) + } + + fn args_mut(&mut self) -> &mut [TypedExpr] { + &mut self.args + } + + fn emit_cranelift_ir( + &self, + return_type: VarType, + context: &mut LoweringContext<'_>, + builder: &mut FunctionBuilder<'_>, + ) -> Result { + debug_assert_eq!(return_type, VarType::Bool); + comparison::lower( + &self.args, + OrderedComparison::GreaterThanOrEqual, + context, + builder, + ) + } +} + +impl From for FnCallEnum { + fn from(call: GtEqFnCall) -> Self { + FnCallEnum::GtEq(call) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{deserialize, infer_types}; + use crate::compile::compile; + use crate::types::VariableValue; + + fn eval(expression: &str) -> Option { + let expression = deserialize(expression).unwrap(); + let mut compiled = compile(&expression, &HashMap::new()).unwrap(); + // SAFETY: These expressions have no inputs and return nullable booleans. + unsafe { compiled.call(&[]).as_bool() } + } + + #[test] + fn test_requires_two_ordered_arguments() { + let expression = deserialize("(GT_EQ left right)").unwrap(); + let inferred_types = infer_types(&expression).unwrap(); + let left = inferred_types.get("left").unwrap(); + assert!(left.string && left.i64 && left.u64 && left.f64 && !left.boolean); + + let expression = deserialize("(GT_EQ 1i64 2i64 3i64)").unwrap(); + assert!(matches!( + infer_types(&expression), + Err(TypeError::InvalidNumberOfArguments { + function: Function::GtEq, + expected: 2, + .. + }) + )); + } + + #[test] + fn test_ordering_equality_nan_and_null() { + assert_eq!(eval(r#"(GT_EQ "same" "same")"#), Some(true)); + assert_eq!(eval(r#"(GT_EQ "alpha" "beta")"#), Some(false)); + assert_eq!(eval("(GT_EQ 0u64 -1i64)"), Some(true)); + assert_eq!( + eval("(GT_EQ 9007199254740993u64 9007199254740992f64)"), + Some(true) + ); + assert_eq!(eval("(GT_EQ nanf64 nanf64)"), Some(false)); + assert_eq!(eval("(GT_EQ nanf64 0i64)"), Some(false)); + assert_eq!(eval("(GT_EQ 0i64 nanf64)"), Some(false)); + assert_eq!(eval("(GT_EQ none 1i64)"), None); + } + + #[test] + 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(); + // SAFETY: The input and output types match the compiled signature. + assert_eq!( + unsafe { + compiled + .call(&[VariableValue::some("a"), VariableValue::none()]) + .as_bool() + }, + None + ); + } +} diff --git a/jitexpr/src/functions/mod.rs b/jitexpr/src/functions/mod.rs index ed5986c36..26009494e 100644 --- a/jitexpr/src/functions/mod.rs +++ b/jitexpr/src/functions/mod.rs @@ -4,6 +4,7 @@ mod comparison; mod divide; mod eq; mod gt; +mod gt_eq; mod is_not_null; mod is_null; mod lower; @@ -24,6 +25,7 @@ pub(crate) use self::and::AndFnCall; pub(crate) use self::divide::DivideFnCall; pub(crate) use self::eq::EqFnCall; pub(crate) use self::gt::GtFnCall; +pub(crate) use self::gt_eq::GtEqFnCall; pub(crate) use self::is_not_null::IsNotNullFnCall; pub(crate) use self::is_null::IsNullFnCall; pub(crate) use self::lower::LowerFnCall; @@ -53,6 +55,8 @@ pub enum Function { Eq, /// Tests whether the first ordered value is greater than the second. Gt, + /// Tests whether the first ordered value is greater than or equal to the second. + GtEq, /// Tests whether the first ordered value is less than the second. Lt, /// Tests whether an expression produced a present value. @@ -88,6 +92,9 @@ impl Function { } Function::Eq => ::call_with_types(args, target_type_set, context), Function::Gt => ::call_with_types(args, target_type_set, context), + Function::GtEq => { + ::call_with_types(args, target_type_set, context) + } Function::Lt => ::call_with_types(args, target_type_set, context), Function::IsNotNull => { ::call_with_types(args, target_type_set, context) @@ -126,6 +133,9 @@ impl Function { } Function::Eq => ::infer_types(args, target_type, inferred_types), Function::Gt => ::infer_types(args, target_type, inferred_types), + Function::GtEq => { + ::infer_types(args, target_type, inferred_types) + } Function::Lt => ::infer_types(args, target_type, inferred_types), Function::IsNotNull => { ::infer_types(args, target_type, inferred_types) @@ -165,6 +175,7 @@ pub(crate) enum FnCallEnum { Divide(DivideFnCall), Eq(EqFnCall), Gt(GtFnCall), + GtEq(GtEqFnCall), Lt(LtFnCall), IsNull(IsNullFnCall), IsNotNull(IsNotNullFnCall), @@ -184,6 +195,7 @@ impl FnCallEnum { FnCallEnum::Divide(call) => call.args_mut(), FnCallEnum::Eq(call) => call.args_mut(), FnCallEnum::Gt(call) => call.args_mut(), + FnCallEnum::GtEq(call) => call.args_mut(), FnCallEnum::Lt(call) => call.args_mut(), FnCallEnum::IsNull(call) => call.args_mut(), FnCallEnum::IsNotNull(call) => call.args_mut(), @@ -209,6 +221,7 @@ impl FnCallEnum { FnCallEnum::Divide(call) => call.emit_cranelift_ir(return_type, context, builder), FnCallEnum::Eq(call) => call.emit_cranelift_ir(return_type, context, builder), FnCallEnum::Gt(call) => call.emit_cranelift_ir(return_type, context, builder), + FnCallEnum::GtEq(call) => call.emit_cranelift_ir(return_type, context, builder), FnCallEnum::Lt(call) => call.emit_cranelift_ir(return_type, context, builder), FnCallEnum::IsNull(call) => call.emit_cranelift_ir(return_type, context, builder), FnCallEnum::IsNotNull(call) => call.emit_cranelift_ir(return_type, context, builder),