Domain expressions are encoded as strings — `'tag=urgent AND priority>3'`, `'$.user.profile.name'` — that the system parses on every use. The grammar lives in comments and parser code; valid expressions are runtime concerns; type checking is impossible; combinators (AND/OR/NOT) bolt on with growing string-manipulation complexity.
String-DSL parsing and dispatch entangled in one method body the agent must trace per call. The grammar is implicit in regex patterns and conditionals; the agent cannot statically enumerate valid expressions or verify that all paths handle malformed input.
Each grammatical element is a domain object: `Equals`, `GreaterThan`, `And`, `Or`. Expressions are built by composition; evaluation is a polymorphic `matches(context)` call. The grammar is the type system; composition replaces parsing.
Each grammar node is one class the agent verifies independently. The composed expression tree is a statically-typed data structure the agent reads structurally; per-node tests pin per-node behaviour.
Before the refactoring
// A string-based DSL carries the filter; parsing and dispatch are coupled in one method.class TaskFilter {matches(task, expression) {// expression like 'tag=urgent AND priority>3'const clauses = expression.split(' AND ');return clauses.every((clause) => {const equalsMatch = clause.match(/^(\w+)=(\S+)$/);if (equalsMatch) return task[equalsMatch[1]] === equalsMatch[2];const greaterMatch = clause.match(/^(\w+)>(\S+)$/);if (greaterMatch) return task[greaterMatch[1]] > Number(greaterMatch[2]);throw new Error(`unparseable clause: ${clause}`);});}}
After the refactoring
// Domain objects compose into an expression tree; each node knows how to evaluate itself.class Equals {constructor(field, value) { this.field = field; this.value = value; }matches(task) { return task[this.field] === this.value; }}class GreaterThan {constructor(field, value) { this.field = field; this.value = value; }matches(task) { return task[this.field] > this.value; }}class And {constructor(left, right) { this.left = left; this.right = right; }matches(task) { return this.left.matches(task) && this.right.matches(task); }}class Or {constructor(left, right) { this.left = left; this.right = right; }matches(task) { return this.left.matches(task) || this.right.matches(task); }}const filter = new And(new Equals('tag', 'urgent'), new GreaterThan('priority', 3));tasks.filter((task) => filter.matches(task));
Implicit DSLs are Primitive Obsession at scale — strings carry domain semantics the type system can't enforce. Parser bugs ship as 'this should match but doesn't'; new operators require parser edits; tests must cover both parsing and evaluation; the two concerns tangle.
Implicit DSLs defeat static analysis at the string boundary — the agent's verification cost spans both parser and evaluator. Adding a syntactic form requires the agent to coordinate parser, evaluator, and tests; misalignments between the three are silent and ship to runtime.
Interpreter introduces a class per grammar node. For two or three operators with a stable grammar, the inline string-and-conditional form is often cheaper than the class hierarchy. The composed form is more verbose at the construction site than the string form.
Interpreter spreads a single conceptual query across multiple class files; the agent traverses the tree to know what an expression does. Static call-graph analysis loses precision on polymorphic `matches` calls — the agent must enumerate concrete node types.
The grammar is statically typed and locally readable. Adding an operator is one new class; combinators are composable; tests target one node at a time. The expression's structure is visible in source rather than encoded in a string.
Adding a new operator is one new class in the grammar hierarchy; the agent enumerates operators by listing the classes that implement the interface, and tests target evaluation against the typed AST instead of raw string parsing.
An Interpreter applied to a grammar that's actually trivial (two operators, stable, low surface) is ceremony — readers must navigate a class hierarchy to understand what would have been three lines of string-matching. The pattern earns its keep when the grammar grows and composition matters.
A grammar with many nodes representing fine-grained syntactic variations forces the agent to load a large hierarchy to reason about any expression. Per-node specialization (NumericEquals vs. StringEquals vs. DateEquals) can multiply file count without proportional reasoning gain.