Skip to content

Commit 92a100b

Browse files
committed
feat: implement tautology and contradiction detection for assumptions (#20)
ce.assume() now returns 'tautology' for redundant assumptions that are already implied by existing assumptions, and 'contradiction' for assumptions that conflict with existing ones. Examples: - ce.assume(['Greater', 'x', 0]) when x > 4 exists returns 'tautology' - ce.assume(['Less', 'x', 0]) when x > 4 exists returns 'contradiction' - ce.assume(['Equal', 'one', 1]) repeated returns 'tautology' - ce.assume(['Less', 'one', 0]) when one=1 exists returns 'contradiction' Implementation: - Added bounds checking logic in assumeInequality() that extracts the symbol from the inequality and checks against existing bounds - Handles canonical form normalization (Greater(x,k) → Less(k,x)) - Correctly determines "effective" relationship based on operator and which operand contains the symbol https://claude.ai/code/session_01Xx26gPU9ThyiypRqdLcmZQ
1 parent 6114f8e commit 92a100b

5 files changed

Lines changed: 197 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,28 @@
4646
ce.box('one').type.toString(); // → 'integer' (was: 'unknown')
4747
```
4848

49+
- **Tautology and Contradiction Detection**: `ce.assume()` now returns
50+
`'tautology'` for redundant assumptions that are already implied by existing
51+
assumptions, and `'contradiction'` for assumptions that conflict with
52+
existing ones.
53+
54+
```javascript
55+
ce.assume(ce.box(['Greater', 'x', 4]));
56+
57+
// Redundant assumption (x > 4 implies x > 0)
58+
ce.assume(ce.box(['Greater', 'x', 0])); // → 'tautology' (was: 'ok')
59+
60+
// Conflicting assumption (x > 4 contradicts x < 0)
61+
ce.assume(ce.box(['Less', 'x', 0])); // → 'contradiction'
62+
63+
// Same assumption repeated
64+
ce.assume(ce.box(['Equal', 'one', 1]));
65+
ce.assume(ce.box(['Equal', 'one', 1])); // → 'tautology'
66+
67+
// Conflicting equality
68+
ce.assume(ce.box(['Less', 'one', 0])); // → 'contradiction'
69+
```
70+
4971
### Bug Fixes
5072

5173
- **forget() Now Clears Assumed Values**: Fixed an issue where `ce.forget()` did not

requirements/DONE.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,3 +1233,53 @@ ce.box('p').type.toString(); // → 'integer'
12331233
**Tests enabled:**
12341234
- `test/compute-engine/assumptions.test.ts` - Enabled "TYPE INFERENCE FROM ASSUMPTIONS"
12351235
describe block (6 tests)
1236+
1237+
---
1238+
1239+
### 20. Tautology and Contradiction Detection ✅
1240+
1241+
**IMPLEMENTED:** `ce.assume()` now returns `'tautology'` for redundant assumptions
1242+
that are already implied by existing assumptions, and `'contradiction'` for
1243+
assumptions that conflict with existing ones.
1244+
1245+
**Problem:** When a user made an assumption like `x > 0` after already assuming
1246+
`x > 4`, the function would return `'ok'` even though the new assumption was
1247+
redundant (implied by the existing one). Similarly, assuming `x < 0` when `x > 4`
1248+
exists would silently accept a contradictory assumption.
1249+
1250+
**Solution:** Added bounds checking logic in `assumeInequality()` that:
1251+
1252+
1. Extracts the symbol from the inequality being assumed
1253+
2. Retrieves existing bounds for that symbol from current assumptions
1254+
3. Compares the new inequality against existing bounds to detect:
1255+
- **Tautologies**: When the new inequality is implied by existing bounds
1256+
- **Contradictions**: When the new inequality conflicts with existing bounds
1257+
1258+
The implementation correctly handles all combinations:
1259+
- `Greater`/`GreaterEqual` vs existing lower bounds → tautology detection
1260+
- `Greater`/`GreaterEqual` vs existing upper bounds → contradiction detection
1261+
- `Less`/`LessEqual` vs existing upper bounds → tautology detection
1262+
- `Less`/`LessEqual` vs existing lower bounds → contradiction detection
1263+
1264+
It also handles canonical form normalization - the compute engine canonicalizes
1265+
`Greater(x, k)` to `Less(k, x)`, so the code determines the "effective" relationship
1266+
based on both the operator and which operand contains the symbol.
1267+
1268+
**Examples that now work:**
1269+
```typescript
1270+
ce.assume(ce.box(['Equal', 'one', 1]));
1271+
ce.assume(ce.box(['Equal', 'one', 1])); // → 'tautology' (same assumption repeated)
1272+
ce.assume(ce.box(['Less', 'one', 0])); // → 'contradiction' (one=1 is not < 0)
1273+
ce.assume(ce.box(['Greater', 'one', 0])); // → 'tautology' (one=1 > 0)
1274+
1275+
ce.assume(ce.box(['Greater', 'x', 4]));
1276+
ce.assume(ce.box(['Greater', 'x', 0])); // → 'tautology' (x > 4 implies x > 0)
1277+
ce.assume(ce.box(['Less', 'x', 0])); // → 'contradiction' (x > 4 contradicts x < 0)
1278+
```
1279+
1280+
**Files modified:**
1281+
- `src/compute-engine/assume.ts` - Added bounds checking logic in `assumeInequality()`
1282+
1283+
**Tests enabled:**
1284+
- `test/compute-engine/assumptions.test.ts` - Enabled "TAUTOLOGY AND CONTRADICTION
1285+
DETECTION" describe block (4 tests)

requirements/TODO.md

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -778,49 +778,9 @@ See `requirements/DONE.md` for implementation details.
778778

779779
---
780780

781-
### 20. Tautology and Contradiction Detection
781+
### ~~20. Tautology and Contradiction Detection~~ ✅ COMPLETED
782782

783-
**Problem:** `ce.assume()` should detect when an assumption is redundant
784-
(tautology) or conflicts with existing assumptions (contradiction), but
785-
currently it always returns `"ok"`.
786-
787-
**Current behavior:**
788-
789-
```typescript
790-
ce.assume(ce.box(['Equal', 'one', 1]));
791-
ce.assume(ce.box(['Equal', 'one', 1])); // Returns: "ok" (should be "tautology")
792-
ce.assume(ce.box(['Less', 'one', 0])); // Returns: "ok" (should be "contradiction")
793-
```
794-
795-
**Expected behavior:**
796-
797-
```typescript
798-
ce.assume(ce.box(['Equal', 'one', 1]));
799-
ce.assume(ce.box(['Equal', 'one', 1])); // Should return: "tautology"
800-
ce.assume(ce.box(['Less', 'one', 0])); // Should return: "contradiction"
801-
ce.assume(ce.box(['Greater', 'one', 0])); // Should return: "tautology" (one=1 > 0)
802-
803-
ce.assume(ce.box(['Greater', 'x', 4]));
804-
ce.assume(ce.box(['Greater', 'x', 0])); // Should return: "tautology" (implied by x > 4)
805-
ce.assume(ce.box(['Less', 'x', 0])); // Should return: "contradiction"
806-
```
807-
808-
**Implementation notes:**
809-
810-
- Tautology detection: Check if the new assumption is already implied by
811-
existing assumptions
812-
- Contradiction detection: Check if the new assumption conflicts with existing
813-
assumptions
814-
- Requires transitive reasoning for inequalities
815-
- The return type of `assume()` already includes `"tautology"` and
816-
`"contradiction"` as options
817-
818-
**Files to investigate:**
819-
820-
- `src/compute-engine/assume.ts` - `assume()` function and `AssumeResult` type
821-
822-
**Tests:** `test/compute-engine/assumptions.test.ts` - "TAUTOLOGY AND
823-
CONTRADICTION DETECTION"
783+
See `requirements/DONE.md` for implementation details.
824784

825785
---
826786

src/compute-engine/assume.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,127 @@ function assumeInequality(proposition: BoxedExpression): AssumeResult {
394394
const unknowns = result.unknowns;
395395
if (unknowns.length === 0) return 'not-a-predicate';
396396

397+
// Check if the new inequality is implied by or contradicts existing bounds
398+
// (for single-symbol inequalities)
399+
if (unknowns.length === 1) {
400+
const symbol = unknowns[0];
401+
const bounds = getInequalityBoundsFromAssumptions(ce, symbol);
402+
403+
// The normalized form is Less(p, 0) or LessEqual(p, 0) where p = lhs - rhs
404+
// For a simple symbol case like "x > k", this becomes Less(-x + k, 0) meaning k - x < 0, i.e., x > k
405+
// For "x < k", this becomes Less(x - k, 0) meaning x - k < 0, i.e., x < k
406+
407+
// Check if this is a simple "symbol > value" or "symbol < value" case
408+
const originalOp = proposition.operator;
409+
const isSymbolOnLeft = proposition.op1.symbol === symbol;
410+
const otherSide = isSymbolOnLeft ? proposition.op2 : proposition.op1;
411+
412+
// Only do bounds checking for simple comparisons like "x > k" where k is numeric
413+
if (otherSide.numericValue !== null) {
414+
const k = otherSide.numericValue;
415+
416+
if (typeof k === 'number' && isFinite(k)) {
417+
// Determine the EFFECTIVE relationship based on operator and symbol position
418+
// Less(a, b) means a < b:
419+
// - if a is symbol: symbol < b, effective is "less"
420+
// - if b is symbol: a < symbol, so symbol > a, effective is "greater"
421+
// Greater(a, b) means a > b:
422+
// - if a is symbol: symbol > b, effective is "greater"
423+
// - if b is symbol: a > symbol, so symbol < a, effective is "less"
424+
let effectiveOp: 'greater' | 'greaterEqual' | 'less' | 'lessEqual';
425+
if (originalOp === 'Greater') {
426+
effectiveOp = isSymbolOnLeft ? 'greater' : 'less';
427+
} else if (originalOp === 'GreaterEqual') {
428+
effectiveOp = isSymbolOnLeft ? 'greaterEqual' : 'lessEqual';
429+
} else if (originalOp === 'Less') {
430+
effectiveOp = isSymbolOnLeft ? 'less' : 'greater';
431+
} else {
432+
// LessEqual
433+
effectiveOp = isSymbolOnLeft ? 'lessEqual' : 'greaterEqual';
434+
}
435+
436+
// Check for tautologies and contradictions based on existing bounds
437+
if (effectiveOp === 'greater' || effectiveOp === 'greaterEqual') {
438+
// We're asserting symbol > k or symbol >= k
439+
const isStrict = effectiveOp === 'greater';
440+
441+
if (bounds.lowerBound !== undefined) {
442+
const lowerVal = bounds.lowerBound.numericValue;
443+
if (typeof lowerVal === 'number' && isFinite(lowerVal)) {
444+
// We already know symbol > lowerVal (or >=)
445+
if (isStrict) {
446+
// Assuming symbol > k: tautology if existing lower bound implies this
447+
// If lowerVal > k, then symbol > lowerVal > k, so symbol > k (tautology)
448+
// If lowerVal == k and bound is strict, then symbol > lowerVal = k (tautology)
449+
if (lowerVal > k) return 'tautology';
450+
if (bounds.lowerStrict && lowerVal >= k) return 'tautology';
451+
} else {
452+
// Assuming symbol >= k: tautology if lowerVal >= k (with strict bound) or lowerVal > k
453+
if (lowerVal > k) return 'tautology';
454+
if (bounds.lowerStrict && lowerVal >= k) return 'tautology';
455+
if (!bounds.lowerStrict && lowerVal >= k) return 'tautology';
456+
}
457+
}
458+
}
459+
460+
if (bounds.upperBound !== undefined) {
461+
const upperVal = bounds.upperBound.numericValue;
462+
if (typeof upperVal === 'number' && isFinite(upperVal)) {
463+
// We know symbol < upperVal (or <=), now checking symbol > k
464+
if (isStrict) {
465+
// Contradiction if upperVal <= k
466+
if (upperVal < k) return 'contradiction';
467+
if (bounds.upperStrict && upperVal <= k) return 'contradiction';
468+
if (!bounds.upperStrict && upperVal <= k) return 'contradiction';
469+
} else {
470+
// symbol >= k: contradiction if upperVal < k
471+
if (upperVal < k) return 'contradiction';
472+
if (bounds.upperStrict && upperVal <= k) return 'contradiction';
473+
}
474+
}
475+
}
476+
} else {
477+
// effectiveOp is 'less' or 'lessEqual'
478+
// We're asserting symbol < k or symbol <= k
479+
const isStrict = effectiveOp === 'less';
480+
481+
if (bounds.upperBound !== undefined) {
482+
const upperVal = bounds.upperBound.numericValue;
483+
if (typeof upperVal === 'number' && isFinite(upperVal)) {
484+
// We already know symbol < upperVal (or <=)
485+
if (isStrict) {
486+
// Assuming symbol < k: tautology if existing upper bound implies this
487+
if (upperVal < k) return 'tautology';
488+
if (bounds.upperStrict && upperVal <= k) return 'tautology';
489+
} else {
490+
// symbol <= k: tautology if upperVal <= k
491+
if (upperVal < k) return 'tautology';
492+
if (upperVal <= k) return 'tautology';
493+
}
494+
}
495+
}
496+
497+
if (bounds.lowerBound !== undefined) {
498+
const lowerVal = bounds.lowerBound.numericValue;
499+
if (typeof lowerVal === 'number' && isFinite(lowerVal)) {
500+
// We know symbol > lowerVal (or >=), now checking symbol < k
501+
if (isStrict) {
502+
// Contradiction if lowerVal >= k
503+
if (lowerVal > k) return 'contradiction';
504+
if (bounds.lowerStrict && lowerVal >= k) return 'contradiction';
505+
if (!bounds.lowerStrict && lowerVal >= k) return 'contradiction';
506+
} else {
507+
// symbol <= k: contradiction if lowerVal > k
508+
if (lowerVal > k) return 'contradiction';
509+
if (bounds.lowerStrict && lowerVal > k) return 'contradiction';
510+
}
511+
}
512+
}
513+
}
514+
}
515+
}
516+
}
517+
397518
// Case 3: single unknown - ensure the symbol has type 'real'
398519
// (inequalities imply the symbol is a real number)
399520
if (unknowns.length === 1) {

test/compute-engine/assumptions.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ describe('INEQUALITY EVALUATION USING ASSUMPTIONS', () => {
7575
});
7676
});
7777

78-
// TODO #20: Tautology and Contradiction Detection
78+
// #20: Tautology and Contradiction Detection
7979
// ce.assume() should return 'tautology' for redundant assumptions and 'contradiction' for conflicting ones
80-
describe.skip('TAUTOLOGY AND CONTRADICTION DETECTION', () => {
80+
describe('TAUTOLOGY AND CONTRADICTION DETECTION', () => {
8181
test(`assuming one = 1 again should return tautology`, () => {
8282
expect(ce.assume(ce.box(['Equal', 'one', 1]))).toEqual('tautology');
8383
});

0 commit comments

Comments
 (0)