From ea23cf6fbc458f4cb4d7cecd60e4c479e2c0ddd8 Mon Sep 17 00:00:00 2001 From: Ara Adkins Date: Wed, 6 May 2020 19:00:03 +0100 Subject: [PATCH] Add sections, underscore args and ignores (#716) --- build.sbt | 2 + doc/types/design/types.md | 3 + .../error/RedefinedMethodException.java | 14 +- .../runtime/scope/ModuleScope.java | 20 +- .../scala/org/enso/compiler/Compiler.scala | 24 +- .../codegen/{AstToIR.scala => AstToIr.scala} | 144 +++-- .../org/enso/compiler/codegen/AstView.scala | 47 +- .../enso/compiler/codegen/IRToTruffle.scala | 19 +- .../scala/org/enso/compiler/core/IR.scala | 515 +++++++++++++++++- .../scala/org/enso/compiler/pass/IRPass.scala | 4 +- .../compiler/pass/analyse/AliasAnalysis.scala | 21 +- .../pass/analyse/DataflowAnalysis.scala | 49 +- .../pass/analyse/DemandAnalysis.scala | 24 +- .../pass/analyse/GatherDiagnostics.scala | 8 + .../enso/compiler/pass/analyse/TailCall.scala | 12 + .../pass/desugar/GenerateMethodBodies.scala | 22 +- .../desugar/LambdaShorthandToLambda.scala | 343 ++++++++++++ .../pass/desugar/LiftSpecialOperators.scala | 106 ---- .../pass/desugar/OperatorToFunction.scala | 31 +- .../pass/desugar/SectionsToBinOp.scala | 208 +++++++ .../compiler/pass/lint/UnusedBindings.scala | 155 ++++++ .../ApplicationSaturation.scala | 17 +- .../pass/optimise/LambdaConsolidate.scala | 24 +- .../org/enso/compiler/pass/resolve/.gitkeep | 0 .../pass/resolve/IgnoredBindings.scala | 235 ++++++++ .../pass/resolve/OverloadsResolution.scala | 99 ++++ .../org/enso/compiler/test/CompilerTest.scala | 6 +- .../compiler/test/codegen/AstToIRTest.scala | 35 -- .../compiler/test/codegen/AstToIrTest.scala | 202 +++++++ .../test/pass/analyse/AliasAnalysisTest.scala | 12 +- .../pass/analyse/DataflowAnalysisTest.scala | 4 +- .../pass/analyse/DemandAnalysisTest.scala | 2 - .../test/pass/analyse/TailCallTest.scala | 2 - .../desugar/LambdaShorthandToLambdaTest.scala | 480 ++++++++++++++++ .../desugar/LiftSpecialOperatorsTest.scala | 93 ---- .../pass/desugar/OperatorToFunctionTest.scala | 28 +- .../pass/desugar/SectionsToBinOpTest.scala | 167 ++++++ .../test/pass/lint/UnusedBindingsTest.scala | 136 +++++ .../ApplicationSaturationTest.scala | 11 +- .../pass/optimise/LambdaConsolidateTest.scala | 2 - .../pass/resolve/IgnoredBindingsTest.scala | 134 +++++ .../resolve/OverloadsResolutionTest.scala | 113 ++++ .../semantic/LambdaShorthandArgsTest.scala | 136 +++++ .../test/semantic/MethodsTest.scala | 18 - .../test/semantic/OperatorSectionsTest.scala | 39 ++ .../OverloadsResolutionErrorTest.scala | 56 ++ .../StrictCompileDiagnosticsTest.scala | 8 +- 47 files changed, 3391 insertions(+), 439 deletions(-) rename engine/runtime/src/main/scala/org/enso/compiler/codegen/{AstToIR.scala => AstToIr.scala} (85%) create mode 100644 engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/LambdaShorthandToLambda.scala delete mode 100644 engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/LiftSpecialOperators.scala create mode 100644 engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/SectionsToBinOp.scala create mode 100644 engine/runtime/src/main/scala/org/enso/compiler/pass/lint/UnusedBindings.scala rename engine/runtime/src/main/scala/org/enso/compiler/pass/{analyse => optimise}/ApplicationSaturation.scala (91%) delete mode 100644 engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/.gitkeep create mode 100644 engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/IgnoredBindings.scala create mode 100644 engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/OverloadsResolution.scala delete mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/codegen/AstToIRTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/codegen/AstToIrTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/LambdaShorthandToLambdaTest.scala delete mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/LiftSpecialOperatorsTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/SectionsToBinOpTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/pass/lint/UnusedBindingsTest.scala rename engine/runtime/src/test/scala/org/enso/compiler/test/pass/{analyse => optimise}/ApplicationSaturationTest.scala (96%) create mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/IgnoredBindingsTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/OverloadsResolutionTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/LambdaShorthandArgsTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/OperatorSectionsTest.scala create mode 100644 engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/OverloadsResolutionErrorTest.scala diff --git a/build.sbt b/build.sbt index 49b845f4d4..2d076fd263 100644 --- a/build.sbt +++ b/build.sbt @@ -76,6 +76,8 @@ val jsSettings = Seq( scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) } ) +scalacOptions in (Compile, console) ~= (_ filterNot (_ == "-Xfatal-warnings")) + // ============================================================================ // === Benchmark Configuration ================================================ // ============================================================================ diff --git a/doc/types/design/types.md b/doc/types/design/types.md index 07ac5fbef5..6e85ceb5b3 100644 --- a/doc/types/design/types.md +++ b/doc/types/design/types.md @@ -323,6 +323,9 @@ Two typesets `A` and `B` are defined to be structurally equal if `A <: B` and > - Reformulate this in terms of row polymorphism. We really want to avoid a > _real_ subtyping relationship as it doesn't play at all well with global > inference. +> - To that end, it is an open question as to whether we can have type unions +> without subtyping. Conventionally we wouldn't be able to, but with our +> theory we may. #### Unsafe Typeset Field Mutation For performance it is sometimes necessary to have the ability to _directly_ diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/RedefinedMethodException.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/RedefinedMethodException.java index ab41820a9f..174c46b955 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/RedefinedMethodException.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/RedefinedMethodException.java @@ -1,7 +1,10 @@ package org.enso.interpreter.runtime.error; +import com.oracle.truffle.api.TruffleException; +import com.oracle.truffle.api.nodes.Node; + /** An exception thrown when the program tries to redefine an already-defined method */ -public class RedefinedMethodException extends RuntimeException { +public class RedefinedMethodException extends RuntimeException implements TruffleException { /** * Creates a new error. @@ -12,4 +15,13 @@ public class RedefinedMethodException extends RuntimeException { public RedefinedMethodException(String atom, String method) { super("Methods cannot be overloaded, but you have tried to overload " + atom + "." + method); } + + /** Gets the location where the exception occurred. + * + * @return the location where the exception occurred + */ + @Override + public Node getLocation() { + return null; + } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java index 63654d079c..f8b82d114f 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java @@ -49,6 +49,19 @@ public class ModuleScope { return module; } + /** + * Looks up a constructor in the module scope locally. + * + * @param name the name of the module binding + * @return the atom constructor associated with {@code name}, or {@link Optional#empty()} + */ + public Optional getLocalConstructor(String name) { + if (associatedType.getName().equals(name)) { + return Optional.of(associatedType); + } + return Optional.ofNullable(this.constructors.get(name)); + } + /** * Looks up a constructor in the module scope. * @@ -56,13 +69,10 @@ public class ModuleScope { * @return the Atom constructor associated with {@code name}, or {@link Optional#empty()} */ public Optional getConstructor(String name) { - if (associatedType.getName().equals(name)) { - return Optional.of(associatedType); - } - Optional locallyDefined = Optional.ofNullable(this.constructors.get(name)); + Optional locallyDefined = getLocalConstructor(name); if (locallyDefined.isPresent()) return locallyDefined; return imports.stream() - .map(scope -> scope.getConstructor(name)) + .map(scope -> scope.getLocalConstructor(name)) .filter(Optional::isPresent) .map(Optional::get) .findFirst(); diff --git a/engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala b/engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala index d0cf4a2e67..3883616605 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/Compiler.scala @@ -4,19 +4,17 @@ import java.io.StringReader import com.oracle.truffle.api.TruffleFile import com.oracle.truffle.api.source.Source -import org.enso.compiler.codegen.{AstToIR, IRToTruffle} +import org.enso.compiler.codegen.{AstToIr, IRToTruffle} import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext} import org.enso.compiler.core.IR import org.enso.compiler.core.IR.{Expression, Module} import org.enso.compiler.exception.{CompilationAbortedException, CompilerError} import org.enso.compiler.pass.PassConfiguration._ import org.enso.compiler.pass.analyse._ -import org.enso.compiler.pass.desugar.{ - GenerateMethodBodies, - LiftSpecialOperators, - OperatorToFunction -} -import org.enso.compiler.pass.optimise.LambdaConsolidate +import org.enso.compiler.pass.desugar._ +import org.enso.compiler.pass.lint.UnusedBindings +import org.enso.compiler.pass.optimise._ +import org.enso.compiler.pass.resolve._ import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} import org.enso.interpreter.Language import org.enso.interpreter.node.{ExpressionNode => RuntimeExpression} @@ -51,15 +49,19 @@ class Compiler( */ val compilerPhaseOrdering: List[IRPass] = List( GenerateMethodBodies, - LiftSpecialOperators, + SectionsToBinOp, OperatorToFunction, + LambdaShorthandToLambda, + IgnoredBindings, AliasAnalysis, LambdaConsolidate, + OverloadsResolution, AliasAnalysis, DemandAnalysis, ApplicationSaturation, TailCall, - DataflowAnalysis + DataflowAnalysis, + UnusedBindings ) /** Configuration for the passes. */ @@ -168,7 +170,7 @@ class Compiler( * @return an IR representation of the program represented by `sourceAST` */ def generateIR(sourceAST: AST): Module = - AstToIR.translate(sourceAST) + AstToIr.translate(sourceAST) /** * Lowers the input AST to the compiler's high-level intermediate @@ -178,7 +180,7 @@ class Compiler( * @return an IR representation of the program represented by `sourceAST` */ def generateIRInline(sourceAST: AST): Option[Expression] = - AstToIR.translateInline(sourceAST) + AstToIr.translateInline(sourceAST) /** Runs the various compiler passes. * diff --git a/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIR.scala b/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIr.scala similarity index 85% rename from engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIR.scala rename to engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIr.scala index 0287f1a8dc..f5f5836777 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIR.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstToIr.scala @@ -2,6 +2,7 @@ package org.enso.compiler.codegen import cats.Foldable import cats.implicits._ +import org.enso.compiler.core.IR import org.enso.compiler.core.IR._ import org.enso.compiler.exception.UnhandledEntity import org.enso.interpreter.Constants @@ -15,7 +16,7 @@ import org.enso.syntax.text.AST * [[Core]] becomes implemented. Most function docs will refer to [[Core]] * now, as this will become true soon. */ -object AstToIR { +object AstToIr { private def getIdentifiedLocation(ast: AST): Option[IdentifiedLocation] = ast.location.map(IdentifiedLocation(_, ast.id)) @@ -300,28 +301,43 @@ object AstToIR { ): DefinitionArgument = { arg match { case AstView.LazyAssignedArgumentDefinition(name, value) => - DefinitionArgument.Specified( - Name.Literal(name.name, getIdentifiedLocation(name)), - Some(translateExpression(value)), - suspended = true, - getIdentifiedLocation(arg) - ) + translateIdent(name) match { + case name: IR.Name => + DefinitionArgument.Specified( + name, + Some(translateExpression(value)), + suspended = true, + getIdentifiedLocation(arg) + ) + case _ => + throw new UnhandledEntity(arg, "translateArgumentDefinition") + } case AstView.LazyArgument(arg) => translateArgumentDefinition(arg, isSuspended = true) case AstView.DefinitionArgument(arg) => - DefinitionArgument.Specified( - Name.Literal(arg.name, getIdentifiedLocation(arg)), - None, - isSuspended, - getIdentifiedLocation(arg) - ) + translateIdent(arg) match { + case name: IR.Name => + DefinitionArgument.Specified( + name, + None, + isSuspended, + getIdentifiedLocation(arg) + ) + case _ => + throw new UnhandledEntity(arg, "translateArgumentDefinition") + } case AstView.AssignedArgument(name, value) => - DefinitionArgument.Specified( - Name.Literal(name.name, getIdentifiedLocation(name)), - Some(translateExpression(value)), - isSuspended, - getIdentifiedLocation(arg) - ) + translateIdent(name) match { + case name: IR.Name => + DefinitionArgument.Specified( + name, + Some(translateExpression(value)), + isSuspended, + getIdentifiedLocation(arg) + ) + case _ => + throw new UnhandledEntity(arg, "translateArgumentDefinition") + } case _ => throw new UnhandledEntity(arg, "translateArgumentDefinition") } @@ -406,19 +422,24 @@ object AstToIR { Function.Lambda(realArgs, realBody, getIdentifiedLocation(callable)) } case AST.App.Infix(left, fn, right) => - Application.Operator.Binary( - translateExpression(left), - Name.Literal(fn.name, getIdentifiedLocation(fn)), - translateExpression(right), - getIdentifiedLocation(callable) - ) + val leftArg = translateCallArgument(left) + val rightArg = translateCallArgument(right) + + if (leftArg.name.isDefined) { + IR.Error.Syntax(left, IR.Error.Syntax.NamedArgInOperator) + } else if (rightArg.name.isDefined) { + IR.Error.Syntax(right, IR.Error.Syntax.NamedArgInOperator) + } else { + Application.Operator.Binary( + leftArg, + Name.Literal(fn.name, getIdentifiedLocation(fn)), + rightArg, + getIdentifiedLocation(callable) + ) + } case AST.App.Prefix(_, _) => throw new UnhandledEntity(callable, "translateCallable") - case AST.App.Section.any(_) => - Error.Syntax( - callable, - Error.Syntax.UnsupportedSyntax("operator sections") - ) + case AST.App.Section.any(sec) => translateOperatorSection(sec) case AST.Mixfix(nameSegments, args) => val realNameSegments = nameSegments.collect { case AST.Ident.Var.any(v) => v.name @@ -431,13 +452,55 @@ object AstToIR { Application.Prefix( translateExpression(functionName), args.map(translateCallArgument).toList, - false, + hasDefaultsSuspended = false, getIdentifiedLocation(callable) ) case _ => throw new UnhandledEntity(callable, "translateCallable") } } + /** Translates an operator section from its [[AST]] representation into the + * [[IR]] representation. + * + * @param section the operator section + * @return the [[IR]] representation of `section` + */ + def translateOperatorSection( + section: AST.App.Section + ): Expression = { + section match { + case AST.App.Section.Left.any(left) => + val leftArg = translateCallArgument(left.arg) + + if (leftArg.name.isDefined) { + Error.Syntax(section, Error.Syntax.NamedArgInSection) + } else { + Application.Operator.Section.Left( + leftArg, + Name.Literal(left.opr.name, getIdentifiedLocation(left.opr)), + getIdentifiedLocation(left) + ) + } + case AST.App.Section.Sides.any(sides) => + Application.Operator.Section.Sides( + Name.Literal(sides.opr.name, getIdentifiedLocation(sides.opr)), + getIdentifiedLocation(sides) + ) + case AST.App.Section.Right.any(right) => + val rightArg = translateCallArgument(right.arg) + + if (rightArg.name.isDefined) { + Error.Syntax(section, Error.Syntax.NamedArgInSection) + } else { + Application.Operator.Section.Right( + Name.Literal(right.opr.name, getIdentifiedLocation(right.opr)), + translateCallArgument(right.arg), + getIdentifiedLocation(right) + ) + } + } + } + /** Translates an arbitrary program identifier from its [[AST]] representation * into [[Core]]. * @@ -457,10 +520,7 @@ object AstToIR { case AST.Ident.Cons(name) => Name.Literal(name, getIdentifiedLocation(identifier)) case AST.Ident.Blank(_) => - Error.Syntax( - identifier, - Error.Syntax.UnsupportedSyntax("blanks") - ) + Name.Blank(getIdentifiedLocation(identifier)) case AST.Ident.Opr.any(_) => Error.Syntax( identifier, @@ -489,15 +549,13 @@ object AstToIR { name: AST, expr: AST ): Expression.Binding = { - name match { - case v @ AST.Ident.Var(name) => - Expression.Binding( - Name.Literal(name, getIdentifiedLocation(v)), - translateExpression(expr), - location - ) + val irName = translateExpression(name) + + irName match { + case n: IR.Name => + Expression.Binding(n, translateExpression(expr), location) case _ => - throw new UnhandledEntity(name, "translateAssignment") + throw new UnhandledEntity(name, "translateBinding") } } diff --git a/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstView.scala b/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstView.scala index 3a32f4ebec..6160a27259 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstView.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/codegen/AstView.scala @@ -1,7 +1,9 @@ package org.enso.compiler.codegen import org.enso.data +import org.enso.data.List1 import org.enso.syntax.text.AST +import org.enso.syntax.text.AST.Ident.{Opr, Var} /** This object contains view patterns that allow matching on the parser [[AST]] * for more sophisticated constructs. @@ -43,7 +45,7 @@ object AstView { * @param ast the structure to try and match on * @return the name to which the block is assigned, and the block itself */ - def unapply(ast: AST): Option[(AST.Ident.Var, AST.Block)] = { + def unapply(ast: AST): Option[(AST.Ident, AST.Block)] = { ast match { case Assignment(name, AST.Block.any(block)) => Some((name, block)) @@ -53,7 +55,7 @@ object AstView { } object Binding { - val bindingOpSym = AST.Ident.Opr("=") + val bindingOpSym: Opr = AST.Ident.Opr("=") /** Matches an arbitrary binding in the program source. * @@ -82,7 +84,7 @@ object AstView { } object Assignment { - val assignmentOpSym = AST.Ident.Opr("=") + val assignmentOpSym: Opr = AST.Ident.Opr("=") /** Matches an assignment. * @@ -92,16 +94,34 @@ object AstView { * @param ast the structure to try and match on * @return the variable name assigned to, and the expression being assigned */ - def unapply(ast: AST): Option[(AST.Ident.Var, AST)] = { + def unapply(ast: AST): Option[(AST.Ident, AST)] = { ast match { - case Binding(AST.Ident.Var.any(left), right) => Some((left, right)) - case _ => None + case Binding(MaybeBlankName(left), right) => Some((left, right)) + case _ => None + } + } + } + + object MaybeBlankName { + val blankSym: String = "_" + + /** Matches an identifier that may be a blank `_`. + * + * @param ast the structure to try and match on + * @return the identifier + */ + def unapply(ast: AST): Option[AST.Ident] = { + ast match { + case AST.Ident.Var.any(variable) => Some(variable) + case AST.Ident.Cons.any(cons) => Some(cons) + case AST.Ident.Blank.any(blank) => Some(blank) + case _ => None } } } object Lambda { - val lambdaOpSym = AST.Ident.Opr("->") + val lambdaOpSym: Opr = AST.Ident.Opr("->") /** Matches a lambda expression in the program source. * @@ -255,7 +275,7 @@ object AstView { * @param ast the structure to try and match on * @return the variable name and the expression being bound to it */ - def unapply(ast: AST): Option[(AST.Ident.Var, AST)] = + def unapply(ast: AST): Option[(AST.Ident, AST)] = MaybeParensed.unapply(ast).flatMap(Assignment.unapply) } @@ -268,7 +288,7 @@ object AstView { * @return the name of the argument being declared and the expression of * the default value being bound to it */ - def unapply(ast: AST): Option[(AST.Ident.Var, AST)] = { + def unapply(ast: AST): Option[(AST.Ident, AST)] = { ast match { case MaybeParensed( Binding( @@ -290,9 +310,9 @@ object AstView { * @param ast the structure to try and match on * @return the name of the argument */ - def unapply(ast: AST): Option[AST.Ident.Var] = ast match { - case MaybeParensed(AST.Ident.Var.any(ast)) => Some(ast) - case _ => None + def unapply(ast: AST): Option[AST.Ident] = ast match { + case MaybeParensed(MaybeBlankName(ast)) => Some(ast) + case _ => None } } @@ -521,7 +541,8 @@ object AstView { } object CaseExpression { - val caseName = data.List1(AST.Ident.Var("case"), AST.Ident.Var("of")) + val caseName: List1[Var] = + data.List1(AST.Ident.Var("case"), AST.Ident.Var("of")) /** Matches on a case expression. * diff --git a/engine/runtime/src/main/scala/org/enso/compiler/codegen/IRToTruffle.scala b/engine/runtime/src/main/scala/org/enso/compiler/codegen/IRToTruffle.scala index 894550b348..b6634b79d6 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/codegen/IRToTruffle.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/codegen/IRToTruffle.scala @@ -9,10 +9,10 @@ import org.enso.compiler.pass.analyse.AliasAnalysis.Graph.{Scope => AliasScope} import org.enso.compiler.pass.analyse.AliasAnalysis.{Graph => AliasGraph} import org.enso.compiler.pass.analyse.{ AliasAnalysis, - ApplicationSaturation, DataflowAnalysis, TailCall } +import org.enso.compiler.pass.optimise.ApplicationSaturation import org.enso.interpreter.node.callable.argument.ReadArgumentNode import org.enso.interpreter.node.callable.function.{ BlockNode, @@ -557,6 +557,10 @@ class IRToTruffle( processName( IR.Name.Literal(Constants.Names.THIS_ARGUMENT, location, passData) ) + case _: IR.Name.Blank => + throw new CompilerError( + "Blanks should not be present at codegen time." + ) } setLocation(nameExpr, name.location) @@ -593,6 +597,14 @@ class IRToTruffle( context.getBuiltins .compileError() .newInstance(err.message) + case err: Error.Redefined.Method => + context.getBuiltins + .compileError() + .newInstance(err.message) + case err: Error.Redefined.Atom => + context.getBuiltins + .compileError() + .newInstance(err.message) } setLocation(ErrorNode.build(payload), error.location) } @@ -740,6 +752,11 @@ class IRToTruffle( throw new CompilerError( s"Explicit operators not supported during codegen but $op found" ) + case sec: IR.Application.Operator.Section => + throw new CompilerError( + s"Explicit operator sections not supported during codegen but " + + s"$sec found" + ) } } diff --git a/engine/runtime/src/main/scala/org/enso/compiler/core/IR.scala b/engine/runtime/src/main/scala/org/enso/compiler/core/IR.scala index 960585a6d4..4bf1e183b9 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/core/IR.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/core/IR.scala @@ -171,6 +171,7 @@ object IR { |IR.Empty( |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -251,6 +252,7 @@ object IR { |bindings = $bindings, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -310,6 +312,7 @@ object IR { |name = $name, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -378,6 +381,7 @@ object IR { |arguments = $arguments, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -457,6 +461,7 @@ object IR { |body = $body, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -559,6 +564,7 @@ object IR { |location = $location, |suspended = $suspended, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -567,6 +573,9 @@ object IR { } /** A binding expression of the form `name = expr` + * + * To create a binding that binds no available name, set the name of the + * binding to an [[IR.Name.Blank]] (e.g. _ = foo a b). * * @param name the name being bound to * @param expression the expression being bound to `name` @@ -618,6 +627,7 @@ object IR { |expression = $expression, |location = $location |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -677,6 +687,7 @@ object IR { |value = $value, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -728,6 +739,7 @@ object IR { |text = $text, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -746,6 +758,48 @@ object IR { } object Name { + /** Represents occurrences of blank (`_`) expressions. + * + * @param location the soure location that the node corresponds to. + * @param passData the pass metadata associated with this node + * @param diagnostics compiler diagnostics for this node + */ + sealed case class Blank( + override val location: Option[IdentifiedLocation], + override val passData: MetadataStorage = MetadataStorage(), + override val diagnostics: DiagnosticStorage = DiagnosticStorage() + ) extends Name + with IRKind.Sugar { + override val name: String = "_" + override protected var id: Identifier = randomId + + def copy( + location: Option[IdentifiedLocation] = location, + passData: MetadataStorage = passData, + diagnostics: DiagnosticStorage = diagnostics, + id: Identifier = id + ): Blank = { + val res = Blank(location, passData, diagnostics) + res.id = id + res + } + + override def mapExpressions(fn: Expression => Expression): Blank = + this + + override def toString: String = + s""" + |IR.Expression.Blank( + |location = $location, + |passData = $passData, + |diagnostics = $diagnostics, + |id = $id + |) + |""".stripMargin + + override def children: List[IR] = List() + } + /** The representation of a literal name. * * @param name the literal text of the name @@ -790,6 +844,7 @@ object IR { |name = $name, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -837,6 +892,7 @@ object IR { |IR.Name.This( |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -884,6 +940,7 @@ object IR { s"""IR.Name.Here( |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -952,13 +1009,14 @@ object IR { override def toString: String = s"""IR.Type.Ascription( - |typed = $typed, - |signature = $signature, - |location = $location, - |passData = ${this.showPassData}, - |id = $id - |) - |""".stripMargin + |typed = $typed, + |signature = $signature, + |location = $location, + |passData = ${this.showPassData}, + |diagnostics = $diagnostics, + |id = $id + |) + |""".toSingleLine override def children: List[IR] = List(typed, signature) } @@ -1018,6 +1076,7 @@ object IR { |context = $context, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -1097,6 +1156,7 @@ object IR { |value = $value, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -1162,6 +1222,7 @@ object IR { |right = $right, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |""".toSingleLine @@ -1224,6 +1285,7 @@ object IR { |right = $right, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |""".toSingleLine @@ -1286,6 +1348,7 @@ object IR { |right = $right, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |""".toSingleLine @@ -1348,6 +1411,7 @@ object IR { |right = $right, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |""".toSingleLine @@ -1412,6 +1476,7 @@ object IR { |right = $right, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |""".toSingleLine @@ -1476,6 +1541,7 @@ object IR { |right = $right, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |""".toSingleLine @@ -1576,6 +1642,7 @@ object IR { |location = $location, |canBeTCO = $canBeTCO, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -1589,8 +1656,16 @@ object IR { /** Definition-site arguments in Enso. */ sealed trait DefinitionArgument extends IR { + + /** The name of the argument. */ + val name: IR.Name + + /** The default value of the argument. */ val defaultValue: Option[Expression] + /** Whether or not the argument is suspended. */ + val suspended: Boolean + override def mapExpressions( fn: Expression => Expression ): DefinitionArgument @@ -1600,6 +1675,9 @@ object IR { /** The representation of an argument from a [[Function]] or * [[IR.Module.Scope.Definition.Atom]] definition site. * + * To create an ignored argument, the argument name should be an + * [[IR.Name.Blank]]. + * * @param name the name of the argument * @param defaultValue the default value of the argument, if present * @param suspended whether or not the argument has its execution suspended @@ -1608,9 +1686,9 @@ object IR { * @param diagnostics compiler diagnostics for this node */ sealed case class Specified( - name: IR.Name, + override val name: IR.Name, override val defaultValue: Option[Expression], - suspended: Boolean, + override val suspended: Boolean, override val location: Option[IdentifiedLocation], override val passData: MetadataStorage = MetadataStorage(), override val diagnostics: DiagnosticStorage = DiagnosticStorage() @@ -1665,15 +1743,13 @@ object IR { |suspended = $suspended, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine override def children: List[IR] = name :: defaultValue.toList - } - - // TODO [AA] Add support for `_` ignored arguments. } // === Applications ========================================================= @@ -1751,6 +1827,7 @@ object IR { |hasDefaultsSuspended = $hasDefaultsSuspended, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -1806,6 +1883,7 @@ object IR { |target = $target, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -1830,9 +1908,9 @@ object IR { * @param diagnostics compiler diagnostics for this node */ sealed case class Binary( - left: Expression, + left: CallArgument, operator: IR.Name, - right: Expression, + right: CallArgument, override val location: Option[IdentifiedLocation], override val passData: MetadataStorage = MetadataStorage(), override val diagnostics: DiagnosticStorage = DiagnosticStorage() @@ -1852,9 +1930,9 @@ object IR { * @return a copy of `this`, updated with the specified values */ def copy( - left: Expression = left, + left: CallArgument = left, operator: IR.Name = operator, - right: Expression = right, + right: CallArgument = right, location: Option[IdentifiedLocation] = location, passData: MetadataStorage = passData, diagnostics: DiagnosticStorage = diagnostics, @@ -1867,7 +1945,7 @@ object IR { } override def mapExpressions(fn: Expression => Expression): Binary = { - copy(left = fn(left), right = fn(right)) + copy(left = left.mapExpressions(fn), right = right.mapExpressions(fn)) } override def toString: String = @@ -1878,6 +1956,7 @@ object IR { |right = $right, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -1885,9 +1964,193 @@ object IR { override def children: List[IR] = List(left, operator, right) } - } - // TODO [AA] Add support for left, right, and centre sections + /** Operator sections. */ + sealed trait Section extends Operator { + override def mapExpressions(fn: Expression => Expression): Section + } + object Section { + + /** Represents a left operator section of the form `(arg op)`. + * + * @param arg the argument (on the left of the operator) + * @param operator the operator + * @param location the source location that the node corresponds to + * @param passData the pass metadata associated with this node + * @param diagnostics compiler diagnostics for this node + */ + sealed case class Left( + arg: CallArgument, + operator: IR.Name, + override val location: Option[IdentifiedLocation], + override val passData: MetadataStorage = MetadataStorage(), + override val diagnostics: DiagnosticStorage = DiagnosticStorage() + ) extends Section + with IRKind.Sugar { + override protected var id: Identifier = randomId + + /** Creates a copy of `this`. + * + * @param arg the argument (on the left of the operator) + * @param operator the operator + * @param location the source location that the node corresponds to + * @param passData the pass metadata associated with this node + * @param diagnostics compiler diagnostics for this node + * @param id the identifier for the new node + * @return a copy of `this`, updated with the specified values + */ + def copy( + arg: CallArgument = arg, + operator: IR.Name = operator, + location: Option[IdentifiedLocation] = location, + passData: MetadataStorage = passData, + diagnostics: DiagnosticStorage = diagnostics, + id: IR.Identifier = id + ): Left = { + val res = Left(arg, operator, location, passData, diagnostics) + res.id = id + res + } + + override def mapExpressions(fn: Expression => Expression): Section = + copy( + arg = arg.mapExpressions(fn), + operator = operator.mapExpressions(fn) + ) + + override def toString: String = + s""" + |IR.Application.Operator.Section.Left( + |arg = $arg, + |operator = $operator, + |location = $location, + |passData = $passData, + |diagnostics = $diagnostics, + |id = $id + |) + |""".toSingleLine + + override def children: List[IR] = List(arg, operator) + } + + /** Represents a sides operator section of the form `(op)` + * + * @param operator the operator + * @param location the source location that the node corresponds to + * @param passData the pass metadata associated with this node + * @param diagnostics compiler diagnostics for this node + */ + sealed case class Sides( + operator: IR.Name, + override val location: Option[IdentifiedLocation], + override val passData: MetadataStorage = MetadataStorage(), + override val diagnostics: DiagnosticStorage = DiagnosticStorage() + ) extends Section + with IRKind.Sugar { + override protected var id: Identifier = randomId + + /** Creates a copy of `this`. + * + * @param operator the operator + * @param location the source location that the node corresponds to + * @param passData the pass metadata associated with this node + * @param diagnostics compiler diagnostics for this node + * @param id the identifier for the new node + * @return a copy of `this`, updated with the specified values + */ + def copy( + operator: IR.Name = operator, + location: Option[IdentifiedLocation] = location, + passData: MetadataStorage = passData, + diagnostics: DiagnosticStorage = diagnostics, + id: Identifier = id + ): Sides = { + val res = Sides(operator, location, passData, diagnostics) + res.id = id + res + } + + override def mapExpressions(fn: Expression => Expression): Section = + copy(operator = operator.mapExpressions(fn)) + + override def toString: String = + s""" + |IR.Application.Operator.Section.Centre( + |operator = $operator, + |location = $location, + |passData = $passData, + |diagnostics = $diagnostics, + |id = $id + |) + |""".toSingleLine + + override def children: List[IR] = List(operator) + } + + /** Represents a right operator section of the form `(op arg)` + * + * @param operator the operator + * @param arg the argument (on the right of the operator) + * @param location the source location that the node corresponds to + * @param passData the pass metadata associated with this node + * @param diagnostics compiler diagnostics for this node + */ + sealed case class Right( + operator: IR.Name, + arg: CallArgument, + override val location: Option[IdentifiedLocation], + override val passData: MetadataStorage = MetadataStorage(), + override val diagnostics: DiagnosticStorage = DiagnosticStorage() + ) extends Section + with IRKind.Sugar { + override protected var id: Identifier = randomId + + /** Creates a copy of `this`. + * + * @param operator the operator + * @param arg the argument (on the right of the operator) + * @param location the source location that the node corresponds to + * @param passData the pass metadata associated with this node + * @param diagnostics compiler diagnostics for this node + * @param id the identifier for the new node + * @return a copy of `this`, updated with the specified values + */ + def copy( + operator: IR.Name = operator, + arg: CallArgument = arg, + location: Option[IdentifiedLocation] = location, + passData: MetadataStorage = passData, + diagnostics: DiagnosticStorage = diagnostics, + id: Identifier = id + ): Right = { + val res = Right(operator, arg, location, passData, diagnostics) + res.id = id + res + } + + override def mapExpressions(fn: Expression => Expression): Section = { + copy( + operator = operator.mapExpressions(fn), + arg = arg.mapExpressions(fn) + ) + } + + override def toString: String = + s""" + |IR.Application.Operator.Section.Right( + |operator = $operator, + |arg = $arg, + |location = $location, + |passData = $passData, + |diagnostics = $diagnostics, + |id = $id + |) + |""".toSingleLine + + override def children: List[IR] = List(operator, arg) + } + } + } } // === Call-Site Arguments ================================================== @@ -1898,6 +2161,9 @@ object IR { /** The name of the argument, if present. */ val name: Option[IR.Name] + /** The expression of the argument, if present. */ + val value: Expression + /** Whether or not the argument should be suspended at code generation time. * * A value of `Some(true)` implies that code generation should suspend the @@ -1912,6 +2178,9 @@ object IR { object CallArgument { /** A representation of an argument at a function call site. + * + * A [[CallArgument]] where the `value` is an [[IR.Name.Blank]] is a + * representation of a lambda shorthand argument. * * @param name the name of the argument being called, if present * @param value the expression being passed as the argument's value @@ -1923,7 +2192,7 @@ object IR { */ sealed case class Specified( override val name: Option[IR.Name], - value: Expression, + override val value: Expression, override val location: Option[IdentifiedLocation], override val shouldBeSuspended: Option[Boolean] = None, override val passData: MetadataStorage = MetadataStorage(), @@ -1977,16 +2246,13 @@ object IR { |location = $location, |shouldBeSuspended = $shouldBeSuspended, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine override def children: List[IR] = name.toList :+ value - } - - // TODO [AA] Add support for the `_` lambda shorthand argument (can be - // called by name) } // === Case Expression ====================================================== @@ -2059,6 +2325,7 @@ object IR { |fallback = $fallback, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -2120,6 +2387,7 @@ object IR { |expression = $expression, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -2202,6 +2470,7 @@ object IR { |doc = $doc, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -2270,6 +2539,7 @@ object IR { |code = $code, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -2316,6 +2586,38 @@ object IR { sealed trait Warning extends Diagnostic object Warning { + /** Warnings about unused language entities. */ + sealed trait Unused extends Warning { + val name: IR.Name + } + object Unused { + + /** A warning about an unused function argument. + * + * @param name the name that is unused + */ + sealed case class FunctionArgument(override val name: Name) + extends Unused { + override def message: String = s"Unused function argument ${name.name}." + + override def toString: String = s"Unused.FunctionArgument(${name.name})" + + override val location: Option[IdentifiedLocation] = name.location + } + + /** A warning about an unused binding. + * + * @param name the name that is unused + */ + sealed case class Binding(override val name: Name) extends Unused { + override def message: String = s"Unused variable ${name.name}." + + override def toString: String = s"Unused.Binding(${name.name})" + + override val location: Option[IdentifiedLocation] = name.location + } + } + /** Warnings about shadowing names. */ sealed trait Shadowed extends Warning { @@ -2337,7 +2639,7 @@ object IR { ) extends Shadowed { override def message: String = - s"The function parameter $shadowedName is being shadowed by $shadower" + s"The argument $shadowedName is shadowed by $shadower." } } } @@ -2399,6 +2701,7 @@ object IR { |ast = $ast, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -2446,6 +2749,13 @@ object IR { override def explanation: String = "Unclosed text literal." } + case object NamedArgInSection extends Reason { + override def explanation: String = "Named argument in operator section." + } + + case object NamedArgInOperator extends Reason { + override def explanation: String = "Named argument in operator section." + } } /** A representation of an invalid piece of IR. @@ -2493,6 +2803,7 @@ object IR { |ir = $ir, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".toSingleLine @@ -2510,6 +2821,138 @@ object IR { sealed trait Redefined extends Error object Redefined { + /** An error representing the redefinition of a method in a given module. + * This is also known as a method overload. + * + * @param atomName the name of the atom the method was being redefined on + * @param methodName the method name being redefined on `atomName` + * @param location the location in the source to which this error + * corresponds + * @param passData the pass metadata for the error + * @param diagnostics any diagnostics associated with this error. + */ + sealed case class Method( + atomName: IR.Name, + methodName: IR.Name, + override val location: Option[IdentifiedLocation], + override val passData: MetadataStorage = MetadataStorage(), + override val diagnostics: DiagnosticStorage = DiagnosticStorage() + ) extends Redefined + with Diagnostic.Kind.Interactive + with Module.Scope.Definition + with IRKind.Primitive { + override protected var id: Identifier = randomId + + /** Creates a copy of `this`. + * + * @param atomName the name of the atom the method was being redefined on + * @param methodName the method name being redefined on `atomName` + * @param location the location in the source to which this error + * corresponds + * @param passData the pass metadata for the error + * @param diagnostics any diagnostics associated with this error. + * @param id the identifier for the node + * @return a copy of `this`, updated with the specified values + */ + def copy( + atomName: IR.Name = atomName, + methodName: IR.Name = methodName, + location: Option[IdentifiedLocation] = location, + passData: MetadataStorage = passData, + diagnostics: DiagnosticStorage = diagnostics, + id: Identifier = id + ): Method = { + val res = + Method(atomName, methodName, location, passData, diagnostics) + res.id = id + res + } + + override def message: String = + s"Method overloads are not supported: ${atomName.name}." + + s"${methodName.name} is defined multiple times in this module." + + override def mapExpressions(fn: Expression => Expression): Method = this + + override def toString: String = + s""" + |IR.Error.Redefined.Method( + |atomName = $atomName, + |methodName = $methodName, + |location = $location, + |passData = ${this.showPassData}, + |diagnostics = $diagnostics, + |id = $id + |) + |""".stripMargin + + override def children: List[IR] = List(atomName, methodName) + } + + /** An error representing the redefinition of an atom in a given module. + * + * @param atomName the name of the atom being redefined + * @param location the location in the source to which this error + * corresponds + * @param passData the pass metadata for the error + * @param diagnostics any diagnostics associated with this error. + */ + sealed case class Atom( + atomName: IR.Name, + override val location: Option[IdentifiedLocation], + override val passData: MetadataStorage = MetadataStorage(), + override val diagnostics: DiagnosticStorage = DiagnosticStorage() + ) extends Redefined + with Diagnostic.Kind.Interactive + with Module.Scope.Definition + with IRKind.Primitive { + override protected var id: Identifier = randomId + + /** Creates a copy of `this`. + * + * @param atomName the name of the atom the method was being redefined + * on + * @param location the location in the source to which this error + * corresponds + * @param passData the pass metadata for the error + * @param diagnostics any diagnostics associated with this error. + * @param id the identifier for the node + * @return a copy of `this`, updated with the specified values + */ + def copy( + atomName: IR.Name = atomName, + location: Option[IdentifiedLocation] = location, + passData: MetadataStorage = passData, + diagnostics: DiagnosticStorage = diagnostics, + id: Identifier = id + ): Atom = { + val res = + Atom(atomName, location, passData, diagnostics) + res.id = id + res + } + + override def message: String = + s"Redefining atoms is not supported: ${atomName.name} is " + + s"defined multiple times in this module." + + + override def mapExpressions(fn: Expression => Expression): Atom = this + + override def toString: String = + s""" + |IR.Error.Redefined.Atom( + |atomName = $atomName, + |location = $location, + |passData = ${this.showPassData}, + |diagnostics = $diagnostics, + |id = $id + |) + |""".stripMargin + + override def children: List[IR] = List(atomName) + } + /** An error representing the redefinition of a binding in a given scope. * * While bindings in child scopes are allowed to _shadow_ bindings in @@ -2524,7 +2967,7 @@ object IR { override val passData: MetadataStorage = MetadataStorage(), override val diagnostics: DiagnosticStorage = DiagnosticStorage() ) extends Redefined - with Diagnostic.Kind.Static + with Diagnostic.Kind.Interactive with IRKind.Primitive { override protected var id: Identifier = randomId @@ -2559,6 +3002,7 @@ object IR { |invalidBinding = $invalidBinding, |location = $location, |passData = ${this.showPassData}, + |diagnostics = $diagnostics, |id = $id |) |""".stripMargin @@ -2772,6 +3216,25 @@ object IR { // === Useful Extension Methods ============================================= // ========================================================================== + /** Adds extension methods for working directly with the diagnostics on the + * IR. + * + * @param ir the IR to add the methods to + * @tparam T the concrete type of the IR + */ + implicit class AsDiagnostics[T <: IR](ir: T) { + + /** Adds a new diagnostic entity to [[IR]]. + * + * @param diagnostic the diagnostic to add + * @return [[ir]] with added diagnostics + */ + def addDiagnostic(diagnostic: IR.Diagnostic): T = { + ir.diagnostics.add(diagnostic) + ir + } + } + /** Adds extension methods for working directly with the metadata on the IR. * * @param ir the IR to add the methods to diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/IRPass.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/IRPass.scala index 5e1838cfae..5bf928bef2 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/IRPass.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/IRPass.scala @@ -15,7 +15,9 @@ import scala.reflect.ClassTag * Passes that depend on the metadata of other passes should pull this metadata * directly from the IR, and not depend on metadata available in the context. * - * Every pass should be implemented as a `case object`. + * Every pass should be implemented as a `case object` and should document in + * its header the requirements it has for pass configuration and for passes + * that must run before it. */ trait IRPass { diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/AliasAnalysis.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/AliasAnalysis.scala index 642f2177ee..a6a8a2be84 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/AliasAnalysis.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/AliasAnalysis.scala @@ -35,6 +35,21 @@ import scala.reflect.ClassTag * the lambda. * - A method whose body is a lambda containing a block as its body allocates * no additional scope for the lambda or the block. + * + * Alias analysis requires its configuration to be in the configuration object. + * + * This pass requires the context to provide: + * + * - A [[org.enso.compiler.pass.PassConfiguration]] containing an instance of + * [[AliasAnalysis.Configuration]]. + * - A [[org.enso.interpreter.runtime.scope.LocalScope]], where relevant. + * + * It must have the following passes run before it: + * + * - [[org.enso.compiler.pass.desugar.GenerateMethodBodies]] + * - [[org.enso.compiler.pass.desugar.SectionsToBinOp]] + * - [[org.enso.compiler.pass.desugar.OperatorToFunction]] + * - [[org.enso.compiler.pass.desugar.LambdaShorthandToLambda]], */ case object AliasAnalysis extends IRPass { @@ -138,6 +153,7 @@ case object AliasAnalysis extends IRPass { analyseArgumentDefs(args, topLevelGraph, topLevelGraph.rootScope) ) .updateMetadata(this -->> Info.Scope.Root(topLevelGraph)) + case err: IR.Error.Redefined => err } } @@ -329,7 +345,10 @@ case object AliasAnalysis extends IRPass { throw new CompilerError( "Binary operator occurred during Alias Analysis." ) - + case _: IR.Application.Operator.Section => + throw new CompilerError( + "Operator section occurred during Alias Analysis." + ) } } diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/DataflowAnalysis.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/DataflowAnalysis.scala index 9d57dbe056..b725fabe17 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/DataflowAnalysis.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/DataflowAnalysis.scala @@ -13,10 +13,18 @@ import scala.collection.mutable * Dataflow analysis is the processes of determining the dependencies between * program expressions. * - * This pass needs to be run after [[AliasAnalysis]], [[DemandAnalysis]], and - * [[TailCall]]. It also assumes that all members of [[IR.IRKind.Primitive]] - * have been removed from the IR by the time it runs. This means that it _must_ - * run after all desugaring passes. + * This pass requires the context to provide: + * + * - A [[org.enso.interpreter.runtime.scope.LocalScope]], where relevant. + * + * It must have the following passes run before it: + * + * - [[AliasAnalysis]] + * - [[DemandAnalysis]] + * - [[TailCall]] + * + * It also requires that all members of [[IR.IRKind.Primitive]] have been + * removed from the IR by the time it runs. */ //noinspection DuplicatedCode case object DataflowAnalysis extends IRPass { @@ -92,6 +100,7 @@ case object DataflowAnalysis extends IRPass { body = analyseExpression(body, info) ) .updateMetadata(this -->> info) + case err: IR.Error.Redefined => err } } @@ -328,21 +337,29 @@ case object DataflowAnalysis extends IRPass { "Name occurrence with missing aliasing information." ) .unsafeAs[AliasAnalysis.Info.Occurrence] - val defIdForName = aliasInfo.graph.defLinkFor(aliasInfo.id) - val key = defIdForName match { - case Some(defLink) => - aliasInfo.graph.getOccurrence(defLink.target) match { - case Some(AliasAnalysis.Graph.Occurrence.Def(_, _, id, _)) => - DependencyInfo.Type.Static(id) - case _ => DependencyInfo.Type.Dynamic(name.name) + + name match { + case _: IR.Name.Blank => + throw new CompilerError( + "Blanks should not be present during dataflow analysis." + ) + case _ => + val defIdForName = aliasInfo.graph.defLinkFor(aliasInfo.id) + val key = defIdForName match { + case Some(defLink) => + aliasInfo.graph.getOccurrence(defLink.target) match { + case Some(AliasAnalysis.Graph.Occurrence.Def(_, _, id, _)) => + DependencyInfo.Type.Static(id) + case _ => DependencyInfo.Type.Dynamic(name.name) + } + + case None => DependencyInfo.Type.Dynamic(name.name) } - case None => DependencyInfo.Type.Dynamic(name.name) + info.updateAt(key, Set(name.getId)) + + name.updateMetadata(this -->> info) } - - info.updateAt(key, Set(name.getId)) - - name.updateMetadata(this -->> info) } /** Performs dependency analysis on a case expression. diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/DemandAnalysis.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/DemandAnalysis.scala index 2df686434e..1e38f881e9 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/DemandAnalysis.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/DemandAnalysis.scala @@ -10,9 +10,23 @@ import org.enso.compiler.pass.IRPass * Demand analysis is the process of determining _when_ a suspended term needs * to be forced (where the suspended value is _demanded_). * - * This pass needs to be run after [[AliasAnalysis]], and also assumes that - * all members of [[IR.IRKind.Primitive]] have been removed from the IR by the - * time that it runs. + * This pass requires the context to provide: + * + * - Nothing + * + * It must have the following passes run before it: + * + * - [[org.enso.compiler.pass.desugar.GenerateMethodBodies]] + * - [[org.enso.compiler.pass.desugar.SectionsToBinOp]] + * - [[org.enso.compiler.pass.desugar.OperatorToFunction]] + * - [[org.enso.compiler.pass.desugar.LambdaShorthandToLambda]] + * - [[org.enso.compiler.pass.resolve.IgnoredBindings]] + * - [[AliasAnalysis]] + * - [[org.enso.compiler.pass.optimise.LambdaConsolidate]] + * - [[org.enso.compiler.pass.resolve.OverloadsResolution]] + * + * Additionally, all members of [[IR.IRKind.Primitive]] must have been removed + * from the IR by the time it runs. */ case object DemandAnalysis extends IRPass { override type Metadata = IRPass.Metadata.Empty @@ -158,6 +172,10 @@ case object DemandAnalysis extends IRPass { case lit: IR.Name.Literal => lit.copy(location = newNameLocation) case ths: IR.Name.This => ths.copy(location = newNameLocation) case here: IR.Name.Here => here.copy(location = newNameLocation) + case _: IR.Name.Blank => + throw new CompilerError( + "Blanks should not be present by the time demand analysis runs." + ) } IR.Application.Force(newName, forceLocation) diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/GatherDiagnostics.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/GatherDiagnostics.scala index b3dd4d35dd..b6e97b4a32 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/GatherDiagnostics.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/GatherDiagnostics.scala @@ -7,6 +7,14 @@ import org.enso.compiler.pass.IRPass /** A pass that traverses the given root IR and accumulates all the encountered * diagnostic nodes in the root. + * + * This pass requires the context to provide: + * + * - Nothing + * + * It must have the following passes run before it: + * + * - None */ case object GatherDiagnostics extends IRPass { diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/TailCall.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/TailCall.scala index 552b43de9c..a70d96a904 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/TailCall.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/TailCall.scala @@ -11,6 +11,17 @@ import org.enso.compiler.pass.IRPass * It is responsible for marking every single expression with whether it is in * tail position or not. This allows the code generator to correctly create the * Truffle nodes. + * + * This pass requires the context to provide: + * + * - The tail position of its expression, where relevant. + * + * It must have the following passes run before it: + * + * - [[org.enso.compiler.pass.desugar.GenerateMethodBodies]] + * - [[org.enso.compiler.pass.desugar.SectionsToBinOp]] + * - [[org.enso.compiler.pass.desugar.OperatorToFunction]] + * - [[org.enso.compiler.pass.desugar.LambdaShorthandToLambda]] */ case object TailCall extends IRPass { @@ -77,6 +88,7 @@ case object TailCall extends IRPass { arguments = args.map(analyseDefArgument) ) .updateMetadata(this -->> TailPosition.Tail) + case err: IR.Error.Redefined => err } } diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/GenerateMethodBodies.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/GenerateMethodBodies.scala index 2bb920cbba..c230018ffd 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/GenerateMethodBodies.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/GenerateMethodBodies.scala @@ -12,6 +12,14 @@ import org.enso.compiler.pass.IRPass * * - The body is a function (lambda) * - The body has `this` at the start of its argument list. + * + * This pass requires the context to provide: + * + * - Nothing + * + * It must have the following passes run before it: + * + * - None */ case object GenerateMethodBodies extends IRPass { @@ -21,13 +29,13 @@ case object GenerateMethodBodies extends IRPass { override type Config = IRPass.Configuration.Default /** Generates and consolidates method bodies. - * - * @param ir the Enso IR to process - * @param moduleContext a context object that contains the information needed - * to process a module - * @return `ir`, possibly having made transformations or annotations to that - * IR. - */ + * + * @param ir the Enso IR to process + * @param moduleContext a context object that contains the information needed + * to process a module + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ override def runModule( ir: IR.Module, moduleContext: ModuleContext diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/LambdaShorthandToLambda.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/LambdaShorthandToLambda.scala new file mode 100644 index 0000000000..ce31fcb0ac --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/LambdaShorthandToLambda.scala @@ -0,0 +1,343 @@ +package org.enso.compiler.pass.desugar + +import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext} +import org.enso.compiler.core.IR +import org.enso.compiler.exception.CompilerError +import org.enso.compiler.pass.IRPass + +/** This pass translates `_` arguments at application sites to lambda functions. + * + * This pass has no configuration. + * + * This pass requires the context to provide: + * + * - A [[FreshNameSupply]] + * + * It must have the following passes run before it: + * + * - [[GenerateMethodBodies]] + * - [[SectionsToBinOp]] + * - [[OperatorToFunction]] + */ +case object LambdaShorthandToLambda extends IRPass { + override type Metadata = IRPass.Metadata.Empty + override type Config = IRPass.Configuration.Default + + /** Desugars underscore arguments to lambdas for a module. + * + * @param ir the Enso IR to process + * @param moduleContext a context object that contains the information needed + * to process a module + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runModule( + ir: IR.Module, + moduleContext: ModuleContext + ): IR.Module = ir.transformExpressions { + case x => + x.mapExpressions( + runExpression( + _, + InlineContext(freshNameSupply = moduleContext.freshNameSupply) + ) + ) + } + + /** Desugars underscore arguments to lambdas for an arbitrary expression. + * + * @param ir the Enso IR to process + * @param inlineContext a context object that contains the information needed + * for inline evaluation + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runExpression( + ir: IR.Expression, + inlineContext: InlineContext + ): IR.Expression = { + val freshNameSupply = inlineContext.freshNameSupply.getOrElse( + throw new CompilerError( + "Desugaring underscore arguments to lambdas requires a fresh name " + + "supply." + ) + ) + + desugarExpression(ir, freshNameSupply) + } + + // === Pass Internals ======================================================= + + /** Performs lambda shorthand desugaring on an arbitrary expression. + * + * @param ir the expression to desugar + * @param freshNameSupply the compiler's fresh name supply + * @return `ir`, with any lambda shorthand arguments desugared + */ + def desugarExpression( + ir: IR.Expression, + freshNameSupply: FreshNameSupply + ): IR.Expression = { + ir.transformExpressions { + case app: IR.Application => desugarApplication(app, freshNameSupply) + case caseExpr: IR.Case.Expr => desugarCaseExpr(caseExpr, freshNameSupply) + case name: IR.Name => desugarName(name, freshNameSupply) + } + } + + /** Desugars an arbitrary name occurrence, turning isolated occurrences of + * `_` into the `id` function. + * + * @param name the name to desugar + * @param supply the compiler's fresh name supply + * @return `name`, desugared where necessary + */ + def desugarName(name: IR.Name, supply: FreshNameSupply): IR.Expression = { + name match { + case blank: IR.Name.Blank => + val newName = supply.newName() + + IR.Function.Lambda( + List( + IR.DefinitionArgument.Specified( + IR.Name.Literal(newName.name, None), + None, + suspended = false, + None + ) + ), + newName, + blank.location + ) + case _ => name + } + } + + /** Desugars lambda shorthand arguments to an arbitrary function application. + * + * @param application the function application to desugar + * @param freshNameSupply the compiler's supply of fresh names + * @return `application`, with any lambda shorthand arguments desugared + */ + def desugarApplication( + application: IR.Application, + freshNameSupply: FreshNameSupply + ): IR.Expression = { + application match { + case p @ IR.Application.Prefix(fn, args, _, _, _, _) => + // Determine which arguments are lambda shorthand + val argIsUnderscore = determineLambdaShorthand(args) + + // Generate a new name for the arg value for each shorthand arg + val updatedArgs = + args + .zip(argIsUnderscore) + .map(updateShorthandArg(_, freshNameSupply)) + .map { + case s @ IR.CallArgument.Specified(_, value, _, _, _, _) => + s.copy(value = desugarExpression(value, freshNameSupply)) + } + + // Generate a definition arg instance for each shorthand arg + val defArgs = updatedArgs.zip(argIsUnderscore).map { + case (arg, isShorthand) => generateDefinitionArg(arg, isShorthand) + } + val actualDefArgs = defArgs.collect { + case Some(defArg) => defArg + } + + // Determine whether or not the function itself is shorthand + val functionIsShorthand = fn.isInstanceOf[IR.Name.Blank] + val (updatedFn, updatedName) = if (functionIsShorthand) { + val newFn = freshNameSupply + .newName() + .copy( + location = fn.location, + passData = fn.passData, + diagnostics = fn.diagnostics + ) + val newName = newFn.name + (newFn, Some(newName)) + } else (fn, None) + + val processedApp = p.copy( + function = updatedFn, + arguments = updatedArgs + ) + + // Wrap the app in lambdas from right to left, lambda / shorthand arg + val appResult = + actualDefArgs.foldRight(processedApp: IR.Expression)((arg, body) => + IR.Function.Lambda(List(arg), body, None) + ) + + // If the function is shorthand, do the same + if (functionIsShorthand) { + IR.Function.Lambda( + List( + IR.DefinitionArgument.Specified( + IR.Name.Literal(updatedName.get, fn.location), + None, + suspended = false, + None + ) + ), + appResult, + None + ) + } else appResult + case f @ IR.Application.Force(tgt, _, _, _) => + f.copy(target = desugarExpression(tgt, freshNameSupply)) + case _: IR.Application.Operator => + throw new CompilerError( + "Operators should be desugared by the point of underscore " + + "to lambda conversion." + ) + } + } + + /** Determines, positionally, which of the application arguments are lambda + * shorthand arguments. + * + * @param args the application arguments + * @return a list containing `true` for a given position if the arg in that + * position is lambda shorthand, otherwise `false` + */ + def determineLambdaShorthand(args: List[IR.CallArgument]): List[Boolean] = { + args.map { + case IR.CallArgument.Specified(_, value, _, _, _, _) => + value match { + case _: IR.Name.Blank => true + case _ => false + } + } + } + + /** Generates a new name to replace a shorthand argument, as well as the + * corresponding definition argument. + * + * @param argAndIsShorthand the arguments, and whether or not the argument in + * the corresponding position is shorthand + * @return the above described pair for a given position if the argument in + * a given position is shorthand, otherwise [[None]]. + */ + def updateShorthandArg( + argAndIsShorthand: (IR.CallArgument, Boolean), + freshNameSupply: FreshNameSupply + ): IR.CallArgument = { + val arg = argAndIsShorthand._1 + val isShorthand = argAndIsShorthand._2 + + arg match { + case s @ IR.CallArgument.Specified(_, value, _, _, _, _) => + if (isShorthand) { + val newName = freshNameSupply + .newName() + .copy( + location = value.location, + passData = value.passData, + diagnostics = value.diagnostics + ) + + s.copy(value = newName) + } else s + } + } + + /** Generates a corresponding definition argument to a call argument that was + * previously lambda shorthand. + * + * @param arg the argument to generate a corresponding def argument to + * @param isShorthand whether or not `arg` was shorthand + * @return a corresponding definition argument if `arg` `isShorthand`, + * otherwise [[None]] + */ + def generateDefinitionArg( + arg: IR.CallArgument, + isShorthand: Boolean + ): Option[IR.DefinitionArgument] = { + if (isShorthand) { + arg match { + case IR.CallArgument.Specified(_, value, _, _, _, _) => + // Note [Safe Casting to IR.Name.Literal] + val defArgName = + IR.Name.Literal(value.asInstanceOf[IR.Name.Literal].name, None) + + Some( + IR.DefinitionArgument.Specified( + defArgName, + None, + suspended = false, + None + ) + ) + } + } else None + } + + /* Note [Safe Casting to IR.Name.Literal] + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * This cast is entirely safe here as, by construction in + * `updateShorthandArg`, any arg for which `isShorthand` is true has its + * value as an `IR.Name.Literal`. + */ + + /** Performs desugaring of lambda shorthand arguments in a case expression. + * + * In the case where a user writes `case _ of`, this gets converted into a + * lambda (`x -> case x of`). + * + * @param caseExpr the case expression to desugar + * @param freshNameSupply the compiler's supply of fresh names + * @return `caseExpr`, with any lambda shorthand desugared + */ + def desugarCaseExpr( + caseExpr: IR.Case.Expr, + freshNameSupply: FreshNameSupply + ): IR.Expression = { + val newBranches = caseExpr.branches.map( + _.mapExpressions(expr => desugarExpression(expr, freshNameSupply)) + ) + val newFallback = + caseExpr.fallback.map(desugarExpression(_, freshNameSupply)) + + caseExpr.scrutinee match { + case IR.Name.Blank(loc, passData, diagnostics) => + val scrutineeName = + freshNameSupply + .newName() + .copy( + location = loc, + passData = passData, + diagnostics = diagnostics + ) + + val lambdaArg = IR.DefinitionArgument.Specified( + scrutineeName.copy(id = IR.randomId), + None, + suspended = false, + None + ) + + val newCaseExpr = caseExpr.copy( + scrutinee = scrutineeName, + branches = newBranches, + fallback = newFallback + ) + + IR.Function.Lambda( + List(lambdaArg), + newCaseExpr, + caseExpr.location, + passData = caseExpr.passData, + diagnostics = caseExpr.diagnostics + ) + case x => + caseExpr.copy( + scrutinee = desugarExpression(x, freshNameSupply), + branches = newBranches, + fallback = newFallback + ) + } + } +} diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/LiftSpecialOperators.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/LiftSpecialOperators.scala deleted file mode 100644 index 2f28cdc949..0000000000 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/LiftSpecialOperators.scala +++ /dev/null @@ -1,106 +0,0 @@ -package org.enso.compiler.pass.desugar - -import org.enso.compiler.context.{InlineContext, ModuleContext} -import org.enso.compiler.core.IR -import org.enso.compiler.pass.IRPass - -/** This pass lifts any special operators (ones reserved by the language - * implementation) into their own special IR constructs. - */ -case object LiftSpecialOperators extends IRPass { - - /** A desugaring pass does not output any data. */ - override type Metadata = IRPass.Metadata.Empty - override type Config = IRPass.Configuration.Default - - override def runModule( - ir: IR.Module, - moduleContext: ModuleContext - ): IR.Module = - ir.transformExpressions({ - case x => runExpression(x, new InlineContext) - }) - - /** Executes the lifting pass in an inline context. - * - * @param ir the Enso IR to process - * @param inlineContext a context object that contains the information needed - * for inline evaluation - * @return `ir`, possibly having made transformations or annotations to that - * IR. - */ - override def runExpression( - ir: IR.Expression, - inlineContext: InlineContext - ): IR.Expression = - ir.transformExpressions({ - case IR.Application.Operator.Binary(l, op, r, loc, meta, _) => - op.name match { - case IR.Type.Ascription.name => - IR.Type.Ascription( - runExpression(l, inlineContext), - runExpression(r, inlineContext), - loc, - meta - ) - case IR.Type.Set.Subsumption.name => - IR.Type.Set - .Subsumption( - runExpression(l, inlineContext), - runExpression(r, inlineContext), - loc, - meta - ) - case IR.Type.Set.Equality.name => - IR.Type.Set - .Equality( - runExpression(l, inlineContext), - runExpression(r, inlineContext), - loc, - meta - ) - case IR.Type.Set.Concat.name => - IR.Type.Set - .Concat( - runExpression(l, inlineContext), - runExpression(r, inlineContext), - loc, - meta - ) - case IR.Type.Set.Union.name => - IR.Type.Set - .Union( - runExpression(l, inlineContext), - runExpression(r, inlineContext), - loc, - meta - ) - case IR.Type.Set.Intersection.name => - IR.Type.Set - .Intersection( - runExpression(l, inlineContext), - runExpression(r, inlineContext), - loc, - meta - ) - case IR.Type.Set.Subtraction.name => - IR.Type.Set - .Subtraction( - runExpression(l, inlineContext), - runExpression(r, inlineContext), - loc, - meta - ) - case _ => - IR.Application.Operator - .Binary( - runExpression(l, inlineContext), - op, - runExpression(r, inlineContext), - loc, - meta - ) - } - }) - -} diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/OperatorToFunction.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/OperatorToFunction.scala index b0b673f8d5..feebb13d42 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/OperatorToFunction.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/OperatorToFunction.scala @@ -4,7 +4,16 @@ import org.enso.compiler.context.{InlineContext, ModuleContext} import org.enso.compiler.core.IR import org.enso.compiler.pass.IRPass -/** This pass converts usages of operators to calls to standard functions. */ +/** This pass converts usages of operators to calls to standard functions. + * + * This pass requires the context to provide: + * + * - Nothing + * + * It must have the following passes run before it: + * - [[GenerateMethodBodies]] + * - [[SectionsToBinOp]] + */ case object OperatorToFunction extends IRPass { /** A purely desugaring pass has no analysis output. */ @@ -13,13 +22,13 @@ case object OperatorToFunction extends IRPass { override type Config = IRPass.Configuration.Default /** Executes the conversion pass. - * - * @param ir the Enso IR to process - * @param moduleContext a context object that contains the information needed - * to process a module - * @return `ir`, possibly having made transformations or annotations to that - * IR. - */ + * + * @param ir the Enso IR to process + * @param moduleContext a context object that contains the information needed + * to process a module + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ override def runModule( ir: IR.Module, moduleContext: ModuleContext @@ -45,10 +54,8 @@ case object OperatorToFunction extends IRPass { IR.Application.Prefix( op, List( - IR.CallArgument - .Specified(None, runExpression(l, inlineContext), l.location), - IR.CallArgument - .Specified(None, runExpression(r, inlineContext), r.location) + l.mapExpressions(runExpression(_, inlineContext)), + r.mapExpressions(runExpression(_, inlineContext)) ), hasDefaultsSuspended = false, loc, diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/SectionsToBinOp.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/SectionsToBinOp.scala new file mode 100644 index 0000000000..5546cc192a --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/desugar/SectionsToBinOp.scala @@ -0,0 +1,208 @@ +package org.enso.compiler.pass.desugar + +import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext} +import org.enso.compiler.core.IR +import org.enso.compiler.core.IR.Application.Operator.Section +import org.enso.compiler.exception.CompilerError +import org.enso.compiler.pass.IRPass + +/** This pass converts operator sections to applications of binary operators. + * + * This pass has no configuration. + * + * This pass requires the context to provide: + * + * - A [[FreshNameSupply]]. + * + * It must have the following passes run before it: + * - [[GenerateMethodBodies]] + */ +case object SectionsToBinOp extends IRPass { + override type Metadata = IRPass.Metadata.Empty + override type Config = IRPass.Configuration.Default + + /** Performs section to binary operator conversion on an IR module. + * + * @param ir the Enso IR to process + * @param moduleContext a context object that contains the information needed + * to process a module + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runModule( + ir: IR.Module, + moduleContext: ModuleContext + ): IR.Module = ir.transformExpressions { + case x => + runExpression( + x, + new InlineContext(freshNameSupply = moduleContext.freshNameSupply) + ) + } + + /** Performs section to binary operator conversion on an IR expression. + * + * @param ir the Enso IR to process + * @param inlineContext a context object that contains the information needed + * for inline evaluation + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runExpression( + ir: IR.Expression, + inlineContext: InlineContext + ): IR.Expression = { + val freshNameSupply = inlineContext.freshNameSupply.getOrElse( + throw new CompilerError( + "A fresh name supply is required for sections desugaring." + ) + ) + + ir.transformExpressions { + case sec: IR.Application.Operator.Section => + desugarSections(sec, freshNameSupply) + } + } + + /** Desugars operator sections to fully-saturated applications of operators. + * + * For a left sections it will generate a partially-applied function. For + * right sections it will generate a lambda. For sides sections it is forced + * to generate a lambda returning a partially applied function as we do not + * currently support partial application without the this argument. + * + * @param section the section to desugar + * @return the result of desugaring `section` + */ + //noinspection DuplicatedCode + def desugarSections( + section: IR.Application.Operator.Section, + freshNameSupply: FreshNameSupply + ): IR.Expression = { + section match { + case Section.Left(arg, op, loc, passData, diagnostics) => + IR.Application.Prefix( + op, + List(arg), + hasDefaultsSuspended = false, + loc, + passData, + diagnostics + ) + case Section.Sides(op, loc, passData, diagnostics) => + val leftArgName = freshNameSupply.newName() + val leftCallArg = + IR.CallArgument.Specified(None, leftArgName, None, None) + val leftDefArg = IR.DefinitionArgument.Specified( + // Ensure it has a different identifier + leftArgName.copy(id = IR.randomId), + None, + suspended = false, + None + ) + + val opCall = IR.Application.Prefix( + op, + List(leftCallArg), + hasDefaultsSuspended = false, + loc, + passData, + diagnostics + ) + + IR.Function.Lambda( + List(leftDefArg), + opCall, + loc, + canBeTCO = true, + passData, + diagnostics + ) + + /* Note [Blanks in Right Sections] + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * While the naiive compositional translation of `(- _)` first translates + * the section into a function applying `-` to two arguments, one of which + * is a blank, the compositional nature of the blanks translation actually + * works against us here. + * + * As the `LambdaShorthandToLambda` pass can only operate on the + * application with the blanks, it can't know to push the blank outside + * that application chain. To that end, we have to handle this case + * specially here instead. What we want it to translate to is as follows: + * + * `(- _)` == `x -> (- x)` == `x -> y -> y - x` + * + * We implement this special case here. + */ + + case Section.Right(op, arg, loc, passData, diagnostics) => + val leftArgName = freshNameSupply.newName() + val leftCallArg = + IR.CallArgument.Specified(None, leftArgName, None, None) + val leftDefArg = + IR.DefinitionArgument.Specified( + // Ensure it has a different identifier + leftArgName.copy(id = IR.randomId), + None, + suspended = false, + None + ) + + if (arg.value.isInstanceOf[IR.Name.Blank]) { + // Note [Blanks in Right Sections] + val rightArgName = freshNameSupply.newName() + val rightCallArg = + IR.CallArgument.Specified(None, rightArgName, None, None) + val rightDefArg = IR.DefinitionArgument.Specified( + rightArgName.copy(id = IR.randomId), + None, + suspended = false, + None + ) + + val opCall = IR.Application.Prefix( + op, + List(leftCallArg, rightCallArg), + hasDefaultsSuspended = false, + loc, + passData, + diagnostics + ) + + val leftLam = IR.Function.Lambda( + List(leftDefArg), + opCall, + None + ) + + IR.Function.Lambda( + List(rightDefArg), + leftLam, + loc, + canBeTCO = true, + passData, + diagnostics + ) + } else { + val opCall = IR.Application.Prefix( + op, + List(leftCallArg, arg), + hasDefaultsSuspended = false, + loc, + passData, + diagnostics + ) + + IR.Function.Lambda( + List(leftDefArg), + opCall, + loc, + canBeTCO = true, + passData, + diagnostics + ) + } + } + } +} diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/lint/UnusedBindings.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/lint/UnusedBindings.scala new file mode 100644 index 0000000000..b03ad0633f --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/lint/UnusedBindings.scala @@ -0,0 +1,155 @@ +package org.enso.compiler.pass.lint + +import org.enso.compiler.context.{InlineContext, ModuleContext} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.IRPass +import org.enso.compiler.pass.analyse.AliasAnalysis +import org.enso.compiler.pass.resolve.IgnoredBindings + +import scala.annotation.unused + +/** This pass performs linting for unused names, generating warnings if it finds + * any. + * + * This pass requires the context to provide: + * + * - Nothing + * + * It must have the following passes run before it: + * + * - [[org.enso.compiler.pass.desugar.GenerateMethodBodies]] + * - [[org.enso.compiler.pass.desugar.SectionsToBinOp]] + * - [[org.enso.compiler.pass.desugar.OperatorToFunction]] + * - [[org.enso.compiler.pass.desugar.LambdaShorthandToLambda]] + * - [[IgnoredBindings]] + * - [[org.enso.compiler.pass.optimise.LambdaConsolidate]] + */ +case object UnusedBindings extends IRPass { + override type Metadata = IRPass.Metadata.Empty + override type Config = IRPass.Configuration.Default + + /** Lints a module. + * + * @param ir the Enso IR to process + * @param moduleContext a context object that contains the information needed + * to process a module + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runModule( + ir: IR.Module, + moduleContext: ModuleContext + ): IR.Module = ir.transformExpressions { + case x => x.mapExpressions(runExpression(_, InlineContext())) + } + + /** Lints an arbitrary expression. + * + * @param ir the Enso IR to process + * @param inlineContext a context object that contains the information needed + * for inline evaluation + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runExpression( + ir: IR.Expression, + inlineContext: InlineContext + ): IR.Expression = ir.transformExpressions { + case binding: IR.Expression.Binding => lintBinding(binding, inlineContext) + case function: IR.Function => lintFunction(function, inlineContext) + } + + // === Pass Internals ======================================================= + + /** Lints a binding. + * + * @param binding the binding to lint + * @param context the inline context in which linting is taking place + * @return `binding`, with any lints attached + */ + def lintBinding( + binding: IR.Expression.Binding, + context: InlineContext + ): IR.Expression.Binding = { + val isIgnored = binding + .unsafeGetMetadata( + IgnoredBindings, + "Binding ignore information is required for linting." + ) + .isIgnored + + val aliasInfo = binding + .unsafeGetMetadata( + AliasAnalysis, + "Aliasing information is required for linting." + ) + .unsafeAs[AliasAnalysis.Info.Occurrence] + val isUsed = aliasInfo.graph.linksFor(aliasInfo.id).nonEmpty + + if (!isIgnored && !isUsed) { + binding.copy( + expression = runExpression(binding.expression, context) + ) + binding.addDiagnostic(IR.Warning.Unused.Binding(binding.name)) + } else { + binding.copy( + expression = runExpression(binding.expression, context) + ) + } + } + + /** Lints a function. + * + * @param function the function to lint + * @param context the inline context in which linting is taking place + * @return `function`, with any lints attached + */ + def lintFunction( + function: IR.Function, + @unused context: InlineContext + ): IR.Function = { + function match { + case lam @ IR.Function.Lambda(args, body, _, _, _, _) => + lam.copy( + arguments = args.map(lintFunctionArgument(_, context)), + body = runExpression(body, context) + ) + } + } + + /** Performs linting on a function argument. + * + * @param argument the function argument to lint + * @param context the inline context in which linting is taking place + * @return `argument`, with any lints attached + */ + def lintFunctionArgument( + argument: IR.DefinitionArgument, + context: InlineContext + ): IR.DefinitionArgument = { + val isIgnored = argument + .unsafeGetMetadata( + IgnoredBindings, + "Argument ignore information is required for linting." + ) + .isIgnored + + val aliasInfo = argument + .unsafeGetMetadata( + AliasAnalysis, + "Aliasing information missing but is required for linting." + ) + .unsafeAs[AliasAnalysis.Info.Occurrence] + val isUsed = aliasInfo.graph.linksFor(aliasInfo.id).nonEmpty + + argument match { + case s @ IR.DefinitionArgument.Specified(name, default, _, _, _, _) => + if (!isIgnored && !isUsed) { + s.copy( + defaultValue = default.map(runExpression(_, context)) + ) + .addDiagnostic(IR.Warning.Unused.FunctionArgument(name)) + } else s + } + } +} diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/ApplicationSaturation.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/optimise/ApplicationSaturation.scala similarity index 91% rename from engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/ApplicationSaturation.scala rename to engine/runtime/src/main/scala/org/enso/compiler/pass/optimise/ApplicationSaturation.scala index 2ceb572fa7..e9b05f16f3 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/analyse/ApplicationSaturation.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/optimise/ApplicationSaturation.scala @@ -1,16 +1,31 @@ -package org.enso.compiler.pass.analyse +package org.enso.compiler.pass.optimise import org.enso.compiler.context.{InlineContext, ModuleContext} import org.enso.compiler.core.IR import org.enso.compiler.core.ir.MetadataStorage._ import org.enso.compiler.exception.CompilerError import org.enso.compiler.pass.IRPass +import org.enso.compiler.pass.analyse.AliasAnalysis import org.enso.interpreter.node.{ExpressionNode => RuntimeExpression} import org.enso.interpreter.runtime.callable.argument.CallArgument /** This optimisation pass recognises fully-saturated applications of known * functions and writes analysis data that allows optimisation of them to * specific nodes at codegen time. + * + * This pass requires the context to provide: + * + * - A [[org.enso.compiler.pass.PassConfiguration]] containing an instance of + * [[ApplicationSaturation.Configuration]]. + * + * It must have the following passes run before it: + * + * - [[org.enso.compiler.pass.desugar.GenerateMethodBodies]] + * - [[org.enso.compiler.pass.desugar.SectionsToBinOp]] + * - [[org.enso.compiler.pass.desugar.OperatorToFunction]] + * - [[org.enso.compiler.pass.desugar.LambdaShorthandToLambda]] + * - [[org.enso.compiler.pass.resolve.IgnoredBindings]] + * - [[LambdaConsolidate]] */ case object ApplicationSaturation extends IRPass { diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/optimise/LambdaConsolidate.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/optimise/LambdaConsolidate.scala index c816c54c27..a204dfe8de 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/pass/optimise/LambdaConsolidate.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/optimise/LambdaConsolidate.scala @@ -29,12 +29,22 @@ import org.enso.syntax.text.Location * x y z -> ... * }}} * - * It requires [[org.enso.compiler.pass.analyse.AliasAnalysis]] to be run - * _directly_ before it. - * * Please note that this pass invalidates _all_ metdata on the transformed * portions of the program, and hence must be run before the deeper analysis * passes. + * + * This pass requires the context to provide: + * + * - A [[FreshNameSupply]]. + * + * It must have the following passes run before it: + * + * - [[org.enso.compiler.pass.desugar.GenerateMethodBodies]] + * - [[org.enso.compiler.pass.desugar.SectionsToBinOp]] + * - [[org.enso.compiler.pass.desugar.OperatorToFunction]] + * - [[org.enso.compiler.pass.desugar.LambdaShorthandToLambda]] + * - [[org.enso.compiler.pass.resolve.IgnoredBindings]] + * - [[AliasAnalysis]], which must be run _directly_ before this pass. */ case object LambdaConsolidate extends IRPass { override type Metadata = IRPass.Metadata.Empty @@ -56,8 +66,7 @@ case object LambdaConsolidate extends IRPass { runExpression( x, new InlineContext( - freshNameSupply = moduleContext.freshNameSupply, - passConfiguration = moduleContext.passConfiguration + freshNameSupply = moduleContext.freshNameSupply ) ) } @@ -278,8 +287,9 @@ case object LambdaConsolidate extends IRPass { case defSpec: IR.DefinitionArgument.Specified => defSpec.name.name } ) - case ths: IR.Name.This => ths - case here: IR.Name.Here => here + case ths: IR.Name.This => ths + case here: IR.Name.Here => here + case blank: IR.Name.Blank => blank } } else { name diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/.gitkeep b/engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/IgnoredBindings.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/IgnoredBindings.scala new file mode 100644 index 0000000000..0ffb05f70a --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/IgnoredBindings.scala @@ -0,0 +1,235 @@ +package org.enso.compiler.pass.resolve + +import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext} +import org.enso.compiler.core.IR +import org.enso.compiler.core.ir.MetadataStorage._ +import org.enso.compiler.exception.CompilerError +import org.enso.compiler.pass.IRPass + +/** This pass translates ignored bindings (of the form `_`) into fresh names + * internally, as well as marks all bindings as whether or not they were + * ignored. + * + * This pass has no configuration. + * + * This pass requires the context to provide: + * + * - A [[FreshNameSupply]]. + * + * It must have the following passes run before it: + * + * - [[org.enso.compiler.pass.desugar.GenerateMethodBodies]] + * - [[org.enso.compiler.pass.desugar.LambdaShorthandToLambda]] + */ +case object IgnoredBindings extends IRPass { + override type Metadata = State + override type Config = IRPass.Configuration.Default + + /** Desugars ignored bindings for a module. + * + * @param ir the Enso IR to process + * @param moduleContext a context object that contains the information needed + * to process a module + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runModule( + ir: IR.Module, + moduleContext: ModuleContext + ): IR.Module = ir.transformExpressions { + case x => + x.mapExpressions( + runExpression( + _, + InlineContext(freshNameSupply = moduleContext.freshNameSupply) + ) + ) + } + + /** Desugars ignored bindings for an arbitrary expression. + * + * @param ir the Enso IR to process + * @param inlineContext a context object that contains the information needed + * for inline evaluation + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runExpression( + ir: IR.Expression, + inlineContext: InlineContext + ): IR.Expression = { + val freshNameSupply = inlineContext.freshNameSupply.getOrElse( + throw new CompilerError( + "Desugaring underscore arguments to lambdas requires a fresh name " + + "supply." + ) + ) + + desugarExpression(ir, freshNameSupply) + } + + // === Pass Internals ======================================================= + + /** Desugars ignored bindings of the form `_` in an arbitrary expression. + * + * @param expression the expression to perform desugaring on + * @param supply the compiler's fresh name supply + * @return `expression`, with any ignored bidings desugared + */ + private def desugarExpression( + expression: IR.Expression, + supply: FreshNameSupply + ): IR.Expression = { + expression.transformExpressions { + case binding: IR.Expression.Binding => desugarBinding(binding, supply) + case function: IR.Function => desugarFunction(function, supply) + } + } + + /** Performs desugaring of ignored bindings for a binding. + * + * @param binding the binding to desugar + * @param supply the compiler's supply of fresh names + * @return `binding`, with any ignored bindings desugared + */ + def desugarBinding( + binding: IR.Expression.Binding, + supply: FreshNameSupply + ): IR.Expression.Binding = { + if (isIgnore(binding.name)) { + val newName = supply + .newName() + .copy( + location = binding.name.location, + passData = binding.name.passData, + diagnostics = binding.name.diagnostics + ) + + binding + .copy( + name = newName, + expression = desugarExpression(binding.expression, supply) + ) + .updateMetadata(this -->> State.Ignored) + } else { + binding + .copy( + expression = desugarExpression(binding.expression, supply) + ) + .updateMetadata(this -->> State.NotIgnored) + } + } + + /** Performs desugaring of ignored function arguments. + * + * @param function the function to perform desugaring on + * @param supply the compiler's fresh name supply + * @return `function`, with any ignores desugared + */ + def desugarFunction( + function: IR.Function, + supply: FreshNameSupply + ): IR.Function = { + function match { + case lam @ IR.Function.Lambda(args, body, _, _, _, _) => + val argIsIgnore = args.map(isIgnoreArg) + val newArgs = args.zip(argIsIgnore).map { + case (arg, isIgnore) => genNewArg(arg, isIgnore, supply) + } + + lam.copy( + arguments = newArgs, + body = desugarExpression(body, supply) + ) + } + } + + /** Generates a new argument name for `arg` if `isIgnored` is true. + * + * It also handles recursing through the default values. + * + * @param arg the argument definition to process + * @param isIgnored whether or not `arg` is ignored + * @param freshNameSupply the compiler's fresh name supply + * @return `arg`, if `isIgnored` is `false`, otherwise `arg` with a new name + */ + def genNewArg( + arg: IR.DefinitionArgument, + isIgnored: Boolean, + freshNameSupply: FreshNameSupply + ): IR.DefinitionArgument = { + arg match { + case spec: IR.DefinitionArgument.Specified => + if (isIgnored) { + val newName = freshNameSupply + .newName() + .copy( + location = arg.name.location, + passData = arg.name.passData, + diagnostics = arg.name.diagnostics + ) + + spec + .copy( + name = newName, + defaultValue = + spec.defaultValue.map(desugarExpression(_, freshNameSupply)) + ) + .updateMetadata(this -->> State.Ignored) + } else { + spec + .copy( + defaultValue = + spec.defaultValue.map(desugarExpression(_, freshNameSupply)) + ) + .updateMetadata(this -->> State.NotIgnored) + } + } + } + + /** Checks if a given function definition argument is an ignore. + * + * @param ir the definition argument to check + * @return `true` if `ir` represents an ignore, otherwise `false` + */ + def isIgnoreArg(ir: IR.DefinitionArgument): Boolean = { + ir match { + case IR.DefinitionArgument.Specified(name, _, _, _, _, _) => + isIgnore(name) + } + } + + /** Checks if a given name represents an ignored argument. + * + * @param ir the name to check + * @return `true` if `ir` represents an ignored argument, otherwise `false` + */ + def isIgnore(ir: IR.Name): Boolean = { + ir match { + case _: IR.Name.Blank => true + case IR.Name.Literal(name, _, _, _) => name == "_" + case _ => false + } + } + + // === Pass Metadata ======================================================== + + /** States whether or not the binding was ignored. */ + sealed trait State extends IRPass.Metadata { + val isIgnored: Boolean + } + object State { + + /** States that the binding is ignored. */ + case object Ignored extends State { + override val metadataName: String = "IgnoredBindings.State.Ignored" + override val isIgnored: Boolean = true + } + + /** States that the binding is not ignored. */ + case object NotIgnored extends State { + override val metadataName: String = "IgnoredBindings.State.NotIgnored" + override val isIgnored: Boolean = false + } + } +} diff --git a/engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/OverloadsResolution.scala b/engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/OverloadsResolution.scala new file mode 100644 index 0000000000..0e695281f5 --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/compiler/pass/resolve/OverloadsResolution.scala @@ -0,0 +1,99 @@ +package org.enso.compiler.pass.resolve + +import org.enso.compiler.context.{InlineContext, ModuleContext} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.IRPass + +import scala.annotation.unused + +/** This pass performs static detection of method overloads and emits errors + * at the overload definition site if they are detected. It also checks for + * overloads of atom contructors using the same rule. + * + * Method resolution proceeds in the following order: + * + * 1. Methods defined directly on the atom are resolved first. + * 2. Extension methods in the current scope are resolved next. + * 3. Extension methods in imported scopes are resolved last. + * + * This means that it is possible to shadow an imported extension method with + * one defined locally, and this does not count as an overload. + * + * This pass requires the context to provide: + * + * - Nothing + * + * It must have the following passes run before it: + * + * - [[org.enso.compiler.pass.desugar.GenerateMethodBodies]] + */ +case object OverloadsResolution extends IRPass { + override type Metadata = IRPass.Metadata.Empty + override type Config = IRPass.Configuration.Default + + /** Performs static detection of method overloads within a given module. + * + * @param ir the Enso IR to process + * @param moduleContext a context object that contains the information needed + * to process a module + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runModule( + ir: IR.Module, + @unused moduleContext: ModuleContext + ): IR.Module = { + var seenAtoms: Set[String] = Set() + var seenMethods: Map[String, Set[String]] = Map() + + val atoms = ir.bindings.collect { + case atom: IR.Module.Scope.Definition.Atom => atom + } + + val newAtoms: List[IR.Module.Scope.Definition] = atoms.map(atom => { + if (seenAtoms.contains(atom.name.name)) { + IR.Error.Redefined.Atom(atom.name, atom.location) + } else { + seenAtoms = seenAtoms + atom.name.name + atom + } + }) + + val methods = ir.bindings.collect { + case meth: IR.Module.Scope.Definition.Method => + seenMethods = seenMethods + (meth.typeName.name -> Set()) + meth + } + + val newMethods: List[IR.Module.Scope.Definition] = methods.map(method => { + if (seenMethods(method.typeName.name).contains(method.methodName.name)) { + IR.Error.Redefined + .Method(method.typeName, method.methodName, method.location) + } else { + val currentMethods = seenMethods(method.typeName.name) + seenMethods = + seenMethods + (method.typeName.name -> + (currentMethods + method.methodName.name)) + + method + } + }) + + ir.copy( + bindings = newAtoms ::: newMethods + ) + } + + /** This pass does nothing for the expression flow. + * + * @param ir the Enso IR to process + * @param inlineContext a context object that contains the information needed + * for inline evaluation + * @return `ir`, possibly having made transformations or annotations to that + * IR. + */ + override def runExpression( + ir: IR.Expression, + inlineContext: InlineContext + ): IR.Expression = ir +} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/CompilerTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/CompilerTest.scala index 1137f92030..290e71eee0 100644 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/CompilerTest.scala +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/CompilerTest.scala @@ -1,6 +1,6 @@ package org.enso.compiler.test -import org.enso.compiler.codegen.AstToIR +import org.enso.compiler.codegen.AstToIr import org.enso.compiler.context.{InlineContext, ModuleContext} import org.enso.compiler.core.IR import org.enso.compiler.pass.PassManager @@ -44,7 +44,7 @@ trait CompilerRunner { * @return the [[IR]] representing [[source]] */ def toIrModule: IR.Module = { - AstToIR.translate(source.toAST) + AstToIr.translate(source.toAST) } } @@ -60,7 +60,7 @@ trait CompilerRunner { * @return the [[IR]] representing [[source]], if it is a valid expression */ def toIrExpression: Option[IR.Expression] = { - AstToIR.translateInline(source.toAST) + AstToIr.translateInline(source.toAST) } } diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/codegen/AstToIRTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/codegen/AstToIRTest.scala deleted file mode 100644 index de28f656f1..0000000000 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/codegen/AstToIRTest.scala +++ /dev/null @@ -1,35 +0,0 @@ -package org.enso.compiler.test.codegen - -import org.enso.compiler.core.IR -import org.enso.compiler.test.CompilerTest -import org.enso.syntax.text.Debug - -class AstToIRTest extends CompilerTest { - - "AST translation of lambda definitions" should { - "result in a syntax error when defined with multiple arguments" in { - val ir = - """x y -> x + y - |""".stripMargin.toIrExpression.get - - ir shouldBe an[IR.Error.Syntax] - - ir.asInstanceOf[IR.Error.Syntax].message shouldEqual - "Syntax is not supported yet: pattern matching function arguments." - } - - "support standard lambda chaining" in { - val ir = - """ - |x -> y -> z -> x - |""".stripMargin.toIrExpression.get - - ir shouldBe an[IR.Function.Lambda] - ir.asInstanceOf[IR.Function.Lambda].body shouldBe an[IR.Function.Lambda] - ir.asInstanceOf[IR.Function.Lambda] - .body - .asInstanceOf[IR.Function.Lambda] - .body shouldBe an[IR.Function.Lambda] - } - } -} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/codegen/AstToIrTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/codegen/AstToIrTest.scala new file mode 100644 index 0000000000..60d6068d33 --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/codegen/AstToIrTest.scala @@ -0,0 +1,202 @@ +package org.enso.compiler.test.codegen + +import org.enso.compiler.core.IR +import org.enso.compiler.test.CompilerTest + +class AstToIrTest extends CompilerTest { + + "AST translation of lambda definitions" should { + "result in a syntax error when defined with multiple arguments" in { + val ir = + """x y -> x + y + |""".stripMargin.toIrExpression.get + + ir shouldBe an[IR.Error.Syntax] + + ir.asInstanceOf[IR.Error.Syntax].message shouldEqual + "Syntax is not supported yet: pattern matching function arguments." + } + + "support standard lambda chaining" in { + val ir = + """ + |x -> y -> z -> x + |""".stripMargin.toIrExpression.get + + ir shouldBe an[IR.Function.Lambda] + ir.asInstanceOf[IR.Function.Lambda].body shouldBe an[IR.Function.Lambda] + ir.asInstanceOf[IR.Function.Lambda] + .body + .asInstanceOf[IR.Function.Lambda] + .body shouldBe an[IR.Function.Lambda] + } + } + + "AST translation of operators" should { + "disallow named arguments to operators" in { + val ir = + """ + |(a = 1) + 10 + |""".stripMargin.toIrExpression.get + + ir shouldBe an[IR.Error.Syntax] + ir.asInstanceOf[IR.Error.Syntax] + .reason shouldBe an[IR.Error.Syntax.NamedArgInOperator.type] + } + } + + "AST translation of operator sections" should { + "work properly for left sections" in { + val ir = + """ + |(1 +) + |""".stripMargin.toIrExpression.get + + ir shouldBe an[IR.Application.Operator.Section.Left] + } + + "work properly for sides sections" in { + val ir = + """ + |(+) + |""".stripMargin.toIrExpression.get + + ir shouldBe an[IR.Application.Operator.Section.Sides] + } + + "work properly for right sections" in { + val ir = + """ + |(+ 1) + |""".stripMargin.toIrExpression.get + + ir shouldBe an[IR.Application.Operator.Section.Right] + } + + "disallow sections with named arguments" in { + val ir = + """ + |(+ (left=1)) + |""".stripMargin.toIrExpression.get + + ir shouldBe an[IR.Error.Syntax] + ir.asInstanceOf[IR.Error.Syntax] + .reason shouldBe an[IR.Error.Syntax.NamedArgInSection.type] + } + } + + "AST translation of function applications" should { + "allow use of blank arguments" in { + val ir = + """ + |a b _ d + |""".stripMargin.toIrExpression.get + .asInstanceOf[IR.Application.Prefix] + + ir.arguments(1) shouldBe an[IR.CallArgument.Specified] + ir.arguments(1) + .asInstanceOf[IR.CallArgument.Specified] + .value shouldBe an[IR.Name.Blank] + } + + "allow use of named blank arguments" in { + val ir = + """ + |a b (f = _) c + |""".stripMargin.toIrExpression.get + .asInstanceOf[IR.Application.Prefix] + + ir.arguments(1) shouldBe an[IR.CallArgument.Specified] + ir.arguments(1) + .asInstanceOf[IR.CallArgument.Specified] + .value shouldBe an[IR.Name.Blank] + } + + "allow method-call syntax on a blank" in { + val ir = + """ + |_.foo a b + |""".stripMargin.toIrExpression.get + .asInstanceOf[IR.Application.Prefix] + + ir.arguments.head shouldBe an[IR.CallArgument.Specified] + ir.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value shouldBe an[IR.Name.Blank] + } + + "allow functions in applications to be blanks" in { + val ir = + """ + |_ a b c + |""".stripMargin.toIrExpression.get + .asInstanceOf[IR.Application.Prefix] + + ir.function shouldBe an[IR.Name.Blank] + } + } + + "AST translation of case expressions" should { + "support a blank scrutinee" in { + val ir = + """ + |case _ of + | Cons a b -> a + b + |""".stripMargin.toIrExpression.get.asInstanceOf[IR.Case.Expr] + + ir.scrutinee shouldBe an[IR.Name.Blank] + } + } + + "AST translation of function definitions" should { + "support ignored arguments" in { + val ir = + """ + |_ -> a -> a + 20 + |""".stripMargin.toIrExpression.get.asInstanceOf[IR.Function.Lambda] + + ir.arguments.head shouldBe an[IR.DefinitionArgument.Specified] + val blankArg = + ir.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified] + blankArg.name shouldBe an[IR.Name.Blank] + } + + "support suspended ignored arguments" in { + val ir = + """ + |~_ -> a -> a + 20 + |""".stripMargin.toIrExpression.get.asInstanceOf[IR.Function.Lambda] + + ir.arguments.head shouldBe an[IR.DefinitionArgument.Specified] + val blankArg = + ir.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified] + blankArg.name shouldBe an[IR.Name.Blank] + } + + "support ignored arguments with defaults" in { + val ir = + """ + |(_ = 10) -> a -> a + 20 + |""".stripMargin.toIrExpression.get.asInstanceOf[IR.Function.Lambda] + + ir.arguments.head shouldBe an[IR.DefinitionArgument.Specified] + val blankArg = + ir.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified] + blankArg.name shouldBe an[IR.Name.Blank] + } + } + + "AST translation of bindings" should { + "allow ignored bindings" in { + val ir = + """ + |_ = foo a b + |""".stripMargin.toIrExpression.get + + ir shouldBe an[IR.Expression.Binding] + val binding = ir.asInstanceOf[IR.Expression.Binding] + + binding.name shouldBe an[IR.Name.Blank] + } + } +} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/AliasAnalysisTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/AliasAnalysisTest.scala index da686573ad..3f57ecace1 100644 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/AliasAnalysisTest.scala +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/AliasAnalysisTest.scala @@ -9,8 +9,9 @@ import org.enso.compiler.pass.analyse.AliasAnalysis.Graph.{Link, Occurrence} import org.enso.compiler.pass.analyse.AliasAnalysis.{Graph, Info} import org.enso.compiler.pass.desugar.{ GenerateMethodBodies, - LiftSpecialOperators, - OperatorToFunction + LambdaShorthandToLambda, + OperatorToFunction, + SectionsToBinOp } import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} import org.enso.compiler.test.CompilerTest @@ -22,11 +23,12 @@ class AliasAnalysisTest extends CompilerTest { /** The passes that need to be run before the alias analysis pass. */ val precursorPasses: List[IRPass] = List( GenerateMethodBodies, - LiftSpecialOperators, - OperatorToFunction + SectionsToBinOp, + OperatorToFunction, + LambdaShorthandToLambda ) - val passConfig = PassConfiguration( + val passConfig: PassConfiguration = PassConfiguration( AliasAnalysis -->> AliasAnalysis.Configuration(true) ) diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/DataflowAnalysisTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/DataflowAnalysisTest.scala index 1ab5a96ef2..f1a9727ee2 100644 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/DataflowAnalysisTest.scala +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/DataflowAnalysisTest.scala @@ -12,7 +12,6 @@ import org.enso.compiler.pass.analyse.{ } import org.enso.compiler.pass.desugar.{ GenerateMethodBodies, - LiftSpecialOperators, OperatorToFunction } import org.enso.compiler.pass.optimise.LambdaConsolidate @@ -28,7 +27,6 @@ class DataflowAnalysisTest extends CompilerTest { /** The passes that must be run before the dataflow analysis pass. */ val precursorPasses: List[IRPass] = List( GenerateMethodBodies, - LiftSpecialOperators, OperatorToFunction, AliasAnalysis, LambdaConsolidate, @@ -37,7 +35,7 @@ class DataflowAnalysisTest extends CompilerTest { TailCall ) - val passConfig = PassConfiguration( + val passConfig: PassConfiguration = PassConfiguration( AliasAnalysis -->> AliasAnalysis.Configuration() ) diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/DemandAnalysisTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/DemandAnalysisTest.scala index 7d18141e41..bd0d2ce984 100644 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/DemandAnalysisTest.scala +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/DemandAnalysisTest.scala @@ -6,7 +6,6 @@ import org.enso.compiler.pass.PassConfiguration._ import org.enso.compiler.pass.analyse.{AliasAnalysis, DemandAnalysis} import org.enso.compiler.pass.desugar.{ GenerateMethodBodies, - LiftSpecialOperators, OperatorToFunction } import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} @@ -20,7 +19,6 @@ class DemandAnalysisTest extends CompilerTest { /** The passes that must be run before the demand analysis pass. */ val precursorPasses: List[IRPass] = List( GenerateMethodBodies, - LiftSpecialOperators, OperatorToFunction, AliasAnalysis ) diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/TailCallTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/TailCallTest.scala index 7b88516c6b..b098a514d1 100644 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/TailCallTest.scala +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/TailCallTest.scala @@ -8,7 +8,6 @@ import org.enso.compiler.pass.analyse.TailCall.TailPosition import org.enso.compiler.pass.analyse.{AliasAnalysis, TailCall} import org.enso.compiler.pass.desugar.{ GenerateMethodBodies, - LiftSpecialOperators, OperatorToFunction } import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} @@ -34,7 +33,6 @@ class TailCallTest extends CompilerTest { val precursorPasses: List[IRPass] = List( GenerateMethodBodies, - LiftSpecialOperators, OperatorToFunction, AliasAnalysis ) diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/LambdaShorthandToLambdaTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/LambdaShorthandToLambdaTest.scala new file mode 100644 index 0000000000..0513e564a6 --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/LambdaShorthandToLambdaTest.scala @@ -0,0 +1,480 @@ +package org.enso.compiler.test.pass.desugar + +import org.enso.compiler.context.{FreshNameSupply, InlineContext} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.desugar.{ + GenerateMethodBodies, + LambdaShorthandToLambda, + OperatorToFunction, + SectionsToBinOp +} +import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} +import org.enso.compiler.test.CompilerTest + +class LambdaShorthandToLambdaTest extends CompilerTest { + + // === Test Setup =========================================================== + + val passes: List[IRPass] = List( + GenerateMethodBodies, + SectionsToBinOp, + OperatorToFunction + ) + + val passConfiguration: PassConfiguration = PassConfiguration() + + implicit val passManager: PassManager = + new PassManager(passes, passConfiguration) + + /** Adds an extension method for running desugaring on the input IR. + * + * @param ir the IR to desugar + */ + implicit class DesugarExpression(ir: IR.Expression) { + + /** Runs lambda shorthand desugaring on [[ir]]. + * + * @param inlineContext the inline context in which the desugaring takes + * place + * @return [[ir]], with all lambda shorthand desugared + */ + def desugar(implicit inlineContext: InlineContext): IR.Expression = { + LambdaShorthandToLambda.runExpression(ir, inlineContext) + } + } + + /** Makes an inline context. + * + * @return a new inline context + */ + def mkInlineContext: InlineContext = { + InlineContext(freshNameSupply = Some(new FreshNameSupply)) + } + + // === The Tests ============================================================ + + "Desugaring of underscore arguments" should { + "Work for simple applications with underscore args" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |foo a _ b _ + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + val irFnArgName = + irFn.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + irFn.body shouldBe an[IR.Function.Lambda] + val irFnNested = irFn.body.asInstanceOf[IR.Function.Lambda] + val irFnNestedArgName = irFnNested.arguments.head + .asInstanceOf[IR.DefinitionArgument.Specified] + .name + + irFnNested.body shouldBe an[IR.Application.Prefix] + val body = irFn.body + .asInstanceOf[IR.Function.Lambda] + .body + .asInstanceOf[IR.Application.Prefix] + + val arg2Name = body + .arguments(1) + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + val arg4Name = body + .arguments(3) + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + irFnArgName.name shouldEqual arg2Name.name + irFnNestedArgName.name shouldEqual arg4Name.name + } + + "Work for named applications of underscore args" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |foo (a = _) b _ + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + val irFnArgName = + irFn.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + irFn.body shouldBe an[IR.Function.Lambda] + val irFnNested = irFn.body.asInstanceOf[IR.Function.Lambda] + val irFnNestedArgName = + irFnNested.arguments.head + .asInstanceOf[IR.DefinitionArgument.Specified] + .name + + irFnNested.body shouldBe an[IR.Application.Prefix] + val app = irFnNested.body.asInstanceOf[IR.Application.Prefix] + val arg1Name = + app.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + val arg3Name = + app + .arguments(2) + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + irFnArgName.name shouldEqual arg1Name.name + irFnNestedArgName.name shouldEqual arg3Name.name + } + + "Work if the function in an application is an underscore arg" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |_ a b + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + val irFnArgName = + irFn.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + irFn.body shouldBe an[IR.Application.Prefix] + val app = irFn.body.asInstanceOf[IR.Application.Prefix] + + val fnName = app.function.asInstanceOf[IR.Name.Literal] + + irFnArgName.name shouldEqual fnName.name + } + + "Work with mixfix functions" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |if _ then a + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + val irFnArgName = + irFn.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + irFn.body shouldBe an[IR.Application.Prefix] + val app = irFn.body.asInstanceOf[IR.Application.Prefix] + val arg1Name = app.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + irFnArgName.name shouldEqual arg1Name.name + } + + "Work for an underscore scrutinee in a case expression" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |case _ of + | Nil -> 0 + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + + val irLam = ir.asInstanceOf[IR.Function.Lambda] + irLam.arguments.length shouldEqual 1 + + val lamArgName = + irLam.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + val lamBody = irLam.body.asInstanceOf[IR.Case.Expr] + + lamBody.scrutinee shouldBe an[IR.Name.Literal] + lamBody.scrutinee + .asInstanceOf[IR.Name.Literal] + .name shouldEqual lamArgName.name + } + + "work correctly for operators" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(10 + _) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + + val argName = + irFn.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + val body = irFn.body.asInstanceOf[IR.Application.Prefix] + val rightArg = body.arguments(1).value.asInstanceOf[IR.Name.Literal] + + argName.name shouldEqual rightArg.name + } + + "work correctly for left operator sections" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(_ +) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + val argName = irFn.arguments.head.name + val body = irFn.body.asInstanceOf[IR.Application.Prefix] + + body.arguments.length shouldEqual 1 + + val leftArgName = body.arguments.head.value.asInstanceOf[IR.Name.Literal] + + argName.name shouldEqual leftArgName.name + } + + "work correctly for centre operator sections" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(_ + _) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + val arg1Name = irFn.arguments.head.name + + val irFn2 = irFn.body.asInstanceOf[IR.Function.Lambda] + val arg2Name = irFn2.arguments.head.name + + val app = irFn2.body.asInstanceOf[IR.Application.Prefix] + app.arguments.length shouldEqual 2 + + val leftArg = app.arguments.head.value.asInstanceOf[IR.Name.Literal] + val rightArg = app.arguments(1).value.asInstanceOf[IR.Name.Literal] + + arg1Name.name shouldEqual leftArg.name + arg2Name.name shouldEqual rightArg.name + } + + "work correctly for right operator sections" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(- _) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + val rightArgName = irFn.arguments.head.name + + irFn.body shouldBe an[IR.Function.Lambda] + val irFn2 = irFn.body.asInstanceOf[IR.Function.Lambda] + val leftArgName = irFn2.arguments.head.name + + irFn2.body shouldBe an[IR.Application.Prefix] + val app = irFn2.body.asInstanceOf[IR.Application.Prefix] + + app.arguments.length shouldEqual 2 + + val appLeftName = app.arguments.head.value.asInstanceOf[IR.Name.Literal] + val appRightName = app.arguments(1).value.asInstanceOf[IR.Name.Literal] + + leftArgName.name shouldEqual appLeftName.name + rightArgName.name shouldEqual appRightName.name + } + } + + "Nested underscore arguments" should { + "work for applications" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |a _ (fn _ c) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + ir.asInstanceOf[IR.Function.Lambda] + .body shouldBe an[IR.Application.Prefix] + val irBody = ir + .asInstanceOf[IR.Function.Lambda] + .body + .asInstanceOf[IR.Application.Prefix] + + irBody + .arguments(1) + .asInstanceOf[IR.CallArgument.Specified] + .value shouldBe an[IR.Function.Lambda] + val lamArg = irBody + .arguments(1) + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Function.Lambda] + val lamArgArgName = + lamArg.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + lamArg.body shouldBe an[IR.Application.Prefix] + val lamArgBody = lamArg.body.asInstanceOf[IR.Application.Prefix] + val lamArgBodyArg1Name = lamArgBody.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + lamArgArgName.name shouldEqual lamArgBodyArg1Name.name + } + + "work in named applications" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |a _ (fn (t = _) c) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + ir.asInstanceOf[IR.Function.Lambda] + .body shouldBe an[IR.Application.Prefix] + val irBody = ir + .asInstanceOf[IR.Function.Lambda] + .body + .asInstanceOf[IR.Application.Prefix] + + irBody + .arguments(1) + .asInstanceOf[IR.CallArgument.Specified] + .value shouldBe an[IR.Function.Lambda] + val lamArg = irBody + .arguments(1) + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Function.Lambda] + val lamArgArgName = + lamArg.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + lamArg.body shouldBe an[IR.Application.Prefix] + val lamArgBody = lamArg.body.asInstanceOf[IR.Application.Prefix] + val lamArgBodyArg1Name = lamArgBody.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + lamArgArgName.name shouldEqual lamArgBodyArg1Name.name + } + + "work in function argument defaults" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |a -> (b = f _ 1) -> f a + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val bArgFn = ir + .asInstanceOf[IR.Function.Lambda] + .body + .asInstanceOf[IR.Function.Lambda] + val bArg1 = + bArgFn.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified] + + bArg1.defaultValue shouldBe defined + bArg1.defaultValue.get shouldBe an[IR.Function.Lambda] + val default = bArg1.defaultValue.get.asInstanceOf[IR.Function.Lambda] + val defaultArgName = default.arguments.head + .asInstanceOf[IR.DefinitionArgument.Specified] + .name + + default.body shouldBe an[IR.Application.Prefix] + val defBody = default.body.asInstanceOf[IR.Application.Prefix] + val defBodyArg1Name = defBody.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + defaultArgName.name shouldEqual defBodyArg1Name.name + } + + "work for case expressions" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |case _ of + | Nil -> f _ b + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val nilBranch = ir + .asInstanceOf[IR.Function.Lambda] + .body + .asInstanceOf[IR.Case.Expr] + .branches + .head + .asInstanceOf[IR.Case.Branch] + .expression + .asInstanceOf[IR.Function.Lambda] + + nilBranch.body shouldBe an[IR.Function.Lambda] + val nilBody = nilBranch.body.asInstanceOf[IR.Function.Lambda] + val nilBodyArgName = + nilBody.arguments.head + .asInstanceOf[IR.DefinitionArgument.Specified] + .name + + nilBody.body shouldBe an[IR.Application.Prefix] + val nilBodyBody = nilBody.body.asInstanceOf[IR.Application.Prefix] + val nilBodyBodyArg1Name = nilBodyBody.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + nilBodyArgName.name shouldEqual nilBodyBodyArg1Name.name + } + } + + "A single lambda shorthand" should { + "translate to `id` in general" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |x = _ + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Expression.Binding] + val expr = ir.asInstanceOf[IR.Expression.Binding].expression + + expr shouldBe an[IR.Function.Lambda] + } + + "translate to an `id` in argument defaults" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(x = _) -> x + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + + irFn.arguments.head shouldBe an[IR.DefinitionArgument.Specified] + val arg = + irFn.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified] + + arg.defaultValue shouldBe defined + arg.defaultValue.get shouldBe an[IR.Function.Lambda] + } + } +} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/LiftSpecialOperatorsTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/LiftSpecialOperatorsTest.scala deleted file mode 100644 index 11cb7d4ebf..0000000000 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/LiftSpecialOperatorsTest.scala +++ /dev/null @@ -1,93 +0,0 @@ -package org.enso.compiler.test.pass.desugar - -import org.enso.compiler.context.{InlineContext, ModuleContext} -import org.enso.compiler.core.IR -import org.enso.compiler.core.IR.IdentifiedLocation -import org.enso.compiler.pass.desugar.LiftSpecialOperators -import org.enso.compiler.test.CompilerTest -import org.enso.syntax.text.Location - -class LiftSpecialOperatorsTest extends CompilerTest { - - // === Utilities ============================================================ - - val ctx = InlineContext() - val modCtx = ModuleContext() - - /** Tests whether a given operator is lifted correctly into the corresponding - * special construct. - * - * @param opInfo the operator symbol - * @param constructor the way to construct the operator - */ - def testOperator( - opInfo: IR.Type.Info, - constructor: ( - IR.Expression, - IR.Expression, - Option[IdentifiedLocation] - ) => IR.Expression - ): Unit = s"The ${opInfo.name} operator" should { - val op = IR.Name.Literal(opInfo.name, None) - val left = IR.Empty(None) - val right = IR.Empty(None) - val loc = IdentifiedLocation(Location(1, 20)) - - val expressionIR = IR.Application.Operator.Binary( - left, - op, - right, - Some(loc) - ) - val outputExpressionIR = constructor( - left, - right, - Some(loc) - ) - - "be lifted by the pass in an inline context" in { - LiftSpecialOperators - .runExpression(expressionIR, ctx) shouldEqual outputExpressionIR - } - - "be lifted by the pass in a module context" in { - val moduleInput = expressionIR.asModuleDefs - val moduleOutput = outputExpressionIR.asModuleDefs - - LiftSpecialOperators - .runModule(moduleInput, modCtx) shouldEqual moduleOutput - } - - "work recursively where necessary" in { - val recursiveIR = - IR.Application.Operator.Binary(expressionIR, op, right, None) - val recursiveIROutput = constructor( - constructor(left, right, Some(loc)), - right, - None - ) - - LiftSpecialOperators - .runExpression(recursiveIR, ctx) shouldEqual recursiveIROutput - } - } - - // === The Tests ============================================================ - - testOperator(IR.Type.Ascription, IR.Type.Ascription(_, _, _)) - - testOperator( - IR.Type.Set.Subsumption, - IR.Type.Set.Subsumption(_, _, _) - ) - - testOperator(IR.Type.Set.Equality, IR.Type.Set.Equality(_, _, _)) - - testOperator(IR.Type.Set.Concat, IR.Type.Set.Concat(_, _, _)) - - testOperator(IR.Type.Set.Union, IR.Type.Set.Union(_, _, _)) - - testOperator(IR.Type.Set.Intersection, IR.Type.Set.Intersection(_, _, _)) - - testOperator(IR.Type.Set.Subtraction, IR.Type.Set.Subtraction(_, _, _)) -} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/OperatorToFunctionTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/OperatorToFunctionTest.scala index a4a57f4a17..5076636264 100644 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/OperatorToFunctionTest.scala +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/OperatorToFunctionTest.scala @@ -28,13 +28,14 @@ class OperatorToFunctionTest extends CompilerTest { ): (IR.Application.Operator.Binary, IR.Application.Prefix) = { val loc = IdentifiedLocation(Location(1, 33)) - val binOp = IR.Application.Operator.Binary(left, name, right, Some(loc)) + val leftArg = IR.CallArgument.Specified(None, left, left.location) + val rightArg = IR.CallArgument.Specified(None, right, right.location) + + val binOp = + IR.Application.Operator.Binary(leftArg, name, rightArg, Some(loc)) val opFn = IR.Application.Prefix( name, - List( - IR.CallArgument.Specified(None, left, left.location), - IR.CallArgument.Specified(None, right, right.location) - ), + List(leftArg, rightArg), hasDefaultsSuspended = false, Some(loc) ) @@ -45,12 +46,16 @@ class OperatorToFunctionTest extends CompilerTest { // === The Tests ============================================================ "Operators" should { - val opName = IR.Name.Literal("=:=", None) - val left = IR.Empty(None) - val right = IR.Empty(None) + val opName = IR.Name.Literal("=:=", None) + val left = IR.Empty(None) + val right = IR.Empty(None) + val rightArg = IR.CallArgument.Specified(None, IR.Empty(None), None) val (operator, operatorFn) = genOprAndFn(opName, left, right) + val oprArg = IR.CallArgument.Specified(None, operator, None) + val oprFnArg = IR.CallArgument.Specified(None, operatorFn, None) + "be translated to functions" in { OperatorToFunction.runExpression(operator, ctx) shouldEqual operatorFn } @@ -64,13 +69,10 @@ class OperatorToFunctionTest extends CompilerTest { "be translated recursively" in { val recursiveIR = - IR.Application.Operator.Binary(operator, opName, right, None) + IR.Application.Operator.Binary(oprArg, opName, rightArg, None) val recursiveIRResult = IR.Application.Prefix( opName, - List( - IR.CallArgument.Specified(None, operatorFn, operatorFn.location), - IR.CallArgument.Specified(None, right, right.location) - ), + List(oprFnArg, rightArg), hasDefaultsSuspended = false, None ) diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/SectionsToBinOpTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/SectionsToBinOpTest.scala new file mode 100644 index 0000000000..dbb740d4d2 --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/desugar/SectionsToBinOpTest.scala @@ -0,0 +1,167 @@ +package org.enso.compiler.test.pass.desugar + +import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} +import org.enso.compiler.pass.desugar.{GenerateMethodBodies, SectionsToBinOp} +import org.enso.compiler.test.CompilerTest + +class SectionsToBinOpTest extends CompilerTest { + + // === Test Configuration =================================================== + + val passes: List[IRPass] = List( + GenerateMethodBodies + ) + + val passConfiguration: PassConfiguration = PassConfiguration() + + implicit val passManager: PassManager = + new PassManager(passes, passConfiguration) + + /** Adds an extension method for running desugaring on the input IR. + * + * @param ir the IR to desugar + */ + implicit class DesugarExpression(ir: IR.Expression) { + + /** Runs section desugaring on [[ir]]. + * + * @param inlineContext the inline context in which the desugaring takes + * place + * @return [[ir]], with all sections desugared + */ + def desugar(implicit inlineContext: InlineContext): IR.Expression = { + SectionsToBinOp.runExpression(ir, inlineContext) + } + } + + /** Makes an inline context. + * + * @return a new inline context + */ + def mkInlineContext: InlineContext = { + InlineContext(freshNameSupply = Some(new FreshNameSupply)) + } + + // === The Tests ============================================================ + + "Operator section desugaring" should { + "work for left sections" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(1 +) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Application.Prefix] + ir.asInstanceOf[IR.Application.Prefix].arguments.length shouldEqual 1 + } + + "work for sides sections" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(+) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + + val irLam = ir.asInstanceOf[IR.Function.Lambda] + irLam.arguments.length shouldEqual 1 + + val lamArgName = + irLam.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + val lamBody = irLam.body.asInstanceOf[IR.Application.Prefix] + lamBody.arguments.length shouldEqual 1 + val lamBodyFirstArg = + lamBody.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + lamBodyFirstArg.name shouldEqual lamArgName.name + lamBodyFirstArg.getId should not equal lamArgName.getId + } + + "work for right sections" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(+ 1) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + + val irLam = ir.asInstanceOf[IR.Function.Lambda] + irLam.arguments.length shouldEqual 1 + + val lamArgName = + irLam.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + val lamBody = irLam.body.asInstanceOf[IR.Application.Prefix] + lamBody.arguments.length shouldEqual 2 + val lamBodyFirstArg = + lamBody.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + + lamBodyFirstArg.name shouldEqual lamArgName.name + lamBodyFirstArg.getId should not equal lamArgName.getId + } + + "work when the section is nested" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |x -> (x +) + |""".stripMargin.preprocessExpression.get.desugar + .asInstanceOf[IR.Function.Lambda] + + ir.body shouldBe an[IR.Application.Prefix] + ir.body.asInstanceOf[IR.Application.Prefix].arguments.length shouldEqual 1 + } + + "flip the arguments when a right section's argument is a blank" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |(- _) + |""".stripMargin.preprocessExpression.get.desugar + + ir shouldBe an[IR.Function.Lambda] + val irFn = ir.asInstanceOf[IR.Function.Lambda] + + val rightArgName = + irFn.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + irFn.body shouldBe an[IR.Function.Lambda] + val irFn2 = irFn.body.asInstanceOf[IR.Function.Lambda] + + val leftArgName = + irFn2.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified].name + + irFn2.body shouldBe an[IR.Application.Prefix] + val app = irFn2.body.asInstanceOf[IR.Application.Prefix] + + val appLeftName = app.arguments.head + .asInstanceOf[IR.CallArgument.Specified] + .value + .asInstanceOf[IR.Name.Literal] + val appRightName = app + .arguments(1) + .value + .asInstanceOf[IR.Name.Literal] + + leftArgName.name shouldEqual appLeftName.name + rightArgName.name shouldEqual appRightName.name + } + } +} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/lint/UnusedBindingsTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/lint/UnusedBindingsTest.scala new file mode 100644 index 0000000000..d23c060865 --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/lint/UnusedBindingsTest.scala @@ -0,0 +1,136 @@ +package org.enso.compiler.test.pass.lint + +import org.enso.compiler.context.{FreshNameSupply, InlineContext} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.PassConfiguration._ +import org.enso.compiler.pass.analyse._ +import org.enso.compiler.pass.desugar.{GenerateMethodBodies, LambdaShorthandToLambda, OperatorToFunction, SectionsToBinOp} +import org.enso.compiler.pass.lint.UnusedBindings +import org.enso.compiler.pass.optimise.{ApplicationSaturation, LambdaConsolidate} +import org.enso.compiler.pass.resolve.IgnoredBindings +import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} +import org.enso.compiler.test.CompilerTest +import org.enso.interpreter.runtime.scope.LocalScope + +import scala.annotation.unused + +class UnusedBindingsTest extends CompilerTest { + + // === Test Setup =========================================================== + + val passes: List[IRPass] = List( + GenerateMethodBodies, + SectionsToBinOp, + OperatorToFunction, + LambdaShorthandToLambda, + IgnoredBindings, + AliasAnalysis, + LambdaConsolidate, + AliasAnalysis, + DemandAnalysis, + ApplicationSaturation, + TailCall, + DataflowAnalysis + ) + + val passConfiguration: PassConfiguration = PassConfiguration( + ApplicationSaturation -->> ApplicationSaturation.Configuration(), + AliasAnalysis -->> AliasAnalysis.Configuration() + ) + + implicit val passManager: PassManager = + new PassManager(passes, passConfiguration) + + /** Adds an extension method for running linting on the input IR. + * + * @param ir the IR to lint + */ + implicit class LintExpression(ir: IR.Expression) { + + /** Runs unused name linting on [[ir]]. + * + * @param inlineContext the inline context in which the desugaring takes + * place + * @return [[ir]], with all unused names linted + */ + def lint(implicit inlineContext: InlineContext): IR.Expression = { + UnusedBindings.runExpression(ir, inlineContext) + } + } + + /** Makes an inline context. + * + * @return a new inline context + */ + def mkInlineContext: InlineContext = { + InlineContext( + freshNameSupply = Some(new FreshNameSupply), + localScope = Some(LocalScope.root), + isInTailPosition = Some(false) + ) + } + + // === The Tests ============================================================ + + "Unused bindings linting" should { + "attach a warning to an unused function argument" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |x -> 10 + |""".stripMargin.preprocessExpression.get.lint + .asInstanceOf[IR.Function.Lambda] + + val lintMeta = ir.arguments.head.diagnostics.collect { + case u: IR.Warning.Unused.FunctionArgument => u + } + + lintMeta should not be empty + lintMeta.head shouldBe an[IR.Warning.Unused.FunctionArgument] + lintMeta.head.name.name shouldEqual "x" + } + + "not attach a warning to an unused function argument if it is an ignore" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |_ -> 10 + |""".stripMargin.preprocessExpression.get.lint + .asInstanceOf[IR.Function.Lambda] + + ir.arguments.head.diagnostics.toList shouldBe empty + } + + "attach a warning to an unused binding" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |a = 10 + |""".stripMargin.preprocessExpression.get.lint + .asInstanceOf[IR.Expression.Binding] + + val lintMeta = ir.diagnostics.collect { + case u: IR.Warning.Unused.Binding => u + } + + lintMeta should not be empty + lintMeta.head shouldBe an[IR.Warning.Unused.Binding] + lintMeta.head.name.name shouldEqual "a" + } + + "not attach a warning to an unused binding if it is an ignore" in { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |_ = 10 + |""".stripMargin.preprocessExpression.get.lint + .asInstanceOf[IR.Expression.Binding] + + ir.diagnostics.toList shouldBe empty + } + } +} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/ApplicationSaturationTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/optimise/ApplicationSaturationTest.scala similarity index 96% rename from engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/ApplicationSaturationTest.scala rename to engine/runtime/src/test/scala/org/enso/compiler/test/pass/optimise/ApplicationSaturationTest.scala index 6a1bd3b828..f60c9a53dd 100644 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/analyse/ApplicationSaturationTest.scala +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/optimise/ApplicationSaturationTest.scala @@ -1,11 +1,12 @@ -package org.enso.compiler.test.pass.analyse +package org.enso.compiler.test.pass.optimise import org.enso.compiler.context.{InlineContext, ModuleContext} import org.enso.compiler.core.IR import org.enso.compiler.pass.PassConfiguration._ -import org.enso.compiler.pass.analyse.ApplicationSaturation.{CallSaturation, FunctionSpec, Metadata} -import org.enso.compiler.pass.analyse.{AliasAnalysis, ApplicationSaturation} -import org.enso.compiler.pass.desugar.{LiftSpecialOperators, OperatorToFunction} +import org.enso.compiler.pass.optimise.ApplicationSaturation.{CallSaturation, FunctionSpec, Metadata} +import org.enso.compiler.pass.analyse.AliasAnalysis +import org.enso.compiler.pass.desugar.OperatorToFunction +import org.enso.compiler.pass.optimise.ApplicationSaturation import org.enso.compiler.pass.{PassConfiguration, PassManager} import org.enso.compiler.test.CompilerTest import org.enso.interpreter.node.ExpressionNode @@ -13,7 +14,6 @@ import org.enso.interpreter.runtime.callable.argument.CallArgument import org.enso.interpreter.runtime.scope.LocalScope import scala.annotation.unused -import scala.reflect.ClassTag class ApplicationSaturationTest extends CompilerTest { @@ -52,7 +52,6 @@ class ApplicationSaturationTest extends CompilerTest { ) val passes = List( - LiftSpecialOperators, OperatorToFunction, AliasAnalysis ) diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/optimise/LambdaConsolidateTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/optimise/LambdaConsolidateTest.scala index da771888cd..6a1d153efd 100644 --- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/optimise/LambdaConsolidateTest.scala +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/optimise/LambdaConsolidateTest.scala @@ -6,7 +6,6 @@ import org.enso.compiler.pass.PassConfiguration._ import org.enso.compiler.pass.analyse.AliasAnalysis import org.enso.compiler.pass.desugar.{ GenerateMethodBodies, - LiftSpecialOperators, OperatorToFunction } import org.enso.compiler.pass.optimise.LambdaConsolidate @@ -19,7 +18,6 @@ class LambdaConsolidateTest extends CompilerTest { val precursorPasses: List[IRPass] = List( GenerateMethodBodies, - LiftSpecialOperators, OperatorToFunction, AliasAnalysis ) diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/IgnoredBindingsTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/IgnoredBindingsTest.scala new file mode 100644 index 0000000000..29399c39b4 --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/IgnoredBindingsTest.scala @@ -0,0 +1,134 @@ +package org.enso.compiler.test.pass.resolve + +import org.enso.compiler.context.{FreshNameSupply, InlineContext} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.desugar.{ + GenerateMethodBodies, + LambdaShorthandToLambda +} +import org.enso.compiler.pass.resolve.IgnoredBindings +import org.enso.compiler.pass.resolve.IgnoredBindings.State +import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} +import org.enso.compiler.test.CompilerTest + +class IgnoredBindingsTest extends CompilerTest { + + // === Test Setup =========================================================== + + val passes: List[IRPass] = List( + GenerateMethodBodies, + LambdaShorthandToLambda + ) + + val passConfiguration: PassConfiguration = PassConfiguration() + + implicit val passManager: PassManager = + new PassManager(passes, passConfiguration) + + /** Adds an extension method for running desugaring on the input IR. + * + * @param ir the IR to desugar + */ + implicit class DesugarExpression(ir: IR.Expression) { + + /** Runs ignores desugaring on [[ir]]. + * + * @param inlineContext the inline context in which the desugaring takes + * place + * @return [[ir]], with all ignores desugared + */ + def desugar(implicit inlineContext: InlineContext): IR.Expression = { + IgnoredBindings.runExpression(ir, inlineContext) + } + } + + /** Makes an inline context. + * + * @return a new inline context + */ + def mkInlineContext: InlineContext = { + InlineContext(freshNameSupply = Some(new FreshNameSupply)) + } + + // === The Tests ============================================================ + + "Ignored bindings desugaring for function args" should { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |_ -> (x = _ -> 1) -> x + |""".stripMargin.preprocessExpression.get.desugar + .asInstanceOf[IR.Function.Lambda] + val blankArg = + ir.arguments.head.asInstanceOf[IR.DefinitionArgument.Specified] + val xArg = ir.body + .asInstanceOf[IR.Function.Lambda] + .arguments + .head + .asInstanceOf[IR.DefinitionArgument.Specified] + + "replace ignored arguments with fresh names" in { + blankArg.name shouldBe an[IR.Name.Literal] + } + + "mark ignored arguments as ignored" in { + blankArg.getMetadata(IgnoredBindings) shouldEqual Some(State.Ignored) + } + + "mark normal arguments as not ignored" in { + xArg.getMetadata(IgnoredBindings) shouldEqual Some(State.NotIgnored) + } + + "work when deeply nested" in { + val nestedIgnore = xArg.defaultValue.get + .asInstanceOf[IR.Function.Lambda] + .arguments + .head + .asInstanceOf[IR.DefinitionArgument.Specified] + + nestedIgnore.name shouldBe an[IR.Name.Literal] + nestedIgnore.getMetadata(IgnoredBindings) shouldEqual Some(State.Ignored) + } + } + + "Ignored bindings desugaring for bindings" should { + implicit val ctx: InlineContext = mkInlineContext + + val ir = + """ + |_ = + | _ = f a b + | x = y + | 10 + |""".stripMargin.preprocessExpression.get.desugar + .asInstanceOf[IR.Expression.Binding] + + val bindingName = ir.name + val bindingBody = ir.expression.asInstanceOf[IR.Expression.Block] + + "replace the ignored binding with a fresh name" in { + bindingName shouldBe an[IR.Name.Literal] + } + + "mark the binding as ignored if it was" in { + ir.getMetadata(IgnoredBindings) shouldEqual Some(State.Ignored) + } + + "mark the binding as not ignored if it wasn't" in { + val nonIgnored = + bindingBody.expressions(1).asInstanceOf[IR.Expression.Binding] + + nonIgnored.getMetadata(IgnoredBindings) shouldEqual Some( + State.NotIgnored + ) + } + + "work when deeply nested" in { + val ignoredInBlock = + bindingBody.expressions.head.asInstanceOf[IR.Expression.Binding] + + ignoredInBlock.name shouldBe an[IR.Name.Literal] + } + } +} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/OverloadsResolutionTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/OverloadsResolutionTest.scala new file mode 100644 index 0000000000..542e33d24c --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/OverloadsResolutionTest.scala @@ -0,0 +1,113 @@ +package org.enso.compiler.test.pass.resolve + +import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.desugar.GenerateMethodBodies +import org.enso.compiler.pass.resolve.OverloadsResolution +import org.enso.compiler.pass.{IRPass, PassConfiguration, PassManager} +import org.enso.compiler.test.CompilerTest + +import scala.annotation.unused + +class OverloadsResolutionTest extends CompilerTest { + + // === Test Setup =========================================================== + + val passes: List[IRPass] = List( + GenerateMethodBodies + ) + + val passConfiguration: PassConfiguration = PassConfiguration() + + implicit val passManager: PassManager = + new PassManager(passes, passConfiguration) + + /** Adds an extension method for resolution on the input IR. + * + * @param ir the IR to desugar + */ + implicit class ResolveModule(ir: IR.Module) { + + /** Runs section desugaring on [[ir]]. + * + * @param moduleContext the module context in which the resolution takes + * place + * @return [[ir]], with all sections desugared + */ + def resolve(implicit moduleContext: ModuleContext): IR.Module = { + OverloadsResolution.runModule(ir, moduleContext) + } + } + + /** Makes a module context. + * + * @return a new module context + */ + def mkModuleContext: ModuleContext = { + ModuleContext() + } + + // === The Tests ============================================================ + + "Method overload resolution" should { + implicit val ctx: ModuleContext = mkModuleContext + + val atomName = "Unit" + val methodName = "foo" + + val ir = + s""" + |$atomName.$methodName = x -> x + |$atomName.$methodName = x -> y -> x + y + |$atomName.$methodName = 10 + |""".stripMargin.preprocessModule.resolve + + "detect overloads within a given module" in { + exactly(2, ir.bindings) shouldBe an[IR.Error.Redefined.Method] + } + + "replace all overloads by an error node" in { + ir.bindings(1) shouldBe an[IR.Error.Redefined.Method] + ir.bindings(2) shouldBe an[IR.Error.Redefined.Method] + val redef1 = ir.bindings(1).asInstanceOf[IR.Error.Redefined.Method] + val redef2 = ir.bindings(2).asInstanceOf[IR.Error.Redefined.Method] + + redef1.atomName.name shouldEqual atomName + redef2.atomName.name shouldEqual atomName + + redef1.methodName.name shouldEqual methodName + redef2.methodName.name shouldEqual methodName + } + } + + "Atom overload resolution" should { + implicit val ctx: ModuleContext = mkModuleContext + + val atomName = "MyAtom" + + val ir = + s""" + |type $atomName a b c + |type $atomName a b + |type $atomName a + |""".stripMargin.preprocessModule.resolve + + "detect overloads within a given module" in { + exactly(2, ir.bindings) shouldBe an[IR.Error.Redefined.Atom] + } + + "replace all overloads by an error node" in { + ir.bindings(1) shouldBe an[IR.Error.Redefined.Atom] + ir.bindings(1) + .asInstanceOf[IR.Error.Redefined.Atom] + .atomName + .name shouldEqual atomName + ir.bindings(2) shouldBe an[IR.Error.Redefined.Atom] + ir.bindings(2) + .asInstanceOf[IR.Error.Redefined.Atom] + .atomName + .name shouldEqual atomName + } + } + +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/LambdaShorthandArgsTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/LambdaShorthandArgsTest.scala new file mode 100644 index 0000000000..cb29b41b0f --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/LambdaShorthandArgsTest.scala @@ -0,0 +1,136 @@ +package org.enso.interpreter.test.semantic + +import org.enso.interpreter.test.InterpreterTest + +class LambdaShorthandArgsTest extends InterpreterTest { + + val subject = "Lambda shorthand arguments" + + subject should "work for simple applications" in { + val code = + """ + |main = + | f = a -> b -> c -> a + b - c + | g = f _ 5 5 + | g 10 + |""".stripMargin + + eval(code) shouldEqual 10 + } + + subject should "work for named applications" in { + val code = + """ + |main = + | f = a -> b -> a - b + | g = f (b = _) + | g 10 5 + |""".stripMargin + + eval(code) shouldEqual -5 + } + + subject should "work for functions in applications" in { + val code = + """ + |main = + | add = a -> b -> a + b + | sub = a -> b -> a - b + | f = _ 10 5 + | res1 = f add + | res2 = f sub + | res1 - res2 + |""".stripMargin + + eval(code) shouldEqual 10 + } + + subject should "work with mixfix functions" in { + val code = + """ + |Number.if_then_else = ~t -> ~f -> ifZero this t f + | + |main = + | f = if _ then 10 else 5 + | res1 = f 0 + | res2 = f 1 + | res1 - res2 + |""".stripMargin + + eval(code) shouldEqual 5 + } + + subject should "work with case expressions" in { + val code = + """ + |main = + | f = case _ of + | Cons a b -> 10 + | Nil -> 0 + | res1 = f (Cons 1 2) + | res2 = f Nil + | res2 - res1 + |""".stripMargin + + eval(code) shouldEqual -10 + } + + subject should "mean id when used alone" in { + val code = + """ + |main = + | f = (x = _) -> x + | g = f.call + | h = _ + | res1 = g 10 + | res2 = h 10 + | res1 - res2 + |""".stripMargin + + eval(code) shouldEqual 0 + } + + subject should "work with operators" in { + val code = + """ + |main = + | f = (_ + 10) + | f 10 + |""".stripMargin + + eval(code) shouldEqual 20 + } + + subject should "work properly with left operator sections" in { + val code = + """ + |main = + | f = (_ -) + | f 10 5 + |""".stripMargin + + eval(code) shouldEqual 5 + } + + subject should "work properly with centre operator sections" in { + val code = + """ + |main = + | f = _ - _ + | f 10 5 + |""".stripMargin + + eval(code) shouldEqual 5 + } + + subject should "work properly with right operator sections" in { + val code = + """ + |main = + | f = (- _) + | f 10 5 + |""".stripMargin + + eval(code) shouldEqual -5 + } +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/MethodsTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/MethodsTest.scala index 695d507d5a..95fe7d9584 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/MethodsTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/MethodsTest.scala @@ -158,22 +158,4 @@ class MethodsTest extends InterpreterTest { eval(code) shouldEqual 6 } - - "Methods" should "not be overloaded on a given atom" in { - val code = - """ - |type MyAtom - | - |MyAtom.foo = a -> a - |MyAtom.foo = a -> b -> a + b - | - |main = foo MyAtom 1 - |""".stripMargin - - val msg = - "org.enso.interpreter.runtime.error.RedefinedMethodException: Methods " + - "cannot be overloaded, but you have tried to overload MyAtom.foo" - - the[InterpreterException] thrownBy eval(code) should have message msg - } } diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/OperatorSectionsTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/OperatorSectionsTest.scala new file mode 100644 index 0000000000..b2ced2f15e --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/OperatorSectionsTest.scala @@ -0,0 +1,39 @@ +package org.enso.interpreter.test.semantic + +import org.enso.interpreter.test.InterpreterTest + +class OperatorSectionsTest extends InterpreterTest { + + "Left operator sections" should "work" in { + val code = + """ + |main = + | f = (1 +) + | f 9 + |""".stripMargin + + eval(code) shouldEqual 10 + } + + "Sides operator sections" should "work" in { + val code = + """ + |main = + | f = (-) + | f 1 6 + |""".stripMargin + + eval(code) shouldEqual -5 + } + + "Right operator sections" should "work" in { + val code = + """ + |main = + | f = (- 10) + | f 5 + |""".stripMargin + + eval(code) shouldEqual -5 + } +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/OverloadsResolutionErrorTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/OverloadsResolutionErrorTest.scala new file mode 100644 index 0000000000..5d53164d3a --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/OverloadsResolutionErrorTest.scala @@ -0,0 +1,56 @@ +package org.enso.interpreter.test.semantic + +import org.enso.interpreter.test.{InterpreterException, InterpreterTest} +import org.enso.polyglot.{LanguageInfo, RuntimeOptions} +import org.graalvm.polyglot.Context + +class OverloadsResolutionErrorTest extends InterpreterTest { + + // === Test Setup =========================================================== + + override val ctx: Context = Context + .newBuilder(LanguageInfo.ID) + .allowExperimentalOptions(true) + .option(RuntimeOptions.STRICT_ERRORS, "true") + .out(output) + .build() + + // === The Tests ============================================================ + + "Method overloads" should "result in an error at runtime" in { + val code = + """ + |Unit.foo = 10 + |Unit.foo = 20 + |""".stripMargin.linesIterator.mkString("\n") + + the[InterpreterException] thrownBy eval(code) should have message + "Compilation aborted due to errors." + + val diagnostics = consumeOut + diagnostics + .filterNot(_.contains("Compiler encountered")) + .toSet shouldEqual Set( + "Test[3:1-3:13]: Method overloads are not supported: Unit.foo is defined multiple times in this module." + ) + } + + "Atom overloads" should "result in an error at runtime" in { + val code = + """ + |type MyAtom + |type MyAtom + |""".stripMargin.linesIterator.mkString("\n") + + the[InterpreterException] thrownBy eval(code) should have message + "Compilation aborted due to errors." + + val diagnostics = consumeOut + diagnostics + .filterNot(_.contains("Compiler encountered")) + .toSet shouldEqual Set( + "Test[3:1-3:11]: Redefining atoms is not supported: MyAtom is defined multiple times in this module." + ) + } + +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/StrictCompileDiagnosticsTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/StrictCompileDiagnosticsTest.scala index c573496e85..bf2718dcd5 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/StrictCompileDiagnosticsTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/StrictCompileDiagnosticsTest.scala @@ -22,11 +22,13 @@ class StrictCompileDiagnosticsTest extends InterpreterTest { the[InterpreterException] thrownBy eval(code) should have message "Compilation aborted due to errors." - val _ :: errors = consumeOut - errors.toSet shouldEqual Set( + val errors = consumeOut + errors.filterNot(_.contains("Compiler encountered")).toSet shouldEqual Set( "Test[2:9-2:10]: Parentheses can't be empty.", "Test[3:5-3:9]: Variable x is being redefined.", - "Test[4:9-4:9]: Unrecognized token." + "Test[4:9-4:9]: Unrecognized token.", + "Test[4:5-4:5]: Unused variable y.", + "Test[2:5-2:5]: Unused variable x." ) } }