A pure-TypeScript implementation of MeTTa (Meta Type Talk), the OpenCog Hyperon language. It runs anywhere TypeScript runs: the browser, Node, Deno, Bun, edge and serverless functions, and inside TypeScript-based AI agents. No native addons, no WASM, no Rust.
npm install @metta-ts/core # the interpreter (works in any JS runtime)
# or: pnpm add @metta-ts/core / yarn add @metta-ts/coreOther packages, add as needed:
npm install @metta-ts/hyperon # a Python-hyperon-style class API
npm install @metta-ts/node # CLI + file import! + a parallel matcher
npm install @metta-ts/browser # web entry + in-memory virtual file systemFor the command-line runner, install @metta-ts/node globally (or use npx):
npm install -g @metta-ts/node
metta-ts path/to/program.metta
# without a global install:
npx -p @metta-ts/node metta-ts path/to/program.mettaRun MeTTa source from TypeScript with the core package:
import { runProgram, format } from "@metta-ts/core";
const results = runProgram(`
(= (fact $n) (unify $n 0 1 (* $n (fact (- $n 1)))))
!(fact 5)
`);
for (const { query, results: rs } of results) {
console.log(format(query), "=>", rs.map(format));
}
// (fact 5) => [ '120' ]runProgram parses the source, adds every non-bang atom to the knowledge base, evaluates each !-query, and returns one result group per query.
The @metta-ts/hyperon package is a class API modeled on Python's hyperon, but TypeScript-native: no Python, no Rust, no FFI. A grounded operation is a TypeScript function the evaluator can call by name.
import { MeTTa, ValueAtom, type GroundedAtom, type Atom } from "@metta-ts/hyperon";
const metta = new MeTTa();
metta.registerOperation("double", (args: Atom[]) => {
const n = (args[0] as GroundedAtom).jsValue<number>();
return [ValueAtom(n * 2)];
});
console.log(metta.run("!(double 21)")[0].map(String)); // [ '42' ]A thrown error becomes a MeTTa (Error ...) atom the program can inspect, rather than crashing the run.
Grounded operations let MeTTa call functions you register by name. The interop layer goes one step further: it lets MeTTa reach into the host runtime itself, calling global functions and methods and building JavaScript values, with no glue code. Enable it with registerJsInterop.
import { MeTTa, registerJsInterop } from "@metta-ts/hyperon";
const metta = new MeTTa();
registerJsInterop(metta);
metta.run(`!((js-atom "Math.max") 3 7 2)`); // [ '7' ] resolve and call a global
metta.run(`!((js-dot "hello world" "toUpperCase"))`); // [ '"HELLO WORLD"' ] call a method on a value
metta.run(`!((js-dot (js-list (5 1 3)) "join") "-")`); // [ '"5-1-3"' ] build a JS array, then join itMeTTa can be asynchronous. A grounded operation can do I/O (a fetch, a database query, a timer) and the evaluator awaits it. Register it with registerAsyncOperation and run with runAsync. A synchronous program gives identical results either way.
import { MeTTa, ValueAtom } from "@metta-ts/hyperon";
const metta = new MeTTa();
metta.registerAsyncOperation("fetch-temperature", async () => {
const res = await fetch("https://example.com/temp"); // any real I/O
return [ValueAtom(await res.json())];
});
const out = await metta.runAsync("!(fetch-temperature)");
console.log(out[0].map(String));Because the host is JavaScript, MeTTa branches can overlap real I/O and, for CPU-bound work, run across cores. par evaluates branches concurrently, race returns the first to finish and cancels the losers, with-mutex serialises a critical section, and transaction commits a body's space mutations only on success.
import { MeTTa, type GroundedAtom } from "@metta-ts/hyperon";
const metta = new MeTTa();
metta.registerAsyncOperation("aw", async (args) => {
await new Promise((r) => setTimeout(r, (args[0] as GroundedAtom).jsValue<number>()));
return [args[0]];
});
// race: the 3 ms branch wins; the 40 ms branch is cancelled
console.log((await metta.runAsync("!(race (aw 40) (aw 3))"))[0].map(String)); // [ '3' ](once (hyperpose …)) goes further: on the Node runner it evaluates the branches on worker threads, so synchronous compiled loops run on separate CPU cores. Run it with the CLI (metta-ts primes.metta) and the one cheap branch settles first, before the expensive ones finish:
!(once (hyperpose ((prime? 535372570000000063) ; expensive
(prime? 5421844300001) ; cheap
(prime? 547344310000000013)))) ; -> TrueFor writing MeTTa in idiomatic TypeScript, @metta-ts/edsl mints symbols, functors, and logic variables from proxies (names(), vars()), builds the special forms with capitalized combinators (If, Case, Match, arithmetic, ...) or a tagged template, and bridges TypeScript functions in both directions. It builds ordinary atoms and runs on the same engine, so you get MeTTa's full semantics: rewrite rules, nondeterminism, pattern matching, and types. Any TypeScript value drops in as a grounded atom automatically.
import { mettaDB, names, vars, If, gt, mul, sub, m } from "@metta-ts/edsl";
const db = mettaDB();
// `names()` mints symbols and functors, `vars()` mints logic variables. No name is written twice:
// the JS binding IS the name. A bare name grounds to its symbol; a called name applies it.
const { Likes, fact, Ada, Coffee, Chocolate } = names();
const { thing, x } = vars();
// Facts + a match query. With no explicit vars, the row keys are inferred from the pattern.
db.add(Likes(Ada, Coffee), Likes(Ada, Chocolate));
db.query(Likes(Ada, thing)); // [{ thing: "Coffee" }, { thing: "Chocolate" }]
// Recursive rewrite rule + grounded arithmetic.
db.rule(fact(x), If(gt(x, 0), mul(x, fact(sub(x, 1))), 1));
db.evalJs(fact(5)); // [120]
// Grounded functions, both directions: a plain typed function in, a MeTTa function out.
db.fn("balance-of", (a: { balance: number }) => a.balance);
db.evalJs(m`(balance-of ${{ owner: "Tom", balance: 100 }})`); // [100]
db.call.fact(5); // [120]
const factorial = db.import<[number], number>("fact"); // typed callable, factorial(6) === 720More runnable examples are in examples/: quickstart.ts, grounded-ops.ts, async.ts, edsl.ts, plus .metta source files. Run one with npx tsx examples/quickstart.ts.
A space does not have to be in memory. @metta-ts/das-client connects to SingularityNET's Distributed AtomSpace (DAS) (singnet/das), a remote, shared atomspace, and presents it as a Space you query like any other. A DAS query is a network round-trip, so it is asynchronous; matchAsync is the async analogue of (match space pattern template).
import { DasLiveSpace, matchAsync } from "@metta-ts/das-client";
import { sym, expr, variable } from "@metta-ts/core";
const A = (...xs) => expr(xs);
// connect to a running DAS (a Query Agent over gRPC)
const das = new DasLiveSpace(/* connection */);
// "which concepts are animals?" against the remote knowledge base
const animals = await matchAsync(
das,
A(sym("EVALUATION"), A(sym("PREDICATE"), sym("is_animal")), A(sym("CONCEPT"), variable("C"))),
variable("C"),
);
console.log(animals.map(String));
// monkey human triceratops earthworm chimp ent rhino snakeThis has been run end to end against a live DAS cluster (see @metta-ts/das-client for the setup). The same atom handles MeTTa TS computes match the AtomDB byte for byte, so a TypeScript program, in Node today and the browser through @metta-ts/das-gateway, can query the same distributed knowledge base the Rust and Python agents use.
A faithful port of hyperon-experimental's minimal interpreter (the nondeterministic stack machine), with the standard library loaded as MeTTa source on top. The core passes all 270 assertions of Hyperon's oracle corpus: the full dependent-type tier (GADTs, dependent types, types-as-propositions), spaces and mutable state, nondeterminism, grounded operations, and documentation. Correctness is also cross-checked against LeaTTa, the machine-checked (Lean 4) MeTTa semantics, pinned to the same commit.
Beyond the core: transactions, async evaluation, concurrency primitives (par, race, once, hyperpose, with-mutex), clause indexing that scales matching to millions of atoms, a flat interned knowledge base with a worker-thread parallel matcher, and a JavaScript interop layer (js-atom, js-dot, js-list, js-dict) that calls into the host runtime directly.
The whole thing is pure TypeScript. The core builds to a single ESM bundle (~23 KB gzipped) that runs in Node and the browser with no native addon and no WASM.
pnpm install
pnpm build
pnpm test # 270/270 Hyperon oracle gate + unit and property tests
node packages/node/dist/cli.js examples/factorial.metta| Package | What it is |
|---|---|
@metta-ts/core |
The interpreter, parser, type system, and standard library. Zero platform dependencies. |
@metta-ts/hyperon |
A TypeScript class API over the core, modeled on Python's hyperon. |
@metta-ts/edsl |
An ergonomic, typed eDSL: term builders, special-form combinators, and a tagged template. |
@metta-ts/node |
The metta-ts CLI, file import!, and a SharedArrayBuffer worker-thread parallel matcher. |
@metta-ts/browser |
Browser entry point with an in-memory virtual file system for import!. |
@metta-ts/das-client |
Optional client to SingularityNET's Distributed AtomSpace via a Connect gateway. |
Pure TypeScript throughout, no escape to native code. The interpreter uses a precomputed-ground short-circuit, structural sharing in substitution, a cons-list instruction stack, and Prolog-style clause indexing (by head functor and by every ground-leaf argument position). A functor-and-argument-keyed query over a 1,000,000-atom knowledge base resolves in about 0.2 to 1.4 ms. See packages/node/bench/RESULTS.md for the full benchmark log.
A reproducible benchmark (packages/node/bench/corpus-bench.mjs) runs the PeTTa example corpus through both engines as subprocesses and checks each program's embedded (test …) assertions. On the Hyperon-faithful subset (host-FFI examples and PeTTa-only execution-model examples are excluded, with the reason recorded for each), MeTTa TS passes 97 of the shared programs and is faster than PeTTa on all 97, median ~2x, on SWI-Prolog's GMP-backed integers, from pure TypeScript.
A representative slice (wall-clock, subprocess including startup; speedup = PeTTa / MeTTa TS):
| Program | PeTTa | MeTTa TS | Speedup |
|---|---|---|---|
peano |
1692 ms | 220 ms | 7.7× |
fib |
456 ms | 79 ms | 5.8× |
fibadd |
459 ms | 84 ms | 5.5× |
peanofast |
520 ms | 112 ms | 4.6× |
tilepuzzle |
1554 ms | 402 ms | 3.9× |
he_minimalmetta |
1807 ms | 484 ms | 3.7× |
matespacefast |
4258 ms | 1890 ms | 2.3× |
factorial |
166 ms | 76 ms | 2.2× |
permutations |
889 ms | 451 ms | 2.0× |
nilbc |
713 ms | 399 ms | 1.8× |
hyperpose_primes |
1100 ms | 1009 ms | 1.1× |
The full per-program table is in RESULTS-corpus.md.
That speed comes from general engine work:
- an O(1)-stack reduce-loop trampoline;
- a Set-based (O(n)) variable/binding path;
- deferred rule-RHS freshening with a head-shape candidate pre-filter;
- an O(1)-stack worklist for nondeterminism;
- ground-atom type memoisation;
- an exact-match ground-fact index;
- automatic tabling of pure functions, including ones defined at runtime (via rule-set-versioned keys);
- 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; - 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; - a compiler for add-atom saturation loops: the add-if-absent idiom becomes one exact-membership probe plus append, and a single-branch
caseover a space match becomes a snapshot-and-thread loop with Empty-pruned branches.
Every one of these is verified against the 270-assertion Hyperon oracle and the LeaTTa differential; all are byte-identical except the nondeterministic compiler, whose results are alpha-equivalent (fresh variables get different gensym numbers, consistently renamed, deterministic run to run).
The last holdouts fell in order. permutations is a 28-relation conjunctive (length (collapse (match &self (, …) …))): MeTTa TS folds the worst-case-optimal join and counts each solution rather than materialising the ~360k answer atoms, which drops it from 3.6 s to 0.45 s, under PeTTa's 0.85 s. hyperpose_primes races (once (hyperpose …)) across Node worker threads. nilbc is a dependently-typed backward chainer: compiling its clauses to a collect-all search with the interpreter's own unification takes it from 2.2 s to 0.40 s, under PeTTa's 0.71 s. peano, the final one, is an impure dedup-build loop: compiling its saturation step (a case over the space with add-if-absent branches) to membership probes on the exact-match index takes it from 2.7 s to 0.22 s, under PeTTa's 1.69 s. The remaining parity work (PLN/NARS library ports, PeTTa-only execution-model examples) is tracked in packages/node/bench/TODO-parity.md.
matespace/matespace2 are PeTTa-specific and excluded from the faithful subset. Their expected counts, 1063919 and 1297533, are produced only by PeTTa's compilation to Prolog: native backtracking over a globally-persistent atomspace, with duplicate adds pruned by failure, which is not minimal-MeTTa semantics. Run through hyperon-experimental itself, (collapse (mate-space-demo K)) is empty, and LeaTTa agrees. PeTTa, real Hyperon, and MeTTa TS each compute a different result for the same program, so no Hyperon-faithful engine reproduces PeTTa's number. The faithful rewrite of the same workload is matespacefast, which uses deterministic tuple recursion instead of a case-driven non-deterministic build. MeTTa TS runs it about 2.0× faster than PeTTa, byte-identical.
- Semantics: hyperon-experimental, pinned to commit
3f76dc4. - Verified spec and differential oracle: LeaTTa (Lean 4).
- Distributed AtomSpace: optional client to SingularityNET DAS via a Connect gateway (Node), reachable from the browser.
MIT.