Lex and parse mult, div, mod and exp. Now, go on and generate the assembly for those!

This commit is contained in:
Manuel Friedli 2024-03-23 22:03:35 +01:00
parent 44de1a76c0
commit 29a3e4c0fd
Signed by: manuel
GPG Key ID: 41D08ABA75634DA1
33 changed files with 875 additions and 2 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea/
target/

View File

@ -1,4 +1,4 @@
# gombaila
Let's write a compiler in Java!
Inspired by Pixeled: https://www.youtube.com/watch?v=vcSijrRsrY0.
Let's write a compiler in Java!
Inspired by Pixeled: https://www.youtube.com/watch?v=vcSijrRsrY0.

63
pom.xml Normal file
View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ch.fritteli</groupId>
<artifactId>fritteli-build-parent</artifactId>
<version>5.0.1-SNAPSHOT</version>
</parent>
<groupId>ch.fritteli.gombaila</groupId>
<artifactId>gombaila</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,57 @@
package ch.fritteli.gombaila;
import io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
public class ElementWalker<T, E> {
@NotNull
private final T back;
@NotNull
private final BiPredicate<T, Integer> hasNextPredicate;
@NotNull
private final BiPredicate<T, Integer> hasPrevPredicate;
@NotNull
private final BiFunction<T, Integer, E> atFunction;
private int index = 0;
public ElementWalker(@NotNull final T back,
@NotNull final BiPredicate<T, Integer> hasNextPredicate,
@NotNull final BiPredicate<T, Integer> hasPrevPredicate,
@NotNull final BiFunction<T, Integer, E> atFunction) {
this.back = back;
this.hasNextPredicate = hasNextPredicate;
this.hasPrevPredicate = hasPrevPredicate;
this.atFunction = atFunction;
}
public boolean hasNext() {
return this.hasNextPredicate.test(this.back, this.index);
}
@NotNull
public E next() {
return this.atFunction.apply(this.back, this.index++);
}
@NotNull
public Option<E> peekNext() {
return Option.when(this.hasNext(), () -> this.atFunction.apply(this.back, this.index));
}
public boolean hasPrev() {
return this.hasPrevPredicate.test(this.back, this.index);
}
@NotNull
public E prev() {
return this.atFunction.apply(this.back, --this.index);
}
@NotNull
public Option<E> peekPrev() {
return Option.when(this.hasPrev(), () -> this.atFunction.apply(this.back, this.index - 1));
}
}

View File

@ -0,0 +1,30 @@
package ch.fritteli.gombaila;
import ch.fritteli.gombaila.domain.common.NodeProg;
import ch.fritteli.gombaila.domain.common.Token;
import ch.fritteli.gombaila.domain.generator.Generator;
import ch.fritteli.gombaila.domain.lexer.Lexer;
import ch.fritteli.gombaila.domain.parser.Parser;
import io.vavr.collection.Stream;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
public class GombailaMain {
public static void main(@NotNull final String[] args) throws URISyntaxException, IOException {
final String string = Files.readString(Paths.get(Lexer.class.getClassLoader().getResource("gombaila/simple.gb").toURI()));
final Lexer lexer = new Lexer(string);
final Stream<Token> tokens = lexer.lex();
System.out.println("TOKENS:\n" + tokens.mkString("\n"));
final Parser parser = new Parser(tokens);
final NodeProg nodeProg = parser.parse();
System.out.println("STMTS:\n" + nodeProg.stmts().mkString("\n"));
final Generator generator = new Generator(nodeProg);
final String asm = generator.generate();
Files.writeString(Paths.get("./target/simple.asm"), asm, StandardCharsets.UTF_8);
}
}

View File

@ -0,0 +1,16 @@
package ch.fritteli.gombaila.domain;
import ch.fritteli.gombaila.ElementWalker;
import io.vavr.collection.Seq;
import org.jetbrains.annotations.NotNull;
public class SeqWalker<E> extends ElementWalker<Seq<E>, E> {
public SeqWalker(@NotNull final Seq<E> seq) {
super(
seq,
(back, index) -> index < back.length(),
(back, index) -> index > 0,
Seq::get
);
}
}

View File

