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 @@
+<?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>
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<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));
+    }
+}
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<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);
+    }
+}
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<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
+        );
+    }
+}
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<NodeStmt> 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<Object> 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<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
+    }
+}
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<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);
+        }
+    }
+}
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<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);
+    }
+}
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<String, Character> {
+
+    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<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());
+    }
+}
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);