Salvage TerraScript codegen code from dead laptop

This commit is contained in:
Astrash
2023-08-08 07:39:47 +00:00
parent 0e9cbd8e2f
commit 9a75ee78a1
5 changed files with 345 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ version = version("1.1.0")
dependencies {
api("commons-io:commons-io:2.7")
api("org.ow2.asm:asm: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)

View File

@@ -0,0 +1,19 @@
package com.dfsek.terra.addons.terrascript.codegen.asm;
import com.dfsek.terra.api.structure.Structure;
public class DynamicClassLoader extends ClassLoader {
public DynamicClassLoader(Class<?> clazz) {
super(clazz.getClassLoader());
}
public Class<?> defineClass(String name, byte[] data) {
return defineClass(name, data, 0, data.length);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return Class.forName(name);
}
}

View File

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

View File

@@ -0,0 +1,305 @@
package com.dfsek.terra.addons.terrascript.codegen.asm;
import com.dfsek.terra.addons.terrascript.ast.Expr;
import com.dfsek.terra.addons.terrascript.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.ast.Expr.Call;
import com.dfsek.terra.addons.terrascript.ast.Expr.Grouping;
import com.dfsek.terra.addons.terrascript.ast.Expr.Literal;
import com.dfsek.terra.addons.terrascript.ast.Expr.Unary;
import com.dfsek.terra.addons.terrascript.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.ast.Stmt;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Break;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Continue;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Expression;
import com.dfsek.terra.addons.terrascript.ast.Stmt.For;
import com.dfsek.terra.addons.terrascript.ast.Stmt.FunctionDeclaration;
import com.dfsek.terra.addons.terrascript.ast.Stmt.If;
import com.dfsek.terra.addons.terrascript.ast.Stmt.NoOp;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Return;
import com.dfsek.terra.addons.terrascript.ast.Stmt.VariableDeclaration;
import com.dfsek.terra.addons.terrascript.ast.Stmt.While;
import 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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
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 {
private static final Class<?> TARGET_CLASS = TerraScript.class;
private static final boolean DUMP = true;
private int generationCount = 0;
private final String debugPath;
public TerraScriptClassGenerator(String debugPath) {
this.debugPath = debugPath;
}
public TerraScript generate(Block root) throws IOException {
String targetClassName = dynamicName(TARGET_CLASS);
String generatedClassName = targetClassName + "_GENERATED_" + generationCount;
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
// Create class
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);
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.visitMaxs(0, 0);
constructor.visitEnd();
// Generate execute method
String methodName = "execute";
// Extract method description
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();
// Finished generating class
classWriter.visitEnd();
DynamicClassLoader loader = new DynamicClassLoader(TARGET_CLASS); // Instantiate a new loader every time so classes can be GC'ed when they are no longer used. (Classes cannot be GC'ed until their loaders are).
generationCount++;
byte[] bytecode = classWriter.toByteArray();
Class<?> generatedClass = loader.defineClass(generatedClassName.replace('/', '.'), bytecode);
if (DUMP) {
File dump = new File(debugPath + "/" + generatedClass.getSimpleName() + ".class");
dump.getParentFile().mkdirs();
try(FileOutputStream out = new FileOutputStream(dump)) {
out.write(bytecode);
} catch(IOException e) {
e.printStackTrace();
}
}
try {
Object instance = generatedClass.getDeclaredConstructor().newInstance();
return (TerraScript) instance;
} catch(ReflectiveOperationException e) {
throw new Error(e); // Should literally never happen
}
}
private static class BytecodeGenerator implements Stmt.Visitor<Void>, Expr.Visitor<Void> {
private final MethodVisitor method;
public BytecodeGenerator(MethodVisitor method) {
this.method = method;
}
@Override
public Void visitBinaryExpr(Binary expr) {
expr.left.accept(this);
expr.right.accept(this);
switch(expr.operator) {
// TODO - Short circuit binary operators
case BOOLEAN_OR -> method.visitInsn(Opcodes.IOR);
case BOOLEAN_AND -> method.visitInsn(Opcodes.IAND);
// case EQUALS -> null;
// case NOT_EQUALS -> null;
case GREATER -> {
Label falseLabel = new Label();
Label finished = new Label();
method.visitInsn(Opcodes.DCMPL);
method.visitJumpInsn(Opcodes.IFLE, falseLabel);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(falseLabel);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished);
}
case GREATER_EQUALS -> {
Label falseLabel = new Label();
Label finished = new Label();
method.visitInsn(Opcodes.DCMPL);
method.visitJumpInsn(Opcodes.IFLT, falseLabel);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(falseLabel);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished);
}
case LESS -> {
Label falseLabel = new Label();
Label finished = new Label();
method.visitInsn(Opcodes.DCMPG);
method.visitJumpInsn(Opcodes.IFGE, falseLabel);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(falseLabel);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished);
}
case LESS_EQUALS -> {
Label falseLabel = new Label();
Label finished = new Label();
method.visitInsn(Opcodes.DCMPG);
method.visitJumpInsn(Opcodes.IFGT, falseLabel);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(falseLabel);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished);
}
case ADD -> method.visitInsn(Opcodes.DADD);
case SUBTRACT -> method.visitInsn(Opcodes.DSUB);
case MULTIPLY -> method.visitInsn(Opcodes.DMUL);
case DIVIDE -> method.visitInsn(Opcodes.DDIV);
// case MODULO ->
}
return null;
}
@Override
public Void visitGroupingExpr(Grouping expr) {
expr.expression.accept(this);
return null;
}
@Override
public Void visitLiteralExpr(Literal expr) {
method.visitLdcInsn(expr.value);
return null;
}
@Override
public Void visitUnaryExpr(Unary expr) {
return null;
}
@Override
public Void visitCallExpr(Call expr) {
return null;
}
@Override
public Void visitVariableExpr(Variable expr) {
return null;
}
@Override
public Void visitAssignmentExpr(Assignment expr) {
return null;
}
@Override
public Void visitVoidExpr(Void expr) {
return null;
}
@Override
public Void visitExpressionStmt(Expression stmt) {
stmt.expression.accept(this);
return null;
}
@Override
public Void visitBlockStmt(Block stmt) {
stmt.statements.forEach(s -> s.accept(this));
return null;
}
@Override
public Void visitFunctionDeclarationStmt(FunctionDeclaration stmt) {
return null;
}
@Override
public Void visitVariableDeclarationStmt(VariableDeclaration stmt) {
return null;
}
@Override
public Void visitReturnStmt(Return stmt) {
return null;
}
@Override
public Void visitIfStmt(If stmt) {
return null;
}
@Override
public Void visitForStmt(For stmt) {
return null;
}
@Override
public Void visitWhileStmt(While stmt) {
return null;
}
@Override
public Void visitNoOpStmt(NoOp stmt) {
return null;
}
@Override
public Void visitBreakStmt(Break stmt) {
return null;
}
@Override
public Void visitContinueStmt(Continue stmt) {
return null;
}
}
private static class MethodExtractor extends ClassVisitor {
private final String methodName;
private String methodDescription;
protected MethodExtractor(String methodName) {
super(Opcodes.ASM9);
this.methodName = methodName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (name.equals(methodName))
methodDescription = descriptor;
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
}

View File

@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.terrascript.util;
public class ASMUtil {
/**
* Dynamically get name to account for possibility of shading
* @param clazz Class instance
* @return Internal class name
*/
public static String dynamicName(Class<?> clazz) {
return clazz.getCanonicalName().replace('.', '/');
}
}