Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 46 additions & 2 deletions lib/src/evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,33 @@ abstract class ExpressionEvaluator<T> extends NullExpressionVisitor {
/// num result = evaluator.evaluate(expression); // 5
///
class RealEvaluator extends ExpressionEvaluator<num> {
/// 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) {
Expand Down Expand Up @@ -437,6 +461,12 @@ class RealEvaluator extends ExpressionEvaluator<num> {
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);
Expand All @@ -452,6 +482,12 @@ class RealEvaluator extends ExpressionEvaluator<num> {
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).
Expand Down Expand Up @@ -571,6 +607,14 @@ class RealEvaluator extends ExpressionEvaluator<num> {
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) {
Expand Down
46 changes: 46 additions & 0 deletions test/evaluator_test_set.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class RealEvaluatorTests extends TestSet {

// Complex expressions
'Expression': evaluateExpression,

// Configurable behavior
'TraditionalMathDefinitions': evaluateTraditionalMathDefinitions,
};

final evaluator = RealEvaluator();
Expand Down Expand Up @@ -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);
}
}
Loading