mirror of
https://github.com/PolyhedralDev/Terra.git
synced 2026-10-28 06:50:02 +00:00
Move terrascript 2 to separate module
This commit is contained in:
1 parent
375f0ba60f
commit
1623a4f958
156 files changed
+1441
-1495
No files matched your search
@@ -1,15 +0,0 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
|
||||||
|
|
||||||
version = version("1.1.0")
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api("commons-io:commons-io:2.7")
|
|
||||||
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.lib.commons")
|
|
||||||
relocate("net.jafama", "com.dfsek.terra.addons.terrascript.lib.jafama")
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +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 com.dfsek.terra.addons.terrascript;
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.exceptions.ParseException;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.functions.FunctionBuilder;
|
|
||||||
import com.dfsek.terra.addons.terrascript.script.StructureScript;
|
|
||||||
import com.dfsek.terra.api.Platform;
|
|
||||||
import com.dfsek.terra.api.addon.BaseAddon;
|
|
||||||
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
|
|
||||||
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
|
|
||||||
import com.dfsek.terra.api.inject.annotations.Inject;
|
|
||||||
import com.dfsek.terra.api.registry.CheckedRegistry;
|
|
||||||
import com.dfsek.terra.api.structure.LootTable;
|
|
||||||
import com.dfsek.terra.api.structure.Structure;
|
|
||||||
import com.dfsek.terra.api.util.StringUtil;
|
|
||||||
|
|
||||||
|
|
||||||
public class TerraScriptAddon implements AddonInitializer {
|
|
||||||
@Inject
|
|
||||||
private Platform platform;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
private BaseAddon addon;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
platform.getEventManager()
|
|
||||||
.getHandler(FunctionalEventHandler.class)
|
|
||||||
.register(addon, ConfigPackPreLoadEvent.class)
|
|
||||||
.then(event -> {
|
|
||||||
CheckedRegistry<Structure> structureRegistry = event.getPack().getOrCreateRegistry(Structure.class);
|
|
||||||
CheckedRegistry<LootTable> lootRegistry = event.getPack().getOrCreateRegistry(LootTable.class);
|
|
||||||
event.getPack().getLoader().open("", ".tesf").thenEntries(
|
|
||||||
entries ->
|
|
||||||
entries.stream()
|
|
||||||
.parallel()
|
|
||||||
.map(entry -> {
|
|
||||||
try {
|
|
||||||
String id = StringUtil.fileName(entry.getKey());
|
|
||||||
return new StructureScript(entry.getValue(),
|
|
||||||
addon.key(id),
|
|
||||||
platform,
|
|
||||||
structureRegistry,
|
|
||||||
lootRegistry,
|
|
||||||
event.getPack().getOrCreateRegistry(FunctionBuilder.class));
|
|
||||||
} catch(ParseException e) {
|
|
||||||
throw new RuntimeException("Failed to load script \"" + entry.getKey() + "\"", e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.toList()
|
|
||||||
.forEach(structureRegistry::register))
|
|
||||||
.close();
|
|
||||||
})
|
|
||||||
.priority(100)
|
|
||||||
.failThrough();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-468
@@ -1,468 +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 com.dfsek.terra.addons.terrascript.parser;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.exceptions.ParseException;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.Block;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.Executable;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.Item;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.Keyword;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.Returnable;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.Returnable.ReturnType;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.Scope.ScopeBuilder;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.constants.BooleanConstant;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.constants.ConstantExpression;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.constants.NumericConstant;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.constants.StringConstant;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.functions.Function;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.functions.FunctionBuilder;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.flow.BreakKeyword;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.flow.ContinueKeyword;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.flow.FailKeyword;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.flow.ReturnKeyword;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.looplike.ForKeyword;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.looplike.IfKeyword;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.looplike.WhileKeyword;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.BinaryOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.BooleanAndOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.BooleanNotOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.BooleanOrOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.ConcatenationOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.DivisionOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.ModuloOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.MultiplicationOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.NegationOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.NumberAdditionOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.SubtractionOperation;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.EqualsStatement;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.GreaterOrEqualsThanStatement;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.GreaterThanStatement;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.LessThanOrEqualsStatement;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.LessThanStatement;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.NotEqualsStatement;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.variables.assign.BoolAssignmentNode;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.variables.assign.NumAssignmentNode;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.variables.assign.StrAssignmentNode;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.variables.assign.VariableAssignmentNode;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.variables.reference.BoolVariableReferenceNode;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.variables.reference.NumVariableReferenceNode;
|
|
||||||
import com.dfsek.terra.addons.terrascript.parser.lang.variables.reference.StrVariableReferenceNode;
|
|
||||||
import com.dfsek.terra.addons.terrascript.tokenizer.Position;
|
|
||||||
import com.dfsek.terra.addons.terrascript.tokenizer.Token;
|
|
||||||
import com.dfsek.terra.addons.terrascript.tokenizer.Tokenizer;
|
|
||||||
import com.dfsek.terra.api.util.generic.pair.Pair;
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public class Parser {
|
|
||||||
private final String data;
|
|
||||||
private final Map<String, FunctionBuilder<? extends Function<?>>> functions = new HashMap<>();
|
|
||||||
private final List<String> ignoredFunctions = new ArrayList<>();
|
|
||||||
|
|
||||||
public Parser(String data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Parser registerFunction(String name, FunctionBuilder<? extends Function<?>> functionBuilder) {
|
|
||||||
functions.put(name, functionBuilder);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Parser ignoreFunction(String name) {
|
|
||||||
ignoredFunctions.add(name);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse input
|
|
||||||
*
|
|
||||||
* @return executable {@link Block}
|
|
||||||
*
|
|
||||||
* @throws ParseException If parsing fails.
|
|
||||||
*/
|
|
||||||
public Executable parse() {
|
|
||||||
ScopeBuilder scopeBuilder = new ScopeBuilder();
|
|
||||||
return new Executable(parseBlock(new Tokenizer(data), false, scopeBuilder), scopeBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Keyword<?> parseLoopLike(Tokenizer tokens, boolean loop, ScopeBuilder scopeBuilder) throws ParseException {
|
|
||||||
|
|
||||||
Token identifier = tokens.consume();
|
|
||||||
ParserUtil.checkType(identifier, Token.Type.IF_STATEMENT, Token.Type.WHILE_LOOP, Token.Type.FOR_LOOP);
|
|
||||||
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_BEGIN);
|
|
||||||
|
|
||||||
return switch(identifier.getType()) {
|
|
||||||
case FOR_LOOP -> parseForLoop(tokens, identifier.getPosition(), scopeBuilder);
|
|
||||||
case IF_STATEMENT -> parseIfStatement(tokens, identifier.getPosition(), loop, scopeBuilder);
|
|
||||||
case WHILE_LOOP -> parseWhileLoop(tokens, identifier.getPosition(), scopeBuilder);
|
|
||||||
default -> throw new UnsupportedOperationException(
|
|
||||||
"Unknown keyword " + identifier.getContent() + ": " + identifier.getPosition());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private WhileKeyword parseWhileLoop(Tokenizer tokens, Position start, ScopeBuilder scopeBuilder) {
|
|
||||||
Returnable<?> first = parseExpression(tokens, true, scopeBuilder);
|
|
||||||
ParserUtil.checkReturnType(first, Returnable.ReturnType.BOOLEAN);
|
|
||||||
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END);
|
|
||||||
|
|
||||||
return new WhileKeyword(parseStatementBlock(tokens, true, scopeBuilder), (Returnable<Boolean>) first, start); // While loop
|
|
||||||
}
|
|
||||||
|
|
||||||
private IfKeyword parseIfStatement(Tokenizer tokens, Position start, boolean loop, ScopeBuilder scopeBuilder) {
|
|
||||||
Returnable<?> condition = parseExpression(tokens, true, scopeBuilder);
|
|
||||||
ParserUtil.checkReturnType(condition, Returnable.ReturnType.BOOLEAN);
|
|
||||||
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END);
|
|
||||||
|
|
||||||
Block elseBlock = null;
|
|
||||||
Block statement = parseStatementBlock(tokens, loop, scopeBuilder);
|
|
||||||
|
|
||||||
List<Pair<Returnable<Boolean>, Block>> elseIf = new ArrayList<>();
|
|
||||||
|
|
||||||
while(tokens.hasNext() && tokens.get().getType().equals(Token.Type.ELSE)) {
|
|
||||||
tokens.consume(); // Consume else.
|
|
||||||
if(tokens.get().getType().equals(Token.Type.IF_STATEMENT)) {
|
|
||||||
tokens.consume(); // Consume if.
|
|
||||||
Returnable<?> elseCondition = parseExpression(tokens, true, scopeBuilder);
|
|
||||||
ParserUtil.checkReturnType(elseCondition, Returnable.ReturnType.BOOLEAN);
|
|
||||||
elseIf.add(Pair.of((Returnable<Boolean>) elseCondition, parseStatementBlock(tokens, loop, scopeBuilder)));
|
|
||||||
} else {
|
|
||||||
elseBlock = parseStatementBlock(tokens, loop, scopeBuilder);
|
|
||||||
break; // Else must be last.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new IfKeyword(statement, (Returnable<Boolean>) condition, elseIf, elseBlock, start); // If statement
|
|
||||||
}
|
|
||||||
|
|
||||||
private Block parseStatementBlock(Tokenizer tokens, boolean loop, ScopeBuilder scopeBuilder) {
|
|
||||||
|
|
||||||
if(tokens.get().getType().equals(Token.Type.BLOCK_BEGIN)) {
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.BLOCK_BEGIN);
|
|
||||||
Block block = parseBlock(tokens, loop, scopeBuilder);
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.BLOCK_END);
|
|
||||||
return block;
|
|
||||||
} else {
|
|
||||||
Position position = tokens.get().getPosition();
|
|
||||||
Block block = new Block(Collections.singletonList(parseItem(tokens, loop, scopeBuilder)), position);
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.STATEMENT_END);
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ForKeyword parseForLoop(Tokenizer tokens, Position start, ScopeBuilder scopeBuilder) {
|
|
||||||
scopeBuilder = scopeBuilder.sub(); // new scope
|
|
||||||
Token f = tokens.get();
|
|
||||||
ParserUtil.checkType(f, Token.Type.NUMBER_VARIABLE, Token.Type.STRING_VARIABLE, Token.Type.BOOLEAN_VARIABLE, Token.Type.IDENTIFIER);
|
|
||||||
Item<?> initializer;
|
|
||||||
if(f.isVariableDeclaration()) {
|
|
||||||
VariableAssignmentNode<?> forVar = parseVariableDeclaration(tokens, scopeBuilder);
|
|
||||||
Token name = tokens.get();
|
|
||||||
if(functions.containsKey(name.getContent()) || scopeBuilder.contains(name.getContent()))
|
|
||||||
throw new ParseException(name.getContent() + " is already defined in this scope", name.getPosition());
|
|
||||||
initializer = forVar;
|
|
||||||
} else initializer = parseExpression(tokens, true, scopeBuilder);
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.STATEMENT_END);
|
|
||||||
Returnable<?> conditional = parseExpression(tokens, true, scopeBuilder);
|
|
||||||
ParserUtil.checkReturnType(conditional, Returnable.ReturnType.BOOLEAN);
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.STATEMENT_END);
|
|
||||||
|
|
||||||
Item<?> incrementer;
|
|
||||||
Token token = tokens.get();
|
|
||||||
if(scopeBuilder.contains(token.getContent())) { // Assume variable assignment
|
|
||||||
incrementer = parseAssignment(tokens, scopeBuilder);
|
|
||||||
} else incrementer = parseFunction(tokens, true, scopeBuilder);
|
|
||||||
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END);
|
|
||||||
|
|
||||||
return new ForKeyword(parseStatementBlock(tokens, true, scopeBuilder), initializer, (Returnable<Boolean>) conditional, incrementer,
|
|
||||||
start);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Returnable<?> parseExpression(Tokenizer tokens, boolean full, ScopeBuilder scopeBuilder) {
|
|
||||||
boolean booleanInverted = false; // Check for boolean not operator
|
|
||||||
boolean negate = false;
|
|
||||||
if(tokens.get().getType().equals(Token.Type.BOOLEAN_NOT)) {
|
|
||||||
booleanInverted = true;
|
|
||||||
tokens.consume();
|
|
||||||
} else if(tokens.get().getType().equals(Token.Type.SUBTRACTION_OPERATOR)) {
|
|
||||||
negate = true;
|
|
||||||
tokens.consume();
|
|
||||||
}
|
|
||||||
|
|
||||||
Token id = tokens.get();
|
|
||||||
|
|
||||||
ParserUtil.checkType(id, Token.Type.IDENTIFIER, Token.Type.BOOLEAN, Token.Type.STRING, Token.Type.NUMBER, Token.Type.GROUP_BEGIN);
|
|
||||||
|
|
||||||
Returnable<?> expression;
|
|
||||||
if(id.isConstant()) {
|
|
||||||
expression = parseConstantExpression(tokens);
|
|
||||||
} else if(id.getType().equals(Token.Type.GROUP_BEGIN)) { // Parse grouped expression
|
|
||||||
expression = parseGroup(tokens, scopeBuilder);
|
|
||||||
} else {
|
|
||||||
if(functions.containsKey(id.getContent()))
|
|
||||||
expression = parseFunction(tokens, false, scopeBuilder);
|
|
||||||
else if(scopeBuilder.contains(id.getContent())) {
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.IDENTIFIER);
|
|
||||||
String varId = id.getContent();
|
|
||||||
ReturnType varType = scopeBuilder.getType(varId);
|
|
||||||
expression = switch(varType) {
|
|
||||||
case NUMBER -> new NumVariableReferenceNode(id.getPosition(), varType, scopeBuilder.getIndex(varId));
|
|
||||||
case STRING -> new StrVariableReferenceNode(id.getPosition(), varType, scopeBuilder.getIndex(varId));
|
|
||||||
case BOOLEAN -> new BoolVariableReferenceNode(id.getPosition(), varType, scopeBuilder.getIndex(varId));
|
|
||||||
default -> throw new ParseException("Illegal type for variable reference: " + varType, id.getPosition());
|
|
||||||
};
|
|
||||||
|
|
||||||
} else throw new ParseException("Unexpected token \" " + id.getContent() + "\"", id.getPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(booleanInverted) { // Invert operation if boolean not detected
|
|
||||||
ParserUtil.checkReturnType(expression, Returnable.ReturnType.BOOLEAN);
|
|
||||||
expression = new BooleanNotOperation((Returnable<Boolean>) expression, expression.getPosition());
|
|
||||||
} else if(negate) {
|
|
||||||
ParserUtil.checkReturnType(expression, Returnable.ReturnType.NUMBER);
|
|
||||||
expression = new NegationOperation((Returnable<Number>) expression, expression.getPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(full && tokens.get().isBinaryOperator()) { // Parse binary operations
|
|
||||||
return parseBinaryOperation(expression, tokens, scopeBuilder);
|
|
||||||
}
|
|
||||||
return expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ConstantExpression<?> parseConstantExpression(Tokenizer tokens) {
|
|
||||||
Token constantToken = tokens.consume();
|
|
||||||
Position position = constantToken.getPosition();
|
|
||||||
switch(constantToken.getType()) {
|
|
||||||
case NUMBER:
|
|
||||||
String content = constantToken.getContent();
|
|
||||||
return new NumericConstant(content.contains(".") ? Double.parseDouble(content) : Integer.parseInt(content), position);
|
|
||||||
case STRING:
|
|
||||||
return new StringConstant(constantToken.getContent(), position);
|
|
||||||
case BOOLEAN:
|
|
||||||
return new BooleanConstant(Boolean.parseBoolean(constantToken.getContent()), position);
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException(
|
|
||||||
"Unsupported constant token: " + constantToken.getType() + " at position: " + position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Returnable<?> parseGroup(Tokenizer tokens, ScopeBuilder scopeBuilder) {
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_BEGIN);
|
|
||||||
Returnable<?> expression = parseExpression(tokens, true, scopeBuilder); // Parse inside of group as a separate expression
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END);
|
|
||||||
return expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BinaryOperation<?, ?> parseBinaryOperation(Returnable<?> left, Tokenizer tokens,
|
|
||||||
ScopeBuilder scopeBuilder) {
|
|
||||||
Token binaryOperator = tokens.consume();
|
|
||||||
ParserUtil.checkBinaryOperator(binaryOperator);
|
|
||||||
|
|
||||||
Returnable<?> right = parseExpression(tokens, false, scopeBuilder);
|
|
||||||
|
|
||||||
Token other = tokens.get();
|
|
||||||
if(ParserUtil.hasPrecedence(binaryOperator.getType(), other.getType())) {
|
|
||||||
return assemble(left, parseBinaryOperation(right, tokens, scopeBuilder), binaryOperator);
|
|
||||||
} else if(other.isBinaryOperator()) {
|
|
||||||
return parseBinaryOperation(assemble(left, right, binaryOperator), tokens, scopeBuilder);
|
|
||||||
}
|
|
||||||
return assemble(left, right, binaryOperator);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BinaryOperation<?, ?> assemble(Returnable<?> left, Returnable<?> right, Token binaryOperator) {
|
|
||||||
if(binaryOperator.isStrictNumericOperator())
|
|
||||||
ParserUtil.checkArithmeticOperation(left, right, binaryOperator); // Numeric type checking
|
|
||||||
if(binaryOperator.isStrictBooleanOperator()) ParserUtil.checkBooleanOperation(left, right, binaryOperator); // Boolean type checking
|
|
||||||
switch(binaryOperator.getType()) {
|
|
||||||
case ADDITION_OPERATOR:
|
|
||||||
if(left.returnType().equals(Returnable.ReturnType.NUMBER) && right.returnType().equals(Returnable.ReturnType.NUMBER)) {
|
|
||||||
return new NumberAdditionOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
|
||||||
}
|
|
||||||
return new ConcatenationOperation((Returnable<Object>) left, (Returnable<Object>) right, binaryOperator.getPosition());
|
|
||||||
case SUBTRACTION_OPERATOR:
|
|
||||||
return new SubtractionOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
|
||||||
case MULTIPLICATION_OPERATOR:
|
|
||||||
return new MultiplicationOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
|
||||||
case DIVISION_OPERATOR:
|
|
||||||
return new DivisionOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
|
||||||
case EQUALS_OPERATOR:
|
|
||||||
return new EqualsStatement((Returnable<Object>) left, (Returnable<Object>) right, binaryOperator.getPosition());
|
|
||||||
case NOT_EQUALS_OPERATOR:
|
|
||||||
return new NotEqualsStatement((Returnable<Object>) left, (Returnable<Object>) right, binaryOperator.getPosition());
|
|
||||||
case GREATER_THAN_OPERATOR:
|
|
||||||
return new GreaterThanStatement((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
|
||||||
case LESS_THAN_OPERATOR:
|
|
||||||
return new LessThanStatement((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
|
||||||
case GREATER_THAN_OR_EQUALS_OPERATOR:
|
|
||||||
return new GreaterOrEqualsThanStatement((Returnable<Number>) left, (Returnable<Number>) right,
|
|
||||||
binaryOperator.getPosition());
|
|
||||||
case LESS_THAN_OR_EQUALS_OPERATOR:
|
|
||||||
return new LessThanOrEqualsStatement((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
|
||||||
case BOOLEAN_AND:
|
|
||||||
return new BooleanAndOperation((Returnable<Boolean>) left, (Returnable<Boolean>) right, binaryOperator.getPosition());
|
|
||||||
case BOOLEAN_OR:
|
|
||||||
return new BooleanOrOperation((Returnable<Boolean>) left, (Returnable<Boolean>) right, binaryOperator.getPosition());
|
|
||||||
case MODULO_OPERATOR:
|
|
||||||
return new ModuloOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException("Unsupported binary operator: " + binaryOperator.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private VariableAssignmentNode<?> parseVariableDeclaration(Tokenizer tokens, ScopeBuilder scopeBuilder) {
|
|
||||||
Token type = tokens.consume();
|
|
||||||
ParserUtil.checkType(type, Token.Type.STRING_VARIABLE, Token.Type.BOOLEAN_VARIABLE, Token.Type.NUMBER_VARIABLE);
|
|
||||||
|
|
||||||
Returnable.ReturnType returnType = ParserUtil.getVariableReturnType(type);
|
|
||||||
|
|
||||||
ParserUtil.checkVarType(type, returnType); // Check for type mismatch
|
|
||||||
Token identifier = tokens.consume();
|
|
||||||
ParserUtil.checkType(identifier, Token.Type.IDENTIFIER);
|
|
||||||
if(functions.containsKey(identifier.getContent()) || scopeBuilder.contains(identifier.getContent()))
|
|
||||||
throw new ParseException(identifier.getContent() + " is already defined in this scope", identifier.getPosition());
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.ASSIGNMENT);
|
|
||||||
|
|
||||||
Returnable<?> value = parseExpression(tokens, true, scopeBuilder);
|
|
||||||
ParserUtil.checkReturnType(value, returnType);
|
|
||||||
|
|
||||||
String id = identifier.getContent();
|
|
||||||
|
|
||||||
return switch(value.returnType()) {
|
|
||||||
case NUMBER -> new NumAssignmentNode((Returnable<Number>) value, identifier.getPosition(), scopeBuilder.num(id));
|
|
||||||
case STRING -> new StrAssignmentNode((Returnable<String>) value, identifier.getPosition(), scopeBuilder.str(id));
|
|
||||||
case BOOLEAN -> new BoolAssignmentNode((Returnable<Boolean>) value, identifier.getPosition(), scopeBuilder.bool(id));
|
|
||||||
default -> throw new ParseException("Illegal type for variable declaration: " + type, value.getPosition());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Block parseBlock(Tokenizer tokens, boolean loop, ScopeBuilder scopeBuilder) {
|
|
||||||
List<Item<?>> parsedItems = new ArrayList<>();
|
|
||||||
|
|
||||||
scopeBuilder = scopeBuilder.sub();
|
|
||||||
|
|
||||||
Token first = tokens.get();
|
|
||||||
|
|
||||||
while(tokens.hasNext()) {
|
|
||||||
Token token = tokens.get();
|
|
||||||
if(token.getType().equals(Token.Type.BLOCK_END)) break; // Stop parsing at block end.
|
|
||||||
Item<?> parsedItem = parseItem(tokens, loop, scopeBuilder);
|
|
||||||
if(parsedItem != Function.NULL) {
|
|
||||||
parsedItems.add(parsedItem);
|
|
||||||
}
|
|
||||||
if(tokens.hasNext() && !token.isLoopLike()) ParserUtil.checkType(tokens.consume(), Token.Type.STATEMENT_END);
|
|
||||||
}
|
|
||||||
return new Block(parsedItems, first.getPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Item<?> parseItem(Tokenizer tokens, boolean loop, ScopeBuilder scopeBuilder) {
|
|
||||||
Token token = tokens.get();
|
|
||||||
if(loop) ParserUtil.checkType(token, Token.Type.IDENTIFIER, Token.Type.IF_STATEMENT, Token.Type.WHILE_LOOP, Token.Type.FOR_LOOP,
|
|
||||||
Token.Type.NUMBER_VARIABLE, Token.Type.STRING_VARIABLE, Token.Type.BOOLEAN_VARIABLE,
|
|
||||||
Token.Type.RETURN, Token.Type.BREAK, Token.Type.CONTINUE, Token.Type.FAIL);
|
|
||||||
else ParserUtil.checkType(token, Token.Type.IDENTIFIER, Token.Type.IF_STATEMENT, Token.Type.WHILE_LOOP, Token.Type.FOR_LOOP,
|
|
||||||
Token.Type.NUMBER_VARIABLE, Token.Type.STRING_VARIABLE, Token.Type.BOOLEAN_VARIABLE, Token.Type.RETURN,
|
|
||||||
Token.Type.FAIL);
|
|
||||||
|
|
||||||
if(token.isLoopLike()) { // Parse loop-like tokens (if, while, etc)
|
|
||||||
return parseLoopLike(tokens, loop, scopeBuilder);
|
|
||||||
} else if(token.isIdentifier()) { // Parse identifiers
|
|
||||||
if(scopeBuilder.contains(token.getContent())) { // Assume variable assignment
|
|
||||||
return parseAssignment(tokens, scopeBuilder);
|
|
||||||
} else return parseFunction(tokens, true, scopeBuilder);
|
|
||||||
} else if(token.isVariableDeclaration()) {
|
|
||||||
|
|
||||||
return parseVariableDeclaration(tokens, scopeBuilder);
|
|
||||||
|
|
||||||
} else if(token.getType().equals(Token.Type.RETURN)) return new ReturnKeyword(tokens.consume().getPosition());
|
|
||||||
else if(token.getType().equals(Token.Type.BREAK)) return new BreakKeyword(tokens.consume().getPosition());
|
|
||||||
else if(token.getType().equals(Token.Type.CONTINUE)) return new ContinueKeyword(tokens.consume().getPosition());
|
|
||||||
else if(token.getType().equals(Token.Type.FAIL)) return new FailKeyword(tokens.consume().getPosition());
|
|
||||||
else throw new UnsupportedOperationException("Unexpected token " + token.getType() + ": " + token.getPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
private VariableAssignmentNode<?> parseAssignment(Tokenizer tokens, ScopeBuilder scopeBuilder) {
|
|
||||||
Token identifier = tokens.consume();
|
|
||||||
|
|
||||||
ParserUtil.checkType(identifier, Token.Type.IDENTIFIER);
|
|
||||||
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.ASSIGNMENT);
|
|
||||||
|
|
||||||
Returnable<?> value = parseExpression(tokens, true, scopeBuilder);
|
|
||||||
|
|
||||||
String id = identifier.getContent();
|
|
||||||
|
|
||||||
ParserUtil.checkReturnType(value, scopeBuilder.getType(id));
|
|
||||||
|
|
||||||
ReturnType type = value.returnType();
|
|
||||||
|
|
||||||
return switch(type) {
|
|
||||||
case NUMBER -> new NumAssignmentNode((Returnable<Number>) value, identifier.getPosition(), scopeBuilder.getIndex(id));
|
|
||||||
case STRING -> new StrAssignmentNode((Returnable<String>) value, identifier.getPosition(), scopeBuilder.getIndex(id));
|
|
||||||
case BOOLEAN -> new BoolAssignmentNode((Returnable<Boolean>) value, identifier.getPosition(), scopeBuilder.getIndex(id));
|
|
||||||
default -> throw new ParseException("Illegal type for variable assignment: " + type, value.getPosition());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Function<?> parseFunction(Tokenizer tokens, boolean fullStatement, ScopeBuilder scopeBuilder) {
|
|
||||||
Token identifier = tokens.consume();
|
|
||||||
ParserUtil.checkType(identifier, Token.Type.IDENTIFIER); // First token must be identifier
|
|
||||||
|
|
||||||
if(!functions.containsKey(identifier.getContent()))
|
|
||||||
throw new ParseException("No such function \"" + identifier.getContent() + "\"", identifier.getPosition());
|
|
||||||
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_BEGIN); // Second is body begin
|
|
||||||
|
|
||||||
|
|
||||||
List<Returnable<?>> args = getArgs(tokens, scopeBuilder); // Extract arguments, consume the rest.
|
|
||||||
|
|
||||||
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END); // Remove body end
|
|
||||||
|
|
||||||
if(fullStatement) ParserUtil.checkType(tokens.get(), Token.Type.STATEMENT_END);
|
|
||||||
|
|
||||||
if(ignoredFunctions.contains(identifier.getContent())) {
|
|
||||||
return Function.NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(functions.containsKey(identifier.getContent())) {
|
|
||||||
FunctionBuilder<?> builder = functions.get(identifier.getContent());
|
|
||||||
|
|
||||||
if(builder.argNumber() != -1 && args.size() != builder.argNumber())
|
|
||||||
throw new ParseException("Expected " + builder.argNumber() + " arguments, found " + args.size(), identifier.getPosition());
|
|
||||||
|
|
||||||
for(int i = 0; i < args.size(); i++) {
|
|
||||||
Returnable<?> argument = args.get(i);
|
|
||||||
if(builder.getArgument(i) == null)
|
|
||||||
throw new ParseException("Unexpected argument at position " + i + " in function " + identifier.getContent(),
|
|
||||||
identifier.getPosition());
|
|
||||||
ParserUtil.checkReturnType(argument, builder.getArgument(i));
|
|
||||||
}
|
|
||||||
return builder.build(args, identifier.getPosition());
|
|
||||||
}
|
|
||||||
throw new UnsupportedOperationException("Unsupported function: " + identifier.getContent());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Returnable<?>> getArgs(Tokenizer tokens, ScopeBuilder scopeBuilder) {
|
|
||||||
List<Returnable<?>> args = new ArrayList<>();
|
|
||||||
|
|
||||||
while(!tokens.get().getType().equals(Token.Type.GROUP_END)) {
|
|
||||||
args.add(parseExpression(tokens, true, scopeBuilder));
|
|
||||||
ParserUtil.checkType(tokens.get(), Token.Type.SEPARATOR, Token.Type.GROUP_END);
|
|
||||||
if(tokens.get().getType().equals(Token.Type.SEPARATOR)) tokens.consume();
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,6 @@ version = version("1.1.0")
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api("commons-io:commons-io:2.7")
|
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"))
|
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
|
||||||
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
|
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
|
||||||
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
|
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
|
||||||
@@ -14,232 +12,4 @@ dependencies {
|
|||||||
tasks.named<ShadowJar>("shadowJar") {
|
tasks.named<ShadowJar>("shadowJar") {
|
||||||
relocate("org.apache.commons", "com.dfsek.terra.addons.terrascript.lib.commons")
|
relocate("org.apache.commons", "com.dfsek.terra.addons.terrascript.lib.commons")
|
||||||
relocate("net.jafama", "com.dfsek.terra.addons.terrascript.lib.jafama")
|
relocate("net.jafama", "com.dfsek.terra.addons.terrascript.lib.jafama")
|
||||||
}
|
}
|
||||||
|
|
||||||
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>,
|
|
||||||
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.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"), 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 "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>", "scopedIdentifier" to "String")),
|
|
||||||
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", "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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-4
@@ -8,6 +8,9 @@
|
|||||||
package com.dfsek.terra.addons.terrascript;
|
package com.dfsek.terra.addons.terrascript;
|
||||||
|
|
||||||
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
|
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.exceptions.ParseException;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.functions.FunctionBuilder;
|
||||||
|
import com.dfsek.terra.addons.terrascript.script.StructureScript;
|
||||||
import com.dfsek.terra.api.Platform;
|
import com.dfsek.terra.api.Platform;
|
||||||
import com.dfsek.terra.api.addon.BaseAddon;
|
import com.dfsek.terra.api.addon.BaseAddon;
|
||||||
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
|
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
|
||||||
@@ -28,14 +31,12 @@ public class TerraScriptAddon implements AddonInitializer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
|
||||||
platform.getEventManager()
|
platform.getEventManager()
|
||||||
.getHandler(FunctionalEventHandler.class)
|
.getHandler(FunctionalEventHandler.class)
|
||||||
.register(addon, ConfigPackPreLoadEvent.class)
|
.register(addon, ConfigPackPreLoadEvent.class)
|
||||||
.then(event -> {
|
.then(event -> {
|
||||||
CheckedRegistry<Structure> structureRegistry = event.getPack().getOrCreateRegistry(Structure.class);
|
CheckedRegistry<Structure> structureRegistry = event.getPack().getOrCreateRegistry(Structure.class);
|
||||||
CheckedRegistry<LootTable> lootRegistry = event.getPack().getOrCreateRegistry(LootTable.class);
|
CheckedRegistry<LootTable> lootRegistry = event.getPack().getOrCreateRegistry(LootTable.class);
|
||||||
/*
|
|
||||||
event.getPack().getLoader().open("", ".tesf").thenEntries(
|
event.getPack().getLoader().open("", ".tesf").thenEntries(
|
||||||
entries ->
|
entries ->
|
||||||
entries.stream()
|
entries.stream()
|
||||||
@@ -56,8 +57,6 @@ public class TerraScriptAddon implements AddonInitializer {
|
|||||||
.toList()
|
.toList()
|
||||||
.forEach(structureRegistry::register))
|
.forEach(structureRegistry::register))
|
||||||
.close();
|
.close();
|
||||||
|
|
||||||
*/
|
|
||||||
})
|
})
|
||||||
.priority(100)
|
.priority(100)
|
||||||
.failThrough();
|
.failThrough();
|
||||||
|
|||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package com.dfsek.terra.addons.terrascript.exception.semanticanalysis;
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.terrascript.exception.CompilationException;
|
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
|
|
||||||
|
|
||||||
|
|
||||||
public class IdentifierAlreadyDeclaredException extends CompilationException {
|
|
||||||
public IdentifierAlreadyDeclaredException(String message, SourcePosition position) {
|
|
||||||
super(message, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package com.dfsek.terra.addons.terrascript.exception.semanticanalysis;
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.terrascript.exception.CompilationException;
|
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
|
|
||||||
|
|
||||||
|
|
||||||
public class InvalidFunctionDeclarationException extends CompilationException {
|
|
||||||
public InvalidFunctionDeclarationException(String message, SourcePosition position) {
|
|
||||||
super(message, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package com.dfsek.terra.addons.terrascript.exception.semanticanalysis;
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.terrascript.exception.CompilationException;
|
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
|
|
||||||
|
|
||||||
|
|
||||||
public class InvalidTypeException extends CompilationException {
|
|
||||||
public InvalidTypeException(String message, SourcePosition position) {
|
|
||||||
super(message, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package com.dfsek.terra.addons.terrascript.exception.semanticanalysis;
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.terrascript.exception.CompilationException;
|
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
|
|
||||||
|
|
||||||
|
|
||||||
public class UndefinedReferenceException extends CompilationException {
|
|
||||||
public UndefinedReferenceException(String message, SourcePosition position) {
|
|
||||||
super(message, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
package com.dfsek.terra.addons.terrascript.parser;
|
|
||||||
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.Token.TokenType;
|
|
||||||
|
|
||||||
|
|
||||||
public enum BinaryOperator {
|
|
||||||
BOOLEAN_OR(TokenType.BOOLEAN_OR),
|
|
||||||
BOOLEAN_AND(TokenType.BOOLEAN_AND),
|
|
||||||
EQUALS(TokenType.EQUALS_EQUALS),
|
|
||||||
NOT_EQUALS(TokenType.BANG_EQUALS),
|
|
||||||
GREATER(TokenType.GREATER),
|
|
||||||
GREATER_EQUALS(TokenType.GREATER_EQUAL),
|
|
||||||
LESS(TokenType.LESS),
|
|
||||||
LESS_EQUALS(TokenType.LESS_EQUALS),
|
|
||||||
ADD(TokenType.PLUS),
|
|
||||||
SUBTRACT(TokenType.MINUS),
|
|
||||||
MULTIPLY(TokenType.STAR),
|
|
||||||
DIVIDE(TokenType.FORWARD_SLASH),
|
|
||||||
MODULO(TokenType.MODULO_OPERATOR);
|
|
||||||
|
|
||||||
public final TokenType tokenType;
|
|
||||||
|
|
||||||
BinaryOperator(TokenType tokenType) { this.tokenType = tokenType; }
|
|
||||||
}
|
|
||||||
+412
-323
@@ -1,379 +1,468 @@
|
|||||||
|
/*
|
||||||
|
* 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.parser;
|
package com.dfsek.terra.addons.terrascript.parser;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.terrascript.Type;
|
import com.dfsek.terra.addons.terrascript.parser.exceptions.ParseException;
|
||||||
import com.dfsek.terra.addons.terrascript.Type.TypeException;
|
import com.dfsek.terra.addons.terrascript.parser.lang.Block;
|
||||||
import com.dfsek.terra.addons.terrascript.ast.Expr;
|
import com.dfsek.terra.addons.terrascript.parser.lang.Executable;
|
||||||
import com.dfsek.terra.addons.terrascript.ast.Expr.Variable;
|
import com.dfsek.terra.addons.terrascript.parser.lang.Item;
|
||||||
import com.dfsek.terra.addons.terrascript.ast.Stmt;
|
import com.dfsek.terra.addons.terrascript.parser.lang.Keyword;
|
||||||
import com.dfsek.terra.addons.terrascript.ast.Stmt.Block;
|
import com.dfsek.terra.addons.terrascript.parser.lang.Returnable;
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
|
import com.dfsek.terra.addons.terrascript.parser.lang.Returnable.ReturnType;
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.Token;
|
import com.dfsek.terra.addons.terrascript.parser.lang.Scope.ScopeBuilder;
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.Token.TokenType;
|
import com.dfsek.terra.addons.terrascript.parser.lang.constants.BooleanConstant;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.constants.ConstantExpression;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.constants.NumericConstant;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.constants.StringConstant;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.functions.Function;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.functions.FunctionBuilder;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.flow.BreakKeyword;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.flow.ContinueKeyword;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.flow.FailKeyword;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.flow.ReturnKeyword;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.looplike.ForKeyword;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.looplike.IfKeyword;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.keywords.looplike.WhileKeyword;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.BinaryOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.BooleanAndOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.BooleanNotOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.BooleanOrOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.ConcatenationOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.DivisionOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.ModuloOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.MultiplicationOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.NegationOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.NumberAdditionOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.SubtractionOperation;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.EqualsStatement;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.GreaterOrEqualsThanStatement;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.GreaterThanStatement;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.LessThanOrEqualsStatement;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.LessThanStatement;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.operations.statements.NotEqualsStatement;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.variables.assign.BoolAssignmentNode;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.variables.assign.NumAssignmentNode;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.variables.assign.StrAssignmentNode;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.variables.assign.VariableAssignmentNode;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.variables.reference.BoolVariableReferenceNode;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.variables.reference.NumVariableReferenceNode;
|
||||||
|
import com.dfsek.terra.addons.terrascript.parser.lang.variables.reference.StrVariableReferenceNode;
|
||||||
|
import com.dfsek.terra.addons.terrascript.tokenizer.Position;
|
||||||
|
import com.dfsek.terra.addons.terrascript.tokenizer.Token;
|
||||||
|
import com.dfsek.terra.addons.terrascript.tokenizer.Tokenizer;
|
||||||
import com.dfsek.terra.api.util.generic.pair.Pair;
|
import com.dfsek.terra.api.util.generic.pair.Pair;
|
||||||
|
|
||||||
|
|
||||||
/**
|
@SuppressWarnings("unchecked")
|
||||||
* TerraScript recursive descent parser
|
|
||||||
*/
|
|
||||||
public class Parser {
|
public class Parser {
|
||||||
|
private final String data;
|
||||||
|
private final Map<String, FunctionBuilder<? extends Function<?>>> functions = new HashMap<>();
|
||||||
|
private final List<String> ignoredFunctions = new ArrayList<>();
|
||||||
|
|
||||||
private final List<Token> tokens;
|
public Parser(String data) {
|
||||||
|
this.data = data;
|
||||||
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) {
|
public Parser registerFunction(String name, FunctionBuilder<? extends Function<?>> functionBuilder) {
|
||||||
return new Parser(tokens).parseTokens();
|
functions.put(name, functionBuilder);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Block parseTokens() {
|
public Parser ignoreFunction(String name) {
|
||||||
List<Stmt> statements = new ArrayList<>();
|
ignoredFunctions.add(name);
|
||||||
while(hasNext()) {
|
return this;
|
||||||
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);
|
* Parse input
|
||||||
|
*
|
||||||
|
* @return executable {@link Block}
|
||||||
|
*
|
||||||
|
* @throws ParseException If parsing fails.
|
||||||
|
*/
|
||||||
|
public Executable parse() {
|
||||||
|
ScopeBuilder scopeBuilder = new ScopeBuilder();
|
||||||
|
return new Executable(parseBlock(new Tokenizer(data), false, scopeBuilder), scopeBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasNext() {
|
private Keyword<?> parseLoopLike(Tokenizer tokens, boolean loop, ScopeBuilder scopeBuilder) throws ParseException {
|
||||||
return !current().isType(TokenType.END_OF_FILE);
|
|
||||||
}
|
Token identifier = tokens.consume();
|
||||||
|
ParserUtil.checkType(identifier, Token.Type.IF_STATEMENT, Token.Type.WHILE_LOOP, Token.Type.FOR_LOOP);
|
||||||
private Token consume(String wrongTypeMessage, TokenType expected, TokenType... more) {
|
|
||||||
if(!current().isType(expected) && Arrays.stream(more).noneMatch(t -> t == current().type())) throw new ParseException(
|
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_BEGIN);
|
||||||
wrongTypeMessage, current().position());
|
|
||||||
return consumeUnchecked();
|
return switch(identifier.getType()) {
|
||||||
}
|
case FOR_LOOP -> parseForLoop(tokens, identifier.getPosition(), scopeBuilder);
|
||||||
|
case IF_STATEMENT -> parseIfStatement(tokens, identifier.getPosition(), loop, scopeBuilder);
|
||||||
public Token consumeUnchecked() {
|
case WHILE_LOOP -> parseWhileLoop(tokens, identifier.getPosition(), scopeBuilder);
|
||||||
if(!hasNext()) return current();
|
default -> throw new UnsupportedOperationException(
|
||||||
Token temp = current();
|
"Unknown keyword " + identifier.getContent() + ": " + identifier.getPosition());
|
||||||
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() {
|
private WhileKeyword parseWhileLoop(Tokenizer tokens, Position start, ScopeBuilder scopeBuilder) {
|
||||||
SourcePosition position = consume("Expected 'fun' keyword at start of function declaration", TokenType.FUNCTION).position();
|
Returnable<?> first = parseExpression(tokens, true, scopeBuilder);
|
||||||
String id = consume("Expected identifier after 'fun' keyword for function declaration", TokenType.IDENTIFIER).lexeme();
|
ParserUtil.checkReturnType(first, Returnable.ReturnType.BOOLEAN);
|
||||||
consume("Expected '(' after function identifier '" + id + "'", TokenType.OPEN_PAREN);
|
|
||||||
|
|
||||||
// Parse parameters
|
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END);
|
||||||
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;
|
return new WhileKeyword(parseStatementBlock(tokens, true, scopeBuilder), (Returnable<Boolean>) first, start); // While loop
|
||||||
|
|
||||||
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() {
|
private IfKeyword parseIfStatement(Tokenizer tokens, Position start, boolean loop, ScopeBuilder scopeBuilder) {
|
||||||
SourcePosition position = consume("Expected 'var' keyword at start of variable declaration", TokenType.VARIABLE).position();
|
Returnable<?> condition = parseExpression(tokens, true, scopeBuilder);
|
||||||
String id = consume("Expected variable name after type for variable declaration", TokenType.IDENTIFIER).lexeme();
|
ParserUtil.checkReturnType(condition, Returnable.ReturnType.BOOLEAN);
|
||||||
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);
|
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END);
|
||||||
}
|
|
||||||
|
|
||||||
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
|
Block elseBlock = null;
|
||||||
Stmt.Block elseBody = null;
|
Block statement = parseStatementBlock(tokens, loop, scopeBuilder);
|
||||||
List<Pair<Expr, Stmt.Block>> elseIfClauses = new ArrayList<>();
|
|
||||||
while(current().isType(TokenType.ELSE)) {
|
List<Pair<Returnable<Boolean>, Block>> elseIf = new ArrayList<>();
|
||||||
consumeUnchecked(); // Consume else
|
|
||||||
|
while(tokens.hasNext() && tokens.get().getType().equals(Token.Type.ELSE)) {
|
||||||
if(!current().isType(TokenType.IF_STATEMENT)) {
|
tokens.consume(); // Consume else.
|
||||||
elseBody = blockOrSingleStatement();
|
if(tokens.get().getType().equals(Token.Type.IF_STATEMENT)) {
|
||||||
break; // Else clause should be last in if statement
|
tokens.consume(); // Consume if.
|
||||||
|
Returnable<?> elseCondition = parseExpression(tokens, true, scopeBuilder);
|
||||||
|
ParserUtil.checkReturnType(elseCondition, Returnable.ReturnType.BOOLEAN);
|
||||||
|
elseIf.add(Pair.of((Returnable<Boolean>) elseCondition, parseStatementBlock(tokens, loop, scopeBuilder)));
|
||||||
|
} else {
|
||||||
|
elseBlock = parseStatementBlock(tokens, loop, scopeBuilder);
|
||||||
|
break; // Else must be last.
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
return new IfKeyword(statement, (Returnable<Boolean>) condition, elseIf, elseBlock, start); // If statement
|
||||||
}
|
}
|
||||||
|
|
||||||
private Stmt.For forLoop() {
|
private Block parseStatementBlock(Tokenizer tokens, boolean loop, ScopeBuilder scopeBuilder) {
|
||||||
SourcePosition position = consume("Expected 'for' keyword at beginning of for loop", TokenType.FOR_LOOP).position();
|
|
||||||
consume("Expected '(' after 'for' keyword", TokenType.OPEN_PAREN);
|
if(tokens.get().getType().equals(Token.Type.BLOCK_BEGIN)) {
|
||||||
Stmt initializer = statement();
|
ParserUtil.checkType(tokens.consume(), Token.Type.BLOCK_BEGIN);
|
||||||
Expr condition;
|
Block block = parseBlock(tokens, loop, scopeBuilder);
|
||||||
if(current().isType(TokenType.STATEMENT_END)) {
|
ParserUtil.checkType(tokens.consume(), Token.Type.BLOCK_END);
|
||||||
condition = new Expr.Literal(true, Type.BOOLEAN,
|
return block;
|
||||||
current().position()); // If no condition is provided, set condition = true
|
|
||||||
consumeUnchecked();
|
|
||||||
} else {
|
} else {
|
||||||
condition = expression();
|
Position position = tokens.get().getPosition();
|
||||||
consumeStatementEnd("loop condition");
|
Block block = new Block(Collections.singletonList(parseItem(tokens, loop, scopeBuilder)), position);
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.STATEMENT_END);
|
||||||
|
return block;
|
||||||
}
|
}
|
||||||
Expr incrementer;
|
}
|
||||||
if(current().isType(TokenType.CLOSE_PAREN)) {
|
|
||||||
incrementer = null;
|
private ForKeyword parseForLoop(Tokenizer tokens, Position start, ScopeBuilder scopeBuilder) {
|
||||||
consumeUnchecked();
|
scopeBuilder = scopeBuilder.sub(); // new scope
|
||||||
|
Token f = tokens.get();
|
||||||
|
ParserUtil.checkType(f, Token.Type.NUMBER_VARIABLE, Token.Type.STRING_VARIABLE, Token.Type.BOOLEAN_VARIABLE, Token.Type.IDENTIFIER);
|
||||||
|
Item<?> initializer;
|
||||||
|
if(f.isVariableDeclaration()) {
|
||||||
|
VariableAssignmentNode<?> forVar = parseVariableDeclaration(tokens, scopeBuilder);
|
||||||
|
Token name = tokens.get();
|
||||||
|
if(functions.containsKey(name.getContent()) || scopeBuilder.contains(name.getContent()))
|
||||||
|
throw new ParseException(name.getContent() + " is already defined in this scope", name.getPosition());
|
||||||
|
initializer = forVar;
|
||||||
|
} else initializer = parseExpression(tokens, true, scopeBuilder);
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.STATEMENT_END);
|
||||||
|
Returnable<?> conditional = parseExpression(tokens, true, scopeBuilder);
|
||||||
|
ParserUtil.checkReturnType(conditional, Returnable.ReturnType.BOOLEAN);
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.STATEMENT_END);
|
||||||
|
|
||||||
|
Item<?> incrementer;
|
||||||
|
Token token = tokens.get();
|
||||||
|
if(scopeBuilder.contains(token.getContent())) { // Assume variable assignment
|
||||||
|
incrementer = parseAssignment(tokens, scopeBuilder);
|
||||||
|
} else incrementer = parseFunction(tokens, true, scopeBuilder);
|
||||||
|
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END);
|
||||||
|
|
||||||
|
return new ForKeyword(parseStatementBlock(tokens, true, scopeBuilder), initializer, (Returnable<Boolean>) conditional, incrementer,
|
||||||
|
start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Returnable<?> parseExpression(Tokenizer tokens, boolean full, ScopeBuilder scopeBuilder) {
|
||||||
|
boolean booleanInverted = false; // Check for boolean not operator
|
||||||
|
boolean negate = false;
|
||||||
|
if(tokens.get().getType().equals(Token.Type.BOOLEAN_NOT)) {
|
||||||
|
booleanInverted = true;
|
||||||
|
tokens.consume();
|
||||||
|
} else if(tokens.get().getType().equals(Token.Type.SUBTRACTION_OPERATOR)) {
|
||||||
|
negate = true;
|
||||||
|
tokens.consume();
|
||||||
|
}
|
||||||
|
|
||||||
|
Token id = tokens.get();
|
||||||
|
|
||||||
|
ParserUtil.checkType(id, Token.Type.IDENTIFIER, Token.Type.BOOLEAN, Token.Type.STRING, Token.Type.NUMBER, Token.Type.GROUP_BEGIN);
|
||||||
|
|
||||||
|
Returnable<?> expression;
|
||||||
|
if(id.isConstant()) {
|
||||||
|
expression = parseConstantExpression(tokens);
|
||||||
|
} else if(id.getType().equals(Token.Type.GROUP_BEGIN)) { // Parse grouped expression
|
||||||
|
expression = parseGroup(tokens, scopeBuilder);
|
||||||
} else {
|
} else {
|
||||||
incrementer = expression();
|
if(functions.containsKey(id.getContent()))
|
||||||
consume("Expected ')' after for loop incrementer", TokenType.CLOSE_PAREN);
|
expression = parseFunction(tokens, false, scopeBuilder);
|
||||||
|
else if(scopeBuilder.contains(id.getContent())) {
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.IDENTIFIER);
|
||||||
|
String varId = id.getContent();
|
||||||
|
ReturnType varType = scopeBuilder.getType(varId);
|
||||||
|
expression = switch(varType) {
|
||||||
|
case NUMBER -> new NumVariableReferenceNode(id.getPosition(), varType, scopeBuilder.getIndex(varId));
|
||||||
|
case STRING -> new StrVariableReferenceNode(id.getPosition(), varType, scopeBuilder.getIndex(varId));
|
||||||
|
case BOOLEAN -> new BoolVariableReferenceNode(id.getPosition(), varType, scopeBuilder.getIndex(varId));
|
||||||
|
default -> throw new ParseException("Illegal type for variable reference: " + varType, id.getPosition());
|
||||||
|
};
|
||||||
|
|
||||||
|
} else throw new ParseException("Unexpected token \" " + id.getContent() + "\"", id.getPosition());
|
||||||
}
|
}
|
||||||
Stmt.Block body = blockOrSingleStatement();
|
|
||||||
return new Stmt.For(initializer, condition, incrementer, body, position);
|
if(booleanInverted) { // Invert operation if boolean not detected
|
||||||
}
|
ParserUtil.checkReturnType(expression, Returnable.ReturnType.BOOLEAN);
|
||||||
|
expression = new BooleanNotOperation((Returnable<Boolean>) expression, expression.getPosition());
|
||||||
private Stmt.While whileLoop() {
|
} else if(negate) {
|
||||||
SourcePosition position = consume("Expected 'for' keyword at beginning of while loop", TokenType.WHILE_LOOP).position();
|
ParserUtil.checkReturnType(expression, Returnable.ReturnType.NUMBER);
|
||||||
consume("Expected '(' after 'while' keyword", TokenType.OPEN_PAREN);
|
expression = new NegationOperation((Returnable<Number>) expression, expression.getPosition());
|
||||||
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);
|
if(full && tokens.get().isBinaryOperator()) { // Parse binary operations
|
||||||
|
return parseBinaryOperation(expression, tokens, scopeBuilder);
|
||||||
|
}
|
||||||
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Stmt expressionStatement() {
|
private ConstantExpression<?> parseConstantExpression(Tokenizer tokens) {
|
||||||
Expr expression = expression();
|
Token constantToken = tokens.consume();
|
||||||
consumeStatementEnd("expression statement");
|
Position position = constantToken.getPosition();
|
||||||
return new Stmt.Expression(expression, expression.position);
|
switch(constantToken.getType()) {
|
||||||
|
case NUMBER:
|
||||||
|
String content = constantToken.getContent();
|
||||||
|
return new NumericConstant(content.contains(".") ? Double.parseDouble(content) : Integer.parseInt(content), position);
|
||||||
|
case STRING:
|
||||||
|
return new StringConstant(constantToken.getContent(), position);
|
||||||
|
case BOOLEAN:
|
||||||
|
return new BooleanConstant(Boolean.parseBoolean(constantToken.getContent()), position);
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"Unsupported constant token: " + constantToken.getType() + " at position: " + position);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expr expression() {
|
private Returnable<?> parseGroup(Tokenizer tokens, ScopeBuilder scopeBuilder) {
|
||||||
return assignment();
|
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_BEGIN);
|
||||||
|
Returnable<?> expression = parseExpression(tokens, true, scopeBuilder); // Parse inside of group as a separate expression
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END);
|
||||||
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expr leftAssociativeBinaryExpression(Supplier<Expr> higherPrecedence, BinaryOperator... operators) {
|
private BinaryOperation<?, ?> parseBinaryOperation(Returnable<?> left, Tokenizer tokens,
|
||||||
Expr expr = higherPrecedence.get();
|
ScopeBuilder scopeBuilder) {
|
||||||
loop:
|
Token binaryOperator = tokens.consume();
|
||||||
while(true) {
|
ParserUtil.checkBinaryOperator(binaryOperator);
|
||||||
for(BinaryOperator operator : operators) {
|
|
||||||
if(current().isType(operator.tokenType)) {
|
Returnable<?> right = parseExpression(tokens, false, scopeBuilder);
|
||||||
SourcePosition position = consumeUnchecked().position(); // Consume operator token
|
|
||||||
expr = new Expr.Binary(expr, operator, higherPrecedence.get(), position);
|
Token other = tokens.get();
|
||||||
continue loop;
|
if(ParserUtil.hasPrecedence(binaryOperator.getType(), other.getType())) {
|
||||||
|
return assemble(left, parseBinaryOperation(right, tokens, scopeBuilder), binaryOperator);
|
||||||
|
} else if(other.isBinaryOperator()) {
|
||||||
|
return parseBinaryOperation(assemble(left, right, binaryOperator), tokens, scopeBuilder);
|
||||||
|
}
|
||||||
|
return assemble(left, right, binaryOperator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BinaryOperation<?, ?> assemble(Returnable<?> left, Returnable<?> right, Token binaryOperator) {
|
||||||
|
if(binaryOperator.isStrictNumericOperator())
|
||||||
|
ParserUtil.checkArithmeticOperation(left, right, binaryOperator); // Numeric type checking
|
||||||
|
if(binaryOperator.isStrictBooleanOperator()) ParserUtil.checkBooleanOperation(left, right, binaryOperator); // Boolean type checking
|
||||||
|
switch(binaryOperator.getType()) {
|
||||||
|
case ADDITION_OPERATOR:
|
||||||
|
if(left.returnType().equals(Returnable.ReturnType.NUMBER) && right.returnType().equals(Returnable.ReturnType.NUMBER)) {
|
||||||
|
return new NumberAdditionOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
||||||
}
|
}
|
||||||
}
|
return new ConcatenationOperation((Returnable<Object>) left, (Returnable<Object>) right, binaryOperator.getPosition());
|
||||||
break; // Break if not any operator
|
case SUBTRACTION_OPERATOR:
|
||||||
|
return new SubtractionOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
||||||
|
case MULTIPLICATION_OPERATOR:
|
||||||
|
return new MultiplicationOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
||||||
|
case DIVISION_OPERATOR:
|
||||||
|
return new DivisionOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
||||||
|
case EQUALS_OPERATOR:
|
||||||
|
return new EqualsStatement((Returnable<Object>) left, (Returnable<Object>) right, binaryOperator.getPosition());
|
||||||
|
case NOT_EQUALS_OPERATOR:
|
||||||
|
return new NotEqualsStatement((Returnable<Object>) left, (Returnable<Object>) right, binaryOperator.getPosition());
|
||||||
|
case GREATER_THAN_OPERATOR:
|
||||||
|
return new GreaterThanStatement((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
||||||
|
case LESS_THAN_OPERATOR:
|
||||||
|
return new LessThanStatement((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
||||||
|
case GREATER_THAN_OR_EQUALS_OPERATOR:
|
||||||
|
return new GreaterOrEqualsThanStatement((Returnable<Number>) left, (Returnable<Number>) right,
|
||||||
|
binaryOperator.getPosition());
|
||||||
|
case LESS_THAN_OR_EQUALS_OPERATOR:
|
||||||
|
return new LessThanOrEqualsStatement((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
||||||
|
case BOOLEAN_AND:
|
||||||
|
return new BooleanAndOperation((Returnable<Boolean>) left, (Returnable<Boolean>) right, binaryOperator.getPosition());
|
||||||
|
case BOOLEAN_OR:
|
||||||
|
return new BooleanOrOperation((Returnable<Boolean>) left, (Returnable<Boolean>) right, binaryOperator.getPosition());
|
||||||
|
case MODULO_OPERATOR:
|
||||||
|
return new ModuloOperation((Returnable<Number>) left, (Returnable<Number>) right, binaryOperator.getPosition());
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException("Unsupported binary operator: " + binaryOperator.getType());
|
||||||
}
|
}
|
||||||
return expr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expr rightAssociativeBinaryExpression(Supplier<Expr> higherPrecedence, BinaryOperator... operators) {
|
private VariableAssignmentNode<?> parseVariableDeclaration(Tokenizer tokens, ScopeBuilder scopeBuilder) {
|
||||||
Expr expr = higherPrecedence.get();
|
Token type = tokens.consume();
|
||||||
for(BinaryOperator operator : operators) {
|
ParserUtil.checkType(type, Token.Type.STRING_VARIABLE, Token.Type.BOOLEAN_VARIABLE, Token.Type.NUMBER_VARIABLE);
|
||||||
if(current().isType(operator.tokenType)) {
|
|
||||||
SourcePosition position = consumeUnchecked().position(); // Consume operator token
|
Returnable.ReturnType returnType = ParserUtil.getVariableReturnType(type);
|
||||||
return new Expr.Binary(expr, operator, rightAssociativeBinaryExpression(higherPrecedence, operators), position);
|
|
||||||
}
|
ParserUtil.checkVarType(type, returnType); // Check for type mismatch
|
||||||
}
|
Token identifier = tokens.consume();
|
||||||
return expr;
|
ParserUtil.checkType(identifier, Token.Type.IDENTIFIER);
|
||||||
}
|
if(functions.containsKey(identifier.getContent()) || scopeBuilder.contains(identifier.getContent()))
|
||||||
|
throw new ParseException(identifier.getContent() + " is already defined in this scope", identifier.getPosition());
|
||||||
private Expr assignment() {
|
ParserUtil.checkType(tokens.consume(), Token.Type.ASSIGNMENT);
|
||||||
Expr expr = logicOr();
|
|
||||||
if(current().isType(TokenType.ASSIGNMENT)) {
|
Returnable<?> value = parseExpression(tokens, true, scopeBuilder);
|
||||||
SourcePosition position = consumeUnchecked().position(); // Consume operator token
|
ParserUtil.checkReturnType(value, returnType);
|
||||||
if(!(expr instanceof Variable variable)) throw new ParseException("Invalid assignment target", position);
|
|
||||||
return new Expr.Assignment(variable, assignment(), position);
|
String id = identifier.getContent();
|
||||||
}
|
|
||||||
return expr;
|
return switch(value.returnType()) {
|
||||||
}
|
case NUMBER -> new NumAssignmentNode((Returnable<Number>) value, identifier.getPosition(), scopeBuilder.num(id));
|
||||||
|
case STRING -> new StrAssignmentNode((Returnable<String>) value, identifier.getPosition(), scopeBuilder.str(id));
|
||||||
private Expr logicOr() {
|
case BOOLEAN -> new BoolAssignmentNode((Returnable<Boolean>) value, identifier.getPosition(), scopeBuilder.bool(id));
|
||||||
return leftAssociativeBinaryExpression(this::logicAnd, BinaryOperator.BOOLEAN_OR);
|
default -> throw new ParseException("Illegal type for variable declaration: " + type, value.getPosition());
|
||||||
}
|
|
||||||
|
|
||||||
private Expr logicAnd() {
|
|
||||||
return leftAssociativeBinaryExpression(this::equality, BinaryOperator.BOOLEAN_AND);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expr equality() {
|
|
||||||
return leftAssociativeBinaryExpression(this::comparison, BinaryOperator.EQUALS, BinaryOperator.NOT_EQUALS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expr comparison() {
|
|
||||||
return leftAssociativeBinaryExpression(this::term, BinaryOperator.GREATER, BinaryOperator.GREATER_EQUALS, BinaryOperator.LESS,
|
|
||||||
BinaryOperator.LESS_EQUALS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expr term() {
|
|
||||||
return leftAssociativeBinaryExpression(this::factor, BinaryOperator.ADD, BinaryOperator.SUBTRACT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expr factor() {
|
|
||||||
return leftAssociativeBinaryExpression(this::unary, BinaryOperator.MULTIPLY, BinaryOperator.DIVIDE, BinaryOperator.MODULO);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expr unary() {
|
|
||||||
UnaryOperator[] operators = { UnaryOperator.NOT, UnaryOperator.NEGATE };
|
|
||||||
for(UnaryOperator operator : operators) {
|
|
||||||
if(current().isType(operator.tokenType)) {
|
|
||||||
SourcePosition position = consumeUnchecked().position();
|
|
||||||
return new Expr.Unary(operator, unary(), position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return primary();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expr primary() {
|
|
||||||
Token token = consumeUnchecked();
|
|
||||||
SourcePosition position = token.position();
|
|
||||||
return switch(token.type()) {
|
|
||||||
case NUMBER -> new Expr.Literal(Double.parseDouble(token.lexeme()), Type.NUMBER, position);
|
|
||||||
case STRING -> new Expr.Literal(token.lexeme(), Type.STRING, position);
|
|
||||||
case BOOLEAN -> new Expr.Literal(Boolean.parseBoolean(token.lexeme()), Type.BOOLEAN, position);
|
|
||||||
case IDENTIFIER -> {
|
|
||||||
if(current().isType(TokenType.OPEN_PAREN)) yield call(token);
|
|
||||||
else yield variable(token);
|
|
||||||
}
|
|
||||||
case OPEN_PAREN -> {
|
|
||||||
if(current().isType(TokenType.CLOSE_PAREN)) {
|
|
||||||
consumeUnchecked(); // Consume ')'
|
|
||||||
yield new Expr.Void(position); // () evaluates to void
|
|
||||||
}
|
|
||||||
Expr expr = expression();
|
|
||||||
consume("Expected ')' to close '(' located at " + position, TokenType.CLOSE_PAREN);
|
|
||||||
yield new Expr.Grouping(expr, position);
|
|
||||||
}
|
|
||||||
default -> throw new ParseException("Unexpected token '" + token.lexeme() + "'", position);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expr call(Token identifier) {
|
private Block parseBlock(Tokenizer tokens, boolean loop, ScopeBuilder scopeBuilder) {
|
||||||
String id = identifier.lexeme();
|
List<Item<?>> parsedItems = new ArrayList<>();
|
||||||
SourcePosition position = consume("Expected '(' to initiate function call on function '" + id + "'",
|
|
||||||
TokenType.OPEN_PAREN).position();
|
|
||||||
|
|
||||||
List<Expr> args = new ArrayList<>();
|
scopeBuilder = scopeBuilder.sub();
|
||||||
while(!current().isType(TokenType.CLOSE_PAREN)) {
|
|
||||||
args.add(expression());
|
Token first = tokens.get();
|
||||||
if(current().isType(TokenType.CLOSE_PAREN)) break;
|
|
||||||
consume("Expected ',' or ')' after passed argument in function call of '" + id + "'", TokenType.SEPARATOR);
|
while(tokens.hasNext()) {
|
||||||
|
Token token = tokens.get();
|
||||||
|
if(token.getType().equals(Token.Type.BLOCK_END)) break; // Stop parsing at block end.
|
||||||
|
Item<?> parsedItem = parseItem(tokens, loop, scopeBuilder);
|
||||||
|
if(parsedItem != Function.NULL) {
|
||||||
|
parsedItems.add(parsedItem);
|
||||||
|
}
|
||||||
|
if(tokens.hasNext() && !token.isLoopLike()) ParserUtil.checkType(tokens.consume(), Token.Type.STATEMENT_END);
|
||||||
|
}
|
||||||
|
return new Block(parsedItems, first.getPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Item<?> parseItem(Tokenizer tokens, boolean loop, ScopeBuilder scopeBuilder) {
|
||||||
|
Token token = tokens.get();
|
||||||
|
if(loop) ParserUtil.checkType(token, Token.Type.IDENTIFIER, Token.Type.IF_STATEMENT, Token.Type.WHILE_LOOP, Token.Type.FOR_LOOP,
|
||||||
|
Token.Type.NUMBER_VARIABLE, Token.Type.STRING_VARIABLE, Token.Type.BOOLEAN_VARIABLE,
|
||||||
|
Token.Type.RETURN, Token.Type.BREAK, Token.Type.CONTINUE, Token.Type.FAIL);
|
||||||
|
else ParserUtil.checkType(token, Token.Type.IDENTIFIER, Token.Type.IF_STATEMENT, Token.Type.WHILE_LOOP, Token.Type.FOR_LOOP,
|
||||||
|
Token.Type.NUMBER_VARIABLE, Token.Type.STRING_VARIABLE, Token.Type.BOOLEAN_VARIABLE, Token.Type.RETURN,
|
||||||
|
Token.Type.FAIL);
|
||||||
|
|
||||||
|
if(token.isLoopLike()) { // Parse loop-like tokens (if, while, etc)
|
||||||
|
return parseLoopLike(tokens, loop, scopeBuilder);
|
||||||
|
} else if(token.isIdentifier()) { // Parse identifiers
|
||||||
|
if(scopeBuilder.contains(token.getContent())) { // Assume variable assignment
|
||||||
|
return parseAssignment(tokens, scopeBuilder);
|
||||||
|
} else return parseFunction(tokens, true, scopeBuilder);
|
||||||
|
} else if(token.isVariableDeclaration()) {
|
||||||
|
|
||||||
|
return parseVariableDeclaration(tokens, scopeBuilder);
|
||||||
|
|
||||||
|
} else if(token.getType().equals(Token.Type.RETURN)) return new ReturnKeyword(tokens.consume().getPosition());
|
||||||
|
else if(token.getType().equals(Token.Type.BREAK)) return new BreakKeyword(tokens.consume().getPosition());
|
||||||
|
else if(token.getType().equals(Token.Type.CONTINUE)) return new ContinueKeyword(tokens.consume().getPosition());
|
||||||
|
else if(token.getType().equals(Token.Type.FAIL)) return new FailKeyword(tokens.consume().getPosition());
|
||||||
|
else throw new UnsupportedOperationException("Unexpected token " + token.getType() + ": " + token.getPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
private VariableAssignmentNode<?> parseAssignment(Tokenizer tokens, ScopeBuilder scopeBuilder) {
|
||||||
|
Token identifier = tokens.consume();
|
||||||
|
|
||||||
|
ParserUtil.checkType(identifier, Token.Type.IDENTIFIER);
|
||||||
|
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.ASSIGNMENT);
|
||||||
|
|
||||||
|
Returnable<?> value = parseExpression(tokens, true, scopeBuilder);
|
||||||
|
|
||||||
|
String id = identifier.getContent();
|
||||||
|
|
||||||
|
ParserUtil.checkReturnType(value, scopeBuilder.getType(id));
|
||||||
|
|
||||||
|
ReturnType type = value.returnType();
|
||||||
|
|
||||||
|
return switch(type) {
|
||||||
|
case NUMBER -> new NumAssignmentNode((Returnable<Number>) value, identifier.getPosition(), scopeBuilder.getIndex(id));
|
||||||
|
case STRING -> new StrAssignmentNode((Returnable<String>) value, identifier.getPosition(), scopeBuilder.getIndex(id));
|
||||||
|
case BOOLEAN -> new BoolAssignmentNode((Returnable<Boolean>) value, identifier.getPosition(), scopeBuilder.getIndex(id));
|
||||||
|
default -> throw new ParseException("Illegal type for variable assignment: " + type, value.getPosition());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<?> parseFunction(Tokenizer tokens, boolean fullStatement, ScopeBuilder scopeBuilder) {
|
||||||
|
Token identifier = tokens.consume();
|
||||||
|
ParserUtil.checkType(identifier, Token.Type.IDENTIFIER); // First token must be identifier
|
||||||
|
|
||||||
|
if(!functions.containsKey(identifier.getContent()))
|
||||||
|
throw new ParseException("No such function \"" + identifier.getContent() + "\"", identifier.getPosition());
|
||||||
|
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_BEGIN); // Second is body begin
|
||||||
|
|
||||||
|
|
||||||
|
List<Returnable<?>> args = getArgs(tokens, scopeBuilder); // Extract arguments, consume the rest.
|
||||||
|
|
||||||
|
ParserUtil.checkType(tokens.consume(), Token.Type.GROUP_END); // Remove body end
|
||||||
|
|
||||||
|
if(fullStatement) ParserUtil.checkType(tokens.get(), Token.Type.STATEMENT_END);
|
||||||
|
|
||||||
|
if(ignoredFunctions.contains(identifier.getContent())) {
|
||||||
|
return Function.NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
consume("Expected ')' after " + (args.size() == 0 ? "')'" : "arguments") + " in function call of '" + id + "'",
|
if(functions.containsKey(identifier.getContent())) {
|
||||||
TokenType.CLOSE_PAREN);
|
FunctionBuilder<?> builder = functions.get(identifier.getContent());
|
||||||
|
|
||||||
|
if(builder.argNumber() != -1 && args.size() != builder.argNumber())
|
||||||
|
throw new ParseException("Expected " + builder.argNumber() + " arguments, found " + args.size(), identifier.getPosition());
|
||||||
|
|
||||||
|
for(int i = 0; i < args.size(); i++) {
|
||||||
|
Returnable<?> argument = args.get(i);
|
||||||
|
if(builder.getArgument(i) == null)
|
||||||
|
throw new ParseException("Unexpected argument at position " + i + " in function " + identifier.getContent(),
|
||||||
|
identifier.getPosition());
|
||||||
|
ParserUtil.checkReturnType(argument, builder.getArgument(i));
|
||||||
|
}
|
||||||
|
return builder.build(args, identifier.getPosition());
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException("Unsupported function: " + identifier.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Returnable<?>> getArgs(Tokenizer tokens, ScopeBuilder scopeBuilder) {
|
||||||
|
List<Returnable<?>> args = new ArrayList<>();
|
||||||
|
|
||||||
return new Expr.Call(id, args, position);
|
while(!tokens.get().getType().equals(Token.Type.GROUP_END)) {
|
||||||
|
args.add(parseExpression(tokens, true, scopeBuilder));
|
||||||
|
ParserUtil.checkType(tokens.get(), Token.Type.SEPARATOR, Token.Type.GROUP_END);
|
||||||
|
if(tokens.get().getType().equals(Token.Type.SEPARATOR)) tokens.consume();
|
||||||
|
}
|
||||||
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expr variable(Token identifier) {
|
|
||||||
return new Expr.Variable(identifier.lexeme(), identifier.position());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
File renamed without changes.
-13
@@ -1,13 +0,0 @@
|
|||||||
package com.dfsek.terra.addons.terrascript.parser;
|
|
||||||
|
|
||||||
import com.dfsek.terra.addons.terrascript.lexer.Token.TokenType;
|
|
||||||
|
|
||||||
|
|
||||||
public enum UnaryOperator {
|
|
||||||
NOT(TokenType.BANG),
|
|
||||||
NEGATE(TokenType.MINUS);
|
|
||||||
|
|
||||||
public final TokenType tokenType;
|
|
||||||
|
|
||||||
UnaryOperator(TokenType tokenType) { this.tokenType = tokenType; }
|
|
||||||
}
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user