Compare commits

...

7 commits

47 changed files with 1344 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,39 @@
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 BiFunction<T, Integer, E> atFunction;
private int index = 0;
public ElementWalker(@NotNull final T back,
@NotNull final BiPredicate<T, Integer> hasNextPredicate,
@NotNull final BiFunction<T, Integer, E> atFunction) {
this.back = back;
this.hasNextPredicate = hasNextPredicate;
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));
}
}

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,15 @@
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(),
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 NodeStmtAssign, NodeStmtExit, NodeStmtIf, NodeStmtIfElse, NodeStmtLet, NodeStmtPrint, NodeStmtScope {
}

View file

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

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,6 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeStmtIf(@NotNull NodeExpr expr, @NotNull NodeStmtScope ifScope) implements NodeStmt {
}

View file

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

View file

@ -0,0 +1,7 @@
package ch.fritteli.gombaila.domain.common;
import org.jetbrains.annotations.NotNull;
public record NodeStmtLet(@NotNull NodeStmtAssign stmtAssign) 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,8 @@
package ch.fritteli.gombaila.domain.common;
import io.vavr.collection.Seq;
import org.jetbrains.annotations.NotNull;
public record NodeStmtScope(@NotNull Seq<NodeStmt> stmts) 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, int line, int column) {
public Token(@NotNull final TokenType type, final int line, final int column) {
this(type, Option.none(), line, column);
}
}

View file

@ -0,0 +1,55 @@
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,
OPEN_CURLY,
CLOSE_CURLY,
EQUALS,
PLUS,
MINUS,
MULT,
DIV,
MOD,
EXP,
// literals, identifiers
INT_LIT,
IDENTIFIER,
// the rest
WHITESPACE,
IF,
ELSE;
@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,100 @@
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 io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
final class ExprVisitor {
@NotNull
private final Generator generator;
ExprVisitor(@NotNull final Generator generator) {
this.generator = generator;
}
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 String varName = (String) expr.ident().value().get();
final Option<Variable> variable = this.generator.vars.find(var -> var.name().equals(varName));
if (variable.isEmpty()) {
throw new GeneratorException("Undeclared identifier: %s".formatted(varName), expr);
}
generator.push("qword [rsp+%d]".formatted(8 * (this.generator.stackSize - variable.get().stackLocation() - 1)));
}
private void visit(@NotNull final NodeExprIntLit expr) {
this.generator.printer.commentedLine("store the value in rax", "mov", "rax, %s".formatted(expr.value().value().get()));
generator.push("rax");
}
private void visit(@NotNull final NodeBinExprAdd expr) {
generator.generateExpr(expr.lhs());
generator.generateExpr(expr.rhs());
generator.pop("rbx");
generator.pop("rax");
generator.printer.commentedLine("add rbx to rax", "add", "rax, rbx");
generator.push("rax");
}
private void visit(@NotNull final NodeBinExprMinus expr) {
generator.generateExpr(expr.lhs());
generator.generateExpr(expr.rhs());
generator.pop("rbx");
generator.pop("rax");
generator.printer.commentedLine("subtract rbx from rax", "sub", "rax, rbx");
generator.push("rax");
}
private void visit(@NotNull final NodeBinExprMult expr) {
generator.generateExpr(expr.lhs());
generator.generateExpr(expr.rhs());
generator.pop("rbx");
generator.pop("rax");
generator.printer.commentedLine("multiply rax by rbx", "mul", "rbx");
generator.push("rax");
}
private void visit(@NotNull final NodeBinExprDiv expr) {
generator.generateExpr(expr.lhs());
generator.generateExpr(expr.rhs());
generator.pop("rbx");
generator.pop("rax");
generator.printer.commentedLine("divide rax by rbx", "div", "rbx");
generator.push("rax");
}
private void visit(@NotNull final NodeBinExprMod expr) {
generator.generateExpr(expr.lhs());
generator.generateExpr(expr.rhs());
generator.pop("rbx");
generator.pop("rax");
generator.printer.commentedLine("divide rax by rbx. The remainder will be in rdx.", "div", "rbx");
generator.push("rdx");
}
private void visit(@NotNull final NodeBinExprExp expr) {
throw new GeneratorException("Not yet implemented!", expr);
}
}

View file

