Key Takeaways

  • Performance: Achieved 100x speedup by compiling rules once vs parsing every evaluation
  • Business Impact: Reduced rule change cycle from 2 weeks to 2 minutes
  • Scale: Processing 10M rule evaluations per day with minimal overhead
  • Empowerment: Non-developers can now modify complex business logic independently

The Business Rules That Ate Our Codebase

January 2023. Our fraud detection system has 347 hardcoded rules scattered across 50,000 lines of Go code.

"Can we change the minimum transaction amount for premium users?" asks the product manager.

"Sure," I say. "That'll be a 2-week sprint, full testing cycle, and production deployment."

For changing one number.

That's when we decided to build our own DSL (Domain Specific Language). Let business people write business rules.

Today: Business rules change in minutes, not weeks. 10M rule evaluations per day. Zero developer involvement for rule changes.

DSL Design: What We Actually Needed

Before writing any code, we spent 2 weeks understanding our domain. Here's what we found:

Current hardcoded rules looked like:

// Scattered throughout the codebase
if user.Plan == "premium" && transaction.Amount > 10000 && user.RiskScore < 0.3 {
    return AllowTransaction
}

if transaction.Country == "US" && user.Age < 18 {
    return RequireParentalConsent  
}

if time.Since(user.LastTransaction) < 5*time.Minute && transaction.Amount > user.AverageAmount*3 {
    return FlagForReview
}

Business people wanted to write:

rule "allow_premium_large_transactions" {
    when {
        user.plan == "premium" AND
        transaction.amount > 10000 AND
        user.risk_score < 0.3
    }
    then {
        allow_transaction()
    }
}

rule "require_parental_consent" {
    when {
        transaction.country == "US" AND
        user.age < 18
    }
    then {
        require_parental_consent()
    }
}

The Lexer: Turning Text into Tokens

Every DSL starts with a lexer that breaks text into tokens:

package lexer

import (
    "fmt"
    "strings"
    "unicode"
)

type TokenType int

const (
    // Literals
    IDENTIFIER TokenType = iota
    NUMBER
    STRING
    BOOLEAN
    
    // Operators
    PLUS
    MINUS
    MULTIPLY
    DIVIDE
    MODULO
    
    // Comparison
    EQUAL
    NOT_EQUAL
    LESS_THAN
    GREATER_THAN
    LESS_EQUAL
    GREATER_EQUAL
    
    // Logical
    AND
    OR
    NOT
    
    // Keywords
    RULE
    WHEN
    THEN
    IF
    ELSE
    
    // Delimiters
    LPAREN
    RPAREN
    LBRACE
    RBRACE
    DOT
    COMMA
    SEMICOLON
    
    // Special
    EOF
    ILLEGAL
)

type Token struct {
    Type     TokenType
    Literal  string
    Position int
}

type Lexer struct {
    input        string
    position     int  // current position in input (points to current char)
    readPosition int  // current reading position in input (after current char)
    ch           byte // current char under examination
}

func New(input string) *Lexer {
    l := &Lexer{input: input}
    l.readChar()
    return l
}

func (l *Lexer) NextToken() Token {
    var tok Token
    
    l.skipWhitespace()
    
    switch l.ch {
    case '+':
        tok = Token{Type: PLUS, Literal: string(l.ch), Position: l.position}
    case '-':
        tok = Token{Type: MINUS, Literal: string(l.ch), Position: l.position}
    case '*':
        tok = Token{Type: MULTIPLY, Literal: string(l.ch), Position: l.position}
    case '/':
        tok = Token{Type: DIVIDE, Literal: string(l.ch), Position: l.position}
    case '%':
        tok = Token{Type: MODULO, Literal: string(l.ch), Position: l.position}
    case '(':
        tok = Token{Type: LPAREN, Literal: string(l.ch), Position: l.position}
    case ')':
        tok = Token{Type: RPAREN, Literal: string(l.ch), Position: l.position}
    case '{':
        tok = Token{Type: LBRACE, Literal: string(l.ch), Position: l.position}
    case '}':
        tok = Token{Type: RBRACE, Literal: string(l.ch), Position: l.position}
    case '.':
        tok = Token{Type: DOT, Literal: string(l.ch), Position: l.position}
    case ',':
        tok = Token{Type: COMMA, Literal: string(l.ch), Position: l.position}
    case ';':
        tok = Token{Type: SEMICOLON, Literal: string(l.ch), Position: l.position}
    case '"':
        tok.Type = STRING
        tok.Literal = l.readString()
        tok.Position = l.position
    case '=':
        if l.peekChar() == '=' {
            ch := l.ch
            l.readChar()
            tok = Token{Type: EQUAL, Literal: string(ch) + string(l.ch), Position: l.position}
        } else {
            tok = Token{Type: ILLEGAL, Literal: string(l.ch), Position: l.position}
        }
    case '!':
        if l.peekChar() == '=' {
            ch := l.ch
            l.readChar()
            tok = Token{Type: NOT_EQUAL, Literal: string(ch) + string(l.ch), Position: l.position}
        } else {
            tok = Token{Type: NOT, Literal: string(l.ch), Position: l.position}
        }
    case '<':
        if l.peekChar() == '=' {
            ch := l.ch
            l.readChar()
            tok = Token{Type: LESS_EQUAL, Literal: string(ch) + string(l.ch), Position: l.position}
        } else {
            tok = Token{Type: LESS_THAN, Literal: string(l.ch), Position: l.position}
        }
    case '>':
        if l.peekChar() == '=' {
            ch := l.ch
            l.readChar()
            tok = Token{Type: GREATER_EQUAL, Literal: string(ch) + string(l.ch), Position: l.position}
        } else {
            tok = Token{Type: GREATER_THAN, Literal: string(l.ch), Position: l.position}
        }
    case 0:
        tok.Literal = ""
        tok.Type = EOF
        tok.Position = l.position
    default:
        if isLetter(l.ch) {
            tok.Literal = l.readIdentifier()
            tok.Type = lookupIdent(tok.Literal)
            tok.Position = l.position
            return tok // early return to avoid l.readChar()
        } else if isDigit(l.ch) {
            tok.Type = NUMBER
            tok.Literal = l.readNumber()
            tok.Position = l.position
            return tok // early return to avoid l.readChar()
        } else {
            tok = Token{Type: ILLEGAL, Literal: string(l.ch), Position: l.position}
        }
    }
    
    l.readChar()
    return tok
}

func (l *Lexer) readChar() {
    if l.readPosition >= len(l.input) {
        l.ch = 0
    } else {
        l.ch = l.input[l.readPosition]
    }
    l.position = l.readPosition
    l.readPosition++
}

func (l *Lexer) peekChar() byte {
    if l.readPosition >= len(l.input) {
        return 0
    }
    return l.input[l.readPosition]
}

func (l *Lexer) readIdentifier() string {
    position := l.position
    for isLetter(l.ch) || isDigit(l.ch) || l.ch == '_' {
        l.readChar()
    }
    return l.input[position:l.position]
}

func (l *Lexer) readNumber() string {
    position := l.position
    for isDigit(l.ch) {
        l.readChar()
    }
    
    // Handle decimal numbers
    if l.ch == '.' && isDigit(l.peekChar()) {
        l.readChar()
        for isDigit(l.ch) {
            l.readChar()
        }
    }
    
    return l.input[position:l.position]
}

func (l *Lexer) readString() string {
    position := l.position + 1
    for {
        l.readChar()
        if l.ch == '"' || l.ch == 0 {
            break
        }
    }
    return l.input[position:l.position]
}

func (l *Lexer) skipWhitespace() {
    for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
        l.readChar()
    }
}

func isLetter(ch byte) bool {
    return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z'
}

func isDigit(ch byte) bool {
    return '0' <= ch && ch <= '9'
}

