summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2026-04-20 13:04:57 +0100
committerMichael Williamson <mike@zwobble.org>2026-04-20 13:04:57 +0100
commit2394ae418641b638db23f66bb289e2771d8a2b1b (patch)
tree9bae55d288dadf1b2982f25c728640ad85cdc6a7
parent7d088b0a827b2ec7d32968ec36c376d13116d76d (diff)
Parse empty namespace
-rw-r--r--src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceNode.java6
-rw-r--r--src/main/java/org/zwobble/hobgoblin/ast/untyped/UntypedNamespaceStatementNode.java4
-rw-r--r--src/main/java/org/zwobble/hobgoblin/errors/SourceError.java20
-rw-r--r--src/main/java/org/zwobble/hobgoblin/parser/ParseError.java12
-rw-r--r--src/main/java/org/zwobble/hobgoblin/parser/Parser.java10
-rw-r--r--src/main/java/org/zwobble/hobgoblin/parser/Token.java3
-rw-r--r--src/main/java/org/zwobble/hobgoblin/parser/TokenIterator.java18
-rw-r--r--src/main/java/org/zwobble/hobgoblin/parser/Tokenizer.java7
-rw-r--r--src/main/java/org/zwobble/hobgoblin/parser/Tokens.java9
-rw-r--r--src/main/java/org/zwobble/hobgoblin/parser/UnexpectedTokenError.java28
-rw-r--r--src/main/java/org/zwobble/hobgoblin/types/NamespaceName.java15
-rw-r--r--src/test/java/org/zwobble/hobgoblin/parser/ParserNamespaceTests.java27
-rw-r--r--src/test/java/org/zwobble/hobgoblin/parser/ParserTesting.java22
-rw-r--r--src/test/java/org/zwobble/hobgoblin/parser/TokenizerTests.java14
14 files changed, 173 insertions, 22 deletions
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<UntypedNamespaceStatementNode> 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<Token> tokens;
+ private int tokenIndex;
+ private final Token tokenEnd;
+
TokenIterator(List<Token> 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<Token> tokenize(SourceText sourceText) {
+ static Tokens tokenize(SourceText sourceText) {
var iterator = new CharacterIterator(sourceText);
var tokens = new ArrayList<Token>();
@@ -34,7 +33,9 @@ class Tokenizer {
}
}
- return tokens;
+ var tokenEnd = new Token(TokenType.END, iterator.characterSourceRange());
+
+ return new Tokens(tokens, tokenEnd);
}
private static Optional<Token> 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<Token> 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<String> 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> T parseString(
+ String sourceContents,
+ Function<TokenIterator, T> parseTokens
+ ) {
+ var sourceText = SourceText.fromString("<string>", 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)))