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

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" -->
- [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.

View File

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

View File

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

View File

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

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

View File

@ -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 ========================================
// ==========================================================================

View File

@ -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 {

View File

@ -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
*

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 = {
argument match {
case arg @ IR.CallArgument.Specified(_, expr, _, _) =>
case arg @ IR.CallArgument.Specified(_, expr, _, _, _) =>
arg
.copy(
// Note [Call Argument Tail Position]

View File

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

View File

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

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 =
"""
|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)
()
}

View File

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

View File

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

View File

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

View File

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

View File

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