var keywords = map[string]TokenType{
    "rule":  RULE,
    "when":  WHEN,
    "then":  THEN,
    "if":    IF,
    "else":  ELSE,
    "AND":   AND,
    "OR":    OR,
    "NOT":   NOT,
    "true":  BOOLEAN,
    "false": BOOLEAN,
}

func lookupIdent(ident string) TokenType {
    if tok, ok := keywords[ident]; ok {
        return tok
    }
    return IDENTIFIER
}

The Parser: Building an Abstract Syntax Tree

The parser takes tokens and builds an AST (Abstract Syntax Tree):

package parser

import (
    "fmt"
    "strconv"
)

// AST Node interfaces
type Node interface {
    String() string
}

type Statement interface {
    Node
    statementNode()
}

type Expression interface {
    Node
    expressionNode()
}

// Program is the root of our AST
type Program struct {
    Rules []Statement
}

func (p *Program) String() string {
    var out strings.Builder
    for _, stmt := range p.Rules {
        out.WriteString(stmt.String())
    }
    return out.String()
}

// Rule statement
type RuleStatement struct {
    Name      string
    Condition Expression
    Actions   []Statement
}

func (rs *RuleStatement) statementNode() {}
func (rs *RuleStatement) String() string {
    return fmt.Sprintf("rule \"%s\" { when { %s } then { ... } }", 
        rs.Name, rs.Condition.String())
}

// Expressions
type BinaryExpression struct {
    Left     Expression
    Operator string
    Right    Expression
}

func (be *BinaryExpression) expressionNode() {}
func (be *BinaryExpression) String() string {
    return fmt.Sprintf("(%s %s %s)", be.Left.String(), be.Operator, be.Right.String())
}

type FieldAccess struct {
    Object string
    Field  string
}

func (fa *FieldAccess) expressionNode() {}
func (fa *FieldAccess) String() string {
    return fmt.Sprintf("%s.%s", fa.Object, fa.Field)
}

type Literal struct {
    Value interface{}
}

func (l *Literal) expressionNode() {}
func (l *Literal) String() string {
    return fmt.Sprintf("%v", l.Value)
}

type FunctionCall struct {
    Name string
    Args []Expression
}

func (fc *FunctionCall) expressionNode() {}
func (fc *FunctionCall) String() string {
    return fmt.Sprintf("%s(...)", fc.Name)
}

// Parser
type Parser struct {
    lexer *Lexer
    
    curToken  Token
    peekToken Token
    
    errors []string
}

func New(lexer *Lexer) *Parser {
    p := &Parser{
        lexer:  lexer,
        errors: []string{},
    }
    
    // Read two tokens, so curToken and peekToken are both set
    p.nextToken()
    p.nextToken()
    
    return p
}

func (p *Parser) nextToken() {
    p.curToken = p.peekToken
    p.peekToken = p.lexer.NextToken()
}

func (p *Parser) ParseProgram() *Program {
    program := &Program{}
    program.Rules = []Statement{}
    
    for p.curToken.Type != EOF {
        stmt := p.parseStatement()
        if stmt != nil {
            program.Rules = append(program.Rules, stmt)
        }
        p.nextToken()
    }
    
    return program
}

func (p *Parser) parseStatement() Statement {
    switch p.curToken.Type {
    case RULE:
        return p.parseRuleStatement()
    default:
        return nil
    }
}

func (p *Parser) parseRuleStatement() *RuleStatement {
    stmt := &RuleStatement{}
    
    if !p.expectPeek(STRING) {
        return nil
    }
    
    stmt.Name = p.curToken.Literal
    
    if !p.expectPeek(LBRACE) {
        return nil
    }
    
    if !p.expectPeek(WHEN) {
        return nil
    }
    
    if !p.expectPeek(LBRACE) {
        return nil
    }
    
    p.nextToken()
    stmt.Condition = p.parseExpression(LOWEST)
    
    if !p.expectPeek(RBRACE) {
        return nil
    }
    
    if !p.expectPeek(THEN) {
        return nil
    }
    
    if !p.expectPeek(LBRACE) {
        return nil
    }
    
    // Parse actions (simplified)
    stmt.Actions = []Statement{}
    
    if !p.expectPeek(RBRACE) {
        return nil
    }
    
    if !p.expectPeek(RBRACE) {
        return nil
    }
    
    return stmt
}

// Operator precedence
const (
    _ int = iota
    LOWEST
    LOGICAL_OR   // OR
    LOGICAL_AND  // AND
    EQUALS       // ==
    LESSGREATER  // > or <
    SUM          // +
    PRODUCT      // *
    PREFIX       // -X or !X
    CALL         // myFunction(X)
    INDEX        // array[index]
)

var precedences = map[TokenType]int{
    OR:            LOGICAL_OR,
    AND:           LOGICAL_AND,
    EQUAL:         EQUALS,
    NOT_EQUAL:     EQUALS,
    LESS_THAN:     LESSGREATER,
    GREATER_THAN:  LESSGREATER,
    LESS_EQUAL:    LESSGREATER,
    GREATER_EQUAL: LESSGREATER,
    PLUS:          SUM,
    MINUS:         SUM,
    DIVIDE:        PRODUCT,
    MULTIPLY:      PRODUCT,
    MODULO:        PRODUCT,
    LPAREN:        CALL,
}

type (
    prefixParseFn func() Expression
    infixParseFn  func(Expression) Expression
)

func (p *Parser) parseExpression(precedence int) Expression {
    prefix := p.prefixParseFns[p.curToken.Type]
    if prefix == nil {
        p.noPrefixParseFnError(p.curToken.Type)
        return nil
    }
    
    leftExp := prefix()
    
    for p.peekToken.Type != SEMICOLON && precedence < p.peekPrecedence() {
        infix := p.infixParseFns[p.peekToken.Type]
        if infix == nil {
            return leftExp
        }
        
        p.nextToken()
        leftExp = infix(leftExp)
    }
    
    return leftExp
}

func (p *Parser) parseBinaryExpression(left Expression) Expression {
    expression := &BinaryExpression{
        Left:     left,
        Operator: p.curToken.Literal,
    }
    
    precedence := p.curPrecedence()
    p.nextToken()
    expression.Right = p.parseExpression(precedence)
    
    return expression
}

func (p *Parser) parseFieldAccess() Expression {
    if p.curToken.Type != IDENTIFIER {
        return nil
    }
    
    object := p.curToken.Literal
    
    if p.peekToken.Type != DOT {
        // Simple identifier
        return &FieldAccess{Object: "", Field: object}
    }
    
    p.nextToken() // consume DOT
    
    if !p.expectPeek(IDENTIFIER) {
        return nil
    }
    
    field := p.curToken.Literal
    
    return &FieldAccess{Object: object, Field: field}
}

func (p *Parser) parseLiteral() Expression {
    switch p.curToken.Type {
    case NUMBER:
        return p.parseNumberLiteral()
    case STRING:
        return &Literal{Value: p.curToken.Literal}
    case BOOLEAN:
        return p.parseBooleanLiteral()
    default:
        return nil
    }
}

func (p *Parser) parseNumberLiteral() Expression {
    lit := &Literal{}
    
    if strings.Contains(p.curToken.Literal, ".") {
        value, err := strconv.ParseFloat(p.curToken.Literal, 64)
        if err != nil {
            msg := fmt.Sprintf("could not parse %q as float", p.curToken.Literal)
            p.errors = append(p.errors, msg)
            return nil
        }
        lit.Value = value
    } else {
        value, err := strconv.ParseInt(p.curToken.Literal, 0, 64)
        if err != nil {
            msg := fmt.Sprintf("could not parse %q as integer", p.curToken.Literal)
            p.errors = append(p.errors, msg)
            return nil
        }
        lit.Value = value
    }
    
    return lit
}

func (p *Parser) parseBooleanLiteral() Expression {
    return &Literal{Value: p.curTokenIs(BOOLEAN) && p.curToken.Literal == "true"}
}

