Better error handling + other changes

This commit is contained in:
Astrash
2023-07-29 20:29:16 +10:00
parent 13300861ee
commit 772675639e
11 changed files with 363 additions and 308 deletions

View File

@@ -7,16 +7,17 @@
package com.dfsek.terra.addons.terrascript.lexer;
import java.util.Objects;
public class Char {
private final char character;
private final int index;
private final int line;
private final SourcePosition position;
public Char(char character, int index, int line) {
public Char(char character, SourcePosition position) {
this.character = character;
this.index = index;
this.line = line;
this.position = position;
}
public boolean is(char... tests) {
@@ -33,18 +34,23 @@ public class Char {
return Character.toString(character);
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Char other = (Char) o;
return character == other.character && Objects.equals(position, other.position);
}
@Override
public int hashCode() {
return Objects.hash(character, position);
}
public char getCharacter() {
return character;
}
public int getIndex() {
return index;
}
public int getLine() {
return line;
}
public boolean isWhitespace() {
return Character.isWhitespace(character);
}

View File

@@ -9,7 +9,7 @@ package com.dfsek.terra.addons.terrascript.lexer;
import com.google.common.collect.Sets;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Set;
import java.util.Stack;
@@ -28,7 +28,7 @@ public class Lexer {
private Token current;
public Lexer(String data) {
reader = new LookaheadStream(new StringReader(data + '\0'));
reader = new LookaheadStream(data + '\0');
current = tokenize();
}
@@ -50,7 +50,12 @@ public class Lexer {
*
* @throws ParseException If token does not exist
*/
public Token consume() {
public Token consume(String wrongTypeMessage, TokenType expected, TokenType... more) {
if(!current.isType(expected) && Arrays.stream(more).noneMatch(t -> t == current.getType())) throw new ParseException(wrongTypeMessage, current.getPosition());
return consumeUnchecked();
}
public Token consumeUnchecked() {
if(current.getType() == TokenType.END_OF_FILE) return current;
Token temp = current;
current = tokenize();
@@ -68,6 +73,7 @@ public class Lexer {
private Token tokenize() throws TokenizerException {
consumeWhitespace();
SourcePosition position = reader.getPosition();
// Skip line if comment
while(reader.matchesString("//", true)) skipLine();
@@ -77,37 +83,37 @@ public class Lexer {
// Reached end of file
if(reader.current().isEOF()) {
if(!bracketStack.isEmpty()) throw new ParseException("Dangling closing brace", bracketStack.peek().getPosition());
return new Token(reader.consume().toString(), TokenType.END_OF_FILE, reader.getPosition());
if(!bracketStack.isEmpty()) throw new ParseException("Dangling open brace", bracketStack.peek().getPosition());
return new Token(reader.consume().toString(), TokenType.END_OF_FILE, position);
}
// Check if operator token
if(reader.matchesString("==", true))
return new Token("==", TokenType.EQUALS_EQUALS, reader.getPosition());
return new Token("==", TokenType.EQUALS_EQUALS, position);
if(reader.matchesString("!=", true))
return new Token("!=", TokenType.BANG_EQUALS, reader.getPosition());
return new Token("!=", TokenType.BANG_EQUALS, position);
if(reader.matchesString(">=", true))
return new Token(">=", TokenType.GREATER_EQUAL, reader.getPosition());
return new Token(">=", TokenType.GREATER_EQUAL, position);
if(reader.matchesString("<=", true))
return new Token("<=", TokenType.LESS_EQUALS, reader.getPosition());
return new Token("<=", TokenType.LESS_EQUALS, position);
if(reader.matchesString(">", true))
return new Token(">", TokenType.GREATER, reader.getPosition());
return new Token(">", TokenType.GREATER, position);
if(reader.matchesString("<", true))
return new Token("<", TokenType.LESS, reader.getPosition());
return new Token("<", TokenType.LESS, position);
// Check if logical operator
if(reader.matchesString("||", true))
return new Token("||", TokenType.BOOLEAN_OR, reader.getPosition());
return new Token("||", TokenType.BOOLEAN_OR, position);
if(reader.matchesString("&&", true))
return new Token("&&", TokenType.BOOLEAN_AND, reader.getPosition());
return new Token("&&", TokenType.BOOLEAN_AND, position);
// Check if number
if(isNumberStart()) {
StringBuilder num = new StringBuilder();
while(!reader.current().isEOF() && isNumberLike()) {
num.append(reader.consume());
num.append(reader.consume().getCharacter());
}
return new Token(num.toString(), TokenType.NUMBER, reader.getPosition());
return new Token(num.toString(), TokenType.NUMBER, position);
}
// Check if string literal
@@ -122,95 +128,95 @@ public class Lexer {
continue;
} else ignoreNext = false;
if(reader.current().isEOF())
throw new FormatException("No end of string literal found. ", reader.getPosition());
throw new FormatException("No end of string literal found. ", position);
string.append(reader.consume());
}
reader.consume(); // Consume last quote
return new Token(string.toString(), TokenType.STRING, reader.getPosition());
return new Token(string.toString(), TokenType.STRING, position);
}
if(reader.current().is('('))
return new Token(reader.consume().toString(), TokenType.OPEN_PAREN, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.OPEN_PAREN, position);
if(reader.current().is(')'))
return new Token(reader.consume().toString(), TokenType.CLOSE_PAREN, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.CLOSE_PAREN, position);
if(reader.current().is(';'))
return new Token(reader.consume().toString(), TokenType.STATEMENT_END, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.STATEMENT_END, position);
if(reader.current().is(','))
return new Token(reader.consume().toString(), TokenType.SEPARATOR, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.SEPARATOR, position);
if(reader.current().is('{')) {
Token token = new Token(reader.consume().toString(), TokenType.BLOCK_BEGIN, reader.getPosition());
Token token = new Token(reader.consume().toString(), TokenType.BLOCK_BEGIN, position);
bracketStack.push(token);
return token;
}
if(reader.current().is('}')) {
if(bracketStack.isEmpty()) throw new ParseException("Dangling opening brace", new SourcePosition(0, 0));
if(bracketStack.isEmpty()) throw new ParseException("Dangling close brace", position);
bracketStack.pop();
return new Token(reader.consume().toString(), TokenType.BLOCK_END, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.BLOCK_END, position);
}
if(reader.current().is('='))
return new Token(reader.consume().toString(), TokenType.ASSIGNMENT, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.ASSIGNMENT, position);
if(reader.current().is('+'))
return new Token(reader.consume().toString(), TokenType.PLUS, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.PLUS, position);
if(reader.current().is('-'))
return new Token(reader.consume().toString(), TokenType.MINUS,
reader.getPosition());
position);
if(reader.current().is('*'))
return new Token(reader.consume().toString(), TokenType.STAR,
reader.getPosition());
position);
if(reader.current().is('/'))
return new Token(reader.consume().toString(), TokenType.FORWARD_SLASH, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.FORWARD_SLASH, position);
if(reader.current().is('%'))
return new Token(reader.consume().toString(), TokenType.MODULO_OPERATOR, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.MODULO_OPERATOR, position);
if(reader.current().is('!'))
return new Token(reader.consume().toString(), TokenType.BANG, reader.getPosition());
return new Token(reader.consume().toString(), TokenType.BANG, position);
// Read word
StringBuilder token = new StringBuilder();
while(!reader.current().isEOF() && !isSyntaxSignificant(reader.current().getCharacter())) {
Char c = reader.consume();
if(c.isWhitespace()) break;
token.append(c);
token.append(c.getCharacter());
}
String tokenString = token.toString();
// Check if word is a keyword
if(tokenString.equals("true"))
return new Token(tokenString, TokenType.BOOLEAN, reader.getPosition());
return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("false"))
return new Token(tokenString, TokenType.BOOLEAN, reader.getPosition());
return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("num"))
return new Token(tokenString, TokenType.TYPE_NUMBER, reader.getPosition());
return new Token(tokenString, TokenType.TYPE_NUMBER, position);
if(tokenString.equals("str"))
return new Token(tokenString, TokenType.TYPE_STRING, reader.getPosition());
return new Token(tokenString, TokenType.TYPE_STRING, position);
if(tokenString.equals("bool"))
return new Token(tokenString, TokenType.TYPE_BOOLEAN, reader.getPosition());
return new Token(tokenString, TokenType.TYPE_BOOLEAN, position);
if(tokenString.equals("void"))
return new Token(tokenString, TokenType.TYPE_VOID, reader.getPosition());
return new Token(tokenString, TokenType.TYPE_VOID, position);
if(tokenString.equals("if"))
return new Token(tokenString, TokenType.IF_STATEMENT, reader.getPosition());
return new Token(tokenString, TokenType.IF_STATEMENT, position);
if(tokenString.equals("else"))
return new Token(tokenString, TokenType.ELSE, reader.getPosition());
return new Token(tokenString, TokenType.ELSE, position);
if(tokenString.equals("while"))
return new Token(tokenString, TokenType.WHILE_LOOP, reader.getPosition());
return new Token(tokenString, TokenType.WHILE_LOOP, position);
if(tokenString.equals("for"))
return new Token(tokenString, TokenType.FOR_LOOP, reader.getPosition());
return new Token(tokenString, TokenType.FOR_LOOP, position);
if(tokenString.equals("return"))
return new Token(tokenString, TokenType.RETURN, reader.getPosition());
return new Token(tokenString, TokenType.RETURN, position);
if(tokenString.equals("continue"))
return new Token(tokenString, TokenType.CONTINUE, reader.getPosition());
return new Token(tokenString, TokenType.CONTINUE, position);
if(tokenString.equals("break"))
return new Token(tokenString, TokenType.BREAK, reader.getPosition());
return new Token(tokenString, TokenType.BREAK, position);
if(tokenString.equals("fail"))
return new Token(tokenString, TokenType.FAIL, reader.getPosition());
return new Token(tokenString, TokenType.FAIL, position);
// If not keyword, assume it is an identifier
return new Token(tokenString, TokenType.IDENTIFIER, reader.getPosition());
return new Token(tokenString, TokenType.IDENTIFIER, position);
}
private void skipLine() {
@@ -241,7 +247,7 @@ public class Lexer {
private boolean isNumberStart() {
return reader.current().isDigit()
|| reader.current().is('.') && reader.next(1).isDigit();
|| reader.current().is('.') && reader.peek().isDigit();
}
public boolean isSyntaxSignificant(char c) {

View File

@@ -1,31 +1,17 @@
/*
* 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.lexer;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
/**
* Stream-like data structure that allows viewing future elements without consuming current.
*/
public class LookaheadStream {
private final List<Char> buffer = new ArrayList<>();
private final Reader input;
private int index = 0;
private int line = 0;
private boolean end = false;
public LookaheadStream(Reader r) {
this.input = r;
private final String source;
private int index;
private SourcePosition position = new SourcePosition(1, 1);
public LookaheadStream(String source) {
this.source = source;
}
/**
@@ -34,10 +20,9 @@ public class LookaheadStream {
* @return current character
*/
public Char current() {
return next(0);
return new Char(source.charAt(index), position);
}
/**
* Consume and return one character.
*
@@ -45,82 +30,55 @@ public class LookaheadStream {
*/
public Char consume() {
Char consumed = current();
consume(1);
incrementIndex(1);
return consumed;
}
/**
* Fetch a future character without consuming it.
*
* @param ahead Distance ahead to peek
*
* @return Character
* @return The next character in sequence.
*/
public Char next(int ahead) {
if(ahead < 0) throw new IllegalArgumentException();
while(buffer.size() <= ahead && !end) {
Char item = fetch();
if(item != null) {
buffer.add(item);
} else end = true;
}
if(ahead >= buffer.size()) {
return null;
} else return buffer.get(ahead);
public Char peek() {
int index = this.index + 1;
if (index + 1 >= source.length()) return null;
return new Char(source.charAt(index), getPositionAfter(1));
}
/**
* Consume an amount of characters
* Determines if the contained sequence of characters matches the string
*
* @param amount Number of characters to consume
* @param check Input string to check against
* @param consumeIfMatched Whether to consume the string if there is a match
* @return If the string matches
*/
public void consume(int amount) {
if(amount < 0) throw new IllegalArgumentException();
while(amount-- > 0) {
if(!buffer.isEmpty()) buffer.remove(0); // Remove top item from buffer.
else {
if(end) return;
Char item = fetch();
if(item == null) end = true;
}
}
}
public boolean matchesString(String check, boolean consume) {
if(check == null) return false;
for(int i = 0; i < check.length(); i++) {
if(!next(i).is(check.charAt(i))) return false;
}
if(consume) consume(check.length()); // Consume string
return true;
public boolean matchesString(String check, boolean consumeIfMatched) {
boolean matches = check.equals(source.substring(index, Math.min(index + check.length(), source.length())));
if (matches && consumeIfMatched) incrementIndex(check.length());
return matches;
}
/**
* Fetch the next character.
*
* @return Next character
* @return Current position within the source file
*/
private Char fetch() {
try {
int c = input.read();
if(c == -1) return null;
if(c == '\n') {
line++;
index = 0;
}
index++;
return new Char((char) c, line, index);
} catch(IOException e) {
e.printStackTrace();
return null;
}
}
public SourcePosition getPosition() {
return new SourcePosition(line, index);
return position;
}
private void incrementIndex(int amount) {
position = getPositionAfter(amount);
index = Math.min(index + amount, source.length() - 1);
}
private SourcePosition getPositionAfter(int chars) {
if (chars < 0) throw new IllegalArgumentException("Negative values are not allowed");
int line = position.line();
int column = position.column();
for (int i = index; i < Math.min(index + chars, source.length() - 1); i++) {
if (source.charAt(i) == '\n') {
line++;
column = 0;
}
column++;
}
return new SourcePosition(line, column);
}
}

View File

@@ -7,17 +7,26 @@
package com.dfsek.terra.addons.terrascript.lexer;
public class SourcePosition {
private final int line;
private final int index;
public SourcePosition(int line, int index) {
this.line = line;
this.index = index;
}
import java.util.Objects;
public record SourcePosition(int line, int column) {
@Override
public String toString() {
return (line + 1) + ":" + index;
return "line " + line + ", column " + column;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
SourcePosition that = (SourcePosition) o;
return line == that.line && column == that.column;
}
@Override
public int hashCode() {
return Objects.hash(line, column);
}
}

View File

@@ -83,23 +83,23 @@ public class Parser {
}
private WhileKeyword parseWhileLoop(ScopeBuilder scopeBuilder) {
SourcePosition start = lexer.consume().getPosition();
ParserUtil.ensureType(lexer.consume(), TokenType.OPEN_PAREN);
SourcePosition start = lexer.consume("Expected 'while' keyword at beginning of while loop", TokenType.WHILE_LOOP).getPosition();
lexer.consume("Expected '(' proceeding 'while' keyword", TokenType.OPEN_PAREN);
scopeBuilder = scopeBuilder.innerLoopScope();
Expression<?> condition = parseExpression(scopeBuilder);
ParserUtil.ensureReturnType(condition, Expression.ReturnType.BOOLEAN);
ParserUtil.ensureType(lexer.consume(), TokenType.CLOSE_PAREN);
lexer.consume("Expected ')' proceeding while loop condition", TokenType.CLOSE_PAREN);
return new WhileKeyword(parseStatementBlock(scopeBuilder, ReturnType.VOID), (Expression<Boolean>) condition,
start); // While loop
}
private IfKeyword parseIfStatement(ScopeBuilder scopeBuilder) {
SourcePosition start = lexer.consume().getPosition();
ParserUtil.ensureType(lexer.consume(), TokenType.OPEN_PAREN);
SourcePosition start = lexer.consume("Expected 'if' keyword at beginning of if statement", TokenType.IF_STATEMENT).getPosition();
lexer.consume("Expected '(' proceeding 'if' keyword", TokenType.OPEN_PAREN);
Expression<?> condition = parseExpression(scopeBuilder);
ParserUtil.ensureReturnType(condition, Expression.ReturnType.BOOLEAN);
ParserUtil.ensureType(lexer.consume(), TokenType.CLOSE_PAREN);
lexer.consume("Expected ')' proceeding if statement condition", TokenType.CLOSE_PAREN);
Block elseBlock = null;
Block statement = parseStatementBlock(scopeBuilder, ReturnType.VOID);
@@ -107,9 +107,9 @@ public class Parser {
List<Pair<Expression<Boolean>, Block>> elseIf = new ArrayList<>();
while(lexer.hasNext() && lexer.current().isType(TokenType.ELSE)) {
lexer.consume(); // Consume else.
lexer.consumeUnchecked(); // Consume else.
if(lexer.current().isType(TokenType.IF_STATEMENT)) {
lexer.consume(); // Consume if.
lexer.consumeUnchecked(); // Consume if.
Expression<?> elseCondition = parseExpression(scopeBuilder);
ParserUtil.ensureReturnType(elseCondition, Expression.ReturnType.BOOLEAN);
elseIf.add(Pair.of((Expression<Boolean>) elseCondition, parseStatementBlock(scopeBuilder, ReturnType.VOID)));
@@ -124,42 +124,58 @@ public class Parser {
private Block parseStatementBlock(ScopeBuilder scopeBuilder, ReturnType blockReturnType) {
if(lexer.current().isType(TokenType.BLOCK_BEGIN)) {
ParserUtil.ensureType(lexer.consume(), TokenType.BLOCK_BEGIN);
lexer.consumeUnchecked();
Block block = parseBlock(scopeBuilder, blockReturnType);
ParserUtil.ensureType(lexer.consume(), TokenType.BLOCK_END);
lexer.consume("Expected block end '}' after block statements", TokenType.BLOCK_END);
return block;
} else {
SourcePosition position = lexer.current().getPosition();
return new Block(Collections.singletonList(parseStatement(lexer, scopeBuilder)), position, blockReturnType);
return new Block(Collections.singletonList(parseStatement(scopeBuilder)), position, blockReturnType);
}
}
private ForKeyword parseForLoop(ScopeBuilder scopeBuilder) {
SourcePosition start = lexer.consume().getPosition();
ParserUtil.ensureType(lexer.consume(), TokenType.OPEN_PAREN);
SourcePosition start = lexer.consume("Expected 'for' keyword at beginning of for loop", TokenType.FOR_LOOP).getPosition();
lexer.consume("Expected '(' after 'for' keyword", TokenType.OPEN_PAREN);
scopeBuilder = scopeBuilder.innerLoopScope(); // new scope
Token f = lexer.current();
ParserUtil.ensureType(f, TokenType.TYPE_NUMBER, TokenType.TYPE_STRING, TokenType.TYPE_BOOLEAN, TokenType.IDENTIFIER);
Expression<?> initializer;
if(f.isVariableDeclaration()) {
Expression<?> forVar = parseDeclaration(scopeBuilder);
Token name = lexer.current();
if(scopeBuilder.containsFunction(name.getContent()) || scopeBuilder.containsVariable(name.getContent()))
throw new ParseException(name.getContent() + " is already defined in this scope", name.getPosition());
initializer = forVar;
} else initializer = parseExpression(scopeBuilder);
ParserUtil.ensureType(lexer.consume(), TokenType.STATEMENT_END);
Expression<?> conditional = parseExpression(scopeBuilder);
Expression<?> initializer = switch(lexer.current().getType()) {
case TYPE_NUMBER, TYPE_STRING, TYPE_BOOLEAN -> {
Token type = lexer.consume("Expected type before declaration", TokenType.TYPE_STRING, TokenType.TYPE_NUMBER, TokenType.TYPE_BOOLEAN, TokenType.TYPE_VOID);
Token identifier = lexer.consume("Expected identifier after type", TokenType.IDENTIFIER);
Expression<?> expr = parseVariableDeclaration(scopeBuilder, type, identifier);
lexer.consume("Expected ';' after initializer within for loop", TokenType.STATEMENT_END);
yield expr;
}
case IDENTIFIER -> {
Expression<?> expr = parseAssignment(scopeBuilder);
lexer.consume("Expected ';' after initializer within for loop", TokenType.STATEMENT_END);
yield expr;
}
case STATEMENT_END -> {
lexer.consumeUnchecked();
yield Expression.NOOP;
}
default -> throw new ParseException("Unexpected token '" + lexer.current() + "', expected variable declaration or assignment", lexer.current().getPosition());
};
Expression<?> conditional;
if (lexer.current().isType(TokenType.STATEMENT_END)) // If no conditional is provided, conditional defaults to true
conditional = new BooleanConstant(true, lexer.current().getPosition());
else
conditional = parseExpression(scopeBuilder);
ParserUtil.ensureReturnType(conditional, Expression.ReturnType.BOOLEAN);
ParserUtil.ensureType(lexer.consume(), TokenType.STATEMENT_END);
lexer.consume("Expected ';' separator after conditional within for loop", TokenType.STATEMENT_END);
Expression<?> incrementer;
Token incrementerToken = lexer.consume();
if(scopeBuilder.containsVariable(incrementerToken.getContent())) { // Assume variable assignment
incrementer = parseAssignment(incrementerToken, scopeBuilder);
} else incrementer = parseFunctionInvocation(true, incrementerToken, scopeBuilder);
ParserUtil.ensureType(lexer.consume(), TokenType.CLOSE_PAREN);
if(lexer.current().isType(TokenType.CLOSE_PAREN))
// If no incrementer is provided, do nothing
incrementer = Expression.NOOP;
else if(scopeBuilder.containsVariable(lexer.current().getContent())) // Assume variable assignment
incrementer = parseAssignment(scopeBuilder);
else
incrementer = parseFunctionInvocation(lexer.consume("Expected function call within for loop incrementer", TokenType.IDENTIFIER), scopeBuilder);
lexer.consume("Expected ')' after for loop incrementer", TokenType.CLOSE_PAREN);
return new ForKeyword(parseStatementBlock(scopeBuilder, ReturnType.VOID), initializer, (Expression<Boolean>) conditional,
incrementer,
@@ -231,7 +247,7 @@ public class Parser {
private Expression<?> parseUnary(ScopeBuilder scopeBuilder) {
if (lexer.current().isType(TokenType.BANG, TokenType.MINUS)) {
Token operator = lexer.consume();
Token operator = lexer.consumeUnchecked();
Expression<?> right = parseUnary(scopeBuilder);
return switch(operator.getType()) {
case BANG -> {
@@ -249,7 +265,7 @@ public class Parser {
}
private Expression<?> parsePrimary(ScopeBuilder scopeBuilder) {
Token token = lexer.consume();
Token token = lexer.consumeUnchecked();
return switch(token.getType()) {
case NUMBER -> {
String content = token.getContent();
@@ -259,12 +275,12 @@ public class Parser {
case BOOLEAN -> new BooleanConstant(Boolean.parseBoolean(token.getContent()), token.getPosition());
case OPEN_PAREN -> {
Expression<?> expr = parseExpression(scopeBuilder);
ParserUtil.ensureType(lexer.consume(), TokenType.CLOSE_PAREN);
lexer.consume("Missing ')' at end of expression group", TokenType.CLOSE_PAREN);
yield expr;
}
case IDENTIFIER -> {
if (scopeBuilder.containsFunction(token.getContent()))
yield parseFunctionInvocation(false, token, scopeBuilder);
yield parseFunctionInvocation(token, scopeBuilder);
else if (scopeBuilder.containsVariable(token.getContent())) {
ReturnType variableType = scopeBuilder.getVaraibleType(token.getContent());
yield switch(variableType) {
@@ -274,9 +290,9 @@ public class Parser {
default -> throw new ParseException("Illegal type for variable reference: " + variableType, token.getPosition());
};
}
throw new ParseException("Identifier " + token.getContent() + " is not defined in this scope", token.getPosition());
throw new ParseException("Identifier '" + token.getContent() + "' is not defined in this scope", token.getPosition());
}
default -> throw new ParseException("Unexpected token " + token.getType(), token.getPosition());
default -> throw new ParseException("Unexpected token '" + token.getContent() + "' when parsing expression", token.getPosition());
};
}
@@ -286,7 +302,7 @@ public class Parser {
Expression<?> expr = higherPrecedence.apply(scopeBuilder);
TokenType[] opTypes = operators.keySet().toArray(new TokenType[0]);
while (lexer.current().isType(opTypes)) {
Token operator = lexer.consume();
Token operator = lexer.consumeUnchecked();
Expression<?> right = higherPrecedence.apply(scopeBuilder);
BinaryOperationInfo op = new BinaryOperationInfo(expr, operator, right);
init.accept(op);
@@ -302,24 +318,21 @@ public class Parser {
private record BinaryOperationInfo(Expression<?> left, Token operator, Expression<?> right) {}
private Expression<?> parseDeclaration(ScopeBuilder scopeBuilder) {
Token type = lexer.consume();
Token type = lexer.consume("Expected type before declaration", TokenType.TYPE_STRING, TokenType.TYPE_NUMBER, TokenType.TYPE_BOOLEAN, TokenType.TYPE_VOID);
Token identifier = lexer.consume("Expected identifier after type", TokenType.IDENTIFIER);
Token identifier = lexer.consume();
ParserUtil.ensureType(identifier, TokenType.IDENTIFIER);
Token declarationType = lexer.consume();
ParserUtil.ensureType(declarationType, TokenType.ASSIGNMENT, TokenType.OPEN_PAREN);
return switch(declarationType.getType()) {
return switch(lexer.current().getType()) {
case ASSIGNMENT -> parseVariableDeclaration(scopeBuilder, type, identifier);
case OPEN_PAREN -> parseFunctionDeclaration(scopeBuilder, type, identifier);
default -> throw new ParseException("Illegal type for declaration: " + type, declarationType.getPosition());
default -> throw new ParseException("Expected '=' for variable assignment or '(' for function declaration after identifier '" + identifier.getContent() + "'", lexer.current().getPosition());
};
}
private Expression<?> parseVariableDeclaration(ScopeBuilder scopeBuilder, Token type, Token identifier) {
ParserUtil.ensureType(type, TokenType.TYPE_STRING, TokenType.TYPE_BOOLEAN, TokenType.TYPE_NUMBER);
lexer.consume("Expected '=' after identifier '" + identifier.getContent() + "' for variable declaration", TokenType.ASSIGNMENT);
if (!type.isVariableDeclaration()) throw new ParseException("Expected type specification at beginning of variable declaration", type.getPosition());
if(scopeBuilder.containsVariable(identifier.getContent()))
throw new ParseException(identifier.getContent() + " is already defined in this scope", identifier.getPosition());
@@ -340,7 +353,10 @@ public class Parser {
}
private Expression<?> parseFunctionDeclaration(ScopeBuilder scopeBuilder, Token type, Token identifier) {
ParserUtil.ensureType(type, TokenType.TYPE_STRING, TokenType.TYPE_BOOLEAN, TokenType.TYPE_NUMBER, TokenType.TYPE_VOID);
lexer.consume("Expected '(' after identifier '" + identifier.getContent() + "' for function declaration", TokenType.OPEN_PAREN);
if(!(type.isType(TokenType.TYPE_STRING, TokenType.TYPE_BOOLEAN, TokenType.TYPE_NUMBER, TokenType.TYPE_VOID)))
throw new ParseException("Invalid function declaration return type specification " + type.getType(), type.getPosition());
if(scopeBuilder.containsVariable(identifier.getContent()))
throw new ParseException(identifier.getContent() + " is already defined in this scope", identifier.getPosition());
@@ -349,43 +365,41 @@ public class Parser {
ScopeBuilder functionBodyScope = scopeBuilder.functionScope();
// Declare argument names into function body scope
List<Pair<Integer, ReturnType>> argumentInfo = getFunctionArgumentsDeclaration().stream().map(
// Declare parameter names into function body scope
List<Pair<Integer, ReturnType>> parameterInfo = getFunctionParameterDeclaration().stream().map(
arg -> Pair.of(switch(arg.getRight()) {
case NUMBER -> functionBodyScope.declareNum(arg.getLeft());
case BOOLEAN -> functionBodyScope.declareBool(arg.getLeft());
case STRING -> functionBodyScope.declareStr(arg.getLeft());
default -> throw new IllegalArgumentException("Unsupported argument type: " + arg.getRight());
default -> throw new IllegalArgumentException("Unsupported parameter type: " + arg.getRight());
}, arg.getRight())).toList();
Block body = parseStatementBlock(functionBodyScope, returnType);
FunctionBuilder<?> functionBuilder = new UserDefinedFunctionBuilder<>(returnType, argumentInfo, body, functionBodyScope);
FunctionBuilder<?> functionBuilder = new UserDefinedFunctionBuilder<>(returnType, parameterInfo, body, functionBodyScope);
scopeBuilder.registerFunction(identifier.getContent(), functionBuilder);
return Expression.NOOP;
}
private List<Pair<String, ReturnType>> getFunctionArgumentsDeclaration() {
List<Pair<String, ReturnType>> arguments = new ArrayList<>();
private List<Pair<String, ReturnType>> getFunctionParameterDeclaration() {
List<Pair<String, ReturnType>> parameters = new ArrayList<>();
while(lexer.current().getType() != TokenType.CLOSE_PAREN) {
// Parse argument type
Token typeToken = lexer.consume();
ParserUtil.ensureType(typeToken, TokenType.TYPE_BOOLEAN, TokenType.TYPE_STRING, TokenType.TYPE_NUMBER);
ReturnType argType = ParserUtil.getVariableReturnType(typeToken);
// Parse parameter type
Token typeToken = lexer.consume("Expected function parameter type declaration", TokenType.TYPE_BOOLEAN, TokenType.TYPE_STRING, TokenType.TYPE_NUMBER);
ReturnType type = ParserUtil.getVariableReturnType(typeToken);
// Parse argument name
Token identifierToken = lexer.consume();
ParserUtil.ensureType(identifierToken, TokenType.IDENTIFIER);
String argName = identifierToken.getContent();
// Parse parameter name
Token identifierToken = lexer.consume("Expected function parameter identifier", TokenType.IDENTIFIER);
String name = identifierToken.getContent();
arguments.add(Pair.of(argName, argType));
parameters.add(Pair.of(name, type));
// Consume separator if present, trailing separators are allowed
if(lexer.current().isType(TokenType.SEPARATOR)) lexer.consume();
if(lexer.current().isType(TokenType.SEPARATOR)) lexer.consumeUnchecked();
}
ParserUtil.ensureType(lexer.consume(), TokenType.CLOSE_PAREN);
return arguments;
lexer.consume("Expected ')' after function parameter declaration", TokenType.CLOSE_PAREN);
return parameters;
}
private Block parseBlock(ScopeBuilder scopeBuilder, ReturnType blockReturnType) {
@@ -397,7 +411,7 @@ public class Parser {
// Parse each statement
while(lexer.hasNext() && !lexer.current().isType(TokenType.BLOCK_END)) {
Expression<?> expression = parseStatement(lexer, scopeBuilder);
Expression<?> expression = parseStatement(scopeBuilder);
if(expression != Expression.NOOP) {
expressions.add(expression);
}
@@ -416,42 +430,36 @@ public class Parser {
return new Block(expressions, startPosition, blockReturnType);
}
private Expression<?> parseStatement(Lexer lexer, ScopeBuilder scopeBuilder) {
private Expression<?> parseStatement(ScopeBuilder scopeBuilder) {
Token token = lexer.current();
// Include BREAK and CONTINUE as valid token types if scope is within a loop
if(scopeBuilder.isInLoop()) ParserUtil.ensureType(token, TokenType.IDENTIFIER, TokenType.IF_STATEMENT, TokenType.WHILE_LOOP,
TokenType.FOR_LOOP,
TokenType.TYPE_NUMBER, TokenType.TYPE_STRING, TokenType.TYPE_BOOLEAN,
TokenType.TYPE_VOID,
TokenType.RETURN, TokenType.BREAK, TokenType.CONTINUE, TokenType.FAIL);
else ParserUtil.ensureType(token, TokenType.IDENTIFIER, TokenType.IF_STATEMENT, TokenType.WHILE_LOOP, TokenType.FOR_LOOP,
TokenType.TYPE_NUMBER, TokenType.TYPE_STRING, TokenType.TYPE_BOOLEAN, TokenType.TYPE_VOID,
TokenType.RETURN,
TokenType.FAIL);
Expression<?> expression = switch(token.getType()) {
case FOR_LOOP -> parseForLoop(scopeBuilder);
case IF_STATEMENT -> parseIfStatement(scopeBuilder);
case WHILE_LOOP -> parseWhileLoop(scopeBuilder);
case IDENTIFIER -> {
if(scopeBuilder.containsVariable(token.getContent())) yield parseAssignment(lexer.consume(), scopeBuilder); // Assume variable assignment
else yield parseFunctionInvocation(true, lexer.consume(), scopeBuilder);
if(scopeBuilder.containsVariable(token.getContent())) yield parseAssignment(scopeBuilder); // Assume variable assignment
else yield parseFunctionInvocation(lexer.consumeUnchecked(), scopeBuilder);
}
case TYPE_NUMBER, TYPE_STRING, TYPE_BOOLEAN, TYPE_VOID -> parseDeclaration(scopeBuilder);
case RETURN -> parseReturn(scopeBuilder);
case BREAK -> new BreakKeyword(lexer.consume().getPosition());
case CONTINUE -> new ContinueKeyword(lexer.consume().getPosition());
case FAIL -> new FailKeyword(lexer.consume().getPosition());
case BREAK -> {
if (!scopeBuilder.isInLoop()) throw new ParseException("Break statements can only be defined inside loops", token.getPosition());
yield new BreakKeyword(lexer.consumeUnchecked().getPosition());
}
case CONTINUE -> {
if (!scopeBuilder.isInLoop()) throw new ParseException("Continue statements can only be defined inside loops", token.getPosition());
yield new ContinueKeyword(lexer.consumeUnchecked().getPosition());
}
case FAIL -> new FailKeyword(lexer.consumeUnchecked().getPosition());
case STATEMENT_END -> Expression.NOOP;
default -> throw new UnsupportedOperationException("Unexpected token " + token.getType() + ": " + token.getPosition());
};
if(!token.isControlStructure() && expression != Expression.NOOP) ParserUtil.ensureType(lexer.consume(), TokenType.STATEMENT_END);
if(!token.isControlStructure() && expression != Expression.NOOP) lexer.consume("Expected ';' at end of statement", TokenType.STATEMENT_END);
return expression;
}
private ReturnKeyword parseReturn(ScopeBuilder scopeBuilder) {
Token returnToken = lexer.consume();
ParserUtil.ensureType(returnToken, TokenType.RETURN);
Token returnToken = lexer.consume("Expected 'return' keyword at beginning of return statement", TokenType.RETURN);
Expression<?> data = null;
if(!lexer.current().isType(TokenType.STATEMENT_END)) {
data = parseExpression(scopeBuilder);
@@ -459,10 +467,10 @@ public class Parser {
return new ReturnKeyword(data, returnToken.getPosition());
}
private VariableAssignmentNode<?> parseAssignment(Token identifier, ScopeBuilder scopeBuilder) {
ParserUtil.ensureType(identifier, TokenType.IDENTIFIER);
private VariableAssignmentNode<?> parseAssignment(ScopeBuilder scopeBuilder) {
Token identifier = lexer.consume("Expected identifier at beginning of assignment", TokenType.IDENTIFIER);
ParserUtil.ensureType(lexer.consume(), TokenType.ASSIGNMENT);
lexer.consume("Expected '=' after identifier for variable assignment", TokenType.ASSIGNMENT);
Expression<?> value = parseExpression(scopeBuilder);
@@ -480,48 +488,37 @@ public class Parser {
};
}
private Expression<?> parseFunctionInvocation(boolean fullStatement, Token identifier, ScopeBuilder scopeBuilder) {
private Expression<?> parseFunctionInvocation(Token identifier, ScopeBuilder scopeBuilder) {
if(!scopeBuilder.containsFunction(identifier.getContent()))
throw new ParseException("Function \"" + identifier.getContent() + "\" is not defined in this scope", identifier.getPosition());
throw new ParseException("Function '" + identifier.getContent() + "' is not defined in this scope", identifier.getPosition());
ParserUtil.ensureType(lexer.consume(), TokenType.OPEN_PAREN); // Invocation starts with open paren
FunctionBuilder<?> builder = scopeBuilder.getFunction(identifier.getContent());
List<Expression<?>> args = getFunctionArgs(scopeBuilder); // Extract arguments, consume the rest.
lexer.consume("Expected '(' after identifier " + identifier.getContent(), TokenType.OPEN_PAREN); // Invocation starts with open paren
ParserUtil.ensureType(lexer.consume(), TokenType.CLOSE_PAREN); // Remove body end
if(fullStatement) ParserUtil.ensureType(lexer.current(), TokenType.STATEMENT_END);
List<Expression<?>> args = new ArrayList<>();
while(!lexer.current().isType(TokenType.CLOSE_PAREN)) {
args.add(parseExpression(scopeBuilder));
if (lexer.current().isType(TokenType.CLOSE_PAREN)) break;
lexer.consume("Expected ',' between function arguments", TokenType.SEPARATOR);
}
lexer.consume("Expected ')' after function arguments", TokenType.CLOSE_PAREN);
if(ignoredFunctions.contains(identifier.getContent())) {
return Expression.NOOP;
}
if(scopeBuilder.containsFunction(identifier.getContent())) {
FunctionBuilder<?> builder = scopeBuilder.getFunction(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++) {
Expression<?> argument = args.get(i);
if(builder.getArgument(i) == null)
throw new ParseException("Unexpected argument at position " + i + " in function " + identifier.getContent(),
identifier.getPosition());
ParserUtil.ensureReturnType(argument, builder.getArgument(i));
}
return builder.build(args, identifier.getPosition());
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++) {
Expression<?> argument = args.get(i);
if(builder.getArgument(i) == null)
throw new ParseException("Unexpected argument at position " + i + " in function " + identifier.getContent(),
identifier.getPosition());
ParserUtil.ensureReturnType(argument, builder.getArgument(i));
}
throw new UnsupportedOperationException("Unsupported function: " + identifier.getContent());
return builder.build(args, identifier.getPosition());
}
private List<Expression<?>> getFunctionArgs(ScopeBuilder scopeBuilder) {
List<Expression<?>> args = new ArrayList<>();
while(!lexer.current().isType(TokenType.CLOSE_PAREN)) {
args.add(parseExpression(scopeBuilder));
ParserUtil.ensureType(lexer.current(), TokenType.SEPARATOR, TokenType.CLOSE_PAREN);
if(lexer.current().isType(TokenType.SEPARATOR)) lexer.consume();
}
return args;
}
}

View File

@@ -20,14 +20,14 @@ import com.dfsek.terra.addons.terrascript.parser.lang.Expression;
public class ParserUtil {
public static void ensureType(Token token, TokenType... expected) {
for(TokenType type : expected) if(token.getType().equals(type)) return;
throw new ParseException("Expected " + Arrays.toString(expected) + " but found " + token.getType(), token.getPosition());
}
// public static void ensureType(Token token, TokenType... expected) {
// for(TokenType type : expected) if(token.getType().equals(type)) return;
// throw new ParseException("Expected " + Arrays.toString(expected) + " but found " + token.getType(), token.getPosition());
// }
public static void ensureReturnType(Expression<?> returnable, Expression.ReturnType... types) {
for(Expression.ReturnType type : types) if(returnable.returnType().equals(type)) return;
throw new ParseException("Expected " + Arrays.toString(types) + " but found " + returnable.returnType(), returnable.getPosition());
throw new ParseException("Invalid type " + returnable.returnType() + ", expected " + (types.length == 1 ? types[0].toString() : "one of " + Arrays.toString(types)), returnable.getPosition());
}
public static Expression.ReturnType getVariableReturnType(Token varToken) {

View File

@@ -29,7 +29,7 @@ public class ParseException extends RuntimeException {
@Override
public String getMessage() {
return super.getMessage() + ": " + position;
return "Error at " + position + ": " + super.getMessage();
}
public SourcePosition getPosition() {

View File

@@ -16,6 +16,9 @@ import com.dfsek.terra.addons.terrascript.parser.lang.Expression;
public interface FunctionBuilder<T extends Function<?>> {
T build(List<Expression<?>> argumentList, SourcePosition position);
/**
* @return Number of function arguments, -1 if the function uses a vararg at the end
*/
int argNumber();
Expression.ReturnType getArgument(int position);

View File

@@ -15,16 +15,16 @@ import com.dfsek.terra.api.util.generic.pair.Pair;
public class UserDefinedFunctionBuilder<T extends Function<?>> implements FunctionBuilder<T> {
private final ReturnType returnType;
private final List<Pair<Integer, ReturnType>> argumentInfo;
private final List<Pair<Integer, ReturnType>> parameterInfo;
private final ScopeBuilder bodyScopeBuilder;
private final Block body;
public UserDefinedFunctionBuilder(ReturnType returnType, List<Pair<Integer, ReturnType>> argumentInfo, Block body,
public UserDefinedFunctionBuilder(ReturnType returnType, List<Pair<Integer, ReturnType>> parameterInfo, Block body,
ScopeBuilder functionBodyScope) {
this.returnType = returnType;
this.bodyScopeBuilder = functionBodyScope;
this.body = body;
this.argumentInfo = argumentInfo;
this.parameterInfo = parameterInfo;
}
@Override
@@ -44,12 +44,12 @@ public class UserDefinedFunctionBuilder<T extends Function<?>> implements Functi
Scope bodyScope = threadLocalScope.get();
// Pass arguments into scope of function body
for(int i = 0; i < argumentList.size(); i++) {
Pair<Integer, ReturnType> argInfo = argumentInfo.get(i);
Pair<Integer, ReturnType> paramInfo = parameterInfo.get(i);
Expression<?> argExpression = argumentList.get(i);
switch(argInfo.getRight()) {
case NUMBER -> bodyScope.setNum(argInfo.getLeft(), argExpression.applyDouble(implementationArguments, scope));
case BOOLEAN -> bodyScope.setBool(argInfo.getLeft(), argExpression.applyBoolean(implementationArguments, scope));
case STRING -> bodyScope.setStr(argInfo.getLeft(), (String) argExpression.evaluate(implementationArguments, scope));
switch(paramInfo.getRight()) {
case NUMBER -> bodyScope.setNum(paramInfo.getLeft(), argExpression.applyDouble(implementationArguments, scope));
case BOOLEAN -> bodyScope.setBool(paramInfo.getLeft(), argExpression.applyBoolean(implementationArguments, scope));
case STRING -> bodyScope.setStr(paramInfo.getLeft(), (String) argExpression.evaluate(implementationArguments, scope));
}
}
return body.evaluate(implementationArguments, bodyScope).data().evaluate(implementationArguments, scope);
@@ -64,11 +64,11 @@ public class UserDefinedFunctionBuilder<T extends Function<?>> implements Functi
@Override
public int argNumber() {
return argumentInfo.size();
return parameterInfo.size();
}
@Override
public ReturnType getArgument(int position) {
return argumentInfo.get(position).getRight();
return parameterInfo.get(position).getRight();
}
}

View File

@@ -7,23 +7,80 @@
package structure;
import com.dfsek.terra.addons.terrascript.lexer.LookaheadStream;
import com.dfsek.terra.addons.terrascript.lexer.Char;
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import com.dfsek.terra.addons.terrascript.lexer.LookaheadStream;
import static org.junit.jupiter.api.Assertions.*;
public class LookaheadStreamTest {
@Test
public void lookahead() {
LookaheadStream lookahead = new LookaheadStream(new StringReader("Test string..."));
String testString = "Test string...\nNew line";
for(int i = 0; lookahead.next(i) != null; i++) {
System.out.print(lookahead.next(i).getCharacter());
}
while(lookahead.next(0) != null) {
System.out.print(lookahead.consume().getCharacter());
}
LookaheadStream lookahead = new LookaheadStream(testString);
Char first = new Char('T', new SourcePosition(1, 1));
Char second = new Char('e', new SourcePosition(1, 2));
Char third = new Char('s', new SourcePosition(1, 3));
Char space = new Char(' ', new SourcePosition(1, 5));
Char newline = new Char('\n', new SourcePosition(1, 15));
Char lineTwoColOne = new Char('N', new SourcePosition(2, 1));
String lineTwo = "New line";
assertTrue(lookahead.matchesString("Test", false));
assertTrue(lookahead.matchesString(testString, false));
assertFalse(lookahead.matchesString(testString + "asdf", false));
assertFalse(lookahead.matchesString("Foo", false));
assertEquals(first, lookahead.current());
assertEquals(first, lookahead.current());
assertEquals(new SourcePosition(1, 1), lookahead.getPosition());
assertEquals(new SourcePosition(1, 1), lookahead.getPosition());
assertEquals(second, lookahead.peek());
assertEquals(second, lookahead.peek());
assertEquals(first, lookahead.consume());
assertFalse(lookahead.matchesString(testString, false));
assertEquals(second, lookahead.current());
assertEquals(second, lookahead.consume());
assertEquals(third, lookahead.current());
assertTrue(lookahead.matchesString("st", true));
assertEquals(space, lookahead.current());
assertEquals(space, lookahead.consume());
assertTrue(lookahead.matchesString("string...", false));
assertTrue(lookahead.matchesString("string...", true));
assertFalse(lookahead.matchesString("string...", false));
assertEquals(newline, lookahead.current());
assertEquals(newline, lookahead.consume());
assertEquals(lineTwoColOne, lookahead.current());
assertTrue(lookahead.matchesString(lineTwo, false));
assertFalse(lookahead.matchesString(lineTwo + "asdf", false));
assertTrue(lookahead.matchesString(lineTwo, true));
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
assertDoesNotThrow(lookahead::consume);
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
assertDoesNotThrow(lookahead::consume);
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
}
}

View File

@@ -1,3 +1,20 @@
str myFunction(str functionString, num functionNumber) {
test(functionString, functionNumber);
void nestedFunction() {
test("Hello from nested function", 69);
}
nestedFunction();
return "Return to sender";
}
str functionResult = myFunction("Hello from myFunction", 535);
test(functionResult, 58);
void noReturn() test("Single statement function", 42);
noReturn();
bool thing1 = 2 > (2+2) || false;
if(2 > 2 || 3 + 4 <= 2 && 4 + 5 > 2 / 3) {
@@ -29,7 +46,9 @@ test("-2 = " + thing, 2);
thing = -thing;
test("--2 = " + thing, 2);
for(;;) {
break;
}
for(num i = 0; i < 5; i = i + 1) {
test("i = " + i, iterator);