From 29a3e4c0fd8e20c2c70d3fcdd9d4ea29a81d567e Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sat, 23 Mar 2024 22:03:35 +0100 Subject: [PATCH] Lex and parse mult, div, mod and exp. Now, go on and generate the assembly for those! --- .gitignore | 2 + README.md | 4 +- pom.xml | 63 ++++++ .../ch/fritteli/gombaila/ElementWalker.java | 57 +++++ .../ch/fritteli/gombaila/GombailaMain.java | 30 +++ .../fritteli/gombaila/domain/SeqWalker.java | 16 ++ .../fritteli/gombaila/domain/common/Node.java | 4 + .../gombaila/domain/common/NodeBinExpr.java | 6 + .../domain/common/NodeBinExprAdd.java | 6 + .../domain/common/NodeBinExprDiv.java | 6 + .../domain/common/NodeBinExprExp.java | 6 + .../domain/common/NodeBinExprMinus.java | 6 + .../domain/common/NodeBinExprMod.java | 6 + .../domain/common/NodeBinExprMult.java | 6 + .../gombaila/domain/common/NodeExpr.java | 4 + .../gombaila/domain/common/NodeExprIdent.java | 6 + .../domain/common/NodeExprIntLit.java | 6 + .../gombaila/domain/common/NodeProg.java | 7 + .../gombaila/domain/common/NodeStmt.java | 4 + .../gombaila/domain/common/NodeStmtExit.java | 7 + .../gombaila/domain/common/NodeStmtLet.java | 7 + .../gombaila/domain/common/NodeStmtPrint.java | 6 + .../gombaila/domain/common/Token.java | 10 + .../gombaila/domain/common/TokenType.java | 58 +++++ .../gombaila/domain/generator/Generator.java | 203 ++++++++++++++++++ .../domain/generator/GeneratorException.java | 23 ++ .../fritteli/gombaila/domain/lexer/Lexer.java | 104 +++++++++ .../gombaila/domain/lexer/LexerException.java | 23 ++ .../gombaila/domain/lexer/StringWalker.java | 16 ++ .../gombaila/domain/parser/Parser.java | 136 ++++++++++++ .../domain/parser/ParserException.java | 22 ++ src/main/resources/docs/readme.md | 14 ++ src/main/resources/gombaila/simple.gb | 3 + 33 files changed, 875 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/ch/fritteli/gombaila/ElementWalker.java create mode 100644 src/main/java/ch/fritteli/gombaila/GombailaMain.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/SeqWalker.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/Node.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExpr.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprAdd.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprDiv.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprExp.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMinus.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMod.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMult.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeExpr.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeExprIdent.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeExprIntLit.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeProg.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeStmt.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtExit.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtLet.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtPrint.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/Token.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/common/TokenType.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/generator/Generator.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/generator/GeneratorException.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/lexer/Lexer.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/lexer/LexerException.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/lexer/StringWalker.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/parser/Parser.java create mode 100644 src/main/java/ch/fritteli/gombaila/domain/parser/ParserException.java create mode 100644 src/main/resources/docs/readme.md create mode 100644 src/main/resources/gombaila/simple.gb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92322c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +target/ diff --git a/README.md b/README.md index 6cd2507..df36ebd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # gombaila -Let's write a compiler in Java! -Inspired by Pixeled: https://www.youtube.com/watch?v=vcSijrRsrY0. \ No newline at end of file +Let's write a compiler in Java! +Inspired by Pixeled: https://www.youtube.com/watch?v=vcSijrRsrY0. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1615bd3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + ch.fritteli + fritteli-build-parent + 5.0.1-SNAPSHOT + + + ch.fritteli.gombaila + gombaila + 0.0.1-SNAPSHOT + + + UTF-8 + + + + + io.vavr + vavr + + + org.jetbrains + annotations + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.projectlombok + lombok + + + org.junit.jupiter + junit-jupiter-api + + + org.assertj + assertj-core + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + + + + diff --git a/src/main/java/ch/fritteli/gombaila/ElementWalker.java b/src/main/java/ch/fritteli/gombaila/ElementWalker.java new file mode 100644 index 0000000..86eaac3 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/ElementWalker.java @@ -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 { + @NotNull + private final T back; + @NotNull + private final BiPredicate hasNextPredicate; + @NotNull + private final BiPredicate hasPrevPredicate; + @NotNull + private final BiFunction atFunction; + private int index = 0; + + public ElementWalker(@NotNull final T back, + @NotNull final BiPredicate hasNextPredicate, + @NotNull final BiPredicate hasPrevPredicate, + @NotNull final BiFunction 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 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 peekPrev() { + return Option.when(this.hasPrev(), () -> this.atFunction.apply(this.back, this.index - 1)); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/GombailaMain.java b/src/main/java/ch/fritteli/gombaila/GombailaMain.java new file mode 100644 index 0000000..e98a29f --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/GombailaMain.java @@ -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 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); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/SeqWalker.java b/src/main/java/ch/fritteli/gombaila/domain/SeqWalker.java new file mode 100644 index 0000000..2f78c76 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/SeqWalker.java @@ -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 extends ElementWalker, E> { + public SeqWalker(@NotNull final Seq seq) { + super( + seq, + (back, index) -> index < back.length(), + (back, index) -> index > 0, + Seq::get + ); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/Node.java b/src/main/java/ch/fritteli/gombaila/domain/common/Node.java new file mode 100644 index 0000000..160fc19 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/Node.java @@ -0,0 +1,4 @@ +package ch.fritteli.gombaila.domain.common; + +public sealed interface Node permits NodeExpr, NodeProg, NodeStmt { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExpr.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExpr.java new file mode 100644 index 0000000..04b3f86 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExpr.java @@ -0,0 +1,6 @@ +package ch.fritteli.gombaila.domain.common; + +public sealed interface NodeBinExpr extends NodeExpr + permits NodeBinExprAdd, NodeBinExprDiv, NodeBinExprExp, NodeBinExprMinus, NodeBinExprMod, NodeBinExprMult { + +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprAdd.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprAdd.java new file mode 100644 index 0000000..b839dbf --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprAdd.java @@ -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 { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprDiv.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprDiv.java new file mode 100644 index 0000000..9951a2c --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprDiv.java @@ -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 { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprExp.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprExp.java new file mode 100644 index 0000000..7885498 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprExp.java @@ -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 { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMinus.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMinus.java new file mode 100644 index 0000000..9fb2027 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMinus.java @@ -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 { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMod.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMod.java new file mode 100644 index 0000000..66b9e50 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMod.java @@ -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 { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMult.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMult.java new file mode 100644 index 0000000..bb5c0e0 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeBinExprMult.java @@ -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 { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeExpr.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeExpr.java new file mode 100644 index 0000000..fa1964f --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeExpr.java @@ -0,0 +1,4 @@ +package ch.fritteli.gombaila.domain.common; + +public sealed interface NodeExpr extends Node permits NodeExprIdent, NodeExprIntLit, NodeBinExpr { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeExprIdent.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeExprIdent.java new file mode 100644 index 0000000..14ade85 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeExprIdent.java @@ -0,0 +1,6 @@ +package ch.fritteli.gombaila.domain.common; + +import org.jetbrains.annotations.NotNull; + +public record NodeExprIdent(@NotNull Token ident) implements NodeExpr { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeExprIntLit.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeExprIntLit.java new file mode 100644 index 0000000..b24e4d0 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeExprIntLit.java @@ -0,0 +1,6 @@ +package ch.fritteli.gombaila.domain.common; + +import org.jetbrains.annotations.NotNull; + +public record NodeExprIntLit(@NotNull Token value) implements NodeExpr { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeProg.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeProg.java new file mode 100644 index 0000000..79ddbec --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeProg.java @@ -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 stmts) implements Node { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmt.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmt.java new file mode 100644 index 0000000..5801f44 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmt.java @@ -0,0 +1,4 @@ +package ch.fritteli.gombaila.domain.common; + +public sealed interface NodeStmt extends Node permits NodeStmtExit, NodeStmtLet, NodeStmtPrint { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtExit.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtExit.java new file mode 100644 index 0000000..0eea3a7 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtExit.java @@ -0,0 +1,7 @@ +package ch.fritteli.gombaila.domain.common; + +import org.jetbrains.annotations.NotNull; + +public record NodeStmtExit(@NotNull NodeExpr expr) implements NodeStmt { + +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtLet.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtLet.java new file mode 100644 index 0000000..58845d6 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtLet.java @@ -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 { + +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtPrint.java b/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtPrint.java new file mode 100644 index 0000000..1e8e65e --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/NodeStmtPrint.java @@ -0,0 +1,6 @@ +package ch.fritteli.gombaila.domain.common; + +import org.jetbrains.annotations.NotNull; + +public record NodeStmtPrint(@NotNull NodeExpr nodeExpr) implements NodeStmt { +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/Token.java b/src/main/java/ch/fritteli/gombaila/domain/common/Token.java new file mode 100644 index 0000000..90b474a --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/Token.java @@ -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 value) { + public Token(@NotNull final TokenType type) { + this(type, Option.none()); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/common/TokenType.java b/src/main/java/ch/fritteli/gombaila/domain/common/TokenType.java new file mode 100644 index 0000000..926e9f0 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/common/TokenType.java @@ -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 precedence() { + return Option.of(switch (this) { + case PLUS, MINUS -> 1; + case MULT, DIV, MOD -> 2; + case EXP -> 3; + default -> null; + }); + } + + @NotNull + public Option 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 + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/generator/Generator.java b/src/main/java/ch/fritteli/gombaila/domain/generator/Generator.java new file mode 100644 index 0000000..a9a2657 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/generator/Generator.java @@ -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 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); + } + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/generator/GeneratorException.java b/src/main/java/ch/fritteli/gombaila/domain/generator/GeneratorException.java new file mode 100644 index 0000000..08c1141 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/generator/GeneratorException.java @@ -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() + ); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/lexer/Lexer.java b/src/main/java/ch/fritteli/gombaila/domain/lexer/Lexer.java new file mode 100644 index 0000000..f7e5951 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/lexer/Lexer.java @@ -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 tokens = Stream.empty(); + + public Lexer(@NotNull final String input) { + this.content = new StringWalker(input); + } + + @NonNls + public Stream lex() { + Option 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.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); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/lexer/LexerException.java b/src/main/java/ch/fritteli/gombaila/domain/lexer/LexerException.java new file mode 100644 index 0000000..bf516b4 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/lexer/LexerException.java @@ -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() + ); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/lexer/StringWalker.java b/src/main/java/ch/fritteli/gombaila/domain/lexer/StringWalker.java new file mode 100644 index 0000000..4109a6f --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/lexer/StringWalker.java @@ -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 { + + public StringWalker(@NotNull final String string) { + super( + string, + (back, index) -> index < back.length(), + (back, index) -> index > 0, + String::charAt + ); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/parser/Parser.java b/src/main/java/ch/fritteli/gombaila/domain/parser/Parser.java new file mode 100644 index 0000000..db74416 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/parser/Parser.java @@ -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 tokens; + + public Parser(@NotNull final Seq tokens) { + + this.tokens = new SeqWalker<>(tokens.reject(token -> TokenType.WHITESPACE.equals(token.type()))); + } + + @NotNull + public NodeProg parse() { + Seq 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()); + } +} diff --git a/src/main/java/ch/fritteli/gombaila/domain/parser/ParserException.java b/src/main/java/ch/fritteli/gombaila/domain/parser/ParserException.java new file mode 100644 index 0000000..60fdc56 --- /dev/null +++ b/src/main/java/ch/fritteli/gombaila/domain/parser/ParserException.java @@ -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() + ); + } +} diff --git a/src/main/resources/docs/readme.md b/src/main/resources/docs/readme.md new file mode 100644 index 0000000..83c0dec --- /dev/null +++ b/src/main/resources/docs/readme.md @@ -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 diff --git a/src/main/resources/gombaila/simple.gb b/src/main/resources/gombaila/simple.gb new file mode 100644 index 0000000..a960348 --- /dev/null +++ b/src/main/resources/gombaila/simple.gb @@ -0,0 +1,3 @@ +let x = 1 + 2; +let y = 8 + 2 * x ^ (2 - 1) ^ 2 - 1 * 0; +exit(y + x % 2);