Cleans up the public exports for the package itself:
* `parseFunctions()` was only used in playground, so this moves the definition
there. I had to update playground's dependencies to ensure the babel version
matched.
* Flattens away the `HIR` const in the export, and exports just 4 functions all
at the top-level: `run()`, `compile()`, `printHIR()`, and
`printReactiveFunction()`.
This will make it very easy to split the core compiler into a separate package
from the babel plugin, though i'm not sure it's worth doing that (yet).
Other than BuildReactiveFunction (HIR -> ReactiveFunction), Codegen.ts was the
only other remaining place that we use HIRTreeVisitor. However, we've already
switched the compiler to use the new form of codegen,
CodegenReactiveFunction.ts. This PR extracts the shared code from Codegen.ts
into the latter, and deletes the unused bits of Codegen.ts which relied on the
visitor API.
This now frees us up to merge BuildReactiveFunction and HIRTreeVisitor, removing
all the complexity of the visitor trait and type params.
Note that the `ReactiveFunction` data type can represent non-reactive functions.
So we can still compile non-React code after this change, the pipeline is `AST`
-> (BuildHIR) -> `HIR` -> (BuildReactiveFunction) -> `ReactiveFunction` ->
(CodegenReactiveFunction) -> `AST`. Which is the exact sequence I mapped out at
the start of this project ;-) (happy that worked out!)
See the background in #982. This PR reimplements part of InferReactiveScopes,
merging overlapping reactive scopes, but against ReactiveFunction instead of the
HIR.
See the background in #982. This PR reimplements part of InferReactiveScopes,
aligning reactive scopes to block boundaries, but against ReactiveFunction
instead of the HIR.
The primary goal of this stack is to change HIRTreeVisitor to make it easier to
handle value blocks. That's complicated by the fact that the visitor is a
general-purpose visitor, used in several analysis passes including
BuildReactiveFunction (which translates HIR->ReactiveFunction while also
grouping instructions into scopes) and InferReactiveScopes (which is actually
two passes, one to align scopes to block boundaries, one to merge overlapping
scopes). The long-term goal then is as follows:
1. Make BuildReactiveFunction transform HIR->ReactiveFunction but _without_
reactive scopes.
2. Align scopes to block boundaries, but rewritten to operate on
ReactiveFunction
3. Merge overlapping scopes, again rewritten to operate on ReactiveFunction
4. Group statements within ReactiveFunction into ReactiveScopeBlocks (today this
occurs when constructing the ReactiveFunction).
This PR implements 1 and 4. Because the implementation is incomplete this would
break the whole compiler, so for now both versions are still around. By default
compilation uses the old pipeline, but if a feature flag is enabled we use the
new version. The plan is to incrementally fix up the new version of the passes
in this stack, and then cutover: removing the flag and the old version of the
passes.
It's not enough to only update the mutable range of the canonical id created
instead of the phi but we need to update the mutable range of each of the
operands of the phi as well to account for the fact that the phi could've been
mutated later.
The operands are updated only if the phi is mutated later. Otherwise these
operands can be cached in their blocks.
Fixes https://github.com/facebook/react-forget/issues/978
Changes playground to use the modified `run()` function of the compiler, polling
the generator and building up a Map of tabs automatically based on the passes
that the compiler runs. This means tabs are always derived from the current
state of the compiler and we can never forget to add a pass.
<img width="1497" alt="Screen Shot 2023-01-10 at 11 06 03 AM"
src="https://user-images.githubusercontent.com/6425824/211639442-da421f73-e19e-4b63-9f33-0ce5a68cceb7.png">
Note the inclusion of some recently added passes that weren't added to the
playground — which was my fault but only bc i intended to ship this PR soon :-)
Turns CompilerPipeline into two functions:
* `run()` is a generator and yields values that are a disjoint union of either
AST/HIR/ReactiveFunction along with a name for that step. The idea is to use
this in the playground so that it always matches the exact steps for
compilation. I'll update playground in a follow-up.
* `compile()` is ast in, ast out, and uses `run()` under the hood.
Implements constant propagation/constant folding for a conservative subset of
the language. The approach is described in detail in the comments in the file
itself, a key note here is that this pass currently emits what looks like
garbage:
```
// input
const x = 1;
const y = x + 1;
// output
const x = 1;
2; // <---- you'll see a bunch of lines like this
const y = 2;
```
These useless lines occur where previously there was a temporary getting
calculated that was used later (so we saved it until it was used), but now it
isn't used later so we just emit it in-place. Dead code elimination (DCE) can
eliminate these and other useless statements later.
Note that a key motivation for implementing this pass is to reduce memoization
blocks to what is strictly required for dynamic computations. Why memoize at
runtime when we compute at build time?
When we convert a LabeledStatement to HIR we can end up emitting "consecutive"
blocks, ie where there are two blocks such that control flow will always go from
from one block to the other, with no other way to reach the second block but
through the first. Example:
```javascript
label: {
foo();
break label;
}
bar();
```
Converts to
```
bb0:
foo()
goto bb1:
bb1:
bar();
...
```
Ideally in this case we would merge these into a single block:
* When debugging, the extra goto makes it look like there is conditional control
flow when there isn't. If the code is consecutive it's easier to understand that
if it's a single block.
* Conversion from HIR -> AST relies on consecutive code all being in a single
block, so this breaks codegen (we never visit the goto target since all gotos
are assumed to be safe to convert to a break or continue).
This PR adds a failing test case, the next PR fixes it.
Phi operands and Block predecessors currently use a `BasicBlock` reference
rather than the BlockId. This diverges from other places (like terminals) where
we use an id and not a direct object reference. Especially since blocks may get
rewritten or pruned, it's a bit cleaner to use the block id in these places.
Changes `shrink()` and `reversePostorderBlocks()` to modify the HIR in-place
rather than return a new function, for consistency with all our other passes
which mutate in-place (for performance reasons).
shrink visits all the fallthroughs even if they are unreachable so this isn't
the right place to prune unreachable blocks.
This PR moves pruning into a separate pass.
Supports computed properties (as LHS and RHS) correctly. Previously we only
handled member expressions where the property was an identifier, and would
incorrect treat `a[b]` the same as `a.b`. Now we correctly distinguish these and
convert `a[b]` as an IndexLoad and `a.b` as a PropertyLoad. Similar for
assignment, `a[b] = c` is an IndexStore. For both IndexLoad and IndexStore we
lower the property to a Place first.