509 lines
19 KiB
Java
509 lines
19 KiB
Java
package io.gitlab.jfronny.muscript.compiler;
|
|
|
|
import io.gitlab.jfronny.muscript.ast.*;
|
|
import io.gitlab.jfronny.muscript.ast.bool.*;
|
|
import io.gitlab.jfronny.muscript.ast.compare.Equal;
|
|
import io.gitlab.jfronny.muscript.ast.compare.Greater;
|
|
import io.gitlab.jfronny.muscript.ast.conditional.UnresolvedConditional;
|
|
import io.gitlab.jfronny.muscript.ast.dynamic.*;
|
|
import io.gitlab.jfronny.muscript.ast.dynamic.assign.DynamicAssign;
|
|
import io.gitlab.jfronny.muscript.ast.literal.DynamicLiteral;
|
|
import io.gitlab.jfronny.muscript.ast.math.*;
|
|
import io.gitlab.jfronny.muscript.ast.string.Concatenate;
|
|
import io.gitlab.jfronny.muscript.compiler.lexer.LegacyLexer;
|
|
import io.gitlab.jfronny.muscript.compiler.lexer.Lexer;
|
|
import io.gitlab.jfronny.muscript.data.Script;
|
|
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
|
|
import io.gitlab.jfronny.muscript.error.*;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import java.util.*;
|
|
|
|
public class Parser extends VersionedComponent {
|
|
private final Lexer lexer;
|
|
|
|
private Lexer.Token previous = null;
|
|
|
|
public static Expr<?> parse(MuScriptVersion version, String source) {
|
|
return parse(version, source, null);
|
|
}
|
|
|
|
public static Expr<?> parse(MuScriptVersion version, String source, String file) {
|
|
return new Parser(new LegacyLexer(version, source, file)).parse().optimize();
|
|
}
|
|
|
|
public static Script parseScript(MuScriptVersion version, String source) {
|
|
return parseScript(version, source, null);
|
|
}
|
|
|
|
public static Script parseScript(MuScriptVersion version, String source, String file) {
|
|
return new Parser(new LegacyLexer(version, source, file)).parseScript().optimize();
|
|
}
|
|
|
|
public static Script parseMultiScript(MuScriptVersion version, String startFile, SourceFS filesystem) {
|
|
return new Script(parseMultiScript(version, startFile, filesystem, new HashSet<>()).stream().flatMap(Script::stream).toList());
|
|
}
|
|
|
|
private static List<Script> parseMultiScript(MuScriptVersion version, String startFile, SourceFS filesystem, Set<String> alreadyIncluded) {
|
|
alreadyIncluded.add(startFile);
|
|
boolean isIncludes = true;
|
|
StringBuilder src = new StringBuilder();
|
|
List<Script> includes = new LinkedList<>();
|
|
int row = 0;
|
|
final String includePrefix = "#include ";
|
|
for (String s : filesystem.read(startFile).split("\n")) {
|
|
row++;
|
|
if (s.isBlank()) {
|
|
src.append("\n");
|
|
} else if (s.startsWith(includePrefix)) {
|
|
if (isIncludes) {
|
|
String file = s.substring(includePrefix.length());
|
|
src.append("// include ").append(file).append("\n");
|
|
if (!alreadyIncluded.contains(file)) {
|
|
includes.addAll(parseMultiScript(version, file, filesystem, alreadyIncluded));
|
|
}
|
|
} else {
|
|
throw new ParseException(PrettyPrintError.builder()
|
|
.setLocation(new PrettyPrintError.Location(s, 0, row), new PrettyPrintError.Location(s, s.length() - 1, row))
|
|
.setMessage("Includes MUST be located at the top of the file")
|
|
.build());
|
|
}
|
|
} else {
|
|
isIncludes = false;
|
|
src.append(s).append("\n");
|
|
}
|
|
}
|
|
includes.add(parseScript(version, src.toString(), startFile));
|
|
return includes;
|
|
}
|
|
|
|
public Parser(io.gitlab.jfronny.muscript.compiler.Lexer lexer) {
|
|
this(new LegacyLexer(lexer));
|
|
}
|
|
|
|
public Parser(Lexer lexer) {
|
|
super(lexer.version());
|
|
this.lexer = lexer;
|
|
}
|
|
|
|
/**
|
|
* Generate a single expression.
|
|
* Must exhaust the source!
|
|
*
|
|
* @return the resulting expression
|
|
*/
|
|
public Expr<?> parse() {
|
|
advance();
|
|
Expr<?> expr = expression();
|
|
if (!isAtEnd() && version.contains(MuScriptVersion.V2))
|
|
throw new ParseException(PrettyPrintError.builder(lexer.location()).setMessage("Unexpected element after end of expression").build());
|
|
return expr;
|
|
}
|
|
|
|
/**
|
|
* Generate a script instance.
|
|
* Multiple instructions will be executed in sequence and the result of the last one will be returned.
|
|
*
|
|
* @return the resulting expression
|
|
*/
|
|
public Script parseScript() {
|
|
advance();
|
|
List<Expr<?>> expressions = new LinkedList<>();
|
|
while (!isAtEnd()) {
|
|
expressions.add(expression());
|
|
// Consume semicolon if present
|
|
if (!lexer.wasNewlinePassed() & !match(Token.Semicolon) & !isAtEnd() & version.contains(MuScriptVersion.V3)) {
|
|
throw error("Either a semicolon or a new line must separate expressions in scripts");
|
|
}
|
|
}
|
|
if (expressions.isEmpty()) throw new ParseException(PrettyPrintError.builder(lexer.location()).setMessage("Missing any elements in closure").build());
|
|
return new Script(expressions);
|
|
}
|
|
|
|
// Expressions
|
|
private Expr<?> expression() {
|
|
try {
|
|
return conditional();
|
|
} catch (RuntimeException e) {
|
|
if (e instanceof ParseException) throw e;
|
|
else if (e instanceof LocationalException le) {
|
|
throw new ParseException(le.asPrintable(), le.getCause());
|
|
} else throw error(e.getMessage());
|
|
}
|
|
}
|
|
|
|
private Expr<?> conditional() {
|
|
Expr<?> expr = and();
|
|
|
|
if (match(Token.QuestionMark)) {
|
|
CodeLocation location = previous.location();
|
|
Expr<?> trueExpr = expression();
|
|
consume(Token.Colon, "Expected ':' after first part of condition");
|
|
Expr<?> falseExpr = expression();
|
|
expr = new UnresolvedConditional(location, asBool(expr), trueExpr, falseExpr);
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> and() {
|
|
Expr<?> expr = or();
|
|
|
|
while (match(Token.And)) {
|
|
CodeLocation location = previous.location();
|
|
Expr<?> right = or();
|
|
expr = new And(location, asBool(expr), asBool(right));
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> or() {
|
|
Expr<?> expr = equality();
|
|
|
|
while (match(Token.Or)) {
|
|
CodeLocation location = previous.location();
|
|
Expr<?> right = equality();
|
|
expr = new Or(location, asBool(expr), asBool(right));
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> equality() {
|
|
Expr<?> expr = concat();
|
|
|
|
while (match(Token.EqualEqual, Token.BangEqual)) {
|
|
Token op = previous.token();
|
|
CodeLocation location = previous.location();
|
|
Expr<?> right = concat();
|
|
BoolExpr e = new Equal(location, expr, right);
|
|
if (op == Token.BangEqual) e = new Not(location, e);
|
|
expr = e;
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> concat() {
|
|
Expr<?> expr = comparison();
|
|
|
|
while (match(Token.Concat)) {
|
|
CodeLocation location = previous.location();
|
|
Expr<?> right = comparison();
|
|
expr = new Concatenate(location, asString(expr), asString(right));
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> comparison() {
|
|
Expr<?> expr = term();
|
|
|
|
while (match(Token.Greater, Token.GreaterEqual, Token.Less, Token.LessEqual)) {
|
|
Token op = previous.token();
|
|
CodeLocation location = previous.location();
|
|
NumberExpr right = asNumber(term());
|
|
expr = switch (op) {
|
|
case Greater -> new Greater(location, asNumber(expr), right);
|
|
case GreaterEqual -> new Not(location, new Greater(location, right, asNumber(expr)));
|
|
case Less -> new Greater(location, right, asNumber(expr));
|
|
case LessEqual -> new Not(location, new Greater(location, asNumber(expr), right));
|
|
default -> throw new IllegalStateException();
|
|
};
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> term() {
|
|
Expr<?> expr = factor();
|
|
|
|
while (match(Token.Plus, Token.Minus)) {
|
|
Token op = previous.token();
|
|
CodeLocation location = previous.location();
|
|
NumberExpr right = asNumber(factor());
|
|
expr = switch (op) {
|
|
case Plus -> new Plus(location, asNumber(expr), right);
|
|
case Minus -> new Minus(location, asNumber(expr), right);
|
|
default -> throw new IllegalStateException();
|
|
};
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> factor() {
|
|
Expr<?> expr = exp();
|
|
|
|
while (match(Token.Star, Token.Slash, Token.Percentage)) {
|
|
Token op = previous.token();
|
|
CodeLocation location = previous.location();
|
|
NumberExpr right = asNumber(exp());
|
|
expr = switch (op) {
|
|
case Star -> new Multiply(location, asNumber(expr), right);
|
|
case Slash -> new Divide(location, asNumber(expr), right);
|
|
case Percentage -> new Modulo(location, asNumber(expr), right);
|
|
default -> throw new IllegalStateException();
|
|
};
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> exp() {
|
|
Expr<?> expr = unary();
|
|
|
|
while (match(Token.UpArrow)) {
|
|
CodeLocation location = previous.location();
|
|
NumberExpr right = asNumber(unary());
|
|
expr = new Power(location, asNumber(expr), right);
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> unary() {
|
|
if (match(Token.Bang, Token.Minus)) {
|
|
Token op = previous.token();
|
|
CodeLocation location = previous.location();
|
|
Expr<?> right = unary();
|
|
return switch (op) {
|
|
case Bang -> new Not(location, asBool(right));
|
|
case Minus -> new Invert(location, asNumber(right));
|
|
default -> throw new IllegalStateException();
|
|
};
|
|
}
|
|
|
|
return call();
|
|
}
|
|
|
|
private Expr<?> call() {
|
|
Expr<?> expr = primary();
|
|
|
|
while (match(Token.LeftParen, Token.Dot, Token.LeftBracket, Token.DoubleColon)) {
|
|
CodeLocation location = previous.location();
|
|
expr = switch (previous.token()) {
|
|
case LeftParen -> finishCall(location, expr);
|
|
case Dot -> {
|
|
Lexer.Token name = consume(Token.Identifier, "Expected field name after '.'");
|
|
yield new Get(location, asDynamic(expr), Expr.literal(name.location(), name.lexeme()));
|
|
}
|
|
case DoubleColon -> {
|
|
DynamicExpr callable;
|
|
if (match(Token.Identifier)) {
|
|
callable = new Variable(previous.location(), previous.lexeme());
|
|
} else if (match(Token.LeftParen)) {
|
|
callable = expression().asDynamicExpr();
|
|
consume(Token.RightParen, "Expected ')' after expression");
|
|
} else throw error("Bind operator requires right side to be a literal identifier or to be wrapped in parentheses.");
|
|
yield new Bind(location, callable, expr.asDynamicExpr());
|
|
}
|
|
case LeftBracket -> {
|
|
expr = new Get(location, asDynamic(expr), expression());
|
|
consume(Token.RightBracket, "Expected closing bracket");
|
|
yield expr;
|
|
}
|
|
default -> throw new IllegalStateException();
|
|
};
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr<?> finishCall(CodeLocation location, Expr<?> callee) {
|
|
List<Call.Arg> args = new ArrayList<>(2);
|
|
|
|
if (!check(Token.RightParen)) {
|
|
do {
|
|
args.add(new Call.Arg(asDynamic(expression()), match(Token.Ellipsis)));
|
|
} while (match(Token.Comma));
|
|
}
|
|
|
|
consume(Token.RightParen, "Expected ')' after function arguments");
|
|
return new Call(location, asDynamic(callee), args);
|
|
}
|
|
|
|
private Expr<?> primary() {
|
|
if (match(Token.Null)) return Expr.literalNull(previous.location());
|
|
if (match(Token.String)) return Expr.literal(previous.location(), previous.lexeme());
|
|
if (match(Token.True, Token.False)) return Expr.literal(previous.location(), previous.lexeme().equals("true"));
|
|
if (match(Token.Number)) return Expr.literal(previous.location(), Double.parseDouble(previous.lexeme()));
|
|
if (match(Token.Identifier)) {
|
|
CodeLocation location = previous.location();
|
|
String name = previous.lexeme();
|
|
if (match(Token.Assign)) return new DynamicAssign(location, name, expression().asDynamicExpr());
|
|
else return new Variable(location, name);
|
|
}
|
|
|
|
if (match(Token.LeftParen)) {
|
|
Expr<?> expr = expression();
|
|
consume(Token.RightParen, "Expected ')' after expression");
|
|
return expr;
|
|
}
|
|
|
|
if (match(Token.LeftBrace)) {
|
|
int start = previous.start();
|
|
if (match(Token.Arrow)) return finishClosure(start, null, false);
|
|
if (match(Token.RightBrace)) return new DynamicLiteral<>(location(start, previous.start()), DFinal.of(Map.of()));
|
|
consume(Token.Identifier, "Expected arrow or identifier as first element in closure or object");
|
|
String first = previous.lexeme();
|
|
if (check(Token.Arrow)) return finishClosure(start, first, false);
|
|
if (match(Token.Ellipsis)) return finishClosure(start, first, true);
|
|
if (check(Token.Comma)) return finishClosure(start, first, false);
|
|
if (match(Token.Assign)) {
|
|
return finishObject(start, first, expression().asDynamicExpr());
|
|
}
|
|
throw error("Unexpected");
|
|
}
|
|
|
|
throw error("Expected expression.");
|
|
}
|
|
|
|
private Expr<?> finishClosure(int start, @Nullable String firstArg, boolean firstVariadic) {
|
|
List<String> boundArgs = new LinkedList<>();
|
|
boolean variadic = false;
|
|
if (firstArg != null) {
|
|
boundArgs.add(firstArg);
|
|
if (firstVariadic) {
|
|
consume(Token.Arrow, "Variadic argument MUST be the last argument");
|
|
variadic = true;
|
|
} else {
|
|
while (!match(Token.Arrow)) {
|
|
consume(Token.Comma, "Closure parameters MUST be comma-seperated");
|
|
consume(Token.Identifier, "Closure arguments MUST be identifiers");
|
|
boundArgs.add(previous.lexeme());
|
|
if (match(Token.Ellipsis)) {
|
|
variadic = true;
|
|
consume(Token.Arrow, "Variadic argument MUST be the last argument");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
List<Expr<?>> expressions = new LinkedList<>();
|
|
while (!match(Token.RightBrace)) {
|
|
expressions.add(expression());
|
|
// Consume semicolon if present
|
|
if (!lexer.wasNewlinePassed() & !match(Token.Semicolon) & version.contains(MuScriptVersion.V3)) {
|
|
if (match(Token.RightBrace)) break;
|
|
throw error("Either a semicolon or a new line must separate expressions in closures");
|
|
}
|
|
}
|
|
int end = previous.start();
|
|
return new Closure(location(start, end), boundArgs, expressions, variadic);
|
|
}
|
|
|
|
private Expr<?> finishObject(int start, @Nullable String firstArg, @Nullable DynamicExpr firstValue) {
|
|
Map<String, DynamicExpr> content = new LinkedHashMap<>();
|
|
content.put(firstArg, firstValue);
|
|
while (match(Token.Comma)) {
|
|
consume(Token.Identifier, "Object element MUST start with an identifier");
|
|
String name = previous.lexeme();
|
|
consume(Token.Assign, "Object element name and value MUST be seperated with '='");
|
|
content.put(name, expression().asDynamicExpr());
|
|
}
|
|
consume(Token.RightBrace, "Expected end of object");
|
|
return new ObjectLiteral(location(start, previous.start()), content);
|
|
}
|
|
|
|
// Type conversion
|
|
private BoolExpr asBool(Expr<?> expression) {
|
|
try {
|
|
return expression.asBoolExpr();
|
|
} catch (TypeMismatchException e) {
|
|
throw error(e.getMessage(), expression);
|
|
}
|
|
}
|
|
|
|
private NumberExpr asNumber(Expr<?> expression) {
|
|
try {
|
|
return expression.asNumberExpr();
|
|
} catch (TypeMismatchException e) {
|
|
throw error(e.getMessage(), expression);
|
|
}
|
|
}
|
|
|
|
private StringExpr asString(Expr<?> expression) {
|
|
try {
|
|
return expression.asStringExpr();
|
|
} catch (TypeMismatchException e) {
|
|
throw error(e.getMessage(), expression);
|
|
}
|
|
}
|
|
|
|
private DynamicExpr asDynamic(Expr<?> expression) {
|
|
try {
|
|
return expression.asDynamicExpr();
|
|
} catch (TypeMismatchException e) {
|
|
throw error(e.getMessage(), expression);
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
private CodeLocation location(int chStart, int chEnd) {
|
|
return new CodeLocation(chStart, chEnd, lexer.getSource(), lexer.getFile());
|
|
}
|
|
|
|
private ParseException error(String message) {
|
|
int loc = lexer.getPrevious().current() - 1;
|
|
return new ParseException(PrettyPrintError.builder(location(loc, loc)).setMessage(message).build());
|
|
}
|
|
|
|
private ParseException error(String message, Expr<?> expr) {
|
|
return new ParseException(PrettyPrintError.builder(expr.location).setMessage(message).build());
|
|
}
|
|
|
|
private Lexer.Token consume(Token token, String message) {
|
|
if (check(token)) return advance();
|
|
throw error(message + " but got " + lexer.getPrevious().token());
|
|
}
|
|
|
|
private boolean match(Token... tokens) {
|
|
for (Token token : tokens) {
|
|
if (check(token)) {
|
|
advance();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private boolean check(Token token) {
|
|
if (isAtEnd()) return false;
|
|
return lexer.getPrevious().token() == token;
|
|
}
|
|
|
|
private Lexer.Token advance() {
|
|
previous = lexer.getPrevious();
|
|
|
|
lexer.advance();
|
|
|
|
if (lexer.getPrevious().token() == Token.Error) {
|
|
throw error(lexer.getPrevious().lexeme());
|
|
}
|
|
|
|
return previous;
|
|
}
|
|
|
|
private boolean isAtEnd() {
|
|
return lexer.getPrevious().token() == Token.EOF;
|
|
}
|
|
|
|
// Parse Exception
|
|
public static class ParseException extends RuntimeException {
|
|
public final PrettyPrintError error;
|
|
|
|
public ParseException(PrettyPrintError error) {
|
|
super(error.toString());
|
|
this.error = error;
|
|
}
|
|
|
|
public ParseException(PrettyPrintError error, Throwable cause) {
|
|
super(error.toString(), cause);
|
|
this.error = error;
|
|
}
|
|
}
|
|
}
|