// Helper methods
func (p *Parser) curTokenIs(t TokenType) bool {
    return p.curToken.Type == t
}

func (p *Parser) peekTokenIs(t TokenType) bool {
    return p.peekToken.Type == t
}

func (p *Parser) expectPeek(t TokenType) bool {
    if p.peekTokenIs(t) {
        p.nextToken()
        return true
    } else {
        p.peekError(t)
        return false
    }
}

func (p *Parser) curPrecedence() int {
    if p, ok := precedences[p.curToken.Type]; ok {
        return p
    }
    return LOWEST
}

func (p *Parser) peekPrecedence() int {
    if p, ok := precedences[p.peekToken.Type]; ok {
        return p
    }
    return LOWEST
}

func (p *Parser) Errors() []string {
    return p.errors
}

func (p *Parser) peekError(t TokenType) {
    msg := fmt.Sprintf("expected next token to be %v, got %v instead", 
        t, p.peekToken.Type)
    p.errors = append(p.errors, msg)
}

func (p *Parser) noPrefixParseFnError(t TokenType) {
    msg := fmt.Sprintf("no prefix parse function for %v found", t)
    p.errors = append(p.errors, msg)
}

The Interpreter: Making Rules Come Alive

The interpreter evaluates the AST against real data:

package interpreter

import (
    "fmt"
    "reflect"
    "strconv"
    "strings"
)

type Context struct {
    Variables map[string]interface{}
    Functions map[string]func([]interface{}) interface{}
}

func NewContext() *Context {
    return &Context{
        Variables: make(map[string]interface{}),
        Functions: make(map[string]func([]interface{}) interface{}),
    }
}

func (c *Context) SetVariable(name string, value interface{}) {
    c.Variables[name] = value
}

func (c *Context) SetFunction(name string, fn func([]interface{}) interface{}) {
    c.Functions[name] = fn
}

type Interpreter struct {
    context *Context
}

func New(context *Context) *Interpreter {
    return &Interpreter{
        context: context,
    }
}

func (i *Interpreter) Eval(node Node) (interface{}, error) {
    switch node := node.(type) {
    case *Program:
        return i.evalProgram(node)
    case *RuleStatement:
        return i.evalRule(node)
    case *BinaryExpression:
        return i.evalBinaryExpression(node)
    case *FieldAccess:
        return i.evalFieldAccess(node)
    case *Literal:
        return node.Value, nil
    case *FunctionCall:
        return i.evalFunctionCall(node)
    default:
        return nil, fmt.Errorf("unknown node type: %T", node)
    }
}

func (i *Interpreter) evalProgram(program *Program) (interface{}, error) {
    var result interface{}
    
    for _, rule := range program.Rules {
        val, err := i.Eval(rule)
        if err != nil {
            return nil, err
        }
        result = val
    }
    
    return result, nil
}

func (i *Interpreter) evalRule(rule *RuleStatement) (interface{}, error) {
    condition, err := i.Eval(rule.Condition)
    if err != nil {
        return nil, err
    }
    
    conditionResult, ok := condition.(bool)
    if !ok {
        return nil, fmt.Errorf("rule condition must evaluate to boolean, got %T", condition)
    }
    
    return RuleResult{
        RuleName:  rule.Name,
        Triggered: conditionResult,
        Actions:   rule.Actions,
    }, nil
}

type RuleResult struct {
    RuleName  string
    Triggered bool
    Actions   []Statement
}

func (i *Interpreter) evalBinaryExpression(node *BinaryExpression) (interface{}, error) {
    left, err := i.Eval(node.Left)
    if err != nil {
        return nil, err
    }
    
    right, err := i.Eval(node.Right)
    if err != nil {
        return nil, err
    }
    
    return i.evalBinaryOperation(node.Operator, left, right)
}

func (i *Interpreter) evalBinaryOperation(operator string, left, right interface{}) (interface{}, error) {
    switch operator {
    case "==":
        return i.compareValues(left, right, "==")
    case "!=":
        return i.compareValues(left, right, "!=")
    case "<":
        return i.compareValues(left, right, "<")
    case ">":
        return i.compareValues(left, right, ">")
    case "<=":
        return i.compareValues(left, right, "<=")
    case ">=":
        return i.compareValues(left, right, ">=")
    case "AND":
        return i.evalLogicalOperation(left, right, "AND")
    case "OR":
        return i.evalLogicalOperation(left, right, "OR")
    case "+":
        return i.evalArithmeticOperation(left, right, "+")
    case "-":
        return i.evalArithmeticOperation(left, right, "-")
    case "*":
        return i.evalArithmeticOperation(left, right, "*")
    case "/":
        return i.evalArithmeticOperation(left, right, "/")
    case "%":
        return i.evalArithmeticOperation(left, right, "%")
    default:
        return nil, fmt.Errorf("unknown operator: %s", operator)
    }
}

func (i *Interpreter) compareValues(left, right interface{}, operator string) (interface{}, error) {
    // Convert both values to the same type for comparison
    leftVal, rightVal, err := i.normalizeForComparison(left, right)
    if err != nil {
        return nil, err
    }
    
    switch operator {
    case "==":
        return leftVal == rightVal, nil
    case "!=":
        return leftVal != rightVal, nil
    case "<":
        return i.lessThan(leftVal, rightVal)
    case ">":
        return i.greaterThan(leftVal, rightVal)
    case "<=":
        return i.lessThanOrEqual(leftVal, rightVal)
    case ">=":
        return i.greaterThanOrEqual(leftVal, rightVal)
    default:
        return nil, fmt.Errorf("unknown comparison operator: %s", operator)
    }
}

func (i *Interpreter) normalizeForComparison(left, right interface{}) (interface{}, interface{}, error) {
    // Handle string comparisons
    if leftStr, ok := left.(string); ok {
        if rightStr, ok := right.(string); ok {
            return leftStr, rightStr, nil
        }
    }
    
    // Handle numeric comparisons
    leftFloat, leftOk := i.toFloat(left)
    rightFloat, rightOk := i.toFloat(right)
    
    if leftOk && rightOk {
        return leftFloat, rightFloat, nil
    }
    
    // Handle boolean comparisons
    if leftBool, ok := left.(bool); ok {
        if rightBool, ok := right.(bool); ok {
            return leftBool, rightBool, nil
        }
    }
    
    // Fallback to interface{} comparison
    return left, right, nil
}

func (i *Interpreter) toFloat(value interface{}) (float64, bool) {
    switch v := value.(type) {
    case float64:
        return v, true
    case int64:
        return float64(v), true
    case int:
        return float64(v), true
    case string:
        if f, err := strconv.ParseFloat(v, 64); err == nil {
            return f, true
        }
    }
    return 0, false
}

func (i *Interpreter) evalLogicalOperation(left, right interface{}, operator string) (interface{}, error) {
    leftBool, ok := left.(bool)
    if !ok {
        return nil, fmt.Errorf("left operand must be boolean for %s, got %T", operator, left)
    }
    
    rightBool, ok := right.(bool)
    if !ok {
        return nil, fmt.Errorf("right operand must be boolean for %s, got %T", operator, right)
    }
    
    switch operator {
    case "AND":
        return leftBool && rightBool, nil
    case "OR":
        return leftBool || rightBool, nil
    default:
        return nil, fmt.Errorf("unknown logical operator: %s", operator)
    }
}

func (i *Interpreter) evalFieldAccess(node *FieldAccess) (interface{}, error) {
    if node.Object == "" {
        // Simple variable access
        if value, ok := i.context.Variables[node.Field]; ok {
            return value, nil
        }
        return nil, fmt.Errorf("undefined variable: %s", node.Field)
    }
    
    // Object field access
    obj, ok := i.context.Variables[node.Object]
    if !ok {
        return nil, fmt.Errorf("undefined object: %s", node.Object)
    }
    
    return i.getFieldValue(obj, node.Field)
}

