Skip to content

Commit c66fb5b

Browse files
committed
feat: fix #225 implement verify() for truth queries and enhance ask() functionality
1 parent 0316ef7 commit c66fb5b

7 files changed

Lines changed: 374 additions & 4 deletions

File tree

ASK.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# `ask()` / `verify()` plan
2+
3+
## Background
4+
5+
Today, `ce.ask(pattern)` is _not_ “ask if this is true”. It is an **assumptions
6+
DB query**:
7+
8+
- It boxes `pattern` (non-canonical) and structurally matches it against entries
9+
in `ce.context.assumptions`.
10+
- It returns `BoxedSubstitution[]` (pattern match bindings), not a truth value.
11+
12+
That makes issue #225 unsurprising: declarations (symbol types/values) and many
13+
derived facts are not stored in `context.assumptions`, and inequality
14+
assumptions are stored in normalized forms that won’t match “user-shaped”
15+
patterns.
16+
17+
SymPy’s `ask()` is closer to “can I prove/disprove this predicate?” and returns
18+
**`True/False/None`** (unknown). We can offer the same UX by making
19+
`ce.verify()` the SymPy-like API, while keeping `ce.ask()` as an explicit
20+
assumptions query tool.
21+
22+
## Goals
23+
24+
- Implement `ce.verify(query): boolean | undefined`
25+
- Returns `true` if the predicate is provably true with current knowledge.
26+
- Returns `false` if it is provably false with current knowledge.
27+
- Returns `undefined` if it cannot be proven either way.
28+
- Expand the set of predicates that can be proven/disproven using:
29+
- explicit assumptions (`ce.assume(...)`),
30+
- symbol declarations/definitions (`ce.declare(...)`, inferred types, assigned
31+
values),
32+
- existing predicate evaluation rules (e.g. inequality evaluation already uses
33+
assumptions).
34+
- Keep `ce.ask(pattern)` as an “assumptions DB matcher”.
35+
36+
## Non-goals (for now)
37+
38+
- Returning witnesses/substitutions/bounds from `verify()` (SymPy `ask()`
39+
doesn’t do this).
40+
- Full SAT-style cross-predicate inference like SymPy’s known-facts system.
41+
- Proving general quantified statements over infinite domains (only
42+
finite-domain evaluation when available).
43+
44+
## Proposed public semantics
45+
46+
### `verify()`
47+
48+
Signature:
49+
50+
- `verify(query: BoxedExpression): boolean | undefined`
51+
- (Optional follow-up) accept a LaTeX string like `assume()` does.
52+
53+
Behavior:
54+
55+
1. Box/parse the input similarly to `assume()` (i.e., accept `LatexString` too,
56+
if we add it).
57+
2. Try to evaluate `query` to a boolean:
58+
- If it evaluates to `True``true`
59+
- If it evaluates to `False``false`
60+
- Otherwise → `undefined`
61+
3. Support boolean connectives (`Not`, `And`, `Or`) by evaluating operands
62+
recursively when needed.
63+
64+
Examples:
65+
66+
- `ce.verify(['Greater', 'x', 0])` after `ce.assume(['Greater', 'x', 4])`
67+
`true`
68+
- `ce.verify(['Less', 'x', 0])` after `ce.assume(['Greater', 'x', 4])``false`
69+
- `ce.verify(['Greater', 'x', 0])` with no facts about `x``undefined`
70+
71+
### `ask()`
72+
73+
Keep behavior: `ask(pattern)` returns a list of structural matches against
74+
`context.assumptions`.
75+
76+
Documentation update:
77+
78+
- Encourage “truth queries” to use `verify()`.
79+
- Encourage “show me matching stored assumptions” to use `ask()`.
80+
81+
## Predicate support roadmap
82+
83+
This is ordered by “high value, low complexity” first.
84+
85+
### Phase 1: Make `verify()` useful immediately
86+
87+
1. Implement `verify()` in `src/compute-engine/index.ts` using evaluation:
88+
- Evaluate `query` (with a “predicate evaluation” helper).
89+
- Map `True`/`False` to `boolean`, else `undefined`.
90+
2. Add tests (new file `test/compute-engine/verify.test.ts` or extend
91+
`test/compute-engine/assumptions.test.ts`):
92+
- inequality truthiness via assumptions (already covered by evaluate() tests,
93+
but add explicit `verify()` coverage)
94+
- equality truthiness via assumed equalities
95+
- boolean connectives over known/unknown components (e.g.
96+
`And(True, unknown)``undefined`)
97+
98+
### Phase 2: Type/domain predicates (`Element`, `NotElement`)
99+
100+
Problem: `Element(value, collection)` currently only works when `collection` is
101+
a _collection/set_ with a `contains()` implementation. It does **not** work for
102+
type-like RHS such as `'finite_real'` or `'any'`.
103+
104+
Plan:
105+
106+
- Extend `Element.evaluate` / `NotElement.evaluate` (in
107+
`src/compute-engine/library/sets.ts`) to recognize a “type RHS”:
108+
- If `collection.contains(value)` is `undefined` and `collection` looks like a
109+
type token, interpret it as a `BoxedType`.
110+
- Proposed rule: if `collection.symbol` corresponds to a known type name (e.g.
111+
`any`, `number`, `real`, `finite_real`, …), then:
112+
- `Element(value, collectionType)` is `true` when
113+
`value.type.matches(collectionType)` is `true`
114+
- `false` when it is definitively incompatible
115+
- `undefined` otherwise
116+
- Add tests:
117+
- `ce.declare('x', 'finite_real')` then
118+
`ce.verify(['Element', 'x', 'finite_real'])``true`
119+
- `ce.declare('x', 'real')` then `ce.verify(['Element', 'x', 'finite_real'])`
120+
`undefined` (can’t prove “finite”)
121+
- `ce.declare('x', 'integer')` then `ce.verify(['Element', 'x', 'real'])`
122+
`true`
123+
124+
Notes:
125+
126+
- Also consider aligning domains with existing set symbols (`RealNumbers`,
127+
`Integers`, etc.) and treating them as the preferred spelling in docs, while
128+
still supporting type spellings for ergonomics.
129+
130+
### Phase 3: More “SymPy-style” predicate surface area
131+
132+
SymPy exposes a large predicate set via `Q.<predicate>` (see
133+
`sympy/assumptions/ask.py` around `AssumptionKeys`).
134+
135+
We don’t need the same surface API (`Q.*`) to get comparable utility; we need
136+
comparable _facts_.
137+
138+
Suggested mapping to existing Compute Engine capabilities:
139+
140+
- Set/type predicates:
141+
- `real`, `complex`, `imaginary`, `rational`, `integer`, `finite`, `infinite`
142+
- Implement via `Element(expr, <TypeOrSet>)` or via `expr.type.matches(...)` /
143+
`expr.isFinite`.
144+
- Order/sign predicates:
145+
- `positive`, `negative`, `zero`, `nonzero`, `nonpositive`, `nonnegative`
146+
- Implement via `expr.isPositive/isNegative/isNonNegative/isNonPositive`, or
147+
by rewriting to comparisons to `0`.
148+
- Parity predicates:
149+
- `even`, `odd` (already present as `expr.isEven`/`expr.isOdd` for many
150+
expression kinds).
151+
152+
Concrete work items:
153+
154+
- Ensure evaluation for basic comparisons (`Equal`, `NotEqual`, `Less`,
155+
`LessEqual`, `Greater`, `GreaterEqual`) continues to consult:
156+
- symbol values (from equality assumptions / declarations),
157+
- inequality assumptions (already in place),
158+
- simplification rules.
159+
- Consider adding lightweight predicate operators (optional):
160+
- e.g. `Positive(x)` as sugar for `Greater(x, 0)` (only if it improves
161+
readability and doesn’t bloat the library surface).
162+
163+
### Phase 4: Quantifiers (finite domains only)
164+
165+
Compute Engine already supports finite-domain evaluation patterns for
166+
quantifiers in some contexts.
167+
168+
Plan:
169+
170+
- Ensure `verify(ForAll(...))` and `verify(Exists(...))` return
171+
`true/false/undefined`:
172+
- `true/false` only when the quantifier can be evaluated over a finite domain
173+
(e.g., `Element(x, Set(...))`).
174+
- `undefined` otherwise (avoid pretending to prove over infinite domains).
175+
176+
## Documentation updates
177+
178+
- Update the API docs for `ask()` to clearly state it queries
179+
`context.assumptions` and returns substitutions.
180+
- Document `verify()` as the truth-query API with `boolean | undefined`.
181+
- Add a short migration note:
182+
- “If you were using `ask()` to check truthiness, use `verify()` instead.”

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
### Improvements
44

