Skip to content

Getting started

This tutorial builds a tiny grammar by hand and runs it. By the end you’ll have transformed a graph and read the result.

Terminal window
npm install graph-grammar

The package is ESM-only and ships TypeScript types. Its only runtime dependency is zod.

We’ll write an edge subdivision rule: every A,A edge gains a B in the middle, becoming A,B,A. The builders DSL keeps the verbose data model out of hand-written grammars.

import { rule, pn, pe, rn, re } from 'graph-grammar'
const subdivide = rule({
name: 'subdivide',
// LHS: two A nodes joined by an edge
lhs: {
nodes: [pn('a', 'A'), pn('b', 'A')],
edges: [pe('e', 'a', 'b')],
},
// RHS: keep both A's (mapFrom), insert a new B between them
rhs: {
nodes: [rn('a', 'A', { mapFrom: 'a' }), rn('mid', 'B'), rn('b', 'A', { mapFrom: 'b' })],
edges: [re('e1', 'a', 'mid'), re('e2', 'mid', 'b')],
},
})
  • pn(id, label) / pe(id, source, target) build the LHS pattern.
  • rn(id, label, { mapFrom }) preserves a matched LHS node; without mapFrom it’s a freshly created node.
  • The morphism (which RHS node preserves which LHS node) is derived from mapFrom, so you don’t declare it twice.
import { grammar, Engine } from 'graph-grammar'
// Host graph: A , A
const start = {
nodes: [
{ id: '1', label: 'A', props: {} },
{ id: '2', label: 'A', props: {} },
],
edges: [{ id: 'x', source: '1', target: '2', label: '', directed: false, props: {} }],
}
const g = grammar('subdivide-demo', [subdivide], start, {
strategy: 'maximal', // apply all non-overlapping matches each step
maxSteps: 3,
})
const engine = new Engine(g)
engine.run()
console.log(engine.steps) // number of rewrite steps taken
console.log(engine.graph.nodes.length) // grew as B's were inserted

new Engine(g) clones the start graph, so running, resetting, and re-running is always reproducible , the axiom is never mutated in place.

For animation or inspection, drive the engine one rewrite at a time:

const e = new Engine(g)
let result = e.step()
while (result.applied) {
console.log(result.ruleId, 'created', result.createdNodes)
result = e.step()
}

Each step() returns a RewriteResult describing what changed (created/deleted node and edge ids) , ideal for highlighting or diffing.

Built by Kiberon Labs