Skip to content

Commit

Permalink
fix: self heuristic: ensure that non-nominal types are detected
Browse files Browse the repository at this point in the history
The heuristic was only comparing against nominal types; if
a non-nominal type such as `(self: integer)` was used, it was
silently accepted as a method and promoted to the `self` type.
  • Loading branch information
hishamhm committed Aug 19, 2024
1 parent 7d000b8 commit 57f1c61
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 35 deletions.
124 changes: 123 additions & 1 deletion spec/subtyping/self_spec.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
local util = require("spec.util")

describe("subtyping of self", function()
it("self type resolves from abstract interface to concrete records (#756)", util.check([[
it("self type resolves from abstract interface to concrete records, implicit self type by name (#756)", util.check([[
local interface SoundMaker
make_sound: function(self)
end
Expand Down Expand Up @@ -39,5 +39,127 @@ describe("subtyping of self", function()
thing:make_sound()
end
]]))

it("self type resolves from abstract interface to concrete records, explicit use of self type (#756)", util.check([[
local interface SoundMaker
make_sound: function(self: self)
end
local record Animal is SoundMaker
species: string
end
function Animal:create(species: string): Animal
return setmetatable({ species = species }, { __index = Animal })
end
function Animal:make_sound()
print("Animal sound")
end
local record Person is SoundMaker
name: string
end
function Person:create(name: string): Person
return setmetatable({ name = name }, { __index = Person })
end
function Person:make_sound()
print("Person sound")
end
local things: {SoundMaker} = {
Animal:create("Dog"),
Person:create("John")
}
for _, thing in ipairs(things) do
thing:make_sound()
end
]]))

it("self type resolves from abstract interface to concrete records, self type self-reference heuristic (#756)", util.check([[
local interface SoundMaker
make_sound: function(self: SoundMaker)
end
local record Animal is SoundMaker
species: string
end
function Animal:create(species: string): Animal
return setmetatable({ species = species }, { __index = Animal })
end
function Animal:make_sound()
print("Animal sound")
end
local record Person is SoundMaker
name: string
end
function Person:create(name: string): Person
return setmetatable({ name = name }, { __index = Person })
end
function Person:make_sound()
print("Person sound")
end
local things: {SoundMaker} = {
Animal:create("Dog"),
Person:create("John")
}
for _, thing in ipairs(things) do
thing:make_sound()
end
]]))

it("a self variable that is not a self-referential type has no special behavior", util.check_type_error([[
local interface SoundMaker
make_sound: function(self: integer)
end
local record Animal is SoundMaker
species: string
end
function Animal:create(species: string): Animal
return setmetatable({ species = species }, { __index = Animal })
end
function Animal:make_sound()
print("Animal sound")
end
local record Person is SoundMaker
name: string
end
function Person:create(name: string): Person
return setmetatable({ name = name }, { __index = Person })
end
function Person:make_sound()
print("Person sound")
end
local things: {SoundMaker} = {
Animal:create("Dog"),
Person:create("John")
}
for _, thing in ipairs(things) do
thing:make_sound()
end
]], {
{ y = 13, msg = "type signature of 'make_sound' does not match its declaration in Animal: argument 0: got Animal, expected integer" },
{ y = 25, msg = "type signature of 'make_sound' does not match its declaration in Person: argument 0: got Person, expected integer" },
{ y = 35, msg = "self: got SoundMaker, expected integer" },
}))

end)

41 changes: 24 additions & 17 deletions tl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3607,6 +3607,9 @@ do
local iok = parse_body(ps, i, ndef, nt)
if iok then
i = iok
if ndef.fields then
ndef.declname = v.tk
end
nt.newtype = new_typedecl(ps, itype, ndef)
end

Expand Down Expand Up @@ -12385,6 +12388,26 @@ self:expand_type(node, values, elements) })
end
end

local function ensure_is_method_self(typ, fargs)
assert(typ.declname)
local selfarg = fargs[1]
if not (selfarg.typename == "nominal") then
return false
end
if selfarg.names[1] ~= typ.declname or (typ.typeargs and not selfarg.typevals) then
return false
end
if typ.typeargs then
for j = 1, #typ.typeargs do
local tv = selfarg.typevals[j]
if not (tv and tv.typename == "typevar" and tv.typevar == typ.typeargs[j].typearg) then
return false
end
end
end
return true
end

local visit_type
visit_type = {
cbs = {
Expand Down Expand Up @@ -12433,23 +12456,7 @@ self:expand_type(node, values, elements) })
if ftype.is_method then
local fargs = ftype.args.tuple
if fargs[1] then
local record_name = typ.declname
if record_name then
local selfarg = fargs[1]
if selfarg.typename == "nominal" then
if selfarg.names[1] ~= record_name or (typ.typeargs and not selfarg.typevals) then
ftype.is_method = false
elseif typ.typeargs then
for j = 1, #typ.typeargs do
local tv = selfarg.typevals[j]
if not (tv and tv.typename == "typevar" and tv.typevar == typ.typeargs[j].typearg) then
ftype.is_method = false
break
end
end
end
end
end
ftype.is_method = ensure_is_method_self(typ, fargs)
if ftype.is_method then
fargs[1] = a_type(fargs[1], "self", {})
end
Expand Down
41 changes: 24 additions & 17 deletions tl.tl
Original file line number Diff line number Diff line change
Expand Up @@ -3607,6 +3607,9 @@ local function parse_nested_type(ps: ParseState, i: integer, def: RecordLikeType
local iok = parse_body(ps, i, ndef, nt)
if iok then
i = iok
if ndef is RecordLikeType then
ndef.declname = v.tk
end
nt.newtype = new_typedecl(ps, itype, ndef)
end

Expand Down Expand Up @@ -12385,6 +12388,26 @@ do
end
end

local function ensure_is_method_self(typ: RecordLikeType, fargs: {Type}): boolean
assert(typ.declname)
local selfarg = fargs[1]
if not selfarg is NominalType then
return false
end
if selfarg.names[1] ~= typ.declname or (typ.typeargs and not selfarg.typevals) then
return false
end
if typ.typeargs then
for j=1,#typ.typeargs do
local tv = selfarg.typevals[j]
if not (tv and tv is TypeVarType and tv.typevar == typ.typeargs[j].typearg) then
return false
end
end
end
return true
end

local visit_type: Visitor<TypeChecker, TypeName,Type,Type>
visit_type = {
cbs = {
Expand Down Expand Up @@ -12433,23 +12456,7 @@ do
if ftype.is_method then
local fargs = ftype.args.tuple
if fargs[1] then
local record_name = typ.declname
if record_name then
local selfarg = fargs[1]
if selfarg is NominalType then
if selfarg.names[1] ~= record_name or (typ.typeargs and not selfarg.typevals) then
ftype.is_method = false
elseif typ.typeargs then
for j=1,#typ.typeargs do
local tv = selfarg.typevals[j]
if not (tv and tv is TypeVarType and tv.typevar == typ.typeargs[j].typearg) then
ftype.is_method = false
break
end
end
end
end
end
ftype.is_method = ensure_is_method_self(typ, fargs)
if ftype.is_method then
fargs[1] = a_type(fargs[1], "self", {})
end
Expand Down

0 comments on commit 57f1c61

Please sign in to comment.