Skip to content

@mettascript/edsl

The ergonomic, typed eDSL. Builders produce ordinary atoms; the runner evaluates them on the core engine. For the conceptual tour, see the typed eDSL; this page is the full surface.

bash
npm install @mettascript/edsl

Names, variables, and terms

ts
type Term = Atom | Name | number | string | boolean | bigint | object | null | undefined;
type Var<T = unknown> = VariableAtom & { __varType?: T };
type Name = ((...args: Term[]) => ExpressionAtom) & {
  /* branded with its head symbol */
};

function names<K extends string = string>(): Record<K, Name>; // symbols + functors, minted per key
function vars<T extends Record<string, unknown> = Record<string, unknown>>(): {
  [K in keyof T]: Var<T[K]>;
};

function ground(x: Term): Atom; //          atoms pass through, a bare Name grounds to its symbol, JS values grounded
function patternVars(atom: Atom): Var[]; // the free variables of a pattern (what query infers keys from)
const e: (...items: Term[]) => ExpressionAtom; //           a raw tuple/expression
const nil: () => ExpressionAtom; //                         the empty list ()
function list(items: Term[], opts?: { cons?: string; nil?: Atom }): Atom; // (:: a (:: b ()))

names() mints a symbol/functor per property (a bare name grounds to its symbol, a called name applies it); vars() mints a fresh logic variable per property, typeable with vars<{ x: number }>(). A Term is anything a builder accepts. ground is the primitive behind "pass a TypeScript object straight in".

Special-form and operator combinators

ts
const rule: (head: Term, body: Term) => ExpressionAtom; //    (= head body)
const decl: (subject: Term, type: Term) => ExpressionAtom; // (: subject type)
const arrow: (...types: Term[]) => ExpressionAtom; //         (-> A B ... R)

// special forms (capitalized: they are forms, not data)
const If: (cond: Term, then: Term, els: Term) => ExpressionAtom;
const Case: (scrutinee: Term, cases: ReadonlyArray<readonly [Term, Term]>) => ExpressionAtom;
const Let: (pattern: Term, value: Term, body: Term) => ExpressionAtom;
const LetStar: (bindings: ReadonlyArray<readonly [Term, Term]>, body: Term) => ExpressionAtom;
const Match: (pattern: Term, template: Term, space?: Term) => ExpressionAtom; // defaults to &self
const Superpose: (...items: Term[]) => ExpressionAtom;
const Collapse: (x: Term) => ExpressionAtom;
const Empty: () => ExpressionAtom;
const Unify: (a: Term, b: Term, then: Term, els: Term) => ExpressionAtom;
const Sealed: (vars: ReadonlyArray<Term>, body: Term) => ExpressionAtom;
const Quote: (x: Term) => ExpressionAtom;

// grounded operations (lowercase)
const add, sub, mul, div, mod: (a: Term, b: Term) => ExpressionAtom; // arithmetic
const eq, neq, gt, lt, ge, le: (a: Term, b: Term) => ExpressionAtom; // comparison
const and, or: (a: Term, b: Term) => ExpressionAtom;
const not: (x: Term) => ExpressionAtom;
const getType, getMetatype: (x: Term) => ExpressionAtom; //           get-type / get-metatype
const unique: (x: Term) => ExpressionAtom; //                         de-duplicate a nondeterministic result
const union, intersection, subtraction: (a: Term, b: Term) => ExpressionAtom; // set ops
const assertEqual, assertAlphaEqual: (a: Term, b: Term) => ExpressionAtom; // test assertions
const println: (x: Term) => ExpressionAtom; //                        println!
const carAtom, cdrAtom, deconsAtom: (x: Term) => ExpressionAtom; //   expression/list ops
const consAtom: (head: Term, tail: Term) => ExpressionAtom;
// JSON module (enable with db.useJson()):
const jsonEncode, jsonDecode, getKeys: (x: Term) => ExpressionAtom;
const getValue: (space: Term, key: Term) => ExpressionAtom;
const dictSpace: (pairs: ReadonlyArray<readonly [Term, Term]>) => ExpressionAtom;

