Implement demand analysis (#658)

This commit is contained in:
Ara Adkins 2020-04-17 12:12:28 +01:00 committed by GitHub
parent 44e5341278
commit 16b24d58e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 952 additions and 227 deletions

View File

@ -1,3 +1,18 @@
# Enso Documentation # Enso Documentation
This folder contains the documentation for the implementation of the Enso 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.

View File

@ -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, read of the design documents listed below. These documents are highly technical,
and are not intended to be user-facing documentation. and are not intended to be user-facing documentation.
- **[Syntax](syntax/syntax.md):** A description of Enso's syntax, with a focus - **[Semantics](semantics/):** A description of Enso's semantics, with a focus
on both the main principles behind its design and its nitty gritty details. on both the main principles and the details.
- **[The Type System](types/types.md):** A description of Enso's type system, - **[Syntax](syntax/):** A description of Enso's syntax, with a focus on both
starting at a very high level and slowly getting into more detail as the the main principles behind its design and its nitty gritty details.
design evolves. - **[The Type System](types/):** A description of Enso's type system, starting
- **[The Runtime](runtime/runtime.md):** A description of the design for the at a very high level and slowly getting into more detail as the design
runtime that is evolving in tandem with the runtime's development. evolves.
- **[The Runtime](runtime/):** A description of the design for the runtime that
is evolving in tandem with the runtime's development.

View 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.

View File

@ -14,9 +14,9 @@ Enso.
<!-- MarkdownTOC levels="2,3" autolink="true" --> <!-- MarkdownTOC levels="2,3" autolink="true" -->
- [Scoping](#scoping) - [Scoping](#scoping)
- [Scoping Rules](#scoping-rules) - [Scoping Rules](#scoping-rules)
- [Strict Evaluation](#strict-evaluation) - [Strict Evaluation](#strict-evaluation)
- [Optional Suspension](#optional-suspension) - [Optional Suspension](#optional-suspension)
- [Bindings](#bindings) - [Bindings](#bindings)
<!-- /MarkdownTOC --> <!-- /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 - A value of type `Suspended a` may be forced to execute the suspended
computation and thereby obtain an `a`. 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: > The actionables for this section are:
> >
> - Make this far better specified. > - Make this far better specified.

View File

@ -77,15 +77,15 @@ class RecursionFixtures extends InterpreterRunner {
""" """
|main = n -> |main = n ->
| doNTimes = n ~block -> | doNTimes = n ~block ->
| ~block | block
| ifZero n-1 Unit (doNTimes n-1 ~block) | ifZero n-1 Unit (doNTimes n-1 block)
| |
| block = | block =
| x = State.get | x = State.get
| State.put x+1 | State.put x+1
| |
| State.put 0 | State.put 0
| doNTimes n ~block | doNTimes n block
| State.get | State.get
|""".stripMargin |""".stripMargin
val nestedThunkSum = getMain(nestedThunkSumCode) val nestedThunkSum = getMain(nestedThunkSumCode)

View File

@ -9,25 +9,13 @@ import org.enso.compiler.core.IR
import org.enso.compiler.core.IR.{Expression, Module} import org.enso.compiler.core.IR.{Expression, Module}
import org.enso.compiler.exception.CompilerError import org.enso.compiler.exception.CompilerError
import org.enso.compiler.pass.IRPass import org.enso.compiler.pass.IRPass
import org.enso.compiler.pass.analyse.{ import org.enso.compiler.pass.analyse.{AliasAnalysis, ApplicationSaturation, DemandAnalysis, TailCall}
AliasAnalysis, import org.enso.compiler.pass.desugar.{GenerateMethodBodies, LiftSpecialOperators, OperatorToFunction}
ApplicationSaturation,
TailCall
}
import org.enso.compiler.pass.desugar.{
GenerateMethodBodies,
LiftSpecialOperators,
OperatorToFunction
}
import org.enso.interpreter.Language import org.enso.interpreter.Language
import org.enso.interpreter.node.{ExpressionNode => RuntimeExpression} import org.enso.interpreter.node.{ExpressionNode => RuntimeExpression}
import org.enso.interpreter.runtime.Context import org.enso.interpreter.runtime.Context
import org.enso.interpreter.runtime.error.ModuleDoesNotExistException import org.enso.interpreter.runtime.error.ModuleDoesNotExistException
import org.enso.interpreter.runtime.scope.{ import org.enso.interpreter.runtime.scope.{LocalScope, ModuleScope, TopLevelScope}
LocalScope,
ModuleScope,
TopLevelScope
}
import org.enso.polyglot.LanguageInfo import org.enso.polyglot.LanguageInfo
import org.enso.syntax.text.{AST, Parser} import org.enso.syntax.text.{AST, Parser}
@ -53,6 +41,7 @@ class Compiler(
LiftSpecialOperators, LiftSpecialOperators,
OperatorToFunction, OperatorToFunction,
AliasAnalysis, AliasAnalysis,
DemandAnalysis,
ApplicationSaturation(), ApplicationSaturation(),
TailCall TailCall
) )

View File

@ -5,7 +5,7 @@ import cats.implicits._
import org.enso.compiler.core.IR._ import org.enso.compiler.core.IR._
import org.enso.compiler.exception.UnhandledEntity import org.enso.compiler.exception.UnhandledEntity
import org.enso.interpreter.Constants 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 // FIXME [AA] All places where we currently throw a `RuntimeException` should
// generate informative and useful nodes in core. // generate informative and useful nodes in core.
@ -129,14 +129,12 @@ object AstToIR {
val pathStr = pathSegments.map(_.name).mkString(".") val pathStr = pathSegments.map(_.name).mkString(".")
val loc = pathSegments.headOption val loc = pathSegments.headOption
.flatMap(_.location) .flatMap(_.location)
.flatMap( .flatMap(locationStart =>
locationStart => pathSegments.lastOption
pathSegments.lastOption .flatMap(_.location)
.flatMap(_.location) .flatMap(locationEnd =>
.flatMap( Some(locationStart.copy(end = locationEnd.end))
locationEnd => )
Some(locationStart.copy(end = locationEnd.end))
)
) )
(pathStr, loc) (pathStr, loc)
@ -264,12 +262,11 @@ object AstToIR {
Literal.Text(fullString, getIdentifiedLocation(literal)) Literal.Text(fullString, getIdentifiedLocation(literal))
case AST.Literal.Text.Block.Raw(lines, _, _) => case AST.Literal.Text.Block.Raw(lines, _, _) =>
val fullString = lines val fullString = lines
.map( .map(t =>
t => t.text.collect {
t.text.collect { case AST.Literal.Text.Segment.Plain(str) => str
case AST.Literal.Text.Segment.Plain(str) => str case AST.Literal.Text.Segment.RawEsc(code) => code.repr
case AST.Literal.Text.Segment.RawEsc(code) => code.repr }.mkString
}.mkString
) )
.mkString("\n") .mkString("\n")
@ -380,11 +377,6 @@ object AstToIR {
translateExpression(context), translateExpression(context),
getIdentifiedLocation(callable) getIdentifiedLocation(callable)
) )
case AstView.ForcedTerm(term) =>
Application.Force(
translateExpression(term),
getIdentifiedLocation(callable)
)
case AstView.Application(name, args) => case AstView.Application(name, args) =>
val (validArguments, hasDefaultsSuspended) = val (validArguments, hasDefaultsSuspended) =
calculateDefaultsSuspension(args) calculateDefaultsSuspension(args)

View File

@ -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 { object ContextAscription {
/** Matches a usage of the `in` keyword for ascribing a monadic context to /** Matches a usage of the `in` keyword for ascribing a monadic context to

View File

@ -719,57 +719,81 @@ class IRToTruffle(
* `arg` * `arg`
*/ */
def run(arg: IR.CallArgument, position: Int): CallArgument = arg match { 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 val scopeInfo = arg
.getMetadata[AliasAnalysis.Info.Scope.Child] .getMetadata[AliasAnalysis.Info.Scope.Child]
.getOrElse( .getOrElse(
throw new CompilerError("No scope attached to a call argument.") 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 val shouldSuspend =
.getMetadata[TailCall.Metadata] shouldBeSuspended.getOrElse(
.getOrElse( throw new CompilerError(
throw new CompilerError( "Demand analysis information missing from call argument."
"Argument with missing tail call information." )
) )
)
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 = val result = if (!shouldSuspend) {
s"call_argument<${name.getOrElse(String.valueOf(position))}>" argumentExpression
} else {
val section = value.location val argExpressionIsTail = value
.map(loc => source.createSection(loc.start, loc.end)) .getMetadata[TailCall.Metadata]
.orNull .getOrElse(
throw new CompilerError(
val callTarget = Truffle.getRuntime.createCallTarget( "Argument with missing tail call information."
ClosureRootNode.build(
language,
childScope,
moduleScope,
argumentExpression,
section,
displayName
) )
) )
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) 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 ======================================== // === Definition Argument Processor ========================================
// ========================================================================== // ==========================================================================

View File

@ -1,6 +1,7 @@
package org.enso.compiler.core package org.enso.compiler.core
import org.enso.compiler.core.IR.{Expression, IdentifiedLocation} 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.Doc
import org.enso.syntax.text.{AST, Debug, Location} import org.enso.syntax.text.{AST, Debug, Location}
@ -41,6 +42,18 @@ sealed trait IR {
this.passData.collectFirst { case data: T => data } 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. */ /** The source location that the node corresponds to. */
val location: Option[IdentifiedLocation] val location: Option[IdentifiedLocation]
@ -161,7 +174,7 @@ object IR {
* module scope * module scope
*/ */
sealed trait Scope extends IR { sealed trait Scope extends IR {
override def addMetadata(newData: Metadata): Scope override def addMetadata(newData: Metadata): Scope
override def mapExpressions(fn: Expression => Expression): Scope override def mapExpressions(fn: Expression => Expression): Scope
} }
object Scope { object Scope {
@ -187,7 +200,7 @@ object IR {
/** A representation of top-level definitions. */ /** A representation of top-level definitions. */
sealed trait Definition extends Scope { sealed trait Definition extends Scope {
override def addMetadata(newData: Metadata): Definition override def addMetadata(newData: Metadata): Definition
override def mapExpressions(fn: Expression => Expression): Definition override def mapExpressions(fn: Expression => Expression): Definition
} }
object Definition { object Definition {
@ -274,7 +287,7 @@ object IR {
} }
override def mapExpressions(fn: Expression => Expression): Expression override def mapExpressions(fn: Expression => Expression): Expression
override def addMetadata(newData: Metadata): Expression override def addMetadata(newData: Metadata): Expression
} }
object Expression { object Expression {
@ -336,7 +349,7 @@ object IR {
/** Enso literals. */ /** Enso literals. */
sealed trait Literal extends Expression with IRKind.Primitive { sealed trait Literal extends Expression with IRKind.Primitive {
override def mapExpressions(fn: Expression => Expression): Literal override def mapExpressions(fn: Expression => Expression): Literal
override def addMetadata(newData: Metadata): Literal override def addMetadata(newData: Metadata): Literal
} }
object Literal { object Literal {
@ -384,7 +397,7 @@ object IR {
val name: String val name: String
override def mapExpressions(fn: Expression => Expression): Name override def mapExpressions(fn: Expression => Expression): Name
override def addMetadata(newData: Metadata): Name override def addMetadata(newData: Metadata): Name
} }
object Name { object Name {
@ -449,7 +462,7 @@ object IR {
/** Constructs that operate on types. */ /** Constructs that operate on types. */
sealed trait Type extends Expression { sealed trait Type extends Expression {
override def mapExpressions(fn: Expression => Expression): Type override def mapExpressions(fn: Expression => Expression): Type
override def addMetadata(newData: Metadata): Type override def addMetadata(newData: Metadata): Type
} }
object Type { object Type {
@ -514,7 +527,7 @@ object IR {
/** IR nodes for dealing with typesets. */ /** IR nodes for dealing with typesets. */
sealed trait Set extends Type { sealed trait Set extends Type {
override def mapExpressions(fn: Expression => Expression): Set override def mapExpressions(fn: Expression => Expression): Set
override def addMetadata(newData: Metadata): Set override def addMetadata(newData: Metadata): Set
} }
object Set { object Set {
@ -737,7 +750,7 @@ object IR {
val canBeTCO: Boolean val canBeTCO: Boolean
override def mapExpressions(fn: Expression => Expression): Function override def mapExpressions(fn: Expression => Expression): Function
override def addMetadata(newData: Metadata): Function override def addMetadata(newData: Metadata): Function
} }
object Function { object Function {
@ -822,7 +835,7 @@ object IR {
/** All function applications in Enso. */ /** All function applications in Enso. */
sealed trait Application extends Expression { sealed trait Application extends Expression {
override def mapExpressions(fn: Expression => Expression): Application override def mapExpressions(fn: Expression => Expression): Application
override def addMetadata(newData: Metadata): Application override def addMetadata(newData: Metadata): Application
} }
object Application { object Application {
@ -876,7 +889,7 @@ object IR {
/** Operator applications in Enso. */ /** Operator applications in Enso. */
sealed trait Operator extends Application { sealed trait Operator extends Application {
override def mapExpressions(fn: Expression => Expression): Operator override def mapExpressions(fn: Expression => Expression): Operator
override def addMetadata(newData: Metadata): Operator override def addMetadata(newData: Metadata): Operator
} }
object Operator { object Operator {
@ -917,8 +930,17 @@ object IR {
/** The name of the argument, if present. */ /** The name of the argument, if present. */
val name: Option[IR.Name] 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 mapExpressions(fn: Expression => Expression): CallArgument
override def addMetadata(newData: Metadata): CallArgument override def addMetadata(newData: Metadata): CallArgument
} }
object CallArgument { object CallArgument {
@ -927,13 +949,16 @@ object IR {
* @param name the name of the argument being called, if present * @param name the name of the argument being called, if present
* @param value the expression being passed as the argument's value * @param value the expression being passed as the argument's value
* @param location the source location that the node corresponds to * @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 * @param passData the pass metadata associated with this node
*/ */
sealed case class Specified( sealed case class Specified(
override val name: Option[IR.Name], override val name: Option[IR.Name],
value: Expression, value: Expression,
override val location: Option[IdentifiedLocation], 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 ) extends CallArgument
with IRKind.Primitive { with IRKind.Primitive {
override def addMetadata(newData: Metadata): Specified = { override def addMetadata(newData: Metadata): Specified = {
@ -954,7 +979,7 @@ object IR {
/** The Enso case expression. */ /** The Enso case expression. */
sealed trait Case extends Expression { sealed trait Case extends Expression {
override def mapExpressions(fn: Expression => Expression): Case override def mapExpressions(fn: Expression => Expression): Case
override def addMetadata(newData: Metadata): Case override def addMetadata(newData: Metadata): Case
} }
object Case { object Case {
@ -1013,7 +1038,7 @@ object IR {
/** The different types of patterns that can occur in a match. */ /** The different types of patterns that can occur in a match. */
sealed trait Pattern extends IR { sealed trait Pattern extends IR {
override def mapExpressions(fn: Expression => Expression): Pattern override def mapExpressions(fn: Expression => Expression): Pattern
override def addMetadata(newData: Metadata): Pattern override def addMetadata(newData: Metadata): Pattern
} }
object Pattern { object Pattern {
// TODO [AA] Better differentiate the types of patterns that can occur // TODO [AA] Better differentiate the types of patterns that can occur
@ -1025,7 +1050,7 @@ object IR {
/** Enso comment entities. */ /** Enso comment entities. */
sealed trait Comment extends Expression { sealed trait Comment extends Expression {
override def mapExpressions(fn: Expression => Expression): Comment override def mapExpressions(fn: Expression => Expression): Comment
override def addMetadata(newData: Metadata): Comment override def addMetadata(newData: Metadata): Comment
/** The expression being commented. */ /** The expression being commented. */
val commented: Expression val commented: Expression
@ -1063,7 +1088,7 @@ object IR {
/** Foreign code entities. */ /** Foreign code entities. */
sealed trait Foreign extends Expression { sealed trait Foreign extends Expression {
override def mapExpressions(fn: Expression => Expression): Foreign override def mapExpressions(fn: Expression => Expression): Foreign
override def addMetadata(newData: Metadata): Foreign override def addMetadata(newData: Metadata): Foreign
} }
object Foreign { object Foreign {
@ -1095,7 +1120,7 @@ object IR {
/** A trait for all errors in Enso's IR. */ /** A trait for all errors in Enso's IR. */
sealed trait Error extends Expression { sealed trait Error extends Expression {
override def mapExpressions(fn: Expression => Expression): Error override def mapExpressions(fn: Expression => Expression): Error
override def addMetadata(newData: Metadata): Error override def addMetadata(newData: Metadata): Error
} }
object Error { object Error {

View File

@ -151,13 +151,12 @@ case object AliasAnalysis extends IRPass {
block block
.copy( .copy(
expressions = expressions.map( expressions = expressions.map((expression: IR.Expression) =>
(expression: IR.Expression) => analyseExpression(
analyseExpression( expression,
expression, graph,
graph, currentScope
currentScope )
)
), ),
returnValue = analyseExpression( returnValue = analyseExpression(
retVal, retVal,
@ -168,8 +167,10 @@ case object AliasAnalysis extends IRPass {
.addMetadata(Info.Scope.Child(graph, currentScope)) .addMetadata(Info.Scope.Child(graph, currentScope))
case binding @ IR.Expression.Binding(name, expression, _, _) => case binding @ IR.Expression.Binding(name, expression, _, _) =>
if (!parentScope.hasSymbolOccurrenceAs[Occurrence.Def](name.name)) { if (!parentScope.hasSymbolOccurrenceAs[Occurrence.Def](name.name)) {
val isSuspended = expression.isInstanceOf[IR.Expression.Block]
val occurrenceId = graph.nextId() val occurrenceId = graph.nextId()
val occurrence = Occurrence.Def(occurrenceId, name.name) val occurrence =
Occurrence.Def(occurrenceId, name.name, isSuspended)
parentScope.add(occurrence) parentScope.add(occurrence)
@ -188,13 +189,12 @@ case object AliasAnalysis extends IRPass {
case app: IR.Application => case app: IR.Application =>
analyseApplication(app, graph, parentScope) analyseApplication(app, graph, parentScope)
case x => case x =>
x.mapExpressions( x.mapExpressions((expression: IR.Expression) =>
(expression: IR.Expression) => analyseExpression(
analyseExpression( expression,
expression, graph,
graph, parentScope
parentScope )
)
) )
} }
} }
@ -225,22 +225,21 @@ case object AliasAnalysis extends IRPass {
scope: Scope scope: Scope
): List[IR.DefinitionArgument] = { ): List[IR.DefinitionArgument] = {
args.map { args.map {
case arg @ IR.DefinitionArgument.Specified(name, value, _, _, _) => case arg @ IR.DefinitionArgument.Specified(name, value, isSusp, _, _) =>
val nameOccursInScope = val nameOccursInScope =
scope.hasSymbolOccurrenceAs[Occurrence.Def](name.name) scope.hasSymbolOccurrenceAs[Occurrence.Def](name.name)
if (!nameOccursInScope) { if (!nameOccursInScope) {
val occurrenceId = graph.nextId() val occurrenceId = graph.nextId()
scope.add(Graph.Occurrence.Def(occurrenceId, name.name)) scope.add(Graph.Occurrence.Def(occurrenceId, name.name, isSusp))
arg arg
.copy( .copy(
defaultValue = value.map( defaultValue = value.map((ir: IR.Expression) =>
(ir: IR.Expression) => analyseExpression(
analyseExpression( ir,
ir, graph,
graph, scope
scope )
)
) )
) )
.addMetadata(Info.Occurrence(graph, occurrenceId)) .addMetadata(Info.Occurrence(graph, occurrenceId))
@ -292,7 +291,7 @@ case object AliasAnalysis extends IRPass {
parentScope: AliasAnalysis.Graph.Scope parentScope: AliasAnalysis.Graph.Scope
): List[IR.CallArgument] = { ): List[IR.CallArgument] = {
args.map { args.map {
case arg @ IR.CallArgument.Specified(_, expr, _, _) => case arg @ IR.CallArgument.Specified(_, expr, _, _, _) =>
val currentScope = expr match { val currentScope = expr match {
case _: IR.Literal => parentScope case _: IR.Literal => parentScope
case _ => parentScope.addChild() case _ => parentScope.addChild()
@ -375,13 +374,12 @@ case object AliasAnalysis extends IRPass {
case caseExpr @ IR.Case.Expr(scrutinee, branches, fallback, _, _) => case caseExpr @ IR.Case.Expr(scrutinee, branches, fallback, _, _) =>
caseExpr.copy( caseExpr.copy(
scrutinee = analyseExpression(scrutinee, graph, parentScope), scrutinee = analyseExpression(scrutinee, graph, parentScope),
branches = branches.map( branches = branches.map(branch =>
branch => branch.copy(
branch.copy( pattern = analyseExpression(branch.pattern, graph, parentScope),
pattern = analyseExpression(branch.pattern, graph, parentScope), expression =
expression = analyseExpression(branch.expression, graph, parentScope)
analyseExpression(branch.expression, graph, parentScope) )
)
), ),
fallback = fallback.map(analyseExpression(_, graph, parentScope)) fallback = fallback.map(analyseExpression(_, graph, parentScope))
) //.addMetadata(Info.Scope.Child(graph, currentScope)) ) //.addMetadata(Info.Scope.Child(graph, currentScope))
@ -495,8 +493,8 @@ case object AliasAnalysis extends IRPass {
): Set[Graph.Link] = { ): Set[Graph.Link] = {
val idsForSym = rootScope.symbolToIds[T](symbol) val idsForSym = rootScope.symbolToIds[T](symbol)
links.filter( links.filter(l =>
l => idsForSym.contains(l.source) || idsForSym.contains(l.target) idsForSym.contains(l.source) || idsForSym.contains(l.target)
) )
} }
@ -518,8 +516,8 @@ case object AliasAnalysis extends IRPass {
linksFor(id).find { edge => linksFor(id).find { edge =>
val occ = getOccurrence(edge.target) val occ = getOccurrence(edge.target)
occ match { occ match {
case Some(Occurrence.Def(_, _)) => true case Some(Occurrence.Def(_, _, _)) => true
case _ => false case _ => false
} }
} }
} }
@ -730,8 +728,8 @@ case object AliasAnalysis extends IRPass {
parentCounter: Int = 0 parentCounter: Int = 0
): Option[Graph.Link] = { ): Option[Graph.Link] = {
val definition = occurrences.find { val definition = occurrences.find {
case Graph.Occurrence.Def(_, n) => n == occurrence.symbol case Graph.Occurrence.Def(_, n, _) => n == occurrence.symbol
case _ => false case _ => false
} }
definition match { definition match {
@ -900,7 +898,11 @@ case object AliasAnalysis extends IRPass {
* @param id the identifier of the name in the graph * @param id the identifier of the name in the graph
* @param symbol the text of the name * @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 /** A usage of a symbol in the aliasing graph
* *

View File

@ -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
)
)
}
}

View File

@ -189,7 +189,7 @@ case object TailCall extends IRPass {
*/ */
def analyseCallArg(argument: IR.CallArgument): IR.CallArgument = { def analyseCallArg(argument: IR.CallArgument): IR.CallArgument = {
argument match { argument match {
case arg @ IR.CallArgument.Specified(_, expr, _, _) => case arg @ IR.CallArgument.Specified(_, expr, _, _, _) =>
arg arg
.copy( .copy(
// Note [Call Argument Tail Position] // Note [Call Argument Tail Position]

View File

@ -96,18 +96,50 @@ trait CompilerRunner {
inlineContext: InlineContext inlineContext: InlineContext
): IR = ir match { ): IR = ir match {
case expr: IR.Expression => case expr: IR.Expression =>
passes.foldLeft(expr)( passes.foldLeft(expr)((intermediate, pass) =>
(intermediate, pass) => pass.runExpression(intermediate, inlineContext)
pass.runExpression(intermediate, inlineContext)
) )
case mod: IR.Module => case mod: IR.Module =>
passes.foldLeft(mod)( passes.foldLeft(mod)((intermediate, pass) =>
(intermediate, pass) => pass.runModule(intermediate) pass.runModule(intermediate)
) )
case _ => throw new RuntimeException(s"Cannot run passes on $ir.") 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 ===================================================== // === IR Testing Utils =====================================================
/** A variety of extension methods on IR expressions to aid testing. /** A variety of extension methods on IR expressions to aid testing.

View File

@ -18,41 +18,12 @@ class AliasAnalysisTest extends CompilerTest {
// === Utilities ============================================================ // === Utilities ============================================================
/** Adds an extension method to preprocess the source as IR. /** The passes that need to be run before the alias analysis pass. */
* implicit val precursorPasses: List[IRPass] = List(
* @param source the source code to preprocess GenerateMethodBodies,
*/ LiftSpecialOperators,
implicit class Preprocess(source: String) { OperatorToFunction
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]
)
}
}
/** Adds an extension method to run alias analysis on an [[IR.Module]]. /** 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 * @param ir the expression to run alias analysis on
*/ */
@ -77,6 +48,8 @@ class AliasAnalysisTest extends CompilerTest {
/** Runs alias analysis on an expression. /** Runs alias analysis on an expression.
* *
* @param inlineContext the inline context in which to process the
* expression
* @return [[ir]], with attached aliasing information * @return [[ir]], with attached aliasing information
*/ */
def analyse(inlineContext: InlineContext): IR.Expression = { def analyse(inlineContext: InlineContext): IR.Expression = {
@ -515,8 +488,8 @@ class AliasAnalysisTest extends CompilerTest {
} }
"assign Info.Occurrence to definitions and usages of symbols" in { "assign Info.Occurrence to definitions and usages of symbols" in {
topLambda.arguments.foreach( topLambda.arguments.foreach(arg =>
arg => arg.getMetadata[Info.Occurrence] shouldBe defined arg.getMetadata[Info.Occurrence] shouldBe defined
) )
topLambdaBody.expressions.foreach( topLambdaBody.expressions.foreach(
@ -524,8 +497,8 @@ class AliasAnalysisTest extends CompilerTest {
.getMetadata[Info.Occurrence] shouldBe defined .getMetadata[Info.Occurrence] shouldBe defined
) )
childLambda.arguments.foreach( childLambda.arguments.foreach(arg =>
arg => arg.getMetadata[Info.Occurrence] shouldBe defined arg.getMetadata[Info.Occurrence] shouldBe defined
) )
childLambdaBody.function.getMetadata[Info.Occurrence] shouldBe defined childLambdaBody.function.getMetadata[Info.Occurrence] shouldBe defined

View File

@ -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)
}
}
}

View File

@ -157,11 +157,11 @@ class CodeLocationsTest extends InterpreterTest {
val code = val code =
""" """
|main = |main =
| bar = a ~b ~c -> ~b | bar = a ~b ~c -> b
| |
| bar 0 10 0 | bar 0 10 0
|""".stripMargin |""".stripMargin
instrumenter.assertNodeExists(29, 2, classOf[ForceNode]) instrumenter.assertNodeExists(29, 1, classOf[ForceNode])
eval(code) eval(code)
() ()
} }

View File

@ -16,7 +16,6 @@ class ExpressionIdTest extends InterpreterTest {
instrumenter.assertNodeExists(id3, "45") instrumenter.assertNodeExists(id3, "45")
eval(meta.appendToCode(code)) eval(meta.appendToCode(code))
()
} }
"Ids" should "be correct with parenthesized expressions" in "Ids" should "be correct with parenthesized expressions" in
@ -29,7 +28,6 @@ class ExpressionIdTest extends InterpreterTest {
instrumenter.assertNodeExists(id1, "940") instrumenter.assertNodeExists(id1, "940")
instrumenter.assertNodeExists(id2, "47") instrumenter.assertNodeExists(id2, "47")
eval(meta.appendToCode(code)) eval(meta.appendToCode(code))
()
} }
"Ids" should "be correct in applications and method calls" in "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(id1, "Cons 5 6")
instrumenter.assertNodeExists(id2, "Cons 5 6") instrumenter.assertNodeExists(id2, "Cons 5 6")
eval(meta.appendToCode(code)) eval(meta.appendToCode(code))
()
} }
"Ids" should "be correct for deeply nested functions" in "Ids" should "be correct for deeply nested functions" in
@ -69,7 +66,6 @@ class ExpressionIdTest extends InterpreterTest {
instrumenter.assertNodeExistsTail(id3) instrumenter.assertNodeExistsTail(id3)
instrumenter.assertNodeExistsTail(id4) instrumenter.assertNodeExistsTail(id4)
eval(meta.appendToCode(code)) eval(meta.appendToCode(code))
()
} }
"Ids" should "be correct inside pattern matches" in "Ids" should "be correct inside pattern matches" in
@ -102,7 +98,6 @@ class ExpressionIdTest extends InterpreterTest {
instrumenter.assertNodeExists(id3, "Unit") instrumenter.assertNodeExists(id3, "Unit")
instrumenter.assertNodeExists(id4, "25") instrumenter.assertNodeExists(id4, "25")
eval(meta.appendToCode(code)) eval(meta.appendToCode(code))
()
} }
"Ids" should "be correct for defaulted arguments" in "Ids" should "be correct for defaulted arguments" in
@ -121,7 +116,6 @@ class ExpressionIdTest extends InterpreterTest {
instrumenter.assertNodeExists(id1, "12") instrumenter.assertNodeExists(id1, "12")
instrumenter.assertNodeExists(id2, "3") instrumenter.assertNodeExists(id2, "3")
eval(meta.appendToCode(code)) eval(meta.appendToCode(code))
()
} }
"Ids" should "be correct for lazy arguments" in "Ids" should "be correct for lazy arguments" in
@ -129,15 +123,14 @@ class ExpressionIdTest extends InterpreterTest {
val code = val code =
""" """
|main = |main =
| bar = a ~b ~c -> ~b | bar = a ~b ~c -> b
| |
| bar 0 10 0 | bar 0 10 0
|""".stripMargin |""".stripMargin
val meta = new Metadata val meta = new Metadata
val id = meta.addItem(29, 2) val id = meta.addItem(29, 1)
instrumenter.assertNodeExists(id, "10") instrumenter.assertNodeExists(id, "10")
eval(meta.appendToCode(code)) eval(meta.appendToCode(code))
()
} }
} }

View File

@ -107,7 +107,7 @@ class GlobalScopeTest extends InterpreterTest {
| 0 | 0
| |
| IO.println 5 | IO.println 5
| ~myFun | myFun
|""".stripMargin |""".stripMargin
eval(code) shouldEqual 0 eval(code) shouldEqual 0
@ -123,7 +123,7 @@ class GlobalScopeTest extends InterpreterTest {
| |
| State.put 5 | State.put 5
| IO.println State.get | IO.println State.get
| ~block | block
| IO.println State.get | IO.println State.get
|""".stripMargin |""".stripMargin

View File

@ -29,7 +29,7 @@ class GroupingTest extends InterpreterTest {
val code = val code =
""" """
|main = |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 = c acc -> ifTest c acc (sum c-1 acc+c)
| sum 10000 0 | sum 10000 0
|""".stripMargin |""".stripMargin

View File

@ -9,7 +9,7 @@ class LazyArgumentsTest extends InterpreterTest {
val code = val code =
""" """
|main = |main =
| lazyId = ~x -> ~x | lazyId = ~x -> x
| lazyId (1 + 1) | lazyId (1 + 1)
|""".stripMargin |""".stripMargin
@ -20,7 +20,7 @@ class LazyArgumentsTest extends InterpreterTest {
val code = val code =
""" """
|main = |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) | foo 1 (IO.println 1) (IO.println 2)
|""".stripMargin |""".stripMargin
eval(code) eval(code)
@ -31,7 +31,7 @@ class LazyArgumentsTest extends InterpreterTest {
val code = val code =
""" """
|main = |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 = c acc -> ifTest c acc (sum c-1 acc+c)
| sum 10000 0 | sum 10000 0
|""".stripMargin |""".stripMargin
@ -42,7 +42,7 @@ class LazyArgumentsTest extends InterpreterTest {
val code = val code =
""" """
|main = |main =
| suspInc = ~x -> 1 + ~x | suspInc = ~x -> 1 + x
| suspInc (suspInc 10) | suspInc (suspInc 10)
|""".stripMargin |""".stripMargin
@ -71,7 +71,7 @@ class LazyArgumentsTest extends InterpreterTest {
val code = val code =
""" """
|main = |main =
| ifTest = c ~ifT ~ifF -> ifZero c ~ifT ~ifF | ifTest = c ~ifT ~ifF -> ifZero c ifT ifF
| foo = c -> ifTest c | foo = c -> ifTest c
| |
| foo 0 (IO.println 1) (IO.println 2) | foo 0 (IO.println 1) (IO.println 2)
@ -88,4 +88,14 @@ class LazyArgumentsTest extends InterpreterTest {
|""".stripMargin |""".stripMargin
eval(code).call(1) shouldEqual 1 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
}
} }

View File

@ -46,7 +46,7 @@ class StateTest extends InterpreterTest {
| State.put 0 | State.put 0
| res | res
| |
| res2 = State.run 10 ~myBlock | res2 = State.run 10 myBlock
| state = State.get | state = State.get
| res2 + state | res2 + state
|""".stripMargin |""".stripMargin
@ -108,7 +108,7 @@ class StateTest extends InterpreterTest {
| Panic.throw Unit | Panic.throw Unit
| |
| State.put 5 | State.put 5
| Panic.recover ~panicker | Panic.recover panicker
| State.get | State.get
|""".stripMargin |""".stripMargin
eval(code) shouldEqual 5 eval(code) shouldEqual 5