Lex and parse mult, div, mod and exp. Now, go on and generate the assembly for those!
This commit is contained in:
parent
44de1a76c0
commit
29a3e4c0fd
33 changed files with 875 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>
|
57
src/main/java/ch/fritteli/gombaila/ElementWalker.java
Normal file
57
src/main/java/ch/fritteli/gombaila/ElementWalker.java
Normal file
|
@ -0,0 +1,57 @@
|
|||
package ch.fritteli.gombaila;
|
||||
|
||||
import io.vavr.control.Option;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
public class ElementWalker<T, E> {
|
||||
@NotNull
|
||||
private final T back;
|
||||
@NotNull
|
||||
private final BiPredicate<T, Integer> hasNextPredicate;
|
||||
@NotNull
|
||||
private final BiPredicate<T, Integer> hasPrevPredicate;
|
||||
@NotNull
|
||||
private final BiFunction<T, Integer, E> atFunction;
|
||||
private int index = 0;
|
||||
|
||||
public ElementWalker(@NotNull final T back,
|
||||
@NotNull final BiPredicate<T, Integer> hasNextPredicate,
|
||||
@NotNull final BiPredicate<T, Integer> hasPrevPredicate,
|
||||
@NotNull final BiFunction<T, Integer, E> atFunction) {
|
||||
this.back = back;
|
||||
this.hasNextPredicate = hasNextPredicate;
|
||||
this.hasPrevPredicate = hasPrevPredicate;
|
||||
this.atFunction = atFunction;
|
||||
}
|
||||
|
||||
public boolean hasNext() {
|
||||
return this.hasNextPredicate.test(this.back, this.index);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public E next() {
|
||||
return this.atFunction.apply(this.back, this.index++);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Option<E> peekNext() {
|
||||
return Option.when(this.hasNext(), () -> this.atFunction.apply(this.back, this.index));
|
||||
}
|
||||
|
||||
public boolean hasPrev() {
|
||||
return this.hasPrevPredicate.test(this.back, this.index);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public E prev() {
|
||||
return this.atFunction.apply(this.back, --this.index);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Option<E> peekPrev() {
|
||||
return Option.when(this.hasPrev(), () -> this.atFunction.apply(this.back, this.index - 1));
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
16
src/main/java/ch/fritteli/gombaila/domain/SeqWalker.java
Normal file
16
src/main/java/ch/fritteli/gombaila/domain/SeqWalker.java
Normal file
|
@ -0,0 +1,16 @@
|
|||
package ch.fritteli.gombaila.domain;
|
||||
|
||||
import ch.fritteli.gombaila.ElementWalker;
|
||||
import io.vavr.collection.Seq;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class SeqWalker<E> extends ElementWalker<Seq<E>, E> {
|
||||
public SeqWalker(@NotNull final Seq<E> seq) {
|
||||
super(
|
||||
seq,
|
||||
(back, index) -> index < back.length(),
|
||||
(back, index) -> index > 0,
|
||||
Seq::get
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 NodeStmtExit, NodeStmtLet, NodeStmtPrint {
|
||||
}
|
|
@ -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,7 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record NodeStmtLet(@NotNull Token ident, @NotNull NodeExpr expr) 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 {
|
||||
}
|
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) {
|
||||
public Token(@NotNull final TokenType type) {
|
||||
this(type, Option.none());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package ch.fritteli.gombaila.domain.common;
|
||||
|
||||
import io.vavr.control.Option;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public enum TokenType {
|
||||
// Keywords
|
||||
LET,
|
||||
EXIT,
|
||||
PRINT,
|
||||
// special characters
|
||||
SEMI,
|
||||
OPEN_PAREN,
|
||||
CLOSE_PAREN,
|
||||
EQUALS,
|
||||
PLUS,
|
||||
MINUS,
|
||||
MULT,
|
||||
DIV,
|
||||
MOD,
|
||||
EXP,
|
||||
// literals, identifiers
|
||||
INT_LIT,
|
||||
IDENTIFIER,
|
||||
// the rest
|
||||
WHITESPACE;
|
||||
|
||||
public boolean isBinaryOperator() {
|
||||
return switch (this) {
|
||||
case PLUS, MINUS, MULT, DIV, MOD, EXP -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Option<Integer> precedence() {
|
||||
return Option.of(switch (this) {
|
||||
case PLUS, MINUS -> 1;
|
||||
case MULT, DIV, MOD -> 2;
|
||||
case EXP -> 3;
|
||||
default -> null;
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Option<Associativity> associativity() {
|
||||
return Option.of(switch (this) {
|
||||
case PLUS, MINUS, MULT, DIV, MOD -> Associativity.LEFT;
|
||||
case EXP -> Associativity.RIGHT;
|
||||
default -> null;
|
||||
});
|
||||
}
|
||||
|
||||
public enum Associativity {
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
package ch.fritteli.gombaila.domain.generator;
|
||||
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprAdd;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprDiv;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprExp;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprMinus;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprMod;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprMult;
|
||||
import ch.fritteli.gombaila.domain.common.NodeExpr;
|
||||
import ch.fritteli.gombaila.domain.common.NodeExprIdent;
|
||||
import ch.fritteli.gombaila.domain.common.NodeExprIntLit;
|
||||
import ch.fritteli.gombaila.domain.common.NodeProg;
|
||||
import ch.fritteli.gombaila.domain.common.NodeStmt;
|
||||
import ch.fritteli.gombaila.domain.common.NodeStmtExit;
|
||||
import ch.fritteli.gombaila.domain.common.NodeStmtLet;
|
||||
import ch.fritteli.gombaila.domain.common.NodeStmtPrint;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Generator {
|
||||
@NotNull
|
||||
private static final String header = """
|
||||
global _start
|
||||
_start:
|
||||
""";
|
||||
@NotNull
|
||||
private static final String defaultExitStmt = """
|
||||
mov rax, 60
|
||||
mov rdi, 0
|
||||
syscall
|
||||
""";
|
||||
|
||||
@NotNull
|
||||
private final NodeProg nodeProg;
|
||||
@NotNull
|
||||
private final StringBuilder asm;
|
||||
@NotNull
|
||||
private final ExprVisitor exprVisitor;
|
||||
@NotNull
|
||||
private final StmtVisitor stmtVisitor;
|
||||
@NotNull
|
||||
private final Map<String, Variable> identifierStackposition = new HashMap<>();
|
||||
private int stackSize = 0;
|
||||
|
||||
public Generator(@NotNull final NodeProg nodeProg) {
|
||||
this.nodeProg = nodeProg;
|
||||
this.asm = new StringBuilder(header);
|
||||
this.exprVisitor = new ExprVisitor();
|
||||
this.stmtVisitor = new StmtVisitor();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String generate() {
|
||||
for (final NodeStmt stmt : this.nodeProg.stmts()) {
|
||||
this.generateStmt(stmt);
|
||||
}
|
||||
this.asm.append(defaultExitStmt);
|
||||
return this.asm.toString();
|
||||
}
|
||||
|
||||
private void generateStmt(@NotNull final NodeStmt stmt) {
|
||||
this.stmtVisitor.visit(stmt);
|
||||
}
|
||||
|
||||
private void generateExpr(@NotNull final NodeExpr expr) {
|
||||
this.exprVisitor.visit(expr);
|
||||
}
|
||||
|
||||
private void push(@NotNull final String reg) {
|
||||
this.line("push %s", reg);
|
||||
this.stackSize++;
|
||||
}
|
||||
|
||||
private void pop(@NotNull final String reg) {
|
||||
this.line("pop %s", reg);
|
||||
this.stackSize--;
|
||||
}
|
||||
|
||||
private void label(@NotNull final String name) {
|
||||
this.asm.append(name)
|
||||
.append(":")
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
private void line(@NotNull final String line, @NotNull final Object... params) {
|
||||
this.asm.append(" ")
|
||||
.append(line.formatted(params))
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
private record Variable(int stackLocation) {
|
||||
}
|
||||
|
||||
private final class StmtVisitor {
|
||||
|
||||
private StmtVisitor() {
|
||||
}
|
||||
|
||||
void visit(@NotNull final NodeStmt stmt) {
|
||||
switch (stmt) {
|
||||
case final NodeStmtLet stmtLet -> visit(stmtLet);
|
||||
case final NodeStmtExit stmtExit -> visit(stmtExit);
|
||||
case final NodeStmtPrint stmtPrint -> visit(stmtPrint);
|
||||
}
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeStmtExit stmt) {
|
||||
generateExpr(stmt.expr());
|
||||
line("mov rax, 60");
|
||||
pop("rdi");
|
||||
line("syscall");
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeStmtLet stmt) {
|
||||
if (identifierStackposition.containsKey(stmt.ident().value().get())) {
|
||||
throw new GeneratorException("Identifier already used", stmt);
|
||||
}
|
||||
identifierStackposition.put(((String) stmt.ident().value().get()), new Variable(stackSize));
|
||||
generateExpr(stmt.expr());
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeStmtPrint stmt) {
|
||||
generateExpr(stmt.nodeExpr());
|
||||
// rdx: length of output
|
||||
line("mov rdx, 8");
|
||||
// rsi: output
|
||||
pop("rsi");
|
||||
// rdi: fd-value (STDOUT=1)
|
||||
line("mov rdi, 1");
|
||||
// rax: syscall (SYS_WRITE=1)
|
||||
line("mov rax, 1");
|
||||
// syscall
|
||||
line("syscall");
|
||||
}
|
||||
}
|
||||
|
||||
private final class ExprVisitor {
|
||||
private ExprVisitor() {
|
||||
}
|
||||
|
||||
void visit(@NotNull final NodeExpr expr) {
|
||||
switch (expr) {
|
||||
case final NodeExprIdent exprIdent -> visit(exprIdent);
|
||||
case final NodeExprIntLit exprIntLit -> visit(exprIntLit);
|
||||
case final NodeBinExprAdd exprAdd -> visit(exprAdd);
|
||||
case final NodeBinExprMinus exprMinus -> visit(exprMinus);
|
||||
case final NodeBinExprMult exprMult -> visit(exprMult);
|
||||
case final NodeBinExprDiv exprDiv -> visit(exprDiv);
|
||||
case final NodeBinExprMod exprMod -> visit(exprMod);
|
||||
case final NodeBinExprExp exprExp -> visit(exprExp);
|
||||
}
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeExprIdent expr) {
|
||||
final Variable variable = identifierStackposition.get(expr.ident().value().get());
|
||||
if (variable == null) {
|
||||
throw new GeneratorException("Undeclared identifier", expr);
|
||||
}
|
||||
push("QWORD [rsp+%d]".formatted(8 * (stackSize - variable.stackLocation - 1)));
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeExprIntLit expr) {
|
||||
line("mov rax, %s", expr.value().value().get());
|
||||
push("rax");
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeBinExprAdd expr) {
|
||||
generateExpr(expr.lhs());
|
||||
generateExpr(expr.rhs());
|
||||
pop("rbx");
|
||||
pop("rax");
|
||||
line("add rax, rbx");
|
||||
push("rax");
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeBinExprMinus expr) {
|
||||
generateExpr(expr.lhs());
|
||||
generateExpr(expr.rhs());
|
||||
pop("rbx");
|
||||
pop("rax");
|
||||
line("sub rax, rbx");
|
||||
push("rax");
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeBinExprMult expr) {
|
||||
throw new GeneratorException("Not yet implemented!", expr);
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeBinExprDiv expr) {
|
||||
throw new GeneratorException("Not yet implemented!", expr);
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeBinExprMod expr) {
|
||||
throw new GeneratorException("Not yet implemented!", expr);
|
||||
}
|
||||
|
||||
private void visit(@NotNull final NodeBinExprExp expr) {
|
||||
throw new GeneratorException("Not yet implemented!", expr);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
104
src/main/java/ch/fritteli/gombaila/domain/lexer/Lexer.java
Normal file
104
src/main/java/ch/fritteli/gombaila/domain/lexer/Lexer.java
Normal file
|
@ -0,0 +1,104 @@
|
|||
package ch.fritteli.gombaila.domain.lexer;
|
||||
|
||||
import ch.fritteli.gombaila.domain.common.Token;
|
||||
import ch.fritteli.gombaila.domain.common.TokenType;
|
||||
import io.vavr.Predicates;
|
||||
import io.vavr.collection.Stream;
|
||||
import io.vavr.control.Option;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class Lexer {
|
||||
@NotNull
|
||||
private final StringWalker content;
|
||||
@NotNull
|
||||
private Stream<Token> tokens = Stream.empty();
|
||||
|
||||
public Lexer(@NotNull final String input) {
|
||||
this.content = new StringWalker(input);
|
||||
}
|
||||
|
||||
@NonNls
|
||||
public Stream<Token> lex() {
|
||||
Option<Character> next;
|
||||
while ((next = this.content.peekNext()).isDefined()) {
|
||||
final char c = next.get();
|
||||
if (Character.isAlphabetic(c) || c == '_') {
|
||||
this.handleAlphabeticOrUnderscore();
|
||||
} else if (Character.isDigit(c)) {
|
||||
this.handleDigit();
|
||||
} else if (Character.isWhitespace(c)) {
|
||||
this.handleWhitespace();
|
||||
} else if (c == ';') {
|
||||
this.handleSimple(TokenType.SEMI);
|
||||
} else if (c == '(') {
|
||||
this.handleSimple(TokenType.OPEN_PAREN);
|
||||
} else if (c == ')') {
|
||||
this.handleSimple(TokenType.CLOSE_PAREN);
|
||||
} else if (c == '=') {
|
||||
this.handleSimple(TokenType.EQUALS);
|
||||
} else if (c == '+') {
|
||||
this.handleSimple(TokenType.PLUS);
|
||||
} else if (c == '-') {
|
||||
this.handleSimple(TokenType.MINUS);
|
||||
} else if (c == '*') {
|
||||
this.handleSimple(TokenType.MULT);
|
||||
} else if (c == '/') {
|
||||
this.handleSimple(TokenType.DIV);
|
||||
} else if (c == '%') {
|
||||
this.handleSimple(TokenType.MOD);
|
||||
} else if (c == '^') {
|
||||
this.handleSimple(TokenType.EXP);
|
||||
} else {
|
||||
throw this.error(c);
|
||||
}
|
||||
}
|
||||
return this.tokens;
|
||||
}
|
||||
|
||||
private LexerException error(final char c) {
|
||||
return new LexerException("Error parsing input: Unexpected character '%c'.".formatted(c), -1, -1);
|
||||
}
|
||||
|
||||
private void handleAlphabeticOrUnderscore() {
|
||||
final StringBuilder s = new StringBuilder();
|
||||
while (this.content.peekNext().exists(Predicates.<Character>anyOf(
|
||||
Character::isAlphabetic,
|
||||
Character::isDigit,
|
||||
c -> c == '_'
|
||||
))) {
|
||||
s.append(this.content.next());
|
||||
}
|
||||
switch (s.toString()) {
|
||||
case "exit" -> this.appendToken(new Token(TokenType.EXIT));
|
||||
case "let" -> this.appendToken(new Token(TokenType.LET));
|
||||
case "print" -> this.appendToken(new Token(TokenType.PRINT));
|
||||
case final String value -> this.appendToken(new Token(TokenType.IDENTIFIER, Option.of(value)));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDigit() {
|
||||
final StringBuilder s = new StringBuilder();
|
||||
while (this.content.peekNext().exists(Character::isDigit)) {
|
||||
s.append(this.content.next());
|
||||
}
|
||||
this.appendToken(new Token(TokenType.INT_LIT, Option.of(Long.parseLong(s.toString()))));
|
||||
}
|
||||
|
||||
private void handleWhitespace() {
|
||||
final StringBuilder s = new StringBuilder();
|
||||
while (this.content.peekNext().exists(Character::isWhitespace)) {
|
||||
s.append(this.content.next());
|
||||
}
|
||||
this.appendToken(new Token(TokenType.WHITESPACE, Option.of(s.toString())));
|
||||
}
|
||||
|
||||
private void handleSimple(@NotNull final TokenType tokenType) {
|
||||
this.content.next();
|
||||
this.appendToken(new Token(tokenType));
|
||||
}
|
||||
|
||||
private void appendToken(@NotNull final Token token) {
|
||||
this.tokens = this.tokens.append(token);
|
||||
}
|
||||
}
|
|
@ -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,16 @@
|
|||
package ch.fritteli.gombaila.domain.lexer;
|
||||
|
||||
import ch.fritteli.gombaila.ElementWalker;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class StringWalker extends ElementWalker<String, Character> {
|
||||
|
||||
public StringWalker(@NotNull final String string) {
|
||||
super(
|
||||
string,
|
||||
(back, index) -> index < back.length(),
|
||||
(back, index) -> index > 0,
|
||||
String::charAt
|
||||
);
|
||||
}
|
||||
}
|
136
src/main/java/ch/fritteli/gombaila/domain/parser/Parser.java
Normal file
136
src/main/java/ch/fritteli/gombaila/domain/parser/Parser.java
Normal file
|
@ -0,0 +1,136 @@
|
|||
package ch.fritteli.gombaila.domain.parser;
|
||||
|
||||
import ch.fritteli.gombaila.domain.SeqWalker;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprAdd;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprDiv;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprExp;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprMinus;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprMod;
|
||||
import ch.fritteli.gombaila.domain.common.NodeBinExprMult;
|
||||
import ch.fritteli.gombaila.domain.common.NodeExpr;
|
||||
import ch.fritteli.gombaila.domain.common.NodeExprIdent;
|
||||
import ch.fritteli.gombaila.domain.common.NodeExprIntLit;
|
||||
import ch.fritteli.gombaila.domain.common.NodeProg;
|
||||
import ch.fritteli.gombaila.domain.common.NodeStmt;
|
||||
import ch.fritteli.gombaila.domain.common.NodeStmtExit;
|
||||
import ch.fritteli.gombaila.domain.common.NodeStmtLet;
|
||||
import ch.fritteli.gombaila.domain.common.NodeStmtPrint;
|
||||
import ch.fritteli.gombaila.domain.common.Token;
|
||||
import ch.fritteli.gombaila.domain.common.TokenType;
|
||||
import io.vavr.collection.List;
|
||||
import io.vavr.collection.Seq;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class Parser {
|
||||
@NotNull
|
||||
private final SeqWalker<Token> tokens;
|
||||
|
||||
public Parser(@NotNull final Seq<Token> tokens) {
|
||||
|
||||
this.tokens = new SeqWalker<>(tokens.reject(token -> TokenType.WHITESPACE.equals(token.type())));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public NodeProg parse() {
|
||||
Seq<NodeStmt> stmts = List.empty();
|
||||
while (this.tokens.hasNext()) {
|
||||
stmts = stmts.append(this.parseStmt());
|
||||
}
|
||||
|
||||
return new NodeProg(stmts);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private NodeStmt parseStmt() {
|
||||
final NodeStmt result;
|
||||
if (this.checkNextTokenTypeConsuming(TokenType.EXIT)) {
|
||||
this.assertAndConsumeNextTokenType(TokenType.OPEN_PAREN);
|
||||
final NodeExpr nodeExpr = this.parseExpr(1);
|
||||
this.assertAndConsumeNextTokenType(TokenType.CLOSE_PAREN);
|
||||
result = new NodeStmtExit(nodeExpr);
|
||||
} else if (this.checkNextTokenTypeConsuming(TokenType.LET)) {
|
||||
final Token identifier = this.assertAndConsumeNextTokenType(TokenType.IDENTIFIER);
|
||||
this.assertAndConsumeNextTokenType(TokenType.EQUALS);
|
||||
final NodeExpr nodeExpr = this.parseExpr(1);
|
||||
result = new NodeStmtLet(identifier, nodeExpr);
|
||||
} else if (this.checkNextTokenTypeConsuming(TokenType.PRINT)) {
|
||||
this.assertAndConsumeNextTokenType(TokenType.OPEN_PAREN);
|
||||
final NodeExpr nodeExpr = this.parseExpr(1);
|
||||
this.assertAndConsumeNextTokenType(TokenType.CLOSE_PAREN);
|
||||
result = new NodeStmtPrint(nodeExpr);
|
||||
} else {
|
||||
throw new ParserException("Could not parse statement", null);
|
||||
}
|
||||
|
||||
this.assertAndConsumeNextTokenType(TokenType.SEMI);
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private NodeExpr parseExpr(final int minPrecedence) {
|
||||
NodeExpr result;
|
||||
if (this.checkNextTokenType(TokenType.INT_LIT)) {
|
||||
result = new NodeExprIntLit(this.tokens.next());
|
||||
} else if (this.checkNextTokenType(TokenType.IDENTIFIER)) {
|
||||
result = new NodeExprIdent(this.tokens.next());
|
||||
} else if (this.checkNextTokenTypeConsuming(TokenType.OPEN_PAREN)) {
|
||||
result = this.parseExpr(1);
|
||||
this.assertAndConsumeNextTokenType(TokenType.CLOSE_PAREN);
|
||||
} else {
|
||||
throw new ParserException(null, this.tokens.peekNext().getOrNull());
|
||||
}
|
||||
|
||||
while (this.hasNextTokenPrecedenceGTE(minPrecedence)) {
|
||||
final Token token = this.tokens.next();
|
||||
final int precedence = token.type().precedence().get();
|
||||
final TokenType.Associativity associativity = token.type().associativity().get();
|
||||
final int nextMinPrecedence;
|
||||
if (associativity == TokenType.Associativity.LEFT) {
|
||||
nextMinPrecedence = precedence + 1;
|
||||
} else {
|
||||
nextMinPrecedence = precedence;
|
||||
}
|
||||
final NodeExpr rhs = this.parseExpr(nextMinPrecedence);
|
||||
result = switch (token.type()) {
|
||||
case PLUS -> new NodeBinExprAdd(result, rhs);
|
||||
case MINUS -> new NodeBinExprMinus(result, rhs);
|
||||
case MULT -> new NodeBinExprMult(result, rhs);
|
||||
case DIV -> new NodeBinExprDiv(result, rhs);
|
||||
case MOD -> new NodeBinExprMod(result, rhs);
|
||||
case EXP -> new NodeBinExprExp(result, rhs);
|
||||
default -> throw new ParserException("Expected binary operator token", token);
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean hasNextTokenPrecedenceGTE(final int minPrecedence) {
|
||||
return this.tokens.peekNext()
|
||||
.map(Token::type)
|
||||
.flatMap(TokenType::precedence)
|
||||
.exists(precedence -> precedence >= minPrecedence);
|
||||
}
|
||||
|
||||
private boolean checkNextTokenType(@NotNull final TokenType type) {
|
||||
return this.tokens.peekNext()
|
||||
.exists(token -> token.type().equals(type));
|
||||
}
|
||||
|
||||
private boolean checkNextTokenTypeConsuming(@NotNull final TokenType type) {
|
||||
if (this.tokens.peekNext()
|
||||
.exists(token -> token.type().equals(type))) {
|
||||
this.tokens.next();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Token assertAndConsumeNextTokenType(@NotNull final TokenType type) {
|
||||
if (this.checkNextTokenType(type)) {
|
||||
return this.tokens.next();
|
||||
}
|
||||
throw new ParserException("Unexpected token", this.tokens.peekNext().getOrNull());
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
14
src/main/resources/docs/readme.md
Normal file
14
src/main/resources/docs/readme.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
[Prog] -> [Stmt]*
|
||||
|
||||
[Stmt] ->
|
||||
exit([Expr]);
|
||||
let ident = [Expr];
|
||||
|
||||
[Expr] ->
|
||||
int_lit
|
||||
ident
|
||||
[BinExpr]
|
||||
|
||||
[BinExpr] ->
|
||||
[Expr] * [Expr]; prec=1
|
||||
[Expr] + [Expr]; prec=0
|
3
src/main/resources/gombaila/simple.gb
Normal file
3
src/main/resources/gombaila/simple.gb
Normal file
|
@ -0,0 +1,3 @@
|
|||
let x = 1 + 2;
|
||||
let y = 8 + 2 * x ^ (2 - 1) ^ 2 - 1 * 0;
|
||||
exit(y + x % 2);
|
Loading…
Reference in a new issue