Each maps to the matching MeTTa form or grounded operation. Builders compose, so nested patterns are nested calls.

The tagged template and source parsing

ts
function m(strings: TemplateStringsArray, ...values: Term[]): Atom; //      exactly one atom
function mAll(strings: TemplateStringsArray, ...values: Term[]): Atom[]; // several atoms
function parseSource(src: string): Atom; //                                one atom from a plain string

m\...`parses MeTTa source with${...}holes auto-grounded; it throws if the template is not exactly one atom (usemAll` for several).

The runner

ts
const mettaDB: <S = {}>() => MettaDB<S>; // optional schema types the host bridge

class MettaDB<S = {}> {
  readonly metta: MeTTa; //                     the underlying hyperon runner
  add(...atoms: Term[]): this; //               facts, rules, type declarations
  rule(head: Term, body: Term): this; //        (= head body)
  declare(subject: Term, type: Term): this; //  (: subject type)
  eval(atom: Term): Atom[]; //                  rewrite, return result atoms
  evalFirst(atom: Term): Atom | undefined;
  evalJs(atom: Term): unknown[]; //             results unwrapped to JS values
  evalAsync(atom: Term): Promise<Atom[]>;
  evalJsAsync(atom: Term): Promise<unknown[]>;
  test(actual: Term, expected: Term): boolean; // assertEqual, pass/fail

  // querying
  query(pattern: Term): Array<Record<string, unknown>>; //           keys inferred from the pattern
  query<V extends Record<string, Var>>(pattern: Term, vars: V): Array<Row<V>>; // typed values
  q<Src extends string>(src: Src): Array<SourceRow<Src>>; //         typed rows from a source string

  // host bridge: TypeScript to MeTTa
  fn<K extends string>(name: K, fn: K extends keyof S ? S[K] : AnyFn): this; // typed function in
  fns(map: Record<string, AnyFn>): this;
  asyncFn(name: string, fn: (...args: never[]) => Promise<unknown>): this;
  op(name: string, fn: (args: Atom[]) => Atom[]): this; //           raw atom control
  asyncOp(name: string, fn: (args: Atom[]) => Promise<Atom[]>): this;

  // host bridge: MeTTa to TypeScript
  get call(): CallProxy<S>; //                  db.call.fact(5), typed from S
  import<K extends string>(name: K): /* typed from S, or permissive */;

  useJson(): this; //                           enable the JSON / dict-space module
  run(src: string): Atom[][]; //                raw MeTTa source
}
type Row<V extends Record<string, Var>> = { [K in keyof V]: VarValue<V[K]> };
type SourceRow<S extends string> = { [K in SourceVars<S>]: unknown }; // $-vars extracted at type level

query runs match &self and returns one row per match (keys inferred from the pattern, or typed by an explicit vars map). q does the same from a source string with the row keys extracted from the source's $-variables at compile time. fn/fns/asyncFn register plain typed functions (args auto-unwrapped, result auto-grounded); op/asyncOp give raw atom control. call and import call MeTTa functions back from TypeScript. Pass a schema to mettaDB<Schema>() to type all of these; both an interface and a type work.

For annotations without a second import, the entry also re-exports Atom, GroundedAtom, ValueAtom, and atomToJs from @mettascript/hyperon.

Optional host interop builders

The @mettascript/edsl/py and @mettascript/edsl/prolog subpaths build atoms for optional host runtimes. They do not import the runtime packages themselves.

