Skip to content

MesTTo/Meta-TypeScript-Talk

Repository files navigation

MeTTa TS

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.

Install

npm install @metta-ts/core        # the interpreter (works in any JS runtime)
# or: pnpm add @metta-ts/core  /  yarn add @metta-ts/core

Other 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 system

For 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.metta

Quick start

Run 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.

Calling TypeScript from MeTTa

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.

Calling into JavaScript

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 it

Async MeTTa

MeTTa 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));

Concurrency and parallelism

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))))   ; -> True

Ergonomic typed eDSL

For 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) === 720

More 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.

Connecting to a Distributed AtomSpace

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 snake

This 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.

What is implemented

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

Packages

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.

Performance

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.

Head-to-head with PeTTa

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 case over 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.

Provenance

  • 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.

License

MIT.

About

MeTTa TS — a pure-TypeScript MeTTa (Hyperon) that runs anywhere: browser, Node, Deno, Bun, edge, and TS AI agents.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors