GraMa is a graph programming language. It stands for Graph Machine.
Programs are directed graphs made of edges (source → identifier → target). There is no syntax tree — computation is encoded as relationships between nodes. A query engine interprets the graph, and a compiler emits JavaScript from the same structure.
A GraMa program is a set of edges. Each edge connects a source node to a target node through an identifier (the relationship type).
call --type--> sum
call --arg--> 1
call --arg--> 2
In TypeScript this looks like:
const [$call] = variable();
const edges = [
{ source: $call, identifier: typeRel, target: sumNode },
{ source: $call, identifier: argRel, target: numberNode(1) },
{ source: $call, identifier: argRel, target: numberNode(2) },
];Primitives like sum are also graphs. They declare:
- A pattern to match (node of type
sumwith twoargedges) - A compute function (how to evaluate it)
- An emit function (how to render it as JS)
// sum primitive (simplified)
const sumEdges = [
{ source: $x, identifier: typeRel, target: sumNode },
{ source: $x, identifier: argRel, target: $a },
{ source: $x, identifier: argRel, target: $b },
{ source: sumNode, identifier: emitRel, target: emitFn((args) => `${args[0]} + ${args[1]}`) },
{ source: sumNode, identifier: computeRel, target: computeFn(/* adds $a + $b */) },
];The query engine backward-chains over edges to compute results:
const results = query(graph, { source: $call, identifier: resultRel });
// → 1 + 2 = 3The compiler detects computation structure in the graph and emits JS:
const js = emitProgram(buildProgram(edges, compGraph));See the examples/ directory for the full set. Run any example with:
pnpm ts-node "examples/02 - sum(1, 2, 3).ts"Two chained sum calls — the result of the first feeds into the second:
const [$call1] = variable();
const [$call2] = variable();
const [$res1] = variable();
const [$result] = anchor("result");
const edges = [
// sum(1, 2)
{ source: $call1, identifier: typeRel, target: sumNode },
{ source: $call1, identifier: argRel, target: numberNode(1) },
{ source: $call1, identifier: argRel, target: numberNode(2) },
{ source: $call1, identifier: resultRel, target: $res1 },
// sum(sum(1,2), 3)
{ source: $call2, identifier: typeRel, target: sumNode },
{ source: $call2, identifier: argRel, target: $res1 },
{ source: $call2, identifier: argRel, target: numberNode(3) },
{ source: $call2, identifier: resultRel, target: $result },
];Compiles to:
function sum(arg0, arg1) {
return arg0 + arg1;
}
const _v0 = sum(1, 2);
const result = sum(_v0, 3);Multiple rules for the same type create branches. Each rule has an existential constraint — eq(x, 3) must equal true or false — and a different return value:
const classifyNode = id("classify");
// Rule 1: if eq(x, 3) = true → "its 3"
const rule1 = [
{ source: $c1, identifier: typeRel, target: classifyNode },
{ source: $c1, identifier: arg1Rel, target: $x1 },
{ source: $eq1, identifier: typeRel, target: eqNode },
{ source: $eq1, identifier: arg1Rel, target: $x1 },
{ source: $eq1, identifier: arg2Rel, target: numberNode(3) },
{ source: $eq1, identifier: resultRel, target: trueNode },
{ id: c1$, source: $c1, identifier: resultRel, target: stringNode("its 3") },
];
// Rule 2: if eq(x, 3) = false → "not 3"
const rule2 = [
{ source: $c2, identifier: typeRel, target: classifyNode },
{ source: $c2, identifier: arg1Rel, target: $x2 },
{ source: $eq2, identifier: typeRel, target: eqNode },
{ source: $eq2, identifier: arg1Rel, target: $x2 },
{ source: $eq2, identifier: arg2Rel, target: numberNode(3) },
{ source: $eq2, identifier: resultRel, target: falseNode },
{ id: c2$, source: $c2, identifier: resultRel, target: stringNode("not 3") },
];Compiles to:
function eq(arg0, arg1) {
return arg0 === arg1;
}
function classify(eq_arg1) {
const eq_result = eq(eq_arg1, 3);
if (eq_result === true) {
return "its 3";
} else if (eq_result === false) {
return "not 3";
}
}