Skip to content

Commit d0a3717

Browse files
committed
fix: subtyping and casting checks for records vs interfaces
* abstract types such as interface names cannot be accepted in casts * record prototypes are accepted where interfaces they implement are wanted Fixes #830.
1 parent fbbae10 commit d0a3717

File tree

3 files changed

+120
-43
lines changed

3 files changed

+120
-43
lines changed

spec/lang/subtyping/interface_spec.lua

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,72 @@ describe("subtyping of interfaces:", function()
1414
local r: MyRecord = {}
1515
print(#r)
1616
]]))
17-
end)
1817

18+
it("record <: interface", util.check([[
19+
local interface MyInterface
20+
x: integer
21+
end
22+
23+
local record MyRecord is MyInterface
24+
end
25+
26+
local function f(p: MyInterface)
27+
print(p.x)
28+
end
29+
30+
local r: MyRecord = { x = 2 }
31+
f(r)
32+
]]))
33+
34+
it("prototype record <: interface", util.check([[
35+
local interface MyInterface
36+
x: integer
37+
end
38+
39+
local record MyRecord is MyInterface
40+
end
41+
42+
local function f(p: MyInterface)
43+
print(p.x)
44+
end
45+
46+
MyRecord.x = 2
47+
f(MyRecord)
48+
]]))
49+
50+
it("regression test for #830", util.check_lines([[
51+
local interface IFoo
52+
end
53+
54+
local record Foo is IFoo
55+
end
56+
57+
local function bar(_value:Foo)
58+
end
59+
60+
local function qux(_value:IFoo)
61+
end
62+
63+
local foo:Foo
64+
65+
]], {
66+
{ line = "bar(foo)" },
67+
{ line = "bar(Foo)" },
68+
{ line = "bar(IFoo)", err = "IFoo is not a Foo" },
69+
{ line = "bar(foo as Foo)" },
70+
{ line = "bar(Foo as Foo)" },
71+
{ line = "bar(IFoo as Foo)", err = "interfaces are abstract" },
72+
{ line = "bar(foo as IFoo)", err = "IFoo is not a Foo" },
73+
{ line = "bar(Foo as IFoo)", err = "IFoo is not a Foo" },
74+
{ line = "bar(IFoo as IFoo)", err = "interfaces are abstract" },
75+
{ line = "qux(foo)" },
76+
{ line = "qux(Foo)" },
77+
{ line = "qux(IFoo)", err = "interfaces are abstract" },
78+
{ line = "qux(foo as Foo)" },
79+
{ line = "qux(Foo as Foo)" },
80+
{ line = "qux(IFoo as Foo)", err = "interfaces are abstract" },
81+
{ line = "qux(foo as IFoo)" },
82+
{ line = "qux(Foo as IFoo)" },
83+
{ line = "qux(IFoo as IFoo)", err = "interfaces are abstract" },
84+
}))
85+
end)

tl.lua

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8922,11 +8922,8 @@ a.types[i], b.types[i]), }
89228922
["emptytable"] = compare_true_inferring_emptytable,
89238923
},
89248924
["typedecl"] = {
8925-
["record"] = function(self, a, b)
8926-
local def = a.def
8927-
if def.fields then
8928-
return self:subtype_record(def, b)
8929-
end
8925+
["*"] = function(self, a, b)
8926+
return self:is_a(a.def, b)
89308927
end,
89318928
},
89328929
["function"] = {
@@ -9000,6 +8997,9 @@ a.types[i], b.types[i]), }
90008997
["tuple"] = function(self, a, b)
90018998
return self:is_a(a_type(a, "tuple", { tuple = { a } }), b)
90028999
end,
9000+
["typedecl"] = function(self, a, b)
9001+
return self:is_a(a, b.def)
9002+
end,
90039003
["typevar"] = function(self, a, b)
90049004
return self:compare_or_infer_typevar(b.typevar, a, nil, self.is_a)
90059005
end,
@@ -9032,27 +9032,28 @@ a.types[i], b.types[i]), }
90329032
["self"] = 3,
90339033
["tuple"] = 4,
90349034
["typevar"] = 5,
9035-
["any"] = 6,
9036-
["boolean_context"] = 7,
9037-
["union"] = 8,
9038-
["poly"] = 9,
9035+
["typedecl"] = 6,
9036+
["any"] = 7,
9037+
["boolean_context"] = 8,
9038+
["union"] = 9,
9039+
["poly"] = 10,
90399040

9040-
["typearg"] = 10,
9041+
["typearg"] = 11,
90419042

9042-
["nominal"] = 11,
9043+
["nominal"] = 12,
90439044

