diff --git a/part-13/build.gradle b/part-13/build.gradle new file mode 100644 index 00000000..34f6fda4 --- /dev/null +++ b/part-13/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'antlr' + id 'me.champeau.jmh' version '0.6.6' +} + +dependencies { + antlr "org.antlr:antlr4:$antlr_version" + implementation "org.graalvm.js:js:$graal_version" + implementation "org.graalvm.tools:profiler:$graal_version" + implementation "org.apache.commons:commons-text:1.10.0" +} + +// required to allow GraalVM to discover our EasyScript language class +test { + jvmArgs '-Dgraalvm.locatorDisabled=true' +} diff --git a/part-13/src/jmh/java/com/endoflineblog/truffle/part_13/InstanceMethodBenchmark.java b/part-13/src/jmh/java/com/endoflineblog/truffle/part_13/InstanceMethodBenchmark.java new file mode 100644 index 00000000..af23d78b --- /dev/null +++ b/part-13/src/jmh/java/com/endoflineblog/truffle/part_13/InstanceMethodBenchmark.java @@ -0,0 +1,69 @@ +package com.endoflineblog.truffle.part_13; + +import org.openjdk.jmh.annotations.Benchmark; + +/** + * A simple benchmark for calling an instance method of a user-defined class. + */ +public class InstanceMethodBenchmark extends TruffleBenchmark { + private static final int INPUT = 1_000_000; + + private static final String ADDER_CLASS = "" + + "class Adder { " + + " add(a, b) { " + + " return a + b; " + + " } " + + "}"; + + @Override + public void setup() { + super.setup(); + + this.truffleContext.eval("ezs", ADDER_CLASS); + this.truffleContext.eval("ezs", COUNT_METHOD_PROP_ALLOC_INSIDE_FOR); + this.truffleContext.eval("ezs", COUNT_METHOD_PROP_ALLOC_OUTSIDE_FOR); + + this.truffleContext.eval("js", ADDER_CLASS); + this.truffleContext.eval("js", COUNT_METHOD_PROP_ALLOC_INSIDE_FOR); + this.truffleContext.eval("js", COUNT_METHOD_PROP_ALLOC_OUTSIDE_FOR); + } + + private static final String COUNT_METHOD_PROP_ALLOC_INSIDE_FOR = "" + + "function countMethodPropAllocInsideFor(n) { " + + " var ret = 0; " + + " for (let i = 0; i < n; i = i + 1) { " + + " ret = new Adder().add(ret, 1); " + + " } " + + " return ret; " + + "}"; + + @Benchmark + public int count_method_prop_alloc_inside_for_ezs() { + return this.truffleContext.eval("ezs", "countMethodPropAllocInsideFor(" + INPUT + ");").asInt(); + } + + @Benchmark + public int count_method_prop_alloc_inside_for_js() { + return this.truffleContext.eval("js", "countMethodPropAllocInsideFor(" + INPUT + ");").asInt(); + } + + private static final String COUNT_METHOD_PROP_ALLOC_OUTSIDE_FOR = "" + + "function countMethodPropAllocOutsideFor(n) { " + + " var ret = 0; " + + " const adder = new Adder(); " + + " for (let i = 0; i < n; i = i + 1) { " + + " ret = adder.add(ret, 1); " + + " } " + + " return ret; " + + "}"; + + @Benchmark + public int count_method_prop_alloc_outside_for_ezs() { + return this.truffleContext.eval("ezs", "countMethodPropAllocOutsideFor(" + INPUT + ");").asInt(); + } + + @Benchmark + public int count_method_prop_alloc_outside_for_js() { + return this.truffleContext.eval("js", "countMethodPropAllocOutsideFor(" + INPUT + ");").asInt(); + } +} diff --git a/part-13/src/jmh/java/com/endoflineblog/truffle/part_13/TruffleBenchmark.java b/part-13/src/jmh/java/com/endoflineblog/truffle/part_13/TruffleBenchmark.java new file mode 100644 index 00000000..6424ed34 --- /dev/null +++ b/part-13/src/jmh/java/com/endoflineblog/truffle/part_13/TruffleBenchmark.java @@ -0,0 +1,43 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +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.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +/** + * The common superclass of all JMH benchmarks. + * Identical to the class with the same name from part 11. + */ +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 1, jvmArgsAppend = { + "-Dgraalvm.locatorDisabled=true", + "--add-exports", + "org.graalvm.truffle/com.oracle.truffle.api.staticobject=ALL-UNNAMED" +}) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +public abstract class TruffleBenchmark { + protected Context truffleContext; + + @Setup + public void setup() { + this.truffleContext = Context.create(); + } + + @TearDown + public void tearDown() { + this.truffleContext.close(); + } +} diff --git a/part-13/src/main/antlr/com/endoflineblog/truffle/part_13/parsing/antlr/EasyScript.g4 b/part-13/src/main/antlr/com/endoflineblog/truffle/part_13/parsing/antlr/EasyScript.g4 new file mode 100644 index 00000000..28264ee9 --- /dev/null +++ b/part-13/src/main/antlr/com/endoflineblog/truffle/part_13/parsing/antlr/EasyScript.g4 @@ -0,0 +1,70 @@ +grammar EasyScript ; + +@header{ +package com.endoflineblog.truffle.part_13.parsing.antlr; +} + +start : stmt+ EOF ; + +stmt : kind=('var' | 'let' | 'const') binding (',' binding)* ';'? #VarDeclStmt + | expr1 ';'? #ExprStmt + | 'function' subroutine_decl ';'? #FuncDeclStmt + | 'return' expr1? ';'? #ReturnStmt + | '{' stmt* '}' ';'? #BlockStmt + | 'if' '(' cond=expr1 ')' then_stmt=stmt ('else' else_stmt=stmt)? #IfStmt + | 'while' '(' cond=expr1 ')' body=stmt #WhileStmt + | 'do' '{' stmt* '}' 'while' '(' cond=expr1 ')' ';'? #DoWhileStmt + | 'for' '(' init=stmt? ';' cond=expr1? ';' updt=expr1? ')' body=stmt #ForStmt + | 'break' ';'? #BreakStmt + | 'continue' ';'? #ContinueStmt + | 'class' ID '{' class_member* '}' ';'? #ClassDeclStmt + ; +binding : ID ('=' expr1)? ; +class_member : subroutine_decl ; +subroutine_decl : name=ID '(' args=func_args ')' '{' stmt* '}' ; +func_args : (ID (',' ID)* )? ; + +expr1 : ID '=' expr1 #AssignmentExpr1 + | arr=expr5 '[' index=expr1 ']' '=' rvalue=expr1 #ArrayIndexWriteExpr1 + | expr2 #PrecedenceTwoExpr1 + ; +expr2 : left=expr2 c=('===' | '!==') right=expr3 #EqNotEqExpr2 + | expr3 #PrecedenceThreeExpr2 + ; +expr3 : left=expr3 c=('<' | '<=' | '>' | '>=') right=expr4 #ComparisonExpr3 + | expr4 #PrecedenceFourExpr3 + ; +expr4 : left=expr4 o=('+' | '-') right=expr5 #AddSubtractExpr4 + | '-' expr5 #UnaryMinusExpr4 + | expr5 #PrecedenceFiveExpr4 + ; +expr5 : expr5 '.' ID #PropertyReadExpr5 + | arr=expr5 '[' index=expr1 ']' #ArrayIndexReadExpr5 + | expr5 '(' (expr1 (',' expr1)*)? ')' #CallExpr5 + | expr6 #PrecedenceSixExpr5 + ; +expr6 : literal #LiteralExpr6 + | ID #ReferenceExpr6 + | '[' (expr1 (',' expr1)*)? ']' #ArrayLiteralExpr6 + | 'new' constr=expr6 ('('(expr1 (',' expr1)*)?')')? #NewExpr6 + | '(' expr1 ')' #PrecedenceOneExpr6 + ; + +literal : INT | DOUBLE | 'undefined' | bool_literal | string_literal ; +bool_literal : 'true' | 'false' ; + +fragment DIGIT : [0-9] ; +INT : DIGIT+ ; +DOUBLE : DIGIT+ '.' DIGIT+ ; + +fragment LETTER : [a-zA-Z$_] ; +ID : LETTER (LETTER | DIGIT)* ; + +string_literal: SINGLE_QUOTE_STRING | DOUBLE_QUOTE_STRING ; +// see https://stackoverflow.com/questions/24557953/handling-string-literals-which-end-in-an-escaped-quote-in-antlr4 +// for details +SINGLE_QUOTE_STRING : '\'' (~[\\'\r\n] | '\\' ~[\r\n])* '\'' ; +DOUBLE_QUOTE_STRING : '"' (~[\\"\r\n] | '\\' ~[\r\n])* '"' ; + +// skip all whitespace +WS : (' ' | '\r' | '\t' | '\n' | '\f')+ -> skip ; diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptLanguageContext.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptLanguageContext.java new file mode 100644 index 00000000..35b6dfc3 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptLanguageContext.java @@ -0,0 +1,36 @@ +package com.endoflineblog.truffle.part_13; + +import com.endoflineblog.truffle.part_13.runtime.GlobalScopeObject; +import com.endoflineblog.truffle.part_13.runtime.StringPrototype; +import com.oracle.truffle.api.TruffleLanguage; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.Shape; + +/** + * The class of the context for the + * {@link EasyScriptTruffleLanguage TruffleLanguage implementaton in this part of the series}. + * Identical to the class with the same name from part 11. + */ +public final class EasyScriptLanguageContext { + private static final TruffleLanguage.ContextReference REF = + TruffleLanguage.ContextReference.create(EasyScriptTruffleLanguage.class); + + /** Retrieve the current language context for the given {@link Node}. */ + public static EasyScriptLanguageContext get(Node node) { + return REF.get(node); + } + + public final DynamicObject globalScopeObject; + + /** + * The object containing the {@code CallTarget}s + * for the built-in methods of strings. + */ + public final StringPrototype stringPrototype; + + public EasyScriptLanguageContext(Shape globalScopeShape, StringPrototype stringPrototype) { + this.globalScopeObject = new GlobalScopeObject(globalScopeShape); + this.stringPrototype = stringPrototype; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptTruffleLanguage.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptTruffleLanguage.java new file mode 100644 index 00000000..1bc57dcb --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptTruffleLanguage.java @@ -0,0 +1,104 @@ +package com.endoflineblog.truffle.part_13; + +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.ReadFunctionArgExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in.AbsFunctionBodyExprNodeFactory; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in.BuiltInFunctionBodyExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in.PowFunctionBodyExprNodeFactory; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in.methods.CharAtMethodBodyExprNodeFactory; +import com.endoflineblog.truffle.part_13.nodes.root.BuiltInFuncRootNode; +import com.endoflineblog.truffle.part_13.nodes.root.StmtBlockRootNode; +import com.endoflineblog.truffle.part_13.parsing.EasyScriptTruffleParser; +import com.endoflineblog.truffle.part_13.parsing.ParsingResult; +import com.endoflineblog.truffle.part_13.runtime.ArrayObject; +import com.endoflineblog.truffle.part_13.runtime.FunctionObject; +import com.endoflineblog.truffle.part_13.runtime.MathObject; +import com.endoflineblog.truffle.part_13.runtime.StringPrototype; +import com.endoflineblog.truffle.part_13.runtime.ClassPrototypeObject; +import com.endoflineblog.truffle.part_13.runtime.GlobalScopeObject; +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.TruffleLanguage; +import com.oracle.truffle.api.dsl.NodeFactory; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.object.DynamicObjectLibrary; +import com.oracle.truffle.api.object.Shape; + +import java.util.stream.IntStream; + +/** + * The {@link TruffleLanguage} implementation for this part of the article series. + * Very similar to the class with the same name from part 11, + * the only difference is that we rename the field {@code globalScopeShape} + * to {@code rootShape}, as it's now used as the {@link Shape} + * of the {@link ClassPrototypeObject} + * {@link com.oracle.truffle.api.object.DynamicObject}, + * in addition to the {@link GlobalScopeObject}, + * and we also pass it to the + * {@link EasyScriptTruffleParser#parse main parsing method}. + */ +@TruffleLanguage.Registration(id = "ezs", name = "EasyScript") +public final class EasyScriptTruffleLanguage extends TruffleLanguage { + private static final LanguageReference REF = + LanguageReference.create(EasyScriptTruffleLanguage.class); + + /** Retrieve the current language instance for the given {@link Node}. */ + public static EasyScriptTruffleLanguage get(Node node) { + return REF.get(node); + } + + /** The root {@link Shape} for {@link ArrayObject} */ + private final Shape arrayShape = Shape.newBuilder().layout(ArrayObject.class).build(); + + /** + * The root {@link Shape} for {@link GlobalScopeObject} + * and {@link ClassPrototypeObject}. + */ + private final Shape rootShape = Shape.newBuilder().build(); + + @Override + protected CallTarget parse(ParsingRequest request) throws Exception { + ParsingResult parsingResult = EasyScriptTruffleParser.parse( + request.getSource().getReader(), this.rootShape, this.arrayShape); + var programRootNode = new StmtBlockRootNode(this, parsingResult.topLevelFrameDescriptor, + parsingResult.programStmtBlock); + return programRootNode.getCallTarget(); + } + + @Override + protected EasyScriptLanguageContext createContext(Env env) { + var context = new EasyScriptLanguageContext(this.rootShape, this.createStringPrototype()); + var globalScopeObject = context.globalScopeObject; + + var objectLibrary = DynamicObjectLibrary.getUncached(); + // the 1 flag indicates Math is a constant, and cannot be reassigned + objectLibrary.putConstant(globalScopeObject, "Math", MathObject.create(this, + this.defineBuiltInFunction(AbsFunctionBodyExprNodeFactory.getInstance()), + this.defineBuiltInFunction(PowFunctionBodyExprNodeFactory.getInstance())), 1); + + return context; + } + + @Override + protected Object getScope(EasyScriptLanguageContext context) { + return context.globalScopeObject; + } + + private StringPrototype createStringPrototype() { + return new StringPrototype( + this.createCallTarget(CharAtMethodBodyExprNodeFactory.getInstance())); + } + + private FunctionObject defineBuiltInFunction(NodeFactory nodeFactory) { + return new FunctionObject(this.createCallTarget(nodeFactory), + nodeFactory.getExecutionSignature().size()); + } + + private CallTarget createCallTarget(NodeFactory nodeFactory) { + int argumentCount = nodeFactory.getExecutionSignature().size(); + ReadFunctionArgExprNode[] functionArguments = IntStream.range(0, argumentCount) + .mapToObj(i -> new ReadFunctionArgExprNode(i)) + .toArray(ReadFunctionArgExprNode[]::new); + var rootNode = new BuiltInFuncRootNode(this, + nodeFactory.createNode((Object) functionArguments)); + return rootNode.getCallTarget(); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptTypeSystem.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptTypeSystem.java new file mode 100644 index 00000000..6707080a --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/EasyScriptTypeSystem.java @@ -0,0 +1,20 @@ +package com.endoflineblog.truffle.part_13; + +import com.oracle.truffle.api.dsl.ImplicitCast; +import com.oracle.truffle.api.dsl.TypeSystem; + +/** + * The {@link TypeSystem} for EasyScript. + * Identical to the class with the same name from part 11. + */ +@TypeSystem({ + boolean.class, + int.class, + double.class, +}) +public abstract class EasyScriptTypeSystem { + @ImplicitCast + public static double castIntToDouble(int value) { + return value; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/common/DeclarationKind.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/common/DeclarationKind.java new file mode 100644 index 00000000..d36d2191 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/common/DeclarationKind.java @@ -0,0 +1,27 @@ +package com.endoflineblog.truffle.part_13.common; + +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; + +/** + * An enum that represents the different kinds of variable declarations in JavaScript. + * Identical to the enum with the same name from part 11. + */ +public enum DeclarationKind { + /** This represents the 'var' declaration kind. */ + VAR, + + /** This represents the 'let' declaration kind. */ + LET, + + /** This represents the 'const' declaration kind. */ + CONST; + + public static DeclarationKind fromToken(String token) { + switch (token) { + case "var": return DeclarationKind.VAR; + case "let": return DeclarationKind.LET; + case "const": return DeclarationKind.CONST; + default: throw new EasyScriptException("Unrecognized variable kind: '" + token + "'"); + } + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/common/LocalVariableFrameSlotId.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/common/LocalVariableFrameSlotId.java new file mode 100644 index 00000000..a93481c3 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/common/LocalVariableFrameSlotId.java @@ -0,0 +1,37 @@ +package com.endoflineblog.truffle.part_13.common; + +import java.util.Objects; + +/** + * A class that represents the full identifier of a local variable used for a frame slot. + * Identical to the class with the same name from part 11. + */ +public final class LocalVariableFrameSlotId { + public final String variableName; + public final int index; + + public LocalVariableFrameSlotId(String variableName, int index) { + this.variableName = variableName; + this.index = index; + } + + @Override + public int hashCode() { + return Objects.hash(variableName, index); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof LocalVariableFrameSlotId)) { + return false; + } + var that = (LocalVariableFrameSlotId) other; + return this.index == that.index && + this.variableName.equals(that.variableName); + } + + @Override + public String toString() { + return this.variableName + "-" + this.index; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/BreakException.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/BreakException.java new file mode 100644 index 00000000..8ffb835f --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/BreakException.java @@ -0,0 +1,13 @@ +package com.endoflineblog.truffle.part_13.exceptions; + +import com.endoflineblog.truffle.part_13.nodes.stmts.controlflow.BreakStmtNode; +import com.oracle.truffle.api.nodes.ControlFlowException; + +/** + * The exception used to implement the {@code break} statement. + * Identical to the class with the same name from part 11. + * + * @see BreakStmtNode + */ +public final class BreakException extends ControlFlowException { +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/ContinueException.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/ContinueException.java new file mode 100644 index 00000000..9e3f9205 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/ContinueException.java @@ -0,0 +1,13 @@ +package com.endoflineblog.truffle.part_13.exceptions; + +import com.endoflineblog.truffle.part_13.nodes.stmts.controlflow.ContinueStmtNode; +import com.oracle.truffle.api.nodes.ControlFlowException; + +/** + * The exception used to implement the {@code continue} statement. + * Identical to the class with the same name from part 11. + * + * @see ContinueStmtNode + */ +public final class ContinueException extends ControlFlowException { +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/EasyScriptException.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/EasyScriptException.java new file mode 100644 index 00000000..a4402665 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/EasyScriptException.java @@ -0,0 +1,18 @@ +package com.endoflineblog.truffle.part_13.exceptions; + +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.nodes.Node; + +/** + * The exception that's thrown from the EasyScript implementation if any semantic errors are found. + * Identical to the class with the same name from part 11. + */ +public final class EasyScriptException extends AbstractTruffleException { + public EasyScriptException(String message) { + this(null, message); + } + + public EasyScriptException(Node location, String message) { + super(message, location); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/ReturnException.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/ReturnException.java new file mode 100644 index 00000000..18ed6239 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/exceptions/ReturnException.java @@ -0,0 +1,19 @@ +package com.endoflineblog.truffle.part_13.exceptions; + +import com.endoflineblog.truffle.part_13.nodes.stmts.controlflow.ReturnStmtNode; +import com.oracle.truffle.api.nodes.ControlFlowException; + +/** + * The exception used to implement the {@code return} statement. + * Identical to the class with the same name from part 11. + * + * @see ReturnStmtNode + */ +public final class ReturnException extends ControlFlowException { + /** The value to return from the function. */ + public final Object returnValue; + + public ReturnException(Object returnValue) { + this.returnValue = returnValue; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/EasyScriptNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/EasyScriptNode.java new file mode 100644 index 00000000..7e8c6d45 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/EasyScriptNode.java @@ -0,0 +1,21 @@ +package com.endoflineblog.truffle.part_13.nodes; + +import com.endoflineblog.truffle.part_13.EasyScriptLanguageContext; +import com.endoflineblog.truffle.part_13.EasyScriptTruffleLanguage; +import com.oracle.truffle.api.nodes.Node; + +/** + * The abstract common ancestor of all EasyScript AST Truffle Nodes. + * Identical to the class with the same name from part 11. + */ +public abstract class EasyScriptNode extends Node { + /** Allows retrieving the current Truffle language instance from within a Node. */ + protected final EasyScriptTruffleLanguage currentTruffleLanguage() { + return EasyScriptTruffleLanguage.get(this); + } + + /** Allows retrieving the current Truffle language Context from within a Node. */ + protected final EasyScriptLanguageContext currentLanguageContext() { + return EasyScriptLanguageContext.get(this); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/BinaryOperationExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/BinaryOperationExprNode.java new file mode 100644 index 00000000..4097c3df --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/BinaryOperationExprNode.java @@ -0,0 +1,14 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs; + +import com.oracle.truffle.api.dsl.NodeChild; + +/** + * The common superclass of all EasyScript expression Nodes that take two arguments. + * Allows us to save having to put the same {@link NodeChild} + * annotations on a bunch of class declarations. + * Identical to the class with the same name from part 11. + */ +@NodeChild("leftSide") +@NodeChild("rightSide") +public abstract class BinaryOperationExprNode extends EasyScriptExprNode { +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/DynamicObjectReferenceExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/DynamicObjectReferenceExprNode.java new file mode 100644 index 00000000..be067f65 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/DynamicObjectReferenceExprNode.java @@ -0,0 +1,27 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs; + +import com.endoflineblog.truffle.part_13.nodes.stmts.variables.FuncDeclStmtNode; +import com.endoflineblog.truffle.part_13.runtime.ClassPrototypeObject; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.object.DynamicObject; + +/** + * A simple expression Node that just returns the given {@link DynamicObject}. + * Used for handling methods inside class declarations, + * by passing an instance of {@link ClassPrototypeObject} + * to the constructor of this class, + * and then passing the instance created from that constructor to + * {@link FuncDeclStmtNode}. + */ +public final class DynamicObjectReferenceExprNode extends EasyScriptExprNode { + private final DynamicObject dynamicObject; + + public DynamicObjectReferenceExprNode(DynamicObject dynamicObject) { + this.dynamicObject = dynamicObject; + } + + @Override + public DynamicObject executeGeneric(VirtualFrame frame) { + return this.dynamicObject; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/EasyScriptExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/EasyScriptExprNode.java new file mode 100644 index 00000000..d9b6ed6c --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/EasyScriptExprNode.java @@ -0,0 +1,51 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs; + +import com.endoflineblog.truffle.part_13.EasyScriptTypeSystem; +import com.endoflineblog.truffle.part_13.EasyScriptTypeSystemGen; +import com.endoflineblog.truffle.part_13.nodes.EasyScriptNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.dsl.TypeSystemReference; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.UnexpectedResultException; +import com.oracle.truffle.api.strings.TruffleString; + +/** + * The abstract common ancestor of all expression Nodes in EasyScript. + * Identical to the class with the same name from part 11. + */ +@TypeSystemReference(EasyScriptTypeSystem.class) +public abstract class EasyScriptExprNode extends EasyScriptNode { + public abstract Object executeGeneric(VirtualFrame frame); + + public boolean executeBool(VirtualFrame frame) { + Object value = this.executeGeneric(frame); + // 'undefined' is falsy + if (value == Undefined.INSTANCE) { + return false; + } + if (value instanceof Boolean) { + return (Boolean) value; + } + + // a number is falsy when it's 0 + if (value instanceof Integer) { + return (Integer) value != 0; + } + if (value instanceof Double) { + return (Double) value != 0.0; + } + if (value instanceof TruffleString) { + return !((TruffleString) value).isEmpty(); + } + // all other values are truthy + return true; + } + + public int executeInt(VirtualFrame frame) throws UnexpectedResultException { + return EasyScriptTypeSystemGen.expectInteger(this.executeGeneric(frame)); + } + + public double executeDouble(VirtualFrame frame) throws UnexpectedResultException { + return EasyScriptTypeSystemGen.expectDouble(this.executeGeneric(frame)); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/GlobalScopeObjectExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/GlobalScopeObjectExprNode.java new file mode 100644 index 00000000..96a27be9 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/GlobalScopeObjectExprNode.java @@ -0,0 +1,25 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs; + +import com.endoflineblog.truffle.part_13.nodes.EasyScriptNode; +import com.endoflineblog.truffle.part_13.runtime.GlobalScopeObject; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.object.DynamicObject; + +/** + * A simple expression class that returns the + * {@link GlobalScopeObject global scope object} + * using the inherited {@link #currentLanguageContext()} method from + * {@link EasyScriptNode}. + * Used by classes that access the global scope, + * like global variable declaration or assignment, + * so that they can have this Node as a child, + * and then use the {@link com.oracle.truffle.api.library.CachedLibrary} + * annotation in their {@link Specialization} methods. + * Identical to the class with the same name from part 11. + */ +public abstract class GlobalScopeObjectExprNode extends EasyScriptExprNode { + @Specialization + protected DynamicObject returnGlobalScopeObject() { + return this.currentLanguageContext().globalScopeObject; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/AdditionExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/AdditionExprNode.java new file mode 100644 index 00000000..5386a60e --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/AdditionExprNode.java @@ -0,0 +1,73 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.arithmetic; + +import com.endoflineblog.truffle.part_13.EasyScriptTypeSystemGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.BinaryOperationExprNode; +import com.endoflineblog.truffle.part_13.runtime.EasyScriptTruffleStrings; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +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.strings.TruffleString; + +/** + * The Node representing number addition. + * Identical to the class with the same name from part 11. + */ +public abstract class AdditionExprNode extends BinaryOperationExprNode { + @Specialization(rewriteOn = ArithmeticException.class) + protected int addInts(int leftValue, int rightValue) { + return Math.addExact(leftValue, rightValue); + } + + @Specialization(replaces = "addInts") + protected double addDoubles(double leftValue, double rightValue) { + return leftValue + rightValue; + } + + /** + * A "fast" string concatenation specialization. + */ + @Specialization + protected TruffleString concatenateTruffleStrings(TruffleString leftValue, TruffleString rightValue, + @Cached TruffleString.ConcatNode concatNode) { + return EasyScriptTruffleStrings.concat(leftValue, rightValue, concatNode); + } + + /** + * The way addition works in JavaScript is that it turns into string concatenation + * if either argument to it is a complex value. + * Complex values are functions, arrays, strings, and objects (like Math). + * Numbers and booleans are not complex values, + * and nor is 'undefined' (and 'null', but EasyScript doesn't support that one yet). + */ + @Specialization(guards = "isComplex(leftValue) || isComplex(rightValue)") + protected TruffleString concatenateComplexAsStrings(Object leftValue, Object rightValue, + @Cached TruffleString.FromJavaStringNode fromJavaStringNode) { + return EasyScriptTruffleStrings.fromJavaString( + EasyScriptTruffleStrings.concatToStrings(leftValue, rightValue), + fromJavaStringNode); + } + + protected static boolean isComplex(Object value) { + return !isPrimitive(value); + } + + private static boolean isPrimitive(Object value) { + return EasyScriptTypeSystemGen.isImplicitDouble(value) || + EasyScriptTypeSystemGen.isBoolean(value) || + value == Undefined.INSTANCE; + } + + /** + * If we get to this specialization, that means neither argument is complex, + * but they are also not both numbers - meaning, + * at least one of them is a boolean or {@code undefined}. + * In this case, always return NaN. + */ + @Fallback + protected double addNonNumber( + @SuppressWarnings("unused") Object leftValue, + @SuppressWarnings("unused") Object rightValue) { + return Double.NaN; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/NegationExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/NegationExprNode.java new file mode 100644 index 00000000..edf3cb01 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/NegationExprNode.java @@ -0,0 +1,34 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.arithmetic; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; + +/** + * The Node that represents the negation expression in JavaScript + * ("unary minus"), like {@code -3}. + * Identical to the class with the same name from part 11. + */ +@NodeChild("expr") +public abstract class NegationExprNode extends EasyScriptExprNode { + @Specialization(rewriteOn = ArithmeticException.class) + protected int negateInt(int value) { + // Integer.MIN_VALUE is too big to fit negated into an int + return Math.negateExact(value); + } + + @Specialization(replaces = "negateInt") + protected double negateDouble(double value) { + return -value; + } + + /** + * Same as with {@link AdditionExprNode}, + * we return NaN when attempting to negate something that's not a number. + */ + @Fallback + protected double negateNonNumber(@SuppressWarnings("unused") Object value) { + return Double.NaN; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/SubtractionExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/SubtractionExprNode.java new file mode 100644 index 00000000..4bbab592 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arithmetic/SubtractionExprNode.java @@ -0,0 +1,27 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.arithmetic; + +import com.endoflineblog.truffle.part_13.nodes.exprs.BinaryOperationExprNode; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; + +/** + * The Node representing number subtraction. + * Identical to the class with the same name from part 11. + */ +public abstract class SubtractionExprNode extends BinaryOperationExprNode { + @Specialization(rewriteOn = ArithmeticException.class) + protected int subtractInts(int leftValue, int rightValue) { + return Math.subtractExact(leftValue, rightValue); + } + + @Specialization(replaces = "subtractInts") + protected double subtractDoubles(double leftValue, double rightValue) { + return leftValue - rightValue; + } + + /** Non-numbers cannot be subtracted, and always result in NaN. */ + @Fallback + protected double subtractNonNumber(Object leftValue, Object rightValue) { + return Double.NaN; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayIndexReadExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayIndexReadExprNode.java new file mode 100644 index 00000000..48b3641b --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayIndexReadExprNode.java @@ -0,0 +1,103 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.arrays; + +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.strings.ReadTruffleStringPropertyNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.dsl.Cached; +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.interop.InteropLibrary; +import com.oracle.truffle.api.interop.InvalidArrayIndexException; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.strings.TruffleString; + +/** + * The Node representing reading array indexes + * (like {@code a[1]}). + * Identical to the class with the same name from part 11. + */ +@NodeChild("arrayExpr") +@NodeChild("indexExpr") +public abstract class ArrayIndexReadExprNode extends EasyScriptExprNode { + /** + * A specialization for reading a string property of a string, + * in code like {@code "a"['length']}. + * We delegate to {@link ReadTruffleStringPropertyNode}, + * but we first convert the index to a Java string, + * which is what {@link ReadTruffleStringPropertyNode} expects. + */ + @Specialization(limit = "1", guards = "indexInteropLibrary.isString(index)") + protected Object readStringPropertyOfString(TruffleString target, + Object index, + @CachedLibrary("index") InteropLibrary indexInteropLibrary, + @Cached ReadTruffleStringPropertyNode readStringPropertyNode) { + try { + return readStringPropertyNode.executeReadTruffleStringProperty(target, + indexInteropLibrary.asString(index)); + } catch (UnsupportedMessageException e) { + throw new EasyScriptException(this, e.getMessage()); + } + } + + /** + * A specialization for reading a non-string property of a string. + * The main usecase for this is string indexing, + * in code like {@code "a"[1]}. + * We delegate the implementation to {@link ReadTruffleStringPropertyNode}. + */ + @Specialization + protected Object readPropertyOfString(TruffleString target, Object index, + @Cached ReadTruffleStringPropertyNode readStringPropertyNode) { + return readStringPropertyNode.executeReadTruffleStringProperty(target, + index); + } + + @Specialization(guards = "arrayInteropLibrary.isArrayElementReadable(array, index)", limit = "1") + protected Object readIntIndex(Object array, int index, + @CachedLibrary("array") InteropLibrary arrayInteropLibrary) { + try { + return arrayInteropLibrary.readArrayElement(array, index); + } catch (UnsupportedMessageException | InvalidArrayIndexException e) { + throw new EasyScriptException(this, e.getMessage()); + } + } + + /** + * A specialization for reading a string property of a non-string target, + * in code like {@code [1, 2]['length']}. + * The implementation is identical to {@code PropertyReadExprNode}. + */ + @Specialization(guards = { + "targetInteropLibrary.hasMembers(target)", + "propertyNameInteropLibrary.isString(propertyName)" + }, limit = "1") + protected Object readProperty(Object target, Object propertyName, + @CachedLibrary("target") InteropLibrary targetInteropLibrary, + @CachedLibrary("propertyName") InteropLibrary propertyNameInteropLibrary) { + try { + return targetInteropLibrary.readMember(target, + propertyNameInteropLibrary.asString(propertyName)); + } catch (UnknownIdentifierException e) { + return Undefined.INSTANCE; + } catch (UnsupportedMessageException e) { + throw new EasyScriptException(this, e.getMessage()); + } + } + + @Specialization(guards = "interopLibrary.isNull(target)", limit = "1") + protected Object indexUndefined(@SuppressWarnings("unused") Object target, + Object index, + @SuppressWarnings("unused") @CachedLibrary("target") InteropLibrary interopLibrary) { + throw new EasyScriptException("Cannot read properties of undefined (reading '" + index + "')"); + } + + @Fallback + protected Object readNonArrayOrNonIntIndex(@SuppressWarnings("unused") Object array, + @SuppressWarnings("unused") Object index) { + return Undefined.INSTANCE; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayIndexWriteExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayIndexWriteExprNode.java new file mode 100644 index 00000000..1fc168a7 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayIndexWriteExprNode.java @@ -0,0 +1,46 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.arrays; + +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +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.interop.InteropLibrary; +import com.oracle.truffle.api.interop.InvalidArrayIndexException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.interop.UnsupportedTypeException; +import com.oracle.truffle.api.library.CachedLibrary; + +/** + * The Node representing writing array indexes + * (like {@code a[1] = 3}). + * Identical to the class with the same name from part 11. + */ +@NodeChild("arrayExpr") +@NodeChild("indexExpr") +@NodeChild("rvalueExpr") +public abstract class ArrayIndexWriteExprNode extends EasyScriptExprNode { + @Specialization(guards = "arrayInteropLibrary.isArrayElementWritable(array, index)", limit = "1") + protected Object writeIntIndex(Object array, int index, Object rvalue, + @CachedLibrary("array") InteropLibrary arrayInteropLibrary) { + try { + arrayInteropLibrary.writeArrayElement(array, index, rvalue); + } catch (UnsupportedMessageException | InvalidArrayIndexException | UnsupportedTypeException e) { + throw new EasyScriptException(this, e.getMessage()); + } + return rvalue; + } + + @Specialization(guards = "interopLibrary.isNull(target)", limit = "1") + protected Object indexUndefined(@SuppressWarnings("unused") Object target, + Object index, @SuppressWarnings("unused") Object rvalue, + @SuppressWarnings("unused") @CachedLibrary("target") InteropLibrary interopLibrary) { + throw new EasyScriptException("Cannot set properties of undefined (setting '" + index + "')"); + } + + @Fallback + protected Object writeNonArrayOrNonIntIndex(@SuppressWarnings("unused") Object array, + @SuppressWarnings("unused") Object index, Object rvalue) { + return rvalue; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayLiteralExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayLiteralExprNode.java new file mode 100644 index 00000000..15d13d3c --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/arrays/ArrayLiteralExprNode.java @@ -0,0 +1,35 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.arrays; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.runtime.ArrayObject; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.object.Shape; + +import java.util.List; + +/** + * The Node representing array literal expressions + * (like {@code [1, 2, 3]}). + * Identical to the class with the same name from part 11. + */ +public final class ArrayLiteralExprNode extends EasyScriptExprNode { + private final Shape arrayShape; + @Children + private final EasyScriptExprNode[] arrayElementExprs; + + public ArrayLiteralExprNode(Shape arrayShape, List arrayElementExprs) { + this.arrayShape = arrayShape; + this.arrayElementExprs = arrayElementExprs.toArray(new EasyScriptExprNode[]{}); + } + + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + Object[] arrayElements = new Object[this.arrayElementExprs.length]; + for (var i = 0; i < this.arrayElementExprs.length; i++) { + arrayElements[i] = this.arrayElementExprs[i].executeGeneric(frame); + } + return new ArrayObject(this.arrayShape, arrayElements); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/EqualityExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/EqualityExprNode.java new file mode 100644 index 00000000..c432168e --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/EqualityExprNode.java @@ -0,0 +1,40 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.comparisons; + +import com.endoflineblog.truffle.part_13.nodes.exprs.BinaryOperationExprNode; +import com.endoflineblog.truffle.part_13.runtime.EasyScriptTruffleStrings; +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.strings.TruffleString; + +/** + * Node class representing the strict equality ({@code ===}) operator. + * Identical to the class with the same name from part 11. + */ +public abstract class EqualityExprNode extends BinaryOperationExprNode { + @Specialization + protected boolean intEquality(int leftValue, int rightValue) { + return leftValue == rightValue; + } + + @Specialization(replaces = "intEquality") + protected boolean doubleEquality(double leftValue, double rightValue) { + return leftValue == rightValue; + } + + @Specialization + protected boolean boolEquality(boolean leftValue, boolean rightValue) { + return leftValue == rightValue; + } + + @Specialization + protected boolean stringEquality(TruffleString leftValue, TruffleString rightValue, + @Cached TruffleString.EqualNode equalNode) { + return EasyScriptTruffleStrings.equals(leftValue, rightValue, equalNode); + } + + @Fallback + protected boolean objectEquality(Object leftValue, Object rightValue) { + return leftValue == rightValue; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/GreaterExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/GreaterExprNode.java new file mode 100644 index 00000000..e5c74cae --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/GreaterExprNode.java @@ -0,0 +1,34 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.comparisons; + +import com.endoflineblog.truffle.part_13.nodes.exprs.BinaryOperationExprNode; +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.strings.TruffleString; + +/** + * Node class representing the greater ({@code >}) operator. + * Identical to the class with the same name from part 11. + */ +public abstract class GreaterExprNode extends BinaryOperationExprNode { + @Specialization + protected boolean intGreater(int leftValue, int rightValue) { + return leftValue > rightValue; + } + + @Specialization(replaces = "intGreater") + protected boolean doubleGreater(double leftValue, double rightValue) { + return leftValue > rightValue; + } + + @Specialization + protected boolean stringGreater(TruffleString leftValue, TruffleString rightValue, + @Cached TruffleString.CompareCharsUTF16Node compareNode) { + return compareNode.execute(leftValue, rightValue) > 0; + } + + @Fallback + protected boolean objectGreater(Object leftValue, Object rightValue) { + return false; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/GreaterOrEqualExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/GreaterOrEqualExprNode.java new file mode 100644 index 00000000..7069f2aa --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/GreaterOrEqualExprNode.java @@ -0,0 +1,34 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.comparisons; + +import com.endoflineblog.truffle.part_13.nodes.exprs.BinaryOperationExprNode; +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.strings.TruffleString; + +/** + * Node class representing the greater or equal ({@code >=}) operator. + * Identical to the class with the same name from part 11. + */ +public abstract class GreaterOrEqualExprNode extends BinaryOperationExprNode { + @Specialization + protected boolean intGreaterOrEqual(int leftValue, int rightValue) { + return leftValue >= rightValue; + } + + @Specialization(replaces = "intGreaterOrEqual") + protected boolean doubleGreaterOrEqual(double leftValue, double rightValue) { + return leftValue >= rightValue; + } + + @Specialization + protected boolean stringGreaterOrEqual(TruffleString leftValue, TruffleString rightValue, + @Cached TruffleString.CompareCharsUTF16Node compareNode) { + return compareNode.execute(leftValue, rightValue) >= 0; + } + + @Fallback + protected boolean objectGreaterOrEqual(Object leftValue, Object rightValue) { + return false; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/InequalityExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/InequalityExprNode.java new file mode 100644 index 00000000..fc4a7bd9 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/InequalityExprNode.java @@ -0,0 +1,40 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.comparisons; + +import com.endoflineblog.truffle.part_13.nodes.exprs.BinaryOperationExprNode; +import com.endoflineblog.truffle.part_13.runtime.EasyScriptTruffleStrings; +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.strings.TruffleString; + +/** + * Node class representing the strict inequality ({@code !==}) operator. + * Identical to the class with the same name from part 11. + */ +public abstract class InequalityExprNode extends BinaryOperationExprNode { + @Specialization + protected boolean intInequality(int leftValue, int rightValue) { + return leftValue != rightValue; + } + + @Specialization(replaces = "intInequality") + protected boolean doubleInequality(double leftValue, double rightValue) { + return leftValue != rightValue; + } + + @Specialization + protected boolean boolInequality(boolean leftValue, boolean rightValue) { + return leftValue != rightValue; + } + + @Specialization + protected boolean stringInequality(TruffleString leftValue, TruffleString rightValue, + @Cached TruffleString.EqualNode equalNode) { + return !EasyScriptTruffleStrings.equals(leftValue, rightValue, equalNode); + } + + @Fallback + protected boolean objectInequality(Object leftValue, Object rightValue) { + return leftValue != rightValue; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/LesserExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/LesserExprNode.java new file mode 100644 index 00000000..45879482 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/LesserExprNode.java @@ -0,0 +1,34 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.comparisons; + +import com.endoflineblog.truffle.part_13.nodes.exprs.BinaryOperationExprNode; +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.strings.TruffleString; + +/** + * Node class representing the lesser ({@code <}) operator. + * Identical to the class with the same name from part 11. + */ +public abstract class LesserExprNode extends BinaryOperationExprNode { + @Specialization + protected boolean intLesser(int leftValue, int rightValue) { + return leftValue < rightValue; + } + + @Specialization(replaces = "intLesser") + protected boolean doubleLesser(double leftValue, double rightValue) { + return leftValue < rightValue; + } + + @Specialization + protected boolean stringLesser(TruffleString leftValue, TruffleString rightValue, + @Cached TruffleString.CompareCharsUTF16Node compareNode) { + return compareNode.execute(leftValue, rightValue) < 0; + } + + @Fallback + protected boolean objectLesser(Object leftValue, Object rightValue) { + return false; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/LesserOrEqualExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/LesserOrEqualExprNode.java new file mode 100644 index 00000000..ef431aab --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/comparisons/LesserOrEqualExprNode.java @@ -0,0 +1,34 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.comparisons; + +import com.endoflineblog.truffle.part_13.nodes.exprs.BinaryOperationExprNode; +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.strings.TruffleString; + +/** + * Node class representing the lesser or equal ({@code <=}) operator. + * Identical to the class with the same name from part 11. + */ +public abstract class LesserOrEqualExprNode extends BinaryOperationExprNode { + @Specialization + protected boolean intLesserOrEqual(int leftValue, int rightValue) { + return leftValue <= rightValue; + } + + @Specialization(replaces = "intLesserOrEqual") + protected boolean doubleLesserOrEqual(double leftValue, double rightValue) { + return leftValue <= rightValue; + } + + @Specialization + protected boolean stringLesserOrEqual(TruffleString leftValue, TruffleString rightValue, + @Cached TruffleString.CompareCharsUTF16Node compareNode) { + return compareNode.execute(leftValue, rightValue) <= 0; + } + + @Fallback + protected boolean objectLesserOrEqual(Object leftValue, Object rightValue) { + return false; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/FunctionCallExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/FunctionCallExprNode.java new file mode 100644 index 00000000..990fed84 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/FunctionCallExprNode.java @@ -0,0 +1,44 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.functions; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; + +import java.util.List; + +/** + * The Node representing the expression of calling a function in EasyScript, + * for example {@code Math.pow(2, 3)}. + * Identical to the class with the same name from part 11. + */ +public final class FunctionCallExprNode extends EasyScriptExprNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptExprNode targetFunction; + + @Children + private final EasyScriptExprNode[] callArguments; + + @SuppressWarnings("FieldMayBeFinal") + @Child + private FunctionDispatchNode dispatchNode; + + public FunctionCallExprNode(EasyScriptExprNode targetFunction, List callArguments) { + this.targetFunction = targetFunction; + this.callArguments = callArguments.toArray(new EasyScriptExprNode[]{}); + this.dispatchNode = FunctionDispatchNodeGen.create(); + } + + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + Object function = this.targetFunction.executeGeneric(frame); + + Object[] argumentValues = new Object[this.callArguments.length]; + for (int i = 0; i < this.callArguments.length; i++) { + argumentValues[i] = this.callArguments[i].executeGeneric(frame); + } + + return this.dispatchNode.executeDispatch(function, argumentValues); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/FunctionDispatchNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/FunctionDispatchNode.java new file mode 100644 index 00000000..1da5a440 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/FunctionDispatchNode.java @@ -0,0 +1,92 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.functions; + +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.runtime.FunctionObject; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +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.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.nodes.Node; + +/** + * A helper Node that contains specialization for functions calls. + * Identical to the class with the same name from part 11. + */ +public abstract class FunctionDispatchNode extends Node { + public abstract Object executeDispatch(Object function, Object[] arguments); + + /** + * A specialization that calls the given target directly. + * This is the fastest case, + * used when the target of a call stays the same during the execution of the program, + * like in {@code Math.abs(-3)}. + */ + @Specialization(guards = "function.callTarget == directCallNode.getCallTarget()", limit = "2") + protected static Object dispatchDirectly( + FunctionObject function, + Object[] arguments, + @Cached("create(function.callTarget)") DirectCallNode directCallNode) { + return directCallNode.call(extendArguments(arguments, function)); + } + + /** + * A specialization that calls the given target indirectly. + * You might be surprised that we need this specialization at all - + * won't the CallTarget of a given function never change? + * But consider the following code: {@code (cond ? f1 : f2)(34)}. + * Suddenly, based on the value of {@code cond}, + * the same expression can evaluate to different functions, + * and that's why we need this specialization. + */ + @Specialization(replaces = "dispatchDirectly") + protected static Object dispatchIndirectly( + FunctionObject function, + Object[] arguments, + @Cached IndirectCallNode indirectCallNode) { + return indirectCallNode.call(function.callTarget, extendArguments(arguments, function)); + } + + /** + * A fallback used in case the expression that's the target of the call + * doesn't evaluate to a function. + */ + @Fallback + protected static Object targetIsNotAFunction( + Object nonFunction, + @SuppressWarnings("unused") Object[] arguments) { + throw new EasyScriptException("'" + nonFunction + "' is not a function"); + } + + private static Object[] extendArguments(Object[] arguments, FunctionObject function) { + // if the function object doesn't have a target + // (meaning, it's a function, not method, call), + // and we were called with at least as many arguments as the function takes, + // there's nothing to do + if (arguments.length >= function.argumentCount && function.methodTarget == null) { + return arguments; + } + + // otherwise, create a new arguments array, + // saving the method target as the first element if it's non-null, + // and filling out the remaining arguments with 'undefined's + // if we got called with less of them than the function expects + Object[] ret = new Object[function.argumentCount]; + for (int i = 0; i < function.argumentCount; i++) { + int j; + if (function.methodTarget == null) { + j = i; + } else { + if (i == 0) { + ret[0] = function.methodTarget; + continue; + } else { + j = i - 1; + } + } + ret[i] = j < arguments.length ? arguments[j] : Undefined.INSTANCE; + } + return ret; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/ReadFunctionArgExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/ReadFunctionArgExprNode.java new file mode 100644 index 00000000..3cb7d360 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/ReadFunctionArgExprNode.java @@ -0,0 +1,24 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.functions; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * An expression Node that represents referencing a given argument of a function - + * either built-in, or user-defined. + * Identical to the class with the same name from part 11. + */ +public final class ReadFunctionArgExprNode extends EasyScriptExprNode { + private final int index; + + public ReadFunctionArgExprNode(int index) { + this.index = index; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + // we are guaranteed the argument array has enough elements, + // because of the logic in FunctionDispatchNode + return frame.getArguments()[this.index]; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/WriteFunctionArgExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/WriteFunctionArgExprNode.java new file mode 100644 index 00000000..1d20f714 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/WriteFunctionArgExprNode.java @@ -0,0 +1,30 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.functions; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * An expression Node that represents an assignment to a function argument. + * Identical to the class with the same name from part 11. + */ +public final class WriteFunctionArgExprNode extends EasyScriptExprNode { + private final int index; + + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptExprNode initializerExpr; + + public WriteFunctionArgExprNode(EasyScriptExprNode initializerExpr, int index) { + this.index = index; + this.initializerExpr = initializerExpr; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + Object value = this.initializerExpr.executeGeneric(frame); + // we are guaranteed the argument array has enough elements, + // because of the logic in FunctionDispatchNode + frame.getArguments()[this.index] = value; + return value; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/AbsFunctionBodyExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/AbsFunctionBodyExprNode.java new file mode 100644 index 00000000..9012105c --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/AbsFunctionBodyExprNode.java @@ -0,0 +1,32 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in; + +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; + +/** + * An expression Node that represents the implementation of the + * {@code Math.abs()} JavaScript function. + * Identical to the class with the same name from part 11. + */ +public abstract class AbsFunctionBodyExprNode extends BuiltInFunctionBodyExprNode { + @Specialization(rewriteOn = ArithmeticException.class) + protected int intAbs(int argument) { + // Integer.MIN_VALUE is too big to fit negated into an int + return argument < 0 ? Math.negateExact(argument) : argument; + } + + @Specialization(replaces = "intAbs") + protected double doubleAbs(double argument) { + return Math.abs(argument); + } + + /** + * It's always possible to get called with 'undefined' passed as an argument, + * or even another function. + * Simply return NaN in all those cases. + */ + @Fallback + protected double nonNumberAbs(@SuppressWarnings("unused") Object argument) { + return Double.NaN; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/BuiltInFunctionBodyExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/BuiltInFunctionBodyExprNode.java new file mode 100644 index 00000000..85f008a2 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/BuiltInFunctionBodyExprNode.java @@ -0,0 +1,16 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.ReadFunctionArgExprNode; +import com.oracle.truffle.api.dsl.GenerateNodeFactory; +import com.oracle.truffle.api.dsl.NodeChild; + +/** + * The common ancestor for Nodes that represent the implementations of + * built-in JavaScript functions and methods. + * Identical to the class with the same name from part 11. + */ +@NodeChild(value = "arguments", type = ReadFunctionArgExprNode[].class) +@GenerateNodeFactory +public abstract class BuiltInFunctionBodyExprNode extends EasyScriptExprNode { +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/PowFunctionBodyExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/PowFunctionBodyExprNode.java new file mode 100644 index 00000000..7155bdd8 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/PowFunctionBodyExprNode.java @@ -0,0 +1,35 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in; + +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; + +/** + * An expression Node that represents the implementation of the + * {@code Math.pow()} JavaScript function. + * Identical to the class with the same name from part 11. + */ +public abstract class PowFunctionBodyExprNode extends BuiltInFunctionBodyExprNode { + @Specialization(guards = "exponent >= 0", rewriteOn = ArithmeticException.class) + protected int intPow(int base, int exponent) { + int ret = 1; + for (int i = 0; i < exponent; i++) { + ret = Math.multiplyExact(ret, base); + } + return ret; + } + + @Specialization(replaces = "intPow") + protected double doublePow(double base, double exponent) { + return Math.pow(base, exponent); + } + + /** + * It's always possible to get called with 'undefined' passed as an argument, + * or even another function. + * Simply return NaN in all those cases. + */ + @Fallback + protected double nonNumberPow(@SuppressWarnings("unused") Object base, @SuppressWarnings("unused") Object exponent) { + return Double.NaN; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/methods/CharAtMethodBodyExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/methods/CharAtMethodBodyExprNode.java new file mode 100644 index 00000000..d1b32471 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/functions/built_in/methods/CharAtMethodBodyExprNode.java @@ -0,0 +1,48 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in.methods; + +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in.BuiltInFunctionBodyExprNode; +import com.endoflineblog.truffle.part_13.runtime.EasyScriptTruffleStrings; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Cached.Shared; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.strings.TruffleString; + +/** + * An expression Node that represents the implementation of the built-in + * {@code charAt()} method of strings. + * Identical to the class with the same name from part 11. + */ +public abstract class CharAtMethodBodyExprNode extends BuiltInFunctionBodyExprNode { + /** + * The specialization for when {@code charAt()} + * is called with an integer argument. + * In that case, we return a one-element substring of the target of the method call - + * unless the index is out of bounds, in which case we return the empty string. + */ + @Specialization + protected TruffleString charAtInt(TruffleString self, int index, + @Cached @Shared("lengthNode") TruffleString.CodePointLengthNode lengthNode, + @Cached @Shared("substringNode") TruffleString.SubstringNode substringNode) { + return index < 0 || index >= EasyScriptTruffleStrings.length(self, lengthNode) + ? EasyScriptTruffleStrings.EMPTY + : EasyScriptTruffleStrings.substring(self, index, 1, substringNode); + } + + /** + * The specialization for when {@code charAt()} + * is called either without an argument, + * or with one that is not an integer. + * In that case, behave as if it was called with {@code 0}. + */ + @Fallback + protected TruffleString charAtNonInt(Object self, + @SuppressWarnings("unused") Object nonIntIndex, + @Cached @Shared("lengthNode") TruffleString.CodePointLengthNode lengthNode, + @Cached @Shared("substringNode") TruffleString.SubstringNode substringNode) { + // we know that 'self' is for sure a TruffleString + // because of how reading string properties works in ReadTruffleStringPropertyNode, + // but we need to declare it as Object here because of @Fallback + return this.charAtInt((TruffleString) self, 0, lengthNode, substringNode); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/BoolLiteralExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/BoolLiteralExprNode.java new file mode 100644 index 00000000..60108feb --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/BoolLiteralExprNode.java @@ -0,0 +1,37 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.literals; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.UnexpectedResultException; + +/** + * The AST node that represents a boolean literal expression in EasyScript. + * Identical to the class with the same name from part 11. + */ +public final class BoolLiteralExprNode extends EasyScriptExprNode { + private final boolean value; + + public BoolLiteralExprNode(boolean value) { + this.value = value; + } + + @Override + public boolean executeBool(VirtualFrame frame) { + return this.value; + } + + @Override + public int executeInt(VirtualFrame frame) throws UnexpectedResultException { + throw new UnexpectedResultException(this.value); + } + + @Override + public double executeDouble(VirtualFrame frame) throws UnexpectedResultException { + throw new UnexpectedResultException(this.value); + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return this.value; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/DoubleLiteralExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/DoubleLiteralExprNode.java new file mode 100644 index 00000000..487de984 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/DoubleLiteralExprNode.java @@ -0,0 +1,37 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.literals; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.UnexpectedResultException; + +/** + * The AST node that represents a decimal number literal expression in EasyScript. + * Identical to the class with the same name from part 11. + */ +public final class DoubleLiteralExprNode extends EasyScriptExprNode { + private final double value; + + public DoubleLiteralExprNode(double value) { + this.value = value; + } + + @Override + public boolean executeBool(VirtualFrame frame) { + return this.value != 0.0; + } + + @Override + public double executeDouble(VirtualFrame frame) { + return this.value; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return this.executeDouble(frame); + } + + @Override + public int executeInt(VirtualFrame frame) throws UnexpectedResultException { + throw new UnexpectedResultException(this.value); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/IntLiteralExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/IntLiteralExprNode.java new file mode 100644 index 00000000..9c1f3950 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/IntLiteralExprNode.java @@ -0,0 +1,36 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.literals; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * The AST node that represents an integer literal expression in EasyScript. + * Identical to the class with the same name from part 11. + */ +public final class IntLiteralExprNode extends EasyScriptExprNode { + private final int value; + + public IntLiteralExprNode(int value) { + this.value = value; + } + + @Override + public boolean executeBool(VirtualFrame frame) { + return this.value != 0; + } + + @Override + public int executeInt(VirtualFrame frame) { + return this.value; + } + + @Override + public double executeDouble(VirtualFrame frame) { + return this.value; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return this.value; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/StringLiteralExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/StringLiteralExprNode.java new file mode 100644 index 00000000..32bf3b0f --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/StringLiteralExprNode.java @@ -0,0 +1,29 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.literals; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.runtime.EasyScriptTruffleStrings; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.strings.TruffleString; + +/** + * The AST node that represents a string literal expression in EasyScript. + * Identical to the class with the same name from part 11. + */ +public final class StringLiteralExprNode extends EasyScriptExprNode { + private final TruffleString value; + + public StringLiteralExprNode(String value) { + this.value = EasyScriptTruffleStrings.fromJavaString(value); + } + + /** Empty strings are considered "falsy" in JavaScript. */ + @Override + public boolean executeBool(VirtualFrame frame) { + return !this.value.isEmpty(); + } + + @Override + public TruffleString executeGeneric(VirtualFrame frame) { + return this.value; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/UndefinedLiteralExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/UndefinedLiteralExprNode.java new file mode 100644 index 00000000..3b573f98 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/literals/UndefinedLiteralExprNode.java @@ -0,0 +1,33 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.literals; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.UnexpectedResultException; + +/** + * The AST node that represents the 'undefined' literal JavaScript expression. + * Identical to the class with the same name from part 11. + */ +public final class UndefinedLiteralExprNode extends EasyScriptExprNode { + @Override + public boolean executeBool(VirtualFrame frame) { + // undefined is falsy + return false; + } + + @Override + public int executeInt(VirtualFrame frame) throws UnexpectedResultException { + throw new UnexpectedResultException(Undefined.INSTANCE); + } + + @Override + public double executeDouble(VirtualFrame frame) throws UnexpectedResultException { + throw new UnexpectedResultException(Undefined.INSTANCE); + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return Undefined.INSTANCE; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/objects/ClassDeclExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/objects/ClassDeclExprNode.java new file mode 100644 index 00000000..1208bc87 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/objects/ClassDeclExprNode.java @@ -0,0 +1,37 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.objects; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.variables.FuncDeclStmtNode; +import com.endoflineblog.truffle.part_13.runtime.ClassPrototypeObject; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; + +import java.util.List; + +/** + * The Node for handling a class declaration. + * It simply handles methods inside the class by delegating to + * {@link FuncDeclStmtNode}. + */ +public final class ClassDeclExprNode extends EasyScriptExprNode { + @Children + private final FuncDeclStmtNode[] classMethodDecls; + + private final ClassPrototypeObject classPrototypeObject; + + public ClassDeclExprNode(List classMethodDecls, + ClassPrototypeObject classPrototypeObject) { + this.classMethodDecls = classMethodDecls.toArray(FuncDeclStmtNode[]::new); + this.classPrototypeObject = classPrototypeObject; + } + + @Override + @ExplodeLoop + public ClassPrototypeObject executeGeneric(VirtualFrame frame) { + for (var classMethodDecl : this.classMethodDecls) { + classMethodDecl.executeStatement(frame); + } + + return this.classPrototypeObject; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/objects/NewExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/objects/NewExprNode.java new file mode 100644 index 00000000..68895de1 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/objects/NewExprNode.java @@ -0,0 +1,62 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.objects; + +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.runtime.ClassInstanceObject; +import com.endoflineblog.truffle.part_13.runtime.ClassPrototypeObject; +import com.oracle.truffle.api.dsl.Executed; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; + +import java.util.List; + +/** + * The Node for handling {@code new} expressions. + */ +public abstract class NewExprNode extends EasyScriptExprNode { + @Child + @Executed + protected EasyScriptExprNode constructorExpr; + + @Children + protected final EasyScriptExprNode[] args; + + protected NewExprNode(EasyScriptExprNode constructorExpr, List args) { + this.constructorExpr = constructorExpr; + this.args = args.toArray(EasyScriptExprNode[]::new); + } + + /** + * The specialization for when the constructor expression evaluates to a + * {@link ClassPrototypeObject}. + */ + @Specialization + protected Object instantiateObject(VirtualFrame frame, ClassPrototypeObject classPrototypeObject) { + this.consumeArguments(frame); + return new ClassInstanceObject(classPrototypeObject); + } + + /** + * The specialization for when the constructor expression evaluates to something other than + * {@link ClassPrototypeObject}. + */ + @Fallback + protected Object instantiateNonConstructor(VirtualFrame frame, Object object) { + this.consumeArguments(frame); + throw new EasyScriptException("'" + object + "' is not a constructor"); + } + + /** + * Even though we don't use the arguments for anything in this part of the series, + * we need to evaluate them, as they can have side effects + * (like assignment). + */ + @ExplodeLoop + private void consumeArguments(VirtualFrame frame) { + for (int i = 0; i < this.args.length; i++) { + this.args[i].executeGeneric(frame); + } + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/properties/PropertyReadExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/properties/PropertyReadExprNode.java new file mode 100644 index 00000000..08613157 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/properties/PropertyReadExprNode.java @@ -0,0 +1,70 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.properties; + +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.strings.ReadTruffleStringPropertyNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.NodeField; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.strings.TruffleString; + +/** + * The Node for reading properties of objects. + * Used in code like {@code t.myProp}. + * Identical to the class with the same name from part 11. + */ +@NodeChild("target") +@NodeField(name = "propertyName", type = String.class) +public abstract class PropertyReadExprNode extends EasyScriptExprNode { + protected abstract String getPropertyName(); + + /** + * The specialization for reading a property of a {@link TruffleString}. + * Simply delegates to {@link ReadTruffleStringPropertyNode}. + */ + @Specialization + protected Object readPropertyOfString(TruffleString target, + @Cached ReadTruffleStringPropertyNode readStringPropertyNode) { + return readStringPropertyNode.executeReadTruffleStringProperty( + target, this.getPropertyName()); + } + + @Specialization(guards = "interopLibrary.hasMembers(target)", limit = "1") + protected Object readProperty(Object target, + @CachedLibrary("target") InteropLibrary interopLibrary) { + try { + return interopLibrary.readMember(target, this.getPropertyName()); + } catch (UnknownIdentifierException e) { + return Undefined.INSTANCE; + } catch (UnsupportedMessageException e) { + throw new EasyScriptException(this, e.getMessage()); + } + } + + /** + * Reading any property of {@code undefined} + * results in an error in JavaScript. + */ + @Specialization(guards = "interopLibrary.isNull(target)", limit = "1") + protected Object readPropertyOfUndefined(@SuppressWarnings("unused") Object target, + @CachedLibrary("target") @SuppressWarnings("unused") InteropLibrary interopLibrary) { + throw new EasyScriptException("Cannot read properties of undefined (reading '" + this.getPropertyName() + "')"); + } + + /** + * Accessing a property of anything that is not {@code undefined} + * but doesn't have any members returns simply {@code undefined} + * in JavaScript. + */ + @Fallback + protected Object readPropertyOfNonUndefinedWithoutMembers(@SuppressWarnings("unused") Object target) { + return Undefined.INSTANCE; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/strings/ReadTruffleStringPropertyNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/strings/ReadTruffleStringPropertyNode.java new file mode 100644 index 00000000..6ddcab06 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/strings/ReadTruffleStringPropertyNode.java @@ -0,0 +1,99 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.strings; + +import com.endoflineblog.truffle.part_13.nodes.EasyScriptNode; +import com.endoflineblog.truffle.part_13.runtime.EasyScriptTruffleStrings; +import com.endoflineblog.truffle.part_13.runtime.FunctionObject; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.strings.TruffleString; + +/** + * An AST node that represents reading properties of strings. + * Identical to the class with the same name from part 11. + */ +@ImportStatic(EasyScriptTruffleStrings.class) +public abstract class ReadTruffleStringPropertyNode extends EasyScriptNode { + protected static final String LENGTH_PROP = "length"; + protected static final String CHAR_AT_PROP = "charAt"; + + /** The abstract {@code execute*()} method for this node. */ + public abstract Object executeReadTruffleStringProperty(TruffleString truffleString, Object property); + + /** + * The specialization used when accessing an integer index of a string, + * in code like {@code "abc"[1]}. + */ + @Specialization + protected Object readStringIndex( + TruffleString truffleString, + int index, + @Cached TruffleString.CodePointLengthNode lengthNode, + @Cached TruffleString.SubstringNode substringNode) { + return index < 0 || index >= EasyScriptTruffleStrings.length(truffleString, lengthNode) + ? Undefined.INSTANCE + : EasyScriptTruffleStrings.substring(truffleString, index, 1, substringNode); + } + + /** + * The specialization used when accessing the {@code length} + * property of a string, in code like {@code "abc".length} + * or {@code "abc"['length']}. + */ + @Specialization(guards = "LENGTH_PROP.equals(propertyName)") + protected int readLengthProperty( + TruffleString truffleString, + @SuppressWarnings("unused") String propertyName, + @Cached TruffleString.CodePointLengthNode lengthNode) { + return EasyScriptTruffleStrings.length(truffleString, lengthNode); + } + + /** + * The specialization used when accessing the {@code charAt} + * property of a string, in code like {@code "abc".charAt} + * or {@code "abc"['charAt']}. + * This is the "fast" version of the specialization, + * which caches the created {@link FunctionObject}, + * instead of creating a new object each time the node is executed. + */ + @Specialization(guards = { + "CHAR_AT_PROP.equals(propertyName)", + "same(charAtMethod.methodTarget, truffleString)" + }) + protected FunctionObject readCharAtPropertyCached( + @SuppressWarnings("unused") TruffleString truffleString, + @SuppressWarnings("unused") String propertyName, + @Cached("createCharAtMethodObject(truffleString)") FunctionObject charAtMethod) { + return charAtMethod; + } + + /** + * The "slow" specialization version of reading the {@code charAt} + * property that we switch to when {@link #readCharAtPropertyCached} + * encounters more than 3 different string targets + * (3 is the default instantiation limit for specializations in Truffle). + * In that case, we no longer cache the {@link FunctionObject} + * representing a given method, + * but simply create a new object each time the node is executed. + */ + @Specialization(guards = "CHAR_AT_PROP.equals(propertyName)", replaces = "readCharAtPropertyCached") + protected FunctionObject readCharAtPropertyUncached( + TruffleString truffleString, + @SuppressWarnings("unused") String propertyName) { + return createCharAtMethodObject(truffleString); + } + + protected FunctionObject createCharAtMethodObject(TruffleString truffleString) { + return new FunctionObject(currentLanguageContext().stringPrototype.charAtMethod, 2, truffleString); + } + + /** Accessing any other string property should return 'undefined'. */ + @Fallback + protected Undefined readUnknownProperty( + @SuppressWarnings("unused") TruffleString truffleString, + @SuppressWarnings("unused") Object property) { + return Undefined.INSTANCE; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/GlobalVarAssignmentExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/GlobalVarAssignmentExprNode.java new file mode 100644 index 00000000..426c2bb6 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/GlobalVarAssignmentExprNode.java @@ -0,0 +1,38 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.variables; + +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.GlobalScopeObjectExprNode; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.NodeField; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.DynamicObjectLibrary; +import com.oracle.truffle.api.object.Property; + +/** + * A Node that represents the expression of assigning a value to a global variable in EasyScript. + * Identical to the class with the same name from part 11. + */ +@NodeChild(value = "globalScopeObjectExpr", type = GlobalScopeObjectExprNode.class) +@NodeChild(value = "assignmentExpr") +@NodeField(name = "name", type = String.class) +public abstract class GlobalVarAssignmentExprNode extends EasyScriptExprNode { + protected abstract String getName(); + + @Specialization(limit = "1") + protected Object assignVariable(DynamicObject globalScopeObject, Object value, + @CachedLibrary("globalScopeObject") DynamicObjectLibrary objectLibrary) { + String variableId = this.getName(); + Property property = objectLibrary.getProperty(globalScopeObject, variableId); + if (property == null) { + throw new EasyScriptException(this, "'" + variableId + "' is not defined"); + } + if (property.getFlags() == 1) { + throw new EasyScriptException("Assignment to constant variable '" + variableId + "'"); + } + objectLibrary.put(globalScopeObject, variableId, value); + return value; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/GlobalVarReferenceExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/GlobalVarReferenceExprNode.java new file mode 100644 index 00000000..e43b5d86 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/GlobalVarReferenceExprNode.java @@ -0,0 +1,33 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.variables; + +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.GlobalScopeObjectExprNode; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.NodeField; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.DynamicObjectLibrary; + +/** + * A Node that represents the expression of referencing a global variable in EasyScript. + * Identical to the class with the same name from part 11. + */ +@NodeChild(value = "globalScopeObjectExpr", type = GlobalScopeObjectExprNode.class) +@NodeField(name = "name", type = String.class) +public abstract class GlobalVarReferenceExprNode extends EasyScriptExprNode { + protected abstract String getName(); + + @Specialization(limit = "1") + protected Object readVariable(DynamicObject globalScopeObject, + @CachedLibrary("globalScopeObject") DynamicObjectLibrary objectLibrary) { + String variableId = this.getName(); + var value = objectLibrary.getOrDefault(globalScopeObject, variableId, null); + if (value == null) { + throw new EasyScriptException(this, "'" + variableId + "' is not defined"); + } else { + return value; + } + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/LocalVarAssignmentExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/LocalVarAssignmentExprNode.java new file mode 100644 index 00000000..e4abbb25 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/LocalVarAssignmentExprNode.java @@ -0,0 +1,56 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.variables; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.NodeField; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameSlotKind; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * A Node that represents the assignment to a variable local to a function. + * Identical to the class with the same name from part 11. + */ +@NodeChild("initializerExpr") +@NodeField(name = "frameSlot", type = int.class) +@ImportStatic(FrameSlotKind.class) +public abstract class LocalVarAssignmentExprNode extends EasyScriptExprNode { + protected abstract int getFrameSlot(); + + @Specialization(guards = "frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Illegal || " + + "frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Int") + protected int intAssignment(VirtualFrame frame, int value) { + int frameSlot = this.getFrameSlot(); + frame.getFrameDescriptor().setSlotKind(frameSlot, FrameSlotKind.Int); + frame.setInt(frameSlot, value); + return value; + } + + @Specialization(replaces = "intAssignment", + guards = "frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Illegal || " + + "frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Double") + protected double doubleAssignment(VirtualFrame frame, double value) { + int frameSlot = this.getFrameSlot(); + frame.getFrameDescriptor().setSlotKind(frameSlot, FrameSlotKind.Double); + frame.setDouble(frameSlot, value); + return value; + } + + @Specialization(guards = "frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Illegal || " + + "frame.getFrameDescriptor().getSlotKind(getFrameSlot()) == Boolean") + protected boolean boolAssignment(VirtualFrame frame, boolean value) { + int frameSlot = this.getFrameSlot(); + frame.getFrameDescriptor().setSlotKind(frameSlot, FrameSlotKind.Boolean); + frame.setBoolean(frameSlot, value); + return value; + } + + @Specialization(replaces = {"intAssignment", "doubleAssignment", "boolAssignment"}) + protected Object objectAssignment(VirtualFrame frame, Object value) { + int frameSlot = this.getFrameSlot(); + frame.getFrameDescriptor().setSlotKind(frameSlot, FrameSlotKind.Object); + frame.setObject(frameSlot, value); + return value; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/LocalVarReferenceExprNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/LocalVarReferenceExprNode.java new file mode 100644 index 00000000..f14c032c --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/exprs/variables/LocalVarReferenceExprNode.java @@ -0,0 +1,35 @@ +package com.endoflineblog.truffle.part_13.nodes.exprs.variables; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.oracle.truffle.api.dsl.NodeField; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * A Node that represents the reference to a variable local to a function. + * Identical to the class with the same name from part 11. + */ +@NodeField(name = "frameSlot", type = int.class) +public abstract class LocalVarReferenceExprNode extends EasyScriptExprNode { + protected abstract int getFrameSlot(); + + @Specialization(guards = "frame.isInt(getFrameSlot())") + protected int readInt(VirtualFrame frame) { + return frame.getInt(this.getFrameSlot()); + } + + @Specialization(guards = "frame.isDouble(getFrameSlot())", replaces = "readInt") + protected double readDouble(VirtualFrame frame) { + return frame.getDouble(this.getFrameSlot()); + } + + @Specialization(guards = "frame.isBoolean(getFrameSlot())") + protected boolean readBool(VirtualFrame frame) { + return frame.getBoolean(this.getFrameSlot()); + } + + @Specialization(replaces = {"readInt", "readDouble", "readBool"}) + protected Object readObject(VirtualFrame frame) { + return frame.getObject(this.getFrameSlot()); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/root/BuiltInFuncRootNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/root/BuiltInFuncRootNode.java new file mode 100644 index 00000000..7be7e068 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/root/BuiltInFuncRootNode.java @@ -0,0 +1,29 @@ +package com.endoflineblog.truffle.part_13.nodes.root; + +import com.endoflineblog.truffle.part_13.EasyScriptTruffleLanguage; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in.BuiltInFunctionBodyExprNode; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.RootNode; + +/** + * The {@link RootNode} for our built-in functions. + * Simply wraps the expression Node representing the body of the function. + * Identical to the class with the same name from part 11. + */ +public final class BuiltInFuncRootNode extends RootNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private BuiltInFunctionBodyExprNode functionBodyExpr; + + public BuiltInFuncRootNode(EasyScriptTruffleLanguage truffleLanguage, + BuiltInFunctionBodyExprNode functionBodyExpr) { + super(truffleLanguage); + + this.functionBodyExpr = functionBodyExpr; + } + + @Override + public Object execute(VirtualFrame frame) { + return this.functionBodyExpr.executeGeneric(frame); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/root/StmtBlockRootNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/root/StmtBlockRootNode.java new file mode 100644 index 00000000..c13de90c --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/root/StmtBlockRootNode.java @@ -0,0 +1,43 @@ +package com.endoflineblog.truffle.part_13.nodes.root; + +import com.endoflineblog.truffle.part_13.EasyScriptTruffleLanguage; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.blocks.BlockStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.blocks.UserFuncBodyStmtNode; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.RootNode; + +/** + * A {@link RootNode} that represents the execution of a block of statements. + * Used as the {@link RootNode} for the entire EasyScript program, + * and also for user-defined functions. + * Identical to the class with the same name from part 11. + */ +public final class StmtBlockRootNode extends RootNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptStmtNode blockStmt; + + public StmtBlockRootNode(EasyScriptTruffleLanguage truffleLanguage, + FrameDescriptor frameDescriptor, BlockStmtNode blockStmt) { + this(truffleLanguage, frameDescriptor, (EasyScriptStmtNode) blockStmt); + } + + public StmtBlockRootNode(EasyScriptTruffleLanguage truffleLanguage, + FrameDescriptor frameDescriptor, UserFuncBodyStmtNode blockStmt) { + this(truffleLanguage, frameDescriptor, (EasyScriptStmtNode) blockStmt); + } + + private StmtBlockRootNode(EasyScriptTruffleLanguage truffleLanguage, + FrameDescriptor frameDescriptor, EasyScriptStmtNode blockStmt) { + super(truffleLanguage, frameDescriptor); + + this.blockStmt = blockStmt; + } + + @Override + public Object execute(VirtualFrame frame) { + return this.blockStmt.executeStatement(frame); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/EasyScriptStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/EasyScriptStmtNode.java new file mode 100644 index 00000000..7ef49318 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/EasyScriptStmtNode.java @@ -0,0 +1,16 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts; + +import com.endoflineblog.truffle.part_13.nodes.EasyScriptNode; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * The abstract common ancestor of all AST Nodes that represent statements in EasyScript, + * like declaring a variable or constant. + * Identical to the class with the same name from part 11. + */ +public abstract class EasyScriptStmtNode extends EasyScriptNode { + /** + * Evaluates this statement, and returns the result of executing it. + */ + public abstract Object executeStatement(VirtualFrame frame); +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/ExprStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/ExprStmtNode.java new file mode 100644 index 00000000..ab281ae5 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/ExprStmtNode.java @@ -0,0 +1,47 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * A Node that represents an expression statement. + * Identical to the class with the same name from part 11. + */ +public final class ExprStmtNode extends EasyScriptStmtNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptExprNode expr; + private final boolean discardExpressionValue; + + /** + * Creates a new instance of the expression statement. + * + * @param expr the expression node + */ + public ExprStmtNode(EasyScriptExprNode expr) { + this(expr, false); + } + + /** + * Creates a new instance of the expression statement. + * + * @param expr the expression node + * @param discardExpressionValue whether evaluating this statement should discard + * the value of the expression it wraps, + * and always return {@code Undefined.INSTANCE} + */ + public ExprStmtNode(EasyScriptExprNode expr, boolean discardExpressionValue) { + this.expr = expr; + this.discardExpressionValue = discardExpressionValue; + } + + /** Evaluating the statement returns the result of executing its expression. */ + @Override + public Object executeStatement(VirtualFrame frame) { + Object exprResult = this.expr.executeGeneric(frame); + // if this statement was created because of transforming a local variable declaration into an assignment, + // return 'undefined', to be consistent with how other declarations work + return this.discardExpressionValue ? Undefined.INSTANCE : exprResult; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/blocks/BlockStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/blocks/BlockStmtNode.java new file mode 100644 index 00000000..62ebc4a9 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/blocks/BlockStmtNode.java @@ -0,0 +1,41 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.blocks; + +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; + +import java.util.List; + +/** + * A Node for representing the EasyScript program itself, + * and any statement blocks, + * for example those used as the branch of an {@code if} statement + * (with the exception of the block for a user-defined function's body, + * which is represented by {@link UserFuncBodyStmtNode}). + * Identical to the class with the same name from part 11. + * + * @see #executeStatement + */ +public final class BlockStmtNode extends EasyScriptStmtNode { + @Children + private final EasyScriptStmtNode[] stmts; + + public BlockStmtNode(List stmts) { + this.stmts = stmts.toArray(new EasyScriptStmtNode[]{}); + } + + /** + * Evaluating the block statement evaluates all statements inside it, + * and returns the result of executing the last statement. + */ + @Override + @ExplodeLoop + public Object executeStatement(VirtualFrame frame) { + int stmtsMinusOne = this.stmts.length - 1; + for (int i = 0; i < stmtsMinusOne; i++) { + this.stmts[i].executeStatement(frame); + } + return stmtsMinusOne < 0 ? Undefined.INSTANCE : this.stmts[stmtsMinusOne].executeStatement(frame); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/blocks/UserFuncBodyStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/blocks/UserFuncBodyStmtNode.java new file mode 100644 index 00000000..f40d858c --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/blocks/UserFuncBodyStmtNode.java @@ -0,0 +1,42 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.blocks; + +import com.endoflineblog.truffle.part_13.exceptions.ReturnException; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; + +import java.util.List; + +/** + * A Node for representing the statement blocks of a user-defined function in EasyScript. + * Returns its value by catching {@link ReturnException}. + * Identical to the class with the same name from part 11. + */ +public final class UserFuncBodyStmtNode extends EasyScriptStmtNode { + @Children + private final EasyScriptStmtNode[] stmts; + + public UserFuncBodyStmtNode(List stmts) { + this.stmts = stmts.toArray(new EasyScriptStmtNode[]{}); + } + + /** + * Evaluating the block statement evaluates all statements inside it, + * and returns whatever a 'return' statement inside it returns. + */ + @Override + @ExplodeLoop + public Object executeStatement(VirtualFrame frame) { + for (EasyScriptStmtNode stmt : this.stmts) { + try { + stmt.executeStatement(frame); + } catch (ReturnException e) { + return e.returnValue; + } + } + // if there was no return statement, + // then we return 'undefined' + return Undefined.INSTANCE; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/BreakStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/BreakStmtNode.java new file mode 100644 index 00000000..d7b260f9 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/BreakStmtNode.java @@ -0,0 +1,16 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.controlflow; + +import com.endoflineblog.truffle.part_13.exceptions.BreakException; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * A Node representing the {@code break} statement. + * Identical to the class with the same name from part 11. + */ +public final class BreakStmtNode extends EasyScriptStmtNode { + @Override + public Object executeStatement(VirtualFrame frame) { + throw new BreakException(); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/ContinueStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/ContinueStmtNode.java new file mode 100644 index 00000000..2a3a2645 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/ContinueStmtNode.java @@ -0,0 +1,16 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.controlflow; + +import com.endoflineblog.truffle.part_13.exceptions.ContinueException; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * A Node representing the {@code continue} statement. + * Identical to the class with the same name from part 11. + */ +public final class ContinueStmtNode extends EasyScriptStmtNode { + @Override + public Object executeStatement(VirtualFrame frame) { + throw new ContinueException(); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/IfStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/IfStmtNode.java new file mode 100644 index 00000000..7d52fac0 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/IfStmtNode.java @@ -0,0 +1,41 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.controlflow; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.profiles.ConditionProfile; + +/** + * A Node that represents an {@code if} statement. + * Identical to the class with the same name from part 11. + */ +public final class IfStmtNode extends EasyScriptStmtNode { + @Child + private EasyScriptExprNode conditionExpr; + + @Child + private EasyScriptStmtNode thenStmt; + + @Child + private EasyScriptStmtNode elseStmt; + + private final ConditionProfile condition = ConditionProfile.createCountingProfile(); + + public IfStmtNode(EasyScriptExprNode conditionExpr, EasyScriptStmtNode thenStmt, EasyScriptStmtNode elseStmt) { + this.conditionExpr = conditionExpr; + this.thenStmt = thenStmt; + this.elseStmt = elseStmt; + } + + @Override + public Object executeStatement(VirtualFrame frame) { + if (this.condition.profile(this.conditionExpr.executeBool(frame))) { + return this.thenStmt.executeStatement(frame); + } else { + return this.elseStmt == null + ? Undefined.INSTANCE + : this.elseStmt.executeStatement(frame); + } + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/ReturnStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/ReturnStmtNode.java new file mode 100644 index 00000000..208fd000 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/controlflow/ReturnStmtNode.java @@ -0,0 +1,26 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.controlflow; + +import com.endoflineblog.truffle.part_13.exceptions.ReturnException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.oracle.truffle.api.frame.VirtualFrame; + +/** + * A Node representing the {@code return} statement. + * Identical to the class with the same name from part 11. + */ +public final class ReturnStmtNode extends EasyScriptStmtNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptExprNode returnExpr; + + public ReturnStmtNode(EasyScriptExprNode returnExpr) { + this.returnExpr = returnExpr; + } + + @Override + public Object executeStatement(VirtualFrame frame) { + Object returnValue = this.returnExpr.executeGeneric(frame); + throw new ReturnException(returnValue); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/DoWhileStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/DoWhileStmtNode.java new file mode 100644 index 00000000..b1977496 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/DoWhileStmtNode.java @@ -0,0 +1,61 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.loops; + +import com.endoflineblog.truffle.part_13.exceptions.BreakException; +import com.endoflineblog.truffle.part_13.exceptions.ContinueException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.Truffle; +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; + +/** + * A Node that represents a {@code do-while} statement. + * Identical to the class with the same name from part 11. + */ +public final class DoWhileStmtNode extends EasyScriptStmtNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private LoopNode loopNode; + + public DoWhileStmtNode(EasyScriptExprNode conditionExpr, EasyScriptStmtNode bodyStmt) { + this.loopNode = Truffle.getRuntime().createLoopNode(new DoWhileRepeatingNode(conditionExpr, bodyStmt)); + } + + @Override + public Object executeStatement(VirtualFrame frame) { + this.loopNode.execute(frame); + return Undefined.INSTANCE; + } + + private static final class DoWhileRepeatingNode extends Node implements RepeatingNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptExprNode conditionExpr; + + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptStmtNode bodyStmt; + + public DoWhileRepeatingNode(EasyScriptExprNode conditionExpr, EasyScriptStmtNode bodyStmt) { + this.conditionExpr = conditionExpr; + this.bodyStmt = bodyStmt; + } + + @Override + public boolean executeRepeating(VirtualFrame frame) { + // in a do-while, we first execute the body of the loop + try { + this.bodyStmt.executeStatement(frame); + } catch (BreakException e) { + // 'break' means 'stop the loop' + return false; + } catch (ContinueException e) { + // fall-through on 'continue' + } + return this.conditionExpr.executeBool(frame); + } + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/ForStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/ForStmtNode.java new file mode 100644 index 00000000..14b5b419 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/ForStmtNode.java @@ -0,0 +1,84 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.loops; + +import com.endoflineblog.truffle.part_13.exceptions.BreakException; +import com.endoflineblog.truffle.part_13.exceptions.ContinueException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.Truffle; +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; + +/** + * A Node that represents a {@code for} statement. + * Identical to the class with the same name from part 11. + */ +public final class ForStmtNode extends EasyScriptStmtNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptStmtNode initStmt; + + @SuppressWarnings("FieldMayBeFinal") + @Child + private LoopNode loopNode; + + public ForStmtNode(EasyScriptStmtNode initStmt, EasyScriptExprNode conditionExpr, + EasyScriptExprNode updateExpr, EasyScriptStmtNode bodyStmt) { + this.initStmt = initStmt; + this.loopNode = Truffle.getRuntime().createLoopNode( + new ForRepeatingNode(conditionExpr, updateExpr, bodyStmt)); + } + + @Override + public Object executeStatement(VirtualFrame frame) { + // first, we execute the init statement, if provided + if (this.initStmt != null) { + this.initStmt.executeStatement(frame); + } + this.loopNode.execute(frame); + return Undefined.INSTANCE; + } + + private static final class ForRepeatingNode extends Node implements RepeatingNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptExprNode conditionExpr; + + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptExprNode updateExpr; + + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptStmtNode bodyStmt; + + public ForRepeatingNode(EasyScriptExprNode conditionExpr, EasyScriptExprNode updateExpr, + EasyScriptStmtNode bodyStmt) { + this.conditionExpr = conditionExpr; + this.updateExpr = updateExpr; + this.bodyStmt = bodyStmt; + } + + @Override + public boolean executeRepeating(VirtualFrame frame) { + if (this.conditionExpr != null && + !this.conditionExpr.executeBool(frame)) { + return false; + } + try { + this.bodyStmt.executeStatement(frame); + } catch (BreakException e) { + // 'break' means 'stop the loop' + return false; + } catch (ContinueException e) { + // fall-through on 'continue' + } + if (this.updateExpr != null) { + this.updateExpr.executeGeneric(frame); + } + return true; + } + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/WhileStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/WhileStmtNode.java new file mode 100644 index 00000000..32593dbf --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/loops/WhileStmtNode.java @@ -0,0 +1,63 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.loops; + +import com.endoflineblog.truffle.part_13.exceptions.BreakException; +import com.endoflineblog.truffle.part_13.exceptions.ContinueException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.Truffle; +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; + +/** + * A Node that represents a {@code while} statement. + * Identical to the class with the same name from part 11. + */ +public final class WhileStmtNode extends EasyScriptStmtNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private LoopNode loopNode; + + public WhileStmtNode(EasyScriptExprNode conditionExpr, EasyScriptStmtNode bodyStmt) { + this.loopNode = Truffle.getRuntime().createLoopNode(new WhileRepeatingNode(conditionExpr, bodyStmt)); + } + + @Override + public Object executeStatement(VirtualFrame frame) { + this.loopNode.execute(frame); + return Undefined.INSTANCE; + } + + private static final class WhileRepeatingNode extends Node implements RepeatingNode { + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptExprNode conditionExpr; + + @SuppressWarnings("FieldMayBeFinal") + @Child + private EasyScriptStmtNode bodyStmt; + + public WhileRepeatingNode(EasyScriptExprNode conditionExpr, EasyScriptStmtNode bodyStmt) { + this.conditionExpr = conditionExpr; + this.bodyStmt = bodyStmt; + } + + @Override + public boolean executeRepeating(VirtualFrame frame) { + if (!this.conditionExpr.executeBool(frame)) { + return false; + } + try { + this.bodyStmt.executeStatement(frame); + } catch (BreakException e) { + // 'break' means 'stop the loop' + return false; + } catch (ContinueException e) { + // fall-through on 'continue' + } + return true; + } + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/variables/FuncDeclStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/variables/FuncDeclStmtNode.java new file mode 100644 index 00000000..93c7ec7b --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/variables/FuncDeclStmtNode.java @@ -0,0 +1,73 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.variables; + +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.root.StmtBlockRootNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.blocks.UserFuncBodyStmtNode; +import com.endoflineblog.truffle.part_13.runtime.FunctionObject; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.endoflineblog.truffle.part_13.nodes.exprs.DynamicObjectReferenceExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.GlobalScopeObjectExprNode; +import com.endoflineblog.truffle.part_13.runtime.ClassPrototypeObject; +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.NodeField; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.DynamicObjectLibrary; + +/** + * A Node that represents the declaration of a function in EasyScript. + * Very similar to the class with the same name from part 11, + * the only difference is that we change the name of the first sub-expression + * (the one that determines in which object to store the resulting function) + * from {@code globalScopeObjectExpr} to {@code containerObjectExpr}, + * and its type (from {@link GlobalScopeObjectExprNode} + * to {@link EasyScriptExprNode}). + * We change this because, in this part of the series, in addition to + * {@link GlobalScopeObjectExprNode}, + * we also pass to this class' static factory method a + * {@link DynamicObjectReferenceExprNode}, + * which is how we handle methods inside class declarations + * (the referenced {@link DynamicObject} is the {@link ClassPrototypeObject} + * in this case). + * Other than those name and type changes, + * the implementation itself is identical to part 11. + */ +@NodeChild(value = "containerObjectExpr", type = EasyScriptExprNode.class) +@NodeField(name = "funcName", type = String.class) +@NodeField(name = "frameDescriptor", type = FrameDescriptor.class) +@NodeField(name = "funcBody", type = UserFuncBodyStmtNode.class) +@NodeField(name = "argumentCount", type = int.class) +public abstract class FuncDeclStmtNode extends EasyScriptStmtNode { + protected abstract String getFuncName(); + protected abstract FrameDescriptor getFrameDescriptor(); + protected abstract UserFuncBodyStmtNode getFuncBody(); + protected abstract int getArgumentCount(); + + @CompilationFinal + private FunctionObject cachedFunction; + + @Specialization(limit = "1") + protected Object declareFunction(DynamicObject containerObject, + @CachedLibrary("containerObject") DynamicObjectLibrary objectLibrary) { + if (this.cachedFunction == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + + var truffleLanguage = this.currentTruffleLanguage(); + var funcRootNode = new StmtBlockRootNode(truffleLanguage, this.getFrameDescriptor(), this.getFuncBody()); + var callTarget = funcRootNode.getCallTarget(); + + this.cachedFunction = new FunctionObject(callTarget, this.getArgumentCount()); + } + + // we allow functions to be redefined, to comply with JavaScript semantics + objectLibrary.putConstant(containerObject, this.getFuncName(), this.cachedFunction, 0); + + // we return 'undefined' for statements that declare functions + return Undefined.INSTANCE; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/variables/GlobalVarDeclStmtNode.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/variables/GlobalVarDeclStmtNode.java new file mode 100644 index 00000000..4ccfe2b9 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/nodes/stmts/variables/GlobalVarDeclStmtNode.java @@ -0,0 +1,54 @@ +package com.endoflineblog.truffle.part_13.nodes.stmts.variables; + +import com.endoflineblog.truffle.part_13.common.DeclarationKind; +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.GlobalScopeObjectExprNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.runtime.Undefined; +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.NodeField; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.DynamicObjectLibrary; + +/** + * A Node that represents the declaration of a global + * (as opposed to local to a function) variable or constant in EasyScript. + * Identical to the class with the same name from part 11. + */ +@NodeChild(value = "globalScopeObjectExpr", type = GlobalScopeObjectExprNode.class) +@NodeChild(value = "initializerExpr", type = EasyScriptExprNode.class) +@NodeField(name = "name", type = String.class) +@NodeField(name = "declarationKind", type = DeclarationKind.class) +public abstract class GlobalVarDeclStmtNode extends EasyScriptStmtNode { + protected abstract String getName(); + protected abstract DeclarationKind getDeclarationKind(); + + @CompilationFinal + private boolean checkVariableExists = true; + + @Specialization(limit = "1") + protected Object createVariable(DynamicObject globalScopeObject, Object value, + @CachedLibrary("globalScopeObject") DynamicObjectLibrary objectLibrary) { + var variableId = this.getName(); + + if (this.checkVariableExists) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + this.checkVariableExists = false; + + if (objectLibrary.containsKey(globalScopeObject, variableId)) { + throw new EasyScriptException(this, "Identifier '" + variableId + "' has already been declared"); + } + } + + int flags = this.getDeclarationKind() == DeclarationKind.CONST ? 1 : 0; + objectLibrary.putWithFlags(globalScopeObject, variableId, value, flags); + + // we return 'undefined' for statements that declare variables + return Undefined.INSTANCE; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/parsing/EasyScriptTruffleParser.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/parsing/EasyScriptTruffleParser.java new file mode 100644 index 00000000..b817da08 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/parsing/EasyScriptTruffleParser.java @@ -0,0 +1,602 @@ +package com.endoflineblog.truffle.part_13.parsing; + +import com.endoflineblog.truffle.part_13.common.DeclarationKind; +import com.endoflineblog.truffle.part_13.common.LocalVariableFrameSlotId; +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.DynamicObjectReferenceExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.EasyScriptExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.GlobalScopeObjectExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.arithmetic.AdditionExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.arithmetic.NegationExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.arithmetic.NegationExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.arithmetic.SubtractionExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.arrays.ArrayIndexReadExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.arrays.ArrayIndexWriteExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.arrays.ArrayLiteralExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.comparisons.EqualityExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.comparisons.GreaterExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.comparisons.GreaterOrEqualExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.comparisons.InequalityExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.comparisons.LesserExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.comparisons.LesserOrEqualExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.FunctionCallExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.ReadFunctionArgExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.WriteFunctionArgExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.literals.BoolLiteralExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.literals.DoubleLiteralExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.literals.IntLiteralExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.literals.StringLiteralExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.literals.UndefinedLiteralExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.objects.ClassDeclExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.objects.NewExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.properties.PropertyReadExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.variables.GlobalVarAssignmentExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.variables.GlobalVarReferenceExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.variables.LocalVarAssignmentExprNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.variables.LocalVarAssignmentExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.variables.LocalVarReferenceExprNodeGen; +import com.endoflineblog.truffle.part_13.nodes.stmts.EasyScriptStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.ExprStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.blocks.BlockStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.blocks.UserFuncBodyStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.controlflow.BreakStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.controlflow.ContinueStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.controlflow.IfStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.controlflow.ReturnStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.loops.DoWhileStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.loops.ForStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.loops.WhileStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.variables.FuncDeclStmtNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.variables.FuncDeclStmtNodeGen; +import com.endoflineblog.truffle.part_13.nodes.stmts.variables.GlobalVarDeclStmtNodeGen; +import com.endoflineblog.truffle.part_13.parsing.antlr.EasyScriptLexer; +import com.endoflineblog.truffle.part_13.parsing.antlr.EasyScriptParser; +import com.endoflineblog.truffle.part_13.runtime.ClassPrototypeObject; +import com.endoflineblog.truffle.part_13.EasyScriptTruffleLanguage; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameSlotKind; +import com.oracle.truffle.api.object.Shape; +import org.antlr.v4.runtime.BailErrorStrategy; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.apache.commons.text.StringEscapeUtils; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.stream.Collectors; + +/** + * This is the class that parses the program and turns it into a Truffle AST. + * It uses ANTLR to perform the actual parsing, + * with the grammar defined in the {@code src/main/antlr/com/endoflineblog/truffle/part_13/parsing/antlr/EasyScript.g4} file. + * This class is invoked by the {@code TruffleLanguage} implementation for this part. + * + * @see #parse + * @see EasyScriptTruffleLanguage + */ +public final class EasyScriptTruffleParser { + public static ParsingResult parse(Reader program, Shape objectShape, Shape arrayShape) throws IOException { + var lexer = new EasyScriptLexer(CharStreams.fromReader(program)); + // remove the default console error listener + lexer.removeErrorListeners(); + var parser = new EasyScriptParser(new CommonTokenStream(lexer)); + // remove the default console error listener + parser.removeErrorListeners(); + // throw an exception when a parsing error is encountered + parser.setErrorHandler(new BailErrorStrategy()); + var easyScriptTruffleParser = new EasyScriptTruffleParser(objectShape, arrayShape); + List stmts = easyScriptTruffleParser.parseStmtsList(parser.start().stmt()); + return new ParsingResult( + new BlockStmtNode(stmts), + easyScriptTruffleParser.frameDescriptor.build()); + } + + private final Shape objectShape, arrayShape; + + private enum ParserState { TOP_LEVEL, NESTED_SCOPE_IN_TOP_LEVEL, FUNC_DEF } + + /** Whether we're parsing a function definition. */ + private ParserState state; + + /** + * The {@link FrameDescriptor} for either a given function definition, + * or for any local variables in the statements of the top-level program. + */ + private FrameDescriptor.Builder frameDescriptor; + + private static abstract class FrameMember {} + private static final class FunctionArgument extends FrameMember { + public final int argumentIndex; + + FunctionArgument(int argumentIndex) { + this.argumentIndex = argumentIndex; + } + } + private static final class LocalVariable extends FrameMember { + public final int variableIndex; + public final DeclarationKind declarationKind; + + LocalVariable(int variableIndex, DeclarationKind declarationKind) { + this.variableIndex = variableIndex; + this.declarationKind = declarationKind; + } + } + + /** + * Map containing bindings for the function arguments and local variables when parsing function definitions + * and nested scopes of the top-level scope. + */ + private Stack> localScopes; + + /** + * The counter that makes it easy to generate unique variable names for local variables + * (as their names can repeat in nested scopes). + */ + private int localVariablesCounter; + + private EasyScriptTruffleParser(Shape objectShape, Shape arrayShape) { + this.objectShape = objectShape; + this.arrayShape = arrayShape; + this.state = ParserState.TOP_LEVEL; + this.frameDescriptor = FrameDescriptor.newBuilder(); + this.localScopes = new Stack<>(); + this.localVariablesCounter = 0; + } + + private List parseStmtsList(List stmts) { + // in the first pass, only handle function declarations, + // as it's legal to invoke functions before they are declared + var funcDecls = new ArrayList(); + for (EasyScriptParser.StmtContext stmt : stmts) { + if (stmt instanceof EasyScriptParser.FuncDeclStmtContext) { + funcDecls.add(this.parseFuncDeclStmt((EasyScriptParser.FuncDeclStmtContext) stmt)); + } + } + + // in the second pass, handle the remaining statements that are not function declarations + var nonFuncDeclStmts = new ArrayList(); + for (EasyScriptParser.StmtContext stmt : stmts) { + if (stmt instanceof EasyScriptParser.ExprStmtContext) { + nonFuncDeclStmts.add(this.parseExprStmt((EasyScriptParser.ExprStmtContext) stmt)); + } else if (stmt instanceof EasyScriptParser.ClassDeclStmtContext) { + nonFuncDeclStmts.add(this.parseClassDeclStmt((EasyScriptParser.ClassDeclStmtContext) stmt)); + } else if (stmt instanceof EasyScriptParser.ReturnStmtContext) { + nonFuncDeclStmts.add(this.parseReturnStmt((EasyScriptParser.ReturnStmtContext) stmt)); + } else if (stmt instanceof EasyScriptParser.IfStmtContext) { + nonFuncDeclStmts.add(this.parseIfStmt((EasyScriptParser.IfStmtContext) stmt)); + } else if (stmt instanceof EasyScriptParser.WhileStmtContext) { + nonFuncDeclStmts.add(this.parseWhileStmt((EasyScriptParser.WhileStmtContext) stmt)); + } else if (stmt instanceof EasyScriptParser.DoWhileStmtContext) { + nonFuncDeclStmts.add(this.parseDoWhileStmt((EasyScriptParser.DoWhileStmtContext) stmt)); + } else if (stmt instanceof EasyScriptParser.ForStmtContext) { + nonFuncDeclStmts.add(this.parseForStmt((EasyScriptParser.ForStmtContext) stmt)); + } else if (stmt instanceof EasyScriptParser.BlockStmtContext) { + nonFuncDeclStmts.add(this.parseStmtBlock((EasyScriptParser.BlockStmtContext) stmt)); + } else if (stmt instanceof EasyScriptParser.BreakStmtContext) { + nonFuncDeclStmts.add(new BreakStmtNode()); + } else if (stmt instanceof EasyScriptParser.ContinueStmtContext) { + nonFuncDeclStmts.add(new ContinueStmtNode()); + } else if (stmt instanceof EasyScriptParser.VarDeclStmtContext) { + EasyScriptParser.VarDeclStmtContext varDeclStmt = (EasyScriptParser.VarDeclStmtContext) stmt; + DeclarationKind declarationKind = DeclarationKind.fromToken(varDeclStmt.kind.getText()); + List varDeclBindings = varDeclStmt.binding(); + for (EasyScriptParser.BindingContext varBinding : varDeclBindings) { + String variableId = varBinding.ID().getText(); + var bindingExpr = varBinding.expr1(); + EasyScriptExprNode initializerExpr; + if (bindingExpr == null) { + if (declarationKind == DeclarationKind.CONST) { + throw new EasyScriptException("Missing initializer in const declaration '" + variableId + "'"); + } + // if a 'let' or 'var' declaration is missing an initializer, + // it means it will be initialized with 'undefined' + initializerExpr = new UndefinedLiteralExprNode(); + } else { + initializerExpr = this.parseExpr1(bindingExpr); + } + + if (this.state == ParserState.TOP_LEVEL) { + // this is a global variable + nonFuncDeclStmts.add(GlobalVarDeclStmtNodeGen.create(GlobalScopeObjectExprNodeGen.create(), initializerExpr, variableId, declarationKind)); + } else { + // this is a local variable (either of a function, or on the top-level) + var frameSlotId = new LocalVariableFrameSlotId(variableId, ++this.localVariablesCounter); + int frameSlot = this.frameDescriptor.addSlot(FrameSlotKind.Illegal, frameSlotId, declarationKind); + if (this.localScopes.peek().putIfAbsent(variableId, new LocalVariable(frameSlot, declarationKind)) != null) { + throw new EasyScriptException("Identifier '" + variableId + "' has already been declared"); + } + LocalVarAssignmentExprNode assignmentExpr = LocalVarAssignmentExprNodeGen.create(initializerExpr, frameSlot); + nonFuncDeclStmts.add(new ExprStmtNode(assignmentExpr, /* discardExpressionValue */ true)); + } + } + } + } + + // return the function declarations first, and then the remaining statements + var result = new ArrayList(funcDecls.size() + nonFuncDeclStmts.size()); + result.addAll(funcDecls); + result.addAll(nonFuncDeclStmts); + return result; + } + + private ExprStmtNode parseExprStmt(EasyScriptParser.ExprStmtContext exprStmt) { + return new ExprStmtNode(this.parseExpr1(exprStmt.expr1())); + } + + private ReturnStmtNode parseReturnStmt(EasyScriptParser.ReturnStmtContext returnStmt) { + if (this.state != ParserState.FUNC_DEF) { + throw new EasyScriptException("return statement is not allowed outside functions"); + } + return new ReturnStmtNode(returnStmt.expr1() == null + ? new UndefinedLiteralExprNode() + : this.parseExpr1(returnStmt.expr1())); + } + + private IfStmtNode parseIfStmt(EasyScriptParser.IfStmtContext ifStmt) { + return new IfStmtNode( + this.parseExpr1(ifStmt.cond), + this.parseStmt(ifStmt.then_stmt), + this.parseStmt(ifStmt.else_stmt)); + } + + private WhileStmtNode parseWhileStmt(EasyScriptParser.WhileStmtContext whileStmt) { + return new WhileStmtNode( + this.parseExpr1(whileStmt.cond), + this.parseStmt(whileStmt.body)); + } + + private DoWhileStmtNode parseDoWhileStmt(EasyScriptParser.DoWhileStmtContext doWhileStmt) { + return new DoWhileStmtNode( + this.parseExpr1(doWhileStmt.cond), + this.parseStmtBlock(doWhileStmt.stmt())); + } + + private ForStmtNode parseForStmt(EasyScriptParser.ForStmtContext forStmt) { + // a 'for' loop is its own scope + ParserState previousParserState = this.state; + + if (this.state == ParserState.TOP_LEVEL) { + this.state = ParserState.NESTED_SCOPE_IN_TOP_LEVEL; + } + this.localScopes.push(new HashMap<>()); + + var ret = new ForStmtNode( + this.parseStmt(forStmt.init), + this.parseExpr1(forStmt.cond), + this.parseExpr1(forStmt.updt), + this.parseStmt(forStmt.body)); + + // bring back the old state + this.state = previousParserState; + this.localScopes.pop(); + + return ret; + } + + private EasyScriptStmtNode parseStmt(EasyScriptParser.StmtContext stmt) { + if (stmt == null) { + return null; + } + List parsedStmts = this.parseStmtsList(List.of(stmt)); + return parsedStmts.size() == 1 + ? parsedStmts.get(0) + : new BlockStmtNode(parsedStmts); + } + + private BlockStmtNode parseStmtBlock(EasyScriptParser.BlockStmtContext blockStmt) { + return parseStmtBlock(blockStmt.stmt()); + } + + private BlockStmtNode parseStmtBlock(List stmts) { + // save the current state of the parser (before entering the block) + ParserState previousParserState = this.state; + + if (this.state == ParserState.TOP_LEVEL) { + this.state = ParserState.NESTED_SCOPE_IN_TOP_LEVEL; + } + this.localScopes.push(new HashMap<>()); + + // perform the parsing + List ret = this.parseStmtsList(stmts); + + // bring back the old state + this.state = previousParserState; + this.localScopes.pop(); + + return new BlockStmtNode(ret); + } + + private FuncDeclStmtNode parseFuncDeclStmt(EasyScriptParser.FuncDeclStmtContext funcDeclStmt) { + return this.parseSubroutineDecl(funcDeclStmt.subroutine_decl(), + GlobalScopeObjectExprNodeGen.create()); + } + + private EasyScriptStmtNode parseClassDeclStmt(EasyScriptParser.ClassDeclStmtContext classDeclStmt) { + if (this.state == ParserState.FUNC_DEF) { + // we do not allow nesting classes inside functions at the moment + // (in theory, we could handle it by assigning the class object to a local variable, + // but the additional complexity doesn't seem worth it) + throw new EasyScriptException("classes nested in functions are not supported in EasyScript"); + } + + String className = classDeclStmt.ID().getText(); + var classPrototype = new ClassPrototypeObject(this.objectShape, className); + List classMethods = new ArrayList<>(); + for (var classMember : classDeclStmt.class_member()) { + classMethods.add(this.parseSubroutineDecl(classMember.subroutine_decl(), + new DynamicObjectReferenceExprNode(classPrototype))); + } + return GlobalVarDeclStmtNodeGen.create( + GlobalScopeObjectExprNodeGen.create(), + new ClassDeclExprNode(classMethods, classPrototype), + className, DeclarationKind.LET); + } + + private FuncDeclStmtNode parseSubroutineDecl(EasyScriptParser.Subroutine_declContext subroutineDecl, + EasyScriptExprNode containerObjectExpr) { + if (this.state == ParserState.FUNC_DEF) { + // we do not allow nested functions (yet 😉) + throw new EasyScriptException("nested functions are not supported in EasyScript yet"); + } + + // save the current state of the parser (before entering the function) + FrameDescriptor.Builder previousFrameDescriptor = this.frameDescriptor; + ParserState previousParserState = this.state; + var previousLocalScopes = this.localScopes; + + // initialize the new state + this.frameDescriptor = FrameDescriptor.newBuilder(); + this.state = ParserState.FUNC_DEF; + this.localScopes = new Stack<>(); + + var localVariables = new HashMap(); + // add each parameter to the map, with the correct index + List funcArgs = subroutineDecl.args.ID(); + int argumentCount = funcArgs.size(); + // first, initialize the locals with function arguments + for (int i = 0; i < argumentCount; i++) { + localVariables.put(funcArgs.get(i).getText(), new FunctionArgument(i)); + } + this.localScopes.push(localVariables); + + // parse the statements in the function definition + List funcStmts = this.parseStmtsList(subroutineDecl.stmt()); + + FrameDescriptor frameDescriptor = this.frameDescriptor.build(); + // bring back the old state + this.frameDescriptor = previousFrameDescriptor; + this.state = previousParserState; + this.localScopes = previousLocalScopes; + + return FuncDeclStmtNodeGen.create(containerObjectExpr, + subroutineDecl.name.getText(), + frameDescriptor, new UserFuncBodyStmtNode(funcStmts), argumentCount); + } + + private EasyScriptExprNode parseExpr1(EasyScriptParser.Expr1Context expr1) { + if (expr1 == null) { + return null; + } + if (expr1 instanceof EasyScriptParser.AssignmentExpr1Context) { + return parseAssignmentExpr((EasyScriptParser.AssignmentExpr1Context) expr1); + } else if (expr1 instanceof EasyScriptParser.ArrayIndexWriteExpr1Context) { + return this.parseArrayIndexWriteExpr((EasyScriptParser.ArrayIndexWriteExpr1Context) expr1); + } else { + return parseExpr2(((EasyScriptParser.PrecedenceTwoExpr1Context) expr1).expr2()); + } + } + + private EasyScriptExprNode parseAssignmentExpr(EasyScriptParser.AssignmentExpr1Context assignmentExpr) { + String variableId = assignmentExpr.ID().getText(); + FrameMember frameMember = this.findFrameMember(variableId); + EasyScriptExprNode initializerExpr = this.parseExpr1(assignmentExpr.expr1()); + if (frameMember == null) { + return GlobalVarAssignmentExprNodeGen.create(GlobalScopeObjectExprNodeGen.create(), initializerExpr, variableId); + } else { + if (frameMember instanceof FunctionArgument) { + return new WriteFunctionArgExprNode(initializerExpr, ((FunctionArgument) frameMember).argumentIndex); + } else { + var localVariable = (LocalVariable) frameMember; + if (localVariable.declarationKind == DeclarationKind.CONST) { + throw new EasyScriptException("Assignment to constant variable '" + variableId + "'"); + } + return LocalVarAssignmentExprNodeGen.create(initializerExpr, localVariable.variableIndex); + } + } + } + + private EasyScriptExprNode parseArrayIndexWriteExpr(EasyScriptParser.ArrayIndexWriteExpr1Context arrayIndexWriteExpr) { + return ArrayIndexWriteExprNodeGen.create( + this.parseExpr5(arrayIndexWriteExpr.arr), + this.parseExpr1(arrayIndexWriteExpr.index), + this.parseExpr1(arrayIndexWriteExpr.rvalue)); + } + + private EasyScriptExprNode parseExpr2(EasyScriptParser.Expr2Context expr2) { + return expr2 instanceof EasyScriptParser.EqNotEqExpr2Context + ? this.parseEqNotEqExpression(((EasyScriptParser.EqNotEqExpr2Context) expr2)) + : this.parseExpr3(((EasyScriptParser.PrecedenceThreeExpr2Context) expr2).expr3()); + } + + private EasyScriptExprNode parseEqNotEqExpression(EasyScriptParser.EqNotEqExpr2Context eqNotEqExpr) { + EasyScriptExprNode leftSide = this.parseExpr2(eqNotEqExpr.expr2()); + EasyScriptExprNode rightSide = this.parseExpr3(eqNotEqExpr.expr3()); + return "===".equals(eqNotEqExpr.c.getText()) + ? EqualityExprNodeGen.create(leftSide, rightSide) + : InequalityExprNodeGen.create(leftSide, rightSide); + } + + private EasyScriptExprNode parseExpr3(EasyScriptParser.Expr3Context expr3) { + if (expr3 instanceof EasyScriptParser.ComparisonExpr3Context) { + return this.parseComparisonExpr(((EasyScriptParser.ComparisonExpr3Context) expr3)); + } else { + return this.parseExpr4(((EasyScriptParser.PrecedenceFourExpr3Context) expr3).expr4()); + } + } + + private EasyScriptExprNode parseComparisonExpr(EasyScriptParser.ComparisonExpr3Context comparisonExpr) { + EasyScriptExprNode leftSide = this.parseExpr3(comparisonExpr.expr3()); + EasyScriptExprNode rightSide = this.parseExpr4(comparisonExpr.expr4()); + switch (comparisonExpr.c.getText()) { + case "<": return LesserExprNodeGen.create(leftSide, rightSide); + case "<=": return LesserOrEqualExprNodeGen.create(leftSide, rightSide); + case ">": return GreaterExprNodeGen.create(leftSide, rightSide); + default: + case ">=": return GreaterOrEqualExprNodeGen.create(leftSide, rightSide); + } + } + + private EasyScriptExprNode parseExpr4(EasyScriptParser.Expr4Context expr4) { + if (expr4 instanceof EasyScriptParser.AddSubtractExpr4Context) { + return this.parseAdditionSubtractionExpr((EasyScriptParser.AddSubtractExpr4Context) expr4); + } else if (expr4 instanceof EasyScriptParser.UnaryMinusExpr4Context) { + return parseUnaryMinusExpr((EasyScriptParser.UnaryMinusExpr4Context) expr4); + } else { + return parseExpr5(((EasyScriptParser.PrecedenceFiveExpr4Context) expr4).expr5()); + } + } + + private EasyScriptExprNode parseAdditionSubtractionExpr(EasyScriptParser.AddSubtractExpr4Context addSubtractExpr) { + EasyScriptExprNode leftSide = this.parseExpr4(addSubtractExpr.left); + EasyScriptExprNode rightSide = this.parseExpr5(addSubtractExpr.right); + switch (addSubtractExpr.o.getText()) { + case "-": + return SubtractionExprNodeGen.create(leftSide, rightSide); + case "+": + default: + return AdditionExprNodeGen.create(leftSide, rightSide); + } + } + + private NegationExprNode parseUnaryMinusExpr(EasyScriptParser.UnaryMinusExpr4Context unaryMinusExpr) { + return NegationExprNodeGen.create(parseExpr5(unaryMinusExpr.expr5())); + } + + private EasyScriptExprNode parseExpr5(EasyScriptParser.Expr5Context expr5) { + if (expr5 instanceof EasyScriptParser.PropertyReadExpr5Context) { + return this.parsePropertyReadExpr((EasyScriptParser.PropertyReadExpr5Context) expr5); + } else if (expr5 instanceof EasyScriptParser.ArrayIndexReadExpr5Context) { + return this.parseArrayIndexReadExpr((EasyScriptParser.ArrayIndexReadExpr5Context) expr5); + } else if (expr5 instanceof EasyScriptParser.CallExpr5Context) { + return parseCallExpr((EasyScriptParser.CallExpr5Context) expr5); + } else { + return this.parseExpr6(((EasyScriptParser.PrecedenceSixExpr5Context) expr5).expr6()); + } + } + + private EasyScriptExprNode parseExpr6(EasyScriptParser.Expr6Context expr6) { + if (expr6 instanceof EasyScriptParser.LiteralExpr6Context) { + return this.parseLiteralExpr((EasyScriptParser.LiteralExpr6Context) expr6); + } else if (expr6 instanceof EasyScriptParser.ReferenceExpr6Context) { + return this.parseReference(((EasyScriptParser.ReferenceExpr6Context) expr6).ID().getText()); + } else if (expr6 instanceof EasyScriptParser.ArrayLiteralExpr6Context) { + return this.parseArrayLiteralExpr((EasyScriptParser.ArrayLiteralExpr6Context) expr6); + } else if (expr6 instanceof EasyScriptParser.NewExpr6Context) { + return this.parseNewExpr((EasyScriptParser.NewExpr6Context) expr6); + } else { + return this.parseExpr1(((EasyScriptParser.PrecedenceOneExpr6Context) expr6).expr1()); + } + } + + private EasyScriptExprNode parseLiteralExpr(EasyScriptParser.LiteralExpr6Context literalExpr) { + TerminalNode intTerminal = literalExpr.literal().INT(); + if (intTerminal != null) { + return parseIntLiteral(intTerminal.getText()); + } + TerminalNode doubleTerminal = literalExpr.literal().DOUBLE(); + if (doubleTerminal != null) { + return parseDoubleLiteral(doubleTerminal.getText()); + } + EasyScriptParser.Bool_literalContext boolLiteral = literalExpr.literal().bool_literal(); + if (boolLiteral != null) { + return new BoolLiteralExprNode("true".equals(boolLiteral.getText())); + } + EasyScriptParser.String_literalContext stringTerminal = literalExpr.literal().string_literal(); + if (stringTerminal != null) { + String stringLiteral = stringTerminal.getText(); + // remove the quotes delineating the string literal, + // and unescape the string (meaning, turn \' into ', etc.) + return new StringLiteralExprNode(StringEscapeUtils.unescapeJson( + stringLiteral.substring(1, stringLiteral.length() - 1))); + } + return new UndefinedLiteralExprNode(); + } + + private EasyScriptExprNode parseReference(String variableId) { + FrameMember frameMember = this.findFrameMember(variableId); + if (frameMember == null) { + // we know for sure this is a reference to a global variable + return GlobalVarReferenceExprNodeGen.create(GlobalScopeObjectExprNodeGen.create(), variableId); + } else { + return frameMember instanceof FunctionArgument + // an int means this is a function parameter + ? new ReadFunctionArgExprNode(((FunctionArgument) frameMember).argumentIndex) + // this means this is a local variable + : LocalVarReferenceExprNodeGen.create(((LocalVariable) frameMember).variableIndex); + } + } + + private EasyScriptExprNode parsePropertyReadExpr(EasyScriptParser.PropertyReadExpr5Context propertyReadExpr) { + return PropertyReadExprNodeGen.create( + this.parseExpr5(propertyReadExpr.expr5()), + propertyReadExpr.ID().getText()); + } + + private EasyScriptExprNode parseNewExpr(EasyScriptParser.NewExpr6Context newExpr) { + return NewExprNodeGen.create( + this.parseExpr6(newExpr.constr), + newExpr.expr1().stream() + .map(this::parseExpr1) + .collect(Collectors.toList())); + } + + private ArrayLiteralExprNode parseArrayLiteralExpr(EasyScriptParser.ArrayLiteralExpr6Context arrayLiteralExpr) { + return new ArrayLiteralExprNode(this.arrayShape, arrayLiteralExpr.expr1().stream() + .map(arrayElExpr -> this.parseExpr1(arrayElExpr)) + .collect(Collectors.toList())); + } + + private EasyScriptExprNode parseArrayIndexReadExpr(EasyScriptParser.ArrayIndexReadExpr5Context arrayIndexReadExpr) { + return ArrayIndexReadExprNodeGen.create( + this.parseExpr5(arrayIndexReadExpr.arr), + this.parseExpr1(arrayIndexReadExpr.index)); + } + + private FunctionCallExprNode parseCallExpr(EasyScriptParser.CallExpr5Context callExpr) { + return new FunctionCallExprNode( + parseExpr5(callExpr.expr5()), + callExpr.expr1().stream() + .map(this::parseExpr1) + .collect(Collectors.toList())); + } + + private EasyScriptExprNode parseIntLiteral(String text) { + try { + return new IntLiteralExprNode(Integer.parseInt(text)); + } catch (NumberFormatException e) { + // it's possible that the integer literal is too big to fit in a 32-bit Java `int` - + // in that case, fall back to a double literal + return parseDoubleLiteral(text); + } + } + + private DoubleLiteralExprNode parseDoubleLiteral(String text) { + return new DoubleLiteralExprNode(Double.parseDouble(text)); + } + + private FrameMember findFrameMember(String memberName) { + for (var scope : this.localScopes) { + FrameMember ret = scope.get(memberName); + if (ret != null) { + return ret; + } + } + return null; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/parsing/ParsingResult.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/parsing/ParsingResult.java new file mode 100644 index 00000000..ef1639b0 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/parsing/ParsingResult.java @@ -0,0 +1,25 @@ +package com.endoflineblog.truffle.part_13.parsing; + +import com.endoflineblog.truffle.part_13.nodes.stmts.blocks.BlockStmtNode; +import com.oracle.truffle.api.frame.FrameDescriptor; + +/** + * The class used as the result of parsing in + * {@link EasyScriptTruffleParser#parse}. + * Identical to the class with the same name from part 11. + */ +public final class ParsingResult { + /** The Node representing the list of statements that the EasyScript program consists of. */ + public final BlockStmtNode programStmtBlock; + + /** + * The {@link FrameDescriptor} used for the Truffle program itself + * (which can contain local variables in this part). + */ + public final FrameDescriptor topLevelFrameDescriptor; + + public ParsingResult(BlockStmtNode programStmtBlock, FrameDescriptor topLevelFrameDescriptor) { + this.programStmtBlock = programStmtBlock; + this.topLevelFrameDescriptor = topLevelFrameDescriptor; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ArrayObject.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ArrayObject.java new file mode 100644 index 00000000..5a995cb7 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ArrayObject.java @@ -0,0 +1,118 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.endoflineblog.truffle.part_13.EasyScriptTruffleLanguage; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.DynamicObjectLibrary; +import com.oracle.truffle.api.object.Shape; + +/** + * A Truffle {@link DynamicObject} that implements integer-indexed JavaScript arrays. + * Identical to the class with the same name from part 11. + */ +@ExportLibrary(InteropLibrary.class) +public final class ArrayObject extends DynamicObject { + /** + * The field that signifies this {@link DynamicObject} + * always has a property called {@code length}. + * Used in the array shape created in the + * {@link EasyScriptTruffleLanguage TruffleLanguage class for this chapter}. + */ + @DynamicField + private long length; + + private Object[] arrayElements; + + public ArrayObject(Shape arrayShape, Object[] arrayElements) { + super(arrayShape); + this.setArrayElements(arrayElements, DynamicObjectLibrary.getUncached()); + } + + @ExportMessage + boolean hasArrayElements() { + return true; + } + + @ExportMessage + long getArraySize() { + return this.arrayElements.length; + } + + @ExportMessage + boolean isArrayElementReadable(long index) { + // In JavaScript, it's legal to take any index of an array - + // indexes out of bounds simply return 'undefined'. + // However, in GraalVM interop, + // the same index cannot be both readable and insertable, + // so we consider elements readable that are in the array + return index >= 0 && index < this.arrayElements.length; + } + + @ExportMessage + Object readArrayElement(long index) { + return this.isArrayElementReadable(index) + ? this.arrayElements[(int) index] + : Undefined.INSTANCE; + } + + @ExportMessage + boolean isArrayElementModifiable(long index) { + return this.isArrayElementReadable(index); + } + + @ExportMessage + boolean isArrayElementInsertable(long index) { + return index >= this.arrayElements.length; + } + + @ExportMessage + void writeArrayElement(long index, Object value, + @CachedLibrary("this") DynamicObjectLibrary objectLibrary) { + if (this.isArrayElementModifiable(index)) { + this.arrayElements[(int) index] = value; + } else { + // in JavaScript, it's legal to write past the array size + Object[] newArrayElements = new Object[(int) index + 1]; + for (int i = 0; i < index; i++) { + newArrayElements[i] = i < this.arrayElements.length + ? this.arrayElements[i] + : Undefined.INSTANCE; + } + newArrayElements[(int) index] = value; + this.setArrayElements(newArrayElements, objectLibrary); + } + } + + @ExportMessage + boolean hasMembers() { + return true; + } + + @ExportMessage + boolean isMemberReadable(String member) { + return "length".equals(member); + } + + @ExportMessage + Object readMember(String member, + @CachedLibrary("this") DynamicObjectLibrary objectLibrary) throws UnknownIdentifierException { + switch (member) { + case "length": return objectLibrary.getOrDefault(this, "length", 0); + default: throw UnknownIdentifierException.create(member); + } + } + + @ExportMessage + Object getMembers(@SuppressWarnings("unused") boolean includeInternal) { + return new MemberNamesObject(new String[]{"length"}); + } + + private void setArrayElements(Object[] arrayElements, DynamicObjectLibrary objectLibrary) { + this.arrayElements = arrayElements; + objectLibrary.putInt(this, "length", arrayElements.length); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ClassInstanceObject.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ClassInstanceObject.java new file mode 100644 index 00000000..d6368639 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ClassInstanceObject.java @@ -0,0 +1,66 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.endoflineblog.truffle.part_13.nodes.exprs.objects.NewExprNode; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.object.DynamicObjectLibrary; + +/** + * A {@link TruffleObject} that represents an instance of a user-defined class. + * Instances of this class are created in the + * {@link NewExprNode 'new' operator expression Node}. + * It contains a pointer to the {@link ClassPrototypeObject prototype object of the class it belongs to}, + * and it delegates all member reads from the {@link InteropLibrary} + * to that prototype, since, in this part of the series, + * we only support instance methods of classes, not fields. + */ +@ExportLibrary(InteropLibrary.class) +public final class ClassInstanceObject implements TruffleObject { + // this can't be private, because it's used in specialization guard expressions + final ClassPrototypeObject classPrototypeObject; + + public ClassInstanceObject(ClassPrototypeObject classPrototypeObject) { + this.classPrototypeObject = classPrototypeObject; + } + + @Override + public String toString() { + return "[object Object]"; + } + + @ExportMessage + Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) { + return this.toString(); + } + + @ExportMessage + boolean hasMembers() { + return true; + } + + @ExportMessage + boolean isMemberReadable(String member, + @CachedLibrary("this.classPrototypeObject") DynamicObjectLibrary dynamicObjectLibrary) { + return dynamicObjectLibrary.containsKey(this.classPrototypeObject, member); + } + + @ExportMessage + Object readMember(String member, @CachedLibrary("this.classPrototypeObject") DynamicObjectLibrary dynamicObjectLibrary) + throws UnknownIdentifierException { + Object value = dynamicObjectLibrary.getOrDefault(this.classPrototypeObject, member, null); + if (value == null) { + throw UnknownIdentifierException.create(member); + } + return value; + } + + @ExportMessage + Object getMembers(@SuppressWarnings("unused") boolean includeInternal, + @CachedLibrary("this.classPrototypeObject") DynamicObjectLibrary dynamicObjectLibrary) { + return new MemberNamesObject(dynamicObjectLibrary.getKeyArray(this.classPrototypeObject)); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ClassPrototypeObject.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ClassPrototypeObject.java new file mode 100644 index 00000000..7bb50780 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/ClassPrototypeObject.java @@ -0,0 +1,40 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.endoflineblog.truffle.part_13.nodes.exprs.objects.ClassDeclExprNode; +import com.endoflineblog.truffle.part_13.nodes.stmts.variables.GlobalVarDeclStmtNode; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.Shape; + +/** + * A {@link DynamicObject} that represents the prototype of a user-defined class. + * Each {@link ClassInstanceObject instance of a class} + * points to the prototype of its class, + * and all property reads of the instance delegate to this prototype object to get a reference to the class' instance method. + * An instance of this class is created when parsing a class declaration, + * passed to the {@link ClassDeclExprNode class declaration Node}, + * and saved as a global variable with the name equal to the name of the class using the + * {@link GlobalVarDeclStmtNode}. + */ +@ExportLibrary(InteropLibrary.class) +public final class ClassPrototypeObject extends DynamicObject { + public final String className; + + public ClassPrototypeObject(Shape shape, String className) { + super(shape); + + this.className = className; + } + + @Override + public String toString() { + return "[class " + this.className + "]"; + } + + @ExportMessage + Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) { + return this.toString(); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/EasyScriptTruffleStrings.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/EasyScriptTruffleStrings.java new file mode 100644 index 00000000..c8ea034a --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/EasyScriptTruffleStrings.java @@ -0,0 +1,49 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.strings.TruffleString; + +/** + * A class containing static helper methods for dealing with {@link TruffleString}s in EasyScript. + * Identical to the class with the same name from part 11. + */ +public final class EasyScriptTruffleStrings { + /** The string encoding used by EasyScript - UTF-16, same as JavaScript. */ + private static final TruffleString.Encoding JAVA_SCRIPT_STRING_ENCODING = TruffleString.Encoding.UTF_16; + public static final TruffleString EMPTY = JAVA_SCRIPT_STRING_ENCODING.getEmpty(); + + public static TruffleString fromJavaString(String value) { + return TruffleString.fromJavaStringUncached(value, JAVA_SCRIPT_STRING_ENCODING); + } + + public static TruffleString fromJavaString(String value, TruffleString.FromJavaStringNode fromJavaStringNode) { + return fromJavaStringNode.execute(value, JAVA_SCRIPT_STRING_ENCODING); + } + + public static boolean equals(TruffleString s1, TruffleString s2, + TruffleString.EqualNode equalNode) { + return equalNode.execute(s1, s2, JAVA_SCRIPT_STRING_ENCODING); + } + + public static int length(TruffleString truffleString, TruffleString.CodePointLengthNode lengthNode) { + return lengthNode.execute(truffleString, JAVA_SCRIPT_STRING_ENCODING); + } + + public static TruffleString concat(TruffleString s1, TruffleString s2, TruffleString.ConcatNode concatNode) { + return concatNode.execute(s1, s2, JAVA_SCRIPT_STRING_ENCODING, true); + } + + public static TruffleString substring(TruffleString truffleString, int index, int length, + TruffleString.SubstringNode substringNode) { + return substringNode.execute(truffleString, index, length, JAVA_SCRIPT_STRING_ENCODING, true); + } + + public static boolean same(Object o1, Object o2) { + return o1 == o2; + } + + @TruffleBoundary + public static String concatToStrings(Object o1, Object o2) { + return o1.toString() + o2.toString(); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/FunctionObject.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/FunctionObject.java new file mode 100644 index 00000000..2e139e89 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/FunctionObject.java @@ -0,0 +1,96 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.endoflineblog.truffle.part_13.EasyScriptTypeSystemGen; +import com.endoflineblog.truffle.part_13.exceptions.EasyScriptException; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.FunctionDispatchNode; +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.FunctionDispatchNodeGen; +import com.endoflineblog.truffle.part_13.nodes.exprs.strings.ReadTruffleStringPropertyNode; +import com.oracle.truffle.api.CallTarget; +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.strings.TruffleString; + +/** + * The object that represents a function in EasyScript. + * Almost identical to the class with the same name from part 11, + * the only difference is adding {@link ClassInstanceObject} + * to the list of allowed EasyScript values when calling this function through the GraalVM polyglot API. + */ +@ExportLibrary(InteropLibrary.class) +public final class FunctionObject implements TruffleObject { + public final CallTarget callTarget; + public final int argumentCount; + + /** + * The target of the method represented by this object. + * If this object represents a function, + * this will be {@code null}. + * If this object represents a method, + * this field will store the object that method was invoked on + * (in this part of the series, that will always be a {@link TruffleString}, + * since we don't support methods that can refer to {@code this} + * in user-defined classes yet). + * This field is read by the {@link FunctionDispatchNode function dispatch Node}, + * and by the Node that reads properties of {@link TruffleString}s. + * + * @see FunctionDispatchNode + * @see ReadTruffleStringPropertyNode + */ + public final Object methodTarget; + + private final FunctionDispatchNode functionDispatchNode; + + public FunctionObject(CallTarget callTarget, int argumentCount) { + this(callTarget, argumentCount, null); + } + + public FunctionObject(CallTarget callTarget, int argumentCount, + Object methodTarget) { + this.callTarget = callTarget; + this.argumentCount = argumentCount; + this.methodTarget = methodTarget; + this.functionDispatchNode = FunctionDispatchNodeGen.create(); + } + + /** + * Returns the string representation of a given function. + * In JavaScript, this returns the actual code of a given function (!). + * We'll simplify in EasyScript, and just return the string {@code "[object Function]"}. + */ + @Override + public String toString() { + return "[object Function]"; + } + + @ExportMessage + boolean isExecutable() { + return true; + } + + @ExportMessage + Object execute(Object[] arguments) { + // we have to make sure the given arguments are valid EasyScript values, + // as this class can be invoked from other languages, like Java + for (Object argument : arguments) { + if (!this.isEasyScriptValue(argument)) { + throw new EasyScriptException("'" + argument + "' is not an EasyScript value"); + } + } + return this.functionDispatchNode.executeDispatch(this, arguments); + } + + private boolean isEasyScriptValue(Object argument) { + // as of this chapter, the only available types in EasyScript are + // numbers (ints and doubles), booleans, 'undefined', functions, and strings + return EasyScriptTypeSystemGen.isImplicitDouble(argument) || + EasyScriptTypeSystemGen.isBoolean(argument) || + argument == Undefined.INSTANCE || + argument instanceof ArrayObject || + argument instanceof TruffleString || + argument instanceof String || + argument instanceof ClassInstanceObject || + argument instanceof FunctionObject; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/GlobalScopeObject.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/GlobalScopeObject.java new file mode 100644 index 00000000..e0fb5b65 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/GlobalScopeObject.java @@ -0,0 +1,89 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.endoflineblog.truffle.part_13.EasyScriptTruffleLanguage; +import com.oracle.truffle.api.TruffleLanguage; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.object.DynamicObject; +import com.oracle.truffle.api.object.DynamicObjectLibrary; +import com.oracle.truffle.api.object.Shape; + +/** + * This is the Truffle interop object that represents the global-level scope + * that contains all global variables. + * Identical to the class with the same name from part 11. + */ +@ExportLibrary(InteropLibrary.class) +public final class GlobalScopeObject extends DynamicObject { + public GlobalScopeObject(Shape shape) { + super(shape); + } + + @ExportMessage + boolean isScope() { + return true; + } + + @ExportMessage + boolean hasMembers() { + return true; + } + + @ExportMessage + boolean isMemberReadable(String member, + @CachedLibrary("this") DynamicObjectLibrary objectLibrary) { + return objectLibrary.containsKey(this, member); + } + + @ExportMessage + Object getMembers(@SuppressWarnings("unused") boolean includeInternal, + @CachedLibrary("this") DynamicObjectLibrary objectLibrary) { + return new MemberNamesObject(objectLibrary.getKeyArray(this)); + } + + @ExportMessage + Object readMember(String member, + @CachedLibrary("this") DynamicObjectLibrary objectLibrary) throws UnknownIdentifierException { + Object value = objectLibrary.getOrDefault(this, member, null); + if (null == value) { + throw UnknownIdentifierException.create(member); + } + return value; + } + + @ExportMessage + boolean isMemberModifiable(String member, + @CachedLibrary("this") DynamicObjectLibrary objectLibrary) { + return objectLibrary.containsKey(this, member); + } + + @ExportMessage + boolean isMemberInsertable(String member, + @CachedLibrary("this") DynamicObjectLibrary objectLibrary) { + return !objectLibrary.containsKey(this, member); + } + + @ExportMessage + void writeMember(String member, Object value, + @CachedLibrary("this") DynamicObjectLibrary objectLibrary) { + objectLibrary.put(this, member, value); + } + + @ExportMessage + Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) { + return "global"; + } + + @ExportMessage + boolean hasLanguage() { + return true; + } + + @ExportMessage + Class> getLanguage() { + return EasyScriptTruffleLanguage.class; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/MathObject.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/MathObject.java new file mode 100644 index 00000000..88f8b01e --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/MathObject.java @@ -0,0 +1,70 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.endoflineblog.truffle.part_13.EasyScriptTruffleLanguage; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.staticobject.DefaultStaticProperty; +import com.oracle.truffle.api.staticobject.StaticProperty; +import com.oracle.truffle.api.staticobject.StaticShape; + +/** + * A Truffle static object that implements the built-in + * {@code Math} JavaScript object. + * Surfaces two properties, {@code abs} and {@code pow}, + * which are built-in functions that any EasyScript program can call. + * Identical to the class with the same name from part 11. + */ +@ExportLibrary(InteropLibrary.class) +public final class MathObject implements TruffleObject { + public static MathObject create(EasyScriptTruffleLanguage language, + FunctionObject absFunction, FunctionObject powFunction) { + StaticShape.Builder shapeBuilder = StaticShape.newBuilder(language); + StaticProperty absProp = new DefaultStaticProperty("abs"); + StaticProperty powProp = new DefaultStaticProperty("pow"); + Object staticObject = shapeBuilder + .property(absProp, Object.class, true) + .property(powProp, Object.class, true) + .build() + .getFactory().create(); + absProp.setObject(staticObject, absFunction); + powProp.setObject(staticObject, powFunction); + return new MathObject(staticObject, absProp, powProp); + } + + private final Object targetObject; + private final StaticProperty absProp; + private final StaticProperty powProp; + + private MathObject(Object targetObject, StaticProperty absProp, StaticProperty powProp) { + this.targetObject = targetObject; + this.absProp = absProp; + this.powProp = powProp; + } + + @ExportMessage + boolean hasMembers() { + return true; + } + + @ExportMessage + boolean isMemberReadable(String member) { + return "abs".equals(member) || "pow".equals(member); + } + + @ExportMessage + Object readMember(String member) throws UnknownIdentifierException { + switch (member) { + case "abs": return this.absProp.getObject(this.targetObject); + case "pow": return this.powProp.getObject(this.targetObject); + default: throw UnknownIdentifierException.create(member); + } + } + + @ExportMessage + Object getMembers(@SuppressWarnings("unused") boolean includeInternal) { + return new MemberNamesObject(new String[]{"abs", "pow"}); + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/MemberNamesObject.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/MemberNamesObject.java new file mode 100644 index 00000000..68ea68ba --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/MemberNamesObject.java @@ -0,0 +1,43 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.InvalidArrayIndexException; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; + +/** + * The class that implements a simple collection of member names of a {@link TruffleObject}. + * Identical to the class with the same name from part 11. + */ +@ExportLibrary(InteropLibrary.class) +final class MemberNamesObject implements TruffleObject { + private final Object[] names; + + MemberNamesObject(Object[] names) { + this.names = names; + } + + @ExportMessage + boolean hasArrayElements() { + return true; + } + + @ExportMessage + long getArraySize() { + return this.names.length; + } + + @ExportMessage + boolean isArrayElementReadable(long index) { + return index >= 0 && index < this.names.length; + } + + @ExportMessage + Object readArrayElement(long index) throws InvalidArrayIndexException { + if (!this.isArrayElementReadable(index)) { + throw InvalidArrayIndexException.create(index); + } + return this.names[(int) index]; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/StringPrototype.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/StringPrototype.java new file mode 100644 index 00000000..ea2e87a7 --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/StringPrototype.java @@ -0,0 +1,22 @@ +package com.endoflineblog.truffle.part_13.runtime; + +import com.endoflineblog.truffle.part_13.nodes.exprs.functions.built_in.methods.CharAtMethodBodyExprNode; +import com.oracle.truffle.api.CallTarget; + +/** + * The object containing the {@code CallTarget}s + * for the built-in methods of strings. + * Identical to the class with the same name from part 11. + */ +public final class StringPrototype { + /** + * The {@link CallTarget} for the {@code charAt()} string method. + * + * @see CharAtMethodBodyExprNode + */ + public final CallTarget charAtMethod; + + public StringPrototype(CallTarget charAtMethod) { + this.charAtMethod = charAtMethod; + } +} diff --git a/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/Undefined.java b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/Undefined.java new file mode 100644 index 00000000..a27baa9e --- /dev/null +++ b/part-13/src/main/java/com/endoflineblog/truffle/part_13/runtime/Undefined.java @@ -0,0 +1,33 @@ +package com.endoflineblog.truffle.part_13.runtime; + +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; + +/** + * The class that represents the 'undefined' value in JavaScript. + * Identical to the class with the same name from part 11. + */ +@ExportLibrary(InteropLibrary.class) +public final class Undefined implements TruffleObject { + public static final Undefined INSTANCE = new Undefined(); + + private Undefined() { + } + + @ExportMessage + boolean isNull() { + return true; + } + + @ExportMessage + Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) { + return this.toString(); + } + + @Override + public String toString() { + return "undefined"; + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/ArraysTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/ArraysTest.java new file mode 100644 index 00000000..b8abfc57 --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/ArraysTest.java @@ -0,0 +1,223 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a set of unit tests for testing support for arrays in EasyScript. + */ +public class ArraysTest { + private Context context; + + @BeforeEach + public void setUp() { + this.context = Context.create(); + } + + @AfterEach + public void tearDown() { + this.context.close(); + } + + @Test + public void array_literal_is_polyglot_value() { + Value result = this.context.eval("ezs", + "[11]" + ); + assertTrue(result.hasArrayElements()); + assertEquals(1, result.getArraySize()); + assertEquals(11, result.getArrayElement(0).asInt()); + } + + @Test + public void array_can_be_indexed() { + Value result = this.context.eval("ezs", "" + + "const arr = [3, 6]; " + + "arr[0] + arr[1]" + ); + assertEquals(9, result.asInt()); + } + + @Test + public void reading_an_out_of_bound_array_index_returns_undefined() { + Value result = this.context.eval("ezs", + "[1, 9][2]" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void reading_a_double_array_index_returns_undefined() { + Value result = this.context.eval("ezs", + "[1, 9][0.5]" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void reading_a_negative_array_index_returns_undefined() { + Value result = this.context.eval("ezs", + "[1, 9][-3]" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void reading_a_non_number_array_index_returns_undefined() { + Value result = this.context.eval("ezs", + "[1, 9][Math.pow]" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void indexing_a_non_array_returns_undefined() { + Value result = this.context.eval("ezs", + "3[1]" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void index_in_array_can_be_written_to() { + Value result = this.context.eval("ezs", "" + + "let a = [9]; " + + "a[0] = 45; " + + "a[0]" + ); + assertEquals(45, result.asInt()); + } + + @Test + public void index_beyond_array_size_can_be_assigned_and_fills_array_with_undefined() { + Value array = this.context.eval("ezs", "" + + "let a = [9]; " + + "a[2] = 45; " + + "a" + ); + + assertEquals(9, array.getArrayElement(0).asInt()); + + Value valueAtIndex1 = array.getArrayElement(1); + assertTrue(valueAtIndex1.isNull()); + assertEquals("undefined", valueAtIndex1.toString()); + + assertEquals(45, array.getArrayElement(2).asInt()); + + assertEquals(3, array.getArraySize()); + } + + @Test + public void non_int_indexes_are_ignored_on_write() { + Value result = this.context.eval("ezs", + "[1][Math.abs] = 45; " + ); + + assertEquals(45, result.asInt()); + } + + @Test + public void negative_indexes_are_ignored_on_write() { + Value array = this.context.eval("ezs", "" + + "let a = [9]; " + + "a[-1] = 45; " + + "a" + ); + + assertEquals(1, array.getArraySize()); + assertEquals(9, array.getArrayElement(0).asInt()); + assertFalse(array.hasMember("-1")); + } + + @Test + public void non_stable_array_reads_work_correctly() { + Value result = this.context.eval("ezs", "" + + "function readFirstArrayEl(array) { " + + " return array[0]; " + + "} " + + "function makeOneElArray() {" + + " return [123]; " + + "} " + + "readFirstArrayEl(1); " + + "readFirstArrayEl(2); " + + "readFirstArrayEl(makeOneElArray()); " + + "readFirstArrayEl(makeOneElArray()); " + + "readFirstArrayEl(makeOneElArray()); " + + "readFirstArrayEl(3); " + + "readFirstArrayEl(makeOneElArray()); " + + "readFirstArrayEl(makeOneElArray()); " + + "readFirstArrayEl(makeOneElArray()); " + + "readFirstArrayEl(makeOneElArray()); " + + "readFirstArrayEl(makeOneElArray()); " + + "readFirstArrayEl(makeOneElArray()); " + ); + + assertEquals(123, result.asInt()); + } + + @Test + public void array_properties_can_be_accessed_with_string_indexes() { + Value result = this.context.eval("ezs", "" + + "[1, 2, 3]['length']" + ); + + assertEquals(3, result.asInt()); + } + + @Test + public void an_array_can_be_passed_to_a_function_exec() { + this.context.eval("ezs", "" + + "let array = [1, 2, 3]; " + + "function secondIndex(arr) { " + + " return arr[1]; " + + "}" + ); + + Value globalVariables = this.context.getBindings("ezs"); + Value array = globalVariables.getMember("array"); + Value secondIndex = globalVariables.getMember("secondIndex"); + assertEquals(2, secondIndex.execute(array).asInt()); + } + + @Test + public void reading_an_index_of_undefined_is_an_error() { + try { + this.context.eval("ezs", + "undefined[0];" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Cannot read properties of undefined (reading '0')", e.getMessage()); + } + } + + @Test + public void writing_an_index_of_undefined_is_an_error() { + try { + this.context.eval("ezs", + "undefined[0] = 3;" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Cannot set properties of undefined (setting '0')", e.getMessage()); + } + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/ClassesTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/ClassesTest.java new file mode 100644 index 00000000..a46d7d86 --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/ClassesTest.java @@ -0,0 +1,222 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a set of unit tests for validating support for user-defined classes in EasyScript. + */ +public class ClassesTest { + private Context context; + + @BeforeEach + void setUp() { + this.context = Context.create(); + } + + @AfterEach + void tearDown() { + this.context.close(); + } + + @Test + void class_declaration_creates_object() { + Value result = this.context.eval("ezs", "" + + "class A { } " + + "A;"); + + assertEquals("[class A]", result.toString()); + assertFalse(result.hasMembers()); + } + + @Test + void class_can_be_instantiated() { + Value result = this.context.eval("ezs", "" + + "class A { " + + " a() { " + + " return 'A.a'; " + + " } " + + "} " + + "new A;"); + + assertTrue(result.hasMembers()); + assertEquals(Set.of("a"), result.getMemberKeys()); + assertTrue(result.hasMember("a")); + Value methodA = result.getMember("a"); + assertTrue(methodA.canExecute()); + assertEquals("A.a", methodA.execute().asString()); + } + + @Test + void methods_can_be_called_on_class_instances() { + Value result = this.context.eval("ezs", "" + + "class A { " + + " a() { " + + " return 'A.a'; " + + " } " + + "} " + + "new A.a();"); + + assertEquals("A.a", result.asString()); + } + + @Test + void classes_can_be_reassigned() { + Value result = this.context.eval("ezs", "" + + "class A { } " + + "A = 5; " + + "A;"); + + assertEquals(5, result.asInt()); + } + + @Test + void classes_and_objects_have_to_string() { + Value result = this.context.eval("ezs", "" + + "class Class { } " + + "let c = new Class; " + + "c + Class;"); + + assertEquals("[object Object][class Class]", result.asString()); + } + + @Test + void arguments_passed_to_new_are_evaluated() { + Value result = this.context.eval("ezs", "" + + "class Class { }; " + + "let l = 3; " + + "new Class(l = 5); " + + "l;"); + + assertEquals(5, result.asInt()); + } + + @Test + void duplicate_methods_override_previous_ones() { + Value result = this.context.eval("ezs", "" + + "class Class { " + + " c() { return 1; } " + + " c() { return 2; } " + + "} " + + "new Class().c();"); + + assertEquals(2, result.asInt()); + } + + @Test + public void class_instances_can_be_used_as_function_arguments() { + this.context.eval("ezs", "" + + "class M { " + + " m(a) { " + + " return a + 1; " + + " } " + + "} " + + "function invokeM(target, argument) { " + + " return target.m(argument); " + + "} " + + "let m = new M; " + ); + Value ezsBindings = this.context.getBindings("ezs"); + Value m = ezsBindings.getMember("m"); + Value invokeM = ezsBindings.getMember("invokeM"); + + assertEquals(14, invokeM.execute(m, 13).asInt()); + } + + @Test + void benchmark_with_alloc_inside_loop_returns_input() { + Value result = this.context.eval("ezs", "" + + "class Adder { " + + " add(a, b) { " + + " return a + b; " + + " } " + + "} " + + "function countForMethodPropAllocInsideLoop(n) { " + + " var ret = 0; " + + " for (let i = 0; i < n; i = i + 1) { " + + " ret = new Adder().add(ret, 1); " + + " } " + + " return ret; " + + "} " + + "countForMethodPropAllocInsideLoop(" + 1_000 + ");" + ); + + assertEquals(1_000, result.asInt()); + } + + @Test + void benchmark_with_alloc_outside_loop_returns_input() { + Value result = this.context.eval("ezs", "" + + "class Adder { " + + " add(a, b) { " + + " return a + b; " + + " } " + + "} " + + "function countForMethodPropAllocOutsideLoop(n) { " + + " var ret = 0; " + + " const adder = new Adder(); " + + " for (let i = 0; i < n; i = i + 1) { " + + " ret = adder['add'](ret, 1); " + + " } " + + " return ret; " + + "} " + + "countForMethodPropAllocOutsideLoop(" + 1_000 + ");" + ); + + assertEquals(1_000, result.asInt()); + } + + @Test + void classes_cannot_be_defined_inside_functions() { + try { + this.context.eval("ezs", "" + + "function f() { " + + " class Class { } " + + "} "); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("classes nested in functions are not supported in EasyScript", e.getMessage()); + } + } + + @Test + void new_with_non_class_is_an_error() { + try { + this.context.eval("ezs", + "new 3();"); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'3' is not a constructor", e.getMessage()); + } + } + + @Test + void duplicate_class_declarations_are_an_error() { + try { + this.context.eval("ezs", "" + + "class A { } " + + "class A { }" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Identifier 'A' has already been declared", e.getMessage()); + } + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/ControlFlowTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/ControlFlowTest.java new file mode 100644 index 00000000..1c3ba52c --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/ControlFlowTest.java @@ -0,0 +1,259 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** This is a set of unit tests for control flow. */ +public class ControlFlowTest { + private Context context; + + @BeforeEach + public void setUp() { + this.context = Context.create(); + } + + @AfterEach + public void tearDown() { + this.context.close(); + } + + @Test + public void var_declarations_are_local_in_nested_blocks() { + Value result = this.context.eval("ezs", + "var v = 3; " + + "{ " + + "var v = 5; " + + "} " + + "v" + ); + assertEquals(3, result.asInt()); + } + + @Test + public void var_declarations_are_local_in_nested_blocks_of_functions() { + Value result = this.context.eval("ezs", + "function f() { " + + "var v = 3; " + + "{ " + + "var v = 5; " + + "} " + + "return v; " + + "} " + + "f() " + ); + assertEquals(3, result.asInt()); + } + + @Test + public void a_function_is_equal_to_itself_but_not_lte() { + this.context.eval("ezs", + "function f() { return false; } " + + "var t1 = f === f; " + + "let f1 = f < f; " + + "var f2 = f <= f; " + ); + Value bindings = this.context.getBindings("ezs"); + assertTrue(bindings.getMember("t1").asBoolean()); + assertFalse(bindings.getMember("f1").asBoolean()); + assertFalse(bindings.getMember("f2").asBoolean()); + } + + @Test + public void if_in_a_function_works() { + this.context.eval("ezs", + "function sig(n) {" + + " if (n < 0) return -1; " + + " else if (n > 0) return 1; " + + " else return 0; " + + "} " + + "var s1 = sig(34); " + + "var s2 = sig(0); " + + "var s3 = sig(-12); " + ); + Value bindings = this.context.getBindings("ezs"); + assertEquals(1, bindings.getMember("s1").asInt()); + assertEquals(0, bindings.getMember("s2").asInt()); + assertEquals(-1, bindings.getMember("s3").asInt()); + } + + @Test + public void iterative_fibonacci_works() { + Value result = this.context.eval("ezs", + "function fib(n) { " + + " if (n < 2) { " + + " return n; " + + " } " + + " let a = 0, b = 1, i = 2; " + + " while (i <= n) { " + + " let f = a + b; " + + " a = b; " + + " b = f; " + + " i = i + 1; " + + " } " + + " return b; " + + "} " + + "fib(7)" + ); + assertEquals(13, result.asInt()); + } + + @Test + public void do_while_always_executes_at_least_once() { + Value result = this.context.eval("ezs", + "function f(n) { " + + " let ret = n + 2; " + + " do { " + + " ret = n + 4; " + + " } while (false); " + + " return ret; " + + "} " + + "f(8)" + ); + assertEquals(12, result.asInt()); + } + + @Test + public void for_parts_are_all_optional() { + Value result = this.context.eval("ezs", + "function fib(n) { " + + " if (n < 2) { " + + " return n; " + + " } " + + " let a = 0, b = 1, i = 2; " + + " for (;;) { " + + " let f = a + b; " + + " a = b; " + + " b = f; " + + " i = i + 1; " + + " if (i > n) " + + " break; " + + " else " + + " continue; " + + " } " + + " return b; " + + "} " + + "fib(8)" + ); + assertEquals(21, result.asInt()); + } + + @Test + public void for_loop_executes_as_expected() { + Value result = this.context.eval("ezs", + "function fib(n) { " + + " if (n < 2) { " + + " return n; " + + " } " + + " var a = 0, b = 1; " + + " for (let i = 2; i <= n; i = i + 1) { " + + " const f = a + b; " + + " a = b; " + + " b = f; " + + " } " + + " return b; " + + "} " + + "fib(6)" + ); + assertEquals(8, result.asInt()); + } + + @Test + public void recursive_fibonacci_works() { + Value result = this.context.eval("ezs", + "function fib(n) { " + + " if (n > -2) { " + + " return Math.abs(n); " + + " } " + + " return fib(n + 1) + fib(n + 2); " + + "} " + + "fib(-9)" + ); + assertEquals(34, result.asInt()); + } + + @Test + public void recursive_fibonacci_with_subtraction_works() { + Value result = this.context.eval("ezs", "" + + "function fib(n) { " + + " if (n < 2) { " + + " return n; " + + " } " + + " return fib(n - 1) + fib(n - 2); " + + "} " + + "fib(9)" + ); + assertEquals(34, result.asInt()); + } + + @Test + public void if_statement_returns_value() { + Value result = this.context.eval("ezs", + "if (true) { " + + " 42; " + + "}" + ); + assertTrue(result.fitsInInt()); + assertEquals(42, result.asInt()); + } + + @Test + public void return_statement_is_not_allowed_on_top_level() { + try { + this.context.eval("ezs", + "return;" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("return statement is not allowed outside functions", e.getMessage()); + } + } + + @Test + public void negative_recursive_fibonacci_is_correct() { + Source fibProgram = Source.create("ezs", "" + + "function fib(n) { " + + " if (n > -2) { " + + " return Math.abs(n); " + + " } " + + " return fib(n + 1) + fib(n + 2); " + + "} " + + "fib(-20) " + + ""); + Value fibProgramValue = this.context.parse(fibProgram); + assertEquals(6765, fibProgramValue.execute().asInt()); + } + + @Test + public void functions_are_redefined_on_subsequent_evals() { + String program = "" + + "function f() { " + + " return 5; " + + "} " + + "var sum = 0; " + + "for (let i = 0; i <= 3; i = i + 1) { " + + " if (i === 3) { " + + " function f() { return 1; } " + + " } " + + " sum = sum + f(); " + + "} " + + "sum"; + + Value firstEvalResult = this.context.eval("ezs", program); + assertEquals(16, firstEvalResult.asInt()); + + Value secondEvalResult = this.context.eval("ezs", program); + assertEquals(16, secondEvalResult.asInt()); + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/FunctionDefinitionsTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/FunctionDefinitionsTest.java new file mode 100644 index 00000000..ae73f1b8 --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/FunctionDefinitionsTest.java @@ -0,0 +1,315 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** This is a set of unit tests for defining functions. */ +public class FunctionDefinitionsTest { + private Context context; + + @BeforeEach + public void setUp() { + this.context = Context.create(); + } + + @AfterEach + public void tearDown() { + this.context.close(); + } + + @Test + public void defining_a_function_works() { + Value result = this.context.eval("ezs", + "function f() { return Math.pow(4, 3); }" + + "f()" + ); + assertEquals(64, result.asInt()); + } + + @Test + public void return_without_expression_returns_undefined() { + Value result = this.context.eval("ezs", + "function f() { return; }" + + "f()" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void no_return_in_function_results_in_undefined() { + Value result = this.context.eval("ezs", "" + + "function f() { 5; }" + + "f()" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void cycle_between_var_and_function_does_not_work() { + try { + this.context.eval("ezs", + "var v = f();" + + "function f() {" + + "return v;" + + "}" + + "v" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'v' is not defined", e.getMessage()); + } + } + + @Test + public void cycle_between_let_and_function_does_not_work() { + try { + this.context.eval("ezs", + "let v = f(); " + + "function f() { " + + "return v; " + + "} " + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'v' is not defined", e.getMessage()); + } + } + + @Test + public void passing_a_parameter_to_a_function_works() { + Value result = this.context.eval("ezs", + "function addOne(a) {" + + "return a + 1; " + + "} " + + "addOne(4)" + ); + assertEquals(5, result.asInt()); + } + + @Test + public void functions_can_have_local_variables() { + Value result = this.context.eval("ezs", "" + + "function addOne(a) { " + + " let res = a + 1; " + + " return res; " + + "} " + + "addOne(4)" + ); + assertEquals(5, result.asInt()); + } + + @Test + public void function_parameters_shadow_each_other() { + Value result = this.context.eval("ezs", + "function f(a, a) { " + + "return a; " + + "} " + + "f(1, 23);" + ); + assertEquals(23, result.asInt()); + } + + @Test + public void local_variables_shadow_globals() { + Value result = this.context.eval("ezs", + "const a = 33; " + + "function f() { " + + "var a = 3; " + + "a = 333; " + + "return a;" + + "} " + + "f()" + ); + assertEquals(333, result.asInt()); + } + + @Test + public void function_arguments_shadow_globals() { + Value result = this.context.eval("ezs", "" + + "const a = 33; " + + "function f(a) { " + + " return a;" + + "} " + + "f(22)" + ); + assertEquals(22, result.asInt()); + } + + @Test + public void function_parameters_can_be_reassigned() { + Value result = this.context.eval("ezs", + "let a = 222; " + + "function f(a, b) { " + + "b = 22; " + + "return b; " + + "} " + + "f(2);" + ); + assertEquals(22, result.asInt()); + } + + @Test + public void functions_can_be_redefined() { + Value result = this.context.eval("ezs", + "function f() { return false; } " + + "function f() { return true; } " + + "f(); " + ); + assertTrue(result.asBoolean()); + } + + @Test + public void local_variables_shadow_globals_only_from_their_declaration() { + Value result = this.context.eval("ezs", + "const b = 5; " + + "function f() { " + + "const a = b; " + + "var b = 3; " + + "return a; " + + "} " + + "f();" + ); + assertEquals(5, result.asInt()); + } + + @Test + public void higher_order_functions_are_supported() { + Value result = this.context.eval("ezs", + "function f() { return 5; } " + + "function g(a) { " + + "return 1 + a(); " + + "} " + + "g(f);" + ); + assertEquals(result.asInt(), 6); + } + + @Test + public void const_local_variables_cannot_be_reassigned() { + try { + this.context.eval("ezs", + "function f() { " + + "const a = 5; " + + "a = 10; " + + "}" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Assignment to constant variable 'a'", e.getMessage()); + } + } + + @Test + public void cannot_use_a_let_local_variable_before_its_declaration() { + try { + this.context.eval("ezs", + "function f() { " + + "const a = b; " + + "let b = 10; " + + "} " + + "f()" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'b' is not defined", e.getMessage()); + } + } + + @Test + public void var_cannot_override_a_function() { + try { + this.context.eval("ezs", + "var f = 5; " + + "function f() { " + + "return 6; " + + "}" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Identifier 'f' has already been declared", e.getMessage()); + } + } + + @Test + public void duplicate_vars_in_a_function_cause_an_error() { + try { + this.context.eval("ezs", + "function f() { " + + "var a = 1; " + + "var a = 2; " + + "}" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Identifier 'a' has already been declared", e.getMessage()); + } + } + + @Test + public void var_shadowing_a_function_argument_is_not_allowed() { + try { + this.context.eval("ezs", + "function f(a) { " + + "var a = 1; " + + "}" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Identifier 'a' has already been declared", e.getMessage()); + } + } + + @Test + public void nested_functions_are_unsupported() { + try { + this.context.eval("ezs", + "function outer() { " + + "function inner() { " + + "} " + + "}" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("nested functions are not supported in EasyScript yet", e.getMessage()); + } + } + + @Test + public void functions_can_be_reassigned_as_simple_values() { + Value result = this.context.eval("ezs", "" + + "function f() { " + + "} " + + "f = 4; " + + "f" + ); + + assertEquals(4, result.asInt()); + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/GlobalVariablesTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/GlobalVariablesTest.java new file mode 100644 index 00000000..f7da4e4c --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/GlobalVariablesTest.java @@ -0,0 +1,205 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class GlobalVariablesTest { + private Context context; + + @BeforeEach + public void setUp() { + this.context = Context.create(); + } + + @AfterEach + public void tearDown() { + this.context.close(); + } + + @Test + public void executing_list_of_statements_returns_the_last_ones_value() { + Value result = this.context.eval("ezs", + "var a = 1; " + + "let b = 2 + 3; " + + "const c = 4 + 5.0; " + + "(a = a + b + a) + a" + ); + + assertEquals(14, result.asInt()); + + Value globalBindings = this.context.getBindings("ezs"); + assertFalse(globalBindings.isNull()); + assertTrue(globalBindings.hasMembers()); + assertTrue(globalBindings.hasMember("a")); + Value a = globalBindings.getMember("a"); + assertEquals(7, a.asInt()); + assertTrue(globalBindings.hasMember("b")); + assertTrue(globalBindings.hasMember("c")); + } + + @Test + public void variable_declaration_statement_returns_undefined() { + Value result = this.context.eval("ezs", + "const $_ = 1;" + ); + + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void global_variables_are_saved_between_executions() { + this.context.eval("ezs", + "var a = 1; " + + "let b = 2; " + + "const c = 3.0; " + ); + Value result = this.context.eval("ezs", "a + b + c;"); + + assertEquals(6.0, result.asDouble(), 0.0); + } + + @Test + public void variables_without_initializers_have_undefined_value() { + Value result = this.context.eval("ezs", + "let a; " + + "a" + ); + + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void addition_with_undefined_returns_nan() { + Value result = this.context.eval("ezs", + "var a, b = 3; " + + "a + b" + ); + + assertTrue(result.fitsInDouble()); + assertTrue(Double.isNaN(result.asDouble())); + } + + @Test + public void using_a_variable_before_it_is_defined_causes_an_error() { + try { + this.context.eval("ezs", + "let b = a; " + + "var a = 3; " + + "b" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'a' is not defined", e.getMessage()); + } + } + + @Test + public void reassigning_a_const_causes_an_error() { + try { + this.context.eval("ezs", + "const a = undefined, b = a; " + + "a = b" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Assignment to constant variable 'a'", e.getMessage()); + } + } + + @Test + public void const_variables_must_have_an_initializer() { + try { + this.context.eval("ezs", + "const a;" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Missing initializer in const declaration 'a'", e.getMessage()); + } + } + + @Test + public void duplicate_variable_causes_an_error() { + try { + this.context.eval("ezs", + "var a = 1; "+ + "let a = 2" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Identifier 'a' has already been declared", e.getMessage()); + } + } + + @Test + public void referencing_an_undeclared_variable_causes_an_error() { + try { + this.context.eval("ezs", "a"); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'a' is not defined", e.getMessage()); + } + } + + @Test + public void assigning_to_an_undeclared_variable_causes_an_error() { + try { + this.context.eval("ezs", "a = 1"); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'a' is not defined", e.getMessage()); + } + } + + @Test + public void using_a_variable_in_its_own_definition_causes_an_error() { + try { + this.context.eval("ezs", "let x = x"); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'x' is not defined", e.getMessage()); + } + } + + @Test + public void const_variables_can_be_re_evaluated() { + String program = "const a = 3; a"; + this.context.eval("ezs", program); + Value result = this.context.eval("ezs", program); + + assertEquals(3, result.asInt()); + } + + @Test + public void parsing_a_large_integer_falls_back_to_double() { + // this is 9,876,543,210 + Value result = this.context.eval("ezs", "9876543210"); + + assertEquals(9_876_543_210D, result.asDouble(), 0.0); + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/ParsingTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/ParsingTest.java new file mode 100644 index 00000000..be3e1ea2 --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/ParsingTest.java @@ -0,0 +1,39 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * This is a set of unit tests for validating parsing edge cases + * (mainly relating to the {@code new} operator) in EasyScript. + */ +public class ParsingTest { + private Context context; + + @BeforeEach + public void setUp() { + this.context = Context.create(); + } + + @AfterEach + public void tearDown() { + this.context.close(); + } + + @Test + public void multiple_calls_of_calls_are_parsed() { + this.context.parse("ezs", "a()()()"); + } + + @Test + public void property_reads_of_property_reads_are_parsed() { + this.context.parse("ezs", "a.b.c"); + } + + @Test + public void method_calls_of_calls_are_parsed() { + this.context.parse("ezs", "a().b().c()"); + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/PropertiesTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/PropertiesTest.java new file mode 100644 index 00000000..e69ae966 --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/PropertiesTest.java @@ -0,0 +1,96 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a set of unit tests for testing support for (read-only) + * properties in EasyScript. + */ +public class PropertiesTest { + private Context context; + + @BeforeEach + public void setUp() { + this.context = Context.create(); + } + + @AfterEach + public void tearDown() { + this.context.close(); + } + + @Test + public void array_has_length_property() { + Value result = this.context.eval("ezs", + "[1, 2, 3].length" + ); + assertEquals(3, result.asInt()); + } + + @Test + public void bubble_sort_changes_array_to_sorted() { + Value result = this.context.eval("ezs", "" + + "const array = [4, 3, 2, 1]; " + + "function bubbleSort(array) { " + + " for (var i = 0; i < array.length - 1; i = i + 1) { " + + " for (var j = 0; j < array.length - 1 - i; j = j + 1) { " + + " if (array[j] > array[j + 1]) { " + + " var tmp = array[j]; " + + " array[j] = array[j + 1]; " + + " array[j + 1] = tmp; " + + " } " + + " } " + + " } " + + "} " + + "bubbleSort(array); " + + "array" + ); + + assertEquals(1, result.getArrayElement(0).asInt()); + assertEquals(2, result.getArrayElement(1).asInt()); + assertEquals(3, result.getArrayElement(2).asInt()); + assertEquals(4, result.getArrayElement(3).asInt()); + } + + @Test + public void reading_a_property_of_undefined_is_an_error() { + try { + this.context.eval("ezs", + "undefined.abc;" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Cannot read properties of undefined (reading 'abc')", e.getMessage()); + } + } + + @Test + public void non_existent_array_property_returns_undefined() { + Value result = this.context.eval("ezs", + "[1, 2, 3].abc" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void any_property_of_integer_returns_undefined() { + Value result = this.context.eval("ezs", + "1.toString" + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/StaticFunctionCallsTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/StaticFunctionCallsTest.java new file mode 100644 index 00000000..66fda2b9 --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/StaticFunctionCallsTest.java @@ -0,0 +1,215 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class StaticFunctionCallsTest { + private Context context; + + @BeforeEach + public void setUp() { + this.context = Context.create(); + } + + @AfterEach + public void tearDown() { + this.context.close(); + } + + @Test + public void calling_Math_abs_works() { + Value result = this.context.eval("ezs", + "Math.abs(-2)" + ); + + assertEquals(2, result.asInt()); + } + + @Test + public void Math_abs_correctly_handles_min_int() { + Value result = this.context.eval("ezs", + // if we just use Integer.MIN_VALUE, that will overflow int, + // as EasyScript parses it as two expressions, + // negation and an int literal + "Math.abs(" + (Integer.MIN_VALUE + 1) + " + (-1))" + ); + + assertEquals(Integer.MAX_VALUE + 1D, result.asDouble(), 0.0); + } + + @Test + public void calling_a_function_with_extra_arguments_ignores_the_extra_ones() { + Value result = this.context.eval("ezs", + "Math.abs(3, 4);" + ); + + assertEquals(3, result.asInt()); + } + + @Test + public void extra_function_arguments_expressions_are_still_evaluated() { + Value result = this.context.eval("ezs", + "var a = -1; " + + "Math.abs(4, a = 5);" + + "a" + ); + + assertEquals(5, result.asInt()); + } + + @Test + public void calling_a_function_with_less_arguments_assigns_them_undefined() { + Value result = this.context.eval("ezs", + "Math.abs()" + ); + + assertTrue(Double.isNaN(result.asDouble())); + } + + @Test + public void abs_of_a_function_is_nan() { + Value result = this.context.eval("ezs", + "Math.abs(Math.abs)" + ); + + assertTrue(Double.isNaN(result.asDouble())); + } + + @Test + public void negating_a_function_or_undefined_returns_NaN() { + this.context.eval("ezs", + "var uNeg = -undefined;" + + "var fNeg = -Math.abs;" + ); + + Value easyScriptBindings = this.context.getBindings("ezs"); + assertTrue(Double.isNaN(easyScriptBindings.getMember("uNeg").asDouble())); + assertTrue(Double.isNaN(easyScriptBindings.getMember("fNeg").asDouble())); + } + + @Test + public void adding_a_function_turns_into_string_concatenation() { + Value result = this.context.eval("ezs", + "Math.abs + 3" + ); + + assertEquals("[object Function]3", result.asString()); + } + + @Test + public void calling_a_non_function_throws_a_guest_polyglot_exception() { + try { + this.context.eval("ezs", + "1(2)" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'1' is not a function", e.getMessage()); + } + } + + @Test + public void an_EasyScript_function_can_be_called_from_Java() { + Value mathAbs = this.context.eval("ezs", + "Math.abs;" + ); + + assertTrue(mathAbs.canExecute()); + + Value result = mathAbs.execute(-3); + assertEquals(3, result.asInt()); + } + + @Test + public void calling_an_EasyScript_function_with_a_byte_throws_an_exception() { + Value mathAbs = this.context.eval("ezs", + "Math.abs;" + ); + + try { + byte b = -1; + mathAbs.execute(b); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("'-1' is not an EasyScript value", e.getMessage()); + } + } + + @Test + public void calling_Math_pow_works() { + Value result = this.context.eval("ezs", + "Math.pow(2, 3)" + ); + + assertEquals(8, result.asInt()); + } + + @Test + public void Math_pow_with_negative_exponent_works_correctly() { + Value result = this.context.eval("ezs", + "Math.pow(2, -1)" + ); + + assertEquals(0.5, result.asDouble(), 0.0); + } + + @Test + public void Math_pow_correctly_switches_to_double_on_overflow() { + Value result = this.context.eval("ezs", + "Math.pow(2, 35)" + ); + + assertEquals(34_359_738_368D, result.asDouble(), 0.0); + } + + @Test + public void Math_can_be_referenced_by_itself() { + Value result = this.context.eval("ezs", + "Math" + ); + + assertTrue(result.hasMembers()); + assertEquals(2, result.getMemberKeys().size()); // 'abs' and 'pow' functions + } + + @Test + public void Math_is_not_a_legal_variable_name() { + try { + this.context.eval("ezs", + "let Math = -5;" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Identifier 'Math' has already been declared", e.getMessage()); + } + } + + @Test + public void Math_cannot_be_reassigned() { + try { + this.context.eval("ezs", + "Math = -5;" + ); + fail("expected PolyglotException to be thrown"); + } catch (PolyglotException e) { + assertTrue(e.isGuestException()); + assertFalse(e.isInternalError()); + assertEquals("Assignment to constant variable 'Math'", e.getMessage()); + } + } +} diff --git a/part-13/src/test/java/com/endoflineblog/truffle/part_13/StringsTest.java b/part-13/src/test/java/com/endoflineblog/truffle/part_13/StringsTest.java new file mode 100644 index 00000000..b41baa7c --- /dev/null +++ b/part-13/src/test/java/com/endoflineblog/truffle/part_13/StringsTest.java @@ -0,0 +1,333 @@ +package com.endoflineblog.truffle.part_13; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a set of unit tests for testing strings in EasyScript. + */ +public class StringsTest { + private Context context; + + @BeforeEach + public void setUp() { + this.context = Context.newBuilder() + .option("cpusampler", "true") + .build(); + } + + @AfterEach + public void tearDown() { + this.context.close(); + } + + @Test + public void strings_can_be_created_with_single_quotes() { + Value result = this.context.eval("ezs", + " '' " + ); + assertTrue(result.isString()); + assertEquals("", result.asString()); + } + + @Test + public void single_quote_strings_can_contain_a_single_quote_by_escaping_it() { + Value result = this.context.eval("ezs", + " '\\'' " + ); + assertTrue(result.isString()); + assertEquals("'", result.asString()); + } + + @Test + public void strings_can_be_created_with_double_quotes() { + Value result = this.context.eval("ezs", + " \"\" " + ); + assertTrue(result.isString()); + assertEquals("", result.asString()); + } + + @Test + public void double_quote_strings_can_contain_a_double_quote_by_escaping_it() { + Value result = this.context.eval("ezs", + " \"\\\"\" " + ); + assertTrue(result.isString()); + assertEquals("\"", result.asString()); + } + + @Test + public void empty_string_is_falsy() { + Value result = this.context.eval("ezs", "" + + "let ret; " + + "if ('') { " + + " ret = 'empty string is truthy'; " + + "} else { " + + " ret = 'empty string is falsy'; " + + "} " + + "ret" + ); + assertEquals("empty string is falsy", result.asString()); + } + + @Test + public void blank_string_is_truthy() { + Value result = this.context.eval("ezs", "" + + "let ret; " + + "if (' ') { " + + " ret = 'blank string is truthy'; " + + "} else { " + + " ret = 'blank string is falsy'; " + + "} " + + "ret" + ); + assertEquals("blank string is truthy", result.asString()); + } + + @Test + public void strings_can_be_concatenated() { + Value result = this.context.eval("ezs", + " 'abc' + '_' + 'def' " + ); + assertEquals("abc_def", result.asString()); + } + + @Test + public void properties_can_be_accessed_through_indexing() { + Value result = this.context.eval("ezs", "" + + "const arr = [0, 1, 2]; " + + "arr['length']" + ); + assertEquals(3, result.asInt()); + } + + @Test + public void property_writes_through_indexing_are_ignored() { + Value result = this.context.eval("ezs", "" + + "const arr = [0, 1, 2]; " + + "const result = arr['length'] = 5; " + + "[result, arr.length]" + ); + assertEquals(5, result.getArrayElement(0).asInt()); + assertEquals(3, result.getArrayElement(1).asInt()); + } + + @Test + public void strings_have_a_length_property() { + Value result = this.context.eval("ezs", + " 'length'.length" + ); + assertEquals(6, result.asInt()); + } + + @Test + public void string_properties_can_be_accessed_through_indexing() { + Value result = this.context.eval("ezs", "" + + "var l = 'length'; " + + "l[l]" + ); + assertEquals(6, result.asInt()); + } + + @Test + public void ezs_strings_in_polyglot_context_have_no_members() { + Value result = this.context.eval("ezs", " 'a' "); + + assertTrue(result.isString()); + assertFalse(result.hasMembers()); + } + + @Test + public void java_strings_can_be_used_for_indexing() { + this.context.eval("ezs", "" + + "function access(o, propertyName) { " + + " return o[propertyName]; " + + "} " + + "let str = 'ab'; " + ); + Value ezsBindings = this.context.getBindings("ezs"); + Value str = ezsBindings.getMember("str"); + Value access = ezsBindings.getMember("access"); + + assertEquals(2, access.execute(str, "length").asInt()); + } + + @Test + public void strings_can_be_indexed() { + Value result = this.context.eval("ezs", + " 'abc'[1][0] " + ); + assertEquals("b", result.asString()); + } + + @Test + public void strings_indexed_out_of_range_return_undefined() { + Value result = this.context.eval("ezs", + " 'abc'[-1] " + ); + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void strings_can_be_compared_for_equality() { + Value result = this.context.eval("ezs", "" + + "let ret = 'string equality is broken'; " + + "if ('abc' === 'a' + 'b' + 'c') " + + " ret = 'string equality works correctly'; " + + "ret" + ); + assertEquals("string equality works correctly", result.asString()); + } + + @Test + public void strings_can_be_compared_with_less() { + Value result = this.context.eval("ezs", + " 'a' < 'b' " + ); + assertTrue(result.asBoolean()); + } + + @Test + public void concatenated_strings_have_a_length_property() { + Value result = this.context.eval("ezs", + "('abc' + 'def').length" + ); + assertEquals(6, result.asInt()); + } + + @Test + public void strings_have_a_charAt_method() { + Value result = this.context.eval("ezs", + " 'abc'.charAt(2)" + ); + assertEquals("c", result.asString()); + } + + @Test + public void charAt_without_argument_defaults_to_0() { + Value result = this.context.eval("ezs", + " 'abc'.charAt()" + ); + assertEquals("a", result.asString()); + } + + @Test + public void charAt_outside_range_returns_empty_string() { + Value result = this.context.eval("ezs", + " 'abc'.charAt(3) + 'abc'.charAt(-1)" + ); + assertEquals("", result.asString()); + } + + @Test + public void charAt_called_on_an_empty_string_without_arguments_returns_empty_string() { + Value result = this.context.eval("ezs", + " ''.charAt()" + ); + assertEquals("", result.asString()); + } + + @Test + public void methods_ignore_extra_arguments() { + Value result = this.context.eval("ezs", + " 'abc'.charAt(1, 2, 99)" + ); + assertEquals("b", result.asString()); + } + + @Test + public void unknown_string_property_returns_undefined() { + Value result = this.context.eval("ezs", " 'a'.someProp"); + + assertTrue(result.isNull()); + assertEquals("undefined", result.toString()); + } + + @Test + public void methods_correctly_resolve_their_targets() { + Value result = this.context.eval("ezs", "" + + "function firstChar(str) { " + + " return str.charAt(0); " + + "} " + + "firstChar('A'); " + + "firstChar('B'); " + + "firstChar('C'); " + + "firstChar('D'); " + ); + assertEquals("D", result.asString()); + } + + @Test + void string_properties_work_after_reading_non_existing_property() { + Value add = this.context.eval("ezs", "" + + "function readProp(str, prop) { " + + " return str[prop]; " + + "} " + + "let v1 = readProp('', 'does not exist'); " + + "let v2 = readProp('', 'length');"); + + Value ezsBindings = this.context.getBindings("ezs"); + Value v1 = ezsBindings.getMember("v1"); + Value v2 = ezsBindings.getMember("v2"); + assertTrue(v1.isNull()); + assertEquals(0, v2.asInt()); + } + + @Test + void chatAt_works_after_passing_it_undefined() { + Value charAtStr = this.context.eval("ezs", "" + + "function charAtStr(index) { " + + " return 'str'.charAt(index); " + + "} " + + "charAtStr; "); + + assertEquals("s", charAtStr.execute().asString()); + assertEquals("t", charAtStr.execute(1).asString()); + assertEquals("r", charAtStr.execute(2).asString()); + } + + @Test + public void Math_props_can_be_accessed_through_indexing() { + Value result = this.context.eval("ezs", + "Math['abs'](-4)" + ); + assertEquals(4, result.asInt()); + } + + @Test + public void string_index_writes_are_ignored() { + Value result = this.context.eval("ezs", "" + + "let s = 'a'; " + + "const tmp = s[0] = 'b'; " + + "s + tmp" + ); + assertEquals("ab", result.asString()); + } + + @Test + public void count_algorithm_returns_its_input() { + int input = 10_000; + Value result = this.context.eval("ezs", "" + + "function countWhileCharAtIndexProp(n) { " + + " var ret = 0; " + + " while (n > 0) { " + + " n = n - ('a'['charAt'](0) + ''['charAt']())['length']; " + + " ret = ret + 1; " + + " } " + + " return ret; " + + "}" + + "countWhileCharAtIndexProp(" + input + ");" + ); + + assertEquals(input, result.asInt()); + } +} diff --git a/settings.gradle b/settings.gradle index 0e27954e..68905192 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,4 +11,5 @@ include 'part-01', 'part-09', 'part-10', 'part-11', - 'part-12' + 'part-12', + 'part-13'