From b142e3fdd7236bb16ab36aee27f693cf688099e0 Mon Sep 17 00:00:00 2001 From: Hisham Muhammad Date: Wed, 18 Sep 2024 18:14:29 -0300 Subject: [PATCH] improve inference of nested emptytables See https://github.com/teal-language/tl/pull/732#issuecomment-2359380390 > 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. --- spec/inference/emptytable_spec.lua | 46 ++++++++++++++++++++++++++++++ tl.lua | 26 ++++++++++++----- tl.tl | 26 ++++++++++++----- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/spec/inference/emptytable_spec.lua b/spec/inference/emptytable_spec.lua index 2781a9d08..906d6c7f4 100644 --- a/spec/inference/emptytable_spec.lua +++ b/spec/inference/emptytable_spec.lua @@ -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) diff --git a/tl.lua b/tl.lua index f3da48189..044dcfbfd 100644 --- a/tl.lua +++ b/tl.lua @@ -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, @@ -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) @@ -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 diff --git a/tl.tl b/tl.tl index 7ba2888b2..dad8de5ab 100644 --- a/tl.tl +++ b/tl.tl @@ -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, @@ -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} @@ -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