Autoformat

This commit is contained in:
Astrash 2023-10-27 14:24:42 +11:00
parent 9a94b26126
commit 5f3a2bb579
14 changed files with 260 additions and 231 deletions

View File

@ -30,13 +30,11 @@ data class ASTNode(
val name: String, val name: String,
val constructorFields: List<Pair<String, String>>, val constructorFields: List<Pair<String, String>>,
val mutableFields: List<Pair<String, String>> = emptyList() // TODO - Remove mutability from AST nodes val mutableFields: List<Pair<String, String>> = emptyList() // TODO - Remove mutability from AST nodes
) )
// 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(clazz: ASTClass) { fun generateClass(clazz: ASTClass) {
val src = StringBuilder() val src = StringBuilder()

View File

@ -13,19 +13,13 @@ import com.dfsek.terra.addons.terrascript.v2.codegen.NativeFunction;
public class Environment { public class Environment {
private final Environment outer;
private final boolean canAccessOuterVariables;
private final Map<String, Symbol> symbolTable = new HashMap<>();
private final boolean inLoop;
private final int index;
private int innerCount = 0;
public final String name; public final String name;
private final Environment outer;
private final boolean canAccessOuterVariables;
private final Map<String, Symbol> symbolTable = new HashMap<>();
private final boolean inLoop;
private final int index;
private int innerCount = 0;
private Environment(@Nullable Environment outer, boolean canAccessOuterVariables, boolean inLoop, int index) { private Environment(@Nullable Environment outer, boolean canAccessOuterVariables, boolean inLoop, int index) {
this.outer = outer; this.outer = outer;
@ -35,8 +29,11 @@ public class Environment {
this.name = String.join("_", getNestedIndexes().stream().map(Object::toString).toList()); this.name = String.join("_", getNestedIndexes().stream().map(Object::toString).toList());
// Populate global scope with built-in Java implemented methods // Populate global scope with built-in Java implemented methods
// TODO - Replace with AST import nodes // TODO - Replace with AST import nodes
if (index == 0) NativeFunction.BUILTIN_FUNCTIONS.forEach((name, function) -> if(index == 0) NativeFunction.BUILTIN_FUNCTIONS.forEach((name, function) ->
symbolTable.put(name, new Symbol.Variable(new Type.Function.Native(function.getReturnType(), function.getParameterTypes(), name, this, function)))); symbolTable.put(name, new Symbol.Variable(
new Type.Function.Native(function.getReturnType(),
function.getParameterTypes(), name,
this, function))));
} }
public static Environment global() { public static Environment global() {
@ -57,7 +54,7 @@ public class Environment {
private List<Integer> getNestedIndexes() { private List<Integer> getNestedIndexes() {
List<Integer> idxs = new ArrayList<>(); List<Integer> idxs = new ArrayList<>();
for (Environment env = this; env.outer != null; env = env.outer) { for(Environment env = this; env.outer != null; env = env.outer) {
idxs.add(0, env.index); idxs.add(0, env.index);
} }
return idxs; return idxs;
@ -78,7 +75,7 @@ public class Environment {
* *
* @return variable symbol table entry * @return variable symbol table entry
* *
* @throws NonexistentSymbolException if symbol is not declared in symbol table * @throws NonexistentSymbolException if symbol is not declared in symbol table
*/ */
public Symbol getVariable(String id) throws NonexistentSymbolException { public Symbol getVariable(String id) throws NonexistentSymbolException {
Symbol symbol = symbolTable.get(id); Symbol symbol = symbolTable.get(id);

View File

@ -12,6 +12,89 @@ import com.dfsek.terra.api.util.generic.pair.Pair;
public interface Type { public interface Type {
Type NUMBER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return double.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.DOUBLE;
}
@Override
public String toString() {
return "num";
}
};
Type INTEGER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return int.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.INTEGER;
}
@Override
public String toString() {
return "int";
}
};
Type STRING = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return String.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.STRING;
}
@Override
public String toString() {
return "str";
}
};
Type BOOLEAN = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return boolean.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.BOOLEAN;
}
@Override
public String toString() {
return "bool";
}
};
Type VOID = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return void.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.VOID;
}
@Override
public String toString() {
return "()";
}
};
static Type fromString(String lexeme) throws TypeException { static Type fromString(String lexeme) throws TypeException {
return switch(lexeme) { return switch(lexeme) {
case "num" -> NUMBER; case "num" -> NUMBER;
@ -31,6 +114,7 @@ public interface Type {
CodegenType getCodegenType(); CodegenType getCodegenType();
class Function implements Type { class Function implements Type {
private final Type returnType; private final Type returnType;
@ -43,6 +127,11 @@ public interface Type {
this.id = identifier == null ? "ANONYMOUS" : identifier + declarationScope.name; this.id = identifier == null ? "ANONYMOUS" : identifier + declarationScope.name;
} }
private static boolean paramsAreSubtypes(List<Type> subtypes, List<Type> superTypes) {
if(subtypes.size() != superTypes.size()) return false;
return Streams.zip(subtypes.stream(), superTypes.stream(), Pair::of).allMatch(p -> p.getLeft().typeOf(p.getRight()));
}
public Type getReturnType() { public Type getReturnType() {
return returnType; return returnType;
} }
@ -66,11 +155,6 @@ public interface Type {
return CodegenType.OBJECT; return CodegenType.OBJECT;
} }
private static boolean paramsAreSubtypes(List<Type> subtypes, List<Type> superTypes) {
if(subtypes.size() != superTypes.size()) return false;
return Streams.zip(subtypes.stream(), superTypes.stream(), Pair::of).allMatch(p -> p.getLeft().typeOf(p.getRight()));
}
@Override @Override
public java.lang.reflect.Type javaType() { public java.lang.reflect.Type javaType() {
return Function.class; return Function.class;
@ -96,92 +180,6 @@ public interface Type {
} }
} }
Type NUMBER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return double.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.DOUBLE;
}
@Override
public String toString() {
return "num";
}
};
Type INTEGER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return int.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.INTEGER;
}
@Override
public String toString() {
return "int";
}
};
Type STRING = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return String.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.STRING;
}
@Override
public String toString() {
return "str";
}
};
Type BOOLEAN = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return boolean.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.BOOLEAN;
}
@Override
public String toString() {
return "bool";
}
};
Type VOID = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return void.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.VOID;
}
@Override
public String toString() {
return "()";
}
};
class TypeException extends Exception { class TypeException extends Exception {
} }

