diff --git a/.gitignore b/.gitignore index 7ff8b61abd8..290628baa94 100644 --- a/.gitignore +++ b/.gitignore @@ -68,8 +68,14 @@ cabal.sandbox.config *.swp .projections.json +############################ +## Rendered Documentation ## +############################ + +javadoc/ ####################### ## Benchmark Reports ## ####################### bench-report.xml + diff --git a/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/AtomBenchmarks.java b/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/AtomBenchmarks.java index e3460d0b953..229ab8c9634 100644 --- a/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/AtomBenchmarks.java +++ b/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/AtomBenchmarks.java @@ -1,7 +1,6 @@ package org.enso.interpreter.benchmarks; -import org.enso.interpreter.AtomFixtures; -import org.enso.interpreter.RecursionFixtures; +import org.enso.interpreter.fixtures.AtomFixtures; import org.openjdk.jmh.annotations.*; import java.util.concurrent.TimeUnit; diff --git a/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/NamedDefaultedArgumentBenchmarks.java b/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/NamedDefaultedArgumentBenchmarks.java new file mode 100644 index 00000000000..5be93e8d705 --- /dev/null +++ b/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/NamedDefaultedArgumentBenchmarks.java @@ -0,0 +1,31 @@ +package org.enso.interpreter.benchmarks; + +import java.util.concurrent.TimeUnit; +import org.enso.interpreter.fixtures.NamedDefaultedArgumentFixtures; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode(Mode.AverageTime) +@Fork(2) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class NamedDefaultedArgumentBenchmarks { + private static NamedDefaultedArgumentFixtures argumentFixtures = + new NamedDefaultedArgumentFixtures(); + + @Benchmark + public void benchSumTCOWithNamedArgs() { + argumentFixtures.sumTCOWithNamedArguments().execute(argumentFixtures.hundredMillion()); + } + + @Benchmark + public void benchSumTCOWithDefaultArgs() { + argumentFixtures.sumTCOWithDefaultedArguments().execute(argumentFixtures.hundredMillion()); + } +} diff --git a/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/RecursionBenchmarks.java b/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/RecursionBenchmarks.java index 55588c8ee68..2410b37a4dc 100644 --- a/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/RecursionBenchmarks.java +++ b/Interpreter/src/bench/java/org/enso/interpreter/benchmarks/RecursionBenchmarks.java @@ -1,6 +1,6 @@ package org.enso.interpreter.benchmarks; -import org.enso.interpreter.RecursionFixtures; +import org.enso.interpreter.fixtures.RecursionFixtures; import org.openjdk.jmh.annotations.*; import java.util.concurrent.TimeUnit; @@ -22,4 +22,9 @@ public class RecursionBenchmarks { public void benchSumTCOFoldLike() { recursionFixtures.sumTCOFoldLike().execute(recursionFixtures.hundredMillion()); } + + @Benchmark + public void benchSumRecursive() { + recursionFixtures.sumTCO().execute(recursionFixtures.hundred()); + } } diff --git a/Interpreter/src/bench/scala/org/enso/interpreter/RegressionTest.scala b/Interpreter/src/bench/scala/org/enso/interpreter/RegressionTest.scala index 73218ce8190..34626863f85 100644 --- a/Interpreter/src/bench/scala/org/enso/interpreter/RegressionTest.scala +++ b/Interpreter/src/bench/scala/org/enso/interpreter/RegressionTest.scala @@ -3,9 +3,10 @@ package org.enso.interpreter import org.scalatest.FlatSpec import org.scalatest.Matchers -import collection.JavaConverters._ +import scala.collection.JavaConverters._ class RegressionTest extends FlatSpec with Matchers { + // This tolerance may be adjusted depending on the stability of CI final val TOLERANCE = 0.2 val runner = new BenchmarksRunner diff --git a/Interpreter/src/bench/scala/org/enso/interpreter/AtomFixtures.scala b/Interpreter/src/bench/scala/org/enso/interpreter/fixtures/AtomFixtures.scala similarity index 94% rename from Interpreter/src/bench/scala/org/enso/interpreter/AtomFixtures.scala rename to Interpreter/src/bench/scala/org/enso/interpreter/fixtures/AtomFixtures.scala index 7894f7e5f20..18ce4227864 100644 --- a/Interpreter/src/bench/scala/org/enso/interpreter/AtomFixtures.scala +++ b/Interpreter/src/bench/scala/org/enso/interpreter/fixtures/AtomFixtures.scala @@ -1,4 +1,6 @@ -package org.enso.interpreter +package org.enso.interpreter.fixtures + +import org.enso.interpreter.LanguageRunner class AtomFixtures extends LanguageRunner { val million: Long = 1000000 diff --git a/Interpreter/src/bench/scala/org/enso/interpreter/fixtures/NamedDefaultedArgumentFixtures.scala b/Interpreter/src/bench/scala/org/enso/interpreter/fixtures/NamedDefaultedArgumentFixtures.scala new file mode 100644 index 00000000000..2099bd67698 --- /dev/null +++ b/Interpreter/src/bench/scala/org/enso/interpreter/fixtures/NamedDefaultedArgumentFixtures.scala @@ -0,0 +1,34 @@ +package org.enso.interpreter.fixtures + +import org.enso.interpreter.LanguageRunner + +class NamedDefaultedArgumentFixtures extends LanguageRunner { + val hundredMillion: Long = 100000000 + + val sumTCOWithNamedArgumentsCode = + """ + |{ |sumTo| + | summator = { |acc, current| + | ifZero: [current, acc, @summator [current = current - 1, acc = acc + current]] + | }; + | res = @summator [current = sumTo, acc = 0]; + | res + |} + """.stripMargin + + val sumTCOWithNamedArguments = eval(sumTCOWithNamedArgumentsCode) + + val sumTCOWithDefaultedArgumentsCode = + """ + |{ |sumTo| + | summator = { |acc = 0, current| + | ifZero: [current, acc, @summator [current = current - 1, acc = acc + current]] + | }; + | res = @summator [current = sumTo]; + | res + |} + """.stripMargin + + val sumTCOWithDefaultedArguments = eval(sumTCOWithDefaultedArgumentsCode) + +} diff --git a/Interpreter/src/bench/scala/org/enso/interpreter/RecursionFixtures.scala b/Interpreter/src/bench/scala/org/enso/interpreter/fixtures/RecursionFixtures.scala similarity index 91% rename from Interpreter/src/bench/scala/org/enso/interpreter/RecursionFixtures.scala rename to Interpreter/src/bench/scala/org/enso/interpreter/fixtures/RecursionFixtures.scala index df395473de3..e40b89735d3 100644 --- a/Interpreter/src/bench/scala/org/enso/interpreter/RecursionFixtures.scala +++ b/Interpreter/src/bench/scala/org/enso/interpreter/fixtures/RecursionFixtures.scala @@ -1,7 +1,10 @@ -package org.enso.interpreter +package org.enso.interpreter.fixtures + +import org.enso.interpreter.{Constants, LanguageRunner} class RecursionFixtures extends LanguageRunner { val hundredMillion: Long = 100000000 + val hundred: Long = 100 // Currently unused as we know this is very slow. val mutRecursiveCode = diff --git a/Interpreter/src/main/java/org/enso/interpreter/Constants.java b/Interpreter/src/main/java/org/enso/interpreter/Constants.java index 7f21edc98e0..66fa1f57efc 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/Constants.java +++ b/Interpreter/src/main/java/org/enso/interpreter/Constants.java @@ -1,5 +1,8 @@ package org.enso.interpreter; +/** + * Language-level constants for use throughout the program. + */ public class Constants { public static final String LANGUAGE_ID = "enso"; public static final String LANGUAGE_NAME = "Enso"; diff --git a/Interpreter/src/main/java/org/enso/interpreter/Language.java b/Interpreter/src/main/java/org/enso/interpreter/Language.java index 46d01ae3824..692c5e13c99 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/Language.java +++ b/Interpreter/src/main/java/org/enso/interpreter/Language.java @@ -12,6 +12,15 @@ import org.enso.interpreter.node.EnsoRootNode; import org.enso.interpreter.node.ExpressionNode; import org.enso.interpreter.runtime.Context; +/** + * The root of the Enso implementation. + * + *

