Compare commits

...

34 Commits

Author SHA1 Message Date
Astrash 5f3a2bb579 Autoformat 2023-10-27 14:26:48 +11:00
Astrash 9a94b26126 Optimize imports 2023-10-27 14:22:35 +11:00
Astrash c3909ca1e0 Make error handler only handle compilation exceptions 2023-10-27 14:21:50 +11:00
Astrash ff031df903 Function -> Callee & Add callee exception 2023-10-27 13:57:53 +11:00
Astrash ceba9512c7 Make functions first class 2023-10-27 13:37:07 +11:00
Astrash 77dbe92ef7 Merge branch 'ver/6.4.0' into dev/terrascript-2 2023-10-23 13:29:57 +11:00
Astrash 1623a4f958 Move terrascript 2 to separate module 2023-10-23 13:28:26 +11:00
Astrash 375f0ba60f Change Type enum to interface
Also removes legacy v1 code from v2
2023-10-23 13:12:35 +11:00
Astrash 433d695e8b Re-add terrascript v1 module 2023-10-23 13:10:32 +11:00
Astrash d139019e01 Validate returns inside if statements 2023-10-20 13:56:52 +11:00
Astrash 08c1447967 Remove hardcoded print native java call 2023-09-12 14:22:37 +10:00
Astrash 37b5a2ec92 Use static ints instead of enum 2023-09-12 13:42:36 +10:00
Astrash defb31e309 Mangle bytecode method names according to declaration scope 2023-09-12 11:24:30 +10:00
Astrash 0a46e9050d Simplify some logic 2023-09-12 09:53:18 +10:00
Astrash 002da30fd5 Improve codegen readability 2023-09-11 19:09:08 +10:00
Astrash e177c9e792 Codegen implementation stuff & typed AST nodes 2023-09-11 18:09:35 +10:00
Astrash b1bfe00bf3 Implement function and variable codegen 2023-09-08 11:36:40 +10:00
Astrash 9a75ee78a1 Salvage TerraScript codegen code from dead laptop 2023-08-08 07:39:47 +00:00
Astrash 0e9cbd8e2f Initial terrascript 2 commit 2023-08-05 15:53:01 +10:00
Astrash 772675639e Better error handling + other changes 2023-07-29 20:29:16 +10:00
Astrash 13300861ee Parse precedence via grammar 2023-07-29 12:03:33 +10:00
Astrash 719b9a06f4 Simplify code 2023-07-29 08:57:43 +10:00
Astrash f5b115e618 Formatting & name changes 2023-07-27 12:52:15 +10:00
Astrash e1e4a63517 Add basic user defined function support 2023-07-27 11:27:15 +10:00
Astrash 0dc1492c4d Handle functions in scope 2023-07-25 14:08:09 +10:00
Astrash a184fe40d0 Name changes 2023-07-24 18:52:54 +10:00
Astrash f462b8198b Move inLoop flag to ScopeBuilder 2023-07-24 18:05:43 +10:00
Astrash de3b213deb Refactor some parsing logic 2023-07-24 17:31:06 +10:00
Astrash be444f75b7 Block -> executable 2023-07-24 17:30:37 +10:00
Astrash d98238c262 Remove statement class 2023-07-23 19:33:17 +10:00
Astrash 8e96745a85 checkReturnType -> ensureReturnType 2023-07-23 19:17:50 +10:00
Astrash 802bce40c8 Move statement end handling to parseExpression 2023-07-23 19:17:08 +10:00
Astrash 76728fe593 More refactoring 2023-07-23 17:55:29 +10:00
Astrash f3d1751c87 Terrascript refactor 2023-07-23 16:11:56 +10:00
44 changed files with 4055 additions and 0 deletions
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2021 Polyhedral Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,4 @@
# structure-terrascript-loader
Implements the TerraScript structure scripting language, and loads all `*.tesf`
files into the Structure registry.
@@ -0,0 +1,244 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
version = version("1.0.0")
dependencies {
api("commons-io:commons-io:2.7")
api("org.ow2.asm:asm:9.5")
api("org.ow2.asm:asm-commons:9.5")
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
}
tasks.named<ShadowJar>("shadowJar") {
relocate("org.apache.commons", "com.dfsek.terra.addons.terrascript.v2.lib.commons")
relocate("net.jafama", "com.dfsek.terra.addons.terrascript.v2.lib.jafama")
}
val astSourceSet = buildDir.resolve("generated/ast")
val astPackage = astSourceSet.resolve("com/dfsek/terra/addons/terrascript/v2/ast")
data class ASTClass(
val name: String,
val imports: List<String>,
val nodes: List<ASTNode>,
val constructorFields: List<Pair<String, String>> = emptyList(),
)
data class ASTNode(
val name: String,
val constructorFields: List<Pair<String, String>>,
val mutableFields: List<Pair<String, String>> = emptyList() // TODO - Remove mutability from AST nodes
)
// Auto generate AST classes rather than writing them by hand
tasks.register("genTerrascriptAstClasses") {
val packageName = astPackage.toRelativeString(astSourceSet).replace('/', '.')
fun generateClass(clazz: ASTClass) {
val src = StringBuilder()
src.appendLine("package $packageName;\n");
for (imprt in clazz.imports) src.appendLine("import $imprt;")
src.appendLine("""
/**
* Auto-generated class via genTerrascriptAstClasses gradle task
*/
public abstract class ${clazz.name} {
""".trimIndent())
for (field in clazz.constructorFields) {
src.appendLine(" public final ${field.second} ${field.first};")
}
src.appendLine("""
|
| public ${clazz.name}(${clazz.constructorFields.joinToString { "${it.second} ${it.first}" }}) {
""".trimMargin())
for (field in clazz.constructorFields) {
src.appendLine(" this.${field.first} = ${field.first};")
}
src.appendLine("""
| }
|
| public interface Visitor<R> {
|
""".trimMargin())
for (node in clazz.nodes) {
src.appendLine(" R visit${node.name}${clazz.name}(${node.name} ${clazz.name.toLowerCase()});")
}
src.appendLine("""
|
| }
|
| public abstract <R> R accept(Visitor<R> visitor);
""".trimMargin())
for (node in clazz.nodes) {
src.appendLine()
// Inner class declaration
src.appendLine(" public static class ${node.name} extends ${clazz.name} {\n")
// Add fields
for (field in node.constructorFields) {
src.appendLine(" public final ${field.second} ${field.first};")
}
for (field in node.mutableFields) {
src.appendLine(" private ${field.second} ${field.first};")
}
src.appendLine()
// Add constructor
src.appendLine("""
| public ${node.name}(${node.constructorFields.plus(clazz.constructorFields).joinToString { "${it.second} ${it.first}" }}) {
| super(${clazz.constructorFields.joinToString { it.first }});
""".trimMargin())
for (field in node.constructorFields) {
src.appendLine(" this.${field.first} = ${field.first};")
}
src.appendLine(" }")
// Add getters and setters for mutable fields
for (field in node.mutableFields) {
src.appendLine("""
|
| public void set${field.first.capitalize()}(${field.second} value) {
| this.${field.first} = value;
| }
|
| public ${field.second} get${field.first.capitalize()}() {
| if (this.${field.first} == null) throw new RuntimeException("Compilation bug! Field ${field.first} has not been set yet");
| return this.${field.first};
| }
""".trimMargin())
}
src.appendLine("""
|
| @Override
| public <R> R accept(Visitor<R> visitor) {
| return visitor.visit${node.name}${clazz.name}(this);
| }
| }
""".trimMargin())
}
src.appendLine("}")
val outputFile = astPackage.resolve("${clazz.name}.java")
outputFile.writeText(src.toString())
}
doLast {
astSourceSet.deleteRecursively()
astPackage.mkdirs()
listOf(
ASTClass(
"Expr",
listOf(
"com.dfsek.terra.addons.terrascript.v2.Type",
"com.dfsek.terra.addons.terrascript.v2.parser.UnaryOperator",
"com.dfsek.terra.addons.terrascript.v2.parser.BinaryOperator",
"com.dfsek.terra.addons.terrascript.v2.Environment",
"com.dfsek.terra.addons.terrascript.v2.Environment.Symbol",
"com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition",
"java.util.List",
),
listOf(
ASTNode("Binary", listOf("left" to "Expr", "operator" to "BinaryOperator", "right" to "Expr")),
ASTNode("Grouping", listOf("expression" to "Expr")),
ASTNode("Literal", listOf("value" to "Object", "type" to "Type")),
ASTNode("Unary", listOf("operator" to "UnaryOperator", "operand" to "Expr")),
ASTNode("Call", listOf("callee" to "Expr", "arguments" to "List<Expr>")),
ASTNode("Variable", listOf("identifier" to "String"), listOf("symbol" to "Symbol", "scope" to "Environment")),
ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "Expr")),
ASTNode("Void", listOf()),
),
listOf("position" to "SourcePosition")
),
ASTClass(
"Stmt",
listOf(
"com.dfsek.terra.addons.terrascript.v2.Type",
"com.dfsek.terra.api.util.generic.pair.Pair",
"com.dfsek.terra.addons.terrascript.v2.Environment",
"com.dfsek.terra.addons.terrascript.v2.Environment.Symbol",
"com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition",
"java.util.List",
"java.util.Optional",
),
listOf(
ASTNode("Expression", listOf("expression" to "Expr")),
ASTNode("Block", listOf("statements" to "List<Stmt>")),
ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List<Pair<String, Type>>", "returnType" to "Type", "body" to "Block"), listOf("symbol" to "Symbol")),
ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "Expr"), listOf("scope" to "Environment")),
ASTNode("Return", listOf("value" to "Expr"), listOf("type" to "Type")),
ASTNode("If", listOf("condition" to "Expr", "trueBody" to "Block", "elseIfClauses" to "List<Pair<Expr, Block>>", "elseBody" to "Optional<Block>")),
ASTNode("For", listOf("initializer" to "Stmt", "condition" to "Expr", "incrementer" to "Expr", "body" to "Block")),
ASTNode("While", listOf("condition" to "Expr", "body" to "Block")),
ASTNode("NoOp", listOf()),
ASTNode("Break", listOf()),
ASTNode("Continue", listOf()),
),
listOf("position" to "SourcePosition")
),
ASTClass(
"TypedExpr",
listOf(
"com.dfsek.terra.addons.terrascript.v2.Type",
"com.dfsek.terra.addons.terrascript.v2.parser.UnaryOperator",
"com.dfsek.terra.addons.terrascript.v2.parser.BinaryOperator",
"java.util.List",
),
listOf(
ASTNode("Binary", listOf("left" to "TypedExpr", "operator" to "BinaryOperator", "right" to "TypedExpr")),
ASTNode("Grouping", listOf("expression" to "TypedExpr")),
ASTNode("Literal", listOf("value" to "Object")),
ASTNode("Unary", listOf("operator" to "UnaryOperator", "operand" to "TypedExpr")),
ASTNode("Call", listOf("callee" to "TypedExpr", "arguments" to "List<TypedExpr>")),
ASTNode("Variable", listOf("identifier" to "String")),
ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "TypedExpr")),
ASTNode("Void", listOf()),
),
listOf("type" to "Type")
),
ASTClass(
"TypedStmt",
listOf(
"com.dfsek.terra.addons.terrascript.v2.Type",
"com.dfsek.terra.api.util.generic.pair.Pair",
"java.util.List",
"java.util.Optional",
),
listOf(
ASTNode("Expression", listOf("expression" to "TypedExpr")),
ASTNode("Block", listOf("statements" to "List<TypedStmt>")),
ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List<Pair<String, Type>>", "returnType" to "Type", "body" to "Block", "scopedIdentifier" to "String")),
ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "TypedExpr")),
ASTNode("Return", listOf("value" to "TypedExpr")),
ASTNode("If", listOf("condition" to "TypedExpr", "trueBody" to "Block", "elseIfClauses" to "List<Pair<TypedExpr, Block>>", "elseBody" to "Optional<Block>")),
ASTNode("For", listOf("initializer" to "TypedStmt", "condition" to "TypedExpr", "incrementer" to "TypedExpr", "body" to "Block")),
ASTNode("While", listOf("condition" to "TypedExpr", "body" to "Block")),
ASTNode("NoOp", listOf()),
ASTNode("Break", listOf()),
ASTNode("Continue", listOf()),
),
),
).forEach(::generateClass)
}
}
tasks.getByName("compileJava") {
dependsOn("genTerrascriptAstClasses")
}
sourceSets.getByName("main") {
java {
srcDirs(astSourceSet)
}
}
@@ -0,0 +1,132 @@
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.codegen.NativeFunction;
public class Environment {
public final String name;
private final Environment outer;
private final boolean canAccessOuterVariables;
private final Map<String, Symbol> symbolTable = new HashMap<>();
private final boolean inLoop;
private final int index;
private int innerCount = 0;
private Environment(@Nullable Environment outer, boolean canAccessOuterVariables, boolean inLoop, int index) {
this.outer = outer;
this.canAccessOuterVariables = canAccessOuterVariables;
this.inLoop = inLoop;
this.index = index;
this.name = String.join("_", getNestedIndexes().stream().map(Object::toString).toList());
// Populate global scope with built-in Java implemented methods
// TODO - Replace with AST import nodes
if(index == 0) NativeFunction.BUILTIN_FUNCTIONS.forEach((name, function) ->
symbolTable.put(name, new Symbol.Variable(
new Type.Function.Native(function.getReturnType(),
function.getParameterTypes(), name,
this, function))));
}
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;
}
/**
* 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
*/
public Symbol getVariable(String id) throws NonexistentSymbolException {
Symbol symbol = symbolTable.get(id);
if(symbol != null) return symbol;
if(outer == null) throw new NonexistentSymbolException();
if(canAccessOuterVariables) return outer.getVariable(id);
// Only functions can be accessed from restricted scopes
// TODO - Only make applicable to functions that cannot be reassigned
Symbol potentialFunction = outer.getVariableUnrestricted(id);
if(!(potentialFunction.type instanceof Type.Function)) throw new NonexistentSymbolException();
return potentialFunction;
}
private Symbol getVariableUnrestricted(String id) throws NonexistentSymbolException {
Symbol symbol = symbolTable.get(id);
if(symbol != null) return symbol;
if(outer == null) throw new NonexistentSymbolException();
return outer.getVariableUnrestricted(id);
}
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 final Type type;
public Symbol(Type type) {
this.type = type;
}
public static class Variable extends Symbol {
public Variable(Type type) {
super(type);
}
}
}
public static class ScopeException extends Exception {
public static class SymbolAlreadyExistsException extends ScopeException {
}
public static class NonexistentSymbolException extends ScopeException {
}
}
}
@@ -0,0 +1,22 @@
package com.dfsek.terra.addons.terrascript.v2;
import java.util.ArrayList;
import java.util.List;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
public class ErrorHandler {
private final List<CompilationException> exceptions = new ArrayList<>();
public void add(CompilationException e) {
exceptions.add(e);
}
public void throwAny() throws CompilationException {
for(CompilationException 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,186 @@
package com.dfsek.terra.addons.terrascript.v2;
import com.google.common.collect.Streams;
import javax.annotation.Nullable;
import java.util.List;
import com.dfsek.terra.addons.terrascript.v2.codegen.CodegenType;
import com.dfsek.terra.addons.terrascript.v2.codegen.NativeFunction;
import com.dfsek.terra.api.util.generic.pair.Pair;
public interface Type {
Type NUMBER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return double.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.DOUBLE;
}
@Override
public String toString() {
return "num";
}
};
Type INTEGER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return int.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.INTEGER;
}
@Override
public String toString() {
return "int";
}
};
Type STRING = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return String.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.STRING;
}
@Override
public String toString() {
return "str";
}
};
Type BOOLEAN = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return boolean.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.BOOLEAN;
}
@Override
public String toString() {
return "bool";
}
};
Type VOID = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return void.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.VOID;
}
@Override
public String toString() {
return "()";
}
};
static Type fromString(String lexeme) throws TypeException {
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);
}
CodegenType getCodegenType();
class Function implements Type {
private final Type returnType;
private final List<Type> parameters;
private final String id;
public Function(Type returnType, List<Type> parameters, @Nullable String identifier, Environment declarationScope) {
this.returnType = returnType;
this.parameters = parameters;
this.id = identifier == null ? "ANONYMOUS" : identifier + declarationScope.name;
}
private static boolean paramsAreSubtypes(List<Type> subtypes, List<Type> superTypes) {
if(subtypes.size() != superTypes.size()) return false;
return Streams.zip(subtypes.stream(), superTypes.stream(), Pair::of).allMatch(p -> p.getLeft().typeOf(p.getRight()));
}
public Type getReturnType() {
return returnType;
}
public List<Type> getParameters() {
return parameters;
}
public String getId() {
return id;
}
@Override
public boolean typeOf(Type type) {
if(!(type instanceof Function function)) return false;
return returnType.typeOf(function.returnType) && paramsAreSubtypes(parameters, function.parameters);
}
@Override
public CodegenType getCodegenType() {
return CodegenType.OBJECT;
}
@Override
public java.lang.reflect.Type javaType() {
return Function.class;
}
@Override
public String toString() {
return "(" + String.join(",", parameters.stream().map(Object::toString).toList()) + ") -> " + returnType;
}
public static class Native extends Function {
private final NativeFunction nativeFunction;
public Native(Type returnType, List<Type> parameters, @org.jetbrains.annotations.Nullable String identifier,
Environment declarationScope, NativeFunction nativeFunction) {
super(returnType, parameters, identifier, declarationScope);
this.nativeFunction = nativeFunction;
}
public NativeFunction getNativeFunction() {
return nativeFunction;
}
}
}
class TypeException extends Exception {
}
}
@@ -0,0 +1,124 @@
package com.dfsek.terra.addons.terrascript.v2.codegen;
import org.objectweb.asm.Opcodes;
public class CodegenType {
public static final CodegenType BOOLEAN = new CodegenType(InstructionType.INTEGER, "Z");
public static final CodegenType STRING = new CodegenType(InstructionType.OBJECT, "Ljava/lang/String;");
public static final CodegenType DOUBLE = new CodegenType(InstructionType.DOUBLE, "D");
public static final CodegenType INTEGER = new CodegenType(InstructionType.INTEGER, "I");
public static final CodegenType VOID = new CodegenType(InstructionType.VOID, "V");
public static final CodegenType OBJECT = new CodegenType(InstructionType.OBJECT, "Ljava/lang/Object;");
private final InstructionType instructionType;
private final String descriptor;
public CodegenType(InstructionType instructionType, String descriptor) {
this.instructionType = instructionType;
this.descriptor = descriptor;
}
public InstructionType bytecodeType() {
return instructionType;
}
public String getDescriptor() {
return descriptor;
}
public enum InstructionType {
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,16 @@
package com.dfsek.terra.addons.terrascript.v2.codegen.asm;
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,565 @@
package com.dfsek.terra.addons.terrascript.v2.codegen.asm;
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 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.api.util.generic.pair.Pair;
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 final String debugPath;
private int generationCount = 0;
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);
}
private static boolean exprTypesEqual(Type type, TypedExpr... exprs) {
for(TypedExpr expr : exprs) {
if(expr.type != type) return false;
}
return true;
}
public void generate(Block root) {
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 = expr.type.getCodegenType();
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(expr.type.getCodegenType() == CodegenType.BOOLEAN)
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) {
// TODO - Remove specific handling of native functions
if(expr.callee.type instanceof Type.Function.Native nativeFunction) {
NativeFunction function = nativeFunction.getNativeFunction();
function.pushInstance(method);
expr.arguments.forEach(a -> a.accept(this));
function.callMethod(method);
return null;
}
// TODO - Add support for invokevirtual
expr.arguments.forEach(a -> a.accept(this));
List<Type> parameters = expr.arguments.stream().map(e -> e.type).toList();
method.visitMethodInsn(Opcodes.INVOKESTATIC, className, ((Type.Function) expr.callee.type).getId(),
getFunctionDescriptor(parameters, expr.type), false);
return null;
}
@Override
public Void visitVariableTypedExpr(Variable expr) {
Type varType = expr.type;
method.visitVarInsn(varType.getCodegenType().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(type.getCodegenType().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 += parameter.getRight().getCodegenType().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(stmt.type.getCodegenType().bytecodeType().storeInsn(), lvTable.get(stmt.identifier));
return null;
}
@Override
public Void visitReturnTypedStmt(Return stmt) {
stmt.value.accept(this);
method.visitInsn(stmt.value.type.getCodegenType().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);
}
/**
* 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 -> parameter.getCodegenType().getDescriptor()).forEach(sb::append);
sb.append(")");
sb.append(returnType.getCodegenType().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 InvalidArgumentsException extends CompilationException {
public InvalidArgumentsException(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 InvalidCalleeException extends CompilationException {
public InvalidCalleeException(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,255 @@
/*
* 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.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.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.dfsek.terra.addons.terrascript.v2.lexer.Token.TokenType;
import com.dfsek.terra.addons.terrascript.v2.parser.ParseException;
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,382 @@
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 postfix();
}
private Expr postfix() {
Expr expr = primary();
while(current().isType(TokenType.OPEN_PAREN)) {
expr = call(expr);
}
return expr;
}
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 -> 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(Expr function) {
SourcePosition position = consume("Expected '(' to initiate function call on function", 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", TokenType.SEPARATOR);
}
consume("Expected ')' after " + (args.size() == 0 ? "')'" : "arguments") + " in function call",
TokenType.CLOSE_PAREN);
return new Expr.Call(function, 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,189 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import java.util.List;
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.Type.Function;
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.api.util.generic.pair.Pair;
import static com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.SymbolAlreadyExistsException;
public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
private final ErrorHandler errorHandler;
private Environment currentScope;
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.callee.accept(this);
expr.arguments.forEach(e -> e.accept(this));
return null;
}
@Override
public Void visitVariableExpr(Variable expr) {
expr.setScope(currentScope);
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 Symbol.Variable(param.getRight()));
} catch(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 {
List<Type> parameters = stmt.parameters.stream().map(Pair::getRight).toList();
Function function = new Function(stmt.returnType, parameters, stmt.identifier, currentScope);
Symbol.Variable symbol = new Symbol.Variable(function);
stmt.setSymbol(symbol);
currentScope.put(stmt.identifier, symbol);
} catch(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.setScope(currentScope);
stmt.value.accept(this);
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 VariableAnalyzer(errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
TypedStmt.Block checkedRoot = (TypedStmt.Block) new TypeChecker(errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
return checkedRoot;
}
}
@@ -0,0 +1,279 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import java.util.List;
import java.util.Optional;
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.InvalidArgumentsException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidCalleeException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidFunctionDeclarationException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidTypeException;
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; }
@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 InvalidTypeException(
"Addition operands must be either both of type '" + Type.NUMBER + "', or one of type '" + Type.STRING + "'",
expr.position));
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) {
TypedExpr function = expr.callee.accept(this);
if(!(function.type instanceof Type.Function functionType)) {
errorHandler.add(
new InvalidCalleeException("Cannot call type '" + function.type + "', only functions can be called", expr.position));
return new TypedExpr.Void(Type.VOID);
}
List<TypedExpr> arguments = expr.arguments.stream().map(a -> a.accept(this)).toList();
List<Type> parameters = functionType.getParameters();
if(arguments.size() != parameters.size())
errorHandler.add(new InvalidArgumentsException(
"Provided " + arguments.size() + " arguments to function call, 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. Function expects type " + expectedType + ", found " +
providedType + " instead", expr.position));
}
return new TypedExpr.Call(function, arguments, functionType.getReturnType());
}
@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,
((Type.Function) stmt.getSymbol().type).getId());
}
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,172 @@
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.SymbolAlreadyExistsException;
import com.dfsek.terra.addons.terrascript.v2.Environment.Symbol;
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.IdentifierAlreadyDeclaredException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.UndefinedReferenceException;
import com.dfsek.terra.api.util.generic.pair.Pair;
public class VariableAnalyzer implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
private final ErrorHandler errorHandler;
public VariableAnalyzer(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) {
expr.callee.accept(this);
expr.arguments.forEach(e -> e.accept(this));
return null;
}
@Override
public Void visitVariableExpr(Variable expr) {
String id = expr.identifier;
try {
expr.setSymbol(expr.getScope().getVariable(id));
} catch(NonexistentSymbolException e) {
errorHandler.add(
new UndefinedReferenceException("'" + id + "' not is defined in this scope", 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(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);
try {
stmt.getScope().put(stmt.identifier, new Symbol.Variable(stmt.type));
} catch(SymbolAlreadyExistsException e) {
errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope",
stmt.position));
}
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,22 @@
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 org.junit.jupiter.api.Test;
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;
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,32 @@
package lexer;
import org.junit.jupiter.api.Test;
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 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,85 @@
/*
* 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 org.junit.jupiter.api.Test;
import com.dfsek.terra.addons.terrascript.v2.lexer.Char;
import com.dfsek.terra.addons.terrascript.v2.lexer.LookaheadStream;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
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,369 @@
package semanticanalysis;
import org.junit.jupiter.api.Test;
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.InvalidCalleeException;
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.exception.semanticanalysis.UndefinedReferenceException;
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 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 call non functions
testInvalid("var test: num = 1; test();", InvalidCalleeException.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));
}
}