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}