The typed eDSL
@metta-ts/edsl lets you write MeTTa in idiomatic, typed TypeScript instead of source strings. It is a thin layer over the engine: every builder produces an ordinary atom, so you get MeTTa's full semantics: rewrite rules, nondeterminism, pattern matching, and types.
npm install @metta-ts/edslA first taste
import { mettaDB, names, vars, If, gt, mul, sub } from "@metta-ts/edsl";
const db = mettaDB();
const { fact } = names();
const { x } = vars();
// a recursive rewrite rule, built from typed combinators
db.rule(fact(x), If(gt(x, 0), mul(x, fact(sub(x, 1))), 1));
db.evalJs(fact(5)); // [120]names() and vars() mint the functors, symbols, and variables you use, so no name is written twice: the JS binding is the name. fact(x) builds the expression (fact $x); If, gt, mul, sub build the standard forms if, >, *, -. The result runs on the same interpreter as any MeTTa program.
Names and variables
names() returns a proxy that mints a symbol or functor per property, and vars() one that mints a fresh logic variable per property. A bare name grounds to its symbol; a called name applies it. Destructure what you use.
import { names, vars, rule, e } from "@metta-ts/edsl";
const { swap, check, Pair } = names();
const { a, b } = vars();
rule(swap(Pair(a, b)), Pair(b, a)); // (= (swap (Pair $a $b)) (Pair $b $a))
rule(check(e(a, b, a)), e(a, b)); // repeated variable in the patternType a variable's unwrapped value with vars<{ x: number }>(). The special forms are capitalized (they are forms, not data): If, Case, Let, LetStar, Match, Superpose, Collapse, Empty, Unify, Sealed, Quote. Grounded operations stay lowercase: arithmetic and comparison, and/or/not, the list ops, and the JSON ops below.
A tagged template
m\...`is the general escape hatch. It runs the real parser, so it expresses every MeTTa form, and${value}` interpolations are auto-grounded:
import { mettaDB, m } from "@metta-ts/edsl";
const db = mettaDB();
db.add(m`(= (gp $x $z) (match &self (parent $x $y) (match &self (parent $y $z) $z)))`);Passing TypeScript values straight in
Any value that is not already an atom is grounded automatically, by every builder and by template interpolation. A grounded function bridges the other way with db.fn: arguments are auto-unwrapped to JS and the result auto-grounded, so a plain typed function is all you write.
import { mettaDB, names, m } from "@metta-ts/edsl";
const db = mettaDB();
db.fn("balance-of", (a: { balance: number }) => a.balance);
const { "balance-of": balanceOf } = names();
const account = { owner: "Tom", balance: 100 };
db.evalJs(balanceOf(account)); // [100] via a builder
db.evalJs(m`(balance-of ${account})`); // [100] via the templateUse db.fns({ ... }) to register several at once, db.asyncFn for I/O, and the raw db.op/db.asyncOp when you need multiple results or full atom control.
The runner
mettaDB() keeps MeTTa's two query mechanisms distinct:
query(pattern)doesmatch &selfover stored atoms and returns binding rows (keys inferred from the pattern, or typed by an explicitvarsmap).eval(atom)rewrites with the=rules and returns the (nondeterministic) result atoms.evalJsunwraps each to a JavaScript value.
import { mettaDB, names, vars } from "@metta-ts/edsl";
const db = mettaDB();
const { Likes, Ada, Coffee, Chocolate } = names();
const { thing } = vars();
db.add(Likes(Ada, Coffee), Likes(Ada, Chocolate));
db.query(Likes(Ada, thing)); // [{ thing: "Coffee" }, { thing: "Chocolate" }]Calling MeTTa from TypeScript
db.call.<name>(...) evaluates (<name> ...args) and returns each result unwrapped to JS; bracket access handles hyphenated names. db.import("name") returns a callable.
db.call.fact(5); // [120]
db.call["is-even"](4); // hyphenated names
const factorial = db.import("fact"); // a callableTyping the host bridge
Pass a schema to mettaDB and call, import, and fn become statically typed; with no schema they stay permissive. Both an interface and a type schema work.
interface Api {
fact: (n: number) => number;
isEven: (n: number) => boolean;
}
const db = mettaDB<Api>();
db.call.fact(5); // number[]
const factorial = db.import("fact"); // (n: number) => number | undefined
db.fn("fact", (n: number) => n + 1); // checked against the schemaTyped source queries
db.q("...") runs match &self from a plain source string and types the result rows by the pattern's $-variables, extracted at compile time. The keys are known and autocompleted, and a key that is not a variable in the source is a compile error.
const rows = db.q("(Likes Ada $thing)"); // Array<{ thing: unknown }>
rows[0]!.thing; // ok, autocompletedThis types the variable structure, not the result values (those come from runtime rewriting, which the type system cannot evaluate), so values are unknown. It works on a plain string, not the m\`tag: TypeScript widens a tagged template's text tostring`, which discards the literal the type-level parser needs.
JSON and dict-spaces
db.useJson() enables the JSON module, then the jsonEncode/jsonDecode/dictSpace/getKeys/getValue builders bridge JSON and MeTTa spaces. json-decode turns a JSON object into a dict-space of (key value) pairs, so a fetched payload becomes a queryable space.
const db = mettaDB().useJson();
db.evalJs(jsonEncode(42)); // ["42"]
const doc = jsonDecode('{"name": "Ada", "age": 36}'); // a dict-space
db.evalFirst(getValue(doc, "name")); // "Ada" (JSON keys decode to strings)Optional host interop builders
The eDSL has helper subpaths for Python and Prolog host interop. They only construct atoms. The runtime still has to be registered explicitly through @metta-ts/py, @metta-ts/prolog, a CLI flag, or a browser host runner.
import { vars } from "@metta-ts/edsl";
import { pyCall } from "@metta-ts/edsl/py";
import { prologCall, importPrologFunction } from "@metta-ts/edsl/prolog";
const { x } = vars();
pyCall("math.add", 40, 2); // (py-call (math.add 40 2))
prologCall(["edge", "alice", x]); // (prolog-call (edge alice $x))
importPrologFunction("edge"); // (import_prolog_function edge)Strings in pyCall arguments are normal MeTTa string atoms and reach Python as Python strings. Strings in Prolog goal arrays are Prolog atoms, so ["edge", "alice", x] builds (edge alice $x).
The eDSL is the most ergonomic way to drive MeTTa from TypeScript. When a script is easier to read as plain MeTTa, reach for m\...`ordb.run(source)`; the two always interoperate.