Skip to content

Commit da8f3c0

Browse files
committed
Merge remote-tracking branch 'upstream/mlscript' into tidy-ir
# Conflicts: # compiler/shared/test/scala/mlscript/compiler/TestIR.scala
2 parents 0d67345 + 4a5a038 commit da8f3c0

File tree

16 files changed

+418
-248
lines changed

16 files changed

+418
-248
lines changed

build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ lazy val compiler = crossProject(JSPlatform, JVMPlatform).in(file("compiler"))
8383
sourceDirectory := baseDirectory.value.getParentFile()/"shared",
8484
watchSources += WatchSource(
8585
baseDirectory.value.getParentFile()/"shared"/"test"/"diff", "*.mls", NothingFilter),
86+
watchSources += WatchSource(
87+
baseDirectory.value.getParentFile()/"shared"/"test"/"diff-ir", "*.mls", NothingFilter),
8688
)
8789
.dependsOn(mlscript % "compile->compile;test->test")
8890

compiler/shared/test/scala/mlscript/compiler/Test.scala

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import scala.collection.mutable.StringBuilder
77
import mlscript.compiler.TreeDebug
88
import simpledef.SimpleDef
99

10-
class DiffTestCompiler extends DiffTests {
11-
import DiffTestCompiler.*
10+
import DiffTestCompiler.*
11+
12+
class DiffTestCompiler extends DiffTests(State) {
13+
1214
override def postProcess(mode: ModeType, basePath: List[Str], testName: Str, unit: TypingUnit, output: Str => Unit, raise: Diagnostic => Unit): (List[Str], Option[TypingUnit]) =
1315
val outputBuilder = StringBuilder()
1416

@@ -47,21 +49,11 @@ class DiffTestCompiler extends DiffTests {
4749
}
4850
None
4951

50-
override protected lazy val files = allFiles.filter { file =>
51-
val fileName = file.baseName
52-
validExt(file.ext) && filter(file.relativeTo(pwd))
53-
}
5452
}
5553

5654
object DiffTestCompiler {
57-
58-
private val pwd = os.pwd
59-
private val dir = pwd/"compiler"/"shared"/"test"/"diff"
6055

61-
private val allFiles = os.walk(dir).filter(_.toIO.isFile)
62-
63-
private val validExt = Set("fun", "mls")
64-
65-
private def filter(file: os.RelPath) = DiffTests.filter(file)
56+
lazy val State =
57+
new DiffTests.State(DiffTests.pwd/"compiler"/"shared"/"test"/"diff")
6658

6759
}

compiler/shared/test/scala/mlscript/compiler/TestIR.scala

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import mlscript.compiler.codegen.cpp._
1010
import mlscript.Diagnostic
1111
import mlscript.compiler.optimizer.TailRecOpt
1212

13-
class IRDiffTestCompiler extends DiffTests {
14-
import IRDiffTestCompiler.*
13+
import IRDiffTestCompiler.*
14+
15+
class IRDiffTestCompiler extends DiffTests(State) {
1516
def printToFile(f: java.io.File)(op: java.io.PrintWriter => Unit) = {
1617
val p = new java.io.PrintWriter(f)
1718
try { op(p) } finally { p.close() }
1819
}
20+
1921
override def postProcess(mode: ModeType, basePath: List[Str], testName: Str, unit: TypingUnit, output: Str => Unit, raise: Diagnostic => Unit): (List[Str], Option[TypingUnit]) =
2022
val outputBuilder = StringBuilder()
2123
if (mode.useIR || mode.irVerbose)
@@ -69,21 +71,11 @@ class IRDiffTestCompiler extends DiffTests {
6971

7072
(outputBuilder.toString.linesIterator.toList, None)
7173

72-
override protected lazy val files = allFiles.filter { file =>
73-
val fileName = file.baseName
74-
validExt(file.ext) && filter(file.relativeTo(pwd))
75-
}
7674
}
7775

7876
object IRDiffTestCompiler {
79-
80-
private val pwd = os.pwd
81-
private val dir = pwd/"compiler"/"shared"/"test"/"diff-ir"
82-
83-
private val allFiles = os.walk(dir).filter(_.toIO.isFile)
8477

85-
private val validExt = Set("fun", "mls")
86-
87-
private def filter(file: os.RelPath) = DiffTests.filter(file)
78+
lazy val State =
79+
new DiffTests.State(DiffTests.pwd/"compiler"/"shared"/"test"/"diff-ir")
8880

8981
}

shared/src/main/scala/mlscript/TyperHelpers.scala

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -646,9 +646,6 @@ abstract class TyperHelpers { Typer: Typer =>
646646
case ComposedType(false, l, r) => l.negNormPos(f, p) | r.negNormPos(f, p)
647647
case NegType(n) => f(n).withProv(p)
648648
case tr: TypeRef if !preserveTypeRefs && tr.canExpand => tr.expandOrCrash.negNormPos(f, p)
649-
case _: RecordType | _: FunctionType => BotType // Only valid in positive positions!
650-
// Because Top<:{x:S}|{y:T}, any record type negation neg{x:S}<:{y:T} for any y=/=x,
651-
// meaning negated records are basically bottoms.
652649
case rw => NegType(f(rw))(p)
653650
}
654651
def withProvOf(ty: SimpleType): ST = withProv(ty.prov)

shared/src/test/diff/fcp/Overloads.mls

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,33 +93,43 @@ if true then IISS else BBNN
9393
//│ res: (0 | 1 | true) -> number
9494

9595

96-
// * Note that type normalization is currently very aggressive at approximating negative non-tag types, to simplify the result:
96+
// * Note that type normalization used to be very aggressive at approximating non-tag type negations,
97+
// * to simplify the result, but this was changed as it was unsound
9798

9899
def test: ~(int -> int)
99-
//│ test: in ~(int -> int) out nothing
100+
//│ test: ~(int -> int)
100101

101-
// * Note about this known unsoundness: see test file BooleanFail.mls
102+
// * See also test file BooleanFail.mls about this previous unsoundness
103+
:e
102104
test = 42
103105
not test
104106
//│ 42
105107
//│ <: test:
106108
//│ ~(int -> int)
107-
//│ res: bool
109+
//│ ╔══[ERROR] Type mismatch in application:
110+
//│ ║ l.105: not test
111+
//│ ║ ^^^^^^^^
112+
//│ ╟── type `~(int -> int)` is not an instance of type `bool`
113+
//│ ║ l.99: def test: ~(int -> int)
114+
//│ ║ ^^^^^^^^^^^^^
115+
//│ ╟── but it flows into reference with expected type `bool`
116+
//│ ║ l.105: not test
117+
//│ ╙── ^^^^
118+
//│ res: bool | error
108119

109-
// :ds
110120
def test: ~(int -> int) & ~bool
111-
//│ test: in ~bool & ~(int -> int) out nothing
121+
//│ test: ~bool & ~(int -> int)
112122

113123
def test: ~(int -> int) & bool
114-
//│ test: in bool out nothing
124+
//│ test: bool
115125

116126
def test: ~(int -> int) & ~(bool -> bool)
117-
//│ test: in ~(nothing -> (bool | int)) out nothing
127+
//│ test: ~(nothing -> (bool | int))
118128

119129
def test: ~(int -> int | bool -> bool)
120-
//│ test: in ~(nothing -> (bool | int)) out nothing
130+
//│ test: ~(nothing -> (bool | int))
121131

122132
def test: ~(int -> int & string -> string) & ~(bool -> bool & number -> number)
123-
//│ test: in ~(nothing -> (number | string) & int -> number & nothing -> (bool | string) & nothing -> (bool | int)) out nothing
133+
//│ test: in ~(nothing -> (number | string) & int -> number & nothing -> (bool | string) & nothing -> (bool | int)) out ~(nothing -> (bool | int) & nothing -> (bool | string) & int -> number & nothing -> (number | string))
124134

125135

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ id (error: A) with { x = 1 } : A | { x: 'a }
190190
//│ res: A | {x: nothing}
191191

192192
def negWeird: ~(~(~(A & { x: int })))
193-
//│ negWeird: in ~(A & {x: int}) out ~A
193+
//│ negWeird: ~(A & {x: int})
194194

195195
def v = negWeird with { x = 1 }
196196
//│ v: ~A\x & {x: 1} | {x: 1} & ~{x: int}

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

Lines changed: 145 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,52 @@
33

44
// * The MLscript subtyping system is currently ill-formed in some corner cases.
55
// * Notably, it considers functions and classes to intersect to nothing
6-
// * and also considers positive negated function/record types equivalent to nothing.
6+
// * and also used to considers positive negated function/record types equivalent to nothing.
77

88
// * (This isn't the case in MLstruct, which has a sound subtyping lattice.)
99

1010

11-
// * Example 1
11+
12+
// * Example 1 – now fixed
13+
1214
oops = 42 : ~(int -> int)
13-
not oops
14-
//│ oops: nothing
15+
//│ oops: ~(int -> int)
1516
//│ = 42
16-
//│ res: bool
17+
18+
:e
19+
not oops
20+
//│ ╔══[ERROR] Type mismatch in application:
21+
//│ ║ l.19: not oops
22+
//│ ║ ^^^^^^^^
23+
//│ ╟── type `~(int -> int)` is not an instance of type `bool`
24+
//│ ║ l.14: oops = 42 : ~(int -> int)
25+
//│ ║ ^^^^^^^^^^^^^
26+
//│ ╟── but it flows into reference with expected type `bool`
27+
//│ ║ l.19: not oops
28+
//│ ╙── ^^^^
29+
//│ res: bool | error
1730
//│ = false
1831

1932

20-
// * OTOH, this doesn't lead to immediate unsoundness:
33+
// * This was accepted but didn't immediately lead to immediate unsoundness:
2134
def f: (~{x: int}) -> 'a
22-
f = id
23-
//│ f: in nothing -> nothing out ~{x: int} -> nothing
35+
//│ f: ~{x: int} -> nothing
2436
//│ = <missing implementation>
37+
38+
:e
39+
f = id
2540
//│ 'a -> 'a
2641
//│ <: f:
27-
//│ nothing -> nothing
42+
//│ ~{x: int} -> nothing
43+
//│ ╔══[ERROR] Type mismatch in def definition:
44+
//│ ║ l.39: f = id
45+
//│ ║ ^^^^^^
46+
//│ ╟── type `~{x: int}` does not match type `'a`
47+
//│ ║ l.34: def f: (~{x: int}) -> 'a
48+
//│ ║ ^^^^^^^^^^^
49+
//│ ╟── Note: constraint arises from type variable:
50+
//│ ║ l.34: def f: (~{x: int}) -> 'a
51+
//│ ╙── ^^
2852
//│ = [Function: id]
2953

3054
:e
@@ -33,48 +57,148 @@ f id
3357
f {}
3458
f (forall 'a. fun (x: 'a) -> x)
3559
//│ ╔══[ERROR] Type mismatch in application:
36-
//│ ║ l.31: f 0
60+
//│ ║ l.55: f 0
3761
//│ ║ ^^^
3862
//│ ╟── integer literal of type `0` does not match type `~{x: int}`
39-
//│ ║ l.31: f 0
63+
//│ ║ l.55: f 0
4064
//│ ║ ^
4165
//│ ╟── Note: constraint arises from type negation:
42-
//│ ║ l.21: def f: (~{x: int}) -> 'a
66+
//│ ║ l.34: def f: (~{x: int}) -> 'a
4367
//│ ╙── ^^^^^^^^^^^
4468
//│ res: error
4569
//│ = 0
4670
//│ ╔══[ERROR] Type mismatch in application:
47-
//│ ║ l.32: f id
71+
//│ ║ l.56: f id
4872
//│ ║ ^^^^
4973
//│ ╟── reference of type `?a -> ?a` does not match type `~{x: int}`
50-
//│ ║ l.32: f id
74+
//│ ║ l.56: f id
5175
//│ ║ ^^
5276
//│ ╟── Note: constraint arises from type negation:
53-
//│ ║ l.21: def f: (~{x: int}) -> 'a
77+
//│ ║ l.34: def f: (~{x: int}) -> 'a
5478
//│ ╙── ^^^^^^^^^^^
5579
//│ res: error
5680
//│ = [Function: id]
5781
//│ ╔══[ERROR] Type mismatch in application:
58-
//│ ║ l.33: f {}
82+
//│ ║ l.57: f {}
5983
//│ ║ ^^^^
6084
//│ ╟── record literal of type `anything` does not match type `~{x: int}`
61-
//│ ║ l.33: f {}
85+
//│ ║ l.57: f {}
6286
//│ ║ ^^
6387
//│ ╟── Note: constraint arises from type negation:
64-
//│ ║ l.21: def f: (~{x: int}) -> 'a
88+
//│ ║ l.34: def f: (~{x: int}) -> 'a
6589
//│ ╙── ^^^^^^^^^^^
6690
//│ res: error
6791
//│ = {}
6892
//│ ╔══[ERROR] Type mismatch in application:
69-
//│ ║ l.34: f (forall 'a. fun (x: 'a) -> x)
93+
//│ ║ l.58: f (forall 'a. fun (x: 'a) -> x)
7094
//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7195
//│ ╟── function of type `'a -> 'a` does not match type `~{x: int}`
72-
//│ ║ l.34: f (forall 'a. fun (x: 'a) -> x)
96+
//│ ║ l.58: f (forall 'a. fun (x: 'a) -> x)
7397
//│ ║ ^^^^^^^^^^^^^^^^
7498
//│ ╟── Note: constraint arises from type negation:
75-
//│ ║ l.21: def f: (~{x: int}) -> 'a
99+
//│ ║ l.34: def f: (~{x: int}) -> 'a
76100
//│ ╙── ^^^^^^^^^^^
77101
//│ res: error
78102
//│ = [Function (anonymous)]
79103

80104

105+
// * Example 2 – now fixed
106+
107+
def g(x: 'a | {f: nothing}) = x.f(0)
108+
//│ g: {f: 0 -> 'a} -> 'a
109+
//│ = [Function: g]
110+
111+
:e
112+
foo = forall 'x. fun (x: 'x) -> g(x)
113+
//│ ╔══[ERROR] Type mismatch in application:
114+
//│ ║ l.112: foo = forall 'x. fun (x: 'x) -> g(x)
115+
//│ ║ ^^^^
116+
//│ ╟── expression of type `'x & ~{f: nothing}` does not have field 'f'
117+
//│ ╟── Note: constraint arises from field selection:
118+
//│ ║ l.107: def g(x: 'a | {f: nothing}) = x.f(0)
119+
//│ ║ ^^^
120+
//│ ╟── from type variable:
121+
//│ ║ l.107: def g(x: 'a | {f: nothing}) = x.f(0)
122+
//│ ╙── ^^
123+
//│ foo: anything -> error
124+
//│ = [Function: foo]
125+
126+
:re
127+
foo 0
128+
//│ res: error
129+
//│ Runtime error:
130+
//│ TypeError: x.f is not a function
131+
132+
133+
134+
// * Now let's consider why functions and classes can't intersect to nothing due to distributivity
135+
136+
137+
class Foo: { x: anything }
138+
//│ Defined class Foo
139+
140+
141+
// * These two types should be equivalent, but they visibly aren't:
142+
143+
def a: (int -> int | {x: int}) & Foo
144+
def b: int -> int & Foo | {x: int} & Foo
145+
//│ a: Foo
146+
//│ = <missing implementation>
147+
//│ b: Foo & {x: int}
148+
//│ = <missing implementation>
149+
150+
:ne
151+
ax = a.x
152+
bx = b.x
153+
//│ ax: anything
154+
//│ bx: int
155+
156+
157+
// * Yet, this does not immediately lead to unsoundness due to the aggressive normalization
158+
// * performed during constraint solving:
159+
160+
:ne
161+
a = b
162+
//│ Foo & {x: int}
163+
//│ <: a:
164+
//│ Foo
165+
166+
:e
167+
:ne
168+
b = a
169+
//│ Foo
170+
//│ <: b:
171+
//│ Foo & {x: int}
172+
//│ ╔══[ERROR] Type mismatch in def definition:
173+
//│ ║ l.168: b = a
174+
//│ ║ ^^^^^
175+
//│ ╟── expression of type `anything` is not an instance of type `int`
176+
//│ ╟── Note: constraint arises from type reference:
177+
//│ ║ l.144: def b: int -> int & Foo | {x: int} & Foo
178+
//│ ╙── ^^^
179+
180+
181+
// * To expose the unsoundness, we need some indirection with abstract types
182+
// * that prevent eagerly distributing the intersection type:
183+
184+
class Test1[A, B]: { f: A & Foo }
185+
method M: B & Foo | {x: int} & Foo
186+
//│ Defined class Test1[+A, +B]
187+
//│ Declared Test1.M: Test1[?, 'B] -> (Foo & 'B | Foo & {x: int})
188+
189+
class Test2[B]: Test1[B | {x: int}, B]
190+
method M = this.f : B & Foo | {x: int} & Foo
191+
//│ Defined class Test2[+B]
192+
//│ Defined Test2.M: Test2['B] -> (Foo & 'B | Foo & {x: int})
193+
194+
oops = (Test2{f = Foo{x = "oops"}} : Test1[anything, int -> int]).M
195+
//│ oops: Foo & {x: int}
196+
//│ = Foo { x: 'oops' }
197+
198+
// * Notice the type confusion:
199+
oops.x + 1
200+
//│ res: int
201+
//│ = 'oops1'
202+
203+
204+

0 commit comments

Comments
 (0)