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.
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.
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);
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.
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.
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.
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.