Move terrascript 2 to separate module

This commit is contained in:
Astrash committed 2023-10-23 13:28:26 +11:00
1 parent 375f0ba60f
commit 1623a4f958
156 files changed
+1441 -1495

No files matched your search

@@ -0,0 +1,158 @@
package com.dfsek.terra.addons.terrascript.v2;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.NonexistentSymbolException;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.SymbolAlreadyExistsException;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.SymbolTypeMismatchException;
import com.dfsek.terra.addons.terrascript.v2.Environment.Symbol.Function;
import com.dfsek.terra.addons.terrascript.v2.Environment.Symbol.Variable;
import com.dfsek.terra.addons.terrascript.v2.codegen.NativeFunction;
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;
private Environment(@Nullable Environment outer, boolean canAccessOuterVariables, boolean inLoop, int index) {
this.outer = outer;
this.canAccessOuterVariables = canAccessOuterVariables;
this.inLoop = inLoop;
this.index = index;
this.name = String.join("_", getNestedIndexes().stream().map(Object::toString).toList());
// Populate symbol tables with built-in Java implemented methods
NativeFunction.BUILTIN_FUNCTIONS.forEach((name, function) -> symbolTable.put(name, new Symbol.Function(function.getReturnType(), function.getParameterTypes(), this)));
}
public static Environment global() {
return new Environment(null, false, false, 0);
}
public Environment lexicalInner() {
return new Environment(this, true, inLoop, innerCount++);
}
public Environment loopInner() {
return new Environment(this, true, true, innerCount++);
}
public Environment functionalInner() {
return new Environment(this, false, inLoop, innerCount++);
}
private List<Integer> getNestedIndexes() {
List<Integer> idxs = new ArrayList<>();
for (Environment env = this; env.outer != null; env = env.outer) {
idxs.add(0, env.index);
}
return idxs;
}
public Environment outer() {
if(outer == null) throw new RuntimeException("Attempted to retrieve outer scope of global scope");
return outer;
}
public Function getFunction(String id) throws NonexistentSymbolException, SymbolTypeMismatchException {
Function function;
Symbol symbol = symbolTable.get(id);
if(symbol == null) {
if(outer == null) throw new NonexistentSymbolException();
function = outer.getFunction(id);
} else {
if(!(symbol instanceof Function)) throw new SymbolTypeMismatchException();
function = (Function) symbol;
}
return function;
}
/**
* Returns symbol table entry for a variable identifier, includes enclosing scopes in lookup.
* <br>
* Does not factor context of lookup, checks for order of declaration should be done while
* symbol tables are being populated.
*
* @param id identifier used in variable declaration
*
* @return variable symbol table entry
*
* @throws NonexistentSymbolException if symbol is not declared in symbol table
* @throws SymbolTypeMismatchException if symbol is not a variable
*/
public Variable getVariable(String id) throws NonexistentSymbolException, SymbolTypeMismatchException {
Variable variable;
Symbol symbol = symbolTable.get(id);
if(symbol == null) {
if(!canAccessOuterVariables || outer == null) throw new NonexistentSymbolException();
variable = outer.getVariable(id);
} else {
if(!(symbol instanceof Variable)) throw new SymbolTypeMismatchException();
variable = (Variable) symbol;
}
return variable;
}
public void put(String id, Symbol symbol) throws SymbolAlreadyExistsException {
if(symbolTable.containsKey(id)) throw new SymbolAlreadyExistsException();
symbolTable.put(id, symbol);
}
public static abstract class Symbol {
public static class Function extends Symbol {
public final Type type;
public final List<Type> parameters;
public final Environment scope;
public Function(Type type, List<Type> parameters, Environment scope) {
this.type = type;
this.parameters = parameters;
this.scope = scope;
}
}
public static class Variable extends Symbol {
public final Type type;
public Variable(Type type) {
this.type = type;
}
}
}
public static class ScopeException extends RuntimeException {
public static class SymbolAlreadyExistsException extends ScopeException {
}
public static class NonexistentSymbolException extends ScopeException {
}
public static class SymbolTypeMismatchException extends ScopeException {
}
}
}
@@ -0,0 +1,20 @@
package com.dfsek.terra.addons.terrascript.v2;
import java.util.ArrayList;
import java.util.List;
public class ErrorHandler {
private final List<Exception> exceptions = new ArrayList<>();
public void add(Exception e) {
exceptions.add(e);
}
public void throwAny() throws Exception {
for(Exception e : exceptions) {
throw e;
}
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.inject.annotations.Inject;
public class TerraScript2Addon implements AddonInitializer {
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
}
}
@@ -0,0 +1,97 @@
package com.dfsek.terra.addons.terrascript.v2;
import java.util.Optional;
public interface Type {
static Type fromString(String lexeme) throws TypeException {
return switch(lexeme) {
case "num" -> NUMBER;
case "int" -> INTEGER;
case "str" -> STRING;
case "bool" -> BOOLEAN;
case "()" -> VOID;
default -> throw new TypeException();
};
}
java.lang.reflect.Type javaType();
default boolean typeOf(Type type) {
return this.equals(type);
}
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);
}
Type NUMBER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return double.class;
}
@Override
public String toString() {
return "num";
}
};
Type INTEGER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return int.class;
}
@Override
public String toString() {
return "int";
}
};
Type STRING = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return String.class;
}
@Override
public String toString() {
return "str";
}
};
Type BOOLEAN = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return boolean.class;
}
@Override
public String toString() {
return "bool";
}
};
Type VOID = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return void.class;
}
@Override
public String toString() {
return "()";
}
};
class TypeException extends Exception {
}
}
@@ -0,0 +1,141 @@
package com.dfsek.terra.addons.terrascript.v2.codegen;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilerBugException;
import org.objectweb.asm.Opcodes;
import java.util.Map;
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_PRIMITIVE = new CodegenType(InstructionType.INTEGER, "Z");
private static final Map<Type, CodegenType> TYPE_MAP = Map.of(
Type.BOOLEAN, BOOLEAN_PRIMITIVE,
Type.STRING, new CodegenType(InstructionType.OBJECT, "Ljava/lang/String;"),
Type.NUMBER, new CodegenType(InstructionType.DOUBLE, "D"),
Type.VOID, new CodegenType(InstructionType.VOID, "V")
);
public static CodegenType codegenType(Type type) {
CodegenType out = TYPE_MAP.get(type);
if(out == null)
throw new CompilerBugException();
return out;
}
public enum InstructionType {
DOUBLE {
@Override
public int slotSize() {
return 2;
}
@Override
public int returnInsn() {
return Opcodes.DRETURN;
}
@Override
public int loadInsn() {
return Opcodes.DLOAD;
}
@Override
public int storeInsn() {
return Opcodes.DSTORE;
}
},
OBJECT {
@Override
public int slotSize() {
return 1;
}
@Override
public int returnInsn() {
return Opcodes.ARETURN;
}
@Override
public int loadInsn() {
return Opcodes.ALOAD;
}
@Override
public int storeInsn() {
return Opcodes.ASTORE;
}
},
INTEGER {
@Override
public int slotSize() {
return 1;
}
@Override
public int returnInsn() {
return Opcodes.IRETURN;
}
@Override
public int loadInsn() {
return Opcodes.ILOAD;
}
@Override
public int storeInsn() {
return Opcodes.ISTORE;
}
},
VOID {
@Override
public int slotSize() {
throw new UnsupportedOperationException();
}
@Override
public int returnInsn() {
return Opcodes.RETURN;
}
@Override
public int loadInsn() {
throw new UnsupportedOperationException();
}
@Override
public int storeInsn() {
throw new UnsupportedOperationException();
}
};
public abstract int slotSize();
public abstract int returnInsn();
public abstract int loadInsn();
public abstract int storeInsn();
}
}
@@ -0,0 +1,86 @@
package com.dfsek.terra.addons.terrascript.v2.codegen;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.dfsek.terra.addons.terrascript.v2.Type;
public interface NativeFunction {
Map<String, NativeFunction> BUILTIN_FUNCTIONS = new HashMap<>() {{
put("print", new StaticMethodOfStaticField(
"java/lang/System",
"out",
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
Type.VOID,
List.of(Type.STRING))
);
put("printNum", new StaticMethodOfStaticField(
"java/lang/System",
"out",
"java/io/PrintStream",
"println",
"(D)V",
Type.VOID,
List.of(Type.NUMBER))
);
}};
void pushInstance(MethodVisitor method);
void callMethod(MethodVisitor method);
Type getReturnType();
List<Type> getParameterTypes();
class StaticMethodOfStaticField implements NativeFunction {
private final String fieldOwner;
private final String fieldName;
private final String className;
private final String methodName;
private final String methodDescriptor;
private final Type returnType;
private final List<Type> parameters;
// TODO - Use reflection to obtain these automatically
public StaticMethodOfStaticField(String fieldOwner, String fieldName, String className, String methodName, String methodDescriptor,
Type returnType, List<Type> parameters) {
this.fieldOwner = fieldOwner;
this.fieldName = fieldName;
this.className = className;
this.methodName = methodName;
this.methodDescriptor = methodDescriptor;
this.returnType = returnType;
this.parameters = parameters;
}
@Override
public void pushInstance(MethodVisitor method) {
method.visitFieldInsn(Opcodes.GETSTATIC, fieldOwner, fieldName, "L" + className + ";");
}
@Override
public void callMethod(MethodVisitor method) {
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, methodName, methodDescriptor, false);
}
@Override
public Type getReturnType() {
return returnType;
}
@Override
public List<Type> getParameterTypes() {
return parameters;
}
}
}
@@ -0,0 +1,8 @@
package com.dfsek.terra.addons.terrascript.v2.codegen;
public interface TerraScript {
void execute();
}
@@ -0,0 +1,19 @@
package com.dfsek.terra.addons.terrascript.v2.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);
}
}
@@ -0,0 +1,17 @@
package com.dfsek.terra.addons.terrascript.v2.codegen.asm;
import org.objectweb.asm.Opcodes;
public class OpcodeAlias {
public static int CMP_GREATER_THAN = Opcodes.IFGT;
public static int CMP_GREATER_EQUALS = Opcodes.IFGE;
public static int CMP_LESS_THAN = Opcodes.IFLT;
public static int CMP_LESS_EQUALS = Opcodes.IFLE;
public static int CMP_EQUALS = Opcodes.IFEQ;
public static int CMP_NOT_EQUALS = Opcodes.IFNE;
public static int BOOL_FALSE = Opcodes.IFEQ;
public static int BOOL_TRUE = Opcodes.IFNE;
public static int INTEGERS_EQUAL = Opcodes.IF_ICMPEQ;
public static int INTEGERS_NOT_EQUAL = Opcodes.IF_ICMPNE;
}
@@ -0,0 +1,557 @@
package com.dfsek.terra.addons.terrascript.v2.codegen.asm;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Assignment;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Binary;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Call;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Grouping;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Literal;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Unary;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Void;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Block;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Break;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Continue;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Expression;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.For;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.FunctionDeclaration;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.If;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.NoOp;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Return;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.VariableDeclaration;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.While;
import com.dfsek.terra.addons.terrascript.v2.codegen.CodegenType;
import com.dfsek.terra.addons.terrascript.v2.codegen.CodegenType.InstructionType;
import com.dfsek.terra.addons.terrascript.v2.codegen.NativeFunction;
import com.dfsek.terra.addons.terrascript.v2.codegen.TerraScript;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilerBugException;
import com.dfsek.terra.addons.terrascript.v2.util.ASMUtil;
import com.dfsek.terra.addons.terrascript.v2.parser.BinaryOperator;
import com.dfsek.terra.addons.terrascript.v2.parser.UnaryOperator;
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.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static com.dfsek.terra.addons.terrascript.v2.parser.BinaryOperator.*;
import static com.dfsek.terra.addons.terrascript.v2.parser.UnaryOperator.*;
import static com.dfsek.terra.addons.terrascript.v2.util.ASMUtil.dynamicName;
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;
}
/**
*
* @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;
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(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
constructor.visitCode();
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();
// Generate execute method
String methodName = "execute";
// Extract method description
MethodExtractor extractor = new MethodExtractor(methodName);
new ClassReader(targetClassName).accept(extractor, 0);
String description = extractor.methodDescription;
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();
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 MethodBytecodeGenerator implements TypedStmt.Visitor<Void>, TypedExpr.Visitor<Void> {
private final ClassWriter classWriter;
private final String className;
private final MethodVisitor method;
private final String descriptor;
private final LocalVariablesSorter lvs;
private final Map<String, Integer> lvTable = new HashMap<>();
private final Deque<Pair<Label, Label>> loopStack = new ArrayDeque<>();
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.visitBlockTypedStmt(root);
}
@Override
public Void visitBinaryTypedExpr(Binary expr) {
switch(expr.operator) {
case EQUALS, NOT_EQUALS, BOOLEAN_AND, BOOLEAN_OR, GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> pushComparisonBool(expr);
case ADD -> {
pushBinaryOperands(expr);
CodegenType codegenType = CodegenType.codegenType(expr.type);
if(codegenType.bytecodeType() == InstructionType.DOUBLE)
method.visitInsn(Opcodes.DADD);
else if (Objects.equals(codegenType.getDescriptor(), "Ljava/lang/String;"))
// TODO - Optimize string concatenation
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);
}
case SUBTRACT -> binaryInsn(expr, Opcodes.DSUB);
case MULTIPLY -> binaryInsn(expr, Opcodes.DMUL);
case DIVIDE -> binaryInsn(expr, Opcodes.DDIV);
default -> throw new RuntimeException("Unhandled binary operator " + expr.operator);
}
return null;
}
@Override
public Void visitGroupingTypedExpr(Grouping expr) {
expr.expression.accept(this);
return null;
}
@Override
public Void visitLiteralTypedExpr(Literal expr) {
if(CodegenType.codegenType(expr.type) == CodegenType.BOOLEAN_PRIMITIVE)
if ((boolean) expr.value) pushTrue(); else pushFalse();
else method.visitLdcInsn(expr.value);
return null;
}
@Override
public Void visitUnaryTypedExpr(Unary expr) {
expr.operand.accept(this);
switch (expr.operator) {
case NOT -> invertBool();
case NEGATE -> method.visitInsn(Opcodes.DNEG);
}
return null;
}
@Override
public Void visitCallTypedExpr(Call expr) {
if (NativeFunction.BUILTIN_FUNCTIONS.containsKey(expr.identifier)) {
NativeFunction function = NativeFunction.BUILTIN_FUNCTIONS.get(expr.identifier);
function.pushInstance(method);
expr.arguments.forEach(a -> a.accept(this));
function.callMethod(method);
return null;
}
expr.arguments.forEach(a -> a.accept(this));
List<Type> parameters = expr.arguments.stream().map(e -> e.type).toList();
method.visitMethodInsn(Opcodes.INVOKESTATIC, className, expr.scopedIdentifier, getFunctionDescriptor(parameters, expr.type), false);
return null;
}
@Override
public Void visitVariableTypedExpr(Variable expr) {
Type varType = expr.type;
method.visitVarInsn(CodegenType.codegenType(varType).bytecodeType().loadInsn(), lvTable.get(expr.identifier));
return null;
}
@Override
public Void visitAssignmentTypedExpr(Assignment expr) {
expr.rValue.accept(this);
Type type = expr.lValue.type;
method.visitVarInsn(CodegenType.codegenType(type).bytecodeType().storeInsn(), lvTable.get(expr.lValue.identifier));
return null;
}
@Override
public Void visitVoidTypedExpr(Void expr) {
return null;
}
@Override
public Void visitExpressionTypedStmt(Expression stmt) {
stmt.expression.accept(this);
return null;
}
@Override
public Void visitBlockTypedStmt(Block stmt) {
stmt.statements.forEach(s -> s.accept(this));
return null;
}
/**
* Writes function as a private static method of the current class
* @param stmt
* @return
*/
@Override
public Void visitFunctionDeclarationTypedStmt(FunctionDeclaration stmt) {
List<Type> parameterTypes = stmt.parameters.stream().map(Pair::getRight).toList();
int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
MethodVisitor method = classWriter.visitMethod(access, stmt.scopedIdentifier, getFunctionDescriptor(parameterTypes, stmt.returnType), 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 += CodegenType.codegenType(parameter.getRight()).bytecodeType().slotSize(); // Increment by how many slots data type takes
}
// Generate method bytecode
funcGenerator.generate(stmt.body);
// Finish up
method.visitInsn(Opcodes.RETURN);
method.visitMaxs(0, 0);
method.visitEnd();
return null;
}
@Override
public Void visitVariableDeclarationTypedStmt(VariableDeclaration stmt) {
stmt.value.accept(this);
lvTable.put(stmt.identifier, lvs.newLocal(ASMUtil.tsTypeToAsmType(stmt.type)));
method.visitVarInsn(CodegenType.codegenType(stmt.type).bytecodeType().storeInsn(), lvTable.get(stmt.identifier));
return null;
}
@Override
public Void visitReturnTypedStmt(Return stmt) {
stmt.value.accept(this);
method.visitInsn(CodegenType.codegenType(stmt.value.type).bytecodeType().returnInsn());
return null;
}
@Override
public Void visitIfTypedStmt(If stmt) {
Label endIf = new Label();
conditionalStmt(stmt.condition, stmt.trueBody, endIf);
for(Pair<TypedExpr, Block> elseIfClause : stmt.elseIfClauses) {
conditionalStmt(elseIfClause.getLeft(), elseIfClause.getRight(), endIf);
}
stmt.elseBody.ifPresent(b -> b.accept(this));
label(endIf);
return null;
}
@Override
public Void visitForTypedStmt(For stmt) {
Label loopStart = new Label();
Label loopBody = new Label();
Label loopEnd = new Label();
stmt.initializer.accept(this);
jump(loopBody); // Skip over incrementer on first loop
label(loopStart);
stmt.incrementer.accept(this);
label(loopBody);
loopStack.push(Pair.of(loopStart, loopEnd));
conditionalStmt(stmt.condition, stmt.body, loopStart);
loopStack.pop();
label(loopEnd);
return null;
}
@Override
public Void visitWhileTypedStmt(While stmt) {
Label loopStart = new Label();
Label loopEnd = new Label();
label(loopStart);
loopStack.push(Pair.of(loopStart, loopEnd));
conditionalStmt(stmt.condition, stmt.body, loopStart);
loopStack.pop();
label(loopEnd);
return null;
}
@Override
public Void visitNoOpTypedStmt(NoOp stmt) {
return null;
}
@Override
public Void visitBreakTypedStmt(Break stmt) {
jump(loopStack.getFirst().getRight());
return null;
}
@Override
public Void visitContinueTypedStmt(Continue stmt) {
jump(loopStack.getFirst().getLeft());
return null;
}
private boolean binaryOperandsSameType(Type type, Binary expr) {
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
*/
private void invertBool() {
Label invertToFalse = new Label();
Label finished = new Label();
jumpIf(OpcodeAlias.BOOL_TRUE, invertToFalse);
pushFalse();
jump(finished);
label(invertToFalse);
pushFalse();
label(finished);
}
private void pushBinaryOperands(Binary expr) {
expr.left.accept(this);
expr.right.accept(this);
}
private void binaryInsn(Binary expr, int insn) {
pushBinaryOperands(expr);
method.visitInsn(insn);
}
/**
* Pushes boolean on to the stack based on comparison result
* @param condition
*/
private void pushComparisonBool(TypedExpr condition) {
Label trueFinished = new Label();
conditionalRunnable(condition, this::pushTrue, trueFinished);
pushFalse();
label(trueFinished);
}
/**
* Executes a statement then jumps to the exit label if the condition is true, jumps over the statement if false
* @param condition
* @param stmt
* @param exit
*/
private void conditionalStmt(TypedExpr condition, TypedStmt stmt, Label exit) {
conditionalRunnable(condition, () -> stmt.accept(this), exit);
}
private void pushTrue() {
method.visitInsn(Opcodes.ICONST_1);
}
private void pushFalse() {
method.visitInsn(Opcodes.ICONST_0);
}
private void jumpIf(int opcode, Label label) {
method.visitJumpInsn(opcode, label);
}
private void jump(Label label) {
method.visitJumpInsn(Opcodes.GOTO, label);
}
private void label(Label label) {
method.visitLabel(label);
}
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
if (condition instanceof Binary binaryCondition) {
switch(binaryCondition.operator) {
case BOOLEAN_AND -> {
// Operands assumed booleans
binaryCondition.left.accept(this);
jumpIf(OpcodeAlias.BOOL_FALSE, exit); // If left is false, short circuit, don't evaluate right
binaryCondition.right.accept(this);
jumpIf(OpcodeAlias.BOOL_FALSE, exit);
}
case BOOLEAN_OR -> {
Label skipRight = new Label();
// Operands assumed booleans
binaryCondition.left.accept(this);
jumpIf(OpcodeAlias.BOOL_TRUE, skipRight);
binaryCondition.right.accept(this);
jumpIf(OpcodeAlias.BOOL_FALSE, exit);
label(skipRight);
}
case EQUALS -> {
if (binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers
pushBinaryOperands(binaryCondition);
jumpIf(OpcodeAlias.INTEGERS_NOT_EQUAL, exit);
} else if (binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles
binaryInsn(binaryCondition, Opcodes.DCMPG);
jumpIf(OpcodeAlias.CMP_NOT_EQUALS, exit);
} else if (binaryOperandsSameType(Type.STRING, binaryCondition)) {
pushBinaryOperands(binaryCondition);
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
jumpIf(OpcodeAlias.BOOL_FALSE, exit);
} else throw new CompilerBugException();
}
case NOT_EQUALS -> {
if (binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers
pushBinaryOperands(binaryCondition);
jumpIf(OpcodeAlias.INTEGERS_EQUAL, exit);
} else if (binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles
binaryInsn(binaryCondition, Opcodes.DCMPG);
jumpIf(OpcodeAlias.CMP_EQUALS, exit);
} else if (binaryOperandsSameType(Type.STRING, binaryCondition)) { // Operands assumed references
pushBinaryOperands(binaryCondition);
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
invertBool();
jumpIf(OpcodeAlias.CMP_EQUALS, exit);
} else throw new CompilerBugException();
}
case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> {
// Left and right assumed double
binaryInsn(binaryCondition, switch(binaryCondition.operator) {
case GREATER, GREATER_EQUALS -> Opcodes.DCMPL;
case LESS, LESS_EQUALS -> Opcodes.DCMPG;
default -> throw new IllegalStateException();
});
jumpIf(switch(binaryCondition.operator) {
case GREATER -> OpcodeAlias.CMP_LESS_EQUALS;
case GREATER_EQUALS -> OpcodeAlias.CMP_LESS_THAN;
case LESS -> OpcodeAlias.CMP_GREATER_EQUALS;
case LESS_EQUALS -> OpcodeAlias.CMP_GREATER_THAN;
default -> throw new IllegalStateException();
}, exit);
}
default -> throw new CompilerBugException();
}
} else {
// Assume condition returns bool
condition.accept(this);
jumpIf(OpcodeAlias.BOOL_FALSE, exit);
}
trueBlock.run();
jump(trueFinished); // Jump to end of statement after execution
label(exit);
}
private String getFunctionDescriptor(List<Type> parameters, Type returnType) {
StringBuilder sb = new StringBuilder().append("(");
parameters.stream().map(parameter -> CodegenType.codegenType(parameter).getDescriptor()).forEach(sb::append);
sb.append(")");
sb.append(CodegenType.codegenType(returnType).getDescriptor());
return sb.toString();
}
}
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);
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.exception;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class CompilationException extends Exception {
@Serial
private static final long serialVersionUID = 6744390543046766386L;
private final SourcePosition position;
public CompilationException(String message, SourcePosition position) {
super(message);
this.position = position;
}
@Override
public String getMessage() {
return "Error at " + position + ": " + super.getMessage();
}
public SourcePosition getPosition() {
return position;
}
}
@@ -0,0 +1,5 @@
package com.dfsek.terra.addons.terrascript.v2.exception;
public class CompilerBugException extends RuntimeException {
// TODO - Add message constructor
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.exception.lexer;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class EOFException extends TokenizerException {
@Serial
private static final long serialVersionUID = 3980047409902809440L;
public EOFException(String message, SourcePosition position) {
super(message, position);
}
public EOFException(String message, SourcePosition position, Throwable cause) {
super(message, position, cause);
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.exception.lexer;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class FormatException extends TokenizerException {
@Serial
private static final long serialVersionUID = -791308012940744455L;
public FormatException(String message, SourcePosition position) {
super(message, position);
}
public FormatException(String message, SourcePosition position, Throwable cause) {
super(message, position, cause);
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.exception.lexer;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
import com.dfsek.terra.addons.terrascript.v2.parser.ParseException;
public abstract class TokenizerException extends ParseException {
@Serial
private static final long serialVersionUID = 2792384010083575420L;
public TokenizerException(String message, SourcePosition position) {
super(message, position);
}
public TokenizerException(String message, SourcePosition position, Throwable cause) {
super(message, position, cause);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class IdentifierAlreadyDeclaredException extends CompilationException {
public IdentifierAlreadyDeclaredException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class InvalidFunctionDeclarationException extends CompilationException {
public InvalidFunctionDeclarationException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class InvalidTypeException extends CompilationException {
public InvalidTypeException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class UndefinedReferenceException extends CompilationException {
public UndefinedReferenceException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.lexer;
import java.util.Objects;
public class Char {
private final char character;
private final SourcePosition position;
public Char(char character, SourcePosition position) {
this.character = character;
this.position = position;
}
public boolean is(char... tests) {
for(char test : tests) {
if(test == character && test != '\0') {
return true;
}
}
return false;
}
@Override
public String toString() {
return Character.toString(character);
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Char other = (Char) o;
return character == other.character && Objects.equals(position, other.position);
}
@Override
public int hashCode() {
return Objects.hash(character, position);
}
public char getCharacter() {
return character;
}
public boolean isWhitespace() {
return Character.isWhitespace(character);
}
public boolean isNewLine() {
return character == '\n';
}
public boolean isDigit() {
return Character.isDigit(character);
}
public boolean isEOF() {
return character == '\0';
}
}
@@ -0,0 +1,257 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.lexer;
import com.dfsek.terra.addons.terrascript.v2.parser.ParseException;
import com.dfsek.terra.addons.terrascript.v2.exception.lexer.EOFException;
import com.dfsek.terra.addons.terrascript.v2.exception.lexer.FormatException;
import com.dfsek.terra.addons.terrascript.v2.exception.lexer.TokenizerException;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token.TokenType;
public class Lexer {
public static final Set<Character> syntaxSignificant = Sets.newHashSet(':', ';', '(', ')', '"', ',', '\\', '=', '{', '}', '+', '-', '*',
'/',
'>', '<', '!'); // Reserved chars
private final LookaheadStream reader;
private Token current;
public Lexer(String data) {
reader = new LookaheadStream(data + '\0');
current = tokenize();
}
public List<Token> analyze() {
List<Token> tokens = new ArrayList<>();
while(hasNext()) {
tokens.add(consumeUnchecked());
}
tokens.add(current()); // Add EOF token
return tokens;
}
/**
* Get the first token.
*
* @return First token
*
* @throws ParseException If token does not exist
*/
public Token current() {
return current;
}
/**
* Consume (get and remove) the first token.
*
* @return First token
*
* @throws ParseException If token does not exist
*/
public Token consume(String wrongTypeMessage, TokenType expected, TokenType... more) {
if(!current.isType(expected) && Arrays.stream(more).noneMatch(t -> t == current.type())) throw new ParseException(wrongTypeMessage,
current.position());
return consumeUnchecked();
}
public Token consumeUnchecked() {
if(current.type() == TokenType.END_OF_FILE) return current;
Token temp = current;
current = tokenize();
return temp;
}
/**
* Whether this {@code Tokenizer} contains additional tokens.
*
* @return {@code true} if more tokens are present, otherwise {@code false}
*/
public boolean hasNext() {
return current.type() != TokenType.END_OF_FILE;
}
private Token tokenize() throws TokenizerException {
consumeWhitespace();
SourcePosition position = reader.getPosition();
// Skip line if comment
while(reader.matchesString("//", true)) skipLine();
// Skip multi line comment
if(reader.matchesString("/*", true)) skipTo("*/");
// Reached end of file
if(reader.current().isEOF()) return new Token(reader.consume().toString(), TokenType.END_OF_FILE, position);
// Check if operator token
if(reader.matchesString("==", true))
return new Token("==", TokenType.EQUALS_EQUALS, position);
if(reader.matchesString("!=", true))
return new Token("!=", TokenType.BANG_EQUALS, position);
if(reader.matchesString(">=", true))
return new Token(">=", TokenType.GREATER_EQUAL, position);
if(reader.matchesString("<=", true))
return new Token("<=", TokenType.LESS_EQUALS, position);
if(reader.matchesString(">", true))
return new Token(">", TokenType.GREATER, position);
if(reader.matchesString("<", true))
return new Token("<", TokenType.LESS, position);
// Check if logical operator
if(reader.matchesString("||", true))
return new Token("||", TokenType.BOOLEAN_OR, position);
if(reader.matchesString("&&", true))
return new Token("&&", TokenType.BOOLEAN_AND, position);
// Check if number
if(isNumberStart()) {
StringBuilder num = new StringBuilder();
while(!reader.current().isEOF() && isNumberLike()) {
num.append(reader.consume().getCharacter());
}
return new Token(num.toString(), TokenType.NUMBER, position);
}
// Check if string literal
if(reader.current().is('"')) {
reader.consume(); // Consume first quote
StringBuilder string = new StringBuilder();
boolean ignoreNext = false;
while((!reader.current().is('"')) || ignoreNext) {
if(reader.current().is('\\') && !ignoreNext) {
ignoreNext = true;
reader.consume();
continue;
} else ignoreNext = false;
if(reader.current().isEOF())
throw new FormatException("No end of string literal found. ", position);
string.append(reader.consume());
}
reader.consume(); // Consume last quote
return new Token(string.toString(), TokenType.STRING, position);
}
if(reader.current().is('('))
return new Token(reader.consume().toString(), TokenType.OPEN_PAREN, position);
if(reader.current().is(')'))
return new Token(reader.consume().toString(), TokenType.CLOSE_PAREN, position);
if(reader.current().is(':'))
return new Token(reader.consume().toString(), TokenType.COLON, position);
if(reader.current().is(';'))
return new Token(reader.consume().toString(), TokenType.STATEMENT_END, position);
if(reader.current().is(','))
return new Token(reader.consume().toString(), TokenType.SEPARATOR, position);
if(reader.current().is('{')) return new Token(reader.consume().toString(), TokenType.BLOCK_BEGIN, position);
if(reader.current().is('}')) return new Token(reader.consume().toString(), TokenType.BLOCK_END, position);
if(reader.current().is('='))
return new Token(reader.consume().toString(), TokenType.ASSIGNMENT, position);
if(reader.current().is('+'))
return new Token(reader.consume().toString(), TokenType.PLUS, position);
if(reader.current().is('-'))
return new Token(reader.consume().toString(), TokenType.MINUS,
position);
if(reader.current().is('*'))
return new Token(reader.consume().toString(), TokenType.STAR,
position);
if(reader.current().is('/'))
return new Token(reader.consume().toString(), TokenType.FORWARD_SLASH, position);
if(reader.current().is('%'))
return new Token(reader.consume().toString(), TokenType.MODULO_OPERATOR, position);
if(reader.current().is('!'))
return new Token(reader.consume().toString(), TokenType.BANG, position);
// Read word
StringBuilder token = new StringBuilder();
while(!reader.current().isEOF() && !isSyntaxSignificant(reader.current().getCharacter())) {
Char c = reader.consume();
if(c.isWhitespace()) break;
token.append(c.getCharacter());
}
String tokenString = token.toString();
// Check if word is a keyword
if(tokenString.equals("true"))
return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("false"))
return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("var"))
return new Token(tokenString, TokenType.VARIABLE, position);
if(tokenString.equals("fun"))
return new Token(tokenString, TokenType.FUNCTION, position);
if(tokenString.equals("if"))
return new Token(tokenString, TokenType.IF_STATEMENT, position);
if(tokenString.equals("else"))
return new Token(tokenString, TokenType.ELSE, position);
if(tokenString.equals("while"))
return new Token(tokenString, TokenType.WHILE_LOOP, position);
if(tokenString.equals("for"))
return new Token(tokenString, TokenType.FOR_LOOP, position);
if(tokenString.equals("return"))
return new Token(tokenString, TokenType.RETURN, position);
if(tokenString.equals("continue"))
return new Token(tokenString, TokenType.CONTINUE, position);
if(tokenString.equals("break"))
return new Token(tokenString, TokenType.BREAK, position);
if(tokenString.equals("fail"))
return new Token(tokenString, TokenType.FAIL, position);
// If not keyword, assume it is an identifier
return new Token(tokenString, TokenType.IDENTIFIER, position);
}
private void skipLine() {
while(!reader.current().isEOF() && !reader.current().isNewLine()) reader.consume();
consumeWhitespace();
}
private void consumeWhitespace() {
while(!reader.current().isEOF() && reader.current().isWhitespace()) reader.consume(); // Consume whitespace.
}
private void skipTo(String s) throws EOFException {
SourcePosition begin = reader.getPosition();
while(!reader.current().isEOF()) {
if(reader.matchesString(s, true)) {
consumeWhitespace();
return;
}
reader.consume();
}
throw new EOFException("Reached end of file without matching '" + s + "'", begin);
}
private boolean isNumberLike() {
return reader.current().isDigit()
|| reader.current().is('_', '.', 'E');
}
private boolean isNumberStart() {
return reader.current().isDigit()
|| reader.current().is('.') && reader.peek().isDigit();
}
public boolean isSyntaxSignificant(char c) {
return syntaxSignificant.contains(c);
}
}
@@ -0,0 +1,84 @@
package com.dfsek.terra.addons.terrascript.v2.lexer;
public class LookaheadStream {
private final String source;
private int index;
private SourcePosition position = new SourcePosition(1, 1);
public LookaheadStream(String source) {
this.source = source;
}
/**
* Get the current character without consuming it.
*
* @return current character
*/
public Char current() {
return new Char(source.charAt(index), position);
}
/**
* Consume and return one character.
*
* @return Character that was consumed.
*/
public Char consume() {
Char consumed = current();
incrementIndex(1);
return consumed;
}
/**
* @return The next character in sequence.
*/
public Char peek() {
int index = this.index + 1;
if(index + 1 >= source.length()) return null;
return new Char(source.charAt(index), getPositionAfter(1));
}
/**
* Determines if the contained sequence of characters matches the string
*
* @param check Input string to check against
* @param consumeIfMatched Whether to consume the string if there is a match
*
* @return If the string matches
*/
public boolean matchesString(String check, boolean consumeIfMatched) {
boolean matches = check.equals(source.substring(index, Math.min(index + check.length(), source.length())));
if(matches && consumeIfMatched) incrementIndex(check.length());
return matches;
}
/**
* @return Current position within the source file
*/
public SourcePosition getPosition() {
return position;
}
private void incrementIndex(int amount) {
position = getPositionAfter(amount);
index = Math.min(index + amount, source.length() - 1);
}
private SourcePosition getPositionAfter(int chars) {
if(chars < 0) throw new IllegalArgumentException("Negative values are not allowed");
int line = position.line();
int column = position.column();
for(int i = index; i < Math.min(index + chars, source.length() - 1); i++) {
if(source.charAt(i) == '\n') {
line++;
column = 0;
}
column++;
}
return new SourcePosition(line, column);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.lexer;
import java.util.Objects;
public record SourcePosition(int line, int column) {
@Override
public String toString() {
return "line " + line + ", column " + column;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
SourcePosition that = (SourcePosition) o;
return line == that.line && column == that.column;
}
@Override
public int hashCode() {
return Objects.hash(line, column);
}
}
@@ -0,0 +1,207 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.lexer;
import java.util.Objects;
public class Token {
private final String lexeme;
private final TokenType type;
private final SourcePosition start;
public Token(String lexeme, TokenType type, SourcePosition start) {
this.lexeme = type == TokenType.END_OF_FILE ? "END OF FILE" : lexeme;
this.type = type;
this.start = start;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Token token = (Token) o;
return Objects.equals(lexeme, token.lexeme) && type == token.type && Objects.equals(start, token.start);
}
@Override
public int hashCode() {
return Objects.hash(lexeme, type, start);
}
@Override
public String toString() {
return type + ": '" + lexeme + "'";
}
public TokenType type() {
return type;
}
public String lexeme() {
return lexeme;
}
public SourcePosition position() {
return start;
}
public boolean isType(TokenType... types) {
for(TokenType t : types) if(t == type) return true;
return false;
}
public enum TokenType {
/**
* Function identifier or language keyword
*/
IDENTIFIER,
/**
* Numeric literal
*/
NUMBER,
/**
* String literal
*/
STRING,
/**
* Boolean literal
*/
BOOLEAN,
/**
* Beginning of group
*/
OPEN_PAREN,
/**
* Ending of group
*/
CLOSE_PAREN,
/**
* End of statement
*/
STATEMENT_END,
/**
* Argument separator
*/
SEPARATOR,
/**
* Beginning of code block
*/
BLOCK_BEGIN,
/**
* End of code block
*/
BLOCK_END,
/**
* assignment operator
*/
ASSIGNMENT,
/**
* Boolean equals operator
*/
EQUALS_EQUALS,
/**
* Boolean not equals operator
*/
BANG_EQUALS,
/**
* Boolean greater than operator
*/
GREATER,
/**
* Boolean less than operator
*/
LESS,
/**
* Boolean greater than or equal to operator
*/
GREATER_EQUAL,
/**
* Boolean less than or equal to operator
*/
LESS_EQUALS,
/**
* Addition/concatenation operator
*/
PLUS,
/**
* Subtraction operator
*/
MINUS,
/**
* Multiplication operator
*/
STAR,
/**
* Division operator
*/
FORWARD_SLASH,
/**
* Modulo operator.
*/
MODULO_OPERATOR,
/**
* Boolean not operator
*/
BANG,
/**
* Boolean or
*/
BOOLEAN_OR,
/**
* Boolean and
*/
BOOLEAN_AND,
/**
* Variable declaration
*/
VARIABLE,
/**
* Function declaration
*/
FUNCTION,
COLON,
/**
* If statement declaration
*/
IF_STATEMENT,
/**
* While loop declaration
*/
WHILE_LOOP,
/**
* Return statement
*/
RETURN,
/**
* Continue statement
*/
CONTINUE,
/**
* Break statement
*/
BREAK,
/**
* Fail statement. Like return keyword, but specifies that generation has failed.
*/
FAIL,
/**
* For loop initializer token
*/
FOR_LOOP,
/**
* Else keyword
*/
ELSE,
/**
* End of file
*/
END_OF_FILE
}
}
@@ -0,0 +1,25 @@
package com.dfsek.terra.addons.terrascript.v2.parser;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token;
public enum BinaryOperator {
BOOLEAN_OR(Token.TokenType.BOOLEAN_OR),
BOOLEAN_AND(Token.TokenType.BOOLEAN_AND),
EQUALS(Token.TokenType.EQUALS_EQUALS),
NOT_EQUALS(Token.TokenType.BANG_EQUALS),
GREATER(Token.TokenType.GREATER),
GREATER_EQUALS(Token.TokenType.GREATER_EQUAL),
LESS(Token.TokenType.LESS),
LESS_EQUALS(Token.TokenType.LESS_EQUALS),
ADD(Token.TokenType.PLUS),
SUBTRACT(Token.TokenType.MINUS),
MULTIPLY(Token.TokenType.STAR),
DIVIDE(Token.TokenType.FORWARD_SLASH),
MODULO(Token.TokenType.MODULO_OPERATOR);
public final Token.TokenType tokenType;
BinaryOperator(Token.TokenType tokenType) { this.tokenType = tokenType; }
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.parser;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class ParseException extends RuntimeException {
@Serial
private static final long serialVersionUID = 6744390543046766386L;
private final SourcePosition position;
public ParseException(String message, SourcePosition position) {
super(message);
this.position = position;
}
public ParseException(String message, SourcePosition position, Throwable cause) {
super(message, cause);
this.position = position;
}
@Override
public String getMessage() {
return "Error at " + position + ": " + super.getMessage();
}
public SourcePosition getPosition() {
return position;
}
}
@@ -0,0 +1,379 @@
package com.dfsek.terra.addons.terrascript.v2.parser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.Type.TypeException;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token.TokenType;
import com.dfsek.terra.api.util.generic.pair.Pair;
/**
* TerraScript recursive descent parser
*/
public class Parser {
private final List<Token> tokens;
private int index = 0;
private Parser(List<Token> tokens) {
if(tokens.stream().noneMatch(t -> t.isType(TokenType.END_OF_FILE)))
throw new IllegalArgumentException("Token list must contain at least one token of type " + TokenType.END_OF_FILE);
this.tokens = tokens;
}
public static Block parse(List<Token> tokens) {
return new Parser(tokens).parseTokens();
}
private Block parseTokens() {
List<Stmt> statements = new ArrayList<>();
while(hasNext()) {
statements.add(statement());
}
if(hasNext()) throw new ParseException("Tokens were remaining after parsing", current().position());
return new Stmt.Block(statements, new SourcePosition(0, 0));
}
private Token current() {
return tokens.get(index);
}
private boolean hasNext() {
return !current().isType(TokenType.END_OF_FILE);
}
private Token consume(String wrongTypeMessage, TokenType expected, TokenType... more) {
if(!current().isType(expected) && Arrays.stream(more).noneMatch(t -> t == current().type())) throw new ParseException(
wrongTypeMessage, current().position());
return consumeUnchecked();
}
public Token consumeUnchecked() {
if(!hasNext()) return current();
Token temp = current();
index++;
return temp;
}
private void consumeStatementEnd(String after) {
consume("Expected ';' after " + after + ", found '" + current().lexeme() + "'", TokenType.STATEMENT_END);
}
private Stmt statement() {
return switch(current().type()) {
case BLOCK_BEGIN -> block();
case FUNCTION -> functionDeclaration();
case VARIABLE -> variableDeclaration();
case RETURN -> returnStmt();
case IF_STATEMENT -> ifStmt();
case FOR_LOOP -> forLoop();
case WHILE_LOOP -> whileLoop();
case BREAK -> breakStmt();
case CONTINUE -> continueStmt();
case STATEMENT_END -> new Stmt.NoOp(consumeUnchecked().position());
default -> expressionStatement();
};
}
private Stmt functionDeclaration() {
SourcePosition position = consume("Expected 'fun' keyword at start of function declaration", TokenType.FUNCTION).position();
String id = consume("Expected identifier after 'fun' keyword for function declaration", TokenType.IDENTIFIER).lexeme();
consume("Expected '(' after function identifier '" + id + "'", TokenType.OPEN_PAREN);
// Parse parameters
List<Pair<String, Type>> params = new ArrayList<>();
while(!current().isType(TokenType.CLOSE_PAREN)) {
Token paramToken = consume("Expected parameter name or ')', found '" + current().lexeme() + "'", TokenType.IDENTIFIER);
String paramId = paramToken.lexeme();
if(params.stream().anyMatch(p -> Objects.equals(p.getLeft(), paramId)))
throw new ParseException("Parameter '" + paramId + "' has already been declared in function '" + id + "'",
paramToken.position());
consume("Expected type declaration after parameter name. Example: '" + paramId + ": <type>'", TokenType.COLON);
Type paramType = typeExpr();
params.add(Pair.of(paramId, paramType));
if(current().isType(TokenType.CLOSE_PAREN)) break;
consume("Expected ',' or ')' after parameter declaration '" + paramId + "' in function '" + id + "'", TokenType.SEPARATOR);
}
Type funcReturn = Type.VOID;
consume("Expected ')' after " + (params.size() == 0 ? "')'" : "parameters") + " in declaration of function '" + id + "'",
TokenType.CLOSE_PAREN);
if(current().isType(TokenType.COLON)) {
consumeUnchecked();
funcReturn = typeExpr();
}
Stmt.Block body = blockOrSingleStatement();
return new Stmt.FunctionDeclaration(id, params, funcReturn, body, position);
}
private Stmt.VariableDeclaration variableDeclaration() {
SourcePosition position = consume("Expected 'var' keyword at start of variable declaration", TokenType.VARIABLE).position();
String id = consume("Expected variable name after type for variable declaration", TokenType.IDENTIFIER).lexeme();
consume("Expected ':' after variable name", TokenType.COLON);
Type type = typeExpr();
consume("Expected '=' following variable type declaration", TokenType.ASSIGNMENT);
Expr expr = expression();
consumeStatementEnd("variable declaration");
return new Stmt.VariableDeclaration(type, id, expr, position);
}
private Type typeExpr() {
Token typeToken = consume("Expected " + TokenType.IDENTIFIER + " specified as variable type", TokenType.IDENTIFIER);
try {
return Type.fromString(typeToken.lexeme());
} catch(TypeException e) {
throw new ParseException("Failed to parse type expression", typeToken.position());
}
}
private Stmt.Return returnStmt() {
SourcePosition position = consume("Expected 'return' keyword, found '" + current().lexeme() + "'", TokenType.RETURN).position();
Expr value = new Expr.Void(position);
if(!current().isType(TokenType.STATEMENT_END))
value = expression();
consumeStatementEnd("return statement");
return new Stmt.Return(value, position);
}
private Stmt.If ifStmt() {
// Parse main if clause
SourcePosition position = consume("Expected 'if' keyword at beginning of if statement", TokenType.IF_STATEMENT).position();
consume("Expected '(' after 'if' keyword", TokenType.OPEN_PAREN);
Expr condition = expression();
consume("Expected ')' after if statement condition", TokenType.CLOSE_PAREN);
Stmt.Block trueBody = blockOrSingleStatement();
// Parse any else clauses
Stmt.Block elseBody = null;
List<Pair<Expr, Stmt.Block>> elseIfClauses = new ArrayList<>();
while(current().isType(TokenType.ELSE)) {
consumeUnchecked(); // Consume else
if(!current().isType(TokenType.IF_STATEMENT)) {
elseBody = blockOrSingleStatement();
break; // Else clause should be last in if statement
}
consumeUnchecked(); // Consume if
consume("Expected '(' after 'else if', e.g. 'if else (<condition>) ...'", TokenType.OPEN_PAREN);
Expr elseIfCondition = expression();
consume("Expected ')' after 'else if' clause, e.g. 'else if (<condition>) ...'", TokenType.CLOSE_PAREN);
Stmt.Block elseIfBody = blockOrSingleStatement();
elseIfClauses.add(Pair.of(elseIfCondition, elseIfBody));
}
return new Stmt.If(condition, trueBody, elseIfClauses, Optional.ofNullable(elseBody), position);
}
private Stmt.For forLoop() {
SourcePosition position = consume("Expected 'for' keyword at beginning of for loop", TokenType.FOR_LOOP).position();
consume("Expected '(' after 'for' keyword", TokenType.OPEN_PAREN);
Stmt initializer = statement();
Expr condition;
if(current().isType(TokenType.STATEMENT_END)) {
condition = new Expr.Literal(true, Type.BOOLEAN,
current().position()); // If no condition is provided, set condition = true
consumeUnchecked();
} else {
condition = expression();
consumeStatementEnd("loop condition");
}
Expr incrementer;
if(current().isType(TokenType.CLOSE_PAREN)) {
incrementer = null;
consumeUnchecked();
} else {
incrementer = expression();
consume("Expected ')' after for loop incrementer", TokenType.CLOSE_PAREN);
}
Stmt.Block body = blockOrSingleStatement();
return new Stmt.For(initializer, condition, incrementer, body, position);
}
private Stmt.While whileLoop() {
SourcePosition position = consume("Expected 'for' keyword at beginning of while loop", TokenType.WHILE_LOOP).position();
consume("Expected '(' after 'while' keyword", TokenType.OPEN_PAREN);
Expr condition = expression();
consume("Expected ')' after while loop condition", TokenType.CLOSE_PAREN);
Stmt.Block body = blockOrSingleStatement();
return new Stmt.While(condition, body, position);
}
private Stmt.Break breakStmt() {
SourcePosition position = consume("Expected 'break' keyword for break statement", TokenType.BREAK).position();
consumeStatementEnd("'break' keyword");
return new Stmt.Break(position);
}
private Stmt.Continue continueStmt() {
SourcePosition position = consume("Expected 'continue' keyword for continue statement", TokenType.CONTINUE).position();
consumeStatementEnd("'continue' keyword");
return new Stmt.Continue(position);
}
private Stmt.Block blockOrSingleStatement() {
if(!current().isType(TokenType.BLOCK_BEGIN)) return new Stmt.Block(List.of(statement()), current().position());
return block();
}
private Stmt.Block block() {
SourcePosition position = consume("Expected '{' at start of block", TokenType.BLOCK_BEGIN).position();
List<Stmt> statements = new ArrayList<>();
while(!current().isType(TokenType.BLOCK_END)) {
statements.add(statement());
}
consume("Expected '}' at end of block", TokenType.BLOCK_END);
return new Stmt.Block(statements, position);
}
private Stmt expressionStatement() {
Expr expression = expression();
consumeStatementEnd("expression statement");
return new Stmt.Expression(expression, expression.position);
}
private Expr expression() {
return assignment();
}
private Expr leftAssociativeBinaryExpression(Supplier<Expr> higherPrecedence, BinaryOperator... operators) {
Expr expr = higherPrecedence.get();
loop:
while(true) {
for(BinaryOperator operator : operators) {
if(current().isType(operator.tokenType)) {
SourcePosition position = consumeUnchecked().position(); // Consume operator token
expr = new Expr.Binary(expr, operator, higherPrecedence.get(), position);
continue loop;
}
}
break; // Break if not any operator
}
return expr;
}
private Expr rightAssociativeBinaryExpression(Supplier<Expr> higherPrecedence, BinaryOperator... operators) {
Expr expr = higherPrecedence.get();
for(BinaryOperator operator : operators) {
if(current().isType(operator.tokenType)) {
SourcePosition position = consumeUnchecked().position(); // Consume operator token
return new Expr.Binary(expr, operator, rightAssociativeBinaryExpression(higherPrecedence, operators), position);
}
}
return expr;
}
private Expr assignment() {
Expr expr = logicOr();
if(current().isType(TokenType.ASSIGNMENT)) {
SourcePosition position = consumeUnchecked().position(); // Consume operator token
if(!(expr instanceof Variable variable)) throw new ParseException("Invalid assignment target", position);
return new Expr.Assignment(variable, assignment(), position);
}
return expr;
}
private Expr logicOr() {
return leftAssociativeBinaryExpression(this::logicAnd, BinaryOperator.BOOLEAN_OR);
}
private Expr logicAnd() {
return leftAssociativeBinaryExpression(this::equality, BinaryOperator.BOOLEAN_AND);
}
private Expr equality() {
return leftAssociativeBinaryExpression(this::comparison, BinaryOperator.EQUALS, BinaryOperator.NOT_EQUALS);
}
private Expr comparison() {
return leftAssociativeBinaryExpression(this::term, BinaryOperator.GREATER, BinaryOperator.GREATER_EQUALS, BinaryOperator.LESS,
BinaryOperator.LESS_EQUALS);
}
private Expr term() {
return leftAssociativeBinaryExpression(this::factor, BinaryOperator.ADD, BinaryOperator.SUBTRACT);
}
private Expr factor() {
return leftAssociativeBinaryExpression(this::unary, BinaryOperator.MULTIPLY, BinaryOperator.DIVIDE, BinaryOperator.MODULO);
}
private Expr unary() {
UnaryOperator[] operators = { UnaryOperator.NOT, UnaryOperator.NEGATE };
for(UnaryOperator operator : operators) {
if(current().isType(operator.tokenType)) {
SourcePosition position = consumeUnchecked().position();
return new Expr.Unary(operator, unary(), position);
}
}
return primary();
}
private Expr primary() {
Token token = consumeUnchecked();
SourcePosition position = token.position();
return switch(token.type()) {
case NUMBER -> new Expr.Literal(Double.parseDouble(token.lexeme()), Type.NUMBER, position);
case STRING -> new Expr.Literal(token.lexeme(), Type.STRING, position);
case BOOLEAN -> new Expr.Literal(Boolean.parseBoolean(token.lexeme()), Type.BOOLEAN, position);
case IDENTIFIER -> {
if(current().isType(TokenType.OPEN_PAREN)) yield call(token);
else yield variable(token);
}
case OPEN_PAREN -> {
if(current().isType(TokenType.CLOSE_PAREN)) {
consumeUnchecked(); // Consume ')'
yield new Expr.Void(position); // () evaluates to void
}
Expr expr = expression();
consume("Expected ')' to close '(' located at " + position, TokenType.CLOSE_PAREN);
yield new Expr.Grouping(expr, position);
}
default -> throw new ParseException("Unexpected token '" + token.lexeme() + "'", position);
};
}
private Expr call(Token identifier) {
String id = identifier.lexeme();
SourcePosition position = consume("Expected '(' to initiate function call on function '" + id + "'",
TokenType.OPEN_PAREN).position();
List<Expr> args = new ArrayList<>();
while(!current().isType(TokenType.CLOSE_PAREN)) {
args.add(expression());
if(current().isType(TokenType.CLOSE_PAREN)) break;
consume("Expected ',' or ')' after passed argument in function call of '" + id + "'", TokenType.SEPARATOR);
}
consume("Expected ')' after " + (args.size() == 0 ? "')'" : "arguments") + " in function call of '" + id + "'",
TokenType.CLOSE_PAREN);
return new Expr.Call(id, args, position);
}
private Expr variable(Token identifier) {
return new Expr.Variable(identifier.lexeme(), identifier.position());
}
}
@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.terrascript.v2.parser;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token;
public enum UnaryOperator {
NOT(Token.TokenType.BANG),
NEGATE(Token.TokenType.MINUS);
public final Token.TokenType tokenType;
UnaryOperator(Token.TokenType tokenType) { this.tokenType = tokenType; }
}
@@ -0,0 +1,166 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.NonexistentSymbolException;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.SymbolTypeMismatchException;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Call;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Grouping;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Literal;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Unary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Break;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Continue;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Expression;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.For;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.FunctionDeclaration;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.If;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.NoOp;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Return;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.VariableDeclaration;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.While;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.UndefinedReferenceException;
import com.dfsek.terra.addons.terrascript.v2.parser.ParseException;
import com.dfsek.terra.api.util.generic.pair.Pair;
public class FunctionReferenceAnalyzer implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
private final ErrorHandler errorHandler;
public FunctionReferenceAnalyzer(ErrorHandler errorHandler) { this.errorHandler = errorHandler; }
@Override
public Void visitBinaryExpr(Binary expr) {
expr.left.accept(this);
expr.right.accept(this);
return null;
}
@Override
public Void visitGroupingExpr(Grouping expr) {
expr.expression.accept(this);
return null;
}
@Override
public Void visitLiteralExpr(Literal expr) {
return null;
}
@Override
public Void visitUnaryExpr(Unary expr) {
expr.operand.accept(this);
return null;
}
@Override
public Void visitCallExpr(Call expr) {
String id = expr.identifier;
try {
expr.setSymbol(expr.getEnvironment().getFunction(expr.identifier));
} catch(NonexistentSymbolException e) {
errorHandler.add(
new UndefinedReferenceException("No function by the name '" + id + "' is defined in this scope", expr.position));
} catch(SymbolTypeMismatchException e) {
errorHandler.add(new ParseException("Identifier '" + id + "' is not defined as a function", expr.position));
}
expr.arguments.forEach(e -> e.accept(this));
return null;
}
@Override
public Void visitVariableExpr(Variable expr) {
return null;
}
@Override
public Void visitAssignmentExpr(Assignment expr) {
expr.lValue.accept(this);
expr.rValue.accept(this);
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) {
stmt.body.accept(this);
return null;
}
@Override
public Void visitVariableDeclarationStmt(VariableDeclaration stmt) {
stmt.value.accept(this);
return null;
}
@Override
public Void visitReturnStmt(Return stmt) {
stmt.value.accept(this);
return null;
}
@Override
public Void visitIfStmt(If stmt) {
stmt.condition.accept(this);
stmt.trueBody.accept(this);
for(Pair<Expr, Block> clause : stmt.elseIfClauses) {
clause.getLeft().accept(this);
clause.getRight().accept(this);
}
stmt.elseBody.ifPresent(b -> b.accept(this));
return null;
}
@Override
public Void visitForStmt(For stmt) {
stmt.initializer.accept(this);
stmt.condition.accept(this);
stmt.incrementer.accept(this);
stmt.body.accept(this);
return null;
}
@Override
public Void visitWhileStmt(While stmt) {
stmt.condition.accept(this);
stmt.body.accept(this);
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;
}
}
@@ -0,0 +1,200 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.Environment;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.NonexistentSymbolException;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.SymbolTypeMismatchException;
import com.dfsek.terra.addons.terrascript.v2.Environment.Symbol;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Call;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Grouping;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Literal;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Unary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Visitor;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.IdentifierAlreadyDeclaredException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.UndefinedReferenceException;
import com.dfsek.terra.addons.terrascript.v2.parser.ParseException;
import com.dfsek.terra.api.util.generic.pair.Pair;
public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
private Environment currentScope;
private final ErrorHandler errorHandler;
public ScopeAnalyzer(Environment globalScope, ErrorHandler errorHandler) {
this.currentScope = globalScope;
this.errorHandler = errorHandler;
}
@Override
public Void visitBinaryExpr(Binary expr) {
expr.right.accept(this);
expr.left.accept(this);
return null;
}
@Override
public Void visitGroupingExpr(Grouping expr) {
expr.expression.accept(this);
return null;
}
@Override
public Void visitLiteralExpr(Literal expr) {
return null;
}
@Override
public Void visitUnaryExpr(Unary expr) {
expr.operand.accept(this);
return null;
}
@Override
public Void visitCallExpr(Call expr) {
expr.setEnvironment(currentScope);
expr.arguments.forEach(e -> e.accept(this));
return null;
}
@Override
public Void visitVariableExpr(Variable expr) {
String id = expr.identifier;
try {
expr.setSymbol(currentScope.getVariable(id));
} catch(NonexistentSymbolException e) {
errorHandler.add(
new UndefinedReferenceException("No variable by the name '" + id + "' is defined in this scope", expr.position));
} catch(SymbolTypeMismatchException e) {
errorHandler.add(new ParseException("Identifier '" + id + "' is not defined as a variable", expr.position));
}
return null;
}
@Override
public Void visitAssignmentExpr(Assignment expr) {
expr.lValue.accept(this);
expr.rValue.accept(this);
return null;
}
@Override
public Void visitVoidExpr(Void expr) {
return null;
}
@Override
public Void visitExpressionStmt(Stmt.Expression stmt) {
stmt.expression.accept(this);
return null;
}
@Override
public Void visitBlockStmt(Stmt.Block stmt) {
currentScope = currentScope.lexicalInner();
stmt.statements.forEach(s -> s.accept(this));
currentScope = currentScope.outer();
return null;
}
@Override
public Void visitFunctionDeclarationStmt(Stmt.FunctionDeclaration stmt) {
currentScope = currentScope.functionalInner();
for(Pair<String, Type> param : stmt.parameters) {
try {
currentScope.put(param.getLeft(), new Environment.Symbol.Variable(param.getRight()));
} catch(Environment.ScopeException.SymbolAlreadyExistsException e) {
throw new IllegalStateException("Formal parameter '" + param.getLeft() + "' defined in '" + stmt.identifier +
"' already exists in the function scope");
}
}
stmt.body.accept(this);
currentScope = currentScope.outer();
try {
Symbol.Function symbol = new Symbol.Function(stmt.returnType, stmt.parameters.stream().map(Pair::getRight).toList(), currentScope);
stmt.setSymbol(symbol);
currentScope.put(stmt.identifier, symbol);
} catch(Environment.ScopeException.SymbolAlreadyExistsException e) {
errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope",
stmt.position));
}
return null;
}
@Override
public Void visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) {
stmt.value.accept(this);
try {
currentScope.put(stmt.identifier, new Environment.Symbol.Variable(stmt.type));
} catch(Environment.ScopeException.SymbolAlreadyExistsException e) {
errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope",
stmt.position));
}
return null;
}
@Override
public Void visitReturnStmt(Stmt.Return stmt) {
stmt.value.accept(this);
return null;
}
@Override
public Void visitIfStmt(Stmt.If stmt) {
stmt.condition.accept(this);
stmt.trueBody.accept(this);
for(Pair<Expr, Stmt.Block> clause : stmt.elseIfClauses) {
clause.getLeft().accept(this);
clause.getRight().accept(this);
}
stmt.elseBody.ifPresent(b -> b.accept(this));
return null;
}
@Override
public Expr.Void visitForStmt(Stmt.For stmt) {
currentScope = currentScope.loopInner(); // Loop initializer, condition, and incrementer belong to inner scope
stmt.initializer.accept(this);
stmt.condition.accept(this);
stmt.incrementer.accept(this);
stmt.body.accept(this);
currentScope = currentScope.outer();
return null;
}
@Override
public Expr.Void visitWhileStmt(Stmt.While stmt) {
stmt.condition.accept(this);
currentScope = currentScope.loopInner();
stmt.body.accept(this);
currentScope = currentScope.outer();
return null;
}
@Override
public Expr.Void visitNoOpStmt(Stmt.NoOp stmt) {
return null;
}
@Override
public Expr.Void visitBreakStmt(Stmt.Break stmt) {
return null;
}
@Override
public Expr.Void visitContinueStmt(Stmt.Continue stmt) {
return null;
}
}
@@ -0,0 +1,24 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.Environment;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt;
public class SemanticAnalyzer {
public static TypedStmt.Block analyze(Stmt.Block root, ErrorHandler errorHandler) throws Exception {
new ScopeAnalyzer(Environment.global(), errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
new FunctionReferenceAnalyzer(errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
TypedStmt.Block checkedRoot = (TypedStmt.Block) new TypeChecker(errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
return checkedRoot;
}
}
@@ -0,0 +1,258 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import java.util.List;
import java.util.Optional;
import com.dfsek.terra.addons.terrascript.v2.Environment;
import com.dfsek.terra.addons.terrascript.v2.Environment.Symbol;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Call;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Grouping;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Literal;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Unary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Visitor;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidFunctionDeclarationException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidTypeException;
import com.dfsek.terra.addons.terrascript.v2.parser.ParseException;
import com.dfsek.terra.api.util.generic.pair.Pair;
import static com.dfsek.terra.addons.terrascript.v2.util.OrdinalUtil.ordinalOf;
public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt> {
private final ErrorHandler errorHandler;
TypeChecker(ErrorHandler errorHandler) { this.errorHandler = errorHandler; }
private static String scopedIdentifier(String identifier, Symbol.Function symbol) {
return identifier + "$" + symbol.scope.name;
}
@Override
public TypedExpr visitBinaryExpr(Binary expr) {
TypedExpr left = expr.left.accept(this);
TypedExpr right = expr.right.accept(this);
Type leftType = left.type;
Type rightType = right.type;
Type type = switch(expr.operator) {
case BOOLEAN_OR, BOOLEAN_AND -> {
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));
yield Type.BOOLEAN;
}
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));
yield Type.BOOLEAN;
}
case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> {
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));
yield Type.BOOLEAN;
}
case ADD -> {
if(leftType.typeOf(Type.NUMBER) && rightType.typeOf(Type.NUMBER)) {
yield Type.NUMBER;
}
if(leftType.typeOf(Type.STRING) || rightType.typeOf(Type.STRING)) {
yield Type.STRING;
}
errorHandler.add(new RuntimeException("Addition operands must be either both of type '" + Type.NUMBER + "', or one of type '" + Type.STRING + "'"));
yield Type.VOID;
}
case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> {
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));
yield Type.NUMBER;
}
};
return new TypedExpr.Binary(left, expr.operator, right, type);
}
@Override
public TypedExpr visitGroupingExpr(Grouping expr) {
return expr.expression.accept(this);
}
@Override
public TypedExpr visitLiteralExpr(Literal expr) {
return new TypedExpr.Literal(expr.value, expr.type);
}
@Override
public TypedExpr visitUnaryExpr(Unary expr) {
TypedExpr right = expr.operand.accept(this);
Type type = switch(expr.operator) {
case NOT -> {
if(!right.type.typeOf(Type.BOOLEAN)) throw new RuntimeException();
yield Type.BOOLEAN;
}
case NEGATE -> {
if(!right.type.typeOf(Type.NUMBER)) throw new RuntimeException();
yield Type.NUMBER;
}
};
return new TypedExpr.Unary(expr.operator, right, type);
}
@Override
public TypedExpr visitCallExpr(Call expr) {
String id = expr.identifier;
Environment.Symbol.Function signature = expr.getSymbol();
List<TypedExpr> arguments = expr.arguments.stream().map(a -> a.accept(this)).toList();
List<Type> parameters = signature.parameters;
if(arguments.size() != parameters.size())
errorHandler.add(new ParseException(
"Provided " + arguments.size() + " arguments to function call of '" + id + "', expected " + parameters.size() +
" arguments", expr.position));
for(int i = 0; i < parameters.size(); i++) {
Type expectedType = parameters.get(i);
Type providedType = arguments.get(i).type;
if(!expectedType.typeOf(providedType))
errorHandler.add(new InvalidTypeException(
ordinalOf(i + 1) + " argument provided for function '" + id + "' expects type " + expectedType + ", found " +
providedType + " instead", expr.position));
}
return new TypedExpr.Call(expr.identifier, arguments, scopedIdentifier(expr.identifier, expr.getSymbol()), signature.type);
}
@Override
public TypedExpr visitVariableExpr(Variable expr) {
return new TypedExpr.Variable(expr.identifier, expr.getSymbol().type);
}
@Override
public TypedExpr visitAssignmentExpr(Assignment expr) {
TypedExpr.Variable left = (TypedExpr.Variable) expr.lValue.accept(this);
TypedExpr right = expr.rValue.accept(this);
Type expected = left.type;
String id = expr.lValue.identifier;
if(!right.type.typeOf(expected))
errorHandler.add(new InvalidTypeException(
"Cannot assign variable '" + id + "' to value of type " + right.type + ", '" + id + "' is declared with type " + expected,
expr.position));
return new TypedExpr.Assignment(left, right, right.type);
}
@Override
public TypedExpr visitVoidExpr(Void expr) {
return new TypedExpr.Void(Type.VOID);
}
@Override
public TypedStmt visitExpressionStmt(Stmt.Expression stmt) {
return new TypedStmt.Expression(stmt.expression.accept(this));
}
@Override
public TypedStmt visitBlockStmt(Stmt.Block stmt) {
return new TypedStmt.Block(stmt.statements.stream().map(s -> s.accept(this)).toList());
}
@Override
public TypedStmt visitFunctionDeclarationStmt(Stmt.FunctionDeclaration stmt) {
TypedStmt.Block body = new TypedStmt.Block(stmt.body.statements.stream().map(s -> s.accept(this)).toList());
boolean hasReturn = alwaysReturns(body, stmt);
if(!stmt.returnType.typeOf(Type.VOID) && !hasReturn) {
errorHandler.add(
new InvalidFunctionDeclarationException("Function body for '" + stmt.identifier + "' does not contain return statement",
stmt.position));
}
return new TypedStmt.FunctionDeclaration(stmt.identifier, stmt.parameters, stmt.returnType, body, scopedIdentifier(stmt.identifier, stmt.getSymbol()));
}
private boolean alwaysReturns(TypedStmt stmt, Stmt.FunctionDeclaration function) {
if(stmt instanceof TypedStmt.Return ret) {
if(!ret.value.type.typeOf(function.returnType))
errorHandler.add(new InvalidTypeException(
"Return statement must match function's return type. Function '" + function.identifier + "' expects " +
function.returnType + ", found " + ret.value.type + " instead", function.position));
return true;
} else if (stmt instanceof TypedStmt.If ifStmt) {
return alwaysReturns(ifStmt.trueBody, 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
} else if (stmt instanceof TypedStmt.Block block) {
return block.statements.stream().anyMatch(s -> alwaysReturns(s, function));
}
return false;
}
@Override
public TypedStmt visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) {
TypedExpr value = stmt.value.accept(this);
if(!stmt.type.typeOf(value.type))
errorHandler.add(new InvalidTypeException(
"Type of value assigned to variable '" + stmt.identifier + "' 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);
}
@Override
public TypedStmt visitReturnStmt(Stmt.Return stmt) {
return new TypedStmt.Return(stmt.value.accept(this));
}
@Override
public TypedStmt visitIfStmt(Stmt.If stmt) {
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));
TypedStmt.Block trueBody = (TypedStmt.Block) stmt.trueBody.accept(this);
List<Pair<TypedExpr, TypedStmt.Block>> elseIfClauses = stmt.elseIfClauses.stream().map(c -> {
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));
return Pair.of(clauseCondition, (TypedStmt.Block) c.getRight().accept(this));
}).toList();
Optional<TypedStmt.Block> elseBody = stmt.elseBody.map(b -> (TypedStmt.Block) b.accept(this));
return new TypedStmt.If(condition, trueBody, elseIfClauses, elseBody);
}
@Override
public TypedStmt visitForStmt(Stmt.For stmt) {
TypedStmt initializer = stmt.initializer.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));
TypedExpr incrementer = stmt.incrementer.accept(this);
return new TypedStmt.For(initializer, condition, incrementer, (TypedStmt.Block) stmt.body.accept(this));
}
@Override
public TypedStmt visitWhileStmt(Stmt.While stmt) {
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));
return new TypedStmt.While(condition, (TypedStmt.Block) stmt.body.accept(this));
}
@Override
public TypedStmt visitNoOpStmt(Stmt.NoOp stmt) {
return new TypedStmt.NoOp();
}
@Override
public TypedStmt visitBreakStmt(Stmt.Break stmt) {
return new TypedStmt.Break();
}
@Override
public TypedStmt visitContinueStmt(Stmt.Continue stmt) {
return new TypedStmt.Continue();
}
}
@@ -0,0 +1,20 @@
package com.dfsek.terra.addons.terrascript.v2.util;
import com.dfsek.terra.addons.terrascript.v2.Type;
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('.', '/');
}
public static org.objectweb.asm.Type tsTypeToAsmType(Type type) {
return org.objectweb.asm.Type.getType((Class<?>) type.javaType());
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.util;
public class OrdinalUtil {
public static String ordinalOf(int i) {
String[] suffixes = new String[]{ "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" };
return switch(i % 100) {
case 11, 12, 13 -> i + "th";
default -> i + suffixes[i % 10];
};
}
}
@@ -0,0 +1,12 @@
schema-version: 1
contributors:
- Terra contributors
id: structure-terrascript-v2
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.terrascript.v2.TerraScript2Addon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License
@@ -0,0 +1,118 @@
package codegen;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.v2.codegen.TerraScript;
import com.dfsek.terra.addons.terrascript.v2.codegen.asm.TerraScriptClassGenerator;
import com.dfsek.terra.addons.terrascript.v2.lexer.Lexer;
import com.dfsek.terra.addons.terrascript.v2.parser.Parser;
import com.dfsek.terra.addons.terrascript.v2.semanticanalysis.SemanticAnalyzer;
import org.junit.jupiter.api.Test;
public class CodeGenTest {
@Test
public void test() {
testValid("""
printNum(12345);
if (1 == 1) print("Dis is true");
var a: num = 1;
var b: num = 2;
var e: str = "test";
if (a <= b) {
print("a is <= b");
} else {
print("a is not <= b");
}
if (e == "foo") {
print("e is == foo");
} else if (e == "bar") {
print("e is == bar");
} else {
print("e is not foo or bar");
}
if (true && false || (false && true)) {
print("Thin is tru");
} else {
print("Thin is not tru :(");
}
fun loopTwiceThenBreak() {
var i: num = 0;
while (true) {
print("looped");
if (i == 1) break;
i = i + 1;
}
}
print("Should loop twice:");
loopTwiceThenBreak();
retNum();
var bln: bool = true;
print(takesArgs("test", 3, true));
print(retStr());
doStuff("Ayo", "world", true);
fun retNum(): num {
return 3 + 3;
}
fun retBool(): bool {
return true;
}
fun concatThree(a: str, b: str, c: str): str {
fun concatTwo(a: str, b: str): str {
return a + b;
}
return concatTwo(a, b) + c;
}
fun retStr(): str {
fun concatTwo(a: str, b: str): str {
return a + b;
}
var hello: str = "Hell";
hello = concatTwo(hello, "o");
var world: str = "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");
}
}
""");
}
private void testValid(String validSource) {
try {
Block script = Parser.parse(new Lexer(validSource).analyze());
TypedStmt.Block typedScript = SemanticAnalyzer.analyze(script, new ErrorHandler());
TerraScript ts = new TerraScriptClassGenerator("./build/codegentest").generate(typedScript);
ts.execute();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,34 @@
package lexer;
import com.dfsek.terra.addons.terrascript.v2.lexer.Lexer;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token.TokenType;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class LexerTest {
private static void tokenTypeTest(String input, TokenType type) {
Lexer lexer = new Lexer(input);
assertEquals(new Token(input, type, new SourcePosition(1, 1)), lexer.current());
}
@Test
public void typeTest() {
tokenTypeTest("identifier", TokenType.IDENTIFIER);
tokenTypeTest("(", TokenType.OPEN_PAREN);
tokenTypeTest(")", TokenType.CLOSE_PAREN);
}
@Test
public void multipleTokensTest() {
Lexer lexer = new Lexer("(3 + 2)");
lexer.analyze().forEach(System.out::println);
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package lexer;
import com.dfsek.terra.addons.terrascript.v2.lexer.LookaheadStream;
import com.dfsek.terra.addons.terrascript.v2.lexer.Char;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class LookaheadStreamTest {
@Test
public void lookahead() {
String testString = "Test string...\nNew line";
LookaheadStream lookahead = new LookaheadStream(testString);
Char first = new Char('T', new SourcePosition(1, 1));
Char second = new Char('e', new SourcePosition(1, 2));
Char third = new Char('s', new SourcePosition(1, 3));
Char space = new Char(' ', new SourcePosition(1, 5));
Char newline = new Char('\n', new SourcePosition(1, 15));
Char lineTwoColOne = new Char('N', new SourcePosition(2, 1));
String lineTwo = "New line";
assertTrue(lookahead.matchesString("Test", false));
assertTrue(lookahead.matchesString(testString, false));
assertFalse(lookahead.matchesString(testString + "asdf", false));
assertFalse(lookahead.matchesString("Foo", false));
assertEquals(first, lookahead.current());
assertEquals(first, lookahead.current());
assertEquals(new SourcePosition(1, 1), lookahead.getPosition());
assertEquals(new SourcePosition(1, 1), lookahead.getPosition());
assertEquals(second, lookahead.peek());
assertEquals(second, lookahead.peek());
assertEquals(first, lookahead.consume());
assertFalse(lookahead.matchesString(testString, false));
assertEquals(second, lookahead.current());
assertEquals(second, lookahead.consume());
assertEquals(third, lookahead.current());
assertTrue(lookahead.matchesString("st", true));
assertEquals(space, lookahead.current());
assertEquals(space, lookahead.consume());
assertTrue(lookahead.matchesString("string...", false));
assertTrue(lookahead.matchesString("string...", true));
assertFalse(lookahead.matchesString("string...", false));
assertEquals(newline, lookahead.current());
assertEquals(newline, lookahead.consume());
assertEquals(lineTwoColOne, lookahead.current());
assertTrue(lookahead.matchesString(lineTwo, false));
assertFalse(lookahead.matchesString(lineTwo + "asdf", false));
assertTrue(lookahead.matchesString(lineTwo, true));
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
assertDoesNotThrow(lookahead::consume);
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
assertDoesNotThrow(lookahead::consume);
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
}
}
@@ -0,0 +1,365 @@
package semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.IdentifierAlreadyDeclaredException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.UndefinedReferenceException;
import com.dfsek.terra.addons.terrascript.v2.semanticanalysis.SemanticAnalyzer;
import com.dfsek.terra.addons.terrascript.v2.lexer.Lexer;
import com.dfsek.terra.addons.terrascript.v2.parser.Parser;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidFunctionDeclarationException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidTypeException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class SemanticAnalyzerTest {
@Test
public void testVariableReference() {
// Use of declared variable
testValid("var a: num = 1; a + a;");
// Can't use undeclared variable
testInvalid("a + a;", UndefinedReferenceException.class);
// Can't reference variable before declaration
testInvalid("a + a; var a: num = 1;", UndefinedReferenceException.class);
// Variable declarations shouldn't be accessible from inner scopes
testInvalid("{ var a: num = 1; } a + a;", UndefinedReferenceException.class);
// Can access variables declared in outer scope
testValid("var a: num = 3; { a + a; }");
// Should not be able to use variables from outer scope if they're declared after scope
testInvalid("{ a + a; } var a: num = 2;", UndefinedReferenceException.class);
// Can't use undeclared variable as function argument
testInvalid("fun test(p: str) {} test(a);", UndefinedReferenceException.class);
// Same as above, but in inner scope
testInvalid("fun test(p: str) {} { test(a); }", UndefinedReferenceException.class);
// Cannot assign undeclared variable
testInvalid("a = 1;", UndefinedReferenceException.class);
// Cannot assign to variable declared after assignment
testInvalid("a = 2; var a: num = 1;", UndefinedReferenceException.class);
}
@Test
public void testAssignment() {
// Simple assignment
testValid("var a: num = 1; a = 2;");
// Can assign variables declared in outer scope
testValid("""
var a: num = 1;
{ a = 2; }
""");
// Cannot assign variables declared in inner scope
testInvalid("""
{ var a: num = 1; }
a = 2;
""", UndefinedReferenceException.class);
// Cannot assign variables declared in outer scope after reference
testInvalid("""
{ a = 2; }
var a: num = 1;
""", UndefinedReferenceException.class);
// Cannot assign variable to expression of different type
testInvalid("var a: num = 1; a = true;", InvalidTypeException.class);
}
@Test
public void testReturnStatement() {
// Function return must match signature
testInvalid("fun returnBool(): bool {}", InvalidFunctionDeclarationException.class);
testInvalid("fun returnNum(): num { return \"Not num\"; }", InvalidTypeException.class);
// Return statements can be empty for void return
testValid("fun returnVoid() { return; }");
// Return statement returns type matching function signature
testValid("fun returnNum(): num { return 3; }");
testValid("fun returnVoid() { return (); }");
}
@Test
public void testFunctionReturnControlFlowAnalysis() {
// Non-void returning function bodies must contain at least one statement that always returns
testInvalid("""
fun returnsNum(): num {
}
""", InvalidFunctionDeclarationException.class);
testValid("""
fun returnsNum(): num {
return 1;
}
""");
testValid("""
fun returnsNum(): num {
1 + 1;
return 1;
}
""");
// Statements after the first always-return-statement are unreachable, unreachable code is legal
testValid("""
fun returnsNum(): num {
return 1;
1 + 1; // Unreachable
}
""");
// Void returning functions can omit returns
testValid("""
fun returnsNothing() {
}
""");
// Returns can still be explicitly used for void returning functions
testValid("""
fun returnsNum(p: bool) {
if (p) {
return;
}
}
""");
// If all if-statement bodies always return, then the statement is considered as always returning
testValid("""
fun returnsNum(p: bool): num {
if (p) {
return 1;
} else {
return 0;
}
}
""");
testValid("""
fun returnsNum(p1: bool, p2: bool): num {
if (p1) {
return 1;
} else if (p2) {
return 2;
} else {
return 3;
}
}
""");
// If no else body is defined, an if-statement does not always return, therefore the function does not contain any always-return-statements
testInvalid("""
fun returnsNum(p: bool): num {
if (p) {
return 1;
}
}
""", InvalidFunctionDeclarationException.class);
testInvalid("""
fun returnsNum(p1: bool, p2: bool): num {
if (p1) {
return 1;
} else if (p2) {
return 2;
}
}
""", InvalidFunctionDeclarationException.class);
// Nested ifs should work
testValid("""
fun returnsNum(p1: bool, p2: bool): num {
if (p1) {
if (p2) {
return 1;
} else {
return 2;
}
} else {
return 3;
}
}
""");
testInvalid("""
fun returnsNum(p1: bool, p2: bool): num {
if (p1) {
if (p2) {
return 1;
}
// No else clause here, so will not always return
} else {
return 3;
}
}
""", InvalidFunctionDeclarationException.class);
// If-statement may not always return but a return statement after it means function will always return
testValid("""
fun returnsNum(p: bool): num {
if (p) {
return 1;
}
return 2;
}
""");
// Same applies when statements are swapped
testValid("""
fun returnsNum(p: bool): num {
return 1;
// Unreachable
if (p) {
return 2;
}
}
""");
}
@Test
public void testFunctionCall() {
// Simple function declaration then call
testValid("fun test() {}; test();");
// Can be used before declaration
testValid("test(); fun test() {};");
// Can be used from outer scope
testValid("fun test() {}; { test(); }");
// Can be used from outer scope before declaration
testValid("{ test(); } fun test() {};");
// Can be used in many outer scopes
testValid("{{{{{ test(); }}}}} fun test() {};");
// Calling function that hasn't been declared
testInvalid("test();", UndefinedReferenceException.class);
// Cannot use functions declared in inner scopes
testInvalid("{ fun test() {} } test();", UndefinedReferenceException.class);
testInvalid("test(); { fun test() {} }", UndefinedReferenceException.class);
// Mutual recursion supported
testValid("fun a() { b(); } fun b() { a(); }");
}
@Test
public void testFunctionArgumentPassing() {
// Simple argument passing
testValid("fun test(p: num); test(1);");
// Passing multiple arguments
testValid("fun test(p1: num, p2: bool); test(1, false);");
// Argument type must match parameter type
testInvalid("fun test(p: num); test(false);", InvalidTypeException.class);
// Function return
testValid("""
fun returnBool(): bool {
return true;
}
fun takeBool(p: bool) {}
takeBool(returnBool());
""");
// Should not be able to pass argument of type not matching parameter type
testInvalid("""
fun returnBool(): bool {
return true;
}
fun takeNum(p: num) {}
takeNum(returnBool());
""", InvalidTypeException.class);
}
@Test
public void testParameterUse() {
// Function bodies should be able to use parameter names
testValid("fun test(a: num, b: num) { a + b; }");
testInvalid("fun test(a: num, b: num) { a + c; }", UndefinedReferenceException.class);
// Function bodies can't use variables from outer scope
testInvalid("var a: num = 1; fun doStuff() { a + 2; }", UndefinedReferenceException.class);
testInvalid("fun doStuff() { a + 2; } var a: num = 1;", UndefinedReferenceException.class);
// Type checking parameters
testValid("fun takesNum(a: num) {} fun test(numberParam: num) { takesNum(numberParam); }");
testInvalid("fun takesNum(a: num) {} fun test(boolParam: bool) { takesNum(boolParam); }", InvalidTypeException.class);
}
@Test
public void testShadowing() {
// Can't shadow variable in immediate scope
testInvalid("var a: num = 1; var a: num = 2;", IdentifierAlreadyDeclaredException.class);
// Can shadow variable from outer scope
testValid("var a: num = 1; { var a: num = 2; }");
// Can declare variable after same identifier is used previously in an inner scope
testValid("{ var a: num = 2; } var a: num = 1;");
// Ensure shadowed variable type is used
testValid("""
fun takesNum(p: num) {}
var a: bool = false;
{
var a: num = 1;
takesNum(a);
}
""");
// Should not be able to use type of shadowed variable in use of shadowing variable
testInvalid("""
fun takesNum(p: num) {}
var a: num = false;
{
var a: bool = 1;
takesNum(a);
}
""", InvalidTypeException.class);
// Functions can be shadowed in inner scopes
testValid("""
fun test() {}
{
fun test() {}
}
{
fun test() {}
}
""");
// Functions can't be shadowed in the same immediate scope
testInvalid("""
fun test() {}
fun test() {}
""", IdentifierAlreadyDeclaredException.class);
// Can't use function name that is already declared as a variable
testInvalid("var id: num = 1; fun id() {}", IdentifierAlreadyDeclaredException.class);
}
private <T extends Exception> void testInvalid(String invalidSource, Class<T> exceptionType) {
ErrorHandler errorHandler = new ErrorHandler();
assertThrows(exceptionType, () -> SemanticAnalyzer.analyze(Parser.parse(new Lexer(invalidSource).analyze()), errorHandler));
}
private void testValid(String validSource) {
ErrorHandler errorHandler = new ErrorHandler();
assertDoesNotThrow(() -> SemanticAnalyzer.analyze(Parser.parse(new Lexer(validSource).analyze()), errorHandler));
}
}