Memo: Transfrom Do Expression
This is a memo to me on how to implement the down level compiling of
ECMAScript proposal Do Expression and
Async do Expression in
TypeScript.
Need to be reviewed to make sure I’m not missing anything.
Part 0: Optimized output
It is possible to generate a better output if the do expression
satisfies the following requirements:
Requirements
Note: All those limitations does not across the declaration boundary.
- The do expression should only contains the following syntax.
-
Block
(BlockStatement
) -
EmptyStatement
-
ExpressionStatement
-
IfStatement
-
ThrowStatement
-
HoistableDeclaration
(refers to functions with async/generator) -
ClassDeclaration
-
LexicalDeclaration
(let
andconst
)
-
- The do expression should not contains the following things:
- (The list is empty currently)
Note: We’re allowing
let
andconst
but novar
because it requires a rewrite to the upper scope.
Note:
DebuggerStatement
not included because a one-line expression is not friendly to the debugging.
Assume we have a do expression
meets the above requirement, we can generate the output
by the following algorithm:
- Emit
%ToExpression(do_expr.block)
.
ToExpression(node: Block | BlockStatement)
- Let
result
be an emptyList<Expression>
. - Let
list
be clone ofnode.StatementList
. - Move all
HoistableDeclaration
to the top of thelist
. - For each element
T
inlist
,- Assert:
ToExpression(T)
matches one of the overload ofToExpression
. - Let
expr
beToExpression(T)
- If
expr
is not ~Empty~, appendexpr
toresult
.
- Assert:
- Return
result
(in the form of(expr1, expr2, expr3, ...)
).
ToExpression(node: EmptyStatement)
- Note: EmptyStatement (
;
) does not contribute to the result. - Return ~Empty~.
ToExpression(node: ExpressionStatement)
- Return
node.Expression
.
ToExpression(node: IfStatement)
- Let
condition
benode.Condition
. - Let
left
be%ToExpression(node.Then)
. - Let
right
be%ToExpression(node.Else)
. - If
left
is ~Empty~, Setleft
tovoid 0
. - If
right
is ~Empty~, Setright
tovoid 0
. - Return
(condition ? left : right)
.
ToExpression(node: ThrowStatement)
- Return
(e => { throw e })(node.Expression)
.
ToExpression(node: HoistableDeclaration | ClassDeclaration)
- Let
name
beGetName(node)
. - Let
x
be a new temp variable. - Set all
Reference
that refers toname
to referx
instead. (need scope analysis). - Return
x = node
.
Note: Function and Class both have expression version, it can be converted easily.
Example:
1 | const x = do { |
ToExpression(node: LexicalDeclaration)
- Let
names
be all reference defined in thenode
. - For each
name
innames
,- Let
x
be a new temp variable. - Set all
Reference
that refers toname
to referx
instead. (need scope analysis).
- Let
- Return
node
withoutlet
orconst
keyword.
Note: All LexicalDeclarations
are valid expression with let
or const
removed.
const { a, b } = c;
=> var _a, _b; ({ a: _a, b: _b } = c);
Example:
1 | const rnd = do { |
Part 1: Tracking completion values
First, add a temp var (let’s call it _C
) to store the completion value.
For every ExpressionStatement E
in the do expression block, replace it with _C = _E
.
Try statements
1 | try { |
If and Switch statements
1 | if (expr) { statements; } |
Skipped statements
The following syntactic structures never contribute to the completion value of the do expression therefore not tracking
for completion value insides it.
- ClassLike
- FunctionLike
- Any Declaration
- Any for loops
- Finally block in
try-finally
Other AST elements
Recursively visit them to track the completion value deeply.
Optimization
- For continuous ExpressionStatement I should only replace the last one.
- Maybe I should only track the last meaningful ExpressionStatement in every possible code branch.
Example
1 | // For code |
Part 2: Control flow in do expression
There’re 5 control flows I need to cover:
-
await
-
yield
-
break
-
continue
-
return
await
await is valid in the following cases:
- It is an async do expression
- It is using Top Level Await
- It is inside the Await context
For case 2 and 3, await is valid to use, therefore I transfrom them as:
1 | const x = do { ... } |
For case 1, just create an async IIFE and returning the Promise.
yield
yield is only valid in normal do expression in a Yield context.
Note there is no arrow function version of generator syntax so I need to take care of the this value.
1 | const x = (yield* (function*() { ... }).call(this)), _CompletionValue |
await+yield
It is only valid in a normal do expression in both Yield and Await context.
1 | const x = (yield* (async function*() { ... }).call(this)), _CompletionValue |
This should be enough. yield*
should delegate both Yield and Await to the inner function.
break/continue/return
This is the tricky part of the transform.
There is no way to break/continue/return
across the function boundary and I’ll use exceptions to do this.
Let’s talk about return
first because it’s only valid in a Return context.
So for return in a do expression, it should generate the code in the following way:
Note this is not an in-place transform, it will transform the containing function entirely to make sure the variable
scopes etc.
1 | function a() { |
break/continue
This part is the same as the return
case but works for LabeledStatement, SwitchStatement, and For loops.
I guess the following transform is safe. Please point out if I’m wrong.
LabeledStatement
1 | outer: { |
Switch and for loops
I guess it the same as LabeledStatement so I’m not going to transform it by hand.
Part 3: Special expressions in do expression
new.target and function.sent
Hoist and replace.
1 | function x() { |
1 | function x() { |
super() call and super.* property
Hoist to function
1 | class X extends Y { |
1 | class X extends Y { |
arguments
Call the wrapped function with upper level arguments
.
Note: This transformation is wrong. Since no one is use arguments in the new code today, we can mark it as a type
script error to use arguments
in the do expression.
1 | function x(a, b) { |
Part 4: Special positions in do expression
Class fields initializer
If do expression appears in a class field initializer, do the following transform.
1 | class T { |
Memo: Transfrom Do Expression
https://blog.jackworks.dev/2021/Memo-Transfrom-Do-Expressions/