From 776fcb476f229030cb5d34f4b948603f05c01a79 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Fri, 24 Apr 2026 12:43:29 +0100 Subject: Type check type level reference --- .../hobgoblin/compiler/ast/typed/TypedNode.java | 7 ++ .../ast/typed/TypedTypeLevelExpressionNode.java | 7 ++ .../ast/typed/TypedTypeLevelReferenceNode.java | 11 ++++ .../compiler/typechecker/TypeChecker.java | 51 +++++++++++++++ .../compiler/typechecker/TypeCheckerContext.java | 25 ++++++++ .../typechecker/UndeclaredVariableError.java | 17 +++++ .../typechecker/UndefinedVariableError.java | 17 +++++ .../compiler/typechecker/UnexpectedTypeError.java | 25 ++++++++ .../hobgoblin/compiler/typechecker/Variable.java | 11 ++++ .../hobgoblin/compiler/types/ScalarType.java | 8 +++ .../org/zwobble/hobgoblin/compiler/types/Type.java | 4 ++ .../hobgoblin/compiler/types/TypeLevelValue.java | 5 ++ .../compiler/types/TypeLevelValueType.java | 8 +++ .../zwobble/hobgoblin/compiler/types/TypeSet.java | 19 ++++++ .../hobgoblin/compiler/ast/untyped/UntypedArb.java | 12 ++++ .../typechecker/TypeCheckerContextArb.java | 10 +++ .../TypeCheckerTypeLevelReferenceTests.java | 75 ++++++++++++++++++++++ 17 files changed, 312 insertions(+) create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedNode.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTypeLevelExpressionNode.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTypeLevelReferenceNode.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerContext.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UndeclaredVariableError.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UndefinedVariableError.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UnexpectedTypeError.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/typechecker/Variable.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/types/ScalarType.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/types/Type.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/types/TypeLevelValue.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/types/TypeLevelValueType.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/types/TypeSet.java create mode 100644 src/test/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedArb.java create mode 100644 src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerContextArb.java create mode 100644 src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTypeLevelReferenceTests.java (limited to 'src') diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedNode.java b/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedNode.java new file mode 100644 index 0000000..6baa8d6 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedNode.java @@ -0,0 +1,7 @@ +package org.zwobble.hobgoblin.compiler.ast.typed; + +import org.zwobble.hobgoblin.compiler.sources.Source; + +public interface TypedNode { + Source source(); +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTypeLevelExpressionNode.java b/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTypeLevelExpressionNode.java new file mode 100644 index 0000000..c588f95 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTypeLevelExpressionNode.java @@ -0,0 +1,7 @@ +package org.zwobble.hobgoblin.compiler.ast.typed; + +import org.zwobble.hobgoblin.compiler.types.TypeLevelValue; + +public sealed interface TypedTypeLevelExpressionNode extends TypedNode permits TypedTypeLevelReferenceNode { + T value(); +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTypeLevelReferenceNode.java b/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTypeLevelReferenceNode.java new file mode 100644 index 0000000..bc2c50d --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTypeLevelReferenceNode.java @@ -0,0 +1,11 @@ +package org.zwobble.hobgoblin.compiler.ast.typed; + +import org.zwobble.hobgoblin.compiler.sources.Source; +import org.zwobble.hobgoblin.compiler.types.TypeLevelValue; + +public record TypedTypeLevelReferenceNode( + String name, + T value, + Source source +) implements TypedTypeLevelExpressionNode { +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java new file mode 100644 index 0000000..56d4645 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java @@ -0,0 +1,51 @@ +package org.zwobble.hobgoblin.compiler.typechecker; + +import org.zwobble.hobgoblin.compiler.ast.typed.TypedTypeLevelExpressionNode; +import org.zwobble.hobgoblin.compiler.ast.typed.TypedTypeLevelReferenceNode; +import org.zwobble.hobgoblin.compiler.ast.untyped.UntypedTypeLevelExpressionNode; +import org.zwobble.hobgoblin.compiler.ast.untyped.UntypedTypeLevelReferenceNode; +import org.zwobble.hobgoblin.compiler.types.TypeLevelValue; +import org.zwobble.hobgoblin.compiler.types.TypeLevelValueType; +import org.zwobble.hobgoblin.compiler.types.TypeSet; + +public class TypeChecker { + private TypeChecker() { + } + + public static TypedTypeLevelExpressionNode typeCheckTypeLevelExpression( + UntypedTypeLevelExpressionNode untyped, + TypeCheckerContext context + ) { + return switch (untyped) { + case UntypedTypeLevelReferenceNode untypedTypeLevelReference -> + typeCheckTypeLevelReference(untypedTypeLevelReference, context); + }; + } + + private static TypedTypeLevelExpressionNode typeCheckTypeLevelReference( + UntypedTypeLevelReferenceNode untyped, + TypeCheckerContext context + ) { + var variable = context.lookup(untyped.name()) + .orElseThrow(() -> new UndeclaredVariableError(untyped.name(), untyped.source())); + + var type = switch (variable) { + case Variable.Declared declared -> + throw new UndefinedVariableError(untyped.name(), untyped.source()); + + case Variable.Defined defined -> + defined.type(); + }; + + // TODO: handle not a type-level value + if (!(type instanceof TypeLevelValueType(TypeLevelValue value))) { + throw new UnexpectedTypeError(new TypeSet.MetaType(), type, untyped.source()); + } + + return new TypedTypeLevelReferenceNode<>( + untyped.name(), + value, + untyped.source() + ); + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerContext.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerContext.java new file mode 100644 index 0000000..e861c30 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerContext.java @@ -0,0 +1,25 @@ +package org.zwobble.hobgoblin.compiler.typechecker; + +import org.zwobble.hobgoblin.compiler.types.Type; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class TypeCheckerContext { + private final Map variables = new HashMap<>(); + + public void declare(String name) { + // TODO: Check variable is not already declared. + this.variables.put(name, new Variable.Declared()); + } + + public void define(String name, Type type) { + // TODO: Check variable is not already defined. + this.variables.put(name, new Variable.Defined(type)); + } + + public Optional lookup(String name) { + return Optional.ofNullable(this.variables.get(name)); + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UndeclaredVariableError.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UndeclaredVariableError.java new file mode 100644 index 0000000..c6130fa --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UndeclaredVariableError.java @@ -0,0 +1,17 @@ +package org.zwobble.hobgoblin.compiler.typechecker; + +import org.zwobble.hobgoblin.compiler.errors.SourceError; +import org.zwobble.hobgoblin.compiler.sources.Source; + +public class UndeclaredVariableError extends SourceError { + private final String variableName; + + public UndeclaredVariableError(String variableName, Source source) { + super("Undeclared variable: " + variableName, source); + this.variableName = variableName; + } + + public String variableName() { + return variableName; + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UndefinedVariableError.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UndefinedVariableError.java new file mode 100644 index 0000000..95be5a6 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UndefinedVariableError.java @@ -0,0 +1,17 @@ +package org.zwobble.hobgoblin.compiler.typechecker; + +import org.zwobble.hobgoblin.compiler.errors.SourceError; +import org.zwobble.hobgoblin.compiler.sources.Source; + +public class UndefinedVariableError extends SourceError { + private final String variableName; + + public UndefinedVariableError(String variableName, Source source) { + super("Variable has been declared but not defined: " + variableName, source); + this.variableName = variableName; + } + + public String variableName() { + return variableName; + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UnexpectedTypeError.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UnexpectedTypeError.java new file mode 100644 index 0000000..c2ca4b6 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/UnexpectedTypeError.java @@ -0,0 +1,25 @@ +package org.zwobble.hobgoblin.compiler.typechecker; + +import org.zwobble.hobgoblin.compiler.errors.SourceError; +import org.zwobble.hobgoblin.compiler.sources.Source; +import org.zwobble.hobgoblin.compiler.types.Type; +import org.zwobble.hobgoblin.compiler.types.TypeSet; + +public class UnexpectedTypeError extends SourceError { + private final TypeSet expected; + private final Type actual; + + public UnexpectedTypeError(TypeSet expected, Type actual, Source source) { + super("Expected " + expected.describe() + " but was " + actual.describe(), source); + this.expected = expected; + this.actual = actual; + } + + public TypeSet expected() { + return expected; + } + + public Type actual() { + return actual; + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/Variable.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/Variable.java new file mode 100644 index 0000000..1d3a4b6 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/Variable.java @@ -0,0 +1,11 @@ +package org.zwobble.hobgoblin.compiler.typechecker; + +import org.zwobble.hobgoblin.compiler.types.Type; + +public sealed interface Variable { + record Declared() implements Variable { + } + + record Defined(Type type) implements Variable { + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/types/ScalarType.java b/src/main/java/org/zwobble/hobgoblin/compiler/types/ScalarType.java new file mode 100644 index 0000000..22032a1 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/types/ScalarType.java @@ -0,0 +1,8 @@ +package org.zwobble.hobgoblin.compiler.types; + +public record ScalarType(String name) implements Type { + @Override + public String describe() { + return name; + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/types/Type.java b/src/main/java/org/zwobble/hobgoblin/compiler/types/Type.java new file mode 100644 index 0000000..2692261 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/types/Type.java @@ -0,0 +1,4 @@ +package org.zwobble.hobgoblin.compiler.types; + +public sealed interface Type extends TypeLevelValue permits ScalarType, TypeLevelValueType { +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeLevelValue.java b/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeLevelValue.java new file mode 100644 index 0000000..9740cf9 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeLevelValue.java @@ -0,0 +1,5 @@ +package org.zwobble.hobgoblin.compiler.types; + +public sealed interface TypeLevelValue permits Type { + String describe(); +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeLevelValueType.java b/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeLevelValueType.java new file mode 100644 index 0000000..b885c25 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeLevelValueType.java @@ -0,0 +1,8 @@ +package org.zwobble.hobgoblin.compiler.types; + +public record TypeLevelValueType(TypeLevelValue value) implements Type { + @Override + public String describe() { + return "TypeLevelValue[" + value.describe() + "]"; + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeSet.java b/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeSet.java new file mode 100644 index 0000000..f587897 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/types/TypeSet.java @@ -0,0 +1,19 @@ +package org.zwobble.hobgoblin.compiler.types; + +public sealed interface TypeSet { + String describe(); + + record SingleType(Type type) implements TypeSet { + @Override + public String describe() { + return type.describe(); + } + } + + record MetaType() implements TypeSet { + @Override + public String describe() { + return "metatype"; + } + } +} diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedArb.java b/src/test/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedArb.java new file mode 100644 index 0000000..5a011d7 --- /dev/null +++ b/src/test/java/org/zwobble/hobgoblin/compiler/ast/untyped/UntypedArb.java @@ -0,0 +1,12 @@ +package org.zwobble.hobgoblin.compiler.ast.untyped; + +import org.zwobble.hobgoblin.compiler.sources.NullSource; + +public class UntypedArb { + private UntypedArb() { + } + + public static UntypedTypeLevelReferenceNode typeLevelReference(String name) { + return new UntypedTypeLevelReferenceNode(name, NullSource.INSTANCE); + } +} diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerContextArb.java b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerContextArb.java new file mode 100644 index 0000000..98d8064 --- /dev/null +++ b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerContextArb.java @@ -0,0 +1,10 @@ +package org.zwobble.hobgoblin.compiler.typechecker; + +public class TypeCheckerContextArb { + private TypeCheckerContextArb() { + } + + public static TypeCheckerContext context() { + return new TypeCheckerContext(); + } +} diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTypeLevelReferenceTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTypeLevelReferenceTests.java new file mode 100644 index 0000000..82704fc --- /dev/null +++ b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTypeLevelReferenceTests.java @@ -0,0 +1,75 @@ +package org.zwobble.hobgoblin.compiler.typechecker; + +import org.junit.jupiter.api.Test; +import org.zwobble.hobgoblin.compiler.ast.typed.TypedTypeLevelReferenceNode; +import org.zwobble.hobgoblin.compiler.ast.untyped.UntypedArb; +import org.zwobble.hobgoblin.compiler.types.ScalarType; +import org.zwobble.hobgoblin.compiler.types.TypeLevelValueType; +import org.zwobble.hobgoblin.compiler.types.TypeSet; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.zwobble.precisely.AssertThat.assertThat; +import static org.zwobble.precisely.Matchers.*; + +public class TypeCheckerTypeLevelReferenceTests { + @Test + public void whenNameIsUnrecognisedThenErrorIsThrown() { + var untyped = UntypedArb.typeLevelReference("X"); + var context = TypeCheckerContextArb.context(); + + var error = assertThrows( + UndeclaredVariableError.class, + () -> TypeChecker.typeCheckTypeLevelExpression(untyped, context) + ); + + assertThat(error.variableName(), equalTo("X")); + } + + @Test + public void whenNameIsUndefinedThenErrorIsThrown() { + var untyped = UntypedArb.typeLevelReference("X"); + var context = TypeCheckerContextArb.context(); + context.declare("X"); + + var error = assertThrows( + UndefinedVariableError.class, + () -> TypeChecker.typeCheckTypeLevelExpression(untyped, context) + ); + + assertThat(error.variableName(), equalTo("X")); + } + + @Test + public void whenVariableIsNotTypeLevelValueThenErrorIsThrown() { + var untyped = UntypedArb.typeLevelReference("X"); + var context = TypeCheckerContextArb.context(); + context.declare("X"); + var scalarType = new ScalarType("Int"); + context.define("X", scalarType); + + var error = assertThrows( + UnexpectedTypeError.class, + () -> TypeChecker.typeCheckTypeLevelExpression(untyped, context) + ); + + assertThat(error.expected(), equalTo(new TypeSet.MetaType())); + assertThat(error.actual(), equalTo(scalarType)); + } + + @Test + public void whenNameIsMetaTypeThenValueIsType() { + var untyped = UntypedArb.typeLevelReference("X"); + var context = TypeCheckerContextArb.context(); + context.declare("X"); + var scalarType = new ScalarType("Int"); + context.define("X", new TypeLevelValueType(scalarType)); + + var typed = TypeChecker.typeCheckTypeLevelExpression(untyped, context); + + assertThat(typed, instanceOf( + TypedTypeLevelReferenceNode.class, + has("name", TypedTypeLevelReferenceNode::name, equalTo("X")), + has("value", TypedTypeLevelReferenceNode::value, equalTo(scalarType)) + )); + } +} -- cgit v1.2.3