@ -0,0 +1,4 @@
package ch.fritteli.gombaila.domain.common;
public sealed interface Node permits NodeExpr, NodeProg, NodeStmt {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
public sealed interface NodeBinExpr extends NodeExpr
permits NodeBinExprAdd, NodeBinExprDiv, NodeBinExprExp, NodeBinExprMinus, NodeBinExprMod, NodeBinExprMult {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeBinExprAdd(@NotNull NodeExpr lhs, @NotNull NodeExpr rhs) implements NodeBinExpr {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeBinExprDiv(@NotNull NodeExpr lhs, @NotNull NodeExpr rhs) implements NodeBinExpr {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeBinExprExp(@NotNull NodeExpr lhs, @NotNull NodeExpr rhs) implements NodeBinExpr {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeBinExprMinus(@NotNull NodeExpr lhs, @NotNull NodeExpr rhs) implements NodeBinExpr {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeBinExprMod(@NotNull NodeExpr lhs, @NotNull NodeExpr rhs) implements NodeBinExpr {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeBinExprMult(@NotNull NodeExpr lhs, @NotNull NodeExpr rhs) implements NodeBinExpr {
}

View File

@ -0,0 +1,4 @@
package ch.fritteli.gombaila.domain.common;
public sealed interface NodeExpr extends Node permits NodeExprIdent, NodeExprIntLit, NodeBinExpr {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeExprIdent(@NotNull Token ident) implements NodeExpr {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeExprIntLit(@NotNull Token value) implements NodeExpr {
}

View File

@ -0,0 +1,7 @@
package ch.fritteli.gombaila.domain.common;
import io.vavr.collection.Seq;
import org.jetbrains.annotations.NotNull;
public record NodeProg(@NotNull Seq<NodeStmt> stmts) implements Node {
}

View File

@ -0,0 +1,4 @@
package ch.fritteli.gombaila.domain.common;
public sealed interface NodeStmt extends Node permits NodeStmtExit, NodeStmtLet, NodeStmtPrint {
}

View File

@ -0,0 +1,7 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeStmtExit(@NotNull NodeExpr expr) implements NodeStmt {
}

View File

@ -0,0 +1,7 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeStmtLet(@NotNull Token ident, @NotNull NodeExpr expr) implements NodeStmt {
}

View File

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeStmtPrint(@NotNull NodeExpr nodeExpr) implements NodeStmt {
}

View File

@ -0,0 +1,10 @@
package ch.fritteli.gombaila.domain.common;
import io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
public record Token(@NotNull TokenType type, @NotNull Option<Object> value) {
public Token(@NotNull final TokenType type) {
this(type, Option.none());
}
}

View File

@ -0,0 +1,58 @@
package ch.fritteli.gombaila.domain.common;
import io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
public enum TokenType {
// Keywords
LET,
EXIT,
PRINT,
// special characters
SEMI,
OPEN_PAREN,
CLOSE_PAREN,
EQUALS,
PLUS,
MINUS,
MULT,
DIV,
MOD,
EXP,
// literals, identifiers
INT_LIT,
IDENTIFIER,
// the rest
WHITESPACE;
public boolean isBinaryOperator() {
return switch (this) {
case PLUS, MINUS, MULT, DIV, MOD, EXP -> true;
default -> false;
};
}
@NotNull
public Option<Integer> precedence() {
return Option.of(switch (this) {
case PLUS, MINUS -> 1;
case MULT, DIV, MOD -> 2;
case EXP -> 3;
default -> null;
});
}
@NotNull
public Option<Associativity> associativity() {
return Option.of(switch (this) {
case PLUS, MINUS, MULT, DIV, MOD -> Associativity.LEFT;
case EXP -> Associativity.RIGHT;
default -> null;
});
}
public enum Associativity {
LEFT,
RIGHT
}
}

View File

@ -0,0 +1,203 @@
package ch.fritteli.gombaila.domain.generator;
import ch.fritteli.gombaila.domain.common.NodeBinExprAdd;
import ch.fritteli.gombaila.domain.common.NodeBinExprDiv;
import ch.fritteli.gombaila.domain.common.NodeBinExprExp;
import ch.fritteli.gombaila.domain.common.NodeBinExprMinus;
import ch.fritteli.gombaila.domain.common.NodeBinExprMod;
import ch.fritteli.gombaila.domain.common.NodeBinExprMult;
import ch.fritteli.gombaila.domain.common.NodeExpr;
import ch.fritteli.gombaila.domain.common.NodeExprIdent;
import ch.fritteli.gombaila.domain.common.NodeExprIntLit;
import ch.fritteli.gombaila.domain.common.NodeProg;
import ch.fritteli.gombaila.domain.common.NodeStmt;
import ch.fritteli.gombaila.domain.common.NodeStmtExit;
import ch.fritteli.gombaila.domain.common.NodeStmtLet;
import ch.fritteli.gombaila.domain.common.NodeStmtPrint;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
public class Generator {
@NotNull
private static final String header = """
global _start
_start:
""";
@NotNull
private static final String defaultExitStmt = """
mov rax, 60
mov rdi, 0
syscall
""";
@NotNull
private final NodeProg nodeProg;
@NotNull
private final StringBuilder asm;
@NotNull
private final ExprVisitor exprVisitor;
@NotNull
private final StmtVisitor stmtVisitor;
@NotNull
private final Map<String, Variable> identifierStackposition = new HashMap<>();
private int stackSize = 0;
public Generator(@NotNull final NodeProg nodeProg) {
this.nodeProg = nodeProg;
this.asm = new StringBuilder(header);
this.exprVisitor = new ExprVisitor();
this.stmtVisitor = new StmtVisitor();
}
@NotNull
public String generate() {
for (final NodeStmt stmt : this.nodeProg.stmts()) {
this.generateStmt(stmt);
}
this.asm.append(defaultExitStmt);
return this.asm.toString();
}
private void generateStmt(@NotNull final NodeStmt stmt) {
this.stmtVisitor.visit(stmt);
}
private void generateExpr(@NotNull final NodeExpr expr) {
this.exprVisitor.visit(expr);
}
private void push(@NotNull final String reg) {
this.line("push %s", reg);
this.stackSize++;
}
private void pop(@NotNull final String reg) {
this.line("pop %s", reg);
this.stackSize--;
}
private void label(@NotNull final String name) {
this.asm.append(name)
.append(":")
.append("\n");
}
private void line(@NotNull final String line, @NotNull final Object... params) {
this.asm.append(" ")
.append(line.formatted(params))
.append("\n");
}
private record Variable(int stackLocation) {
}
private final class StmtVisitor {
private StmtVisitor() {
}
void visit(@NotNull final NodeStmt stmt) {
switch (stmt) {
case final NodeStmtLet stmtLet -> visit(stmtLet);
case final NodeStmtExit stmtExit -> visit(stmtExit);
case final NodeStmtPrint stmtPrint -> visit(stmtPrint);
}
}
private void visit(@NotNull final NodeStmtExit stmt) {
generateExpr(stmt.expr());
line("mov rax, 60");
pop("rdi");
line("syscall");
}
private void visit(@NotNull final NodeStmtLet stmt) {
if (identifierStackposition.containsKey(stmt.ident().value().get())) {
throw new GeneratorException("Identifier already used", stmt);
}
identifierStackposition.put(((String) stmt.ident().value().get()), new Variable(stackSize));
generateExpr(stmt.expr());
}
private void visit(@NotNull final NodeStmtPrint stmt) {
generateExpr(stmt.nodeExpr());
// rdx: length of output
line("mov rdx, 8");
// rsi: output
pop("rsi");
// rdi: fd-value (STDOUT=1)
line("mov rdi, 1");
// rax: syscall (SYS_WRITE=1)
line("mov rax, 1");
// syscall
line("syscall");
}
}
private final class ExprVisitor {
private ExprVisitor() {
}
void visit(@NotNull final NodeExpr expr) {
switch (expr) {
case final NodeExprIdent exprIdent -> visit(exprIdent);
case final NodeExprIntLit exprIntLit -> visit(exprIntLit);
case final NodeBinExprAdd exprAdd -> visit(exprAdd);
case final NodeBinExprMinus exprMinus -> visit(exprMinus);
case final NodeBinExprMult exprMult -> visit(exprMult);
case final NodeBinExprDiv exprDiv -> visit(exprDiv);
case final NodeBinExprMod exprMod -> visit(exprMod);
case final NodeBinExprExp exprExp -> visit(exprExp);
}
}
private void visit(@NotNull final NodeExprIdent expr) {
final Variable variable = identifierStackposition.get(expr.ident().value().get());
if (variable == null) {
throw new GeneratorException("Undeclared identifier", expr);
}
push("QWORD [rsp+%d]".formatted(8 * (stackSize - variable.stackLocation - 1)));
}
private void visit(@NotNull final NodeExprIntLit expr) {
line("mov rax, %s", expr.value().value().get());
push("rax");
}
private void visit(@NotNull final NodeBinExprAdd expr) {
generateExpr(expr.lhs());
generateExpr(expr.rhs());
pop("rbx");
pop("rax");
line("add rax, rbx");
push("rax");
}
private void visit(@NotNull final NodeBinExprMinus expr) {
generateExpr(expr.lhs());
generateExpr(expr.rhs());
pop("rbx");
pop("rax");
line("sub rax, rbx");
push("rax");
}
private void visit(@NotNull final NodeBinExprMult expr) {
throw new GeneratorException("Not yet implemented!", expr);
}
private void visit(@NotNull final NodeBinExprDiv expr) {
throw new GeneratorException("Not yet implemented!", expr);
}
private void visit(@NotNull final NodeBinExprMod expr) {
throw new GeneratorException("Not yet implemented!", expr);
}
private void visit(@NotNull final NodeBinExprExp expr) {
throw new GeneratorException("Not yet implemented!", expr);
}
}
}

View File

@ -0,0 +1,23 @@
package ch.fritteli.gombaila.domain.generator;
import ch.fritteli.gombaila.domain.common.Node;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class GeneratorException extends RuntimeException {
@NotNull
private final Node node;
public GeneratorException(@Nullable final String s, @NotNull final Node node) {
super(s);
this.node = node;
}
@Override
public String getMessage() {
return "Generator error at node '%s': %s".formatted(
this.node.getClass().getSimpleName(),
super.getMessage()
);
}
}

View File

@ -0,0 +1,104 @@
package ch.fritteli.gombaila.domain.lexer;
import ch.fritteli.gombaila.domain.common.Token;
import ch.fritteli.gombaila.domain.common.TokenType;
import io.vavr.Predicates;
import io.vavr.collection.Stream;
import io.vavr.control.Option;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
public class Lexer {
@NotNull
private final StringWalker content;
@NotNull
private Stream<Token> tokens = Stream.empty();
public Lexer(@NotNull final String input) {
this.content = new StringWalker(input);
}
@NonNls
public Stream<Token> lex() {
Option<Character> next;
while ((next = this.content.peekNext()).isDefined()) {
final char c = next.get();
if (Character.isAlphabetic(c) || c == '_') {
this.handleAlphabeticOrUnderscore();
} else if (Character.isDigit(c)) {
this.handleDigit();
} else if (Character.isWhitespace(c)) {
this.handleWhitespace();
} else if (c == ';') {
this.handleSimple(TokenType.SEMI);
} else if (c == '(') {
this.handleSimple(TokenType.OPEN_PAREN);
} else if (c == ')') {
this.handleSimple(TokenType.CLOSE_PAREN);
} else if (c == '=') {
this.handleSimple(TokenType.EQUALS);
} else if (c == '+') {
this.handleSimple(TokenType.PLUS);
} else if (c == '-') {
this.handleSimple(TokenType.MINUS);
} else if (c == '*') {
this.handleSimple(TokenType.MULT);
} else if (c == '/') {
this.handleSimple(TokenType.DIV);
} else if (c == '%') {
this.handleSimple(TokenType.MOD);
} else if (c == '^') {
this.handleSimple(TokenType.EXP);
} else {
throw this.error(c);
}
}
return this.tokens;
}
private LexerException error(final char c) {
return new LexerException("Error parsing input: Unexpected character '%c'.".formatted(c), -1, -1);
}
private void handleAlphabeticOrUnderscore() {
final StringBuilder s = new StringBuilder();
while (this.content.peekNext().exists(Predicates.<Character>anyOf(
Character::isAlphabetic,
Character::isDigit,
c -> c == '_'
))) {
s.append(this.content.next());
}
switch (s.toString()) {
case "exit" -> this.appendToken(new Token(TokenType.EXIT));
case "let" -> this.appendToken(new Token(TokenType.LET));
case "print" -> this.appendToken(new Token(TokenType.PRINT));
case final String value -> this.appendToken(new Token(TokenType.IDENTIFIER, Option.of(value)));
}
}
private void handleDigit() {
final StringBuilder s = new StringBuilder();
while (this.content.peekNext().exists(Character::isDigit)) {
s.append(this.content.next());
}
this.appendToken(new Token(TokenType.INT_LIT, Option.of(Long.parseLong(s.toString()))));
}
private void handleWhitespace() {
final StringBuilder s = new StringBuilder();
while (this.content.peekNext().exists(Character::isWhitespace)) {
s.append(this.content.next());
}
this.appendToken(new Token(TokenType.WHITESPACE, Option.of(s.toString())));
}
private void handleSimple(@NotNull final TokenType tokenType) {
this.content.next();
this.appendToken(new Token(tokenType));
}
private void appendToken(@NotNull final Token token) {
this.tokens = this.tokens.append(token);
}
}

View File

@ -0,0 +1,23 @@
package ch.fritteli.gombaila.domain.lexer;
import org.jetbrains.annotations.Nullable;
public class LexerException extends RuntimeException {
private final int lineNumber;
private final int colNumber;
public LexerException(@Nullable final String s, final int line, final int column) {
super(s);
this.lineNumber = line;
this.colNumber = column;
}
@Override
public String getMessage() {
return "Lexer error at line %d, position %d: %s".formatted(
this.lineNumber,
this.colNumber,
super.getMessage()
);
}
}

View File

@ -0,0 +1,16 @@
package ch.fritteli.gombaila.domain.lexer;
import ch.fritteli.gombaila.ElementWalker;
import org.jetbrains.annotations.NotNull;
public class StringWalker extends ElementWalker<String, Character> {
public StringWalker(@NotNull final String string) {
super(
string,
(back, index) -> index < back.length(),
(back, index) -> index > 0,
String::charAt
);
}
}

View File

@ -0,0 +1,136 @@
package ch.fritteli.gombaila.domain.parser;
import ch.fritteli.gombaila.domain.SeqWalker;
import ch.fritteli.gombaila.domain.common.NodeBinExprAdd;
import ch.fritteli.gombaila.domain.common.NodeBinExprDiv;
import ch.fritteli.gombaila.domain.common.NodeBinExprExp;
import ch.fritteli.gombaila.domain.common.NodeBinExprMinus;
import ch.fritteli.gombaila.domain.common.NodeBinExprMod;
import ch.fritteli.gombaila.domain.common.NodeBinExprMult;
import ch.fritteli.gombaila.domain.common.NodeExpr;
import ch.fritteli.gombaila.domain.common.NodeExprIdent;
import ch.fritteli.gombaila.domain.common.NodeExprIntLit;
import ch.fritteli.gombaila.domain.common.NodeProg;
import ch.fritteli.gombaila.domain.common.NodeStmt;
import ch.fritteli.gombaila.domain.common.NodeStmtExit;
import ch.fritteli.gombaila.domain.common.NodeStmtLet;
import ch.fritteli.gombaila.domain.common.NodeStmtPrint;
import ch.fritteli.gombaila.domain.common.Token;
import ch.fritteli.gombaila.domain.common.TokenType;
import io.vavr.collection.List;
import io.vavr.collection.Seq;
import org.jetbrains.annotations.NotNull;
public class Parser {
@NotNull
private final SeqWalker<Token> tokens;
public Parser(@NotNull final Seq<Token> tokens) {
this.tokens = new SeqWalker<>(tokens.reject(token -> TokenType.WHITESPACE.equals(token.type())));
}
@NotNull
public NodeProg parse() {
Seq<NodeStmt> stmts = List.empty();
while (this.tokens.hasNext()) {
stmts = stmts.append(this.parseStmt());
}
return new NodeProg(stmts);
}
@NotNull
private NodeStmt parseStmt() {
final NodeStmt result;
if (this.checkNextTokenTypeConsuming(TokenType.EXIT)) {
this.assertAndConsumeNextTokenType(TokenType.OPEN_PAREN);
final NodeExpr nodeExpr = this.parseExpr(1);
this.assertAndConsumeNextTokenType(TokenType.CLOSE_PAREN);
result = new NodeStmtExit(nodeExpr);
} else if (this.checkNextTokenTypeConsuming(TokenType.LET)) {
final Token identifier = this.assertAndConsumeNextTokenType(TokenType.IDENTIFIER);
this.assertAndConsumeNextTokenType(TokenType.EQUALS);
final NodeExpr nodeExpr = this.parseExpr(1);
result = new NodeStmtLet(identifier, nodeExpr);
} else if (this.checkNextTokenTypeConsuming(TokenType.PRINT)) {
this.assertAndConsumeNextTokenType(TokenType.OPEN_PAREN);
final NodeExpr nodeExpr = this.parseExpr(1);
this.assertAndConsumeNextTokenType(TokenType.CLOSE_PAREN);
result = new NodeStmtPrint(nodeExpr);
} else {
throw new ParserException("Could not parse statement", null);
}
this.assertAndConsumeNextTokenType(TokenType.SEMI);
return result;
}
@NotNull
private NodeExpr parseExpr(final int minPrecedence) {
NodeExpr result;
if (this.checkNextTokenType(TokenType.INT_LIT)) {
result = new NodeExprIntLit(this.tokens.next());
} else if (this.checkNextTokenType(TokenType.IDENTIFIER)) {
result = new NodeExprIdent(this.tokens.next());
} else if (this.checkNextTokenTypeConsuming(TokenType.OPEN_PAREN)) {
result = this.parseExpr(1);
this.assertAndConsumeNextTokenType(TokenType.CLOSE_PAREN);
} else {
throw new ParserException(null, this.tokens.peekNext().getOrNull());
}
while (this.hasNextTokenPrecedenceGTE(minPrecedence)) {
final Token token = this.tokens.next();
final int precedence = token.type().precedence().get();
final TokenType.Associativity associativity = token.type().associativity().get();
final int nextMinPrecedence;
if (associativity == TokenType.Associativity.LEFT) {
nextMinPrecedence = precedence + 1;
} else {
nextMinPrecedence = precedence;
}
final NodeExpr rhs = this.parseExpr(nextMinPrecedence);
result = switch (token.type()) {
case PLUS -> new NodeBinExprAdd(result, rhs);
case MINUS -> new NodeBinExprMinus(result, rhs);
case MULT -> new NodeBinExprMult(result, rhs);
case DIV -> new NodeBinExprDiv(result, rhs);
case MOD -> new NodeBinExprMod(result, rhs);
case EXP -> new NodeBinExprExp(result, rhs);
default -> throw new ParserException("Expected binary operator token", token);
};
}
return result;
}
private boolean hasNextTokenPrecedenceGTE(final int minPrecedence) {
return this.tokens.peekNext()
.map(Token::type)
.flatMap(TokenType::precedence)
.exists(precedence -> precedence >= minPrecedence);
}
private boolean checkNextTokenType(@NotNull final TokenType type) {
return this.tokens.peekNext()
.exists(token -> token.type().equals(type));
}
private boolean checkNextTokenTypeConsuming(@NotNull final TokenType type) {
if (this.tokens.peekNext()
.exists(token -> token.type().equals(type))) {
this.tokens.next();
return true;
}
return false;
}
@NotNull
private Token assertAndConsumeNextTokenType(@NotNull final TokenType type) {
if (this.checkNextTokenType(type)) {
return this.tokens.next();
}
throw new ParserException("Unexpected token", this.tokens.peekNext().getOrNull());
}
}

View File

@ -0,0 +1,22 @@
package ch.fritteli.gombaila.domain.parser;
import ch.fritteli.gombaila.domain.common.Token;
import org.jetbrains.annotations.Nullable;
public class ParserException extends RuntimeException {
@Nullable
private final Token token;
public ParserException(@Nullable final String s, @Nullable final Token token) {
super(s);
this.token = token;
}
@Override
public String getMessage() {
return "Parser error at token '%s': %s".formatted(
this.token,
super.getMessage()
);
}
}

View File

@ -0,0 +1,14 @@
[Prog] -> [Stmt]*
[Stmt] ->
exit([Expr]);
let ident = [Expr];
[Expr] ->
int_lit
ident
[BinExpr]
[BinExpr] ->
[Expr] * [Expr]; prec=1
[Expr] + [Expr]; prec=0

View File

@ -0,0 +1,3 @@
let x = 1 + 2;
let y = 8 + 2 * x ^ (2 - 1) ^ 2 - 1 * 0;
exit(y + x % 2);