func (i *Interpreter) getFieldValue(obj interface{}, fieldName string) (interface{}, error) {
    value := reflect.ValueOf(obj)
    
    // Handle pointers
    if value.Kind() == reflect.Ptr {
        value = value.Elem()
    }
    
    switch value.Kind() {
    case reflect.Struct:
        return i.getStructField(value, fieldName)
    case reflect.Map:
        return i.getMapField(value, fieldName)
    default:
        return nil, fmt.Errorf("cannot access field %s on type %T", fieldName, obj)
    }
}

func (i *Interpreter) getStructField(value reflect.Value, fieldName string) (interface{}, error) {
    // Convert fieldName to proper case (e.g., "user_id" -> "UserID")
    properFieldName := i.toCamelCase(fieldName)
    
    field := value.FieldByName(properFieldName)
    if !field.IsValid() {
        // Try original name
        field = value.FieldByName(fieldName)
        if !field.IsValid() {
            return nil, fmt.Errorf("field %s not found", fieldName)
        }
    }
    
    return field.Interface(), nil
}

func (i *Interpreter) getMapField(value reflect.Value, fieldName string) (interface{}, error) {
    key := reflect.ValueOf(fieldName)
    field := value.MapIndex(key)
    
    if !field.IsValid() {
        return nil, fmt.Errorf("key %s not found in map", fieldName)
    }
    
    return field.Interface(), nil
}

func (i *Interpreter) toCamelCase(snake string) string {
    parts := strings.Split(snake, "_")
    for i := range parts {
        if len(parts[i]) > 0 {
            parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
        }
    }
    return strings.Join(parts, "")
}

func (i *Interpreter) evalFunctionCall(node *FunctionCall) (interface{}, error) {
    fn, ok := i.context.Functions[node.Name]
    if !ok {
        return nil, fmt.Errorf("undefined function: %s", node.Name)
    }
    
    var args []interface{}
    for _, arg := range node.Args {
        value, err := i.Eval(arg)
        if err != nil {
            return nil, err
        }
        args = append(args, value)
    }
    
    return fn(args), nil
}

Performance: The 100x Mistake

Our first implementation was embarrassingly slow. Here's what we learned:

The Problem:

// What we did first (DON'T DO THIS)
func EvaluateRules(user User, transaction Transaction) ([]RuleResult, error) {
    var results []RuleResult
    
    for _, ruleText := range allRules {
        // Parse EVERY TIME (SLOW!)
        lexer := lexer.New(ruleText)
        parser := parser.New(lexer)
        program := parser.ParseProgram()
        
        // Create context EVERY TIME (SLOW!)
        context := NewContext()
        context.SetVariable("user", user)
        context.SetVariable("transaction", transaction)
        
        // Interpret EVERY TIME (SLOW!)
        interpreter := New(context)
        result, err := interpreter.Eval(program)
        if err != nil {
            return nil, err
        }
        
        results = append(results, result.(RuleResult))
    }
    
    return results, nil
}

The Solution:

// Optimized version - parse once, reuse forever
type RuleEngine struct {
    compiledRules []*CompiledRule
    functionRegistry map[string]func([]interface{}) interface{}
}

type CompiledRule struct {
    Name    string
    AST     *RuleStatement
    Hash    string // for cache invalidation
}

func NewRuleEngine() *RuleEngine {
    return &RuleEngine{
        compiledRules: make([]*CompiledRule, 0),
        functionRegistry: make(map[string]func([]interface{}) interface{}),
    }
}

func (re *RuleEngine) CompileRule(name, ruleText string) error {
    // Parse once during compilation
    lexer := lexer.New(ruleText)
    parser := parser.New(lexer)
    program := parser.ParseProgram()
    
    if len(parser.Errors()) > 0 {
        return fmt.Errorf("parse errors: %v", parser.Errors())
    }
    
    if len(program.Rules) != 1 {
        return fmt.Errorf("expected exactly one rule, got %d", len(program.Rules))
    }
    
    rule := program.Rules[0].(*RuleStatement)
    
    compiledRule := &CompiledRule{
        Name: name,
        AST:  rule,
        Hash: fmt.Sprintf("%x", sha256.Sum256([]byte(ruleText))),
    }
    
    re.compiledRules = append(re.compiledRules, compiledRule)
    return nil
}

func (re *RuleEngine) EvaluateRules(user User, transaction Transaction) ([]RuleResult, error) {
    // Create context once
    context := NewContext()
    context.SetVariable("user", user)
    context.SetVariable("transaction", transaction)
    
    // Add functions
    for name, fn := range re.functionRegistry {
        context.SetFunction(name, fn)
    }
    
    interpreter := New(context)
    
    var results []RuleResult
    
    // Evaluate pre-compiled ASTs (FAST!)
    for _, compiledRule := range re.compiledRules {
        result, err := interpreter.Eval(compiledRule.AST)
        if err != nil {
            return nil, fmt.Errorf("error evaluating rule %s: %w", compiledRule.Name, err)
        }
        
        results = append(results, result.(RuleResult))
    }
    
    return results, nil
}

// Result: 100x performance improvement
// Before: 500ms for 100 rules
// After: 5ms for 100 rules

Advanced Features: Functions and Context

Real business rules need more than simple comparisons:

package functions

import (
    "time"
    "strings"
    "math"
)

// Register built-in functions
func RegisterBuiltinFunctions(engine *RuleEngine) {
    // String functions
    engine.RegisterFunction("contains", func(args []interface{}) interface{} {
        if len(args) != 2 {
            return false
        }
        
        str, ok1 := args[0].(string)
        substr, ok2 := args[1].(string)
        
        if !ok1 || !ok2 {
            return false
        }
        
        return strings.Contains(str, substr)
    })
    
    engine.RegisterFunction("starts_with", func(args []interface{}) interface{} {
        if len(args) != 2 {
            return false
        }
        
        str, ok1 := args[0].(string)
        prefix, ok2 := args[1].(string)
        
        if !ok1 || !ok2 {
            return false
        }
        
        return strings.HasPrefix(str, prefix)
    })
    
    // Time functions
    engine.RegisterFunction("hours_since", func(args []interface{}) interface{} {
        if len(args) != 1 {
            return float64(0)
        }
        
        t, ok := args[0].(time.Time)
        if !ok {
            return float64(0)
        }
        
        return time.Since(t).Hours()
    })
    
    engine.RegisterFunction("is_weekend", func(args []interface{}) interface{} {
        if len(args) != 1 {
            return false
        }
        
        t, ok := args[0].(time.Time)
        if !ok {
            return false
        }
        
        weekday := t.Weekday()
        return weekday == time.Saturday || weekday == time.Sunday
    })
    
    // Math functions
    engine.RegisterFunction("abs", func(args []interface{}) interface{} {
        if len(args) != 1 {
            return float64(0)
        }
        
        switch v := args[0].(type) {
        case float64:
            return math.Abs(v)
        case int64:
            return math.Abs(float64(v))
        default:
            return float64(0)
        }
    })
    
    engine.RegisterFunction("max", func(args []interface{}) interface{} {
        if len(args) != 2 {
            return float64(0)
        }
        
        v1, ok1 := toFloat64(args[0])
        v2, ok2 := toFloat64(args[1])
        
        if !ok1 || !ok2 {
            return float64(0)
        }
        
        return math.Max(v1, v2)
    })
    
    // Business-specific functions
    engine.RegisterFunction("is_business_hours", func(args []interface{}) interface{} {
        if len(args) != 1 {
            return false
        }
        
        t, ok := args[0].(time.Time)
        if !ok {
            return false
        }
        
        hour := t.Hour()
        return hour >= 9 && hour <= 17 && t.Weekday() >= time.Monday && t.Weekday() <= time.Friday
    })
    
    engine.RegisterFunction("calculate_risk_score", func(args []interface{}) interface{} {
        // Complex risk calculation
        if len(args) != 3 {
            return float64(0)
        }
        
        amount, ok1 := toFloat64(args[0])
        avgAmount, ok2 := toFloat64(args[1])
        userAge, ok3 := toFloat64(args[2])
        
        if !ok1 || !ok2 || !ok3 {
            return float64(0)
        }
        
        // Simple risk scoring algorithm
        risk := 0.0
        
        // Amount factor
        if amount > avgAmount*3 {
            risk += 0.3
        } else if amount > avgAmount*2 {
            risk += 0.2
        }
        
        // User age factor
        if userAge < 30 {
            risk += 0.1
        }
        
        return math.Min(risk, 1.0)
    })
}

