diff --git a/common/addons/structure-terrascript-loader/build.gradle.kts b/common/addons/structure-terrascript-loader/build.gradle.kts index 32d5ba99b..92f154c50 100644 --- a/common/addons/structure-terrascript-loader/build.gradle.kts +++ b/common/addons/structure-terrascript-loader/build.gradle.kts @@ -1,5 +1,4 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import java.util.* version = version("1.1.0") @@ -20,9 +19,19 @@ tasks.named("shadowJar") { val astSourceSet = buildDir.resolve("generated/ast") val astPackage = astSourceSet.resolve("com/dfsek/terra/addons/terrascript/ast") -data class ASTClass(val name: String, val imports: List, val nodes: List) +data class ASTClass( + val name: String, + val imports: List, + val nodes: List, + val constructorFields: List> = emptyList(), +) -data class ASTNode(val name: String, val constructorFields: List>, val mutableFields: List> = emptyList()) +data class ASTNode( + val name: String, + val constructorFields: List>, + val mutableFields: List> = emptyList() // TODO - Remove mutability from AST nodes + +) // Auto generate AST classes rather than writing them by hand tasks.register("genTerrascriptAstClasses") { @@ -34,22 +43,33 @@ tasks.register("genTerrascriptAstClasses") { src.appendLine("package $packageName;\n"); for (imprt in clazz.imports) src.appendLine("import $imprt;") src.appendLine(""" - import com.dfsek.terra.addons.terrascript.lexer.SourcePosition; /** * Auto-generated class via genTerrascriptAstClasses gradle task */ public abstract class ${clazz.name} { - public final SourcePosition position; - - public ${clazz.name}(SourcePosition position) { - this.position = position; - } - - public interface Visitor { - """.trimIndent()) + + for (field in clazz.constructorFields) { + src.appendLine(" public final ${field.second} ${field.first};") + } + + src.appendLine(""" + | + | public ${clazz.name}(${clazz.constructorFields.joinToString { "${it.second} ${it.first}" }}) { + """.trimMargin()) + + for (field in clazz.constructorFields) { + src.appendLine(" this.${field.first} = ${field.first};") + } + + src.appendLine(""" + | } + | + | public interface Visitor { + | + """.trimMargin()) for (node in clazz.nodes) { src.appendLine(" R visit${node.name}${clazz.name}(${node.name} ${clazz.name.toLowerCase()});") } @@ -76,10 +96,11 @@ tasks.register("genTerrascriptAstClasses") { src.appendLine() // Add constructor - src.append(" public ${node.name}(") - for (field in node.constructorFields) - src.append("${field.second} ${field.first}, ") - src.appendLine("SourcePosition position) {\n super(position);") + src.appendLine(""" + | public ${node.name}(${node.constructorFields.plus(clazz.constructorFields).joinToString { "${it.second} ${it.first}" }}) { + | super(${clazz.constructorFields.joinToString { it.first }}); + """.trimMargin()) + for (field in node.constructorFields) { src.appendLine(" this.${field.first} = ${field.first};") } @@ -117,42 +138,99 @@ tasks.register("genTerrascriptAstClasses") { doLast { astSourceSet.deleteRecursively() astPackage.mkdirs() - generateClass(ASTClass("Expr", listOf( - "com.dfsek.terra.addons.terrascript.Type", - "com.dfsek.terra.addons.terrascript.parser.UnaryOperator", - "com.dfsek.terra.addons.terrascript.parser.BinaryOperator", - "com.dfsek.terra.addons.terrascript.Environment.Symbol", - "java.util.List", - ), + listOf( - ASTNode("Binary", listOf("left" to "Expr", "operator" to "BinaryOperator", "right" to "Expr",), listOf("type" to "Type")), - ASTNode("Grouping", listOf("expression" to "Expr")), - ASTNode("Literal", listOf("value" to "Object", "type" to "Type")), - ASTNode("Unary", listOf("operator" to "UnaryOperator", "operand" to "Expr")), - ASTNode("Call", listOf("identifier" to "String", "arguments" to "List"), listOf("symbol" to "Symbol.Function")), - ASTNode("Variable", listOf("identifier" to "String"), listOf("symbol" to "Symbol.Variable")), - ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "Expr")), - ASTNode("Void", listOf()), - ))) - generateClass(ASTClass("Stmt", listOf( - "com.dfsek.terra.addons.terrascript.Type", - "com.dfsek.terra.api.util.generic.pair.Pair", - "com.dfsek.terra.addons.terrascript.Environment.Symbol", - "java.util.List", - ), - listOf( - ASTNode("Expression", listOf("expression" to "Expr")), - ASTNode("Block", listOf("statements" to "List")), - ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List>", "type" to "Type", "body" to "Block"), listOf("symbol" to "Symbol.Function")), - ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "Expr")), - ASTNode("Return", listOf("value" to "Expr"), listOf("type" to "Type")), - ASTNode("If", listOf("condition" to "Expr", "trueBody" to "Block", "elseIfClauses" to "List>", "elseBody" to "Block")), - ASTNode("For", listOf("initializer" to "Stmt", "condition" to "Expr", "incrementer" to "Expr", "body" to "Block")), - ASTNode("While", listOf("condition" to "Expr", "body" to "Block")), - ASTNode("NoOp", listOf()), - ASTNode("Break", listOf()), - ASTNode("Continue", listOf()), - ))) + ASTClass( + "Expr", + listOf( + "com.dfsek.terra.addons.terrascript.Type", + "com.dfsek.terra.addons.terrascript.parser.UnaryOperator", + "com.dfsek.terra.addons.terrascript.parser.BinaryOperator", + "com.dfsek.terra.addons.terrascript.Environment", + "com.dfsek.terra.addons.terrascript.Environment.Symbol", + "com.dfsek.terra.addons.terrascript.lexer.SourcePosition", + "java.util.List", + ), + listOf( + ASTNode("Binary", listOf("left" to "Expr", "operator" to "BinaryOperator", "right" to "Expr")), + ASTNode("Grouping", listOf("expression" to "Expr")), + ASTNode("Literal", listOf("value" to "Object", "type" to "Type")), + ASTNode("Unary", listOf("operator" to "UnaryOperator", "operand" to "Expr")), + ASTNode("Call", listOf("identifier" to "String", "arguments" to "List"), listOf("environment" to "Environment", "symbol" to "Symbol.Function")), + ASTNode("Variable", listOf("identifier" to "String"), listOf("symbol" to "Symbol.Variable")), + ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "Expr")), + ASTNode("Void", listOf()), + ), + listOf("position" to "SourcePosition") + ), + ASTClass( + "Stmt", + listOf( + "com.dfsek.terra.addons.terrascript.Type", + "com.dfsek.terra.api.util.generic.pair.Pair", + "com.dfsek.terra.addons.terrascript.Environment.Symbol", + "com.dfsek.terra.addons.terrascript.lexer.SourcePosition", + "java.util.List", + "java.util.Optional", + ), + listOf( + ASTNode("Expression", listOf("expression" to "Expr")), + ASTNode("Block", listOf("statements" to "List")), + ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List>", "returnType" to "Type", "body" to "Block")), + ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "Expr")), + ASTNode("Return", listOf("value" to "Expr"), listOf("type" to "Type")), + ASTNode("If", listOf("condition" to "Expr", "trueBody" to "Block", "elseIfClauses" to "List>", "elseBody" to "Optional")), + ASTNode("For", listOf("initializer" to "Stmt", "condition" to "Expr", "incrementer" to "Expr", "body" to "Block")), + ASTNode("While", listOf("condition" to "Expr", "body" to "Block")), + ASTNode("NoOp", listOf()), + ASTNode("Break", listOf()), + ASTNode("Continue", listOf()), + ), + listOf("position" to "SourcePosition") + ), + ASTClass( + "TypedExpr", + listOf( + "com.dfsek.terra.addons.terrascript.Type", + "com.dfsek.terra.addons.terrascript.parser.UnaryOperator", + "com.dfsek.terra.addons.terrascript.parser.BinaryOperator", + "java.util.List", + ), + listOf( + ASTNode("Binary", listOf("left" to "TypedExpr", "operator" to "BinaryOperator", "right" to "TypedExpr")), + ASTNode("Grouping", listOf("expression" to "TypedExpr")), + ASTNode("Literal", listOf("value" to "Object")), + ASTNode("Unary", listOf("operator" to "UnaryOperator", "operand" to "TypedExpr")), + ASTNode("Call", listOf("identifier" to "String", "arguments" to "List")), + ASTNode("Variable", listOf("identifier" to "String")), + ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "TypedExpr")), + ASTNode("Void", listOf()), + ), + listOf("type" to "Type") + ), + ASTClass( + "TypedStmt", + listOf( + "com.dfsek.terra.addons.terrascript.Type", + "com.dfsek.terra.api.util.generic.pair.Pair", + "java.util.List", + "java.util.Optional", + ), + listOf( + ASTNode("Expression", listOf("expression" to "TypedExpr")), + ASTNode("Block", listOf("statements" to "List")), + ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List>", "returnType" to "Type", "body" to "Block")), + ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "TypedExpr")), + ASTNode("Return", listOf("value" to "TypedExpr")), + ASTNode("If", listOf("condition" to "TypedExpr", "trueBody" to "Block", "elseIfClauses" to "List>", "elseBody" to "Optional")), + ASTNode("For", listOf("initializer" to "TypedStmt", "condition" to "TypedExpr", "incrementer" to "TypedExpr", "body" to "Block")), + ASTNode("While", listOf("condition" to "TypedExpr", "body" to "Block")), + ASTNode("NoOp", listOf()), + ASTNode("Break", listOf()), + ASTNode("Continue", listOf()), + ), + ), + ).forEach(::generateClass) } } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/TerraScript.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/TerraScript.java index db9271a8c..09db30350 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/TerraScript.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/TerraScript.java @@ -12,6 +12,7 @@ public interface TerraScript { Map BUILTIN_FUNCTIONS = new HashMap<>() {{ try { put("print", System.out.getClass().getMethod("println", String.class)); + put("printNum", System.out.getClass().getMethod("println", double.class)); } catch(NoSuchMethodException e) { throw new RuntimeException(e); } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/asm/TerraScriptClassGenerator.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/asm/TerraScriptClassGenerator.java index a191e9b56..0acda06de 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/asm/TerraScriptClassGenerator.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/asm/TerraScriptClassGenerator.java @@ -1,30 +1,30 @@ package com.dfsek.terra.addons.terrascript.codegen.asm; -import com.dfsek.terra.addons.terrascript.Environment.Symbol; import com.dfsek.terra.addons.terrascript.Type; -import com.dfsek.terra.addons.terrascript.ast.Expr; -import com.dfsek.terra.addons.terrascript.ast.Expr.Assignment; -import com.dfsek.terra.addons.terrascript.ast.Expr.Binary; -import com.dfsek.terra.addons.terrascript.ast.Expr.Call; -import com.dfsek.terra.addons.terrascript.ast.Expr.Grouping; -import com.dfsek.terra.addons.terrascript.ast.Expr.Literal; -import com.dfsek.terra.addons.terrascript.ast.Expr.Unary; -import com.dfsek.terra.addons.terrascript.ast.Expr.Variable; -import com.dfsek.terra.addons.terrascript.ast.Expr.Void; -import com.dfsek.terra.addons.terrascript.ast.Stmt; -import com.dfsek.terra.addons.terrascript.ast.Stmt.Block; -import com.dfsek.terra.addons.terrascript.ast.Stmt.Break; -import com.dfsek.terra.addons.terrascript.ast.Stmt.Continue; -import com.dfsek.terra.addons.terrascript.ast.Stmt.Expression; -import com.dfsek.terra.addons.terrascript.ast.Stmt.For; -import com.dfsek.terra.addons.terrascript.ast.Stmt.FunctionDeclaration; -import com.dfsek.terra.addons.terrascript.ast.Stmt.If; -import com.dfsek.terra.addons.terrascript.ast.Stmt.NoOp; -import com.dfsek.terra.addons.terrascript.ast.Stmt.Return; -import com.dfsek.terra.addons.terrascript.ast.Stmt.VariableDeclaration; -import com.dfsek.terra.addons.terrascript.ast.Stmt.While; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Assignment; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Binary; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Call; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Grouping; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Literal; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Unary; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Variable; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Void; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Block; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Break; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Continue; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Expression; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.For; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.FunctionDeclaration; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.If; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.NoOp; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Return; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.VariableDeclaration; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt.While; import com.dfsek.terra.addons.terrascript.codegen.TerraScript; +import com.dfsek.terra.addons.terrascript.exception.CompilerBugException; import com.dfsek.terra.addons.terrascript.util.ASMUtil; import com.dfsek.terra.api.util.generic.pair.Pair; @@ -39,6 +39,9 @@ import org.objectweb.asm.commons.LocalVariablesSorter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -127,7 +130,7 @@ public class TerraScriptClassGenerator { } } - private static class MethodBytecodeGenerator implements Stmt.Visitor, Expr.Visitor { + private static class MethodBytecodeGenerator implements TypedStmt.Visitor, TypedExpr.Visitor { private final ClassWriter classWriter; @@ -141,6 +144,8 @@ public class TerraScriptClassGenerator { private final Map lvTable = new HashMap<>(); + private final Deque> loopStack = new ArrayDeque<>(); + public MethodBytecodeGenerator(ClassWriter classWriter, String className, MethodVisitor method, int access, String descriptor) { this.classWriter = classWriter; this.className = className; @@ -150,101 +155,61 @@ public class TerraScriptClassGenerator { } public void generate(Block root) { - this.visitBlockStmt(root); + this.visitBlockTypedStmt(root); } @Override - public Void visitBinaryExpr(Binary expr) { - expr.left.accept(this); - expr.right.accept(this); + public Void visitBinaryTypedExpr(Binary expr) { switch(expr.operator) { - // TODO - Short circuit binary operators - case BOOLEAN_OR -> method.visitInsn(Opcodes.IOR); - case BOOLEAN_AND -> method.visitInsn(Opcodes.IAND); -// case EQUALS -> null; -// case NOT_EQUALS -> null; - case GREATER -> { - Label falseLabel = new Label(); - Label finished = new Label(); - method.visitInsn(Opcodes.DCMPL); - method.visitJumpInsn(Opcodes.IFLE, falseLabel); - method.visitInsn(Opcodes.ICONST_1); - method.visitJumpInsn(Opcodes.GOTO, finished); - method.visitLabel(falseLabel); - method.visitInsn(Opcodes.ICONST_0); - method.visitLabel(finished); - } - case GREATER_EQUALS -> { - Label falseLabel = new Label(); - Label finished = new Label(); - method.visitInsn(Opcodes.DCMPL); - method.visitJumpInsn(Opcodes.IFLT, falseLabel); - method.visitInsn(Opcodes.ICONST_1); - method.visitJumpInsn(Opcodes.GOTO, finished); - method.visitLabel(falseLabel); - method.visitInsn(Opcodes.ICONST_0); - method.visitLabel(finished); - } - case LESS -> { - Label falseLabel = new Label(); - Label finished = new Label(); - method.visitInsn(Opcodes.DCMPG); - method.visitJumpInsn(Opcodes.IFGE, falseLabel); - method.visitInsn(Opcodes.ICONST_1); - method.visitJumpInsn(Opcodes.GOTO, finished); - method.visitLabel(falseLabel); - method.visitInsn(Opcodes.ICONST_0); - method.visitLabel(finished); - } - case LESS_EQUALS -> { - Label falseLabel = new Label(); - Label finished = new Label(); - method.visitInsn(Opcodes.DCMPG); - method.visitJumpInsn(Opcodes.IFGT, falseLabel); - method.visitInsn(Opcodes.ICONST_1); - method.visitJumpInsn(Opcodes.GOTO, finished); - method.visitLabel(falseLabel); - method.visitInsn(Opcodes.ICONST_0); - method.visitLabel(finished); - } + case EQUALS, NOT_EQUALS, BOOLEAN_AND, BOOLEAN_OR, GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> pushComparisonResult(expr); case ADD -> { - switch(expr.getType()) { + pushBinaryOperands(expr); + switch(expr.type) { case NUMBER -> method.visitInsn(Opcodes.DADD); // TODO - Optimize string concatenation case STRING -> method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat", "(Ljava/lang/String;)Ljava/lang/String;", false); - default -> throw new RuntimeException("Could not generate bytecode for ADD binary operator returning type " + expr.getType()); + default -> throw new RuntimeException("Could not generate bytecode for ADD binary operator returning type " + expr.type); } } - case SUBTRACT -> method.visitInsn(Opcodes.DSUB); - case MULTIPLY -> method.visitInsn(Opcodes.DMUL); - case DIVIDE -> method.visitInsn(Opcodes.DDIV); + case SUBTRACT -> binaryInsn(expr, Opcodes.DSUB); + case MULTIPLY -> binaryInsn(expr, Opcodes.DMUL); + case DIVIDE -> binaryInsn(expr, Opcodes.DDIV); // case MODULO -> + default -> throw new RuntimeException("Unhandled binary operator " + expr.operator); } return null; } @Override - public Void visitGroupingExpr(Grouping expr) { + public Void visitGroupingTypedExpr(Grouping expr) { expr.expression.accept(this); return null; } @Override - public Void visitLiteralExpr(Literal expr) { - method.visitLdcInsn(expr.value); + public Void visitLiteralTypedExpr(Literal expr) { + switch (expr.type) { + case BOOLEAN -> method.visitInsn((boolean) expr.value ? Opcodes.ICONST_1 : Opcodes.ICONST_0); + case NUMBER, STRING -> method.visitLdcInsn(expr.value); + } return null; } @Override - public Void visitUnaryExpr(Unary expr) { + public Void visitUnaryTypedExpr(Unary expr) { + expr.operand.accept(this); + switch (expr.operator) { + case NOT -> invertBool(); + case NEGATE -> method.visitInsn(Opcodes.DNEG); + } return null; } @Override - public Void visitCallExpr(Call expr) { - Symbol.Function function = expr.getSymbol(); + public Void visitCallTypedExpr(Call expr) { if (TerraScript.BUILTIN_FUNCTIONS.containsKey(expr.identifier)) { + Method m = TerraScript.BUILTIN_FUNCTIONS.get(expr.identifier); if (expr.identifier.equals("print")) { // TODO - remove quick dirty print function call method.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); expr.arguments.get(0).accept(this); @@ -253,76 +218,60 @@ public class TerraScriptClassGenerator { return null; } expr.arguments.forEach(a -> a.accept(this)); - List parameters = function.parameters.stream().map(Pair::getRight).toList(); - method.visitMethodInsn(Opcodes.INVOKESTATIC, className, expr.identifier, getFunctionDescriptor(parameters, function.type), false); + List parameters = expr.arguments.stream().map(e -> e.type).toList(); + method.visitMethodInsn(Opcodes.INVOKESTATIC, className, expr.identifier, getFunctionDescriptor(parameters, expr.type), false); return null; } @Override - public Void visitVariableExpr(Variable expr) { - Type varType = expr.getSymbol().type; + public Void visitVariableTypedExpr(Variable expr) { + Type varType = expr.type; method.visitVarInsn(switch(varType) { case NUMBER -> Opcodes.DLOAD; case STRING -> Opcodes.ALOAD; case BOOLEAN -> Opcodes.ILOAD; - default -> throw new RuntimeException("Unable to load local variable, unknown parameter type '" + varType + "'"); + default -> throw new RuntimeException("Unable to load local variable, unhandled type '" + varType + "'"); }, lvTable.get(expr.identifier)); return null; } @Override - public Void visitAssignmentExpr(Assignment expr) { + public Void visitAssignmentTypedExpr(Assignment expr) { expr.rValue.accept(this); - Type type = expr.lValue.getSymbol().type; + Type type = expr.lValue.type; method.visitVarInsn(switch(type) { case NUMBER -> Opcodes.DSTORE; case STRING -> Opcodes.ASTORE; case BOOLEAN -> Opcodes.ISTORE; - default -> throw new RuntimeException("Unable to assign local variable, unknown parameter type '" + type + "'"); + default -> throw new RuntimeException("Unable to assign local variable, unhandled type '" + type + "'"); }, lvTable.get(expr.lValue.identifier)); return null; } @Override - public Void visitVoidExpr(Void expr) { + public Void visitVoidTypedExpr(Void expr) { return null; } @Override - public Void visitExpressionStmt(Expression stmt) { + public Void visitExpressionTypedStmt(Expression stmt) { stmt.expression.accept(this); return null; } @Override - public Void visitBlockStmt(Block stmt) { + public Void visitBlockTypedStmt(Block stmt) { stmt.statements.forEach(s -> s.accept(this)); return null; } - private String getFunctionDescriptor(List parameters, Type returnType) { - StringBuilder sb = new StringBuilder().append("("); - parameters.stream().map(p -> switch (p) { - case NUMBER -> "D"; - case STRING -> "Ljava/lang/String;"; - case BOOLEAN -> "Z"; - default -> throw new RuntimeException("Unable to generate method descriptor, unknown parameter type '" + p + "'"); - }).forEach(sb::append); - sb.append(")"); - sb.append(switch (returnType) { - case NUMBER -> "D"; - case STRING -> "Ljava/lang/String;"; - case BOOLEAN -> "Z"; - case VOID -> "V"; - }); - return sb.toString(); - } - @Override - public Void visitFunctionDeclarationStmt(FunctionDeclaration stmt) { - int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC; + public Void visitFunctionDeclarationTypedStmt(FunctionDeclaration stmt) { List parameterTypes = stmt.parameters.stream().map(Pair::getRight).toList(); - MethodVisitor method = classWriter.visitMethod(access, stmt.identifier, getFunctionDescriptor(parameterTypes, stmt.type), null, null); + + int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC; + // TODO - Mangle identifier based on scope to avoid issues with using the same identifier in different scopes + MethodVisitor method = classWriter.visitMethod(access, stmt.identifier, getFunctionDescriptor(parameterTypes, stmt.returnType), null, null); method.visitCode(); // Start method body @@ -351,7 +300,7 @@ public class TerraScriptClassGenerator { } @Override - public Void visitVariableDeclarationStmt(VariableDeclaration stmt) { + public Void visitVariableDeclarationTypedStmt(VariableDeclaration stmt) { stmt.value.accept(this); lvTable.put(stmt.identifier, lvs.newLocal(ASMUtil.tsTypeToAsmType(stmt.type))); method.visitVarInsn(switch(stmt.type) { @@ -364,45 +313,237 @@ public class TerraScriptClassGenerator { } @Override - public Void visitReturnStmt(Return stmt) { + public Void visitReturnTypedStmt(Return stmt) { stmt.value.accept(this); - switch(stmt.getType()) { + switch(stmt.value.type) { case NUMBER -> method.visitInsn(Opcodes.DRETURN); case STRING -> method.visitInsn(Opcodes.ARETURN); case BOOLEAN -> method.visitInsn(Opcodes.IRETURN); + default -> throw new CompilerBugException(); } return null; } @Override - public Void visitIfStmt(If stmt) { + public Void visitIfTypedStmt(If stmt) { + Label endIf = new Label(); + conditionalStmt(stmt.condition, stmt.trueBody, endIf); + for(Pair elseIfClause : stmt.elseIfClauses) { + conditionalStmt(elseIfClause.getLeft(), elseIfClause.getRight(), endIf); + } + stmt.elseBody.ifPresent(b -> b.accept(this)); + method.visitLabel(endIf); return null; } @Override - public Void visitForStmt(For stmt) { + public Void visitForTypedStmt(For stmt) { + Label loopStart = new Label(); + Label loopBody = new Label(); + Label loopEnd = new Label(); + + stmt.initializer.accept(this); + method.visitJumpInsn(Opcodes.GOTO, loopBody); // Skip over incrementer on first loop + + method.visitLabel(loopStart); + stmt.incrementer.accept(this); + method.visitLabel(loopBody); + loopStack.push(Pair.of(loopStart, loopEnd)); + conditionalStmt(stmt.condition, stmt.body, loopStart); + loopStack.pop(); + method.visitLabel(loopEnd); return null; } @Override - public Void visitWhileStmt(While stmt) { + public Void visitWhileTypedStmt(While stmt) { + Label loopStart = new Label(); + Label loopEnd = new Label(); + + method.visitLabel(loopStart); + loopStack.push(Pair.of(loopStart, loopEnd)); + conditionalStmt(stmt.condition, stmt.body, loopStart); + loopStack.pop(); + method.visitLabel(loopEnd); return null; } @Override - public Void visitNoOpStmt(NoOp stmt) { + public Void visitNoOpTypedStmt(NoOp stmt) { return null; } @Override - public Void visitBreakStmt(Break stmt) { + public Void visitBreakTypedStmt(Break stmt) { + method.visitJumpInsn(Opcodes.GOTO, loopStack.getFirst().getRight()); return null; } @Override - public Void visitContinueStmt(Continue stmt) { + public Void visitContinueTypedStmt(Continue stmt) { + method.visitJumpInsn(Opcodes.GOTO, loopStack.getFirst().getLeft()); return null; } + + private boolean binaryOperandsSameType(Type type, Binary expr) { + return exprTypesEqual(type, expr.left, expr.right); + } + + private static boolean exprTypesEqual(Type type, TypedExpr... exprs) { + for(TypedExpr expr : exprs) { + if (expr.type != type) return false; + } + return true; + } + + /** + * Inverts a boolean on the stack + */ + private void invertBool() { + Label invertToFalse = new Label(); + Label finished = new Label(); + method.visitJumpInsn(Opcodes.IFNE, invertToFalse); + + method.visitInsn(Opcodes.ICONST_1); + method.visitJumpInsn(Opcodes.GOTO, finished); + + method.visitLabel(invertToFalse); + method.visitInsn(Opcodes.ICONST_0); + + method.visitLabel(finished); + } + + private void pushBinaryOperands(Binary expr) { + expr.left.accept(this); + expr.right.accept(this); + } + + private void binaryInsn(Binary expr, int insn) { + pushBinaryOperands(expr); + method.visitInsn(insn); + } + + /** + * Pushes boolean on to the stack based on comparison result + * @param condition + */ + private void pushComparisonResult(TypedExpr condition) { + Label trueFinished = new Label(); + conditionalRunnable(condition, () -> method.visitInsn(Opcodes.ICONST_1), trueFinished); + method.visitInsn(Opcodes.ICONST_0); + method.visitLabel(trueFinished); + } + + /** + * Executes a statement then jumps to the exit label if the condition is true, jumps over the statement if false + * @param condition + * @param stmt + * @param exit + */ + private void conditionalStmt(TypedExpr condition, TypedStmt stmt, Label exit) { + conditionalRunnable(condition, () -> stmt.accept(this), exit); + } + + private void conditionalRunnable(TypedExpr condition, Runnable trueSection, Label trueFinished) { + Label exit = new Label(); // If the first conditional is false, jump over statement and don't execute it + if (condition instanceof Binary binaryCondition) { + switch(binaryCondition.operator) { + case BOOLEAN_AND -> { + // Operands assumed booleans + binaryCondition.left.accept(this); + method.visitJumpInsn(Opcodes.IFEQ, exit); // If left is false, short circuit, don't evaluate right + binaryCondition.right.accept(this); + method.visitJumpInsn(Opcodes.IFEQ, exit); + } + case BOOLEAN_OR -> { + Label skipRight = new Label(); + // Operands assumed booleans + binaryCondition.left.accept(this); + method.visitJumpInsn(Opcodes.IFNE, skipRight); // If left is true, skip evaluating right + binaryCondition.right.accept(this); + method.visitJumpInsn(Opcodes.IFEQ, exit); + method.visitLabel(skipRight); + } + case EQUALS -> { + if (binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers + pushBinaryOperands(binaryCondition); + method.visitJumpInsn(Opcodes.IF_ICMPNE, exit); + + } else if (binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles + pushBinaryOperands(binaryCondition); + method.visitInsn(Opcodes.DCMPG); + method.visitJumpInsn(Opcodes.IFNE, exit); + + } else if (binaryOperandsSameType(Type.STRING, binaryCondition)) { + pushBinaryOperands(binaryCondition); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false); + method.visitJumpInsn(Opcodes.IFEQ, exit); + } else throw new CompilerBugException(); + } + case NOT_EQUALS -> { + if (binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers + pushBinaryOperands(binaryCondition); + method.visitJumpInsn(Opcodes.IF_ICMPEQ, exit); + + } else if (binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles + pushBinaryOperands(binaryCondition); + method.visitInsn(Opcodes.DCMPG); + method.visitJumpInsn(Opcodes.IFEQ, exit); + + } else if (binaryOperandsSameType(Type.STRING, binaryCondition)) { // Operands assumed references + pushBinaryOperands(binaryCondition); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false); + invertBool(); + method.visitJumpInsn(Opcodes.IFEQ, exit); + } else throw new CompilerBugException(); + } + case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> { + // Left and right assumed double + pushBinaryOperands(binaryCondition); + + method.visitInsn(switch(binaryCondition.operator) { + case GREATER, GREATER_EQUALS -> Opcodes.DCMPL; + case LESS, LESS_EQUALS -> Opcodes.DCMPG; + default -> throw new IllegalStateException(); + }); + + method.visitJumpInsn(switch(binaryCondition.operator) { + case GREATER -> Opcodes.IFLE; + case GREATER_EQUALS -> Opcodes.IFLT; + case LESS -> Opcodes.IFGE; + case LESS_EQUALS -> Opcodes.IFGT; + default -> throw new IllegalStateException(); + }, exit); + } + default -> throw new CompilerBugException(); + } + } else { + // Assume condition returns bool + condition.accept(this); + method.visitJumpInsn(Opcodes.IFEQ, exit); + } + trueSection.run(); + method.visitJumpInsn(Opcodes.GOTO, trueFinished); // Jump to end of statement after execution + method.visitLabel(exit); + } + + private String getFunctionDescriptor(List parameters, Type returnType) { + StringBuilder sb = new StringBuilder().append("("); + parameters.stream().map(p -> switch (p) { + case NUMBER -> "D"; + case STRING -> "Ljava/lang/String;"; + case BOOLEAN -> "Z"; + default -> throw new RuntimeException("Unable to generate method descriptor, unknown parameter type '" + p + "'"); + }).forEach(sb::append); + sb.append(")"); + sb.append(switch (returnType) { + case NUMBER -> "D"; + case STRING -> "Ljava/lang/String;"; + case BOOLEAN -> "Z"; + case VOID -> "V"; + }); + return sb.toString(); + } } private static class MethodExtractor extends ClassVisitor { diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/exception/CompilerBugException.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/exception/CompilerBugException.java new file mode 100644 index 000000000..3941699da --- /dev/null +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/exception/CompilerBugException.java @@ -0,0 +1,5 @@ +package com.dfsek.terra.addons.terrascript.exception; + +public class CompilerBugException extends RuntimeException { + // TODO - Add message constructor +} diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/legacy/parser/Parser.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/legacy/parser/Parser.java index c39e32acc..560f6f1b8 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/legacy/parser/Parser.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/legacy/parser/Parser.java @@ -387,8 +387,10 @@ public class Parser { private Expression parseVariableDeclaration(Scope.ScopeBuilder scopeBuilder, Token type, Token identifier) { lexer.consume("Expected '=' after identifier '" + identifier.lexeme() + "' for variable declaration", TokenType.ASSIGNMENT); - - if(!type.isVariableDeclaration()) throw new ParseException("Expected type specification at beginning of variable declaration", + + if(!(type.isType(TokenType.TYPE_STRING) + || type.isType(TokenType.TYPE_BOOLEAN) + || type.isType(TokenType.TYPE_NUMBER))) throw new ParseException("Expected type specification at beginning of variable declaration", type.position()); if(scopeBuilder.containsVariable(identifier.lexeme())) @@ -514,8 +516,10 @@ public class Parser { case STATEMENT_END -> Expression.NOOP; default -> throw new ParseException("Unexpected token '" + token.lexeme() + "' while parsing statement", token.position()); }; - if(!token.isControlStructure() && expression != Expression.NOOP) lexer.consume("Expected ';' at end of statement", - TokenType.STATEMENT_END); + if(!(token.isType(TokenType.IF_STATEMENT) + || token.isType(TokenType.WHILE_LOOP) + || token.isType(TokenType.FOR_LOOP)) && expression != Expression.NOOP) lexer.consume("Expected ';' at end of statement", + TokenType.STATEMENT_END); return expression; } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Lexer.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Lexer.java index db4ea3f94..68e2eb01c 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Lexer.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Lexer.java @@ -242,7 +242,7 @@ public class Lexer { } reader.consume(); } - throw new EOFException("No end of expression found.", begin); + throw new EOFException("Reached end of file without matching '" + s + "'", begin); } private boolean isNumberLike() { diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Token.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Token.java index 93650d003..2825b99d1 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Token.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Token.java @@ -9,9 +9,6 @@ package com.dfsek.terra.addons.terrascript.lexer; import java.util.Objects; -import com.dfsek.terra.addons.terrascript.parser.BinaryOperator; -import com.dfsek.terra.addons.terrascript.parser.UnaryOperator; - public class Token { private final String lexeme; @@ -59,60 +56,6 @@ public class Token { return false; } - public boolean isOperator(BinaryOperator... operators) { - for(BinaryOperator o : operators) if(o.tokenType == type) return true; - return false; - } - - public boolean isOperator(UnaryOperator... operators) { - for(UnaryOperator o : operators) if(o.tokenType == type) return true; - return false; - } - - public boolean isBinaryOperator() { - return type.equals(TokenType.PLUS) - || type.equals(TokenType.MINUS) - || type.equals(TokenType.STAR) - || type.equals(TokenType.FORWARD_SLASH) - || type.equals(TokenType.EQUALS_EQUALS) - || type.equals(TokenType.BANG_EQUALS) - || type.equals(TokenType.LESS) - || type.equals(TokenType.GREATER) - || type.equals(TokenType.LESS_EQUALS) - || type.equals(TokenType.GREATER_EQUAL) - || type.equals(TokenType.BOOLEAN_OR) - || type.equals(TokenType.BOOLEAN_AND) - || type.equals(TokenType.MODULO_OPERATOR); - } - - public boolean isStrictNumericOperator() { - return type.equals(TokenType.MINUS) - || type.equals(TokenType.STAR) - || type.equals(TokenType.FORWARD_SLASH) - || type.equals(TokenType.GREATER) - || type.equals(TokenType.LESS) - || type.equals(TokenType.LESS_EQUALS) - || type.equals(TokenType.GREATER_EQUAL) - || type.equals(TokenType.MODULO_OPERATOR); - } - - public boolean isStrictBooleanOperator() { - return type.equals(TokenType.BOOLEAN_AND) - || type.equals(TokenType.BOOLEAN_OR); - } - - public boolean isVariableDeclaration() { - return type.equals(TokenType.TYPE_STRING) - || type.equals(TokenType.TYPE_BOOLEAN) - || type.equals(TokenType.TYPE_NUMBER); - } - - public boolean isControlStructure() { - return type.equals(TokenType.IF_STATEMENT) - || type.equals(TokenType.WHILE_LOOP) - || type.equals(TokenType.FOR_LOOP); - } - public enum TokenType { /** * Function identifier or language keyword diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/Parser.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/Parser.java index 68c03b790..da869245c 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/Parser.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/Parser.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Supplier; import com.dfsek.terra.addons.terrascript.Type; @@ -178,7 +179,7 @@ public class Parser { elseIfClauses.add(Pair.of(elseIfCondition, elseIfBody)); } - return new Stmt.If(condition, trueBody, elseIfClauses, elseBody, position); + return new Stmt.If(condition, trueBody, elseIfClauses, Optional.ofNullable(elseBody), position); } private Stmt.For forLoop() { diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/FunctionReferenceAnalyzer.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/FunctionReferenceAnalyzer.java index 2041c0208..4579f63f6 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/FunctionReferenceAnalyzer.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/FunctionReferenceAnalyzer.java @@ -63,7 +63,7 @@ public class FunctionReferenceAnalyzer implements Expr.Visitor, Stmt.Visit public Void visitCallExpr(Call expr) { String id = expr.identifier; try { - expr.getSymbol(); + expr.setSymbol(expr.getEnvironment().getFunction(expr.identifier)); } catch(NonexistentSymbolException e) { errorHandler.add( new UndefinedReferenceException("No function by the name '" + id + "' is defined in this scope", expr.position)); @@ -129,9 +129,7 @@ public class FunctionReferenceAnalyzer implements Expr.Visitor, Stmt.Visit clause.getLeft().accept(this); clause.getRight().accept(this); } - if(stmt.elseBody != null) { - stmt.elseBody.accept(this); - } + stmt.elseBody.ifPresent(b -> b.accept(this)); return null; } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/ScopeAnalyzer.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/ScopeAnalyzer.java index 4a1d5a1b4..39e649671 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/ScopeAnalyzer.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/ScopeAnalyzer.java @@ -60,7 +60,7 @@ public class ScopeAnalyzer implements Visitor, Stmt.Visitor { @Override public Void visitCallExpr(Call expr) { - expr.setSymbol(currentScope.getFunction(expr.identifier)); + expr.setEnvironment(currentScope); expr.arguments.forEach(e -> e.accept(this)); return null; } @@ -69,14 +69,13 @@ public class ScopeAnalyzer implements Visitor, Stmt.Visitor { public Void visitVariableExpr(Variable expr) { String id = expr.identifier; try { - currentScope.getVariable(id); // Ensure variable has been declared in current scope + expr.setSymbol(currentScope.getVariable(id)); } catch(NonexistentSymbolException e) { errorHandler.add( new UndefinedReferenceException("No variable by the name '" + id + "' is defined in this scope", expr.position)); } catch(SymbolTypeMismatchException e) { errorHandler.add(new ParseException("Identifier '" + id + "' is not defined as a variable", expr.position)); } - expr.setSymbol(currentScope.getVariable(expr.identifier)); return null; } @@ -120,7 +119,7 @@ public class ScopeAnalyzer implements Visitor, Stmt.Visitor { stmt.body.accept(this); currentScope = currentScope.outer(); try { - currentScope.put(stmt.identifier, new Environment.Symbol.Function(stmt.type, stmt.parameters)); + currentScope.put(stmt.identifier, new Environment.Symbol.Function(stmt.returnType, stmt.parameters)); } catch(Environment.ScopeException.SymbolAlreadyExistsException e) { errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope", stmt.position)); @@ -130,6 +129,7 @@ public class ScopeAnalyzer implements Visitor, Stmt.Visitor { @Override public Void visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) { + stmt.value.accept(this); try { currentScope.put(stmt.identifier, new Environment.Symbol.Variable(stmt.type)); } catch(Environment.ScopeException.SymbolAlreadyExistsException e) { @@ -153,9 +153,7 @@ public class ScopeAnalyzer implements Visitor, Stmt.Visitor { clause.getLeft().accept(this); clause.getRight().accept(this); } - if(stmt.elseBody != null) { - stmt.elseBody.accept(this); - } + stmt.elseBody.ifPresent(b -> b.accept(this)); return null; } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/SemanticAnalyzer.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/SemanticAnalyzer.java index 3fc19707d..3b92afec6 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/SemanticAnalyzer.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/SemanticAnalyzer.java @@ -3,19 +3,22 @@ package com.dfsek.terra.addons.terrascript.semanticanalysis; import com.dfsek.terra.addons.terrascript.Environment; import com.dfsek.terra.addons.terrascript.ErrorHandler; import com.dfsek.terra.addons.terrascript.ast.Stmt; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt; public class SemanticAnalyzer { - public static void analyze(Stmt.Block root, ErrorHandler errorHandler) throws Exception { + public static TypedStmt.Block analyze(Stmt.Block root, ErrorHandler errorHandler) throws Exception { new ScopeAnalyzer(Environment.global(), errorHandler).visitBlockStmt(root); errorHandler.throwAny(); new FunctionReferenceAnalyzer(errorHandler).visitBlockStmt(root); errorHandler.throwAny(); - new TypeChecker(errorHandler).visitBlockStmt(root); + TypedStmt.Block checkedRoot = (TypedStmt.Block) new TypeChecker(errorHandler).visitBlockStmt(root); errorHandler.throwAny(); + + return checkedRoot; } } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/TypeChecker.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/TypeChecker.java index 07d75bf5d..789c18618 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/TypeChecker.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/semanticanalysis/TypeChecker.java @@ -1,11 +1,12 @@ package com.dfsek.terra.addons.terrascript.semanticanalysis; import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import com.dfsek.terra.addons.terrascript.Environment; import com.dfsek.terra.addons.terrascript.ErrorHandler; import com.dfsek.terra.addons.terrascript.Type; -import com.dfsek.terra.addons.terrascript.ast.Expr; import com.dfsek.terra.addons.terrascript.ast.Expr.Assignment; import com.dfsek.terra.addons.terrascript.ast.Expr.Binary; import com.dfsek.terra.addons.terrascript.ast.Expr.Call; @@ -16,6 +17,8 @@ import com.dfsek.terra.addons.terrascript.ast.Expr.Variable; import com.dfsek.terra.addons.terrascript.ast.Expr.Visitor; import com.dfsek.terra.addons.terrascript.ast.Expr.Void; import com.dfsek.terra.addons.terrascript.ast.Stmt; +import com.dfsek.terra.addons.terrascript.ast.TypedExpr; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt; import com.dfsek.terra.addons.terrascript.exception.semanticanalysis.InvalidFunctionDeclarationException; import com.dfsek.terra.addons.terrascript.exception.semanticanalysis.InvalidTypeException; import com.dfsek.terra.addons.terrascript.legacy.parser.exceptions.ParseException; @@ -24,217 +27,221 @@ import com.dfsek.terra.api.util.generic.pair.Pair; import static com.dfsek.terra.addons.terrascript.util.OrdinalUtil.ordinalOf; -public class TypeChecker implements Visitor, Stmt.Visitor { +public class TypeChecker implements Visitor, Stmt.Visitor { private final ErrorHandler errorHandler; TypeChecker(ErrorHandler errorHandler) { this.errorHandler = errorHandler; } @Override - public Type visitBinaryExpr(Binary expr) { - Type left = expr.left.accept(this); - Type right = expr.right.accept(this); + public TypedExpr visitBinaryExpr(Binary expr) { + TypedExpr left = expr.left.accept(this); + TypedExpr right = expr.right.accept(this); - return switch(expr.operator) { + Type leftType = left.type; + Type rightType = right.type; + + Type type = switch(expr.operator) { case BOOLEAN_OR, BOOLEAN_AND -> { - if(left != Type.BOOLEAN || right != Type.BOOLEAN) - throw new RuntimeException(); + if(leftType != Type.BOOLEAN || rightType != Type.BOOLEAN) + errorHandler.add(new InvalidTypeException("Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" + leftType + "' and '" + rightType + "'", expr.position)); yield Type.BOOLEAN; } case EQUALS, NOT_EQUALS -> { - if(left != right) throw new RuntimeException(); + if(leftType != rightType) errorHandler.add(new InvalidTypeException("Both operands of equality operator (==) must be of the same type, found mismatched types '" + leftType + "' and '" + rightType + "'", expr.position)); yield Type.BOOLEAN; } case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> { - if(left != Type.NUMBER || right != Type.NUMBER) - throw new RuntimeException(); + if(leftType != Type.NUMBER || rightType != Type.NUMBER) + errorHandler.add(new InvalidTypeException("Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" + leftType + "' and '" + rightType + "'", expr.position)); yield Type.BOOLEAN; } case ADD -> { - if(left == Type.NUMBER && right == Type.NUMBER) { - expr.setType(Type.NUMBER); + if(leftType == Type.NUMBER && rightType == Type.NUMBER) { yield Type.NUMBER; } - if(left == Type.STRING || right == Type.STRING) { - expr.setType(Type.STRING); + if(leftType == Type.STRING || rightType == Type.STRING) { yield Type.STRING; } - throw new RuntimeException("Addition operands must be either both numbers, or one of type string"); + errorHandler.add(new RuntimeException("Addition operands must be either both numbers, or one of type string")); + yield Type.VOID; } case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> { - if(left != Type.NUMBER || right != Type.NUMBER) - throw new RuntimeException(); + if(leftType != Type.NUMBER || rightType != Type.NUMBER) + errorHandler.add(new InvalidTypeException("Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" + leftType + "' and '" + rightType + "'", expr.position)); yield Type.NUMBER; } }; + return new TypedExpr.Binary(left, expr.operator, right, type); } @Override - public Type visitGroupingExpr(Grouping expr) { + public TypedExpr visitGroupingExpr(Grouping expr) { return expr.expression.accept(this); } @Override - public Type visitLiteralExpr(Literal expr) { - return expr.type; + public TypedExpr visitLiteralExpr(Literal expr) { + return new TypedExpr.Literal(expr.value, expr.type); } @Override - public Type visitUnaryExpr(Unary expr) { - Type right = expr.operand.accept(this); - return switch(expr.operator) { + public TypedExpr visitUnaryExpr(Unary expr) { + TypedExpr right = expr.operand.accept(this); + Type type = switch(expr.operator) { case NOT -> { - if(right != Type.BOOLEAN) throw new RuntimeException(); + if(right.type != Type.BOOLEAN) throw new RuntimeException(); yield Type.BOOLEAN; } case NEGATE -> { - if(right != Type.NUMBER) throw new RuntimeException(); + if(right.type != Type.NUMBER) throw new RuntimeException(); yield Type.NUMBER; } }; + return new TypedExpr.Unary(expr.operator, right, type); } @Override - public Type visitCallExpr(Call expr) { + public TypedExpr visitCallExpr(Call expr) { String id = expr.identifier; Environment.Symbol.Function signature = expr.getSymbol(); - List argumentTypes = expr.arguments.stream().map(a -> a.accept(this)).toList(); + List arguments = expr.arguments.stream().map(a -> a.accept(this)).toList(); List parameters = signature.parameters.stream().map(Pair::getRight).toList(); - if(argumentTypes.size() != parameters.size()) + if(arguments.size() != parameters.size()) errorHandler.add(new ParseException( - "Provided " + argumentTypes.size() + " arguments to function call of '" + id + "', expected " + parameters.size() + + "Provided " + arguments.size() + " arguments to function call of '" + id + "', expected " + parameters.size() + " arguments", expr.position)); for(int i = 0; i < parameters.size(); i++) { Type expectedType = parameters.get(i); - Type providedType = argumentTypes.get(i); + Type providedType = arguments.get(i).type; if(expectedType != providedType) errorHandler.add(new InvalidTypeException( ordinalOf(i + 1) + " argument provided for function '" + id + "' expects type " + expectedType + ", found " + providedType + " instead", expr.position)); } - return signature.type; + return new TypedExpr.Call(expr.identifier, arguments, signature.type); } @Override - public Type visitVariableExpr(Variable expr) { - return expr.getSymbol().type; + public TypedExpr visitVariableExpr(Variable expr) { + return new TypedExpr.Variable(expr.identifier, expr.getSymbol().type); } @Override - public Type visitAssignmentExpr(Assignment expr) { - Type right = expr.rValue.accept(this); - Type expected = expr.lValue.accept(this); + public TypedExpr visitAssignmentExpr(Assignment expr) { + TypedExpr.Variable left = (TypedExpr.Variable) expr.lValue.accept(this); + TypedExpr right = expr.rValue.accept(this); + Type expected = left.type; String id = expr.lValue.identifier; - if(right != expected) + if(right.type != expected) errorHandler.add(new InvalidTypeException( "Cannot assign variable '" + id + "' to type " + right + ", '" + id + "' is declared with type " + expected, expr.position)); - return right; + return new TypedExpr.Assignment(left, right, right.type); } @Override - public Type visitVoidExpr(Void expr) { - return Type.VOID; + public TypedExpr visitVoidExpr(Void expr) { + return new TypedExpr.Void(Type.VOID); } @Override - public Type visitExpressionStmt(Stmt.Expression stmt) { - stmt.expression.accept(this); - return Type.VOID; + public TypedStmt visitExpressionStmt(Stmt.Expression stmt) { + return new TypedStmt.Expression(stmt.expression.accept(this)); } @Override - public Type visitBlockStmt(Stmt.Block stmt) { - stmt.statements.forEach(s -> s.accept(this)); - return Type.VOID; + public TypedStmt visitBlockStmt(Stmt.Block stmt) { + return new TypedStmt.Block(stmt.statements.stream().map(s -> s.accept(this)).toList()); } @Override - public Type visitFunctionDeclarationStmt(Stmt.FunctionDeclaration stmt) { - boolean hasReturn = false; - for(Stmt s : stmt.body.statements) { - if(s instanceof Stmt.Return ret) { - hasReturn = true; - Type provided = ret.value.accept(this); - if(provided != stmt.type) + public TypedStmt visitFunctionDeclarationStmt(Stmt.FunctionDeclaration stmt) { + AtomicBoolean hasReturn = new AtomicBoolean(false); + TypedStmt.Block body = new TypedStmt.Block(stmt.body.statements.stream().map(s -> { + TypedStmt bodyStmt = s.accept(this); + if(bodyStmt instanceof TypedStmt.Return ret) { + hasReturn.set(true); + if(ret.value.type != stmt.returnType) errorHandler.add(new InvalidTypeException( "Return statement must match function's return type. Function '" + stmt.identifier + "' expects " + - stmt.type + ", found " + provided + " instead", s.position)); + stmt.returnType + ", found " + ret.value.type + " instead", s.position)); } - s.accept(this); - } - if(stmt.type != Type.VOID && !hasReturn) { + return bodyStmt; + }).toList()); + if(stmt.returnType != Type.VOID && !hasReturn.get()) { errorHandler.add( new InvalidFunctionDeclarationException("Function body for '" + stmt.identifier + "' does not contain return statement", stmt.position)); } - return Type.VOID; + return new TypedStmt.FunctionDeclaration(stmt.identifier, stmt.parameters, stmt.returnType, body); } @Override - public Type visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) { - Type valueType = stmt.value.accept(this); - if(stmt.type != valueType) + public TypedStmt visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) { + TypedExpr value = stmt.value.accept(this); + if(stmt.type != value.type) errorHandler.add(new InvalidTypeException( "Type " + stmt.type + " declared for variable '" + stmt.identifier + "' does not match assigned value type " + - valueType, stmt.position)); - return Type.VOID; + value.type, stmt.position)); + return new TypedStmt.VariableDeclaration(stmt.type, stmt.identifier, value); } @Override - public Type visitReturnStmt(Stmt.Return stmt) { - stmt.setType(stmt.value.accept(this)); - return Type.VOID; + public TypedStmt visitReturnStmt(Stmt.Return stmt) { + return new TypedStmt.Return(stmt.value.accept(this)); } @Override - public Type visitIfStmt(Stmt.If stmt) { - if(stmt.condition.accept(this) != Type.BOOLEAN) throw new RuntimeException(); - stmt.trueBody.accept(this); - for(Pair clause : stmt.elseIfClauses) { - if(clause.getLeft().accept(this) != Type.BOOLEAN) throw new RuntimeException(); - clause.getRight().accept(this); - } - if(stmt.elseBody != null) { - stmt.elseBody.accept(this); - } - return Type.VOID; + public TypedStmt visitIfStmt(Stmt.If stmt) { + TypedExpr condition = stmt.condition.accept(this); + if(condition.type != Type.BOOLEAN) errorHandler.add(new InvalidTypeException("If statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position)); + + TypedStmt.Block trueBody = (TypedStmt.Block) stmt.trueBody.accept(this); + List> elseIfClauses = stmt.elseIfClauses.stream().map(c -> { + TypedExpr clauseCondition = c.getLeft().accept(this); + if (clauseCondition.type != Type.BOOLEAN) errorHandler.add(new InvalidTypeException("Else if clause conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position)); + return Pair.of(clauseCondition, (TypedStmt.Block) c.getRight().accept(this)); + }).toList(); + + Optional elseBody = stmt.elseBody.map(b -> (TypedStmt.Block) b.accept(this)); + + return new TypedStmt.If(condition, trueBody, elseIfClauses, elseBody); } @Override - public Type visitForStmt(Stmt.For stmt) { - stmt.initializer.accept(this); - if(stmt.condition.accept(this) != Type.BOOLEAN) throw new RuntimeException(); - stmt.incrementer.accept(this); - stmt.body.accept(this); - return Type.VOID; + public TypedStmt visitForStmt(Stmt.For stmt) { + TypedStmt initializer = stmt.initializer.accept(this); + TypedExpr condition = stmt.condition.accept(this); + if(condition.type != Type.BOOLEAN) errorHandler.add(new InvalidTypeException("For statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position)); + TypedExpr incrementer = stmt.incrementer.accept(this); + return new TypedStmt.For(initializer, condition, incrementer, (TypedStmt.Block) stmt.body.accept(this)); } @Override - public Type visitWhileStmt(Stmt.While stmt) { - if(stmt.condition.accept(this) != Type.BOOLEAN) throw new RuntimeException(); - stmt.body.accept(this); - return Type.VOID; + public TypedStmt visitWhileStmt(Stmt.While stmt) { + TypedExpr condition = stmt.condition.accept(this); + if(condition.type != Type.BOOLEAN) errorHandler.add(new InvalidTypeException("While statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position)); + return new TypedStmt.While(condition, (TypedStmt.Block) stmt.body.accept(this)); } @Override - public Type visitNoOpStmt(Stmt.NoOp stmt) { - return Type.VOID; + public TypedStmt visitNoOpStmt(Stmt.NoOp stmt) { + return new TypedStmt.NoOp(); } @Override - public Type visitBreakStmt(Stmt.Break stmt) { - return Type.VOID; + public TypedStmt visitBreakStmt(Stmt.Break stmt) { + return new TypedStmt.Break(); } @Override - public Type visitContinueStmt(Stmt.Continue stmt) { - return Type.VOID; + public TypedStmt visitContinueStmt(Stmt.Continue stmt) { + return new TypedStmt.Continue(); } - } diff --git a/common/addons/structure-terrascript-loader/src/test/java/codegen/CodeGenTest.java b/common/addons/structure-terrascript-loader/src/test/java/codegen/CodeGenTest.java index ecefe68cd..1bbb9311f 100644 --- a/common/addons/structure-terrascript-loader/src/test/java/codegen/CodeGenTest.java +++ b/common/addons/structure-terrascript-loader/src/test/java/codegen/CodeGenTest.java @@ -2,6 +2,7 @@ package codegen; import com.dfsek.terra.addons.terrascript.ErrorHandler; import com.dfsek.terra.addons.terrascript.ast.Stmt.Block; +import com.dfsek.terra.addons.terrascript.ast.TypedStmt; import com.dfsek.terra.addons.terrascript.codegen.TerraScript; import com.dfsek.terra.addons.terrascript.codegen.asm.TerraScriptClassGenerator; import com.dfsek.terra.addons.terrascript.lexer.Lexer; @@ -10,11 +11,59 @@ import com.dfsek.terra.addons.terrascript.semanticanalysis.SemanticAnalyzer; import org.junit.jupiter.api.Test; +import java.util.Objects; + + public class CodeGenTest { @Test public void test() { testValid(""" + + if (1 == 1) print("Dis is true"); + + num a = 1; + num b = 2; + str e = "test"; + + if (a <= b) { + print("a is <= b"); + } else { + print("a is not <= b"); + } + + if (e == "foo") { + print("e is == foo"); + } else if (e == "bar") { + print("e is == bar"); + } else { + print("e is not foo or bar"); + } + + if (true && false || (false && true)) { + print("Thin is tru"); + } else { + print("Thin is not tru :("); + } + + fun loopTwiceThenBreak() { + num i = 0; + while (true) { + if (i == 2) break; + i = i + 1; + } + } + + loopTwiceThenBreak(); + + retNum(); + bool bln = true; + + print(takesArgs("test", 3, true)); + print(retStr()); + + doStuff("Ayo", "world", true); + fun retNum(): num { return 3 + 3; } @@ -49,26 +98,14 @@ public class CodeGenTest { print("c is false"); } } - - num a = 1; - num b = 2; - str e = "test"; - - retNum(); - bool bln = true; - - print(takesArgs("test", 3, true)); - print(retStr()); - - doStuff("Ay0o", "world", true); """); } private void testValid(String validSource) { try { Block script = Parser.parse(new Lexer(validSource).analyze()); - SemanticAnalyzer.analyze(script, new ErrorHandler()); - TerraScript ts = new TerraScriptClassGenerator("./build/codegentest").generate(script); + TypedStmt.Block typedScript = SemanticAnalyzer.analyze(script, new ErrorHandler()); + TerraScript ts = new TerraScriptClassGenerator("./build/codegentest").generate(typedScript); ts.execute(); } catch(Exception e) { throw new RuntimeException(e); diff --git a/common/addons/structure-terrascript-loader/src/test/java/legacy/ParserTest.java b/common/addons/structure-terrascript-loader/src/test/java/legacy/ParserTest.java deleted file mode 100644 index af0be2eb8..000000000 --- a/common/addons/structure-terrascript-loader/src/test/java/legacy/ParserTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2020-2021 Polyhedral Development - * - * The Terra Core Addons are licensed under the terms of the MIT License. For more details, - * reference the LICENSE file in this module's root directory. - */ - -package legacy; - - -import com.dfsek.terra.addons.terrascript.Type; -import com.dfsek.terra.addons.terrascript.lexer.Lexer; -import com.dfsek.terra.addons.terrascript.legacy.parser.lang.Scope.ScopeBuilder; - -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.List; -import java.util.Objects; - -import com.dfsek.terra.addons.terrascript.legacy.parser.Parser; -import com.dfsek.terra.addons.terrascript.legacy.parser.exceptions.ParseException; -import com.dfsek.terra.addons.terrascript.legacy.parser.lang.Executable; -import com.dfsek.terra.addons.terrascript.legacy.parser.lang.ImplementationArguments; -import com.dfsek.terra.addons.terrascript.legacy.parser.lang.Expression; -import com.dfsek.terra.addons.terrascript.legacy.parser.lang.Scope; -import com.dfsek.terra.addons.terrascript.legacy.parser.lang.functions.Function; -import com.dfsek.terra.addons.terrascript.legacy.parser.lang.functions.FunctionBuilder; -import com.dfsek.terra.addons.terrascript.lexer.SourcePosition; - - -public class ParserTest { - @Test - public void parse() throws IOException, ParseException { - Lexer lexer = new Lexer(IOUtils.toString(Objects.requireNonNull(getClass().getResourceAsStream("/test.tesf")), Charset.defaultCharset())); - Parser parser = new Parser(lexer); - - ScopeBuilder scope = new ScopeBuilder(); - scope.registerFunction("test", new FunctionBuilder() { - @Override - public Test1 build(List> argumentList, SourcePosition position) { - return new Test1(argumentList.get(0), argumentList.get(1), position); - } - - @Override - public int argNumber() { - return 2; - } - - @Override - public Type getArgument(int position) { - return switch(position) { - case 0 -> Type.STRING; - case 1 -> Type.NUMBER; - default -> null; - }; - } - - }); - - long l = System.nanoTime(); - Executable block = parser.parse(scope); - long t = System.nanoTime() - l; - System.out.println("Took " + (double) t / 1000000); - - block.execute(null); - - block.execute(null); - } - - private static class Test1 implements Function { - private final Expression a; - private final Expression b; - private final SourcePosition position; - - public Test1(Expression a, Expression b, SourcePosition position) { - this.a = a; - this.b = b; - this.position = position; - } - - @Override - public Void evaluate(ImplementationArguments implementationArguments, Scope scope) { - System.out.println("string: " + a.evaluate(implementationArguments, scope) + ", double: " + - b.evaluate(implementationArguments, scope)); - return null; - } - - @Override - public SourcePosition getPosition() { - return position; - } - - @Override - public Type returnType() { - return Type.VOID; - } - } -} diff --git a/common/addons/structure-terrascript-loader/src/test/java/semanticanalysis/SemanticAnalyzerTest.java b/common/addons/structure-terrascript-loader/src/test/java/semanticanalysis/SemanticAnalyzerTest.java index 8639898c2..3cc012714 100644 --- a/common/addons/structure-terrascript-loader/src/test/java/semanticanalysis/SemanticAnalyzerTest.java +++ b/common/addons/structure-terrascript-loader/src/test/java/semanticanalysis/SemanticAnalyzerTest.java @@ -92,7 +92,7 @@ public class SemanticAnalyzerTest { testValid("fun returnVoid() { return (); }"); } - @Test + //Not implemented yet @Test public void testControlFlowAnalysis() { testValid(""" diff --git a/common/addons/structure-terrascript-loader/src/test/resources/test.tesf b/common/addons/structure-terrascript-loader/src/test/resources/test.tesf deleted file mode 100644 index 2368bd948..000000000 --- a/common/addons/structure-terrascript-loader/src/test/resources/test.tesf +++ /dev/null @@ -1,116 +0,0 @@ -fun myFunction(functionString: str, functionNumber: num): str { - test(functionString, functionNumber); - fun nestedFunction() { - test("Hello from nested function", 69); - } - nestedFunction(); - return "Return to sender"; -} - -str functionResult = myFunction("Hello from myFunction", 535); - -test(functionResult, 58); - -fun noReturn() test("Single statement function", 42); - -noReturn(); - -bool thing1 = 2 > (2+2) || false; - -if(2 > 2 || 3 + 4 <= 2 && 4 + 5 > 2 / 3) { - test("ok", 2); -} - -test("minecraft:green_w" + "ool", (2 * (3+1) * (2 * (1+1)))); -// - -num testVar = 3.4; -bool boolean = true; -str stringVar = "hello!"; - -num precedence = 3 + 2 * 2 + 3; -test("precedence: " + precedence, 2); -num precedence2 = 3 * 2 + 2 * 3; -test("precedence 2: " + precedence2, 2); - -bool iftest = false; - -bool truetest = false; - -num iterator = 0; -num thing = 4 - 2-2+2-2+2; -test("4 - 2 = " + thing, 2); - -thing = -2; -test("-2 = " + thing, 2); -thing = -thing; -test("--2 = " + thing, 2); - -for(;;) { - break; -} - -for(num i = 0; i < 5; i = i + 1) { - test("i = " + i, iterator); - if(i > 1 + 1) { - test("more than 2", iterator); - continue; - } -} - -for(num i = 0; i < 5; i = i + 1) { - test("i = " + i, iterator); -} - -for(num j = 0; j < 5; j = j + 1) test("single statement j = " + j, iterator); - -if(4 + 2 == 2 + 4) { - test("new thing " + 2, iterator); -} - -while(iterator < 5) { - test("always, even after " + 2, iterator); - iterator = iterator + 1; - if(iterator > 2) { - continue; - } - test("not after " + 2, iterator); -} - -if(true) test("single statement" + 2, iterator); -else if(true) test("another single statement" + 2, iterator); - -if(true) { - test("true!" + 2, iterator); -} else { - test("false!" + 2, iterator); - } - -if(false) { - test("true!" + 2, iterator); -} else { - test("false!" + 2, iterator); -} - -if(false) { - test("true again!" + 2, iterator); -} else if(true == true) { - test("false again!" + 2, iterator); -} else { - test("not logged!" + 2, iterator); -} - - - -// comment - -/* -fsdfsd -*/ - -test("fdsgdf" + 2, 1 + testVar); - -if(true && !(boolean && false) && true) { - num scopedVar = 2; - test("if statement" + 2 + stringVar, 1 + testVar + scopedVar); -} \ No newline at end of file