From 5d41166750e0f05da436bf30c7a642749f7a71c5 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Mon, 10 Aug 2026 10:12:48 +0100 Subject: Prevent singleton struct from being generic --- .../ast/untyped/UntypedStructDefinitionNode.java | 7 +++++++ .../compiler/typechecker/TypeChecker.java | 4 ++++ .../SingletonStructCannotHaveTypeParamsError.java | 10 +++++++++ .../TypeCheckerStructDefinitionTests.java | 24 ++++++++++++++++++---- 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/typechecker/errors/SingletonStructCannotHaveTypeParamsError.java (limited to 'src') diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedStructDefinitionNode.java b/src/main/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedStructDefinitionNode.java index 9742068..29cb885 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedStructDefinitionNode.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedStructDefinitionNode.java @@ -60,5 +60,12 @@ public record UntypedStructDefinitionNode( } // Custom area start: org.zwobble.hobgoblin.compiler.ast.untyped.UntypedStructDefinitionNode body + public boolean isGeneric() { + return this.typeParams.isPresent(); + } + + public boolean isSingleton() { + return this.fields.isEmpty(); + } // Custom area end: org.zwobble.hobgoblin.compiler.ast.untyped.UntypedStructDefinitionNode body } diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java index 63fc357..95ce840 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java @@ -227,6 +227,10 @@ public class TypeChecker { UntypedStructDefinitionNode untyped, TypeCheckerNamespaceContext context ) { + if (untyped.isSingleton() && untyped.isGeneric()) { + throw new SingletonStructCannotHaveTypeParamsError(untyped.source()); + } + var metaType = lookupTypeLevelValue(untyped.name(), untyped.source(), context); var typeOrConstructor = switch (metaType) { case SimpleStructType type -> diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/errors/SingletonStructCannotHaveTypeParamsError.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/errors/SingletonStructCannotHaveTypeParamsError.java new file mode 100644 index 0000000..a630646 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/errors/SingletonStructCannotHaveTypeParamsError.java @@ -0,0 +1,10 @@ +package org.zwobble.hobgoblin.compiler.typechecker.errors; + +import org.zwobble.hobgoblin.compiler.errors.SourceError; +import org.zwobble.hobgoblin.compiler.sources.Source; + +public class SingletonStructCannotHaveTypeParamsError extends SourceError { + public SingletonStructCannotHaveTypeParamsError(Source source) { + super("singleton struct cannot have type params", source); + } +} 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 1de6cbf..fa913fa 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerStructDefinitionTests.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerStructDefinitionTests.java @@ -6,11 +6,13 @@ import org.zwobble.hobgoblin.compiler.ast.typed.TypedStructDefinitionNode; import org.zwobble.hobgoblin.compiler.ast.typed.TypedStructFieldDefinitionNode; import org.zwobble.hobgoblin.compiler.ast.typed.TypedTypeLevelExpressionNode; import org.zwobble.hobgoblin.compiler.ast.untyped.*; +import org.zwobble.hobgoblin.compiler.typechecker.errors.SingletonStructCannotHaveTypeParamsError; 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; import static org.zwobble.hobgoblin.compiler.types.TypeMatchers.isField; import static org.zwobble.precisely.AssertThat.assertThat; @@ -48,6 +50,7 @@ public class TypeCheckerStructDefinitionTests { UntypedTypeParamNode.arbitrary().withName("A").build(), UntypedTypeParamNode.arbitrary().withName("B").build() ))) + .withFields(List.of()) .build(); var context = TypeCheckerContextArb.namespaceContext(NamespaceName.of("a", "b")); @@ -130,15 +133,11 @@ public class TypeCheckerStructDefinitionTests { @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); @@ -159,6 +158,23 @@ public class TypeCheckerStructDefinitionTests { ); } + @Test + public void singletonStructCannotHaveTypeParams() { + var untyped = UntypedStructDefinitionNode.arbitrary() + .withName("X") + .withTypeParams(List.of(UntypedTypeParamNode.arbitrary().build())) + .withFields(Optional.empty()) + .build(); + var globalContext = TypeCheckerGlobalContext.initial(); + var namespaceName = NamespaceName.of("a", "b"); + var namespaceContext = globalContext.enterNamespace(namespaceName); + + assertThrows( + SingletonStructCannotHaveTypeParamsError.class, + () -> typeCheckNamespaceStatement(untyped, namespaceContext) + ); + } + @Test public void whenStructDefinitionHasTypeParamsThenTypeParamsAreBoundWithinStructDefinition() { var untyped = UntypedStructDefinitionNode.arbitrary() -- cgit v1.2.3