mirror of
https://github.com/PolyhedralDev/Terra.git
synced 2026-04-06 07:46:13 +00:00
Implement function and variable codegen
This commit is contained in:
@@ -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>("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<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
|
||||
tasks.register("genTerrascriptAstClasses") {
|
||||
|
||||
|
||||
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()
|
||||
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<R> {
|
||||
|
||||
""".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> R accept(Visitor<R> 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> R accept(Visitor<R> 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<Expr> 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<Expr>"), 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<Stmt> statements")),
|
||||
Pair("FunctionDeclaration", listOf("String identifier", "List<Pair<String, Type>> 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<Pair<Expr, Block>> 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<Stmt>")),
|
||||
ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List<Pair<String, Type>>", "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<Pair<Expr, Block>>", "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()),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, Symbol> symbolTable = new HashMap<>();
|
||||
private final Map<String, Symbol> 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() {
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
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, "<init>", "()V", null, null);
|
||||
MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
constructor.visitCode();
|
||||
constructor.visitVarInsn(ALOAD, 0); // Put this reference on stack
|
||||
constructor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
|
||||
constructor.visitInsn(RETURN); // Void return
|
||||
constructor.visitVarInsn(Opcodes.ALOAD, 0); // Put this reference on stack
|
||||
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()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<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;
|
||||
|
||||
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.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<Type> 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<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
|
||||
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;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public class FunctionReferenceAnalyzer implements Expr.Visitor<Void>, 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));
|
||||
|
||||
@@ -60,7 +60,7 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
|
||||
|
||||
@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<Void>, Stmt.Visitor<Void> {
|
||||
} 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<Void>, Stmt.Visitor<Void> {
|
||||
errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope",
|
||||
stmt.position));
|
||||
}
|
||||
stmt.setEnvironment(currentScope);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,14 @@ public class TypeChecker implements Visitor<Type>, Stmt.Visitor<Type> {
|
||||
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<Type>, Stmt.Visitor<Type> {
|
||||
public Type visitCallExpr(Call expr) {
|
||||
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<Pair<String, Type>> parameters = signature.parameters;
|
||||
List<Type> 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<Type>, Stmt.Visitor<Type> {
|
||||
" 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<Type>, Stmt.Visitor<Type> {
|
||||
|
||||
@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<Type>, Stmt.Visitor<Type> {
|
||||
|
||||
@Override
|
||||
public Type visitReturnStmt(Stmt.Return stmt) {
|
||||
stmt.value.accept(this);
|
||||
stmt.setType(stmt.value.accept(this));
|
||||
return Type.VOID;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user