diff options
| author | Michael Williamson <mike@zwobble.org> | 2023-11-05 20:54:40 +0000 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2023-11-05 20:54:40 +0000 |
| commit | f5dc163e766e389ddec4f5bbe4620c583391cfec (patch) | |
| tree | bb9a38f08b8d6ebcb716abdc33d0d7e725f4a989 /src | |
| parent | f98ea69c9d401217af2a6a5d7ab42dae0ae65b6d (diff) | |
Type-check effect handler fields
Diffstat (limited to 'src')
5 files changed, 256 insertions, 76 deletions
diff --git a/src/main/java/org/shedlang/compiler/typechecker/PartialFunctionSignature.java b/src/main/java/org/shedlang/compiler/typechecker/PartialFunctionSignature.java index e86a2840..68541f64 100644 --- a/src/main/java/org/shedlang/compiler/typechecker/PartialFunctionSignature.java +++ b/src/main/java/org/shedlang/compiler/typechecker/PartialFunctionSignature.java @@ -6,34 +6,43 @@ import org.shedlang.compiler.sources.Sources; import org.shedlang.compiler.types.Effect; import org.shedlang.compiler.types.Effects; import org.shedlang.compiler.types.Type; +import org.shedlang.compiler.util.Lists; import org.shedlang.compiler.util.P; import java.util.List; import java.util.Optional; public record PartialFunctionSignature( - List<Param> params, + List<Param> positionalParams, + List<Param> namedParams, Effect effect, Optional<Type> returnType ) { public record Param(String name, Type type, Source source) {} + public List<Param> params() { + return Lists.concat(positionalParams, namedParams); + } + public static Builder builder() { return new Builder( P.vector(), + P.vector(), Effects.NONE, Optional.empty() ); } public record Builder( - PVector<Param> params, + PVector<Param> positionalParams, + PVector<Param> namedParams, Effect effect, Optional<Type> returnType ) { public PartialFunctionSignature build() { return new PartialFunctionSignature( - params, + positionalParams, + namedParams, effect, returnType ); @@ -41,7 +50,8 @@ public record PartialFunctionSignature( public Builder addPositionalParam(String name, Type type) { return new Builder( - this.params.plus(new Param(name, type, Sources.NULL)), + this.positionalParams.plus(new Param(name, type, Sources.NULL)), + this.namedParams, this.effect, this.returnType ); @@ -49,7 +59,8 @@ public record PartialFunctionSignature( public Builder addNamedParam(String name, Type type) { return new Builder( - this.params.plus(new Param(name, type, Sources.NULL)), + this.positionalParams, + this.namedParams.plus(new Param(name, type, Sources.NULL)), this.effect, this.returnType ); @@ -57,7 +68,8 @@ public record PartialFunctionSignature( public Builder withEffect(Effect effect) { return new Builder( - this.params, + this.positionalParams, + this.namedParams, effect, this.returnType ); @@ -65,7 +77,8 @@ public record PartialFunctionSignature( public Builder withReturnType(Type returnType) { return new Builder( - this.params, + this.positionalParams, + this.namedParams, this.effect, Optional.of(returnType) ); @@ -73,7 +86,8 @@ public record PartialFunctionSignature( public Builder withoutReturnType() { return new Builder( - this.params, + this.positionalParams, + this.namedParams, this.effect, Optional.empty() ); diff --git a/src/main/java/org/shedlang/compiler/typechecker/TypeChecker.java b/src/main/java/org/shedlang/compiler/typechecker/TypeChecker.java index a1139a98..03c60c93 100644 --- a/src/main/java/org/shedlang/compiler/typechecker/TypeChecker.java +++ b/src/main/java/org/shedlang/compiler/typechecker/TypeChecker.java @@ -701,15 +701,14 @@ public class TypeChecker { .map(typedParamNodeToParam) .toList(); - var partialSignatureParams = Lists.concat(positionalParams, namedParams); - var effect = functionSignatureToEffect(typedSignature); var returnType = functionSignatureToReturnType(typedSignature) .orElseThrow(() -> new FunctionSignatureMissingReturnTypeError(untyped.signature().source())); var partialSignature = new PartialFunctionSignature( - partialSignatureParams, + positionalParams, + namedParams, effect, Optional.of(returnType) ); @@ -766,55 +765,7 @@ public class TypeChecker { ) { var typedSignature = typeCheckFunctionSignature(untyped.signature(), context); - var functionTypeHint = Optionals.filterToType(FunctionType.class, hint); - - var positionalParams = new ArrayList<PartialFunctionSignature.Param>(); - - for (var i = 0; i < typedSignature.params().positional().size(); i++) { - var paramIndex = i; - var paramNode = typedSignature.params().positional().get(paramIndex); - var paramHint = functionTypeHint.flatMap(h -> Lists.tryGet(h.positionalParams(), paramIndex)); - - var param = new PartialFunctionSignature.Param( - paramNode.name(), - paramNode.type() - .map(x -> x.value()) - .or(() -> paramHint) - .orElseThrow(() -> new FunctionSignatureMissingParamTypeError(paramNode.source())), - paramNode.source() - ); - positionalParams.add(param); - } - - var namedParams = new ArrayList<PartialFunctionSignature.Param>(); - - for (var namedParam : typedSignature.params().named()) { - var paramHint = functionTypeHint.flatMap( - h -> h.namedParams().stream() - .filter(p -> p.name().equals(namedParam.name())) - .map(p -> p.type()) - .findFirst() - ); - - namedParams.add(new PartialFunctionSignature.Param( - namedParam.name(), - namedParam.type() - .map(x -> x.value()) - .or(() -> paramHint) - .orElseThrow(() -> new FunctionSignatureMissingParamTypeError(namedParam.source())), - namedParam.source() - )); - } - - var partialSignatureParams = Lists.concat(positionalParams, namedParams); - - var effect = functionSignatureToEffect(typedSignature); - - var partialSignature = new PartialFunctionSignature( - partialSignatureParams, - effect, - typedSignature.returnType().map(node -> node.value()) - ); + var partialSignature = inferFunctionPartialSignature(typedSignature, hint); var typedBodyResult = typeCheckFunctionBody( untyped.body(), @@ -825,13 +776,13 @@ public class TypeChecker { context = typedBodyResult.context(); var type = new FunctionType( - positionalParams.stream() + partialSignature.positionalParams().stream() .map(x -> x.type()) .toList(), - namedParams.stream() + partialSignature.namedParams().stream() .map(x -> new NamedParam(x.name(), x.type())) .toList(), - effect, + partialSignature.effect(), typedBody.type() ); @@ -923,6 +874,60 @@ public class TypeChecker { ); } + private static PartialFunctionSignature inferFunctionPartialSignature( + TypedFunctionSignatureNode typedSignature, + Optional<Type> hint + ) { + var functionTypeHint = Optionals.filterToType(FunctionType.class, hint); + + var positionalParams = new ArrayList<PartialFunctionSignature.Param>(); + + for (var i = 0; i < typedSignature.params().positional().size(); i++) { + var paramIndex = i; + var paramNode = typedSignature.params().positional().get(paramIndex); + var paramHint = functionTypeHint.flatMap(h -> Lists.tryGet(h.positionalParams(), paramIndex)); + + var param = new PartialFunctionSignature.Param( + paramNode.name(), + paramNode.type() + .map(x -> x.value()) + .or(() -> paramHint) + .orElseThrow(() -> new FunctionSignatureMissingParamTypeError(paramNode.source())), + paramNode.source() + ); + positionalParams.add(param); + } + + var namedParams = new ArrayList<PartialFunctionSignature.Param>(); + + for (var namedParam : typedSignature.params().named()) { + var paramHint = functionTypeHint.flatMap( + h -> h.namedParams().stream() + .filter(p -> p.name().equals(namedParam.name())) + .map(p -> p.type()) + .findFirst() + ); + + namedParams.add(new PartialFunctionSignature.Param( + namedParam.name(), + namedParam.type() + .map(x -> x.value()) + .or(() -> paramHint) + .orElseThrow(() -> new FunctionSignatureMissingParamTypeError(namedParam.source())), + namedParam.source() + )); + } + + var effect = functionSignatureToEffect(typedSignature); + + return new PartialFunctionSignature( + positionalParams, + namedParams, + effect, + typedSignature.returnType().map(node -> node.value()) + ); + } + private static Effect functionSignatureToEffect(TypedFunctionSignatureNode signature) { return signature.effect().map(e -> e.value()).orElse(Effects.NONE); } @@ -1617,27 +1622,89 @@ public class TypeChecker { context = context.exit(); var effectInfo = context.userDefinedEffectInfo(handledEffect, typedHandledEffect.source()); - // TODO: Don't create unnecessary list - for (var effectField : effectInfo.fields().stream().toList()) { - throw new MissingEffectHandlerFieldError(effectField.name(), untyped.source()); - } + var handledFieldNames = new HashSet<String>(); + + var typedHandlerFields = new ArrayList<TypedFunctionDefinitionNode>(); + for (var untypedHandlerField : untyped.handlers()) { + handledFieldNames.add(untypedHandlerField.name()); + + var effectFieldType = effectInfo.fields() + .fieldType(untypedHandlerField.name()) + .orElseThrow(() -> new NoSuchFieldError( + Types.effectType(handledEffect), + untypedHandlerField.name(), + untypedHandlerField.source() + )); + + var typedHandlerFieldResult = typeCheckTryHandlerField(untypedHandlerField, effectFieldType, context); + var typedHandlerField = typedHandlerFieldResult.value(); + context = typedHandlerFieldResult.context(); - for (var handlerField : untyped.handlers()) { - throw new NoSuchFieldError( - Types.effectType(handledEffect), - handlerField.name(), - handlerField.source() + checkIsSubType( + typedHandlerField.type(), + effectFieldType, + untypedHandlerField.source(), + context ); + + typedHandlerFields.add(typedHandlerField); + } + + // TODO: Don't create unnecessary list + for (var effectField : effectInfo.fields().stream().toList()) { + if (!handledFieldNames.contains(effectField.name())) { + throw new MissingEffectHandlerFieldError(effectField.name(), untyped.source()); + } } return context.toResult(new TypedTryNode( typedBody, typedHandledEffect, - P.vector(), + typedHandlerFields, untyped.source() )); } + private static TypeCheckerResult<TypedFunctionDefinitionNode> typeCheckTryHandlerField( + UntypedFunctionDefinitionNode untyped, + FunctionType effectFieldType, + TypeCheckerContext context + ) { + var typedSignature = typeCheckFunctionSignature(untyped.signature(), context); + + var partialSignature = inferFunctionPartialSignature(typedSignature, Optional.of(effectFieldType)); + + // TODO: don't allow returns, allow resumes + var typedBodyResult = typeCheckFunctionBody( + untyped.body(), + partialSignature, + context + ); + var typedBody = typedBodyResult.value(); + context = typedBodyResult.context(); + + var type = new FunctionType( + partialSignature.positionalParams().stream() + .map(x -> x.type()) + .toList(), + partialSignature.namedParams().stream() + .map(x -> new NamedParam(x.name(), x.type())) + .toList(), + partialSignature.effect(), + typedBody.type() + ); + + var typed = new TypedFunctionDefinitionNode( + untyped.name(), + typedSignature, + typedBody, + type, + untyped.source() + ); + + return context.toResult(typed); + } + private static TypeCheckerResult<TypedExpressionNode> typeCheckUnitLiteral( UntypedUnitLiteralNode untyped, TypeCheckerContext context diff --git a/src/test/java/org/shedlang/compiler/ast/typed/TypedTryNodeMatcher.java b/src/test/java/org/shedlang/compiler/ast/typed/TypedTryNodeMatcher.java index b6a124b9..19ac0d2a 100644 --- a/src/test/java/org/shedlang/compiler/ast/typed/TypedTryNodeMatcher.java +++ b/src/test/java/org/shedlang/compiler/ast/typed/TypedTryNodeMatcher.java @@ -37,4 +37,10 @@ public class TypedTryNodeMatcher extends MatcherBuilder< ) { return with(has("body", x -> x.body(), body)); } + + public TypedTryNodeMatcher withHandlers( + Matcher<? super Iterable<TypedFunctionDefinitionNode>> handlers + ) { + return with(has("handlers", x -> x.handlers(), handlers)); + } } diff --git a/src/test/java/org/shedlang/compiler/ast/untyped/Untyped.java b/src/test/java/org/shedlang/compiler/ast/untyped/Untyped.java index 07f3021d..adae966a 100644 --- a/src/test/java/org/shedlang/compiler/ast/untyped/Untyped.java +++ b/src/test/java/org/shedlang/compiler/ast/untyped/Untyped.java @@ -153,6 +153,10 @@ public class Untyped { return new UntypedReferenceNode(name, Sources.NULL); } + public static UntypedResumeNode resume() { + return new UntypedResumeNode(Optional.empty(), Sources.NULL); + } + public static UntypedStringLiteralNode stringLiteral(String value) { return new UntypedStringLiteralNode(value, Sources.NULL); } diff --git a/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerTryTests.java b/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerTryTests.java index c7a98211..f509331c 100644 --- a/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerTryTests.java +++ b/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerTryTests.java @@ -5,10 +5,9 @@ import org.shedlang.compiler.ast.untyped.Untyped; import org.shedlang.compiler.ast.untyped.UntypedCallNode; import org.shedlang.compiler.ast.untyped.UntypedFunctionDefinitionNode; import org.shedlang.compiler.ast.untyped.UntypedTryNode; -import org.shedlang.compiler.typechecker.errors.MissingEffectHandlerFieldError; +import org.shedlang.compiler.typechecker.errors.*; import org.shedlang.compiler.typechecker.errors.NoSuchFieldError; -import org.shedlang.compiler.typechecker.errors.NotUserDefinedEffectError; -import org.shedlang.compiler.typechecker.errors.UnexpectedEffectError; +import org.shedlang.compiler.typechecker.typesets.TypeSets; import org.shedlang.compiler.types.*; import org.shedlang.compiler.util.P; @@ -224,4 +223,94 @@ public class TypeCheckerTryTests { assertThat(error.fieldName(), equalTo("mark")); } + + @Test + public void whenHandlerFieldTypeIsNotSubtypeOfEffectFieldTypeThenErrorIsThrown() { + var handledEffect = Effects.userDefined(NAMESPACE_NAME, "HandledEffect"); + var untyped = UntypedTryNode.builder() + .withHandledEffect(Untyped.typeLevelReference(handledEffect.name())) + .addHandlerField( + UntypedFunctionDefinitionNode.builder() + .addPositionalParam("x", Untyped.typeLevelReference("String")) + // TODO: support type-checking resume + //.addStatement(Untyped.resume()) + .withName("mark") + .build() + ) + .build(); + var context = TypeCheckerContext.EMPTY + .addUserDefinedEffectInfo( + handledEffect, + UserDefinedEffectInfo.builder() + .addField("mark", FunctionType.builder().withReturnType(Types.UNIT).build()) + .build() + ) + .enterFunction( + P.map( + Map.entry("String", Variable.defined(Types.metaType(Types.STRING), VariableType.CONSTANT)), + Map.entry(handledEffect.name(), Variable.defined(Types.effectType(handledEffect), VariableType.CONSTANT)) + ), + Effects.NONE, + Optional.empty() + ); + + var error = assertThrows( + UnexpectedTypeError.class, + () -> typeCheckExpression(untyped, context) + ); + + assertThat(error.expected(), equalTo(TypeSets.type( + FunctionType.builder() + .withReturnType(Types.UNIT) + .build() + ))); + assertThat(error.actual(), equalTo( + FunctionType.builder() + .addPositionalParam(Types.STRING) + .withReturnType(Types.UNIT) + .build() + )); + } + + @Test + public void whenHandlerFieldTypeIsSubtypeOfEffectFieldTypeThenHandlerFieldTypeChecks() { + var handledEffect = Effects.userDefined(NAMESPACE_NAME, "HandledEffect"); + var untyped = UntypedTryNode.builder() + .withHandledEffect(Untyped.typeLevelReference(handledEffect.name())) + .addHandlerField( + UntypedFunctionDefinitionNode.builder() + // TODO: support type-checking resume + //.addStatement(Untyped.resume()) + .withName("mark") + .build() + ) + .build(); + var context = TypeCheckerContext.EMPTY + .addUserDefinedEffectInfo( + handledEffect, + UserDefinedEffectInfo.builder() + .addField("mark", FunctionType.builder().withReturnType(Types.UNIT).build()) + .build() + ) + .enterFunction( + P.map( + Map.entry("String", Variable.defined(Types.metaType(Types.STRING), VariableType.CONSTANT)), + Map.entry(handledEffect.name(), Variable.defined(Types.effectType(handledEffect), VariableType.CONSTANT)) + ), + Effects.NONE, + Optional.empty() + ); + + var result = typeCheckExpression(untyped, context); + + assertThat( + result.value(), + isTypedTryNode() + .withHandlers(isSequence( + isTypedFunctionDefinitionNode() + .withName("mark") + .withBodyBlock(isTypedBlockNode(isSequence())) + )) + ); + } } |