ts
// @mettascript/edsl/py
function pyCall(path: string, ...args: Term[]): ExpressionAtom; // (py-call (<path> ...args))
function pyCall(spec: Term): ExpressionAtom; //                 (py-call <spec>)
const pyEval: (source: Term) => ExpressionAtom; //              (py-eval source)
const pyImport: (moduleOrPath: Term) => ExpressionAtom; //      (py-import module-or-path)
function pyAtom(path: string | Term, type?: Term): ExpressionAtom;
function pyDot(object: Term, attr: string | Term, type?: Term): ExpressionAtom;
function pyList(items: readonly Term[] | Term): ExpressionAtom;
function pyTuple(items: readonly Term[] | Term): ExpressionAtom;
function pyDict(pairs: ReadonlyArray<readonly [Term, Term]> | Term): ExpressionAtom;
function pyChain(items: readonly Term[] | Term): ExpressionAtom;
ts
// @mettascript/edsl/prolog
type PrologGoal = Term | readonly Term[];
const prologCall: (goal: PrologGoal) => ExpressionAtom;
const Predicate: (goal: PrologGoal) => ExpressionAtom;
const callPredicate: (goal: PrologGoal) => ExpressionAtom;
const assertaPredicate: (goal: PrologGoal) => ExpressionAtom;
const assertzPredicate: (goal: PrologGoal) => ExpressionAtom;
const retractPredicate: (goal: PrologGoal) => ExpressionAtom;
const prologMatch: (goal: PrologGoal, template: Term) => ExpressionAtom;
const prologFunction: (name: Term, args: readonly Term[] | Term) => ExpressionAtom;
const importPrologFunction: (name: Term) => ExpressionAtom;
const prologConsult: (path: Term) => ExpressionAtom;
const importPrologFunctionsFromFile: (path: Term, names: readonly Term[] | Term) => ExpressionAtom;

Strings in Prolog goal arrays are treated as Prolog atoms, so prologCall(["edge", "alice", x]) builds (prolog-call (edge alice $x)). Pass ground("text") when a Prolog string is required.

Terms as arrays

An array in term position is an expression: [parent, Tom, Bob] is (parent Tom Bob), at any depth. val(x) keeps an array a grounded value instead. sym(name) is the single-name form of names(), for a head that is not a valid identifier (sym("get-atoms"), sym("&limit"), sym("+")). A JavaScript string is a grounded string in head position too, so ["get-atoms", x] builds ("get-atoms" $x), which never reduces.

An array whose every element is an expression reads as a conjunction at query; e(...) builds an expression with no second reading.

Modules

mettaModule() answers a builder with .grounded(name, fn), .asyncGrounded, .define(name, body), .relation(name, cols), .atoms(...), .source(src), .declare(name, args, ret) and .use(other). Each declaring method also takes a value signature: .define("quad", ["Number"], "Number", body) types the TypeScript side and emits (: quad (-> Number Number)).

db.use(module) applies it once per runner and returns the runner retyped with everything the module declares. m.declarations() returns the (: Name (-> ...)) atoms without emitting them. m.relations and m.signatures are the recorded declarations.

Matching a result

matchAtom(a) and db.match(a) answer a chain of .with(pattern, handler) arms, ending in .otherwise(fallback), .run() or .exhaustive(). db.match checks exhaustiveness against the relation heads the schema declares.

Patterns: P._, P.any, P.str, P.num, P.bool, P.sym, P.var, P.expr, P.rest(name), P.when(pred, name?), P.union(alts, name?), P.not(pattern, name?). .withAny([p1, p2], handler) runs one handler off several shapes; .returnType<T>() fixes every arm's result.

isMatching(pattern, atom) answers a boolean and matchBindings(pattern, atom) the bindings or undefined. NonExhaustiveError is thrown when .exhaustive() runs and nothing matched.

Transactions and the change log

db.transaction(body) commits the whole body or none of it, db.dryRun(body) reports and restores either way, and db.undo(report) returns to report.before. A report carries value, changes, before, after and committed.

db.onChange(fn) reports every write outside a transaction and answers a function that stops it. A Change is { op, space, atom }, and formatChanges(changes) renders a list.

Spaces

db.space has size, add, delete, deleteAll, has, clear, iteration, the array methods, atoms(), of(rel), byHead(), toJS(). db.useSpace(name, backend) serves a named space from any Space implementation, and undefined unregisters it. PersistentSpace is one whose versions are values: snapshot(), restore(v), fork(), size.

Diagnosing

db.why(pattern) returns why a pattern matched nothing as data and db.explain(pattern) as a line to print. db.watch(pattern, fn) runs a live query and answers a stop function. db.declareRelations(decls) emits the column types to the engine and db.typeCheck() turns enforcement on.

Released under the MIT License.