@ -0,0 +1,101 @@
package ch.fritteli.gombaila.domain.generator;
import ch.fritteli.gombaila.domain.common.NodeExpr;
import ch.fritteli.gombaila.domain.common.NodeProg;
import ch.fritteli.gombaila.domain.common.NodeStmt;
import ch.fritteli.gombaila.domain.printer.Printer;
import io.vavr.collection.List;
import io.vavr.collection.Seq;
import org.jetbrains.annotations.NotNull;
import java.time.ZonedDateTime;
import java.util.Stack;
public class Generator {
@NotNull
final Printer printer = new Printer();
@NotNull
final Stack<Integer> scopes = new Stack<>();
@NotNull
private final NodeProg nodeProg;
@NotNull
private final ExprVisitor exprVisitor;
@NotNull
private final StmtVisitor stmtVisitor;
@NotNull
Seq<Variable> vars = List.empty();
int stackSize = 0;
public Generator(@NotNull final NodeProg nodeProg) {
this.nodeProg = nodeProg;
this.exprVisitor = new ExprVisitor(this);
this.stmtVisitor = new StmtVisitor(this);
}
@NotNull
public String generate() {
this.generateHeader();
for (final NodeStmt stmt : this.nodeProg.stmts()) {
this.generateStmt(stmt);
}
this.generateFooter();
return this.printer.toString();
}
private void generateHeader() {
this.printer.comment("Program generated by Gombaila on %1$tF, %1$tT".formatted(ZonedDateTime.now()));
this.printer.section(".data");
this.printer.line("EXIT_SUCCESS", "equ", 0);
this.printer.line("SYSCALL_exit", "equ", 60);
this.printer.blank();
this.printer.section(".text");
this.printer.rawLine("global _start");
this.printer.blank();
this.printer.label("_start");
}
private void generateFooter() {
this.printer.commentedLine("Prepare for successful exit", "mov", "rdi, EXIT_SUCCESS");
this.printer.commentedLine("Jump to the default exit routine", "jmp", "exit");
this.printer.blank();
this.printer.label("exit");
this.printer.commentedLine("Prepare for the EXIT syscall", "mov", "rax, SYSCALL_exit");
this.printer.line("syscall");
}
void generateStmt(@NotNull final NodeStmt stmt) {
this.stmtVisitor.visit(stmt);
}
void generateExpr(@NotNull final NodeExpr expr) {
this.exprVisitor.visit(expr);
}
void push(@NotNull final String reg) {
this.printer.commentedLine("push %s onto top of the stack".formatted(reg), "push", reg);
this.stackSize++;
}
void pop(@NotNull final String reg) {
this.printer.commentedLine("pop top of the stack into %s".formatted(reg), "pop", reg);
this.stackSize--;
}
void beginScope() {
this.scopes.push(this.vars.size());
}
void endScope() {
final int popCount = this.vars.size() - this.scopes.pop();
if (popCount > 0) {
this.printer.commentedLine(
"Reset the stack pointer to discard out-of-scope variables",
"add", "rsp, %d".formatted(popCount * 8));
this.stackSize -= popCount;
}
this.vars = this.vars.dropRight(popCount);
}
}

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,131 @@
package ch.fritteli.gombaila.domain.generator;
import ch.fritteli.gombaila.domain.common.NodeStmt;
import ch.fritteli.gombaila.domain.common.NodeStmtAssign;
import ch.fritteli.gombaila.domain.common.NodeStmtExit;
import ch.fritteli.gombaila.domain.common.NodeStmtIf;
import ch.fritteli.gombaila.domain.common.NodeStmtIfElse;
import ch.fritteli.gombaila.domain.common.NodeStmtLet;
import ch.fritteli.gombaila.domain.common.NodeStmtPrint;
import ch.fritteli.gombaila.domain.common.NodeStmtScope;
import io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
final class StmtVisitor {
@NotNull
private final Generator generator;
private int labelCounter = 0;
StmtVisitor(@NotNull final Generator generator) {
this.generator = generator;
}
void visit(@NotNull final NodeStmt stmt) {
switch (stmt) {
case final NodeStmtLet stmtLet -> visit(stmtLet);
case final NodeStmtAssign stmtAssign -> visit(stmtAssign);
case final NodeStmtExit stmtExit -> visit(stmtExit);
case final NodeStmtPrint stmtPrint -> visit(stmtPrint);
case final NodeStmtScope stmtScope -> visit(stmtScope);
case final NodeStmtIf stmtIf -> visit(stmtIf);
case final NodeStmtIfElse stmtIfElse -> visit(stmtIfElse);
}
}
private void visit(@NotNull final NodeStmtLet stmt) {
final String varName = (String) stmt.stmtAssign().ident().value().get();
if (this.generator.vars.exists(var -> var.name().equals(varName))) {
throw new GeneratorException("Identifier already used: %s".formatted(varName), stmt);
}
this.generator.vars = this.generator.vars.append(new Variable(varName, generator.stackSize));
this.generator.generateExpr(stmt.stmtAssign().expr());
}
private void visit(@NotNull final NodeStmtAssign stmt) {
final String varName = (String) stmt.ident().value().get();
final Option<Variable> variable = this.generator.vars.find(var -> var.name().equals(varName));
if (variable.isEmpty()) {
throw new GeneratorException("Undeclared identifier: %s".formatted(varName), stmt);
}
final int stackLocation = variable.get().stackLocation();
final int targetLocation = 8 * (this.generator.stackSize - stackLocation - 1);
this.generator.generateExpr(stmt.expr());
this.generator.pop("rbx");
this.generator.printer.commentedLine(
"Overwrite the old value of %s in the stack",
"mov",
"[rsp+%d], rbx".formatted(targetLocation)
);
}
private void visit(@NotNull final NodeStmtExit stmt) {
this.generator.generateExpr(stmt.expr());
this.generator.pop("rdi");
this.generator.printer.commentedLine("Jump to the default exit routine", "jmp", "exit");
}
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");
throw new GeneratorException("Not implemented yet", stmt);
}
private void visit(@NotNull final NodeStmtScope stmt) {
this.generator.beginScope();
stmt.stmts().forEach(this.generator::generateStmt);
this.generator.endScope();
}
private void visit(@NotNull final NodeStmtIf stmt) {
final String label = this.createNextLabel();
this.generator.generateExpr(stmt.expr());
this.generator.pop("rax");
this.generator.printer.commentedLine("test the condition",
"test", "rax, rax");
this.generator.printer.commentedLine(
"skip to %s if condition is not met".formatted(label),
"jz", label
);
this.visit(stmt.ifScope());
this.generator.printer.label(label);
}
private void visit(@NotNull final NodeStmtIfElse stmt) {
final String afterIfLabel = this.createNextLabel();
final String afterElseLabel = this.createNextLabel();
this.generator.generateExpr(stmt.expr());
this.generator.pop("rax");
this.generator.printer.commentedLine("test the condition",
"test", "rax, rax");
this.generator.printer.commentedLine(
"skip to %s (else) if condition is not met".formatted(afterIfLabel),
"jz", afterIfLabel
);
this.visit(stmt.ifScope());
this.generator.printer.commentedLine(
"skip to %s to skip the else".formatted(afterElseLabel),
"jmp",
afterElseLabel
);
this.generator.printer.label(afterIfLabel);
this.visit(stmt.elseScope());
this.generator.printer.label(afterElseLabel);
}
@NotNull
private String createNextLabel() {
return "label" + (this.labelCounter++);
}
}

View file

@ -0,0 +1,6 @@
package ch.fritteli.gombaila.domain.generator;
import org.jetbrains.annotations.NotNull;
record Variable(@NotNull String name, int stackLocation) {
}

View file

@ -0,0 +1,153 @@
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();
final int line = this.content.line();
final int column = this.content.column();
if (Character.isAlphabetic(c) || c == '_') {
this.handleAlphabeticOrUnderscore(line, column);
} else if (Character.isDigit(c)) {
this.handleDigit(line, column);
} else if (Character.isWhitespace(c)) {
this.handleWhitespace(line, column);
} else if (c == ';') {
this.handleSimple(TokenType.SEMI, line, column);
} else if (c == '(') {
this.handleSimple(TokenType.OPEN_PAREN, line, column);
} else if (c == ')') {
this.handleSimple(TokenType.CLOSE_PAREN, line, column);
} else if (c == '{') {
this.handleSimple(TokenType.OPEN_CURLY, line, column);
} else if (c == '}') {
this.handleSimple(TokenType.CLOSE_CURLY, line, column);
} else if (c == '=') {
this.handleSimple(TokenType.EQUALS, line, column);
} else if (c == '+') {
this.handleSimple(TokenType.PLUS, line, column);
} else if (c == '-') {
this.handleSimple(TokenType.MINUS, line, column);
} else if (c == '*') {
this.handleSimple(TokenType.MULT, line, column);
} else if (c == '/') {
this.handleForwardSlash(line, column);
} else if (c == '%') {
this.handleSimple(TokenType.MOD, line, column);
} else if (c == '^') {
this.handleSimple(TokenType.EXP, line, column);
} else {
throw this.error(c, line, column);
}
}
return this.tokens;
}
private LexerException error(final char c, final int line, final int column) {
return new LexerException("Error parsing input: Unexpected character '%c'.".formatted(c), line, column);
}
private void handleAlphabeticOrUnderscore(final int line, final int column) {
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, line, column));
case "let" -> this.appendToken(new Token(TokenType.LET, line, column));
case "print" -> this.appendToken(new Token(TokenType.PRINT, line, column));
case "if" -> this.appendToken(new Token(TokenType.IF, line, column));
case "else" -> this.appendToken(new Token(TokenType.ELSE, line, column));
case final String value ->
this.appendToken(new Token(TokenType.IDENTIFIER, Option.of(value), line, column));
}
}
private void handleDigit(final int line, final int column) {
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())), line, column));
}
private void handleWhitespace(final int line, final int column) {
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()), line, column));
}
private void handleSimple(@NotNull final TokenType tokenType, final int line, final int column) {
this.content.next();
this.appendToken(new Token(tokenType, line, column));
}
private void handleForwardSlash(final int line, final int column) {
this.content.next();
if (this.content.peekNext().exists(n -> n == '/')) {
this.content.next();
this.handleLineComment();
}
if (this.content.peekNext().exists(n -> n == '*')) {
this.content.next();
this.handleBlockComment();
} else {
this.tokens.append(new Token(TokenType.DIV, line, column));
}
}
private void handleLineComment() {
while (this.content.peekNext().exists(next -> next != '\n')) {
this.content.next();
}
if (this.content.peekNext().exists(next -> next == '\n')) {
this.content.next();
}
}
private void handleBlockComment() {
// ...*/
while (this.content.peekNext().exists(next -> next != '*')) {
this.content.next();
}
if (this.content.peekNext().exists(n -> n == '*')) {
this.content.next();
if (this.content.peekNext().exists(n -> n == '/')) {
this.content.next();
return;
}
handleBlockComment();
} else {
return;
}
}
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,38 @@
package ch.fritteli.gombaila.domain.lexer;
import ch.fritteli.gombaila.ElementWalker;
import org.jetbrains.annotations.NotNull;
public class StringWalker extends ElementWalker<String, Character> {
private int line = 0;
private int column = 0;
public StringWalker(@NotNull final String string) {
super(
string,
(back, index) -> index < back.length(),
String::charAt
);
}
public int line() {
return this.line + 1;
}
public int column() {
return this.column + 1;
}
@NotNull
@Override
public Character next() {
final char c = super.next();
if (c == '\n') {
this.line++;
this.column = 0;
} else {
this.column++;
}
return c;
}
}

