[Feature Request] Automatically overload with `__call` metamethod.
There is already custom behavior when __index is defined in a metatable, why not __call? You can manually define the overload, but then you have to define the signature twice.
---@overload fun(a: string, b: number): boolean
local foo = setmetatable({}, {
---@param a string
---@param b number
---@return boolean
__call = function(a, b)
return true;
end
});
This seems to be feature requested long ago: https://github.com/LuaLS/lua-language-server/issues/95
This issue seems difficult to resolve, and I think maintaining the current state is a good option. For example, simulating a 'class' often involves more complex sub-processing logic, such as dispatching to functions like 'init' or 'alloc' defined in subclasses.
Also I think the code snippet is a bit incorrect ๐
the 1st param of __call will always be the table it self?
i.e. foo(a, b) => __call(t=foo, a, b)
local foo = setmetatable({}, {
---@param a string
---@param b number
---@return boolean
__call = function(a, b)
print(a, b)
return true
end
});
print(foo)
foo(1, 2)
table: 0x7f8b47c09860
table: 0x7f8b47c09860 1
- the 1st param
ainside__callis actually thefootable
Thus the __call(t, a, b) signature is actually different from the actual call syntax foo(a, b) as seen by users
This adds another difficulty as well.
Thus the
__call(t, a, b)signature is actually different from the actual call syntaxfoo(a, b)as seen by users This adds another difficulty as well.
The signature for __call is different than the calling syntax but its the same as the call syntax for a table when you want to pass self as the first param.
TestClass:testFunction(1, 2)
In this case, testFunction would receive 3 parameters with the first one being a reference to the instance of TestClass.
And to be honest, most of the metamethods should be able to auto-detect the types coming in, even without them specified.
The signature for
__callis different than the calling syntax but its the same as the call syntax for a table when you want to pass self as the first param.
Yes, I know this __call metamethod as well as lua's : method call syntax. But I am saying that the function signature is different between the __call and the regular function call using that table, in the viewpoint of luals. ๐
Using your example TestClass:testFunction(a, b)
- I know it is identical to
TestClass.testFunction(TestClass, a, b). So in this case their function signature is the same, in which both of them accept3params:self, a, b. - However this is definitely different from
TestClass.testFunction(a, b)which only accepts2params:a, bwithoutself
So when we write a __call(t, a, b) in a metatable, certainly luals can detect the existence of such metamethod, but it cannot directly apply this function signature (fun(t, a, b)) to a t(a, b) call, since their function signatures are different. I guess this is where the difficulty lies in. ๐ค And thus requires a manual @overload fun(a, b) to table t
I want to contribute and actually get this working (along with some kind of ---@environment/proper setfenv support) but I wanted to possibly do it in the 4.0 branch though I cannot get it to build on macos
I wanted to possibly do it in the 4.0 branch though I cannot get it to build on macos
Thanks for your interest in contributing ๐.
AFAIK the 4.0 branch is a WIP and a total rewrite of LuaLS, but it is not ready yet. I don't think it is a good time to PR to that branch now even if it is buildable, as that may affect the design architecture that maintainer has planned.
I am sure maintainer will announce it when it is ready.
I wanted to come back here and get/share for info.
---@class Test
local test = {}
LuaLS will "autocast" self as the functions prefix expression ONLY IF it is specified as the first parameter.
Below, self has type of Test.
test.__newindex = function(self, k, v) end
If I name the first parameter of the function anything OTHER than self, LuaLS will not autocast. LuaLS will also not autocast any other parameter with in functions call signature as self, even if you name it that. Below, t is of type any. self, while colored differently then the other 2 parameters, is still of type any. This is a bit odd as Lua parameters are evaluated based on position.
Here, Lua assigns t as a reference to test.
test.__newindex = function(t, k, v) end
test.print = function(self, a, b) end
If I call __newindex as a "method", self is "autocast". This way I can create a function using one "signature"
test.__newindex = function(self, k, v)
LuaLS resolves the proper signature based on the calling method, regardless how it was originally "created".
test:__newindex(1, "key", "value")
Below, if I try to call test:__newindex with 3 params, LuaLS will warn me.
test:__newindex(1, "key", "value")
In this instance, LuaLS treats self on fun1 as any even tough its the first param provided to the prefixed function. But only when functions are created with the table.
---@class Test2 : table<number, boolean>
local test1 = {
fun1 = function(self, a, b) end,
__call = function(self, a, b)
return true;
end
}
setmetatable(test1, test1)
At this point, LuaLS shows fun1 asfunction fun1(self: any, a: any, b: any) -> boolean
test1:fun1(1, 2)
But does not return a warning about too many params being provided.
-- or if its defined like this
test1.fun1 = function(self, a, b) end
function test1.fun1(self, a, b) end
function test1:fun1(a, b) end
In each example, the functions execute the same, but LuaLS but LuaLS handles self and signatures different and very consistently across situations.
> So when we write a `__call(t, a, b)` in a metatable, certainly luals can detect the existence of such metamethod, but it cannot directly **apply** this function signature (`fun(t, a, b)`) to a `t(a, b)` call, since their function signatures are different. I guess this is where the difficulty lies in. ๐ค And thus requires a manual `@overload fun(a, b)` to table `t`
I am confused here. Why is test1.__call handled differently than say, test1.fun1?
local r = test1:fun1(1, 2)
In this instance, r == true
Another example:
local test2 = {}
setmetatable(test2, test2)
test2.__call = function(self, ...)
print(self, select("#",...), ...)
return true;
end
local r2 = test2(1, 2, 3, 4, 5)
r2 == true
This would be the resulting print.
table: 0X401bb480, 5, 1, 2, 3, 4, 5 to be printed.
In the sense of function signatures, metamethods (meta functions) should NOT be handled differently then functions. A function is a function, regardless of how its "created". If the getmetatable function is called on a class, then luaLS should treat functions on mt the same as "inheriting" another class.
---@class Test4
local test4 = {}
test4.this = true;
local mt = getmetatable(test1)
mt.__call = function(self, a, b)
return true;
end
In this case, mt.__call should have self resolved to Test4. If we modify the return of getmetatable LuaLS should treat functions added to mt as functions as also being added to Test4 (and any future classes that inherit.
The above example is the same as:
---@class Test5
local test5 = setmetatable({
this = true,
}, {
__call = function(self, a, b)
return true;
end
})
The same idea of we call setmetatable(test6, test6)
---@class Test6
local test6 = {}
setmetatable(test6, test6)
test6.__call = function(...)
return ...
end
In this case, self on __call resolves to Test6. Since setmetatable(test6, test6) is called, test6(true, 2, 1, true) should return test6, true, 2, 1, true
Basically, when functions are "created" in certain ways, LuaLS "evaluates" them differently and inconsistently. This should not be the case though.
A functions calling method, parameters, and return values (the "signature") are evaluated by Lua the same way regardless of how they are called later. LuaLS does this as well, sometimes and not for all functions (like some metamethods).
A function call in Lua has the following syntax:
functioncall ::= prefixexp argsIn a function call, first prefixexp and args are evaluated. If the value of prefixexp has type function, then this function is called with the given arguments. Otherwise, the prefixexp "call" metamethod is called, having as first parameter the value of prefixexp, followed by the original call arguments (see ยง2.8).
The form
functioncall ::= prefixexp `:ยด Name args`can be used to call "methods". A call v:name(args) is syntactic sugar for v.name(v,args), except that v is evaluated only once.
Arguments have the following syntax:
args ::= `(ยด [explist] `)ยด args ::= tableconstructor args ::= StringAll argument expressions are evaluated before the call. A call of the form f{fields} is syntactic sugar for f({fields}); that is, the argument list is a single new table. A call of the form f'string' (or f"string" or f[[string]]) is syntactic sugar for f('string'); that is, the argument list is a single literal string.
As an exception to the free-format syntax of Lua, you cannot put a line break before the '(' in a function call. This restriction avoids some ambiguities in the language. If you write
a = f (g).x(a)Lua would see that as a single statement, a = f(g).x(a). So, if you want two statements, you must add a semi-colon between them. If you actually want to call f, you must remove the line break before (g).
A call of the form return functioncall is called a tail call. Lua implements proper tail calls (or proper tail recursion): in a tail call, the called function reuses the stack entry of the calling function. Therefore, there is no limit on the number of nested tail calls that a program can execute. However, a tail call erases any debug information about the calling function. Note that a tail call only happens with a particular syntax, where the return has one single function call as argument; this syntax makes the calling function return exactly the returns of the called function. So, none of the following examples are tail calls:
return (f(x)) -- results adjusted to 1 return 2 * f(x) return x, f(x) -- additional results f(x); return -- results discarded return x or f(x) -- results adjusted to 12.5.9 โ Function Definitions The syntax for function definition is
function ::= function funcbody funcbody ::= `(ยด [parlist] `)ยด block endThe following syntactic sugar simplifies function definitions:
stat ::= function funcname funcbody stat ::= local function Name funcbody funcname ::= Name {`.ยด Name} [`:ยด Name]The statement
function f () body endtranslates to
f = function () body endThe statement
function t.a.b.c.f () body endtranslates to
t.a.b.c.f = function () body endThe statement
local function f () body endtranslates to
local f; f = function () body endnot to
local f = function () body end(This only makes a difference when the body of the function contains references to f.)A function definition is an executable expression, whose value has type function. When Lua pre-compiles a chunk, all its function bodies are pre-compiled too. Then, whenever Lua executes the function definition, the function is instantiated (or closed). This function instance (or closure) is the final value of the expression. Different instances of the same function can refer to different external local variables and can have different environment tables.
Parameters act as local variables that are initialized with the argument values:
parlist ::= namelist [`,ยด `...ยด] | `...ยดWhen a function is called, the list of arguments is adjusted to the length of the list of parameters, unless the function is a variadic or vararg function, which is indicated by three dots ('...') at the end of its parameter list. A vararg function does not adjust its argument list; instead, it collects all extra arguments and supplies them to the function through a vararg expression, which is also written as three dots. The value of this expression is a list of all actual extra arguments, similar to a function with multiple results. If a vararg expression is used inside another expression or in the middle of a list of expressions, then its return list is adjusted to one element. If the expression is used as the last element of a list of expressions, then no adjustment is made (unless that last expression is enclosed in parentheses).
As an example, consider the following definitions:
function f(a, b) end function g(a, b, ...) end function r() return 1,2,3 endThen, we have the following mapping from arguments to parameters and to the vararg expression:
CALL PARAMETERS f(3) a=3, b=nil f(3, 4) a=3, b=4 f(3, 4, 5) a=3, b=4 f(r(), 10) a=1, b=10 f(r()) a=1, b=2 g(3) a=3, b=nil, ... --> (nothing) g(3, 4) a=3, b=4, ... --> (nothing) g(3, 4, 5, 8) a=3, b=4, ... --> 5 8 g(5, r()) a=5, b=1, ... --> 2 3Results are returned using the return statement (see ยง2.4.4). If control reaches the end of a function without encountering a return statement, then the function returns with no results.
The colon syntax is used for defining methods, that is, functions that have an implicit extra parameter self. Thus, the statement
function t.a.b.c:f (params) body endis syntactic sugar fort.a.b.c.f = function (self, params) body end
Maybe I have been using the wrong term signature to express my viewpoint. ๐ By signature, what I actually mean is the param list and its order.
Just consider the following:
-
fun(self, a, b) -
fun(a, b) - do you think they are different?
Now consider the following
-
test.__call = function (self, a, b)=>fun(self, a, b) -
test = function (a, b)=>fun(a, b) - do you think they are different?
I fully understand that with a __call metamethod, we can make a test = {} table to be callable => test(a, b).
However, in the viewpoint of LuaLS, it would think that this call operation is calling a function with signature fun(a, b).
This is different from the function signature you defined in test.__call = function (self, a, b).
How can luals link the two?? ๐ณ
I just read through your whole reply.
If I understand correctly, you are talking / sharing another info related to how the param named self is auto inferred. ๐ค
But this seems unrelated to what I want to express: fun(a, b) is different from fun(self, a, b).
Whether the 1st param in a t.__call() can be auto inferred or not is unrelated here.
The key point is there exists an extra first param in the actual function signature.
---@class test1
local test1 = {}
test1.__call = function (t, a, b) --> `fun(t, a, b)`
return a + b
end
setmetatable(test1, test1)
test1(1, 2) --> in luals's view, this is calling `fun(a, b)`
-- different from `test1.call: fun(t, a, b)`
-- param list is different
-- param ordering is different also
-
test1.__callis having a signaturefun(t, a, b)(orfun(self, a, b), but that's not important) - if luals automatically overload
testwith this__callmetamethod, this will be equal to adding@overload fun(t, a, b)to thistest{}object - then this signature will certainly be different from how you actually call the
test{}table. because when we use it, we want it's signature to befun(a, b)๐ ๐
If I call
__newindexas a "method",selfis "autocast". This way I can create a function using one "signature"test.__newindex = function(self, k, v)LuaLS resolves the proper signature based on the calling method, regardless how it was originally "created".
test:__newindex(1, "key", "value")
Yes, your understanding and observation is correct ๐
- But the reason LuaLS can resolves the proper signature, is because you are using the colon call syntax
test:__newindex. If you write the code astest.__newindex(1, "key", "value"), then it will be wrong. - Similary when you have a
test.__call = function(self, a, b), it will only be correct if your write it astest:__call(a, b). But not when you writetest.__call(a, b)ortest(a, b).test(a, b)call syntax in the viewpoint of luals is just a function call liketest.__call, not a method call.
I am confused here. Why is
test1.__callhandled differently than say,test1.fun1?
No, I didn't say it will be handled differently? They are handled the same. The difference comes from how you call the function
- you will be calling
test1.__callby justtest(a, b)=>fun(a, b)- but the actual signature of
test.callisfun(t, a, b), so this is just unmatched - you will be getting incorrect param hint
- but the actual signature of
- for
test1.fun1you are usingtest1:fun1(a, b)==test1.fun1(test1, a, b)=>fun(self, a, b)- this matches the actual
test1.fun1signaturefun(self, a, b) - you can get correct param hint
- this matches the actual
In short, even if we auto overload the function signature of __call metamethod into the table, this signature is incorrect.
I don't know if I have made myself clear ๐