From 17dde712475ac4439f82abf9f80e20c07e50aaad Mon Sep 17 00:00:00 2001 From: Frederik Leonhardt Date: Tue, 21 Jul 2026 11:42:49 +0000 Subject: [PATCH 1/2] feat(evaluator): add option for traditional math NaN behavior Adds a useTraditionalMathDefinitions flag to RealEvaluator that makes division by zero, 0^0, and tan at its undefined asymptotes evaluate to double.nan instead of following Dart's default floating-point behavior. --- lib/src/evaluator.dart | 48 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/src/evaluator.dart b/lib/src/evaluator.dart index 4640db5..76ad4c0 100644 --- a/lib/src/evaluator.dart +++ b/lib/src/evaluator.dart @@ -364,9 +364,33 @@ abstract class ExpressionEvaluator extends NullExpressionVisitor { /// num result = evaluator.evaluate(expression); // 5 /// class RealEvaluator extends ExpressionEvaluator { + /// Whether to return [double.nan] for operations that are undefined in + /// traditional mathematics, instead of following Dart's default + /// floating-point semantics. + /// + /// When set to `true`, the following operations will evaluate to + /// [double.nan]: + /// + /// - Division by zero, e.g. `1/0` (Dart returns [double.infinity] or + /// [double.negativeInfinity]). + /// - Zero raised to the power of zero, e.g. `0^0` (Dart's `math.pow` + /// returns `1`). + /// - The tangent function evaluated at one of its undefined points, + /// e.g. `tan(pi/2)` (Dart's `math.tan` returns a very large, but + /// finite, value due to the limited precision of `pi`). + /// + /// Defaults to `false`, which preserves Dart's floating-point behavior. + final bool useTraditionalMathDefinitions; + /// Create a new evaluator with the given context. - RealEvaluator([ContextModel? context]) - : super(EvaluationType.REAL, context ?? ContextModel()); + /// + /// Set [useTraditionalMathDefinitions] to `true` to have undefined + /// operations (e.g. division by zero) evaluate to [double.nan] rather + /// than following Dart's default floating-point behavior. + RealEvaluator([ + ContextModel? context, + this.useTraditionalMathDefinitions = false, + ]) : super(EvaluationType.REAL, context ?? ContextModel()); @override bool visitEnter(Expression exp) { @@ -437,6 +461,12 @@ class RealEvaluator extends ExpressionEvaluator { void visitDivide(Divide op) { var (divisor, dividend) = pop2(); + if (useTraditionalMathDefinitions && divisor == 0) { + // Division by zero is undefined in traditional mathematics. + push1(double.nan); + return; + } + /// For real numbers this method performs a double divison and /// returns [double.infinity] if a divide by zero is encountered. push1(dividend / divisor); @@ -452,6 +482,12 @@ class RealEvaluator extends ExpressionEvaluator { void visitPower(Power op) { var (exponent, base) = pop2(); + if (useTraditionalMathDefinitions && base == 0 && exponent == 0) { + // Zero to the power of zero is undefined in traditional mathematics. + push1(double.nan); + return; + } + // TODO: Unnecessary calculations already done in case of expression rewrite below. // Consider the following equation: x^(2/y). @@ -571,6 +607,14 @@ class RealEvaluator extends ExpressionEvaluator { void visitTan(Tan func) { var (val,) = pop1(); + if (useTraditionalMathDefinitions && + ((val - math.pi / 2) / math.pi).abs() % 1 == 0) { + // Tangent is undefined at odd multiples of pi/2 in traditional + // mathematics. + push1(double.nan); + return; + } + // Compensate for inaccuracies in machine-pi. // If val divides cleanly from pi, return 0. if ((val / math.pi).abs() % 1 == 0) { From a3734e5d10a2511ccfc1ce9e85e69a7db5e286df Mon Sep 17 00:00:00 2001 From: Frederik Leonhardt Date: Tue, 21 Jul 2026 11:43:43 +0000 Subject: [PATCH 2/2] test(evaluator): add coverage for useTraditionalMathDefinitions Covers division by zero, 0^0, and tan(pi/2) under both the default Dart behavior and the new traditional math (NaN) behavior. Also updates the changelog. --- CHANGELOG.md | 4 ++++ test/evaluator_test_set.dart | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9bd56..3063d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add support for implicit multiplication to `GrammarParser`. +- Add `useTraditionalMathDefinitions` option to `RealEvaluator` to return + `double.nan` for mathematically undefined operations (e.g. division by + zero, `0^0`, `tan(pi/2)`) instead of following Dart's default + floating-point behavior. ## [3.1.0] - 2025-06-29 diff --git a/test/evaluator_test_set.dart b/test/evaluator_test_set.dart index 5f7f846..3a2bb5e 100644 --- a/test/evaluator_test_set.dart +++ b/test/evaluator_test_set.dart @@ -51,6 +51,9 @@ class RealEvaluatorTests extends TestSet { // Complex expressions 'Expression': evaluateExpression, + + // Configurable behavior + 'TraditionalMathDefinitions': evaluateTraditionalMathDefinitions, }; final evaluator = RealEvaluator(); @@ -604,4 +607,47 @@ class RealEvaluatorTests extends TestSet { parameterized(cases, evaluator: eval); } + + /// Tests the [RealEvaluator.useTraditionalMathDefinitions] flag, which + /// controls whether mathematically undefined operations evaluate to + /// [double.nan] (traditional math) or follow Dart's default + /// floating-point behavior. + void evaluateTraditionalMathDefinitions() { + var piOverTwo = pi / two; + + // Default (Dart) behavior: no NaN for these operations. + var dartCases = { + // 1/0 -> Infinity + one / zero: double.infinity, + // -1/0 -> -Infinity + -one / zero: double.negativeInfinity, + // 0/0 -> NaN (already Dart's own behavior) + zero / zero: isNaN, + // 0^0 -> 1 + zero ^ zero: 1.0, + // tan(pi/2) -> large finite value, not NaN, due to imprecise pi. + Tan(piOverTwo): isNot(isNaN), + }; + parameterized(dartCases); + + // Traditional math behavior: undefined operations evaluate to NaN. + var traditionalEvaluator = RealEvaluator(ContextModel(), true); + var traditionalCases = { + // 1/0 -> NaN + one / zero: isNaN, + // -1/0 -> NaN + -one / zero: isNaN, + // 0/0 -> NaN + zero / zero: isNaN, + // 0^0 -> NaN + zero ^ zero: isNaN, + // tan(pi/2) -> NaN + Tan(piOverTwo): isNaN, + // Regular operations are unaffected. + two / one: 2.0, + two ^ two: 4.0, + Tan(zero): 0.0, + }; + parameterized(traditionalCases, evaluator: traditionalEvaluator); + } }