Skip to content

Commit

Permalink
fix: do not infer type variables as boolean in boolean contexts
Browse files Browse the repository at this point in the history
Introduces a special internal type, to be used only as the node.expected
type in boolean contexts such as `if _ then`. It behaves exactly like
boolean except that type variables do not infer to it.

See #768.
  • Loading branch information
hishamhm committed Aug 26, 2024
1 parent 7374a42 commit e19725b
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 11 deletions.
36 changes: 32 additions & 4 deletions tl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,7 @@ end






local table_types = {
Expand Down Expand Up @@ -1569,6 +1570,7 @@ local table_types = {
["unresolved_typearg"] = false,
["unresolvable_typearg"] = false,
["circular_require"] = false,
["boolean_context"] = false,
["tuple"] = false,
["poly"] = false,
["any"] = false,
Expand Down Expand Up @@ -1891,6 +1893,20 @@ end




















Expand Down Expand Up @@ -5630,6 +5646,7 @@ local typename_to_typecode = {
["union"] = tl.typecodes.UNION,
["nominal"] = tl.typecodes.NOMINAL,
["circular_require"] = tl.typecodes.NOMINAL,
["boolean_context"] = tl.typecodes.BOOLEAN,
["emptytable"] = tl.typecodes.EMPTY_TABLE,
["unresolved_emptytable_value"] = tl.typecodes.EMPTY_TABLE,
["poly"] = tl.typecodes.POLY,
Expand Down Expand Up @@ -6688,6 +6705,8 @@ local function show_type_base(t, short, seen)
return "<any type>"
elseif t.typename == "nil" then
return "nil"
elseif t.typename == "boolean_context" then
return "boolean"
elseif t.typename == "none" then
return ""
elseif t.typename == "typealias" then
Expand Down Expand Up @@ -8350,7 +8369,11 @@ do
return self:same_type(self:type_of_self(a), b)
end,
},
["boolean_context"] = {
["boolean"] = compare_true,
},
["*"] = {
["boolean_context"] = compare_true,
["self"] = function(self, a, b)
return self:same_type(a, self:type_of_self(b))
end,
Expand Down Expand Up @@ -8673,8 +8696,12 @@ a.types[i], b.types[i]), }
end
end,
},
["boolean_context"] = {
["boolean"] = compare_true,
},
["*"] = {
["any"] = compare_true,
["boolean_context"] = compare_true,
["self"] = function(self, a, b)
return self:is_a(a, self:type_of_self(b))
end,
Expand Down Expand Up @@ -8712,6 +8739,7 @@ a.types[i], b.types[i]), }
["typevar"] = 3,
["nil"] = 4,
["any"] = 5,
["boolean_context"] = 5,
["union"] = 6,
["poly"] = 7,

Expand Down Expand Up @@ -11044,7 +11072,7 @@ self:expand_type(node, values, elements) })
self:infer_negation_of_if_blocks(node, node.if_parent, node.if_block_n - 1)
end
if node.exp then
node.exp.expected = a_type(node, "boolean", {})
node.exp.expected = a_type(node, "boolean_context", {})
end
end,
before_statements = function(self, node)
Expand All @@ -11066,7 +11094,7 @@ self:expand_type(node, values, elements) })
before = function(self, node)

self:widen_all_unions(node)
node.exp.expected = a_type(node, "boolean", {})
node.exp.expected = a_type(node, "boolean_context", {})
end,
before_statements = function(self, node)
self:begin_scope(node)
Expand Down Expand Up @@ -11130,7 +11158,7 @@ self:expand_type(node, values, elements) })
before = function(self, node)

self:widen_all_unions(node)
node.exp.expected = a_type(node, "boolean", {})
node.exp.expected = a_type(node, "boolean_context", {})
end,

after = end_scope_and_none_type,
Expand Down Expand Up @@ -12001,7 +12029,7 @@ self:expand_type(node, values, elements) })
t = drop_constant_value(t)
end

