Skip to content

[Discussion] Compilation, intermediary functions, statements, expressions, etc #15

Description

@tizoc

Opening this issue so that we can share our ideas here, may help with figuring out the best strategy.

My understanding (and correct me if I'm wrong) is that a lot (it not all) the intermediary functions introduced inside the body of a compiled defun are compiled let forms.
And since most functions end being async, this also introduces a lot of asyncs once those functions have been introduced.

If we have an expression, most of the time it can be converted into an equivalent statement, with its last expression becoming a return or an assignment.

When some piece of code can be compiled into a Javascript expression, then it is better to do so because it results in a smaller code size, statements would be used when it is not possible to produce an expression.

What comes ahead is an incomplete analysis, and there are probably many holes in my undestanding, so please point those out.

let expressions

Since lets are expressions, using blocks requires inserting a return to the expresion that
produces the value of the let expression.

Example:

(let X 1 (+ X X))

Gets compiled to:

(X => $.asNumber(X) + $.asNumber(X))(1)

With blocks and statements the equivalent would be:

{ const X = 1;
  return $.asNumber(X) + $.asNumber(X); }

But this only works when the result of the let expression is also the result of the defun.
If the let expression shows up someplace where its result will be bound to a variable or used as an argument to a function call, then a return cannot be used. What can be done instead, in the case of a variable bind, is to declare the variable as let Var; (instead of const), and then introduce the block as before, but with an assigment to Var instead of a return.

Example:

Source:

(let X (let Y 1 Y) (+ X X))

Current:

(X => $.asNumber(X) + $.asNumber(X))((Y => Y)(1))

With statement

{ let X;
  { const Y = 1;
    X = Y; }
  return $.asNumber(X) + $.asNumber(X); }

The compiler should include this information in the context, so that when the let is compiled it knows what to do (either return or variable assignment).
Since expressions are compiled "from the outside to the inside", annotating the context with this information shouldn't be hard.

The situation where the result of the let is used as a function call parameter is more complicated: (+ (let X 1 X) 2).

If the expression doesn't have side-effects, then it can just be assigned to a gen-symed variable using the method described above, and then that variable used as an argument.

But if there are side effects, this will not work because by doing this transformation naively we are changing the order of execution (Kl should be evaluated left-to-right).

A solution in such cases is to perform a conversion to A-normal form, so that every argument passed to the function call is bound to a variable first, then the variable passed (the transformation is easy to do, I wrote code to do this long ago because it was required for Chibi because it evaluates right-to-left, and also an OCaml port I was working on, this conversion can be done to the Kl code before the compiler even sees it).

It also has a cascading effect, every time this conversion is done, the function call that was an expression becomes wrapped in a let, which gets compiled into a statement.
And unless side-effectful code is tracked, this conversion would be required every time a let shows up as an argument to a function.

The good news is that such lets are very rare, so all this would only happen in very few cases, if at all.

Example conversion:

(+ (let A (get-A)
        B (get-B)
     (some-func B A))
   10)

becomes:

(let Dn664 (let A (get-A)
                B (get-B)
             (some-func B A))
  (+ Dn664 10))

if expressions

if expressions are compiled into Javascript's ternary operator, which work quite well because it is an expression. But statements cannot show up inside such expressions, which is a problem now once let expressions get compiled into statements.

In such cases ifs can be compiled into Javascript's ifs, and the same rules as with let can be used to handle the results.

For example (basic case, this one wouldn't require any of this, but once compiled this way then statements can be part of if branches):

Source:

(let X (if (> 2 1) 2 1) (+ X X))

Current:

(X => $.asNumber(X) + $.asNumber(X))(2 > 1 ? 2 : 1)

Proposed:

{ let X;
  if (2 > 1) {
      X = 2;
  } else {
      X = 1;
  }
  return $.asNumber(X) + $.asNumber(X); }

trap-error expressions

I see these are wrapped in a function too, but for a try/catch same rules as above would apply.

And throwing is done with $.raise, so throwing an exception doesn't convert an expression in a statement like using throw would.

Factorization

If intermediary functions are removed, the constructs introduced by the factorise-defun extension can be compiled trivially into labelled blocks and labelled breaks.
Since the constructs are new and separate there is no ambiguity and the transformation is direct (none of the analysis required for the above cases is required here, and at the same time, the above cased cannot be confused with these constructs).

The translation is described here: #13 (comment)

Since the conversion done by factorise-defun already adds %%return expressions, the Kl->Js compiled will not have to figure out where to insert returns.

Sync/Async

This part is something I'm not very clear on. Is it important for the async stuff to be implicit? Is it possible to instead use sync versions of the primitives that Shen requires, and then provide a new construct to make async calls explicit? if yes, what would the drawbacks be?

I'm adding libuv support on Shen/Scheme, so I'm interested in this not just because of ShenScript, and it is something I haven't given much tought to.

Code size

The statement versions require more bytes than the expressions, but I don't think the difference is THAT big, and if it results in considerably faster code, I think it is worth it.

Many calls that are repeated use functions that are properties of $, I think that with some analysis some can be removed (at least when typeckecking and the optimise flag are enabled).

Also I think expressions like (+ A B) can be compiled into $.add(A, B) instead of $.asNumber(A) + $.asNumber(B).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions