Skip to content

Commit 052b097

Browse files
committed
MeTTa TS 1.1.6
1 parent ba7e71d commit 052b097

40 files changed

Lines changed: 940 additions & 978 deletions

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -303,21 +303,23 @@ A representative slice (wall-clock, subprocess including startup; `speedup` = Pe
303303

304304
The full per-program table is in [`RESULTS-corpus.md`](packages/node/bench/RESULTS-corpus.md).
305305

306-
The checked nondeterminism benchmark also includes four adversarial programs reported by Patrick Hammer and the `jarr` and `loowoz` bounded backward-chaining queries reported by Nil Geisweiller. Both BFC files extract the exact `obc` definitions and target queries from [`trueagi-io/chaining@bc9beb2`](https://github.com/trueagi-io/chaining/blob/bc9beb2672953e07971b3abecc1fe67651ecddc4/experimental/backward-via-forward/bfc-xp.metta). These are 15-run subprocess medians measured on 2026-07-15 on an AMD Ryzen 9 9950X, including startup, with Node 22.22.1 and the clean PeTTa `6b7f52f` checkout on SWI-Prolog 9.3.33:
306+
The checked nondeterminism benchmark also includes four adversarial programs reported by Patrick Hammer and the `jarr` and `loowoz` bounded backward-chaining queries reported by Nil Geisweiller. Both BFC files extract the exact `obc` definitions and target queries from [`trueagi-io/chaining@bc9beb2`](https://github.com/trueagi-io/chaining/blob/bc9beb2672953e07971b3abecc1fe67651ecddc4/experimental/backward-via-forward/bfc-xp.metta). These are 15-run subprocess medians measured on 2026-07-15 on an AMD Ryzen 9 9950X, including startup, with Node 22.22.1 and the clean PeTTa `6f5639a` checkout on SWI-Prolog 9.2.9:
307307

308308
| Program | PeTTa ms | PeTTa MiB | MeTTa TS ms | MeTTa TS MiB | Speedup |
309309
| -------------------------------- | -------: | --------: | ----------: | -----------: | --------: |
310-
| BFC `jarr` | 134.6 | 16.4 | 125.6 | 86.5 | **1.07x** |
311-
| BFC `loowoz` | 2441.2 | 16.4 | 872.8 | 108.9 | **2.80x** |
312-
| filtered `matespacefast` matches | 6157.9 | 3334.5 | 3568.3 | 424.0 | **1.73x** |
313-
| 22^4 `superpose` cross product | 357.5 | 87.9 | 147.7 | 148.8 | **2.42x** |
314-
| nondeterministic tabled `fib(7)` | 127.8 | 16.4 | 94.1 | 79.8 | **1.36x** |
315-
| duplicate-heavy `TupleConcat` | 126.0 | 16.3 | 96.3 | 79.4 | **1.31x** |
310+
| BFC `jarr` | 136.5 | 16.4 | 113.6 | 91.4 | **1.20x** |
311+
| BFC `loowoz` | 2466.3 | 16.4 | 743.6 | 100.0 | **3.32x** |
312+
| filtered `matespacefast` matches | 6140.3 | 3331.0 | 3380.2 | 424.6 | **1.82x** |
313+
| 22^4 `superpose` cross product | 354.5 | 87.9 | 150.3 | 146.8 | **2.36x** |
314+
| nondeterministic tabled `fib(7)` | 128.6 | 16.3 | 98.5 | 79.9 | **1.31x** |
315+
| duplicate-heavy `TupleConcat` | 127.6 | 16.3 | 98.0 | 78.6 | **1.30x** |
316316

317317
The MiB columns are the highest sampled process-tree RSS across the 15 runs on Linux.
318318

319319
The benchmark validates both `jarr` proofs and all three `loowoz` proofs in exact order, all 234,256 cross-product results, all 196 distinct Fibonacci answers, the exact `TupleConcat` result, and the embedded matespace assertion. MeTTa TS uses the same default evaluator as the CLI. No benchmark, PeTTa, curry, or tabling mode is selected.
320320

321+
Against the untouched 1.1.5 build on the same machine, alternating exact-output comparisons improved `jarr` by 1.14x cold and 1.12x loaded. `loowoz` improved by 1.18x cold and 1.31x loaded. The `jarr` cold path also improved on Node 20, 22, and 24. Ineligible programs skip the new route and retain the old generated module shape.
322+
321323
Automatic tabling does not memoize every recursive function. Admission requires transitive purity and a recursive strongly connected component that branches back into itself at least twice. Linear recursion such as factorial stays on the compiled path. Custom host operations and space, state, file, random, time, import, and output operations are excluded.
322324

323325
Completed and active tables share hard entry, answer, retained-cell, per-entry, and interner limits. Completed entries are LRU-evictable. Active entries cannot be evicted while their producer runs, so a call that cannot fit returns `TableResourceLimit` instead of continuing toward process exhaustion. See [Scaling to millions of atoms](website/advanced/scaling.md#automatic-tabling) for the current limits and completion policy.
@@ -336,7 +338,7 @@ That speed comes from general engine work:
336338
- a slot-based evaluator for closed pure `let` and `superpose` products that preserves result order and multiplicity without allocating general binding frames;
337339
- a native-code compiler for the pure deterministic int/bool/tuple subset, with tail-recursion compiled to loops and PeTTa-style **higher-order specialisation** so a function passed as an argument (e.g. `iterate`'s `$step`) is bound and compiled rather than interpreted;
338340
- a compiler for **nondeterministic `let*`-chain functions** (the backward-chainer class): a multi-equation function whose clause bodies chain space matches and recursive calls compiles to a clause-major depth-first search, the same fragment PeTTa hands to Prolog's clause alternatives;
339-
- dependency-sensitive dispatch for static pure search: answer-dependent recursive joins use the compiler first, while independent overlapping calls retain moded tabling; common-result-layout compilation passes only changing result fields through recursive continuations and rebuilds the full MeTTa atom once at the evaluator boundary;
341+
- dependency-sensitive dispatch for static pure search: answer-dependent recursive joins use the compiler first, while independent overlapping calls retain moded tabling; common-result-layout compilation omits input-projected fields from recursive continuations and constructs a proven independent output only after child searches succeed;
340342
- WAM-style fresh write-mode binding: when static dataflow proves that every slot in a constructed pattern subtree is introduced at that site, generated code binds the new structure directly; input and previously introduced slots retain the full occurs-checking unifier;
341343
- a compiler for **add-atom saturation loops**: the add-if-absent idiom becomes one exact-membership probe plus append, and a single-branch `case` over a space match becomes a snapshot-and-thread loop with Empty-pruned branches.
342344

RELEASE_NOTES.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,99 @@
1+
# MeTTa TS 1.1.6
2+
3+
MeTTa TS 1.1.6 reduces the cold and loaded cost of compiled nondeterministic
4+
proof search. It also adds Hyperon-style structural inequality to the core and
5+
TypeScript EDSL.
6+
7+
## Deferred proof output
8+
9+
The nondeterministic compiler can now prove that one result field is an
10+
unbound output projection which does not affect clause choice, matching,
11+
guards, or recursive call arguments. For those relations, generated search
12+
code carries only the control fields. It constructs the deferred field after
13+
all child searches succeed. Result fields already projected from call inputs
14+
are recovered at the consumer instead of being passed through every recursive
15+
continuation.
16+
17+
The analysis uses result and input projections from the existing compiler. It
18+
does not recognize `obc`, theorem names, or benchmark source. Runtime admission
19+
requires the projected input to be unbound and unaliased, and requires a
20+
natural-number descent field for the existing bounded-recursion guard. If any
21+
proof fails, the ordinary compiled search runs with unchanged ordered-bag
22+
semantics. Groups with no deferred plan retain the 1.1.5 generated module shape
23+
and skip the deferred attempt entirely.
24+
25+
Large generated clause matchers are emitted as separate JavaScript functions,
26+
while small recurrence clauses stay inline. This reduces V8 compilation work
27+
for large rule groups without changing source-order dispatch.
28+
29+
An alternating same-host A/B against the untouched 1.1.5 build compared exact
30+
ordered output on every run. On Node 22.22.1, `jarr` improved from 127.9 ms to
31+
111.9 ms over 51 cold pairs and from 3.32 ms to 2.97 ms over 401 loaded pairs.
32+
`loowoz` improved from 892.0 ms to 754.8 ms cold and from 922.3 ms to 705.3 ms
33+
loaded.
34+
35+
The cold `jarr` comparison was also repeated on three Node majors:
36+
37+
| Runtime | 1.1.5 | 1.1.6 | Speedup |
38+
| ------------ | -------: | -------: | ------: |
39+
| Node 20.20.2 | 86.0 ms | 69.4 ms | 1.24x |
40+
| Node 22.22.1 | 127.9 ms | 111.9 ms | 1.14x |
41+
| Node 24.18.0 | 56.6 ms | 54.3 ms | 1.04x |
42+
43+
The clean 15-run subprocess comparison on an AMD Ryzen 9 9950X used PeTTa
44+
`6f5639a` on SWI-Prolog 9.2.9. Times include process startup and use the normal
45+
MeTTa TS evaluator:
46+
47+
| Program | PeTTa | MeTTa TS | Speedup |
48+
| ------------ | --------: | -------: | ------: |
49+
| BFC `jarr` | 136.5 ms | 113.6 ms | 1.20x |
50+
| BFC `loowoz` | 2466.3 ms | 743.6 ms | 3.32x |
51+
52+
Maximum sampled MeTTa TS process-tree RSS was 91.4 MiB for `jarr` and 100.0
53+
MiB for `loowoz`. The benchmark validates both `jarr` proofs and all three
54+
`loowoz` proofs in exact order.
55+
56+
## Inequality
57+
58+
MeTTa TS now provides `!=` as a core grounded operation. It is the Boolean
59+
complement of Hyperon's structural `==` for non-error operands, including
60+
integer/float promotion and NaN behavior. Both operators use the same
61+
`(-> $t $t Bool)` type, arity checks, argument evaluation, and error
62+
propagation. The TypeScript EDSL exports the matching `neq(a, b)` builder.
63+
64+
## Verification
65+
66+
The release candidate was checked on Linux with Node 22 and pnpm 11.
67+
68+
- All 116 executed test files pass: 1,164 tests passed and 38 optional live
69+
integration tests were skipped.
70+
- The checked 270-assertion oracle passes all 23 corpus files.
71+
- Core/ST conformance is byte-identical to 1.1.5 at 431 passed, 77 established
72+
failures, 60 manifest expected failures, and zero skips.
73+
- The standard benchmark, all six nondeterminism cases, concurrency checks,
74+
and all 33 scale cases pass.
75+
- The documentation site builds, and all ten package tarballs install together
76+
in a clean npm project. The packed evaluator, EDSL, `jarr`, GIF renderer, and
77+
TypeScript declarations pass their smoke checks.
78+
- Browser and grapher base entries import from a clean install without Sharp or
79+
`gifenc`. The production dependency audit reports no known vulnerabilities.
80+
81+
## Packages
82+
83+
All public packages use version `1.1.6`:
84+
85+
```bash
86+
npm install @metta-ts/core@1.1.6
87+
npm install -g @metta-ts/node@1.1.6
88+
```
89+
90+
Optional host packages use the same version:
91+
92+
```bash
93+
npm install @metta-ts/py@1.1.6 pythonia
94+
npm install @metta-ts/prolog@1.1.6
95+
```
96+
197
# MeTTa TS 1.1.5
298

399
MeTTa TS 1.1.5 adds a programmatic reduction-GIF API for plain Node.js. It

packages/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@metta-ts/browser",
3-
"version": "1.1.5",
3+
"version": "1.1.6",
44
"license": "MIT",
55
"type": "module",
66
"main": "./dist/index.js",

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@metta-ts/core",
3-
"version": "1.1.5",
3+
"version": "1.1.6",
44
"license": "MIT",
55
"type": "module",
66
"main": "./dist/index.js",

packages/core/src/builtins.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ export type ReduceResult =
7373
export type GroundFn = (args: readonly Atom[]) => ReduceResult;
7474
export type GroundingTable = Map<string, GroundFn>;
7575

76+
const groundedOperationTypes = new WeakMap<GroundFn, Atom>();
77+
78+
function withGroundedOperationType(op: GroundFn, type: Atom): GroundFn {
79+
groundedOperationTypes.set(op, type);
80+
return op;
81+
}
82+
83+
/** Return the type carried by a grounded operation, if it has one. */
84+
export function groundedOperationType(op: GroundFn): Atom | undefined {
85+
return groundedOperationTypes.get(op);
86+
}
87+
7688
// Monotonic counter for `sealed`'s fresh variable names (process-wide uniqueness, like Hyperon's make_unique).
7789
let sealCounter = 0;
7890
const ok = (...results: Atom[]): ReduceResult => ({ tag: "ok", results });
@@ -340,15 +352,23 @@ function boolBin(f: (x: boolean, y: boolean) => boolean): GroundFn {
340352
};
341353
}
342354

343-
// `==`: error operands pass through; otherwise structural equality as a Bool.
344-
const eqAtom: GroundFn = (args) => {
345-
if (args.length !== 2) return ierr("expected exactly two arguments");
346-
const a = args[0]!;
347-
const b = args[1]!;
348-
if (isErrorAtom(a)) return ok(a);
349-
if (isErrorAtom(b)) return ok(b);
350-
return ok(gbool(atomEq(a, b)));
351-
};
355+
// Equality operators pass through error operands and compare every other atom structurally.
356+
function equalityCmp(expectEqual: boolean): GroundFn {
357+
return (args) => {
358+
if (args.length !== 2) return ierr("expected exactly two arguments");
359+
const a = args[0]!;
360+
const b = args[1]!;
361+
if (isErrorAtom(a)) return ok(a);
362+
if (isErrorAtom(b)) return ok(b);
363+
return ok(gbool(atomEq(a, b) === expectEqual));
364+
};
365+
}
366+
const eqAtom = equalityCmp(true);
367+
// Hyperon carries operation types on grounded values instead of inserting declarations into &self.
368+
const neqAtom = withGroundedOperationType(
369+
equalityCmp(false),
370+
expr([sym("->"), variable("t"), variable("t"), sym("Bool")]),
371+
);
352372

353373
// --- list surgery ---
354374
const consAtom: GroundFn = (args) => {
@@ -474,6 +494,7 @@ const coreEntries: Array<[string, GroundFn]> = [
474494
[">", numCmp((c) => c > 0)],
475495
[">=", numCmp((c) => c >= 0)],
476496
["==", eqAtom],
497+
["!=", neqAtom],
477498
["and", boolBin((a, b) => a && b)],
478499
["or", boolBin((a, b) => a || b)],
479500
["cons-atom", consAtom],

packages/core/src/choice-plan.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,19 @@ function compileEquality(
163163
args: readonly Atom[],
164164
scope: ReadonlyMap<string, number>,
165165
state: CompileState,
166+
expectEqual: boolean,
166167
): ChoiceNode | undefined {
167168
const nodes = compileBinaryChoices(args, scope, state);
168169
if (nodes === undefined) return undefined;
169170
const [left, right] = nodes;
170171
if (left.one !== undefined && right.one !== undefined) {
171-
const bool: BoolNode = (frame) => atomEq(left.one!(frame), right.one!(frame));
172+
const bool: BoolNode = (frame) => atomEq(left.one!(frame), right.one!(frame)) === expectEqual;
172173
return oneChoice((frame) => gbool(bool(frame)), undefined, bool);
173174
}
174175
return {
175176
run(frame, emit) {
176177
left.run(frame, (leftAtom) =>
177-
right.run(frame, (rightAtom) => emit(gbool(atomEq(leftAtom, rightAtom)))),
178+
right.run(frame, (rightAtom) => emit(gbool(atomEq(leftAtom, rightAtom) === expectEqual))),
178179
);
179180
},
180181
};
@@ -329,7 +330,9 @@ function compileChoice(
329330
case ">=":
330331
return compileComparison(args, scope, state, (comparison) => comparison >= 0);
331332
case "==":
332-
return compileEquality(args, scope, state);
333+
return compileEquality(args, scope, state, true);
334+
case "!=":
335+
return compileEquality(args, scope, state, false);
333336
case "if":
334337
return compileIf(args, scope, state);
335338
case "superpose":

packages/core/src/compile-impure.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ describe("compiled impure body (matespace VM de-risk)", () => {
3838
expect(run(g, true)).toEqual(run(g, false));
3939
});
4040

41+
it("compiles != in an impure recursive guard", () => {
42+
const src = `
43+
(= (g-neq $n)
44+
(if (!= $n 0)
45+
(let $x (add-atom &self (item-neq $n)) (g-neq (- $n 1)))
46+
done))
47+
!(g-neq 4)
48+
!(collapse (match &self (item-neq $k) $k))`;
49+
expect(compiledEnvWith(src).compiled!.get("g-neq")?.kind).toBe("imperative");
50+
expect(run(src, true)).toEqual(run(src, false));
51+
});
52+
4153
// A longer completing run: every looped application must advance the fresh-variable counter in lockstep
4254
// with the interpreter, and the 50 add-atoms must accumulate in the same order, across 50 iterations.
4355
const g50 = `

packages/core/src/compile.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,32 @@ describe("deterministic-core compiler", () => {
9393
expect(runFunctional(c, "odd", [10])).toBe(false);
9494
});
9595

96+
it("compiles != as the complement of integer equality", () => {
97+
const c = compileEnv(envWith("(= (different $a $b) (!= $a $b))"));
98+
expect(runFunctional(c, "different", [4, 4])).toBe(false);
99+
expect(runFunctional(c, "different", [4, 5])).toBe(true);
100+
});
101+
96102
it("a match-using function is outside the pure int/bool core but compiles as nondet", () => {
97103
const c = compileEnv(envWith("(= (q $x) (match &self ($x) $x))"));
98104
expect(c.get("q")?.kind).toBe("nondet");
99105
});
100106

107+
it("compiles != guards in nondeterministic recursion", () => {
108+
const src = `
109+
(= (walk 0) left)
110+
(= (walk 0) right)
111+
(= (walk $n)
112+
(if (!= $n 0)
113+
(let $child (walk (- $n 1)) (S $child))
114+
(empty)))`;
115+
const c = compileEnv(envWith(src));
116+
expect(c.get("walk")?.kind).toBe("nondet");
117+
expect(compareCompiledAndInterpreted(`${src}\n!(walk 2)`)).toEqual([
118+
["(S (S left))", "(S (S right))"],
119+
]);
120+
});
121+
101122
it("declines a nondeterministic group with a wrong-arity internal call", () => {
102123
const c = compileEnv(
103124
envWith(`

packages/core/src/compile.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ const KNOWN_OPS = new Set([
189189
">",
190190
">=",
191191
"==",
192+
"!=",
192193
"if",
193194
"unify",
194195
"let",
@@ -288,7 +289,7 @@ function compileBody(a: Atom, scope: Scope, holders: FunctionalFns): Compiled |
288289
type: "int",
289290
};
290291
}
291-
if (op === "<" || op === "<=" || op === ">" || op === ">=" || op === "==") {
292+
if (op === "<" || op === "<=" || op === ">" || op === ">=" || op === "==" || op === "!=") {
292293
const xy = binIntArgs(args, scope, holders);
293294
if (!xy) return undefined;
294295
const [xn, yn] = xy;
@@ -301,7 +302,9 @@ function compileBody(a: Atom, scope: Scope, holders: FunctionalFns): Compiled |
301302
? (c: number) => c > 0
302303
: op === ">="
303304
? (c: number) => c >= 0
304-
: (c: number) => c === 0;
305+
: op === "=="
306+
? (c: number) => c === 0
307+
: (c: number) => c !== 0;
305308
return { node: (fr) => test(cmpIntVal(xn(fr), yn(fr))), type: "bool" };
306309
}
307310
if (op === "if") {
@@ -554,7 +557,8 @@ function inferType(
554557
: undefined;
555558
const op = (hd as { name: string }).name;
556559
if (op === "+" || op === "-" || op === "*" || op === "/" || op === "%") return "int";
557-
if (op === "<" || op === "<=" || op === ">" || op === ">=" || op === "==") return "bool";
560+
if (op === "<" || op === "<=" || op === ">" || op === ">=" || op === "==" || op === "!=")
561+
return "bool";
558562
if (op === "if" && a.items.length === 4) {
559563
const tt = inferType(a.items[2]!, varTypes, holders);
560564
const te = inferType(a.items[3]!, varTypes, holders);
@@ -1062,7 +1066,7 @@ function nondetIsData(env: MinEnv, a: Atom): boolean {
10621066
// comparison operators an `(if ...)` guard may test. Both evaluate at runtime through the interpreter's
10631067
// own grounded ops (callGrounded), so the compiled result is byte-identical to the interpreted one.
10641068
const ARITH_FOLD = new Set(["+", "-", "*"]);
1065-
const NONDET_COMPARE = new Set(["<", "<=", ">", ">=", "=="]);
1069+
const NONDET_COMPARE = new Set(["<", "<=", ">", ">=", "==", "!="]);
10661070

10671071
/** A call argument or result template the search can build and resolve: interpreter data, but also
10681072
* binary arithmetic (`(- $s 2)`, `(+ (+ $fs $xs) 1)`) anywhere inside, folded to an int once its
@@ -1810,6 +1814,8 @@ function compileNondetGroup(
18101814
return c >= 0;
18111815
case "==":
18121816
return c === 0;
1817+
case "!=":
1818+
return c !== 0;
18131819
default:
18141820
throw BAIL;
18151821
}
@@ -1983,7 +1989,11 @@ function compileNondetGroup(
19831989
}
19841990
};
19851991

1986-
return attempt("direct") ?? attempt("frontier");
1992+
return (
1993+
(jg.tryDeferred === undefined ? undefined : attempt("deferred")) ??
1994+
attempt("direct") ??
1995+
attempt("frontier")
1996+
);
19871997
};
19881998

19891999
const holders = new Map<string, NondetHolder>();
@@ -2038,7 +2048,7 @@ type ImpNode = (
20382048
) => ImpEval;
20392049
type ImperativeFns = Map<string, ImperativeHolder>;
20402050

2041-
const IMP_GROUNDED = new Set(["==", "<", ">", "<=", ">=", "+", "-", "*", "%"]);
2051+
const IMP_GROUNDED = new Set(["==", "!=", "<", ">", "<=", ">=", "+", "-", "*", "%"]);
20422052
// Heads that are never inert data: the compiled language's own constructs, plus every evaluation
20432053
// op (IMPURE_OPS: match, collapse, once, superpose, metta, ...). Without the latter, a body like
20442054
// `(match &self p t)` whose head happens to have no rule and no grounding would freeze as a tuple,

0 commit comments

Comments
 (0)