This class contains all of the services needed by a Truffle language to enable interoperation + * with other guest languages on the same VM. This ensures that Enso is usable via the polyglot API, + * and hence that it can both call other languages seamlessly, and be called from other languages. + * + * See {@link TruffleLanguage} for more information on the lifecycle of a language. + */ @TruffleLanguage.Registration( id = Constants.LANGUAGE_ID, name = Constants.LANGUAGE_NAME, @@ -29,21 +38,47 @@ import org.enso.interpreter.runtime.Context; }) public final class Language extends TruffleLanguage { + /** + * Creates a new Enso context. + * + * @param env the language execution environment + * @return a new Enso context + */ @Override protected Context createContext(Env env) { return new Context(this, env); } + /** + * Checks if a given object is native to Enso. + * + * @param object the object to check + * @return {@code true} if {@code object} belongs to Enso, {@code false} otherwise + */ @Override protected boolean isObjectOfLanguage(Object object) { return false; } + /** + * Checks if this Enso execution environment is accessible in a multithreaded context. + * + * @param thread the thread to check access for + * @param singleThreaded whether or not execution is single threaded + * @return whether or not thread access is allowed + */ @Override protected boolean isThreadAccessAllowed(Thread thread, boolean singleThreaded) { return super.isThreadAccessAllowed(thread, singleThreaded); } + /** + * Parses Enso source code ready for execution. + * + * @param request the source to parse, plus contextual information + * @return a ready-to-execute node representing the code provided in {@code request} + * @throws Exception when parsing or AST construction fail + */ @Override protected CallTarget parse(ParsingRequest request) throws Exception { AstGlobalScope parsed = @@ -53,6 +88,11 @@ public final class Language extends TruffleLanguage { return Truffle.getRuntime().createCallTarget(root); } + /** + * Gets the current Enso execution context. + * + * @return the current execution context + */ public Context getCurrentContext() { return getCurrentContext(Language.class); } diff --git a/Interpreter/src/main/java/org/enso/interpreter/builder/ArgDefinitionFactory.java b/Interpreter/src/main/java/org/enso/interpreter/builder/ArgDefinitionFactory.java new file mode 100644 index 00000000000..84cedd46970 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/builder/ArgDefinitionFactory.java @@ -0,0 +1,82 @@ +package org.enso.interpreter.builder; + +import org.enso.interpreter.AstArgDefinitionVisitor; +import org.enso.interpreter.AstExpression; +import org.enso.interpreter.Language; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; +import org.enso.interpreter.runtime.scope.GlobalScope; +import org.enso.interpreter.runtime.scope.LocalScope; + +/** + * An {@code ArgDefinitionFactory} is responsible for converting argument definitions in the Enso + * AST into runtime nodes for evaluation in the interpreter. + */ +public class ArgDefinitionFactory implements AstArgDefinitionVisitor { + private final LocalScope scope; + private final Language language; + private final String scopeName; + private final GlobalScope globalScope; + + /** + * Explicitly specifies all constructor arguments. + * + * @param scope the language scope into which the arguments are defined + * @param language the name of the language for which the arguments are defined + * @param scopeName the name of the scope in which the arguments are defined + * @param globalScope the current language global scope + */ + public ArgDefinitionFactory( + LocalScope scope, Language language, String scopeName, GlobalScope globalScope) { + this.scope = scope; + this.language = language; + this.scopeName = scopeName; + this.globalScope = globalScope; + } + + /** + * Defaults the local scope to a newly created one. + * + * @param language the name of the language for which the arguments are defined + * @param scopeName the name of the scope in which the arguments are defined + * @param globalScope the current language global scope + */ + public ArgDefinitionFactory(Language language, String scopeName, GlobalScope globalScope) { + this(new LocalScope(), language, scopeName, globalScope); + } + + /** + * Default constructs the {@code LocalScope} and defaults the scope name to {@code }. + * + * @param language the name of the language for which the arguments are defined + * @param globalScope the current language global scope + */ + public ArgDefinitionFactory(Language language, GlobalScope globalScope) { + this(language, "", globalScope); + } + + /** + * Processes a function argument that is provided without a default. + * + * @param name the name of the argument + * @param position the position of the argument in the definition list + * @return a runtime type representing the argument input + */ + @Override + public ArgumentDefinition visitBareArg(String name, int position) { + return new ArgumentDefinition(position, name); + } + + /** + * Processes a function argument that is provided with a default value. + * + * @param name the name of the argument + * @param value the default value of the argument + * @param position the position of the argument in the definition list + * @return a runtime type representing the argument input + */ + @Override + public ArgumentDefinition visitDefaultedArg(String name, AstExpression value, int position) { + ExpressionFactory exprFactory = new ExpressionFactory(language, scope, scopeName, globalScope); + return new ArgumentDefinition(position, name, value.visit(exprFactory)); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/builder/CallArgFactory.java b/Interpreter/src/main/java/org/enso/interpreter/builder/CallArgFactory.java new file mode 100644 index 00000000000..550e42415e5 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/builder/CallArgFactory.java @@ -0,0 +1,82 @@ +package org.enso.interpreter.builder; + +import org.enso.interpreter.AstCallArgVisitor; +import org.enso.interpreter.AstExpression; +import org.enso.interpreter.Language; +import org.enso.interpreter.runtime.callable.argument.CallArgument; +import org.enso.interpreter.runtime.scope.GlobalScope; +import org.enso.interpreter.runtime.scope.LocalScope; + +/** + * A {@code CallArgFactory} is responsible for converting arguments passed to a function call into + * runtime nodes used by the interpreter to guide function evaluation. + */ +public class CallArgFactory implements AstCallArgVisitor { + private final LocalScope scope; + private final Language language; + private final String scopeName; + private final GlobalScope globalScope; + + /** + * Explicitly specifies all constructor parameters. + * + * @param scope the language scope in which the arguments are called + * @param language the name of the language for which the arguments are defined + * @param scopeName the name of the scope in which the arguments are called + * @param globalScope the current language global scope + */ + public CallArgFactory( + LocalScope scope, Language language, String scopeName, GlobalScope globalScope) { + this.scope = scope; + this.language = language; + this.scopeName = scopeName; + this.globalScope = globalScope; + } + + /** + * Processes an ignore argument. + * + *

Such arguments are used to disable the function's usage of a default with which it was + * defined, and become useful in the presence of partial function application and currying. + * + * @param name the name of the argument whose default is ignored + * @param position the position of this argument in the calling arguments list + * @return a runtime representation of the argument + */ + @Override + public CallArgument visitIgnore(String name, int position) { + return new CallArgument(name); + } + + /** + * Processes a named argument application. + * + *

Arguments can be applied by name, and can occur at any point in the parameter list. + * + * @param name the name of the argument being applied + * @param value the value of the argument being applied + * @param position the position of this argument in the calling arguments list + * @return a runtime representation of the argument + */ + @Override + public CallArgument visitNamedCallArg(String name, AstExpression value, int position) { + ExpressionFactory factory = new ExpressionFactory(language, scope, scopeName, globalScope); + return new CallArgument(name, value.visit(factory)); + } + + /** + * Processes a positional argument application. + * + *

Though all arguments have positions at the call site, an argument without a name is applied + * purely based on its position. + * + * @param value the value of the argument being applied + * @param position the position of this argument in the calling arguments list + * @return a runtime representation of the argument + */ + @Override + public CallArgument visitUnnamedCallArg(AstExpression value, int position) { + ExpressionFactory factory = new ExpressionFactory(language, scope, scopeName, globalScope); + return new CallArgument(value.visit(factory)); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/builder/ExpressionFactory.java b/Interpreter/src/main/java/org/enso/interpreter/builder/ExpressionFactory.java index 60009e532f8..04a76223056 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/builder/ExpressionFactory.java +++ b/Interpreter/src/main/java/org/enso/interpreter/builder/ExpressionFactory.java @@ -4,70 +4,144 @@ import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.frame.FrameSlot; import com.oracle.truffle.api.nodes.RootNode; -import org.enso.interpreter.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.enso.interpreter.AstArgDefinition; +import org.enso.interpreter.AstCallArg; +import org.enso.interpreter.AstCase; +import org.enso.interpreter.AstCaseFunction; +import org.enso.interpreter.AstExpression; +import org.enso.interpreter.AstExpressionVisitor; +import org.enso.interpreter.Language; import org.enso.interpreter.node.EnsoRootNode; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.node.controlflow.*; +import org.enso.interpreter.node.callable.InvokeCallableNodeGen; +import org.enso.interpreter.node.callable.argument.ReadArgumentNode; +import org.enso.interpreter.node.callable.function.CreateFunctionNode; +import org.enso.interpreter.node.callable.function.FunctionBodyNode; +import org.enso.interpreter.node.controlflow.CaseNode; +import org.enso.interpreter.node.controlflow.ConstructorCaseNode; +import org.enso.interpreter.node.controlflow.DefaultFallbackNode; +import org.enso.interpreter.node.controlflow.FallbackNode; +import org.enso.interpreter.node.controlflow.IfZeroNode; +import org.enso.interpreter.node.controlflow.MatchNode; import org.enso.interpreter.node.expression.builtin.PrintNode; import org.enso.interpreter.node.expression.constant.ConstructorNode; import org.enso.interpreter.node.expression.literal.IntegerLiteralNode; -import org.enso.interpreter.node.expression.operator.*; -import org.enso.interpreter.node.function.CreateFunctionNode; -import org.enso.interpreter.node.function.FunctionBodyNode; -import org.enso.interpreter.node.function.InvokeNodeGen; -import org.enso.interpreter.node.function.ReadArgumentNode; +import org.enso.interpreter.node.expression.operator.AddOperatorNodeGen; +import org.enso.interpreter.node.expression.operator.DivideOperatorNodeGen; +import org.enso.interpreter.node.expression.operator.ModOperatorNodeGen; +import org.enso.interpreter.node.expression.operator.MultiplyOperatorNodeGen; +import org.enso.interpreter.node.expression.operator.SubtractOperatorNodeGen; import org.enso.interpreter.node.scope.AssignmentNode; import org.enso.interpreter.node.scope.AssignmentNodeGen; import org.enso.interpreter.node.scope.ReadGlobalTargetNode; import org.enso.interpreter.node.scope.ReadLocalTargetNodeGen; -import org.enso.interpreter.runtime.errors.VariableDoesNotExistException; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; +import org.enso.interpreter.runtime.callable.argument.CallArgument; +import org.enso.interpreter.runtime.error.DuplicateArgumentNameException; +import org.enso.interpreter.runtime.error.VariableDoesNotExistException; +import org.enso.interpreter.runtime.scope.GlobalScope; +import org.enso.interpreter.runtime.scope.LocalScope; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; +/** + * An {@code ExpressionFactory} is responsible for converting the majority of Enso's parsed AST into + * nodes evaluated by the interpreter at runtime. + */ public class ExpressionFactory implements AstExpressionVisitor { - private final LocalScope scope; private final Language language; private final String scopeName; private final GlobalScope globalScope; - private String currentVarName = "annonymous"; + /** + * Explicitly specifies all contructor parameters. + * + * @param language the name of the language for which the nodes are defined + * @param scope the language scope in which definitions are processed + * @param scopeName the name of the scope in which definitions are processed + * @param globalScope the current language global scope + */ public ExpressionFactory( - Language language, LocalScope scope, String name, GlobalScope globalScope) { + Language language, LocalScope scope, String scopeName, GlobalScope globalScope) { this.language = language; this.scope = scope; - this.scopeName = name; + this.scopeName = scopeName; this.globalScope = globalScope; } - public ExpressionFactory(Language lang, String scopeName, GlobalScope globalScope) { - this(lang, new LocalScope(), scopeName, globalScope); + /** + * Defaults the local scope to a freshly constructed scope. + * + * @param language the name of the language for which the nodes are defined + * @param scopeName the name of the scope in which definitions are processed + * @param globalScope the current language global scope + */ + public ExpressionFactory(Language language, String scopeName, GlobalScope globalScope) { + this(language, new LocalScope(), scopeName, globalScope); } + /** + * Defaults the local scope, and defaults the name of said scope to {@code ""} + * + * @param language the name of the language for which the nodes are defined + * @param globalScope the current language global scope + */ public ExpressionFactory(Language language, GlobalScope globalScope) { this(language, "", globalScope); } + /** + * Creates a child of this {@code ExpressionFactory}. + * + *

This child will be initialized with a {@code LocalScope} that is a child of the local scope + * contained within {@code this}. + * + * @param name the name of the new scope + * @return a child of this current expression factory + */ public ExpressionFactory createChild(String name) { return new ExpressionFactory(language, scope.createChild(), name, this.globalScope); } + /** + * Creates an executable expression from an AST expression. + * + * @param expr the expression to make executable + * @return a node representing the provided computation + */ public ExpressionNode run(AstExpression expr) { ExpressionNode result = expr.visit(this); result.markNotTail(); return result; } + /** + * Creates a runtime {@code long} value from an AST node. + * + * @param l the value to represent + * @return a runtime node representing that value + */ public ExpressionNode visitLong(long l) { return new IntegerLiteralNode(l); } + /** + * Creates runtime nodes representing arithmetic expressions. + * + * @param operator the operator to represent + * @param leftAst the expressions to the left of the operator + * @param rightAst the expressions to the right of the operator + * @return a runtime node representing the arithmetic expression + */ @Override public ExpressionNode visitArithOp( String operator, AstExpression leftAst, AstExpression rightAst) { @@ -81,21 +155,37 @@ public class ExpressionFactory implements AstExpressionVisitor { return null; } + /** + * Creates runtime nodes representing foreign code blocks. + * + * @param lang the name of the foreign language + * @param code the code in the foreign language + * @return a runtime node representing the foreign code + */ @Override public ExpressionNode visitForeign(String lang, String code) { - return null; + throw new NotImplementedException(); } + /** + * Creates a runtime node representing a variable definition. + * + *

This method is solely responsible for creating a variable in the parent scope with the + * provided name and does not handle associating that variable with a value. + * + * @param name the name of the variable + * @return a runtime node representing the variable + */ @Override public ExpressionNode visitVariable(String name) { - Supplier> localNode = + Supplier> localVariableNode = () -> scope.getSlot(name).map(ReadLocalTargetNodeGen::create); - Supplier> constructor = + Supplier> constructorNode = () -> globalScope.getConstructor(name).map(ConstructorNode::new); - Supplier> globalFun = + Supplier> globalDefinitionNode = () -> globalScope.getGlobalCallTarget(name).map(ReadGlobalTargetNode::new); - return Stream.of(localNode, constructor, globalFun) + return Stream.of(localVariableNode, constructorNode, globalDefinitionNode) .map(Supplier::get) .filter(Optional::isPresent) .map(Optional::get) @@ -103,51 +193,167 @@ public class ExpressionFactory implements AstExpressionVisitor { .orElseThrow(() -> new VariableDoesNotExistException(name)); } + /** + * Creates a runtime node representing the body of a function. + * + *

In addition to the creation of the node, this method is also responsible for rewriting + * function arguments into a state where they can actually be read. + * + * @param arguments the arguments the function is defined for + * @param expressions the body of the function + * @param retValue the return value of the function + * @return a runtime node representing the function body + */ public ExpressionNode processFunctionBody( - List arguments, List statements, AstExpression retValue) { - List argRewrites = new ArrayList<>(); + List arguments, List expressions, AstExpression retValue) { + + ArgDefinitionFactory argFactory = + new ArgDefinitionFactory(scope, language, scopeName, globalScope); + ArgumentDefinition[] argDefinitions = new ArgumentDefinition[arguments.size()]; + List argExpressions = new ArrayList<>(); + Set seenArgNames = new HashSet<>(); + + // Note [Rewriting Arguments] for (int i = 0; i < arguments.size(); i++) { - FrameSlot slot = scope.createVarSlot(arguments.get(i)); - ReadArgumentNode readArg = new ReadArgumentNode(i); + ArgumentDefinition arg = arguments.get(i).visit(argFactory, i); + argDefinitions[i] = arg; + FrameSlot slot = scope.createVarSlot(arg.getName()); + ReadArgumentNode readArg = new ReadArgumentNode(i, arg.getDefaultValue().orElse(null)); AssignmentNode assignArg = AssignmentNodeGen.create(readArg, slot); - argRewrites.add(assignArg); + argExpressions.add(assignArg); + + String argName = arg.getName(); + + if (seenArgNames.contains(argName)) { + throw new DuplicateArgumentNameException(argName); + } else { + seenArgNames.add(argName); + } } - List statementNodes = - statements.stream().map(stmt -> stmt.visit(this)).collect(Collectors.toList()); - List allStatements = new ArrayList<>(); - allStatements.addAll(argRewrites); - allStatements.addAll(statementNodes); - ExpressionNode expr = retValue.visit(this); - FunctionBodyNode functionBodyNode = - new FunctionBodyNode(allStatements.toArray(new ExpressionNode[0]), expr); - RootNode rootNode = + + List fnBodyExpressionNodes = + expressions.stream().map(stmt -> stmt.visit(this)).collect(Collectors.toList()); + + List allFnExpressions = new ArrayList<>(); + allFnExpressions.addAll(argExpressions); + allFnExpressions.addAll(fnBodyExpressionNodes); + + ExpressionNode returnExpr = retValue.visit(this); + + FunctionBodyNode fnBodyNode = + new FunctionBodyNode(allFnExpressions.toArray(new ExpressionNode[0]), returnExpr); + RootNode fnRootNode = new EnsoRootNode( - language, scope.getFrameDescriptor(), functionBodyNode, null, "lambda::" + scopeName); - RootCallTarget callTarget = Truffle.getRuntime().createCallTarget(rootNode); - return new CreateFunctionNode(callTarget); + language, scope.getFrameDescriptor(), fnBodyNode, null, "lambda::" + scopeName); + RootCallTarget callTarget = Truffle.getRuntime().createCallTarget(fnRootNode); + + return new CreateFunctionNode(callTarget, argDefinitions); } + /* Note [Rewriting Arguments] + * ~~~~~~~~~~~~~~~~~~~~~~~~~~ + * While it would be tempting to handle function arguments as a special case of a lookup, it is + * instead far simpler to rewrite them such that they just become bindings in the function local + * scope. This occurs for both explicitly passed argument values, and those that have been + * defaulted. + * + * For each argument, the following algorithm is executed: + * + * 1. Argument Conversion: Arguments are converted into their definitions so as to provide a + * compact representation of all known information about that argument. + * 2. Frame Conversion: A variable slot is created in the function's local frame to contain the + * value of the function argument. + * 3. Read Provision: A `ReadArgumentNode` is generated to allow that function argument to be + * treated purely as a local variable access. See Note [Handling Argument Defaults] for more + * information on how this works. + * 4. Value Assignment: A `AssignmentNode` is created to connect the argument value to the frame + * slot created in Step 2. + * 5. Body Rewriting: The expression representing the argument is written into the function body, + * thus allowing it to be read simply. + */ + + /** + * Creates a runtime node representing a function. + * + *

Given that most of the work takes place in {@link #processFunctionBody(List, List, + * AstExpression) processFunctionBody}, this node is solely responsible for handling the creation + * of a new scope for the function, and marking it as tail recursive. + * + * @param arguments the arguments to the function + * @param expressions the expressions that make up the function body + * @param retValue the return value of the function + * @return a runtime node representing the function + */ @Override public ExpressionNode visitFunction( - List arguments, List statements, AstExpression retValue) { + List arguments, List expressions, AstExpression retValue) { ExpressionFactory child = createChild(currentVarName); - ExpressionNode fun = child.processFunctionBody(arguments, statements, retValue); + ExpressionNode fun = child.processFunctionBody(arguments, expressions, retValue); fun.markTail(); return fun; } + /** + * Creates a runtime node representing a case function. + * + *

Given that most of the work takes place in {@link #processFunctionBody(List, List, + * AstExpression) processFunctionBody}, this node is solely responsible for handling the creation + * of a new scope for the function. + * + * @param arguments the arguments to the function + * @param expressions the expressions that make up the function body + * @param retValue the return value of the function + * @return a runtime node representing the function + */ @Override - public ExpressionNode visitApplication(AstExpression function, List arguments) { - return InvokeNodeGen.create( - arguments.stream().map(arg -> arg.visit(this)).toArray(ExpressionNode[]::new), - function.visit(this)); + public ExpressionNode visitCaseFunction( + List arguments, List expressions, AstExpression retValue) { + ExpressionFactory child = createChild(currentVarName); + return child.processFunctionBody(arguments, expressions, retValue); } + /** + * Creates a runtime node representing function application. + * + * @param function the function being called + * @param arguments the arguments being applied to the function + * @return a runtime node representing the function call + */ + @Override + public ExpressionNode visitFunctionApplication( + AstExpression function, List arguments) { + CallArgFactory argFactory = new CallArgFactory(scope, language, scopeName, globalScope); + + List callArgs = new ArrayList<>(); + for (int position = 0; position < arguments.size(); ++position) { + CallArgument arg = arguments.get(position).visit(argFactory, position); + callArgs.add(arg); + } + + return InvokeCallableNodeGen.create( + callArgs.stream().toArray(CallArgument[]::new), function.visit(this)); + } + + /** + * Creates a runtime node representing a conditional expression. + * + * @param cond the condition + * @param ifTrue the code to execute if {@code cond} is true + * @param ifFalse the code to execute if {@code cond} is false + * @return a runtime node representing the conditional + */ @Override public ExpressionNode visitIf(AstExpression cond, AstExpression ifTrue, AstExpression ifFalse) { return new IfZeroNode(cond.visit(this), ifTrue.visit(this), ifFalse.visit(this)); } + /** + * Creates a runtime node representing an assignment expression. + * + * @param varName the name of the variable + * @param expr the expression whose result is assigned to {@code varName} + * @return a runtime node representing the assignment + */ @Override public ExpressionNode visitAssignment(String varName, AstExpression expr) { currentVarName = varName; @@ -155,19 +361,25 @@ public class ExpressionFactory implements AstExpressionVisitor { return AssignmentNodeGen.create(expr.visit(this), slot); } + /** + * Creates a runtime node representing a print expression. + * + * @param body an expression that computes the value to print + * @return a runtime node representing the print + */ @Override public ExpressionNode visitPrint(AstExpression body) { return new PrintNode(body.visit(this)); } - @Override - public ExpressionNode visitCaseFunction( - List arguments, List statements, AstExpression retValue) { - ExpressionFactory child = createChild(currentVarName); - ExpressionNode fun = child.processFunctionBody(arguments, statements, retValue); - return fun; - } - + /** + * Creates a runtime node representing a pattern match. + * + * @param target the value to destructure in the pattern match + * @param branches the cases of the pattern match + * @param fallback any fallback case for the pattern match + * @return a runtime node representing a pattern match expression + */ @Override public ExpressionNode visitMatch( AstExpression target, List branches, Optional fallback) { @@ -179,6 +391,8 @@ public class ExpressionFactory implements AstExpressionVisitor { new ConstructorCaseNode( branch.cons().visit(this), branch.function().visit(this))) .toArray(CaseNode[]::new); + + // Note [Pattern Match Fallbacks] CaseNode fallbackNode = fallback .map(fb -> (CaseNode) new FallbackNode(fb.visit(this))) @@ -186,4 +400,12 @@ public class ExpressionFactory implements AstExpressionVisitor { return new MatchNode(targetNode, cases, fallbackNode); } + + /* Note [Pattern Match Fallbacks] + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Enso in its current state has no coverage checking for constructors on pattern matches as it + * has no sense of what constructors contribute to make a 'type'. This means that, in absence of a + * user-provided fallback or catch-all case in a pattern match, the interpreter has to ensure that + * it has one to catch that error. + */ } diff --git a/Interpreter/src/main/java/org/enso/interpreter/builder/FileDetector.java b/Interpreter/src/main/java/org/enso/interpreter/builder/FileDetector.java index bbfb1e12865..92435036203 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/builder/FileDetector.java +++ b/Interpreter/src/main/java/org/enso/interpreter/builder/FileDetector.java @@ -1,13 +1,26 @@ package org.enso.interpreter.builder; import com.oracle.truffle.api.TruffleFile; +import com.oracle.truffle.api.TruffleFile.FileTypeDetector; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import org.apache.tika.detect.DefaultEncodingDetector; +import org.apache.tika.detect.EncodingDetector; import org.enso.interpreter.Constants; -import java.io.IOException; -import java.nio.charset.Charset; - +/** + * A detector for finding a {@link TruffleFile file's} MIME type and encoding. + */ public final class FileDetector implements TruffleFile.FileTypeDetector { + /** + * Finds the MINE type for a given {@link TruffleFile}. + * + * @param file the {@link TruffleFile file} to find a MIME type for + * @return the MIME type or {@code null} if the MIME type is not recognized + * @throws IOException if an I/O error occurs + */ @Override public String findMimeType(TruffleFile file) throws IOException { String name = file.getName(); @@ -17,9 +30,22 @@ public final class FileDetector implements TruffleFile.FileTypeDetector { return null; } + /** + * For a file containing an encoding information returns the encoding. + * + * Enso uses Apache Tika for determining the encoding for a provided file, but this is an inexact + * science at best. + * + * @param file the {@link TruffleFile file} to find an encoding for. It's guaranteed that + * the {@code file} has a MIME type supported by the language registering this + * {@link FileTypeDetector}. + * @return the file encoding or {@code null} if the file does not provide encoding + * @throws IOException if an I/O error occurs + */ @Override public Charset findEncoding(TruffleFile file) throws IOException { - // TODO [AA] Give this a proper implementation. - return null; + InputStream fileReader = file.newInputStream(); + EncodingDetector detector = new DefaultEncodingDetector(); + return detector.detect(fileReader, null); } } diff --git a/Interpreter/src/main/java/org/enso/interpreter/builder/FramePointer.java b/Interpreter/src/main/java/org/enso/interpreter/builder/FramePointer.java deleted file mode 100644 index 324cc7720a2..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/builder/FramePointer.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.enso.interpreter.builder; - -import com.oracle.truffle.api.frame.FrameSlot; - -public class FramePointer { - private final int parentLevel; - private final FrameSlot frameSlot; - - public FramePointer(int parentLevel, FrameSlot frameSlot) { - this.parentLevel = parentLevel; - this.frameSlot = frameSlot; - } - - public int getParentLevel() { - return parentLevel; - } - - public FrameSlot getFrameSlot() { - return frameSlot; - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/builder/GlobalScopeExpressionFactory.java b/Interpreter/src/main/java/org/enso/interpreter/builder/GlobalScopeExpressionFactory.java index 922230c5fec..5f2f9d0585a 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/builder/GlobalScopeExpressionFactory.java +++ b/Interpreter/src/main/java/org/enso/interpreter/builder/GlobalScopeExpressionFactory.java @@ -3,34 +3,69 @@ package org.enso.interpreter.builder; import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.frame.FrameDescriptor; -import org.enso.interpreter.*; +import java.util.List; +import org.enso.interpreter.AstAssignment; +import org.enso.interpreter.AstExpression; +import org.enso.interpreter.AstGlobalScope; +import org.enso.interpreter.AstGlobalScopeVisitor; +import org.enso.interpreter.AstTypeDef; +import org.enso.interpreter.Language; import org.enso.interpreter.node.EnsoRootNode; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.AtomConstructor; - -import java.util.List; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +import org.enso.interpreter.runtime.scope.GlobalScope; +/** + * A {@code GlobalScopeExpressionFactory} is responsible for converting the top-level definitions of + * an Enso program into AST nodes for the interpreter to evaluate. + */ public class GlobalScopeExpressionFactory implements AstGlobalScopeVisitor { - private final Language language; + /** + * Creates a factory for the given language. + * + * @param language the name of the language for which this factory is creating nodes + */ public GlobalScopeExpressionFactory(Language language) { this.language = language; } + /** + * Executes the factory on a global expression. + * + * @param expr the expression to execute on + * @return a runtime node representing the top-level expression + */ public ExpressionNode run(AstGlobalScope expr) { return expr.visit(this); } + /** + * Processes definitions in the language global scope. + * + * @param typeDefs any type definitions defined in the global scope + * @param bindings any bindings made in the global scope + * @param executableExpression the executable expression for the program + * @return a runtime node representing the whole top-level program scope + */ @Override public ExpressionNode visitGlobalScope( - List typeDefs, List bindings, AstExpression expression) { + List typeDefs, List bindings, AstExpression executableExpression) { GlobalScope globalScope = new GlobalScope(); bindings.forEach(binding -> globalScope.registerName(binding.name())); for (AstTypeDef type : typeDefs) { - globalScope.registerConstructor(new AtomConstructor(type.name(), type.getArguments())); + ArgDefinitionFactory argFactory = new ArgDefinitionFactory(language, globalScope); + ArgumentDefinition[] argDefs = new ArgumentDefinition[type.getArguments().size()]; + + for (int i = 0; i < type.getArguments().size(); ++i) { + argDefs[i] = type.getArguments().get(i).visit(argFactory, i); + } + + globalScope.registerConstructor(new AtomConstructor(type.name(), argDefs)); } for (AstAssignment binding : bindings) { @@ -47,6 +82,6 @@ public class GlobalScopeExpressionFactory implements AstGlobalScopeVisitor items; - private FrameDescriptor frameDescriptor; - private LocalScope parent; - - public LocalScope() { - items = new HashMap<>(); - frameDescriptor = new FrameDescriptor(); - parent = null; - } - - public LocalScope(LocalScope parent) { - this(); - this.parent = parent; - } - - public FrameDescriptor getFrameDescriptor() { - return frameDescriptor; - } - - public LocalScope getParent() { - return parent; - } - - public LocalScope createChild() { - return new LocalScope(this); - } - - public FrameSlot createVarSlot(String name) { - if (items.containsKey(name)) throw new VariableRedefinitionException(name); - FrameSlot slot = frameDescriptor.addFrameSlot(name); - items.put(name, slot); - return slot; - } - - public Optional getSlot(String name) { - LocalScope scope = this; - int parentCounter = 0; - while (scope != null) { - FrameSlot slot = scope.items.get(name); - if (slot != null) { - return Optional.of(new FramePointer(parentCounter, slot)); - } - scope = scope.parent; - parentCounter++; - } - return Optional.empty(); - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/BaseNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/BaseNode.java new file mode 100644 index 00000000000..333903b9939 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/BaseNode.java @@ -0,0 +1,45 @@ +package org.enso.interpreter.node; + +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.dsl.ReportPolymorphism; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.NodeInfo; + +/** A base type for all Enso language nodes. */ +@NodeInfo(shortName = "Base", description = "A base node for the Enso AST") +@ReportPolymorphism +public class BaseNode extends Node { + private @CompilationFinal boolean isTail = false; + + /** + * Sets whether the node is tail-recursive. + * + * @param isTail whether or not the node is tail-recursive. + */ + public void setTail(boolean isTail) { + this.isTail = isTail; + } + + /** + * Marks the node as tail-recursive. + */ + final public void markTail() { + setTail(true); + } + + /** + * Marks the node as not tail-recursive. + */ + final public void markNotTail() { + setTail(false); + } + + /** + * Checks if the node is tail-recursive. + * + * @return {@code true} if the node is tail-recursive, otherwise {@code false} + */ + public boolean isTail() { + return isTail; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/EnsoRootNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/EnsoRootNode.java index 688fc036820..cc95a4df4d6 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/EnsoRootNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/EnsoRootNode.java @@ -6,11 +6,26 @@ import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.source.SourceSection; import org.enso.interpreter.Language; +/** + * This node represents the root of all Enso computations. + * + *

All new computations in Enso must be executed from within an {@link EnsoRootNode}, as + * determined by the API provided by Truffle. + */ public class EnsoRootNode extends RootNode { private final String name; private final SourceSection sourceSection; @Child private ExpressionNode body; + /** + * Creates a new root node. + * + * @param language the language identifier + * @param frameDescriptor a description of the stack frame + * @param body the program body to be executed + * @param section a mapping from {@code body} to the program source + * @param name a name for the node + */ public EnsoRootNode( Language language, FrameDescriptor frameDescriptor, @@ -23,17 +38,43 @@ public class EnsoRootNode extends RootNode { this.name = name; } + /** + * Executes the node. + * + * @param frame the stack frame to execute in + * @return the result of executing this node + */ @Override public Object execute(VirtualFrame frame) { return body.executeGeneric(frame); } + /** + * Converts this node to a textual representation good for debugging. + * + * @return a {@link String} representation of this node + */ @Override public String toString() { return this.name; } + /** Marks the node as tail-recursive. */ public void markTail() { body.markTail(); } + + /** Marks the node as not tail-recursive. */ + public void markNotTail() { + body.markNotTail(); + } + + /** + * Sets whether the node is tail-recursive. + * + * @param isTail whether or not the node is tail-recursive. + */ + public void setTail(boolean isTail) { + body.setTail(isTail); + } } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/ExpressionNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/ExpressionNode.java index eeea0b3f3b6..aceb99904af 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/ExpressionNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/ExpressionNode.java @@ -1,53 +1,88 @@ package org.enso.interpreter.node; -import com.oracle.truffle.api.CompilerDirectives; -import com.oracle.truffle.api.dsl.ReportPolymorphism; import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.nodes.UnexpectedResultException; -import org.enso.interpreter.runtime.Atom; -import org.enso.interpreter.runtime.AtomConstructor; -import org.enso.interpreter.runtime.Function; -import org.enso.interpreter.runtime.TypesGen; +import org.enso.interpreter.runtime.callable.atom.Atom; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.type.TypesGen; +/** + * A base class for all Enso expressions. + * + *

Enso is an expression-oriented language, and hence doesn't have any statements. This means + * that all expression execution will return a value, even if that is just the {@link + * AtomConstructor#UNIT} type. + * + *

This class contains specialisations of the {@link #executeGeneric(VirtualFrame) + * executeGeneric} method for various scenarios in order to improve performance. + */ @NodeInfo(shortName = "EnsoExpression", description = "The base node for all enso expressions.") -@ReportPolymorphism -public abstract class ExpressionNode extends Node { - - @CompilerDirectives.CompilationFinal private boolean isTail = false; - - public void markTail() { - isTail = true; - } - - public void markNotTail() { - isTail = false; - } - - public boolean isTail() { - return isTail; - } +public abstract class ExpressionNode extends BaseNode { + /** + * Executes the current node, returning the result as a {@code long}. + * + * @param frame the stack frame for execution + * @return the {@code long} value obtained by executing the node + * @throws UnexpectedResultException if the result cannot be represented as a value of the return + * type + */ public long executeLong(VirtualFrame frame) throws UnexpectedResultException { return TypesGen.expectLong(executeGeneric(frame)); } + /** + * Executes the current node, returning the result as an {@link AtomConstructor}. + * + * @param frame the stack frame for execution + * @return the Atom constructor obtained by executing the node + * @throws UnexpectedResultException if the result cannot be represented as a value of the return + * type + */ public AtomConstructor executeAtomConstructor(VirtualFrame frame) throws UnexpectedResultException { return TypesGen.expectAtomConstructor(executeGeneric(frame)); } + /** + * Executes the current node, returning the result as an {@link Atom}. + * + * @param frame the stack frame for execution + * @return the Atom obtained by executing the node + * @throws UnexpectedResultException if the result cannot be represented as a value of the return + * type + */ public Atom executeAtom(VirtualFrame frame) throws UnexpectedResultException { return TypesGen.expectAtom(executeGeneric(frame)); } + /** + * Executes the current node, returning the result as a {@link Function}. + * + * @param frame the stack frame for execution + * @return the function obtained by executing the node + * @throws UnexpectedResultException if the result cannot be represented as a value of the return + * type + */ public Function executeFunction(VirtualFrame frame) throws UnexpectedResultException { return TypesGen.expectFunction(executeGeneric(frame)); } + /** + * Executes the current node and returns a result. + * + * @param frame the stack frame for execution + * @return the result of executing the node + */ public abstract Object executeGeneric(VirtualFrame frame); + /** + * Executes the current node without returning a result. + * + * @param frame the stack frame for execution + */ public void executeVoid(VirtualFrame frame) { executeGeneric(frame); } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/ExecuteCallNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/ExecuteCallNode.java new file mode 100644 index 00000000000..ca1651ecc40 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/ExecuteCallNode.java @@ -0,0 +1,66 @@ +package org.enso.interpreter.node.callable; + +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.nodes.Node; +import org.enso.interpreter.runtime.callable.function.Function; + +/** + * This node is responsible for optimising function calls. + * + *

Where possible, it will make the call as a 'direct' call, one with no lookup needed, but will + * fall back to performing a lookup if necessary. + */ +public abstract class ExecuteCallNode extends Node { + + /** + * Calls the function directly. + * + *

This specialisation comes into play where the call target for the provided function is + * already cached. THis means that the call can be made quickly. + * + * @param function the function to execute + * @param arguments the arguments passed to {@code function} in the expected positional order + * @param cachedTarget the cached call target for {@code function} + * @param callNode the cached call node for {@code cachedTarget} + * @return the result of executing {@code function} on {@code arguments} + */ + @Specialization(guards = "function.getCallTarget() == cachedTarget") + protected Object callDirect( + Function function, + Object[] arguments, + @Cached("function.getCallTarget()") RootCallTarget cachedTarget, + @Cached("create(cachedTarget)") DirectCallNode callNode) { + return callNode.call(Function.ArgumentsHelper.buildArguments(function, arguments)); + } + + /** + * Calls the function with a lookup. + * + *

This specialisation is used in the case where there is no cached call target for the + * provided function. This is much slower and should, in general, be avoided. + * + * @param function the function to execute + * @param arguments the arguments passed to {@code function} in the expected positional order + * @param callNode the cached call node for making indirect calls + * @return the result of executing {@code function} on {@code arguments} + */ + @Specialization(replaces = "callDirect") + protected Object callIndirect( + Function function, Object[] arguments, @Cached IndirectCallNode callNode) { + return callNode.call( + function.getCallTarget(), Function.ArgumentsHelper.buildArguments(function, arguments)); + } + + /** + * Executes the function call. + * + * @param function the function to execute + * @param arguments the arguments to be passed to {@code function} + * @return the result of executing {@code function} on {@code arguments} + */ + public abstract Object executeCall(Object function, Object[] arguments); +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/InvokeCallableNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/InvokeCallableNode.java new file mode 100644 index 00000000000..932b3e1808e --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/InvokeCallableNode.java @@ -0,0 +1,122 @@ +package org.enso.interpreter.node.callable; + +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.NodeInfo; +import java.util.Arrays; +import org.enso.interpreter.node.ExpressionNode; +import org.enso.interpreter.node.callable.argument.sorter.ArgumentSorterNode; +import org.enso.interpreter.node.callable.argument.sorter.ArgumentSorterNodeGen; +import org.enso.interpreter.node.callable.dispatch.CallOptimiserNode; +import org.enso.interpreter.node.callable.dispatch.SimpleCallOptimiserNode; +import org.enso.interpreter.optimiser.tco.TailCallException; +import org.enso.interpreter.runtime.callable.argument.CallArgument; +import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; +import org.enso.interpreter.runtime.callable.atom.Atom; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.error.NotInvokableException; + +/** + * This node is responsible for organising callable calls so that they are ready to be made. + * + *

It handles computing the values of the arguments to the callable, and also the sorting of + * those arguments into the correct positional order for the callable being called. + */ +@NodeInfo(shortName = "@", description = "Executes function") +@NodeChild(value = "callable", type = ExpressionNode.class) +public abstract class InvokeCallableNode extends ExpressionNode { + @Children + private @CompilationFinal(dimensions = 1) ExpressionNode[] argExpressions; + + @Child private ArgumentSorterNode argumentSorter; + @Child private CallOptimiserNode callOptimiserNode; + + /** + * Creates a new node for performing callable invocation. + * + * @param callArguments information on the arguments being passed to the {@link + * org.enso.interpreter.runtime.callable.Callable} + */ + public InvokeCallableNode(CallArgument[] callArguments) { + this.argExpressions = + Arrays.stream(callArguments) + .map(CallArgument::getExpression) + .toArray(ExpressionNode[]::new); + + CallArgumentInfo[] argSchema = + Arrays.stream(callArguments).map(CallArgumentInfo::new).toArray(CallArgumentInfo[]::new); + + this.callOptimiserNode = new SimpleCallOptimiserNode(); + this.argumentSorter = ArgumentSorterNodeGen.create(argSchema); + } + + /** + * Evaluates the arguments being passed to the callable. + * + * @param frame the stack frame in which to execute + * @return the results of evaluating the function arguments + */ + @ExplodeLoop + public Object[] evaluateArguments(VirtualFrame frame) { + Object[] computedArguments = new Object[this.argExpressions.length]; + + for (int i = 0; i < this.argExpressions.length; ++i) { + computedArguments[i] = this.argExpressions[i].executeGeneric(frame); + } + + return computedArguments; + } + + /** + * Invokes a function directly on the arguments contained in this node. + * + * @param frame the stack frame in which to execute + * @param callable the function to be executed + * @return the result of executing {@code callable} on the known arguments + */ + @Specialization + public Object invokeFunction(VirtualFrame frame, Function callable) { + Object[] evaluatedArguments = evaluateArguments(frame); + Object[] sortedArguments = this.argumentSorter.execute(callable, evaluatedArguments); + + if (this.isTail()) { + throw new TailCallException(callable, sortedArguments); + } else { + return this.callOptimiserNode.executeDispatch(callable, sortedArguments); + } + } + + /** + * Invokes a constructor directly on the arguments contained in this node. + * + * @param frame the stack frame in which to execute + * @param callable the constructor to be executed + * @return the result of executing {@code callable} on the known arguments + */ + @Specialization + public Atom invokeConstructor(VirtualFrame frame, AtomConstructor callable) { + Object[] evaluatedArguments = evaluateArguments(frame); + Object[] sortedArguments = this.argumentSorter.execute(callable, evaluatedArguments); + return callable.newInstance(sortedArguments); + } + + /** + * A fallback that should never be called. + * + *

If this is called, something has gone horribly wrong. It throws a {@link + * NotInvokableException} to signal this. + * + * @param frame the stack frame in which to execute + * @param callable the callable to be executed + * @return error + */ + @Fallback + public Object invokeGeneric(VirtualFrame frame, Object callable) { + throw new NotInvokableException(callable, this); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/ReadArgumentNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/ReadArgumentNode.java new file mode 100644 index 00000000000..3c1b25428f1 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/ReadArgumentNode.java @@ -0,0 +1,75 @@ +package org.enso.interpreter.node.callable.argument; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.profiles.ConditionProfile; +import org.enso.interpreter.node.ExpressionNode; +import org.enso.interpreter.runtime.callable.argument.sentinel.DefaultedArgumentSentinel; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.callable.function.Function.ArgumentsHelper; + +/** + * Reads and evaluates the expression provided as a function argument. It handles the case where + * none is given and the default should be used instead. + */ +@NodeInfo(description = "Read function argument.") +public class ReadArgumentNode extends ExpressionNode { + private final int index; + @Child ExpressionNode defaultValue; + private final ConditionProfile defaultingProfile = ConditionProfile.createCountingProfile(); + private final ConditionProfile argAcquisitionProfile = ConditionProfile.createCountingProfile(); + + /** + * Creates a node to compute a function argument. + * + * @param position the argument's position at the definition site + * @param defaultValue the default value provided for that argument + */ + public ReadArgumentNode(int position, ExpressionNode defaultValue) { + this.index = position; + this.defaultValue = defaultValue; + } + + /** + * Computes the value of an argument in a function. + * + *

This function also handles the defaulted case by checking for a {@code null} value at the + * argument's position. This works in conjunction with {@link + * org.enso.interpreter.runtime.callable.argument.CallArgumentInfo#reorderArguments(int[], + * Object[], int)}, which will place nulls in any position where an argument has not been applied. + * + * @param frame the stack frame to execute in + * @return the computed value of the argument at this position + */ + @Override + public Object executeGeneric(VirtualFrame frame) { + if (defaultValue == null) { + return Function.ArgumentsHelper.getPositionalArguments(frame.getArguments())[index]; + } + + Object argument = null; + + if (argAcquisitionProfile.profile( + index < ArgumentsHelper.getPositionalArguments(frame.getArguments()).length)) { + argument = Function.ArgumentsHelper.getPositionalArguments(frame.getArguments())[index]; + } + + // Note [Handling Argument Defaults] + if (defaultingProfile.profile(argument instanceof DefaultedArgumentSentinel || argument == null)) { + return defaultValue.executeGeneric(frame); + } else { + return argument; + } + } + + /* Note [Handling Argument Defaults] + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * While it is tempting to handle defaulted arguments as a special case, we instead treat them as + * the absence of an argument for that position in the function definition. If none is provided, + * we can detect this using a sentinel, and hence evaluate the default value in its place. + * + * Any `null` value is treated as a usage of the default argument, so when this execution + * encounters a null value at the argument position, then it will instead execute the default + * value. + */ +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/ArgumentSorterNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/ArgumentSorterNode.java new file mode 100644 index 00000000000..c05fa39354b --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/ArgumentSorterNode.java @@ -0,0 +1,137 @@ +package org.enso.interpreter.node.callable.argument.sorter; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.NodeInfo; +import java.util.Arrays; +import org.enso.interpreter.node.BaseNode; +import org.enso.interpreter.runtime.callable.Callable; +import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; +import org.enso.interpreter.runtime.error.NotInvokableException; + +/** + * This class represents the protocol for remapping the arguments provided at a call site into the + * positional order expected by the definition of the {@link Callable}. + */ +@NodeInfo(shortName = "ArgumentSorter") +public abstract class ArgumentSorterNode extends BaseNode { + private @CompilationFinal(dimensions = 1) CallArgumentInfo[] schema; + private @CompilationFinal boolean isFullyPositional; + + /** + * Creates a node that performs the argument organisation for the provided schema. + * + * @param schema information about the call arguments in positional order + */ + public ArgumentSorterNode(CallArgumentInfo[] schema) { + this.schema = schema; + this.isFullyPositional = Arrays.stream(schema).allMatch(CallArgumentInfo::isPositional); + } + + /** + * Generates the argument mapping in the fully positional case. + * + *

This specialisation is executed when all of the arguments provided at the call-site are + * specified in a purely positional fashion (no ignores and no named argument applications). This + * is intended to be a very quick path. + * + * @param callable the callable to sort arguments for + * @param arguments the arguments being passed to {@code callable} + * @return the provided {@code} arguments in the order expected by {@code callable} + */ + @Specialization(guards = "isFullyPositional()") + public Object[] invokePositional(Object callable, Object[] arguments) { + CompilerDirectives.ensureVirtualizedHere(arguments); + return arguments; + } + + /** + * Generates the argument mapping where it has already been computed. + * + *

This specialisation is executed in the cases where the interpreter has already computed the + * mapping necessary to reorder call-stite arguments into the order expected by the definition + * site. It is also a fast path. + * + *

This specialisation can only execute when the {@link Callable} provided to the method + * matches with the one stored in the cached argument sorter object. + * + * @param callable the callable to sort arguments for + * @param arguments the arguments being passed to {@code callable} + * @param mappingNode a cached node that tracks information about the mapping to enable a fast + * path + * @return the provided {@code arguments} in the order expected by {@code callable} + */ + @Specialization(guards = "mappingNode.hasSameCallable(callable)") + @ExplodeLoop + public Object[] invokeCached( + Callable callable, + Object[] arguments, + @Cached("create(callable, getSchema())") CachedArgumentSorterNode mappingNode) { + return mappingNode.execute(arguments, callable.getArgs().length); + } + + /** + * Generates the argument mapping freshly. + * + *

This specialisation is executed only when we cache miss, and will compute the argument + * mapping freshly each time. + * + * @param callable the callable to sort arguments for + * @param arguments the arguments being passed to {@code callable} + * @param mappingNode a cached node that computes argument mappings freshly each time + * @return the provided {@code arguments} in the order expected by {@code callable} + */ + @Specialization + public Object[] invokeUncached( + Callable callable, + Object[] arguments, + @Cached("create(getSchema())") UncachedArgumentSorterNode mappingNode) { + return mappingNode.execute(callable, arguments, callable.getArgs().length); + } + + /** + * A fallback that should never be called. + * + *

The only cases in which this specialisation can be called are system-wide error conditions, + * and so we stop with a {@link NotInvokableException}. + * + * @param callable the callable to sort arguments for + * @param arguments the arguments being passed to {@code callable} + * @return error + */ + @Fallback + public Object[] invokeGeneric(Object callable, Object[] arguments) { + throw new NotInvokableException(callable, this); + } + + /** + * Executes the {@link ArgumentSorterNode} to reorder the arguments. + * + * @param callable the callable to sort arguments for + * @param arguments the arguments being passed to {@code callable} + * @return the provided {@code arguments} in the order expected by {@code callable} + */ + public abstract Object[] execute(Object callable, Object[] arguments); + + /** + * Gets the schema for use in Truffle DSL guards. + * + * @return the argument schema + */ + CallArgumentInfo[] getSchema() { + return schema; + } + + /** + * Checks if the argument schema is fully positional for use in Truffle DLS guards. + * + * @return {@code true} if the arguments are all positional, otherwise {@false} + */ + boolean isFullyPositional() { + return isFullyPositional; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/CachedArgumentSorterNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/CachedArgumentSorterNode.java new file mode 100644 index 00000000000..1a645f7060c --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/CachedArgumentSorterNode.java @@ -0,0 +1,80 @@ +package org.enso.interpreter.node.callable.argument.sorter; + +import com.oracle.truffle.api.CompilerAsserts; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.profiles.ConditionProfile; +import org.enso.interpreter.node.BaseNode; +import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.type.TypesGen; + +/** + * This class handles the case where a mapping for reordering arguments to a given callable has + * already been computed. + */ +@NodeInfo(shortName = "CachedArgumentSorter") +public class CachedArgumentSorterNode extends BaseNode { + private @CompilationFinal Object callable; + private @CompilationFinal(dimensions = 1) int[] mapping; + private final ConditionProfile otherIsAtomCons = ConditionProfile.createCountingProfile(); + private final ConditionProfile otherIsFunction = ConditionProfile.createCountingProfile(); + private @CompilationFinal boolean callableIsFunction; + + /** + * Creates a node that generates and then caches the argument mapping. + * + * @param callable the callable to sort arguments for + * @param schema information on the calling arguments + */ + public CachedArgumentSorterNode(Object callable, CallArgumentInfo[] schema) { + this.callable = callable; + this.mapping = CallArgumentInfo.generateArgMapping(callable, schema); + + this.callableIsFunction = TypesGen.isFunction(callable); + CompilerAsserts.compilationConstant(callableIsFunction); + } + + /** + * Creates a node that generates and then caches the argument mapping. + * + * @param callable the callable to sort arguments for + * @param schema information on the calling arguments + * @return a sorter node for the arguments in {@code schema} being passed to {@code callable} + */ + public static CachedArgumentSorterNode create(Object callable, CallArgumentInfo[] schema) { + return new CachedArgumentSorterNode(callable, schema); + } + + /** + * Reorders the provided arguments into the necessary order for the cached callable. + * + * @param arguments the arguments to reorder + * @param numArgsDefinedForCallable the number of arguments that the cached callable was defined + * for + * @return the provided {@code arguments} in the order expected by the cached {@link + * org.enso.interpreter.runtime.callable.Callable} + */ + @ExplodeLoop + public Object[] execute(Object[] arguments, int numArgsDefinedForCallable) { + return CallArgumentInfo.reorderArguments(this.mapping, arguments, numArgsDefinedForCallable); + } + + /** + * Determines if the provided callable is the same as the cached one. + * + * @param other the callable to check for equality + * @return {@code true} if {@code other} matches the cached callable, otherwise {@code false} + */ + public boolean hasSameCallable(Object other) { + if (otherIsAtomCons.profile(TypesGen.isAtomConstructor(other))) { + return this.callable == other; + } else if (this.callableIsFunction) { + if (otherIsFunction.profile(TypesGen.isFunction(other))) { + return ((Function) this.callable).getCallTarget() == ((Function) other).getCallTarget(); + } + } + return false; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/UncachedArgumentSorterNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/UncachedArgumentSorterNode.java new file mode 100644 index 00000000000..6cb3d80861b --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/argument/sorter/UncachedArgumentSorterNode.java @@ -0,0 +1,59 @@ +package org.enso.interpreter.node.callable.argument.sorter; + +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.profiles.ConditionProfile; +import org.enso.interpreter.node.BaseNode; +import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.error.NotInvokableException; +import org.enso.interpreter.runtime.type.TypesGen; + +/** + * This class handles the case where we have no cached mapping for a given callable's arguments, and + * will compute it on the fly. + */ +@NodeInfo(shortName = "UncachedArgumentSorter") +public class UncachedArgumentSorterNode extends BaseNode { + private @CompilationFinal(dimensions = 1) CallArgumentInfo[] schema; + private final ConditionProfile isCallableProfile = ConditionProfile.createCountingProfile(); + + /** + * Creates a node to sort arguments on the fly. + * + * @param schema information on the calling arguments + */ + public UncachedArgumentSorterNode(CallArgumentInfo[] schema) { + this.schema = schema; + } + + /** + * Creates a node to sort arguments on the fly. + * + * @param schema information on the calling arguments + * @return a sorter node for the arguments in {@code schema} for an unknown callable + */ + public static UncachedArgumentSorterNode create(CallArgumentInfo[] schema) { + return new UncachedArgumentSorterNode(schema); + } + + /** + * Reorders the provided {@code arguments} for the given {@link + * org.enso.interpreter.runtime.callable.Callable}. + * + * @param callable the callable to reorder arguments for + * @param arguments the arguments passed to {@code callable} + * @param numArgsDefinedForCallable the number of arguments defined on {@code callable} + * @return the provided {@code arguments} in the order expected by the cached {@link + * org.enso.interpreter.runtime.callable.Callable} + */ + public Object[] execute(Object callable, Object[] arguments, int numArgsDefinedForCallable) { + if (isCallableProfile.profile(TypesGen.isCallable(callable))) { + Function actualCallable = (Function) callable; + int[] order = CallArgumentInfo.generateArgMapping(actualCallable, this.schema); + return CallArgumentInfo.reorderArguments(order, arguments, numArgsDefinedForCallable); + } else { + throw new NotInvokableException(callable, this); + } + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/CallOptimiserNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/CallOptimiserNode.java new file mode 100644 index 00000000000..f7165b53809 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/CallOptimiserNode.java @@ -0,0 +1,19 @@ +package org.enso.interpreter.node.callable.dispatch; + +import com.oracle.truffle.api.nodes.Node; + +/** + * This node handles optimising calls. It performs detection based on the kind of call being made, + * and then executes the call in the most efficient manner possible for it. + */ +public abstract class CallOptimiserNode extends Node { + + /** + * Calls the provided {@code callable} using the provided {@code arguments}. + * + * @param callable the callable to execute + * @param arguments the arguments to {@code callable} + * @return the result of executing {@code callable} using {@code arguments} + */ + public abstract Object executeDispatch(Object callable, Object[] arguments); +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/LoopingCallOptimiserNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/LoopingCallOptimiserNode.java new file mode 100644 index 00000000000..32426788f58 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/LoopingCallOptimiserNode.java @@ -0,0 +1,139 @@ +package org.enso.interpreter.node.callable.dispatch; + +import com.oracle.truffle.api.Truffle; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameSlot; +import com.oracle.truffle.api.frame.FrameSlotKind; +import com.oracle.truffle.api.frame.FrameUtil; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.LoopNode; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.RepeatingNode; +import org.enso.interpreter.node.callable.ExecuteCallNode; +import org.enso.interpreter.node.callable.ExecuteCallNodeGen; +import org.enso.interpreter.optimiser.tco.TailCallException; + +/** + * A version of {@link CallOptimiserNode} that is fully prepared to handle tail calls. Tail calls + * are handled through exceptions – whenever a tail-recursive call would be executed, an exception + * containing the next unevaluated call and arguments is thrown instead. + * + *

This node executes the function in a loop, following all the continuations, until obtaining + * the actual return value. + * + * @see TailCallException + */ +public class LoopingCallOptimiserNode extends CallOptimiserNode { + private final FrameDescriptor loopFrameDescriptor = new FrameDescriptor(); + @Child private LoopNode loopNode; + + /** Creates a new node for computing tail-call-optimised functions. */ + public LoopingCallOptimiserNode() { + loopNode = Truffle.getRuntime().createLoopNode(new RepeatedCallNode(loopFrameDescriptor)); + } + + /** + * Calls the provided {@code callable} using the provided {@code arguments}. + * + * @param callable the callable to execute + * @param arguments the arguments to {@code callable} + * @return the result of executing {@code callable} using {@code arguments} + */ + @Override + public Object executeDispatch(Object callable, Object[] arguments) { + VirtualFrame frame = Truffle.getRuntime().createVirtualFrame(null, loopFrameDescriptor); + ((RepeatedCallNode) loopNode.getRepeatingNode()).setNextCall(frame, callable, arguments); + loopNode.executeLoop(frame); + + return ((RepeatedCallNode) loopNode.getRepeatingNode()).getResult(frame); + } + + /** + * This node handles the actually looping computation computed in a tail-recursive function. It + * will execute the computation repeatedly and is intended to be used in the context of a Truffle + * {@link RepeatingNode}. + */ + public static final class RepeatedCallNode extends Node implements RepeatingNode { + private final FrameSlot resultSlot; + private final FrameSlot functionSlot; + private final FrameSlot argsSlot; + @Child private ExecuteCallNode dispatchNode; + + /** + * Creates a new node used for repeating a call. + * + * @param descriptor a handle to the slots of the current stack frame + */ + public RepeatedCallNode(FrameDescriptor descriptor) { + functionSlot = descriptor.findOrAddFrameSlot("", FrameSlotKind.Object); + resultSlot = descriptor.findOrAddFrameSlot("", FrameSlotKind.Object); + argsSlot = descriptor.findOrAddFrameSlot("", FrameSlotKind.Object); + dispatchNode = ExecuteCallNodeGen.create(); + } + + /** + * Sets the call target for the next call made using {@code frame}. + * + * @param frame the stack frame for execution + * @param function the function to execute in {@code frame} + * @param arguments the arguments to execute {@code function} with + */ + public void setNextCall(VirtualFrame frame, Object function, Object[] arguments) { + frame.setObject(functionSlot, function); + frame.setObject(argsSlot, arguments); + } + + /** + * Obtains the result of looping execution. + * + * @param frame the stack frame for execution + * @return the result of execution in {@code frame} + */ + public Object getResult(VirtualFrame frame) { + return FrameUtil.getObjectSafe(frame, resultSlot); + } + + /** + * Generates the next call to be made during looping execution. + * + * @param frame the stack frame for execution + * @return the function to be executed next in the loop + */ + public Object getNextFunction(VirtualFrame frame) { + Object result = FrameUtil.getObjectSafe(frame, functionSlot); + frame.setObject(functionSlot, null); + return result; + } + + /** + * Generates the next set of arguments to the looping function. + * + * @param frame the stack frame for execution + * @return the arguments to be applied to the next function + */ + public Object[] getNextArgs(VirtualFrame frame) { + Object[] result = (Object[]) FrameUtil.getObjectSafe(frame, argsSlot); + frame.setObject(argsSlot, null); + return result; + } + + /** + * Executes the node in a repeating fashion until the call is complete. + * + * @param frame the stack frame for execution + * @return {@code true} if execution is continuing, {@code false} otherwise + */ + @Override + public boolean executeRepeating(VirtualFrame frame) { + try { + Object function = getNextFunction(frame); + Object[] arguments = getNextArgs(frame); + frame.setObject(resultSlot, dispatchNode.executeCall(function, arguments)); + return false; + } catch (TailCallException e) { + setNextCall(frame, e.getFunction(), e.getArguments()); + return true; + } + } + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/SimpleCallOptimiserNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/SimpleCallOptimiserNode.java new file mode 100644 index 00000000000..f6851e48c75 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/dispatch/SimpleCallOptimiserNode.java @@ -0,0 +1,35 @@ +package org.enso.interpreter.node.callable.dispatch; + +import com.oracle.truffle.api.CompilerDirectives; +import org.enso.interpreter.node.callable.ExecuteCallNode; +import org.enso.interpreter.node.callable.ExecuteCallNodeGen; +import org.enso.interpreter.optimiser.tco.TailCallException; + +/** + * Optimistic version of {@link CallOptimiserNode} for the non tail call recursive case. Tries to + * just call the function. If that turns out to be a tail call, it replaces itself with a {@link + * LoopingCallOptimiserNode}. Thanks to this design, the (much more common) case of calling a + * function in a non-tail position does not force the overhead of loop. + */ +public class SimpleCallOptimiserNode extends CallOptimiserNode { + @Child private ExecuteCallNode executeCallNode = ExecuteCallNodeGen.create(); + + /** + * Calls the provided {@code callable} using the provided {@code arguments}. + * + * @param callable the callable to execute + * @param arguments the arguments to {@code callable} + * @return the result of executing {@code callable} using {@code arguments} + */ + @Override + public Object executeDispatch(Object callable, Object[] arguments) { + try { + return executeCallNode.executeCall(callable, arguments); + } catch (TailCallException e) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + CallOptimiserNode replacement = new LoopingCallOptimiserNode(); + this.replace(replacement); + return replacement.executeDispatch(e.getFunction(), e.getArguments()); + } + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/function/CreateFunctionNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/function/CreateFunctionNode.java new file mode 100644 index 00000000000..5b9cb3d9da6 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/function/CreateFunctionNode.java @@ -0,0 +1,62 @@ +package org.enso.interpreter.node.callable.function; + +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.frame.MaterializedFrame; +import com.oracle.truffle.api.frame.VirtualFrame; +import org.enso.interpreter.node.EnsoRootNode; +import org.enso.interpreter.node.ExpressionNode; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; +import org.enso.interpreter.runtime.callable.function.Function; + +/** + * This node is responsible for representing the definition of a function. It contains information + * about the function's arguments, as well as the target for calling said function. + */ +public class CreateFunctionNode extends ExpressionNode { + private final RootCallTarget callTarget; + private final ArgumentDefinition[] args; + + /** + * Creates a new node to represent a function definition. + * + * @param callTarget the target for calling the function represented by this node + * @param args information on the arguments to the function + */ + public CreateFunctionNode(RootCallTarget callTarget, ArgumentDefinition[] args) { + this.callTarget = callTarget; + this.args = args; + } + + /** + * Sets the tail-recursiveness of the function. + * + * @param isTail whether or not the function is tail-recursive + */ + @Override + public void setTail(boolean isTail) { + ((EnsoRootNode) callTarget.getRootNode()).setTail(isTail); + } + + /** + * Generates the provided function definition in the given stack {@code frame}. + * + * @param frame the stack frame for execution + * @return the function defined by this node + */ + @Override + public Function executeFunction(VirtualFrame frame) { + MaterializedFrame scope = frame.materialize(); + return new Function(callTarget, scope, this.args); + } + + /** + * Executes the current node. + * + * @param frame the stack frame for execution + * @return the result of executing the node + */ + @Override + public Object executeGeneric(VirtualFrame frame) { + return executeFunction(frame); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/callable/function/FunctionBodyNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/callable/function/FunctionBodyNode.java new file mode 100644 index 00000000000..4c94ae42477 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/node/callable/function/FunctionBodyNode.java @@ -0,0 +1,52 @@ +package org.enso.interpreter.node.callable.function; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.NodeInfo; +import org.enso.interpreter.node.ExpressionNode; + +/** + * This node defines the body of a function for execution, as well as the protocol for executing the + * function body. + */ +@NodeInfo(shortName = "{}") +public class FunctionBodyNode extends ExpressionNode { + @Children private final ExpressionNode[] statements; + @Child private ExpressionNode returnExpr; + + /** + * Creates a new node to represent the body of a function. + * + * @param expressions the function body + * @param returnExpr the return expression from the function + */ + public FunctionBodyNode(ExpressionNode[] expressions, ExpressionNode returnExpr) { + this.statements = expressions; + this.returnExpr = returnExpr; + } + + /** + * Sets whether or not the function is tail-recursive. + * + * @param isTail whether or not the function is tail-recursive. + */ + @Override + public void setTail(boolean isTail) { + returnExpr.setTail(isTail); + } + + /** + * Executes the body of the function. + * + * @param frame the stack frame for execution + * @return the result of executing this function + */ + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + for (ExpressionNode statement : statements) { + statement.executeVoid(frame); + } + return returnExpr.executeGeneric(frame); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/BranchSelectedException.java b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/BranchSelectedException.java index a404786e483..51c8866ad7b 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/BranchSelectedException.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/BranchSelectedException.java @@ -2,13 +2,24 @@ package org.enso.interpreter.node.controlflow; import com.oracle.truffle.api.nodes.ControlFlowException; +/** This exception is used to signal when a certain branch in a case expression has been taken. */ public class BranchSelectedException extends ControlFlowException { private final Object result; + /** + * Creates a new exception instance. + * + * @param result the result of executing the branch this is thrown from + */ public BranchSelectedException(Object result) { this.result = result; } + /** + * Gets the result of executing the case branch in question. + * + * @return the result of executing the case branch from which this is thrown + */ public Object getResult() { return result; } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/CaseNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/CaseNode.java index 5b106c775c1..610cad76b61 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/CaseNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/CaseNode.java @@ -1,11 +1,19 @@ package org.enso.interpreter.node.controlflow; import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.UnexpectedResultException; -import org.enso.interpreter.runtime.Atom; +import org.enso.interpreter.node.BaseNode; +import org.enso.interpreter.runtime.callable.atom.Atom; -public abstract class CaseNode extends Node { - public void markTail() {} +/** An abstract representation of a case expression. */ +public abstract class CaseNode extends BaseNode { + /** + * Executes the case expression. + * + * @param frame the stack frame in which to execute + * @param target the constructor to destructure + * @throws UnexpectedResultException when the result of desctructuring {@code target} can't be + * represented as a value of the expected return type + */ public abstract void execute(VirtualFrame frame, Atom target) throws UnexpectedResultException; } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/ConstructorCaseNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/ConstructorCaseNode.java index c982326b494..9c74dc86813 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/ConstructorCaseNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/ConstructorCaseNode.java @@ -4,33 +4,56 @@ import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.UnexpectedResultException; import com.oracle.truffle.api.profiles.ConditionProfile; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.node.function.CallNode; -import org.enso.interpreter.node.function.CallNodeGen; -import org.enso.interpreter.runtime.Atom; -import org.enso.interpreter.runtime.AtomConstructor; -import org.enso.interpreter.runtime.Function; +import org.enso.interpreter.node.callable.ExecuteCallNode; +import org.enso.interpreter.node.callable.ExecuteCallNodeGen; +import org.enso.interpreter.runtime.callable.atom.Atom; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +import org.enso.interpreter.runtime.callable.function.Function; +/** An implementation of the case expression specialised to working on explicit constructors. */ public class ConstructorCaseNode extends CaseNode { @Child private ExpressionNode matcher; @Child private ExpressionNode branch; - @Child private CallNode callNode = CallNodeGen.create(); + @Child private ExecuteCallNode executeCallNode = ExecuteCallNodeGen.create(); private final ConditionProfile profile = ConditionProfile.createCountingProfile(); + /** + * Creates a new node for handling matching on a case expression. + * + * @param matcher the expression to use for matching + * @param branch the expression to be executed if (@code matcher} matches + */ public ConstructorCaseNode(ExpressionNode matcher, ExpressionNode branch) { this.matcher = matcher; this.branch = branch; } + /** + * Sets whether or not the case expression is tail recursive. + * + * @param isTail whether or not the case expression is tail-recursive + */ @Override - public void markTail() { - branch.markTail(); + public void setTail(boolean isTail) { + branch.setTail(isTail); } + /** + * Executes the case expression. + * + *

It has no direct return value and instead uses a {@link BranchSelectedException} to signal + * the correct result back to the parent of the case expression. + * + * @param frame the stack frame in which to execute + * @param target the constructor to destructure + * @throws UnexpectedResultException when the result of desctructuring {@code target} can't be + * represented as a value of the expected return type + */ public void execute(VirtualFrame frame, Atom target) throws UnexpectedResultException { AtomConstructor matcherVal = matcher.executeAtomConstructor(frame); if (profile.profile(matcherVal == target.getConstructor())) { Function function = branch.executeFunction(frame); - throw new BranchSelectedException(callNode.executeCall(function, target.getFields())); + throw new BranchSelectedException(executeCallNode.executeCall(function, target.getFields())); } } } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/DefaultFallbackNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/DefaultFallbackNode.java index 9c16731300f..2a2dc5be692 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/DefaultFallbackNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/DefaultFallbackNode.java @@ -1,10 +1,21 @@ package org.enso.interpreter.node.controlflow; import com.oracle.truffle.api.frame.VirtualFrame; -import org.enso.interpreter.runtime.Atom; -import org.enso.interpreter.runtime.errors.InexhaustivePatternMatchException; +import org.enso.interpreter.runtime.callable.atom.Atom; +import org.enso.interpreter.runtime.error.InexhaustivePatternMatchException; +/** + * This node is used when there is no explicit default case provided by the user for a pattern + * match. It is used to signal inexhaustivity. + */ public class DefaultFallbackNode extends CaseNode { + + /** + * Executes the case expression's error case. + * + * @param frame the stack frame in which to execute + * @param target the constructor to destructure + */ @Override public void execute(VirtualFrame frame, Atom target) { throw new InexhaustivePatternMatchException(this.getParent()); diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/FallbackNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/FallbackNode.java index 77bd20adc8f..feb349bf8b3 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/FallbackNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/FallbackNode.java @@ -3,22 +3,42 @@ package org.enso.interpreter.node.controlflow; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.UnexpectedResultException; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.node.function.CallNode; -import org.enso.interpreter.node.function.CallNodeGen; -import org.enso.interpreter.runtime.Atom; -import org.enso.interpreter.runtime.Function; +import org.enso.interpreter.node.callable.ExecuteCallNode; +import org.enso.interpreter.node.callable.ExecuteCallNodeGen; +import org.enso.interpreter.runtime.callable.atom.Atom; +import org.enso.interpreter.runtime.callable.function.Function; +/** + * This node represents an explicit catch-call case in a pattern match, as provided by the user. It + * executes the catch-all case code. + */ public class FallbackNode extends CaseNode { @Child private ExpressionNode functionNode; - @Child private CallNode callNode = CallNodeGen.create(); + @Child private ExecuteCallNode executeCallNode = ExecuteCallNodeGen.create(); + /** + * Creates a node to handle the case catch-call. + * + * @param functionNode the function to execute in this case + */ public FallbackNode(ExpressionNode functionNode) { this.functionNode = functionNode; } + /** + * Executes the case expression catch-all case. + * + *

It has no direct return value and instead uses a {@link BranchSelectedException} to signal + * the correct result back to the parent of the case expression. + * + * @param frame the stack frame in which to execute + * @param target the constructor to destructure + * @throws UnexpectedResultException when the result of desctructuring {@code target} can't be + * represented as a value of the expected return type + */ @Override public void execute(VirtualFrame frame, Atom target) throws UnexpectedResultException { Function function = functionNode.executeFunction(frame); - throw new BranchSelectedException(callNode.executeCall(function, new Object[0])); + throw new BranchSelectedException(executeCallNode.executeCall(function, new Object[0])); } } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/IfZeroNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/IfZeroNode.java index 943f24ea00d..b95f9558c2e 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/IfZeroNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/IfZeroNode.java @@ -5,8 +5,12 @@ import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.nodes.UnexpectedResultException; import com.oracle.truffle.api.profiles.ConditionProfile; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.errors.TypeError; +import org.enso.interpreter.runtime.error.TypeError; +/** + * This node represents a very simple form of the if-then-else expression, specialised to the case + * of checking whether its input is equal to zero. + */ @NodeInfo(shortName = "if_then_else", description = "if arg0 = 0 then arg1 else arg2") public class IfZeroNode extends ExpressionNode { private final ConditionProfile conditionProf = ConditionProfile.createCountingProfile(); @@ -14,12 +18,26 @@ public class IfZeroNode extends ExpressionNode { @Child private ExpressionNode ifTrue; @Child private ExpressionNode ifFalse; + /** + * Creates a new conditional expression node. + * + * @param condition the condition to execute and check for equality with zero + * @param ifTrue the code to execute if {@code condition} is true + * @param ifFalse the code to execute if {@code condition} is false + */ public IfZeroNode(ExpressionNode condition, ExpressionNode ifTrue, ExpressionNode ifFalse) { this.condition = condition; this.ifTrue = ifTrue; this.ifFalse = ifFalse; } + /** + * Executes the conditional expression. + * + * @param frame the stack frame for execution + * @return the result of the {@code isTrue} branch if the condition is {@code true}, otherwise the + * result of the {@code isFalse} branch + */ @Override public Object executeGeneric(VirtualFrame frame) { if (conditionProf.profile(executeCondition(frame) == 0)) { @@ -29,6 +47,12 @@ public class IfZeroNode extends ExpressionNode { } } + /** + * Executes the condition to be checked. + * + * @param frame the stack frame for execution + * @return the result of executing the condition + */ private long executeCondition(VirtualFrame frame) { try { return condition.executeLong(frame); @@ -37,9 +61,14 @@ public class IfZeroNode extends ExpressionNode { } } + /** + * Sets whether the conditional is tail-recursive or not. + * + * @param isTail whether or not the conditional is tail recursive + */ @Override - public void markTail() { - ifTrue.markTail(); - ifFalse.markTail(); + public void setTail(boolean isTail) { + ifTrue.setTail(isTail); + ifFalse.setTail(isTail); } } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/MatchNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/MatchNode.java index b5465db6e0a..ec3d21b2838 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/MatchNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/controlflow/MatchNode.java @@ -6,28 +6,49 @@ import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.UnexpectedResultException; import com.oracle.truffle.api.profiles.BranchProfile; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.Atom; -import org.enso.interpreter.runtime.errors.TypeError; +import org.enso.interpreter.runtime.callable.atom.Atom; +import org.enso.interpreter.runtime.error.TypeError; +/** A node representing a pattern match on an Atom. */ public class MatchNode extends ExpressionNode { @Child private ExpressionNode target; @Children private final CaseNode[] cases; @Child private CaseNode fallback; private final BranchProfile typeErrorProfile = BranchProfile.create(); + /** + * Creates a node that pattern matches on an Atom. + * + * @param target the atom to pattern match on + * @param cases the branches of the pattern match + * @param fallback the fallback case of the pattern match + */ public MatchNode(ExpressionNode target, CaseNode[] cases, CaseNode fallback) { this.target = target; this.cases = cases; this.fallback = fallback; } - @Override public void markTail() { - for (CaseNode caseNode: cases) { - caseNode.markTail(); + /** + * Sets whether or not the pattern match is tail-recursive. + * + * @param isTail whether or not the expression is tail-recursive + */ + @Override + @ExplodeLoop + public void setTail(boolean isTail) { + for (CaseNode caseNode : cases) { + caseNode.setTail(isTail); } - fallback.markTail(); + fallback.setTail(isTail); } + /** + * Executes the pattern match. + * + * @param frame the stack frame for execution + * @return the result of the pattern match + */ @ExplodeLoop @Override public Object executeGeneric(VirtualFrame frame) { @@ -39,11 +60,27 @@ public class MatchNode extends ExpressionNode { fallback.execute(frame, atom); CompilerDirectives.transferToInterpreter(); throw new RuntimeException("Impossible behavior."); + } catch (BranchSelectedException e) { + // Note [Branch Selection Control Flow] return e.getResult(); + } catch (UnexpectedResultException e) { typeErrorProfile.enter(); throw new TypeError("Expected an Atom.", this); } } + + /* Note [Branch Selection Control Flow] + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Truffle provides no easy way to return control flow from multiple paths. This is entirely due + * to Java's (current) lack of support for case-expressions. + * + * As a result, this implementation resorts to using an exception to short-circuit evaluation on + * a successful match. This short-circuiting also allows us to hand the result of evaluation back + * out to the caller (here) wrapped in the exception. + * + * The main alternative to this was desugaring to a nested-if, which would've been significantly + * harder to maintain, and also resulted in significantly higher code complexity. + */ } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/builtin/PrintNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/builtin/PrintNode.java index 441f5e7e95d..e2afcb33a2f 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/builtin/PrintNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/builtin/PrintNode.java @@ -4,17 +4,28 @@ import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.NodeInfo; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.AtomConstructor; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +/** This node allows for printing the result of an arbitrary expression to standard output. */ @NodeInfo(shortName = "print", description = "Prints the value of child expression.") public final class PrintNode extends ExpressionNode { - @Child private ExpressionNode expression; + /** + * Creates a node that prints the result of its expression. + * + * @param expression the expression to print the result of + */ public PrintNode(ExpressionNode expression) { this.expression = expression; } + /** + * Executes the print node. + * + * @param frame the stack frame for execution + * @return unit {@link AtomConstructor#UNIT unit} type + */ @Override public Object executeGeneric(VirtualFrame frame) { doPrint(expression.executeGeneric(frame)); @@ -22,6 +33,11 @@ public final class PrintNode extends ExpressionNode { return AtomConstructor.UNIT.newInstance(); } + /** + * Prints the provided value to standard output. + * + * @param object the value to print + */ @CompilerDirectives.TruffleBoundary private void doPrint(Object object) { System.out.println(object); diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/constant/ConstructorNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/constant/ConstructorNode.java index b220c50cfe7..45a782f1e5e 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/constant/ConstructorNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/constant/ConstructorNode.java @@ -2,20 +2,38 @@ package org.enso.interpreter.node.expression.constant; import com.oracle.truffle.api.frame.VirtualFrame; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.AtomConstructor; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +/** Represents a type constructor definition. */ public class ConstructorNode extends ExpressionNode { private final AtomConstructor constructor; + /** + * Creates a new type constructor definition. + * + * @param constructor the constructor to define + */ public ConstructorNode(AtomConstructor constructor) { this.constructor = constructor; } + /** + * Executes the type constructor definition. + * + * @param frame the frame to execute in + * @return the constructor of the type defined + */ @Override public AtomConstructor executeAtomConstructor(VirtualFrame frame) { return constructor; } + /** + * Executes the type constructor definition. + * + * @param frame the frame to execute in + * @return the constructor of the type defined + */ @Override public Object executeGeneric(VirtualFrame frame) { return constructor; diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/literal/IntegerLiteralNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/literal/IntegerLiteralNode.java index 3b8d59bbb2d..2c77f373590 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/literal/IntegerLiteralNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/literal/IntegerLiteralNode.java @@ -3,14 +3,26 @@ package org.enso.interpreter.node.expression.literal; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.NodeInfo; +/** A representation of integer literals in Enso. */ @NodeInfo(shortName = "IntegerLiteral") public final class IntegerLiteralNode extends LiteralNode { private final long value; + /** + * Creates a new integer literal. + * + * @param value the integer value of the literal + */ public IntegerLiteralNode(long value) { this.value = value; } + /** + * Gets the value of the literal. + * + * @param frame the stack frame for execution + * @return the value of the integer literal + */ @Override public Object executeGeneric(VirtualFrame frame) { return this.value; diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/literal/LiteralNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/literal/LiteralNode.java index 6e8edd36320..1de87ea38f6 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/literal/LiteralNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/literal/LiteralNode.java @@ -3,5 +3,10 @@ package org.enso.interpreter.node.expression.literal; import com.oracle.truffle.api.nodes.NodeInfo; import org.enso.interpreter.node.ExpressionNode; +/** + * A base class for all Enso literals. + * + *

It exists only to enable working with literal values as an aggregate. + */ @NodeInfo(shortName = "Literal", description = "A representation of literal values.") public abstract class LiteralNode extends ExpressionNode {} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/AddOperatorNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/AddOperatorNode.java index e747ede50f9..48f39da784e 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/AddOperatorNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/AddOperatorNode.java @@ -3,8 +3,19 @@ package org.enso.interpreter.node.expression.operator; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.NodeInfo; +/** + * The addition operator for Enso. + */ @NodeInfo(shortName = "+") public abstract class AddOperatorNode extends BinaryOperatorNode { + + /** + * Adds two numbers together. + * + * @param left the first summand + * @param right the second summand + * @return the result of adding {@code left} and {@code right} + */ @Specialization protected long add(long left, long right) { return left + right; diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/BinaryOperatorNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/BinaryOperatorNode.java index e9bf503e5e3..a493f7fe142 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/BinaryOperatorNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/BinaryOperatorNode.java @@ -4,6 +4,9 @@ import com.oracle.truffle.api.dsl.NodeChild; import com.oracle.truffle.api.nodes.NodeInfo; import org.enso.interpreter.node.ExpressionNode; +/** + * A base class for all binary operators in Enso. + */ @NodeInfo( shortName = "BinaryOperator", description = "A representation of generic binary operators.") diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/DivideOperatorNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/DivideOperatorNode.java index 48e410ea2ae..a2983e7895f 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/DivideOperatorNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/DivideOperatorNode.java @@ -3,9 +3,17 @@ package org.enso.interpreter.node.expression.operator; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.NodeInfo; +/** The division operator for Enso. */ @NodeInfo(shortName = "/") public abstract class DivideOperatorNode extends BinaryOperatorNode { + /** + * Performs integer division of two numbers. + * + * @param left the dividend + * @param right the divisor + * @return the result of dividing {@code left} by {@code right} + */ @Specialization protected long divide(long left, long right) { return left / right; diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/ModOperatorNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/ModOperatorNode.java index 4dcf992e89f..205c11fb72e 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/ModOperatorNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/ModOperatorNode.java @@ -3,9 +3,19 @@ package org.enso.interpreter.node.expression.operator; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.NodeInfo; +/** + * The modulo operator for Enso. + */ @NodeInfo(shortName = "%") public abstract class ModOperatorNode extends BinaryOperatorNode { + /** + * Calculates the modulus of two numbers. + * + * @param left the dividend + * @param right the divisor + * @return the result of calculating {@code left} modulo {@code right} + */ @Specialization protected long mod(long left, long right) { return left % right; diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/MultiplyOperatorNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/MultiplyOperatorNode.java index 64c2bfc26d8..28545e9bf75 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/MultiplyOperatorNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/MultiplyOperatorNode.java @@ -3,9 +3,19 @@ package org.enso.interpreter.node.expression.operator; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.NodeInfo; +/** + * The multiplication operator for Enso. + */ @NodeInfo(shortName = "*") public abstract class MultiplyOperatorNode extends BinaryOperatorNode { + /** + * Multiplies two numbers together. + * + * @param left the first factor + * @param right the second factor + * @return the result of {@code left} multiplied by {@code right} + */ @Specialization protected long multiply(long left, long right) { return left * right; diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/SubtractOperatorNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/SubtractOperatorNode.java index e038a2f3b1d..f953830fac1 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/SubtractOperatorNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/expression/operator/SubtractOperatorNode.java @@ -3,9 +3,19 @@ package org.enso.interpreter.node.expression.operator; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.NodeInfo; +/** + * The subtraction operator for Enso. + */ @NodeInfo(shortName = "-") public abstract class SubtractOperatorNode extends BinaryOperatorNode { + /** + * Subtracts one number from another. + * + * @param left the minuend + * @param right the subtrahend + * @return the result of subtracting {@code right} from {@code left} + */ @Specialization protected long subtract(long left, long right) { return left - right; diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/function/CallNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/function/CallNode.java deleted file mode 100644 index f71becfe05e..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/node/function/CallNode.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.enso.interpreter.node.function; - -import com.oracle.truffle.api.RootCallTarget; -import com.oracle.truffle.api.dsl.Cached; -import com.oracle.truffle.api.dsl.Specialization; -import com.oracle.truffle.api.nodes.DirectCallNode; -import com.oracle.truffle.api.nodes.IndirectCallNode; -import com.oracle.truffle.api.nodes.Node; -import org.enso.interpreter.runtime.Function; - -public abstract class CallNode extends Node { - @Specialization(guards = "function.getCallTarget() == cachedTarget") - protected Object callDirect( - Function function, - Object[] arguments, - @Cached("function.getCallTarget()") RootCallTarget cachedTarget, - @Cached("create(cachedTarget)") DirectCallNode callNode) { - return callNode.call(Function.ArgumentsHelper.buildArguments(function, arguments)); - } - - @Specialization(replaces = "callDirect") - protected Object callIndirect( - Function function, Object[] arguments, @Cached IndirectCallNode callNode) { - return callNode.call( - function.getCallTarget(), Function.ArgumentsHelper.buildArguments(function, arguments)); - } - - public abstract Object executeCall(Object receiver, Object[] arguments); -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/function/CreateFunctionNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/function/CreateFunctionNode.java deleted file mode 100644 index 0d320c7ed71..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/node/function/CreateFunctionNode.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.enso.interpreter.node.function; - -import com.oracle.truffle.api.RootCallTarget; -import com.oracle.truffle.api.frame.MaterializedFrame; -import com.oracle.truffle.api.frame.VirtualFrame; -import org.enso.interpreter.node.EnsoRootNode; -import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.Function; - -public class CreateFunctionNode extends ExpressionNode { - private final RootCallTarget callTarget; - - public CreateFunctionNode(RootCallTarget callTarget) { - this.callTarget = callTarget; - } - - @Override - public void markTail() { - ((EnsoRootNode) callTarget.getRootNode()).markTail(); - } - - @Override - public Function executeFunction(VirtualFrame frame) { - MaterializedFrame scope = frame.materialize(); - return new Function(callTarget, scope); - } - - @Override - public Object executeGeneric(VirtualFrame frame) { - return executeFunction(frame); - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/function/DispatchNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/function/DispatchNode.java deleted file mode 100644 index c32743f75a7..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/node/function/DispatchNode.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.enso.interpreter.node.function; - -import com.oracle.truffle.api.nodes.Node; - -public abstract class DispatchNode extends Node { - public abstract Object executeDispatch(Object receiver, Object[] arguments); -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/function/FunctionBodyNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/function/FunctionBodyNode.java deleted file mode 100644 index 3b2bcc80336..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/node/function/FunctionBodyNode.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.enso.interpreter.node.function; - -import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.nodes.ExplodeLoop; -import com.oracle.truffle.api.nodes.NodeInfo; -import org.enso.interpreter.node.ExpressionNode; - -@NodeInfo(shortName = "{}") -public class FunctionBodyNode extends ExpressionNode { - - @Children private final ExpressionNode[] statements; - @Child private ExpressionNode returnExpr; - - public FunctionBodyNode(ExpressionNode[] statements, ExpressionNode returnExpr) { - this.statements = statements; - this.returnExpr = returnExpr; - } - - @Override - public void markTail() { - returnExpr.markTail(); - } - - @Override - @ExplodeLoop - public Object executeGeneric(VirtualFrame frame) { - for (ExpressionNode statement : statements) { - statement.executeVoid(frame); - } - return returnExpr.executeGeneric(frame); - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/function/InvokeNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/function/InvokeNode.java deleted file mode 100644 index 3197adb296c..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/node/function/InvokeNode.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.enso.interpreter.node.function; - -import com.oracle.truffle.api.CompilerAsserts; -import com.oracle.truffle.api.dsl.Fallback; -import com.oracle.truffle.api.dsl.NodeChild; -import com.oracle.truffle.api.dsl.Specialization; -import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.nodes.ExplodeLoop; -import com.oracle.truffle.api.nodes.NodeInfo; -import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.optimiser.TailCallException; -import org.enso.interpreter.runtime.*; -import org.enso.interpreter.runtime.errors.NotInvokableException; -import org.enso.interpreter.runtime.errors.TypeError; - -@NodeInfo(shortName = "@", description = "Executes function") -@NodeChild("target") -public abstract class InvokeNode extends ExpressionNode { - @Children private final ExpressionNode[] arguments; - @Child private DispatchNode dispatchNode; - - public InvokeNode(ExpressionNode[] arguments) { - this.arguments = arguments; - this.dispatchNode = new SimpleDispatchNode(); - } - - @ExplodeLoop - public Object[] computeArguments(VirtualFrame frame) { - Object[] positionalArguments = new Object[arguments.length]; - for (int i = 0; i < arguments.length; i++) { - positionalArguments[i] = arguments[i].executeGeneric(frame); - } - return positionalArguments; - } - - @Specialization - public Object invokeFunction(VirtualFrame frame, Function target) { - Object[] positionalArguments = computeArguments(frame); - - CompilerAsserts.compilationConstant(this.isTail()); - if (this.isTail()) { - throw new TailCallException(target, positionalArguments); - } else { - return dispatchNode.executeDispatch(target, positionalArguments); - } - } - - @Specialization - public Atom invokeConstructor(VirtualFrame frame, AtomConstructor constructor) { - Object[] positionalArguments = computeArguments(frame); - return constructor.newInstance(positionalArguments); - } - - @Fallback - public Object invokeGeneric(VirtualFrame frame, Object target) { - if (TypesGen.isFunction(target)) - return invokeFunction(frame, (Function) target); - if (TypesGen.isAtomConstructor(target)) - return invokeConstructor(frame, (AtomConstructor) target); - throw new NotInvokableException(target, this); - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/function/LoopingDispatchNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/function/LoopingDispatchNode.java deleted file mode 100644 index 01bffc9f69e..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/node/function/LoopingDispatchNode.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.enso.interpreter.node.function; - -import com.oracle.truffle.api.Truffle; -import com.oracle.truffle.api.frame.*; -import com.oracle.truffle.api.nodes.LoopNode; -import com.oracle.truffle.api.nodes.Node; -import com.oracle.truffle.api.nodes.RepeatingNode; -import org.enso.interpreter.optimiser.TailCallException; - -/** - * A version of {@link DispatchNode} that is fully prepared to handle tail calls. Tail calls are - * handled through exceptions – whenever a tail-recursive call would be executed, an exception - * containing the next unevaluated call and arguments is thrown instead (see: {@link - * TailCallException}). - * - *

This node executes the function in a loop, following all the continuations, until obtaining - * the actual return value. - */ -public class LoopingDispatchNode extends DispatchNode { - - private final FrameDescriptor loopFrameDescriptor = new FrameDescriptor(); - @Child private LoopNode loopNode; - - public LoopingDispatchNode() { - loopNode = Truffle.getRuntime().createLoopNode(new RepeatedCallNode(loopFrameDescriptor)); - } - - @Override - public Object executeDispatch(Object receiver, Object[] arguments) { - VirtualFrame frame = Truffle.getRuntime().createVirtualFrame(null, loopFrameDescriptor); - ((RepeatedCallNode) loopNode.getRepeatingNode()).setNextCall(frame, receiver, arguments); - loopNode.executeLoop(frame); - return ((RepeatedCallNode) loopNode.getRepeatingNode()).getResult(frame); - } - - public static final class RepeatedCallNode extends Node implements RepeatingNode { - private final FrameSlot resultSlot; - private final FrameSlot functionSlot; - private final FrameSlot argsSlot; - @Child private CallNode dispatchNode; - - public RepeatedCallNode(FrameDescriptor descriptor) { - functionSlot = descriptor.findOrAddFrameSlot("", FrameSlotKind.Object); - resultSlot = descriptor.findOrAddFrameSlot("", FrameSlotKind.Object); - argsSlot = descriptor.findOrAddFrameSlot("", FrameSlotKind.Object); - dispatchNode = CallNodeGen.create(); - } - - public void setNextCall(VirtualFrame frame, Object function, Object[] arguments) { - frame.setObject(functionSlot, function); - frame.setObject(argsSlot, arguments); - } - - public Object getResult(VirtualFrame frame) { - return FrameUtil.getObjectSafe(frame, resultSlot); - } - - public Object getNextFunction(VirtualFrame frame) { - Object result = FrameUtil.getObjectSafe(frame, functionSlot); - frame.setObject(functionSlot, null); - return result; - } - - public Object[] getNextArgs(VirtualFrame frame) { - Object[] result = (Object[]) FrameUtil.getObjectSafe(frame, argsSlot); - frame.setObject(argsSlot, null); - return result; - } - - @Override - public boolean executeRepeating(VirtualFrame frame) { - try { - Object function = getNextFunction(frame); - Object[] arguments = getNextArgs(frame); - frame.setObject(resultSlot, dispatchNode.executeCall(function, arguments)); - return false; - } catch (TailCallException e) { - setNextCall(frame, e.getFunction(), e.getArguments()); - return true; - } - } - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/function/ReadArgumentNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/function/ReadArgumentNode.java deleted file mode 100644 index 656999cbf72..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/node/function/ReadArgumentNode.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.enso.interpreter.node.function; - -import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.nodes.NodeInfo; -import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.Function; - -@NodeInfo(description = "Read function argument.") -public class ReadArgumentNode extends ExpressionNode { - private final int index; - - public ReadArgumentNode(int index) { - this.index = index; - } - - @Override - public Object executeGeneric(VirtualFrame frame) { - return Function.ArgumentsHelper.getPositionalArguments(frame.getArguments())[index]; - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/function/SimpleDispatchNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/function/SimpleDispatchNode.java deleted file mode 100644 index a4358e029a8..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/node/function/SimpleDispatchNode.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.enso.interpreter.node.function; - -import com.oracle.truffle.api.CompilerDirectives; -import org.enso.interpreter.optimiser.TailCallException; - -/** - * Optimistic version of {@link DispatchNode} for the non tail call recursive case. Tries to just - * call the function. If that turns out to be a tail call, it replaces itself with a {@link - * LoopingDispatchNode}. Thanks to this design, the (much more common) case of calling a function in - * a non-tail position does not force the overhead of loop. - */ -public class SimpleDispatchNode extends DispatchNode { - @Child private CallNode callNode = CallNodeGen.create(); - - @Override - public Object executeDispatch(Object receiver, Object[] arguments) { - try { - return callNode.executeCall(receiver, arguments); - } catch (TailCallException e) { - CompilerDirectives.transferToInterpreterAndInvalidate(); - DispatchNode replacement = new LoopingDispatchNode(); - this.replace(replacement); - return replacement.executeDispatch(e.getFunction(), e.getArguments()); - } - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/scope/AssignmentNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/scope/AssignmentNode.java index a71f9f18ce8..e40d72e11b3 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/scope/AssignmentNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/scope/AssignmentNode.java @@ -8,15 +8,20 @@ import com.oracle.truffle.api.frame.FrameSlotKind; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.NodeInfo; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.AtomConstructor; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +/** This node represents an assignment to a variable in a given scope. */ @NodeInfo(shortName = "=", description = "Assigns expression result to a variable.") @NodeChild(value = "rhsNode", type = ExpressionNode.class) @NodeField(name = "frameSlot", type = FrameSlot.class) public abstract class AssignmentNode extends ExpressionNode { - - public abstract FrameSlot getFrameSlot(); - + /** + * Writes a long value into the provided frame. + * + * @param frame the frame to write to + * @param value the value to write + * @return the {@link AtomConstructor#UNIT unit} type + */ @Specialization protected Object writeLong(VirtualFrame frame, long value) { frame.getFrameDescriptor().setFrameSlotKind(getFrameSlot(), FrameSlotKind.Long); @@ -25,6 +30,13 @@ public abstract class AssignmentNode extends ExpressionNode { return AtomConstructor.UNIT.newInstance(); } + /** + * Writes an object value into the provided frame. + * + * @param frame the frame to write to + * @param value the value to write + * @return the {@link AtomConstructor#UNIT unit} type + */ @Specialization protected Object writeObject(VirtualFrame frame, Object value) { frame.getFrameDescriptor().setFrameSlotKind(getFrameSlot(), FrameSlotKind.Object); @@ -32,4 +44,11 @@ public abstract class AssignmentNode extends ExpressionNode { return AtomConstructor.UNIT.newInstance(); } + + /** + * Gets the current frame slot + * + * @return the frame slot being written to + */ + public abstract FrameSlot getFrameSlot(); } diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/scope/ReadGlobalTargetNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/scope/ReadGlobalTargetNode.java index 43247075236..3edc43e949d 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/scope/ReadGlobalTargetNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/scope/ReadGlobalTargetNode.java @@ -8,16 +8,27 @@ import com.oracle.truffle.api.nodes.NodeInfo; import org.enso.interpreter.node.ExpressionNode; import org.enso.interpreter.runtime.GlobalCallTarget; +/** A representation of a read from a global call target. */ @NodeInfo(shortName = "Global Call Target") public class ReadGlobalTargetNode extends ExpressionNode { - private final GlobalCallTarget globalCallTarget; @Child private DirectCallNode directCallNode = null; + /** + * Creates a new node to read from a global call target. + * + * @param globalCallTarget the call target to read from + */ public ReadGlobalTargetNode(GlobalCallTarget globalCallTarget) { this.globalCallTarget = globalCallTarget; } + /** + * Reads from the global call target. + * + * @param frame the stack frame for execution + * @return the result of executing the call target + */ @Override public Object executeGeneric(VirtualFrame frame) { if (directCallNode == null) { diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/scope/ReadLocalTargetNode.java b/Interpreter/src/main/java/org/enso/interpreter/node/scope/ReadLocalTargetNode.java index 78a8135ed63..c34a4e89a11 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/node/scope/ReadLocalTargetNode.java +++ b/Interpreter/src/main/java/org/enso/interpreter/node/scope/ReadLocalTargetNode.java @@ -2,32 +2,31 @@ package org.enso.interpreter.node.scope; import com.oracle.truffle.api.dsl.NodeField; import com.oracle.truffle.api.dsl.Specialization; -import com.oracle.truffle.api.frame.*; +import com.oracle.truffle.api.frame.Frame; +import com.oracle.truffle.api.frame.FrameSlotTypeException; +import com.oracle.truffle.api.frame.FrameUtil; +import com.oracle.truffle.api.frame.MaterializedFrame; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.NodeInfo; -import org.enso.interpreter.builder.FramePointer; import org.enso.interpreter.node.ExpressionNode; -import org.enso.interpreter.runtime.Function; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.scope.FramePointer; +/** Reads from a local target (variable or call target). */ @NodeInfo(shortName = "readVar", description = "Access local variable value.") @NodeField(name = "framePointer", type = FramePointer.class) public abstract class ReadLocalTargetNode extends ExpressionNode { - public abstract FramePointer getFramePointer(); - public MaterializedFrame getParentFrame(Frame frame) { - return Function.ArgumentsHelper.getLocalScope(frame.getArguments()); - } - - @ExplodeLoop - public MaterializedFrame getProperFrame(Frame frame) { - MaterializedFrame currentFrame = getParentFrame(frame); - for (int i = 1; i < getFramePointer().getParentLevel(); i++) { - currentFrame = getParentFrame(currentFrame); - } - return currentFrame; - } - + /** + * Reads a {@code long} value from the frame. + * + * @param frame the stack frame to read from + * @return the value read from the appropriate slot in {@code frame} + * @throws FrameSlotTypeException when the specified frame slot does not contain a value with the + * expected type + */ @Specialization(rewriteOn = FrameSlotTypeException.class) protected long readLong(VirtualFrame frame) throws FrameSlotTypeException { if (getFramePointer().getParentLevel() == 0) @@ -36,6 +35,14 @@ public abstract class ReadLocalTargetNode extends ExpressionNode { return currentFrame.getLong(getFramePointer().getFrameSlot()); } + /** + * Reads an generic value from the frame. + * + * @param frame the stack frame to read from + * @return the value read from the appropriate slot in {@code frame} + * @throws FrameSlotTypeException when the specified frame slot does not contain a value with the + * expected type + */ @Specialization protected Object readGeneric(VirtualFrame frame) { if (getFramePointer().getParentLevel() == 0) @@ -43,4 +50,32 @@ public abstract class ReadLocalTargetNode extends ExpressionNode { MaterializedFrame currentFrame = getProperFrame(frame); return FrameUtil.getObjectSafe(currentFrame, getFramePointer().getFrameSlot()); } + + /** + * Obtains the direct parent frame for a given frame. + * + * @param frame the frame whose parent needs to be found + * @return the parent frame of {@code frame} + */ + public MaterializedFrame getParentFrame(Frame frame) { + return Function.ArgumentsHelper.getLocalScope(frame.getArguments()); + } + + /** + * Gets the Enso parent frame for a given frame. + * + *

This method is responsible for getting the guest language parent frame for the current frame + * by walking up the stack based on the scope in which the function was defined. + * + * @param frame the frame to find the Enso parent frame for + * @return the guest language parent frame of {@code frame} + */ + @ExplodeLoop + public MaterializedFrame getProperFrame(Frame frame) { + MaterializedFrame currentFrame = getParentFrame(frame); + for (int i = 1; i < getFramePointer().getParentLevel(); i++) { + currentFrame = getParentFrame(currentFrame); + } + return currentFrame; + } } diff --git a/Interpreter/src/main/java/org/enso/interpreter/optimiser/TailCallException.java b/Interpreter/src/main/java/org/enso/interpreter/optimiser/TailCallException.java deleted file mode 100644 index d2e783c55c4..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/optimiser/TailCallException.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.enso.interpreter.optimiser; - -import com.oracle.truffle.api.nodes.ControlFlowException; -import org.enso.interpreter.runtime.Function; - -public class TailCallException extends ControlFlowException { - private final Function function; - private final Object[] arguments; - - public TailCallException(Function function, Object[] arguments) { - this.function = function; - this.arguments = arguments; - } - - public Function getFunction() { - return function; - } - - public Object[] getArguments() { - return arguments; - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/node/data/.gitkeep b/Interpreter/src/main/java/org/enso/interpreter/optimiser/constant/.gitkeep similarity index 100% rename from Interpreter/src/main/java/org/enso/interpreter/node/data/.gitkeep rename to Interpreter/src/main/java/org/enso/interpreter/optimiser/constant/.gitkeep diff --git a/Interpreter/src/main/java/org/enso/interpreter/optimiser/tco/TailCallException.java b/Interpreter/src/main/java/org/enso/interpreter/optimiser/tco/TailCallException.java new file mode 100644 index 00000000000..6f2675fb019 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/optimiser/tco/TailCallException.java @@ -0,0 +1,43 @@ +package org.enso.interpreter.optimiser.tco; + +import com.oracle.truffle.api.nodes.ControlFlowException; +import org.enso.interpreter.runtime.callable.function.Function; + +/** + * Used to model the switch of control-flow from standard stack-based execution to looping. + * + *

This is used as part of the tail-call optimisation functionality in the interpreter. + */ +public class TailCallException extends ControlFlowException { + private final Function function; + private final Object[] arguments; + + /** + * Creates a new exception containing the necessary data to continue computation. + * + * @param function the function to execute in a loop + * @param arguments the arguments to {@code function} + */ + public TailCallException(Function function, Object[] arguments) { + this.function = function; + this.arguments = arguments; + } + + /** + * Gets the function to execute. + * + * @return the {@link Function} awaiting execution + */ + public Function getFunction() { + return function; + } + + /** + * Gets the arguments for the function. + * + * @return the arguments for the associated {@link Function} + */ + public Object[] getArguments() { + return arguments; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/AtomConstructor.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/AtomConstructor.java deleted file mode 100644 index 3d8ff53e571..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/AtomConstructor.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.enso.interpreter.runtime; - -import com.oracle.truffle.api.interop.TruffleObject; -import org.enso.interpreter.runtime.errors.ArityException; - -import java.util.List; - -public class AtomConstructor implements TruffleObject { - public static final AtomConstructor CONS = new AtomConstructor("Cons", 2); - public static final AtomConstructor NIL = new AtomConstructor("Nil", 0); - public static final AtomConstructor UNIT = new AtomConstructor("Unit", 0); - - private final String name; - private final int arity; - private final Atom cachedInstance; - - public AtomConstructor(String name, List argNames) { - this(name, argNames.size()); - } - - public AtomConstructor(String name, int arity) { - this.name = name; - this.arity = arity; - if (arity == 0) { - cachedInstance = new Atom(this); - } else { - cachedInstance = null; - } - } - - public String getName() { - return name; - } - - public int getArity() { - return arity; - } - - public Atom newInstance(Object... arguments) { - if (arguments.length != arity) - throw new ArityException(arity, arguments.length); - if (cachedInstance != null) return cachedInstance; - return new Atom(this, arguments); - } - - @Override - public String toString() { - return super.toString() + "<" + name + "/" + arity + ">"; - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/Context.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/Context.java index a43df55dd95..f1d79d89068 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/Context.java +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/Context.java @@ -1,18 +1,28 @@ package org.enso.interpreter.runtime; +import com.oracle.truffle.api.TruffleLanguage; import com.oracle.truffle.api.TruffleLanguage.Env; -import org.enso.interpreter.Language; - import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; +import org.enso.interpreter.Language; +/** + * The language context is the internal state of the language that is associated with each thread in + * a running Enso program. + */ public class Context { private final Language language; private final Env environment; private final BufferedReader input; private final PrintWriter output; + /** + * Creates a new Enso context. + * + * @param language the language identifier + * @param environment the execution environment of the {@link TruffleLanguage} + */ public Context(Language language, Env environment) { this.language = language; this.environment = environment; diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/Function.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/Function.java deleted file mode 100644 index 373b494c406..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/Function.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.enso.interpreter.runtime; - -import com.oracle.truffle.api.RootCallTarget; -import com.oracle.truffle.api.dsl.Cached; -import com.oracle.truffle.api.dsl.Specialization; -import com.oracle.truffle.api.frame.MaterializedFrame; -import com.oracle.truffle.api.interop.InteropLibrary; -import com.oracle.truffle.api.interop.TruffleObject; -import com.oracle.truffle.api.library.ExportLibrary; -import com.oracle.truffle.api.library.ExportMessage; -import com.oracle.truffle.api.nodes.DirectCallNode; -import com.oracle.truffle.api.nodes.IndirectCallNode; - -@ExportLibrary(InteropLibrary.class) -public final class Function implements TruffleObject { - private final RootCallTarget callTarget; - private final MaterializedFrame scope; - - public Function(RootCallTarget callTarget, MaterializedFrame scope) { - this.callTarget = callTarget; - this.scope = scope; - } - - public RootCallTarget getCallTarget() { - return callTarget; - } - - public MaterializedFrame getScope() { - return scope; - } - - @ExportMessage - public boolean isExecutable() { - return true; - } - - @ExportMessage - abstract static class Execute { - - @Specialization(guards = "function.getCallTarget() == cachedTarget") - protected static Object callDirect( - Function function, - Object[] arguments, - @Cached("function.getCallTarget()") RootCallTarget cachedTarget, - @Cached("create(cachedTarget)") DirectCallNode callNode) { - return callNode.call(function.getScope(), arguments); - } - - @Specialization(replaces = "callDirect") - protected static Object callIndirect( - Function function, Object[] arguments, @Cached IndirectCallNode callNode) { - return callNode.call(function.getCallTarget(), function.getScope(), arguments); - } - } - - public static class ArgumentsHelper { - public static Object[] buildArguments(Function function, Object[] positionalArguments) { - return new Object[] {function.getScope(), positionalArguments}; - } - - public static Object[] getPositionalArguments(Object[] arguments) { - return (Object[]) arguments[1]; - } - - public static MaterializedFrame getLocalScope(Object[] arguments) { - return (MaterializedFrame) arguments[0]; - } - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/GlobalCallTarget.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/GlobalCallTarget.java index fa5793f5d28..7decf44a523 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/GlobalCallTarget.java +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/GlobalCallTarget.java @@ -4,18 +4,35 @@ import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.interop.TruffleObject; +/** + * A representation of the top-level callable pieces of code in Enso. + */ public class GlobalCallTarget implements TruffleObject { - @CompilationFinal private RootCallTarget target; + /** + * Creates a new call target. + * + * @param target a handle to the code to associate with this target + */ public GlobalCallTarget(RootCallTarget target) { this.target = target; } + /** + * Gets the handle to the code associated with this target. + * + * @return a handle to the associated code + */ public RootCallTarget getTarget() { return target; } + /** + * Sets the internal code handle. + * + * @param target a handle to the code to be associated with this target + */ public void setTarget(RootCallTarget target) { this.target = target; } diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/Types.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/Types.java deleted file mode 100644 index 63eeca20284..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/Types.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.enso.interpreter.runtime; - -import com.oracle.truffle.api.CompilerDirectives; -import com.oracle.truffle.api.dsl.ImplicitCast; -import com.oracle.truffle.api.dsl.TypeSystem; - -@TypeSystem({long.class, Function.class, Atom.class, AtomConstructor.class}) -public class Types { - - @ImplicitCast - @CompilerDirectives.TruffleBoundary - public static long castLong(int value) { - return value; - } - -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/Callable.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/Callable.java new file mode 100644 index 00000000000..71dde342cfd --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/Callable.java @@ -0,0 +1,28 @@ +package org.enso.interpreter.runtime.callable; + +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; + +/** A class that represents all runtime types in Enso that can be called. */ +public abstract class Callable { + private @CompilationFinal(dimensions = 1) ArgumentDefinition[] args; + + /** + * Creates a new callable with the specified arguments. + * + * @param args information on the arguments defined for the callable, in the order they are + * defined + */ + public Callable(ArgumentDefinition[] args) { + this.args = args; + } + + /** + * Gets the arguments defined on this callable. + * + * @return information on the arguments defined for the callable + */ + public ArgumentDefinition[] getArgs() { + return this.args; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/ArgumentDefinition.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/ArgumentDefinition.java new file mode 100644 index 00000000000..d5aec60c755 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/ArgumentDefinition.java @@ -0,0 +1,70 @@ +package org.enso.interpreter.runtime.callable.argument; + +import java.util.Optional; +import org.enso.interpreter.node.ExpressionNode; + +/** Tracks the specifics about how arguments are defined at the callable definition site. */ +public class ArgumentDefinition { + private final int position; + private final String name; + private final Optional defaultValue; + + /** + * Creates a new argument definition without a default value. + * + * @param position the position of the argument at the definition site + * @param name the name of the argument + */ + public ArgumentDefinition(int position, String name) { + this(position, name, null); + } + + /** + * Creates a new argument definition with a default value. + * + * @param position the position of the argument at the definition site + * @param name the name of the argument + * @param defaultValue the default value of the argument + */ + public ArgumentDefinition(int position, String name, ExpressionNode defaultValue) { + this.position = position; + this.name = name; + this.defaultValue = Optional.ofNullable(defaultValue); + } + + /** + * Gets the argument position at the definition site. + * + * @return the argument's position at the definition side + */ + public int getPosition() { + return this.position; + } + + /** + * Gets the argument's name. + * + * @return the name of the argument + */ + public String getName() { + return this.name; + } + + /** + * Gets the argument's default value. + * + * @return the default value, if present, otherwise {@link Optional#empty()} + */ + public Optional getDefaultValue() { + return this.defaultValue; + } + + /** + * Checks if the argument has a default value. + * + * @return {@code true} if a default value is present, otherwise {@code false} + */ + public boolean hasDefaultValue() { + return this.defaultValue.isPresent(); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/CallArgument.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/CallArgument.java new file mode 100644 index 00000000000..827b6a34fc7 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/CallArgument.java @@ -0,0 +1,85 @@ +package org.enso.interpreter.runtime.callable.argument; + +import org.enso.interpreter.node.ExpressionNode; + +/** + * Tracks the specifics about how arguments are specified at a call site. + */ +public class CallArgument { + private final String name; + private ExpressionNode expression; + + /** + * Creates an argument passed positionally. + * + * @param expression the value of the argument + */ + public CallArgument(ExpressionNode expression) { + this(null, expression); + } + + /** + * Creates an argument that ignores the default (for currying purposes). + * + * @param name the name of the argument whose default should be ignored + */ + public CallArgument(String name) { + this(name, null); + } + + /** + * Creates an argument passed by name. + * + * @param name the name of the argument being applied + * @param expression the value of the argument + */ + public CallArgument(String name, ExpressionNode expression) { + this.name = name; + this.expression = expression; + } + + /** + * Checks if the argument is an ignore. + * + * @return {@code true} if it is an ignore, otherwise {@code false} + */ + public boolean isIgnored() { + return (this.name != null) && (this.expression == null); + } + + /** + * Checks if the argument is passed by name. + * + * @return {@code true} if it is passed by name, otherwise {@code false} + */ + public boolean isNamed() { + return (this.name != null) && (this.expression != null); + } + + /** + * Checks if the argument is passed by position. + * + * @return {@code true} if it is passed by position, otherwise {@code false} + */ + public boolean isPositional() { + return !isNamed() && !isIgnored(); + } + + /** + * Gets the argument name. + * + * @return the name of the argument + */ + public String getName() { + return this.name; + } + + /** + * Gets the expression representing the argument's value. + * + * @return the expression representing the value of the argument + */ + public ExpressionNode getExpression() { + return expression; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/CallArgumentInfo.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/CallArgumentInfo.java new file mode 100644 index 00000000000..26a04a0109d --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/CallArgumentInfo.java @@ -0,0 +1,195 @@ +package org.enso.interpreter.runtime.callable.argument; + +import com.oracle.truffle.api.nodes.ExplodeLoop; +import org.enso.interpreter.runtime.callable.argument.sentinel.UnappliedArgumentSentinel; +import org.enso.interpreter.runtime.callable.Callable; +import org.enso.interpreter.runtime.error.ArgumentMappingException; +import org.enso.interpreter.runtime.error.NotInvokableException; +import org.enso.interpreter.runtime.type.TypesGen; + +/** + * Tracks simple information about call-site arguments, used to make processing of caller argument + * lists much mpre simple. + */ +public class CallArgumentInfo { + private final String name; + private final boolean isNamed; + private final boolean isPositional; + private final boolean isIgnored; + + /** + * Creates the information from a {@link CallArgument}. + * + * @param callArgNode the structure to take information from + */ + public CallArgumentInfo(CallArgument callArgNode) { + this( + callArgNode.getName(), + callArgNode.isNamed(), + callArgNode.isPositional(), + callArgNode.isIgnored()); + } + + /** + * Creates the information explicitly. + * + * @param name the name of the argument, if present + * @param isNamed whether or not the argument is passed by name + * @param isPositional whether or not the argument is passed by position + * @param isIgnored whether or not the argument is an ignore + */ + public CallArgumentInfo(String name, boolean isNamed, boolean isPositional, boolean isIgnored) { + this.name = name; + this.isNamed = isNamed; + this.isPositional = isPositional; + this.isIgnored = isIgnored; + } + + /** + * Gets the name of the argument. + * + * @return the name of the argument at the call site, if specified + */ + public String getName() { + return name; + } + + /** + * Checks whether the argument was applied by name or not. + * + * @return {@code true} if the argument was applied by name, otherwise {@code false} + */ + public boolean isNamed() { + return isNamed; + } + + /** + * Checks whether the argument was applied by position or not. + * + * @return {@code true} if the argument was applied positionally, otherwise {@code false} + */ + public boolean isPositional() { + return isPositional; + } + + /** + * Checks whether the argument is ignoring a default value. + * + * @return {@code true} if the argument ignores a default, otherwise {@code false} + */ + public boolean isIgnored() { + return isIgnored; + } + + /** + * Reorders the arguments from the call-site to match the order at the definition site. + * + *

If an argument is not applied in {@code args}, the resultant array will contain {@code null} + * in any places where an argument was not applied. This is then handled later at the point of + * reading the arguments, where {@link + * org.enso.interpreter.node.callable.argument.ReadArgumentNode} will use the default value for + * that argument. + * + *

If an argument is explicitly left unapplied (for the purposes of currying), the result array + * will contain a sentinel value of type {@link + * UnappliedArgumentSentinel} in that position. + * This ensures that execution knows not to use a possible default value for this position. + * + * @param order a mapping where position {@code i} in the array contains the destination position + * in the target array for the calling argument in position {@code i} + * @param args the function arguments to reorder, ordered as at the call site + * @param numDefinedArgs the number of arguments the function was defined for + * @return {@code args} sorted according to the provided {@code order} in an array with slots for + * the number of arguments the function was defined for + */ + @ExplodeLoop + public static Object[] reorderArguments(int[] order, Object[] args, int numDefinedArgs) { + Object[] result = new Object[numDefinedArgs]; // Note [Defined Arguments] + + for (int i = 0; i < args.length; i++) { + result[order[i]] = args[i]; + } + return result; + } + + /* Note [Defined Arguments] + * ~~~~~~~~~~~~~~~~~~~~~~~~ + * In a language where it is possible to both partially-apply functions and to have default + * function arguments, it is important that we track information about where arguments have and + * have not been applied. + * + * To do this, we take advantage of the fact that Java will `null` initialise an array. As a + * result, we know that any nulls in the result array must correspond to arguments that haven't + * been applied. + * + * However, this doesn't handle the case where arguments are intentionally ignored, so we use a + * special sentinel value to handle this. + */ + + /** + * Generates a mapping between the call-site argument positions and the definition-site argument + * positions. + * + * @param callable the construct being called + * @param callArgs information on the arguments at the call site, ordered as at the call site + * @return a mapping where position {@code i} in the array contains the destination position in + * the target array for the calling argument in position {@code i} + */ + public static int[] generateArgMapping(Object callable, CallArgumentInfo[] callArgs) { + if (TypesGen.isCallable(callable)) { + Callable realCallable = (Callable) callable; + + ArgumentDefinition[] definedArgs = realCallable.getArgs(); + int numberOfDefinedArgs = definedArgs.length; + + boolean[] definedArgumentIsUsed = new boolean[numberOfDefinedArgs]; + int[] argumentSortOrder = new int[callArgs.length]; + + for (int i = 0; i < callArgs.length; ++i) { + boolean argumentProcessed = false; + CallArgumentInfo currentArgument = callArgs[i]; + + boolean argumentIsPositional = currentArgument.isPositional(); + + if (argumentIsPositional) { + for (int j = 0; j < numberOfDefinedArgs; j++) { + boolean argumentIsUnused = !definedArgumentIsUsed[j]; + + if (argumentIsUnused) { + argumentSortOrder[i] = j; + definedArgumentIsUsed[j] = true; + argumentProcessed = true; + break; + } + } + + if (!argumentProcessed) { + throw new ArgumentMappingException(realCallable, currentArgument, i); + } + + } else { + for (int j = 0; j < numberOfDefinedArgs; j++) { + boolean argumentIsValidAndNamed = + currentArgument.getName().equals(definedArgs[j].getName()) + && !definedArgumentIsUsed[j]; + + if (argumentIsValidAndNamed) { + argumentSortOrder[i] = j; + definedArgumentIsUsed[j] = true; + argumentProcessed = true; + break; + } + } + + if (!argumentProcessed) { + throw new ArgumentMappingException(realCallable, currentArgument, i); + } + } + } + + return argumentSortOrder; + } else { + throw new NotInvokableException(callable, null); + } + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/sentinel/DefaultedArgumentSentinel.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/sentinel/DefaultedArgumentSentinel.java new file mode 100644 index 00000000000..71745e76056 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/sentinel/DefaultedArgumentSentinel.java @@ -0,0 +1,17 @@ +package org.enso.interpreter.runtime.callable.argument.sentinel; + +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; + +/** A representation of an argument that has been explicitly defaulted in the program source. */ +public class DefaultedArgumentSentinel { + private ArgumentDefinition argument; + + /** + * Creates a defaulted argument sentinel value. + * + * @param argument information about the defaulted argument + */ + public DefaultedArgumentSentinel(ArgumentDefinition argument) { + this.argument = argument; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/sentinel/UnappliedArgumentSentinel.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/sentinel/UnappliedArgumentSentinel.java new file mode 100644 index 00000000000..597a01244f7 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/argument/sentinel/UnappliedArgumentSentinel.java @@ -0,0 +1,19 @@ +package org.enso.interpreter.runtime.callable.argument.sentinel; + +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; + +/** + * A value used to sentinel an argument that has been defined for a function but not yet applied. + */ +public class UnappliedArgumentSentinel { + private ArgumentDefinition argument; + + /** + * Creates a sentinel value for the given argument. + * + * @param argument information about the argument being signalled + */ + public UnappliedArgumentSentinel(ArgumentDefinition argument) { + this.argument = argument; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/Atom.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/atom/Atom.java similarity index 57% rename from Interpreter/src/main/java/org/enso/interpreter/runtime/Atom.java rename to Interpreter/src/main/java/org/enso/interpreter/runtime/callable/atom/Atom.java index fbc84cafc39..deed56cd837 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/Atom.java +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/atom/Atom.java @@ -1,29 +1,51 @@ -package org.enso.interpreter.runtime; +package org.enso.interpreter.runtime.callable.atom; import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.interop.TruffleObject; - import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +/** A runtime representation of an Atom in Enso. */ public class Atom implements TruffleObject { private final AtomConstructor constructor; - private final Object[] fields; + private @CompilationFinal(dimensions = 1) Object[] fields; + /** + * Creates a new Atom for a given constructor. + * + * @param constructor the Atom's constructor + * @param fields the Atom's fields + */ public Atom(AtomConstructor constructor, Object... fields) { this.constructor = constructor; this.fields = fields; } + /** + * Gets the Atom's constructor. + * + * @return the constructor for this Atom + */ public AtomConstructor getConstructor() { return constructor; } + /** + * Gets the fields from the Atom. + * + * @return this Atom's fields + */ public Object[] getFields() { return fields; } + /** + * Creates a textual representation of this Atom, useful for debugging. + * + * @return a textual representation of this Atom. + */ @Override @CompilerDirectives.TruffleBoundary public String toString() { @@ -34,6 +56,7 @@ public class Atom implements TruffleObject { Arrays.stream(fields).map(Object::toString).collect(Collectors.toList()); builder.append(String.join(", ", fieldStrings)); builder.append(">"); + return builder.toString(); } } diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/atom/AtomConstructor.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/atom/AtomConstructor.java new file mode 100644 index 00000000000..bbf8cfe97be --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/atom/AtomConstructor.java @@ -0,0 +1,79 @@ +package org.enso.interpreter.runtime.callable.atom; + +import com.oracle.truffle.api.interop.TruffleObject; +import org.enso.interpreter.runtime.callable.Callable; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; +import org.enso.interpreter.runtime.error.ArityException; + +/** A representation of an Atom constructor. */ +public class AtomConstructor extends Callable implements TruffleObject { + public static final AtomConstructor CONS = + new AtomConstructor( + "Cons", + new ArgumentDefinition[] { + new ArgumentDefinition(0, "head"), new ArgumentDefinition(1, "rest") + }); + public static final AtomConstructor NIL = new AtomConstructor("Nil", new ArgumentDefinition[0]); + public static final AtomConstructor UNIT = new AtomConstructor("Unit", new ArgumentDefinition[0]); + + private final String name; + private final Atom cachedInstance; + + /** + * Creates a new Atom constructor for a given name and with the provided arguments. + * + * @param name the name of the Atom constructor + * @param args the fields associated with the constructor + */ + public AtomConstructor(String name, ArgumentDefinition[] args) { + super(args); + this.name = name; + if (args.length == 0) { + cachedInstance = new Atom(this); + } else { + cachedInstance = null; + } + } + + /** + * Gets the name of the constructor. + * + * @return the name of the Atom constructor + */ + public String getName() { + return name; + } + + /** + * Gets the number of arguments expected by the constructor. + * + * @return the number of args expected by the constructor. + */ + public int getArity() { + return getArgs().length; + } + + /** + * Creates a new runtime instance of the Atom represented by this constructor. + * + * @param arguments the runtime arguments to the constructor + * @return a new instance of the atom represented by this constructor + */ + public Atom newInstance(Object... arguments) { + if (arguments.length > getArity()) { + throw new ArityException(getArity(), arguments.length); + } + if (cachedInstance != null) return cachedInstance; + return new Atom(this, arguments); + } + + /** + * Creates a textual representation of this Atom constructor, useful for debugging. + * + * @return a textual representation of this Atom constructor + */ + @Override + public String toString() { + return super.toString() + "<" + name + "/" + getArity() + ">"; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/function/Function.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/function/Function.java new file mode 100644 index 00000000000..b09a4bf1768 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/callable/function/Function.java @@ -0,0 +1,157 @@ +package org.enso.interpreter.runtime.callable.function; + +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.MaterializedFrame; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.enso.interpreter.runtime.callable.Callable; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; + +/** A runtime representation of a function object in Enso. */ +@ExportLibrary(InteropLibrary.class) +public final class Function extends Callable implements TruffleObject { + private final RootCallTarget callTarget; + private final MaterializedFrame scope; + + /** + * Creates a new function. + * + * @param callTarget the target containing the function's code + * @param scope a frame representing the function's scope + * @param arguments the arguments with which the function was defined + */ + public Function( + RootCallTarget callTarget, MaterializedFrame scope, ArgumentDefinition[] arguments) { + super(arguments); + this.callTarget = callTarget; + this.scope = scope; + } + + /** + * Gets the target containing the function's code. + * + * @return the target containing the function's code + */ + public RootCallTarget getCallTarget() { + return callTarget; + } + + /** + * Gets the function's scope. + * + * @return the function's scope + */ + public MaterializedFrame getScope() { + return scope; + } + + /** + * Checks if this runtime object is executable. + * + * @return {@code true} + */ + @ExportMessage + public boolean isExecutable() { + return true; + } + + /** + * A class representing the executable behaviour of the function. + * + *

This class gets exposed via the Truffle interop library to allow Enso functions to be called + * from other guest languages running on GraalVM. + */ + @ExportMessage + public abstract static class Execute { + + /** + * Calls the function directly. + * + *

This specialisation comes into play where the call target for the provided function is + * already cached. THis means that the call can be made quickly. + * + * @param function the function to execute + * @param arguments the arguments passed to {@code function} in the expected positional order + * @param cachedTarget the cached call target for {@code function} + * @param callNode the cached call node for {@code cachedTarget} + * @return the result of executing {@code function} on {@code arguments} + */ + @Specialization(guards = "function.getCallTarget() == cachedTarget") + protected static Object callDirect( + Function function, + Object[] arguments, + @Cached("function.getCallTarget()") RootCallTarget cachedTarget, + @Cached("create(cachedTarget)") DirectCallNode callNode) { + return callNode.call(function.getScope(), arguments); + } + + /** + * Calls the function with a lookup. + * + *

This specialisation is used in the case where there is no cached call target for the + * provided function. This is much slower and should, in general, be avoided. + * + * @param function the function to execute + * @param arguments the arguments passed to {@code function} in the expected positional order + * @param callNode the cached call node for making indirect calls + * @return the result of executing {@code function} on {@code arguments} + */ + @Specialization(replaces = "callDirect") + protected static Object callIndirect( + Function function, Object[] arguments, @Cached IndirectCallNode callNode) { + return callNode.call(function.getCallTarget(), function.getScope(), arguments); + } + } + + /** + * Defines a simple schema for accessing arguments from call targets. + * + *

As Truffle call targets can only take a simple {@code Object[]}, this class provides a way + * to get the various necessary pieces of information out of that array. + */ + public static class ArgumentsHelper { + + /** + * Generates an array of arguments using the schema to be passed to a call target. + * + *

The arguments passed to this function must be in positional order. For more information on + * how to do this, see {@link + * org.enso.interpreter.node.callable.argument.sorter.ArgumentSorterNode}. + * + * @param function the function to be called + * @param positionalArguments the arguments to that function, sorted into positional order + * @return an array containing the necessary information to call an Enso function + */ + public static Object[] buildArguments(Function function, Object[] positionalArguments) { + return new Object[] {function.getScope(), positionalArguments}; + } + + /** + * Gets the positional arguments out of the array. + * + * @param arguments an array produced by {@link ArgumentsHelper#buildArguments(Function, + * Object[])} + * @return the positional arguments to the function + */ + public static Object[] getPositionalArguments(Object[] arguments) { + return (Object[]) arguments[1]; + } + + /** + * Gets the function's local scope out of the array. + * + * @param arguments an array produced by {@link ArgumentsHelper#buildArguments(Function, + * Object[])} + * @return the local scope for the associated function + */ + public static MaterializedFrame getLocalScope(Object[] arguments) { + return (MaterializedFrame) arguments[0]; + } + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/error/ArgumentMappingException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/ArgumentMappingException.java new file mode 100644 index 00000000000..b82094582ab --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/ArgumentMappingException.java @@ -0,0 +1,32 @@ +package org.enso.interpreter.runtime.error; + +import java.util.Arrays; +import java.util.stream.Collectors; +import org.enso.interpreter.runtime.callable.Callable; +import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; + +/** + * An error that is thrown when the interpreter is unable to match the arguments provided at the + * call site to the arguments defined for the callable. + */ +public class ArgumentMappingException extends RuntimeException { + + /** + * Creates a new error. + * + * @param callable the callable for which argument matching is taking place + * @param argument information on the call-site argument that failed to match + * @param callPosition the position at the call site of {@code argument} + */ + public ArgumentMappingException(Callable callable, CallArgumentInfo argument, int callPosition) { + super( + String.format( + "Cannot match argument %s to callable with arguments %s", + argument.isPositional() + ? "at position " + callPosition + : "with name " + argument.getName(), + Arrays.stream(callable.getArgs()) + .map(arg -> arg.getName() + ", ") + .collect(Collectors.joining()))); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/error/ArityException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/ArityException.java new file mode 100644 index 00000000000..6abf70c46cd --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/ArityException.java @@ -0,0 +1,18 @@ +package org.enso.interpreter.runtime.error; + +/** + * An exception that is thrown whenever a call is made to a {@link + * org.enso.interpreter.runtime.callable.Callable} with the wrong number of arguments. + */ +public class ArityException extends RuntimeException { + + /** + * Creates a new error. + * + * @param expected the expected number of arguments + * @param actual the provided number of arguments + */ + public ArityException(int expected, int actual) { + super("Wrong number of arguments. Expected: " + expected + " but got: " + actual + "."); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/error/DuplicateArgumentNameException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/DuplicateArgumentNameException.java new file mode 100644 index 00000000000..8eadba1698f --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/DuplicateArgumentNameException.java @@ -0,0 +1,16 @@ +package org.enso.interpreter.runtime.error; + +/** + * An exception thrown when a function is defined with duplicate argument names. + */ +public class DuplicateArgumentNameException extends RuntimeException { + + /** + * Creates a new error. + * + * @param name the name defined multiple times + */ + public DuplicateArgumentNameException(String name) { + super("A function cannot have multiple arguments called " + name); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/InexhaustivePatternMatchException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/InexhaustivePatternMatchException.java similarity index 51% rename from Interpreter/src/main/java/org/enso/interpreter/runtime/errors/InexhaustivePatternMatchException.java rename to Interpreter/src/main/java/org/enso/interpreter/runtime/error/InexhaustivePatternMatchException.java index 7d4d0f0a5f3..78c4a5aee24 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/InexhaustivePatternMatchException.java +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/InexhaustivePatternMatchException.java @@ -1,18 +1,31 @@ -package org.enso.interpreter.runtime.errors; +package org.enso.interpreter.runtime.error; import com.oracle.truffle.api.TruffleException; import com.oracle.truffle.api.nodes.Node; +/** + * An error thrown when a pattern match is is inexhaustive and execution cannot find a branch that + * is able to be executed. + */ public class InexhaustivePatternMatchException extends RuntimeException implements TruffleException { - private final Node node; + /** + * Creates a new error. + * + * @param node the node where the fallthrough occurred + */ public InexhaustivePatternMatchException(Node node) { super("Inexhaustive pattern match."); this.node = node; } + /** + * Gets the location where the error occurred. + * + * @return the node where the error occurred + */ @Override public Node getLocation() { return node; diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/error/InvalidArgumentNameException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/InvalidArgumentNameException.java new file mode 100644 index 00000000000..314d3a5b3cb --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/InvalidArgumentNameException.java @@ -0,0 +1,23 @@ +package org.enso.interpreter.runtime.error; + +import java.util.Set; + +/** + * An error thrown when an argument is provided by name, but doesn't match any of the arguments + * defined on the callable. + */ +public class InvalidArgumentNameException extends RuntimeException { + + /** + * Creates a new error. + * + * @param name the erroneous argument name at the call site + * @param availableNames the argument names defined on the callable + */ + public InvalidArgumentNameException(String name, Set availableNames) { + super( + name + + " is not a valid argument name for a function with arguments: " + + availableNames.stream().reduce("", (l, r) -> l + ", " + r)); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/error/NotInvokableException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/NotInvokableException.java new file mode 100644 index 00000000000..113908ff261 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/NotInvokableException.java @@ -0,0 +1,30 @@ +package org.enso.interpreter.runtime.error; + +import com.oracle.truffle.api.TruffleException; +import com.oracle.truffle.api.nodes.Node; + +/** An exception thrown when execution attempts to invoke something that cannot be invoked. */ +public class NotInvokableException extends RuntimeException implements TruffleException { + private final Node node; + + /** + * Creates the error. + * + * @param target the construct that was attempted to be invoked + * @param node the node where the erroneous invocation took place + */ + public NotInvokableException(Object target, Node node) { + super("Object " + target + " is not invokable."); + this.node = node; + } + + /** + * The location where the erroneous invocation took place. + * + * @return the node where the error occurred + */ + @Override + public Node getLocation() { + return node; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/TypeError.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/TypeError.java similarity index 51% rename from Interpreter/src/main/java/org/enso/interpreter/runtime/errors/TypeError.java rename to Interpreter/src/main/java/org/enso/interpreter/runtime/error/TypeError.java index 1333547c0cb..813bb810e6a 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/TypeError.java +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/TypeError.java @@ -1,17 +1,30 @@ -package org.enso.interpreter.runtime.errors; +package org.enso.interpreter.runtime.error; import com.oracle.truffle.api.TruffleException; import com.oracle.truffle.api.nodes.Node; +/** + * An exception thrown on a type mismatch. + */ public class TypeError extends RuntimeException implements TruffleException { - private final Node node; + /** + * Creates the error. + * + * @param message an informative error message + * @param node the node where the error occurred + */ public TypeError(String message, Node node) { super(message); this.node = node; } + /** + * Gets the location of the error. + * + * @return the node where the error occurred. + */ @Override public Node getLocation() { return node; diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/error/UnsaturatedCallException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/UnsaturatedCallException.java new file mode 100644 index 00000000000..1075ffd34bb --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/UnsaturatedCallException.java @@ -0,0 +1,19 @@ +package org.enso.interpreter.runtime.error; + +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; + +/** + * An error thrown when an attempt is made to execute a callable that has not had all of its + * arguments provided. + */ +public class UnsaturatedCallException extends RuntimeException { + + /** + * Creates the error. + * + * @param arg the unapplied argument + */ + public UnsaturatedCallException(ArgumentDefinition arg) { + super("The argument named " + arg.getName() + " has not been applied."); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/VariableDoesNotExistException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/VariableDoesNotExistException.java similarity index 51% rename from Interpreter/src/main/java/org/enso/interpreter/runtime/errors/VariableDoesNotExistException.java rename to Interpreter/src/main/java/org/enso/interpreter/runtime/error/VariableDoesNotExistException.java index dca812208b3..9dff5806fa4 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/VariableDoesNotExistException.java +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/VariableDoesNotExistException.java @@ -1,13 +1,27 @@ -package org.enso.interpreter.runtime.errors; +package org.enso.interpreter.runtime.error; import com.oracle.truffle.api.TruffleException; import com.oracle.truffle.api.nodes.Node; +/** + * An error thrown when the program attempts to read from a variable that has not been defined. + */ public class VariableDoesNotExistException extends RuntimeException implements TruffleException { + + /** + * Creates the error. + * + * @param name the name of the undefined variable + */ public VariableDoesNotExistException(String name) { super("Variable " + name + " is not defined."); } + /** + * Gets the location of the error. + * + * @return the node where the error occurred. + */ @Override public Node getLocation() { return null; diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/VariableRedefinitionException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/VariableRedefinitionException.java similarity index 52% rename from Interpreter/src/main/java/org/enso/interpreter/runtime/errors/VariableRedefinitionException.java rename to Interpreter/src/main/java/org/enso/interpreter/runtime/error/VariableRedefinitionException.java index 407640c4b77..f3c296b2025 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/VariableRedefinitionException.java +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/error/VariableRedefinitionException.java @@ -1,13 +1,27 @@ -package org.enso.interpreter.runtime.errors; +package org.enso.interpreter.runtime.error; import com.oracle.truffle.api.TruffleException; import com.oracle.truffle.api.nodes.Node; +/** + * An exception thrown when a program attempts to redefine a variable that has already been defined. + */ public class VariableRedefinitionException extends RuntimeException implements TruffleException { + + /** + * Creates the error. + * + * @param name the name of the already-defined variable + */ public VariableRedefinitionException(String name) { super("Variable " + name + " was already defined in this scope."); } + /** + * Gets the location of the error. + * + * @return the node where the error occurred. + */ @Override public Node getLocation() { return null; diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/ArityException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/ArityException.java deleted file mode 100644 index 8daff9c7069..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/ArityException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.enso.interpreter.runtime.errors; - -import com.oracle.truffle.api.TruffleException; -import com.oracle.truffle.api.nodes.Node; - -public class ArityException extends RuntimeException { - public ArityException(int expected, int actual) { - super("Wrong number of arguments. Expected: " + expected + " but got: " + actual + "."); - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/NotInvokableException.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/NotInvokableException.java deleted file mode 100644 index 3eb1696c712..00000000000 --- a/Interpreter/src/main/java/org/enso/interpreter/runtime/errors/NotInvokableException.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.enso.interpreter.runtime.errors; - -import com.oracle.truffle.api.TruffleException; -import com.oracle.truffle.api.nodes.Node; - -public class NotInvokableException extends RuntimeException implements TruffleException { - private final Node node; - - public NotInvokableException(Object target, Node node) { - super("Object " + target + " is not invokable."); - this.node = node; - } - - @Override - public Node getLocation() { - return node; - } -} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/scope/FramePointer.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/scope/FramePointer.java new file mode 100644 index 00000000000..e0ca6666a90 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/scope/FramePointer.java @@ -0,0 +1,40 @@ +package org.enso.interpreter.runtime.scope; + +import com.oracle.truffle.api.frame.FrameSlot; + +/** + * A representation of a pointer into a stack frame at a given number of levels above the current. + */ +public class FramePointer { + private final int parentLevel; + private final FrameSlot frameSlot; + + /** + * A representation of a frame slot at a given level above the current frame. + * + * @param parentLevel the number of parents to move from the current frame to get here + * @param frameSlot the slot in the n-th parent frame + */ + public FramePointer(int parentLevel, FrameSlot frameSlot) { + this.parentLevel = parentLevel; + this.frameSlot = frameSlot; + } + + /** + * Gets the parent level. + * + * @return the parent level represented by this {@code FramePointer} + */ + public int getParentLevel() { + return parentLevel; + } + + /** + * Gets the frame slot. + * + * @return the frame slot represented by this {@code FramePointer} + */ + public FrameSlot getFrameSlot() { + return frameSlot; + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/builder/GlobalScope.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/scope/GlobalScope.java similarity index 53% rename from Interpreter/src/main/java/org/enso/interpreter/builder/GlobalScope.java rename to Interpreter/src/main/java/org/enso/interpreter/runtime/scope/GlobalScope.java index 0a4c55bf6b6..ea30555c95a 100644 --- a/Interpreter/src/main/java/org/enso/interpreter/builder/GlobalScope.java +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/scope/GlobalScope.java @@ -1,36 +1,60 @@ -package org.enso.interpreter.builder; +package org.enso.interpreter.runtime.scope; import com.oracle.truffle.api.RootCallTarget; -import org.enso.interpreter.runtime.AtomConstructor; -import org.enso.interpreter.runtime.GlobalCallTarget; - import java.util.HashMap; import java.util.Map; import java.util.Optional; +import org.enso.interpreter.runtime.GlobalCallTarget; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +/** A representation of Enso's top-level scope. */ public class GlobalScope { - private final Map globalNames = new HashMap<>(); private final Map constructors = new HashMap<>(); + /** + * Creates a new instance of the global scope. + * + * This constructor will take on the duty of registering any built-in constructors for use by the + * program. + */ public GlobalScope() { registerBuiltinConstructors(); } + /** + * Registers any built-in Atom constructors under name in the global scope. + */ private void registerBuiltinConstructors() { registerConstructor(AtomConstructor.UNIT); registerConstructor(AtomConstructor.NIL); registerConstructor(AtomConstructor.CONS); } + /** + * Adds an Atom constructor definition to the global scope. + * + * @param constructor the constructor to register + */ public void registerConstructor(AtomConstructor constructor) { constructors.put(constructor.getName(), constructor); } + /** + * Registers a new name for a call target in the global scope. + * + * @param name the name of the global variable + */ public void registerName(String name) { this.globalNames.put(name, new GlobalCallTarget(null)); } + /** + * Assigns a value to an existing global variable. + * + * @param name the name to assign to + * @param rootBinding the code to bind to that name + */ public void updateCallTarget(String name, RootCallTarget rootBinding) { GlobalCallTarget globalTarget = this.globalNames.get(name); @@ -41,10 +65,22 @@ public class GlobalScope { } } + /** + * Looks up a call target in the global scope. + * + * @param name the name of the global binding + * @return the call target associated with {@code name}, or {@link Optional#empty()} + */ public Optional getGlobalCallTarget(String name) { return Optional.ofNullable(this.globalNames.get(name)); } + /** + * Looks up a constructor in the global scope. + * + * @param name the name of the global binding + * @return the Atom constructor associated with {@code name}, or {@link Optional#empty()} + */ public Optional getConstructor(String name) { return Optional.ofNullable(this.constructors.get(name)); } diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/scope/LocalScope.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/scope/LocalScope.java new file mode 100644 index 00000000000..377e4f5e7d0 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/scope/LocalScope.java @@ -0,0 +1,100 @@ +package org.enso.interpreter.runtime.scope; + +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameSlot; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.enso.interpreter.runtime.error.VariableRedefinitionException; + +/** + * A representation of an Enso local scope. These can be arbitrarily nested and are used to map + * between the interpreter's concept of stack frames and the guest language's concept of stack + * frames. + */ +public class LocalScope { + private Map items; + private FrameDescriptor frameDescriptor; + private LocalScope parent; + + /** Creates a new local scope with defaulted arguments. */ + public LocalScope() { + items = new HashMap<>(); + frameDescriptor = new FrameDescriptor(); + parent = null; + } + + /** + * Creates a new local scope with a known parent. + * + * @param parent the parent scope + */ + public LocalScope(LocalScope parent) { + this(); + this.parent = parent; + } + + /** + * Gets the frame descriptor for this scope. + * + *

A {@link FrameDescriptor} is a handle to an interpreter frame. This provides the means to + * map between Enso's concept of frames, and the interpreter's concept of frames. + * + * @return the frame descriptor for this scope + */ + public FrameDescriptor getFrameDescriptor() { + return frameDescriptor; + } + + /** + * Gets the Enso-semantics parent of this scope. + * + * @return the parent scope + */ + public LocalScope getParent() { + return parent; + } + + /** + * Creates a scope that is the Enso-semantics child of this. + * + * @return a new scope with {@code this} as its parent + */ + public LocalScope createChild() { + return new LocalScope(this); + } + + /** + * Creates a new variable in the Enso frame. + * + * @param name the name of the variable + * @return a handle to the defined variable + */ + public FrameSlot createVarSlot(String name) { + if (items.containsKey(name)) throw new VariableRedefinitionException(name); + // The FrameSlot is created for a given identifier. + FrameSlot slot = frameDescriptor.addFrameSlot(name); + items.put(name, slot); + return slot; + } + + /** + * Reads a variable from the Enso frame. + * + * @param name the name of the variable + * @return a handle to the variable, otherwise {@link Optional#empty()} + */ + public Optional getSlot(String name) { + LocalScope scope = this; + int parentCounter = 0; + while (scope != null) { + FrameSlot slot = scope.items.get(name); + if (slot != null) { + return Optional.of(new FramePointer(parentCounter, slot)); + } + scope = scope.parent; + parentCounter++; + } + return Optional.empty(); + } +} diff --git a/Interpreter/src/main/java/org/enso/interpreter/runtime/type/Types.java b/Interpreter/src/main/java/org/enso/interpreter/runtime/type/Types.java new file mode 100644 index 00000000000..a8d046ebd86 --- /dev/null +++ b/Interpreter/src/main/java/org/enso/interpreter/runtime/type/Types.java @@ -0,0 +1,35 @@ +package org.enso.interpreter.runtime.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.ImplicitCast; +import com.oracle.truffle.api.dsl.TypeSystem; +import org.enso.interpreter.runtime.callable.Callable; +import org.enso.interpreter.runtime.callable.atom.Atom; +import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +import org.enso.interpreter.runtime.callable.function.Function; + +/** + * This class defines the interpreter-level type system for Enso. + * + *

While the language has support for rich types, the interpreter only cares about a small set of + * primitive-level types in order to make execution fast. All higher-level types can be desugared in + * terms of the more limited set of types expressed here. + * + * By declaring the primitive types here, the interpreter obtains automatically generated utilities + * for working with them. + */ +@TypeSystem({long.class, Function.class, Atom.class, AtomConstructor.class, Callable.class}) +public class Types { + + /** + * An implicit conversion between {@code int} and {@code long} for Enso programs. + * + * @param value the value to convert + * @return {@code value} as the appropriate type + */ + @ImplicitCast + @CompilerDirectives.TruffleBoundary + public static long castLong(int value) { + return value; + } +} diff --git a/Interpreter/src/main/scala/org/enso/interpreter/Parser.scala b/Interpreter/src/main/scala/org/enso/interpreter/Parser.scala index dca9a4e5300..64d02117ee8 100644 --- a/Interpreter/src/main/scala/org/enso/interpreter/Parser.scala +++ b/Interpreter/src/main/scala/org/enso/interpreter/Parser.scala @@ -13,20 +13,20 @@ trait AstExpressionVisitor[+T] { def visitVariable(name: String): T def visitFunction( - arguments: java.util.List[String], + arguments: java.util.List[AstArgDefinition], statements: java.util.List[AstExpression], retValue: AstExpression ): T def visitCaseFunction( - arguments: java.util.List[String], + arguments: java.util.List[AstArgDefinition], statements: java.util.List[AstExpression], retValue: AstExpression ): T - def visitApplication( + def visitFunctionApplication( function: AstExpression, - arguments: java.util.List[AstExpression] + arguments: java.util.List[AstCallArg] ): T def visitIf( @@ -57,15 +57,14 @@ trait AstGlobalScopeVisitor[+T] { sealed trait AstGlobalSymbol -case class AstTypeDef(name: String, arguments: List[String]) +case class AstTypeDef(name: String, arguments: List[AstArgDefinition]) extends AstGlobalSymbol { - def getArguments: java.util.List[String] = arguments.asJava + def getArguments: java.util.List[AstArgDefinition] = arguments.asJava } case class AstGlobalScope( bindings: List[AstGlobalSymbol], - expression: AstExpression) - extends { + expression: AstExpression) { def visit[T](visitor: AstGlobalScopeVisitor[T]): T = { val types = new util.ArrayList[AstTypeDef]() @@ -84,6 +83,52 @@ sealed trait AstExpression { def visit[T](visitor: AstExpressionVisitor[T]): T } +sealed trait AstArgDefinition { + def visit[T](visitor: AstArgDefinitionVisitor[T], position: Int): T +} + +trait AstArgDefinitionVisitor[+T] { + def visitDefaultedArg(name: String, value: AstExpression, position: Int): T + def visitBareArg(name: String, position: Int): T +} + +case class AstDefaultedArgDefinition(name: String, value: AstExpression) + extends AstArgDefinition { + override def visit[T](visitor: AstArgDefinitionVisitor[T], position: Int): T = + visitor.visitDefaultedArg(name, value, position) +} + +case class AstBareArgDefinition(name: String) extends AstArgDefinition { + override def visit[T](visitor: AstArgDefinitionVisitor[T], position: Int): T = + visitor.visitBareArg(name, position) +} + +sealed trait AstCallArg { + def visit[T](visitor: AstCallArgVisitor[T], position: Int): T +} + +trait AstCallArgVisitor[+T] { + def visitNamedCallArg(name: String, value: AstExpression, position: Int): T + def visitUnnamedCallArg(value: AstExpression, position: Int): T + def visitIgnore(name: String, position: Int): T +} + +case class AstNamedCallArg(name: String, value: AstExpression) + extends AstCallArg { + override def visit[T](visitor: AstCallArgVisitor[T], position: Int): T = + visitor.visitNamedCallArg(name, value, position) +} + +case class AstUnnamedCallArg(value: AstExpression) extends AstCallArg { + override def visit[T](visitor: AstCallArgVisitor[T], position: Int): T = + visitor.visitUnnamedCallArg(value, position) +} + +case class AstIgnore(name: String) extends AstCallArg { + override def visit[T](visitor: AstCallArgVisitor[T], position: Int): T = + visitor.visitIgnore(name, position) +} + case class AstLong(l: Long) extends AstExpression { override def visit[T](visitor: AstExpressionVisitor[T]): T = visitor.visitLong(l) @@ -105,14 +150,14 @@ case class AstVariable(name: String) extends AstExpression { visitor.visitVariable(name) } -case class AstApply(fun: AstExpression, args: List[AstExpression]) +case class AstApply(fun: AstExpression, args: List[AstCallArg]) extends AstExpression { override def visit[T](visitor: AstExpressionVisitor[T]): T = - visitor.visitApplication(fun, args.asJava) + visitor.visitFunctionApplication(fun, args.asJava) } case class AstFunction( - arguments: List[String], + arguments: List[AstArgDefinition], statements: List[AstExpression], ret: AstExpression) extends AstExpression { @@ -121,7 +166,7 @@ case class AstFunction( } case class AstCaseFunction( - arguments: List[String], + arguments: List[AstArgDefinition], statements: List[AstExpression], ret: AstExpression) extends AstExpression { @@ -185,10 +230,29 @@ class EnsoParserInternal extends JavaTokenParsers { case lang ~ code => AstForeign(lang, code) } - def argList: Parser[List[AstExpression]] = - delimited("[", "]", nonEmptyList(expression)) + def argList: Parser[List[AstCallArg]] = + delimited("[", "]", nonEmptyList(ignore | namedCallArg | unnamedCallArg)) - def inArgList: Parser[List[String]] = delimited("|", "|", nonEmptyList(ident)) + def namedCallArg: Parser[AstNamedCallArg] = ident ~ ("=" ~> expression) ^^ { + case name ~ expr => AstNamedCallArg(name, expr) + } + + def unnamedCallArg: Parser[AstCallArg] = expression ^^ AstUnnamedCallArg + + def defaultedArgDefinition: Parser[AstDefaultedArgDefinition] = + ident ~ ("=" ~> expression) ^^ { + case name ~ value => AstDefaultedArgDefinition(name, value) + } + + def bareArgDefinition: Parser[AstBareArgDefinition] = + ident ^^ AstBareArgDefinition + + def inArgList: Parser[List[AstArgDefinition]] = + delimited( + "|", + "|", + nonEmptyList(defaultedArgDefinition | bareArgDefinition) + ) def foreignLiteral: Parser[String] = "**" ~> "[^\\*]*".r <~ "**" @@ -206,6 +270,8 @@ class EnsoParserInternal extends JavaTokenParsers { def expression: Parser[AstExpression] = ifZero | matchClause | arith | function + def ignore: Parser[AstIgnore] = ident ~> "=" ~> "!!" ^^ AstIgnore + def call: Parser[AstApply] = "@" ~> expression ~ (argList ?) ^^ { case expr ~ args => AstApply(expr, args.getOrElse(Nil)) } @@ -216,9 +282,10 @@ class EnsoParserInternal extends JavaTokenParsers { def print: Parser[AstPrint] = "print:" ~> expression ^^ AstPrint - def ifZero: Parser[AstIfZero] = "ifZero:" ~> argList ^^ { - case List(cond, ift, iff) => AstIfZero(cond, ift, iff) - } + def ifZero: Parser[AstIfZero] = + "ifZero:" ~> "[" ~> (expression ~ ("," ~> expression ~ ("," ~> expression))) <~ "]" ^^ { + case cond ~ (ift ~ iff) => AstIfZero(cond, ift, iff) + } def function: Parser[AstFunction] = ("{" ~> (inArgList ?) ~ ((statement <~ ";") *) ~ expression <~ "}") ^^ { @@ -242,9 +309,10 @@ class EnsoParserInternal extends JavaTokenParsers { def statement: Parser[AstExpression] = assignment | print | expression - def typeDef: Parser[AstGlobalSymbol] = "type" ~> ident ~ (ident *) <~ ";" ^^ { - case name ~ args => AstTypeDef(name, args) - } + def typeDef: Parser[AstGlobalSymbol] = + "type" ~> ident ~ ((bareArgDefinition | ("(" ~> defaultedArgDefinition <~ ")")) *) <~ ";" ^^ { + case name ~ args => AstTypeDef(name, args) + } def globalScope: Parser[AstGlobalScope] = ((typeDef | assignment) *) ~ expression ^^ { diff --git a/Interpreter/src/test/scala/org/enso/interpreter/CurryingTest.scala b/Interpreter/src/test/scala/org/enso/interpreter/CurryingTest.scala new file mode 100644 index 00000000000..17ee0ab6319 --- /dev/null +++ b/Interpreter/src/test/scala/org/enso/interpreter/CurryingTest.scala @@ -0,0 +1,20 @@ +package org.enso.interpreter; + +class CurryingTest extends LanguageTest { + + "Functions" should "allow defaulted values to be removed" in { + pending + val code = + """ + |addNum = { |a, num = 10| a + num } + | + |add = @addNum [num = !!] + | + |0 + """.stripMargin + + noException should be thrownBy parse(code) + eval(code) shouldEqual 0 + } + +} diff --git a/Interpreter/src/test/scala/org/enso/interpreter/FunctionArgumentsTest.scala b/Interpreter/src/test/scala/org/enso/interpreter/FunctionArgumentsTest.scala index 7771cfee793..5dfab253180 100644 --- a/Interpreter/src/test/scala/org/enso/interpreter/FunctionArgumentsTest.scala +++ b/Interpreter/src/test/scala/org/enso/interpreter/FunctionArgumentsTest.scala @@ -1,5 +1,7 @@ package org.enso.interpreter +import org.graalvm.polyglot.PolyglotException + class FunctionArgumentsTest extends LanguageTest { "Functions" should "take arguments and use them in their bodies" in { val code = "{ |x| x * x }" @@ -24,7 +26,7 @@ class FunctionArgumentsTest extends LanguageTest { "Recursion" should "work" in { val code = """ - |@{ + |@{ | sumTo = { |x| ifZero: [x, 0, x + (@sumTo [x - 1])] }; | @sumTo [10] |} @@ -32,4 +34,35 @@ class FunctionArgumentsTest extends LanguageTest { eval(code) shouldEqual 55 } + + "Functions" should "not take more arguments than they are defined for" in { + pending + val code = + """ + |@{ + | sumTo = { |x| ifZero: [x, 0, x + (@sumTo [x - 1])] }; + | @sumTo [10, 11] + |} + """.stripMargin + + val errMsg = + """ + |org.enso.interpreter.runtime.errors.ArityException: Wrong number of + | arguments. Expected: 1 but got: 2. + |""".stripMargin.replaceAll("\n", "") + + the[PolyglotException] thrownBy eval(code) should have message errMsg + } + + "Function calls" should "accept more arguments than needed and pass them to the result upon execution" in { + pending + val code = + """ + |f = { |x| { |z| x + z } } + |@f [1, 2] + |""".stripMargin + + eval(code) shouldEqual 3 + } + } diff --git a/Interpreter/src/test/scala/org/enso/interpreter/GlobalScopeTest.scala b/Interpreter/src/test/scala/org/enso/interpreter/GlobalScopeTest.scala index 0d6f872944f..ad355bab80d 100644 --- a/Interpreter/src/test/scala/org/enso/interpreter/GlobalScopeTest.scala +++ b/Interpreter/src/test/scala/org/enso/interpreter/GlobalScopeTest.scala @@ -3,7 +3,6 @@ package org.enso.interpreter import org.graalvm.polyglot.PolyglotException class GlobalScopeTest extends LanguageTest { - // TODO [AA] Should work with bare vars. "Variables" should "be able to be read from the global scope" in { val code = """ diff --git a/Interpreter/src/test/scala/org/enso/interpreter/LanguageTest.scala b/Interpreter/src/test/scala/org/enso/interpreter/LanguageTest.scala index c60983b85e3..f153757fbb3 100644 --- a/Interpreter/src/test/scala/org/enso/interpreter/LanguageTest.scala +++ b/Interpreter/src/test/scala/org/enso/interpreter/LanguageTest.scala @@ -1,8 +1,10 @@ package org.enso.interpreter -import org.graalvm.polyglot.{Context, Value} +import org.graalvm.polyglot.Context +import org.graalvm.polyglot.Value import org.scalactic.Equality -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.FlatSpec +import org.scalatest.Matchers trait LanguageRunner { implicit class RichValue(value: Value) { @@ -10,6 +12,9 @@ trait LanguageRunner { } val ctx = Context.newBuilder(Constants.LANGUAGE_ID).build() def eval(code: String): Value = ctx.eval(Constants.LANGUAGE_ID, code) + + def parse(code: String): AstGlobalScope = + new EnsoParser().parseEnso(code) } abstract class LanguageTest extends FlatSpec with Matchers with LanguageRunner { diff --git a/Interpreter/src/test/scala/org/enso/interpreter/NamedArgumentsTest.scala b/Interpreter/src/test/scala/org/enso/interpreter/NamedArgumentsTest.scala new file mode 100644 index 00000000000..f9f3e9f05d8 --- /dev/null +++ b/Interpreter/src/test/scala/org/enso/interpreter/NamedArgumentsTest.scala @@ -0,0 +1,268 @@ +package org.enso.interpreter + +import org.graalvm.polyglot.PolyglotException + +class NamedArgumentsTest extends LanguageTest { + "Functions" should "take arguments by name and use them in their bodies" in { + + val code = + """ + |a = 10 + |addTen = { |b| a + b } + | + |@addTen [b = 10] + """.stripMargin + + eval(code) shouldEqual 20 + } + + "Functions" should "be able to have named arguments given out of order" in { + val code = + """ + |subtract = { |a, b| a - b } + | + |@subtract [b = 10, a = 5] + """.stripMargin + + eval(code) shouldEqual -5 + } + + "Functions" should "be able to have scope values as named arguments" in { + val code = + """ + |a = 10 + |addTen = { |num| num + a } + | + |@addTen [num = a] + """.stripMargin + + eval(code) shouldEqual 20 + } + + "Functions" should "be able to be defined with default argument values" in { + val code = + """ + |addNum = { |a, num = 10| a + num } + | + |@addNum [5] + """.stripMargin + + eval(code) shouldEqual 15 + } + + "Default arguments" should "be able to default to complex expressions" in { + val code = + """ + |add = { |a, b| a + b } + | + |doThing = { |a, b = @add [1, 2]| a + b } + | + |@doThing [10] + |""".stripMargin + + eval(code) shouldEqual 13 + } + + "Default arguments" should "be able to close over their outer scope" in { + val code = + """ + |id = { |x| x } + | + |apply = { |val, fn = id| @id [val] } + | + |@apply [val = 1] + |""".stripMargin + + eval(code) shouldEqual 1 + } + + "Functions" should "use their default values when none is supplied" in { + val code = + """ + |addTogether = { |a = 5, b = 6| a + b } + | + |@addTogether + """.stripMargin + + eval(code) shouldEqual 11 + } + + "Functions" should "override defaults by name" in { + val code = + """ + |addNum = { |a, num = 10| a + num } + | + |@addNum [1, num = 1] + """.stripMargin + + eval(code) shouldEqual 2 + } + + "Functions" should "override defaults by position" in { + val code = + """ + |addNum = { |a, num = 10| a + num } + | + |@addNum [1, 2] + |""".stripMargin + + eval(code) shouldEqual 3 + } + + "Defaulted arguments" should "work in a recursive context" in { + val code = + """ + |summer = { |sumTo| + | summator = { |acc = 0, current| + | ifZero: [current, acc, @summator [current = current - 1, acc = acc + current]] + | }; + | res = @summator [current = sumTo]; + | res + |} + | + |@summer [100] + """.stripMargin + + eval(code) shouldEqual 5050 + } + + "Named Arguments" should "only be scoped to their definitions" in { + val code = + """ + |foo = { |x, y| x - y } + |bar = { |y, x| x - y } + | + |baz = { |f| @f [x = 10, y = 11] } + | + |a = @baz [foo] + |b = @baz [bar] + | + |a - b + |""".stripMargin + + eval(code) shouldEqual 0 + } + + "Named arguments" should "be applied in a sequence compatible with Eta-expansions" in { + pending + val code = + """ + |foo = { |a, b, c| a + b } + |@foo [20, a = 10] + |""".stripMargin + } + + "Default arguments" should "be able to depend on prior arguments" in { + val code = + """ + |doubleOrAdd = { |a, b = a| a + b } + | + |@doubleOrAdd [5] + |""".stripMargin + + eval(code) shouldEqual 10 + } + + "Default arguments" should "not be able to depend on later arguments" in { + val code = + """ + |badArgFn = { | a, b = c, c = a | a + b + c } + | + |@badArgFn [3] + |""".stripMargin + + val errMsg = "java.lang.RuntimeException: No result when parsing failed" + + the[PolyglotException] thrownBy eval(code) should have message errMsg + } + + "Constructors" should "be able to use named arguments" in { + val code = + """ + |type Cons2 head rest; + |type Nil2; + | + |genList = { |i| ifZero: [i, @Nil2, @Cons2 [rest = @genList [i-1], head = i]] } + | + |sumList = { |list| match list < + | Cons2 ~ { |head, rest| head + @sumList [rest] }; + | Nil2 ~ { 0 }; + |>} + | + |@sumList [@genList [10]] + """.stripMargin + + eval(code) shouldEqual 55 + } + + "Constructors" should "be able to take default arguments that are overridden" in { + val code = + """ + |type Nil2; + |type Cons2 head (rest = Nil2); + | + |genList = { |i| ifZero: [i, @Nil2, @Cons2 [rest = @genList [i-1], head = i]] } + | + |sumList = { |list| match list < + | Cons2 ~ { |head, rest| head + @sumList [rest] }; + | Nil2 ~ { 0 }; + |>} + | + |@sumList [@genList [5]] + """.stripMargin + + eval(code) shouldEqual 15 + } + + "Default arguments to constructors" should "be resolved dynamically" in { + val code = + """ + |type Cons2 head (rest = Nil2); + |type Nil2; + | + |5 + |""".stripMargin + + pending + eval(code) shouldEqual 5 + } + + "Constructors" should "be able to take and use default arguments" in { + pending + val code = + """ + |type Nil2; + |type Cons2 head (rest = Nil2); + | + |sumList = { |list| match list < + | Cons2 ~ { |head, rest| head + @sumList [rest] }; + | Nil2 ~ { 0 }; + |>} + | + |@sumList [@Cons2 [10]] + """.stripMargin + + eval(code) shouldEqual 10 + } + + "Constructor arguments" should "be matchable in arbitrary order by name" in { + val code = + """ + |type Nil2; + |type Cons2 head (rest = Nil2); + | + |genList = { |i| ifZero: [i, @Nil2, @Cons2 [rest = @genList [i-1], head = i]] } + | + |sumList = { |list| match list < + | Cons2 ~ { |rest, head| head + @sumList [rest] }; + | Nil2 ~ { 0 }; + |>} + | + |@sumList [@genList [5]] + """.stripMargin + + pending + eval(code) shouldEqual 15 + } + +} diff --git a/build.sbt b/build.sbt index 356900766d0..2f7a4bcdf04 100644 --- a/build.sbt +++ b/build.sbt @@ -102,6 +102,8 @@ lazy val interpreter = (project in file("Interpreter")) .settings( libraryDependencies ++= Seq( "com.chuusai" %% "shapeless" % "2.3.3", + "org.apache.commons" % "commons-lang3" % "3.9", + "org.apache.tika" % "tika-core" % "1.21", "org.graalvm.sdk" % "graal-sdk" % "19.0.0", "org.graalvm.sdk" % "polyglot-tck" % "19.0.0", "org.graalvm.truffle" % "truffle-api" % "19.0.0", @@ -109,12 +111,11 @@ lazy val interpreter = (project in file("Interpreter")) "org.graalvm.truffle" % "truffle-nfi" % "19.0.0", "org.graalvm.truffle" % "truffle-tck" % "19.0.0", "org.graalvm.truffle" % "truffle-tck-common" % "19.0.0", - "org.scalacheck" %% "scalacheck" % "1.14.0" % Test, - "org.scalatest" %% "scalatest" % "3.2.0-SNAP10" % Test, - "org.scalactic" %% "scalactic" % "3.0.8" % Test, - "org.typelevel" %% "cats-core" % "2.0.0-M4", "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.4", - "org.apache.commons" % "commons-lang3" % "3.9" + "org.scalacheck" %% "scalacheck" % "1.14.0" % Test, + "org.scalactic" %% "scalactic" % "3.0.8" % Test, + "org.scalatest" %% "scalatest" % "3.2.0-SNAP10" % Test, + "org.typelevel" %% "cats-core" % "2.0.0-M4", ), libraryDependencies ++= jmh )