From 2394ae418641b638db23f66bb289e2771d8a2b1b Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Mon, 20 Apr 2026 13:04:57 +0100 Subject: Parse empty namespace --- .../ast/untyped/UntypedNamespaceNode.java | 6 ++++- .../ast/untyped/UntypedNamespaceStatementNode.java | 4 ++++ .../org/zwobble/hobgoblin/errors/SourceError.java | 20 ++++++++++++++++ .../org/zwobble/hobgoblin/parser/ParseError.java | 12 +++------- .../java/org/zwobble/hobgoblin/parser/Parser.java | 10 ++++++-- .../java/org/zwobble/hobgoblin/parser/Token.java | 3 +++ .../zwobble/hobgoblin/parser/TokenIterator.java | 18 ++++++++++++++ .../org/zwobble/hobgoblin/parser/Tokenizer.java | 7 +++--- .../java/org/zwobble/hobgoblin/parser/Tokens.java | 9 +++++++ .../hobgoblin/parser/UnexpectedTokenError.java | 28 ++++++++++++++++++++++ .../org/zwobble/hobgoblin/types/NamespaceName.java | 15 ++++++++++++ .../hobgoblin/parser/ParserNamespaceTests.java | 27 +++++++++++++++++++++ .../zwobble/hobgoblin/parser/ParserTesting.java | 22 +++++++++++++++++ .../zwobble/hobgoblin/parser/TokenizerTests.java | 14 +++++------ 14 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceStatementNode.java create mode 100644 src/main/java/org/zwobble/hobgoblin/errors/SourceError.java create mode 100644 src/main/java/org/zwobble/hobgoblin/parser/Tokens.java create mode 100644 src/main/java/org/zwobble/hobgoblin/parser/UnexpectedTokenError.java create mode 100644 src/main/java/org/zwobble/hobgoblin/types/NamespaceName.java create mode 100644 src/test/java/org/zwobble/hobgoblin/parser/ParserNamespaceTests.java create mode 100644 src/test/java/org/zwobble/hobgoblin/parser/ParserTesting.java (limited to 'src') diff --git a/src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceNode.java b/src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceNode.java index bbbbd8a..abcf7cc 100644 --- a/src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceNode.java +++ b/src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceNode.java @@ -1,4 +1,8 @@ package org.zwobble.hobgoblin.ast.untyped; -public class UntypedNamespaceNode { +import org.zwobble.hobgoblin.types.NamespaceName; + +import java.util.List; + +public record UntypedNamespaceNode(NamespaceName namespaceName, List body) { } diff --git a/src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceStatementNode.java b/src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceStatementNode.java new file mode 100644 index 0000000..21c20d9 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceStatementNode.java @@ -0,0 +1,4 @@ +package org.zwobble.hobgoblin.ast.untyped; + +public interface UntypedNamespaceStatementNode { +} diff --git a/src/main/java/org/zwobble/hobgoblin/errors/SourceError.java b/src/main/java/org/zwobble/hobgoblin/errors/SourceError.java new file mode 100644 index 0000000..1ebb679 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/errors/SourceError.java @@ -0,0 +1,20 @@ +package org.zwobble.hobgoblin.errors; + +import org.zwobble.sourcetext.SourceRange; + +/** + * An error in the source code. These errors are expected when attempting to + * compile invalid code. + */ +public class SourceError extends RuntimeException { + private final SourceRange sourceRange; + + public SourceError(String message, SourceRange sourceRange) { + super(message); + this.sourceRange = sourceRange; + } + + public SourceRange sourceRange() { + return sourceRange; + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/parser/ParseError.java b/src/main/java/org/zwobble/hobgoblin/parser/ParseError.java index 27fa8c5..16b0c4f 100644 --- a/src/main/java/org/zwobble/hobgoblin/parser/ParseError.java +++ b/src/main/java/org/zwobble/hobgoblin/parser/ParseError.java @@ -1,8 +1,9 @@ package org.zwobble.hobgoblin.parser; +import org.zwobble.hobgoblin.errors.SourceError; import org.zwobble.sourcetext.SourceRange; -public class ParseError extends RuntimeException { +public class ParseError extends SourceError { public static ParseError unexpectedTextError( String expected, String actual, @@ -16,14 +17,7 @@ public class ParseError extends RuntimeException { return new ParseError(message, sourceRange); } - private final SourceRange sourceRange; - public ParseError(String message, SourceRange sourceRange) { - super(message); - this.sourceRange = sourceRange; - } - - public SourceRange sourceRange() { - return sourceRange; + super(message, sourceRange); } } diff --git a/src/main/java/org/zwobble/hobgoblin/parser/Parser.java b/src/main/java/org/zwobble/hobgoblin/parser/Parser.java index baa769e..ac391e8 100644 --- a/src/main/java/org/zwobble/hobgoblin/parser/Parser.java +++ b/src/main/java/org/zwobble/hobgoblin/parser/Parser.java @@ -1,12 +1,18 @@ package org.zwobble.hobgoblin.parser; import org.zwobble.hobgoblin.ast.untyped.UntypedNamespaceNode; +import org.zwobble.hobgoblin.types.NamespaceName; + +import java.util.List; public class Parser { private Parser() { } - public static UntypedNamespaceNode parseNamespace() { - return new UntypedNamespaceNode(); + public static UntypedNamespaceNode parseNamespace( + TokenIterator tokens, + NamespaceName namespaceName + ) { + return new UntypedNamespaceNode(namespaceName, List.of()); } } diff --git a/src/main/java/org/zwobble/hobgoblin/parser/Token.java b/src/main/java/org/zwobble/hobgoblin/parser/Token.java index ca25fa7..a3edf50 100644 --- a/src/main/java/org/zwobble/hobgoblin/parser/Token.java +++ b/src/main/java/org/zwobble/hobgoblin/parser/Token.java @@ -3,4 +3,7 @@ package org.zwobble.hobgoblin.parser; import org.zwobble.sourcetext.SourceRange; record Token(TokenType tokenType, SourceRange sourceRange) { + CharSequence charSequence() { + return sourceRange.charSequence(); + } } diff --git a/src/main/java/org/zwobble/hobgoblin/parser/TokenIterator.java b/src/main/java/org/zwobble/hobgoblin/parser/TokenIterator.java index e1f4ae2..b457313 100644 --- a/src/main/java/org/zwobble/hobgoblin/parser/TokenIterator.java +++ b/src/main/java/org/zwobble/hobgoblin/parser/TokenIterator.java @@ -3,7 +3,25 @@ package org.zwobble.hobgoblin.parser; import java.util.List; class TokenIterator { + private final List tokens; + private int tokenIndex; + private final Token tokenEnd; + TokenIterator(List tokens, Token tokenEnd) { + this.tokens = tokens; + this.tokenEnd = tokenEnd; + this.tokenIndex = 0; + } + + public Token peek() { + return this.getAbsolute(this.tokenIndex); + } + private Token getAbsolute(int index) { + if (index < this.tokens.size()) { + return this.tokens.get(index); + } else { + return this.tokenEnd; + } } } diff --git a/src/main/java/org/zwobble/hobgoblin/parser/Tokenizer.java b/src/main/java/org/zwobble/hobgoblin/parser/Tokenizer.java index a642d76..82c843d 100644 --- a/src/main/java/org/zwobble/hobgoblin/parser/Tokenizer.java +++ b/src/main/java/org/zwobble/hobgoblin/parser/Tokenizer.java @@ -6,14 +6,13 @@ import org.zwobble.sourcetext.SourceRange; import org.zwobble.sourcetext.SourceText; import java.util.ArrayList; -import java.util.List; import java.util.Optional; class Tokenizer { private Tokenizer() { } - static List tokenize(SourceText sourceText) { + static Tokens tokenize(SourceText sourceText) { var iterator = new CharacterIterator(sourceText); var tokens = new ArrayList(); @@ -34,7 +33,9 @@ class Tokenizer { } } - return tokens; + var tokenEnd = new Token(TokenType.END, iterator.characterSourceRange()); + + return new Tokens(tokens, tokenEnd); } private static Optional tokenizeToken(CharacterIterator iterator) { diff --git a/src/main/java/org/zwobble/hobgoblin/parser/Tokens.java b/src/main/java/org/zwobble/hobgoblin/parser/Tokens.java new file mode 100644 index 0000000..3e979a3 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/parser/Tokens.java @@ -0,0 +1,9 @@ +package org.zwobble.hobgoblin.parser; + +import java.util.List; + +record Tokens(List tokens, Token tokenEnd) { + TokenIterator iterator() { + return new TokenIterator(this.tokens, this.tokenEnd); + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/parser/UnexpectedTokenError.java b/src/main/java/org/zwobble/hobgoblin/parser/UnexpectedTokenError.java new file mode 100644 index 0000000..2ff6e2c --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/parser/UnexpectedTokenError.java @@ -0,0 +1,28 @@ +package org.zwobble.hobgoblin.parser; + +import org.zwobble.hobgoblin.errors.SourceError; + +public class UnexpectedTokenError extends SourceError { + private final String expected; + private final Token actual; + + public UnexpectedTokenError( + String expected, + Token actual + ) { + super( + "Expected: " + expected + "\nBut got: " + actual.tokenType() + " \"" + actual.charSequence() + "\"", + actual.sourceRange() + ); + this.expected = expected; + this.actual = actual; + } + + public String getExpected() { + return expected; + } + + public Token getActual() { + return actual; + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/types/NamespaceName.java b/src/main/java/org/zwobble/hobgoblin/types/NamespaceName.java new file mode 100644 index 0000000..143d0c4 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/types/NamespaceName.java @@ -0,0 +1,15 @@ +package org.zwobble.hobgoblin.types; + +import java.util.Arrays; +import java.util.List; + +public record NamespaceName(List parts) { + public static NamespaceName of(String... parts) { + return new NamespaceName(Arrays.asList(parts)); + } + + @Override + public String toString() { + return String.join("/", parts); + } +} diff --git a/src/test/java/org/zwobble/hobgoblin/parser/ParserNamespaceTests.java b/src/test/java/org/zwobble/hobgoblin/parser/ParserNamespaceTests.java new file mode 100644 index 0000000..89527e9 --- /dev/null +++ b/src/test/java/org/zwobble/hobgoblin/parser/ParserNamespaceTests.java @@ -0,0 +1,27 @@ +package org.zwobble.hobgoblin.parser; + +import org.junit.jupiter.api.Test; +import org.zwobble.hobgoblin.ast.untyped.UntypedNamespaceNode; +import org.zwobble.hobgoblin.types.NamespaceName; + +import static org.zwobble.hobgoblin.parser.ParserTesting.parseString; +import static org.zwobble.precisely.AssertThat.assertThat; +import static org.zwobble.precisely.Matchers.*; + +public class ParserNamespaceTests { + @Test + public void emptyNamespaceHasEmptyBody() { + var source = ""; + + var node = parseString( + source, + tokens -> Parser.parseNamespace(tokens, NamespaceName.of("Example", "Project")) + ); + + assertThat(node, instanceOf( + UntypedNamespaceNode.class, + has("namespaceName", x -> x.namespaceName(), equalTo(NamespaceName.of("Example", "Project"))), + has("body", x -> x.body(), isSequence()) + )); + } +} diff --git a/src/test/java/org/zwobble/hobgoblin/parser/ParserTesting.java b/src/test/java/org/zwobble/hobgoblin/parser/ParserTesting.java new file mode 100644 index 0000000..14cfb62 --- /dev/null +++ b/src/test/java/org/zwobble/hobgoblin/parser/ParserTesting.java @@ -0,0 +1,22 @@ +package org.zwobble.hobgoblin.parser; + +import org.zwobble.sourcetext.SourceText; + +import java.util.function.Function; + +import static org.zwobble.precisely.AssertThat.assertThat; +import static org.zwobble.precisely.Matchers.equalTo; + +public class ParserTesting { + public static T parseString( + String sourceContents, + Function parseTokens + ) { + var sourceText = SourceText.fromString("", sourceContents); + var tokens = Tokenizer.tokenize(sourceText); + var tokenIterator = tokens.iterator(); + var result = parseTokens.apply(tokenIterator); + assertThat(tokenIterator.peek().tokenType(), equalTo(TokenType.END)); + return result; + } +} diff --git a/src/test/java/org/zwobble/hobgoblin/parser/TokenizerTests.java b/src/test/java/org/zwobble/hobgoblin/parser/TokenizerTests.java index 05848a3..6f77470 100644 --- a/src/test/java/org/zwobble/hobgoblin/parser/TokenizerTests.java +++ b/src/test/java/org/zwobble/hobgoblin/parser/TokenizerTests.java @@ -15,7 +15,7 @@ public class TokenizerTests { var tokens = Tokenizer.tokenize(sourceText); - assertThat(tokens, isSequence()); + assertThat(tokens.tokens(), isSequence()); } @Test @@ -24,7 +24,7 @@ public class TokenizerTests { var tokens = Tokenizer.tokenize(sourceText); - assertThat(tokens, isSequence( + assertThat(tokens.tokens(), isSequence( equalTo(new Token(TokenType.KEYWORD_STRUCT, sourceRange(sourceText, 0, 6))) )); } @@ -35,7 +35,7 @@ public class TokenizerTests { var tokens = Tokenizer.tokenize(sourceText); - assertThat(tokens, isSequence( + assertThat(tokens.tokens(), isSequence( equalTo(new Token(TokenType.IDENTIFIER, sourceRange(sourceText, 0, 4))) )); } @@ -46,7 +46,7 @@ public class TokenizerTests { var tokens = Tokenizer.tokenize(sourceText); - assertThat(tokens, isSequence( + assertThat(tokens.tokens(), isSequence( equalTo(new Token(TokenType.SYMBOL_BRACE_OPEN, sourceRange(sourceText, 0, 1))) )); } @@ -57,7 +57,7 @@ public class TokenizerTests { var tokens = Tokenizer.tokenize(sourceText); - assertThat(tokens, isSequence( + assertThat(tokens.tokens(), isSequence( equalTo(new Token(TokenType.SYMBOL_BRACE_CLOSE, sourceRange(sourceText, 0, 1))) )); } @@ -68,7 +68,7 @@ public class TokenizerTests { var tokens = Tokenizer.tokenize(sourceText); - assertThat(tokens, isSequence( + assertThat(tokens.tokens(), isSequence( equalTo(new Token(TokenType.IDENTIFIER, sourceRange(sourceText, 2, 6))) )); } @@ -79,7 +79,7 @@ public class TokenizerTests { var tokens = Tokenizer.tokenize(sourceText); - assertThat(tokens, isSequence( + assertThat(tokens.tokens(), isSequence( equalTo(new Token(TokenType.IDENTIFIER, sourceRange(sourceText, 0, 3))), equalTo(new Token(TokenType.IDENTIFIER, sourceRange(sourceText, 4, 7))), equalTo(new Token(TokenType.IDENTIFIER, sourceRange(sourceText, 10, 15))) -- cgit v1.2.3