package gott import ( "fmt" "reflect" "sort" "strconv" "strings" ) // Evaluator evaluates an AST with the given variables type Evaluator struct { renderer *Renderer // for filters, virtual methods, includes vars map[string]any // current variable scope blocks map[string]*BlockStmt // defined blocks output strings.Builder // accumulated output } // NewEvaluator creates a new evaluator func NewEvaluator(r *Renderer, vars map[string]any) *Evaluator { if vars == nil { vars = make(map[string]any) } return &Evaluator{ renderer: r, vars: vars, blocks: make(map[string]*BlockStmt), } } // Eval evaluates the template and returns the output string func (e *Evaluator) Eval(t *Template) (string, error) { // First pass: collect block definitions for _, node := range t.Nodes { if block, ok := node.(*BlockStmt); ok { e.blocks[block.Name] = block } } // Second pass: evaluate nodes for _, node := range t.Nodes { // Skip block definitions in output (they're just definitions) if _, ok := node.(*BlockStmt); ok { continue } if err := e.evalNode(node); err != nil { return "", err } } return e.output.String(), nil } // evalNode evaluates a single AST node func (e *Evaluator) evalNode(node Node) error { switch n := node.(type) { case *TextNode: e.output.WriteString(n.Text) case *OutputStmt: val, err := e.evalExpr(n.Expr) if err != nil { return err } if val != nil { e.output.WriteString(e.toString(val)) } case *IfStmt: return e.evalIf(n) case *UnlessStmt: return e.evalUnless(n) case *ForeachStmt: return e.evalForeach(n) case *IncludeStmt: return e.evalInclude(n) case *WrapperStmt: return e.evalWrapper(n) case *SetStmt: val, err := e.evalExpr(n.Value) if err != nil { return err } e.vars[n.Var] = val case *TryStmt: return e.evalTry(n) case *BlockStmt: // Block definitions are handled in first pass, skip here } return nil } // evalNodes evaluates a slice of nodes func (e *Evaluator) evalNodes(nodes []Node) error { for _, node := range nodes { if err := e.evalNode(node); err != nil { return err } } return nil } // evalIf evaluates an IF statement func (e *Evaluator) evalIf(n *IfStmt) error { cond, err := e.evalExpr(n.Condition) if err != nil { return err } if e.isTruthy(cond) { return e.evalNodes(n.Body) } // Check ELSIF chain for _, elsif := range n.ElsIf { cond, err := e.evalExpr(elsif.Condition) if err != nil { return err } if e.isTruthy(cond) { return e.evalNodes(elsif.Body) } } // Fall through to ELSE if n.Else != nil { return e.evalNodes(n.Else) } return nil } // evalUnless evaluates an UNLESS statement func (e *Evaluator) evalUnless(n *UnlessStmt) error { cond, err := e.evalExpr(n.Condition) if err != nil { return err } if !e.isTruthy(cond) { return e.evalNodes(n.Body) } if n.Else != nil { return e.evalNodes(n.Else) } return nil } // evalForeach evaluates a FOREACH loop func (e *Evaluator) evalForeach(n *ForeachStmt) error { list, err := e.evalExpr(n.ListExpr) if err != nil { return err } if list == nil { return nil } rv := reflect.ValueOf(list) switch rv.Kind() { case reflect.Slice, reflect.Array: for i := 0; i < rv.Len(); i++ { // Create new scope with loop variable loopEval := e.withVar(n.ItemVar, rv.Index(i).Interface()) if err := loopEval.evalNodes(n.Body); err != nil { return err } e.output.WriteString(loopEval.output.String()) } case reflect.Map: // TT2-style: iterate as key/value pairs, sorted by key keys := rv.MapKeys() sort.Slice(keys, func(i, j int) bool { return fmt.Sprintf("%v", keys[i].Interface()) < fmt.Sprintf("%v", keys[j].Interface()) }) for _, key := range keys { // Each iteration gets a map with "key" and "value" fields entry := map[string]any{ "key": key.Interface(), "value": rv.MapIndex(key).Interface(), } loopEval := e.withVar(n.ItemVar, entry) if err := loopEval.evalNodes(n.Body); err != nil { return err } e.output.WriteString(loopEval.output.String()) } default: return &EvalError{ Pos: n.Position, Message: fmt.Sprintf("cannot iterate over %T", list), } } return nil } // evalInclude evaluates an INCLUDE directive func (e *Evaluator) evalInclude(n *IncludeStmt) error { // Resolve the path (may be static or dynamic) includeName, err := e.resolvePath(n.Name, n.PathParts) if err != nil { return err } // First check if it's a defined block if block, ok := e.blocks[includeName]; ok { return e.evalNodes(block.Body) } // Otherwise, try to load from filesystem content, err := e.renderer.loadFile(includeName) if err != nil { return &EvalError{ Pos: n.Position, Message: fmt.Sprintf("include '%s' not found", includeName), } } // Parse with caching tmpl, err := e.renderer.parseTemplate(includeName, content) if err != nil { return err } // Evaluate with same scope includeEval := NewEvaluator(e.renderer, e.copyVars()) // Copy blocks for name, block := range e.blocks { includeEval.blocks[name] = block } result, err := includeEval.Eval(tmpl) if err != nil { return err } e.output.WriteString(result) return nil } // resolvePath resolves a static or dynamic path to its final string value. // If pathParts is non-empty, variables are interpolated; otherwise staticPath is used. func (e *Evaluator) resolvePath(staticPath string, pathParts []PathPart) (string, error) { // Static path - no interpolation needed if len(pathParts) == 0 { return staticPath, nil } // Dynamic path - interpolate variables var result strings.Builder for _, part := range pathParts { if part.IsVariable { // Resolve the variable val, err := e.resolveIdent(part.Parts) if err != nil { return "", err } if val == nil { return "", &EvalError{ Message: fmt.Sprintf("undefined variable in path: $%s", strings.Join(part.Parts, ".")), } } result.WriteString(e.toString(val)) } else { result.WriteString(part.Value) } } return result.String(), nil } // evalWrapper evaluates a WRAPPER directive func (e *Evaluator) evalWrapper(n *WrapperStmt) error { // Resolve the path (may be static or dynamic) wrapperPath, err := e.resolvePath(n.Name, n.PathParts) if err != nil { return err } // First, evaluate the wrapped content contentEval := NewEvaluator(e.renderer, e.copyVars()) for name, block := range e.blocks { contentEval.blocks[name] = block } // Collect block definitions from wrapper content so they're available to the wrapper for _, node := range n.Content { if block, ok := node.(*BlockStmt); ok { contentEval.blocks[block.Name] = block } } for _, node := range n.Content { if err := contentEval.evalNode(node); err != nil { return err } } wrappedContent := contentEval.output.String() // Load the wrapper template var wrapperSource string var wrapperName string if block, ok := e.blocks[wrapperPath]; ok { // Wrapper is a defined block - evaluate it blockEval := NewEvaluator(e.renderer, e.copyVars()) for name, b := range e.blocks { blockEval.blocks[name] = b } for _, node := range block.Body { if err := blockEval.evalNode(node); err != nil { return err } } wrapperSource = blockEval.output.String() wrapperName = "block:" + wrapperPath } else { content, err := e.renderer.loadFile(wrapperPath) if err != nil { return &EvalError{ Pos: n.Position, Message: fmt.Sprintf("wrapper '%s' not found", wrapperPath), } } wrapperSource = content wrapperName = wrapperPath } // Parse the wrapper template with caching tmpl, err := e.renderer.parseTemplate(wrapperName, wrapperSource) if err != nil { return err } // Evaluate wrapper with "content" variable set to wrapped content wrapperVars := e.copyVars() wrapperVars["content"] = wrappedContent wrapperEval := NewEvaluator(e.renderer, wrapperVars) for name, block := range e.blocks { wrapperEval.blocks[name] = block } // Include blocks defined in wrapper content for name, block := range contentEval.blocks { wrapperEval.blocks[name] = block } result, err := wrapperEval.Eval(tmpl) if err != nil { return err } e.output.WriteString(result) return nil } // evalTry evaluates a TRY/CATCH block func (e *Evaluator) evalTry(n *TryStmt) error { // Create a new evaluator for the TRY block to isolate output tryEval := NewEvaluator(e.renderer, e.copyVars()) for name, block := range e.blocks { tryEval.blocks[name] = block } // Attempt to evaluate the TRY block var tryErr error for _, node := range n.Try { if err := tryEval.evalNode(node); err != nil { tryErr = err break } } // If no error, use the TRY output if tryErr == nil { e.output.WriteString(tryEval.output.String()) return nil } // Error occurred - evaluate CATCH block if present if len(n.Catch) > 0 { catchEval := NewEvaluator(e.renderer, e.copyVars()) for name, block := range e.blocks { catchEval.blocks[name] = block } for _, node := range n.Catch { if err := catchEval.evalNode(node); err != nil { // Error in CATCH block - propagate it return err } } e.output.WriteString(catchEval.output.String()) } return nil } // evalExpr evaluates an expression and returns its value func (e *Evaluator) evalExpr(expr Expr) (any, error) { switch x := expr.(type) { case *LiteralExpr: return x.Value, nil case *IdentExpr: return e.resolveIdent(x.Parts) case *BinaryExpr: return e.evalBinary(x) case *UnaryExpr: return e.evalUnary(x) case *CallExpr: return e.evalCall(x) case *MethodCallExpr: return e.evalMethodCall(x) case *FilterExpr: return e.evalFilter(x) case *DefaultExpr: val, err := e.evalExpr(x.Expr) if err != nil || !e.isDefined(val) { return e.evalExpr(x.Default) } return val, nil } return nil, fmt.Errorf("unknown expression type: %T", expr) } // evalBinary evaluates a binary expression func (e *Evaluator) evalBinary(b *BinaryExpr) (any, error) { left, err := e.evalExpr(b.Left) if err != nil { return nil, err } right, err := e.evalExpr(b.Right) if err != nil { return nil, err } switch b.Op { case TokenPlus: // If either operand is a string, do string concatenation if e.isString(left) || e.isString(right) { return e.toString(left) + e.toString(right), nil } return e.toFloat(left) + e.toFloat(right), nil case TokenMinus: return e.toFloat(left) - e.toFloat(right), nil case TokenMul: return e.toFloat(left) * e.toFloat(right), nil case TokenDiv: r := e.toFloat(right) if r == 0 { return nil, &EvalError{Pos: b.Position, Message: "division by zero"} } return e.toFloat(left) / r, nil case TokenMod: return int(e.toFloat(left)) % int(e.toFloat(right)), nil case TokenEq: return e.equals(left, right), nil case TokenNe: return !e.equals(left, right), nil case TokenLt: return e.toFloat(left) < e.toFloat(right), nil case TokenLe: return e.toFloat(left) <= e.toFloat(right), nil case TokenGt: return e.toFloat(left) > e.toFloat(right), nil case TokenGe: return e.toFloat(left) >= e.toFloat(right), nil case TokenAnd: return e.isTruthy(left) && e.isTruthy(right), nil case TokenOr: return e.isTruthy(left) || e.isTruthy(right), nil } return nil, fmt.Errorf("unknown operator: %v", b.Op) } // evalUnary evaluates a unary expression func (e *Evaluator) evalUnary(u *UnaryExpr) (any, error) { val, err := e.evalExpr(u.X) if err != nil { return nil, err } switch u.Op { case TokenMinus: return -e.toFloat(val), nil } return nil, fmt.Errorf("unknown unary operator: %v", u.Op) } // evalCall evaluates a function call // All arguments are converted to strings before being passed to the function // The function signature should be: func(args ...string) any // or for no-arg functions: func() any func (e *Evaluator) evalCall(c *CallExpr) (any, error) { // Parse the function path (e.g., "h.mytest" -> ["h", "mytest"]) parts := strings.Split(c.Func, ".") // Resolve the function using dot notation without auto-invoking fn, err := e.resolveIdentNoAutoInvoke(parts) if err != nil { return nil, err } if fn == nil { return nil, &EvalError{ Pos: c.Position, Message: fmt.Sprintf("undefined function: %s", c.Func), } } fnValue := reflect.ValueOf(fn) if fnValue.Kind() != reflect.Func { return nil, &EvalError{ Pos: c.Position, Message: fmt.Sprintf("%s is not a function", c.Func), } } var args []reflect.Value for _, arg := range c.Args { val, err := e.evalExpr(arg) if err != nil { return nil, err } args = append(args, reflect.ValueOf(e.toString(val))) } results := fnValue.Call(args) if len(results) > 0 { return results[0].Interface(), nil } return nil, nil } // evalMethodCall evaluates a virtual method call: obj.method(args...) // Virtual methods take precedence over function calls in vars map func (e *Evaluator) evalMethodCall(m *MethodCallExpr) (any, error) { // First check if receiver is an IdentExpr and obj.method is a function (for backward compatibility) if ident, ok := m.Receiver.(*IdentExpr); ok { // Construct the full path: e.g., ["h", "inner", "nestedFunc"] fullPath := make([]string, len(ident.Parts)+1) copy(fullPath, ident.Parts) fullPath[len(ident.Parts)] = m.Method // Try to resolve it as a function fn, err := e.resolveIdentNoAutoInvoke(fullPath) if err == nil && fn != nil { fnValue := reflect.ValueOf(fn) if fnValue.Kind() == reflect.Func { // It's a function, call it as a regular function call var args []reflect.Value for _, arg := range m.Args { val, err := e.evalExpr(arg) if err != nil { return nil, err } args = append(args, reflect.ValueOf(e.toString(val))) } results := fnValue.Call(args) if len(results) > 0 { return results[0].Interface(), nil } return nil, nil } } } // Evaluate receiver (the object) receiver, err := e.evalExpr(m.Receiver) if err != nil { return nil, err } // Get the virtual method from custom methods methodFn, ok := e.renderer.getVirtualMethod(m.Method) if !ok { // Try built-in virtual methods with args return e.evalBuiltinVirtualMethodWithArgs(receiver, m.Method, m.Args) } // Evaluate arguments var args []any for _, arg := range m.Args { val, err := e.evalExpr(arg) if err != nil { return nil, err } args = append(args, val) } // Call virtual method: func(receiver, ...args) (any, bool) result, ok := methodFn(receiver, args...) if !ok { return nil, &EvalError{ Pos: m.Position, Message: fmt.Sprintf("virtual method '%s' returned false for %T", m.Method, receiver), } } return result, nil } // evalBuiltinVirtualMethodWithArgs evaluates built-in virtual methods that accept arguments func (e *Evaluator) evalBuiltinVirtualMethodWithArgs(receiver any, method string, args []Expr) (any, error) { // Convert args to evaluated values var argValues []any for _, arg := range args { val, err := e.evalExpr(arg) if err != nil { return nil, err } argValues = append(argValues, val) } switch method { case "get": // Dynamic map access: map.get(key) if len(argValues) != 1 { return nil, &EvalError{ Pos: args[0].(Expr).(*LiteralExpr).Position, Message: fmt.Sprintf("%s() expects 1 argument, got %d", method, len(argValues)), } } if m, ok := receiver.(map[string]any); ok { key := e.toString(argValues[0]) val, exists := m[key] if exists { return val, nil } return nil, nil } return nil, &EvalError{ Message: fmt.Sprintf("get() not supported for %T", receiver), } } return nil, &EvalError{ Message: fmt.Sprintf("unknown virtual method: %s", method), } } // evalFilter evaluates a filter expression func (e *Evaluator) evalFilter(f *FilterExpr) (any, error) { input, err := e.evalExpr(f.Input) if err != nil { return nil, err } inputStr := e.toString(input) filter, ok := e.renderer.getFilter(f.Filter) if !ok { return inputStr + fmt.Sprintf("[Filter '%s' not found]", f.Filter), nil } var args []string for _, arg := range f.Args { val, err := e.evalExpr(arg) if err != nil { return nil, err } args = append(args, e.toString(val)) } return filter(inputStr, args...), nil } // resolveIdent resolves a dot-notation identifier func (e *Evaluator) resolveIdent(parts []string) (any, error) { val, err := e.resolveIdentNoAutoInvoke(parts) if err != nil { return nil, err } if val == nil { return nil, nil } // Auto-invoke functions at the end of dot notation (for bareword syntax) fnValue := reflect.ValueOf(val) if fnValue.Kind() == reflect.Func { fnType := fnValue.Type() // Check if function takes no required arguments (0 args, or variadic that accepts 0) canCallWithNoArgs := fnType.NumIn() == 0 || (fnType.NumIn() == 1 && fnType.IsVariadic()) if canCallWithNoArgs { // Call the function with no arguments results := fnValue.Call([]reflect.Value{}) if len(results) > 0 { return results[0].Interface(), nil } return nil, nil } // Function requires arguments but called without parens - return nil return nil, nil } return val, nil } // resolveIdentNoAutoInvoke resolves a dot-notation identifier without auto-invoking functions // This is used when we need to resolve a function reference for calling func (e *Evaluator) resolveIdentNoAutoInvoke(parts []string) (any, error) { if len(parts) == 0 { return nil, nil } // Get the root variable val, ok := e.vars[parts[0]] // Handle .exists virtual method at top level if len(parts) == 2 && parts[1] == "exists" { return ok, nil } // Handle .defined virtual method at top level if len(parts) == 2 && parts[1] == "defined" { if !ok { return false, nil } return e.isDefined(val), nil } if !ok { return nil, nil } if len(parts) == 1 { return val, nil } // Navigate through the remaining parts for i := 1; i < len(parts); i++ { property := parts[i] // Virtual methods if property == "exists" { return true, nil } if property == "defined" { return e.isDefined(val), nil } // Check custom virtual methods if method, ok := e.renderer.getVirtualMethod(property); ok { result, _ := method(val) val = result continue } // Built-in virtual methods with args (called without parens) switch property { case "get": // get() called without args - return nil (requires args) val = nil continue } // Built-in virtual methods switch property { case "length", "size": switch v := val.(type) { case string: return len(v), nil case map[string]any: return len(v), nil default: rv := reflect.ValueOf(val) if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { return rv.Len(), nil } return nil, nil } case "first": rv := reflect.ValueOf(val) if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { if rv.Len() > 0 { val = rv.Index(0).Interface() continue } } return nil, nil case "last": rv := reflect.ValueOf(val) if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { if rv.Len() > 0 { val = rv.Index(rv.Len() - 1).Interface() continue } } return nil, nil default: // Try map access if m, ok := val.(map[string]any); ok { if v, exists := m[property]; exists { val = v continue } return nil, nil } // Try struct field access via reflection rv := reflect.ValueOf(val) if rv.Kind() == reflect.Ptr { rv = rv.Elem() } if rv.Kind() == reflect.Struct { field := rv.FieldByName(property) if field.IsValid() { val = field.Interface() continue } } return nil, nil } } return val, nil } // ---- Helper methods ---- // withVar creates a new evaluator with an additional variable func (e *Evaluator) withVar(name string, value any) *Evaluator { newVars := e.copyVars() newVars[name] = value eval := NewEvaluator(e.renderer, newVars) for k, v := range e.blocks { eval.blocks[k] = v } return eval } // copyVars creates a shallow copy of the variables map func (e *Evaluator) copyVars() map[string]any { newVars := make(map[string]any, len(e.vars)) for k, v := range e.vars { newVars[k] = v } return newVars } // isTruthy determines if a value is considered "true" func (e *Evaluator) isTruthy(val any) bool { if val == nil { return false } switch v := val.(type) { case bool: return v case string: return v != "" case int: return v != 0 case int64: return v != 0 case float64: return v != 0 case float32: return v != 0 default: rv := reflect.ValueOf(val) switch rv.Kind() { case reflect.Slice, reflect.Array: return rv.Len() > 0 case reflect.Map: return rv.Len() > 0 } return true } } // isDefined checks if a value is "defined" (non-nil and non-empty for strings) func (e *Evaluator) isDefined(val any) bool { if val == nil { return false } switch v := val.(type) { case string: return v != "" case map[string]any: return v != nil default: rv := reflect.ValueOf(val) if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { return !rv.IsNil() } return true } } // isString checks if a value is a string func (e *Evaluator) isString(v any) bool { _, ok := v.(string) return ok } // toString converts a value to string func (e *Evaluator) toString(v any) string { switch val := v.(type) { case string: return val case nil: return "" case bool: if val { return "true" } return "false" case float64: if val == float64(int64(val)) { return strconv.FormatInt(int64(val), 10) } return strconv.FormatFloat(val, 'f', -1, 64) case int: return strconv.Itoa(val) case int64: return strconv.FormatInt(val, 10) default: return fmt.Sprintf("%v", val) } } // toFloat converts a value to float64 func (e *Evaluator) toFloat(v any) float64 { switch val := v.(type) { case float64: return val case float32: return float64(val) case int: return float64(val) case int64: return float64(val) case int32: return float64(val) case string: f, _ := strconv.ParseFloat(val, 64) return f default: return 0 } } // equals compares two values for equality func (e *Evaluator) equals(left, right any) bool { // Try numeric comparison leftNum, leftOk := e.tryFloat(left) rightNum, rightOk := e.tryFloat(right) if leftOk && rightOk { return leftNum == rightNum } // Fall back to string comparison return fmt.Sprintf("%v", left) == fmt.Sprintf("%v", right) } // tryFloat attempts to convert a value to float64 func (e *Evaluator) tryFloat(v any) (float64, bool) { switch val := v.(type) { case float64: return val, true case float32: return float64(val), true case int: return float64(val), true case int64: return float64(val), true case int32: return float64(val), true case string: if f, err := strconv.ParseFloat(val, 64); err == nil { return f, true } } return 0, false } // ---- Error type ---- // EvalError represents an error during evaluation type EvalError struct { Pos Position Message string } func (e *EvalError) Error() string { return fmt.Sprintf("line %d, column %d: %s", e.Pos.Line, e.Pos.Column, e.Message) } // ParseError represents an error during parsing type ParseError struct { Pos Position Message string } func (e *ParseError) Error() string { return fmt.Sprintf("parse error at line %d, column %d: %s", e.Pos.Line, e.Pos.Column, e.Message) }