Compare
Symptom
Human

A small but stable mini-language is embedded as strings in code, parsed and evaluated by ad-hoc functions that grow operator-by-operator. Operator precedence is implicit in eval order; new operators require editing the parser AND the evaluator; static analysis of rules is impossible because they're untyped strings.

Agent

Ad-hoc string parsing the agent must read sequentially to understand evaluation semantics. Operator precedence is implicit; bugs in the parser cascade silently across every rule. Static analysis of 'what variables does this rule depend on' requires re-implementing the parser in the analysis tool.

Goal
Human

Each grammar rule becomes a class with an interpret(context) method. The AST is the program; building it is explicit (constructors), evaluating it is recursive descent through interpret(). New operators are new classes; the parser (if needed) and the evaluator stay structurally separate.

Agent

One class per grammar rule, each with interpret(env). The agent reads the AST shape directly from the construction expression; static traversal returns complete answers; per-rule edits scope to one class file.

Before the pattern

function evaluateRule(ruleString, context) {
if (ruleString.includes(' AND ')) {
const [left, right] = ruleString.split(' AND ');
return evaluateRule(left, context) && evaluateRule(right, context);
}
if (ruleString.includes(' OR ')) {
const [left, right] = ruleString.split(' OR ');
return evaluateRule(left, context) || evaluateRule(right, context);
}
if (ruleString.startsWith('NOT ')) {
return !evaluateRule(ruleString.slice(4), context);
}
return Boolean(context[ruleString]);
}
const pass = evaluateRule('verified AND NOT banned', user);
// Strings as DSL, ad-hoc parser, operator precedence implicit in eval order,
// no caching, no static analysis, every consumer re-implements its own dialect.

After the pattern

class Variable {
constructor(name) {
this.name = name;
}
interpret(env) {
return Boolean(env[this.name]);
}
}
class And {
constructor(left, right) {
this.left = left;
this.right = right;
}
interpret(env) {
return this.left.interpret(env) && this.right.interpret(env);
}
}
class Or {
constructor(left, right) {
this.left = left;
this.right = right;
}
interpret(env) {
return this.left.interpret(env) || this.right.interpret(env);
}
}
class Not {
constructor(operand) {
this.operand = operand;
}
interpret(env) {
return !this.operand.interpret(env);
}
}
const rule = new And(new Variable('verified'), new Not(new Variable('banned')));
const pass = rule.interpret(user);
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 5. The book's running example is a regular-expression matcher; this JavaScript adaptation uses a tiny boolean rule language (AND, OR, NOT, variables) because the AST is small enough to read in one screen and the structural-vs-string-DSL contrast is sharp.
Pressure
Human

String-DSL rules are Primitive Obsession at the worst level — the type system sees a string; the runtime expects a domain language. Every consumer either re-implements the parser or imports it; bugs in the embedded parser silently corrupt evaluation across every consumer in lockstep.

Agent

String DSLs are opaque to the type system. The agent reasoning about rule correctness must trace through the parser's logic at every consumer; partial verification produces misleading confidence. New operators add cascading edits the agent must verify across every parsing path.

Tradeoff
Human

Building the AST in client code is verbose: new And(new Variable('a'), new Or(...)) is harder to read than 'a AND (b OR c)'. The pattern shines when the system controls rule construction (rules come from a config file or builder); it strains when human-readable input is required and a parser is needed anyway.

Agent

AST construction in code is verbose and harder to read than the string DSL. The agent's reading cost on the construction site is higher; tracing a runtime error in an AND.interpret requires walking the recursive call chain through the tree, which can be deep.

Relief
Human

Operator semantics live in one class per operator; new operators are isolated changes; AST is statically typed and traversable. Static analysis (which variables does this rule depend on?) becomes a single recursive walk of the AST.

Agent

Per-rule edits are one-file; static traversal of 'rules using variable X' is a complete enumeration; type system enforces interpret-method existence on every rule class. The agent's verification budget on grammar changes is bounded and structurally provable.

Trap
Human

Grammars that grow beyond a handful of operators outgrow the pattern fast — every new operator adds a class, every binary operator with precedence needs explicit AST shape, and traversal logic accumulates. At that point reach for a real parser generator or a proper AST-walker library; don't keep adding interpret() methods to a sprawling class hierarchy.

Agent

Grammars growing past ~10 operators turn into class proliferation the agent navigates as a graph rather than a hierarchy. Visitor or AST-walker tooling becomes mandatory at that scale; without it, every traversal-style analysis requires editing every rule class — the cross-cutting problem the pattern was supposed to avoid.