Better error handling + other changes

This commit is contained in:
Astrash
2023-07-29 20:29:16 +10:00
parent 13300861ee
commit 772675639e
11 changed files with 363 additions and 308 deletions
@@ -7,16 +7,17 @@
package com.dfsek.terra.addons.terrascript.lexer; package com.dfsek.terra.addons.terrascript.lexer;
import java.util.Objects;
public class Char { public class Char {
private final char character; private final char character;
private final int index; private final SourcePosition position;
private final int line;
public Char(char character, int index, int line) { public Char(char character, SourcePosition position) {
this.character = character; this.character = character;
this.index = index; this.position = position;
this.line = line;
} }
public boolean is(char... tests) { public boolean is(char... tests) {
@@ -33,18 +34,23 @@ public class Char {
return Character.toString(character); 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() { public char getCharacter() {
return character; return character;
} }
public int getIndex() {
return index;
}
public int getLine() {
return line;
}
public boolean isWhitespace() { public boolean isWhitespace() {
return Character.isWhitespace(character); return Character.isWhitespace(character);
} }
@@ -9,7 +9,7 @@ package com.dfsek.terra.addons.terrascript.lexer;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import java.io.StringReader; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.Stack; import java.util.Stack;
@@ -28,7 +28,7 @@ public class Lexer {
private Token current; private Token current;
public Lexer(String data) { public Lexer(String data) {
reader = new LookaheadStream(new StringReader(data + '\0')); reader = new LookaheadStream(data + '\0');
current = tokenize(); current = tokenize();
} }
@@ -50,7 +50,12 @@ public class Lexer {
* *
* @throws ParseException If token does not exist * @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; if(current.getType() == TokenType.END_OF_FILE) return current;
Token temp = current; Token temp = current;
current = tokenize(); current = tokenize();
@@ -68,6 +73,7 @@ public class Lexer {
private Token tokenize() throws TokenizerException { private Token tokenize() throws TokenizerException {
consumeWhitespace(); consumeWhitespace();
SourcePosition position = reader.getPosition();
// Skip line if comment // Skip line if comment
while(reader.matchesString("//", true)) skipLine(); while(reader.matchesString("//", true)) skipLine();
@@ -77,37 +83,37 @@ public class Lexer {
// Reached end of file // Reached end of file
if(reader.current().isEOF()) { if(reader.current().isEOF()) {
if(!bracketStack.isEmpty()) throw new ParseException("Dangling closing brace", bracketStack.peek().getPosition()); if(!bracketStack.isEmpty()) throw new ParseException("Dangling open brace", bracketStack.peek().getPosition());
return new Token(reader.consume().toString(), TokenType.END_OF_FILE, reader.getPosition()); return new Token(reader.consume().toString(), TokenType.END_OF_FILE, position);
} }
// Check if operator token // Check if operator token
if(reader.matchesString("==", true)) if(reader.matchesString("==", true))
return new Token("==", TokenType.EQUALS_EQUALS, reader.getPosition()); return new Token("==", TokenType.EQUALS_EQUALS, position);
if(reader.matchesString("!=", true)) if(reader.matchesString("!=", true))
return new Token("!=", TokenType.BANG_EQUALS, reader.getPosition()); return new Token("!=", TokenType.BANG_EQUALS, position);
if(reader.matchesString(">=", true)) if(reader.matchesString(">=", true))
return new Token(">=", TokenType.GREATER_EQUAL, reader.getPosition()); return new Token(">=", TokenType.GREATER_EQUAL, position);
if(reader.matchesString("<=", true)) if(reader.matchesString("<=", true))
return new Token("<=", TokenType.LESS_EQUALS, reader.getPosition()); return new Token("<=", TokenType.LESS_EQUALS, position);
if(reader.matchesString(">", true)) if(reader.matchesString(">", true))
return new Token(">", TokenType.GREATER, reader.getPosition()); return new Token(">", TokenType.GREATER, position);
if(reader.matchesString("<", true)) if(reader.matchesString("<", true))
return new Token("<", TokenType.LESS, reader.getPosition()); return new Token("<", TokenType.LESS, position);
// Check if logical operator // Check if logical operator
if(reader.matchesString("||", true)) if(reader.matchesString("||", true))
return new Token("||", TokenType.BOOLEAN_OR, reader.getPosition()); return new Token("||", TokenType.BOOLEAN_OR, position);
if(reader.matchesString("&&", true)) if(reader.matchesString("&&", true))
return new Token("&&", TokenType.BOOLEAN_AND, reader.getPosition()); return new Token("&&", TokenType.BOOLEAN_AND, position);
// Check if number // Check if number
if(isNumberStart()) { if(isNumberStart()) {
StringBuilder num = new StringBuilder(); StringBuilder num = new StringBuilder();
while(!reader.current().isEOF() && isNumberLike()) { 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 // Check if string literal
@@ -122,95 +128,95 @@ public class Lexer {
continue; continue;
} else ignoreNext = false; } else ignoreNext = false;
if(reader.current().isEOF()) 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()); string.append(reader.consume());
} }
reader.consume(); // Consume last quote 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('(')) 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(')')) 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(';')) 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(',')) 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('{')) { 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); bracketStack.push(token);
return token; return token;
} }
if(reader.current().is('}')) { 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(); 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('=')) 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('+')) 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('-')) if(reader.current().is('-'))
return new Token(reader.consume().toString(), TokenType.MINUS, return new Token(reader.consume().toString(), TokenType.MINUS,
reader.getPosition()); position);
if(reader.current().is('*')) if(reader.current().is('*'))
return new Token(reader.consume().toString(), TokenType.STAR, return new Token(reader.consume().toString(), TokenType.STAR,
reader.getPosition()); position);
if(reader.current().is('/')) 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('%')) 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('!')) 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 // Read word
StringBuilder token = new StringBuilder(); StringBuilder token = new StringBuilder();
while(!reader.current().isEOF() && !isSyntaxSignificant(reader.current().getCharacter())) { while(!reader.current().isEOF() && !isSyntaxSignificant(reader.current().getCharacter())) {
Char c = reader.consume(); Char c = reader.consume();
if(c.isWhitespace()) break; if(c.isWhitespace()) break;
token.append(c); token.append(c.getCharacter());
} }
String tokenString = token.toString(); String tokenString = token.toString();
// Check if word is a keyword // Check if word is a keyword
if(tokenString.equals("true")) if(tokenString.equals("true"))
return new Token(tokenString, TokenType.BOOLEAN, reader.getPosition()); return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("false")) if(tokenString.equals("false"))
return new Token(tokenString, TokenType.BOOLEAN, reader.getPosition()); return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("num")) 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")) 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")) 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")) 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")) 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")) if(tokenString.equals("else"))
return new Token(tokenString, TokenType.ELSE, reader.getPosition()); return new Token(tokenString, TokenType.ELSE, position);
if(tokenString.equals("while")) 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")) 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")) if(tokenString.equals("return"))
return new Token(tokenString, TokenType.RETURN, reader.getPosition()); return new Token(tokenString, TokenType.RETURN, position);
if(tokenString.equals("continue")) if(tokenString.equals("continue"))
return new Token(tokenString, TokenType.CONTINUE, reader.getPosition()); return new Token(tokenString, TokenType.CONTINUE, position);
if(tokenString.equals("break")) if(tokenString.equals("break"))
return new Token(tokenString, TokenType.BREAK, reader.getPosition()); return new Token(tokenString, TokenType.BREAK, position);
if(tokenString.equals("fail")) 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 // 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() { private void skipLine() {
@@ -241,7 +247,7 @@ public class Lexer {
private boolean isNumberStart() { private boolean isNumberStart() {
return reader.current().isDigit() return reader.current().isDigit()
|| reader.current().is('.') && reader.next(1).isDigit(); || reader.current().is('.') && reader.peek().isDigit();
} }
public boolean isSyntaxSignificant(char c) { 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; 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 { 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) { private final String source;
this.input = r;
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 * @return current character
*/ */
public Char current() { public Char current() {
return next(0); return new Char(source.charAt(index), position);
} }
/** /**
* Consume and return one character. * Consume and return one character.
* *
@@ -45,82 +30,55 @@ public class LookaheadStream {
*/ */
public Char consume() { public Char consume() {
Char consumed = current(); Char consumed = current();
consume(1); incrementIndex(1);
return consumed; return consumed;
} }
/** /**
* Fetch a future character without consuming it. * @return The next character in sequence.
*
* @param ahead Distance ahead to peek
*
* @return Character
*/ */
public Char next(int ahead) { public Char peek() {
if(ahead < 0) throw new IllegalArgumentException(); int index = this.index + 1;
if (index + 1 >= source.length()) return null;
while(buffer.size() <= ahead && !end) { return new Char(source.charAt(index), getPositionAfter(1));
Char item = fetch();
if(item != null) {
buffer.add(item);
} else end = true;
}
if(ahead >= buffer.size()) {
return null;
} else return buffer.get(ahead);
} }
/** /**
* 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) { public boolean matchesString(String check, boolean consumeIfMatched) {
if(amount < 0) throw new IllegalArgumentException(); boolean matches = check.equals(source.substring(index, Math.min(index + check.length(), source.length())));
while(amount-- > 0) { if (matches && consumeIfMatched) incrementIndex(check.length());
if(!buffer.isEmpty()) buffer.remove(0); // Remove top item from buffer. return matches;
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;
} }
/** /**
* Fetch the next character. * @return Current position within the source file
*
* @return Next character
*/ */
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() { 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; package com.dfsek.terra.addons.terrascript.lexer;
public class SourcePosition { import java.util.Objects;
private final int line;
private final int index;
public record SourcePosition(int line, int column) {
public SourcePosition(int line, int index) {
this.line = line;
this.index = index;
}
@Override @Override
public String toString() { 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) { private WhileKeyword parseWhileLoop(ScopeBuilder scopeBuilder) {
SourcePosition start = lexer.consume().getPosition(); SourcePosition start = lexer.consume("Expected 'while' keyword at beginning of while loop", TokenType.WHILE_LOOP).getPosition();
ParserUtil.ensureType(lexer.consume(), TokenType.OPEN_PAREN); lexer.consume("Expected '(' proceeding 'while' keyword", TokenType.OPEN_PAREN);
scopeBuilder = scopeBuilder.innerLoopScope(); scopeBuilder = scopeBuilder.innerLoopScope();
Expression<?> condition = parseExpression(scopeBuilder); Expression<?> condition = parseExpression(scopeBuilder);
ParserUtil.ensureReturnType(condition, Expression.ReturnType.BOOLEAN); 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, return new WhileKeyword(parseStatementBlock(scopeBuilder, ReturnType.VOID), (Expression<Boolean>) condition,
start); // While loop start); // While loop
} }
private IfKeyword parseIfStatement(ScopeBuilder scopeBuilder) { private IfKeyword parseIfStatement(ScopeBuilder scopeBuilder) {
SourcePosition start = lexer.consume().getPosition(); SourcePosition start = lexer.consume("Expected 'if' keyword at beginning of if statement", TokenType.IF_STATEMENT).getPosition();
ParserUtil.ensureType(lexer.consume(), TokenType.OPEN_PAREN); lexer.consume("Expected '(' proceeding 'if' keyword", TokenType.OPEN_PAREN);
Expression<?> condition = parseExpression(scopeBuilder); Expression<?> condition = parseExpression(scopeBuilder);
ParserUtil.ensureReturnType(condition, Expression.ReturnType.BOOLEAN); 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 elseBlock = null;
Block statement = parseStatementBlock(scopeBuilder, ReturnType.VOID); Block statement = parseStatementBlock(scopeBuilder, ReturnType.VOID);
@@ -107,9 +107,9 @@ public class Parser {
List<Pair<Expression<Boolean>, Block>> elseIf = new ArrayList<>(); List<Pair<Expression<Boolean>, Block>> elseIf = new ArrayList<>();
while(lexer.hasNext() && lexer.current().isType(TokenType.ELSE)) { while(lexer.hasNext() && lexer.current().isType(TokenType.ELSE)) {
lexer.consume(); // Consume else. lexer.consumeUnchecked(); // Consume else.
if(lexer.current().isType(TokenType.IF_STATEMENT)) { if(lexer.current().isType(TokenType.IF_STATEMENT)) {
lexer.consume(); // Consume if. lexer.consumeUnchecked(); // Consume if.
Expression<?> elseCondition = parseExpression(scopeBuilder); Expression<?> elseCondition = parseExpression(scopeBuilder);
ParserUtil.ensureReturnType(elseCondition, Expression.ReturnType.BOOLEAN); ParserUtil.ensureReturnType(elseCondition, Expression.ReturnType.BOOLEAN);
elseIf.add(Pair.of((Expression<Boolean>) elseCondition, parseStatementBlock(scopeBuilder, ReturnType.VOID))); 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) { private Block parseStatementBlock(ScopeBuilder scopeBuilder, ReturnType blockReturnType) {
if(lexer.current().isType(TokenType.BLOCK_BEGIN)) { if(lexer.current().isType(TokenType.BLOCK_BEGIN)) {
ParserUtil.ensureType(lexer.consume(), TokenType.BLOCK_BEGIN); lexer.consumeUnchecked();
Block block = parseBlock(scopeBuilder, blockReturnType); 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; return block;
} else { } else {
SourcePosition position = lexer.current().getPosition(); 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) { private ForKeyword parseForLoop(ScopeBuilder scopeBuilder) {
SourcePosition start = lexer.consume().getPosition(); SourcePosition start = lexer.consume("Expected 'for' keyword at beginning of for loop", TokenType.FOR_LOOP).getPosition();
ParserUtil.ensureType(lexer.consume(), TokenType.OPEN_PAREN); lexer.consume("Expected '(' after 'for' keyword", TokenType.OPEN_PAREN);
scopeBuilder = scopeBuilder.innerLoopScope(); // new scope 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 = switch(lexer.current().getType()) {
Expression<?> initializer; case TYPE_NUMBER, TYPE_STRING, TYPE_BOOLEAN -> {
if(f.isVariableDeclaration()) { Token type = lexer.consume("Expected type before declaration", TokenType.TYPE_STRING, TokenType.TYPE_NUMBER, TokenType.TYPE_BOOLEAN, TokenType.TYPE_VOID);
Expression<?> forVar = parseDeclaration(scopeBuilder); Token identifier = lexer.consume("Expected identifier after type", TokenType.IDENTIFIER);
Token name = lexer.current(); Expression<?> expr = parseVariableDeclaration(scopeBuilder, type, identifier);
if(scopeBuilder.containsFunction(name.getContent()) || scopeBuilder.containsVariable(name.getContent())) lexer.consume("Expected ';' after initializer within for loop", TokenType.STATEMENT_END);
throw new ParseException(name.getContent() + " is already defined in this scope", name.getPosition()); yield expr;
initializer = forVar; }
} else initializer = parseExpression(scopeBuilder); case IDENTIFIER -> {
ParserUtil.ensureType(lexer.consume(), TokenType.STATEMENT_END); Expression<?> expr = parseAssignment(scopeBuilder);
Expression<?> conditional = parseExpression(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.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; Expression<?> incrementer;
Token incrementerToken = lexer.consume(); if(lexer.current().isType(TokenType.CLOSE_PAREN))
if(scopeBuilder.containsVariable(incrementerToken.getContent())) { // Assume variable assignment // If no incrementer is provided, do nothing
incrementer = parseAssignment(incrementerToken, scopeBuilder); incrementer = Expression.NOOP;
} else incrementer = parseFunctionInvocation(true, incrementerToken, scopeBuilder); else if(scopeBuilder.containsVariable(lexer.current().getContent())) // Assume variable assignment
incrementer = parseAssignment(scopeBuilder);
ParserUtil.ensureType(lexer.consume(), TokenType.CLOSE_PAREN); 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, return new ForKeyword(parseStatementBlock(scopeBuilder, ReturnType.VOID), initializer, (Expression<Boolean>) conditional,
incrementer, incrementer,
@@ -231,7 +247,7 @@ public class Parser {
private Expression<?> parseUnary(ScopeBuilder scopeBuilder) { private Expression<?> parseUnary(ScopeBuilder scopeBuilder) {
if (lexer.current().isType(TokenType.BANG, TokenType.MINUS)) { if (lexer.current().isType(TokenType.BANG, TokenType.MINUS)) {
Token operator = lexer.consume(); Token operator = lexer.consumeUnchecked();
Expression<?> right = parseUnary(scopeBuilder); Expression<?> right = parseUnary(scopeBuilder);
return switch(operator.getType()) { return switch(operator.getType()) {
case BANG -> { case BANG -> {
@@ -249,7 +265,7 @@ public class Parser {
} }
private Expression<?> parsePrimary(ScopeBuilder scopeBuilder) { private Expression<?> parsePrimary(ScopeBuilder scopeBuilder) {
Token token = lexer.consume(); Token token = lexer.consumeUnchecked();
return switch(token.getType()) { return switch(token.getType()) {
case NUMBER -> { case NUMBER -> {
String content = token.getContent(); String content = token.getContent();
@@ -259,12 +275,12 @@ public class Parser {
case BOOLEAN -> new BooleanConstant(Boolean.parseBoolean(token.getContent()), token.getPosition()); case BOOLEAN -> new BooleanConstant(Boolean.parseBoolean(token.getContent()), token.getPosition());
case OPEN_PAREN -> { case OPEN_PAREN -> {
Expression<?> expr = parseExpression(scopeBuilder); Expression<?> expr = parseExpression(scopeBuilder);
ParserUtil.ensureType(lexer.consume(), TokenType.CLOSE_PAREN); lexer.consume("Missing ')' at end of expression group", TokenType.CLOSE_PAREN);
yield expr; yield expr;
} }
case IDENTIFIER -> { case IDENTIFIER -> {
if (scopeBuilder.containsFunction(token.getContent())) if (scopeBuilder.containsFunction(token.getContent()))
yield parseFunctionInvocation(false, token, scopeBuilder); yield parseFunctionInvocation(token, scopeBuilder);
else if (scopeBuilder.containsVariable(token.getContent())) { else if (scopeBuilder.containsVariable(token.getContent())) {
ReturnType variableType = scopeBuilder.getVaraibleType(token.getContent()); ReturnType variableType = scopeBuilder.getVaraibleType(token.getContent());
yield switch(variableType) { yield switch(variableType) {
@@ -274,9 +290,9 @@ public class Parser {
default -> throw new ParseException("Illegal type for variable reference: " + variableType, token.getPosition()); 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); Expression<?> expr = higherPrecedence.apply(scopeBuilder);
TokenType[] opTypes = operators.keySet().toArray(new TokenType[0]); TokenType[] opTypes = operators.keySet().toArray(new TokenType[0]);
while (lexer.current().isType(opTypes)) { while (lexer.current().isType(opTypes)) {
Token operator = lexer.consume(); Token operator = lexer.consumeUnchecked();
Expression<?> right = higherPrecedence.apply(scopeBuilder); Expression<?> right = higherPrecedence.apply(scopeBuilder);
BinaryOperationInfo op = new BinaryOperationInfo(expr, operator, right); BinaryOperationInfo op = new BinaryOperationInfo(expr, operator, right);
init.accept(op); init.accept(op);
@@ -302,24 +318,21 @@ public class Parser {
private record BinaryOperationInfo(Expression<?> left, Token operator, Expression<?> right) {} private record BinaryOperationInfo(Expression<?> left, Token operator, Expression<?> right) {}
private Expression<?> parseDeclaration(ScopeBuilder scopeBuilder) { 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(); return switch(lexer.current().getType()) {
ParserUtil.ensureType(identifier, TokenType.IDENTIFIER);
Token declarationType = lexer.consume();
ParserUtil.ensureType(declarationType, TokenType.ASSIGNMENT, TokenType.OPEN_PAREN);
return switch(declarationType.getType()) {
case ASSIGNMENT -> parseVariableDeclaration(scopeBuilder, type, identifier); case ASSIGNMENT -> parseVariableDeclaration(scopeBuilder, type, identifier);
case OPEN_PAREN -> parseFunctionDeclaration(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) { 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())) if(scopeBuilder.containsVariable(identifier.getContent()))
throw new ParseException(identifier.getContent() + " is already defined in this scope", identifier.getPosition()); 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) { 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())) if(scopeBuilder.containsVariable(identifier.getContent()))
throw new ParseException(identifier.getContent() + " is already defined in this scope", identifier.getPosition()); throw new ParseException(identifier.getContent() + " is already defined in this scope", identifier.getPosition());
@@ -349,43 +365,41 @@ public class Parser {
ScopeBuilder functionBodyScope = scopeBuilder.functionScope(); ScopeBuilder functionBodyScope = scopeBuilder.functionScope();
// Declare argument names into function body scope // Declare parameter names into function body scope
List<Pair<Integer, ReturnType>> argumentInfo = getFunctionArgumentsDeclaration().stream().map( List<Pair<Integer, ReturnType>> parameterInfo = getFunctionParameterDeclaration().stream().map(
arg -> Pair.of(switch(arg.getRight()) { arg -> Pair.of(switch(arg.getRight()) {
case NUMBER -> functionBodyScope.declareNum(arg.getLeft()); case NUMBER -> functionBodyScope.declareNum(arg.getLeft());
case BOOLEAN -> functionBodyScope.declareBool(arg.getLeft()); case BOOLEAN -> functionBodyScope.declareBool(arg.getLeft());
case STRING -> functionBodyScope.declareStr(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(); }, arg.getRight())).toList();
Block body = parseStatementBlock(functionBodyScope, returnType); 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); scopeBuilder.registerFunction(identifier.getContent(), functionBuilder);
return Expression.NOOP; return Expression.NOOP;
} }
private List<Pair<String, ReturnType>> getFunctionArgumentsDeclaration() { private List<Pair<String, ReturnType>> getFunctionParameterDeclaration() {
List<Pair<String, ReturnType>> arguments = new ArrayList<>(); List<Pair<String, ReturnType>> parameters = new ArrayList<>();
while(lexer.current().getType() != TokenType.CLOSE_PAREN) { while(lexer.current().getType() != TokenType.CLOSE_PAREN) {
// Parse argument type // Parse parameter type
Token typeToken = lexer.consume(); Token typeToken = lexer.consume("Expected function parameter type declaration", TokenType.TYPE_BOOLEAN, TokenType.TYPE_STRING, TokenType.TYPE_NUMBER);
ParserUtil.ensureType(typeToken, TokenType.TYPE_BOOLEAN, TokenType.TYPE_STRING, TokenType.TYPE_NUMBER); ReturnType type = ParserUtil.getVariableReturnType(typeToken);
ReturnType argType = ParserUtil.getVariableReturnType(typeToken);
// Parse argument name // Parse parameter name
Token identifierToken = lexer.consume(); Token identifierToken = lexer.consume("Expected function parameter identifier", TokenType.IDENTIFIER);
ParserUtil.ensureType(identifierToken, TokenType.IDENTIFIER); String name = identifierToken.getContent();
String argName = identifierToken.getContent();
arguments.add(Pair.of(argName, argType)); parameters.add(Pair.of(name, type));
// Consume separator if present, trailing separators are allowed // 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); lexer.consume("Expected ')' after function parameter declaration", TokenType.CLOSE_PAREN);
return arguments; return parameters;
} }
private Block parseBlock(ScopeBuilder scopeBuilder, ReturnType blockReturnType) { private Block parseBlock(ScopeBuilder scopeBuilder, ReturnType blockReturnType) {
@@ -397,7 +411,7 @@ public class Parser {
// Parse each statement // Parse each statement
while(lexer.hasNext() && !lexer.current().isType(TokenType.BLOCK_END)) { while(lexer.hasNext() && !lexer.current().isType(TokenType.BLOCK_END)) {
Expression<?> expression = parseStatement(lexer, scopeBuilder); Expression<?> expression = parseStatement(scopeBuilder);
if(expression != Expression.NOOP) { if(expression != Expression.NOOP) {
expressions.add(expression); expressions.add(expression);
} }
@@ -416,42 +430,36 @@ public class Parser {
return new Block(expressions, startPosition, blockReturnType); return new Block(expressions, startPosition, blockReturnType);
} }
private Expression<?> parseStatement(Lexer lexer, ScopeBuilder scopeBuilder) { private Expression<?> parseStatement(ScopeBuilder scopeBuilder) {
Token token = lexer.current(); 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()) { Expression<?> expression = switch(token.getType()) {
case FOR_LOOP -> parseForLoop(scopeBuilder); case FOR_LOOP -> parseForLoop(scopeBuilder);
case IF_STATEMENT -> parseIfStatement(scopeBuilder); case IF_STATEMENT -> parseIfStatement(scopeBuilder);
case WHILE_LOOP -> parseWhileLoop(scopeBuilder); case WHILE_LOOP -> parseWhileLoop(scopeBuilder);
case IDENTIFIER -> { case IDENTIFIER -> {
if(scopeBuilder.containsVariable(token.getContent())) yield parseAssignment(lexer.consume(), scopeBuilder); // Assume variable assignment if(scopeBuilder.containsVariable(token.getContent())) yield parseAssignment(scopeBuilder); // Assume variable assignment
else yield parseFunctionInvocation(true, lexer.consume(), scopeBuilder); else yield parseFunctionInvocation(lexer.consumeUnchecked(), scopeBuilder);
} }
case TYPE_NUMBER, TYPE_STRING, TYPE_BOOLEAN, TYPE_VOID -> parseDeclaration(scopeBuilder); case TYPE_NUMBER, TYPE_STRING, TYPE_BOOLEAN, TYPE_VOID -> parseDeclaration(scopeBuilder);
case RETURN -> parseReturn(scopeBuilder); case RETURN -> parseReturn(scopeBuilder);
case BREAK -> new BreakKeyword(lexer.consume().getPosition()); case BREAK -> {
case CONTINUE -> new ContinueKeyword(lexer.consume().getPosition()); if (!scopeBuilder.isInLoop()) throw new ParseException("Break statements can only be defined inside loops", token.getPosition());
case FAIL -> new FailKeyword(lexer.consume().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()); 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; return expression;
} }
private ReturnKeyword parseReturn(ScopeBuilder scopeBuilder) { private ReturnKeyword parseReturn(ScopeBuilder scopeBuilder) {
Token returnToken = lexer.consume(); Token returnToken = lexer.consume("Expected 'return' keyword at beginning of return statement", TokenType.RETURN);
ParserUtil.ensureType(returnToken, TokenType.RETURN);
Expression<?> data = null; Expression<?> data = null;
if(!lexer.current().isType(TokenType.STATEMENT_END)) { if(!lexer.current().isType(TokenType.STATEMENT_END)) {
data = parseExpression(scopeBuilder); data = parseExpression(scopeBuilder);
@@ -459,10 +467,10 @@ public class Parser {
return new ReturnKeyword(data, returnToken.getPosition()); return new ReturnKeyword(data, returnToken.getPosition());
} }
private VariableAssignmentNode<?> parseAssignment(Token identifier, ScopeBuilder scopeBuilder) { private VariableAssignmentNode<?> parseAssignment(ScopeBuilder scopeBuilder) {
ParserUtil.ensureType(identifier, TokenType.IDENTIFIER); 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); 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())) 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 List<Expression<?>> args = new ArrayList<>();
while(!lexer.current().isType(TokenType.CLOSE_PAREN)) {
if(fullStatement) ParserUtil.ensureType(lexer.current(), TokenType.STATEMENT_END); 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())) { if(ignoredFunctions.contains(identifier.getContent())) {
return Expression.NOOP; return Expression.NOOP;
} }
if(scopeBuilder.containsFunction(identifier.getContent())) { if(builder.argNumber() != -1 && args.size() != builder.argNumber())
FunctionBuilder<?> builder = scopeBuilder.getFunction(identifier.getContent()); throw new ParseException("Expected " + builder.argNumber() + " arguments, found " + args.size(), identifier.getPosition());
if(builder.argNumber() != -1 && args.size() != builder.argNumber()) for(int i = 0; i < args.size(); i++) {
throw new ParseException("Expected " + builder.argNumber() + " arguments, found " + args.size(), identifier.getPosition()); Expression<?> argument = args.get(i);
if(builder.getArgument(i) == null)
for(int i = 0; i < args.size(); i++) { throw new ParseException("Unexpected argument at position " + i + " in function " + identifier.getContent(),
Expression<?> argument = args.get(i); identifier.getPosition());
if(builder.getArgument(i) == null) ParserUtil.ensureReturnType(argument, builder.getArgument(i));
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());
} }
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 class ParserUtil {
public static void ensureType(Token token, TokenType... expected) { // public static void ensureType(Token token, TokenType... expected) {
for(TokenType type : expected) if(token.getType().equals(type)) return; // for(TokenType type : expected) if(token.getType().equals(type)) return;
throw new ParseException("Expected " + Arrays.toString(expected) + " but found " + token.getType(), token.getPosition()); // throw new ParseException("Expected " + Arrays.toString(expected) + " but found " + token.getType(), token.getPosition());
} // }
public static void ensureReturnType(Expression<?> returnable, Expression.ReturnType... types) { public static void ensureReturnType(Expression<?> returnable, Expression.ReturnType... types) {
for(Expression.ReturnType type : types) if(returnable.returnType().equals(type)) return; 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) { public static Expression.ReturnType getVariableReturnType(Token varToken) {
@@ -29,7 +29,7 @@ public class ParseException extends RuntimeException {
@Override @Override
public String getMessage() { public String getMessage() {
return super.getMessage() + ": " + position; return "Error at " + position + ": " + super.getMessage();
} }
public SourcePosition getPosition() { public SourcePosition getPosition() {
@@ -16,6 +16,9 @@ import com.dfsek.terra.addons.terrascript.parser.lang.Expression;
public interface FunctionBuilder<T extends Function<?>> { public interface FunctionBuilder<T extends Function<?>> {
T build(List<Expression<?>> argumentList, SourcePosition position); T build(List<Expression<?>> argumentList, SourcePosition position);
/**
* @return Number of function arguments, -1 if the function uses a vararg at the end
*/
int argNumber(); int argNumber();
Expression.ReturnType getArgument(int position); 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> { public class UserDefinedFunctionBuilder<T extends Function<?>> implements FunctionBuilder<T> {
private final ReturnType returnType; private final ReturnType returnType;
private final List<Pair<Integer, ReturnType>> argumentInfo; private final List<Pair<Integer, ReturnType>> parameterInfo;
private final ScopeBuilder bodyScopeBuilder; private final ScopeBuilder bodyScopeBuilder;
private final Block body; 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) { ScopeBuilder functionBodyScope) {
this.returnType = returnType; this.returnType = returnType;
this.bodyScopeBuilder = functionBodyScope; this.bodyScopeBuilder = functionBodyScope;
this.body = body; this.body = body;
this.argumentInfo = argumentInfo; this.parameterInfo = parameterInfo;
} }
@Override @Override
@@ -44,12 +44,12 @@ public class UserDefinedFunctionBuilder<T extends Function<?>> implements Functi
Scope bodyScope = threadLocalScope.get(); Scope bodyScope = threadLocalScope.get();
// Pass arguments into scope of function body // Pass arguments into scope of function body
for(int i = 0; i < argumentList.size(); i++) { 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); Expression<?> argExpression = argumentList.get(i);
switch(argInfo.getRight()) { switch(paramInfo.getRight()) {
case NUMBER -> bodyScope.setNum(argInfo.getLeft(), argExpression.applyDouble(implementationArguments, scope)); case NUMBER -> bodyScope.setNum(paramInfo.getLeft(), argExpression.applyDouble(implementationArguments, scope));
case BOOLEAN -> bodyScope.setBool(argInfo.getLeft(), argExpression.applyBoolean(implementationArguments, scope)); case BOOLEAN -> bodyScope.setBool(paramInfo.getLeft(), argExpression.applyBoolean(implementationArguments, scope));
case STRING -> bodyScope.setStr(argInfo.getLeft(), (String) argExpression.evaluate(implementationArguments, scope)); case STRING -> bodyScope.setStr(paramInfo.getLeft(), (String) argExpression.evaluate(implementationArguments, scope));
} }
} }
return body.evaluate(implementationArguments, bodyScope).data().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 @Override
public int argNumber() { public int argNumber() {
return argumentInfo.size(); return parameterInfo.size();
} }
@Override @Override
public ReturnType getArgument(int position) { public ReturnType getArgument(int position) {
return argumentInfo.get(position).getRight(); return parameterInfo.get(position).getRight();
} }
} }
@@ -7,23 +7,80 @@
package structure; 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 org.junit.jupiter.api.Test;
import java.io.StringReader; import static org.junit.jupiter.api.Assertions.*;
import com.dfsek.terra.addons.terrascript.lexer.LookaheadStream;
public class LookaheadStreamTest { public class LookaheadStreamTest {
@Test @Test
public void lookahead() { 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++) { LookaheadStream lookahead = new LookaheadStream(testString);
System.out.print(lookahead.next(i).getCharacter());
} Char first = new Char('T', new SourcePosition(1, 1));
while(lookahead.next(0) != null) { Char second = new Char('e', new SourcePosition(1, 2));
System.out.print(lookahead.consume().getCharacter()); 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; bool thing1 = 2 > (2+2) || false;
if(2 > 2 || 3 + 4 <= 2 && 4 + 5 > 2 / 3) { if(2 > 2 || 3 + 4 <= 2 && 4 + 5 > 2 / 3) {
@@ -29,7 +46,9 @@ test("-2 = " + thing, 2);
thing = -thing; thing = -thing;
test("--2 = " + thing, 2); test("--2 = " + thing, 2);
for(;;) {
break;
}
for(num i = 0; i < 5; i = i + 1) { for(num i = 0; i < 5; i = i + 1) {
test("i = " + i, iterator); test("i = " + i, iterator);