func toFloat64(value interface{}) (float64, bool) {
    switch v := value.(type) {
    case float64:
        return v, true
    case int64:
        return float64(v), true
    case int:
        return float64(v), true
    default:
        return 0, false
    }
}

// Now we can write rules like:
// rule "complex_fraud_detection" {
//     when {
//         transaction.amount > user.average_amount * 2 AND
//         contains(transaction.description, "ATM") AND
//         hours_since(user.last_transaction) < 1 AND
//         calculate_risk_score(transaction.amount, user.average_amount, user.age) > 0.5
//     }
//     then {
//         flag_for_review()
//     }
// }

Rule Compilation and Caching

For production systems, rules need to be compiled and cached efficiently:

package cache

import (
    "crypto/sha256"
    "fmt"
    "sync"
    "time"
)

type RuleCache struct {
    cache map[string]*CachedRule
    mutex sync.RWMutex
    ttl   time.Duration
}

type CachedRule struct {
    CompiledRule *CompiledRule
    CompiledAt   time.Time
    Hash         string
}

func NewRuleCache(ttl time.Duration) *RuleCache {
    return &RuleCache{
        cache: make(map[string]*CachedRule),
        ttl:   ttl,
    }
}

func (rc *RuleCache) Get(name, ruleText string) (*CompiledRule, bool) {
    rc.mutex.RLock()
    defer rc.mutex.RUnlock()
    
    cached, exists := rc.cache[name]
    if !exists {
        return nil, false
    }
    
    // Check if expired
    if time.Since(cached.CompiledAt) > rc.ttl {
        return nil, false
    }
    
    // Check if rule text changed
    currentHash := fmt.Sprintf("%x", sha256.Sum256([]byte(ruleText)))
    if cached.Hash != currentHash {
        return nil, false
    }
    
    return cached.CompiledRule, true
}

func (rc *RuleCache) Set(name, ruleText string, compiledRule *CompiledRule) {
    rc.mutex.Lock()
    defer rc.mutex.Unlock()
    
    hash := fmt.Sprintf("%x", sha256.Sum256([]byte(ruleText)))
    
    rc.cache[name] = &CachedRule{
        CompiledRule: compiledRule,
        CompiledAt:   time.Now(),
        Hash:         hash,
    }
}

func (rc *RuleCache) Invalidate(name string) {
    rc.mutex.Lock()
    defer rc.mutex.Unlock()
    
    delete(rc.cache, name)
}

func (rc *RuleCache) Clear() {
    rc.mutex.Lock()
    defer rc.mutex.Unlock()
    
    rc.cache = make(map[string]*CachedRule)
}

// Enhanced rule engine with caching
type CachedRuleEngine struct {
    *RuleEngine
    cache *RuleCache
}

func NewCachedRuleEngine() *CachedRuleEngine {
    return &CachedRuleEngine{
        RuleEngine: NewRuleEngine(),
        cache:      NewRuleCache(time.Hour), // Cache for 1 hour
    }
}

func (cre *CachedRuleEngine) CompileRule(name, ruleText string) error {
    // Check cache first
    if compiledRule, found := cre.cache.Get(name, ruleText); found {
        // Use cached version
        cre.RuleEngine.compiledRules = append(cre.RuleEngine.compiledRules, compiledRule)
        return nil
    }
    
    // Compile new rule
    err := cre.RuleEngine.CompileRule(name, ruleText)
    if err != nil {
        return err
    }
    
    // Cache the compiled rule
    if len(cre.RuleEngine.compiledRules) > 0 {
        lastRule := cre.RuleEngine.compiledRules[len(cre.RuleEngine.compiledRules)-1]
        cre.cache.Set(name, ruleText, lastRule)
    }
    
    return nil
}

Error Handling and Debugging

Production DSLs need comprehensive error handling:

package errors

import (
    "fmt"
    "strings"
)

type RuleError struct {
    Type     string
    Message  string
    RuleName string
    Position int
    Context  map[string]interface{}
}

func (re *RuleError) Error() string {
    return fmt.Sprintf("%s error in rule '%s': %s", re.Type, re.RuleName, re.Message)
}

type ErrorCollector struct {
    errors []RuleError
}

func NewErrorCollector() *ErrorCollector {
    return &ErrorCollector{
        errors: make([]RuleError, 0),
    }
}

func (ec *ErrorCollector) AddError(errorType, message, ruleName string, position int, context map[string]interface{}) {
    ec.errors = append(ec.errors, RuleError{
        Type:     errorType,
        Message:  message,
        RuleName: ruleName,
        Position: position,
        Context:  context,
    })
}

func (ec *ErrorCollector) HasErrors() bool {
    return len(ec.errors) > 0
}

func (ec *ErrorCollector) GetErrors() []RuleError {
    return ec.errors
}

// Enhanced interpreter with error handling
type SafeInterpreter struct {
    *Interpreter
    errorCollector *ErrorCollector
    currentRule    string
}

func NewSafeInterpreter(context *Context) *SafeInterpreter {
    return &SafeInterpreter{
        Interpreter:    New(context),
        errorCollector: NewErrorCollector(),
    }
}

func (si *SafeInterpreter) EvalRule(rule *RuleStatement) (RuleResult, error) {
    si.currentRule = rule.Name
    
    defer func() {
        if r := recover(); r != nil {
            si.errorCollector.AddError(
                "runtime_panic",
                fmt.Sprintf("panic during rule evaluation: %v", r),
                si.currentRule,
                0,
                map[string]interface{}{
                    "panic_value": r,
                },
            )
        }
    }()
    
    result, err := si.Interpreter.Eval(rule)
    if err != nil {
        si.errorCollector.AddError(
            "evaluation_error",
            err.Error(),
            si.currentRule,
            0,
            nil,
        )
        return RuleResult{}, err
    }
    
    return result.(RuleResult), nil
}

func (si *SafeInterpreter) evalFieldAccess(node *FieldAccess) (interface{}, error) {
    result, err := si.Interpreter.evalFieldAccess(node)
    if err != nil {
        si.errorCollector.AddError(
            "field_access_error",
            fmt.Sprintf("cannot access field %s.%s: %s", node.Object, node.Field, err.Error()),
            si.currentRule,
            0,
            map[string]interface{}{
                "object": node.Object,
                "field":  node.Field,
            },
        )
        return nil, err
    }
    
    return result, nil
}

func (si *SafeInterpreter) GetErrors() []RuleError {
    return si.errorCollector.GetErrors()
}

// Rule validation
type RuleValidator struct {
    allowedObjects []string
    allowedFields  map[string][]string
    allowedFunctions []string
}

func NewRuleValidator() *RuleValidator {
    return &RuleValidator{
        allowedObjects:   []string{"user", "transaction", "account"},
        allowedFields:    make(map[string][]string),
        allowedFunctions: []string{"contains", "starts_with", "hours_since", "is_weekend"},
    }
}

func (rv *RuleValidator) ValidateRule(rule *RuleStatement) []RuleError {
    var errors []RuleError
    
    // Validate condition
    conditionErrors := rv.validateExpression(rule.Condition, rule.Name)
    errors = append(errors, conditionErrors...)
    
    return errors
}

