Skip to content

Commit

Permalink
fix: check fields using self in table literals
Browse files Browse the repository at this point in the history
Fixes #846.
  • Loading branch information
hishamhm committed Jan 11, 2025
1 parent 39d06ba commit e0fa687
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 37 deletions.
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# 0.24.3

2025-01-11

A minor bugfix release, thanks to feedback received upon the release
of 0.24.2.

This release features commits by Hisham Muhammad.

## What's New

### Fixes

* Fixes a regression in generic records with a __call metamethod (#901).
* Record fields declared with self type were not being properly type checked
in table literals (#846).

# 0.24.2

2025-01-10
Expand Down Expand Up @@ -347,7 +364,7 @@ and Hisham Muhammad.
2023-01-23

A minor bugfix release, thanks to feedback received upon the release
of 0.15.03
of 0.15.0.

This release features commits by Hisham Muhammad.

Expand Down
54 changes: 54 additions & 0 deletions spec/lang/inference/table_literal_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,58 @@ describe("bidirectional inference for table literals", function()
]], {
{ msg = 'in record field: item: got string "wtf", expected number' }
}))

it("resolves self type from records (regression test for #846)", util.check_type_error([[
local record Struct<T>
a: T
b: T
c: self
end
local a: Struct<integer> = {
a = 1,
b = 2,
c = 3,
}
print(a.a, a.b, a.c)
]], {
{ msg = 'in record field: c: got integer, expected Struct' }
}))

it("resolves self type in function fields (regression test for #846)", util.check_type_error([[
local record Struct<T>
a: T
b: T
c: function<T>(self)
end
local a: Struct<integer> = {
a = 1,
b = 2,
c = function(a: integer) end,
}
print(a.a, a.b, a.c)
]], {
{ msg = 'in record field: c: argument 1: got integer, expected Struct' }
}))

it("resolves self type from interfaces (regression test for #846)", util.check_type_error([[
local interface Struct<T>
a: T
b: T
c: self
end
local a: Struct<integer> = {
a = 1,
b = 2,
c = 3,
}
print(a.a, a.b, a.c)
]], {
{ msg = 'in record field: c: got integer, expected Struct' }
}))
end)
42 changes: 24 additions & 18 deletions tl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7388,6 +7388,7 @@ do






function TypeChecker:find_var(name, use)
Expand Down Expand Up @@ -7834,6 +7835,7 @@ do
copy.macroexp = t.macroexp
copy.min_arity = t.min_arity
copy.is_method = t.is_method
copy.is_record_function = t.is_record_function
copy.args, same = resolve(t.args, same)
copy.rets, same = resolve(t.rets, same)
elseif t.fields then
Expand Down Expand Up @@ -12171,6 +12173,13 @@ self:expand_type(node, values, elements) })
return infer_table_literal(self, node, children)
end

if decltype.fields then
self:begin_scope()
self:add_var(nil, "@self", a_type(node, "typedecl", { def = decltype }))
decltype = self:resolve_self(decltype, true)
self:end_scope()
end

local force_array = nil

local seen_keys = {}
Expand Down Expand Up @@ -13181,24 +13190,21 @@ 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,
}
function TypeChecker:resolve_self(t, resolve_interface)
local selftype, selfdecl = self:type_of_self(t)
local checktype = selftype
if selftype.typename == "generic" then
checktype = selftype.t
end

resolve_self = function(self, t)
return map_type(self, t, resolve_self_fns)
if (resolve_interface and checktype.typename == "interface") or checktype.typename == "record" then
return map_type(self, t, {
["self"] = function(_, typ)
return typedecl_to_nominal(typ, checktype.declname, selfdecl)
end,
})
else
return t
end
end

Expand All @@ -13215,7 +13221,7 @@ self:expand_type(node, values, elements) })
if ftype.typename == "typedecl" then
fields[fname] = ftype
else
fields[fname] = resolve_self(self, ftype)
fields[fname] = self:resolve_self(ftype)
end
end
end
Expand Down
42 changes: 24 additions & 18 deletions tl.tl
Original file line number Diff line number Diff line change
Expand Up @@ -7379,6 +7379,7 @@ do
type_check_funcall: function(TypeChecker, node: Node, a: Type, b: TupleType, argdelta?: integer): InvalidOrTupleType

expand_type: function(TypeChecker, w: Where, old: Type, new: Type): Type
resolve_self: function(TypeChecker, Type, resolve_interface?: boolean): Type, {Error}

get_rets: function(TupleType): TupleType
end
Expand Down Expand Up @@ -7834,6 +7835,7 @@ do
copy.macroexp = t.macroexp
copy.min_arity = t.min_arity
copy.is_method = t.is_method
copy.is_record_function = t.is_record_function
copy.args, same = resolve(t.args, same) as (TupleType, boolean)
copy.rets, same = resolve(t.rets, same) as (TupleType, boolean)
elseif t is RecordLikeType then
Expand Down Expand Up @@ -12171,6 +12173,13 @@ do
return infer_table_literal(self, node, children)
end

if decltype is RecordLikeType then
self:begin_scope()
self:add_var(nil, "@self", a_typedecl(node, decltype))
decltype = self:resolve_self(decltype, true)
self:end_scope()
end

local force_array: Type = nil

local seen_keys: {CheckableKey:Where} = {}
Expand Down Expand Up @@ -13181,24 +13190,21 @@ 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,
}
function TypeChecker:resolve_self(t: Type, resolve_interface?: boolean): Type, {Error}
local selftype, selfdecl = self:type_of_self(t)
local checktype = selftype
if selftype is GenericType then
checktype = selftype.t
end

resolve_self = function(self: TypeChecker, t: Type): Type, {Error}
return map_type(self, t, resolve_self_fns)
if (resolve_interface and checktype is InterfaceType) or checktype is RecordType then
return map_type(self, t, {
["self"] = function(_: TypeChecker, typ: SelfType): Type, boolean
return typedecl_to_nominal(typ, checktype.declname, selfdecl)
end,
})
else
return t
end
end

Expand All @@ -13215,7 +13221,7 @@ do
if ftype is TypeDeclType then
fields[fname] = ftype
else
fields[fname] = resolve_self(self, ftype)
fields[fname] = self:resolve_self(ftype)
end
end
end
Expand Down

0 comments on commit e0fa687

Please sign in to comment.