Parse a node pattern
# grade this stage (from your course directory)
npx theinference test --stage parse-node-pattern --watchParse (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.
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}] }.
// 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 };
}
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"}
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.
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.