Compare commits
7 commits
44de1a76c0
...
15b877a368
Author | SHA1 | Date | |
---|---|---|---|
15b877a368 | |||
ceb0d5aaba | |||
3235cbab7b | |||
6e0b8f950d | |||
19c9409ccd | |||
4adfed238e | |||
29a3e4c0fd |
47 changed files with 1344 additions and 2 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.idea/
|
||||
target/
|
|
@ -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
63
pom.xml
Normal 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>
|
39
src/main/java/ch/fritteli/gombaila/ElementWalker.java
Normal file
39
src/main/java/ch/fritteli/gombaila/ElementWalker.java
Normal 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));
|
||||
}
|
||||
}
|
30
src/main/java/ch/fritteli/gombaila/GombailaMain.java
Normal file
30
src/main/java/ch/fritteli/gombaila/GombailaMain.java
Normal 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);
|
||||
}
|
||||
}
|
15
src/main/java/ch/fritteli/gombaila/domain/SeqWalker.java
Normal file
15
src/main/java/ch/fritteli/gombaila/domain/SeqWalker.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
public sealed interface Node permits NodeExpr, NodeProg, NodeStmt {
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
public sealed interface NodeBinExpr extends NodeExpr
|
||||
permits NodeBinExprAdd, NodeBinExprDiv, NodeBinExprExp, NodeBinExprMinus, NodeBinExprMod, NodeBinExprMult {
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
public sealed interface NodeExpr extends Node permits NodeExprIdent, NodeExprIntLit, NodeBinExpr {
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record NodeExprIdent(@NotNull Token ident) implements NodeExpr {
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record NodeExprIntLit(@NotNull Token value) implements NodeExpr {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
public sealed interface NodeStmt extends Node permits NodeStmtAssign, NodeStmtExit, NodeStmtIf, NodeStmtIfElse, NodeStmtLet, NodeStmtPrint, NodeStmtScope {
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record NodeStmtExit(@NotNull NodeExpr expr) implements NodeStmt {
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record NodeStmtLet(@NotNull NodeStmtAssign stmtAssign) implements NodeStmt {
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record NodeStmtPrint(@NotNull NodeExpr nodeExpr) implements NodeStmt {
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
10
src/main/java/ch/fritteli/gombaila/domain/common/Token.java
Normal file
10
src/main/java/ch/fritteli/gombaila/domain/common/Token.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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++);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.fritteli.gombaila.domain.generator;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
record Variable(@NotNull String name, int stackLocation) {
|
||||
}
|
153
src/main/java/ch/fritteli/gombaila/domain/lexer/Lexer.java
Normal file
153
src/main/java/ch/fritteli/gombaila/domain/lexer/Lexer.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
179
src/main/java/ch/fritteli/gombaila/domain/parser/Parser.java
Normal file
179
src/main/java/ch/fritteli/gombaila/domain/parser/Parser.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 "";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package ch.fritteli.gombaila.domain.printer;
|
||||
|
||||
interface Line extends Printable {
|
||||
int indentationLevel();
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package ch.fritteli.gombaila.domain.printer;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface Printable {
|
||||
void updateWithMap();
|
||||
|
||||
@NotNull
|
||||
String toString();
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
19
src/main/resources/docs/readme.md
Normal file
19
src/main/resources/docs/readme.md
Normal 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
|
0
src/main/resources/gombaila/simple.gb
Normal file
0
src/main/resources/gombaila/simple.gb
Normal file
Loading…
Reference in a new issue