9044-
["enum"] = 12,
9045-
["string"] = 12,
9046-
["integer"] = 12,
9047-
["boolean"] = 12,
9045+
["enum"] = 13,
9046+
["string"] = 13,
9047+
["integer"] = 13,
9048+
["boolean"] = 13,
90489049

9049-
["interface"] = 13,
9050+
["interface"] = 14,
90509051

9051-
["tupletable"] = 14,
9052-
["record"] = 14,
9053-
["array"] = 14,
9054-
["map"] = 14,
9055-
["function"] = 14,
9052+
["tupletable"] = 15,
9053+
["record"] = 15,
9054+
["array"] = 15,
9055+
["map"] = 15,
9056+
["function"] = 15,
90569057
}
90579058

90589059
local function compare_types(self, relations, t1, t2)
@@ -12290,6 +12291,10 @@ self:expand_type(node, values, elements) })
1229012291
return t
1229112292

1229212293
elseif node.op.op == "as" then
12294+
local ok, err = ensure_not_abstract(ra)
12295+
if not ok then
12296+
return self.errs:invalid_at(node.e1, err)
12297+
end
1229312298
return gb
1229412299

1229512300
elseif node.op.op == "is" and ra.typename == "typedecl" then

tl.tl

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8922,11 +8922,8 @@ do
89228922
["emptytable"] = compare_true_inferring_emptytable,
89238923
},
89248924
["typedecl"] = {
8925-
["record"] = function(self: TypeChecker, a: TypeDeclType, b: RecordType): boolean, {Error}
8926-
local def = a.def
8927-
if def is RecordLikeType then
8928-
return self:subtype_record(def, b) -- record as prototype
8929-
end
8925+
["*"] = function(self: TypeChecker, a: TypeDeclType, b: RecordType): boolean, {Error}
8926+
return self:is_a(a.def, b)
89308927
end,
89318928
},
89328929
["function"] = {
@@ -9000,6 +8997,9 @@ do
90008997
["tuple"] = function(self: TypeChecker, a: Type, b: Type): boolean, {Error}
90018998
return self:is_a(a_tuple(a, {a}), b)
90028999
end,
9000+
["typedecl"] = function(self: TypeChecker, a: Type, b: TypeDeclType): boolean, {Error}
9001+
return self:is_a(a, b.def)
9002+
end,
90039003
["typevar"] = function(self: TypeChecker, a: Type, b: TypeVarType): boolean, {Error}
90049004
return self:compare_or_infer_typevar(b.typevar, a, nil, self.is_a)
90059005
end,
@@ -9032,27 +9032,28 @@ do
90329032
["self"] = 3,
90339033
["tuple"] = 4,
90349034
["typevar"] = 5,
9035-
["any"] = 6,
9036-
["boolean_context"] = 7,
9037-
["union"] = 8,
9038-
["poly"] = 9,
9035+
["typedecl"] = 6,
9036+
["any"] = 7,
9037+
["boolean_context"] = 8,
9038+
["union"] = 9,
9039+
["poly"] = 10,
90399040
-- then typeargs
9040-
["typearg"] = 10,
9041+
["typearg"] = 11,
90419042
-- then nominals
9042-
["nominal"] = 11,
9043+
["nominal"] = 12,
90439044
-- then base types
9044-
["enum"] = 12,
9045-
["string"] = 12,
9046-
["integer"] = 12,
9047-
["boolean"] = 12,
9045+
["enum"] = 13,
9046+
["string"] = 13,
9047+
["integer"] = 13,
9048+
["boolean"] = 13,
90489049
-- then interfaces
9049-
["interface"] = 13,
9050+
["interface"] = 14,
90509051
-- then special cases of tables
9051-
["tupletable"] = 14,
9052-
["record"] = 14,
9053-
["array"] = 14,
9054-
["map"] = 14,
9055-
["function"] = 14,
9052+
["tupletable"] = 15,
9053+
["record"] = 15,
9054+
["array"] = 15,
9055+
["map"] = 15,
9056+
["function"] = 15,
90569057
}
90579058

90589059
local function compare_types(self: TypeChecker, relations: TypeRelations, t1: Type, t2: Type): boolean, {Error}
@@ -12290,6 +12291,10 @@ do
1229012291
return t
1229112292

1229212293
elseif node.op.op == "as" then
12294+
local ok, err = ensure_not_abstract(ra)
12295+
if not ok then
12296+
return self.errs:invalid_at(node.e1, err)
12297+
end
1229312298
return gb
1229412299

1229512300
elseif node.op.op == "is" and ra is TypeDeclType then

0 commit comments

Comments
 (0)