Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client side construction of l:href and l:action #1134

Open
jamescheney opened this issue Apr 17, 2022 · 4 comments
Open

client side construction of l:href and l:action #1134

jamescheney opened this issue Apr 17, 2022 · 4 comments

Comments

@jamescheney
Copy link
Contributor

As mentioned in desugarLAttributes.ml (written around 7 years ago):

(* TODO:

   Either disallow l:href and l:action in client-side XML or, more
   usefully, provide proper support for sending client-side closures
   to the server.
*)

handling l:href and l:action in client-side XML is not thought to work. This issue was tripped over in other places (#219) where it originally seemed to be related to a problem with replaceNode, but that problem now seems to have been fixed by fixing client-side closure calls. The following example illustrates that desugaring of these attributes on the client side does not work properly:

mutual {
fun testPage(msg) {
  page
  <html>
    <body>
	<div>Message: {stringToXml(msg)}</div>
      <div id="t">
        <h4>HAHAH</h4>
      </div>
      <div id="p">
        <p>LALA</p>
      </div>
      <div id="x">
        <p>GAGA</p>
      </div>
      <a href="" l:onclick="{changeContent()}">Click me</a>
    </body>
  </html>
}



fun changeContent() client {
  var time = clientTime();
  replaceNode(
      <form l:onsubmit="{error("asdf")}" method="post">
        onSubmit handled OK
	<button type="submit">Submit!</button>
      </form>
    ,
    getNodeById("t")
  );
  replaceNode(<form l:action="{testPage(intToString(time))}" method="post">
        action handled OK at <b>{intToXml(time)}</b>
	<button type="submit">Submit!</button>
      </form>,getNodeById("p"));
  replaceNode(<a l:href="{testPage(intToString(time))}">href handled OK at <b>{intToXml(time)}</b></a>,getNodeById("x"))
}
}

fun main () {
  addRoute("",testPage);
  servePages()
}

main()

since if the l:action or l:href bodies are called (after first replacing the content so they appear in the web page) you get an error like this:

Links Error

***: Error: Cannot call client side function '_$ClosureTable.apply' because of before server page is ready

which I think is happening because the local functions constructed after are not marked server and so the client is sending a request to perform a client-side function to the server as a continuation, which does not work since the web page and (new) client environment isn't initialized yet.

However, by manually eta-expanding, lambda-lifting, and hoisting the computation of the first argument of replaceNode to top-level functions the desired behavior can be made to work:

mutual {
fun testPage(msg) {
  page
  <html>
    <body>
	<div>Message: {stringToXml(msg)}</div>
      <div id="t">
        <h4>HAHAH</h4>
      </div>
      <div id="p">
        <p>LALA</p>
      </div>
      <div id="x">
        <p>GAGA</p>
      </div>
      <a href="" l:onclick="{changeContent()}">Click me</a>
    </body>
  </html>
}

fun snippet1 (time)  server {
      <form l:action="{testPage(intToString(time))}" method="post">
        action handled OK at <b>{intToXml(time)}</b>
	<button type="submit">Submit!</button>
      </form>
}

fun snippet2 (time) server {
        <a l:href="{testPage(intToString(time))}">href handled OK at <b>{intToXml(time)}</b></a>
}

fun changeContent() client {
  var time = clientTime();
  replaceNode(
      <form l:onsubmit="{error("asdf")}" method="post">
        onSubmit handled OK
	<button type="submit">Submit!</button>
      </form>
    ,
    getNodeById("t")
  );
  replaceNode(snippet1(time),getNodeById("p"));
  replaceNode(snippet2(time),getNodeById("x"))
}
}

fun main () {
  addRoute("",testPage);
  servePages()
}

main()

Hoisting to the toplevel is necessary because currently in Links client and server annotations are only allowed on top-level functions, and there is no way to directly provide the local functions that gets created when l:href or l:action are desugared, but ensuring that they are desugared inside the body of a server function seems sufficient. However, it may be possible to do this more directly, by modifying desugarLAttributes.ml so that the local functions created during l: attribute desugaring inherit the server environment.

@jamescheney
Copy link
Contributor Author

As @slindley pointed out one perfectly rational reaction to this issue is to say let's just get rid of the l:attribute stuff (or at least, make it a run-time error to do the above strange things so that unwary users know that they are intentionally not supported.) I don't mind that but it would then be nice to port the examples and tutorial programs that use l:attributes to use something else, e.g. MVU. In my somewhat limited experience, the existing l:attribute stuff, albeit clunky, looks familiar enough to students already familiar with conventional web programming that they can get going fairly fast, even if there are nontrivial limitations on what you can do.

@jamescheney
Copy link
Contributor Author

Following #1133 we can now place client and server annotations on local functions, but this does not seem to suffice (or if it does, it is masked by another bug). Here is a simpler example that does not rely on non-local variable references in functions:

mutual {
fun testPage(msg) {
  page
  <html>
    <body>
	<div>Message: {stringToXml(msg)}</div>
      <div id="t">
        <h4>HAHAH</h4>
      </div>
      <div id="p">
        <p>LALA</p>
      </div>
      <div id="x">
        <p>GAGA</p>
      </div>
      <a href="" l:onclick="{changeContent()}">Click me</a>
    </body>
  </html>
}



fun snippet1()  server {
      <form l:action="{testPage(intToString(42))}" method="post">
        action handled OK at <b>{intToXml(42)}</b>
	<button type="submit">Submit!</button>
      </form>
}
fun snippet2 () server {
        <a l:href="{testPage(intToString(42))}">href handled OK at <b>{intToXml(42)}</b></a>
}
 
fun changeContent() client {
  replaceNode(
      <form l:onsubmit="{error("asdf")}" method="post">
        onSubmit handled OK
	<button type="submit">Submit!</button>
      </form>
    ,
    getNodeById("t")
  );
  replaceNode(snippet1(),getNodeById("p"));
  replaceNode(snippet2(),getNodeById("x"))
}
}

fun main () {
  addRoute("",testPage);
  servePages()
}

main()

This works correctly (after clicking on the link/button added by replaceNode, the value 42 appears).

However, if we move the snippet functions into changeContent the seemingly equivalent program

mutual {
fun testPage(msg) {
  page
  <html>
    <body>
	<div>Message: {stringToXml(msg)}</div>
      <div id="t">
        <h4>HAHAH</h4>
      </div>
      <div id="p">
        <p>LALA</p>
      </div>
      <div id="x">
        <p>GAGA</p>
      </div>
      <a href="" l:onclick="{changeContent()}">Click me</a>
    </body>
  </html>
}

fun changeContent() client {
  fun snippet1()  server {
      <form l:action="{testPage(intToString(42))}" method="post">
        action handled OK at <b>{intToXml(42)}</b>
	<button type="submit">Submit!</button>
      </form>
  }
  fun snippet2 () server {
        <a l:href="{testPage(intToString(42))}">href handled OK at <b>{intToXml(42)}</b></a>
  }
 
  replaceNode(
      <form l:onsubmit="{error("asdf")}" method="post">
        onSubmit handled OK
	<button type="submit">Submit!</button>
      </form>
    ,
    getNodeById("t")
  );
  replaceNode(snippet1(),getNodeById("p"));
  replaceNode(snippet2(),getNodeById("x"))
}
}

fun main () {
  addRoute("",testPage);
  servePages()
}

main()

yields the same "before server is ready" error.

@jamescheney
Copy link
Contributor Author

Hmm. I wonder if the problem may be something like this. In jslib, when a function call is encountered the following gets run:

links/lib/js/jslib.js

Lines 1675 to 1685 in 0605152

if (typeof value === "function") {
if (value.location === "server") {
return {
_serverFunc: value.func,
_env: value.environment,
};
}
const id = _$ClosureTable.add(value);
return {"_closureTable": id};
} else if ( // SL: HACK for sending XML to the server

so, when a function call is being sent to the server, any function not marked server gets treated as a client call.
I think this may be happening with the local functions introduced by l:attribute calls, which may be being placed somewhere different when the snippets are local functions vs. when they are top-level ones, since the local functions occur syntactically within a top-level client function, even if they are then nested inside a local function that's marked server. Presumably after closure conversion and lifting, these additional local functions become unmarked top-level functions. So perhaps explicitly adding server annotations to them in desugarLAttributes will fix this.

@jamescheney
Copy link
Contributor Author

Well, looking at desugarLAttributes it seems that the local functions created during desugaring for l:href and l:action are already being tagged as server functions, so there must be some other problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant