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;
012
013import gololang.ir.*;
014import gololang.Messages;
015import java.util.List;
016import java.util.Deque;
017import java.util.LinkedList;
018import java.util.function.Function;
019
020import static gololang.ir.MethodInvocation.invoke;
021import static gololang.ir.ConditionalBranching.branch;
022import static gololang.ir.TryCatchFinally.DUMMY_TRY_RESULT_VARIABLE;
023
024/**
025 * Visitor to expand some syntactic sugar.
026 * <p>
027 * Modify the IR to transform some nodes, in order to expand syntactic sugar, such as case and match
028 * statements, foreach loops, list comprehension, and so on.
029 */
030public class SugarExpansionVisitor extends AbstractGoloIrVisitor {
031
032  private final SymbolGenerator symbols = new SymbolGenerator("golo.compiler.sugar");
033  private final List<GoloFunction> functionsToAdd = new LinkedList<>();
034  private GoloModule module;
035
036  private boolean useNewStyleDestruct() {
037    if (this.module.metadata("golo.destruct.newstyle") != null) {
038      return (boolean) this.module.metadata("golo.destruct.newstyle");
039    }
040    return gololang.Runtime.loadBoolean("true", "golo.destruct.newstyle", "GOLO_DESTRUCT_NEWSTYLE", true);
041  }
042
043  @Override
044  public void visitModule(GoloModule module) {
045    this.module = module;
046    module.walk(this);
047    for (GoloFunction f : functionsToAdd) {
048      module.addFunction(f);
049    }
050  }
051
052  /**
053   * Generate the local function corresponding to this closure.
054   */
055  @Override
056  public void visitBlock(Block block) {
057    visitExpression(block);
058    block.flatten();
059  }
060
061  @Override
062  public void visitClosureReference(ClosureReference closure) {
063    functionsToAdd.add(closure.getTarget().name(symbols.next("closure")));
064    visitExpression(closure);
065  }
066
067  @Override
068  public void visitFunction(GoloFunction function) {
069    function.insertMissingReturnStatement();
070    if (!expressionToBlock(function)) {
071      function.walk(this);
072      if (function.isDecorated() && function.hasParent()) {
073        FunctionContainer parent = (FunctionContainer) function.parent();
074        GoloFunction decorator = function.createDecorator();
075        parent.addFunction(decorator);
076        decorator.accept(this);
077      }
078      function.insertMissingReturnStatement();
079    }
080  }
081
082  /**
083   * Case expansion.
084   * <p>
085   * Convert a {@link CaseStatement} node into a imbrication of {@link ConditionalBranching}, and
086   * replace it in the parent node.
087   * For instance:
088   * <pre class="listing"><code class="lang-golo" data-lang="golo">
089   * case {
090   *   when cond1 { block1 }
091   *   when cond2 { block2 }
092   *   otherwise { block3 }
093   * }
094   * </code></pre>
095   * is converted into the equivalent of
096   * <pre class="listing"><code class="lang-golo" data-lang="golo">
097   * if cond1 { block1 }
098   * else if cond2 { block2 }
099   * else { block3 }
100   * </code></pre>
101   */
102  @Override
103  public void visitCaseStatement(CaseStatement caseStatement) {
104    ConditionalBranching branch = convertCaseToConditional(caseStatement);
105    caseStatement.replaceInParentBy(branch);
106    branch.accept(this);
107  }
108
109  private ConditionalBranching convertCaseToConditional(CaseStatement caseStatement) {
110    Deque<WhenClause<Block>> clauses = new LinkedList<>(caseStatement.getClauses());
111    WhenClause<Block> lastClause = clauses.removeLast();
112    ConditionalBranching branch = branch()
113      .condition(lastClause.condition())
114      .whenTrue(lastClause.action())
115      .whenFalse(caseStatement.getOtherwise())
116      .positionInSourceCode(lastClause.positionInSourceCode());
117    while (!clauses.isEmpty()) {
118      lastClause = clauses.removeLast();
119      branch = branch()
120        .condition(lastClause.condition())
121        .whenTrue(lastClause.action())
122        .elseBranch(branch)
123        .positionInSourceCode(lastClause.positionInSourceCode());
124    }
125    return branch;
126  }
127
128  /**
129   * Match expansion.
130   * <p>
131   * Convert a {@link MatchExpression} node into a block containing a {@link CaseStatement}.
132   * For instance
133   * <pre class="listing"><code class="lang-golo" data-lang="golo">
134   * match {
135   *   when cond1 then val1
136   *   when cond2 then val2
137   *   otherwise val3
138   * }
139   * </code></pre>
140   * is converted into the equivalent of
141   * <pre class="listing"><code class="lang-golo" data-lang="golo">
142   * tmp = null
143   * case {
144   *   when cond1 { tmp = val1 }
145   *   when cond2 { tmp = val2 }
146   *   otherwise { tmp = val3 }
147   * }
148   * tmp
149   * </code></pre>
150   */
151  @Override
152  public void visitMatchExpression(MatchExpression matchExpression) {
153    LocalReference tempVar = LocalReference.of(symbols.next("match"))
154      .variable()
155      .synthetic();
156    Block converted = convertMatchToBlock(matchExpression, (e) -> AssignmentStatement.create(tempVar, e, false));
157    converted.prepend(AssignmentStatement.create(tempVar, ConstantStatement.of(null), true));
158    converted.add(tempVar.lookup());
159    matchExpression.replaceInParentBy(converted);
160    converted.accept(this);
161  }
162
163  private static Block convertMatchToBlock(MatchExpression matchExpression, Function<ExpressionStatement<?>, ? extends GoloStatement<?>> mapping) {
164    Block converted = Block.empty();
165    for (GoloAssignment<?> a : matchExpression.declarations()) {
166      converted.add(a);
167    }
168    matchExpression.clearDeclarations();
169    converted.add(convertMatchToMappedCase(matchExpression, mapping));
170    return converted;
171  }
172
173  private static CaseStatement convertMatchToMappedCase(MatchExpression matchExpression, Function<ExpressionStatement<?>, ? extends GoloStatement<?>> mapping) {
174    CaseStatement caseStatement = CaseStatement.cases()
175      .otherwise(Block.of(mapping.apply(matchExpression.getOtherwise())));
176
177    for (WhenClause<ExpressionStatement<?>> c : matchExpression.getClauses()) {
178      caseStatement.when(c.condition())
179        .then(mapping.apply(c.action()).positionInSourceCode(c.action().positionInSourceCode()))
180        .positionInSourceCode(c.positionInSourceCode());
181    }
182    return caseStatement;
183  }
184
185  /**
186   * Special returned values.
187   *
188   * <p>
189   * Deal with special return cases:
190   * <ul>
191   *   <li>when returning a {@link MatchExpression}, convert it to a
192   * {@link CaseStatement} containing {@link ReturnStatement} instead of assignments.
193   * For instance
194   * <pre>
195   * return match {
196   *   when cond1 then val1
197   *   when cond2 then val2
198   *   otherwise val3
199   * }
200   * </pre>
201   * is converted into the equivalent of
202   * <pre>
203   * case {
204   *   when cond1 {
205   *     return val1
206   *   }
207   *   when cond2 {
208   *     return val2
209   *   }
210   *   otherwise {
211   *     return val3
212   *   }
213   * }
214   * </pre>
215   *
216   * This will allows for better optimisations (e.g. TCE).
217   * </li>
218   * </ul>
219   */
220  @Override
221  public void visitReturnStatement(ReturnStatement ret) {
222    if (ret.expression() instanceof MatchExpression) {
223      MatchExpression matchExpression = (MatchExpression) ret.expression();
224      Block converted = convertMatchToBlock(matchExpression, ReturnStatement::of);
225      ret.replaceInParentBy(converted);
226      converted.accept(this);
227    } else {
228      super.visitReturnStatement(ret);
229    }
230  }
231
232  /**
233   * Literal expansion.
234   * <p>
235   * Converts a collection literal into a call to {@code gololang.Predefined.<type>}.
236   */
237  @Override
238  public void visitCollectionLiteral(CollectionLiteral collection) {
239    if (!expressionToBlock(collection)) {
240      collection.walk(this);
241      AbstractInvocation<?> construct = FunctionInvocation.of("gololang.Predefined." + collection.getType().toString())
242        .withArgs(collection.getExpressions().toArray());
243      collection.replaceInParentBy(construct);
244      construct.accept(this);
245    }
246  }
247
248  /**
249   * Converts a literal function reference into a call to {@code Predefined.fun}.
250   */
251  @Override
252  public void visitConstantStatement(ConstantStatement constantStatement) {
253    constantStatement.walk(this);
254    Object value = constantStatement.value();
255    if (value instanceof FunctionRef) {
256      FunctionInvocation fun = literalFunctionRefToCall((FunctionRef) value);
257      constantStatement.replaceInParentBy(fun);
258      fun.accept(this);
259    }
260  }
261
262  private FunctionInvocation literalFunctionRefToCall(FunctionRef ref) {
263    return FunctionInvocation.of("gololang.Predefined.fun").constant().withArgs(
264            ConstantStatement.of(ref.name()),
265            ConstantStatement.of(ClassReference.of(ref.module() == null
266              ? this.module.getPackageAndClass()
267              : ref.module())),
268            ConstantStatement.of(ref.arity()),
269            ConstantStatement.of(ref.varargs()));
270
271  }
272
273  /**
274   * Collection comprehension expansion.
275   * <p>
276   * Convert a list comprehension expression into a block initialising a collection from nested
277   * loops defined in the comprehension.
278   * For instance
279   * <pre class="listing"><code class="lang-golo" data-lang="golo">
280   * list[ f(x, y) foreach x in col1 foreach y in col2 ]
281   * </code></pre>
282   * is converted to the equivalent of
283   * <pre class="listing"><code class="lang-golo" data-lang="golo">
284   * let collection = list[]
285   * foreach x in col1 {
286   *   foreach y in col2 {
287   *     collection: add(f(x, y))
288   *   }
289   * }
290   * </code></pre>
291   */
292  @Override
293  public void visitCollectionComprehension(CollectionComprehension collection) {
294    CollectionLiteral.Type tempColType = collection.getMutableType();
295    LocalReference tempVar = LocalReference.of(symbols.next("comprehension"))
296      .variable()
297      .synthetic();
298    Block mainBlock = Block.empty();
299    for (GoloAssignment<?> a : collection.declarations()) {
300      mainBlock.add(a);
301    }
302    collection.clearDeclarations();
303    mainBlock.add(AssignmentStatement.create(tempVar, CollectionLiteral.create(tempColType), true));
304    Block innerBlock = mainBlock;
305    for (GoloStatement<?> loop : collection.loops()) {
306      innerBlock.add(loop);
307      innerBlock = ((BlockContainer) loop).getBlock();
308    }
309    innerBlock.add(
310        invoke("add").withArgs(collection.expression()).on(tempVar.lookup()));
311
312    if (collection.getType() == CollectionLiteral.Type.array
313        || collection.getType() == CollectionLiteral.Type.tuple) {
314      mainBlock.add(
315          AssignmentStatement.create(tempVar, invoke("toArray").on(tempVar.lookup()), false));
316    }
317
318    if (collection.getType() == CollectionLiteral.Type.tuple) {
319      mainBlock.add(
320          AssignmentStatement.create(tempVar, FunctionInvocation.of("Tuple.fromArray").withArgs(tempVar.lookup()), false));
321    }
322
323    mainBlock.add(tempVar.lookup());
324    collection.replaceInParentBy(mainBlock);
325    mainBlock.accept(this);
326  }
327
328  /**
329   * ForEach expansion.
330   * <p>
331   * Convert a {@code ForEachLoopStatement} into a loop using the associated iterator.
332   * For instance:
333   * <pre class="listing"><code class="lang-golo" data-lang="golo">
334   * foreach x in expr {
335   *   block
336   * }
337   * </code></pre>
338   * is converted to:
339   * <pre class="listing"><code class="lang-golo" data-lang="golo">
340   * for (__$$_iterator_0 = expr: iterator(), __$$_iterator_0: hasNext(),) {
341   *   let x = __$$_iterator_0: next()
342   *   block
343   * }
344   * </code></pre>
345   */
346  @Override
347  public void visitForEachLoopStatement(ForEachLoopStatement foreachStatement) {
348    LocalReference iterVar = LocalReference.of(symbols.next("forEachIterator"))
349      .variable()
350      .synthetic();
351
352    // deal with when clause
353    Block loopInnerBlock;
354    if (foreachStatement.hasWhenClause()) {
355      loopInnerBlock = Block.of(
356          branch()
357          .condition(foreachStatement.getWhenClause())
358          .whenTrue(foreachStatement.getBlock()))
359        .positionInSourceCode(foreachStatement.positionInSourceCode());
360    } else {
361      loopInnerBlock = foreachStatement.getBlock();
362    }
363
364    // init the reference to the next iterator value
365    if (foreachStatement.isDestructuring()) {
366      loopInnerBlock.prepend(
367          DestructuringAssignment.destruct(invoke("next").on(iterVar.lookup())).declaring()
368          .varargs(foreachStatement.isVarargs())
369          .to((Object[]) foreachStatement.getReferences()));
370    } else {
371      loopInnerBlock.prepend(
372          AssignmentStatement.create(foreachStatement.getLocalReference(), invoke("next").on(iterVar.lookup()), true));
373    }
374
375    // build the equivalent loop
376    LoopStatement newLoop = LoopStatement.loop()
377      .init(
378          AssignmentStatement.create(iterVar, invoke("iterator").on(foreachStatement.getIterable()), true))
379      .condition(
380          invoke("hasNext").on(iterVar.lookup()))
381      .block(loopInnerBlock);
382    foreachStatement.replaceInParentBy(newLoop);
383    newLoop.accept(this);
384  }
385
386  /**
387   * Destructuring assignment expansion.
388   */
389  @Override
390  public void visitDestructuringAssignment(DestructuringAssignment assignment) {
391    Block replacement = useNewStyleDestruct() ? newDestructuring(assignment) : oldDestructuring(assignment);
392    assignment.replaceInParentBy(replacement);
393    replacement.accept(this);
394  }
395
396  /**
397   * Destructuring assignment expansion.
398   * <p>Old version (before 3.4.0)
399   * <p>
400   * Converts code like
401   * <pre class="listing"><code class="lang-golo" data-lang="golo">
402   * let a, b, c... = expr
403   * </code></pre>
404   * into something equivalent to
405   * <pre class="listing"><code class="lang-golo" data-lang="golo">
406   * let tmp = expr: destruct()
407   * let a = tmp: get(0)
408   * let b = tmp: get(1)
409   * let c = tmp: subTuple(2)
410   * </code></pre>
411   */
412  private Block oldDestructuring(DestructuringAssignment assignment) {
413    Messages.warning(Messages.message("oldstyle_destruct", this.module.getPackageAndClass()), assignment);
414    LocalReference tmpRef = LocalReference.of(symbols.next("destruct")).synthetic();
415    Block block = Block.of(AssignmentStatement.create(tmpRef, invoke("destruct").on(assignment.expression()), true));
416    int last = assignment.getReferencesCount() - 1;
417    int idx = 0;
418    for (LocalReference ref : assignment.getReferences()) {
419      block.add(
420          AssignmentStatement.create(
421            ref,
422            invoke(assignment.isVarargs() && idx == last ? "subTuple" : "get")
423            .withArgs(ConstantStatement.of(idx))
424            .on(tmpRef.lookup()),
425            assignment.isDeclaring()));
426      idx++;
427    }
428    return block;
429  }
430
431  /**
432   * Destructuring assignment expansion.
433   * <p>Old version (after 3.4.0)
434   * <p>
435   * Converts code like
436   * <pre class="listing"><code class="lang-golo" data-lang="golo">
437   * let a, b, c... = expr
438   * </code></pre>
439   * into something equivalent to
440   * <pre class="listing"><code class="lang-golo" data-lang="golo">
441   * let tmp = expr: __$$_destruct(3, true, array[false, false, false])
442   * let a = tmp: get(0)
443   * let b = tmp: get(1)
444   * let c = tmp: get(2)
445   * </code></pre>
446   */
447  private Block newDestructuring(DestructuringAssignment assignment) {
448    LocalReference tmpRef = LocalReference.of(symbols.next("destruct")).synthetic();
449    Block block = Block.empty();
450    Object[] toSkip = new Object[assignment.getReferencesCount()];
451    int idx = 0;
452    for (LocalReference ref : assignment.getReferences()) {
453      if ("_".equals(ref.getName())) {
454        toSkip[idx] = ConstantStatement.of(true);
455      } else {
456        toSkip[idx] = ConstantStatement.of(false);
457        block.add(
458            AssignmentStatement.create(
459              ref,
460              invoke("get").withArgs(ConstantStatement.of(idx)).on(tmpRef.lookup()),
461              assignment.isDeclaring()));
462      }
463      idx++;
464    }
465    block.prepend(AssignmentStatement.create(tmpRef,
466          invoke("__$$_destruct")
467          .withArgs(
468            ConstantStatement.of(assignment.getReferencesCount()),
469            ConstantStatement.of(assignment.isVarargs()),
470            CollectionLiteral.create(CollectionLiteral.Type.array, toSkip))
471          .on(assignment.expression()),
472        true));
473    return block;
474  }
475
476  /**
477   * Add struct factories if they don't already exist.
478   *
479   */
480  @Override
481  public void visitStruct(Struct struct) {
482    module.addFunctions(struct.createFactories());
483  }
484
485  @Override
486  public void visitBinaryOperation(BinaryOperation op) {
487    visitExpression(op);
488  }
489
490  @Override
491  public void visitUnaryOperation(UnaryOperation op) {
492    visitExpression(op);
493  }
494
495  @Override
496  public void visitReferenceLookup(ReferenceLookup ref) {
497    visitExpression(ref);
498  }
499
500  @Override
501  public void visitFunctionInvocation(FunctionInvocation fun) {
502    visitExpression(fun);
503  }
504
505  @Override
506  public void visitMethodInvocation(MethodInvocation invoke) {
507    visitExpression(invoke);
508  }
509
510  private void visitExpression(ExpressionStatement<?> expr) {
511    if (!expressionToBlock(expr)) {
512      expr.walk(this);
513    }
514  }
515
516  /**
517   * Convert an expression with local declarations into a block.
518   */
519  private boolean expressionToBlock(ExpressionStatement<?> expr) {
520    // TODO: make TCO aware expansion? (or wait for a more general optimization pass)
521    if (!expr.hasLocalDeclarations()) {
522      return false;
523    }
524    Block b = Block.block((Object[]) expr.declarations());
525    expr.replaceInParentBy(b);
526    expr.clearDeclarations();
527    LocalReference r = LocalReference.of(symbols.next("localdec")).synthetic();
528    b.add(AssignmentStatement.create(r, expr, true));
529    b.add(r.lookup());
530    b.accept(this);
531    return true;
532  }
533
534  public void visitTryCatchFinally(TryCatchFinally tryCatchFinally) {
535    // We add a dummy variable to hold the value to be returned in the try.
536    // The return statement in the try block will indeed be replaced by a store and jump to execute the finally block.
537    // see bug #568.
538    // This happens only at bytecode level, but we however add the variable to know the store index.
539    if (tryCatchFinally.mustJumpToFinally()) {
540      tryCatchFinally.getLocalReferenceTable().ifPresent(table -> {
541        table.add(LocalReference.of(DUMMY_TRY_RESULT_VARIABLE).variable().synthetic());
542      });
543    }
544    tryCatchFinally.walk(this);
545  }
546}