Add AND function

This commit is contained in:
Paul Masurel
2026-08-18 14:37:39 +02:00
parent 253da7503d
commit 871b81eb27
4 changed files with 200 additions and 2 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ excluded from this pass.
| Tag | Function | Status |
|---:|---|---|
| 1 | `AND` | pending |
| 1 | `AND` | done |
| 2 | `OR` | pending |
| 3 | `NOT` | done |
| 4 | `ADD` | done |
@@ -76,4 +76,4 @@ excluded from this pass.
| 79 | `SUBSTRING_COUNT` | pending |
| 80 | `REGEXP_LIKE` | pending |
Progress: **7 / 48 in-scope** functions implemented; **7** functions are out-of-scope.
Progress: **8 / 48 in-scope** functions implemented; **7** functions are out-of-scope.
+2
View File
@@ -118,6 +118,7 @@ fn format_string(value: &str, formatter: &mut fmt::Formatter<'_>) -> fmt::Result
fn function_name(function: Function) -> &'static str {
match function {
Function::And => "AND",
Function::Add => "ADD",
Function::Eq => "EQ",
Function::IsNull => "IS_NULL",
@@ -130,6 +131,7 @@ fn function_name(function: Function) -> &'static str {
fn parse_function(name: &str, offset: usize) -> Result<Function, DeserializeError> {
match name {
"AND" => Ok(Function::And),
"ADD" => Ok(Function::Add),
"EQ" => Ok(Function::Eq),
"IS_NULL" => Ok(Function::IsNull),
+187
View File
@@ -0,0 +1,187 @@
//! `AND` combines one or more nullable boolean expressions.
//!
//! All arguments must be boolean. With present inputs it is ordinary conjunction. Null handling is
//! deliberately stricter than SQL: if any argument is absent, the result is absent even when
//! another argument is already `false`. Thus `FALSE AND NULL = NULL`. Null result rows carry a
//! false payload, matching dd-go's bitset representation.
//!
//! dd-go implements this by unioning the operands' null sets and clearing result bits at every null
//! row (`interpreter/vector.go`, `propagateNullsBoolean`). The production type checker accepts one
//! or more boolean operands. Evaluation order and short-circuiting are query-engine concerns; this
//! scalar implementation evaluates every child expression.
use std::collections::HashMap;
use cranelift::prelude::{FunctionBuilder, InstBuilder, types};
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 AndFnCall {
pub(crate) args: Box<[TypedExpr]>,
}
impl FnCall for AndFnCall {
fn infer_types<'a>(
args: &'a [UntypedExpr],
target_type: InferredTypeSet,
inferred_types: &mut HashMap<&'a str, InferredTypeSet>,
) -> Result<InferredTypeSet, TypeError> {
if target_type.intersect(InferredTypeSet::BOOLEAN).is_none() {
return Err(TypeError::WrongFunctionReturnType {
function: Function::And,
expected: target_type,
got: InferredTypeSet::BOOLEAN,
});
}
if args.is_empty() {
return Err(TypeError::InvalidNumberOfArguments {
function: Function::And,
expected: 1,
got: 0,
});
}
for arg in args {
crate::ast::infer_types_aux(arg, InferredTypeSet::BOOLEAN, inferred_types)?;
}
Ok(InferredTypeSet::BOOLEAN)
}
fn call_with_types(
args: &[UntypedExpr],
target_type_set: InferredTypeSet,
context: &mut CompileFnBuilder<'_, '_>,
) -> Result<TypedExpr, CompileError> {
assert!(!args.is_empty(), "expected at least 1 arg for AND");
debug_assert!(target_type_set.contains(VarType::Bool));
let args = args
.iter()
.map(|arg| context.apply_types(arg, InferredTypeSet::BOOLEAN))
.collect::<Result<Vec<_>, _>>()?;
Ok(TypedExpr {
return_type: VarType::Bool,
ast: TypedExprAst::from_call(AndFnCall {
args: args.into_boxed_slice(),
}),
})
}
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);
let mut value = builder.ins().iconst(types::I8, 1);
let mut is_present = value;
for arg in &self.args {
let lowered = context.compile_expr(arg, builder)?;
is_present = builder.ins().band(is_present, lowered.is_present);
if arg.return_type == VarType::None {
value = builder.ins().iconst(types::I8, 0);
} else {
value = builder.ins().band(value, lowered.value);
}
}
value = builder.ins().band(value, is_present);
Ok(LoweredValue {
value,
is_present,
string_len: builder.ins().iconst(types::I64, 0),
})
}
}
impl From<AndFnCall> for FnCallEnum {
fn from(call: AndFnCall) -> Self {
FnCallEnum::And(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 runtime inputs and return nullable booleans.
unsafe { compiled.call(&[]).as_bool() }
}
#[test]
fn test_infer_types_requires_boolean_arguments() {
let expression = deserialize("(AND left right)").unwrap();
let inferred_types = infer_types(&expression).unwrap();
assert_eq!(inferred_types.get("left"), Some(&InferredTypeSet::BOOLEAN));
assert_eq!(inferred_types.get("right"), Some(&InferredTypeSet::BOOLEAN));
let expression = deserialize("(AND)").unwrap();
assert!(matches!(
infer_types(&expression),
Err(TypeError::InvalidNumberOfArguments {
function: Function::And,
expected: 1,
got: 0,
})
));
}
#[test]
fn test_present_truth_table_and_variadic_inputs() {
assert_eq!(eval("(AND true)"), Some(true));
assert_eq!(eval("(AND true true true)"), Some(true));
assert_eq!(eval("(AND true false true)"), Some(false));
assert_eq!(eval("(AND false false)"), Some(false));
}
#[test]
fn test_any_absent_argument_makes_result_absent() {
assert_eq!(eval("(AND true none)"), None);
assert_eq!(eval("(AND false none)"), None);
assert_eq!(eval("(AND none none)"), None);
}
#[test]
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();
// SAFETY: The input and output types match the compiled signature.
assert_eq!(
unsafe {
compiled
.call(&[VariableValue::some(false), VariableValue::none()])
.as_bool()
},
None
);
// SAFETY: The input and output types match the compiled signature.
assert_eq!(
unsafe {
compiled
.call(&[VariableValue::some(true), VariableValue::some(false)])
.as_bool()
},
Some(false)
);
}
}
+9
View File
@@ -1,4 +1,5 @@
mod add;
mod and;
mod eq;
mod is_not_null;
mod is_null;
@@ -12,6 +13,7 @@ use std::collections::HashMap;
use cranelift::frontend::FunctionBuilder;
pub(crate) use self::add::AddFnCall;
pub(crate) use self::and::AndFnCall;
pub(crate) use self::eq::EqFnCall;
pub(crate) use self::is_not_null::IsNotNullFnCall;
pub(crate) use self::is_null::IsNullFnCall;
@@ -28,6 +30,8 @@ use crate::types::VarType;
/// A function supported by the first expression-language milestone.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Function {
/// Conjoins one or more booleans with strict null propagation.
And,
/// Adds zero or more numerical expressions.
Add,
/// Compares two expressions for value equality.
@@ -52,6 +56,7 @@ impl Function {
context: &mut CompileFnBuilder<'_, '_>,
) -> Result<TypedExpr, CompileError> {
match self {
Function::And => <AndFnCall as FnCall>::call_with_types(args, target_type_set, context),
Function::Add => <AddFnCall as FnCall>::call_with_types(args, target_type_set, context),
Function::Eq => <EqFnCall as FnCall>::call_with_types(args, target_type_set, context),
Function::IsNotNull => {
@@ -77,6 +82,7 @@ impl Function {
inferred_types: &mut HashMap<&'a str, InferredTypeSet>,
) -> Result<InferredTypeSet, TypeError> {
match self {
Function::And => <AndFnCall as FnCall>::infer_types(args, target_type, inferred_types),
Function::Add => <AddFnCall as FnCall>::infer_types(args, target_type, inferred_types),
Function::Eq => <EqFnCall as FnCall>::infer_types(args, target_type, inferred_types),
Function::IsNotNull => {
@@ -105,6 +111,7 @@ impl Function {
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum FnCallEnum {
And(AndFnCall),
Add(AddFnCall),
Eq(EqFnCall),
IsNull(IsNullFnCall),
@@ -117,6 +124,7 @@ pub(crate) enum FnCallEnum {
impl FnCallEnum {
pub(crate) fn args_mut(&mut self) -> &mut [TypedExpr] {
match self {
FnCallEnum::And(call) => call.args_mut(),
FnCallEnum::Add(call) => call.args_mut(),
FnCallEnum::Eq(call) => call.args_mut(),
FnCallEnum::IsNull(call) => call.args_mut(),
@@ -135,6 +143,7 @@ impl FnCallEnum {
builder: &mut FunctionBuilder<'_>,
) -> Result<LoweredValue, CompileError> {
match self {
FnCallEnum::And(call) => call.emit_cranelift_ir(return_type, context, builder),
FnCallEnum::Add(call) => call.emit_cranelift_ir(return_type, context, builder),
FnCallEnum::Eq(call) => call.emit_cranelift_ir(return_type, context, builder),
FnCallEnum::IsNull(call) => call.emit_cranelift_ir(return_type, context, builder),