Repository navigation
[Feature]: Replace hand-written workflow expression parser with a Lark LALR grammar #5060
Description
Problem Statement
The workflow expression evaluator (expressions.py) is a hand-written parser. Of the 22 fixes it received between April 2026 and the package split, 11 were structural parser bugs (quoting, whitespace, parentheses, precedence, list literals, filter chains, trailing tokens, multi-argument calls). Its hardest function scores 70 cognitive / 38 cyclomatic (rank E). Some invalid input is silently accepted ([1,2,3][1] returns None; an unclosed quote in a condition evaluates to False).
Background, prototypes and measurements: #4917
Proposed Solution
Replace the hand-written parser with a Lark LALR grammar, without changing the workflow expression language. Only the parser is replaced. The template scanner, namespace resolution, filters, operator semantics and diagnostics stay in our code.
Prototype: chore/workflow-expression-lark-a2 (diff against base).
Delivery as three PRs, in this order:
- Package split:
expressions.pybecomes a package andengine.pyis the single place where the engine is selected. No behavior change. - Gap tests: tests for behavior that is implemented but never asserted (
>=/<=, literals,map('a.b'),item/fan_in, …). They pass onmain. - Lark engine: the LALR grammar, the
larkdependency and the 10 documented test changes.
Results against the 96-case behavior corpus and the full suite:
| Today | Lark LALR | |
|---|---|---|
| Corpus cases passed (96) | n/a | 96 |
| Valid expressions whose result changes | n/a | 0 |
| Full suite failures | n/a | 0 (after 10 documented test changes) |
| Owned lines of code | 313 | 334 |
| Cognitive complexity (total / hardest function) | 166 / 70 | 110 / 31 |
| Cyclomatic complexity (total / hardest function) | 115 / 38 (E) | 113 / 20 (C) |
| Runtime | 1× | 1.3–1.6× |
| New dependency | none | lark 1.3.1 (pure Python, no transitive dependencies) |
Alternatives Considered
Each was prototyped on its own branch against the same base and corpus (details in the discussion):
- Lark Earley: 94/96, 26 suite failures, 25–30× slower. It missed the precedence guards. Superseded by LALR.
- pyparsing: 94/96, 25 suite failures, 9–10× slower, larger grammar. It is the fallback if a Lark dependency is not acceptable.
- Jinja2: 89/96, 48 suite failures, 5 valid expressions change result.
steps.run-tests.outputparses as a subtraction, and'10' > '9'compares as text. These cannot be fixed through an extension point. - python-liquid: 69/96, 93 suite failures, 27 valid expressions change result. Different filter syntax, no
in, no list literals. - simpleeval, cel-python: both fail on hyphenated paths and our filter syntax. They would mean moving to a new language.
Component
Other
Use Cases
- Maintainers get a grammar that rules out the structural parser bug class by construction.
- Parse errors include the column, and condition diagnostics use it.
- Workflow authors keep the existing expression language; nothing in the shipped workflows changes.
Acceptance Criteria
Maintainer decisions needed before work starts:
- Approve a new runtime dependency:
lark1.3.1. - Accept the 10 test changes. Invalid input is rejected with
ValueErrorinstead of silently passing, and some error messages come from the parser (English, with the column). - Decide whether to keep the precedence guards (a filter after a comparison or after
notis a parse error). The prototype keeps them; dropping them would mean adopting Jinja-style precedence.
Implementation:
- PR 1: package split, no behavior change.
- PR 2: gap tests, passing on
main. - PR 3: Lark LALR engine.
- All 96 corpus cases pass and the full suite is green.
-
larkis declared as a dependency.
Additional Context
- Behavior corpus, harness, report and raw suite logs: https://github.com/markuswondrak/spec-kit/tree/chore/workflow-expression-harness/spec/workflow_expression_evaluator (reproduction steps in its README).
- Branches:
eval-base,lark-a2.
AI Disclosure
Issue text drafted with Pi Coding Agent using Claude Sonnet 5.5 (github-copilot), reasoning effort high, human-supervised; text generation only, summarizing the linked discussion. The prototypes, harness and measurements in the discussion were produced with OpenCode and Claude Opus 5.5 (github-copilot/claude-opus-5.5), autonomously under human direction, as disclosed there.
Activity
mnriem commentedon Oct 22, 2026
Go for it!
hnipps commentedon Oct 23, 2026
Three related reports against the current hand-written expression evaluator, each with a self-contained reproducer, in case they are useful as acceptance cases for the grammar:
0 <= x <= 10in workflow expressions silently return the wrong result #5071: chained comparisons such as0 <= inputs.n <= 10are split at the first operator, so0 <= 50 <= 10evaluates to true and anifstep takes the wrong branch.==and!=do not compare numeric strings as numbers, while<,>,<=and>=do #5072:==/!=do not compare numeric strings as numbers, while<,>,<=and>=do.1e3, escaped quotes,a[0][1], operators without spaces) evaluates toNonesilently instead of raising.Posted with Claude Code (Claude Opus 4.8) under human supervision.