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/server annotations on local functions #907

Closed
jamescheney opened this issue Aug 1, 2020 · 4 comments
Closed

client/server annotations on local functions #907

jamescheney opened this issue Aug 1, 2020 · 4 comments

Comments

@jamescheney
Copy link
Contributor

jamescheney commented Aug 1, 2020

In concrete syntax, Links only allows toplevel functions to carry client or server annotations. Inner and anonymous functions are not allowed to have locations in Links source code, and the location component of the abstract syntax is initialized to loc_unknown for these.

Internally, there are a few places where functions with explicit locations are created by desugaring (e.g. in desugarLAttributes) and it seems that these do not cause problems.

This issue proposes that the concrete syntax be extended to allow client/server locations on any function. Relatedly, we could also support client and server blocks, which would simply be syntactic sugar for:

client {...} --> (fun () client {...})()
server {...} --> (fun () server {...})()

The implications of allowing client and server annotations anywhere are not clear. There may be places where we implicitly assume only top-level definitions have such annotations, which may result in wrong behavior if this is not the case. An initial experiment with this discussed in #906 resulted in an assertion failure in evalir, suggesting that in at least one place the existing code is not dealing with located local functions correctly. The non-working example is as follows:

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")
  );
  fun snippet1 () server {
      <form l:action="{testPage(intToString(time))}" method="post">
        action handled OK at <b>{intToXml(time)}</b>
	<button type="submit">Submit!</button>
      </form>
  }
  replaceNode(snippet1(),getNodeById("p"));
  fun snippet2() server {
    <a l:href="{testPage(intToString(time))}">href handled OK at <b>{intToXml(time)}</b></a>
  }
  replaceNode(snippet2(),getNodeById("x"))
}
}

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

main()

(Manually lambda-lifting this example and hoisting the server-functions to the toplevel resulted in correct behavior.)

@jamescheney
Copy link
Contributor Author

So after simply adding syntax, this seems to work in some cases, but not in general. In particular variants of the example in #219 do not work, encountering an assertion failure in evailr. A smaller example that seems to encounter the same problem is this:

fun testPage(msg) {
  fun foo(x) client { replaceNode(
      (fun () server {<p><b>{stringToXml(x)}</b>:<b>{intToXml(serverTime())}</b></p>})()
    , getNodeById("xyz"))
  }
  page
  <html>
    <body>
      <div>Message: {stringToXml(msg)}</div>
        <form l:onsubmit="{foo(x)}" method="post">
          <input type="text" l:name="x"/>
  	<button type="submit">Submit!</button>
        </form>
      <div id="xyz"/>
    </body>
  </html>
}

This creates a form that when submitted calls a client function that replaces another node with content provided by a server function that refers to the argument x to the client function. When run, if we submit the form we get a client-side error message:

Uncaught Error: Fatal error: call to server returned an error. Details: ***: Error: "Assert_failure core/evalir.ml:246:18" 
    remoteCallHandler http://localhost:8080/lib/jslib.js:899

If instead we manually lambda-lift and hoist the inner client and server functions to the top level everything works:

fun bar(x) server {
  <p><b>{stringToXml(x)}</b>:<b>{intToXml(serverTime())}</b></p>
}

fun foo(x) client {
  replaceNode(
    bar(x),
   getNodeById("xyz"))
}

fun testPage(msg) {
  page
  <html>
    <body>
      <div>Message: {stringToXml(msg)}</div>
        <form l:onsubmit="{foo(x)}" method="post">
          <input type="text" l:name="x"/>
  	<button type="submit">Submit!</button>
        </form>
      <div id="xyz"/>
    </body>
  </html>
}

Likewise, if we lambda-lift the anonymous server function in-place things also work:

fun testPage(msg) {
  fun foo(x) client { replaceNode(
      (fun (y) server {<p><b>{stringToXml(y)}</b>:<b>{intToXml(serverTime())}</b></p>})(x)
    , getNodeById("xyz"))
  }
  page
  <html>
    <body>
      <div>Message: {stringToXml(msg)}</div>
        <form l:onsubmit="{foo(x)}" method="post">
          <input type="text" l:name="x"/>
  	<button type="submit">Submit!</button>
        </form>
      <div id="xyz"/>
    </body>
  </html>
}

The assertion failure in evalir line 246 seems to be an unhandled case where it is expected that either we have both free variables and an environment, or neither free variables nor an environment, in a server call. So somewhere something things are getting out of sync.

@kwanghoon
Copy link
Contributor

So after simply adding syntax, this seems to work in some cases, but not in general. In particular variants of the example in #219 do not work, encountering an assertion failure in evailr. A smaller example that seems to encounter the same problem is this:

NOTE: the following code is a slightly revised JC's original example as 'fun () server' is changed into 'fun ()' with no location for making it runnable on Links 0.9.3.

