mirror of
https://github.com/PolyhedralDev/Terra.git
synced 2026-04-06 15:56:14 +00:00
Better error handling + other changes
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user