Skip to content

Commit

Permalink
improve inference of nested emptytables
Browse files Browse the repository at this point in the history
See #732 (comment)

> There is one remaining tricky error triggering in the day12 for which I
> already found a workaround (but which would merit a nicer solution since this
> uncovered the fact that my empty-table inference does not backpropagate
> correctly in the case of map[c1][c2] = true). I'll try to code a solution
> quickly, otherwise I'll document the edge case in the testsuite and go with
> the workaround.

This is the workaround, and the documentation of the edge case is in the
test case included in this commit.
  • Loading branch information
hishamhm committed Oct 7, 2024
1 parent 130f089 commit b142e3f
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 14 deletions.
46 changes: 46 additions & 0 deletions spec/inference/emptytable_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,50 @@ describe("empty table without type annotation", function()
print(x)
end
]]))

it("does not fail when resolving nested emptytables", util.check([[
local function parse_input() : {string: {string: boolean}}
local m = {}
for line in io.lines("input.txt") do
local c1, c2 = line:match("^(%w+)-(%w+)$")
if not m[c1] then m[c1] = {} end
if not m[c2] then m[c2] = {} end
-- Summary of the emptytable propagation (desired) behavior:
local x = m[c1]
-- infer x to { typename = "unresolved_emptytable_value", emptytable_type = m, keys = STRING }
local y = x[c2]
-- here we want to:
-- declare a new_emptytable
-- infer m to { typename = "map", keys = STRING, values = new_emptytable }
-- infer y to { typename = "unresolved_emptytable_value", emptytable_type = new_emptytable, keys = "string" }
y = true
-- here we want to:
-- infer y to boolean
-- infer emptytable_type to { typename = "map", keys = "string", values = "boolean" }
-- by propagation, infer m to { typename = "map", keys = STRING, values = { typename = "map", keys = "string", values = "boolean" } }
-- FIXME: this is not propagating backwards correctly (probably because table objects are copied)
-- same thing as the above, but written in the
-- idiomatic style as it first appeared in @catwell's code:
m[c1][c2] = true
m[c2][c1] = true
end
return m
end
]]))

it("does not fail when resolving nested emptytables, three levels deep", util.check([[
local function f(a: string, b: string, c: string) : {string: {string: {string: boolean}}}
local m = {}
if not m[a] then m[a] = {} end
if not m[a][b] then m[a][b] = {} end
m[a][b][c] = true
return m
end
]]))

end)
26 changes: 19 additions & 7 deletions tl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8462,6 +8462,18 @@ do
return compare_true_inferring_emptytable(self, a, b)
end

local function infer_emptytable_from_unresolved_value(self, w, u, values)
local et = u.emptytable_type
assert(et.typename == "emptytable", u.typename)
local keys = et.keys
if not (values.typename == "emptytable" or values.typename == "unresolved_emptytable_value") then
local infer_to = is_numeric_type(keys) and
a_type(w, "array", { elements = values }) or
a_type(w, "map", { keys = keys, values = values })
self:infer_emptytable(et, self:infer_at(w, infer_to))
end
end


local emptytable_relations = {
["array"] = compare_true,
Expand Down Expand Up @@ -8908,13 +8920,7 @@ a.types[i], b.types[i]), }
return false, { Err("assigning %s to a variable declared with {}", a) }
end,
["unresolved_emptytable_value"] = function(self, a, b)
local bt = b.emptytable_type
assert(bt.typename == "emptytable", b.typename)
local bkeys = bt.keys
local infer_to = is_numeric_type(bkeys) and
a_type(b, "array", { elements = a }) or
a_type(b, "map", { keys = bkeys, values = a })
self:infer_emptytable(bt, self:infer_at(b, infer_to))
infer_emptytable_from_unresolved_value(self, b, b, a)
return true
end,
["self"] = function(self, a, b)
Expand Down Expand Up @@ -9911,6 +9917,12 @@ a.types[i], b.types[i]), }
end

errm, erra, errb = "inconsistent index type: got %s, expected %s" .. inferred_msg(ra.keys, "type of keys "), b, ra.keys
elseif ra.typename == "unresolved_emptytable_value" then
local et = a_type(ra, "emptytable", { keys = b })
infer_emptytable_from_unresolved_value(self, a, ra, et)
return a_type(anode, "unresolved_emptytable_value", {
emptytable_type = et,
})
elseif ra.typename == "map" then
if self:is_a(b, ra.keys) then
return ra.values
Expand Down
26 changes: 19 additions & 7 deletions tl.tl
Original file line number Diff line number Diff line change
Expand Up @@ -8462,6 +8462,18 @@ do
return compare_true_inferring_emptytable(self, a, b)
end

local function infer_emptytable_from_unresolved_value(self: TypeChecker, w: Where, u: UnresolvedEmptyTableValueType, values: Type)
local et = u.emptytable_type
assert(et is EmptyTableType, u.typename)
local keys = et.keys
if not (values is EmptyTableType or values is UnresolvedEmptyTableValueType) then
local infer_to = keys is NumericType -- ideally integer only
and an_array(w, values)
or a_map(w, keys, values)
self:infer_emptytable(et, self:infer_at(w, infer_to))
end
end

-- emptytable rules are the same in eqtype_relations and subtype_relations
local emptytable_relations: {TypeName:CompareTypes} = {
["array"] = compare_true,
Expand Down Expand Up @@ -8908,13 +8920,7 @@ do
return false, { Err("assigning %s to a variable declared with {}", a) }
end,
["unresolved_emptytable_value"] = function(self: TypeChecker, a: Type, b: UnresolvedEmptyTableValueType): boolean, {Error}
local bt = b.emptytable_type
assert(bt is EmptyTableType, b.typename)
local bkeys = bt.keys
local infer_to = bkeys is NumericType -- ideally integer only
and an_array(b, a)
or a_map(b, bkeys, a)
self:infer_emptytable(bt, self:infer_at(b, infer_to))
infer_emptytable_from_unresolved_value(self, b, b, a)
return true
end,
["self"] = function(self: TypeChecker, a: Type, b: SelfType): boolean, {Error}
Expand Down Expand Up @@ -9911,6 +9917,12 @@ do
end

errm, erra, errb = "inconsistent index type: got %s, expected %s" .. inferred_msg(ra.keys, "type of keys "), b, ra.keys
elseif ra is UnresolvedEmptyTableValueType then
local et = a_type(ra, "emptytable", { keys = b } as EmptyTableType)
infer_emptytable_from_unresolved_value(self, a, ra, et)
return a_type(anode, "unresolved_emptytable_value", {
emptytable_type = et
} as UnresolvedEmptyTableValueType)
elseif ra is MapType then
if self:is_a(b, ra.keys) then
return ra.values
Expand Down

0 comments on commit b142e3f

Please sign in to comment.