From 1ef6f0e6b9083748e5b91743f15f10f09c6b8432 Mon Sep 17 00:00:00 2001 From: Elias Haugsbakk Date: Fri, 18 Sep 2026 23:10:31 +0200 Subject: Implement parser creating AST --- .../java/no/eliashaugsbakk/kompilator/Main.java | 17 +++++ .../no/eliashaugsbakk/kompilator/parsing/AST.java | 7 ++ .../eliashaugsbakk/kompilator/parsing/Parser.java | 79 ++++++++++++++++++++++ .../kompilator/parsing/ParserException.java | 7 ++ .../kompilator/parsing/node/ASTNode.java | 7 ++ .../kompilator/parsing/node/Program.java | 16 +++++ .../parsing/node/expression/Expression.java | 17 +++++ .../parsing/node/expression/FunctionCall.java | 16 +++++ .../parsing/node/expression/StringLiteral.java | 12 ++++ .../node/statement/ExpressionStatement.java | 10 +++ .../parsing/node/statement/Statement.java | 15 ++++ .../kompilator/tokenization/Lexer.java | 2 +- .../kompilator/tokenization/Token.java | 13 +--- .../kompilator/tokenization/TokenType.java | 2 +- .../kompilator/parsing/ParserTest.java | 70 +++++++++++++++++++ .../kompilator/tokenization/LexerTest.java | 18 ++--- 16 files changed, 285 insertions(+), 23 deletions(-) create mode 100644 src/main/java/no/eliashaugsbakk/kompilator/parsing/ParserException.java create mode 100644 src/main/java/no/eliashaugsbakk/kompilator/parsing/node/ASTNode.java create mode 100644 src/main/java/no/eliashaugsbakk/kompilator/parsing/node/Program.java create mode 100644 src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/Expression.java create mode 100644 src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/FunctionCall.java create mode 100644 src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/StringLiteral.java create mode 100644 src/main/java/no/eliashaugsbakk/kompilator/parsing/node/statement/ExpressionStatement.java create mode 100644 src/main/java/no/eliashaugsbakk/kompilator/parsing/node/statement/Statement.java create mode 100644 src/test/java/no/eliashaugsbakk/kompilator/parsing/ParserTest.java diff --git a/src/main/java/no/eliashaugsbakk/kompilator/Main.java b/src/main/java/no/eliashaugsbakk/kompilator/Main.java index aad3753..fa7f79c 100644 --- a/src/main/java/no/eliashaugsbakk/kompilator/Main.java +++ b/src/main/java/no/eliashaugsbakk/kompilator/Main.java @@ -6,6 +6,11 @@ import no.eliashaugsbakk.kompilator.IO.FileReaderWriter; import no.eliashaugsbakk.kompilator.IO.FileReaderWriterException; import no.eliashaugsbakk.kompilator.asmGeneration.AssemblyBuilder; import no.eliashaugsbakk.kompilator.assembleAndLink.AssemblerAndLinker; +import no.eliashaugsbakk.kompilator.parsing.AST; +import no.eliashaugsbakk.kompilator.parsing.Parser; +import no.eliashaugsbakk.kompilator.parsing.ParserException; +import no.eliashaugsbakk.kompilator.tokenization.Lexer; +import no.eliashaugsbakk.kompilator.tokenization.Token; public class Main { public static final String programFileExtension = "spÄ"; @@ -33,6 +38,18 @@ public class Main { System.exit(1); } + List tokens = new Lexer(inputProgram.fileBody()).tokenize(); + AST ast = null; + try { + ast = new Parser(tokens).parse(); + } catch (ParserException e) { + IO.println("Error while parsing: " + e.getMessage()); + } + + // TODO: + // new Analyzer(ast).analyze(); + // List IR = new IRGenerator(ast).generate(); + List IR = List.of(); String assembly = new AssemblyBuilder().createAssembly(IR); diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/AST.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/AST.java index 3f54e3c..95ec77c 100644 --- a/src/main/java/no/eliashaugsbakk/kompilator/parsing/AST.java +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/AST.java @@ -1,4 +1,11 @@ package no.eliashaugsbakk.kompilator.parsing; +import no.eliashaugsbakk.kompilator.parsing.node.ASTNode; + public class AST { + ASTNode root; + + AST(ASTNode root) { + this.root = root; + } } diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/Parser.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/Parser.java index a41a1bf..53af71c 100644 --- a/src/main/java/no/eliashaugsbakk/kompilator/parsing/Parser.java +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/Parser.java @@ -1,4 +1,83 @@ package no.eliashaugsbakk.kompilator.parsing; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.EOF; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.KEYWORD; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.LPAREN; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.RPAREN; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.SEMICOLON; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.STRING; + +import java.util.ArrayList; +import java.util.List; +import no.eliashaugsbakk.kompilator.parsing.node.Program; +import no.eliashaugsbakk.kompilator.parsing.node.expression.Expression; +import no.eliashaugsbakk.kompilator.parsing.node.expression.FunctionCall; +import no.eliashaugsbakk.kompilator.parsing.node.expression.StringLiteral; +import no.eliashaugsbakk.kompilator.parsing.node.statement.ExpressionStatement; +import no.eliashaugsbakk.kompilator.tokenization.Token; + public class Parser { + private final List tokens; + private int position = 0; + + public Parser(List tokens) { + this.tokens = tokens; + } + + public AST parse() throws ParserException { + Program rootNode = new Program(); + + while (position < tokens.size()) { + Token token = tokens.get(position); + + if (token.type() == KEYWORD) { + String functionName = token.value(); + position++; + + List arguments = parseArguments(); + + FunctionCall functionCall = new FunctionCall(functionName, arguments); + rootNode.addStatement(new ExpressionStatement(functionCall)); + } else if (token.type() == EOF) { + break; + } else { + IO.println("Unknown token: " + token.value() + ". Skipping..."); + position++; + } + } + + return new AST(rootNode); + } + + List parseArguments() throws ParserException { + List arguments = new ArrayList<>(); + + if (position >= tokens.size() || tokens.get(position).type() != LPAREN) { + throw new ParserException(tokens.get(position).line(), tokens.get(position).colum(), + "Unexpected token:" + tokens.get(position).value() + "\n Expected: ("); + } + position++; + + while (position < tokens.size() && tokens.get(position).type() != RPAREN) { + if (tokens.get(position).type() == STRING) { + arguments.add(new StringLiteral(tokens.get(position).value())); + } + position++; + } + + if (position >= tokens.size()) { + throw new ParserException(-1, -1, "Unexpected end of file, expected )"); + } + position++; + + if (position >= tokens.size() || tokens.get(position).type() != SEMICOLON) { + throw new ParserException(position < tokens.size() ? tokens.get(position).line() : -1, + position < tokens.size() ? tokens.get(position).colum() : -1, + "Unexpected token: " + (position < tokens.size() ? tokens.get(position).value() : "EOF") + + "\n" + "Expected: ;"); + } + position++; + + return arguments; + } } diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/ParserException.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/ParserException.java new file mode 100644 index 0000000..ac96a5c --- /dev/null +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/ParserException.java @@ -0,0 +1,7 @@ +package no.eliashaugsbakk.kompilator.parsing; + +public class ParserException extends Exception { + public ParserException(int line, int column, String message) { + super(line + ":" + column + ", " + message); + } +} diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/ASTNode.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/ASTNode.java new file mode 100644 index 0000000..1eb201c --- /dev/null +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/ASTNode.java @@ -0,0 +1,7 @@ +package no.eliashaugsbakk.kompilator.parsing.node; + +/** + * Base class for all nodes in the Abstract Syntax Tree. + */ +public abstract class ASTNode { +} diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/Program.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/Program.java new file mode 100644 index 0000000..0c7cd32 --- /dev/null +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/Program.java @@ -0,0 +1,16 @@ +package no.eliashaugsbakk.kompilator.parsing.node; + +import java.util.ArrayList; +import java.util.List; +import no.eliashaugsbakk.kompilator.parsing.node.statement.Statement; + +/** + * Root node of the AST. Contains all top-level statements. + */ +public class Program extends ASTNode { + public List statements = new ArrayList<>(); + + public void addStatement(Statement statement) { + this.statements.add(statement); + } +} diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/Expression.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/Expression.java new file mode 100644 index 0000000..6ca58b2 --- /dev/null +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/Expression.java @@ -0,0 +1,17 @@ +package no.eliashaugsbakk.kompilator.parsing.node.expression; + +import no.eliashaugsbakk.kompilator.parsing.node.ASTNode; + +/** + * Base class for all expression nodes. + * An expression is a piece of code that evaluates to a value. + * Expressions cannot stand alone as statements; they must be used within statements. + *

+ * Examples: + * - "Hello" (string literal expression) + * - 42 (number literal expression) + * - x + 5 (binary operation expression) + * - myFunction() (function call expression) + */ +public abstract class Expression extends ASTNode { +} diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/FunctionCall.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/FunctionCall.java new file mode 100644 index 0000000..3c46ec4 --- /dev/null +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/FunctionCall.java @@ -0,0 +1,16 @@ +package no.eliashaugsbakk.kompilator.parsing.node.expression; + +import java.util.List; + +/** + * Represents a function call statement (e.g., print("Hello, world")). + */ +public class FunctionCall extends Expression { + public String functionName; + public List arguments; + + public FunctionCall(String functionName, List arguments) { + this.functionName = functionName; + this.arguments = arguments; + } +} diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/StringLiteral.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/StringLiteral.java new file mode 100644 index 0000000..67ce405 --- /dev/null +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/expression/StringLiteral.java @@ -0,0 +1,12 @@ +package no.eliashaugsbakk.kompilator.parsing.node.expression; + +/** + * Represents a string literal expression (e.g., "Hello, World"). + */ +public class StringLiteral extends Expression { + public final String value; + + public StringLiteral(String value) { + this.value = value; + } +} diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/statement/ExpressionStatement.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/statement/ExpressionStatement.java new file mode 100644 index 0000000..64bc0a8 --- /dev/null +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/statement/ExpressionStatement.java @@ -0,0 +1,10 @@ +package no.eliashaugsbakk.kompilator.parsing.node.statement; + +import no.eliashaugsbakk.kompilator.parsing.node.expression.Expression; + +public class ExpressionStatement extends Statement { + public Expression expression; + public ExpressionStatement(Expression expression) { + this.expression = expression; + } +} diff --git a/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/statement/Statement.java b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/statement/Statement.java new file mode 100644 index 0000000..fc52f9a --- /dev/null +++ b/src/main/java/no/eliashaugsbakk/kompilator/parsing/node/statement/Statement.java @@ -0,0 +1,15 @@ +package no.eliashaugsbakk.kompilator.parsing.node.statement; + +import no.eliashaugsbakk.kompilator.parsing.node.ASTNode; + +/** + * Base class for all statement nodes. + * A statement is a top-level line of code that performs an action. + *

+ * Examples: + * - print("Hello"); (function call statement) + * - var x: int = 5; (variable declaration statement) + * - if (x > 0) { } (conditional statement) + */ +public abstract class Statement extends ASTNode { +} diff --git a/src/main/java/no/eliashaugsbakk/kompilator/tokenization/Lexer.java b/src/main/java/no/eliashaugsbakk/kompilator/tokenization/Lexer.java index 4149b4f..62d2359 100644 --- a/src/main/java/no/eliashaugsbakk/kompilator/tokenization/Lexer.java +++ b/src/main/java/no/eliashaugsbakk/kompilator/tokenization/Lexer.java @@ -30,7 +30,7 @@ public class Lexer { this.input = input; } - List tokenize() { + public List tokenize() { while (position < input.length()) { char current = input.charAt(position); diff --git a/src/main/java/no/eliashaugsbakk/kompilator/tokenization/Token.java b/src/main/java/no/eliashaugsbakk/kompilator/tokenization/Token.java index 519b6b6..09c5d83 100644 --- a/src/main/java/no/eliashaugsbakk/kompilator/tokenization/Token.java +++ b/src/main/java/no/eliashaugsbakk/kompilator/tokenization/Token.java @@ -1,15 +1,4 @@ package no.eliashaugsbakk.kompilator.tokenization; -class Token { - TokenType type; - String value; - int line; - int colum; - - Token(TokenType type, String value, int line, int colum) { - this.type = type; - this.value = value; - this.line = line; - this.colum = colum; - } +public record Token(TokenType type, String value, int line, int colum) { } diff --git a/src/main/java/no/eliashaugsbakk/kompilator/tokenization/TokenType.java b/src/main/java/no/eliashaugsbakk/kompilator/tokenization/TokenType.java index 408abb2..696b46c 100644 --- a/src/main/java/no/eliashaugsbakk/kompilator/tokenization/TokenType.java +++ b/src/main/java/no/eliashaugsbakk/kompilator/tokenization/TokenType.java @@ -1,6 +1,6 @@ package no.eliashaugsbakk.kompilator.tokenization; -enum TokenType { +public enum TokenType { KEYWORD, // print, var, if, while, function, etc. IDENTIFIER, // variable_1 STRING, // "Hello, World!" diff --git a/src/test/java/no/eliashaugsbakk/kompilator/parsing/ParserTest.java b/src/test/java/no/eliashaugsbakk/kompilator/parsing/ParserTest.java new file mode 100644 index 0000000..133e9a6 --- /dev/null +++ b/src/test/java/no/eliashaugsbakk/kompilator/parsing/ParserTest.java @@ -0,0 +1,70 @@ +package no.eliashaugsbakk.kompilator.parsing; + +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.KEYWORD; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.LPAREN; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.RPAREN; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.SEMICOLON; +import static no.eliashaugsbakk.kompilator.tokenization.TokenType.STRING; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import no.eliashaugsbakk.kompilator.parsing.node.Program; +import no.eliashaugsbakk.kompilator.parsing.node.expression.FunctionCall; +import no.eliashaugsbakk.kompilator.parsing.node.expression.StringLiteral; +import no.eliashaugsbakk.kompilator.parsing.node.statement.ExpressionStatement; +import no.eliashaugsbakk.kompilator.tokenization.Token; +import org.junit.jupiter.api.Test; + +class ParserTest { + + @Test + void validSyntaxBuildsCorrectTree() throws ParserException { + List tokens = List.of( + new Token(KEYWORD, "print", 1, 0), + new Token(LPAREN, "(", 1, 5), + new Token(STRING, "Hello", 1, 6), + new Token(RPAREN, ")", 1, 13), + new Token(SEMICOLON, ";", 1, 14) + ); + + AST ast = new Parser(tokens).parse(); + + // Verify tree structure + assertNotNull(ast.root); + assertInstanceOf(Program.class, ast.root); + Program program = (Program) ast.root; + assertEquals(1, program.statements.size()); + + ExpressionStatement stmt = (ExpressionStatement) program.statements.getFirst(); + assertInstanceOf(FunctionCall.class, stmt.expression); + + FunctionCall call = (FunctionCall) stmt.expression; + assertEquals("print", call.functionName); + assertEquals(1, call.arguments.size()); + assertEquals("Hello", ((StringLiteral) call.arguments.getFirst()).value); + } + + @Test + void missingSemicolonThrows() { + List tokens = List.of( + new Token(KEYWORD, "print", 1, 0), + new Token(LPAREN, "(", 1, 5), + new Token(STRING, "Hello", 1, 6), + new Token(RPAREN, ")", 1, 13) + ); + + assertThrows(ParserException.class, () -> new Parser(tokens).parse()); + } + + @Test + void missingParenthesisThrows() { + List tokens = List.of( + new Token(KEYWORD, "print", 1, 0), + new Token(STRING, "Hello", 1, 5), + new Token(RPAREN, ")", 1, 12), + new Token(SEMICOLON, ";", 1, 13) + ); + + assertThrows(ParserException.class, () -> new Parser(tokens).parse()); + } +} diff --git a/src/test/java/no/eliashaugsbakk/kompilator/tokenization/LexerTest.java b/src/test/java/no/eliashaugsbakk/kompilator/tokenization/LexerTest.java index d1fedb9..7f4a5b3 100644 --- a/src/test/java/no/eliashaugsbakk/kompilator/tokenization/LexerTest.java +++ b/src/test/java/no/eliashaugsbakk/kompilator/tokenization/LexerTest.java @@ -24,17 +24,17 @@ class LexerTest { // tokens.forEach(t-> IO.println(t.type + " : " + t.value)); - assertSame(KEYWORD, tokens.getFirst().type); - assertEquals(1, tokens.get(0).line); - assertEquals(1, tokens.get(0).colum); + assertSame(KEYWORD, tokens.getFirst().type()); + assertEquals(1, tokens.get(0).line()); + assertEquals(1, tokens.get(0).colum()); - assertSame(LPAREN, tokens.get(1).type); + assertSame(LPAREN, tokens.get(1).type()); - assertSame(STRING, tokens.get(2).type); - assertTrue(tokens.get(2).value.contains("Hello, World")); + assertSame(STRING, tokens.get(2).type()); + assertTrue(tokens.get(2).value().contains("Hello, World")); - assertSame(RPAREN, tokens.get(3).type); - assertSame(SEMICOLON, tokens.get(4).type); - assertSame(EOF, tokens.get(5).type); + assertSame(RPAREN, tokens.get(3).type()); + assertSame(SEMICOLON, tokens.get(4).type()); + assertSame(EOF, tokens.get(5).type()); } } -- cgit v1.2.3