From 772675639e2c957209b20d45bf80424f4fa7e0a2 Mon Sep 17 00:00:00 2001 From: Astrash Date: Sat, 29 Jul 2023 20:29:16 +1000 Subject: [PATCH] Better error handling + other changes --- .../terra/addons/terrascript/lexer/Char.java | 32 ++- .../terra/addons/terrascript/lexer/Lexer.java | 102 ++++---- .../terrascript/lexer/LookaheadStream.java | 130 ++++----- .../terrascript/lexer/SourcePosition.java | 27 +- .../addons/terrascript/parser/Parser.java | 247 +++++++++--------- .../addons/terrascript/parser/ParserUtil.java | 10 +- .../parser/exceptions/ParseException.java | 2 +- .../lang/functions/FunctionBuilder.java | 3 + .../functions/UserDefinedFunctionBuilder.java | 20 +- .../java/structure/LookaheadStreamTest.java | 77 +++++- .../src/test/resources/test.tesf | 21 +- 11 files changed, 363 insertions(+), 308 deletions(-) diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Char.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Char.java index c32bec553..b06d07122 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Char.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Char.java @@ -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); } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Lexer.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Lexer.java index 041981408..3698a9a0d 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Lexer.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/Lexer.java @@ -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) { diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/LookaheadStream.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/LookaheadStream.java index 1a429a05c..0d0186d3a 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/LookaheadStream.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/LookaheadStream.java @@ -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 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); } } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/SourcePosition.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/SourcePosition.java index 1bd5877cf..b6a5fc4f3 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/SourcePosition.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/lexer/SourcePosition.java @@ -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); } } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/Parser.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/Parser.java index c531ec7af..5412c4c65 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/Parser.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/Parser.java @@ -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) 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, 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) 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) 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> argumentInfo = getFunctionArgumentsDeclaration().stream().map( + // Declare parameter names into function body scope + List> 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> getFunctionArgumentsDeclaration() { - List> arguments = new ArrayList<>(); + private List> getFunctionParameterDeclaration() { + List> 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> 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> 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> getFunctionArgs(ScopeBuilder scopeBuilder) { - List> 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; - } } diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/ParserUtil.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/ParserUtil.java index 6d1ac8263..6703f3708 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/ParserUtil.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/ParserUtil.java @@ -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) { diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/exceptions/ParseException.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/exceptions/ParseException.java index da788864e..3a0956a1e 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/exceptions/ParseException.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/exceptions/ParseException.java @@ -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() { diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/lang/functions/FunctionBuilder.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/lang/functions/FunctionBuilder.java index 345b86528..9460601fa 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/lang/functions/FunctionBuilder.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/lang/functions/FunctionBuilder.java @@ -16,6 +16,9 @@ import com.dfsek.terra.addons.terrascript.parser.lang.Expression; public interface FunctionBuilder> { T build(List> 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); diff --git a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/lang/functions/UserDefinedFunctionBuilder.java b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/lang/functions/UserDefinedFunctionBuilder.java index 24849dd57..eb38f3f1e 100644 --- a/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/lang/functions/UserDefinedFunctionBuilder.java +++ b/common/addons/structure-terrascript-loader/src/main/java/com/dfsek/terra/addons/terrascript/parser/lang/functions/UserDefinedFunctionBuilder.java @@ -15,16 +15,16 @@ import com.dfsek.terra.api.util.generic.pair.Pair; public class UserDefinedFunctionBuilder> implements FunctionBuilder { private final ReturnType returnType; - private final List> argumentInfo; + private final List> parameterInfo; private final ScopeBuilder bodyScopeBuilder; private final Block body; - public UserDefinedFunctionBuilder(ReturnType returnType, List> argumentInfo, Block body, + public UserDefinedFunctionBuilder(ReturnType returnType, List> 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> implements Functi Scope bodyScope = threadLocalScope.get(); // Pass arguments into scope of function body for(int i = 0; i < argumentList.size(); i++) { - Pair argInfo = argumentInfo.get(i); + Pair 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> 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(); } } diff --git a/common/addons/structure-terrascript-loader/src/test/java/structure/LookaheadStreamTest.java b/common/addons/structure-terrascript-loader/src/test/java/structure/LookaheadStreamTest.java index 3676008e9..2816da706 100644 --- a/common/addons/structure-terrascript-loader/src/test/java/structure/LookaheadStreamTest.java +++ b/common/addons/structure-terrascript-loader/src/test/java/structure/LookaheadStreamTest.java @@ -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()); } } diff --git a/common/addons/structure-terrascript-loader/src/test/resources/test.tesf b/common/addons/structure-terrascript-loader/src/test/resources/test.tesf index 8bcce7177..e30635c9c 100644 --- a/common/addons/structure-terrascript-loader/src/test/resources/test.tesf +++ b/common/addons/structure-terrascript-loader/src/test/resources/test.tesf @@ -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);