Implement function and variable codegen

This commit is contained in:
Astrash
2023-09-08 11:36:40 +10:00
parent 9a75ee78a1
commit b1bfe00bf3
11 changed files with 370 additions and 97 deletions
@@ -1,10 +1,12 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import java.util.*
version = version("1.1.0") version = version("1.1.0")
dependencies { dependencies {
api("commons-io:commons-io:2.7") api("commons-io:commons-io:2.7")
api("org.ow2.asm:asm:9.5") api("org.ow2.asm:asm:9.5")
api("org.ow2.asm:asm-commons:9.5")
compileOnlyApi(project(":common:addons:manifest-addon-loader")) compileOnlyApi(project(":common:addons:manifest-addon-loader"))
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama) implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama) testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
@@ -18,44 +20,38 @@ tasks.named<ShadowJar>("shadowJar") {
val astSourceSet = buildDir.resolve("generated/ast") val astSourceSet = buildDir.resolve("generated/ast")
val astPackage = astSourceSet.resolve("com/dfsek/terra/addons/terrascript/ast") val astPackage = astSourceSet.resolve("com/dfsek/terra/addons/terrascript/ast")
data class ASTClass(val name: String, val imports: List<String>, val nodes: List<ASTNode>)
data class ASTNode(val name: String, val constructorFields: List<Pair<String, String>>, val mutableFields: List<Pair<String, String>> = emptyList())
// Auto generate AST classes rather than writing them by hand // Auto generate AST classes rather than writing them by hand
tasks.register("genTerrascriptAstClasses") { tasks.register("genTerrascriptAstClasses") {
val packageName = astPackage.toRelativeString(astSourceSet).replace('/', '.') val packageName = astPackage.toRelativeString(astSourceSet).replace('/', '.')
fun generateClass(name: String, imports: List<String>, nodes: List<Pair<String, List<String>>>) { fun generateClass(clazz: ASTClass) {
val src = StringBuilder() val src = StringBuilder()
src.appendLine("package $packageName;\n"); src.appendLine("package $packageName;\n");
for (imprt in imports) src.appendLine("import $imprt;") for (imprt in clazz.imports) src.appendLine("import $imprt;")
src.appendLine(""" src.appendLine("""
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition; import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
import com.dfsek.terra.addons.terrascript.Environment;
/** /**
* Auto-generated class via genTerrascriptAstClasses gradle task * Auto-generated class via genTerrascriptAstClasses gradle task
*/ */
public abstract class $name { public abstract class ${clazz.name} {
public final SourcePosition position; public final SourcePosition position;
private Environment environment;
public $name(SourcePosition position) { public ${clazz.name}(SourcePosition position) {
this.position = 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<R> { public interface Visitor<R> {
""".trimIndent()) """.trimIndent())
for (node in nodes) { for (node in clazz.nodes) {
src.appendLine(" R visit${node.first}$name(${node.first} ${name.toLowerCase()});") src.appendLine(" R visit${node.name}${clazz.name}(${node.name} ${clazz.name.toLowerCase()});")
} }
src.appendLine(""" src.appendLine("""
@@ -65,79 +61,98 @@ tasks.register("genTerrascriptAstClasses") {
| public abstract <R> R accept(Visitor<R> visitor); | public abstract <R> R accept(Visitor<R> visitor);
""".trimMargin()) """.trimMargin())
for (node in nodes) { for (node in clazz.nodes) {
src.appendLine() src.appendLine()
// Inner class declaration // 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 // Add fields
for (field in node.second) { for (field in node.constructorFields) {
src.appendLine(" public final $field;") src.appendLine(" public final ${field.second} ${field.first};")
}
for (field in node.mutableFields) {
src.appendLine(" private ${field.second} ${field.first};")
} }
src.appendLine() src.appendLine()
// Add constructor // Add constructor
src.append(" public ${node.first}(") src.append(" public ${node.name}(")
for (field in node.second) for (field in node.constructorFields)
src.append("$field, ") src.append("${field.second} ${field.first}, ")
src.appendLine("SourcePosition position) {\n super(position);") src.appendLine("SourcePosition position) {\n super(position);")
for (field in node.second) { for (field in node.constructorFields) {
val fieldName = field.split(' ').last() src.appendLine(" this.${field.first} = ${field.first};")
src.appendLine(" this.$fieldName = $fieldName;") }
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(""" src.appendLine("""
| }
| |
| @Override | @Override
| public <R> R accept(Visitor<R> visitor) { | public <R> R accept(Visitor<R> visitor) {
| return visitor.visit${node.first}$name(this); | return visitor.visit${node.name}${clazz.name}(this);
| } | }
| } | }
""".trimMargin()) """.trimMargin())
} }
src.appendLine("}") src.appendLine("}")
val outputFile = astPackage.resolve("$name.java") val outputFile = astPackage.resolve("${clazz.name}.java")
outputFile.writeText(src.toString()) outputFile.writeText(src.toString())
} }
doLast { doLast {
astSourceSet.deleteRecursively() astSourceSet.deleteRecursively()
astPackage.mkdirs() astPackage.mkdirs()
generateClass("Expr", listOf( generateClass(ASTClass("Expr", listOf(
"com.dfsek.terra.addons.terrascript.Type", "com.dfsek.terra.addons.terrascript.Type",
"com.dfsek.terra.addons.terrascript.parser.UnaryOperator", "com.dfsek.terra.addons.terrascript.parser.UnaryOperator",
"com.dfsek.terra.addons.terrascript.parser.BinaryOperator", "com.dfsek.terra.addons.terrascript.parser.BinaryOperator",
"com.dfsek.terra.addons.terrascript.Environment.Symbol",
"java.util.List", "java.util.List",
), ),
listOf( listOf(
Pair("Binary", listOf("Expr left", "BinaryOperator operator", "Expr right",)), ASTNode("Binary", listOf("left" to "Expr", "operator" to "BinaryOperator", "right" to "Expr",), listOf("type" to "Type")),
Pair("Grouping", listOf("Expr expression")), ASTNode("Grouping", listOf("expression" to "Expr")),
Pair("Literal", listOf("Object value", "Type type")), ASTNode("Literal", listOf("value" to "Object", "type" to "Type")),
Pair("Unary", listOf("UnaryOperator operator", "Expr operand")), ASTNode("Unary", listOf("operator" to "UnaryOperator", "operand" to "Expr")),
Pair("Call", listOf("String identifier", "List<Expr> arguments")), ASTNode("Call", listOf("identifier" to "String", "arguments" to "List<Expr>"), listOf("symbol" to "Symbol.Function")),
Pair("Variable", listOf("String identifier")), ASTNode("Variable", listOf("identifier" to "String"), listOf("symbol" to "Symbol.Variable")),
Pair("Assignment", listOf("Variable lValue", "Expr rValue")), ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "Expr")),
Pair("Void", listOf()), ASTNode("Void", listOf()),
)) )))
generateClass("Stmt", listOf( generateClass(ASTClass("Stmt", listOf(
"com.dfsek.terra.addons.terrascript.Type", "com.dfsek.terra.addons.terrascript.Type",
"com.dfsek.terra.api.util.generic.pair.Pair", "com.dfsek.terra.api.util.generic.pair.Pair",
"com.dfsek.terra.addons.terrascript.Environment.Symbol",
"java.util.List", "java.util.List",
), ),
listOf( listOf(
Pair("Expression", listOf("Expr expression")), ASTNode("Expression", listOf("expression" to "Expr")),
Pair("Block", listOf("List<Stmt> statements")), ASTNode("Block", listOf("statements" to "List<Stmt>")),
Pair("FunctionDeclaration", listOf("String identifier", "List<Pair<String, Type>> parameters", "Type type", "Block body")), ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List<Pair<String, Type>>", "type" to "Type", "body" to "Block"), listOf("symbol" to "Symbol.Function")),
Pair("VariableDeclaration", listOf("Type type", "String identifier", "Expr value")), ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "Expr")),
Pair("Return", listOf("Expr value")), ASTNode("Return", listOf("value" to "Expr"), listOf("type" to "Type")),
Pair("If", listOf("Expr condition", "Block trueBody", "List<Pair<Expr, Block>> elseIfClauses", "Block elseBody")), ASTNode("If", listOf("condition" to "Expr", "trueBody" to "Block", "elseIfClauses" to "List<Pair<Expr, Block>>", "elseBody" to "Block")),
Pair("For", listOf("Stmt initializer", "Expr condition", "Expr incrementer", "Block body")), ASTNode("For", listOf("initializer" to "Stmt", "condition" to "Expr", "incrementer" to "Expr", "body" to "Block")),
Pair("While", listOf("Expr condition", "Block body")), ASTNode("While", listOf("condition" to "Expr", "body" to "Block")),
Pair("NoOp", listOf()), ASTNode("NoOp", listOf()),
Pair("Break", listOf()), ASTNode("Break", listOf()),
Pair("Continue", listOf()), ASTNode("Continue", listOf()),
)) )))
} }
} }
@@ -1,14 +1,23 @@
package com.dfsek.terra.addons.terrascript; package com.dfsek.terra.addons.terrascript;
import javax.annotation.Nullable; 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.HashMap;
import java.util.List; 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.NonexistentSymbolException;
import com.dfsek.terra.addons.terrascript.Environment.ScopeException.SymbolAlreadyExistsException; 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.ScopeException.SymbolTypeMismatchException;
import com.dfsek.terra.addons.terrascript.Environment.Symbol.Function; import com.dfsek.terra.addons.terrascript.Environment.Symbol.Function;
import com.dfsek.terra.addons.terrascript.Environment.Symbol.Variable; 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; import com.dfsek.terra.api.util.generic.pair.Pair;
@@ -18,7 +27,7 @@ public class Environment {
private final boolean canAccessOuterVariables; private final boolean canAccessOuterVariables;
private final HashMap<String, Symbol> symbolTable = new HashMap<>(); private final Map<String, Symbol> symbolTable = new HashMap<>();
private final boolean inLoop; private final boolean inLoop;
@@ -26,6 +35,13 @@ public class Environment {
this.outer = outer; this.outer = outer;
this.canAccessOuterVariables = canAccessOuterVariables; this.canAccessOuterVariables = canAccessOuterVariables;
this.inLoop = inLoop; 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() { public static Environment global() {
@@ -1,9 +1,30 @@
package com.dfsek.terra.addons.terrascript; package com.dfsek.terra.addons.terrascript;
import java.util.Optional;
// TODO - Make not enum // TODO - Make not enum
public enum Type { public enum Type {
NUMBER, NUMBER,
STRING, STRING,
BOOLEAN, 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<Type> from(Class<?> clazz) {
return Optional.ofNullable(
clazz == double.class ? NUMBER :
clazz == String.class ? STRING :
clazz == boolean.class ? BOOLEAN :
clazz == void.class ? VOID :
null);
}
} }
@@ -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<String, Method> BUILTIN_FUNCTIONS = new HashMap<>() {{
try {
put("print", System.out.getClass().getMethod("println", String.class));
} catch(NoSuchMethodException e) {
throw new RuntimeException(e);
}
}};
}
@@ -1,7 +0,0 @@
package com.dfsek.terra.addons.terrascript.codegen.asm;
public interface TerraScript {
void execute();
}
@@ -1,5 +1,7 @@
package com.dfsek.terra.addons.terrascript.codegen.asm; 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;
import com.dfsek.terra.addons.terrascript.ast.Expr.Assignment; 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.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.VariableDeclaration;
import com.dfsek.terra.addons.terrascript.ast.Stmt.While; 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.ClassReader;
import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label; import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes; import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.LocalVariablesSorter;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; 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 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 { public class TerraScriptClassGenerator {
@@ -55,6 +59,12 @@ public class TerraScriptClassGenerator {
this.debugPath = debugPath; this.debugPath = debugPath;
} }
/**
*
* @param root Assumed to be semantically correct
* @return Generated TerraScript instance
* @throws IOException
*/
public TerraScript generate(Block root) throws IOException { public TerraScript generate(Block root) throws IOException {
String targetClassName = dynamicName(TARGET_CLASS); String targetClassName = dynamicName(TARGET_CLASS);
String generatedClassName = targetClassName + "_GENERATED_" + generationCount; 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 }); classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, generatedClassName, null, "java/lang/Object", new String[]{ targetClassName });
// Generate constructor method // Generate constructor method
MethodVisitor constructor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
constructor.visitCode(); constructor.visitCode();
constructor.visitVarInsn(ALOAD, 0); // Put this reference on stack constructor.visitVarInsn(Opcodes.ALOAD, 0); // Put this reference on stack
constructor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
constructor.visitInsn(RETURN); // Void return constructor.visitInsn(Opcodes.RETURN); // Void return
constructor.visitMaxs(0, 0); constructor.visitMaxs(0, 0);
constructor.visitEnd(); constructor.visitEnd();
@@ -79,16 +89,14 @@ public class TerraScriptClassGenerator {
MethodExtractor extractor = new MethodExtractor(methodName); MethodExtractor extractor = new MethodExtractor(methodName);
new ClassReader(targetClassName).accept(extractor, 0); new ClassReader(targetClassName).accept(extractor, 0);
String description = extractor.methodDescription; String description = extractor.methodDescription;
MethodVisitor execute = classWriter.visitMethod(Opcodes.ACC_PUBLIC, methodName, description, null, null); int exeAcc = Opcodes.ACC_PUBLIC;
execute.visitCode(); // Start method body MethodVisitor executeMethod = classWriter.visitMethod(exeAcc, methodName, description, null, null);
executeMethod.visitCode(); // Start method body
execute.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); new MethodBytecodeGenerator(classWriter, generatedClassName, executeMethod, exeAcc, description).generate(root); // Generate bytecode
new BytecodeGenerator(execute).visitBlockStmt(root); // Finish up method
execute.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Z)V", false); executeMethod.visitInsn(Opcodes.RETURN);
executeMethod.visitMaxs(0, 0);
execute.visitInsn(RETURN); executeMethod.visitEnd();
execute.visitMaxs(0, 0);
execute.visitEnd();
// Finished generating class // Finished generating class
classWriter.visitEnd(); classWriter.visitEnd();
@@ -119,12 +127,31 @@ public class TerraScriptClassGenerator {
} }
} }
private static class BytecodeGenerator implements Stmt.Visitor<Void>, Expr.Visitor<Void> { private static class MethodBytecodeGenerator implements Stmt.Visitor<Void>, Expr.Visitor<Void> {
private final ClassWriter classWriter;
private final String className;
private final MethodVisitor method; private final MethodVisitor method;
public BytecodeGenerator(MethodVisitor method) { private final String descriptor;
private final LocalVariablesSorter lvs;
private final Map<String, Integer> 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.method = method;
this.descriptor = descriptor;
this.lvs = new LocalVariablesSorter(access, descriptor, method);
}
public void generate(Block root) {
this.visitBlockStmt(root);
} }
@Override @Override
@@ -181,7 +208,14 @@ public class TerraScriptClassGenerator {
method.visitInsn(Opcodes.ICONST_0); method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished); 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 SUBTRACT -> method.visitInsn(Opcodes.DSUB);
case MULTIPLY -> method.visitInsn(Opcodes.DMUL); case MULTIPLY -> method.visitInsn(Opcodes.DMUL);
case DIVIDE -> method.visitInsn(Opcodes.DDIV); case DIVIDE -> method.visitInsn(Opcodes.DDIV);
@@ -209,16 +243,43 @@ public class TerraScriptClassGenerator {
@Override @Override
public Void visitCallExpr(Call expr) { 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<Type> parameters = function.parameters.stream().map(Pair::getRight).toList();
method.visitMethodInsn(Opcodes.INVOKESTATIC, className, expr.identifier, getFunctionDescriptor(parameters, function.type), false);
return null; return null;
} }
@Override @Override
public Void visitVariableExpr(Variable expr) { 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; return null;
} }
@Override @Override
public Void visitAssignmentExpr(Assignment expr) { 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; return null;
} }
@@ -239,18 +300,77 @@ public class TerraScriptClassGenerator {
return null; return null;
} }
private String getFunctionDescriptor(List<Type> 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 @Override
public Void visitFunctionDeclarationStmt(FunctionDeclaration stmt) { public Void visitFunctionDeclarationStmt(FunctionDeclaration stmt) {
int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
List<Type> 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<String, Type> 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; return null;
} }
@Override @Override
public Void visitVariableDeclarationStmt(VariableDeclaration stmt) { 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; return null;
} }
@Override @Override
public Void visitReturnStmt(Return stmt) { 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; return null;
} }
@@ -63,7 +63,7 @@ public class FunctionReferenceAnalyzer implements Expr.Visitor<Void>, Stmt.Visit
public Void visitCallExpr(Call expr) { public Void visitCallExpr(Call expr) {
String id = expr.identifier; String id = expr.identifier;
try { try {
expr.getEnvironment().getFunction(id); expr.getSymbol();
} catch(NonexistentSymbolException e) { } catch(NonexistentSymbolException e) {
errorHandler.add( errorHandler.add(
new UndefinedReferenceException("No function by the name '" + id + "' is defined in this scope", expr.position)); new UndefinedReferenceException("No function by the name '" + id + "' is defined in this scope", expr.position));
@@ -60,7 +60,7 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
@Override @Override
public Void visitCallExpr(Call expr) { public Void visitCallExpr(Call expr) {
expr.setEnvironment(currentScope); expr.setSymbol(currentScope.getFunction(expr.identifier));
expr.arguments.forEach(e -> e.accept(this)); expr.arguments.forEach(e -> e.accept(this));
return null; return null;
} }
@@ -76,7 +76,7 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
} catch(SymbolTypeMismatchException e) { } catch(SymbolTypeMismatchException e) {
errorHandler.add(new ParseException("Identifier '" + id + "' is not defined as a variable", expr.position)); 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; return null;
} }
@@ -136,7 +136,6 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope", errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope",
stmt.position)); stmt.position));
} }
stmt.setEnvironment(currentScope);
return null; return null;
} }
@@ -51,8 +51,14 @@ public class TypeChecker implements Visitor<Type>, Stmt.Visitor<Type> {
yield Type.BOOLEAN; yield Type.BOOLEAN;
} }
case ADD -> { case ADD -> {
if(left == Type.NUMBER && right == Type.NUMBER) yield Type.NUMBER; if(left == Type.NUMBER && right == Type.NUMBER) {
if(left == Type.STRING || right == Type.STRING) yield Type.STRING; 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"); throw new RuntimeException("Addition operands must be either both numbers, or one of type string");
} }
case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> { case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> {
@@ -92,10 +98,10 @@ public class TypeChecker implements Visitor<Type>, Stmt.Visitor<Type> {
public Type visitCallExpr(Call expr) { public Type visitCallExpr(Call expr) {
String id = expr.identifier; String id = expr.identifier;
Environment.Symbol.Function signature = expr.getEnvironment().getFunction(id); Environment.Symbol.Function signature = expr.getSymbol();
List<Type> argumentTypes = expr.arguments.stream().map(a -> a.accept(this)).toList(); List<Type> argumentTypes = expr.arguments.stream().map(a -> a.accept(this)).toList();
List<Pair<String, Type>> parameters = signature.parameters; List<Type> parameters = signature.parameters.stream().map(Pair::getRight).toList();
if(argumentTypes.size() != parameters.size()) if(argumentTypes.size() != parameters.size())
errorHandler.add(new ParseException( errorHandler.add(new ParseException(
@@ -103,7 +109,7 @@ public class TypeChecker implements Visitor<Type>, Stmt.Visitor<Type> {
" arguments", expr.position)); " arguments", expr.position));
for(int i = 0; i < parameters.size(); i++) { for(int i = 0; i < parameters.size(); i++) {
Type expectedType = parameters.get(i).getRight(); Type expectedType = parameters.get(i);
Type providedType = argumentTypes.get(i); Type providedType = argumentTypes.get(i);
if(expectedType != providedType) if(expectedType != providedType)
errorHandler.add(new InvalidTypeException( errorHandler.add(new InvalidTypeException(
@@ -116,7 +122,7 @@ public class TypeChecker implements Visitor<Type>, Stmt.Visitor<Type> {
@Override @Override
public Type visitVariableExpr(Variable expr) { public Type visitVariableExpr(Variable expr) {
return expr.getEnvironment().getVariable(expr.identifier).type; return expr.getSymbol().type;
} }
@Override @Override
@@ -182,7 +188,7 @@ public class TypeChecker implements Visitor<Type>, Stmt.Visitor<Type> {
@Override @Override
public Type visitReturnStmt(Stmt.Return stmt) { public Type visitReturnStmt(Stmt.Return stmt) {
stmt.value.accept(this); stmt.setType(stmt.value.accept(this));
return Type.VOID; return Type.VOID;
} }
@@ -1,5 +1,8 @@
package com.dfsek.terra.addons.terrascript.util; package com.dfsek.terra.addons.terrascript.util;
import com.dfsek.terra.addons.terrascript.Type;
public class ASMUtil { public class ASMUtil {
/** /**
@@ -10,4 +13,8 @@ public class ASMUtil {
public static String dynamicName(Class<?> clazz) { public static String dynamicName(Class<?> clazz) {
return clazz.getCanonicalName().replace('.', '/'); return clazz.getCanonicalName().replace('.', '/');
} }
public static org.objectweb.asm.Type tsTypeToAsmType(Type type) {
return org.objectweb.asm.Type.getType((Class<?>) type.javaType());
}
} }
@@ -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);
}
}
}