5+
- **Improved `ask()` Queries**: `ce.ask()` now matches patterns with wildcards
6+
correctly, can answer common “bound” queries such as
7+
`ask(["Greater", "x", "_k"])`, and falls back to `verify()` for closed
8+
predicates when the fact is known but not stored as an explicit assumption.
9+
10+
- **Tri-state `verify()`**: Implemented `ce.verify()` as a truth query that
11+
returns `true`, `false` or `undefined` when a predicate cannot be determined
12+
from the current assumptions and declarations. `And`/`Or`/`Not` use 3-valued
13+
logic.
14+
15+
- **`Element`/`NotElement` Type Membership**: `Element(x, T)` and
16+
`NotElement(x, T)` now support type-style RHS (e.g. `real`, `finite_real`,
17+
`number`, `any`) in addition to set collections (e.g. `RealNumbers`,
18+
`Integers`).
19+
520
- **Interval Notation Parsing**: Added support for parsing mathematical interval
621
notation from LaTeX, including half-open intervals. Addresses #254.
722

src/compute-engine/global-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3880,7 +3880,7 @@ export interface ComputeEngine extends IBigNum {
38803880

38813881
ask(pattern: BoxedExpression): BoxedSubstitution[];
38823882

3883-
verify(query: BoxedExpression): boolean;
3883+
verify(query: BoxedExpression): boolean | undefined;
38843884

38853885
/** @internal */
38863886
_shouldContinueExecution(): boolean;

src/compute-engine/index.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,9 +2201,52 @@ export class ComputeEngine implements IComputeEngine {
22012201
*
22022202
*/
22032203

2204-
verify(_query: BoxedExpression): boolean {
2205-
// @todo
2206-
return false;
2204+
verify(query: BoxedExpression): boolean | undefined {
2205+
const boxed = isLatexString(query)
2206+
? this.parse(query, { canonical: false })
2207+
: this.box(query, { canonical: false });
2208+
2209+
const expr = boxed.evaluate();
2210+
if (expr.symbol === 'True') return true;
2211+
if (expr.symbol === 'False') return false;
2212+
2213+
const op = expr.operator;
2214+
2215+
if (op === 'Not') {
2216+
const result = this.verify(expr.op1);
2217+
if (result === undefined) return undefined;
2218+
return !result;
2219+
}
2220+
2221+
if (op === 'And') {
2222+
// Kleene 3-valued logic:
2223+
// - if any operand is false, the result is false
2224+
// - if all operands are true, the result is true
2225+
// - otherwise the result is unknown
2226+
let hasUnknown = false;
2227+
for (const x of expr.ops ?? []) {
2228+
const r = this.verify(x);
2229+
if (r === false) return false;
2230+
if (r === undefined) hasUnknown = true;
2231+
}
2232+
return hasUnknown ? undefined : true;
2233+
}
2234+
2235+
if (op === 'Or') {
2236+
// Kleene 3-valued logic:
2237+
// - if any operand is true, the result is true
2238+
// - if all operands are false, the result is false
2239+
// - otherwise the result is unknown
2240+
let hasUnknown = false;
2241+
for (const x of expr.ops ?? []) {
2242+
const r = this.verify(x);
2243+
if (r === true) return true;
2244+
if (r === undefined) hasUnknown = true;
2245+
}
2246+
return hasUnknown ? undefined : false;
2247+
}
2248+
2249+
return undefined;
22072250
}
22082251

22092252
/**

src/compute-engine/library/sets.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import { BoxedType } from '../../common/type/boxed-type';
55
import { parseType } from '../../common/type/parse';
6+
import { reduceType } from '../../common/type/reduce';
7+
import type { Type } from '../../common/type/types';
68
import { flatten } from '../boxed-expression/flatten';
79
import { validateArguments } from '../boxed-expression/validate';
810
import {
@@ -21,6 +23,10 @@ import {
2123
cantorEnumerateRationals,
2224
} from '../numerics/numeric';
2325

26+
function typeIntersection(a: Type, b: Type): Type {
27+
return reduceType({ kind: 'intersection', types: [a, b] });
28+
}
29+
2430
/**
2531
* Transform a List or Tuple with exactly 2 elements to an Interval in set contexts.
2632
*
@@ -539,6 +545,20 @@ export const SETS_LIBRARY: SymbolDefinitions = {
539545
const result = collection.contains(value);
540546
if (result === true) return ce.True;
541547
if (result === false) return ce.False;
548+
549+
// Support type-style membership checks, e.g. Element(x, real) or
550+
// Element(x, finite_real). This complements set membership checks.
551+
const typeName = collection.symbol;
552+
if (typeName) {
553+
const type = ce.type(typeName);
554+
if (!type.isUnknown) {
555+
const valueType = value.type;
556+
if (valueType.matches(type)) return ce.True;
557+
if (typeIntersection(valueType.type, type.type) === 'nothing')
558+
return ce.False;
559+
}
560+
}
561+
542562
return undefined;
543563
},
544564
},
@@ -551,6 +571,19 @@ export const SETS_LIBRARY: SymbolDefinitions = {
551571
const result = collection.contains(value);
552572
if (result === true) return ce.False;
553573
if (result === false) return ce.True;
574+
575+
// Support type-style membership checks, e.g. NotElement(x, real).
576+
const typeName = collection.symbol;
577+
if (typeName) {
578+
const type = ce.type(typeName);
579+
if (!type.isUnknown) {
580+
const valueType = value.type;
581+
if (valueType.matches(type)) return ce.False;
582+
if (typeIntersection(valueType.type, type.type) === 'nothing')
583+
return ce.True;
584+
}
585+
}
586+
554587
return undefined;
555588
},
556589
},

test/compute-engine/ask.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ComputeEngine } from '../../src/compute-engine';
2+
3+
describe('ASK', () => {
4+
test('matches patterns with wildcards against stored assumptions', () => {
5+
const ce = new ComputeEngine();
6+
ce.assume(ce.parse('x > 0'));
7+
8+
const r = ce.ask(['Less', ['Negate', 'x'], '_k']);
9+
expect(r.length).toBe(1);
10+
expect(r[0]!._k.json).toBe(0);
11+
});
12+
13+
test('answers inequality bound queries in user form', () => {
14+
const ce = new ComputeEngine();
15+
ce.assume(ce.parse('x > 0'));
16+
17+
const r = ce.ask(['Greater', 'x', '_k']);
18+
expect(r.length).toBe(1);
19+
expect(r[0]!._k.json).toBe(0);
20+
});
21+
22+
test('is conservative about strictness of bounds', () => {
23+
const ce = new ComputeEngine();
24+
ce.assume(ce.parse('x \\ge 0'));
25+
26+
expect(ce.ask(['Greater', 'x', '_k'])).toEqual([]);
27+
28+
const r = ce.ask(['GreaterEqual', 'x', '_k']);
29+
expect(r.length).toBe(1);
30+
expect(r[0]!._k.json).toBe(0);
31+
});
32+
33+
test('can answer Element queries from declarations', () => {
34+
const ce = new ComputeEngine();
35+
ce.declare('x', 'finite_real');
36+
37+
// Closed predicate fallback (B3)
38+
expect(ce.ask(['Element', 'x', 'any'])).toEqual([{}]);
39+
40+
// Type extraction from declaration (B1)
41+
const r = ce.ask(['Element', 'x', '_T']);
42+
expect(r.length).toBe(1);
43+
expect(r[0]!._T.json).toBe('finite_real');
44+
});
45+
});
46+

0 commit comments

Comments
 (0)