Skip to content

Commit

Permalink
generalize internal representation of generic types
Browse files Browse the repository at this point in the history
All types that have type variables are now represented as
a GenericType record, which holds a non-generic Type and
an array of type arguments.

This change is because originally we only cared about
generic records and generic functions, but once we have
the `local type MyType<T> = ...` syntax, other types
can be generic as well (in particular, unions).

Instead of replicating generic support logic in the
implementation of each type, we factor it out into a
type-level term which encapsulates the application of
type variables, which is something more like second-order
lambda calculus.

See https://en.wikipedia.org/wiki/System_F and the ensuing
rabbit hole.
  • Loading branch information
hishamhm committed Jan 2, 2025
1 parent 0a6294e commit b286e25
Show file tree
Hide file tree
Showing 5 changed files with 987 additions and 543 deletions.
2 changes: 1 addition & 1 deletion spec/cli/types_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ describe("tl types works like check", function()
assert.same({
["17"] = 7,
["20"] = 2,
["25"] = 10,
["25"] = 17,
["31"] = 9,
}, by_pos["2"])
end)
Expand Down
38 changes: 37 additions & 1 deletion spec/lang/call/generic_function_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,19 @@ describe("generic function", function()
{ y = 6, msg = "cannot infer declaration type; an explicit type annotation is necessary" },
}))

it("can use record type variable in record function", util.check_type_error([[
local record Container<T>
try_resolve: function(Container):T
end
function Container:resolve<U>():U
local t = self:try_resolve()
return t
end
]], {
{ y = 7, msg = "got T, expected U" },
}))

it("works when an annotation is given", util.check([[
local record Container
try_resolve: function<T>(Container):T
Expand Down Expand Up @@ -514,13 +527,36 @@ describe("generic function", function()
return #s
end
local pok1, pok2, msg = pcall2(pcall1, greet, "hello")
local pok1, pok2, msg: boolean, boolean, number = pcall2(pcall1, greet, "hello")
print(pok1)
print(pok2)
print(msg)
]]))

it("nested uses of generic functions using the same names for type variables don't cause conflicts", util.check_type_error([[
local function pcall1<A, B>(f: function(A):(B), a: A): boolean, B
return true, f(a)
end
local function pcall2<A, A2, B, B2>(f: function(A, A2):(B, B2), a: A, a2: A2): boolean, B, B2
return true, f(a, a2)
end
local function greet(s: string): number
print(s .. "!")
return #s
end
local pok1, pok2, msg: boolean, boolean, string = pcall2(pcall1, greet, "hello")
print(pok1)
print(pok2)
print(msg)
]], {
{ y = 14, msg = "argument 2: return 1: got number, expected string" }
}))

it("nested uses of generic record functions using the same names for type variables don't cause conflicts (#560)", util.check([[
local M = {}
Expand Down
34 changes: 34 additions & 0 deletions spec/lang/code_gen/local_type_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,40 @@ describe("local type code generation", function()
local lunchbox = L2.new({ "apple", "peach" })
]]))

it("alias for a type that shouldn't be elided, with function generics", util.gen([[
local type List2 = record<T>
new: function<U>(initialItems: {T}, u: U): List2<T>
end
function List2.new<Y>(initialItems: {T}, u: Y): List2<T>
end
local type Fruit2 = enum
"apple"
"peach"
"banana"
end
local type L2 = List2<Fruit2>
local lunchbox = L2.new({"apple", "peach"}, true)
]], [[
local List2 = {}
function List2.new(initialItems, u)
end
local L2 = List2
local lunchbox = L2.new({ "apple", "peach" }, true)
]]))

it("if alias shouldn't be elided, type shouldn't be elided either", util.gen([[
local type List = record<T>
new: function(initialItems: {T}): List<T>
Expand Down
Loading

0 comments on commit b286e25

Please sign in to comment.