Grammar, Parsing & Abstract Syntax Trees¶
DynSpec uses Lark, a parsing toolkit for Python, to build typed, error-resilient Abstract Syntax Trees (ASTs) from economic model files.
The Grammar Definition (grammar.lark)¶
DynSpec's grammar is defined in grammars/grammar.lark. It utilizes an LALR(1) parsing engine with character-level position tracking.
Entry Points¶
The parser supports multiple start symbols depending on what is being parsed:
| Start Symbol | Scope | Used For |
|---|---|---|
free_block (default) |
Full file | Complete .dyno files containing metadata, blocks, and assignments |
equation_block |
Multi-line equations | Blocks enclosed in { ... } or equation groups |
assignment_block |
Calibration blocks | Series of assignments (<- or :=) |
formula |
Single expression | Standalone mathematical expressions |
from dyno.dynspec.grammar import parser
# Parse a complete model
full_text = "y[t] = c[t] + i[t]\nk[t] = (1-d)*k[t-1] + i[t]"
tree_full = parser.parse(full_text, start="free_block")
# Parse an equation block
tree_eqs = parser.parse("y[t] = c[t] + i[t]\nk[t] = (1-d)*k[t-1] + i[t]", start="equation_block")
# Parse a standalone formula
tree_expr = parser.parse("alpha * (k[t-1]^alpha) * (n[t]^(1-alpha))", start="formula")
Core AST Hierarchy¶
The parsed tree decomposes source code into a clean, hierarchical structure:
graph TD
FB[free_block] --> AS1[annotated_statement]
FB --> AB[annotated_block]
FB --> MM[model_metadata]
AS1 --> SC[statement_core]
AS1 --> SM[statement_metadata]
SC --> EQ[equality]
SC --> BF[bare_formula]
SC --> ASG[assignment]
SC --> QA[quantified_assignment]
EQ --> F1[formula: LHS]
EQ --> F2[formula: RHS]
F1 --> SYM[symbol: variable]
SYM --> CN[cname: 'y']
SYM --> TI[time_index: 't']
SYM --> TS[time_shift: '0']
Key Syntax Rules¶
# Equality vs. Residual (bare_formula)
equality: formula "=" formula
bare_formula: formula
# Assignments
assignment: symbol _ASSIGN formula
quantified_assignment: _FORALL "t" ["," t_double_bound] ":" variable _ASSIGN formula
# Symbols
variable: cname "[" time_index time_shift "]"
value: cname "[" time "]"
constant: cname
The TimeFixer Transformer¶
In economic models, researchers write x[t] to denote contemporaneous variables, and x[t-1] or x[t+1] for lagged and lead variables.
In the raw Lark grammar, time_shift is an optional rule:
When no shift is written (as in k[t]), time_shift initially yields None. To prevent downstream analyzers from needing null-checks everywhere, DynSpec integrates a built-in TimeFixer transformer:
from lark import Transformer, Tree, v_args
class TimeFixer(Transformer):
@v_args(tree=True)
def shift(self, tree):
if tree.children[0] is None:
return Tree("shift", ["0"])
return tree
Every variable node in the resulting AST is guaranteed to possess a normalized shift:
k[t]\(\implies\)Tree('variable', [cname('k'), index('t'), shift('0')])k[t-1]\(\implies\)Tree('variable', [cname('k'), index('t'), shift('-1')])k[~]\(\implies\)Tree('variable', [cname('k'), index('~'), shift('0')])
Source Location Tracking & Diagnostics¶
Lark is configured with propagate_positions=True, attaching accurate line and column coordinates to every AST node:
tree = parser.parse("y[t] = exp(z[t]) * k[t-1]^alpha", start="equation_block")
eq = tree.children[0]
print(f"Line: {eq.meta.line}, Column: {eq.meta.column}")
print(f"End Line: {eq.meta.end_line}, End Column: {eq.meta.end_column}")
This precise location tracking powers Dyno's human-readable error diagnostics and warnings.