aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/escapevelocity/Macro.java
blob: afa7bf01c81150d4cfa12cedf00e9f1ce6a55581 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/*
 * Copyright (C) 2018 Google, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.escapevelocity;

import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * A macro definition. Macros appear in templates using the syntax {@code #macro (m $x $y) ... #end}
 * and each one produces an instance of this class. Evaluating a macro involves setting the
 * parameters (here {$x $y)} and evaluating the macro body. Macro arguments are call-by-name, which
 * means that we need to set each parameter variable to the node in the parse tree that corresponds
 * to it, and arrange for that node to be evaluated when the variable is actually referenced.
 *
 * @author emcmanus@google.com (Éamonn McManus)
 */
class Macro {
  private final int definitionLineNumber;
  private final String name;
  private final ImmutableList<String> parameterNames;
  private final Node body;

  Macro(int definitionLineNumber, String name, List<String> parameterNames, Node body) {
    this.definitionLineNumber = definitionLineNumber;
    this.name = name;
    this.parameterNames = ImmutableList.copyOf(parameterNames);
    this.body = body;
  }

  String name() {
    return name;
  }

  int parameterCount() {
    return parameterNames.size();
  }

  Object evaluate(EvaluationContext context, List<Node> thunks) {
    try {
      Verify.verify(thunks.size() == parameterNames.size(), "Argument mistmatch for %s", name);
      Map<String, Node> parameterThunks = new LinkedHashMap<>();
      for (int i = 0; i < parameterNames.size(); i++) {
        parameterThunks.put(parameterNames.get(i), thunks.get(i));
      }
      EvaluationContext newContext = new MacroEvaluationContext(parameterThunks, context);
      return body.evaluate(newContext);
    } catch (EvaluationException e) {
      EvaluationException newException = new EvaluationException(
          "In macro #" + name + " defined on line " + definitionLineNumber + ": " + e.getMessage());
      newException.setStackTrace(e.getStackTrace());
      throw e;
    }
  }

  /**
   * The context for evaluation within macros. This wraps an existing {@code EvaluationContext}
   * but intercepts reads of the macro's parameters so that they result in a call-by-name evaluation
   * of whatever was passed as the parameter. For example, if you write...
   * <pre>{@code
   * #macro (mymacro $x)
   * $x $x
   * #end
   * #mymacro($foo.bar(23))
   * }</pre>
   * ...then the {@code #mymacro} call will result in {@code $foo.bar(23)} being evaluated twice,
   * once for each time {@code $x} appears. The way this works is that {@code $x} is a <i>thunk</i>.
   * Historically a thunk is a piece of code to evaluate an expression in the context where it
   * occurs, for call-by-name procedures as in Algol 60. Here, it is not exactly a piece of code,
   * but it has the same responsibility.
   */
  static class MacroEvaluationContext implements EvaluationContext {
    private final Map<String, Node> parameterThunks;
    private final EvaluationContext originalEvaluationContext;

    MacroEvaluationContext(
        Map<String, Node> parameterThunks, EvaluationContext originalEvaluationContext) {
      this.parameterThunks = parameterThunks;
      this.originalEvaluationContext = originalEvaluationContext;
    }

    @Override
    public Object getVar(String var) {
      Node thunk = parameterThunks.get(var);
      if (thunk == null) {
        return originalEvaluationContext.getVar(var);
      } else {
        // Evaluate the thunk in the context where it appeared, not in this context. Otherwise
        // if you pass $x to a parameter called $x you would get an infinite recursion. Likewise
        // if you had #macro(mymacro $x $y) and a call #mymacro($y 23), you would expect that $x
        // would expand to whatever $y meant at the call site, rather than to the value of the $y
        // parameter.
        return thunk.evaluate(originalEvaluationContext);
      }
    }

    @Override
    public boolean varIsDefined(String var) {
      return parameterThunks.containsKey(var) || originalEvaluationContext.varIsDefined(var);
    }

    @Override
    public Runnable setVar(final String var, Object value) {
      // Copy the behaviour that #set will shadow a macro parameter, even though the Velocity peeps
      // seem to agree that that is not good.
      final Node thunk = parameterThunks.get(var);
      if (thunk == null) {
        return originalEvaluationContext.setVar(var, value);
      } else {
        parameterThunks.remove(var);
        final Runnable originalUndo = originalEvaluationContext.setVar(var, value);
        return () -> {
          originalUndo.run();
          parameterThunks.put(var, thunk);
        };
      }
    }

    @Override
    public ImmutableSet<Method> publicMethodsWithName(Class<?> startClass, String name) {
      return originalEvaluationContext.publicMethodsWithName(startClass, name);
    }
  }
}