func (rv *RuleValidator) validateExpression(expr Expression, ruleName string) []RuleError {
    var errors []RuleError
    
    switch node := expr.(type) {
    case *FieldAccess:
        if !rv.isAllowedObject(node.Object) {
            errors = append(errors, RuleError{
                Type:     "validation_error",
                Message:  fmt.Sprintf("object '%s' is not allowed", node.Object),
                RuleName: ruleName,
                Context: map[string]interface{}{
                    "object": node.Object,
                    "allowed_objects": rv.allowedObjects,
                },
            })
        }
        
        if !rv.isAllowedField(node.Object, node.Field) {
            errors = append(errors, RuleError{
                Type:     "validation_error",
                Message:  fmt.Sprintf("field '%s' is not allowed on object '%s'", node.Field, node.Object),
                RuleName: ruleName,
                Context: map[string]interface{}{
                    "object": node.Object,
                    "field":  node.Field,
                },
            })
        }
        
    case *FunctionCall:
        if !rv.isAllowedFunction(node.Name) {
            errors = append(errors, RuleError{
                Type:     "validation_error",
                Message:  fmt.Sprintf("function '%s' is not allowed", node.Name),
                RuleName: ruleName,
                Context: map[string]interface{}{
                    "function": node.Name,
                    "allowed_functions": rv.allowedFunctions,
                },
            })
        }
        
        // Validate function arguments
        for _, arg := range node.Args {
            argErrors := rv.validateExpression(arg, ruleName)
            errors = append(errors, argErrors...)
        }
        
    case *BinaryExpression:
        leftErrors := rv.validateExpression(node.Left, ruleName)
        rightErrors := rv.validateExpression(node.Right, ruleName)
        errors = append(errors, leftErrors...)
        errors = append(errors, rightErrors...)
    }
    
    return errors
}

func (rv *RuleValidator) isAllowedObject(object string) bool {
    for _, allowed := range rv.allowedObjects {
        if allowed == object {
            return true
        }
    }
    return false
}

func (rv *RuleValidator) isAllowedField(object, field string) bool {
    allowedFields, exists := rv.allowedFields[object]
    if !exists {
        return true // If no restrictions, allow all fields
    }
    
    for _, allowed := range allowedFields {
        if allowed == field {
            return true
        }
    }
    return false
}

func (rv *RuleValidator) isAllowedFunction(function string) bool {
    for _, allowed := range rv.allowedFunctions {
        if allowed == function {
            return true
        }
    }
    return false
}

Production Usage and Performance

Here's how we deploy our DSL in production:

package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type RuleService struct {
    engine   *CachedRuleEngine
    db       *sql.DB
    cache    *RuleCache
    metrics  *Metrics
}

func NewRuleService(db *sql.DB) *RuleService {
    engine := NewCachedRuleEngine()
    
    // Register all built-in functions
    RegisterBuiltinFunctions(engine)
    
    return &RuleService{
        engine:  engine,
        db:      db,
        cache:   NewRuleCache(time.Hour),
        metrics: NewMetrics(),
    }
}

func (rs *RuleService) LoadRulesFromDatabase() error {
    rows, err := rs.db.Query(`
        SELECT name, rule_text, is_active 
        FROM business_rules 
        WHERE is_active = true
        ORDER BY priority DESC
    `)
    if err != nil {
        return err
    }
    defer rows.Close()
    
    var loadedCount int
    
    for rows.Next() {
        var name, ruleText string
        var isActive bool
        
        err := rows.Scan(&name, &ruleText, &isActive)
        if err != nil {
            log.Printf("Error scanning rule %s: %v", name, err)
            continue
        }
        
        err = rs.engine.CompileRule(name, ruleText)
        if err != nil {
            log.Printf("Error compiling rule %s: %v", name, err)
            continue
        }
        
        loadedCount++
    }
    
    log.Printf("Loaded %d rules from database", loadedCount)
    return nil
}

// HTTP handler for rule evaluation
func (rs *RuleService) EvaluateHandler(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    defer func() {
        rs.metrics.RequestDuration.Observe(time.Since(start).Seconds())
    }()
    
    var request struct {
        User        User        `json:"user"`
        Transaction Transaction `json:"transaction"`
    }
    
    err := json.NewDecoder(r.Body).Decode(&request)
    if err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    results, err := rs.engine.EvaluateRules(request.User, request.Transaction)
    if err != nil {
        rs.metrics.ErrorCount.Inc()
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    // Filter only triggered rules
    var triggeredRules []RuleResult
    for _, result := range results {
        if result.Triggered {
            triggeredRules = append(triggeredRules, result)
        }
    }
    
    response := struct {
        TriggeredRules []RuleResult `json:"triggered_rules"`
        EvaluationTime string       `json:"evaluation_time"`
    }{
        TriggeredRules: triggeredRules,
        EvaluationTime: time.Since(start).String(),
    }
    
    rs.metrics.RulesEvaluated.Add(float64(len(results)))
    rs.metrics.RulesTriggered.Add(float64(len(triggeredRules)))
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

// Background service for rule reloading
func (rs *RuleService) StartRuleReloader(ctx context.Context) {
    ticker := time.NewTicker(5 * time.Minute)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            rs.reloadRules()
        case <-ctx.Done():
            return
        }
    }
}

func (rs *RuleService) reloadRules() {
    log.Println("Reloading rules from database...")
    
    newEngine := NewCachedRuleEngine()
    RegisterBuiltinFunctions(newEngine)
    
    // Load rules into new engine
    rows, err := rs.db.Query(`
        SELECT name, rule_text, is_active 
        FROM business_rules 
        WHERE is_active = true
        ORDER BY priority DESC
    `)
    if err != nil {
        log.Printf("Error reloading rules: %v", err)
        return
    }
    defer rows.Close()
    
    var loadedCount int
    
    for rows.Next() {
        var name, ruleText string
        var isActive bool
        
        err := rows.Scan(&name, &ruleText, &isActive)
        if err != nil {
            continue
        }
        
        err = newEngine.CompileRule(name, ruleText)
        if err != nil {
            log.Printf("Error compiling rule %s during reload: %v", name, err)
            continue
        }
        
        loadedCount++
    }
    
    // Atomic swap
    rs.engine = newEngine
    
    log.Printf("Reloaded %d rules", loadedCount)
}

// Performance benchmarks
func BenchmarkRuleEvaluation(b *testing.B) {
    engine := NewCachedRuleEngine()
    RegisterBuiltinFunctions(engine)
    
    // Compile test rules
    testRules := []string{
        `rule "test1" { when { user.age > 18 AND transaction.amount > 1000 } then { allow() } }`,
        `rule "test2" { when { contains(user.country, "US") AND is_business_hours(transaction.timestamp) } then { verify() } }`,
        `rule "test3" { when { user.risk_score > 0.5 OR transaction.amount > user.credit_limit } then { deny() } }`,
    }
    
    for i, rule := range testRules {
        engine.CompileRule(fmt.Sprintf("test%d", i+1), rule)
    }
    
    user := User{
        Age:         25,
        Country:     "US",
        RiskScore:   0.3,
        CreditLimit: 5000,
    }
    
    transaction := Transaction{
        Amount:    2000,
        Timestamp: time.Now(),
    }
    
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        _, err := engine.EvaluateRules(user, transaction)
        if err != nil {
            b.Fatal(err)
        }
    }
}

// Results from our production system:
// BenchmarkRuleEvaluation-8   	  100000	     15234 ns/op	    2048 B/op	      24 allocs/op
// 
// Translation: 65,000 evaluations per second per core
// 10M evaluations per day = 115 evaluations per second average
// Our system can handle 560x our current load

Real-World Rules in Production

Here are actual rules from our production system:

rule "high_risk_transaction" {
    when {
        transaction.amount > user.average_transaction * 5 AND
        hours_since(user.last_login) > 24 AND
        NOT contains(transaction.description, "RECURRING")
    }
    then {
        require_additional_verification()
        notify_user("High risk transaction detected")
        log_security_event("HIGH_RISK_TRANSACTION")
    }
}

