From ba87d647e68a52f92dc859008f0ed20fe72659a0 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sat, 28 Oct 2023 23:50:09 +0100 Subject: Type check function expressions --- .../ast/typed/TypedFunctionBodyNodeMatcher.java | 46 +++ .../typed/TypedFunctionExpressionNodeMatcher.java | 64 ++++ .../compiler/ast/typed/TypedNodeMatchers.java | 8 + .../typechecker/TypeCheckerFunctionBodyTests.java | 337 +++++++++++++++++++++ .../TypeCheckerFunctionDefinitionTests.java | 318 +------------------ .../TypeCheckerFunctionExpressionTests.java | 41 +++ 6 files changed, 498 insertions(+), 316 deletions(-) create mode 100644 src/test/java/org/shedlang/compiler/ast/typed/TypedFunctionBodyNodeMatcher.java create mode 100644 src/test/java/org/shedlang/compiler/ast/typed/TypedFunctionExpressionNodeMatcher.java create mode 100644 src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionBodyTests.java create mode 100644 src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionExpressionTests.java (limited to 'src/test/java') diff --git a/src/test/java/org/shedlang/compiler/ast/typed/TypedFunctionBodyNodeMatcher.java b/src/test/java/org/shedlang/compiler/ast/typed/TypedFunctionBodyNodeMatcher.java new file mode 100644 index 00000000..d392ec4d --- /dev/null +++ b/src/test/java/org/shedlang/compiler/ast/typed/TypedFunctionBodyNodeMatcher.java @@ -0,0 +1,46 @@ +package org.shedlang.compiler.ast.typed; + +import org.pcollections.Empty; +import org.pcollections.PVector; +import org.shedlang.compiler.testing.MatcherBuilder; +import org.zwobble.precisely.Matcher; + +import static org.zwobble.precisely.Matchers.has; + +public class TypedFunctionBodyNodeMatcher extends MatcherBuilder< + TypedFunctionBodyNode, + TypedFunctionBodyNode, + TypedFunctionBodyNodeMatcher +> { + public static TypedFunctionBodyNodeMatcher empty() { + return new TypedFunctionBodyNodeMatcher(Empty.vector()); + } + + public TypedFunctionBodyNodeMatcher( + PVector> submatchers + ) { + super(submatchers); + } + + @Override + protected Class expectedType() { + return TypedFunctionBodyNode.class; + } + + @Override + protected TypedFunctionBodyNodeMatcher create( + PVector> submatchers + ) { + return new TypedFunctionBodyNodeMatcher(submatchers); + } + + public TypedFunctionBodyNodeMatcher withBlock(Matcher block) { + return with(has("body", x -> x.block(), block)); + } + + public TypedFunctionBodyNodeMatcher withFreeVariables( + Matcher> freeVariables + ) { + return with(has("freeVariables", x -> x.freeVariables(), freeVariables)); + } +} diff --git a/src/test/java/org/shedlang/compiler/ast/typed/TypedFunctionExpressionNodeMatcher.java b/src/test/java/org/shedlang/compiler/ast/typed/TypedFunctionExpressionNodeMatcher.java new file mode 100644 index 00000000..a2c047ec --- /dev/null +++ b/src/test/java/org/shedlang/compiler/ast/typed/TypedFunctionExpressionNodeMatcher.java @@ -0,0 +1,64 @@ +package org.shedlang.compiler.ast.typed; + +import org.pcollections.Empty; +import org.pcollections.PVector; +import org.shedlang.compiler.testing.MatcherBuilder; +import org.shedlang.compiler.types.FunctionType; +import org.zwobble.precisely.Matcher; + +import java.util.Optional; + +import static org.zwobble.precisely.Matchers.equalTo; +import static org.zwobble.precisely.Matchers.has; + +public class TypedFunctionExpressionNodeMatcher extends MatcherBuilder< + TypedExpressionNode, + TypedFunctionExpressionNode, + TypedFunctionExpressionNodeMatcher +> { + public static TypedFunctionExpressionNodeMatcher empty() { + return new TypedFunctionExpressionNodeMatcher(Empty.vector()); + } + + public TypedFunctionExpressionNodeMatcher( + PVector> submatchers + ) { + super(submatchers); + } + + @Override + protected Class expectedType() { + return TypedFunctionExpressionNode.class; + } + + @Override + protected TypedFunctionExpressionNodeMatcher create( + PVector> submatchers + ) { + return new TypedFunctionExpressionNodeMatcher(submatchers); + } + + public TypedFunctionExpressionNodeMatcher withBodyBlock(Matcher body) { + return with(has("body", x -> x.body().block(), body)); + } + + public TypedFunctionExpressionNodeMatcher withEffect(Matcher>> effect) { + return with(has("effect", x -> x.signature().effect(), effect)); + } + + public TypedFunctionExpressionNodeMatcher withFreeVariables(Matcher> freeVariables) { + return with(has("freeVariables", x -> x.body().freeVariables(), freeVariables)); + } + + public TypedFunctionExpressionNodeMatcher withParams(Matcher params) { + return with(has("params", x -> x.signature().params(), params)); + } + + public TypedFunctionExpressionNodeMatcher withReturnType(Matcher> returnType) { + return with(has("returnType", x -> x.signature().returnType(), returnType)); + } + + public TypedFunctionExpressionNodeMatcher withType(FunctionType type) { + return with(has("type", x -> x.signature().type(), equalTo(type))); + } +} diff --git a/src/test/java/org/shedlang/compiler/ast/typed/TypedNodeMatchers.java b/src/test/java/org/shedlang/compiler/ast/typed/TypedNodeMatchers.java index 2723883e..8a438233 100644 --- a/src/test/java/org/shedlang/compiler/ast/typed/TypedNodeMatchers.java +++ b/src/test/java/org/shedlang/compiler/ast/typed/TypedNodeMatchers.java @@ -112,10 +112,18 @@ public class TypedNodeMatchers { ); } + public static TypedFunctionBodyNodeMatcher isTypedFunctionBodyNode() { + return TypedFunctionBodyNodeMatcher.empty(); + } + public static TypedFunctionDefinitionNodeMatcher isTypedFunctionDefinitionNode() { return TypedFunctionDefinitionNodeMatcher.empty(); } + public static TypedFunctionExpressionNodeMatcher isTypedFunctionExpressionNode() { + return TypedFunctionExpressionNodeMatcher.empty(); + } + public static Matcher isTypedFunctionReturnNode( Matcher expression ) { diff --git a/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionBodyTests.java b/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionBodyTests.java new file mode 100644 index 00000000..be83f50d --- /dev/null +++ b/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionBodyTests.java @@ -0,0 +1,337 @@ +package org.shedlang.compiler.typechecker; + +import org.junit.jupiter.api.Test; +import org.shedlang.compiler.ast.typed.Typed; +import org.shedlang.compiler.ast.typed.TypedFunctionSignatureNode; +import org.shedlang.compiler.ast.untyped.Untyped; +import org.shedlang.compiler.ast.untyped.UntypedCallNode; +import org.shedlang.compiler.typechecker.errors.DuplicateVariableNameError; +import org.shedlang.compiler.typechecker.errors.UnexpectedTypeError; +import org.shedlang.compiler.typechecker.typesets.TypeSets; +import org.shedlang.compiler.types.Effects; +import org.shedlang.compiler.types.FunctionType; +import org.shedlang.compiler.types.Types; +import org.shedlang.compiler.util.P; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.shedlang.compiler.ast.typed.TypedNodeMatchers.*; +import static org.shedlang.compiler.typechecker.TypeChecker.typeCheckFunctionBody; +import static org.zwobble.precisely.AssertThat.assertThat; +import static org.zwobble.precisely.Matchers.*; + +public class TypeCheckerFunctionBodyTests { + @Test + public void canTypeCheckEmptyBody() { + var untyped = Untyped.block(List.of()); + var signature = TypedFunctionSignatureNode.builder() + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY; + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode().withBlock(isTypedBlockNode(isSequence())) + ); + } + + @Test + public void paramsMustHaveUniqueNames() { + var untyped = Untyped.block(List.of()); + var signature = TypedFunctionSignatureNode.builder() + .addPositionalParam("x", Typed.typeLevelReference("String", Types.STRING)) + .addPositionalParam("x", Typed.typeLevelReference("Int", Types.INT)) + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY; + + var error = assertThrows( + DuplicateVariableNameError.class, + () -> typeCheckFunctionBody(untyped, signature, context) + ); + + assertThat(error.variableName(), equalTo("x")); + } + + @Test + public void blockIsTypeChecked() { + var untyped = Untyped.block(List.of( + Untyped.expressionStatement(Untyped.stringLiteral("hello")) + )); + var signature = TypedFunctionSignatureNode.builder() + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY; + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode() + .withBlock(isTypedBlockNode(isSequence( + isTypedExpressionStatementNode(isTypedStringLiteralNode("hello")) + ))) + ); + } + + @Test + public void canReferencePositionalArgInBody() { + var untyped = Untyped.block(List.of( + Untyped.expressionStatement(Untyped.reference("x")) + )); + var signature = TypedFunctionSignatureNode.builder() + .addPositionalParam("x", Typed.typeLevelReference("String", Types.STRING)) + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY; + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode() + .withBlock(isTypedBlockNode(isSequence( + isTypedExpressionStatementNode(isTypedReferenceNode("x", Types.STRING)) + ))) + ); + } + + @Test + public void canReferenceNamedArgInBody() { + var untyped = Untyped.block(List.of( + Untyped.expressionStatement(Untyped.reference("x")) + )); + var signature = TypedFunctionSignatureNode.builder() + .addNamedParam("x", Typed.typeLevelReference("String", Types.STRING)) + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY; + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode() + .withBlock(isTypedBlockNode(isSequence( + isTypedExpressionStatementNode(isTypedReferenceNode("x", Types.STRING)) + ))) + ); + } + + @Test + public void bodyHasEffectAsDefinedInSignature() { + var untyped = Untyped.block(List.of( + Untyped.expressionStatement( + UntypedCallNode.builder() + .withReceiver(Untyped.reference("f")) + .withHasEffects(true) + .build() + ) + )); + var signature = TypedFunctionSignatureNode.builder() + .withEffect(Typed.typeLevelReference("Io", Effects.IO)) + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var functionType = FunctionType.builder() + .withEffect(Effects.IO) + .withReturnType(Types.UNIT) + .build(); + var context = TypeCheckerContext.EMPTY + .enterGlobal(P.map( + Map.entry("f", functionType) + )); + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode() + .withBlock(isTypedBlockNode(isSequence( + isTypedExpressionStatementNode(isTypedCallNode().withHasEffects(true)) + ))) + ); + } + + @Test + public void bodyHasReturnTypeAsDefinedInSignature() { + var untyped = Untyped.block(List.of( + Untyped.functionReturn(Untyped.intLiteral(42)) + )); + var signature = TypedFunctionSignatureNode.builder() + .withReturnType(Typed.typeLevelReference("String", Types.STRING)) + .build(); + var context = TypeCheckerContext.EMPTY; + + var error = assertThrows( + UnexpectedTypeError.class, + () -> typeCheckFunctionBody(untyped, signature, context) + ); + + assertThat(error.expected(), equalTo(TypeSets.type(Types.STRING))); + assertThat(error.actual(), equalTo(Types.INT)); + } + + @Test + public void givenReturnTypeIsUnitWhenBodyHasNoReturnThenFunctionTypeChecks() { + var untyped = Untyped.block(List.of()); + var signature = TypedFunctionSignatureNode.builder() + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY; + + typeCheckFunctionBody(untyped, signature, context); + } + + @Test + public void givenReturnTypeIsNotUnitWhenBodyHasNoReturnThenFunctionFailsTypeCheck() { + var untyped = Untyped.block(List.of()); + var signature = TypedFunctionSignatureNode.builder() + .withReturnType(Typed.typeLevelReference("String", Types.STRING)) + .build(); + var context = TypeCheckerContext.EMPTY; + + var error = assertThrows( + UnexpectedTypeError.class, + () -> typeCheckFunctionBody(untyped, signature, context) + ); + + assertThat(error.expected(), equalTo(TypeSets.type(Types.STRING))); + assertThat(error.actual(), equalTo(Types.UNIT)); + } + + @Test + public void givenReturnTypeIsNotUnitWhenBodyHasBlockReturnOfCorrectTypeThenFunctionTypeChecks() { + var untyped = Untyped.block(List.of( + Untyped.blockReturn(Untyped.stringLiteral("hello")) + )); + var signature = TypedFunctionSignatureNode.builder() + .withReturnType(Typed.typeLevelReference("String", Types.STRING)) + .build(); + var context = TypeCheckerContext.EMPTY; + + typeCheckFunctionBody(untyped, signature, context); + } + + @Test + public void freeVariablesIncludeVariablesReferencedInFunctionButDefinedOutsideOfFunction() { + var untyped = Untyped.block(List.of( + Untyped.expressionStatement(Untyped.reference("y")) + )); + var signature = TypedFunctionSignatureNode.builder() + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY + .enterGlobal(P.map( + Map.entry("x", Types.STRING), + Map.entry("y", Types.STRING) + )); + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode() + .withFreeVariables(containsExactly( + has("name", x -> x.name(), equalTo("y")) + )) + ); + } + + @Test + public void freeVariablesDoNotIncludeArgsReferencedInFunction() { + var untyped = Untyped.block(List.of( + Untyped.expressionStatement(Untyped.reference("x")) + )); + var signature = TypedFunctionSignatureNode.builder() + .addPositionalParam("x", Typed.typeLevelReference("String", Types.STRING)) + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY; + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode() + .withFreeVariables(containsExactly()) + ); + } + + @Test + public void freeVariablesIncludeVariablesReferencedInBlockInFunctionButDefinedOutsideOfFunction() { + var untyped = Untyped.block(List.of( + Untyped.expressionStatement(Untyped.if_( + List.of( + Untyped.conditionalBranch( + Untyped.boolLiteral(true), + Untyped.block(List.of()) + ) + ), + Untyped.block(List.of( + Untyped.expressionStatement(Untyped.reference("x")) + )) + )) + )); + var signature = TypedFunctionSignatureNode.builder() + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY + .enterGlobal(P.map( + Map.entry("x", Types.STRING) + )); + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode() + .withFreeVariables(containsExactly( + has("name", x -> x.name(), equalTo("x")) + )) + ); + } + + @Test + public void boundVariablesAreSet() { + var untyped = Untyped.block(List.of( + Untyped.val("local1", Untyped.intLiteral(1)), + Untyped.expressionStatement(Untyped.reference("y")), + Untyped.expressionStatement(Untyped.if_( + List.of( + Untyped.conditionalBranch( + Untyped.boolLiteral(true), + Untyped.block(List.of( + Untyped.val("local2", Untyped.intLiteral(2)) + )) + ) + ), + Untyped.block(List.of()) + )) + )); + var signature = TypedFunctionSignatureNode.builder() + .addPositionalParam("argPositional", Typed.typeLevelReference("Unit", Types.UNIT)) + .addNamedParam("argNamed", Typed.typeLevelReference("Unit", Types.UNIT)) + .withReturnType(Typed.typeLevelReference("Unit", Types.UNIT)) + .build(); + var context = TypeCheckerContext.EMPTY + .enterGlobal(P.map( + Map.entry("y", Types.STRING) + )); + + var typed = typeCheckFunctionBody(untyped, signature, context); + + assertThat( + typed.value(), + isTypedFunctionBodyNode() + .withBlock(isTypedBlockNodeWithBoundVariableNames(containsExactly( + equalTo("argPositional"), + equalTo("argNamed"), + equalTo("local1") + ))) + ); + } +} diff --git a/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionDefinitionTests.java b/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionDefinitionTests.java index eaa7f007..b44b0f96 100644 --- a/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionDefinitionTests.java +++ b/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionDefinitionTests.java @@ -2,11 +2,7 @@ package org.shedlang.compiler.typechecker; import org.junit.jupiter.api.Test; 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.typechecker.errors.DuplicateVariableNameError; -import org.shedlang.compiler.typechecker.errors.UnexpectedTypeError; -import org.shedlang.compiler.typechecker.typesets.TypeSets; import org.shedlang.compiler.types.Effects; import org.shedlang.compiler.types.FunctionType; import org.shedlang.compiler.types.Types; @@ -15,11 +11,11 @@ import org.shedlang.compiler.util.P; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.shedlang.compiler.ast.typed.TypedNodeMatchers.*; import static org.shedlang.compiler.typechecker.TypeCheckerTesting.typeCheckNamespaceStatement; import static org.zwobble.precisely.AssertThat.assertThat; -import static org.zwobble.precisely.Matchers.*; +import static org.zwobble.precisely.Matchers.isOptionalEmpty; +import static org.zwobble.precisely.Matchers.isSequence; public class TypeCheckerFunctionDefinitionTests { @Test @@ -44,314 +40,4 @@ public class TypeCheckerFunctionDefinitionTests { .withType(new FunctionType(List.of(), List.of(), Effects.NONE, Types.UNIT)) ); } - - @Test - public void paramsMustHaveUniqueNames() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withName("f") - .addPositionalParam("x", Untyped.typeLevelReference("String")) - .addPositionalParam("x", Untyped.typeLevelReference("Int")) - .withReturnType(Untyped.typeLevelReference("Unit")) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("Int", Types.metaType(Types.INT)), - Map.entry("String", Types.metaType(Types.STRING)), - Map.entry("Unit", Types.metaType(Types.UNIT)) - )); - - var error = assertThrows( - DuplicateVariableNameError.class, - () -> typeCheckNamespaceStatement(untyped, context) - ); - - assertThat(error.variableName(), equalTo("x")); - } - - @Test - public void bodyIsTypeChecked() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withReturnType(Untyped.typeLevelReference("Unit")) - .addStatement(Untyped.expressionStatement(Untyped.stringLiteral("hello"))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map(Map.entry("Unit", Types.metaType(Types.UNIT)))); - - var typed = typeCheckNamespaceStatement(untyped, context); - - assertThat( - typed.value(), - isTypedFunctionDefinitionNode() - .withBodyBlock(isTypedBlockNode(isSequence( - isTypedExpressionStatementNode(isTypedStringLiteralNode("hello")) - ))) - ); - } - - @Test - public void canReferencePositionalArgInBody() { - var untyped = UntypedFunctionDefinitionNode.builder() - .addPositionalParam("x", Untyped.typeLevelReference("String")) - .withReturnType(Untyped.typeLevelReference("Unit")) - .addStatement(Untyped.expressionStatement(Untyped.reference("x"))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("String", Types.metaType(Types.STRING)), - Map.entry("Unit", Types.metaType(Types.UNIT)) - )); - - var typed = typeCheckNamespaceStatement(untyped, context); - - assertThat( - typed.value(), - isTypedFunctionDefinitionNode() - .withBodyBlock(isTypedBlockNode(isSequence( - isTypedExpressionStatementNode(isTypedReferenceNode("x", Types.STRING)) - ))) - ); - } - - @Test - public void canReferenceNamedArgInBody() { - var untyped = UntypedFunctionDefinitionNode.builder() - .addNamedParam("x", Untyped.typeLevelReference("String")) - .withReturnType(Untyped.typeLevelReference("Unit")) - .addStatement(Untyped.expressionStatement(Untyped.reference("x"))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("String", Types.metaType(Types.STRING)), - Map.entry("Unit", Types.metaType(Types.UNIT)) - )); - - var typed = typeCheckNamespaceStatement(untyped, context); - - assertThat( - typed.value(), - isTypedFunctionDefinitionNode() - .withBodyBlock(isTypedBlockNode(isSequence( - isTypedExpressionStatementNode(isTypedReferenceNode("x", Types.STRING)) - ))) - ); - } - - @Test - public void bodyHasEffectAsDefinedInSignature() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withName("g") - .withEffect(Untyped.typeLevelReference("Io")) - .withReturnType(Untyped.typeLevelReference("Unit")) - .addStatement(Untyped.expressionStatement( - UntypedCallNode.builder() - .withReceiver(Untyped.reference("f")) - .withHasEffects(true) - .build() - )) - .build(); - var functionType = FunctionType.builder() - .withEffect(Effects.IO) - .withReturnType(Types.UNIT) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("f", functionType), - Map.entry("Io", Types.effectType(Effects.IO)), - Map.entry("Unit", Types.metaType(Types.UNIT)) - )); - - var typed = typeCheckNamespaceStatement(untyped, context); - - assertThat( - typed.value(), - isTypedFunctionDefinitionNode() - .withBodyBlock(isTypedBlockNode(isSequence( - isTypedExpressionStatementNode(isTypedCallNode().withHasEffects(true)) - ))) - ); - } - - @Test - public void bodyHasReturnTypeAsDefinedInSignature() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withReturnType(Untyped.typeLevelReference("String")) - .addStatement(Untyped.functionReturn(Untyped.intLiteral(42))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("String", Types.metaType(Types.STRING)) - )); - - var error = assertThrows( - UnexpectedTypeError.class, - () -> typeCheckNamespaceStatement(untyped, context) - ); - - assertThat(error.expected(), equalTo(TypeSets.type(Types.STRING))); - assertThat(error.actual(), equalTo(Types.INT)); - } - - @Test - public void givenReturnTypeIsUnitWhenBodyHasNoReturnThenFunctionTypeChecks() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withReturnType(Untyped.typeLevelReference("Unit")) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("Unit", Types.metaType(Types.UNIT)) - )); - - typeCheckNamespaceStatement(untyped, context); - } - - @Test - public void givenReturnTypeIsNotUnitWhenBodyHasNoReturnThenFunctionFailsTypeCheck() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withReturnType(Untyped.typeLevelReference("String")) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("String", Types.metaType(Types.STRING)) - )); - - var error = assertThrows( - UnexpectedTypeError.class, - () -> typeCheckNamespaceStatement(untyped, context) - ); - - assertThat(error.expected(), equalTo(TypeSets.type(Types.STRING))); - assertThat(error.actual(), equalTo(Types.UNIT)); - } - - @Test - public void givenReturnTypeIsNotUnitWhenBodyHasBlockReturnOfCorrectTypeThenFunctionTypeChecks() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withReturnType(Untyped.typeLevelReference("String")) - .addStatement(Untyped.blockReturn(Untyped.stringLiteral("hello"))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("String", Types.metaType(Types.STRING)) - )); - - typeCheckNamespaceStatement(untyped, context); - } - - @Test - public void freeVariablesIncludeVariablesReferencedInFunctionButDefinedOutsideOfFunction() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withReturnType(Untyped.typeLevelReference("Unit")) - .addStatement(Untyped.expressionStatement(Untyped.reference("y"))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("Unit", Types.metaType(Types.UNIT)), - Map.entry("x", Types.STRING), - Map.entry("y", Types.STRING) - )); - - var typed = typeCheckNamespaceStatement(untyped, context); - - assertThat( - typed.value(), - isTypedFunctionDefinitionNode() - .withFreeVariables(containsExactly( - has("name", x -> x.name(), equalTo("y")) - )) - ); - } - - @Test - public void freeVariablesDoNotIncludeArgsReferencedInFunction() { - var untyped = UntypedFunctionDefinitionNode.builder() - .addPositionalParam("x", Untyped.typeLevelReference("String")) - .withReturnType(Untyped.typeLevelReference("Unit")) - .addStatement(Untyped.expressionStatement(Untyped.reference("x"))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("String", Types.metaType(Types.STRING)), - Map.entry("Unit", Types.metaType(Types.UNIT)) - )); - - var typed = typeCheckNamespaceStatement(untyped, context); - - assertThat( - typed.value(), - isTypedFunctionDefinitionNode() - .withFreeVariables(containsExactly()) - ); - } - - @Test - public void freeVariablesIncludeVariablesReferencedInBlockInFunctionButDefinedOutsideOfFunction() { - var untyped = UntypedFunctionDefinitionNode.builder() - .withReturnType(Untyped.typeLevelReference("Unit")) - .addStatement(Untyped.expressionStatement(Untyped.if_( - List.of( - Untyped.conditionalBranch( - Untyped.boolLiteral(true), - Untyped.block(List.of()) - ) - ), - Untyped.block(List.of( - Untyped.expressionStatement(Untyped.reference("x")) - )) - ))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("Unit", Types.metaType(Types.UNIT)), - Map.entry("x", Types.STRING) - )); - - var typed = typeCheckNamespaceStatement(untyped, context); - - assertThat( - typed.value(), - isTypedFunctionDefinitionNode() - .withFreeVariables(containsExactly( - has("name", x -> x.name(), equalTo("x")) - )) - ); - } - - @Test - public void boundVariablesAreSet() { - var untyped = UntypedFunctionDefinitionNode.builder() - .addPositionalParam("argPositional", Untyped.typeLevelReference("Unit")) - .addNamedParam("argNamed", Untyped.typeLevelReference("Unit")) - .withReturnType(Untyped.typeLevelReference("Unit")) - .addStatement(Untyped.val("local1", Untyped.intLiteral(1))) - .addStatement(Untyped.expressionStatement(Untyped.reference("y"))) - .addStatement(Untyped.expressionStatement(Untyped.if_( - List.of( - Untyped.conditionalBranch( - Untyped.boolLiteral(true), - Untyped.block(List.of( - Untyped.val("local2", Untyped.intLiteral(2)) - )) - ) - ), - Untyped.block(List.of()) - ))) - .build(); - var context = TypeCheckerContext.EMPTY - .enterGlobal(P.map( - Map.entry("Unit", Types.metaType(Types.UNIT)), - Map.entry("y", Types.STRING) - )); - - var typed = typeCheckNamespaceStatement(untyped, context); - - assertThat( - typed.value(), - isTypedFunctionDefinitionNode() - .withBodyBlock(isTypedBlockNodeWithBoundVariableNames(containsExactly( - equalTo("argPositional"), - equalTo("argNamed"), - equalTo("local1") - ))) - ); - } } diff --git a/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionExpressionTests.java b/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionExpressionTests.java new file mode 100644 index 00000000..85f6b3b7 --- /dev/null +++ b/src/test/java/org/shedlang/compiler/typechecker/TypeCheckerFunctionExpressionTests.java @@ -0,0 +1,41 @@ +package org.shedlang.compiler.typechecker; + +import org.junit.jupiter.api.Test; +import org.shedlang.compiler.ast.untyped.Untyped; +import org.shedlang.compiler.ast.untyped.UntypedFunctionExpressionNode; +import org.shedlang.compiler.types.Effects; +import org.shedlang.compiler.types.FunctionType; +import org.shedlang.compiler.types.Types; +import org.shedlang.compiler.util.P; + +import java.util.List; +import java.util.Map; + +import static org.shedlang.compiler.ast.typed.TypedNodeMatchers.*; +import static org.shedlang.compiler.typechecker.TypeChecker.typeCheckExpression; +import static org.zwobble.precisely.AssertThat.assertThat; +import static org.zwobble.precisely.Matchers.isOptionalEmpty; +import static org.zwobble.precisely.Matchers.isSequence; + +public class TypeCheckerFunctionExpressionTests { + @Test + public void canTypeCheckMinimalFunctionDefinition() { + var untyped = UntypedFunctionExpressionNode.builder() + .withReturnType(Untyped.typeLevelReference("Unit")) + .build(); + var context = TypeCheckerContext.EMPTY + .enterGlobal(P.map(Map.entry("Unit", Types.metaType(Types.UNIT)))); + + var typed = typeCheckExpression(untyped, context); + + assertThat( + typed.value(), + isTypedFunctionExpressionNode() + .withParams(isTypedParamsNode(isSequence(), isSequence())) + .withEffect(isOptionalEmpty()) + .withReturnType(isTypedTypeLevelReferenceNode("Unit", Types.UNIT)) + .withBodyBlock(isTypedBlockNode(isSequence())) + .withType(new FunctionType(List.of(), List.of(), Effects.NONE, Types.UNIT)) + ); + } +} -- cgit v1.2.3