Skip to content

Commit

Permalink
fix: resolve self on records
Browse files Browse the repository at this point in the history
This fixes #877, at least for cases without generics. There is a pending test
showcasing the cases which are not covered by this commit.

Fixes #877.
  • Loading branch information
hishamhm committed Jan 8, 2025
1 parent d449988 commit 365fbd3
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 20 deletions.
63 changes: 63 additions & 0 deletions spec/lang/subtyping/record_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,68 @@ describe("records", function()
{ y = 15, "record field doesn't match: m: got integer, expected string" },
{ y = 17, "record field doesn't match: m: got integer, expected string" },
}))

it("refines self on fields inherited from interfaces (regression test for #877)", util.check([[
-- Define an interface that uses self.
-- Using self should automatically refine implementations to map self to the impl.
local interface Container
new: function(): self
end
-- Calling Foo.new should return a Foo (that is, replace `self` with `Foo`)
local record Foo is Container end
-- Use another collaborating record to demonstrate the bug.
local record SomeRecord
update: function(self: SomeRecord)
end
local function foo<C is Container>(c: C)
end
-- Calling foo, which expects a Container, from this outer scope works.
local outerValue = Foo.new()
foo(outerValue) -- works!
-- Calling foo from within SomeRecord.update does not work.
function SomeRecord:update()
local value = Foo.new()
foo(value) -- works!
end
]]))

pending("refines generic self on fields inherited from interfaces (regression test for #877)", util.check([[
-- Define an interface that uses self.
-- Using self should automatically refine implementations to map self to the impl.
local interface Container<T>
new: function(val: T): self
myfield: T
end
-- Calling Foo.new should return a Foo (that is, replace `self` with `Foo`)
local record Foo<T> is Container<T>
end
-- Use another collaborating record to demonstrate the bug.
local record SomeRecord
update: function(self: SomeRecord)
end
-- FIXME resolution of generic constraints doesn't work
local function foo<T, C is Container<T>>(c: C)
print(tostring(c.myfield))
end
-- Calling foo, which expects a Container, from this outer scope works.
local outerValue = Foo.new(1)
foo(outerValue) -- works!
-- Calling foo from within SomeRecord.update does not work.
function SomeRecord:update()
local value = Foo.new("hello")
foo(value) -- works!
end
]]))

end)

51 changes: 41 additions & 10 deletions tl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2254,9 +2254,9 @@ end



local function a_nominal(n, names)
return a_type(n, "nominal", { names = names })
end






Expand Down Expand Up @@ -2449,6 +2449,7 @@ do
local function new_generic(ps, i, typeargs, typ)
local gt = new_type(ps, i, "generic")
gt.typeargs = typeargs
assert(not (typ.typename == "typedecl"))
gt.t = typ
return gt
end
Expand Down Expand Up @@ -7469,6 +7470,8 @@ do
return t
end

assert(not (t.typename == "typedecl"))

local gt = a_type(t, "generic", { t = t })
gt.typeargs = typeargs
return gt
Expand Down Expand Up @@ -7838,6 +7841,13 @@ do
copy.elements, same = resolve(t.elements, same)
end

if t.interface_list then
copy.interface_list = {}
for i, v in ipairs(t.interface_list) do
copy.interface_list[i], same = resolve(v, same)
end
end

copy.is_userdata = t.is_userdata

copy.fields = {}
Expand Down Expand Up @@ -8755,10 +8765,10 @@ do
function TypeChecker:type_of_self(w)
local t = self:find_var_type("@self")
if not t then
return a_type(w, "invalid", {})
return a_type(w, "invalid", {}), nil
end
assert(t.typename == "typedecl")
return t.def
return t.def, t
end


Expand Down Expand Up @@ -8932,7 +8942,7 @@ do
["*"] = {
["boolean_context"] = compare_true,
["self"] = function(self, a, b)
return self:same_type(a, self:type_of_self(b))
return self:same_type(a, (self:type_of_self(b)))
end,
["typevar"] = function(self, a, b)
return self:compare_or_infer_typevar(b.typevar, a, nil, self.same_type)
Expand Down Expand Up @@ -9287,7 +9297,7 @@ a.types[i], b.types[i]), }
return ok, errs
end,
["self"] = function(self, a, b)
return self:is_a(a, self:type_of_self(b))
return self:is_a(a, (self:type_of_self(b)))
end,
["tuple"] = function(self, a, b)
return self:is_a(a_type(a, "tuple", { tuple = { a } }), b)
Expand Down Expand Up @@ -10515,7 +10525,7 @@ a.types[i], b.types[i]), }
end
end

local function typedecl_to_nominal(node, name, t, resolved)
local function typedecl_to_nominal(w, name, t, resolved)
local typevals
local def = t.def
if def.typename == "generic" then
Expand All @@ -10527,7 +10537,7 @@ a.types[i], b.types[i]), }
}))
end
end
local nom = a_nominal(node, { name })
local nom = a_type(w, "nominal", { names = { name } })
nom.typevals = typevals
nom.found = t
nom.resolved = resolved
Expand Down Expand Up @@ -13166,6 +13176,27 @@ self:expand_type(node, values, elements) })
return t
end

local resolve_self
do
local resolve_self_fns = {
["self"] = function(tc, t)
local selftype, selfdecl = tc:type_of_self(t)
local checktype = selftype
if selftype.typename == "generic" then
checktype = selftype.t
end
if checktype.typename == "record" then
return typedecl_to_nominal(t, checktype.declname, selfdecl)
end
return t
end,
}