View File

@ -5,29 +5,27 @@ import org.objectweb.asm.Opcodes;
public class CodegenType { public class CodegenType {
private final InstructionType instructionType;
private final String descriptor;
public CodegenType(InstructionType instructionType, String descriptor) {
this.instructionType = instructionType;
this.descriptor = descriptor;
}
public InstructionType bytecodeType() {
return instructionType;
}
public String getDescriptor() {
return descriptor;
}
public static final CodegenType BOOLEAN = new CodegenType(InstructionType.INTEGER, "Z"); public static final CodegenType BOOLEAN = new CodegenType(InstructionType.INTEGER, "Z");
public static final CodegenType STRING = new CodegenType(InstructionType.OBJECT, "Ljava/lang/String;"); public static final CodegenType STRING = new CodegenType(InstructionType.OBJECT, "Ljava/lang/String;");
public static final CodegenType DOUBLE = new CodegenType(InstructionType.DOUBLE, "D"); public static final CodegenType DOUBLE = new CodegenType(InstructionType.DOUBLE, "D");
public static final CodegenType INTEGER = new CodegenType(InstructionType.INTEGER, "I"); public static final CodegenType INTEGER = new CodegenType(InstructionType.INTEGER, "I");
public static final CodegenType VOID = new CodegenType(InstructionType.VOID, "V"); public static final CodegenType VOID = new CodegenType(InstructionType.VOID, "V");
public static final CodegenType OBJECT = new CodegenType(InstructionType.OBJECT, "Ljava/lang/Object;"); public static final CodegenType OBJECT = new CodegenType(InstructionType.OBJECT, "Ljava/lang/Object;");
private final InstructionType instructionType;
private final String descriptor;
public CodegenType(InstructionType instructionType, String descriptor) {
this.instructionType = instructionType;
this.descriptor = descriptor;
}
public InstructionType bytecodeType() {
return instructionType;
}
public String getDescriptor() {
return descriptor;
}
public enum InstructionType { public enum InstructionType {
DOUBLE { DOUBLE {

View File

@ -4,7 +4,7 @@ public class DynamicClassLoader extends ClassLoader {
public DynamicClassLoader(Class<?> clazz) { public DynamicClassLoader(Class<?> clazz) {
super(clazz.getClassLoader()); super(clazz.getClassLoader());
} }
public Class<?> defineClass(String name, byte[] data) { public Class<?> defineClass(String name, byte[] data) {
return defineClass(name, data, 0, data.length); return defineClass(name, data, 0, data.length);
} }

View File

@ -50,24 +50,24 @@ import com.dfsek.terra.api.util.generic.pair.Pair;
import static com.dfsek.terra.addons.terrascript.v2.util.ASMUtil.dynamicName; import static com.dfsek.terra.addons.terrascript.v2.util.ASMUtil.dynamicName;
public class TerraScriptClassGenerator { public class TerraScriptClassGenerator {
private static final Class<?> TARGET_CLASS = TerraScript.class; private static final Class<?> TARGET_CLASS = TerraScript.class;
private static final boolean DUMP = true; private static final boolean DUMP = true;
private int generationCount = 0;
private final String debugPath; private final String debugPath;
private int generationCount = 0;
public TerraScriptClassGenerator(String debugPath) { public TerraScriptClassGenerator(String debugPath) {
this.debugPath = debugPath; this.debugPath = debugPath;
} }
/** /**
*
* @param root Assumed to be semantically correct * @param root Assumed to be semantically correct
*
* @return Generated TerraScript instance * @return Generated TerraScript instance
*
* @throws IOException * @throws IOException
*/ */
public TerraScript generate(Block root) throws IOException { public TerraScript generate(Block root) throws IOException {
@ -78,7 +78,7 @@ public class TerraScriptClassGenerator {
// Create class // Create class
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(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
constructor.visitCode(); constructor.visitCode();
@ -97,7 +97,8 @@ public class TerraScriptClassGenerator {
int exeAcc = Opcodes.ACC_PUBLIC; int exeAcc = Opcodes.ACC_PUBLIC;
MethodVisitor executeMethod = classWriter.visitMethod(exeAcc, methodName, description, null, null); MethodVisitor executeMethod = classWriter.visitMethod(exeAcc, methodName, description, null, null);
executeMethod.visitCode(); // Start method body executeMethod.visitCode(); // Start method body
new MethodBytecodeGenerator(classWriter, generatedClassName, executeMethod, exeAcc, description).generate(root); // Generate bytecode new MethodBytecodeGenerator(classWriter, generatedClassName, executeMethod, exeAcc, description).generate(
root); // Generate bytecode
// Finish up method // Finish up method
executeMethod.visitInsn(Opcodes.RETURN); executeMethod.visitInsn(Opcodes.RETURN);
executeMethod.visitMaxs(0, 0); executeMethod.visitMaxs(0, 0);
@ -106,7 +107,9 @@ public class TerraScriptClassGenerator {
// Finished generating class // Finished generating class
classWriter.visitEnd(); 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). 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++; generationCount++;
@ -114,7 +117,7 @@ public class TerraScriptClassGenerator {
Class<?> generatedClass = loader.defineClass(generatedClassName.replace('/', '.'), bytecode); Class<?> generatedClass = loader.defineClass(generatedClassName.replace('/', '.'), bytecode);
if (DUMP) { if(DUMP) {
File dump = new File(debugPath + "/" + generatedClass.getSimpleName() + ".class"); File dump = new File(debugPath + "/" + generatedClass.getSimpleName() + ".class");
dump.getParentFile().mkdirs(); dump.getParentFile().mkdirs();
try(FileOutputStream out = new FileOutputStream(dump)) { try(FileOutputStream out = new FileOutputStream(dump)) {
@ -156,6 +159,13 @@ public class TerraScriptClassGenerator {
this.lvs = new LocalVariablesSorter(access, descriptor, method); this.lvs = new LocalVariablesSorter(access, descriptor, method);
} }
private static boolean exprTypesEqual(Type type, TypedExpr... exprs) {
for(TypedExpr expr : exprs) {
if(expr.type != type) return false;
}
return true;
}
public void generate(Block root) { public void generate(Block root) {
this.visitBlockTypedStmt(root); this.visitBlockTypedStmt(root);
} }
@ -169,9 +179,10 @@ public class TerraScriptClassGenerator {
CodegenType codegenType = expr.type.getCodegenType(); CodegenType codegenType = expr.type.getCodegenType();
if(codegenType.bytecodeType() == InstructionType.DOUBLE) if(codegenType.bytecodeType() == InstructionType.DOUBLE)
method.visitInsn(Opcodes.DADD); method.visitInsn(Opcodes.DADD);
else if (Objects.equals(codegenType.getDescriptor(), "Ljava/lang/String;")) else if(Objects.equals(codegenType.getDescriptor(), "Ljava/lang/String;"))
// TODO - Optimize string concatenation // TODO - Optimize string concatenation
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat", "(Ljava/lang/String;)Ljava/lang/String;", false); method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat",
"(Ljava/lang/String;)Ljava/lang/String;", false);
else throw new RuntimeException("Could not generate bytecode for ADD binary operator returning type " + expr.type); else throw new RuntimeException("Could not generate bytecode for ADD binary operator returning type " + expr.type);
} }
case SUBTRACT -> binaryInsn(expr, Opcodes.DSUB); case SUBTRACT -> binaryInsn(expr, Opcodes.DSUB);
@ -191,7 +202,8 @@ public class TerraScriptClassGenerator {
@Override @Override
public Void visitLiteralTypedExpr(Literal expr) { public Void visitLiteralTypedExpr(Literal expr) {
if(expr.type.getCodegenType() == CodegenType.BOOLEAN) if(expr.type.getCodegenType() == CodegenType.BOOLEAN)
if ((boolean) expr.value) pushTrue(); else pushFalse(); if((boolean) expr.value) pushTrue();
else pushFalse();
else method.visitLdcInsn(expr.value); else method.visitLdcInsn(expr.value);
return null; return null;
} }
@ -199,7 +211,7 @@ public class TerraScriptClassGenerator {
@Override @Override
public Void visitUnaryTypedExpr(Unary expr) { public Void visitUnaryTypedExpr(Unary expr) {
expr.operand.accept(this); expr.operand.accept(this);
switch (expr.operator) { switch(expr.operator) {
case NOT -> invertBool(); case NOT -> invertBool();
case NEGATE -> method.visitInsn(Opcodes.DNEG); case NEGATE -> method.visitInsn(Opcodes.DNEG);
} }
@ -209,7 +221,7 @@ public class TerraScriptClassGenerator {
@Override @Override
public Void visitCallTypedExpr(Call expr) { public Void visitCallTypedExpr(Call expr) {
// TODO - Remove specific handling of native functions // TODO - Remove specific handling of native functions
if (expr.callee.type instanceof Type.Function.Native nativeFunction) { if(expr.callee.type instanceof Type.Function.Native nativeFunction) {
NativeFunction function = nativeFunction.getNativeFunction(); NativeFunction function = nativeFunction.getNativeFunction();
function.pushInstance(method); function.pushInstance(method);
expr.arguments.forEach(a -> a.accept(this)); expr.arguments.forEach(a -> a.accept(this));
@ -219,7 +231,8 @@ public class TerraScriptClassGenerator {
// TODO - Add support for invokevirtual // TODO - Add support for invokevirtual
expr.arguments.forEach(a -> a.accept(this)); expr.arguments.forEach(a -> a.accept(this));
List<Type> parameters = expr.arguments.stream().map(e -> e.type).toList(); List<Type> parameters = expr.arguments.stream().map(e -> e.type).toList();
method.visitMethodInsn(Opcodes.INVOKESTATIC, className, ((Type.Function) expr.callee.type).getId(), getFunctionDescriptor(parameters, expr.type), false); method.visitMethodInsn(Opcodes.INVOKESTATIC, className, ((Type.Function) expr.callee.type).getId(),
getFunctionDescriptor(parameters, expr.type), false);
return null; return null;
} }
@ -257,7 +270,9 @@ public class TerraScriptClassGenerator {
/** /**
* Writes function as a private static method of the current class * Writes function as a private static method of the current class
*
* @param stmt * @param stmt
*
* @return * @return
*/ */
@Override @Override
@ -266,7 +281,8 @@ public class TerraScriptClassGenerator {
int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC; int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
MethodVisitor method = classWriter.visitMethod(access, stmt.scopedIdentifier, getFunctionDescriptor(parameterTypes, stmt.returnType), null, null); MethodVisitor method = classWriter.visitMethod(access, stmt.scopedIdentifier,
getFunctionDescriptor(parameterTypes, stmt.returnType), null, null);
method.visitCode(); // Start method body method.visitCode(); // Start method body
@ -274,7 +290,7 @@ public class TerraScriptClassGenerator {
// Add local variable indexes for each parameter // Add local variable indexes for each parameter
int lvidx = 0; int lvidx = 0;
for (Pair<String, Type> parameter : stmt.parameters) { for(Pair<String, Type> parameter : stmt.parameters) {
funcGenerator.lvTable.put(parameter.getLeft(), lvidx); funcGenerator.lvTable.put(parameter.getLeft(), lvidx);
lvidx += parameter.getRight().getCodegenType().bytecodeType().slotSize(); // Increment by how many slots data type takes lvidx += parameter.getRight().getCodegenType().bytecodeType().slotSize(); // Increment by how many slots data type takes
} }
@ -370,13 +386,6 @@ public class TerraScriptClassGenerator {
return exprTypesEqual(type, expr.left, expr.right); return exprTypesEqual(type, expr.left, expr.right);
} }
private static boolean exprTypesEqual(Type type, TypedExpr... exprs) {
for(TypedExpr expr : exprs) {
if (expr.type != type) return false;
}
return true;
}
/** /**
* Inverts a boolean on the stack * Inverts a boolean on the stack
*/ */
@ -406,6 +415,7 @@ public class TerraScriptClassGenerator {
/** /**
* Pushes boolean on to the stack based on comparison result * Pushes boolean on to the stack based on comparison result
*
* @param condition * @param condition
*/ */
private void pushComparisonBool(TypedExpr condition) { private void pushComparisonBool(TypedExpr condition) {
@ -417,6 +427,7 @@ public class TerraScriptClassGenerator {
/** /**
* Executes a statement then jumps to the exit label if the condition is true, jumps over the statement if false * Executes a statement then jumps to the exit label if the condition is true, jumps over the statement if false
*
* @param condition * @param condition
* @param stmt * @param stmt
* @param exit * @param exit
@ -447,7 +458,7 @@ public class TerraScriptClassGenerator {
private void conditionalRunnable(TypedExpr condition, Runnable trueBlock, Label trueFinished) { private void conditionalRunnable(TypedExpr condition, Runnable trueBlock, Label trueFinished) {
Label exit = new Label(); // If the first conditional is false, jump over statement and don't execute it Label exit = new Label(); // If the first conditional is false, jump over statement and don't execute it
if (condition instanceof Binary binaryCondition) { if(condition instanceof Binary binaryCondition) {
switch(binaryCondition.operator) { switch(binaryCondition.operator) {
case BOOLEAN_AND -> { case BOOLEAN_AND -> {
// Operands assumed booleans // Operands assumed booleans
@ -466,30 +477,30 @@ public class TerraScriptClassGenerator {
label(skipRight); label(skipRight);
} }
case EQUALS -> { case EQUALS -> {
if (binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers if(binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers
pushBinaryOperands(binaryCondition); pushBinaryOperands(binaryCondition);
jumpIf(OpcodeAlias.INTEGERS_NOT_EQUAL, exit); jumpIf(OpcodeAlias.INTEGERS_NOT_EQUAL, exit);
} else if (binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles } else if(binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles
binaryInsn(binaryCondition, Opcodes.DCMPG); binaryInsn(binaryCondition, Opcodes.DCMPG);
jumpIf(OpcodeAlias.CMP_NOT_EQUALS, exit); jumpIf(OpcodeAlias.CMP_NOT_EQUALS, exit);
} else if (binaryOperandsSameType(Type.STRING, binaryCondition)) { } else if(binaryOperandsSameType(Type.STRING, binaryCondition)) {
pushBinaryOperands(binaryCondition); pushBinaryOperands(binaryCondition);
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false); method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
jumpIf(OpcodeAlias.BOOL_FALSE, exit); jumpIf(OpcodeAlias.BOOL_FALSE, exit);
} else throw new CompilerBugException(); } else throw new CompilerBugException();
} }
case NOT_EQUALS -> { case NOT_EQUALS -> {
if (binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers if(binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers
pushBinaryOperands(binaryCondition); pushBinaryOperands(binaryCondition);
jumpIf(OpcodeAlias.INTEGERS_EQUAL, exit); jumpIf(OpcodeAlias.INTEGERS_EQUAL, exit);
} else if (binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles } else if(binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles
binaryInsn(binaryCondition, Opcodes.DCMPG); binaryInsn(binaryCondition, Opcodes.DCMPG);
jumpIf(OpcodeAlias.CMP_EQUALS, exit); jumpIf(OpcodeAlias.CMP_EQUALS, exit);
} else if (binaryOperandsSameType(Type.STRING, binaryCondition)) { // Operands assumed references } else if(binaryOperandsSameType(Type.STRING, binaryCondition)) { // Operands assumed references
pushBinaryOperands(binaryCondition); pushBinaryOperands(binaryCondition);
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false); method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
invertBool(); invertBool();
@ -533,6 +544,7 @@ public class TerraScriptClassGenerator {
} }
} }
private static class MethodExtractor extends ClassVisitor { private static class MethodExtractor extends ClassVisitor {
private final String methodName; private final String methodName;
@ -545,7 +557,7 @@ public class TerraScriptClassGenerator {
@Override @Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (name.equals(methodName)) if(name.equals(methodName))
methodDescription = descriptor; methodDescription = descriptor;
return super.visitMethod(access, name, descriptor, signature, exceptions); return super.visitMethod(access, name, descriptor, signature, exceptions);
} }

View File

@ -331,7 +331,7 @@ public class Parser {
private Expr postfix() { private Expr postfix() {
Expr expr = primary(); Expr expr = primary();
while (current().isType(TokenType.OPEN_PAREN)) { while(current().isType(TokenType.OPEN_PAREN)) {
expr = call(expr); expr = call(expr);
} }
return expr; return expr;

View File

@ -26,9 +26,8 @@ import static com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.S
public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> { public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
private Environment currentScope;
private final ErrorHandler errorHandler; private final ErrorHandler errorHandler;
private Environment currentScope;
public ScopeAnalyzer(Environment globalScope, ErrorHandler errorHandler) { public ScopeAnalyzer(Environment globalScope, ErrorHandler errorHandler) {

View File

@ -43,16 +43,22 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
Type type = switch(expr.operator) { Type type = switch(expr.operator) {
case BOOLEAN_OR, BOOLEAN_AND -> { case BOOLEAN_OR, BOOLEAN_AND -> {
if(!leftType.typeOf(Type.BOOLEAN) || !rightType.typeOf(Type.BOOLEAN)) if(!leftType.typeOf(Type.BOOLEAN) || !rightType.typeOf(Type.BOOLEAN))
errorHandler.add(new InvalidTypeException("Both operands of '" + expr.operator + "' operator must be of type '" + Type.BOOLEAN + "', found types '" + leftType + "' and '" + rightType + "'", expr.position)); errorHandler.add(new InvalidTypeException(
"Both operands of '" + expr.operator + "' operator must be of type '" + Type.BOOLEAN + "', found types '" +
leftType + "' and '" + rightType + "'", expr.position));
yield Type.BOOLEAN; yield Type.BOOLEAN;
} }
case EQUALS, NOT_EQUALS -> { case EQUALS, NOT_EQUALS -> {
if(!leftType.typeOf(rightType)) errorHandler.add(new InvalidTypeException("Both operands of equality operator (==) must be of the same type, found mismatched types '" + leftType + "' and '" + rightType + "'", expr.position)); if(!leftType.typeOf(rightType)) errorHandler.add(new InvalidTypeException(
"Both operands of equality operator (==) must be of the same type, found mismatched types '" + leftType +
"' and '" + rightType + "'", expr.position));
yield Type.BOOLEAN; yield Type.BOOLEAN;
} }
case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> { case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> {
if(!leftType.typeOf(Type.NUMBER) || !rightType.typeOf(Type.NUMBER)) if(!leftType.typeOf(Type.NUMBER) || !rightType.typeOf(Type.NUMBER))
errorHandler.add(new InvalidTypeException("Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" + leftType + "' and '" + rightType + "'", expr.position)); errorHandler.add(new InvalidTypeException(
"Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" +
leftType + "' and '" + rightType + "'", expr.position));
yield Type.BOOLEAN; yield Type.BOOLEAN;
} }
case ADD -> { case ADD -> {
@ -62,12 +68,16 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
if(leftType.typeOf(Type.STRING) || rightType.typeOf(Type.STRING)) { if(leftType.typeOf(Type.STRING) || rightType.typeOf(Type.STRING)) {
yield Type.STRING; yield Type.STRING;
} }
errorHandler.add(new InvalidTypeException("Addition operands must be either both of type '" + Type.NUMBER + "', or one of type '" + Type.STRING + "'", expr.position)); errorHandler.add(new InvalidTypeException(
"Addition operands must be either both of type '" + Type.NUMBER + "', or one of type '" + Type.STRING + "'",
expr.position));
yield Type.VOID; yield Type.VOID;
} }
case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> { case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> {
if(!leftType.typeOf(Type.NUMBER) || !rightType.typeOf(Type.NUMBER)) if(!leftType.typeOf(Type.NUMBER) || !rightType.typeOf(Type.NUMBER))
errorHandler.add(new InvalidTypeException("Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" + leftType + "' and '" + rightType + "'", expr.position)); errorHandler.add(new InvalidTypeException(
"Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" +
leftType + "' and '" + rightType + "'", expr.position));
yield Type.NUMBER; yield Type.NUMBER;
} }
}; };
@ -106,7 +116,8 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
TypedExpr function = expr.callee.accept(this); TypedExpr function = expr.callee.accept(this);
if(!(function.type instanceof Type.Function functionType)) { if(!(function.type instanceof Type.Function functionType)) {
errorHandler.add(new InvalidCalleeException("Cannot call type '" + function.type + "', only functions can be called", expr.position)); errorHandler.add(
new InvalidCalleeException("Cannot call type '" + function.type + "', only functions can be called", expr.position));
return new TypedExpr.Void(Type.VOID); return new TypedExpr.Void(Type.VOID);
} }
@ -115,7 +126,8 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
if(arguments.size() != parameters.size()) if(arguments.size() != parameters.size())
errorHandler.add(new InvalidArgumentsException( errorHandler.add(new InvalidArgumentsException(
"Provided " + arguments.size() + " arguments to function call, expected " + parameters.size() + " arguments", expr.position)); "Provided " + arguments.size() + " arguments to function call, expected " + parameters.size() + " arguments",
expr.position));
for(int i = 0; i < parameters.size(); i++) { for(int i = 0; i < parameters.size(); i++) {
Type expectedType = parameters.get(i); Type expectedType = parameters.get(i);
@ -142,7 +154,8 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
String id = expr.lValue.identifier; String id = expr.lValue.identifier;
if(!right.type.typeOf(expected)) if(!right.type.typeOf(expected))
errorHandler.add(new InvalidTypeException( errorHandler.add(new InvalidTypeException(
"Cannot assign variable '" + id + "' to value of type '" + right.type + "', '" + id + "' is declared with type '" + expected + "'", "Cannot assign variable '" + id + "' to value of type '" + right.type + "', '" + id + "' is declared with type '" +
expected + "'",
expr.position)); expr.position));
return new TypedExpr.Assignment(left, right, right.type); return new TypedExpr.Assignment(left, right, right.type);
} }
@ -171,7 +184,8 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
new InvalidFunctionDeclarationException("Function body for '" + stmt.identifier + "' does not contain return statement", new InvalidFunctionDeclarationException("Function body for '" + stmt.identifier + "' does not contain return statement",
stmt.position)); stmt.position));
} }
return new TypedStmt.FunctionDeclaration(stmt.identifier, stmt.parameters, stmt.returnType, body, ((Type.Function) stmt.getSymbol().type).getId()); return new TypedStmt.FunctionDeclaration(stmt.identifier, stmt.parameters, stmt.returnType, body,
((Type.Function) stmt.getSymbol().type).getId());
} }
private boolean alwaysReturns(TypedStmt stmt, Stmt.FunctionDeclaration function) { private boolean alwaysReturns(TypedStmt stmt, Stmt.FunctionDeclaration function) {
@ -181,11 +195,12 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
"Return statement must match function's return type. Function '" + function.identifier + "' expects " + "Return statement must match function's return type. Function '" + function.identifier + "' expects " +
function.returnType + ", found " + ret.value.type + " instead", function.position)); function.returnType + ", found " + ret.value.type + " instead", function.position));
return true; return true;
} else if (stmt instanceof TypedStmt.If ifStmt) { } else if(stmt instanceof TypedStmt.If ifStmt) {
return alwaysReturns(ifStmt.trueBody, function) && return alwaysReturns(ifStmt.trueBody, function) &&
ifStmt.elseIfClauses.stream().map(Pair::getRight).allMatch(s -> alwaysReturns(s, function)) && ifStmt.elseIfClauses.stream().map(Pair::getRight).allMatch(s -> alwaysReturns(s, function)) &&
ifStmt.elseBody.map(body -> alwaysReturns(body, function)).orElse(false); // If else body is not defined then statement does not always return ifStmt.elseBody.map(body -> alwaysReturns(body, function)).orElse(
} else if (stmt instanceof TypedStmt.Block block) { false); // If else body is not defined then statement does not always return
} else if(stmt instanceof TypedStmt.Block block) {
return block.statements.stream().anyMatch(s -> alwaysReturns(s, function)); return block.statements.stream().anyMatch(s -> alwaysReturns(s, function));
} }
return false; return false;
@ -196,8 +211,9 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
TypedExpr value = stmt.value.accept(this); TypedExpr value = stmt.value.accept(this);
if(!stmt.type.typeOf(value.type)) if(!stmt.type.typeOf(value.type))
errorHandler.add(new InvalidTypeException( errorHandler.add(new InvalidTypeException(
"Type of value assigned to variable '" + stmt.identifier + "' does not match variable's declared type. Expected type '" + "Type of value assigned to variable '" + stmt.identifier +
stmt.type + "', found '" + value.type +"' instead", stmt.position)); "' does not match variable's declared type. Expected type '" +
stmt.type + "', found '" + value.type + "' instead", stmt.position));
return new TypedStmt.VariableDeclaration(stmt.type, stmt.identifier, value); return new TypedStmt.VariableDeclaration(stmt.type, stmt.identifier, value);
} }
@ -209,12 +225,15 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
@Override @Override
public TypedStmt visitIfStmt(Stmt.If stmt) { public TypedStmt visitIfStmt(Stmt.If stmt) {
TypedExpr condition = stmt.condition.accept(this); TypedExpr condition = stmt.condition.accept(this);
if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException("If statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position)); if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException(
"If statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position));
TypedStmt.Block trueBody = (TypedStmt.Block) stmt.trueBody.accept(this); TypedStmt.Block trueBody = (TypedStmt.Block) stmt.trueBody.accept(this);
List<Pair<TypedExpr, TypedStmt.Block>> elseIfClauses = stmt.elseIfClauses.stream().map(c -> { List<Pair<TypedExpr, TypedStmt.Block>> elseIfClauses = stmt.elseIfClauses.stream().map(c -> {
TypedExpr clauseCondition = c.getLeft().accept(this); TypedExpr clauseCondition = c.getLeft().accept(this);
if (!clauseCondition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException("Else if clause conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position)); if(!clauseCondition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException(
"Else if clause conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.",
stmt.position));
return Pair.of(clauseCondition, (TypedStmt.Block) c.getRight().accept(this)); return Pair.of(clauseCondition, (TypedStmt.Block) c.getRight().accept(this));
}).toList(); }).toList();
@ -227,7 +246,9 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
public TypedStmt visitForStmt(Stmt.For stmt) { public TypedStmt visitForStmt(Stmt.For stmt) {
TypedStmt initializer = stmt.initializer.accept(this); TypedStmt initializer = stmt.initializer.accept(this);
TypedExpr condition = stmt.condition.accept(this); TypedExpr condition = stmt.condition.accept(this);
if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException("For statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position)); if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException(
"For statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.",
stmt.position));
TypedExpr incrementer = stmt.incrementer.accept(this); TypedExpr incrementer = stmt.incrementer.accept(this);
return new TypedStmt.For(initializer, condition, incrementer, (TypedStmt.Block) stmt.body.accept(this)); return new TypedStmt.For(initializer, condition, incrementer, (TypedStmt.Block) stmt.body.accept(this));
} }
@ -235,7 +256,9 @@ public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt>
@Override @Override
public TypedStmt visitWhileStmt(Stmt.While stmt) { public TypedStmt visitWhileStmt(Stmt.While stmt) {
TypedExpr condition = stmt.condition.accept(this); TypedExpr condition = stmt.condition.accept(this);
if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException("While statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position)); if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException(
"While statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.",
stmt.position));
return new TypedStmt.While(condition, (TypedStmt.Block) stmt.body.accept(this)); return new TypedStmt.While(condition, (TypedStmt.Block) stmt.body.accept(this));
} }

View File

@ -7,7 +7,9 @@ public class ASMUtil {
/** /**
* Dynamically get name to account for possibility of shading * Dynamically get name to account for possibility of shading
*
* @param clazz Class instance * @param clazz Class instance
*
* @return Internal class name * @return Internal class name
*/ */
public static String dynamicName(Class<?> clazz) { public static String dynamicName(Class<?> clazz) {

View File

@ -59,7 +59,7 @@ public class CodeGenTest {
retNum(); retNum();
var bln: bool = true; var bln: bool = true;
print(takesArgs("test", 3, true)); print(takesArgs("test", 3, true));
print(retStr()); print(retStr());

View File

@ -9,6 +9,7 @@ import com.dfsek.terra.addons.terrascript.v2.lexer.Token.TokenType;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
public class LexerTest { public class LexerTest {
private static void tokenTypeTest(String input, TokenType type) { private static void tokenTypeTest(String input, TokenType type) {

View File

@ -38,7 +38,7 @@ public class LookaheadStreamTest {
assertEquals(first, lookahead.current()); assertEquals(first, lookahead.current());
assertEquals(first, lookahead.current()); assertEquals(first, lookahead.current());
assertEquals(new SourcePosition(1, 1), lookahead.getPosition()); assertEquals(new SourcePosition(1, 1), lookahead.getPosition());
assertEquals(new SourcePosition(1, 1), lookahead.getPosition()); assertEquals(new SourcePosition(1, 1), lookahead.getPosition());
@ -52,7 +52,7 @@ public class LookaheadStreamTest {
assertEquals(second, lookahead.current()); assertEquals(second, lookahead.current());
assertEquals(second, lookahead.consume()); assertEquals(second, lookahead.consume());
assertEquals(third, lookahead.current()); assertEquals(third, lookahead.current());
assertTrue(lookahead.matchesString("st", true)); assertTrue(lookahead.matchesString("st", true));

View File

@ -129,12 +129,12 @@ public class SemanticAnalyzerTest {
// Returns can still be explicitly used for void returning functions // Returns can still be explicitly used for void returning functions
testValid(""" testValid("""
fun returnsNum(p: bool) { fun returnsNum(p: bool) {
if (p) { if (p) {
return; return;
} }
} }
"""); """);
// If all if-statement bodies always return, then the statement is considered as always returning // If all if-statement bodies always return, then the statement is considered as always returning
testValid(""" testValid("""
@ -159,7 +159,8 @@ public class SemanticAnalyzerTest {
} }
"""); """);
// If no else body is defined, an if-statement does not always return, therefore the function does not contain any always-return-statements // If no else body is defined, an if-statement does not always return, therefore the function does not contain any
// always-return-statements
testInvalid(""" testInvalid("""
fun returnsNum(p: bool): num { fun returnsNum(p: bool): num {
if (p) { if (p) {
@ -194,38 +195,38 @@ public class SemanticAnalyzerTest {
"""); """);
testInvalid(""" testInvalid("""
fun returnsNum(p1: bool, p2: bool): num { fun returnsNum(p1: bool, p2: bool): num {
if (p1) { if (p1) {
if (p2) { if (p2) {
return 1; return 1;
} }
// No else clause here, so will not always return // No else clause here, so will not always return
} else { } else {
return 3; return 3;
} }
} }
""", InvalidFunctionDeclarationException.class); """, InvalidFunctionDeclarationException.class);
// If-statement may not always return but a return statement after it means function will always return // If-statement may not always return but a return statement after it means function will always return
testValid(""" testValid("""
fun returnsNum(p: bool): num { fun returnsNum(p: bool): num {
if (p) { if (p) {
return 1; return 1;
} }
return 2; return 2;
} }
"""); """);
// Same applies when statements are swapped // Same applies when statements are swapped
testValid(""" testValid("""
fun returnsNum(p: bool): num { fun returnsNum(p: bool): num {
return 1; return 1;
// Unreachable // Unreachable
if (p) { if (p) {
return 2; return 2;
} }
} }
"""); """);
} }
@Test @Test
@ -281,12 +282,12 @@ public class SemanticAnalyzerTest {
// Should not be able to pass argument of type not matching parameter type // Should not be able to pass argument of type not matching parameter type
testInvalid(""" testInvalid("""
fun returnBool(): bool { fun returnBool(): bool {
return true; return true;
} }
fun takeNum(p: num) {} fun takeNum(p: num) {}
takeNum(returnBool()); takeNum(returnBool());
""", InvalidTypeException.class); """, InvalidTypeException.class);
} }
@Test @Test
@ -327,13 +328,13 @@ public class SemanticAnalyzerTest {
// Should not be able to use type of shadowed variable in use of shadowing variable // Should not be able to use type of shadowed variable in use of shadowing variable
testInvalid(""" testInvalid("""
fun takesNum(p: num) {} fun takesNum(p: num) {}
var a: num = false; var a: num = false;
{ {
var a: bool = 1; var a: bool = 1;
takesNum(a); takesNum(a);
} }
""", InvalidTypeException.class); """, InvalidTypeException.class);
// Functions can be shadowed in inner scopes // Functions can be shadowed in inner scopes
testValid(""" testValid("""
@ -348,9 +349,9 @@ public class SemanticAnalyzerTest {
// Functions can't be shadowed in the same immediate scope // Functions can't be shadowed in the same immediate scope
testInvalid(""" testInvalid("""
fun test() {} fun test() {}
fun test() {} fun test() {}
""", IdentifierAlreadyDeclaredException.class); """, IdentifierAlreadyDeclaredException.class);
// Can't use function name that is already declared as a variable // Can't use function name that is already declared as a variable
testInvalid("var id: num = 1; fun id() {}", IdentifierAlreadyDeclaredException.class); testInvalid("var id: num = 1; fun id() {}", IdentifierAlreadyDeclaredException.class);