1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
use super::*;
// ── Compute result ingestion tests ──
#[test]
fn query_time_compute_ingest_does_not_grow_the_domain() {
// The trusted-oracle ingest (`assert_typed_fact`) stores `product(6,2,3)`
// as a ground fact mid-query, but deliberately does NOT note 6 as a
// quantifier-domain member: query evaluation must not grow the domain —
// the one residual of the numbers-join-the-domain change (GUARANTEES
// §Disclosed Sharp Edges; the assertion path notes via
// `collect_and_note_constants`, this path never does).
let kb = new_kb();
let ground = |nodes: &mut Vec<LogicNode>| {
compute(
nodes,
"product",
vec![
LogicalTerm::Number(6.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
)
};
let mut q_nodes = Vec::new();
let q_root = ground(&mut q_nodes);
assert!(query(
&kb,
LogicBuffer {
nodes: q_nodes,
roots: vec![q_root]
}
));
// If the ingest had noted 6, this universal would find it as a
// counterexample (6 ≠ 2 + 2 → FALSE); the still-empty domain keeps it
// vacuously TRUE instead.
let mut u_nodes = Vec::new();
let body = compute(
&mut u_nodes,
"product",
vec![
LogicalTerm::Variable("_v0".to_string()),
LogicalTerm::Number(2.0),
LogicalTerm::Number(2.0),
],
);
let u_root = forall(&mut u_nodes, "_v0", body);
assert!(query(
&kb,
LogicBuffer {
nodes: u_nodes,
roots: vec![u_root]
}
));
}
#[test]
fn test_compute_result_ingested_into_kb() {
let kb = new_kb();
// Query pilji(6, 2, 3) via ComputeNode → TRUE (built-in arithmetic)
// This should auto-ingest the fact into the KB
let mut q_nodes = Vec::new();
let q_root = compute(
&mut q_nodes,
"product",
vec![
LogicalTerm::Number(6.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
assert!(query(
&kb,
LogicBuffer {
nodes: q_nodes,
roots: vec![q_root]
}
));
// Now query the SAME fact as a plain Predicate (not ComputeNode)
// It should be found directly in the KB because of auto-ingestion
let mut p_nodes = Vec::new();
let p_root = pred(
&mut p_nodes,
"product",
vec![
LogicalTerm::Number(6.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
assert!(query(
&kb,
LogicBuffer {
nodes: p_nodes,
roots: vec![p_root]
}
));
}
#[test]
fn test_compute_false_not_ingested() {
let kb = new_kb();
// Query pilji(7, 2, 3) via ComputeNode → FALSE (7 != 2*3)
let mut q_nodes = Vec::new();
let q_root = compute(
&mut q_nodes,
"product",
vec![
LogicalTerm::Number(7.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
assert!(query_false(
&kb,
LogicBuffer {
nodes: q_nodes,
roots: vec![q_root]
}
));
// Verify the false fact was NOT ingested as a plain Predicate
let mut p_nodes = Vec::new();
let p_root = pred(
&mut p_nodes,
"product",
vec![
LogicalTerm::Number(7.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
assert!(query_false(
&kb,
LogicBuffer {
nodes: p_nodes,
roots: vec![p_root]
}
));
}
#[test]
fn test_ingested_result_available_for_reasoning() {
let kb = new_kb();
// Step 1: Query sumji(5, 2, 3) via ComputeNode → TRUE, auto-ingests
let mut q_nodes = Vec::new();
let q_root = compute(
&mut q_nodes,
"sum",
vec![
LogicalTerm::Number(5.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
assert!(query(
&kb,
LogicBuffer {
nodes: q_nodes,
roots: vec![q_root]
}
));
// Step 2: Assert another fact
assert_buf(&kb, make_assertion("ok", "derived"));
// Step 3: Query conjunction: And(sumji(5,2,3), derived("ok", Zoe))
// Both facts should be in KB: sumji from compute ingestion, derived from assertion
let mut q2_nodes = Vec::new();
let left = pred(
&mut q2_nodes,
"sum",
vec![
LogicalTerm::Number(5.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
let right = pred(
&mut q2_nodes,
"derived",
vec![
LogicalTerm::Constant("ok".to_string()),
LogicalTerm::Unspecified,
],
);
let root = and(&mut q2_nodes, left, right);
assert!(query(
&kb,
LogicBuffer {
nodes: q2_nodes,
roots: vec![root]
}
));
// Step 4: Conjunctive query with a non-ingested compute fact fails
let mut q3_nodes = Vec::new();
let l2 = pred(
&mut q3_nodes,
"sum",
vec![
LogicalTerm::Number(99.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
let r2 = pred(
&mut q3_nodes,
"derived",
vec![
LogicalTerm::Constant("ok".to_string()),
LogicalTerm::Unspecified,
],
);
let root2 = and(&mut q3_nodes, l2, r2);
assert!(query_false(
&kb,
LogicBuffer {
nodes: q3_nodes,
roots: vec![root2]
}
));
}
#[test]
fn assert_typed_fact_invalidates_pred_cache() {
// The cache-freshness invariant AT THE MUTATION POINT: cache a derived
// predicate as False, then assert (via `assert_typed_fact` — the path ALL
// five mid-query compute auto-ingestion sites funnel through) the base fact
// that makes it derivable. The stale False must NOT survive. Pre-fix the
// re-check returned the cached False (order-dependent wrong answers);
// post-fix `assert_typed_fact` clears the cache so the re-check re-derives.
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu")); // ∀x. dog(x) → danlu(x)
let danlu_adam = StoredFact::Bare(GroundFact::new(
"danlu",
vec![
GroundTerm::Constant("adam".to_string()),
GroundTerm::Unspecified,
],
));
let dog_adam = StoredFact::Bare(GroundFact::new(
"gerku",
vec![
GroundTerm::Constant("adam".to_string()),
GroundTerm::Unspecified,
],
));
clear_and_enable_pred_cache(&kb.inner.borrow());
// (1) danlu(adam) is not derivable yet (gerku(adam) absent) → caches False.
{
let inner = kb.inner.borrow();
let mut visited = std::collections::HashSet::new();
let r = check_predicate_in_kb_typed(&danlu_adam, &inner, 0, &mut visited);
assert!(
r.is_false(),
"danlu(adam) is not derivable before gerku(adam)"
);
}
// (2) Assert gerku(adam) through the mutation point.
{
let mut inner = kb.inner.borrow_mut();
assert_typed_fact(dog_adam, &mut inner);
}
// (3) Re-check: a stale cached False here means the cache was NOT invalidated.
{
let inner = kb.inner.borrow();
let mut visited = std::collections::HashSet::new();
let r = check_predicate_in_kb_typed(&danlu_adam, &inner, 0, &mut visited);
assert!(
r.is_true(),
"danlu(adam) must derive True once dog(adam) is asserted — a stale \
cached False means assert_typed_fact failed to invalidate the cache"
);
}
}
#[test]
fn pred_cache_is_per_instance_no_cross_kb_leak() {
// Two KBs on the same thread. KB-A enables its cache and caches a False for
// danlu(adam) (gerku(adam) absent). KB-B has gerku(adam), so danlu(adam) IS
// derivable. With the old THREAD-LOCAL cache, KB-A's enabled+cached False
// leaked to KB-B on the same thread; per-instance caches keep them isolated.
let danlu_adam = StoredFact::Bare(GroundFact::new(
"danlu",
vec![
GroundTerm::Constant("adam".to_string()),
GroundTerm::Unspecified,
],
));
let kb_a = new_kb();
assert_buf(&kb_a, make_universal("gerku", "danlu")); // ∀x. dog(x) → danlu(x)
{
let inner = kb_a.inner.borrow();
clear_and_enable_pred_cache(&inner);
let mut visited = std::collections::HashSet::new();
let r = check_predicate_in_kb_typed(&danlu_adam, &inner, 0, &mut visited);
assert!(
r.is_false(),
"KB-A: danlu(adam) is not derivable (no gerku) → caches False"
);
}
let kb_b = new_kb();
assert_buf(&kb_b, make_universal("gerku", "danlu"));
assert_buf(&kb_b, make_assertion("adam", "gerku")); // dog(adam)
{
let inner = kb_b.inner.borrow();
let mut visited = std::collections::HashSet::new();
let r = check_predicate_in_kb_typed(&danlu_adam, &inner, 0, &mut visited);
assert!(
r.is_true(),
"KB-B: danlu(adam) IS derivable (dog(adam) present); KB-A's cached \
False must not leak across per-instance caches"
);
}
}
#[test]
fn compute_autoassert_invalidates_stale_cache() {
// End-to-end through the actual compute auto-assert caller, in a SINGLE query
// (the bug window — the cache is cleared once at query entry, then persists).
// Rule `sumji(5,2,3) ⇒ derived(c)` (a ground material conditional), with
// sumji NOT stored. One query: a probe that returns True but caches
// derived(c)=False, then a compute conjunct that auto-asserts sumji(5,2,3),
// then a re-check of derived(c). Pre-fix the re-check reads the stale False
// (query wrongly False); post-fix the auto-assert clears the cache and
// derived(c) re-derives True.
let kb = new_kb();
assert_buf(&kb, make_assertion("c", "marker")); // always-true right branch
// Material conditional: Or(Not(sumji(5,2,3)), derived(c)) ⇒ zero-var rule.
let mut r_nodes = Vec::new();
let cond = pred(
&mut r_nodes,
"sum",
vec![
LogicalTerm::Number(5.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
let concl = pred(
&mut r_nodes,
"derived",
vec![
LogicalTerm::Constant("c".to_string()),
LogicalTerm::Unspecified,
],
);
let ncond = not(&mut r_nodes, cond);
let rule_root = or(&mut r_nodes, ncond, concl);
assert_buf(
&kb,
LogicBuffer {
nodes: r_nodes,
roots: vec![rule_root],
},
);
// Query: And( Or(derived(c), marker(c)) , And( sumji(5,2,3)[compute] , derived(c) ) )
let mut q = Vec::new();
let derived_probe = pred(
&mut q,
"derived",
vec![
LogicalTerm::Constant("c".to_string()),
LogicalTerm::Unspecified,
],
);
let marker = pred(
&mut q,
"marker",
vec![
LogicalTerm::Constant("c".to_string()),
LogicalTerm::Unspecified,
],
);
let probe = or(&mut q, derived_probe, marker); // True overall, caches derived(c)=False
let compute_node = compute(
&mut q,
"sum",
vec![
LogicalTerm::Number(5.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
); // computes True, auto-asserts sumji(5,2,3)
let derived_final = pred(
&mut q,
"derived",
vec![
LogicalTerm::Constant("c".to_string()),
LogicalTerm::Unspecified,
],
);
let inner_and = and(&mut q, compute_node, derived_final);
let root = and(&mut q, probe, inner_and);
assert!(
query(
&kb,
LogicBuffer {
nodes: q,
roots: vec![root]
}
),
"derived(c) must re-derive True after the compute conjunct auto-asserts \
sumji(5,2,3) — a stale cached False makes this query wrongly False"
);
}