rule "new_user_limits" {
    when {
        user.days_since_registration < 30 AND
        transaction.amount > 500 AND
        user.verification_level < 2
    }
    then {
        deny_transaction("New user limit exceeded")
        suggest_verification_upgrade()
    }
}

rule "suspicious_location" {
    when {
        transaction.location.country != user.home_country AND
        NOT contains(user.travel_notifications, transaction.location.country) AND
        transaction.amount > 100
    }
    then {
        require_sms_verification()
        send_location_alert()
    }
}

rule "business_hours_large_transfer" {
    when {
        transaction.type == "TRANSFER" AND
        transaction.amount > 10000 AND
        NOT is_business_hours(transaction.timestamp) AND
        user.account_type != "premium"
    }
    then {
        delay_until_business_hours()
        notify_compliance_team()
    }
}

Lessons Learned: What Works and What Doesn't

After 18 months in production with 10M evaluations per day:

What Works:

  1. Simple syntax - Business people can actually write rules
  2. Compile once, run many - 100x performance improvement
  3. Comprehensive error handling - Bad rules don't crash the system
  4. Hot reloading - Rules update without deployment

What Doesn't Work:

  1. Complex syntax - Loops and complex control flow confuse business users
  2. Runtime parsing - Our first version was 100x slower
  3. No validation - Invalid rules cause runtime failures
  4. No versioning - Hard to track rule changes over time

When to Build a DSL

Don't build a DSL unless:

  1. You have complex domain logic
    Simple if-statements don't justify DSL complexity.
  2. Non-developers need to modify logic
    If only developers use it, just write Go code.
  3. Rules change frequently
    Static rules don't need a DSL.
  4. You have time to do it right
    Lexers, parsers, and interpreters take months to get right.

The Bottom Line

Building a DSL was one of the hardest projects we've done. But also one of the most rewarding.

Before: Changing a rule required developers, testing, and deployment. 2-week cycle.

After: Business people change rules in real-time. 2-minute cycle.

Our fraud detection catches 15% more fraud because rules adapt faster to new attack patterns.

Revenue impact: +$2.3M annually from reduced fraud and faster rule deployment.

The real win? Business people feel empowered to solve business problems themselves.

P.S.

That 100x performance improvement from compiling rules once? It happened because we read one Stack Overflow answer about interpreter optimization.

Sometimes the biggest wins come from the smallest changes.

And remember: A DSL isn't about showing off your parsing skills. It's about giving domain experts the tools to solve domain problems.

Build for your users, not your ego.

Security Considerations

DSLs run user-provided code and require comprehensive security measures.

Input Validation and Sanitization

type SecurityValidator struct {
    maxRuleLength     int
    maxExpressionDepth int
    allowedFunctions  map[string]bool
    blockedPatterns   []*regexp.Regexp
}

func NewSecurityValidator() *SecurityValidator {
    return &SecurityValidator{
        maxRuleLength:     10000, // 10KB max rule size
        maxExpressionDepth: 20,   // Prevent deeply nested expressions
        allowedFunctions: map[string]bool{
            "contains":     true,
            "starts_with":  true,
            "hours_since":  true,
            "is_weekend":   true,
            "abs":          true,
            "max":          true,
        },
        blockedPatterns: []*regexp.Regexp{
            regexp.MustCompile(`eval\s*\(`),     // Block eval calls
            regexp.MustCompile(`exec\s*\(`),     // Block exec calls
            regexp.MustCompile(`system\s*\(`),   // Block system calls
            regexp.MustCompile(`\$\{.*\}`),      // Block template injection
        },
    }
}

func (sv *SecurityValidator) ValidateRuleText(ruleText string) error {
    if len(ruleText) > sv.maxRuleLength {
        return fmt.Errorf("rule text too long: %d bytes (max %d)", 
            len(ruleText), sv.maxRuleLength)
    }
    
    // Check for blocked patterns
    for _, pattern := range sv.blockedPatterns {
        if pattern.MatchString(ruleText) {
            return fmt.Errorf("rule contains blocked pattern: %s", 
                pattern.String())
        }
    }
    
    return nil
}

func (sv *SecurityValidator) ValidateAST(node Node) error {
    return sv.validateNode(node, 0)
}

func (sv *SecurityValidator) validateNode(node Node, depth int) error {
    if depth > sv.maxExpressionDepth {
        return fmt.Errorf("expression too deeply nested: %d levels (max %d)", 
            depth, sv.maxExpressionDepth)
    }
    
    switch n := node.(type) {
    case *FunctionCall:
        if !sv.allowedFunctions[n.Name] {
            return fmt.Errorf("function not allowed: %s", n.Name)
        }
        
        for _, arg := range n.Args {
            if err := sv.validateNode(arg, depth+1); err != nil {
                return err
            }
        }
        
    case *BinaryExpression:
        if err := sv.validateNode(n.Left, depth+1); err != nil {
            return err
        }
        if err := sv.validateNode(n.Right, depth+1); err != nil {
            return err
        }
        
    case *FieldAccess:
        // Validate field access permissions
        if err := sv.validateFieldAccess(n.Object, n.Field); err != nil {
            return err
        }
    }
    
    return nil
}

Resource Limits and DoS Protection

type ResourceLimiter struct {
    maxEvaluationTime time.Duration
    maxMemoryUsage    int64
    maxRulesPerUser   int
}

func NewResourceLimiter() *ResourceLimiter {
    return &ResourceLimiter{
        maxEvaluationTime: 100 * time.Millisecond,
        maxMemoryUsage:    10 * 1024 * 1024, // 10MB
        maxRulesPerUser:   100,
    }
}

func (rl *ResourceLimiter) EvaluateWithLimits(ctx context.Context, 
    interpreter *Interpreter, rule *RuleStatement) (interface{}, error) {
    
    // Set evaluation timeout
    ctx, cancel := context.WithTimeout(ctx, rl.maxEvaluationTime)
    defer cancel()
    
    // Monitor memory usage
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    startMem := m.Alloc
    
    // Channel for result
    resultChan := make(chan evalResult, 1)
    
    go func() {
        result, err := interpreter.Eval(rule)
        resultChan <- evalResult{result: result, err: err}
    }()
    
    select {
    case result := <-resultChan:
        // Check memory usage after evaluation
        runtime.ReadMemStats(&m)
        memUsed := int64(m.Alloc - startMem)
        
        if memUsed > rl.maxMemoryUsage {
            return nil, fmt.Errorf("rule evaluation exceeded memory limit: %d bytes", 
                memUsed)
        }
        
        return result.result, result.err
        
    case <-ctx.Done():
        return nil, fmt.Errorf("rule evaluation timeout after %v", 
            rl.maxEvaluationTime)
    }
}

type evalResult struct {
    result interface{}
    err    error
}

Audit Logging and Monitoring

type AuditLogger struct {
    logger *log.Logger
    events chan AuditEvent
}

type AuditEvent struct {
    Timestamp  time.Time             `json:"timestamp"`
    EventType  string                `json:"event_type"`
    UserID     string                `json:"user_id"`
    RuleName   string                `json:"rule_name"`
    Action     string                `json:"action"`
    Success    bool                  `json:"success"`
    Error      string                `json:"error,omitempty"`
    Metadata   map[string]interface{} `json:"metadata,omitempty"`
}

func NewAuditLogger() *AuditLogger {
    al := &AuditLogger{
        logger: log.New(os.Stdout, "[AUDIT] ", log.LstdFlags),
        events: make(chan AuditEvent, 1000),
    }
    
    go al.processEvents()
    return al
}

