diff --git a/CHANGELOG.md b/CHANGELOG.md index 951db1d9..1fcec1ae 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,49 @@ This also fixes comparison evaluation: `Equal(symbol, assumed_value)` now correctly evaluates to `True` instead of staying symbolic. +- **Inequality Evaluation Using Assumptions**: When an inequality assumption + is made (e.g., `ce.assume(['Greater', 'x', 4])`), inequality comparisons + can now use transitive reasoning to determine results. + + ```javascript + ce.assume(ce.box(['Greater', 'x', 4])); + ce.box(['Greater', 'x', 0]).evaluate(); // → True (x > 4 > 0) + ce.box(['Less', 'x', 0]).evaluate(); // → False + ce.box('x').isGreater(0); // → true + ce.box('x').isPositive; // → true + ``` + + This works by extracting lower/upper bounds from inequality assumptions + and using them during comparison operations. + ### Bug Fixes +- **forget() Now Clears Assumed Values**: Fixed an issue where `ce.forget()` did not + clear values that were set by equality assumptions. After calling + `ce.assume(['Equal', 'x', 5])` followed by `ce.forget('x')`, the symbol would + incorrectly still evaluate to `5`. Now `forget()` properly clears values from + all evaluation context frames. + + ```javascript + ce.assume(ce.box(['Equal', 'x', 5])); + ce.box('x').evaluate(); // → 5 + ce.forget('x'); + ce.box('x').evaluate(); // → 'x' (was: 5) + ``` + +- **Scoped Assumptions Now Clean Up on popScope()**: Fixed an issue where + assumptions made inside a nested scope would persist after `popScope()` was + called. Values set by assumptions are now properly scoped to where the + assumption was made, and are automatically removed when the scope exits. + + ```javascript + ce.pushScope(); + ce.assume(ce.box(['Equal', 'y', 10])); + ce.box('y').evaluate(); // → 10 + ce.popScope(); + ce.box('y').evaluate(); // → 'y' (was: 10) + ``` + - **Extraneous Root Filtering for Sqrt Equations**: Fixed an issue where solving square root equations could return extraneous roots. When solving equations like `√x = x - 2` or `√x - x + 2 = 0` using the quadratic substitution method diff --git a/requirements/DONE.md b/requirements/DONE.md index 160ac9b8..276f95ab 100644 --- a/requirements/DONE.md +++ b/requirements/DONE.md @@ -1049,3 +1049,138 @@ ce.box('one').type.matches('integer') // → true **Tests enabled:** - `test/compute-engine/assumptions.test.ts` - Enabled "VALUE RESOLUTION FROM EQUALITY ASSUMPTIONS" describe block (6 tests) + +--- + +### 19. Inequality Evaluation Using Assumptions ✅ + +**IMPLEMENTED:** When inequality assumptions are made via `ce.assume(['Greater', symbol, value])`, +comparisons can now use transitive reasoning to determine results. + +**Problem:** When `x > 4` was assumed, evaluating `['Greater', 'x', 0]` would return the expression +unchanged instead of `True` (since x > 4 implies x > 0). + +**Solution:** Added a new function `getInequalityBoundsFromAssumptions` that extracts lower/upper +bounds for a symbol from inequality assumptions. The bounds are then used in the `cmp` function +to determine comparison results. + +**Key insight:** Assumptions are normalized to forms like `Less(Add(Negate(x), k), 0)` (meaning +`k - x < 0`, i.e., `x > k`). The implementation parses these normalized forms to extract bounds. + +**Examples that now work:** +```typescript +ce.assume(ce.box(['Greater', 'x', 4])); +ce.box(['Greater', 'x', 0]).evaluate(); // → True (x > 4 > 0) +ce.box(['Less', 'x', 0]).evaluate(); // → False +ce.box('x').isGreater(0); // → true +ce.box('x').isGreater(4); // → true (strict inequality) +ce.box('x').isGreater(5); // → undefined (can't determine) +ce.box('x').isPositive; // → true + +ce.assume(ce.box(['Greater', 't', 0])); +ce.box(['Greater', 't', 0]).evaluate(); // → True +ce.box('t').isGreater(-1); // → true +``` + +**Files modified:** +- `src/compute-engine/assume.ts` - Added `getInequalityBoundsFromAssumptions()` function +- `src/compute-engine/boxed-expression/compare.ts` - Modified `cmp()` to use bounds from assumptions + +**Tests enabled:** +- `test/compute-engine/assumptions.test.ts` - Enabled "INEQUALITY EVALUATION USING ASSUMPTIONS" + describe block (6 tests) + +--- + +### 24. BUG FIX: forget() Now Clears Assumed Values ✅ + +**FIXED:** The `forget()` function now properly clears values from the evaluation +context when a symbol is forgotten. + +**Problem:** When `ce.assume(['Equal', 'x', 5])` was called followed by `ce.forget('x')`, +the value `5` would persist in the evaluation context, causing `ce.box('x').evaluate()` +to still return `5` instead of the symbol `'x'`. + +**Root cause:** When task #18 was implemented, values were stored in the evaluation +context via `ce._setSymbolValue()`. However, `forget()` only removed assumptions from +`ce.context.assumptions` - it didn't clear the value from the evaluation context. + +**Solution:** Added code to `forget()` to iterate through all evaluation context frames +and delete the symbol's value: + +```typescript +// In forget() function, after removing assumptions: +for (const ctx of this._evalContextStack) { + if (symbol in ctx.values) { + delete ctx.values[symbol]; + } +} +``` + +**Examples that now work:** +```typescript +const ce = new ComputeEngine(); +ce.assume(ce.box(['Equal', 'x', 5])); +ce.box('x').evaluate().json; // → 5 + +ce.forget('x'); +ce.box('x').evaluate().json; // → 'x' (was: 5) +``` + +**Files modified:** +- `src/compute-engine/index.ts` - Added value cleanup in `forget()` function + +**Tests added:** +- `test/compute-engine/bug-fixes.test.ts` - Test for forget() value clearing + +--- + +### 25. BUG FIX: Scoped Assumptions Now Clean Up on popScope() ✅ + +**FIXED:** Assumptions made inside a scope via `pushScope()`/`popScope()` now properly +clean up when the scope is exited. + +**Problem:** When assumptions were made inside a nested scope, the values set via +`ce._setSymbolValue()` would persist after `popScope()` was called, breaking scope +isolation. + +**Root cause:** The `_setSymbolValue()` function stores values in the context where +the symbol was declared (which might be a parent scope), not necessarily the current +scope. When `popScope()` was called, only the current scope's context was removed, +but the value remained in the parent context. + +**Solution:** Created a new internal method `_setCurrentContextValue()` that stores +values directly in the current context's values map. Modified `assumeEquality()` to +use this method instead of `_setSymbolValue()`, ensuring that assumption values are +scoped to where the assumption was made. + +```typescript +// New method in ComputeEngine: +_setCurrentContextValue(id, value): void { + this._evalContextStack[this._evalContextStack.length - 1].values[id] = value; +} + +// In assumeEquality(), changed from: +ce._setSymbolValue(lhs, val); +// to: +ce._setCurrentContextValue(lhs, val); +``` + +**Examples that now work:** +```typescript +const ce = new ComputeEngine(); +ce.pushScope(); +ce.assume(ce.box(['Equal', 'y', 10])); +ce.box('y').evaluate().json; // → 10 + +ce.popScope(); +ce.box('y').evaluate().json; // → 'y' (was: 10) +``` + +**Files modified:** +- `src/compute-engine/index.ts` - Added `_setCurrentContextValue()` method +- `src/compute-engine/global-types.ts` - Added method signature +- `src/compute-engine/assume.ts` - Changed to use `_setCurrentContextValue()` + +**Tests added:** +- `test/compute-engine/bug-fixes.test.ts` - Test for scoped assumption cleanup diff --git a/requirements/TODO.md b/requirements/TODO.md index bbdaa730..12eca548 100644 --- a/requirements/TODO.md +++ b/requirements/TODO.md @@ -772,54 +772,9 @@ See `requirements/DONE.md` for implementation details. --- -### 19. Inequality Evaluation Using Assumptions +### ~~19. Inequality Evaluation Using Assumptions~~ ✅ COMPLETED -**Problem:** When `x > 4` is assumed, evaluating `['Greater', 'x', 0]` should -return `True` (since x > 4 implies x > 0), but currently it returns the -expression unchanged. - -**Current behavior:** - -```typescript -ce.assume(ce.box(['Greater', 'x', 4])); -ce.box(['Greater', 'x', 0]).evaluate().json // Returns: ["Less", 0, "x"] (unchanged) -ce.box(['Less', 'x', 0]).evaluate().json // Returns: ["Less", "x", 0] (unchanged) -``` - -**Expected behavior:** - -```typescript -ce.assume(ce.box(['Greater', 'x', 4])); -ce.box(['Greater', 'x', 0]).evaluate().json // Should return: "True" -ce.box(['Less', 'x', 0]).evaluate().json // Should return: "False" -ce.box(['Greater', 'x', 10]).evaluate().json // Should return: undefined (can't determine) -``` - -**Implementation notes:** - -- Requires reasoning about transitive inequality relationships -- `x > 4` implies `x > 0`, `x > 1`, `x > 2`, etc. -- `x > 4` implies `x >= 0`, `x >= 4` -- Combined with equality assumptions: `one = 1` should make `one > 0` evaluate - to True -- This is related to but distinct from sign-based simplification (which already - works) - -**Algorithm sketch:** - -1. When evaluating `Greater(x, c)`, check if there's an assumption - `Greater(x, k)` where `k >= c` -2. When evaluating `Less(x, c)`, check if there's an assumption `Greater(x, k)` - where `k >= c` (returns False) -3. Handle all inequality operators: Greater, GreaterEqual, Less, LessEqual - -**Files to investigate:** - -- `src/compute-engine/library/relational-operator.ts` - Inequality evaluation -- `src/compute-engine/assume.ts` - Assumption querying - -**Tests:** `test/compute-engine/assumptions.test.ts` - "INEQUALITY EVALUATION -USING ASSUMPTIONS" +See `requirements/DONE.md` for implementation details. --- @@ -912,6 +867,18 @@ ASSUMPTIONS" --- +### ~~24. BUG: forget() Doesn't Clear Assumed Values~~ ✅ FIXED + +See `requirements/DONE.md` for implementation details. + +--- + +### ~~25. BUG: Scoped Assumptions Don't Clean Up on popScope()~~ ✅ FIXED + +See `requirements/DONE.md` for implementation details. + +--- + ## Future Improvements (Not Yet Detailed) ### Trigonometric Simplification diff --git a/src/compute-engine/assume.ts b/src/compute-engine/assume.ts index e16e483c..19c6ea87 100755 --- a/src/compute-engine/assume.ts +++ b/src/compute-engine/assume.ts @@ -235,8 +235,10 @@ function assumeEquality(proposition: BoxedExpression): AssumeResult { if (def.value.type && !val.type.matches(def.value.type)) if (!def.value.inferredType) return 'contradiction'; - // Set the value for the symbol with an existing definition - ce._setSymbolValue(lhs, val); + // Set the value for the symbol with an existing definition. + // Use _setCurrentContextValue so the value is scoped to the current context + // and will be automatically removed when the scope is popped. + ce._setCurrentContextValue(lhs, val); // If the type was inferred, update it based on the value if (def.value.inferredType) def.value.type = val.type; return 'ok'; @@ -264,8 +266,10 @@ function assumeEquality(proposition: BoxedExpression): AssumeResult { !sols.every((sol) => !sol.type || val.type.matches(sol.type)) ) return 'contradiction'; - // Set the value for the symbol with an existing definition - ce._setSymbolValue(lhs, val); + // Set the value for the symbol with an existing definition. + // Use _setCurrentContextValue so the value is scoped to the current context + // and will be automatically removed when the scope is popped. + ce._setCurrentContextValue(lhs, val); // If the type was inferred, update it based on the value if (def.value.inferredType) def.value.type = val.type; return 'ok'; @@ -568,3 +572,148 @@ export function getSignFromAssumptions( return undefined; } + +/** + * Get inequality bounds for a symbol from the assumption database. + * + * For example, if `x > 4` is assumed, this returns `{ lowerBound: 4, lowerStrict: true }`. + * If `x <= 10` is assumed, this returns `{ upperBound: 10, upperStrict: false }`. + * + * Note: Assumptions are normalized to forms like: + * - `x > 4` becomes `Less(Add(Negate(x), 4), 0)` i.e., `4 - x < 0` + * - `x > 0` becomes `Less(Negate(x), 0)` i.e., `-x < 0` + * + * @param ce - The compute engine instance + * @param symbol - The symbol name to query + * @returns An object with lowerBound, upperBound, and strictness flags + */ +export function getInequalityBoundsFromAssumptions( + ce: ComputeEngine, + symbol: string +): { + lowerBound?: BoxedExpression; + lowerStrict?: boolean; + upperBound?: BoxedExpression; + upperStrict?: boolean; +} { + const result: { + lowerBound?: BoxedExpression; + lowerStrict?: boolean; + upperBound?: BoxedExpression; + upperStrict?: boolean; + } = {}; + + const assumptions = ce.context?.assumptions; + if (!assumptions) return result; + + for (const [assumption, _] of assumptions.entries()) { + const op = assumption.operator; + if (!op) continue; + + // Assumptions are normalized to Less or LessEqual with RHS = 0 + if (op !== 'Less' && op !== 'LessEqual') continue; + + const ops = assumption.ops; + if (!ops || ops.length !== 2) continue; + + const [lhs, rhs] = ops; + + // RHS should be 0 for normalized assumptions + if (!rhs.is(0)) continue; + + const isStrict = op === 'Less'; + + // Case 1: Negate(symbol) < 0 => -symbol < 0 => symbol > 0 + // This gives us a lower bound of 0 + if (lhs.operator === 'Negate' && lhs.op1?.symbol === symbol) { + const bound = ce.Zero; + if ( + result.lowerBound === undefined || + bound.isGreater(result.lowerBound) === true + ) { + result.lowerBound = bound; + result.lowerStrict = isStrict; + } + } + + // Case 2: Add(Negate(symbol), k) < 0 => k - symbol < 0 => symbol > k + // This gives us a lower bound of k + if (lhs.operator === 'Add' && lhs.ops) { + let hasNegatedSymbol = false; + let constantSum = 0; + + for (const term of lhs.ops) { + if (term.operator === 'Negate' && term.op1?.symbol === symbol) { + hasNegatedSymbol = true; + } else if (term.isNumberLiteral) { + const val = + typeof term.numericValue === 'number' + ? term.numericValue + : term.numericValue?.re; + if (val !== undefined && Number.isFinite(val)) { + constantSum += val; + } + } + } + + if (hasNegatedSymbol && constantSum !== 0) { + // k - symbol < 0 => symbol > k + const bound = ce.box(constantSum); + if ( + result.lowerBound === undefined || + bound.isGreater(result.lowerBound) === true + ) { + result.lowerBound = bound; + result.lowerStrict = isStrict; + } + } + } + + // Case 3: symbol < 0 => symbol has upper bound 0 + if (lhs.symbol === symbol) { + const bound = ce.Zero; + if ( + result.upperBound === undefined || + bound.isLess(result.upperBound) === true + ) { + result.upperBound = bound; + result.upperStrict = isStrict; + } + } + + // Case 4: Add(symbol, k) < 0 => symbol + k < 0 => symbol < -k + // This gives us an upper bound of -k + if (lhs.operator === 'Add' && lhs.ops) { + let hasSymbol = false; + let constantSum = 0; + + for (const term of lhs.ops) { + if (term.symbol === symbol) { + hasSymbol = true; + } else if (term.isNumberLiteral) { + const val = + typeof term.numericValue === 'number' + ? term.numericValue + : term.numericValue?.re; + if (val !== undefined && Number.isFinite(val)) { + constantSum += val; + } + } + } + + if (hasSymbol && constantSum !== 0) { + // symbol + k < 0 => symbol < -k + const bound = ce.box(-constantSum); + if ( + result.upperBound === undefined || + bound.isLess(result.upperBound) === true + ) { + result.upperBound = bound; + result.upperStrict = isStrict; + } + } + } + } + + return result; +} diff --git a/src/compute-engine/boxed-expression/compare.ts b/src/compute-engine/boxed-expression/compare.ts index fdaebbcd..0dca3955 100644 --- a/src/compute-engine/boxed-expression/compare.ts +++ b/src/compute-engine/boxed-expression/compare.ts @@ -1,6 +1,7 @@ import { NumericValue } from '../numeric-value/types'; import type { BoxedExpression } from '../global-types'; import { AbstractTensor } from '../tensor/tensors'; +import { getInequalityBoundsFromAssumptions } from '../assume'; /** * Structural equality of boxed expressions. @@ -197,7 +198,56 @@ export function cmp( return undefined; } - if (!b.isNumberLiteral) return undefined; + if (!b.isNumberLiteral) { + // Check if b is a symbol with inequality assumptions + if (b.symbol) { + const bounds = getInequalityBoundsFromAssumptions(a.engine, b.symbol); + const aNum = + typeof a.numericValue === 'number' + ? a.numericValue + : a.numericValue?.re; + + if (aNum !== undefined && Number.isFinite(aNum)) { + // We're comparing a (number) to b (symbol) + // If b has a lower bound > a, then a < b + if (bounds.lowerBound !== undefined) { + const lowerNum = + typeof bounds.lowerBound.numericValue === 'number' + ? bounds.lowerBound.numericValue + : bounds.lowerBound.numericValue?.re; + + if (lowerNum !== undefined && Number.isFinite(lowerNum)) { + // b > lowerBound (if strict) or b >= lowerBound (if not strict) + // If lowerBound > a, then b > a, so a < b + if (lowerNum > aNum) return '<'; + // If lowerBound = a and strict (b > a), then a < b + if (lowerNum === aNum && bounds.lowerStrict) return '<'; + // If lowerBound = a and not strict (b >= a), then a <= b + if (lowerNum === aNum && !bounds.lowerStrict) return '<='; + } + } + + // If b has an upper bound < a, then a > b + if (bounds.upperBound !== undefined) { + const upperNum = + typeof bounds.upperBound.numericValue === 'number' + ? bounds.upperBound.numericValue + : bounds.upperBound.numericValue?.re; + + if (upperNum !== undefined && Number.isFinite(upperNum)) { + // b < upperBound (if strict) or b <= upperBound (if not strict) + // If upperBound < a, then b < a, so a > b + if (upperNum < aNum) return '>'; + // If upperBound = a and strict (b < a), then a > b + if (upperNum === aNum && bounds.upperStrict) return '>'; + // If upperBound = a and not strict (b <= a), then a >= b + if (upperNum === aNum && !bounds.upperStrict) return '>='; + } + } + } + } + return undefined; + } const av = a.numericValue!; const bv = b.numericValue! as NumericValue; @@ -209,7 +259,50 @@ export function cmp( return av.eq(bv) ? '=' : av.lt(bv) ? '<' : '>'; } - if (typeof b === 'number') return undefined; + if (typeof b === 'number') { + // Check if a is a symbol with inequality assumptions + if (a.symbol) { + const bounds = getInequalityBoundsFromAssumptions(a.engine, a.symbol); + + // We're comparing a (symbol) to b (number) + // If a has a lower bound >= b, then a > b (or a >= b) + if (bounds.lowerBound !== undefined) { + const lowerNum = + typeof bounds.lowerBound.numericValue === 'number' + ? bounds.lowerBound.numericValue + : bounds.lowerBound.numericValue?.re; + + if (lowerNum !== undefined && Number.isFinite(lowerNum)) { + // a > lowerBound (if strict) or a >= lowerBound (if not strict) + // If lowerBound > b, then a > b + if (lowerNum > b) return '>'; + // If lowerBound = b and strict (a > b), then a > b + if (lowerNum === b && bounds.lowerStrict) return '>'; + // If lowerBound = b and not strict (a >= b), then a >= b + if (lowerNum === b && !bounds.lowerStrict) return '>='; + } + } + + // If a has an upper bound <= b, then a < b (or a <= b) + if (bounds.upperBound !== undefined) { + const upperNum = + typeof bounds.upperBound.numericValue === 'number' + ? bounds.upperBound.numericValue + : bounds.upperBound.numericValue?.re; + + if (upperNum !== undefined && Number.isFinite(upperNum)) { + // a < upperBound (if strict) or a <= upperBound (if not strict) + // If upperBound < b, then a < b + if (upperNum < b) return '<'; + // If upperBound = b and strict (a < b), then a < b + if (upperNum === b && bounds.upperStrict) return '<'; + // If upperBound = b and not strict (a <= b), then a <= b + if (upperNum === b && !bounds.upperStrict) return '<='; + } + } + } + return undefined; + } // // Do we have at least one function expression? @@ -248,10 +341,58 @@ export function cmp( if (a.symbol === b.symbol) return '='; // Symbols may have special comparision handlers - const cmp = a.valueDefinition?.cmp?.(b); - if (cmp) return cmp; - const eq = a.valueDefinition?.eq?.(b); - if (eq === true) return '='; + const cmpResult = a.valueDefinition?.cmp?.(b); + if (cmpResult) return cmpResult; + const eqResult = a.valueDefinition?.eq?.(b); + if (eqResult === true) return '='; + + // Check inequality assumptions for the symbol + if (b.isNumberLiteral) { + const bounds = getInequalityBoundsFromAssumptions(a.engine, a.symbol); + const bNum = + typeof b.numericValue === 'number' + ? b.numericValue + : b.numericValue?.re; + + if (bNum !== undefined && Number.isFinite(bNum)) { + // If symbol has a lower bound >= b, then symbol > b (or symbol >= b) + if (bounds.lowerBound !== undefined) { + const lowerNum = + typeof bounds.lowerBound.numericValue === 'number' + ? bounds.lowerBound.numericValue + : bounds.lowerBound.numericValue?.re; + + if (lowerNum !== undefined && Number.isFinite(lowerNum)) { + // symbol > lowerBound (if strict) or symbol >= lowerBound (if not strict) + // If lowerBound > b, then symbol > b + if (lowerNum > bNum) return '>'; + // If lowerBound = b and strict (symbol > b), then symbol > b + if (lowerNum === bNum && bounds.lowerStrict) return '>'; + // If lowerBound = b and not strict (symbol >= b), then symbol >= b + if (lowerNum === bNum && !bounds.lowerStrict) return '>='; + } + } + + // If symbol has an upper bound <= b, then symbol < b (or symbol <= b) + if (bounds.upperBound !== undefined) { + const upperNum = + typeof bounds.upperBound.numericValue === 'number' + ? bounds.upperBound.numericValue + : bounds.upperBound.numericValue?.re; + + if (upperNum !== undefined && Number.isFinite(upperNum)) { + // symbol < upperBound (if strict) or symbol <= upperBound (if not strict) + // If upperBound < b, then symbol < b + if (upperNum < bNum) return '<'; + // If upperBound = b and strict (symbol < b), then symbol < b + if (upperNum === bNum && bounds.upperStrict) return '<'; + // If upperBound = b and not strict (symbol <= b), then symbol <= b + if (upperNum === bNum && !bounds.upperStrict) return '<='; + } + } + } + } + return undefined; } diff --git a/src/compute-engine/global-types.ts b/src/compute-engine/global-types.ts index 0a2ab441..c9f28ec3 100644 --- a/src/compute-engine/global-types.ts +++ b/src/compute-engine/global-types.ts @@ -3691,6 +3691,15 @@ export interface ComputeEngine extends IBigNum { value: BoxedExpression | boolean | number | undefined ): void; + /** + * Set a value directly in the current context's values map. + * Used for assumptions so values are properly scoped. + * @internal */ + _setCurrentContextValue( + id: MathJsonSymbol, + value: BoxedExpression | boolean | number | undefined + ): void; + /** A list of the function calls to the current evaluation context */ trace: ReadonlyArray; diff --git a/src/compute-engine/index.ts b/src/compute-engine/index.ts index 83a714de..4673e02c 100755 --- a/src/compute-engine/index.ts +++ b/src/compute-engine/index.ts @@ -1347,6 +1347,26 @@ export class ComputeEngine implements IComputeEngine { ctx.values[id] = value; } + /** + * Set a value directly in the current context's values map. + * This is used for assumptions so that the value is scoped to the current + * evaluation context and is automatically removed when the scope is popped. + * @internal + */ + _setCurrentContextValue( + id: MathJsonSymbol, + value: BoxedExpression | boolean | number | undefined + ): void { + const l = this._evalContextStack.length - 1; + if (l < 0) throw new Error(`No evaluation context`); + + if (typeof value === 'number') value = this.number(value); + else if (typeof value === 'boolean') value = value ? this.True : this.False; + + this._evalContextStack[l].values[id] = value; + this._generation += 1; + } + /** * Declare a symbol in the current lexical scope: specify their type and * other attributes, including optionally a value. @@ -2257,6 +2277,14 @@ export class ComputeEngine implements IComputeEngine { for (const [assumption, _val] of this.context.assumptions) { if (assumption.has(symbol)) this.context.assumptions.delete(assumption); } + + // Also clear any values that were set for this symbol in the evaluation context. + // Values can be stored in any frame of the context stack, so we need to check all of them. + for (const ctx of this._evalContextStack) { + if (symbol in ctx.values) { + delete ctx.values[symbol]; + } + } } // The removed assumptions could affect existing expressions this._generation += 1; diff --git a/test/compute-engine/assumptions.test.ts b/test/compute-engine/assumptions.test.ts index 96f1671f..cf5746ad 100755 --- a/test/compute-engine/assumptions.test.ts +++ b/test/compute-engine/assumptions.test.ts @@ -47,9 +47,9 @@ describe('VALUE RESOLUTION FROM EQUALITY ASSUMPTIONS', () => { }); }); -// TODO #19: Inequality Evaluation Using Assumptions +// #19: Inequality Evaluation Using Assumptions // When `x > 4` is assumed, `['Greater', 'x', 0]` should evaluate to True -describe.skip('INEQUALITY EVALUATION USING ASSUMPTIONS', () => { +describe('INEQUALITY EVALUATION USING ASSUMPTIONS', () => { test(`Greater(x, 0) should be True (x > 4 assumed)`, () => { expect(ce.box(['Greater', 'x', 0]).evaluate().json).toEqual('True'); }); diff --git a/test/compute-engine/bug-fixes.test.ts b/test/compute-engine/bug-fixes.test.ts new file mode 100644 index 00000000..535dc66f --- /dev/null +++ b/test/compute-engine/bug-fixes.test.ts @@ -0,0 +1,29 @@ +import { ComputeEngine } from '../../src/compute-engine'; +import '../utils'; + +describe('BUG FIXES', () => { + describe('Bug #24: forget() should clear assumed values', () => { + test('forget() clears values from evaluation context', () => { + const ce = new ComputeEngine(); + ce.assume(ce.box(['Equal', 'x', 5])); + expect(ce.box('x').evaluate().json).toEqual(5); + + ce.forget('x'); + expect(ce.box('x').evaluate().json).toEqual('x'); + }); + }); + + describe('Bug #25: Scoped assumptions should clean up on popScope()', () => { + test('popScope() removes values set by assumptions in that scope', () => { + const ce = new ComputeEngine(); + expect(ce.box('y').evaluate().json).toEqual('y'); + + ce.pushScope(); + ce.assume(ce.box(['Equal', 'y', 10])); + expect(ce.box('y').evaluate().json).toEqual(10); + + ce.popScope(); + expect(ce.box('y').evaluate().json).toEqual('y'); + }); + }); +});