resolve_self = function(self, t)
return map_type(self, t, resolve_self_fns)
end
end

do
local function add_interface_fields(self, fields, field_order, resolved, named, list)
for fname, ftype in fields_of(resolved, list) do
Expand All @@ -13176,7 +13207,7 @@ self:expand_type(node, values, elements) })
end
else
table.insert(field_order, fname)
fields[fname] = ftype
fields[fname] = resolve_self(self, ftype)
end
end
end
Expand Down
51 changes: 41 additions & 10 deletions tl.tl
Original file line number Diff line number Diff line change
Expand Up @@ -2254,8 +2254,8 @@ local macroexp a_map(w: Where, k: Type, v: Type): MapType
return a_type(w, "map", { keys = k, values = v } as MapType)
end

local function a_nominal(n: Node, names: {string}): NominalType
return a_type(n, "nominal", { names = names } as NominalType)
local macroexp a_nominal(w: Where, names: {string}): NominalType
return a_type(w, "nominal", { names = names } as NominalType)
end

local macroexp an_invalid(w: Where): InvalidType
Expand Down Expand Up @@ -2449,6 +2449,7 @@ end
local function new_generic(ps: ParseState, i: integer, typeargs: {TypeArgType}, typ: Type): GenericType
local gt = new_type(ps, i, "generic") as GenericType
gt.typeargs = typeargs
assert(not typ is TypeDeclType)
gt.t = typ
return gt
end
Expand Down Expand Up @@ -7469,6 +7470,8 @@ do
return t
end

assert(not t is TypeDeclType)

local gt = a_type(t, "generic", { t = t } as GenericType)
gt.typeargs = typeargs
return gt
Expand Down Expand Up @@ -7838,6 +7841,13 @@ do
copy.elements, same = resolve(t.elements, same)
end

if t.interface_list then
copy.interface_list = {}
for i, v in ipairs(t.interface_list) do
copy.interface_list[i], same = resolve(v, same)
end
end

copy.is_userdata = t.is_userdata

copy.fields = {}
Expand Down Expand Up @@ -8752,13 +8762,13 @@ do
end
end

function TypeChecker:type_of_self(w: Where): Type
function TypeChecker:type_of_self(w: Where): Type, TypeDeclType
local t = self:find_var_type("@self")
if not t then
return an_invalid(w)
return an_invalid(w), nil
end
assert(t is TypeDeclType)
return t.def
return t.def, t
end

-- ∃ x ∈ xs. t <: x
Expand Down Expand Up @@ -8932,7 +8942,7 @@ do
["*"] = {
["boolean_context"] = compare_true,
["self"] = function(self: TypeChecker, a: Type, b: SelfType): boolean, {Error}
return self:same_type(a, self:type_of_self(b))
return self:same_type(a, (self:type_of_self(b)))
end,
["typevar"] = function(self: TypeChecker, a: Type, b: TypeVarType): boolean, {Error}
return self:compare_or_infer_typevar(b.typevar, a, nil, self.same_type)
Expand Down Expand Up @@ -9287,7 +9297,7 @@ do
return ok, errs
end,
["self"] = function(self: TypeChecker, a: Type, b: SelfType): boolean, {Error}
return self:is_a(a, self:type_of_self(b))
return self:is_a(a, (self:type_of_self(b)))
end,
["tuple"] = function(self: TypeChecker, a: Type, b: Type): boolean, {Error}
return self:is_a(a_tuple(a, {a}), b)
Expand Down Expand Up @@ -10515,7 +10525,7 @@ do
end
end

local function typedecl_to_nominal(node: Node, name: string, t: TypeDeclType, resolved?: Type): Type
local function typedecl_to_nominal(w: Where, name: string, t: TypeDeclType, resolved?: Type): Type
local typevals: {Type}
local def = t.def
if def is GenericType then
Expand All @@ -10527,7 +10537,7 @@ do
} as TypeVarType))
end
end
local nom = a_nominal(node, { name })
local nom = a_nominal(w, { name })
nom.typevals = typevals
nom.found = t
nom.resolved = resolved
Expand Down Expand Up @@ -13166,6 +13176,27 @@ do
return t
end

local resolve_self: function(self: TypeChecker, t: Type): Type, {Error}
do
local resolve_self_fns: TypeFunctionMap<TypeChecker> = {
["self"] = function(tc: TypeChecker, t: SelfType): Type, boolean
local selftype, selfdecl = tc:type_of_self(t)
local checktype = selftype
if selftype is GenericType then
checktype = selftype.t
end
if checktype is RecordType then
return typedecl_to_nominal(t, checktype.declname, selfdecl)
end
return t
end,
}

resolve_self = function(self: TypeChecker, t: Type): Type, {Error}
return map_type(self, t, resolve_self_fns)
end
end

do
local function add_interface_fields(self: TypeChecker, fields: {string:Type}, field_order: {string}, resolved: RecordLikeType, named: NominalType, list?: MetaMode)
for fname, ftype in fields_of(resolved, list) do
Expand All @@ -13176,7 +13207,7 @@ do
end
else
table.insert(field_order, fname)
fields[fname] = ftype
fields[fname] = resolve_self(self, ftype)
end
end
end
Expand Down

0 comments on commit 365fbd3

Please sign in to comment.