if expected and expected.typename == "boolean" then
if expected and expected.typename == "boolean_context" then
t = a_type(node, "boolean", {})
end
end
Expand Down
42 changes: 35 additions & 7 deletions tl.tl
Original file line number Diff line number Diff line change
Expand Up @@ -1532,11 +1532,12 @@ local enum TypeName
"unresolved_typearg"
"unresolvable_typearg"
"circular_require"
"boolean_context"
"tuple"
"poly" -- intersection types, currently restricted to polymorphic functions defined inside records
"poly"
"any"
"unknown" -- to be used in lax mode only
"invalid" -- producing a new value of this type (not propagating) must always produce a type error
"unknown"
"invalid"
"none"
"*"
end
Expand Down Expand Up @@ -1569,6 +1570,7 @@ local table_types <total>: {TypeName:boolean} = {
["unresolved_typearg"] = false,
["unresolvable_typearg"] = false,
["circular_require"] = false,
["boolean_context"] = false,
["tuple"] = false,
["poly"] = false,
["any"] = false,
Expand Down Expand Up @@ -1617,6 +1619,14 @@ local record BooleanType
where self.typename == "boolean"
end

-- This is a special internal type, to be used only as the node.expected
-- type in boolean contexts such as `if _ then`. It behaves exactly like
-- boolean except that type variables do not infer to it.
local record BooleanContextType
is Type
where self.typename == "boolean_context"
end

local interface HasTypeArgs
is Type
where self.typeargs
Expand Down Expand Up @@ -1717,11 +1727,15 @@ local record InterfaceType
where self.typename == "interface"
end

-- producing a new value of this type (not propagating)
-- must always produce a type error
local record InvalidType
is Type
where self.typename == "invalid"
end

-- To be used in lax mode only:
-- this represents non-annotated types in .lua files.
local record UnknownType
is Type
where self.typename == "unknown"
Expand Down Expand Up @@ -1819,6 +1833,8 @@ local record TupleTableType
where self.typename == "tupletable"
end

-- Intersection types, currently restricted to polymorphic functions
-- defined inside records, representing polymorphic Lua APIs.
local record PolyType
is AggregateType
where self.typename == "poly"
Expand Down Expand Up @@ -5630,6 +5646,7 @@ local typename_to_typecode <total>: {TypeName:integer} = {
["union"] = tl.typecodes.UNION,
["nominal"] = tl.typecodes.NOMINAL,
["circular_require"] = tl.typecodes.NOMINAL,
["boolean_context"] = tl.typecodes.BOOLEAN,
["emptytable"] = tl.typecodes.EMPTY_TABLE,
["unresolved_emptytable_value"] = tl.typecodes.EMPTY_TABLE,
["poly"] = tl.typecodes.POLY,
Expand Down Expand Up @@ -6688,6 +6705,8 @@ local function show_type_base(t: Type, short: boolean, seen: {Type:string}): str
return "<any type>"
elseif t.typename == "nil" then
return "nil"
elseif t.typename == "boolean_context" then
return "boolean"
elseif t.typename == "none" then
return ""
elseif t is TypeAliasType then
Expand Down Expand Up @@ -8350,7 +8369,11 @@ do
return self:same_type(self:type_of_self(a), b)
end,
},
["boolean_context"] = {
["boolean"] = compare_true,
},
["*"] = {
["boolean_context"] = compare_true,
["self"] = function(self: TypeChecker, a: Type, b: SelfType): boolean, {Error}
return self:same_type(a, self:type_of_self(b))
end,
Expand Down Expand Up @@ -8673,8 +8696,12 @@ do
end
end,
},
["boolean_context"] = {
["boolean"] = compare_true,
},
["*"] = {
["any"] = compare_true,
["boolean_context"] = compare_true,
["self"] = function(self: TypeChecker, a: Type, b: SelfType): boolean, {Error}
return self:is_a(a, self:type_of_self(b))
end,
Expand Down Expand Up @@ -8712,6 +8739,7 @@ do
["typevar"] = 3,
["nil"] = 4,
["any"] = 5,
["boolean_context"] = 5,
["union"] = 6,
["poly"] = 7,
-- then typeargs
Expand Down Expand Up @@ -11044,7 +11072,7 @@ do
self:infer_negation_of_if_blocks(node, node.if_parent, node.if_block_n - 1)
end
if node.exp then
node.exp.expected = a_type(node, "boolean", {})
node.exp.expected = a_type(node, "boolean_context", {})
end
end,
before_statements = function(self: TypeChecker, node: Node)
Expand All @@ -11066,7 +11094,7 @@ do
before = function(self: TypeChecker, node: Node)
-- widen all narrowed variables because we don't calculate a fixpoint yet
self:widen_all_unions(node)
node.exp.expected = a_type(node, "boolean", {})
node.exp.expected = a_type(node, "boolean_context", {})
end,
before_statements = function(self: TypeChecker, node: Node)
self:begin_scope(node)
Expand Down Expand Up @@ -11130,7 +11158,7 @@ do
before = function(self: TypeChecker, node: Node)
-- widen all narrowed variables because we don't calculate a fixpoint yet
self:widen_all_unions(node)
node.exp.expected = a_type(node, "boolean", {})
node.exp.expected = a_type(node, "boolean_context", {})
end,
-- only end scope after checking `until`, `statements` in repeat body has is_repeat == true
after = end_scope_and_none_type,
Expand Down Expand Up @@ -12001,7 +12029,7 @@ do
t = drop_constant_value(t)
end

if expected and expected is BooleanType then
if expected and expected is BooleanContextType then
t = a_type(node, "boolean", {})
end
end
Expand Down

0 comments on commit e19725b

Please sign in to comment.