use crate::Engine;
use crate::OpCode;
use crate::node::CompiledNode;
use crate::operators::meta::{Algebra, ArithOp};
use datavalue::{NumberValue, OwnedDataValue};
pub(crate) fn fold(node: CompiledNode) -> (CompiledNode, bool) {
match &node {
CompiledNode::BuiltinOperator {
id, opcode, args, ..
} => {
if associative_op(*opcode).is_some() {
match try_partial_fold(*id, *opcode, args) {
Some(new) => (new, true),
None => (node, false),
}
} else if opcode.algebra() == Some(Algebra::Concat) && args.len() >= 2 {
match try_fold_concat(*id, *opcode, args) {
Some(new) => (new, true),
None => (node, false),
}
} else {
(node, false)
}
}
_ => (node, false),
}
}
fn associative_op(opcode: OpCode) -> Option<ArithOp> {
match opcode.meta().algebra {
Some(Algebra::Arith(op)) if op.is_associative() => Some(op),
_ => None,
}
}
fn try_partial_fold(
outer_id: crate::node::NodeId,
opcode: OpCode,
args: &[CompiledNode],
) -> Option<CompiledNode> {
let op = associative_op(opcode)?;
let lead = args
.iter()
.take_while(|a| {
matches!(
a,
CompiledNode::Value {
value: OwnedDataValue::Number(NumberValue::Integer(_)),
..
}
)
})
.count();
if lead < 2 || args.len() - lead + 1 < 3 {
return None;
}
let combine = match op {
ArithOp::Mul => i64::checked_mul,
_ => i64::checked_add,
};
let folded = args[..lead]
.iter()
.try_fold(op.right_identity(), |acc, a| match a {
CompiledNode::Value {
value: OwnedDataValue::Number(NumberValue::Integer(i)),
..
} => combine(acc, *i),
_ => None,
})?;
let mut new_args = Vec::with_capacity(1 + args.len() - lead);
new_args.push(CompiledNode::synthetic_value(OwnedDataValue::Number(
NumberValue::Integer(folded),
)));
new_args.extend(args[lead..].iter().cloned());
Some(CompiledNode::BuiltinOperator {
id: outer_id,
opcode,
args: new_args.into_boxed_slice(),
predicate_hint: None,
iter_arg_kind: crate::operators::array::IterArgKind::General,
})
}
fn try_fold_concat(
outer_id: crate::node::NodeId,
opcode: OpCode,
args: &[CompiledNode],
) -> Option<CompiledNode> {
let has_adjacent_strings = args.windows(2).any(|w| {
matches!(
(&w[0], &w[1]),
(
CompiledNode::Value {
value: OwnedDataValue::String(_),
..
},
CompiledNode::Value {
value: OwnedDataValue::String(_),
..
},
)
)
});
if !has_adjacent_strings {
return None;
}
let mut new_args: Vec<CompiledNode> = Vec::new();
let mut current_static_str: Option<String> = None;
let mut folded_any = false;
for arg in args {
if let CompiledNode::Value {
value: OwnedDataValue::String(s),
..
} = arg
{
match &mut current_static_str {
Some(accumulated) => {
accumulated.push_str(s);
folded_any = true;
}
None => {
current_static_str = Some(s.clone());
}
}
} else {
if let Some(s) = current_static_str.take() {
new_args.push(CompiledNode::synthetic_value(OwnedDataValue::String(s)));
}
new_args.push(arg.clone());
}
}
if let Some(s) = current_static_str.take() {
new_args.push(CompiledNode::synthetic_value(OwnedDataValue::String(s)));
}
if !folded_any {
return None;
}
if new_args.len() == 1 {
return Some(new_args.into_iter().next().unwrap());
}
Some(CompiledNode::BuiltinOperator {
id: outer_id,
opcode,
args: new_args.into_boxed_slice(),
predicate_hint: None,
iter_arg_kind: crate::operators::array::IterArgKind::General,
})
}
pub(crate) fn fold_static_node(node: &CompiledNode, engine: &Engine) -> Option<OwnedDataValue> {
let arena = bumpalo::Bump::new();
let null_root: &crate::arena::DataValue<'_> = crate::arena::singletons::singleton_null();
let mut ctx = crate::arena::ContextStack::new(null_root, false);
let av = engine.dispatch_node(node, &mut ctx, &arena).ok()?;
Some(av.to_owned())
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::{builtin, val, var_node};
use super::*;
use datavalue::OwnedDataValue;
fn ov(s: &str) -> OwnedDataValue {
OwnedDataValue::from_json(s).unwrap()
}
fn partial(opcode: OpCode, args: Vec<CompiledNode>) -> Option<(OwnedDataValue, usize)> {
let (result, changed) = fold(builtin(opcode, args));
if !changed {
return None;
}
let CompiledNode::BuiltinOperator { args, .. } = &result else {
panic!("expected BuiltinOperator");
};
let CompiledNode::Value { value, .. } = &args[0] else {
panic!("expected folded value");
};
Some((value.clone(), args.len()))
}
#[test]
fn test_partial_fold_add() {
let got = partial(
OpCode::Add,
vec![val(ov("1")), val(ov("2")), var_node("x"), val(ov("3"))],
);
assert_eq!(got, Some((ov("3"), 3)));
let got = partial(
OpCode::Multiply,
vec![
val(ov("2")),
val(ov("3")),
val(ov("4")),
var_node("x"),
var_node("y"),
],
);
assert_eq!(got, Some((ov("24"), 3)));
}
#[test]
fn partial_folds_that_could_change_the_result_are_left_alone() {
let max = || val(ov(&i64::MAX.to_string()));
for (opcode, args) in [
(
OpCode::Add,
vec![var_node("x"), val(ov("1")), val(ov("2")), var_node("y")],
),
(
OpCode::Add,
vec![max(), val(ov("1")), var_node("x"), var_node("y")],
),
(
OpCode::Multiply,
vec![val(ov("0.1")), val(ov("10")), var_node("x"), var_node("y")],
),
(
OpCode::Add,
vec![val(ov("1")), val(ov("\"5\"")), var_node("x"), var_node("y")],
),
(OpCode::Add, vec![val(ov("1")), val(ov("2")), var_node("x")]),
] {
assert_eq!(partial(opcode, args), None);
}
}
#[test]
fn test_fold_cat_adjacent() {
let node = builtin(
OpCode::Concat,
vec![val(ov("\"hello \"")), val(ov("\"world\"")), var_node("x")],
);
let (result, _changed) = fold(node);
if let CompiledNode::BuiltinOperator { args, .. } = &result {
assert_eq!(args.len(), 2);
if let CompiledNode::Value { value, .. } = &args[0] {
assert_eq!(value.as_str(), Some("hello world"));
}
}
}
#[test]
fn numeric_strings_are_not_precoerced() {
let node = builtin(OpCode::Add, vec![val(ov("\"5\"")), var_node("x")]);
let (result, changed) = fold(node);
assert!(!changed);
if let CompiledNode::BuiltinOperator { args, .. } = &result {
assert!(matches!(
&args[0],
CompiledNode::Value {
value: OwnedDataValue::String(_),
..
}
));
} else {
panic!("expected BuiltinOperator");
}
}
}