Codegen implementation stuff & typed AST nodes

This commit is contained in:
Astrash
2023-09-11 18:09:35 +10:00
parent b1bfe00bf3
commit e177c9e792
16 changed files with 581 additions and 582 deletions

View File

@@ -1,5 +1,4 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import java.util.*
version = version("1.1.0")
@@ -20,9 +19,19 @@ tasks.named<ShadowJar>("shadowJar") {
val astSourceSet = buildDir.resolve("generated/ast")
val astPackage = astSourceSet.resolve("com/dfsek/terra/addons/terrascript/ast")
data class ASTClass(val name: String, val imports: List<String>, val nodes: List<ASTNode>)
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())
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") {
@@ -34,22 +43,33 @@ tasks.register("genTerrascriptAstClasses") {
src.appendLine("package $packageName;\n");
for (imprt in clazz.imports) src.appendLine("import $imprt;")
src.appendLine("""
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
/**
* Auto-generated class via genTerrascriptAstClasses gradle task
*/
public abstract class ${clazz.name} {
public final SourcePosition position;
public ${clazz.name}(SourcePosition position) {
this.position = position;
}
public interface Visitor<R> {
""".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()});")
}
@@ -76,10 +96,11 @@ tasks.register("genTerrascriptAstClasses") {
src.appendLine()
// Add constructor
src.append(" public ${node.name}(")
for (field in node.constructorFields)
src.append("${field.second} ${field.first}, ")
src.appendLine("SourcePosition position) {\n super(position);")
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};")
}
@@ -117,42 +138,99 @@ tasks.register("genTerrascriptAstClasses") {
doLast {
astSourceSet.deleteRecursively()
astPackage.mkdirs()
generateClass(ASTClass("Expr", listOf(
"com.dfsek.terra.addons.terrascript.Type",
"com.dfsek.terra.addons.terrascript.parser.UnaryOperator",
"com.dfsek.terra.addons.terrascript.parser.BinaryOperator",
"com.dfsek.terra.addons.terrascript.Environment.Symbol",
"java.util.List",
),
listOf(
ASTNode("Binary", listOf("left" to "Expr", "operator" to "BinaryOperator", "right" to "Expr",), listOf("type" to "Type")),
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("identifier" to "String", "arguments" to "List<Expr>"), listOf("symbol" to "Symbol.Function")),
ASTNode("Variable", listOf("identifier" to "String"), listOf("symbol" to "Symbol.Variable")),
ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "Expr")),
ASTNode("Void", listOf()),
)))
generateClass(ASTClass("Stmt", listOf(
"com.dfsek.terra.addons.terrascript.Type",
"com.dfsek.terra.api.util.generic.pair.Pair",
"com.dfsek.terra.addons.terrascript.Environment.Symbol",
"java.util.List",
),
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>>", "type" to "Type", "body" to "Block"), listOf("symbol" to "Symbol.Function")),
ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "Expr")),
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 "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()),
)))
ASTClass(
"Expr",
listOf(
"com.dfsek.terra.addons.terrascript.Type",
"com.dfsek.terra.addons.terrascript.parser.UnaryOperator",
"com.dfsek.terra.addons.terrascript.parser.BinaryOperator",
"com.dfsek.terra.addons.terrascript.Environment",
"com.dfsek.terra.addons.terrascript.Environment.Symbol",
"com.dfsek.terra.addons.terrascript.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("identifier" to "String", "arguments" to "List<Expr>"), listOf("environment" to "Environment", "symbol" to "Symbol.Function")),
ASTNode("Variable", listOf("identifier" to "String"), listOf("symbol" to "Symbol.Variable")),
ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "Expr")),
ASTNode("Void", listOf()),
),
listOf("position" to "SourcePosition")
),
ASTClass(
"Stmt",
listOf(
"com.dfsek.terra.addons.terrascript.Type",
"com.dfsek.terra.api.util.generic.pair.Pair",
"com.dfsek.terra.addons.terrascript.Environment.Symbol",
"com.dfsek.terra.addons.terrascript.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")),
ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "Expr")),
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.Type",
"com.dfsek.terra.addons.terrascript.parser.UnaryOperator",
"com.dfsek.terra.addons.terrascript.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("identifier" to "String", "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.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")),
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)
}
}

View File

