From b1bfe00bf3440fb35b6815a8dd4d916d406b6a29 Mon Sep 17 00:00:00 2001 From: Astrash Date: Fri, 8 Sep 2023 11:36:40 +1000 Subject: [PATCH] Implement function and variable codegen --- .../build.gradle.kts | 123 +++++++------ .../terra/addons/terrascript/Environment.java | 18 +- .../dfsek/terra/addons/terrascript/Type.java | 23 ++- .../terrascript/codegen/TerraScript.java | 19 ++ .../terrascript/codegen/asm/TerraScript.java | 7 - .../asm/TerraScriptClassGenerator.java | 166 +++++++++++++++--- .../FunctionReferenceAnalyzer.java | 2 +- .../semanticanalysis/ScopeAnalyzer.java | 5 +- .../semanticanalysis/TypeChecker.java | 20 ++- .../addons/terrascript/util/ASMUtil.java | 7 + .../src/test/java/codegen/CodeGenTest.java | 77 ++++++++ 11 files changed, 370 insertions(+), 97 deletions(-) create mode 100644 common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/TerraScript.java delete mode 100644 common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/asm/TerraScript.java create mode 100644 common/addons/structure-terrascript-loader/src/test/java/codegen/CodeGenTest.java diff --git a/common/addons/structure-terrascript-loader/build.gradle.kts b/common/addons/structure-terrascript-loader/build.gradle.kts index 883cc5395..32d5ba99b 100644 --- a/common/addons/structure-terrascript-loader/build.gradle.kts +++ b/common/addons/structure-terrascript-loader/build.gradle.kts @@ -1,10 +1,12 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import java.util.* version = version("1.1.0") dependencies { api("commons-io:commons-io:2.7") api("org.ow2.asm:asm:9.5") + api("org.ow2.asm:asm-commons:9.5") compileOnlyApi(project(":common:addons:manifest-addon-loader")) implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama) testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama) @@ -18,44 +20,38 @@ 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 ASTNode(val name: String, val constructorFields: List>, val mutableFields: List> = emptyList()) + // Auto generate AST classes rather than writing them by hand tasks.register("genTerrascriptAstClasses") { + + val packageName = astPackage.toRelativeString(astSourceSet).replace('/', '.') - fun generateClass(name: String, imports: List, nodes: List>>) { + fun generateClass(clazz: ASTClass) { val src = StringBuilder() src.appendLine("package $packageName;\n"); - for (imprt in imports) src.appendLine("import $imprt;") + for (imprt in clazz.imports) src.appendLine("import $imprt;") src.appendLine(""" import com.dfsek.terra.addons.terrascript.lexer.SourcePosition; - import com.dfsek.terra.addons.terrascript.Environment; /** * Auto-generated class via genTerrascriptAstClasses gradle task */ - public abstract class $name { + public abstract class ${clazz.name} { public final SourcePosition position; - private Environment environment; - public $name(SourcePosition position) { + public ${clazz.name}(SourcePosition position) { this.position = position; } - - public Environment getEnvironment() { - if (this.environment == null) throw new RuntimeException("Compilation bug! environment has not been set yet for AST node"); - return environment; - } - - public void setEnvironment(Environment environment) { - if (this.environment != null) throw new RuntimeException("Compilation bug! environment has already been set for AST node and cannot be changed"); - this.environment = environment; - } public interface Visitor { """.trimIndent()) - for (node in nodes) { - src.appendLine(" R visit${node.first}$name(${node.first} ${name.toLowerCase()});") + for (node in clazz.nodes) { + src.appendLine(" R visit${node.name}${clazz.name}(${node.name} ${clazz.name.toLowerCase()});") } src.appendLine(""" @@ -65,79 +61,98 @@ tasks.register("genTerrascriptAstClasses") { | public abstract R accept(Visitor visitor); """.trimMargin()) - for (node in nodes) { + for (node in clazz.nodes) { src.appendLine() // Inner class declaration - src.appendLine(" public static class ${node.first} extends $name {\n") + src.appendLine(" public static class ${node.name} extends ${clazz.name} {\n") // Add fields - for (field in node.second) { - src.appendLine(" public final $field;") + for (field in node.constructorFields) { + src.appendLine(" public final ${field.second} ${field.first};") + } + for (field in node.mutableFields) { + src.appendLine(" private ${field.second} ${field.first};") } src.appendLine() // Add constructor - src.append(" public ${node.first}(") - for (field in node.second) - src.append("$field, ") + src.append(" public ${node.name}(") + for (field in node.constructorFields) + src.append("${field.second} ${field.first}, ") src.appendLine("SourcePosition position) {\n super(position);") - for (field in node.second) { - val fieldName = field.split(' ').last() - src.appendLine(" this.$fieldName = $fieldName;") + for (field in node.constructorFields) { + src.appendLine(" this.${field.first} = ${field.first};") + } + src.appendLine(" }") + + // Add getters and setters for mutable fields + for (field in node.mutableFields) { + src.appendLine(""" + | + | public void set${field.first.capitalize()}(${field.second} value) { + | this.${field.first} = value; + | } + | + | public ${field.second} get${field.first.capitalize()}() { + | if (this.${field.first} == null) throw new RuntimeException("Compilation bug! Field ${field.first} has not been set yet"); + | return this.${field.first}; + | } + """.trimMargin()) } src.appendLine(""" - | } | | @Override | public R accept(Visitor visitor) { - | return visitor.visit${node.first}$name(this); + | return visitor.visit${node.name}${clazz.name}(this); | } | } """.trimMargin()) } src.appendLine("}") - val outputFile = astPackage.resolve("$name.java") + val outputFile = astPackage.resolve("${clazz.name}.java") outputFile.writeText(src.toString()) } doLast { astSourceSet.deleteRecursively() astPackage.mkdirs() - generateClass("Expr", listOf( + 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( - Pair("Binary", listOf("Expr left", "BinaryOperator operator", "Expr right",)), - Pair("Grouping", listOf("Expr expression")), - Pair("Literal", listOf("Object value", "Type type")), - Pair("Unary", listOf("UnaryOperator operator", "Expr operand")), - Pair("Call", listOf("String identifier", "List arguments")), - Pair("Variable", listOf("String identifier")), - Pair("Assignment", listOf("Variable lValue", "Expr rValue")), - Pair("Void", listOf()), - )) - generateClass("Stmt", 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( - Pair("Expression", listOf("Expr expression")), - Pair("Block", listOf("List statements")), - Pair("FunctionDeclaration", listOf("String identifier", "List> parameters", "Type type", "Block body")), - Pair("VariableDeclaration", listOf("Type type", "String identifier", "Expr value")), - Pair("Return", listOf("Expr value")), - Pair("If", listOf("Expr condition", "Block trueBody", "List> elseIfClauses", "Block elseBody")), - Pair("For", listOf("Stmt initializer", "Expr condition", "Expr incrementer", "Block body")), - Pair("While", listOf("Expr condition", "Block body")), - Pair("NoOp", listOf()), - Pair("Break", listOf()), - Pair("Continue", 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()), + ))) } } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/Environment.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/Environment.java index b7824f14c..378d3a27b 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/Environment.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/Environment.java @@ -1,14 +1,23 @@ package com.dfsek.terra.addons.terrascript; import javax.annotation.Nullable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import com.dfsek.terra.addons.terrascript.Environment.ScopeException.NonexistentSymbolException; import com.dfsek.terra.addons.terrascript.Environment.ScopeException.SymbolAlreadyExistsException; import com.dfsek.terra.addons.terrascript.Environment.ScopeException.SymbolTypeMismatchException; import com.dfsek.terra.addons.terrascript.Environment.Symbol.Function; import com.dfsek.terra.addons.terrascript.Environment.Symbol.Variable; +import com.dfsek.terra.addons.terrascript.codegen.TerraScript; import com.dfsek.terra.api.util.generic.pair.Pair; @@ -18,7 +27,7 @@ public class Environment { private final boolean canAccessOuterVariables; - private final HashMap symbolTable = new HashMap<>(); + private final Map symbolTable = new HashMap<>(); private final boolean inLoop; @@ -26,6 +35,13 @@ public class Environment { this.outer = outer; this.canAccessOuterVariables = canAccessOuterVariables; this.inLoop = inLoop; + // Populate symbol tables with built-in Java implemented methods + TerraScript.BUILTIN_FUNCTIONS.forEach((name, method) -> symbolTable + .put(name, + new Function( + Type.from(method.getReturnType()).orElseThrow(() -> new RuntimeException("")), + // Map Java classes to TerraScript types + IntStream.range(0, method.getParameterCount()).mapToObj(i -> Pair.of("param" + i, Type.from(method.getParameterTypes()[i]).orElseThrow(() -> new RuntimeException("")))).toList()))); } public static Environment global() { diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/Type.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/Type.java index dbcfff372..6bd704d5b 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/Type.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/Type.java @@ -1,9 +1,30 @@ package com.dfsek.terra.addons.terrascript; +import java.util.Optional; + + // TODO - Make not enum public enum Type { NUMBER, STRING, BOOLEAN, - VOID, + VOID; + + public java.lang.reflect.Type javaType() { + return switch(this) { + case NUMBER -> double.class; + case STRING -> String.class; + case BOOLEAN -> boolean.class; + case VOID -> void.class; + }; + } + + public static Optional from(Class clazz) { + return Optional.ofNullable( + clazz == double.class ? NUMBER : + clazz == String.class ? STRING : + clazz == boolean.class ? BOOLEAN : + clazz == void.class ? VOID : + null); + } } 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 new file mode 100644 index 000000000..db9271a8c --- /dev/null +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/TerraScript.java @@ -0,0 +1,19 @@ +package com.dfsek.terra.addons.terrascript.codegen; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + + +public interface TerraScript { + + void execute(); + + Map BUILTIN_FUNCTIONS = new HashMap<>() {{ + try { + put("print", System.out.getClass().getMethod("println", String.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/TerraScript.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/asm/TerraScript.java deleted file mode 100644 index a4ee94c33..000000000 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/codegen/asm/TerraScript.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.dfsek.terra.addons.terrascript.codegen.asm; - -public interface TerraScript { - - void execute(); - -} 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 88fad9482..a191e9b56 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,5 +1,7 @@ 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; @@ -22,24 +24,26 @@ 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.codegen.TerraScript; +import com.dfsek.terra.addons.terrascript.util.ASMUtil; +import com.dfsek.terra.api.util.generic.pair.Pair; + import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.LocalVariablesSorter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static com.dfsek.terra.addons.terrascript.util.ASMUtil.dynamicName; -import static org.objectweb.asm.Opcodes.ACC_PUBLIC; -import static org.objectweb.asm.Opcodes.ALOAD; -import static org.objectweb.asm.Opcodes.INVOKESPECIAL; -import static org.objectweb.asm.Opcodes.IOR; -import static org.objectweb.asm.Opcodes.RETURN; - public class TerraScriptClassGenerator { @@ -55,6 +59,12 @@ public class TerraScriptClassGenerator { this.debugPath = debugPath; } + /** + * + * @param root Assumed to be semantically correct + * @return Generated TerraScript instance + * @throws IOException + */ public TerraScript generate(Block root) throws IOException { String targetClassName = dynamicName(TARGET_CLASS); String generatedClassName = targetClassName + "_GENERATED_" + generationCount; @@ -65,11 +75,11 @@ public class TerraScriptClassGenerator { classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, generatedClassName, null, "java/lang/Object", new String[]{ targetClassName }); // Generate constructor method - MethodVisitor constructor = classWriter.visitMethod(ACC_PUBLIC, "", "()V", null, null); + MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); constructor.visitCode(); - constructor.visitVarInsn(ALOAD, 0); // Put this reference on stack - constructor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); - constructor.visitInsn(RETURN); // Void return + constructor.visitVarInsn(Opcodes.ALOAD, 0); // Put this reference on stack + constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + constructor.visitInsn(Opcodes.RETURN); // Void return constructor.visitMaxs(0, 0); constructor.visitEnd(); @@ -79,16 +89,14 @@ public class TerraScriptClassGenerator { MethodExtractor extractor = new MethodExtractor(methodName); new ClassReader(targetClassName).accept(extractor, 0); String description = extractor.methodDescription; - MethodVisitor execute = classWriter.visitMethod(Opcodes.ACC_PUBLIC, methodName, description, null, null); - execute.visitCode(); // Start method body - - execute.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); - new BytecodeGenerator(execute).visitBlockStmt(root); - execute.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Z)V", false); - - execute.visitInsn(RETURN); - execute.visitMaxs(0, 0); - execute.visitEnd(); + int exeAcc = Opcodes.ACC_PUBLIC; + MethodVisitor executeMethod = classWriter.visitMethod(exeAcc, methodName, description, null, null); + executeMethod.visitCode(); // Start method body + new MethodBytecodeGenerator(classWriter, generatedClassName, executeMethod, exeAcc, description).generate(root); // Generate bytecode + // Finish up method + executeMethod.visitInsn(Opcodes.RETURN); + executeMethod.visitMaxs(0, 0); + executeMethod.visitEnd(); // Finished generating class classWriter.visitEnd(); @@ -119,12 +127,31 @@ public class TerraScriptClassGenerator { } } - private static class BytecodeGenerator implements Stmt.Visitor, Expr.Visitor { + private static class MethodBytecodeGenerator implements Stmt.Visitor, Expr.Visitor { + + private final ClassWriter classWriter; + + private final String className; private final MethodVisitor method; - public BytecodeGenerator(MethodVisitor method) { + private final String descriptor; + + private final LocalVariablesSorter lvs; + + private final Map lvTable = new HashMap<>(); + + public MethodBytecodeGenerator(ClassWriter classWriter, String className, MethodVisitor method, int access, String descriptor) { + this.classWriter = classWriter; + this.className = className; this.method = method; + this.descriptor = descriptor; + this.lvs = new LocalVariablesSorter(access, descriptor, method); + } + + public void generate(Block root) { + this.visitBlockStmt(root); + } @Override @@ -181,7 +208,14 @@ public class TerraScriptClassGenerator { method.visitInsn(Opcodes.ICONST_0); method.visitLabel(finished); } - case ADD -> method.visitInsn(Opcodes.DADD); + case ADD -> { + switch(expr.getType()) { + 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()); + } + } case SUBTRACT -> method.visitInsn(Opcodes.DSUB); case MULTIPLY -> method.visitInsn(Opcodes.DMUL); case DIVIDE -> method.visitInsn(Opcodes.DDIV); @@ -209,16 +243,43 @@ public class TerraScriptClassGenerator { @Override public Void visitCallExpr(Call expr) { + Symbol.Function function = expr.getSymbol(); + if (TerraScript.BUILTIN_FUNCTIONS.containsKey(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); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); + } + 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); return null; } @Override public Void visitVariableExpr(Variable expr) { + Type varType = expr.getSymbol().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 + "'"); + }, lvTable.get(expr.identifier)); return null; } @Override public Void visitAssignmentExpr(Assignment expr) { + expr.rValue.accept(this); + Type type = expr.lValue.getSymbol().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 + "'"); + }, lvTable.get(expr.lValue.identifier)); return null; } @@ -239,18 +300,77 @@ public class TerraScriptClassGenerator { 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; + List parameterTypes = stmt.parameters.stream().map(Pair::getRight).toList(); + MethodVisitor method = classWriter.visitMethod(access, stmt.identifier, getFunctionDescriptor(parameterTypes, stmt.type), null, null); + + method.visitCode(); // Start method body + + MethodBytecodeGenerator funcGenerator = new MethodBytecodeGenerator(classWriter, className, method, access, descriptor); + + // Add local variable indexes for each parameter + int lvidx = 0; + for (Pair parameter : stmt.parameters) { + funcGenerator.lvTable.put(parameter.getLeft(), lvidx); + lvidx += switch(parameter.getRight()) { + case NUMBER -> 2; + case STRING, BOOLEAN -> 1; + default -> throw new RuntimeException("Unable to register local variable index for parameter, unknown parameter type '" + parameter.getRight() + "'"); + }; + } + + // Generate method bytecode + funcGenerator.generate(stmt.body); + + // Finish up + method.visitInsn(Opcodes.RETURN); + method.visitMaxs(0, 0); + method.visitEnd(); + return null; } @Override public Void visitVariableDeclarationStmt(VariableDeclaration stmt) { + stmt.value.accept(this); + lvTable.put(stmt.identifier, lvs.newLocal(ASMUtil.tsTypeToAsmType(stmt.type))); + method.visitVarInsn(switch(stmt.type) { + case NUMBER -> Opcodes.DSTORE; + case STRING -> Opcodes.ASTORE; + case BOOLEAN -> Opcodes.ISTORE; + default -> throw new RuntimeException("Unable to declare local variable, unknown parameter type '" + stmt.type + "'"); + }, lvTable.get(stmt.identifier)); return null; } @Override public Void visitReturnStmt(Return stmt) { + stmt.value.accept(this); + switch(stmt.getType()) { + case NUMBER -> method.visitInsn(Opcodes.DRETURN); + case STRING -> method.visitInsn(Opcodes.ARETURN); + case BOOLEAN -> method.visitInsn(Opcodes.IRETURN); + } return null; } 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 2983af853..2041c0208 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.getEnvironment().getFunction(id); + expr.getSymbol(); } catch(NonexistentSymbolException e) { errorHandler.add( new UndefinedReferenceException("No function by the name '" + id + "' is defined in this scope", expr.position)); 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 adeed6a06..4a1d5a1b4 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.setEnvironment(currentScope); + expr.setSymbol(currentScope.getFunction(expr.identifier)); expr.arguments.forEach(e -> e.accept(this)); return null; } @@ -76,7 +76,7 @@ public class ScopeAnalyzer implements Visitor, Stmt.Visitor { } catch(SymbolTypeMismatchException e) { errorHandler.add(new ParseException("Identifier '" + id + "' is not defined as a variable", expr.position)); } - expr.setEnvironment(currentScope); + expr.setSymbol(currentScope.getVariable(expr.identifier)); return null; } @@ -136,7 +136,6 @@ public class ScopeAnalyzer implements Visitor, Stmt.Visitor { errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope", stmt.position)); } - stmt.setEnvironment(currentScope); return null; } 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 106ea096d..07d75bf5d 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 @@ -51,8 +51,14 @@ public class TypeChecker implements Visitor, Stmt.Visitor { yield Type.BOOLEAN; } case ADD -> { - if(left == Type.NUMBER && right == Type.NUMBER) yield Type.NUMBER; - if(left == Type.STRING || right == Type.STRING) yield Type.STRING; + if(left == Type.NUMBER && right == Type.NUMBER) { + expr.setType(Type.NUMBER); + yield Type.NUMBER; + } + if(left == Type.STRING || right == Type.STRING) { + expr.setType(Type.STRING); + yield Type.STRING; + } throw new RuntimeException("Addition operands must be either both numbers, or one of type string"); } case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> { @@ -92,10 +98,10 @@ public class TypeChecker implements Visitor, Stmt.Visitor { public Type visitCallExpr(Call expr) { String id = expr.identifier; - Environment.Symbol.Function signature = expr.getEnvironment().getFunction(id); + Environment.Symbol.Function signature = expr.getSymbol(); List argumentTypes = expr.arguments.stream().map(a -> a.accept(this)).toList(); - List> parameters = signature.parameters; + List parameters = signature.parameters.stream().map(Pair::getRight).toList(); if(argumentTypes.size() != parameters.size()) errorHandler.add(new ParseException( @@ -103,7 +109,7 @@ public class TypeChecker implements Visitor, Stmt.Visitor { " arguments", expr.position)); for(int i = 0; i < parameters.size(); i++) { - Type expectedType = parameters.get(i).getRight(); + Type expectedType = parameters.get(i); Type providedType = argumentTypes.get(i); if(expectedType != providedType) errorHandler.add(new InvalidTypeException( @@ -116,7 +122,7 @@ public class TypeChecker implements Visitor, Stmt.Visitor { @Override public Type visitVariableExpr(Variable expr) { - return expr.getEnvironment().getVariable(expr.identifier).type; + return expr.getSymbol().type; } @Override @@ -182,7 +188,7 @@ public class TypeChecker implements Visitor, Stmt.Visitor { @Override public Type visitReturnStmt(Stmt.Return stmt) { - stmt.value.accept(this); + stmt.setType(stmt.value.accept(this)); return Type.VOID; } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/util/ASMUtil.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/util/ASMUtil.java index 7859d05f7..911ac10ef 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/util/ASMUtil.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/util/ASMUtil.java @@ -1,5 +1,8 @@ package com.dfsek.terra.addons.terrascript.util; +import com.dfsek.terra.addons.terrascript.Type; + + public class ASMUtil { /** @@ -10,4 +13,8 @@ public class ASMUtil { public static String dynamicName(Class clazz) { return clazz.getCanonicalName().replace('.', '/'); } + + public static org.objectweb.asm.Type tsTypeToAsmType(Type type) { + return org.objectweb.asm.Type.getType((Class) type.javaType()); + } } 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 new file mode 100644 index 000000000..ecefe68cd --- /dev/null +++ b/common/addons/structure-terrascript-loader/src/test/java/codegen/CodeGenTest.java @@ -0,0 +1,77 @@ +package codegen; + +import com.dfsek.terra.addons.terrascript.ErrorHandler; +import com.dfsek.terra.addons.terrascript.ast.Stmt.Block; +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; +import com.dfsek.terra.addons.terrascript.parser.Parser; +import com.dfsek.terra.addons.terrascript.semanticanalysis.SemanticAnalyzer; + +import org.junit.jupiter.api.Test; + +public class CodeGenTest { + + @Test + public void test() { + testValid(""" + fun retNum(): num { + return 3 + 3; + } + + fun retBool(): bool { + return true; + } + + fun concatThree(a: str, b: str, c: str): str { + return a + b + c; + } + + fun retStr(): str { + fun concatTwo(a: str, b: str): str { + return a + b; + } + str hello = "Hell"; + hello = concatTwo(hello, "o"); + str world = "world!"; + return concatThree(hello, " ", world); + } + + fun takesArgs(a: str, b: num, c: bool): str { + return a; + } + + fun doStuff(a: str, b: str, c: bool) { + print("Doing stuff"); + if (c) { + print(concatThree(a, " ", b)); + } else { + 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); + ts.execute(); + } catch(Exception e) { + throw new RuntimeException(e); + } + } +}