feat(muscript): implement parsing

This commit is contained in:
Johannes Frohnmeyer 2024-04-05 14:20:55 +02:00
parent 1a210837f3
commit 55b3b4cbe0
Signed by: Johannes
GPG Key ID: E76429612C2929F4
15 changed files with 1153 additions and 0 deletions

View File

@ -0,0 +1,29 @@
import io.gitlab.jfronny.scripts.*
plugins {
commons.library
}
dependencies {
implementation(projects.commons)
api(projects.muscriptAst)
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = "io.gitlab.jfronny"
artifactId = "muscript-parser"
from(components["java"])
}
}
}
tasks.javadoc {
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons/$version/raw", projects.commons)
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/muscript-ast/$version/raw", projects.muscriptAst)
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.muscript.parser;
public enum MuScriptVersion {
V1, V2, V3;
public static final MuScriptVersion DEFAULT = V3;
public boolean contains(MuScriptVersion version) {
return compareTo(version) >= 0;
}
}

View File

@ -0,0 +1,509 @@
package io.gitlab.jfronny.muscript.parser;
import io.gitlab.jfronny.muscript.ast.*;
import io.gitlab.jfronny.muscript.ast.bool.And;
import io.gitlab.jfronny.muscript.ast.bool.Not;
import io.gitlab.jfronny.muscript.ast.bool.Or;
import io.gitlab.jfronny.muscript.ast.context.ExprUtils;
import io.gitlab.jfronny.muscript.ast.context.Script;
import io.gitlab.jfronny.muscript.ast.context.TypeMismatchException;
import io.gitlab.jfronny.muscript.ast.dynamic.*;
import io.gitlab.jfronny.muscript.ast.number.*;
import io.gitlab.jfronny.muscript.ast.string.Concatenate;
import io.gitlab.jfronny.muscript.core.CodeLocation;
import io.gitlab.jfronny.muscript.parser.lexer.LegacyLexer;
import io.gitlab.jfronny.muscript.parser.lexer.Lexer;
import io.gitlab.jfronny.muscript.parser.lexer.LexerImpl;
import io.gitlab.jfronny.muscript.parser.lexer.Token;
import io.gitlab.jfronny.muscript.parser.util.PrettyPrintError;
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();
}
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();
}
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(LexerImpl 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 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 DynamicConditional(location, asBool(expr), ExprUtils.asDynamic(trueExpr), ExprUtils.asDynamic(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 Equals(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 GreaterThan(location, asNumber(expr), right);
case GreaterEqual -> new Not(location, new GreaterThan(location, right, asNumber(expr)));
case Less -> new GreaterThan(location, right, asNumber(expr));
case LessEqual -> new Not(location, new GreaterThan(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 Add(location, asNumber(expr), right);
case Minus -> new Subtract(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 Negate(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)) {
Expr expr1 = expression();
callable = ExprUtils.asDynamic(expr1);
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, ExprUtils.asDynamic(expr));
}
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.Argument> args = new ArrayList<>(2);
if (!check(Token.RightParen)) {
do {
args.add(new Call.Argument(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 "this".equals(previous.lexeme()) ? new This(previous.location()) : 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)) {
Expr expr = expression();
return new DynamicAssign(location, name, ExprUtils.asDynamic(expr));
}
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 ObjectLiteral(location(start, previous.start()), 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)) {
Expr expr = expression();
return finishObject(start, first, ExprUtils.asDynamic(expr));
}
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, variadic, expressions);
}
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 '='");
Expr expr = expression();
content.put(name, ExprUtils.asDynamic(expr));
}
consume(Token.RightBrace, "Expected end of object");
return new ObjectLiteral(location(start, previous.start()), content);
}
// Type conversion
private BoolExpr asBool(Expr expression) {
try {
return ExprUtils.asBool(expression);
} catch (TypeMismatchException e) {
throw error(e.getMessage(), expression);
}
}
private NumberExpr asNumber(Expr expression) {
try {
return ExprUtils.asNumber(expression);
} catch (TypeMismatchException e) {
throw error(e.getMessage(), expression);
}
}
private StringExpr asString(Expr expression) {
try {
return ExprUtils.asString(expression);
} catch (TypeMismatchException e) {
throw error(e.getMessage(), expression);
}
}
private DynamicExpr asDynamic(Expr expression) {
return ExprUtils.asDynamic(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;
}
}
}

View File

@ -0,0 +1,14 @@
package io.gitlab.jfronny.muscript.parser;
import java.util.Map;
public interface SourceFS {
String read(String file);
record Immutable(Map<String, String> files) implements SourceFS {
@Override
public String read(String file) {
return files.get(file);
}
}
}

View File

@ -0,0 +1,5 @@
package io.gitlab.jfronny.muscript.parser;
public enum Type {
String, Number, Boolean, Dynamic
}

View File

@ -0,0 +1,15 @@
package io.gitlab.jfronny.muscript.parser;
import java.util.Objects;
public abstract class VersionedComponent {
protected final MuScriptVersion version;
public VersionedComponent(MuScriptVersion version) {
this.version = Objects.requireNonNull(version);
}
public MuScriptVersion getComponentVersion() {
return version;
}
}

View File

@ -0,0 +1,13 @@
package io.gitlab.jfronny.muscript.parser.impl;
import io.gitlab.jfronny.muscript.ast.Expr;
import io.gitlab.jfronny.muscript.ast.context.IExprParser;
import io.gitlab.jfronny.muscript.parser.MuScriptVersion;
import io.gitlab.jfronny.muscript.parser.Parser;
public class ExprParserImpl implements IExprParser {
@Override
public Expr parse(String expr) {
return Parser.parse(MuScriptVersion.DEFAULT, expr);
}
}

View File

@ -0,0 +1,68 @@
package io.gitlab.jfronny.muscript.parser.lexer;
import io.gitlab.jfronny.muscript.core.CodeLocation;
import io.gitlab.jfronny.muscript.parser.MuScriptVersion;
import org.jetbrains.annotations.Nullable;
/**
* Wraps the old Lexer implementation in the new Lexer interface for compatibility
*/
public class LegacyLexer implements Lexer {
private final LexerImpl backend;
private Token previous = null;
public LegacyLexer(MuScriptVersion version, String source) {
this(new LexerImpl(version, source));
}
public LegacyLexer(MuScriptVersion version, String source, String file) {
this(new LexerImpl(version, source, file));
}
public LegacyLexer(LexerImpl backend) {
this.backend = backend;
}
@Override
public CodeLocation location() {
return backend.location();
}
@Override
public MuScriptVersion version() {
return backend.getComponentVersion();
}
@Override
public boolean wasNewlinePassed() {
return backend.passedNewline;
}
@Override
public Token getPrevious() {
return previous;
}
@Override
public Token advance() {
backend.next();
return previous = new Token(
backend.lexeme,
backend.token,
backend.start,
backend.current,
backend.ch,
new CodeLocation(backend.start, backend.current - 1, backend.source, backend.file)
);
}
@Override
public @Nullable String getSource() {
return backend.source;
}
@Override
public @Nullable String getFile() {
return backend.file;
}
}

View File

@ -0,0 +1,23 @@
package io.gitlab.jfronny.muscript.parser.lexer;
import io.gitlab.jfronny.muscript.core.CodeLocation;
import io.gitlab.jfronny.muscript.parser.MuScriptVersion;
import org.jetbrains.annotations.Nullable;
public interface Lexer {
CodeLocation location();
MuScriptVersion version();
boolean wasNewlinePassed();
Token getPrevious();
Token advance();
@Nullable String getSource();
@Nullable String getFile();
record Token(String lexeme, io.gitlab.jfronny.muscript.parser.lexer.Token token, int start, int current, char ch, CodeLocation location) {
@Override
public String toString() {
return token + " '" + lexeme + "'";
}
}
}

View File

@ -0,0 +1,268 @@
package io.gitlab.jfronny.muscript.parser.lexer;
import io.gitlab.jfronny.muscript.core.CodeLocation;
import io.gitlab.jfronny.muscript.parser.MuScriptVersion;
import io.gitlab.jfronny.muscript.parser.VersionedComponent;
import java.util.Objects;
//TODO refactor to use the new Lexer interface directly
/**
* The lexer for muScript, heavily inspired by starscript
*/
public class LexerImpl extends VersionedComponent {
public final String file;
/**
* The type of the token
*/
public Token token;
/**
* The string representation of the token
*/
public String lexeme;
public char ch;
public final String source;
public int start, current;
public boolean passedNewline = false;
public LexerImpl(MuScriptVersion version, String source) {
this(version, source, null);
}
public LexerImpl(MuScriptVersion version, String source, String file) {
super(version);
this.source = Objects.requireNonNull(source);
this.file = file;
}
public CodeLocation location() {
return new CodeLocation(start, current - 1, source, file);
}
/**
* Scans for the next token storing it in {@link LexerImpl#token} and {@link LexerImpl#lexeme}. Produces {@link Token#EOF} if the end of source code has been reached and {@link Token#Error} if there has been an error
*/
public void next() {
start = current;
passedNewline = false;
if (isAtEnd()) {
createToken(Token.EOF);
return;
}
// Scan expression
skipWhitespaceAndComments();
if (isAtEnd()) {
createToken(Token.EOF);
return;
}
char c = advance();
if (isDigit(c)) number();
else if (isIdentifier(c)) identifier(false);
else {
switch (c) {
case '\'', '"' -> string(c);
case '`' -> identifier(true);
case '=' -> {
if (match('=')) createToken(Token.EqualEqual);
else createToken(Token.Assign);
}
case '!' -> createToken(match('=') ? Token.BangEqual : Token.Bang);
case '+' -> createToken(Token.Plus);
case '-' -> {
if (match('>')) createToken(Token.Arrow);
else createToken(Token.Minus);
}
case '*' -> createToken(Token.Star);
case '/' -> createToken(Token.Slash);
case '%' -> createToken(Token.Percentage);
case '>' -> createToken(match('=') ? Token.GreaterEqual : Token.Greater);
case '<' -> createToken(match('=') ? Token.LessEqual : Token.Less);
case '&' -> createToken(Token.And);
case '|' -> createToken(match('|') ? Token.Concat : Token.Or);
case '^' -> createToken(Token.UpArrow);
case '.' -> {
if (match('.')) {
if (match('.')) createToken(Token.Ellipsis);
else createToken(Token.Error, "Unexpected '..', did you mean '...'?");
} else createToken(Token.Dot);
}
case ',' -> createToken(Token.Comma);
case '?' -> createToken(Token.QuestionMark);
case ':' -> createToken(match(':') ? Token.DoubleColon : Token.Colon);
case '(' -> createToken(Token.LeftParen);
case ')' -> createToken(Token.RightParen);
case '[' -> createToken(Token.LeftBracket);
case ']' -> createToken(Token.RightBracket);
case '{' -> createToken(Token.LeftBrace);
case '}' -> createToken(Token.RightBrace);
case ';' -> {
createToken(Token.Semicolon);
passedNewline = true;
}
default -> unexpected();
}
}
}
private void string(char stringChar) {
while (!isAtEnd() && peek() != stringChar) {
advance();
}
if (isAtEnd()) {
createToken(Token.Error, "Unterminated expression");
} else {
advance();
createToken(Token.String, source.substring(start + 1, current - 1));
}
}
private void number() {
while (isDigit(peek())) advance();
if (peek() == '.' && isDigit(peekNext())) {
advance();
while (isDigit(peek())) advance();
}
createToken(Token.Number);
}
private void identifier(boolean explicit) {
if (explicit) {
while (!isAtEnd() && peek() != '`') {
advance();
}
if (isAtEnd()) {
createToken(Token.Error, "Unterminated expression");
} else {
advance();
createToken(Token.Identifier, source.substring(start + 1, current - 1));
}
} else {
while (!isAtEnd()) {
char c = peek();
if (isIdentifier(c) || isDigit(c)) {
advance();
} else break;
}
createToken(Token.Identifier);
switch (lexeme) {
case "null" -> token = Token.Null;
case "true" -> token = Token.True;
case "false" -> token = Token.False;
}
}
}
private void skipWhitespaceAndComments() {
while (true) {
if (isAtEnd()) return;
switch (peek()) {
case ' ', '\t' -> advance();
case '\r', '\n' -> {
advance();
passedNewline = true;
}
case '/' -> {
switch (peekNext()) {
case '/' -> {
while (!isAtEnd() && peek() != '\r' && peek() != '\n') advance();
}
case '*' -> {
advance();
advance();
while (!isAtEnd() && (peek() != '*' || peekNext() != '/')) advance();
if (!isAtEnd()) {
advance();
advance();
}
}
default -> {
start = current;
return;
}
}
}
default -> {
start = current;
return;
}
}
}
}
// Helpers
private void unexpected() {
createToken(Token.Error, "Unexpected character");
}
private void createToken(Token token, String lexeme) {
this.token = token;
this.lexeme = lexeme;
}
private void createToken(Token token) {
createToken(token, source.substring(start, current));
}
private boolean match(char expected) {
if (isAtEnd()) return false;
if (source.charAt(current) != expected) return false;
advance();
return true;
}
private char advance() {
return ch = source.charAt(current++);
}
private char peek() {
if (isAtEnd()) return '\0';
return source.charAt(current);
}
private char peekNext() {
if (current + 1 >= source.length()) return '\0';
return source.charAt(current + 1);
}
private boolean isAtEnd() {
return current >= source.length();
}
private boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
private boolean isIdentifier(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$';
}
// Visualization
@Override
public String toString() {
return source.substring(0, start) + '[' + source.substring(start, current) + ']' + source.substring(current);
}
}

View File

@ -0,0 +1,35 @@
package io.gitlab.jfronny.muscript.parser.lexer;
public enum Token {
String, Identifier, Number,
Null,
True, False,
And, Or,
Assign,
EqualEqual, BangEqual,
Concat,
Greater, GreaterEqual,
Less, LessEqual,
Plus, Minus,
Star, Slash, Percentage, UpArrow,
Bang,
Dot, Ellipsis, Comma,
QuestionMark, Colon,
LeftParen, RightParen,
LeftBracket, RightBracket,
LeftBrace, RightBrace,
Arrow,
Semicolon,
DoubleColon,
Error, EOF
}

View File

@ -0,0 +1,131 @@
package io.gitlab.jfronny.muscript.parser.util;
import io.gitlab.jfronny.muscript.core.CodeLocation;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* Class for storing errors with code context
* Can be generated from a LocationalException with asPrintable
*
* @param message The error message
* @param start The location of the start of this error. May be null.
* @param end The location of the end of this error. May be null.
*/
public record PrettyPrintError(String message, @Nullable Location start, @Nullable Location end, List<? extends StackFrame> callStack) {
/**
* A location in the source code
*
* @param line The code of the line this is in
* @param column The column of this character, starting with 0
* @param row The row of this line, starting with 1
*/
public record Location(String line, int column, int row) {
public static Location create(@Nullable String source, int index) {
if (source == null || index < 0) return new Location("", -1, -1);
int lineStart = source.lastIndexOf('\n', index);
int lineEnd = source.indexOf('\n', index);
if (lineEnd == -1) lineEnd = source.length();
int lineIndex = lineStart > 0 ? (int) source.substring(0, lineStart).chars().filter(c -> c == '\n').count() : 0;
int column = index - lineStart - 1;
String line = lineStart == source.length() - 1 ? "" : source.substring(lineStart + 1, lineEnd);
return new Location(line, column, lineIndex + 1);
}
@Override
public String toString() {
if (column >= line.length()) return "character " + (column + 1) + " of line " + row;
else return "'" + line.charAt(column) + "' (character " + (column + 1) + ")";
}
public String prettyPrint() {
String linePrefix = String.format("%1$6d", row) + " | ";
return linePrefix + line + "\n" + " ".repeat(linePrefix.length() + column) + "^-- ";
}
}
/**
* Converts this error to a human-readable error message
*
* @return A string representation
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (start == null || start.column < 0) sb.append("Error at unknown location: ").append(message);
else {
sb.append("Error at ").append(start).append(": ").append(message).append("\n");
if (end == null) sb.append(start.prettyPrint()).append("Here");
else if (start.row == end.row) {
String linePrefix = String.format("%1$6d", start.row) + " | ";
sb.append(linePrefix).append(start.line).append("\n").append(" ".repeat(linePrefix.length() + start.column)).append("^").append("-".repeat(end.column - start.column - 1)).append("^-- Here");
}
else sb.append(start.prettyPrint()).append("From here").append(end.prettyPrint()).append("to here");
}
callStack.forEach(frame -> sb.append("\n at ").append(frame));
return sb.toString();
}
public static Builder builder() {
return new Builder();
}
public static Builder builder(CodeLocation location) {
return builder().setSource(location.source()).setLocation(location.chStart(), location.chEnd());
}
public static class Builder {
private String message;
private Location start;
private @Nullable Location end;
private List<? extends StackFrame> callStack = List.of();
private String source;
public Builder setSource(String source) {
this.source = source;
return this;
}
public Builder setMessage(String message) {
this.message = message;
return this;
}
public Builder setLocation(int chStart) {
return setLocation(Location.create(source, chStart), null);
}
public Builder setLocation(int chStart, int chEnd) {
if (chEnd == chStart) return setLocation(chStart);
if (chEnd < chStart) {
int a = chEnd;
chEnd = chStart;
chStart = a;
}
return setLocation(Location.create(source, chStart), Location.create(source, chEnd));
}
public Builder setLocation(Location start, Location end) {
this.start = start;
this.end = end;
return this;
}
public Builder setCallStack(List<StackFrame> callStack) {
this.callStack = callStack.stream()
.map(frame -> frame instanceof StackFrame.Lined lined
? lined
: source == null ? frame : ((StackFrame.Raw) frame).lined(source))
.toList();
return this;
}
public PrettyPrintError build() {
return new PrettyPrintError(message, start, end, callStack);
}
}
}

View File

@ -0,0 +1,23 @@
package io.gitlab.jfronny.muscript.parser.util;
public sealed interface StackFrame {
record Raw(String file, String name, int chStart) implements StackFrame {
@Override
public String toString() {
return name + " (call: character " + chStart + (file == null ? ")" : " in " + file + ")");
}
public Lined lined(String source) {
int lineStart = source.lastIndexOf('\n', chStart);
int lineIndex = lineStart > 0 ? (int) source.substring(0, lineStart).chars().filter(c -> c == '\n').count() : 0;
return new Lined(file, name, lineIndex + 1);
}
}
record Lined(String file, String name, int row) implements StackFrame {
@Override
public String toString() {
return name + " (call: line " + row + (file == null ? ")" : " in " + file + ")");
}
}
}

View File

@ -0,0 +1,8 @@
module io.gitlab.jfronny.commons.muscript.parser {
requires io.gitlab.jfronny.commons;
requires static org.jetbrains.annotations;
requires io.gitlab.jfronny.commons.muscript.ast;
requires io.gitlab.jfronny.commons.muscript.core;
exports io.gitlab.jfronny.muscript.parser;
exports io.gitlab.jfronny.muscript.parser.lexer;
}

View File

@ -0,0 +1 @@
io.gitlab.jfronny.muscript.parser.impl.ExprParserImpl