Build your own knowledge graph database
Cypher-lite front-end: tokenize then parse·12 min

Parse a node pattern

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage parse-node-pattern --watch

Parse (var:Label {k:v, k2:v2}) into a NodePattern AST node — your first recursive-descent rule. Consume (, an optional variable identifier, an optional :Label, an optional { ... } property map, then ). Each piece is optional except the parentheses.

Your task

Implement parseNodePattern

Introduce the parser helpers peek() / next() / expect(kind). Each optional piece is just an if (peek() is ...) guard. Build { variable, label, props: [{key, value}] }.

src/parser.ts
// NodePattern = { variable: string|null; label: string|null;
//                 props: Array<{ key: string; value: Literal }> }
parseNodePattern(): NodePattern {
  this.expectPunct('(');
  let variable = null, label = null; const props = [];
  if (this.peek().kind === 'IDENT') variable = this.next().value;
  if (this.isPunct(':')) { this.next(); label = this.next().value; }
  if (this.isPunct('{')) {
    this.next();
    while (!this.isPunct('}')) {
      const key = this.next().value; this.expectPunct(':');
      props.push({ key, value: this.parseLiteral() });
      if (this.isPunct(',')) this.next(); else break;
    }
    this.expectPunct('}');
  }
  this.expectPunct(')');
  return { variable, label, props };
}
Process command
your_program.sh parse-node "(p:Person {name:'Tom Hanks', born:1956})"
# -> {"label":"Person","props":[{"key":"name","value":{"kind":"string","value":"Tom Hanks"}},
#     {"key":"born","value":{"kind":"int","value":1956}}],"variable":"p"}
Recursive descent is one function per rule

Each grammar rule is one function that consumes exactly the tokens it owns and returns its AST node. Every optional piece is an if (peek().kind === ...) guard. No magic — just disciplined peek/next.

How it's graded

Process mode, stdout_json_match against the exact AST (object keys compared as sets via canonicalization). Literals carry their kind (string/int/bool) so a string '1956' would parse differently from the int 1956.