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}