View file

@ -0,0 +1,179 @@
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.NodeStmtAssign;
import ch.fritteli.gombaila.domain.common.NodeStmtExit;
import ch.fritteli.gombaila.domain.common.NodeStmtIf;
import ch.fritteli.gombaila.domain.common.NodeStmtIfElse;
import ch.fritteli.gombaila.domain.common.NodeStmtLet;
import ch.fritteli.gombaila.domain.common.NodeStmtPrint;
import ch.fritteli.gombaila.domain.common.NodeStmtScope;
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 NodeStmtAssign nodeStmtAssign = this.parseStmtAssign();
result = new NodeStmtLet(nodeStmtAssign);
} 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 if (this.checkNextTokenType(TokenType.OPEN_CURLY)) {
// NB: We do NOT expect a SEMI here, so we return directly.
return this.parseScope();
} else if (this.checkNextTokenTypeConsuming(TokenType.IF)) {
this.assertAndConsumeNextTokenType(TokenType.OPEN_PAREN);
final NodeExpr expr = this.parseExpr(1);
this.assertAndConsumeNextTokenType(TokenType.CLOSE_PAREN);
final NodeStmtScope ifScope = this.parseScope();
// there /could/ be an else, so let's check that
if (this.checkNextTokenTypeConsuming(TokenType.ELSE)) {
final NodeStmtScope elseScope = this.parseScope();
return new NodeStmtIfElse(expr, ifScope, elseScope);
}
// NB: We do NOT expect a SEMI here, so we return directly.
return new NodeStmtIf(expr, ifScope);
} else if (this.checkNextTokenType(TokenType.IDENTIFIER)) {
result = this.parseStmtAssign();
} else {
throw new ParserException("Could not parse statement", this.tokens.peekNext().getOrNull());
}
this.assertAndConsumeNextTokenType(TokenType.SEMI);
return result;
}
@NotNull
private NodeStmtAssign parseStmtAssign() {
final Token identifier = this.assertAndConsumeNextTokenType(TokenType.IDENTIFIER);
this.assertAndConsumeNextTokenType(TokenType.EQUALS);
final NodeExpr nodeExpr = this.parseExpr(1);
return new NodeStmtAssign(identifier, nodeExpr);
}
@NotNull
private NodeStmtScope parseScope() {
this.assertAndConsumeNextTokenType(TokenType.OPEN_CURLY);
final Seq<NodeStmt> nodeStmts = this.parseStatements();
this.assertAndConsumeNextTokenType(TokenType.CLOSE_CURLY);
return new NodeStmtScope(nodeStmts);
}
@NotNull
private Seq<NodeStmt> parseStatements() {
Seq<NodeStmt> result = List.empty();
while (!this.checkNextTokenType(TokenType.CLOSE_CURLY)) {
result = result.append(this.parseStmt());
}
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,21 @@
package ch.fritteli.gombaila.domain.printer;
import org.jetbrains.annotations.NotNull;
record BlankLine() implements Line {
@Override
public int indentationLevel() {
return 0;
}
@Override
public void updateWithMap() {
}
@NotNull
@Override
public String toString() {
return "";
}
}

View file

@ -0,0 +1,49 @@
package ch.fritteli.gombaila.domain.printer;
import io.vavr.collection.Seq;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
record FormattedLine(@NotNull Seq<String> tokens, @Nullable String comment, int indentationLevel,
@NotNull WidthMap widthMap) implements Line {
@Override
public void updateWithMap() {
this.tokens
.forEachWithIndex((token, index) -> {
this.widthMap.updateWidthForTokenIndex(index, token.length());
});
}
@NotNull
@Override
public String toString() {
final Seq<String> lineElements;
if (this.comment == null) {
lineElements = this.tokens.dropRight(1)
.zipWithIndex()
.map(tuple -> tuple.map2(this.widthMap::getTokenWidthByIndex))
.map(tuple -> {
final String token = tuple._1();
final int width = tuple._2();
return String.format("%-" + width + "s", token);
})
.appendAll(this.tokens.lastOption());
} else {
lineElements = this.tokens.zipWithIndex()
.map(tuple -> tuple.map2(this.widthMap::getTokenWidthByIndex))
.map(tuple -> {
final String token = tuple._1();
final int width = tuple._2();
return String.format("%-" + width + "s", token);
})
.append(";")
.append(this.comment);
}
final String line = lineElements.mkString(" ");
final int indents = this.widthMap.getIndentationSize() * this.indentationLevel;
if (indents == 0) {
return line;
}
return ("%-" + indents + "s").formatted("") + line;
}
}

View file

@ -0,0 +1,5 @@
package ch.fritteli.gombaila.domain.printer;
interface Line extends Printable {
int indentationLevel();
}

View file

@ -0,0 +1,10 @@
package ch.fritteli.gombaila.domain.printer;
import org.jetbrains.annotations.NotNull;
public interface Printable {
void updateWithMap();
@NotNull
String toString();
}

View file

@ -0,0 +1,69 @@
package ch.fritteli.gombaila.domain.printer;
import io.vavr.collection.List;
import lombok.NonNull;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
public class Printer {
private static final int INDENTATION_SIZE = 4;
private final java.util.List<Printable> printables = new ArrayList<>();
private WidthMap widthMap = new WidthMap(INDENTATION_SIZE);
private int indentationLevel = 0;
public void increaseIndent() {
this.indentationLevel++;
}
public void decreaseIndent() {
this.indentationLevel--;
}
public void resetIndent() {
this.indentationLevel = 0;
}
public void comment(@NotNull final String line) {
this.printables.add(new RawLine("; %s".formatted(line), this.indentationLevel, this.widthMap));
}
public void section(@NotNull final String sectionName) {
this.resetIndent();
this.widthMap = new WidthMap(INDENTATION_SIZE);
this.printables.add(new RawLine("section %s".formatted(sectionName), this.indentationLevel, this.widthMap));
}
public void label(@NotNull final String label) {
this.resetIndent();
this.widthMap = new WidthMap(INDENTATION_SIZE);
this.printables.add(new RawLine("%s:".formatted(label), this.indentationLevel, this.widthMap));
this.increaseIndent();
}
public void blank() {
this.printables.add(new RawLine("", 0, this.widthMap));
}
public void line(@NonNull final Object... instructionTokens) {
final List<String> tokens = List.of(instructionTokens)
.map(String::valueOf);
this.printables.add(new FormattedLine(tokens, null, this.indentationLevel, this.widthMap));
}
public void commentedLine(@NotNull final String comment, @NotNull final Object... instructionTokens) {
final List<String> tokens = List.of(instructionTokens)
.map(String::valueOf);
this.printables.add(new FormattedLine(tokens, comment, this.indentationLevel, this.widthMap));
}
public void rawLine(@NotNull final String line) {
this.printables.add(new RawLine(line, this.indentationLevel, this.widthMap));
}
@Override
public String toString() {
this.printables.forEach(Printable::updateWithMap);
return List.ofAll(this.printables).mkString("\n");
}
}

View file

@ -0,0 +1,20 @@
package ch.fritteli.gombaila.domain.printer;
import org.jetbrains.annotations.NotNull;
record RawLine(@NotNull String line, int indentationLevel, @NotNull WidthMap widthMap) implements Line {
@Override
public void updateWithMap() {
// nop
}
@NotNull
@Override
public String toString() {
final int indents = this.widthMap.getIndentationSize() * this.indentationLevel;
if (indents == 0) {
return this.line;
}
return ("%-" + indents + "s%s").formatted("", this.line);
}
}

View file

@ -0,0 +1,38 @@
package ch.fritteli.gombaila.domain.printer;
import io.vavr.collection.HashMap;
import io.vavr.collection.Map;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
public class WidthMap {
@Getter
private final int indentationSize;
@NotNull
private Map<Integer, Integer> widthByTokenIndex = HashMap.empty();
public WidthMap(final int indentationSize) {
this.indentationSize = indentationSize;
}
int getMaxLineLengthWithoutComment() {
return this.widthByTokenIndex.values()
.sum()
.intValue();
}
int getTokenWidthByIndex(final int tokenIndex) {
return this.widthByTokenIndex.get(tokenIndex).getOrElse(0);
}
void updateWidthForTokenIndex(final int tokenIndex, final int width) {
if (!this.hasBiggerWidthAt(tokenIndex, width)) {
this.widthByTokenIndex = this.widthByTokenIndex.put(tokenIndex, width);
}
}
private boolean hasBiggerWidthAt(final int tokenIndex, final int width) {
return this.widthByTokenIndex.get(tokenIndex)
.exists(existingWidth -> existingWidth > width);
}
}

View file

@ -0,0 +1,19 @@
[Prog] -> [Stmt]*
[Stmt] ->
exit([Expr]);
let ident = [Expr];
[Expr] ->
int_lit
ident
[BinExpr]
([Expr])
[BinExpr] ->
[Expr] ^ [Expr]; prec=3 assoc=right
[Expr] * [Expr]; prec=2 assoc=left
[Expr] / [Expr]; prec=2 assoc=left
[Expr] % [Expr]; prec=2 assoc=left
[Expr] + [Expr]; prec=1 assoc=left
[Expr] - [Expr]; prec=1 assoc=left

View file