Open jamescheney opened 2 years ago
Hello @jamescheney,
In Links, CPS-converted functions are thought to take its environment as their first argument.
When the example program calls a server function with a text "zzzzz", cgi_args is as follows:
__name: "MjQwMQ==" ===> 2401
__args: "eyIxIjp7IjIzOTYiOiJ6enp6eiJ9LCIyIjoieHl6enkifQ==" ===> {"1":{"2396":"zzzzz"},"2":"xyzzy"}
__env: "e30=" ===> {}
__client_id: "Y2lkXzA=" ===> cid_0
A quick workaround is to change webif.ml
https://github.com/links-lang/links/blob/474137f31a43d7589d2711968d8a564bb14c173f/core/webif.ml#L44
let func,args =
match fvs with
| `Record [] -> let i_fname = int_of_string fname in
if Lib.is_primitive_var i_fname
then `PrimitiveFunction (Lib.primitive_name i_fname, Some i_fname),args
else match hd args with
| `Record _ -> `FunctionPtr (int_of_string fname, Some (hd args)), tl args
| _ -> `FunctionPtr (int_of_string fname, None), args
| _ -> `FunctionPtr (int_of_string fname, Some fvs), args
in
RemoteCall (func, valenv, args)
Thanks. As I understand it, thanks to closure conversion the environment part of the call is supposed to always be empty (there is even a comment in jslib suggesting to get rid of it). So like my workaround mentioned above this seems to fix the symptom but not the root cause of the problem.
I think the issue is that after closure conversion, when we call the server from the client we look up information about the function's arguments and closure variable (if any) using find_fun, and this information is inconsistent with the closure converted version: a closure variable is present, but the environment is being passed as an ordinary argument. My workaround handles that case by patching the argument list with the closure variable if there is one but no environment was passed.
I think the right way to do this is fix the dictionary used by fun_info so that after closure conversion is finished, the closure variable are all added to the argument list, but haven't had the time to look at what is currently done to see if this actually makes sense.
Thanks for the explanation.
One thing still strange to me is that in the web interface, only FunctionPtr with None as the second argument is created if the environment part of the remote call is supposed to always be empty (i.e., Record []), whatever the closure variable (z) is. I don't know what I am missing.
It seems in the server-side IR, after closure conversion things are still represented as "closures", but the function bodies refer to the environment through the closure variable defined in the functions fun_def entry. Thus, the IR code isn't changed so that the IR manages the environment itself, instead whenever a closure is created the current environment is stored in it and when a call happens, that environment is passed in bound to the closure variable, if any. However, when compiling code to the client for server calls we can't do this because we are generating really-closed JS functions.
It looks like Record [] is being used to encode NONE, since environments / closure variables only get created if the function had some local variable references.
I suspect a reason it'd be annoying to fully closure-convert the IR code (i.e. make the closure variable a normal parameter, and rewrite calls/closure creation to manage the environment using IR code rather than having the interpreter do it) is that IR typing would become more complicated: functions with the same input/output types but different environments would have different types. This problem can be solved by adding existential types so that the type of a closure-converted function can be something like "exists Env. ((Env,t1,...,tn) -> t , Env)" but we currently don't have this even in the IR. That being the case, since the mismatch seems to arise at the point where we create a FunctionPtr for a server-side call decoded from a client request, I think my suggestion is: when the server-side function has a closure variable, but the environment provided is empty, pop off the first parameter and treat it as the environment. My fix does the same thing but in a much more mysterious way (and one which, if there were other bugs leading to mismatches between the function info and optional closure in server side code, would potentially lead to other hard-to-debug situations).
Having looked at some of the generated IR to see how the above two examples differ, I came up with a smaller example that seems to result in the same problem. The following code works:
mutual {
fun testPage(msg) {
page
<html>
<body>
<div>Message: {stringToXml(msg)}</div>
<div id="x">
<p>GAGA</p>
</div>
<a href="" l:onclick="{changeContent()}">Click me</a>
</body>
</html>
}
fun snippet (time,testPage) server {
<a l:href="{testPage(intToString(time))}">href handled OK</a>
}
fun changeContent() client {
var time = clientTime();
replaceNode(snippet(time,testPage),getNodeById("x"))
}
}
fun main () {
addRoute("",testPage);
servePages()
}
main()
Here, I've modified snippet
to get rid of the time but also be completely closed, i.e. not refer to the other functions in the same mutual block.
This version where snippet
is lifted out of the mutual block however fails with the same error as above:
fun snippet (testPage) server {
<a l:href="{testPage("42")}">href handled OK</a>
}
mutual {
fun testPage(msg) {
page
<html>
<body>
<div>Message: {stringToXml(msg)}</div>
<div id="x">
<p>GAGA</p>
</div>
<a href="" l:onclick="{changeContent()}">Click me</a>
</body>
</html>
}
fun changeContent() client {
replaceNode(snippet(testPage),getNodeById("x"))
}
}
fun main () {
addRoute("",testPage);
servePages()
}
main()
I think what may be happening is that for some reason, the call to testPage
in the second version is being attempted as a client call, which is of course silly and unnecessary. But even by annotating testPage
as server
in the second version the same error still occurs, which suggests that somewhere a client closure is being incorrectly created.
Hmm. So changing the working version to:
mutual {
fun testPage(msg) {
page
<html>
<body>
<div>Message: {stringToXml(msg)}</div>
<div id="x">
<p>GAGA</p>
</div>
<a href="" l:onclick="{changeContent()}">Click me</a>
</body>
</html>
}
fun snippet (time,f) server {
<a l:href="{f(intToString(time))}">href handled OK</a>
}
fun changeContent() client {
var time = clientTime();
replaceNode(snippet(time,testPage),getNodeById("x"))
}
}
fun main () {
addRoute("",testPage);
servePages()
}
main()
makes it fail again. So it seems that there may be another issue, relating to scoping/resolution of names in mutual blocks.
This issue seems to have been worked around by #1138. I think it should be closed, and another issue collecting ideas for a more principled design created.
Some of the examples in this page still encounter the "Error: Cannot call client side function '_$ClosureTable.apply' because of before server page is ready" problem also covered by #1134, but I think that is a separate problem - the examples there illustrate that the problem can occur with local server-annotated functions that are closed.
Thanks to #1133 it is now possible to write local / anonymous functions that have client or server annotations. Such functions need not be closed, for example:
However, running this and clicking the Submit button yields (I think) the same error @kwanghoon reported in a comment on #907, adjusting for changes:
This error appears in the browser console, but relays an error raised on the server.
Removing the
server
label from the anonymous function yields correct behavior, presumably because the function is called on the client and it is closure converted correctly there.Replacing
foo
with the following:so that the anonymous server function is lambda-lifted in place, yields correct behavior.
The error is being raised here: https://github.com/links-lang/links/blob/474137f31a43d7589d2711968d8a564bb14c173f/core/evalir.ml#L252
which is an "unreachable" case of a pattern match that tries to match up the closure variable and environment. Evidently these are getting out of sync somehow, possibly due to an unhandled case or no-longer-guaranteed invariant in closure conversion. Through random hacking I found that the change in commit 8cee258 "fixes" this problem. But this is almost certainly not the right way to fix it, the right way is to make sure that the function info and environment/argument list do not get out of sync in the first place.