func (al *AuditLogger) LogRuleExecution(userID, ruleName string, 
    success bool, err error, metadata map[string]interface{}) {
    
    event := AuditEvent{
        Timestamp: time.Now(),
        EventType: "rule_execution",
        UserID:    userID,
        RuleName:  ruleName,
        Action:    "evaluate",
        Success:   success,
        Metadata:  metadata,
    }
    
    if err != nil {
        event.Error = err.Error()
    }
    
    select {
    case al.events <- event:
    default:
        // Log buffer full - critical security issue
        al.logger.Printf("CRITICAL: Audit log buffer full, dropping event: %+v", event)
    }
}

func (al *AuditLogger) processEvents() {
    for event := range al.events {
        eventJSON, _ := json.Marshal(event)
        al.logger.Printf("%s", eventJSON)
        
        // Send to SIEM or security monitoring system
        al.sendToSecuritySystem(event)
    }
}

Testing Strategy

DSL testing requires multiple layers from unit tests to integration and security testing.

Lexer and Parser Testing

func TestLexerTokenization(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        expected []TokenType
    }{
        {
            name:  "basic_rule",
            input: `rule "test" { when { user.age > 18 } then { allow() } }`,
            expected: []TokenType{
                RULE, STRING, LBRACE, WHEN, LBRACE,
                IDENTIFIER, DOT, IDENTIFIER, GREATER_THAN, NUMBER,
                RBRACE, THEN, LBRACE, IDENTIFIER, LPAREN, RPAREN,
                RBRACE, RBRACE, EOF,
            },
        },
        {
            name:  "complex_expression",
            input: `user.plan == "premium" AND transaction.amount >= 1000.50`,
            expected: []TokenType{
                IDENTIFIER, DOT, IDENTIFIER, EQUAL, STRING,
                AND, IDENTIFIER, DOT, IDENTIFIER, GREATER_EQUAL, NUMBER,
                EOF,
            },
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            lexer := NewLexer(tt.input)
            
            for i, expectedType := range tt.expected {
                token := lexer.NextToken()
                assert.Equal(t, expectedType, token.Type, 
                    "Token %d: expected %v, got %v", i, expectedType, token.Type)
            }
        })
    }
}

func TestParserAST(t *testing.T) {
    input := `rule "test_rule" { 
        when { 
            user.age > 21 AND transaction.amount < 1000 
        } 
        then { 
            allow_transaction() 
        } 
    }`
    
    lexer := NewLexer(input)
    parser := NewParser(lexer)
    program := parser.ParseProgram()
    
    assert.Empty(t, parser.Errors(), "Parser should not have errors")
    assert.Len(t, program.Rules, 1, "Should have exactly one rule")
    
    rule := program.Rules[0].(*RuleStatement)
    assert.Equal(t, "test_rule", rule.Name)
    
    // Verify AST structure
    condition, ok := rule.Condition.(*BinaryExpression)
    assert.True(t, ok, "Condition should be binary expression")
    assert.Equal(t, "AND", condition.Operator)
}

Interpreter Integration Tests

func TestRuleInterpreter(t *testing.T) {
    engine := NewRuleEngine()
    RegisterBuiltinFunctions(engine)
    
    tests := []struct {
        name        string
        rule        string
        user        User
        transaction Transaction
        expected    bool
    }{
        {
            name: "age_check",
            rule: `rule "age_check" { when { user.age >= 18 } then { allow() } }`,
            user: User{Age: 25},
            transaction: Transaction{},
            expected: true,
        },
        {
            name: "amount_limit",
            rule: `rule "amount_limit" { when { transaction.amount > 1000 } then { flag() } }`,
            user: User{},
            transaction: Transaction{Amount: 1500},
            expected: true,
        },
        {
            name: "complex_condition",
            rule: `rule "complex" { when { user.age > 21 AND transaction.amount < user.credit_limit } then { approve() } }`,
            user: User{Age: 25, CreditLimit: 5000},
            transaction: Transaction{Amount: 2000},
            expected: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := engine.CompileRule(tt.name, tt.rule)
            require.NoError(t, err, "Rule compilation should succeed")
            
            results, err := engine.EvaluateRules(tt.user, tt.transaction)
            require.NoError(t, err, "Rule evaluation should succeed")
            
            assert.Len(t, results, 1, "Should have one result")
            assert.Equal(t, tt.expected, results[0].Triggered, 
                "Rule trigger result mismatch")
        })
    }
}

func TestErrorHandling(t *testing.T) {
    engine := NewRuleEngine()
    
    errorTests := []struct {
        name        string
        rule        string
        expectError bool
        errorType   string
    }{
        {
            name:        "syntax_error",
            rule:        `rule "bad" { when { user.age > } then { allow() } }`,
            expectError: true,
            errorType:   "parse_error",
        },
        {
            name:        "undefined_function",
            rule:        `rule "bad" { when { undefined_func(user.age) } then { allow() } }`,
            expectError: true,
            errorType:   "undefined_function",
        },
        {
            name:        "invalid_field",
            rule:        `rule "bad" { when { user.nonexistent_field > 0 } then { allow() } }`,
            expectError: true,
            errorType:   "field_error",
        },
    }
    
    for _, tt := range errorTests {
        t.Run(tt.name, func(t *testing.T) {
            err := engine.CompileRule(tt.name, tt.rule)
            
            if tt.expectError {
                assert.Error(t, err, "Should have compilation error")
            } else {
                assert.NoError(t, err, "Should compile successfully")
            }
        })
    }
}

Performance and Load Testing

func TestRulePerformance(t *testing.T) {
    engine := NewCachedRuleEngine()
    RegisterBuiltinFunctions(engine)
    
    // Compile many rules
    for i := 0; i < 100; i++ {
        rule := fmt.Sprintf(`rule "rule_%d" { 
            when { 
                user.age > %d AND transaction.amount > %d 
            } 
            then { 
                process() 
            } 
        }`, i, 18+i%10, 100*(i+1))
        
        err := engine.CompileRule(fmt.Sprintf("rule_%d", i), rule)
        require.NoError(t, err)
    }
    
    user := User{Age: 25, CreditLimit: 10000}
    transaction := Transaction{Amount: 5000}
    
    // Warm up
    for i := 0; i < 10; i++ {
        _, err := engine.EvaluateRules(user, transaction)
        require.NoError(t, err)
    }
    
    // Performance test
    start := time.Now()
    iterations := 1000
    
    for i := 0; i < iterations; i++ {
        _, err := engine.EvaluateRules(user, transaction)
        require.NoError(t, err)
    }
    
    duration := time.Since(start)
    avgTime := duration / time.Duration(iterations)
    
    t.Logf("Average evaluation time: %v", avgTime)
    t.Logf("Evaluations per second: %.0f", 
        float64(iterations)/duration.Seconds())
    
    // Assert performance requirements
    assert.Less(t, avgTime, 10*time.Millisecond, 
        "Average evaluation should be under 10ms")
}

func TestConcurrentRuleEvaluation(t *testing.T) {
    engine := NewCachedRuleEngine()
    RegisterBuiltinFunctions(engine)
    
    rule := `rule "concurrent_test" { 
        when { user.age > 18 AND transaction.amount > 100 } 
        then { allow() } 
    }`
    
    err := engine.CompileRule("concurrent_test", rule)
    require.NoError(t, err)
    
    const numGoroutines = 100
    const evaluationsPerGoroutine = 100
    
    var wg sync.WaitGroup
    errors := make(chan error, numGoroutines)
    
    for i := 0; i < numGoroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            user := User{Age: 20 + (id % 10)}
            transaction := Transaction{Amount: 200 + float64(id*10)}
            
            for j := 0; j < evaluationsPerGoroutine; j++ {
                _, err := engine.EvaluateRules(user, transaction)
                if err != nil {
                    errors <- fmt.Errorf("goroutine %d, iteration %d: %w", 
                        id, j, err)
                    return
                }
            }
        }(i)
    }
    
    wg.Wait()
    close(errors)
    
    // Check for any errors
    for err := range errors {
        t.Errorf("Concurrent evaluation error: %v", err)
    }
}