@@ -12,6 +12,7 @@ public interface TerraScript {
Map<String, Method> BUILTIN_FUNCTIONS = new HashMap<>() {{
try {
put("print", System.out.getClass().getMethod("println", String.class));
put("printNum", System.out.getClass().getMethod("println", double.class));
} catch(NoSuchMethodException e) {
throw new RuntimeException(e);
}

View File

@@ -1,30 +1,30 @@
package com.dfsek.terra.addons.terrascript.codegen.asm;
import com.dfsek.terra.addons.terrascript.Environment.Symbol;
import com.dfsek.terra.addons.terrascript.Type;
import com.dfsek.terra.addons.terrascript.ast.Expr;
import com.dfsek.terra.addons.terrascript.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.ast.Expr.Call;
import com.dfsek.terra.addons.terrascript.ast.Expr.Grouping;
import com.dfsek.terra.addons.terrascript.ast.Expr.Literal;
import com.dfsek.terra.addons.terrascript.ast.Expr.Unary;
import com.dfsek.terra.addons.terrascript.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.ast.Stmt;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Break;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Continue;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Expression;
import com.dfsek.terra.addons.terrascript.ast.Stmt.For;
import com.dfsek.terra.addons.terrascript.ast.Stmt.FunctionDeclaration;
import com.dfsek.terra.addons.terrascript.ast.Stmt.If;
import com.dfsek.terra.addons.terrascript.ast.Stmt.NoOp;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Return;
import com.dfsek.terra.addons.terrascript.ast.Stmt.VariableDeclaration;
import com.dfsek.terra.addons.terrascript.ast.Stmt.While;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Assignment;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Binary;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Call;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Grouping;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Literal;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Unary;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Variable;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr.Void;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Block;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Break;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Continue;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Expression;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.For;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.FunctionDeclaration;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.If;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.NoOp;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.Return;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.VariableDeclaration;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt.While;
import com.dfsek.terra.addons.terrascript.codegen.TerraScript;
import com.dfsek.terra.addons.terrascript.exception.CompilerBugException;
import com.dfsek.terra.addons.terrascript.util.ASMUtil;
import com.dfsek.terra.api.util.generic.pair.Pair;
@@ -39,6 +39,9 @@ import org.objectweb.asm.commons.LocalVariablesSorter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -127,7 +130,7 @@ public class TerraScriptClassGenerator {
}
}
private static class MethodBytecodeGenerator implements Stmt.Visitor<Void>, Expr.Visitor<Void> {
private static class MethodBytecodeGenerator implements TypedStmt.Visitor<Void>, TypedExpr.Visitor<Void> {
private final ClassWriter classWriter;
@@ -141,6 +144,8 @@ public class TerraScriptClassGenerator {
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;
@@ -150,101 +155,61 @@ public class TerraScriptClassGenerator {
}
public void generate(Block root) {
this.visitBlockStmt(root);
this.visitBlockTypedStmt(root);
}
@Override
public Void visitBinaryExpr(Binary expr) {
expr.left.accept(this);
expr.right.accept(this);
public Void visitBinaryTypedExpr(Binary expr) {
switch(expr.operator) {
// TODO - Short circuit binary operators
case BOOLEAN_OR -> method.visitInsn(Opcodes.IOR);
case BOOLEAN_AND -> method.visitInsn(Opcodes.IAND);
// case EQUALS -> null;
// case NOT_EQUALS -> null;
case GREATER -> {
Label falseLabel = new Label();
Label finished = new Label();
method.visitInsn(Opcodes.DCMPL);
method.visitJumpInsn(Opcodes.IFLE, falseLabel);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(falseLabel);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished);
}
case GREATER_EQUALS -> {
Label falseLabel = new Label();
Label finished = new Label();
method.visitInsn(Opcodes.DCMPL);
method.visitJumpInsn(Opcodes.IFLT, falseLabel);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(falseLabel);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished);
}
case LESS -> {
Label falseLabel = new Label();
Label finished = new Label();
method.visitInsn(Opcodes.DCMPG);
method.visitJumpInsn(Opcodes.IFGE, falseLabel);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(falseLabel);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished);
}
case LESS_EQUALS -> {
Label falseLabel = new Label();
Label finished = new Label();
method.visitInsn(Opcodes.DCMPG);
method.visitJumpInsn(Opcodes.IFGT, falseLabel);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(falseLabel);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(finished);
}
case EQUALS, NOT_EQUALS, BOOLEAN_AND, BOOLEAN_OR, GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> pushComparisonResult(expr);
case ADD -> {
switch(expr.getType()) {
pushBinaryOperands(expr);
switch(expr.type) {
case NUMBER -> method.visitInsn(Opcodes.DADD);
// TODO - Optimize string concatenation
case STRING -> method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat", "(Ljava/lang/String;)Ljava/lang/String;", false);
default -> throw new RuntimeException("Could not generate bytecode for ADD binary operator returning type " + expr.getType());
default -> throw new RuntimeException("Could not generate bytecode for ADD binary operator returning type " + expr.type);
}
}
case SUBTRACT -> method.visitInsn(Opcodes.DSUB);
case MULTIPLY -> method.visitInsn(Opcodes.DMUL);
case DIVIDE -> method.visitInsn(Opcodes.DDIV);
case SUBTRACT -> binaryInsn(expr, Opcodes.DSUB);
case MULTIPLY -> binaryInsn(expr, Opcodes.DMUL);
case DIVIDE -> binaryInsn(expr, Opcodes.DDIV);
// case MODULO ->
default -> throw new RuntimeException("Unhandled binary operator " + expr.operator);
}
return null;
}
@Override
public Void visitGroupingExpr(Grouping expr) {
public Void visitGroupingTypedExpr(Grouping expr) {
expr.expression.accept(this);
return null;
}
@Override
public Void visitLiteralExpr(Literal expr) {
method.visitLdcInsn(expr.value);
public Void visitLiteralTypedExpr(Literal expr) {
switch (expr.type) {
case BOOLEAN -> method.visitInsn((boolean) expr.value ? Opcodes.ICONST_1 : Opcodes.ICONST_0);
case NUMBER, STRING -> method.visitLdcInsn(expr.value);
}
return null;
}
@Override
public Void visitUnaryExpr(Unary expr) {
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 visitCallExpr(Call expr) {
Symbol.Function function = expr.getSymbol();
public Void visitCallTypedExpr(Call expr) {
if (TerraScript.BUILTIN_FUNCTIONS.containsKey(expr.identifier)) {
Method m = TerraScript.BUILTIN_FUNCTIONS.get(expr.identifier);
if (expr.identifier.equals("print")) { // TODO - remove quick dirty print function call
method.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
expr.arguments.get(0).accept(this);
@@ -253,76 +218,60 @@ public class TerraScriptClassGenerator {
return null;
}
expr.arguments.forEach(a -> a.accept(this));
List<Type> parameters = function.parameters.stream().map(Pair::getRight).toList();
method.visitMethodInsn(Opcodes.INVOKESTATIC, className, expr.identifier, getFunctionDescriptor(parameters, function.type), false);
List<Type> parameters = expr.arguments.stream().map(e -> e.type).toList();
method.visitMethodInsn(Opcodes.INVOKESTATIC, className, expr.identifier, getFunctionDescriptor(parameters, expr.type), false);
return null;
}
@Override
public Void visitVariableExpr(Variable expr) {
Type varType = expr.getSymbol().type;
public Void visitVariableTypedExpr(Variable expr) {
Type varType = expr.type;
method.visitVarInsn(switch(varType) {
case NUMBER -> Opcodes.DLOAD;
case STRING -> Opcodes.ALOAD;
case BOOLEAN -> Opcodes.ILOAD;
default -> throw new RuntimeException("Unable to load local variable, unknown parameter type '" + varType + "'");
default -> throw new RuntimeException("Unable to load local variable, unhandled type '" + varType + "'");
}, lvTable.get(expr.identifier));
return null;
}
@Override
public Void visitAssignmentExpr(Assignment expr) {
public Void visitAssignmentTypedExpr(Assignment expr) {
expr.rValue.accept(this);
Type type = expr.lValue.getSymbol().type;
Type type = expr.lValue.type;
method.visitVarInsn(switch(type) {
case NUMBER -> Opcodes.DSTORE;
case STRING -> Opcodes.ASTORE;
case BOOLEAN -> Opcodes.ISTORE;
default -> throw new RuntimeException("Unable to assign local variable, unknown parameter type '" + type + "'");
default -> throw new RuntimeException("Unable to assign local variable, unhandled type '" + type + "'");
}, lvTable.get(expr.lValue.identifier));
return null;
}
@Override
public Void visitVoidExpr(Void expr) {
public Void visitVoidTypedExpr(Void expr) {
return null;
}
@Override
public Void visitExpressionStmt(Expression stmt) {
public Void visitExpressionTypedStmt(Expression stmt) {
stmt.expression.accept(this);
return null;
}
@Override
public Void visitBlockStmt(Block stmt) {
public Void visitBlockTypedStmt(Block stmt) {
stmt.statements.forEach(s -> s.accept(this));
return null;
}
private String getFunctionDescriptor(List<Type> parameters, Type returnType) {
StringBuilder sb = new StringBuilder().append("(");
parameters.stream().map(p -> switch (p) {
case NUMBER -> "D";
case STRING -> "Ljava/lang/String;";
case BOOLEAN -> "Z";
default -> throw new RuntimeException("Unable to generate method descriptor, unknown parameter type '" + p + "'");
}).forEach(sb::append);
sb.append(")");
sb.append(switch (returnType) {
case NUMBER -> "D";
case STRING -> "Ljava/lang/String;";
case BOOLEAN -> "Z";
case VOID -> "V";
});
return sb.toString();
}
@Override
public Void visitFunctionDeclarationStmt(FunctionDeclaration stmt) {
int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
public Void visitFunctionDeclarationTypedStmt(FunctionDeclaration stmt) {
List<Type> parameterTypes = stmt.parameters.stream().map(Pair::getRight).toList();
MethodVisitor method = classWriter.visitMethod(access, stmt.identifier, getFunctionDescriptor(parameterTypes, stmt.type), null, null);
int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
// TODO - Mangle identifier based on scope to avoid issues with using the same identifier in different scopes
MethodVisitor method = classWriter.visitMethod(access, stmt.identifier, getFunctionDescriptor(parameterTypes, stmt.returnType), null, null);
method.visitCode(); // Start method body
@@ -351,7 +300,7 @@ public class TerraScriptClassGenerator {
}
@Override
public Void visitVariableDeclarationStmt(VariableDeclaration stmt) {
public Void visitVariableDeclarationTypedStmt(VariableDeclaration stmt) {
stmt.value.accept(this);
lvTable.put(stmt.identifier, lvs.newLocal(ASMUtil.tsTypeToAsmType(stmt.type)));
method.visitVarInsn(switch(stmt.type) {
@@ -364,45 +313,237 @@ public class TerraScriptClassGenerator {
}
@Override
public Void visitReturnStmt(Return stmt) {
public Void visitReturnTypedStmt(Return stmt) {
stmt.value.accept(this);
switch(stmt.getType()) {
switch(stmt.value.type) {
case NUMBER -> method.visitInsn(Opcodes.DRETURN);
case STRING -> method.visitInsn(Opcodes.ARETURN);
case BOOLEAN -> method.visitInsn(Opcodes.IRETURN);
default -> throw new CompilerBugException();
}
return null;
}
@Override
public Void visitIfStmt(If stmt) {
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));
method.visitLabel(endIf);
return null;
}
@Override
public Void visitForStmt(For stmt) {
public Void visitForTypedStmt(For stmt) {
Label loopStart = new Label();
Label loopBody = new Label();
Label loopEnd = new Label();
stmt.initializer.accept(this);
method.visitJumpInsn(Opcodes.GOTO, loopBody); // Skip over incrementer on first loop
method.visitLabel(loopStart);
stmt.incrementer.accept(this);
method.visitLabel(loopBody);
loopStack.push(Pair.of(loopStart, loopEnd));
conditionalStmt(stmt.condition, stmt.body, loopStart);
loopStack.pop();
method.visitLabel(loopEnd);
return null;
}
@Override
public Void visitWhileStmt(While stmt) {
public Void visitWhileTypedStmt(While stmt) {
Label loopStart = new Label();
Label loopEnd = new Label();
method.visitLabel(loopStart);
loopStack.push(Pair.of(loopStart, loopEnd));
conditionalStmt(stmt.condition, stmt.body, loopStart);
loopStack.pop();
method.visitLabel(loopEnd);
return null;
}
@Override
public Void visitNoOpStmt(NoOp stmt) {
public Void visitNoOpTypedStmt(NoOp stmt) {
return null;
}
@Override
public Void visitBreakStmt(Break stmt) {
public Void visitBreakTypedStmt(Break stmt) {
method.visitJumpInsn(Opcodes.GOTO, loopStack.getFirst().getRight());
return null;
}
@Override
public Void visitContinueStmt(Continue stmt) {
public Void visitContinueTypedStmt(Continue stmt) {
method.visitJumpInsn(Opcodes.GOTO, loopStack.getFirst().getLeft());
return null;
}
private boolean binaryOperandsSameType(Type type, Binary expr) {
return exprTypesEqual(type, expr.left, expr.right);
}
private static boolean exprTypesEqual(Type type, TypedExpr... exprs) {
for(TypedExpr expr : exprs) {
if (expr.type != type) return false;
}
return true;
}
/**
* Inverts a boolean on the stack
*/
private void invertBool() {
Label invertToFalse = new Label();
Label finished = new Label();
method.visitJumpInsn(Opcodes.IFNE, invertToFalse);
method.visitInsn(Opcodes.ICONST_1);
method.visitJumpInsn(Opcodes.GOTO, finished);
method.visitLabel(invertToFalse);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(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 pushComparisonResult(TypedExpr condition) {
Label trueFinished = new Label();
conditionalRunnable(condition, () -> method.visitInsn(Opcodes.ICONST_1), trueFinished);
method.visitInsn(Opcodes.ICONST_0);
method.visitLabel(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 conditionalRunnable(TypedExpr condition, Runnable trueSection, 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);
method.visitJumpInsn(Opcodes.IFEQ, exit); // If left is false, short circuit, don't evaluate right
binaryCondition.right.accept(this);
method.visitJumpInsn(Opcodes.IFEQ, exit);
}
case BOOLEAN_OR -> {
Label skipRight = new Label();
// Operands assumed booleans
binaryCondition.left.accept(this);
method.visitJumpInsn(Opcodes.IFNE, skipRight); // If left is true, skip evaluating right
binaryCondition.right.accept(this);
method.visitJumpInsn(Opcodes.IFEQ, exit);
method.visitLabel(skipRight);
}
case EQUALS -> {
if (binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers
pushBinaryOperands(binaryCondition);
method.visitJumpInsn(Opcodes.IF_ICMPNE, exit);
} else if (binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles
pushBinaryOperands(binaryCondition);
method.visitInsn(Opcodes.DCMPG);
method.visitJumpInsn(Opcodes.IFNE, exit);
} else if (binaryOperandsSameType(Type.STRING, binaryCondition)) {
pushBinaryOperands(binaryCondition);
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
method.visitJumpInsn(Opcodes.IFEQ, exit);
} else throw new CompilerBugException();
}
case NOT_EQUALS -> {
if (binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers
pushBinaryOperands(binaryCondition);
method.visitJumpInsn(Opcodes.IF_ICMPEQ, exit);
} else if (binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles
pushBinaryOperands(binaryCondition);
method.visitInsn(Opcodes.DCMPG);
method.visitJumpInsn(Opcodes.IFEQ, 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();
method.visitJumpInsn(Opcodes.IFEQ, exit);
} else throw new CompilerBugException();
}
case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> {
// Left and right assumed double
pushBinaryOperands(binaryCondition);
method.visitInsn(switch(binaryCondition.operator) {
case GREATER, GREATER_EQUALS -> Opcodes.DCMPL;
case LESS, LESS_EQUALS -> Opcodes.DCMPG;
default -> throw new IllegalStateException();
});
method.visitJumpInsn(switch(binaryCondition.operator) {
case GREATER -> Opcodes.IFLE;
case GREATER_EQUALS -> Opcodes.IFLT;
case LESS -> Opcodes.IFGE;
case LESS_EQUALS -> Opcodes.IFGT;
default -> throw new IllegalStateException();
}, exit);
}
default -> throw new CompilerBugException();
}
} else {
// Assume condition returns bool
condition.accept(this);
method.visitJumpInsn(Opcodes.IFEQ, exit);
}
trueSection.run();
method.visitJumpInsn(Opcodes.GOTO, trueFinished); // Jump to end of statement after execution
method.visitLabel(exit);
}
private String getFunctionDescriptor(List<Type> parameters, Type returnType) {
StringBuilder sb = new StringBuilder().append("(");
parameters.stream().map(p -> switch (p) {
case NUMBER -> "D";
case STRING -> "Ljava/lang/String;";
case BOOLEAN -> "Z";
default -> throw new RuntimeException("Unable to generate method descriptor, unknown parameter type '" + p + "'");
}).forEach(sb::append);
sb.append(")");
sb.append(switch (returnType) {
case NUMBER -> "D";
case STRING -> "Ljava/lang/String;";
case BOOLEAN -> "Z";
case VOID -> "V";
});
return sb.toString();
}
}
private static class MethodExtractor extends ClassVisitor {

View File

@@ -0,0 +1,5 @@
package com.dfsek.terra.addons.terrascript.exception;
public class CompilerBugException extends RuntimeException {
// TODO - Add message constructor
}

View File

@@ -387,8 +387,10 @@ public class Parser {
private Expression<?> parseVariableDeclaration(Scope.ScopeBuilder scopeBuilder, Token type, Token identifier) {
lexer.consume("Expected '=' after identifier '" + identifier.lexeme() + "' for variable declaration", TokenType.ASSIGNMENT);
if(!type.isVariableDeclaration()) throw new ParseException("Expected type specification at beginning of variable declaration",
if(!(type.isType(TokenType.TYPE_STRING)
|| type.isType(TokenType.TYPE_BOOLEAN)
|| type.isType(TokenType.TYPE_NUMBER))) throw new ParseException("Expected type specification at beginning of variable declaration",
type.position());
if(scopeBuilder.containsVariable(identifier.lexeme()))
@@ -514,8 +516,10 @@ public class Parser {
case STATEMENT_END -> Expression.NOOP;
default -> throw new ParseException("Unexpected token '" + token.lexeme() + "' while parsing statement", token.position());
};
if(!token.isControlStructure() && expression != Expression.NOOP) lexer.consume("Expected ';' at end of statement",
TokenType.STATEMENT_END);
if(!(token.isType(TokenType.IF_STATEMENT)
|| token.isType(TokenType.WHILE_LOOP)
|| token.isType(TokenType.FOR_LOOP)) && expression != Expression.NOOP) lexer.consume("Expected ';' at end of statement",
TokenType.STATEMENT_END);
return expression;
}

View File

@@ -242,7 +242,7 @@ public class Lexer {
}
reader.consume();
}
throw new EOFException("No end of expression found.", begin);
throw new EOFException("Reached end of file without matching '" + s + "'", begin);
}
private boolean isNumberLike() {

View File

@@ -9,9 +9,6 @@ package com.dfsek.terra.addons.terrascript.lexer;
import java.util.Objects;
import com.dfsek.terra.addons.terrascript.parser.BinaryOperator;
import com.dfsek.terra.addons.terrascript.parser.UnaryOperator;
public class Token {
private final String lexeme;
@@ -59,60 +56,6 @@ public class Token {
return false;
}
public boolean isOperator(BinaryOperator... operators) {
for(BinaryOperator o : operators) if(o.tokenType == type) return true;
return false;
}
public boolean isOperator(UnaryOperator... operators) {
for(UnaryOperator o : operators) if(o.tokenType == type) return true;
return false;
}
public boolean isBinaryOperator() {
return type.equals(TokenType.PLUS)
|| type.equals(TokenType.MINUS)
|| type.equals(TokenType.STAR)
|| type.equals(TokenType.FORWARD_SLASH)
|| type.equals(TokenType.EQUALS_EQUALS)
|| type.equals(TokenType.BANG_EQUALS)
|| type.equals(TokenType.LESS)
|| type.equals(TokenType.GREATER)
|| type.equals(TokenType.LESS_EQUALS)
|| type.equals(TokenType.GREATER_EQUAL)
|| type.equals(TokenType.BOOLEAN_OR)
|| type.equals(TokenType.BOOLEAN_AND)
|| type.equals(TokenType.MODULO_OPERATOR);
}
public boolean isStrictNumericOperator() {
return type.equals(TokenType.MINUS)
|| type.equals(TokenType.STAR)
|| type.equals(TokenType.FORWARD_SLASH)
|| type.equals(TokenType.GREATER)
|| type.equals(TokenType.LESS)
|| type.equals(TokenType.LESS_EQUALS)
|| type.equals(TokenType.GREATER_EQUAL)
|| type.equals(TokenType.MODULO_OPERATOR);
}
public boolean isStrictBooleanOperator() {
return type.equals(TokenType.BOOLEAN_AND)
|| type.equals(TokenType.BOOLEAN_OR);
}
public boolean isVariableDeclaration() {
return type.equals(TokenType.TYPE_STRING)
|| type.equals(TokenType.TYPE_BOOLEAN)
|| type.equals(TokenType.TYPE_NUMBER);
}
public boolean isControlStructure() {
return type.equals(TokenType.IF_STATEMENT)
|| type.equals(TokenType.WHILE_LOOP)
|| type.equals(TokenType.FOR_LOOP);
}
public enum TokenType {
/**
* Function identifier or language keyword

View File

@@ -4,6 +4,7 @@ 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.Type;
@@ -178,7 +179,7 @@ public class Parser {
elseIfClauses.add(Pair.of(elseIfCondition, elseIfBody));
}
return new Stmt.If(condition, trueBody, elseIfClauses, elseBody, position);
return new Stmt.If(condition, trueBody, elseIfClauses, Optional.ofNullable(elseBody), position);
}
private Stmt.For forLoop() {

View File

@@ -63,7 +63,7 @@ public class FunctionReferenceAnalyzer implements Expr.Visitor<Void>, Stmt.Visit
public Void visitCallExpr(Call expr) {
String id = expr.identifier;
try {
expr.getSymbol();
expr.setSymbol(expr.getEnvironment().getFunction(expr.identifier));
} catch(NonexistentSymbolException e) {
errorHandler.add(
new UndefinedReferenceException("No function by the name '" + id + "' is defined in this scope", expr.position));
@@ -129,9 +129,7 @@ public class FunctionReferenceAnalyzer implements Expr.Visitor<Void>, Stmt.Visit
clause.getLeft().accept(this);
clause.getRight().accept(this);
}
if(stmt.elseBody != null) {
stmt.elseBody.accept(this);
}
stmt.elseBody.ifPresent(b -> b.accept(this));
return null;
}

View File

@@ -60,7 +60,7 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
@Override
public Void visitCallExpr(Call expr) {
expr.setSymbol(currentScope.getFunction(expr.identifier));
expr.setEnvironment(currentScope);
expr.arguments.forEach(e -> e.accept(this));
return null;
}
@@ -69,14 +69,13 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
public Void visitVariableExpr(Variable expr) {
String id = expr.identifier;
try {
currentScope.getVariable(id); // Ensure variable has been declared in current scope
expr.setSymbol(currentScope.getVariable(id));
} catch(NonexistentSymbolException e) {
errorHandler.add(
new UndefinedReferenceException("No variable by the name '" + id + "' is defined in this scope", expr.position));
} catch(SymbolTypeMismatchException e) {
errorHandler.add(new ParseException("Identifier '" + id + "' is not defined as a variable", expr.position));
}
expr.setSymbol(currentScope.getVariable(expr.identifier));
return null;
}
@@ -120,7 +119,7 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
stmt.body.accept(this);
currentScope = currentScope.outer();
try {
currentScope.put(stmt.identifier, new Environment.Symbol.Function(stmt.type, stmt.parameters));
currentScope.put(stmt.identifier, new Environment.Symbol.Function(stmt.returnType, stmt.parameters));
} catch(Environment.ScopeException.SymbolAlreadyExistsException e) {
errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope",
stmt.position));
@@ -130,6 +129,7 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
@Override
public Void visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) {
stmt.value.accept(this);
try {
currentScope.put(stmt.identifier, new Environment.Symbol.Variable(stmt.type));
} catch(Environment.ScopeException.SymbolAlreadyExistsException e) {
@@ -153,9 +153,7 @@ public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
clause.getLeft().accept(this);
clause.getRight().accept(this);
}
if(stmt.elseBody != null) {
stmt.elseBody.accept(this);
}
stmt.elseBody.ifPresent(b -> b.accept(this));
return null;
}

View File

@@ -3,19 +3,22 @@ package com.dfsek.terra.addons.terrascript.semanticanalysis;
import com.dfsek.terra.addons.terrascript.Environment;
import com.dfsek.terra.addons.terrascript.ErrorHandler;
import com.dfsek.terra.addons.terrascript.ast.Stmt;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt;
public class SemanticAnalyzer {
public static void analyze(Stmt.Block root, ErrorHandler errorHandler) throws Exception {
public static TypedStmt.Block analyze(Stmt.Block root, ErrorHandler errorHandler) throws Exception {
new ScopeAnalyzer(Environment.global(), errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
new FunctionReferenceAnalyzer(errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
new TypeChecker(errorHandler).visitBlockStmt(root);
TypedStmt.Block checkedRoot = (TypedStmt.Block) new TypeChecker(errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
return checkedRoot;
}
}

View File

@@ -1,11 +1,12 @@
package com.dfsek.terra.addons.terrascript.semanticanalysis;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import com.dfsek.terra.addons.terrascript.Environment;
import com.dfsek.terra.addons.terrascript.ErrorHandler;
import com.dfsek.terra.addons.terrascript.Type;
import com.dfsek.terra.addons.terrascript.ast.Expr;
import com.dfsek.terra.addons.terrascript.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.ast.Expr.Call;
@@ -16,6 +17,8 @@ import com.dfsek.terra.addons.terrascript.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.ast.Expr.Visitor;
import com.dfsek.terra.addons.terrascript.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.ast.Stmt;
import com.dfsek.terra.addons.terrascript.ast.TypedExpr;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.exception.semanticanalysis.InvalidFunctionDeclarationException;
import com.dfsek.terra.addons.terrascript.exception.semanticanalysis.InvalidTypeException;
import com.dfsek.terra.addons.terrascript.legacy.parser.exceptions.ParseException;
@@ -24,217 +27,221 @@ import com.dfsek.terra.api.util.generic.pair.Pair;
import static com.dfsek.terra.addons.terrascript.util.OrdinalUtil.ordinalOf;
public class TypeChecker implements Visitor<Type>, Stmt.Visitor<Type> {
public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt> {
private final ErrorHandler errorHandler;
TypeChecker(ErrorHandler errorHandler) { this.errorHandler = errorHandler; }
@Override
public Type visitBinaryExpr(Binary expr) {
Type left = expr.left.accept(this);
Type right = expr.right.accept(this);
public TypedExpr visitBinaryExpr(Binary expr) {
TypedExpr left = expr.left.accept(this);
TypedExpr right = expr.right.accept(this);
return switch(expr.operator) {
Type leftType = left.type;
Type rightType = right.type;
Type type = switch(expr.operator) {
case BOOLEAN_OR, BOOLEAN_AND -> {
if(left != Type.BOOLEAN || right != Type.BOOLEAN)
throw new RuntimeException();
if(leftType != Type.BOOLEAN || rightType != Type.BOOLEAN)
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 EQUALS, NOT_EQUALS -> {
if(left != right) throw new RuntimeException();
if(leftType != 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(left != Type.NUMBER || right != Type.NUMBER)
throw new RuntimeException();
if(leftType != Type.NUMBER || rightType != 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(left == Type.NUMBER && right == Type.NUMBER) {
expr.setType(Type.NUMBER);
if(leftType == Type.NUMBER && rightType == Type.NUMBER) {
yield Type.NUMBER;
}
if(left == Type.STRING || right == Type.STRING) {
expr.setType(Type.STRING);
if(leftType == Type.STRING || rightType == Type.STRING) {
yield Type.STRING;
}
throw new RuntimeException("Addition operands must be either both numbers, or one of type string");
errorHandler.add(new RuntimeException("Addition operands must be either both numbers, or one of type string"));
yield Type.VOID;
}
case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> {
if(left != Type.NUMBER || right != Type.NUMBER)
throw new RuntimeException();
if(leftType != Type.NUMBER || rightType != 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 Type visitGroupingExpr(Grouping expr) {
public TypedExpr visitGroupingExpr(Grouping expr) {
return expr.expression.accept(this);
}
@Override
public Type visitLiteralExpr(Literal expr) {
return expr.type;
public TypedExpr visitLiteralExpr(Literal expr) {
return new TypedExpr.Literal(expr.value, expr.type);
}
@Override
public Type visitUnaryExpr(Unary expr) {
Type right = expr.operand.accept(this);
return switch(expr.operator) {
public TypedExpr visitUnaryExpr(Unary expr) {
TypedExpr right = expr.operand.accept(this);
Type type = switch(expr.operator) {
case NOT -> {
if(right != Type.BOOLEAN) throw new RuntimeException();
if(right.type != Type.BOOLEAN) throw new RuntimeException();
yield Type.BOOLEAN;
}
case NEGATE -> {
if(right != Type.NUMBER) throw new RuntimeException();
if(right.type != Type.NUMBER) throw new RuntimeException();
yield Type.NUMBER;
}
};
return new TypedExpr.Unary(expr.operator, right, type);
}
@Override
public Type visitCallExpr(Call expr) {
public TypedExpr visitCallExpr(Call expr) {
String id = expr.identifier;
Environment.Symbol.Function signature = expr.getSymbol();
List<Type> argumentTypes = expr.arguments.stream().map(a -> a.accept(this)).toList();
List<TypedExpr> arguments = expr.arguments.stream().map(a -> a.accept(this)).toList();
List<Type> parameters = signature.parameters.stream().map(Pair::getRight).toList();
if(argumentTypes.size() != parameters.size())
if(arguments.size() != parameters.size())
errorHandler.add(new ParseException(
"Provided " + argumentTypes.size() + " arguments to function call of '" + id + "', expected " + parameters.size() +
"Provided " + arguments.size() + " arguments to function call of '" + id + "', expected " + parameters.size() +
" arguments", expr.position));
for(int i = 0; i < parameters.size(); i++) {
Type expectedType = parameters.get(i);
Type providedType = argumentTypes.get(i);
Type providedType = arguments.get(i).type;
if(expectedType != providedType)
errorHandler.add(new InvalidTypeException(
ordinalOf(i + 1) + " argument provided for function '" + id + "' expects type " + expectedType + ", found " +
providedType + " instead", expr.position));
}
return signature.type;
return new TypedExpr.Call(expr.identifier, arguments, signature.type);
}
@Override
public Type visitVariableExpr(Variable expr) {
return expr.getSymbol().type;
public TypedExpr visitVariableExpr(Variable expr) {
return new TypedExpr.Variable(expr.identifier, expr.getSymbol().type);
}
@Override
public Type visitAssignmentExpr(Assignment expr) {
Type right = expr.rValue.accept(this);
Type expected = expr.lValue.accept(this);
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 != expected)
if(right.type != expected)
errorHandler.add(new InvalidTypeException(
"Cannot assign variable '" + id + "' to type " + right + ", '" + id + "' is declared with type " + expected,
expr.position));
return right;
return new TypedExpr.Assignment(left, right, right.type);
}
@Override
public Type visitVoidExpr(Void expr) {
return Type.VOID;
public TypedExpr visitVoidExpr(Void expr) {
return new TypedExpr.Void(Type.VOID);
}
@Override
public Type visitExpressionStmt(Stmt.Expression stmt) {
stmt.expression.accept(this);
return Type.VOID;
public TypedStmt visitExpressionStmt(Stmt.Expression stmt) {
return new TypedStmt.Expression(stmt.expression.accept(this));
}
@Override
public Type visitBlockStmt(Stmt.Block stmt) {
stmt.statements.forEach(s -> s.accept(this));
return Type.VOID;
public TypedStmt visitBlockStmt(Stmt.Block stmt) {
return new TypedStmt.Block(stmt.statements.stream().map(s -> s.accept(this)).toList());
}
@Override
public Type visitFunctionDeclarationStmt(Stmt.FunctionDeclaration stmt) {
boolean hasReturn = false;
for(Stmt s : stmt.body.statements) {
if(s instanceof Stmt.Return ret) {
hasReturn = true;
Type provided = ret.value.accept(this);
if(provided != stmt.type)
public TypedStmt visitFunctionDeclarationStmt(Stmt.FunctionDeclaration stmt) {
AtomicBoolean hasReturn = new AtomicBoolean(false);
TypedStmt.Block body = new TypedStmt.Block(stmt.body.statements.stream().map(s -> {
TypedStmt bodyStmt = s.accept(this);
if(bodyStmt instanceof TypedStmt.Return ret) {
hasReturn.set(true);
if(ret.value.type != stmt.returnType)
errorHandler.add(new InvalidTypeException(
"Return statement must match function's return type. Function '" + stmt.identifier + "' expects " +
stmt.type + ", found " + provided + " instead", s.position));
stmt.returnType + ", found " + ret.value.type + " instead", s.position));
}
s.accept(this);
}
if(stmt.type != Type.VOID && !hasReturn) {
return bodyStmt;
}).toList());
if(stmt.returnType != Type.VOID && !hasReturn.get()) {
errorHandler.add(
new InvalidFunctionDeclarationException("Function body for '" + stmt.identifier + "' does not contain return statement",
stmt.position));
}
return Type.VOID;
return new TypedStmt.FunctionDeclaration(stmt.identifier, stmt.parameters, stmt.returnType, body);
}
@Override
public Type visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) {
Type valueType = stmt.value.accept(this);
if(stmt.type != valueType)
public TypedStmt visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) {
TypedExpr value = stmt.value.accept(this);
if(stmt.type != value.type)
errorHandler.add(new InvalidTypeException(
"Type " + stmt.type + " declared for variable '" + stmt.identifier + "' does not match assigned value type " +
valueType, stmt.position));
return Type.VOID;
value.type, stmt.position));
return new TypedStmt.VariableDeclaration(stmt.type, stmt.identifier, value);
}
@Override
public Type visitReturnStmt(Stmt.Return stmt) {
stmt.setType(stmt.value.accept(this));
return Type.VOID;
public TypedStmt visitReturnStmt(Stmt.Return stmt) {
return new TypedStmt.Return(stmt.value.accept(this));
}
@Override
public Type visitIfStmt(Stmt.If stmt) {
if(stmt.condition.accept(this) != Type.BOOLEAN) throw new RuntimeException();
stmt.trueBody.accept(this);
for(Pair<Expr, Stmt.Block> clause : stmt.elseIfClauses) {
if(clause.getLeft().accept(this) != Type.BOOLEAN) throw new RuntimeException();
clause.getRight().accept(this);
}
if(stmt.elseBody != null) {
stmt.elseBody.accept(this);
}
return Type.VOID;
public TypedStmt visitIfStmt(Stmt.If stmt) {
TypedExpr condition = stmt.condition.accept(this);
if(condition.type != 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 != 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 Type visitForStmt(Stmt.For stmt) {
stmt.initializer.accept(this);
if(stmt.condition.accept(this) != Type.BOOLEAN) throw new RuntimeException();
stmt.incrementer.accept(this);
stmt.body.accept(this);
return Type.VOID;
public TypedStmt visitForStmt(Stmt.For stmt) {
TypedStmt initializer = stmt.initializer.accept(this);
TypedExpr condition = stmt.condition.accept(this);
if(condition.type != 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 Type visitWhileStmt(Stmt.While stmt) {
if(stmt.condition.accept(this) != Type.BOOLEAN) throw new RuntimeException();
stmt.body.accept(this);
return Type.VOID;
public TypedStmt visitWhileStmt(Stmt.While stmt) {
TypedExpr condition = stmt.condition.accept(this);
if(condition.type != 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 Type visitNoOpStmt(Stmt.NoOp stmt) {
return Type.VOID;
public TypedStmt visitNoOpStmt(Stmt.NoOp stmt) {
return new TypedStmt.NoOp();
}
@Override
public Type visitBreakStmt(Stmt.Break stmt) {
return Type.VOID;
public TypedStmt visitBreakStmt(Stmt.Break stmt) {
return new TypedStmt.Break();
}
@Override
public Type visitContinueStmt(Stmt.Continue stmt) {
return Type.VOID;
public TypedStmt visitContinueStmt(Stmt.Continue stmt) {
return new TypedStmt.Continue();
}
}

View File

@@ -2,6 +2,7 @@ package codegen;
import com.dfsek.terra.addons.terrascript.ErrorHandler;
import com.dfsek.terra.addons.terrascript.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.codegen.TerraScript;
import com.dfsek.terra.addons.terrascript.codegen.asm.TerraScriptClassGenerator;
import com.dfsek.terra.addons.terrascript.lexer.Lexer;
@@ -10,11 +11,59 @@ import com.dfsek.terra.addons.terrascript.semanticanalysis.SemanticAnalyzer;
import org.junit.jupiter.api.Test;
import java.util.Objects;
public class CodeGenTest {
@Test
public void test() {
testValid("""
if (1 == 1) print("Dis is true");
num a = 1;
num b = 2;
str e = "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() {
num i = 0;
while (true) {
if (i == 2) break;
i = i + 1;
}
}
loopTwiceThenBreak();
retNum();
bool bln = true;
print(takesArgs("test", 3, true));
print(retStr());
doStuff("Ayo", "world", true);
fun retNum(): num {
return 3 + 3;
}
@@ -49,26 +98,14 @@ public class CodeGenTest {
print("c is false");
}
}
num a = 1;
num b = 2;
str e = "test";
retNum();
bool bln = true;
print(takesArgs("test", 3, true));
print(retStr());
doStuff("Ay0o", "world", true);
""");
}
private void testValid(String validSource) {
try {
Block script = Parser.parse(new Lexer(validSource).analyze());
SemanticAnalyzer.analyze(script, new ErrorHandler());
TerraScript ts = new TerraScriptClassGenerator("./build/codegentest").generate(script);
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);

View File

@@ -1,101 +0,0 @@
/*
* 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 legacy;
import com.dfsek.terra.addons.terrascript.Type;
import com.dfsek.terra.addons.terrascript.lexer.Lexer;
import com.dfsek.terra.addons.terrascript.legacy.parser.lang.Scope.ScopeBuilder;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Objects;
import com.dfsek.terra.addons.terrascript.legacy.parser.Parser;
import com.dfsek.terra.addons.terrascript.legacy.parser.exceptions.ParseException;
import com.dfsek.terra.addons.terrascript.legacy.parser.lang.Executable;
import com.dfsek.terra.addons.terrascript.legacy.parser.lang.ImplementationArguments;
import com.dfsek.terra.addons.terrascript.legacy.parser.lang.Expression;
import com.dfsek.terra.addons.terrascript.legacy.parser.lang.Scope;
import com.dfsek.terra.addons.terrascript.legacy.parser.lang.functions.Function;
import com.dfsek.terra.addons.terrascript.legacy.parser.lang.functions.FunctionBuilder;
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
public class ParserTest {
@Test
public void parse() throws IOException, ParseException {
Lexer lexer = new Lexer(IOUtils.toString(Objects.requireNonNull(getClass().getResourceAsStream("/test.tesf")), Charset.defaultCharset()));
Parser parser = new Parser(lexer);
ScopeBuilder scope = new ScopeBuilder();
scope.registerFunction("test", new FunctionBuilder<Test1>() {
@Override
public Test1 build(List<Expression<?>> argumentList, SourcePosition position) {
return new Test1(argumentList.get(0), argumentList.get(1), position);
}
@Override
public int argNumber() {
return 2;
}
@Override
public Type getArgument(int position) {
return switch(position) {
case 0 -> Type.STRING;
case 1 -> Type.NUMBER;
default -> null;
};
}
});
long l = System.nanoTime();
Executable block = parser.parse(scope);
long t = System.nanoTime() - l;
System.out.println("Took " + (double) t / 1000000);
block.execute(null);
block.execute(null);
}
private static class Test1 implements Function<Void> {
private final Expression<?> a;
private final Expression<?> b;
private final SourcePosition position;
public Test1(Expression<?> a, Expression<?> b, SourcePosition position) {
this.a = a;
this.b = b;
this.position = position;
}
@Override
public Void evaluate(ImplementationArguments implementationArguments, Scope scope) {
System.out.println("string: " + a.evaluate(implementationArguments, scope) + ", double: " +
b.evaluate(implementationArguments, scope));
return null;
}
@Override
public SourcePosition getPosition() {
return position;
}
@Override
public Type returnType() {
return Type.VOID;
}
}
}

View File

@@ -92,7 +92,7 @@ public class SemanticAnalyzerTest {
testValid("fun returnVoid() { return (); }");
}
@Test
//Not implemented yet @Test
public void testControlFlowAnalysis() {
testValid("""

View File

@@ -1,116 +0,0 @@
fun myFunction(functionString: str, functionNumber: num): str {
test(functionString, functionNumber);
fun nestedFunction() {
test("Hello from nested function", 69);
}
nestedFunction();
return "Return to sender";
}
str functionResult = myFunction("Hello from myFunction", 535);
test(functionResult, 58);
fun noReturn() test("Single statement function", 42);
noReturn();
bool thing1 = 2 > (2+2) || false;
if(2 > 2 || 3 + 4 <= 2 && 4 + 5 > 2 / 3) {
test("ok", 2);
}
test("minecraft:green_w" + "ool", (2 * (3+1) * (2 * (1+1))));
//
num testVar = 3.4;
bool boolean = true;
str stringVar = "hello!";
num precedence = 3 + 2 * 2 + 3;
test("precedence: " + precedence, 2);
num precedence2 = 3 * 2 + 2 * 3;
test("precedence 2: " + precedence2, 2);
bool iftest = false;
bool truetest = false;
num iterator = 0;
num thing = 4 - 2-2+2-2+2;
test("4 - 2 = " + thing, 2);
thing = -2;
test("-2 = " + thing, 2);
thing = -thing;
test("--2 = " + thing, 2);
for(;;) {
break;
}
for(num i = 0; i < 5; i = i + 1) {
test("i = " + i, iterator);
if(i > 1 + 1) {
test("more than 2", iterator);
continue;
}
}
for(num i = 0; i < 5; i = i + 1) {
test("i = " + i, iterator);
}
for(num j = 0; j < 5; j = j + 1) test("single statement j = " + j, iterator);
if(4 + 2 == 2 + 4) {
test("new thing " + 2, iterator);
}
while(iterator < 5) {
test("always, even after " + 2, iterator);
iterator = iterator + 1;
if(iterator > 2) {
continue;
}
test("not after " + 2, iterator);
}
if(true) test("single statement" + 2, iterator);
else if(true) test("another single statement" + 2, iterator);
if(true) {
test("true!" + 2, iterator);
} else {
test("false!" + 2, iterator);
}
if(false) {
test("true!" + 2, iterator);
} else {
test("false!" + 2, iterator);
}
if(false) {
test("true again!" + 2, iterator);
} else if(true == true) {
test("false again!" + 2, iterator);
} else {
test("not logged!" + 2, iterator);
}
// comment
/*
fsdfsd
*/
test("fdsgdf" + 2, 1 + testVar);
if(true && !(boolean && false) && true) {
num scopedVar = 2;
test("if statement" + 2 + stringVar, 1 + testVar + scopedVar);
}