Skip to content

Commit f990dfe

Browse files
committed
Explicitly derive example of known unsoundness in normal forms
1 parent 13ba521 commit f990dfe

File tree

1 file changed

+89
-0
lines changed

1 file changed

+89
-0
lines changed

shared/src/test/diff/mlscript/BooleanFail.mls

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,92 @@ f (forall 'a. fun (x: 'a) -> x)
7878
//│ = [Function (anonymous)]
7979

8080

81+
// * Example 2
82+
83+
def g(x: 'a | {f: nothing}) = x.f(0)
84+
//│ g: {f: 0 -> 'a} -> 'a
85+
//│ = [Function: g]
86+
87+
foo = forall 'x. fun (x: 'x) -> g(x)
88+
//│ foo: anything -> nothing
89+
//│ = [Function: foo]
90+
91+
:re
92+
foo 0
93+
//│ res: nothing
94+
//│ Runtime error:
95+
//│ TypeError: x.f is not a function
96+
97+
98+
99+
// * Now let's consider why functions and classes can't intersect to nothing due to distributivity
100+
101+
102+
class Foo: { x: anything }
103+
//│ Defined class Foo
104+
105+
106+
// * These two types should be equivalent, but they visibly aren't:
107+
108+
def a: (int -> int | {x: int}) & Foo
109+
def b: int -> int & Foo | {x: int} & Foo
110+
//│ a: Foo
111+
//│ = <missing implementation>
112+
//│ b: Foo & {x: int}
113+
//│ = <missing implementation>
114+
115+
:ne
116+
ax = a.x
117+
bx = b.x
118+
//│ ax: anything
119+
//│ bx: int
120+
121+
122+
// * Yet, this does not immediately lead to unsoundness due to the aggressive normalization
123+
// * performed during constraint solving:
124+
125+
:ne
126+
a = b
127+
//│ Foo & {x: int}
128+
//│ <: a:
129+
//│ Foo
130+
131+
:e
132+
:ne
133+
b = a
134+
//│ Foo
135+
//│ <: b:
136+
//│ Foo & {x: int}
137+
//│ ╔══[ERROR] Type mismatch in def definition:
138+
//│ ║ l.133: b = a
139+
//│ ║ ^^^^^
140+
//│ ╟── expression of type `anything` is not an instance of type `int`
141+
//│ ╟── Note: constraint arises from type reference:
142+
//│ ║ l.109: def b: int -> int & Foo | {x: int} & Foo
143+
//│ ╙── ^^^
144+
145+
146+
// * To expose the unsoundness, we need some indirection with abstract types
147+
// * that prevent eagerly distributing the intersection type:
148+
149+
class Test1[A, B]: { f: A & Foo }
150+
method M: B & Foo | {x: int} & Foo
151+
//│ Defined class Test1[+A, +B]
152+
//│ Declared Test1.M: Test1[?, 'B] -> (Foo & 'B | Foo & {x: int})
153+
154+
class Test2[B]: Test1[B | {x: int}, B]
155+
method M = this.f : B & Foo | {x: int} & Foo
156+
//│ Defined class Test2[+B]
157+
//│ Defined Test2.M: Test2['B] -> (Foo & 'B | Foo & {x: int})
158+
159+
oops = (Test2{f = Foo{x = "oops"}} : Test1[anything, int -> int]).M
160+
//│ oops: Foo & {x: int}
161+
//│ = Foo { x: 'oops' }
162+
163+
// * Notice the type confusion:
164+
oops.x + 1
165+
//│ res: int
166+
//│ = 'oops1'
167+
168+
169+

0 commit comments

Comments
 (0)