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
				
			
		
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue