001/*
002 * Copyright (c) 2012-2021 Institut National des Sciences Appliquées de Lyon (INSA Lyon) and others
003 *
004 * This program and the accompanying materials are made available under the
005 * terms of the Eclipse Public License 2.0 which is available at
006 * http://www.eclipse.org/legal/epl-2.0.
007 *
008 * SPDX-License-Identifier: EPL-2.0
009 */
010
011package org.eclipse.golo.compiler.macro;
012
013import gololang.ir.*;
014import org.eclipse.golo.compiler.GoloCompilationException;
015import org.eclipse.golo.compiler.PositionInSourceCode;
016import org.eclipse.golo.compiler.StopCompilationException;
017
018import java.util.*;
019import java.lang.invoke.MethodHandle;
020import java.util.function.Function;
021
022import static java.util.Objects.requireNonNull;
023import static org.eclipse.golo.compiler.GoloCompilationException.Problem.Type.*;
024import static org.eclipse.golo.cli.command.Metadata.GUIDE_BASE;
025import static gololang.Messages.message;
026import static gololang.Messages.info;
027
028
029/**
030 * Visitor to expand macro calls.
031 * <p>
032 * This visitor replace the {@code MacroInvocation} nodes with the result of the macro
033 * expansion.
034 */
035public final class MacroExpansionIrVisitor extends AbstractGoloIrVisitor {
036
037  public static final class RecursionLimitException extends RuntimeException {
038    RecursionLimitException(String message) {
039      super(message);
040    }
041
042    static RecursionLimitException of(int limit) {
043      return new RecursionLimitException(message("macro_recursion_limit", limit, GUIDE_BASE));
044    }
045  }
046
047  private static final boolean DEBUG = Boolean.getBoolean("golo.debug.macros");
048  private static final int RECURSION_LIMIT = Integer.getInteger("golo.macros.recursion-limit", 42);
049
050  private GoloCompilationException.Builder exceptionBuilder;
051  private final MacroFinder finder;
052
053  private boolean expandRegularCalls = true;
054  private boolean recurse = true;
055  private int recursionLimit = RECURSION_LIMIT;
056  private int recursionLevel = 0;
057  private boolean defaultRecurse = true;
058
059  public MacroExpansionIrVisitor(ClassLoader loader, boolean defaultRecurse, GoloCompilationException.Builder exceptionBuilder) {
060    this.finder = new MacroFinder(loader);
061    this.defaultRecurse = defaultRecurse;
062    this.exceptionBuilder = exceptionBuilder;
063  }
064
065  private static void debug(String message, Object... args) {
066    if (DEBUG || gololang.Runtime.debugMode()) {
067      info("Macro expansion: " + String.format(message, args));
068    }
069  }
070
071  /**
072   * Reset the internal state for the given module.
073   *
074   * We don't keep the previous state. Should we ever implement submodules, the previous state would then need to be
075   * restored when leaving the submodule (state stack).
076   */
077  private MacroExpansionIrVisitor reset(GoloModule module) {
078    this.finder.init(module.getImports().stream().map(mi -> mi.getPackageAndClass().toString()));
079    this.expandRegularCalls = true;
080    this.recurse = defaultRecurse;
081    this.recursionLimit = RECURSION_LIMIT;
082    this.recursionLevel = 0;
083    if (this.exceptionBuilder == null) {
084      this.exceptionBuilder = new GoloCompilationException.Builder(module == null ? "null" : module.sourceFile());
085    }
086    debug("reset for module %s", module);
087    return this;
088  }
089
090  /**
091   * Defines if the macros must be expanded recursively.
092   * <p>
093   * Mainly for debugging purpose.
094   */
095  public MacroExpansionIrVisitor recurse(boolean v) {
096    this.recurse = v;
097    return this;
098  }
099
100  /**
101   * Check if we must recurse.
102   */
103  private boolean mustRecurse(AbstractInvocation<?> macro) {
104    if (recursionLimit > 0 && recursionLevel >= recursionLimit) {
105      expansionFailed(macro, RecursionLimitException.of(recursionLimit));
106      return false;
107    }
108    return this.recurse;
109  }
110
111  /**
112   * Defines the expansion recursion limit.
113   */
114  public MacroExpansionIrVisitor recursionLimit(int v) {
115    this.recursionLimit = v;
116    return this;
117  }
118
119  /**
120   * Returns the current macro recursion level.
121   */
122  public int recursionLevel() {
123    return this.recursionLevel;
124  }
125
126  /**
127   * Defines if regular function invocations must be tried to expand.
128   * <p>
129   * Mainly for debugging purpose.
130   */
131  public MacroExpansionIrVisitor expandRegularCalls(boolean v) {
132    this.expandRegularCalls = v;
133    return this;
134  }
135
136  public void setExceptionBuilder(GoloCompilationException.Builder builder) {
137    exceptionBuilder = builder;
138  }
139
140  private void replace(AbstractInvocation<?> invocation, GoloElement<?> original, GoloElement<?> replacement) {
141    try {
142      original.replaceInParentBy(replacement);
143    } catch (StopCompilationException t) {
144      // TODO: test
145        throw t;
146    } catch (Throwable t) {
147      expansionFailed(invocation, t);
148    }
149  }
150
151  @Override
152  public void visitFunction(GoloFunction function) {
153    GoloElement<?> converted = convertMacroDecorator(function);
154    if (converted instanceof MacroInvocation) {
155      replace((MacroInvocation) converted, function, converted);
156      converted.accept(this);
157    } else {
158      function.walk(this);
159    }
160  }
161
162  /**
163   * Convert a function with macro decorators into nested macro calls.
164   * <p>
165   * The function node is <em>mutated</em> (decorator removed).
166   */
167  private GoloElement<?> convertMacroDecorator(GoloFunction function) {
168    GoloFunction newFunction = GoloFunction.function(function);
169    GoloElement<?> newElement = newFunction;
170    List<Decorator> decos = new LinkedList<>();
171    for (Decorator decorator : function.getDecorators()) {
172      MacroInvocation invocation = decoratorToMacroInvocation(decorator, newElement);
173      if (invocation != null && macroExists(invocation)) {
174        newElement = invocation;
175      } else {
176        decos.add(decorator);
177      }
178    }
179    if (newElement != newFunction) {
180      newFunction.block(function.getBlock());
181      for (Decorator d : decos) {
182        newFunction.addDecorator(d);
183      }
184      return newElement;
185    }
186    return function;
187  }
188
189  /**
190   * Convert a macro decorator into macro call on the function declaration.
191   * <p>
192   * For instance
193   * <pre><code>
194   * @myMacro
195   * function foo = |x| -> x
196   * </code></pre>
197   *
198   * is converted into something equivalent to:
199   * <pre><code>
200   * &myMacro {
201   * function foo = |x| -> x
202   * }
203   * </pre></code>
204   * that is a macro call on a function declaration node.
205   */
206  private MacroInvocation decoratorToMacroInvocation(Decorator decorator, GoloElement<?> function) {
207    ExpressionStatement<?> expr = decorator.expression();
208    if (expr instanceof FunctionInvocation) {
209      FunctionInvocation invocation = (FunctionInvocation) expr;
210      return MacroInvocation.call(invocation.getName())
211        .withArgs(invocation.getArguments().toArray())
212        .withArgs(function);
213    } else if (expr instanceof ReferenceLookup) {
214      return MacroInvocation.call(((ReferenceLookup) expr).getName())
215        .withArgs(function);
216    } else if (expr instanceof ClosureReference) {
217      // Not (yet?) a valid macro call
218      return null;
219    } else if (expr instanceof BinaryOperation) {
220      // Not (yet?) a valid macro call
221      return null;
222    } else {
223      // must not happen
224      throw new IllegalArgumentException("Invalid decorator type");
225    }
226  }
227
228  public GoloElement<?> expand(GoloElement<?> element) {
229    element.accept(this);
230    return element;
231  }
232
233  @Override
234  public void visitModule(GoloModule module) {
235    this.reset(module);
236    module.walk(this);
237    module.decoratorMacro().map(this::expandMacro);
238    module.decoratorMacro(null);
239  }
240
241  @Override
242  public void visitMacroInvocation(MacroInvocation macroInvocation) {
243    macroInvocation.walk(this);
244    GoloElement<?> expanded = expandMacro(macroInvocation);
245    replace(macroInvocation, macroInvocation, expanded);
246    if (mustRecurse(macroInvocation)) {
247      recursionLevel++;
248      expanded.accept(this);
249      recursionLevel--;
250    }
251  }
252
253  @Override
254  public void visitFunctionInvocation(FunctionInvocation macroInvocation) {
255    macroInvocation.walk(this);
256    if (tryExpand(macroInvocation)) {
257      // Let's try to expand a regular call as a macro
258      GoloElement<?> expanded = expandMacro(macroInvocation);
259      if (expanded == null) {
260        // Maybe it was not a macro after all...
261        return;
262      }
263      replace(macroInvocation, macroInvocation, expanded);
264      if (mustRecurse(macroInvocation)) {
265        recursionLevel++;
266        expanded.accept(this);
267        recursionLevel--;
268      }
269    }
270  }
271
272  private boolean tryExpand(FunctionInvocation invocation) {
273    return expandRegularCalls && !invocation.isAnonymous() && !invocation.isConstant();
274  }
275
276  public MacroExpansionIrVisitor useMacroModule(String name) {
277    this.finder.addMacroClass(name);
278    return this;
279  }
280
281  private GoloElement<?> expandMacro(FunctionInvocation invocation) {
282    debug("try to expand %s", invocation);
283    Optional<MethodHandle> macro = findMacro(invocation);
284    if (!macro.isPresent()) {
285      debug("macro not found");
286      return null;
287    }
288    return macro.map(invokeMacroWith(invocation)).orElse(noMacroResult(invocation.getName()));
289  }
290
291  private GoloElement<?> expandMacro(MacroInvocation invocation) {
292    debug("try to expand %s", invocation);
293    return findMacro(invocation)
294      .map(invokeMacroWith(invocation))
295      .orElse(noMacroResult(invocation.getName()));
296  }
297
298  private Function<MethodHandle, GoloElement<?>> invokeMacroWith(AbstractInvocation<?> invocation) {
299    return (macro) -> {
300      try {
301        GoloElement<?> result = (GoloElement<?>) macro.invokeWithArguments(invocation.getArguments());
302        debug("macro expanded to %s", result);
303        return result;
304      } catch (StopCompilationException e) {
305        throw e;
306      } catch (Throwable t) {
307        expansionFailed(invocation, t);
308        debug("expansion failed");
309        return null;
310      }
311    };
312  }
313
314  private GoloElement<?> noMacroResult(String macroName) {
315    return Noop.of("macro `" + macroName + "` expanded without results");
316  }
317
318  private void loadingFailed(MacroInvocation invocation) {
319    String errorMessage = message("macro_loading_failed", invocation.getName(), invocation.getArity())
320        + ' ' + position(invocation) + ".";
321    exceptionBuilder.report(UNKNOWN_MACRO, invocation, errorMessage);
322  }
323
324  private void expansionFailed(AbstractInvocation<?> invocation, Throwable t) {
325    String errorMessage = message("macro_expansion_failed", invocation.getName())
326      + ' ' + position(invocation) + ".";
327    exceptionBuilder.report(MACRO_EXPANSION, invocation, errorMessage, t);
328  }
329
330  private String position(GoloElement<?> elt) {
331    PositionInSourceCode position = elt.positionInSourceCode();
332    if (position == null || position.isUndefined()) {
333      return message("generated_code");
334    }
335    return message("source_position", position.getStartLine(), position.getStartColumn());
336  }
337
338  private Optional<MethodHandle> findMacro(FunctionInvocation invocation) {
339    return finder.find(invocation).map(m -> m.binded(this, invocation));
340  }
341
342  private Optional<MethodHandle> findMacro(MacroInvocation invocation) {
343    Optional<MethodHandle> macro = finder.find(invocation).map(m -> m.binded(this, invocation));
344    if (!macro.isPresent()) {
345      loadingFailed(invocation);
346    }
347    return macro;
348  }
349
350  public boolean macroExists(MacroInvocation invocation) {
351    requireNonNull(invocation);
352    boolean exists = finder.find(invocation).isPresent();
353    debug("Check if %s exists: %s", invocation.getName(), exists);
354    return exists;
355  }
356}