From 5df13c867303971582f055e2c76d70c921aac1d8 Mon Sep 17 00:00:00 2001 From: GaspardCulis Date: Tue, 3 Feb 2026 13:56:31 +0100 Subject: [PATCH 1/7] feat(operator): POC for `Add` and `Sub` support for tuples --- src/error/display.rs | 5 +++ src/error/mod.rs | 16 ++++++++ src/operator/mod.rs | 92 +++++++++++++++++++++++++++++--------------- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/error/display.rs b/src/error/display.rs index effcf95..cc9fe5b 100644 --- a/src/error/display.rs +++ b/src/error/display.rs @@ -50,6 +50,11 @@ impl fmt::Display for EvalexprError write!( + f, + "Expected a the same length for tuples {:?} and {:?}.", + expected, actual + ), ExpectedRangedLengthTuple { expected_length, actual, diff --git a/src/error/mod.rs b/src/error/mod.rs index 3af244e..ff1ac91 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -93,6 +93,14 @@ pub enum EvalexprError actual: Value, }, + /// A tuple value of a certain length was expected. + ExpectedSameLengthTuples { + /// The tuple with expected length. + expected: Value, + /// The actual tuple with unexpected length. + actual: Value, + }, + /// A tuple value of a certain length range was expected. ExpectedRangedLengthTuple { /// The expected length range. @@ -328,6 +336,14 @@ impl EvalexprError { } } + /// Constructs `EvalexprError::ExpectedSameLengthTuples {expected, actual}`. + pub fn expected_same_len_tuples( + expected: Value, + actual: Value, + ) -> Self { + EvalexprError::ExpectedSameLengthTuples { expected, actual } + } + /// Constructs `EvalexprError::ExpectedFixedLenTuple{expected_len, actual}`. pub fn expected_ranged_len_tuple( expected_len: RangeInclusive, diff --git a/src/operator/mod.rs b/src/operator/mod.rs index a815f79..a1662a1 100644 --- a/src/operator/mod.rs +++ b/src/operator/mod.rs @@ -203,42 +203,70 @@ impl Operator { Ok(Value::Empty) } }, - Add => { + Add | Sub => { expect_operator_argument_amount(arguments.len(), 2)?; - expect_number_or_string(&arguments[0])?; - expect_number_or_string(&arguments[1])?; - if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) { - let mut result = String::with_capacity(a.len() + b.len()); - result.push_str(&a); - result.push_str(&b); - Ok(Value::String(result)) - } else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { - a.checked_add(&b).map(Value::Int) - } else if let (Ok(a), Ok(b)) = (arguments[0].as_number(), arguments[1].as_number()) - { - Ok(Value::Float(a + b)) - } else { - Err(EvalexprError::wrong_type_combination( - self.clone(), - vec![ - arguments.get(0).unwrap().into(), - arguments.get(1).unwrap().into(), - ], - )) - } - }, - Sub => { - expect_operator_argument_amount(arguments.len(), 2)?; - arguments[0].as_number()?; - arguments[1].as_number()?; + if let (Ok(a), Ok(b)) = (arguments[0].as_tuple(), arguments[1].as_tuple()) { + if a.len() == b.len() { + let mut result = Vec::with_capacity(a.len()); + for (a, b) in a.into_iter().zip(b) { + result.push(self.eval(&[a, b], context)?); + } - if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { - a.checked_sub(&b).map(Value::Int) + Ok(Value::Tuple(result)) + } else { + Err(EvalexprError::expected_same_len_tuples( + arguments[0].clone(), + arguments[1].clone(), + )) + } } else { - Ok(Value::Float( - arguments[0].as_number()? - arguments[1].as_number()?, - )) + match self { + Add => { + expect_operator_argument_amount(arguments.len(), 2)?; + expect_number_or_string(&arguments[0])?; + expect_number_or_string(&arguments[1])?; + + if let (Ok(a), Ok(b)) = + (arguments[0].as_string(), arguments[1].as_string()) + { + let mut result = String::with_capacity(a.len() + b.len()); + result.push_str(&a); + result.push_str(&b); + Ok(Value::String(result)) + } else if let (Ok(a), Ok(b)) = + (arguments[0].as_int(), arguments[1].as_int()) + { + a.checked_add(&b).map(Value::Int) + } else if let (Ok(a), Ok(b)) = + (arguments[0].as_number(), arguments[1].as_number()) + { + Ok(Value::Float(a + b)) + } else { + Err(EvalexprError::wrong_type_combination( + self.clone(), + vec![ + arguments.get(0).unwrap().into(), + arguments.get(1).unwrap().into(), + ], + )) + } + }, + Sub => { + expect_operator_argument_amount(arguments.len(), 2)?; + arguments[0].as_number()?; + arguments[1].as_number()?; + + if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { + a.checked_sub(&b).map(Value::Int) + } else { + Ok(Value::Float( + arguments[0].as_number()? - arguments[1].as_number()?, + )) + } + }, + _ => unreachable!(), + } } }, Neg => { From 36fabced5f1324d125c952bf9c1752a14ba26ff9 Mon Sep 17 00:00:00 2001 From: GaspardCulis Date: Tue, 3 Feb 2026 14:13:42 +0100 Subject: [PATCH 2/7] test(integration): Add tuple operation tests --- tests/integration.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/integration.rs b/tests/integration.rs index f40bc38..d1d0815 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1547,6 +1547,38 @@ fn test_tuple_definitions() { ); } +#[test] +fn test_tuple_operations() { + assert_eq!( + eval("(1, 2, 3) + (4, 5, 6)"), + Ok(Value::Tuple(vec![ + Value::from_int(5), + Value::from_int(7), + Value::from_int(9), + ])) + ); + assert_eq!( + eval("(6, 5, 4) - (1, 2, 3)"), + Ok(Value::Tuple(vec![ + Value::from_int(5), + Value::from_int(3), + Value::from_int(1), + ])) + ); + + assert_eq!( + eval("(1, 2, 3) + (4, 5)"), + Err(EvalexprError::ExpectedSameLengthTuples { + expected: Value::Tuple(vec![ + Value::from_int(1), + Value::from_int(2), + Value::from_int(3), + ]), + actual: Value::Tuple(vec![Value::from_int(4), Value::from_int(5),]) + }) + ); +} + #[test] fn test_implicit_context() { assert_eq!( From af0f38031448961ff61b0d02f30846a43867cb0d Mon Sep 17 00:00:00 2001 From: GaspardCulis Date: Fri, 20 Feb 2026 22:06:25 +0100 Subject: [PATCH 3/7] Revert "feat(operator): POC for `Add` and `Sub` support for tuples" This reverts commit 5df13c867303971582f055e2c76d70c921aac1d8. --- src/error/display.rs | 5 --- src/error/mod.rs | 16 -------- src/operator/mod.rs | 92 +++++++++++++++----------------------------- 3 files changed, 32 insertions(+), 81 deletions(-) diff --git a/src/error/display.rs b/src/error/display.rs index cc9fe5b..effcf95 100644 --- a/src/error/display.rs +++ b/src/error/display.rs @@ -50,11 +50,6 @@ impl fmt::Display for EvalexprError write!( - f, - "Expected a the same length for tuples {:?} and {:?}.", - expected, actual - ), ExpectedRangedLengthTuple { expected_length, actual, diff --git a/src/error/mod.rs b/src/error/mod.rs index ff1ac91..3af244e 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -93,14 +93,6 @@ pub enum EvalexprError actual: Value, }, - /// A tuple value of a certain length was expected. - ExpectedSameLengthTuples { - /// The tuple with expected length. - expected: Value, - /// The actual tuple with unexpected length. - actual: Value, - }, - /// A tuple value of a certain length range was expected. ExpectedRangedLengthTuple { /// The expected length range. @@ -336,14 +328,6 @@ impl EvalexprError { } } - /// Constructs `EvalexprError::ExpectedSameLengthTuples {expected, actual}`. - pub fn expected_same_len_tuples( - expected: Value, - actual: Value, - ) -> Self { - EvalexprError::ExpectedSameLengthTuples { expected, actual } - } - /// Constructs `EvalexprError::ExpectedFixedLenTuple{expected_len, actual}`. pub fn expected_ranged_len_tuple( expected_len: RangeInclusive, diff --git a/src/operator/mod.rs b/src/operator/mod.rs index a1662a1..a815f79 100644 --- a/src/operator/mod.rs +++ b/src/operator/mod.rs @@ -203,70 +203,42 @@ impl Operator { Ok(Value::Empty) } }, - Add | Sub => { + Add => { expect_operator_argument_amount(arguments.len(), 2)?; + expect_number_or_string(&arguments[0])?; + expect_number_or_string(&arguments[1])?; - if let (Ok(a), Ok(b)) = (arguments[0].as_tuple(), arguments[1].as_tuple()) { - if a.len() == b.len() { - let mut result = Vec::with_capacity(a.len()); - for (a, b) in a.into_iter().zip(b) { - result.push(self.eval(&[a, b], context)?); - } + if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) { + let mut result = String::with_capacity(a.len() + b.len()); + result.push_str(&a); + result.push_str(&b); + Ok(Value::String(result)) + } else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { + a.checked_add(&b).map(Value::Int) + } else if let (Ok(a), Ok(b)) = (arguments[0].as_number(), arguments[1].as_number()) + { + Ok(Value::Float(a + b)) + } else { + Err(EvalexprError::wrong_type_combination( + self.clone(), + vec![ + arguments.get(0).unwrap().into(), + arguments.get(1).unwrap().into(), + ], + )) + } + }, + Sub => { + expect_operator_argument_amount(arguments.len(), 2)?; + arguments[0].as_number()?; + arguments[1].as_number()?; - Ok(Value::Tuple(result)) - } else { - Err(EvalexprError::expected_same_len_tuples( - arguments[0].clone(), - arguments[1].clone(), - )) - } + if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { + a.checked_sub(&b).map(Value::Int) } else { - match self { - Add => { - expect_operator_argument_amount(arguments.len(), 2)?; - expect_number_or_string(&arguments[0])?; - expect_number_or_string(&arguments[1])?; - - if let (Ok(a), Ok(b)) = - (arguments[0].as_string(), arguments[1].as_string()) - { - let mut result = String::with_capacity(a.len() + b.len()); - result.push_str(&a); - result.push_str(&b); - Ok(Value::String(result)) - } else if let (Ok(a), Ok(b)) = - (arguments[0].as_int(), arguments[1].as_int()) - { - a.checked_add(&b).map(Value::Int) - } else if let (Ok(a), Ok(b)) = - (arguments[0].as_number(), arguments[1].as_number()) - { - Ok(Value::Float(a + b)) - } else { - Err(EvalexprError::wrong_type_combination( - self.clone(), - vec![ - arguments.get(0).unwrap().into(), - arguments.get(1).unwrap().into(), - ], - )) - } - }, - Sub => { - expect_operator_argument_amount(arguments.len(), 2)?; - arguments[0].as_number()?; - arguments[1].as_number()?; - - if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { - a.checked_sub(&b).map(Value::Int) - } else { - Ok(Value::Float( - arguments[0].as_number()? - arguments[1].as_number()?, - )) - } - }, - _ => unreachable!(), - } + Ok(Value::Float( + arguments[0].as_number()? - arguments[1].as_number()?, + )) } }, Neg => { From f829b5a2642148b380089aa934d6f04518a92958 Mon Sep 17 00:00:00 2001 From: GaspardCulis Date: Fri, 20 Feb 2026 22:32:28 +0100 Subject: [PATCH 4/7] feat(operator): Add more flexible tuple operation support --- src/error/display.rs | 5 +++++ src/error/mod.rs | 16 ++++++++++++++++ src/operator/mod.rs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/error/display.rs b/src/error/display.rs index effcf95..cc9fe5b 100644 --- a/src/error/display.rs +++ b/src/error/display.rs @@ -50,6 +50,11 @@ impl fmt::Display for EvalexprError write!( + f, + "Expected a the same length for tuples {:?} and {:?}.", + expected, actual + ), ExpectedRangedLengthTuple { expected_length, actual, diff --git a/src/error/mod.rs b/src/error/mod.rs index 3af244e..ff1ac91 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -93,6 +93,14 @@ pub enum EvalexprError actual: Value, }, + /// A tuple value of a certain length was expected. + ExpectedSameLengthTuples { + /// The tuple with expected length. + expected: Value, + /// The actual tuple with unexpected length. + actual: Value, + }, + /// A tuple value of a certain length range was expected. ExpectedRangedLengthTuple { /// The expected length range. @@ -328,6 +336,14 @@ impl EvalexprError { } } + /// Constructs `EvalexprError::ExpectedSameLengthTuples {expected, actual}`. + pub fn expected_same_len_tuples( + expected: Value, + actual: Value, + ) -> Self { + EvalexprError::ExpectedSameLengthTuples { expected, actual } + } + /// Constructs `EvalexprError::ExpectedFixedLenTuple{expected_len, actual}`. pub fn expected_ranged_len_tuple( expected_len: RangeInclusive, diff --git a/src/operator/mod.rs b/src/operator/mod.rs index a815f79..eb3f4b6 100644 --- a/src/operator/mod.rs +++ b/src/operator/mod.rs @@ -195,6 +195,39 @@ impl Operator { context: &C, ) -> EvalexprResultValue { use crate::operator::Operator::*; + + // Check if we can do piecewise operations on tuples + match self { + FunctionIdentifier { identifier: _ } => { + // Don't wrap function tuple operations + }, + _ => match arguments { + [Value::Tuple(a_elements), Value::Tuple(b_elements)] => { + if a_elements.len() != b_elements.len() { + return Err(EvalexprError::expected_same_len_tuples( + arguments[0].clone(), + arguments[1].clone(), + )); + } + + let mut computed = Vec::with_capacity(a_elements.len()); + for (a, b) in a_elements.into_iter().zip(b_elements) { + computed.push(self.eval(&[a.clone(), b.clone()], context)?); + } + + return Ok(Value::Tuple(computed)); + }, + [Value::Tuple(elements)] => { + let mut computed = Vec::with_capacity(elements.len()); + for element in elements { + computed.push(self.eval(&[element.clone()], context)?); + } + return Ok(Value::Tuple(computed)); + }, + _ => {}, + }, + } + match self { RootNode => { if let Some(first) = arguments.first() { From a2b6432dece9eb63c3f098f528e896aebf4ff83d Mon Sep 17 00:00:00 2001 From: GaspardCulis Date: Fri, 20 Feb 2026 22:52:20 +0100 Subject: [PATCH 5/7] tests(integration): Add test case for unary tuple piecewise operation --- tests/integration.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/integration.rs b/tests/integration.rs index d1d0815..428f32b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1549,6 +1549,14 @@ fn test_tuple_definitions() { #[test] fn test_tuple_operations() { + assert_eq!( + eval("!(false, true, true)"), + Ok(Value::Tuple(vec![ + Value::Boolean(true), + Value::Boolean(false), + Value::Boolean(false), + ])) + ); assert_eq!( eval("(1, 2, 3) + (4, 5, 6)"), Ok(Value::Tuple(vec![ From e1b300afecc847ba9d444c44d461b3171dfaf561 Mon Sep 17 00:00:00 2001 From: GaspardCulis Date: Wed, 4 Mar 2026 15:28:59 +0100 Subject: [PATCH 6/7] fix(operator): Hardcode supported tuple piecewise operations --- src/operator/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/operator/mod.rs b/src/operator/mod.rs index eb3f4b6..94ad6b7 100644 --- a/src/operator/mod.rs +++ b/src/operator/mod.rs @@ -198,10 +198,7 @@ impl Operator { // Check if we can do piecewise operations on tuples match self { - FunctionIdentifier { identifier: _ } => { - // Don't wrap function tuple operations - }, - _ => match arguments { + Add | Sub | Neg | Mul | Div | And | Or | Not => match arguments { [Value::Tuple(a_elements), Value::Tuple(b_elements)] => { if a_elements.len() != b_elements.len() { return Err(EvalexprError::expected_same_len_tuples( @@ -226,6 +223,7 @@ impl Operator { }, _ => {}, }, + _ => {}, } match self { From 73716808ad562b445744e91a4407a8e48105eb35 Mon Sep 17 00:00:00 2001 From: GaspardCulis Date: Wed, 4 Mar 2026 15:31:59 +0100 Subject: [PATCH 7/7] chore(operator): Fix clippy warns --- src/operator/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/operator/mod.rs b/src/operator/mod.rs index 94ad6b7..e56ea4d 100644 --- a/src/operator/mod.rs +++ b/src/operator/mod.rs @@ -208,7 +208,7 @@ impl Operator { } let mut computed = Vec::with_capacity(a_elements.len()); - for (a, b) in a_elements.into_iter().zip(b_elements) { + for (a, b) in a_elements.iter().zip(b_elements) { computed.push(self.eval(&[a.clone(), b.clone()], context)?); } @@ -217,7 +217,7 @@ impl Operator { [Value::Tuple(elements)] => { let mut computed = Vec::with_capacity(elements.len()); for element in elements { - computed.push(self.eval(&[element.clone()], context)?); + computed.push(self.eval(std::slice::from_ref(element), context)?); } return Ok(Value::Tuple(computed)); },