Skip to content

How does Flatscript translate synchronous syntax tree into asynchronous codes

Neuron Teckid edited this page Jan 27, 2018 · 5 revisions

This post is an explanation about how regular asynchronous calls are handled by the compiler.

Let's start with code-generating with the syntax tree. Suppose we have the following code as a statement

x: f(p, q) + y

It will be parsed like this

  define
 /      \
x        +
        / \
     CALL  y
      /  \
     f   (p, q)

Generally this tree is traversed and then the target code is generated and be added to its parent. Yes, don't forget that. Any statement has its parent, the scope. Therefore, the code generating sequence for this tree is like this

      +-<1> a reference to "f"
      |-<2> a reference to "p"
      |-<3> a reference to "q"
    +-<4> a call where callee is <1> and arguments are [<2>, <3>]
    |-<5> a reference to "y"
  +-<6> a binary + where left is <4> and right is <5>
+-<7> define a name "x" where its initial value is <6>
<8> add <7> to the scope

And the target syntax tree, with those number annotated

    scope
    |         <7>
<8> +---- define
    |    /      \
    |   x        + <6>
    :           / \
    :    <4> CALL  y <5>
    :         /  \
         <1> f   (p, q)
                <2>  <3>

Since this is an ordinary case the target tree looks exactly like the source tree and it doesn't seem to be interesting at all. So let's talk about the regular asynchronous call in Flatscript.

Suppose this is the code, with the second argument replaced by the regular asynchronous placeholder.

x: f(p, %%) + y

  define
 /      \
x        +
        / \
    ASYNC  y
     CALL
     /  \
    f   (q, %%)

After <1> and <2>, the Flatscript compiler will do something completely different. As we know the placeholder %% is supposed to be replaced by a callback function, whose second parameter is what to be added to y. So the compiler will first generate such callback function and replace the call with the parameter in the syntax tree.

The intermediate result looks like this

  define            ASYNC
 /      \            CALL
x        +           /  \
        / \         f   (q, function ($error, $result) {???})
  $result  y                                   :
     .:.                                       :
      :........................................:

Now we have 2 trees. The idea is that the asynchronous call goes to the present scope, and the name definition goes the the function body of the generated callback. So the compiler will add the asynchronous call to the parent scope as a statement. Note that a call is an expression but a scope is a list of statements, so that we need to wrap it with a statement that contains only the call.

The situation will look like

scope
|           <4>
+---- Statement
|      |
|     CALL
:    /    \
:   f     (q, function($error, $result) {...})
:  <1>   <2>  <3>

  define
 /      \
x        +
        / \
  $result  y

Actually, this is not precise. In fact the parent scope would end here, and all the following code goes to the callback function's body.

Therefore after the call wrap itself and add the statement to the scope, it replaces the parent scope with the body scope of the callback function. That is the very key to Flatscript's solution.

scope
|           <4>
+---- Statement
x      |
      CALL
     /    \
    f     (q, function($error, $result) { scope' })
   <1>   <2>  <3>                          |
                                           |
                                           :
                                           :
                                           :

After that the compiling will go on and when the name definition add itself to the "parent scope", it actually add itself to the function body.

scope
|           <4>
+---- Statement
x      |
      CALL
     /    \
    f     (q, function($error, $result) { scope' })
   <1>   <2>  <3>                          |
                                           +----- define <7>
                                                 /      \
                                                x        + <6>
                                                        / \
                                              <4> $result  y <5>

This tree we will certainly result in asynchronous codes.

The remaining problem is how to deal with the $error. Right after the callback is generated an error handler will generated in the body but the exact code depends on the parent scope. For example if the parent scope is a try block, its catch block will be generated as a function so that could be called with this $error argument. This specification will be discussed in the "method selection" section.

Clone this wiki locally