@metta-ts/node 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MesTTo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @metta-ts/node
2
+
3
+ Node.js entry for [MeTTa TS](https://github.com/MesTTo/Meta-TypeScript-Talk): the `metta-ts` command-line runner, file-based `import!`, and a `SharedArrayBuffer` worker-thread parallel matcher. Re-exports everything from [`@metta-ts/core`](https://github.com/MesTTo/Meta-TypeScript-Talk/tree/main/packages/core).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @metta-ts/node
9
+ # for the CLI on your PATH:
10
+ npm install -g @metta-ts/node
11
+ ```
12
+
13
+ ## CLI
14
+
15
+ ```bash
16
+ metta-ts path/to/program.metta
17
+ # or without a global install:
18
+ npx -p @metta-ts/node metta-ts path/to/program.metta
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```ts
24
+ import { runFile, ParallelFlatMatcher } from "@metta-ts/node";
25
+
26
+ // Run a .metta file (resolves import! against the file system).
27
+ for (const { query, results } of runFile("program.metta")) {
28
+ console.log(query, results);
29
+ }
30
+ ```
31
+
32
+ `ParallelFlatMatcher` scans a large flat knowledge base across `worker_threads` over a shared token buffer. It pays off only for a large KB scanned by a non-selective query whose result set is small; a keyed query is already near-constant-time via the in-memory argument index.
33
+
34
+ ## License
35
+
36
+ [MIT](https://github.com/MesTTo/Meta-TypeScript-Talk/blob/main/LICENSE).
@@ -0,0 +1,250 @@
1
+ // src/index.ts
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { resolve, dirname, sep } from "path";
4
+ import {
5
+ parseAll,
6
+ standardTokenizer,
7
+ evalSequential,
8
+ collectImports,
9
+ DEFAULT_FUEL
10
+ } from "@metta-ts/core";
11
+
12
+ // src/par-hyperpose.ts
13
+ import { Worker } from "worker_threads";
14
+ import { createRequire } from "module";
15
+ var WORKER_SRC = `
16
+ const { workerData } = require("node:worker_threads");
17
+ const { corePath, rulesSrc, branchSrc, sab, base, cap, fuel } = workerData;
18
+ const view = new Int32Array(sab);
19
+ (async () => {
20
+ try {
21
+ const m = await import(corePath);
22
+ const r = m.runProgram(rulesSrc + "\\n!" + branchSrc, fuel);
23
+ const last = r[r.length - 1];
24
+ const out = JSON.stringify((last && last.results ? last.results : []).map((a) => m.format(a)));
25
+ if (out.length > cap) { Atomics.store(view, base, -1); }
26
+ else {
27
+ for (let i = 0; i < out.length; i++) view[base + 2 + i] = out.charCodeAt(i);
28
+ Atomics.store(view, base + 1, out.length);
29
+ Atomics.store(view, base, 1);
30
+ }
31
+ } catch (e) {
32
+ Atomics.store(view, base, -1); // import failure, fuel exhaustion, anything: this branch bailed
33
+ } finally {
34
+ // Always signal completion, even on a caught failure, so the main thread's Atomics.wait cannot block
35
+ // forever waiting for a branch that already errored.
36
+ Atomics.add(view, 0, 1);
37
+ Atomics.notify(view, 0);
38
+ }
39
+ })();
40
+ `;
41
+ var CAP = 16384;
42
+ function evalBranchesParallel(corePath, rulesSrc, branchSrcs, firstOnly, fuel) {
43
+ const n = branchSrcs.length;
44
+ const region = 2 + CAP;
45
+ const sab = new SharedArrayBuffer((1 + n * region) * 4);
46
+ const view = new Int32Array(sab);
47
+ const workers = branchSrcs.map(
48
+ (br, i) => new Worker(WORKER_SRC, {
49
+ eval: true,
50
+ workerData: {
51
+ corePath,
52
+ rulesSrc,
53
+ branchSrc: br,
54
+ sab,
55
+ base: 1 + i * region,
56
+ cap: CAP,
57
+ fuel
58
+ }
59
+ })
60
+ );
61
+ const out = new Array(n).fill(null);
62
+ const read = (i) => {
63
+ const base = 1 + i * region;
64
+ const status = Atomics.load(view, base);
65
+ if (status === 0) return void 0;
66
+ if (status === -1) return null;
67
+ const len = Atomics.load(view, base + 1);
68
+ let s = "";
69
+ for (let j = 0; j < len; j++) s += String.fromCharCode(view[base + 2 + j]);
70
+ return JSON.parse(s);
71
+ };
72
+ const WAIT_MS = 6e4;
73
+ let done = false;
74
+ while (!done) {
75
+ const prev = Atomics.load(view, 0);
76
+ for (let i = 0; i < n; i++) {
77
+ if (out[i] !== null) continue;
78
+ const r = read(i);
79
+ if (r === void 0) continue;
80
+ out[i] = r;
81
+ if (firstOnly && r !== null && r.length > 0) {
82
+ done = true;
83
+ break;
84
+ }
85
+ }
86
+ if (Atomics.load(view, 0) >= n) done = true;
87
+ if (!done && Atomics.wait(view, 0, prev, WAIT_MS) === "timed-out" && Atomics.load(view, 0) === prev)
88
+ break;
89
+ }
90
+ for (const w of workers) void w.terminate();
91
+ return out;
92
+ }
93
+ function makeParEvalImpl(fuel) {
94
+ const corePath = createRequire(import.meta.url).resolve("@metta-ts/core");
95
+ return (rulesSrc, branchSrcs, firstOnly) => evalBranchesParallel(corePath, rulesSrc, branchSrcs, firstOnly, fuel);
96
+ }
97
+
98
+ // src/index.ts
99
+ export * from "@metta-ts/core";
100
+
101
+ // src/flat-parallel.ts
102
+ import { Worker as Worker2 } from "worker_threads";
103
+ import { availableParallelism } from "os";
104
+ import { encodePattern, decodeAt } from "@metta-ts/core";
105
+ var WORKER_SRC2 = `
106
+ const { parentPort, workerData } = require("node:worker_threads");
107
+ const tokens = new Int32Array(workerData.tokensSAB);
108
+ const offsets = new Int32Array(workerData.offsetsSAB);
109
+ const counter = new Int32Array(workerData.counterSAB);
110
+ const numFacts = workerData.numFacts;
111
+ const T_ARITY = 0, T_SYMBOL = 1, T_NEWVAR = 2, T_VARREF = 3;
112
+ const tg = (t) => (t >>> 28) & 0xf, pl = (t) => t & 0x0fffffff;
113
+ function subLen(tk, pos) {
114
+ const t = tk[pos];
115
+ if (tg(t) === T_ARITY) { let l = 1, p = pos + 1; for (let i = 0; i < pl(t); i++) { const x = subLen(tk, p); l += x; p += x; } return l; }
116
+ return 1;
117
+ }
118
+ function rangeEq(tk, as, bs) {
119
+ const l = subLen(tk, as); if (l !== subLen(tk, bs)) return false;
120
+ for (let i = 0; i < l; i++) if (tk[as + i] !== tk[bs + i]) return false; return true;
121
+ }
122
+ function matchAt(pat, fact, fs) {
123
+ const binds = []; let vc = 0, pp = 0, fp = fs;
124
+ function go() {
125
+ const pt = pat[pp], ptag = tg(pt);
126
+ if (ptag === T_NEWVAR) { const l = subLen(fact, fp); binds[vc++] = [fp, fp + l]; pp++; fp += l; return true; }
127
+ if (ptag === T_VARREF) { const ref = binds[pl(pt)]; if (!ref) return false; if (!rangeEq(fact, ref[0], fp)) return false; pp++; fp += subLen(fact, fp); return true; }
128
+ if (ptag === T_SYMBOL) { if (fact[fp] !== pt) return false; pp++; fp++; return true; }
129
+ if (fact[fp] !== pt) return false; const n = pl(pt); pp++; fp++;
130
+ for (let i = 0; i < n; i++) if (!go()) return false; return true;
131
+ }
132
+ return go() ? binds : null;
133
+ }
134
+ parentPort.on("message", (pat) => {
135
+ const out = [];
136
+ for (;;) {
137
+ const idx = Atomics.add(counter, 0, 1);
138
+ if (idx >= numFacts) break;
139
+ const binds = matchAt(pat, tokens, offsets[idx]);
140
+ if (binds) out.push(binds); // binds: array indexed by de Bruijn id -> [start, end)
141
+ }
142
+ parentPort.postMessage(out);
143
+ });
144
+ `;
145
+ var ParallelFlatMatcher = class {
146
+ kb;
147
+ tokens;
148
+ counter;
149
+ numFacts;
150
+ workers;
151
+ // All workers share one work-stealing counter, so calls must not overlap; serialize them on a queue.
152
+ queue = Promise.resolve();
153
+ constructor(kb, workerCount = Math.max(1, availableParallelism() - 1)) {
154
+ this.kb = kb;
155
+ const toks = kb.tokenArray;
156
+ const offs = kb.factOffsets;
157
+ this.numFacts = offs.length;
158
+ const tokensSAB = new SharedArrayBuffer(toks.length * 4);
159
+ this.tokens = new Int32Array(tokensSAB);
160
+ this.tokens.set(toks);
161
+ const offsetsSAB = new SharedArrayBuffer(Math.max(1, offs.length) * 4);
162
+ new Int32Array(offsetsSAB).set(offs);
163
+ const counterSAB = new SharedArrayBuffer(4);
164
+ this.counter = new Int32Array(counterSAB);
165
+ this.workers = Array.from(
166
+ { length: Math.max(1, workerCount) },
167
+ () => new Worker2(WORKER_SRC2, {
168
+ eval: true,
169
+ workerData: { tokensSAB, offsetsSAB, counterSAB, numFacts: this.numFacts }
170
+ })
171
+ );
172
+ }
173
+ /** Match `pattern` across the KB in parallel. Returns one binding map (variable name -> atom) per
174
+ * match, identical (up to order) to the single-threaded `FlatKB.match`. Concurrent calls are
175
+ * serialized internally (the workers share one counter), so it is safe to call without awaiting. */
176
+ async match(pattern) {
177
+ const run = this.queue.then(() => this.matchOne(pattern));
178
+ this.queue = run.catch(() => void 0);
179
+ return run;
180
+ }
181
+ async matchOne(pattern) {
182
+ const enc = encodePattern(pattern, this.kb.interner);
183
+ if (enc === null) return [];
184
+ Atomics.store(this.counter, 0, 0);
185
+ const replies = this.workers.map(
186
+ (w) => (
187
+ // Reject on worker error or early exit, so a crashing worker fails the match instead of leaving
188
+ // `Promise.all` and the serialized queue hung forever. Remove all listeners when any fires so a
189
+ // long-lived worker does not accumulate them across calls.
190
+ new Promise((res, rej) => {
191
+ const onMsg = (m) => (cleanup(), res(m));
192
+ const onErr = (e) => (cleanup(), rej(e));
193
+ const onExit = (code) => (cleanup(), rej(new Error(`match worker exited (${code})`)));
194
+ const cleanup = () => {
195
+ w.off("message", onMsg);
196
+ w.off("error", onErr);
197
+ w.off("exit", onExit);
198
+ };
199
+ w.on("message", onMsg);
200
+ w.on("error", onErr);
201
+ w.on("exit", onExit);
202
+ })
203
+ )
204
+ );
205
+ for (const w of this.workers) w.postMessage(enc.tokens);
206
+ const perWorker = await Promise.all(replies);
207
+ const out = [];
208
+ for (const matches of perWorker)
209
+ for (const ranges of matches) {
210
+ const m = /* @__PURE__ */ new Map();
211
+ ranges.forEach(
212
+ (r, idx) => m.set(enc.varNames[idx], decodeAt(this.tokens, r[0], this.kb.interner)[0])
213
+ );
214
+ out.push(m);
215
+ }
216
+ return out;
217
+ }
218
+ /** Terminate the worker pool. */
219
+ async close() {
220
+ await Promise.all(this.workers.map((w) => w.terminate()));
221
+ }
222
+ };
223
+
224
+ // src/index.ts
225
+ function readImports(src, baseDir) {
226
+ const m = /* @__PURE__ */ new Map();
227
+ const base = resolve(baseDir);
228
+ for (const name of collectImports(src)) {
229
+ const p = resolve(base, name.endsWith(".metta") ? name : name + ".metta");
230
+ if (p !== base && !p.startsWith(base + sep)) continue;
231
+ if (existsSync(p))
232
+ m.set(
233
+ name,
234
+ parseAll(readFileSync(p, "utf8"), standardTokenizer()).filter((t) => !t.bang).map((t) => t.atom)
235
+ );
236
+ }
237
+ return m;
238
+ }
239
+ function runFile(path, fuel, opts) {
240
+ const src = readFileSync(path, "utf8");
241
+ const tops = parseAll(src, standardTokenizer());
242
+ const withPar = opts?.parEvalImpl === void 0 ? { ...opts, parEvalImpl: makeParEvalImpl(fuel ?? DEFAULT_FUEL) } : opts;
243
+ return evalSequential(tops, fuel, readImports(src, dirname(resolve(path))), withPar);
244
+ }
245
+
246
+ export {
247
+ ParallelFlatMatcher,
248
+ readImports,
249
+ runFile
250
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runFile
4
+ } from "./chunk-FIH2Z2UY.js";
5
+
6
+ // src/cli.ts
7
+ import { parseArgs } from "util";
8
+ import { spawnSync } from "child_process";
9
+ import { fileURLToPath } from "url";
10
+ import { format } from "@metta-ts/core";
11
+ function reexecWithLargerStack() {
12
+ if (process.env.METTA_TS_STACK !== void 0) return;
13
+ const res = spawnSync(
14
+ process.execPath,
15
+ ["--stack-size=8000", fileURLToPath(import.meta.url), ...process.argv.slice(2)],
16
+ { stdio: "inherit", env: { ...process.env, METTA_TS_STACK: "1" } }
17
+ );
18
+ process.exit(res.status ?? 1);
19
+ }
20
+ function main() {
21
+ reexecWithLargerStack();
22
+ const { positionals, values } = parseArgs({
23
+ allowPositionals: true,
24
+ options: {
25
+ "max-steps": { type: "string" },
26
+ "max-stack-depth": { type: "string" }
27
+ }
28
+ });
29
+ const file = positionals[0];
30
+ if (file === void 0) {
31
+ process.stderr.write("usage: metta-ts [--max-steps=N] [--max-stack-depth=N] <file.metta>\n");
32
+ process.exit(2);
33
+ }
34
+ const fuel = values["max-steps"] !== void 0 ? Number(values["max-steps"]) : void 0;
35
+ const maxStackDepth = values["max-stack-depth"] !== void 0 ? Number(values["max-stack-depth"]) : void 0;
36
+ for (const r of runFile(
37
+ file,
38
+ fuel,
39
+ maxStackDepth !== void 0 ? { maxStackDepth } : void 0
40
+ )) {
41
+ process.stdout.write("[" + r.results.map(format).join(", ") + "]\n");
42
+ }
43
+ }
44
+ main();
@@ -0,0 +1,29 @@
1
+ import { FlatKB, Atom, RunOptions, QueryResult } from '@metta-ts/core';
2
+ export * from '@metta-ts/core';
3
+
4
+ /** A worker-pool, SharedArrayBuffer-backed parallel matcher over a {@link FlatKB}. Build it once from a
5
+ * KB, reuse the warm pool across queries, and `close()` when done. */
6
+ declare class ParallelFlatMatcher {
7
+ private readonly kb;
8
+ private readonly tokens;
9
+ private readonly counter;
10
+ private readonly numFacts;
11
+ private readonly workers;
12
+ private queue;
13
+ constructor(kb: FlatKB, workerCount?: number);
14
+ /** Match `pattern` across the KB in parallel. Returns one binding map (variable name -> atom) per
15
+ * match, identical (up to order) to the single-threaded `FlatKB.match`. Concurrent calls are
16
+ * serialized internally (the workers share one counter), so it is safe to call without awaiting. */
17
+ match(pattern: Atom): Promise<Array<Map<string, Atom>>>;
18
+ private matchOne;
19
+ /** Terminate the worker pool. */
20
+ close(): Promise<void>;
21
+ }
22
+
23
+ /** Pre-read every `import!` target referenced in `src`, resolving names against `baseDir`. */
24
+ declare function readImports(src: string, baseDir: string): Map<string, Atom[]>;
25
+ /** Run a `.metta` file from disk, resolving `import!` relative to the file's directory. `fuel` is the step
26
+ * ceiling; `opts` carries interpreter settings such as the initial `maxStackDepth`. */
27
+ declare function runFile(path: string, fuel?: number, opts?: RunOptions): QueryResult[];
28
+
29
+ export { ParallelFlatMatcher, readImports, runFile };
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import {
2
+ ParallelFlatMatcher,
3
+ readImports,
4
+ runFile
5
+ } from "./chunk-FIH2Z2UY.js";
6
+ export {
7
+ ParallelFlatMatcher,
8
+ readImports,
9
+ runFile
10
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@metta-ts/node",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "metta-ts": "./dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "dependencies": {
21
+ "@metta-ts/core": "1.0.0"
22
+ },
23
+ "author": "MesTTo",
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "sideEffects": false,
28
+ "module": "./dist/index.js",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "description": "Node.js entry for MeTTa TS: the metta-ts CLI, file import!, and a worker-thread parallel matcher.",
33
+ "keywords": [
34
+ "metta",
35
+ "hyperon",
36
+ "cli",
37
+ "atomspace",
38
+ "typescript"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/MesTTo/Meta-TypeScript-Talk.git",
43
+ "directory": "packages/node"
44
+ },
45
+ "homepage": "https://github.com/MesTTo/Meta-TypeScript-Talk#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/MesTTo/Meta-TypeScript-Talk/issues"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean"
51
+ }
52
+ }