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

View File

@@ -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() {

View File

@@ -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);
}
}

View File

@@ -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);
}
}};
}

View File

@@ -1,7 +0,0 @@
package com.dfsek.terra.addons.terrascript.codegen.asm;
public interface TerraScript {
void execute();
}

View File

@@ -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;
}

View File

@@ -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));

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}