mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 10:42:05 +03:00
Implement demand analysis (#658)
This commit is contained in:
parent
44e5341278
commit
16b24d58e7
@ -1,3 +1,18 @@
|
||||
# Enso Documentation
|
||||
This folder contains the documentation for the implementation of the Enso
|
||||
programming language.
|
||||
programming language. The documentation is generally broken up into three
|
||||
categories: design, specification, and implementation.
|
||||
|
||||
It is broken up into categories as follows:
|
||||
|
||||
- **[Language Server](language-server/):** Documentation pertaining to the Enso
|
||||
language server.
|
||||
- **[Runtime](runtime/):** Documentation pertaining to the Enso runtime.
|
||||
- **[Semantics](semantics/):** Documentation pertaining to the Enso language
|
||||
semantics, insofar as it is seperable from the other categories.
|
||||
- **[Style Guides](style-guides/):** Style guides for the various languages with
|
||||
which we work.
|
||||
- **[Syntax](syntax/):** Documentation pertaining to the syntax of Enso.
|
||||
- **[Types](types/):** Documentation pertaining to Enso's type system.
|
||||
- **[Enso Philosophy](enso-philosophy.md):** Information on the design
|
||||
philosophy that underlies the development of Enso.
|
||||
|
@ -352,10 +352,12 @@ You can find out more about the various components that make up Enso by having a
|
||||
read of the design documents listed below. These documents are highly technical,
|
||||
and are not intended to be user-facing documentation.
|
||||
|
||||
- **[Syntax](syntax/syntax.md):** A description of Enso's syntax, with a focus
|
||||
on both the main principles behind its design and its nitty gritty details.
|
||||
- **[The Type System](types/types.md):** A description of Enso's type system,
|
||||
starting at a very high level and slowly getting into more detail as the
|
||||
design evolves.
|
||||
- **[The Runtime](runtime/runtime.md):** A description of the design for the
|
||||
runtime that is evolving in tandem with the runtime's development.
|
||||
- **[Semantics](semantics/):** A description of Enso's semantics, with a focus
|
||||
on both the main principles and the details.
|
||||
- **[Syntax](syntax/):** A description of Enso's syntax, with a focus on both
|
||||
the main principles behind its design and its nitty gritty details.
|
||||
- **[The Type System](types/):** A description of Enso's type system, starting
|
||||
at a very high level and slowly getting into more detail as the design
|
||||
evolves.
|
||||
- **[The Runtime](runtime/):** A description of the design for the runtime that
|
||||
is evolving in tandem with the runtime's development.
|
||||
|
59
doc/runtime/specification/demand-analysis.md
Normal file
59
doc/runtime/specification/demand-analysis.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Demand Analysis
|
||||
Demand analysis is the process of deciding when to compute the value of a
|
||||
suspended computation in a language which supports suspended computation.
|
||||
|
||||
<!-- MarkdownTOC levels="2,3" autolink="true" -->
|
||||
|
||||
- [Determining When To Force](#determining-when-to-force)
|
||||
- [Avoiding Pathological Force-Thunk Chains](#avoiding-pathological-force-thunk-chains)
|
||||
- [The Demand Analysis Algorithm](#the-demand-analysis-algorithm)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
## Determining When To Force
|
||||
As Enso supports dynamic dispatch for methods, we cannot always (even in the
|
||||
presence of a typechecker), statically determine whether or not the function
|
||||
that will eventually be called at any given call site. This means that we have
|
||||
to come up with a more efficient way to handle suspended arguments.
|
||||
|
||||
This is done by making the _function application_ responsible for determining
|
||||
when a value passed to it should be evaluated. It works as follows:
|
||||
|
||||
1. _All_ function arguments are passed suspended.
|
||||
2. The function application is provided with knowledge of its argument
|
||||
declarations.
|
||||
3. As a result, the _application_ is responsible for deciding which arguments
|
||||
should be evaluated up front, and which should be deferred.
|
||||
4. In conjunction with this, the suspended arguments to the application need to
|
||||
be explicitly forced at their use sites in the function body.
|
||||
|
||||
## Avoiding Pathological Force-Thunk Chains
|
||||
The above approach does, when done naively, result in a severe performance
|
||||
pathology when passing suspended arguments into functions also expecting
|
||||
suspended arguments. Without intervention, the suspended argument gets wrapped
|
||||
in a chain of thunks that has a significant performance cost to resolve, when
|
||||
necessary.
|
||||
|
||||
In order to avoid this issue, we do the following:
|
||||
|
||||
1. Where a suspended argument is passed into a function, we pass it _without_
|
||||
explicitly forcing it.
|
||||
2. All other uses of that argument are forced.
|
||||
|
||||
## The Demand Analysis Algorithm
|
||||
The algorithm for performing demand analysis on Enso code is as follows.
|
||||
|
||||
```
|
||||
for (node <- ir):
|
||||
if (node is a usage):
|
||||
if (node is not used in a function application):
|
||||
node = force(node)
|
||||
```
|
||||
|
||||
This, however, is not entirely sufficient to support codegen. At the time of
|
||||
generating truffle code, we want to know whether a given usage in an argument to
|
||||
a function application needs to be wrapped in a suspension or left alone (as is
|
||||
the case for a suspended term passed to a function).
|
||||
|
||||
To this end, we instead explicitly mark the arguments to the application with
|
||||
whether or not they should be suspended during codegen.
|
@ -14,9 +14,9 @@ Enso.
|
||||
<!-- MarkdownTOC levels="2,3" autolink="true" -->
|
||||
|
||||
- [Scoping](#scoping)
|
||||
- [Scoping Rules](#scoping-rules)
|
||||
- [Scoping Rules](#scoping-rules)
|
||||
- [Strict Evaluation](#strict-evaluation)
|
||||
- [Optional Suspension](#optional-suspension)
|
||||
- [Optional Suspension](#optional-suspension)
|
||||
- [Bindings](#bindings)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
@ -82,6 +82,15 @@ optional _suspension_, through the built-in `Suspended` type.
|
||||
- A value of type `Suspended a` may be forced to execute the suspended
|
||||
computation and thereby obtain an `a`.
|
||||
|
||||
This forcing can take place in two ways:
|
||||
|
||||
- The user calls the standard library function `force : Suspended a -> a` on the
|
||||
value.
|
||||
- Automatically, at a site where its evaluation is demanded. The algorithm for
|
||||
this is simple. If a value of type `Suspended a` is provided in a location
|
||||
that expects a value of type `a`, the compiler will insert an implicit call to
|
||||
`force` to produce the `a`.
|
||||
|
||||
> The actionables for this section are:
|
||||
>
|
||||
> - Make this far better specified.
|
||||
|
@ -77,15 +77,15 @@ class RecursionFixtures extends InterpreterRunner {
|
||||
"""
|
||||
|main = n ->
|
||||
| doNTimes = n ~block ->
|
||||
| ~block
|
||||
| ifZero n-1 Unit (doNTimes n-1 ~block)
|
||||
| block
|
||||
| ifZero n-1 Unit (doNTimes n-1 block)
|
||||
|
|
||||
| block =
|
||||
| x = State.get
|
||||
| State.put x+1
|
||||
|
|
||||
| State.put 0
|
||||
| doNTimes n ~block
|
||||
| doNTimes n block
|
||||
| State.get
|
||||
|""".stripMargin
|
||||
val nestedThunkSum = getMain(nestedThunkSumCode)
|
||||
|
@ -9,25 +9,13 @@ import org.enso.compiler.core.IR
|
||||
import org.enso.compiler.core.IR.{Expression, Module}
|
||||
import org.enso.compiler.exception.CompilerError
|
||||
import org.enso.compiler.pass.IRPass
|
||||
import org.enso.compiler.pass.analyse.{
|
||||
AliasAnalysis,
|
||||
ApplicationSaturation,
|
||||
TailCall
|
||||
}
|
||||
import org.enso.compiler.pass.desugar.{
|
||||
GenerateMethodBodies,
|
||||
LiftSpecialOperators,
|
||||
OperatorToFunction
|
||||
}
|
||||
import org.enso.compiler.pass.analyse.{AliasAnalysis, ApplicationSaturation, DemandAnalysis, TailCall}
|
||||
import org.enso.compiler.pass.desugar.{GenerateMethodBodies, LiftSpecialOperators, OperatorToFunction}
|
||||
import org.enso.interpreter.Language
|
||||
import org.enso.interpreter.node.{ExpressionNode => RuntimeExpression}
|
||||
import org.enso.interpreter.runtime.Context
|
||||
import org.enso.interpreter.runtime.error.ModuleDoesNotExistException
|
||||
import org.enso.interpreter.runtime.scope.{
|
||||
LocalScope,
|
||||
ModuleScope,
|
||||
TopLevelScope
|
||||
}
|
||||
import org.enso.interpreter.runtime.scope.{LocalScope, ModuleScope, TopLevelScope}
|
||||
import org.enso.polyglot.LanguageInfo
|
||||
import org.enso.syntax.text.{AST, Parser}
|
||||
|
||||
@ -53,6 +41,7 @@ class Compiler(
|
||||
LiftSpecialOperators,
|
||||
OperatorToFunction,
|
||||
AliasAnalysis,
|
||||
DemandAnalysis,
|
||||
ApplicationSaturation(),
|
||||
TailCall
|
||||
)
|
||||
|
@ -5,7 +5,7 @@ import cats.implicits._
|
||||
import org.enso.compiler.core.IR._
|
||||
import org.enso.compiler.exception.UnhandledEntity
|
||||
import org.enso.interpreter.Constants
|
||||
import org.enso.syntax.text.{AST, Location}
|
||||
import org.enso.syntax.text.AST
|
||||
|
||||
// FIXME [AA] All places where we currently throw a `RuntimeException` should
|
||||
// generate informative and useful nodes in core.
|
||||
@ -129,14 +129,12 @@ object AstToIR {
|
||||
val pathStr = pathSegments.map(_.name).mkString(".")
|
||||
val loc = pathSegments.headOption
|
||||
.flatMap(_.location)
|
||||
.flatMap(
|
||||
locationStart =>
|
||||
pathSegments.lastOption
|
||||
.flatMap(_.location)
|
||||
.flatMap(
|
||||
locationEnd =>
|
||||
Some(locationStart.copy(end = locationEnd.end))
|
||||
)
|
||||
.flatMap(locationStart =>
|
||||
pathSegments.lastOption
|
||||
.flatMap(_.location)
|
||||
.flatMap(locationEnd =>
|
||||
Some(locationStart.copy(end = locationEnd.end))
|
||||
)
|
||||
)
|
||||
|
||||
(pathStr, loc)
|
||||
@ -264,12 +262,11 @@ object AstToIR {
|
||||
Literal.Text(fullString, getIdentifiedLocation(literal))
|
||||
case AST.Literal.Text.Block.Raw(lines, _, _) =>
|
||||
val fullString = lines
|
||||
.map(
|
||||
t =>
|
||||
t.text.collect {
|
||||
case AST.Literal.Text.Segment.Plain(str) => str
|
||||
case AST.Literal.Text.Segment.RawEsc(code) => code.repr
|
||||
}.mkString
|
||||
.map(t =>
|
||||
t.text.collect {
|
||||
case AST.Literal.Text.Segment.Plain(str) => str
|
||||
case AST.Literal.Text.Segment.RawEsc(code) => code.repr
|
||||
}.mkString
|
||||
)
|
||||
.mkString("\n")
|
||||
|
||||
@ -380,11 +377,6 @@ object AstToIR {
|
||||
translateExpression(context),
|
||||
getIdentifiedLocation(callable)
|
||||
)
|
||||
case AstView.ForcedTerm(term) =>
|
||||
Application.Force(
|
||||
translateExpression(term),
|
||||
getIdentifiedLocation(callable)
|
||||
)
|
||||
case AstView.Application(name, args) =>
|
||||
val (validArguments, hasDefaultsSuspended) =
|
||||
calculateDefaultsSuspension(args)
|
||||
|
@ -133,28 +133,6 @@ object AstView {
|
||||
}
|
||||
}
|
||||
|
||||
object ForcedTerm {
|
||||
|
||||
/** Matches a forced term.
|
||||
*
|
||||
* A forced term is one of the form `~t`, where `t` is an arbitrary program
|
||||
* expression. This is temporary syntax and will be removed once we have
|
||||
* the ability to insert these analytically.
|
||||
*
|
||||
* @param ast the structure to try and match on
|
||||
* @return the term being forced
|
||||
*/
|
||||
def unapply(ast: AST): Option[AST] = {
|
||||
ast match {
|
||||
case MaybeParensed(
|
||||
AST.App.Section.Right(AST.Ident.Opr("~"), ast)
|
||||
) =>
|
||||
Some(ast)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ContextAscription {
|
||||
|
||||
/** Matches a usage of the `in` keyword for ascribing a monadic context to
|
||||
|
@ -719,57 +719,81 @@ class IRToTruffle(
|
||||
* `arg`
|
||||
*/
|
||||
def run(arg: IR.CallArgument, position: Int): CallArgument = arg match {
|
||||
case IR.CallArgument.Specified(name, value, _, _) =>
|
||||
case IR.CallArgument.Specified(name, value, _, shouldBeSuspended, _) =>
|
||||
val scopeInfo = arg
|
||||
.getMetadata[AliasAnalysis.Info.Scope.Child]
|
||||
.getOrElse(
|
||||
throw new CompilerError("No scope attached to a call argument.")
|
||||
)
|
||||
val result = value match {
|
||||
case term: IR.Application.Force =>
|
||||
val childScope =
|
||||
scope.createChild(scopeInfo.scope, flattenToParent = true)
|
||||
new ExpressionProcessor(childScope, scopeName).run(term.target)
|
||||
case _ =>
|
||||
val childScope = scope.createChild(scopeInfo.scope)
|
||||
val argumentExpression =
|
||||
new ExpressionProcessor(childScope, scopeName).run(value)
|
||||
|
||||
val argExpressionIsTail = value
|
||||
.getMetadata[TailCall.Metadata]
|
||||
.getOrElse(
|
||||
throw new CompilerError(
|
||||
"Argument with missing tail call information."
|
||||
)
|
||||
)
|
||||
val shouldSuspend =
|
||||
shouldBeSuspended.getOrElse(
|
||||
throw new CompilerError(
|
||||
"Demand analysis information missing from call argument."
|
||||
)
|
||||
)
|
||||
|
||||
argumentExpression.setTail(argExpressionIsTail)
|
||||
val childScope = if (shouldSuspend) {
|
||||
scope.createChild(scopeInfo.scope)
|
||||
} else {
|
||||
// Note [Scope Flattening]
|
||||
scope.createChild(scopeInfo.scope, flattenToParent = true)
|
||||
}
|
||||
val argumentExpression =
|
||||
new ExpressionProcessor(childScope, scopeName).run(value)
|
||||
|
||||
val displayName =
|
||||
s"call_argument<${name.getOrElse(String.valueOf(position))}>"
|
||||
|
||||
val section = value.location
|
||||
.map(loc => source.createSection(loc.start, loc.end))
|
||||
.orNull
|
||||
|
||||
val callTarget = Truffle.getRuntime.createCallTarget(
|
||||
ClosureRootNode.build(
|
||||
language,
|
||||
childScope,
|
||||
moduleScope,
|
||||
argumentExpression,
|
||||
section,
|
||||
displayName
|
||||
val result = if (!shouldSuspend) {
|
||||
argumentExpression
|
||||
} else {
|
||||
val argExpressionIsTail = value
|
||||
.getMetadata[TailCall.Metadata]
|
||||
.getOrElse(
|
||||
throw new CompilerError(
|
||||
"Argument with missing tail call information."
|
||||
)
|
||||
)
|
||||
|
||||
CreateThunkNode.build(callTarget)
|
||||
argumentExpression.setTail(argExpressionIsTail)
|
||||
|
||||
val displayName =
|
||||
s"call_argument<${name.getOrElse(String.valueOf(position))}>"
|
||||
|
||||
val section = value.location
|
||||
.map(loc => source.createSection(loc.start, loc.end))
|
||||
.orNull
|
||||
|
||||
val callTarget = Truffle.getRuntime.createCallTarget(
|
||||
ClosureRootNode.build(
|
||||
language,
|
||||
childScope,
|
||||
moduleScope,
|
||||
argumentExpression,
|
||||
section,
|
||||
displayName
|
||||
)
|
||||
)
|
||||
|
||||
CreateThunkNode.build(callTarget)
|
||||
}
|
||||
|
||||
new CallArgument(name.map(_.name).orNull, result)
|
||||
}
|
||||
}
|
||||
|
||||
/* Note [Scope Flattening]
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* Given that we represent _all_ function arguments as thunks at runtime, we
|
||||
* account for this during alias analysis by allocating new scopes for the
|
||||
* function arguments as they are passed. However, in the case of an argument
|
||||
* that is _already_ suspended, we want to pass this directly. However, we do
|
||||
* not have demand information at the point of alias analysis, and so we have
|
||||
* allocated a new scope for it regardless.
|
||||
*
|
||||
* As a result, we flatten that scope back into the parent during codegen to
|
||||
* work around the differences between the semantic meaning of the language
|
||||
* and the runtime representation of function arguments.
|
||||
*/
|
||||
|
||||
// ==========================================================================
|
||||
// === Definition Argument Processor ========================================
|
||||
// ==========================================================================
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.enso.compiler.core
|
||||
|
||||
import org.enso.compiler.core.IR.{Expression, IdentifiedLocation}
|
||||
import org.enso.compiler.exception.CompilerError
|
||||
import org.enso.syntax.text.ast.Doc
|
||||
import org.enso.syntax.text.{AST, Debug, Location}
|
||||
|
||||
@ -41,6 +42,18 @@ sealed trait IR {
|
||||
this.passData.collectFirst { case data: T => data }
|
||||
}
|
||||
|
||||
// TODO [AA] Use this throughout the passes
|
||||
/** Gets the metadata of the given type from the node, throwing a fatal
|
||||
* compiler error with the specified message if it doesn't exist
|
||||
*
|
||||
* @param message the message to throw on error
|
||||
* @tparam T the type of the metadata to be obtained
|
||||
* @return the requested metadata
|
||||
*/
|
||||
def unsafeGetMetadata[T <: IR.Metadata : ClassTag](message: String): T = {
|
||||
this.getMetadata[T].getOrElse(throw new CompilerError(message))
|
||||
}
|
||||
|
||||
/** The source location that the node corresponds to. */
|
||||
val location: Option[IdentifiedLocation]
|
||||
|
||||
@ -161,7 +174,7 @@ object IR {
|
||||
* module scope
|
||||
*/
|
||||
sealed trait Scope extends IR {
|
||||
override def addMetadata(newData: Metadata): Scope
|
||||
override def addMetadata(newData: Metadata): Scope
|
||||
override def mapExpressions(fn: Expression => Expression): Scope
|
||||
}
|
||||
object Scope {
|
||||
@ -187,7 +200,7 @@ object IR {
|
||||
|
||||
/** A representation of top-level definitions. */
|
||||
sealed trait Definition extends Scope {
|
||||
override def addMetadata(newData: Metadata): Definition
|
||||
override def addMetadata(newData: Metadata): Definition
|
||||
override def mapExpressions(fn: Expression => Expression): Definition
|
||||
}
|
||||
object Definition {
|
||||
@ -274,7 +287,7 @@ object IR {
|
||||
}
|
||||
|
||||
override def mapExpressions(fn: Expression => Expression): Expression
|
||||
override def addMetadata(newData: Metadata): Expression
|
||||
override def addMetadata(newData: Metadata): Expression
|
||||
}
|
||||
object Expression {
|
||||
|
||||
@ -336,7 +349,7 @@ object IR {
|
||||
/** Enso literals. */
|
||||
sealed trait Literal extends Expression with IRKind.Primitive {
|
||||
override def mapExpressions(fn: Expression => Expression): Literal
|
||||
override def addMetadata(newData: Metadata): Literal
|
||||
override def addMetadata(newData: Metadata): Literal
|
||||
}
|
||||
object Literal {
|
||||
|
||||
@ -384,7 +397,7 @@ object IR {
|
||||
val name: String
|
||||
|
||||
override def mapExpressions(fn: Expression => Expression): Name
|
||||
override def addMetadata(newData: Metadata): Name
|
||||
override def addMetadata(newData: Metadata): Name
|
||||
}
|
||||
object Name {
|
||||
|
||||
@ -449,7 +462,7 @@ object IR {
|
||||
/** Constructs that operate on types. */
|
||||
sealed trait Type extends Expression {
|
||||
override def mapExpressions(fn: Expression => Expression): Type
|
||||
override def addMetadata(newData: Metadata): Type
|
||||
override def addMetadata(newData: Metadata): Type
|
||||
}
|
||||
object Type {
|
||||
|
||||
@ -514,7 +527,7 @@ object IR {
|
||||
/** IR nodes for dealing with typesets. */
|
||||
sealed trait Set extends Type {
|
||||
override def mapExpressions(fn: Expression => Expression): Set
|
||||
override def addMetadata(newData: Metadata): Set
|
||||
override def addMetadata(newData: Metadata): Set
|
||||
}
|
||||
object Set {
|
||||
|
||||
@ -737,7 +750,7 @@ object IR {
|
||||
val canBeTCO: Boolean
|
||||
|
||||
override def mapExpressions(fn: Expression => Expression): Function
|
||||
override def addMetadata(newData: Metadata): Function
|
||||
override def addMetadata(newData: Metadata): Function
|
||||
}
|
||||
object Function {
|
||||
|
||||
@ -822,7 +835,7 @@ object IR {
|
||||
/** All function applications in Enso. */
|
||||
sealed trait Application extends Expression {
|
||||
override def mapExpressions(fn: Expression => Expression): Application
|
||||
override def addMetadata(newData: Metadata): Application
|
||||
override def addMetadata(newData: Metadata): Application
|
||||
}
|
||||
object Application {
|
||||
|
||||
@ -876,7 +889,7 @@ object IR {
|
||||
/** Operator applications in Enso. */
|
||||
sealed trait Operator extends Application {
|
||||
override def mapExpressions(fn: Expression => Expression): Operator
|
||||
override def addMetadata(newData: Metadata): Operator
|
||||
override def addMetadata(newData: Metadata): Operator
|
||||
}
|
||||
object Operator {
|
||||
|
||||
@ -917,8 +930,17 @@ object IR {
|
||||
/** The name of the argument, if present. */
|
||||
val name: Option[IR.Name]
|
||||
|
||||
/** Whether or not the argument should be suspended at code generation time.
|
||||
*
|
||||
* A value of `Some(true)` implies that code generation should suspend the
|
||||
* argument. A value of `Some(false)` implies that code generation should
|
||||
* _not_ suspend the argument. A value of [[None]] implies that the
|
||||
* information is missing.
|
||||
*/
|
||||
val shouldBeSuspended: Option[Boolean]
|
||||
|
||||
override def mapExpressions(fn: Expression => Expression): CallArgument
|
||||
override def addMetadata(newData: Metadata): CallArgument
|
||||
override def addMetadata(newData: Metadata): CallArgument
|
||||
}
|
||||
object CallArgument {
|
||||
|
||||
@ -927,13 +949,16 @@ object IR {
|
||||
* @param name the name of the argument being called, if present
|
||||
* @param value the expression being passed as the argument's value
|
||||
* @param location the source location that the node corresponds to
|
||||
* @param shouldBeSuspended whether or not the argument should be passed
|
||||
* suspended
|
||||
* @param passData the pass metadata associated with this node
|
||||
*/
|
||||
sealed case class Specified(
|
||||
override val name: Option[IR.Name],
|
||||
value: Expression,
|
||||
override val location: Option[IdentifiedLocation],
|
||||
override val passData: ISet[Metadata] = ISet()
|
||||
override val shouldBeSuspended: Option[Boolean] = None,
|
||||
override val passData: ISet[Metadata] = ISet()
|
||||
) extends CallArgument
|
||||
with IRKind.Primitive {
|
||||
override def addMetadata(newData: Metadata): Specified = {
|
||||
@ -954,7 +979,7 @@ object IR {
|
||||
/** The Enso case expression. */
|
||||
sealed trait Case extends Expression {
|
||||
override def mapExpressions(fn: Expression => Expression): Case
|
||||
override def addMetadata(newData: Metadata): Case
|
||||
override def addMetadata(newData: Metadata): Case
|
||||
}
|
||||
object Case {
|
||||
|
||||
@ -1013,7 +1038,7 @@ object IR {
|
||||
/** The different types of patterns that can occur in a match. */
|
||||
sealed trait Pattern extends IR {
|
||||
override def mapExpressions(fn: Expression => Expression): Pattern
|
||||
override def addMetadata(newData: Metadata): Pattern
|
||||
override def addMetadata(newData: Metadata): Pattern
|
||||
}
|
||||
object Pattern {
|
||||
// TODO [AA] Better differentiate the types of patterns that can occur
|
||||
@ -1025,7 +1050,7 @@ object IR {
|
||||
/** Enso comment entities. */
|
||||
sealed trait Comment extends Expression {
|
||||
override def mapExpressions(fn: Expression => Expression): Comment
|
||||
override def addMetadata(newData: Metadata): Comment
|
||||
override def addMetadata(newData: Metadata): Comment
|
||||
|
||||
/** The expression being commented. */
|
||||
val commented: Expression
|
||||
@ -1063,7 +1088,7 @@ object IR {
|
||||
/** Foreign code entities. */
|
||||
sealed trait Foreign extends Expression {
|
||||
override def mapExpressions(fn: Expression => Expression): Foreign
|
||||
override def addMetadata(newData: Metadata): Foreign
|
||||
override def addMetadata(newData: Metadata): Foreign
|
||||
}
|
||||
object Foreign {
|
||||
|
||||
@ -1095,7 +1120,7 @@ object IR {
|
||||
/** A trait for all errors in Enso's IR. */
|
||||
sealed trait Error extends Expression {
|
||||
override def mapExpressions(fn: Expression => Expression): Error
|
||||
override def addMetadata(newData: Metadata): Error
|
||||
override def addMetadata(newData: Metadata): Error
|
||||
}
|
||||
object Error {
|
||||
|
||||
|
@ -151,13 +151,12 @@ case object AliasAnalysis extends IRPass {
|
||||
|
||||
block
|
||||
.copy(
|
||||
expressions = expressions.map(
|
||||
(expression: IR.Expression) =>
|
||||
analyseExpression(
|
||||
expression,
|
||||
graph,
|
||||
currentScope
|
||||
)
|
||||
expressions = expressions.map((expression: IR.Expression) =>
|
||||
analyseExpression(
|
||||
expression,
|
||||
graph,
|
||||
currentScope
|
||||
)
|
||||
),
|
||||
returnValue = analyseExpression(
|
||||
retVal,
|
||||
@ -168,8 +167,10 @@ case object AliasAnalysis extends IRPass {
|
||||
.addMetadata(Info.Scope.Child(graph, currentScope))
|
||||
case binding @ IR.Expression.Binding(name, expression, _, _) =>
|
||||
if (!parentScope.hasSymbolOccurrenceAs[Occurrence.Def](name.name)) {
|
||||
val isSuspended = expression.isInstanceOf[IR.Expression.Block]
|
||||
val occurrenceId = graph.nextId()
|
||||
val occurrence = Occurrence.Def(occurrenceId, name.name)
|
||||
val occurrence =
|
||||
Occurrence.Def(occurrenceId, name.name, isSuspended)
|
||||
|
||||
parentScope.add(occurrence)
|
||||
|
||||
@ -188,13 +189,12 @@ case object AliasAnalysis extends IRPass {
|
||||
case app: IR.Application =>
|
||||
analyseApplication(app, graph, parentScope)
|
||||
case x =>
|
||||
x.mapExpressions(
|
||||
(expression: IR.Expression) =>
|
||||
analyseExpression(
|
||||
expression,
|
||||
graph,
|
||||
parentScope
|
||||
)
|
||||
x.mapExpressions((expression: IR.Expression) =>
|
||||
analyseExpression(
|
||||
expression,
|
||||
graph,
|
||||
parentScope
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -225,22 +225,21 @@ case object AliasAnalysis extends IRPass {
|
||||
scope: Scope
|
||||
): List[IR.DefinitionArgument] = {
|
||||
args.map {
|
||||
case arg @ IR.DefinitionArgument.Specified(name, value, _, _, _) =>
|
||||
case arg @ IR.DefinitionArgument.Specified(name, value, isSusp, _, _) =>
|
||||
val nameOccursInScope =
|
||||
scope.hasSymbolOccurrenceAs[Occurrence.Def](name.name)
|
||||
if (!nameOccursInScope) {
|
||||
val occurrenceId = graph.nextId()
|
||||
scope.add(Graph.Occurrence.Def(occurrenceId, name.name))
|
||||
scope.add(Graph.Occurrence.Def(occurrenceId, name.name, isSusp))
|
||||
|
||||
arg
|
||||
.copy(
|
||||
defaultValue = value.map(
|
||||
(ir: IR.Expression) =>
|
||||
analyseExpression(
|
||||
ir,
|
||||
graph,
|
||||
scope
|
||||
)
|
||||
defaultValue = value.map((ir: IR.Expression) =>
|
||||
analyseExpression(
|
||||
ir,
|
||||
graph,
|
||||
scope
|
||||
)
|
||||
)
|
||||
)
|
||||
.addMetadata(Info.Occurrence(graph, occurrenceId))
|
||||
@ -292,7 +291,7 @@ case object AliasAnalysis extends IRPass {
|
||||
parentScope: AliasAnalysis.Graph.Scope
|
||||
): List[IR.CallArgument] = {
|
||||
args.map {
|
||||
case arg @ IR.CallArgument.Specified(_, expr, _, _) =>
|
||||
case arg @ IR.CallArgument.Specified(_, expr, _, _, _) =>
|
||||
val currentScope = expr match {
|
||||
case _: IR.Literal => parentScope
|
||||
case _ => parentScope.addChild()
|
||||
@ -375,13 +374,12 @@ case object AliasAnalysis extends IRPass {
|
||||
case caseExpr @ IR.Case.Expr(scrutinee, branches, fallback, _, _) =>
|
||||
caseExpr.copy(
|
||||
scrutinee = analyseExpression(scrutinee, graph, parentScope),
|
||||
branches = branches.map(
|
||||
branch =>
|
||||
branch.copy(
|
||||
pattern = analyseExpression(branch.pattern, graph, parentScope),
|
||||
expression =
|
||||
analyseExpression(branch.expression, graph, parentScope)
|
||||
)
|
||||
branches = branches.map(branch =>
|
||||
branch.copy(
|
||||
pattern = analyseExpression(branch.pattern, graph, parentScope),
|
||||
expression =
|
||||
analyseExpression(branch.expression, graph, parentScope)
|
||||
)
|
||||
),
|
||||
fallback = fallback.map(analyseExpression(_, graph, parentScope))
|
||||
) //.addMetadata(Info.Scope.Child(graph, currentScope))
|
||||
@ -495,8 +493,8 @@ case object AliasAnalysis extends IRPass {
|
||||
): Set[Graph.Link] = {
|
||||
val idsForSym = rootScope.symbolToIds[T](symbol)
|
||||
|
||||
links.filter(
|
||||
l => idsForSym.contains(l.source) || idsForSym.contains(l.target)
|
||||
links.filter(l =>
|
||||
idsForSym.contains(l.source) || idsForSym.contains(l.target)
|
||||
)
|
||||
}
|
||||
|
||||
@ -518,8 +516,8 @@ case object AliasAnalysis extends IRPass {
|
||||
linksFor(id).find { edge =>
|
||||
val occ = getOccurrence(edge.target)
|
||||
occ match {
|
||||
case Some(Occurrence.Def(_, _)) => true
|
||||
case _ => false
|
||||
case Some(Occurrence.Def(_, _, _)) => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -730,8 +728,8 @@ case object AliasAnalysis extends IRPass {
|
||||
parentCounter: Int = 0
|
||||
): Option[Graph.Link] = {
|
||||
val definition = occurrences.find {
|
||||
case Graph.Occurrence.Def(_, n) => n == occurrence.symbol
|
||||
case _ => false
|
||||
case Graph.Occurrence.Def(_, n, _) => n == occurrence.symbol
|
||||
case _ => false
|
||||
}
|
||||
|
||||
definition match {
|
||||
@ -900,7 +898,11 @@ case object AliasAnalysis extends IRPass {
|
||||
* @param id the identifier of the name in the graph
|
||||
* @param symbol the text of the name
|
||||
*/
|
||||
sealed case class Def(id: Id, symbol: Graph.Symbol) extends Occurrence
|
||||
sealed case class Def(
|
||||
id: Id,
|
||||
symbol: Graph.Symbol,
|
||||
isLazy: Boolean = false
|
||||
) extends Occurrence
|
||||
|
||||
/** A usage of a symbol in the aliasing graph
|
||||
*
|
||||
|
@ -0,0 +1,345 @@
|
||||
package org.enso.compiler.pass.analyse
|
||||
|
||||
import org.enso.compiler.InlineContext
|
||||
import org.enso.compiler.core.IR
|
||||
import org.enso.compiler.exception.CompilerError
|
||||
import org.enso.compiler.pass.IRPass
|
||||
|
||||
/** This pass implements demand analysis for Enso.
|
||||
*
|
||||
* Demand analysis is the process of determining _when_ a suspended term needs
|
||||
* to be forced (where the suspended value is _demanded_). It does the
|
||||
* following:
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
case object DemandAnalysis extends IRPass {
|
||||
override type Metadata = IR.Metadata.Empty
|
||||
|
||||
/** Executes the demand analysis process on an Enso module.
|
||||
*
|
||||
* @param ir the Enso IR to process
|
||||
* @return `ir`, transformed to correctly force terms
|
||||
*/
|
||||
override def runModule(ir: IR.Module): IR.Module = {
|
||||
ir.copy(bindings =
|
||||
ir.bindings.map(t => t.mapExpressions(runExpression(_, InlineContext())))
|
||||
)
|
||||
}
|
||||
|
||||
/** Executes the demand analysis process on an Enso expression.
|
||||
*
|
||||
* @param expression the Enso IR to process
|
||||
* @param inlineContext a context object that contains the information needed
|
||||
* for inline evaluation
|
||||
* @return `ir`, transformed to correctly force terms
|
||||
*/
|
||||
override def runExpression(
|
||||
expression: IR.Expression,
|
||||
inlineContext: InlineContext
|
||||
): IR.Expression =
|
||||
analyseExpression(
|
||||
expression,
|
||||
isInsideApplication = false,
|
||||
isInsideCallArgument = false
|
||||
)
|
||||
|
||||
/** Performs demand analysis on an arbitrary program expression.
|
||||
*
|
||||
* @param expression the expression to perform demand analysis on
|
||||
* @param isInsideApplication whether the current expression occurs _inside_
|
||||
* an application (note that this should not be
|
||||
* set for the application itself)
|
||||
* @param isInsideCallArgument whether the current expression occurs _inside_
|
||||
* a call argument (note that this should not be
|
||||
* set for the call argument itself)
|
||||
* @return `expression`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseExpression(
|
||||
expression: IR.Expression,
|
||||
isInsideApplication: Boolean,
|
||||
isInsideCallArgument: Boolean
|
||||
): IR.Expression = {
|
||||
expression match {
|
||||
case fn: IR.Function => analyseFunction(fn, isInsideApplication)
|
||||
case name: IR.Name => analyseName(name, isInsideCallArgument)
|
||||
case app: IR.Application =>
|
||||
analyseApplication(app, isInsideApplication, isInsideCallArgument)
|
||||
case typ: IR.Type =>
|
||||
analyseType(typ, isInsideApplication, isInsideCallArgument)
|
||||
case cse: IR.Case =>
|
||||
analyseCase(cse, isInsideApplication, isInsideCallArgument)
|
||||
case block @ IR.Expression.Block(expressions, retVal, _, _, _) =>
|
||||
block.copy(
|
||||
expressions = expressions.map(x =>
|
||||
analyseExpression(x, isInsideApplication, isInsideCallArgument)
|
||||
),
|
||||
returnValue =
|
||||
analyseExpression(retVal, isInsideApplication, isInsideCallArgument)
|
||||
)
|
||||
case binding @ IR.Expression.Binding(_, expression, _, _) =>
|
||||
binding.copy(expression =
|
||||
analyseExpression(
|
||||
expression,
|
||||
isInsideApplication,
|
||||
isInsideCallArgument = false
|
||||
)
|
||||
)
|
||||
case lit: IR.Literal => lit
|
||||
case err: IR.Error => err
|
||||
case foreign: IR.Foreign => foreign
|
||||
case comment: IR.Comment =>
|
||||
comment.mapExpressions(x =>
|
||||
analyseExpression(
|
||||
x,
|
||||
isInsideApplication = false,
|
||||
isInsideCallArgument
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Performs demand analysis for a function.
|
||||
*
|
||||
* @param function the function to perform demand analysis on
|
||||
* @param isInsideApplication whether or not the function occurs inside an
|
||||
* application
|
||||
* @return `function`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseFunction(
|
||||
function: IR.Function,
|
||||
isInsideApplication: Boolean
|
||||
): IR.Function = function match {
|
||||
case lam @ IR.Function.Lambda(args, body, _, _, _) =>
|
||||
lam.copy(
|
||||
arguments = args.map(analyseDefinitionArgument),
|
||||
body = analyseExpression(
|
||||
body,
|
||||
isInsideApplication,
|
||||
isInsideCallArgument = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** Performs demand analysis for a name.
|
||||
*
|
||||
* If the name refers to a term that is suspended, this name is forced unless
|
||||
* it is being passed to a function. If the name is being passed to a function
|
||||
* it is passed raw.
|
||||
*
|
||||
* @param name the name to perform demand analysis on.
|
||||
* @param isInsideCallArgument whether or not the name occurs inside a call
|
||||
* call argument
|
||||
* @return `name`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseName(
|
||||
name: IR.Name,
|
||||
isInsideCallArgument: Boolean
|
||||
): IR.Expression = {
|
||||
val usesLazyTerm = isUsageOfSuspendedTerm(name)
|
||||
|
||||
if (isInsideCallArgument) {
|
||||
name
|
||||
} else {
|
||||
if (usesLazyTerm) {
|
||||
val forceLocation = name.location
|
||||
val newNameLocation = name.location.map(l => l.copy(id = None))
|
||||
|
||||
val newName = name match {
|
||||
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)
|
||||
}
|
||||
|
||||
IR.Application.Force(newName, forceLocation)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Performs demand analysis on an application.
|
||||
*
|
||||
* @param application the function application to perform demand analysis on
|
||||
* @param isInsideApplication whether or not the application is occuring
|
||||
* inside another application
|
||||
* @param isInsideCallArgument whether or not the application is occurring
|
||||
* inside a call argument
|
||||
* @return `application`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseApplication(
|
||||
application: IR.Application,
|
||||
isInsideApplication: Boolean,
|
||||
isInsideCallArgument: Boolean
|
||||
): IR.Application = application match {
|
||||
case pref @ IR.Application.Prefix(fn, args, _, _, _) =>
|
||||
pref.copy(
|
||||
function = analyseExpression(
|
||||
fn,
|
||||
isInsideApplication = true,
|
||||
isInsideCallArgument = false
|
||||
),
|
||||
arguments = args.map(analyseCallArgument)
|
||||
)
|
||||
case force @ IR.Application.Force(target, _, _) =>
|
||||
force.copy(target =
|
||||
analyseExpression(
|
||||
target,
|
||||
isInsideApplication,
|
||||
isInsideCallArgument
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
throw new CompilerError(
|
||||
"Unexpected application type during demand analysis."
|
||||
)
|
||||
}
|
||||
|
||||
/** Determines whether a particular piece of IR represents the usage of a
|
||||
* suspended term (and hence requires forcing).
|
||||
*
|
||||
* @param expr the expression to check
|
||||
* @return `true` if `expr` represents the usage of a suspended term, `false`
|
||||
* otherwise
|
||||
*/
|
||||
def isUsageOfSuspendedTerm(expr: IR.Expression): Boolean = {
|
||||
expr match {
|
||||
case name: IR.Name =>
|
||||
val aliasInfo = name.unsafeGetMetadata[AliasAnalysis.Info.Occurrence](
|
||||
"Missing alias occurrence information for a name usage"
|
||||
)
|
||||
|
||||
aliasInfo.graph
|
||||
.defLinkFor(aliasInfo.id)
|
||||
.flatMap(link => {
|
||||
aliasInfo.graph
|
||||
.getOccurrence(link.target)
|
||||
.getOrElse(
|
||||
throw new CompilerError(
|
||||
s"Malformed aliasing link with target ${link.target}"
|
||||
)
|
||||
) match {
|
||||
case AliasAnalysis.Graph.Occurrence.Def(_, _, isLazy) =>
|
||||
if (isLazy) Some(true) else None
|
||||
case _ => None
|
||||
}
|
||||
})
|
||||
.isDefined
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
/** Performs demand analysis on a function call argument.
|
||||
*
|
||||
* In keeping with the requirement by the runtime to pass all function
|
||||
* arguments as thunks, we mark the argument as needing suspension based on
|
||||
* whether it already is a thunk or not.
|
||||
*
|
||||
* @param arg the argument to perform demand analysis on
|
||||
* @return `arg`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseCallArgument(arg: IR.CallArgument): IR.CallArgument = {
|
||||
arg match {
|
||||
case spec @ IR.CallArgument.Specified(_, expr, _, _, _) =>
|
||||
spec.copy(
|
||||
value = analyseExpression(
|
||||
expr,
|
||||
isInsideApplication = true,
|
||||
isInsideCallArgument = true
|
||||
),
|
||||
shouldBeSuspended = Some(!isUsageOfSuspendedTerm(expr))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Performs demand analysis on a function definition argument.
|
||||
*
|
||||
* @param arg the argument to perform demand analysis on
|
||||
* @return `arg`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseDefinitionArgument(
|
||||
arg: IR.DefinitionArgument
|
||||
): IR.DefinitionArgument = {
|
||||
arg match {
|
||||
case spec @ IR.DefinitionArgument.Specified(_, default, _, _, _) =>
|
||||
spec.copy(
|
||||
defaultValue = default.map(x =>
|
||||
analyseExpression(
|
||||
x,
|
||||
isInsideApplication = false,
|
||||
isInsideCallArgument = false
|
||||
)
|
||||
)
|
||||
)
|
||||
case redef: IR.Error.Redefined.Argument => redef
|
||||
}
|
||||
}
|
||||
|
||||
/** Performs demand analysis on a typing expression.
|
||||
*
|
||||
* @param typ the expression to perform demand analysis on
|
||||
* @param isInsideApplication whether the typing expression occurs inside a
|
||||
* function application
|
||||
* @param isInsideCallArgument whether the typing expression occurs inside a
|
||||
* function call argument
|
||||
* @return `typ`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseType(
|
||||
typ: IR.Type,
|
||||
isInsideApplication: Boolean,
|
||||
isInsideCallArgument: Boolean
|
||||
): IR.Type =
|
||||
typ.mapExpressions(x =>
|
||||
analyseExpression(x, isInsideApplication, isInsideCallArgument)
|
||||
)
|
||||
|
||||
/** Performs demand analysis on a case expression.
|
||||
*
|
||||
* @param cse the case expression to perform demand analysis on
|
||||
* @param isInsideApplication whether the case expression occurs inside a
|
||||
* function application
|
||||
* @param isInsideCallArgument whether the case expression occurs inside a
|
||||
* function call argument
|
||||
* @return `cse`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseCase(
|
||||
cse: IR.Case,
|
||||
isInsideApplication: Boolean,
|
||||
isInsideCallArgument: Boolean
|
||||
): IR.Case = cse match {
|
||||
case expr @ IR.Case.Expr(scrutinee, branches, fallback, _, _) =>
|
||||
expr.copy(
|
||||
scrutinee = analyseExpression(
|
||||
scrutinee,
|
||||
isInsideApplication,
|
||||
isInsideCallArgument
|
||||
),
|
||||
branches = branches.map(b => analyseCaseBranch(b)),
|
||||
fallback = fallback.map(x =>
|
||||
analyseExpression(
|
||||
x,
|
||||
isInsideApplication = false,
|
||||
isInsideCallArgument = false
|
||||
)
|
||||
)
|
||||
)
|
||||
case _ => throw new CompilerError("Unexpected case construct.")
|
||||
}
|
||||
|
||||
/** Performs demand analysis on a case branch.
|
||||
*
|
||||
* @param branch the case branch to perform demand analysis on
|
||||
* @return `branch`, transformed by the demand analysis process
|
||||
*/
|
||||
def analyseCaseBranch(branch: IR.Case.Branch): IR.Case.Branch = {
|
||||
branch.copy(
|
||||
expression = analyseExpression(
|
||||
branch.expression,
|
||||
isInsideApplication = false,
|
||||
isInsideCallArgument = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -189,7 +189,7 @@ case object TailCall extends IRPass {
|
||||
*/
|
||||
def analyseCallArg(argument: IR.CallArgument): IR.CallArgument = {
|
||||
argument match {
|
||||
case arg @ IR.CallArgument.Specified(_, expr, _, _) =>
|
||||
case arg @ IR.CallArgument.Specified(_, expr, _, _, _) =>
|
||||
arg
|
||||
.copy(
|
||||
// Note [Call Argument Tail Position]
|
||||
|
@ -96,18 +96,50 @@ trait CompilerRunner {
|
||||
inlineContext: InlineContext
|
||||
): IR = ir match {
|
||||
case expr: IR.Expression =>
|
||||
passes.foldLeft(expr)(
|
||||
(intermediate, pass) =>
|
||||
pass.runExpression(intermediate, inlineContext)
|
||||
passes.foldLeft(expr)((intermediate, pass) =>
|
||||
pass.runExpression(intermediate, inlineContext)
|
||||
)
|
||||
case mod: IR.Module =>
|
||||
passes.foldLeft(mod)(
|
||||
(intermediate, pass) => pass.runModule(intermediate)
|
||||
passes.foldLeft(mod)((intermediate, pass) =>
|
||||
pass.runModule(intermediate)
|
||||
)
|
||||
case _ => throw new RuntimeException(s"Cannot run passes on $ir.")
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds an extension method to preprocess the source as IR.
|
||||
*
|
||||
* @param source the source code to preprocess
|
||||
*/
|
||||
implicit class Preprocess(source: String)(
|
||||
implicit precursorPasses: List[IRPass]
|
||||
) {
|
||||
|
||||
/** Translates the source code into appropriate IR for testing this pass.
|
||||
*
|
||||
* @return IR appropriate for testing the alias analysis pass as a module
|
||||
*/
|
||||
def preprocessModule: IR.Module = {
|
||||
source.toIrModule
|
||||
.runPasses(precursorPasses, InlineContext())
|
||||
.asInstanceOf[IR.Module]
|
||||
}
|
||||
|
||||
/** Translates the source code into appropriate IR for testing this pass
|
||||
*
|
||||
* @return IR appropriate for testing the alias analysis pass as an
|
||||
* expression
|
||||
*/
|
||||
def preprocessExpression(
|
||||
implicit inlineContext: InlineContext
|
||||
): Option[IR.Expression] = {
|
||||
source.toIrExpression.map(
|
||||
_.runPasses(precursorPasses, inlineContext)
|
||||
.asInstanceOf[IR.Expression]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// === IR Testing Utils =====================================================
|
||||
|
||||
/** A variety of extension methods on IR expressions to aid testing.
|
||||
|
@ -18,41 +18,12 @@ class AliasAnalysisTest extends CompilerTest {
|
||||
|
||||
// === Utilities ============================================================
|
||||
|
||||
/** Adds an extension method to preprocess the source as IR.
|
||||
*
|
||||
* @param source the source code to preprocess
|
||||
*/
|
||||
implicit class Preprocess(source: String) {
|
||||
val precursorPasses: List[IRPass] = List(
|
||||
GenerateMethodBodies,
|
||||
LiftSpecialOperators,
|
||||
OperatorToFunction
|
||||
)
|
||||
|
||||
/** Translates the source code into appropriate IR for testing this pass.
|
||||
*
|
||||
* @return IR appropriate for testing the alias analysis pass as a module
|
||||
*/
|
||||
def preprocessModule: IR.Module = {
|
||||
source.toIrModule
|
||||
.runPasses(precursorPasses, InlineContext())
|
||||
.asInstanceOf[IR.Module]
|
||||
}
|
||||
|
||||
/** Translates the source code into appropriate IR for testing this pass
|
||||
*
|
||||
* @return IR appropriate for testing the alias analysis pass as an
|
||||
* expression
|
||||
*/
|
||||
def preprocessExpression(
|
||||
inlineContext: InlineContext
|
||||
): Option[IR.Expression] = {
|
||||
source.toIrExpression.map(
|
||||
_.runPasses(precursorPasses, inlineContext)
|
||||
.asInstanceOf[IR.Expression]
|
||||
)
|
||||
}
|
||||
}
|
||||
/** The passes that need to be run before the alias analysis pass. */
|
||||
implicit val precursorPasses: List[IRPass] = List(
|
||||
GenerateMethodBodies,
|
||||
LiftSpecialOperators,
|
||||
OperatorToFunction
|
||||
)
|
||||
|
||||
/** Adds an extension method to run alias analysis on an [[IR.Module]].
|
||||
*
|
||||
@ -69,7 +40,7 @@ class AliasAnalysisTest extends CompilerTest {
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds an extension method to run alias analusis on an [[IR.Expression]].
|
||||
/** Adds an extension method to run alias analysis on an [[IR.Expression]].
|
||||
*
|
||||
* @param ir the expression to run alias analysis on
|
||||
*/
|
||||
@ -77,6 +48,8 @@ class AliasAnalysisTest extends CompilerTest {
|
||||
|
||||
/** Runs alias analysis on an expression.
|
||||
*
|
||||
* @param inlineContext the inline context in which to process the
|
||||
* expression
|
||||
* @return [[ir]], with attached aliasing information
|
||||
*/
|
||||
def analyse(inlineContext: InlineContext): IR.Expression = {
|
||||
@ -515,8 +488,8 @@ class AliasAnalysisTest extends CompilerTest {
|
||||
}
|
||||
|
||||
"assign Info.Occurrence to definitions and usages of symbols" in {
|
||||
topLambda.arguments.foreach(
|
||||
arg => arg.getMetadata[Info.Occurrence] shouldBe defined
|
||||
topLambda.arguments.foreach(arg =>
|
||||
arg.getMetadata[Info.Occurrence] shouldBe defined
|
||||
)
|
||||
|
||||
topLambdaBody.expressions.foreach(
|
||||
@ -524,8 +497,8 @@ class AliasAnalysisTest extends CompilerTest {
|
||||
.getMetadata[Info.Occurrence] shouldBe defined
|
||||
)
|
||||
|
||||
childLambda.arguments.foreach(
|
||||
arg => arg.getMetadata[Info.Occurrence] shouldBe defined
|
||||
childLambda.arguments.foreach(arg =>
|
||||
arg.getMetadata[Info.Occurrence] shouldBe defined
|
||||
)
|
||||
|
||||
childLambdaBody.function.getMetadata[Info.Occurrence] shouldBe defined
|
||||
|
@ -0,0 +1,277 @@
|
||||
package org.enso.compiler.test.pass.analyse
|
||||
|
||||
import org.enso.compiler.InlineContext
|
||||
import org.enso.compiler.core.IR
|
||||
import org.enso.compiler.pass.IRPass
|
||||
import org.enso.compiler.pass.analyse.{AliasAnalysis, DemandAnalysis}
|
||||
import org.enso.compiler.pass.desugar.{
|
||||
GenerateMethodBodies,
|
||||
LiftSpecialOperators,
|
||||
OperatorToFunction
|
||||
}
|
||||
import org.enso.compiler.test.CompilerTest
|
||||
import org.enso.interpreter.runtime.scope.LocalScope
|
||||
|
||||
class DemandAnalysisTest extends CompilerTest {
|
||||
|
||||
// === Test Setup ===========================================================
|
||||
|
||||
/** The passes that must be run before the demand analysis pass. */
|
||||
implicit val precursorPasses: List[IRPass] = List(
|
||||
GenerateMethodBodies,
|
||||
LiftSpecialOperators,
|
||||
OperatorToFunction,
|
||||
AliasAnalysis
|
||||
)
|
||||
|
||||
/** Adds an extension method to run alias analysis on an [[IR.Module]].
|
||||
*
|
||||
* @param ir the module to run alias analysis on
|
||||
*/
|
||||
implicit class AnalyseModule(ir: IR.Module) {
|
||||
|
||||
/** Runs demand analysis on a module.
|
||||
*
|
||||
* @return [[ir]], transformed by the demand analysis pass
|
||||
*/
|
||||
def analyse: IR.Module = {
|
||||
DemandAnalysis.runModule(ir)
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds an extension method to run alias analysis on an [[IR.Expression]].
|
||||
*
|
||||
* @param ir the expression to run alias analysis on
|
||||
*/
|
||||
implicit class AnalyseExpression(ir: IR.Expression) {
|
||||
|
||||
/** Runs demand analysis on an expression.
|
||||
*
|
||||
* @param inlineContext the inline context in which to process the
|
||||
* expression
|
||||
* @return [[ir]], transformed by the demand analysis pass
|
||||
*/
|
||||
def analyse(implicit inlineContext: InlineContext): IR.Expression = {
|
||||
DemandAnalysis.runExpression(ir, inlineContext)
|
||||
}
|
||||
}
|
||||
|
||||
// === The Tests ============================================================
|
||||
|
||||
"Suspended arguments" should {
|
||||
"be forced when assigned" in {
|
||||
implicit val ctx: InlineContext =
|
||||
InlineContext(localScope = Some(LocalScope.root))
|
||||
|
||||
val ir =
|
||||
"""
|
||||
|~x ~y z ->
|
||||
| a = x
|
||||
| z
|
||||
|""".stripMargin.preprocessExpression.get.analyse
|
||||
|
||||
val boundX = ir
|
||||
.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Expression.Block]
|
||||
.expressions
|
||||
.head
|
||||
.asInstanceOf[IR.Expression.Binding]
|
||||
.expression
|
||||
|
||||
boundX shouldBe an[IR.Application.Force]
|
||||
boundX.asInstanceOf[IR.Application.Force].target shouldBe an[IR.Name]
|
||||
}
|
||||
|
||||
"work correctly when deeply nested" in {
|
||||
implicit val ctx: InlineContext =
|
||||
InlineContext(localScope = Some(LocalScope.root))
|
||||
|
||||
val ir =
|
||||
"""
|
||||
|~x -> b -> a -> x
|
||||
|""".stripMargin.preprocessExpression.get.analyse
|
||||
|
||||
val xUsage =
|
||||
ir.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
|
||||
xUsage shouldBe an[IR.Application.Force]
|
||||
xUsage.asInstanceOf[IR.Application.Force].target shouldBe an[IR.Name]
|
||||
}
|
||||
|
||||
"not be forced when passed to functions" in {
|
||||
implicit val ctx: InlineContext =
|
||||
InlineContext(localScope = Some(LocalScope.root))
|
||||
|
||||
val ir =
|
||||
"""
|
||||
|~x ~y z -> foo x y z
|
||||
|""".stripMargin.preprocessExpression.get.analyse
|
||||
|
||||
val app = ir
|
||||
.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Application.Prefix]
|
||||
|
||||
app.arguments.head
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
.value shouldBe an[IR.Name]
|
||||
|
||||
app
|
||||
.arguments(1)
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
.value shouldBe an[IR.Name]
|
||||
}
|
||||
|
||||
"be marked as not to suspend during codegen when passed to a function" in {
|
||||
implicit val ctx: InlineContext =
|
||||
InlineContext(localScope = Some(LocalScope.root))
|
||||
|
||||
val ir =
|
||||
"""
|
||||
|~x ~y z -> foo x y z
|
||||
|""".stripMargin.preprocessExpression.get.analyse
|
||||
|
||||
val app = ir
|
||||
.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Application.Prefix]
|
||||
|
||||
app.arguments.head
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
.shouldBeSuspended shouldEqual Some(false)
|
||||
|
||||
app
|
||||
.arguments(1)
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
.shouldBeSuspended shouldEqual Some(false)
|
||||
}
|
||||
}
|
||||
|
||||
"Non-suspended arguments" should {
|
||||
implicit val ctx: InlineContext =
|
||||
InlineContext(localScope = Some(LocalScope.root))
|
||||
|
||||
val ir =
|
||||
"""x y ->
|
||||
| a = x
|
||||
| foo x a
|
||||
|""".stripMargin.preprocessExpression.get.analyse
|
||||
|
||||
val body = ir
|
||||
.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Expression.Block]
|
||||
|
||||
"be left alone by demand analysis" in {
|
||||
body.expressions.head
|
||||
.asInstanceOf[IR.Expression.Binding]
|
||||
.expression shouldBe an[IR.Name]
|
||||
|
||||
body.returnValue
|
||||
.asInstanceOf[IR.Application.Prefix]
|
||||
.arguments
|
||||
.head
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
.value shouldBe an[IR.Name]
|
||||
}
|
||||
|
||||
"be marked for suspension during codegen when passed to a function" in {
|
||||
val xArg = body.returnValue
|
||||
.asInstanceOf[IR.Application.Prefix]
|
||||
.arguments
|
||||
.head
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
|
||||
val aArg = body.returnValue
|
||||
.asInstanceOf[IR.Application.Prefix]
|
||||
.arguments(1)
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
|
||||
xArg.value shouldBe an[IR.Name]
|
||||
xArg.shouldBeSuspended shouldEqual Some(true)
|
||||
|
||||
aArg.value shouldBe an[IR.Name]
|
||||
aArg.shouldBeSuspended shouldEqual Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
"Suspended blocks" should {
|
||||
"be forced when used" in {
|
||||
implicit val ctx: InlineContext =
|
||||
InlineContext(localScope = Some(LocalScope.root))
|
||||
|
||||
val ir =
|
||||
"""
|
||||
|x ->
|
||||
| blck =
|
||||
| foo a b
|
||||
| test = blck
|
||||
| blck
|
||||
|""".stripMargin.preprocessExpression.get.analyse
|
||||
|
||||
val irBody = ir
|
||||
.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Expression.Block]
|
||||
|
||||
irBody
|
||||
.expressions(1)
|
||||
.asInstanceOf[IR.Expression.Binding]
|
||||
.expression shouldBe an[IR.Application.Force]
|
||||
|
||||
irBody.returnValue shouldBe an[IR.Application.Force]
|
||||
}
|
||||
|
||||
"not be forced when passed to a function" in {
|
||||
implicit val ctx: InlineContext =
|
||||
InlineContext(localScope = Some(LocalScope.root))
|
||||
|
||||
val ir =
|
||||
"""
|
||||
|x ->
|
||||
| blck =
|
||||
| foo a b
|
||||
| bar blck
|
||||
|""".stripMargin.preprocessExpression.get.analyse
|
||||
|
||||
ir.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Expression.Block]
|
||||
.returnValue
|
||||
.asInstanceOf[IR.Application.Prefix]
|
||||
.arguments
|
||||
.head
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
.value shouldBe an[IR.Name]
|
||||
}
|
||||
|
||||
"be marked as not to suspend during codegen when passed to a function" in {
|
||||
implicit val ctx: InlineContext =
|
||||
InlineContext(localScope = Some(LocalScope.root))
|
||||
|
||||
val ir =
|
||||
"""
|
||||
|x ->
|
||||
| blck =
|
||||
| foo a b
|
||||
| bar blck
|
||||
|""".stripMargin.preprocessExpression.get.analyse
|
||||
|
||||
ir.asInstanceOf[IR.Function.Lambda]
|
||||
.body
|
||||
.asInstanceOf[IR.Expression.Block]
|
||||
.returnValue
|
||||
.asInstanceOf[IR.Application.Prefix]
|
||||
.arguments
|
||||
.head
|
||||
.asInstanceOf[IR.CallArgument.Specified]
|
||||
.shouldBeSuspended shouldEqual Some(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -157,11 +157,11 @@ class CodeLocationsTest extends InterpreterTest {
|
||||
val code =
|
||||
"""
|
||||
|main =
|
||||
| bar = a ~b ~c -> ~b
|
||||
| bar = a ~b ~c -> b
|
||||
|
|
||||
| bar 0 10 0
|
||||
|""".stripMargin
|
||||
instrumenter.assertNodeExists(29, 2, classOf[ForceNode])
|
||||
instrumenter.assertNodeExists(29, 1, classOf[ForceNode])
|
||||
eval(code)
|
||||
()
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ class ExpressionIdTest extends InterpreterTest {
|
||||
instrumenter.assertNodeExists(id3, "45")
|
||||
|
||||
eval(meta.appendToCode(code))
|
||||
()
|
||||
}
|
||||
|
||||
"Ids" should "be correct with parenthesized expressions" in
|
||||
@ -29,7 +28,6 @@ class ExpressionIdTest extends InterpreterTest {
|
||||
instrumenter.assertNodeExists(id1, "940")
|
||||
instrumenter.assertNodeExists(id2, "47")
|
||||
eval(meta.appendToCode(code))
|
||||
()
|
||||
}
|
||||
|
||||
"Ids" should "be correct in applications and method calls" in
|
||||
@ -42,7 +40,6 @@ class ExpressionIdTest extends InterpreterTest {
|
||||
instrumenter.assertNodeExists(id1, "Cons 5 6")
|
||||
instrumenter.assertNodeExists(id2, "Cons 5 6")
|
||||
eval(meta.appendToCode(code))
|
||||
()
|
||||
}
|
||||
|
||||
"Ids" should "be correct for deeply nested functions" in
|
||||
@ -69,7 +66,6 @@ class ExpressionIdTest extends InterpreterTest {
|
||||
instrumenter.assertNodeExistsTail(id3)
|
||||
instrumenter.assertNodeExistsTail(id4)
|
||||
eval(meta.appendToCode(code))
|
||||
()
|
||||
}
|
||||
|
||||
"Ids" should "be correct inside pattern matches" in
|
||||
@ -102,7 +98,6 @@ class ExpressionIdTest extends InterpreterTest {
|
||||
instrumenter.assertNodeExists(id3, "Unit")
|
||||
instrumenter.assertNodeExists(id4, "25")
|
||||
eval(meta.appendToCode(code))
|
||||
()
|
||||
}
|
||||
|
||||
"Ids" should "be correct for defaulted arguments" in
|
||||
@ -121,7 +116,6 @@ class ExpressionIdTest extends InterpreterTest {
|
||||
instrumenter.assertNodeExists(id1, "12")
|
||||
instrumenter.assertNodeExists(id2, "3")
|
||||
eval(meta.appendToCode(code))
|
||||
()
|
||||
}
|
||||
|
||||
"Ids" should "be correct for lazy arguments" in
|
||||
@ -129,15 +123,14 @@ class ExpressionIdTest extends InterpreterTest {
|
||||
val code =
|
||||
"""
|
||||
|main =
|
||||
| bar = a ~b ~c -> ~b
|
||||
| bar = a ~b ~c -> b
|
||||
|
|
||||
| bar 0 10 0
|
||||
|""".stripMargin
|
||||
val meta = new Metadata
|
||||
val id = meta.addItem(29, 2)
|
||||
val id = meta.addItem(29, 1)
|
||||
|
||||
instrumenter.assertNodeExists(id, "10")
|
||||
eval(meta.appendToCode(code))
|
||||
()
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ class GlobalScopeTest extends InterpreterTest {
|
||||
| 0
|
||||
|
|
||||
| IO.println 5
|
||||
| ~myFun
|
||||
| myFun
|
||||
|""".stripMargin
|
||||
|
||||
eval(code) shouldEqual 0
|
||||
@ -123,7 +123,7 @@ class GlobalScopeTest extends InterpreterTest {
|
||||
|
|
||||
| State.put 5
|
||||
| IO.println State.get
|
||||
| ~block
|
||||
| block
|
||||
| IO.println State.get
|
||||
|""".stripMargin
|
||||
|
||||
|
@ -29,7 +29,7 @@ class GroupingTest extends InterpreterTest {
|
||||
val code =
|
||||
"""
|
||||
|main =
|
||||
| ifTest = c (~ifT) ~ifF -> ifZero c ~ifT (~ifF)
|
||||
| ifTest = c (~ifT) ~ifF -> ifZero c ifT ifF
|
||||
| sum = c acc -> ifTest c acc (sum c-1 acc+c)
|
||||
| sum 10000 0
|
||||
|""".stripMargin
|
||||
|
@ -9,7 +9,7 @@ class LazyArgumentsTest extends InterpreterTest {
|
||||
val code =
|
||||
"""
|
||||
|main =
|
||||
| lazyId = ~x -> ~x
|
||||
| lazyId = ~x -> x
|
||||
| lazyId (1 + 1)
|
||||
|""".stripMargin
|
||||
|
||||
@ -20,7 +20,7 @@ class LazyArgumentsTest extends InterpreterTest {
|
||||
val code =
|
||||
"""
|
||||
|main =
|
||||
| foo = i ~x ~y -> ifZero i ~x ~y
|
||||
| foo = i ~x ~y -> ifZero i x y
|
||||
| foo 1 (IO.println 1) (IO.println 2)
|
||||
|""".stripMargin
|
||||
eval(code)
|
||||
@ -31,7 +31,7 @@ class LazyArgumentsTest extends InterpreterTest {
|
||||
val code =
|
||||
"""
|
||||
|main =
|
||||
| ifTest = c ~ifT ~ifF -> ifZero c ~ifT ~ifF
|
||||
| ifTest = c ~ifT ~ifF -> ifZero c ifT ifF
|
||||
| sum = c acc -> ifTest c acc (sum c-1 acc+c)
|
||||
| sum 10000 0
|
||||
|""".stripMargin
|
||||
@ -42,7 +42,7 @@ class LazyArgumentsTest extends InterpreterTest {
|
||||
val code =
|
||||
"""
|
||||
|main =
|
||||
| suspInc = ~x -> 1 + ~x
|
||||
| suspInc = ~x -> 1 + x
|
||||
| suspInc (suspInc 10)
|
||||
|""".stripMargin
|
||||
|
||||
@ -71,7 +71,7 @@ class LazyArgumentsTest extends InterpreterTest {
|
||||
val code =
|
||||
"""
|
||||
|main =
|
||||
| ifTest = c ~ifT ~ifF -> ifZero c ~ifT ~ifF
|
||||
| ifTest = c ~ifT ~ifF -> ifZero c ifT ifF
|
||||
| foo = c -> ifTest c
|
||||
|
|
||||
| foo 0 (IO.println 1) (IO.println 2)
|
||||
@ -88,4 +88,14 @@ class LazyArgumentsTest extends InterpreterTest {
|
||||
|""".stripMargin
|
||||
eval(code).call(1) shouldEqual 1
|
||||
}
|
||||
|
||||
subject should "allow passing suspended functions" in {
|
||||
val code =
|
||||
"""main =
|
||||
| foo = ~x -> x 1
|
||||
| foo (x -> x)
|
||||
|""".stripMargin
|
||||
|
||||
eval(code) shouldEqual 1
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class StateTest extends InterpreterTest {
|
||||
| State.put 0
|
||||
| res
|
||||
|
|
||||
| res2 = State.run 10 ~myBlock
|
||||
| res2 = State.run 10 myBlock
|
||||
| state = State.get
|
||||
| res2 + state
|
||||
|""".stripMargin
|
||||
@ -108,7 +108,7 @@ class StateTest extends InterpreterTest {
|
||||
| Panic.throw Unit
|
||||
|
|
||||
| State.put 5
|
||||
| Panic.recover ~panicker
|
||||
| Panic.recover panicker
|
||||
| State.get
|
||||
|""".stripMargin
|
||||
eval(code) shouldEqual 5
|
||||
|
Loading…
Reference in New Issue
Block a user