Add GT_EQ function

This commit is contained in:
Paul Masurel
2026-08-18 15:00:30 +02:00
parent e1fb01f920
commit f6fedef60e
4 changed files with 158 additions and 2 deletions
+2 -2
View File
@@ -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.
+2
View File
@@ -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<Function, DeserializeErro
"DIVIDE" => 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),
+141
View File
@@ -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<InferredTypeSet, TypeError> {
comparison::infer_types(Function::GtEq, args, target_type, inferred_types)
}
fn call_with_types(
args: &[UntypedExpr],
target_type_set: InferredTypeSet,
context: &mut CompileFnBuilder<'_, '_>,
) -> Result<TypedExpr, CompileError> {
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<LoweredValue, CompileError> {
debug_assert_eq!(return_type, VarType::Bool);
comparison::lower(
&self.args,
OrderedComparison::GreaterThanOrEqual,
context,
builder,
)
}
}
impl From<GtEqFnCall> 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<bool> {
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
);
}
}
+13
View File
@@ -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 => <EqFnCall as FnCall>::call_with_types(args, target_type_set, context),
Function::Gt => <GtFnCall as FnCall>::call_with_types(args, target_type_set, context),
Function::GtEq => {
<GtEqFnCall as FnCall>::call_with_types(args, target_type_set, context)
}
Function::Lt => <LtFnCall as FnCall>::call_with_types(args, target_type_set, context),
Function::IsNotNull => {
<IsNotNullFnCall as FnCall>::call_with_types(args, target_type_set, context)
@@ -126,6 +133,9 @@ impl Function {
}
Function::Eq => <EqFnCall as FnCall>::infer_types(args, target_type, inferred_types),
Function::Gt => <GtFnCall as FnCall>::infer_types(args, target_type, inferred_types),
Function::GtEq => {
<GtEqFnCall as FnCall>::infer_types(args, target_type, inferred_types)
}
Function::Lt => <LtFnCall as FnCall>::infer_types(args, target_type, inferred_types),
Function::IsNotNull => {
<IsNotNullFnCall as FnCall>::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),