From 578bf1f04b48f48225cfa794a27426cca4f6384a Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Fri, 31 Jul 2026 10:50:10 +0100 Subject: Support singleton structs --- .../UntypedStructDefinitionNodeMatcher.java | 2 +- .../parser/ParserStructDefinitionTests.java | 25 ++++++++++-- .../typechecker/TypeCheckerNamespaceTests.java | 4 +- .../TypeCheckerStructDefinitionTests.java | 46 ++++++++++++++++++---- .../typechecker/TypeCheckerSumDefinitionTests.java | 7 ++-- 5 files changed, 68 insertions(+), 16 deletions(-) (limited to 'src/test/java') diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedStructDefinitionNodeMatcher.java b/src/test/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedStructDefinitionNodeMatcher.java index 429e438..dd66994 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedStructDefinitionNodeMatcher.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedStructDefinitionNodeMatcher.java @@ -36,7 +36,7 @@ public class UntypedStructDefinitionNodeMatcher implements org.zwobble.precisely return new org.zwobble.hobgoblin.compiler.ast.untyped.UntypedStructDefinitionNodeMatcher(submatchers); } - public org.zwobble.hobgoblin.compiler.ast.untyped.UntypedStructDefinitionNodeMatcher withFields(org.zwobble.precisely.Matcher> fields) { + public org.zwobble.hobgoblin.compiler.ast.untyped.UntypedStructDefinitionNodeMatcher withFields(org.zwobble.precisely.Matcher>> fields) { var submatchers = new java.util.ArrayList>(this.submatchers); submatchers.add(org.zwobble.precisely.Matchers.has("fields", org.zwobble.hobgoblin.compiler.ast.untyped.UntypedStructDefinitionNode::fields, fields)); return new org.zwobble.hobgoblin.compiler.ast.untyped.UntypedStructDefinitionNodeMatcher(submatchers); diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/parser/ParserStructDefinitionTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/parser/ParserStructDefinitionTests.java index 6c90356..0b8ba5e 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/parser/ParserStructDefinitionTests.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/parser/ParserStructDefinitionTests.java @@ -24,7 +24,8 @@ public class ParserStructDefinitionTests { assertThat(node, instanceOf( UntypedStructDefinitionNode.class, - has("name", UntypedStructDefinitionNode::name, equalTo("Point")) + has("name", UntypedStructDefinitionNode::name, equalTo("Point")), + has("fields", UntypedStructDefinitionNode::fields, isOptionalOf(isSequence())) )); } @@ -43,7 +44,7 @@ public class ParserStructDefinitionTests { assertThat(node, instanceOf( UntypedStructDefinitionNode.class, - has("fields", UntypedStructDefinitionNode::fields, isSequence( + has("fields", UntypedStructDefinitionNode::fields, isOptionalOf(isSequence( instanceOf( UntypedStructFieldDefinitionNode.class, has("name", UntypedStructFieldDefinitionNode::name, equalTo("x")), @@ -68,7 +69,25 @@ public class ParserStructDefinitionTests { ) ) ) - )) + ))) + )); + } + + @Test + public void singletonStruct() { + var source = """ + struct Point { + singleton; + }"""; + + var node = parseString( + source, + Parser::parseNamespaceStatement + ); + + assertThat(node, instanceOf( + UntypedStructDefinitionNode.class, + has("fields", UntypedStructDefinitionNode::fields, isOptionalEmpty()) )); } diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerNamespaceTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerNamespaceTests.java index 2f7cc6b..b5814a8 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerNamespaceTests.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerNamespaceTests.java @@ -100,9 +100,9 @@ public class TypeCheckerNamespaceTests { )); assertThat( context.toTypesInfo().fieldsOf(new StructType(NamespaceName.of("a", "b"), "X")), - isSequence( + isOptionalOf(isSequence( isField("y", new SimpleNativeType(NamespaceName.of("a", "c"), "Y")) - ) + )) ); } diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerStructDefinitionTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerStructDefinitionTests.java index 0efe3b4..d86fd59 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerStructDefinitionTests.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerStructDefinitionTests.java @@ -14,6 +14,7 @@ import org.zwobble.hobgoblin.compiler.types.SimpleNativeType; import org.zwobble.hobgoblin.compiler.types.StructType; import java.util.List; +import java.util.Optional; import static org.zwobble.hobgoblin.compiler.typechecker.TypeCheckerTesting.typeCheckNamespaceStatement; import static org.zwobble.hobgoblin.compiler.types.TypeMatchers.isField; @@ -64,7 +65,7 @@ public class TypeCheckerStructDefinitionTests { has( "fields", TypedStructDefinitionNode::fields, - isSequence( + isOptionalOf(isSequence( allOf( has("name", TypedStructFieldDefinitionNode::name, equalTo("a")), has("type", TypedStructFieldDefinitionNode::type, has( @@ -81,16 +82,47 @@ public class TypeCheckerStructDefinitionTests { equalTo(int64Type) )) ) - ) + )) ) )); var typesInfo = globalContext.toTypesInfo(); assertThat( typesInfo.fieldsOf(new StructType(namespaceName, "X")), - isSequence( + isOptionalOf(isSequence( isField("a", int32Type), isField("b", int64Type) + )) + ); + } + + @Test + public void singletonStructHasNoFields() { + var int32Type = SimpleNativeType.builtin("Int32"); + var int64Type = SimpleNativeType.builtin("Int64"); + var untyped = UntypedStructDefinitionNode.arbitrary() + .withName("X") + .withFields(Optional.empty()) + .build(); + var globalContext = TypeCheckerGlobalContext.initial(); + globalContext.addNativeType(int32Type); + globalContext.addNativeType(int64Type); + var namespaceName = NamespaceName.of("a", "b"); + var namespaceContext = globalContext.enterNamespace(namespaceName); + + var typed = typeCheckNamespaceStatement(untyped, namespaceContext); + + assertThat(typed, instanceOf( + TypedStructDefinitionNode.class, + has( + "fields", + TypedStructDefinitionNode::fields, + isOptionalEmpty() ) + )); + var typesInfo = globalContext.toTypesInfo(); + assertThat( + typesInfo.fieldsOf(new StructType(namespaceName, "X")), + isOptionalEmpty() ); } @@ -130,7 +162,7 @@ public class TypeCheckerStructDefinitionTests { has( "fields", TypedStructDefinitionNode::fields, - isSequence( + isOptionalOf(isSequence( allOf( has("name", TypedStructFieldDefinitionNode::name, equalTo("value")), has("type", TypedStructFieldDefinitionNode::type, has( @@ -139,7 +171,7 @@ public class TypeCheckerStructDefinitionTests { equalTo(new StructType(namespaceName, "Y")) )) ) - ) + )) ) ), instanceOf( @@ -148,7 +180,7 @@ public class TypeCheckerStructDefinitionTests { has( "fields", TypedStructDefinitionNode::fields, - isSequence( + isOptionalOf(isSequence( allOf( has("name", TypedStructFieldDefinitionNode::name, equalTo("value")), has("type", TypedStructFieldDefinitionNode::type, has( @@ -157,7 +189,7 @@ public class TypeCheckerStructDefinitionTests { equalTo(int32Type) )) ) - ) + )) ) ) ) diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerSumDefinitionTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerSumDefinitionTests.java index bfc9280..68b1b06 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerSumDefinitionTests.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerSumDefinitionTests.java @@ -12,6 +12,7 @@ import org.zwobble.hobgoblin.compiler.typechecker.errors.UnexpectedTypeError; import org.zwobble.hobgoblin.compiler.types.*; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.zwobble.hobgoblin.compiler.typechecker.TypeCheckerTesting.typeCheckNamespaceStatement; @@ -159,7 +160,7 @@ public class TypeCheckerSumDefinitionTests { .enterNamespace(namespaceName); var variantType = new StructType(namespaceName, "Square"); namespaceContext.declare("Square", new TypeLevelValueType(variantType), NullSource.INSTANCE); - namespaceContext.defineStructType(variantType, List.of()); + namespaceContext.defineStructType(variantType, Optional.of(List.of())); var error = assertThrows( SubtypeIsMissingFieldError.class, @@ -196,7 +197,7 @@ public class TypeCheckerSumDefinitionTests { .enterNamespace(namespaceName); var variantType = new StructType(namespaceName, "Square"); namespaceContext.declare("Square", new TypeLevelValueType(variantType), NullSource.INSTANCE); - namespaceContext.defineStructType(variantType, List.of(new Field("area", int64Type, NullSource.INSTANCE))); + namespaceContext.defineStructType(variantType, Optional.of(List.of(new Field("area", int64Type, NullSource.INSTANCE)))); // TODO: more specific error var error = assertThrows( @@ -232,7 +233,7 @@ public class TypeCheckerSumDefinitionTests { .enterNamespace(namespaceName); var variantType = new StructType(namespaceName, "Square"); namespaceContext.declare("Square", new TypeLevelValueType(variantType), NullSource.INSTANCE); - namespaceContext.defineStructType(variantType, List.of()); + namespaceContext.defineStructType(variantType, Optional.of(List.of())); assertThrows( SubtypeIsMissingFieldError.class, -- cgit v1.2.3