fun testPage(msg) {
  fun foo(x) client { replaceNode(
      (fun () {<p><b>{stringToXml(x)}</b>:<b>{intToXml(serverTime())}</b></p>})()
    , getNodeById("xyz"))   
  }
  page
  <html>
    <body>
      <div>Message: {stringToXml(msg)}</div>
        <form l:onsubmit="{foo(x)}" method="post">
          <input type="text" l:name="x"/>
  	<button type="submit">Submit!</button>
        </form>
      <div id="xyz"/>
    </body>
  </html>
}

This creates a form that when submitted calls a client function that replaces another node with content provided by a server function that refers to the argument x to the client function. When run, if we submit the form we get a client-side error message:

Uncaught Error: Fatal error: call to server returned an error. Details: ***: Error: "Assert_failure core/evalir.ml:246:18" 
    remoteCallHandler http://localhost:8080/lib/jslib.js:899

The assertion failure in evalir line 246 seems to be an unhandled case where it is expected that either we have both free variables and an environment, or neither free variables nor an environment, in a server call. So somewhere something things are getting out of sync.

This (client-side) error above is actually caused by a (server-side) error in evalir.ml

let (_finfo, (xs, body), z, _location) = find_fun f in

where find_fun f throws a NotFound exception because it cannot find serverTime (Variable 144) in fun_defs. It is a primitive function that can be found in env.

Line 240-250 could be replaced by the following code

    | `FunctionPtr (f, fvs), ps -> (
      try 
        let (_finfo, (xs, body), z, _location) = find_fun f in
	let env =
	  match z, fvs with
	  | None, None            -> env
	  | Some z, Some fvs -> Value.Env.bind z (fvs, Scope.Local) env
	  | _, _ -> assert false in

	(* extend env with arguments *)
	let env = List.fold_right2 (fun x p -> Value.Env.bind x (p, Scope.Local)) xs ps env in
	computation_yielding env cont body 

      with NotFound s ->
            apply_cont cont env (Lib.apply_pfun_by_code f ps (Value.Env.request_data env)) )

The code suggested above is merely a combination with a PrimitiveFunction case of apply in evalir.ml:

| `PrimitiveFunction (n,None), args ->

    | `PrimitiveFunction (n,None), args ->      
          apply_cont cont env (Lib.apply_pfun n args (Value.Env.request_data env))

There is a need to clarify why the two variants, which are manually rewritten by JC, do work. The point is this. Requests with a global function (in fun_defs) work but those with a primitive function (that cannot be found there) do not work.

  • Global functions are values of type FunctionPtr while primitive functions are values of type PrimitiveFunction.

The example above does a request with a primitive function, serverTime, because the anonymous function that serverTime is inside is a client function (though the location is marked as Unknown). The two variants do requests with global functions.

In parse_remote_call of webif.ml, every remote call request from clients is made to be represented with FunctionPtr all the time. This makes the evalir (apply) confused, and so any primitive functions would be attempted to be searched in fun_defs in the FunctionPtr case of apply in evalir.ml.

let func =

In Line 41 of the parse_remote_call function (webif.ml)

| `Record [] -> `FunctionPtr (int_of_string fname, None)

can be changed as shown in the following code.

  let parse_remote_call (valenv, _, _) cgi_args =
    let fname = Utility.base64decode (assoc "__name" cgi_args) in
    let args = Utility.base64decode (assoc "__args" cgi_args) in
    (* Debug.print ("args: " ^ Value.show (Json.parse_json args)); *)
    let args = Value.untuple (U.Value.load (Yojson.Basic.from_string args)) in

    let fvs = U.Value.load (Yojson.Basic.from_string (Utility.base64decode (assoc "__env" cgi_args))) in

    let i_fname = int_of_string fname in
    
    let func =
      match fvs with
      | `Record [] -> if Lib.is_primitive_var i_fname
                      then `PrimitiveFunction (Lib.primitive_name i_fname, Some i_fname)
                      else `FunctionPtr (int_of_string fname, None)
      | _          -> `FunctionPtr (int_of_string fname, Some fvs) in
    RemoteCall(func, valenv, args)

By doing so, at least one problem is resolved. But another problem with not properly handling "l:action" and "l:href" seems to need a separate solution. Closures (in JavaScript) being created for handling "l:action" and "l:href" do not seem to be passed to the server properly. This is a totally different problem that should be resolve to have client/server annotations on local functions.

@jamescheney
Copy link
Contributor Author

Thanks for looking into this @kwanghoon ! I suggest we create a separate bug report with the breaking example, and then you can submit a PR to fix it. I'll do so now.

The original enhancement suggestion was to add support for client and server annotations to the surface language, but this is not strictly necessary to deal with #219. Links already supports such annotations on arbitrary functions in the frontend abstract syntax, there is just no way to write them in source code. However, by trying to enable this to see if we can fix #219 by a local translation (as described in the issue comments) I ran into problems like those you describe. So it may be that if we fix these problems, things will just work, or if not hopefully we will have a better idea of what is needed without the problem you've identified confusing matters.

@jamescheney
Copy link
Contributor Author

Closed by #1133, though the bug mentioned above still surfaces, will create a separate issue. #1134 considers fixing the problem mentioned above regarding handling l:href and l:action properly.

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

2 participants