-
-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
report an error when a type annotation is too general #2109
Changes from 10 commits
c98d022
5f04bfc
a0df145
da66f9d
df19ef2
9810996
e4cbd1b
79de82f
267c326
17681c0
8081117
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
From now on, the type checker reports an error, when the inferred type | ||
is more specific than the annotated type, see #2109. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
---------------------- MODULE PolyTooGeneral ------------------------------ | ||
\* A library author did not think carefully about the types | ||
\* and wrote a too general type. The type checker should complain. | ||
\* | ||
\* @type: a => b; | ||
Id(x) == x | ||
=========================================================================== |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,14 +1,14 @@ | ||||||||||||||
package at.forsyte.apalache.tla.typecheck.etc | ||||||||||||||
|
||||||||||||||
import at.forsyte.apalache.tla.lir.{BoolT1, IntT1, OperT1, SeqT1, SetT1, StrT1, TlaType1, TlaType1Scheme, TupT1, VarT1} | ||||||||||||||
import at.forsyte.apalache.tla.typecheck._ | ||||||||||||||
import at.forsyte.apalache.io.typecheck.parser.{DefaultType1Parser, Type1Parser} | ||||||||||||||
import at.forsyte.apalache.tla.lir._ | ||||||||||||||
import at.forsyte.apalache.tla.typecheck._ | ||||||||||||||
import org.easymock.EasyMock | ||||||||||||||
import org.junit.runner.RunWith | ||||||||||||||
import org.scalatestplus.easymock.EasyMockSugar | ||||||||||||||
import org.scalatestplus.junit.JUnitRunner | ||||||||||||||
import org.scalatest.BeforeAndAfterEach | ||||||||||||||
import org.scalatest.funsuite.AnyFunSuite | ||||||||||||||
import org.scalatestplus.easymock.EasyMockSugar | ||||||||||||||
import org.scalatestplus.junit.JUnitRunner | ||||||||||||||
|
||||||||||||||
@RunWith(classOf[JUnitRunner]) | ||||||||||||||
class TestEtcTypeChecker extends AnyFunSuite with EasyMockSugar with BeforeAndAfterEach with EtcBuilder { | ||||||||||||||
|
@@ -521,7 +521,7 @@ class TestEtcTypeChecker extends AnyFunSuite with EasyMockSugar with BeforeAndAf | |||||||||||||
|
||||||||||||||
test("annotated polymorphic let-definition") { | ||||||||||||||
// The names of type variables may change during type inference | ||||||||||||||
// @type: a => b; | ||||||||||||||
// @type: a => a; | ||||||||||||||
// let F == lambda x \in Set(a): ((c, c) => c) x d in F(Int) | ||||||||||||||
val xDomain = mkUniqConst(parser("Set(c)")) | ||||||||||||||
val xNameInApp = mkUniqName("x") | ||||||||||||||
|
@@ -552,8 +552,8 @@ class TestEtcTypeChecker extends AnyFunSuite with EasyMockSugar with BeforeAndAf | |||||||||||||
listener.onTypeFound(fName.sourceRef.asInstanceOf[ExactRef], parser("Int => Int")) | ||||||||||||||
} | ||||||||||||||
whenExecuting(listener) { | ||||||||||||||
// add the type annotation F: \A a: a => b | ||||||||||||||
val context = TypeContext("F" -> TlaType1Scheme(parser("a => b"), Set(0, 1))) | ||||||||||||||
// add the type annotation F: \A a: a => a | ||||||||||||||
val context = TypeContext("F" -> TlaType1Scheme(parser("a => a"), Set(0, 1))) | ||||||||||||||
val computed = checker.compute(listener, context, letIn) | ||||||||||||||
assert(computed.contains(parser("Int"))) | ||||||||||||||
} | ||||||||||||||
|
@@ -565,7 +565,7 @@ class TestEtcTypeChecker extends AnyFunSuite with EasyMockSugar with BeforeAndAf | |||||||||||||
// The bug appeared only in the case when using two instances of the constraint solver, so it needed a complex setup. | ||||||||||||||
// | ||||||||||||||
// let G == | ||||||||||||||
// @type: a => b; | ||||||||||||||
// @type: a => a; | ||||||||||||||
// let F == lambda x \in Set(a): ((c, c) => c) x d | ||||||||||||||
Comment on lines
+568
to
569
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question here: I may be being dense, but I don't understand how it is that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the same thing again. We have a lambda that applies an operator of type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That helps a bit, but the annotation here is really quite confusing, imo, as it seems to be mixing up value and type level syntax. Would this comment convey the same thing but within using types to stand in for expressions?
Suggested change
In any case, I can see that this comment is not change by this PR, so it should be outside the scope of the PR. You've cleared up my confusion, thanks. The part of this that should perhaps be addressed (at least in discussion) is described in #2109 (comment). |
||||||||||||||
// in F(Int) | ||||||||||||||
// in | ||||||||||||||
|
@@ -580,7 +580,8 @@ class TestEtcTypeChecker extends AnyFunSuite with EasyMockSugar with BeforeAndAf | |||||||||||||
val fName = mkUniqName("F") | ||||||||||||||
val fApp = mkUniqAppByName(fName, fArg) | ||||||||||||||
val letInF = mkUniqLet("F", fBody, fApp) | ||||||||||||||
val annotatedLetInF = mkUniqTypeDecl("F", parser("a => b"), letInF) | ||||||||||||||
// add the type annotation F: \A a: a => a | ||||||||||||||
val annotatedLetInF = mkUniqTypeDecl("F", parser("a => a"), letInF) | ||||||||||||||
val bool = mkUniqConst(BoolT1) | ||||||||||||||
val gAbs = mkUniqAbs(annotatedLetInF) // we have to wrap the parameterless body of G with a lambda expression | ||||||||||||||
val letInG = mkUniqLet("G", gAbs, bool) | ||||||||||||||
|
@@ -609,7 +610,6 @@ class TestEtcTypeChecker extends AnyFunSuite with EasyMockSugar with BeforeAndAf | |||||||||||||
listener.onTypeFound(letInG.sourceRef.asInstanceOf[ExactRef], parser("Bool")) | ||||||||||||||
} | ||||||||||||||
whenExecuting(listener) { | ||||||||||||||
// add the type annotation F: \A a, b: a => b | ||||||||||||||
val context = TypeContext.empty | ||||||||||||||
val computed = checker.compute(listener, context, letInG) | ||||||||||||||
assert(computed.contains(parser("Bool"))) | ||||||||||||||
|
@@ -899,6 +899,43 @@ class TestEtcTypeChecker extends AnyFunSuite with EasyMockSugar with BeforeAndAf | |||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
test("imprecise annotation") { | ||||||||||||||
// Test case for: /~https://github.com/informalsystems/apalache/issues/2102 | ||||||||||||||
|
||||||||||||||
// let @type: (a, b) => Bool; | ||||||||||||||
// LET F(x, y) == x = y in TRUE | ||||||||||||||
// | ||||||||||||||
// The principal type of F is (c, c) => Bool, which is more specific than (a, b) => Bool. | ||||||||||||||
val eq = parser("(c, c) => Bool") | ||||||||||||||
val x = mkUniqName("x") | ||||||||||||||
val y = mkUniqName("y") | ||||||||||||||
val fBody = mkUniqApp(Seq(eq), x, y) | ||||||||||||||
// introduce a lambda over the parameters x and y, whose domains are captured with type variables f and g | ||||||||||||||
val domX = mkUniqConst(VarT1("f")) | ||||||||||||||
val domY = mkUniqConst(VarT1("g")) | ||||||||||||||
val lambda = mkUniqAbs(fBody, (x, domX), (y, domY)) | ||||||||||||||
val underLet = mkUniqConst(BoolT1) | ||||||||||||||
val letIn = mkUniqLet("F", lambda, underLet) | ||||||||||||||
val annotatedLetInF = mkUniqTypeDecl("F", parser("(a, b) => Bool"), letIn) | ||||||||||||||
val bool = mkUniqConst(BoolT1) | ||||||||||||||
val gAbs = mkUniqAbs(annotatedLetInF) // we have to wrap the parameterless body of G with a lambda expression | ||||||||||||||
val letInG = mkUniqLet("G", gAbs, bool) | ||||||||||||||
|
||||||||||||||
val listener = mock[TypeCheckerListener] | ||||||||||||||
expecting { | ||||||||||||||
listener.onTypeError(letIn.sourceRef.asInstanceOf[ExactRef], | ||||||||||||||
"F's type annotation ((a, b) => Bool) is too general, inferred: ((a, a) => Bool)") | ||||||||||||||
// consume all found types | ||||||||||||||
for (ex <- Seq(fBody, domX, domY, x, y, lambda, underLet, letIn, annotatedLetInF, bool, gAbs, letInG)) { | ||||||||||||||
listener.onTypeFound(EasyMock.eq(ex.sourceRef.asInstanceOf[ExactRef]), EasyMock.anyObject[TlaType1]).anyTimes() | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
whenExecuting(listener) { | ||||||||||||||
// Notice that the annotation is imprecise! The type checker should complain. | ||||||||||||||
checker.compute(listener, TypeContext(), letInG) | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
test("regression for principal types (issue #1259)") { | ||||||||||||||
val fooType = parser("(a -> b) => Bool") | ||||||||||||||
// \* @type: (a -> b) => Bool; | ||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my own edification: I don't know how to read the body of this lambda. What is
((c, c) => c) x d
? It looks like it's applying an implication to two arguments? Or is that only on the type level? But then what's the labmda doing? And how doesF
end up being typed as the identify function?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This expression is written in the syntax of EtcExpr, a lambda calculus over types. The body of the lambda is an application of
x
andd
to a type, which in this case is an operator of two arguments of typec
and returns typec
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is an identity from the types of view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for explaining. It helps a lot. So is the lambda here a type-level lambda? It seems like it's kind of mixing type level syntax with pseudo-TNT syntax, or am I just totally missing the thread here :D?
So, we are maybe getting outside the scope of this PR, but one thing that I think is throwing me off here is that, afaik, it is not possible to write an operator that actually has type
a => a
and also relies on some constantd
that comes from outside the operator.The only way we can write an operator of type
a => a
is if we treat the provided argument as fully abstract, because we can't know anything about the value of typea
, since we don't know what type it is (or, equivalently, it must work for any type). So afaik, there would be no way of having ad
available that could work for any possible type for application to(c, c) => c
. afaiu, This is why (in a HM-ish type system) any function with typea => a
must be (isomorphic to) the identity function.The fact that this test is going through makes me think that either our type inference is off, or we're constructing type here that shouldn't actually be possible to assign to any expression (or else I am just quite confused about what is going on here). But in any of these three cases, it seems like we might be looking to either fix the test, fix the type inference, or fix my understanding :)