Loading content...
The Interpreter Pattern is one tool in a rich landscape of approaches for processing and evaluating structured expressions. Understanding the alternatives is crucial—not only to recognize when they're better choices, but also to appreciate the Interpreter Pattern's specific strengths by contrast.
This page provides a comprehensive survey of alternatives, from simple data-driven approaches for trivial cases to sophisticated compiler infrastructure for complex languages. By the end, you'll have a complete mental map of the interpretation solution space and the wisdom to navigate it effectively.
By the end of this page, you will understand five major alternative approaches to the Interpreter Pattern: parser generators, embedded scripting languages, bytecode compilation, data-driven configuration, and existing DSL solutions. For each alternative, you'll learn when it's preferred, how to evaluate it against your requirements, and how to integrate it into your systems.
Before diving into specific alternatives, let's understand the full spectrum of approaches available for expression processing. Each approach occupies a different position on the complexity-performance-flexibility trade-off space.
1234567891011121314151617181920212223242526272829303132333435
┌─────────────────────────────────────────────────────────────────────────────┐│ INTERPRETATION SOLUTION SPECTRUM │├─────────────────────────────────────────────────────────────────────────────┤│ ││ COMPLEXITY OF IMPLEMENTATION ││ ═════════════════════════════ ││ ││ LOW MEDIUM HIGH ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ Data-Driven │ │ Interpreter │ │ Parser │ │ Custom │ ││ │ Config/Rules│ │ Pattern │ │ Generators │ │ Compiler │ ││ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ││ ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ Existing │ │ Embedded │ │ Bytecode │ ││ │ DSL Libs │ │ Scripting │ │ Compilation │ ││ └─────────────┘ └─────────────┘ └─────────────┘ ││ ││ ││ LANGUAGE COMPLEXITY SUPPORTED ││ ═════════════════════════════════ ││ ││ │ Approach │ Grammar Size │ Performance │ Flexibility ││ ├────────────────────────┼────────────────┼───────────────┼───────────────┤│ │ Data-Driven Config │ Trivial (~5) │ Fast │ Very Low ││ │ Existing DSL Libs │ Fixed │ Varies │ Limited ││ │ Interpreter Pattern │ Small (<20) │ Slow │ High ││ │ Embedded Scripting │ Any │ Medium │ Very High ││ │ Parser Generators │ Large (20+) │ Fast │ Very High ││ │ Bytecode Compilation │ Any │ Very Fast │ High ││ │ Custom Compiler │ Any │ Native │ Complete ││ │└─────────────────────────────────────────────────────────────────────────────┘Choosing the right position on the spectrum:
The right choice depends on your specific requirements:
Let's examine each major alternative in detail.
For very simple rule systems, you might not need a language at all. Data-driven approaches represent rules as structured data (JSON, YAML, database records) and use a fixed evaluator.
When data-driven approaches work:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
/** * A data-driven rule engine without the Interpreter Pattern. * Rules are structured data, not a language. */ // Rule definition as pure datainterface RuleDefinition { id: string; field: string; operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'matches'; value: any; errorMessage: string;} // Rules are stored as JSON/YAML/database recordsconst rules: RuleDefinition[] = [ { id: 'age-check', field: 'age', operator: 'gte', value: 18, errorMessage: 'Must be 18+' }, { id: 'email-present', field: 'email', operator: 'neq', value: '', errorMessage: 'Email required' }, { id: 'country-valid', field: 'country', operator: 'contains', value: 'US,UK,CA', errorMessage: 'Invalid country' },]; // Simple evaluator - no classes, just functionsclass DataDrivenRuleEngine { constructor(private rules: RuleDefinition[]) {} evaluate(data: Record<string, any>): { valid: boolean; errors: string[] } { const errors: string[] = []; for (const rule of this.rules) { const fieldValue = data[rule.field]; const satisfied = this.evaluateOperator(fieldValue, rule.operator, rule.value); if (!satisfied) { errors.push(rule.errorMessage); } } return { valid: errors.length === 0, errors }; } private evaluateOperator(fieldValue: any, operator: string, ruleValue: any): boolean { switch (operator) { case 'eq': return fieldValue === ruleValue; case 'neq': return fieldValue !== ruleValue; case 'gt': return fieldValue > ruleValue; case 'gte': return fieldValue >= ruleValue; case 'lt': return fieldValue < ruleValue; case 'lte': return fieldValue <= ruleValue; case 'contains': return ruleValue.split(',').includes(String(fieldValue)); case 'matches': return new RegExp(ruleValue).test(String(fieldValue)); default: throw new Error(`Unknown operator: ${operator}`); } }} // BENEFITS:// ✓ Simple to implement and understand// ✓ Rules are easily serializable (JSON-friendly)// ✓ No parser needed// ✓ Easy to build admin UI for rule creation // LIMITATIONS:// ✗ Cannot express compound conditions (A AND B)// ✗ Cannot express nested logic ((A OR B) AND C)// ✗ Fixed set of operators - adding means code changes// ✗ Limited expressiveness by designExtending data-driven with compound rules:
You can add limited composition while staying data-driven:
12345678910111213141516171819202122232425262728293031323334353637
// Enhanced rule definition supporting AND/OR at top levelinterface CompoundRuleDefinition { id: string; combinator: 'AND' | 'OR'; // How to combine conditions conditions: ConditionDefinition[]; errorMessage: string;} interface ConditionDefinition { field: string; operator: string; value: any;} const compoundRules: CompoundRuleDefinition[] = [ { id: 'premium-access', combinator: 'OR', conditions: [ { field: 'subscriptionTier', operator: 'eq', value: 'premium' }, { field: 'isStaff', operator: 'eq', value: true } ], errorMessage: 'Requires premium subscription or staff access' }, { id: 'adult-verification', combinator: 'AND', conditions: [ { field: 'age', operator: 'gte', value: 21 }, { field: 'idVerified', operator: 'eq', value: true } ], errorMessage: 'Must be 21+ with verified ID' }]; // Still data-driven, but with one level of composition// Beyond this level of complexity, consider moving to Interpreter PatternData-driven approaches work when rules follow a fixed template. The moment users need arbitrary nesting, recursive structures, or variable expressions—you've crossed into territory that benefits from the Interpreter Pattern or formal parsing.
Watch for these signals that you've outgrown data-driven: • Requests for nested AND/OR logic • Need for parentheses or grouping • Expressions referencing other expressions • User-defined variables or functions
Before building a custom interpreter, investigate whether an existing domain-specific language library solves your problem. Mature DSL libraries offer battle-tested parsing, evaluation, and often strong tooling.
The 'Use Before Build' principle:
Many 'custom language needs' are actually well-solved by existing tools. Consider these established DSLs:
| Domain | Existing DSL | Use Case Examples |
|---|---|---|
| Data querying | SQL, GraphQL | Database queries, API queries |
| Text pattern matching | Regular expressions | Validation, extraction, search |
| Document querying | XPath, CSS selectors, JSONPath | HTML/XML/JSON traversal |
| Math expressions | Math.js, expr-eval | Calculator, formula evaluation |
| Boolean logic | json-rules-engine, json-logic | Rule engines, validation |
| Template languages | Handlebars, Mustache, Liquid, Nunjucks | Email templates, dynamic content |
| Scripting | Lua, JavaScript (V8), Python | Plugins, extensions, automation |
| Configuration | HCL (HashiCorp), Cue, Dhall | Infrastructure as code, typed config |
| Access control | OPA Rego, Cedar (AWS) | Authorization policies |
| Workflow | BPMN, Workflow DSLs | Business process automation |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
/** * Using json-logic-js instead of building a custom interpreter. * JSON Logic is a standard for portable, safe boolean logic. */ import jsonLogic from 'json-logic-js'; // Rules as JSON (can be stored in database, sent via API)const premiumAccessRule = { "or": [ { "==": [{ "var": "subscription" }, "premium"] }, { "==": [{ "var": "isStaff" }, true] } ]}; const complexValidation = { "and": [ { ">=": [{ "var": "age" }, 18] }, { "or": [ { "==": [{ "var": "country" }, "US"] }, { "==": [{ "var": "verified" }, true] } ] } ]}; // Evaluation is simpleconst userData = { subscription: 'basic', isStaff: false, age: 25, country: 'UK', verified: true }; const hasPremiumAccess = jsonLogic.apply(premiumAccessRule, userData);// false (not premium, not staff) const passesValidation = jsonLogic.apply(complexValidation, userData);// true (age >= 18 AND verified == true) // BENEFITS OF EXISTING DSL:// ✓ No parsing code to write// ✓ Battle-tested implementation// ✓ Standard format (interoperable)// ✓ Security: controlled operation set// ✓ Portable (JSON works everywhere) // LIMITATIONS:// ✗ Syntax is less human-friendly// ✗ Limited to library's capabilities// ✗ May not match your mental model123456789101112131415161718192021222324252627282930313233343536
/** * Using math.js for mathematical expression evaluation. * Mature library with full expression parsing and evaluation. */ import { evaluate, parse, compile } from 'mathjs'; // Direct evaluationconst result1 = evaluate('2 * (3 + 4)'); // 14 // With variables (scope)const result2 = evaluate('price * quantity * (1 + taxRate)', { price: 100, quantity: 5, taxRate: 0.08}); // 540 // Compile for repeated evaluation (performance optimization)const formula = compile('principal * (1 + rate/12)^(12*years)'); const scenarios = [ { principal: 10000, rate: 0.05, years: 1 }, { principal: 10000, rate: 0.05, years: 5 }, { principal: 10000, rate: 0.05, years: 10 },]; const results = scenarios.map(scope => formula.evaluate(scope));// [10511.619..., 12833.586..., 16470.094...] // Parse to AST for analysis/transformationconst ast = parse('x^2 + 2*x + 1');console.log(ast.toString()); // "x ^ 2 + 2 * x + 1"console.log(ast.toTex()); // "x^{2}+2 x+1" (LaTeX) // PERFECT FOR: Calculators, spreadsheet formulas, scientific computing// NOT FOR: Boolean logic, conditionals, custom operatorsWhen evaluating an existing DSL library:
• Coverage — Does it support all your required operations? • Extensibility — Can you add custom functions/operators if needed? • Security — Is it safe for user input? (sandboxing, resource limits) • Performance — Does it meet your throughput requirements? • Maintenance — Is the library actively maintained? Good community? • Documentation — Is it well-documented with examples? • Error handling — Does it provide good error messages for invalid input?
When your language complexity exceeds what the Interpreter Pattern handles comfortably (20+ expression types, complex precedence, sophisticated syntax), parser generators become the appropriate tool.
What parser generators provide:
Popular parser generators:
| Tool | Language | Grammar Type | Best For |
|---|---|---|---|
| ANTLR | Java, Python, JS, C#, etc. | LL(*) | Complex languages, tooling |
| PEG.js / Peggy | JavaScript | PEG | Browser DSLs, templates |
| tree-sitter | C (with bindings) | GLR | IDE/editor support |
| Nearley | JavaScript | Earley | Ambiguous grammars |
| Ohm | JavaScript | PEG-like | Prototyping, learning |
| Bison/Flex | C/C++ | LALR | Traditional compilers |
| Lark | Python | LALR/Earley | Python applications |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
/** * ANTLR Grammar for a rule expression language. * Save as RuleExpr.g4 */ grammar RuleExpr; // Parser Rulesexpression : orExpression ; orExpression : andExpression (OR andExpression)* ; andExpression : unaryExpression (AND unaryExpression)* ; unaryExpression : NOT unaryExpression | primaryExpression ; primaryExpression : LPAREN expression RPAREN | comparison | functionCall | literal ; comparison : identifier comparisonOp value ; comparisonOp : EQ | NEQ | LT | GT | LTE | GTE | MATCHES | IN ; functionCall : IDENTIFIER LPAREN (expression (COMMA expression)*)? RPAREN ; literal : NUMBER | STRING | TRUE | FALSE | identifier ; identifier : IDENTIFIER (DOT IDENTIFIER)* ; value : NUMBER | STRING | TRUE | FALSE | identifier | arrayLiteral ; arrayLiteral : LBRACKET (value (COMMA value)*)? RBRACKET ; // Lexer RulesAND : 'AND' | 'and' | '&&' ;OR : 'OR' | 'or' | '||' ;NOT : 'NOT' | 'not' | '!' ;TRUE : 'true' | 'TRUE' ;FALSE : 'false' | 'FALSE' ;EQ : '==' | '=' ;NEQ : '!=' | '<>' ;LT : '<' ;GT : '>' ;LTE : '<=' ;GTE : '>=' ;MATCHES : 'MATCHES' | 'matches' ;IN : 'IN' | 'in' ; LPAREN : '(' ;RPAREN : ')' ;LBRACKET: '[' ;RBRACKET: ']' ;COMMA : ',' ;DOT : '.' ; IDENTIFIER : [a-zA-Z_][a-zA-Z0-9_]* ;NUMBER : '-'? [0-9]+ ('.' [0-9]+)? ;STRING : '"' (~["])* '"' | '\'' (~['])* '\'' ; WS : [ \t\r\n]+ -> skip ;1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
/** * Using ANTLR-generated parser in TypeScript. * After running: antlr4 -Dlanguage=TypeScript RuleExpr.g4 */ import { CharStream, CommonTokenStream } from 'antlr4';import { RuleExprLexer } from './generated/RuleExprLexer';import { RuleExprParser } from './generated/RuleExprParser';import { RuleExprVisitor } from './generated/RuleExprVisitor'; // Custom visitor to evaluate expressionsclass RuleEvaluator extends RuleExprVisitor<any> { constructor(private context: Record<string, any>) { super(); } visitOrExpression(ctx: any): boolean { const children = ctx.andExpression(); return children.some((child: any) => this.visit(child)); } visitAndExpression(ctx: any): boolean { const children = ctx.unaryExpression(); return children.every((child: any) => this.visit(child)); } visitComparison(ctx: any): boolean { const field = this.visit(ctx.identifier()); const operator = ctx.comparisonOp().getText(); const value = this.visit(ctx.value()); const fieldValue = this.resolveField(field); return this.compare(fieldValue, operator, value); } // ... more visit methods for each grammar rule private resolveField(path: string): any { return path.split('.').reduce( (obj, key) => obj?.[key], this.context ); } private compare(left: any, op: string, right: any): boolean { switch (op) { case '==': case '=': return left === right; case '!=': case '<>': return left !== right; case '<': return left < right; case '>': return left > right; case '<=': return left <= right; case '>=': return left >= right; default: throw new Error(`Unknown operator: ${op}`); } }} // Usagefunction evaluateRule(ruleText: string, data: Record<string, any>): boolean { const charStream = CharStream.fromString(ruleText); const lexer = new RuleExprLexer(charStream); const tokenStream = new CommonTokenStream(lexer); const parser = new RuleExprParser(tokenStream); const tree = parser.expression(); const evaluator = new RuleEvaluator(data); return evaluator.visit(tree);} // Now we can parse complex expressionsconst result = evaluateRule( '(age >= 18 AND country IN ["US", "UK", "CA"]) OR isVerified == true', { age: 25, country: 'UK', isVerified: false }); // trueParser generators add significant infrastructure:
• Build complexity — Need to integrate grammar compilation into build • Learning curve — Team must learn the specific tool and grammar notation • Generated code — Often verbose; maintenance happens in grammar, not code • Debugging — Errors in grammar syntax can be cryptic • Dependencies — Runtime libraries required for parsing
This investment pays off for complex languages but is overkill for simple ones.
For maximum flexibility, you can embed a full scripting language into your application. This gives users a complete programming language while you control the execution environment.
When embedding makes sense:
| Language | Host Languages | Sandboxing | Performance | Use Case |
|---|---|---|---|---|
| Lua | C, C++, Java, C# | Excellent | Very Fast | Game modding, plugins |
| JavaScript (V8/QuickJS) | C++, Rust | Good | Fast | Browser extensions, serverless |
| Python (embedded) | C, C++ | Challenging | Medium | Scientific, data analysis |
| Wren | C | Good | Fast | Game scripting |
| Groovy | JVM | Moderate | Medium | Build scripts, DSLs |
| mruby | C | Good | Fast | Embedded systems |
| Rhai | Rust | Excellent | Fast | Rust applications |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
/** * Embedding JavaScript for user-defined transformations. * Using quickjs-emscripten for sandboxed execution. */ import { getQuickJS, QuickJSContext } from 'quickjs-emscripten'; class ScriptEngine { private qjs: QuickJSContext | null = null; async initialize() { const QuickJS = await getQuickJS(); this.qjs = QuickJS.newContext(); // Expose controlled API to scripts this.exposeAPI(); } private exposeAPI() { if (!this.qjs) return; // Expose a safe 'context' object with data this.qjs.evalCode(` const api = { log: (msg) => { /* sandboxed logging */ }, math: { round: Math.round, floor: Math.floor, ceil: Math.ceil, abs: Math.abs, }, // Only expose what you want users to access }; `); } evaluate(script: string, context: Record<string, any>): any { if (!this.qjs) throw new Error('Engine not initialized'); // Inject context as JSON (safe serialization) const contextJson = JSON.stringify(context); // Wrap user script in safe execution context const wrappedScript = ` const data = ${contextJson}; (${script}) `; // Execute with resource limits const result = this.qjs.evalCode(wrappedScript); if (result.error) { const error = this.qjs.dump(result.error); result.error.dispose(); throw new Error(`Script error: ${error}`); } const value = this.qjs.dump(result.value); result.value.dispose(); return value; } dispose() { this.qjs?.dispose(); }} // Usageconst engine = new ScriptEngine();await engine.initialize(); // User-defined transformation scriptconst userScript = ` const { items, taxRate } = data; const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0); const tax = subtotal * taxRate; ({ subtotal, tax, total: subtotal + tax })`; const result = engine.evaluate(userScript, { items: [ { price: 10, qty: 2 }, { price: 25, qty: 1 } ], taxRate: 0.08}); console.log(result); // { subtotal: 45, tax: 3.6, total: 48.6 } engine.dispose();123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
/** * Lua embedding pattern for game mod support. * Conceptual example - implementation varies by Lua binding library. */ interface LuaEngine { doString(script: string): any; setGlobal(name: string, value: any): void; getGlobal(name: string): any; callFunction(name: string, ...args: any[]): any;} class GameModSystem { private lua: LuaEngine; private loadedMods: Map<string, ModDefinition> = new Map(); constructor(lua: LuaEngine) { this.lua = lua; this.registerGameAPI(); } private registerGameAPI() { // Expose controlled game functions to Lua this.lua.setGlobal('game', { spawnEnemy: (type: string, x: number, y: number) => { // Validated spawn, can't spawn outside allowed areas if (this.isValidSpawnLocation(x, y)) { return this.gameWorld.spawn(type, x, y); } throw new Error('Invalid spawn location'); }, giveItem: (playerId: string, itemId: string, count: number) => { // Rate-limited, prevents economy exploits if (count <= 10 && this.isValidItem(itemId)) { return this.gameWorld.giveItem(playerId, itemId, count); } throw new Error('Invalid item grant'); }, playSound: (soundId: string) => { // Only allowed sounds if (this.allowedSounds.has(soundId)) { this.audioSystem.play(soundId); } }, // Read-only access to game state getPlayerPosition: (playerId: string) => { return this.gameWorld.getPlayer(playerId)?.position; }, }); } loadMod(modId: string, luaScript: string): void { // Validate script before loading const validationResult = this.validateModScript(luaScript); if (!validationResult.valid) { throw new Error(`Invalid mod: ${validationResult.error}`); } // Execute mod registration this.lua.doString(` local mod = {} ${luaScript} _G.mods = _G.mods or {} _G.mods["${modId}"] = mod `); this.loadedMods.set(modId, { id: modId, loaded: true }); } triggerEvent(eventName: string, eventData: any): void { // Notify all mods of game events for (const [modId, mod] of this.loadedMods) { try { this.lua.doString(` local mod = _G.mods["${modId}"] if mod.on${eventName} then mod.on${eventName}(${JSON.stringify(eventData)}) end `); } catch (error) { console.error(`Mod ${modId} error on ${eventName}:`, error); // Isolate mod failures - don't crash the game } } }} // Mod authors write Lua scripts like:/*mod.name = "Bonus Spawns"mod.version = "1.0" function mod.onPlayerEnterZone(event) if event.zone == "dungeon_entrance" then game.spawnEnemy("skeleton", event.x + 10, event.y) game.spawnEnemy("skeleton", event.x - 10, event.y) endend function mod.onPlayerDefeatBoss(event) game.giveItem(event.playerId, "legendary_sword", 1) game.playSound("victory_fanfare")end*/Embedding scripting languages introduces significant security risks:
• Sandboxing — Prevent access to filesystem, network, system APIs • Resource limits — Cap CPU time, memory usage, recursion depth • API surface — Only expose intentionally allowed functions • Input validation — Validate all data passed between host and script • Error isolation — Script failures must not crash the host application
Without proper sandboxing, user scripts become arbitrary code execution vulnerabilities.
When performance matters but you still need runtime flexibility, bytecode compilation offers a middle ground. Instead of interpreting an AST directly, you compile expressions to a simple instruction set that executes faster.
The bytecode approach:
This amortizes parsing and compilation cost over many evaluations.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
/** * Simple bytecode VM for expression evaluation. * Demonstrates the concept - production VMs are more sophisticated. */ // Instruction set for our expression languageenum OpCode { // Stack operations PUSH_CONST, // Push constant value PUSH_VAR, // Push variable from context POP, // Discard top of stack // Comparisons CMP_EQ, // == CMP_NEQ, // != CMP_LT, // < CMP_GT, // > CMP_LTE, // <= CMP_GTE, // >= // Logic AND, // Logical AND OR, // Logical OR NOT, // Logical NOT // Arithmetic ADD, SUB, MUL, DIV, // Control JUMP, // Unconditional jump JUMP_IF_FALSE, // Conditional jump HALT, // End execution} interface Instruction { op: OpCode; arg?: any;} // Compiled bytecode programclass BytecodeProgram { constructor( public instructions: Instruction[], public constants: any[], public variableNames: string[] ) {}} // Bytecode virtual machineclass ExpressionVM { private stack: any[] = []; private ip = 0; // Instruction pointer execute(program: BytecodeProgram, context: Record<string, any>): any { this.stack = []; this.ip = 0; while (this.ip < program.instructions.length) { const instr = program.instructions[this.ip]; switch (instr.op) { case OpCode.PUSH_CONST: this.stack.push(program.constants[instr.arg]); break; case OpCode.PUSH_VAR: const varName = program.variableNames[instr.arg]; this.stack.push(this.resolveVariable(varName, context)); break; case OpCode.CMP_EQ: this.binaryOp((a, b) => a === b); break; case OpCode.CMP_GTE: this.binaryOp((a, b) => a >= b); break; case OpCode.AND: this.binaryOp((a, b) => a && b); break; case OpCode.OR: this.binaryOp((a, b) => a || b); break; case OpCode.NOT: this.stack.push(!this.stack.pop()); break; case OpCode.JUMP_IF_FALSE: if (!this.stack.pop()) { this.ip = instr.arg; continue; } break; case OpCode.HALT: return this.stack.pop(); } this.ip++; } return this.stack.pop(); } private binaryOp(fn: (a: any, b: any) => any) { const b = this.stack.pop(); const a = this.stack.pop(); this.stack.push(fn(a, b)); } private resolveVariable(path: string, context: Record<string, any>): any { return path.split('.').reduce((obj, key) => obj?.[key], context); }} // Compiler from AST to bytecodeclass BytecodeCompiler { private instructions: Instruction[] = []; private constants: any[] = []; private variableNames: string[] = []; compile(ast: Expression): BytecodeProgram { this.instructions = []; this.constants = []; this.variableNames = []; this.compileNode(ast); this.instructions.push({ op: OpCode.HALT }); return new BytecodeProgram( this.instructions, this.constants, this.variableNames ); } private compileNode(node: Expression) { // Dispatch based on node type if (node instanceof NumberLiteral) { const idx = this.addConstant(node.value); this.emit(OpCode.PUSH_CONST, idx); } else if (node instanceof VariableExpression) { const idx = this.addVariable(node.name); this.emit(OpCode.PUSH_VAR, idx); } else if (node instanceof AndExpression) { // Compile with short-circuit evaluation this.compileNode(node.left); const jumpIdx = this.emit(OpCode.JUMP_IF_FALSE, 0); // Placeholder this.compileNode(node.right); this.emit(OpCode.AND); // Patch jump target this.instructions[jumpIdx].arg = this.instructions.length; } // ... other node types } private emit(op: OpCode, arg?: any): number { this.instructions.push({ op, arg }); return this.instructions.length - 1; } private addConstant(value: any): number { const existing = this.constants.indexOf(value); if (existing >= 0) return existing; this.constants.push(value); return this.constants.length - 1; } private addVariable(name: string): number { const existing = this.variableNames.indexOf(name); if (existing >= 0) return existing; this.variableNames.push(name); return this.variableNames.length - 1; }} // USAGE: Compile once, execute many timesconst compiler = new BytecodeCompiler();const vm = new ExpressionVM(); const ast = parse('age >= 18 AND verified == true'); // Parse onceconst bytecode = compiler.compile(ast); // Compile once // Execute fast, many timesfor (const user of millionUsers) { const result = vm.execute(bytecode, user); // O(instructions), not O(AST)}Bytecode compilation is worthwhile when:
• Expressions are evaluated thousands+ times with different data • The same expression is reused across requests/sessions • Startup time for compilation is acceptable • Performance profiling shows AST interpretation as a bottleneck
For one-off evaluations, the compilation cost doesn't amortize.
To synthesize everything we've covered, here's a comprehensive comparison matrix for all the approaches to expression evaluation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
┌────────────────────────────────────────────────────────────────────────────────┐│ EXPRESSION EVALUATION APPROACHES COMPARISON │├────────────────────────────────────────────────────────────────────────────────┤│ ││ │ Data- │ Existing │ Inter- │ Parser │ Embedded │ Byte- ││ CRITERIA │ Driven │ DSL │ preter │ Gen │ Scripting│ code ││ │ │ │ Pattern │ │ │ ││ ═════════════════│═════════│══════════│═════════│═════════│══════════│═══════││ │ │ │ │ │ │ ││ Grammar Size │ Trivial │ Fixed │ Small │ Large │ Complete │ Any ││ (expression │ (~5) │ │ (<20) │ (20+) │ PL │ ││ types) │ │ │ │ │ │ ││ │ │ │ │ │ │ ││ Impl. Effort │ ★ │ ★★ │ ★★★ │ ★★★★ │ ★★★ │ ★★★★★ ││ │ │ │ │ │ │ ││ Performance │ ★★★★★ │ ★★★ │ ★★ │ ★★★★ │ ★★★ │ ★★★★★ ││ │ │ │ │ │ │ ││ Flexibility │ ★ │ ★★ │ ★★★★ │ ★★★★★ │ ★★★★★ │ ★★★★ ││ │ │ │ │ │ │ ││ Extensibility │ ★★ │ ★ │ ★★★★★ │ ★★★★★ │ (N/A) │ ★★★★ ││ │ │ │ │ │ │ ││ Learning Curve │ ★ │ ★★ │ ★★★ │ ★★★★ │ ★★ │ ★★★★★ ││ │ │ │ │ │ │ ││ Maintenance │ ★ │ ★★ │ ★★★ │ ★★★★ │ ★★★ │ ★★★★★ ││ Burden │ │ │ │ │ │ ││ │ │ │ │ │ │ ││ Tooling Built-in │ ★ │ ★★★★★ │ ★ │ ★★★★ │ ★★★★ │ ★ ││ │ │ │ │ │ │ ││ Security │ ★★★★★ │ ★★★★ │ ★★★★ │ ★★★ │ ★★ │ ★★★★ ││ (sandbox-able) │ │ │ │ │ │ ││ │ │ │ │ │ │ ││ ═════════════════│═════════│══════════│═════════│═════════│══════════│═══════││ │ │ │ │ │ │ ││ BEST FOR: │ │ │ │ │ │ ││ │ │ │ │ │ │ ││ • Simple rules │ ✓ │ │ │ │ │ ││ • Standard DSL │ │ ✓ │ │ │ │ ││ • Custom simple │ │ │ ✓ │ │ │ ││ • Complex lang │ │ │ │ ✓ │ │ ││ • Full scripting │ │ │ │ │ ✓ │ ││ • High perf │ │ │ │ │ │ ✓ ││ │ │ │ │ │ │ │└────────────────────────────────────────────────────────────────────────────────┘ ★ = Low/Easy ★★★ = Medium ★★★★★ = High/DifficultWe've explored the full landscape of alternatives to the Interpreter Pattern. Here are the key takeaways to guide your architectural decisions:
Module complete:
You've now completed the comprehensive overview of the Interpreter Pattern. You understand:
With this knowledge, you can make informed architectural decisions about expression evaluation in your systems, choosing the right tool for your specific requirements rather than defaulting to any single approach.
Congratulations! You've mastered the Interpreter Pattern. You understand its problem domain, solution architecture, appropriate use cases, and the rich landscape of alternatives. This knowledge equips you to make wise decisions about expression evaluation in your systems—knowing when the Interpreter Pattern shines and when other approaches serve you better.