A Fix is not a Plan
Posted on Sun 05 July 2026 in tech
The Loop
Are you the victim of a finite automation? You could be entitled to compensation!
A finite automation is a driven process that forces an outcome. An engineer is both an
orchestrator and a facilitator. Facilitation always has an answer ready, orchestration has to be
known. Left alone, facilitation wins. Call it project myopia. It happens in big companies, where
facilitation gets praised and orchestration doesn't; it happens just as easily alone, in a project
like parsm, where the only one to please is yourself.
Two things put parsm's patch layer into production and both of them are virtues. One: the original idea was good enough to be worth shipping — a single declarative global grammar for filtering and templating structured text, one source of truth. Two: I kept it working under real use, the ordinary pull of a tool people actually run. I'd defend both traits in any engineer. Together they still don't make a plan, because neither one carries any memory of the fixes that came before it. What accumulates instead is drift.
I pride myself on being pragmatic and shipping a lot, and every one of these patches was that pragmatism working exactly as intended. I never sat down and decided to build a second parser. I fixed the bug in front of me, the same way I'd fixed the one before it, the same way I'd fix the next one. Repeat that enough times and you get a system that's still shipping, still passing its tests — and rewarding "don't fail" over "be correct," without anyone ever choosing that trade.
parsm — a small Rust CLI I've been building to filter and template structured text (JSON, YAML, TOML, CSV, logfmt) with one query language — is a clean, contained instance of that loop. This is the story of the patch layer that grew, unremarked, alongside the global grammar it was meant to be a stopgap for; why every patch to it was the right call in isolation; why the loop producing it couldn't have converged on its own no matter how carefully I made each call; and what it actually took to choose a destination instead of drifting to one.
The Global Grammar and the Patch Layer
parsm's syntax has lived in one place since the project turned serious: a single pest grammar — the global grammar — compiled into a parser at build time. That was the pitch when I first wrote about parsm — one source of truth, no drift between what's documented and what the tool accepts. That part never changed. What changed is what happened when the grammar said no.
Twenty-three days after the first working version, I added a patch layer: a small hand-written routine that caught whatever the grammar rejected and took its own pass at it. A stopgap behind a grammar is a reasonable thing to build and for the next thirteen months it kept being a reasonable thing to extend. I rewrote it when the module got split out of a 3,000-line monolith. I patched it again four months later for a formatting nit. I patched it a third time three days before I finally deleted it, fixing a bracket-scanning edge case nobody had hit yet. Every one of those commits had the same shape: here's an input a user just typed, here's the smallest change that makes it work.
By the time I sat down to actually audit it, fallback.rs had grown into four independent
strategies — manual filter parsing, boolean expressions built from truthy fields, template pattern
matching, and field selection — living behind one match:
match DSLParser::parse_dsl(input) {
Ok(result) => result,
Err(_) => fallback::try_fallback_parsing(input), // four strategies, ~800 lines
}
No single commit made that happen. More than a dozen did, spread across the thirteen months between the first working version and the day I deleted it — every one of them a correct answer to the question actually being asked at the time.
The Loop Is a Finite Automaton
The loop couldn't have converged on its own and being a better engineer inside it wouldn't have changed that — a memoryless machine doesn't have a destination, only a next step.
Model the loop as what it actually is — a machine whose next move depends only on its current state and the input in front of it. The input is "here's a bug." The transition is "patch it." The state the decision gets made from is this bug, right now — not "how many times have I already patched around something like this," not "does a strategy already exist that this collides with." Nothing ever wired that history into the decision.
That's a claim about what the loop, as built, could notice, not about carelessness. Every one of the patch layer's dozen-plus commits was a correct, isolated call. Run it faster, staff it with someone more careful and more senior — same loop, same result, arrived at sooner. The missing variable is memory.
The clearest proof is a bug nobody wrote on purpose. !active on its own, as a whole expression,
correctly fails — parsm's design requires an explicit ? for truthy checks. But !active && age >
25 silently parses, and evaluates the !active half as if the ? weren't required at all. Same
syntax, two different answers, depending on which of two independently-written patch-layer
strategies happens to see it first. Nobody decided that inconsistency should exist. It's what two
locally-correct patches produce when neither one knows the other exists — which is exactly what a
memoryless loop guarantees, sooner or later.
What "Easier in the Patch Layer" Actually Meant
"Easier to patch downstream than to fix the global grammar" was usually true and true for a different reason each time.
${cond?a:b} — a template conditional — is the cleanest case. The grammar rule for it existed. It
never fired, because a sibling rule, template_literal, was written to consume every character up
to } or ] except a short list of terminators and : wasn't on that list. By the time parsing
reached the point where the conditional rule needed to see its own :, template_literal had
already eaten it as plain text. That's a real structural conflict, not a typo — pest, like any PEG,
resolves competing rules by ordered choice: first match wins, no backtracking across alternatives
once one commits. Both rules wanted the same character, and ordered choice handed it to the wrong
one every single time. (The conditional also had no dispatch arm wired up to consume a match if it
ever won — a successful parse would have hit an unreachable!() and panicked.) Patching this in
the patch layer took ten minutes: special-case the string ?...:..., move on. Actually fixing it
meant noticing that the conflict lived in a rule that had nothing to do with conditionals at all.
The bare ~ contains operator — email ~ "@example.com" — was a gap rather than a conflict. ~
simply wasn't in the grammar's operator list, so pest didn't reject the input so much as never
recognize it as an operator at all. The patch layer caught it either way and the fact that it was
missing rather than colliding stopped being visible the moment the patch layer made it work anyway.
Some of what accreted was worse than either — a bug native to the patch layer itself, with no
grammar involved on either side. email ~ "@example.com" && age > 25 should split into two
clauses. The patch layer's own hand-rolled value capture didn't know where the first clause ended,
so it grabbed "@example.com" && age > 25 whole and treated all of it as the string being matched
against. The grammar had nothing to do with that one. The patch layer introduced it on its own,
because a hand-written parser patched under pressure doesn't get the scrutiny a grammar file does.
Reading Back the Reward
None of those three calls was wrong on its own terms. Each one fixed a real input, fast, without touching a grammar that was already load-bearing. What made them a different kind of problem, in aggregate, is that "does this match what the grammar was supposed to mean here, or does it just avoid an error" was never the question being asked — because the loop asking questions only ever asked "does this input now produce an answer." Once that's the thing quietly being optimized, it doesn't need reinforcing patch by patch. It just sits there, absorbing the gap between "doesn't crash" and "correct," for as long as nobody goes looking.
The most uncomfortable version of that finding wasn't even in the patch layer. echo '{"a": 5,
"b": 5}' | parsm 'a == b' should compare two fields and match — pest's own grammar parses a ==
b correctly, no patch layer involved. But the code that turned a bare identifier on the right
side of == into a value treated it as a string literal instead of a field reference, so the
comparison silently ran 5 == "b" and always came back false. The "good" parser had exactly the
same class of bug as the patch layer. Neither path had anything checking its answer against what
the language was supposed to mean, so which path handled a given input was never the variable that
mattered.
Choosing the Destination
Deleting the patch layer outright wasn't an option — some inputs people actually relied on only worked because of it. Undoing a year of accretion took more care than running it in reverse:
- Measure the surface. Build a corpus — every grammar rule, every patch-layer strategy, real and adversarial inputs, 79 entries in total — and run each one through both parsers, tracing which one actually produced the answer.
- Classify what's load-bearing. Of the patch layer's branches, 26 were reachable by some real input the grammar couldn't handle, 3 were dead code (the grammar always won that shape first), and 4 were unconfirmed and had to be resolved one way or the other before deletion.
- Delete first, then earn every input back. Remove the patch layer. For every input that used to depend on it, pin its correct answer as a test, marked to skip so the suite stays green while the gaps stay visible and tracked, instead of silently absorbed again.
- Fix the actual cause, one at a time, un-skipping each test as its real fix lands — full suite run after every change, so a fix for one input can't quietly break another without getting caught the same hour it happened.
Twelve inputs came back wrong, across four distinct root causes: a genuine grammar gap (the ~
operator, the ternary conditional), a value-resolution bug in code the grammar itself drives (the
field-vs-field case), dropped regex flags, and unescaped string literals. Once the corpus and the
pinned tests existed, all twelve landed inside a single afternoon.
The One Real Near-Miss
Restoring behavior raises an honest question for every input the patch layer used to handle: is this a real feature worth keeping, or an accident that never should have worked? The rule was: restore it only if a test, doc, or example already treats it as intended behavior. Otherwise, it stays rejected.
That rule is only as good as where you look for evidence. One template pattern — a [...] bracket
span nested inside a {...} brace template — got checked against the docs and examples, came up
empty, and got marked "stays rejected." It turned out the project's own integration test harness
had been quietly exercising that exact pattern the whole time, passing against the patch layer the
entire way through. Nobody thought to check the test harness itself as a place "intended behavior"
could live. An independent review pass caught it before it shipped as a silent regression, and the
actual grammar rule got fixed properly.
"Nothing documents this as intended" is only true once you've checked every place intent could be recorded, including the places that don't run under the usual test command.
What Changed, and What Didn't
parsm's syntax didn't change. age > 25 [${name} is ${age}] still means exactly what it always
meant. What changed is that it now means that one way, by construction, instead of usually meaning
that unless a grammar gap routed it somewhere else.
I checked whether deleting the patch layer made anything faster, because I expected it might — one parse attempt instead of a failed one followed by a full second pass. It didn't, measurably; process startup dominates a CLI's wall clock regardless and the patch layer only ever ran on the minority of inputs the grammar rejected in the first place.
What matters is what a tool actually optimizes for and that the loop producing it never has to be badly engineered to get there. A dozen reasonable patches, each one a correct call given what was known when it was made, add up to a system optimizing for "produces an answer" over "produces the right answer" — not because anyone chose that trade, but because the loop making the calls had no way to notice it was making the same trade twice. A single implementation can't disagree with itself: every future fix lands on the thing that's actually wrong, not on a negotiation with the patch before it. That's the whole difference between choosing a destination and getting driven to one.
Try Parsm
cargo install parsm
- Repository: github.com/jac18281828/parsm
- Crate: crates.io/crates/parsm
- Full changelog: CHANGELOG.md