From 57f1c61be8eacc950d7f3b040e459356b16a370c Mon Sep 17 00:00:00 2001 From: Hisham Muhammad Date: Sun, 18 Aug 2024 22:24:27 -0300 Subject: [PATCH] fix: self heuristic: ensure that non-nominal types are detected 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. --- spec/subtyping/self_spec.lua | 124 ++++++++++++++++++++++++++++++++++- tl.lua | 41 +++++++----- tl.tl | 41 +++++++----- 3 files changed, 171 insertions(+), 35 deletions(-) diff --git a/spec/subtyping/self_spec.lua b/spec/subtyping/self_spec.lua index bf9b2aa19..4da770d47 100644 --- a/spec/subtyping/self_spec.lua +++ b/spec/subtyping/self_spec.lua @@ -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 @@ -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) diff --git a/tl.lua b/tl.lua index 10b21718d..e61a173db 100644 --- a/tl.lua +++ b/tl.lua @@ -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 @@ -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 = { @@ -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 diff --git a/tl.tl b/tl.tl index 525a9ca64..063235a45 100644 --- a/tl.tl +++ b/tl.tl @@ -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 @@ -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 visit_type = { cbs = { @@ -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