Commit Graph
1708 Commits
Author SHA1 Message Date
Joe Savona c6d1181ba3 Refactor DCE internal state into a class
Tidying up the implementation to make the next PR's changes easier to follow.
2023-03-15 13:20:26 -07:00
Joe Savona f54f653277 DeclareLocal instruction
Adds a `DeclareLocal` instruction which represents declaring a named variable 
without initializing it. Currently declarations without an initializer (`let x`) 
are transformed into a declaration to undefined (`let x = undefined`) which 
changes the semantics due to hoisting and TDZ (temporary dead zone). The correct 
thing is to represent declaration without initialization.
2023-03-15 13:12:07 -07:00
Joe Savona f88713bba5 Support spread elements in CallExpression args 2023-03-15 17:13:50 -07:00
Joe Savona 957d8f1d44 Remove incomplete try/catch support 2023-03-15 15:36:17 -07:00
Joe Savona 5bc2c807c3 Support array pattern params 2023-03-15 15:33:14 -07:00
Joe Savona b69afe0506 More precise error for canonical reassignment in a value block
These examples previously errored all the way in codegen, when we detected that 
a value block (eg a `while` test expression) was declaring a new variable. We 
now detect this in LeaveSSA and error. The actual fix is a bit tricky, we'd need 
to add a new declaration in the nearest block scope (or selectively not DCE the 
declaration if its reassigned in just this way).
2023-03-15 09:39:23 -07:00
Joe Savona 9200ad027d Throw CompilerError (todo) for unused conditional/logical
If a logical or conditional expression is unused, then a phi node isn't created 
for the identifier it assigns to. Then when we leave SSA form the two branches 
will assign to separate values, and we aren't sure which identifier to use as 
the lvalue of the resulting ReactiveInstruction (remember that 
logicals/conditionals decompose into control flow in HIR, but are a single 
compound instruction in ReactiveFunction). If the two sides don't assign to the 
same location, it could be because of a bug in the compiler or because the value 
wasn't used. Ideally we'd represent this explicitly, but for now i'm just making 
this a TODO since most logicals/conditionals should have their value used.
2023-03-15 09:39:23 -07:00
Joe Savona 7537df3d4e Support assignment expressions in value blocks
Enables support for assignment expressions in value blocks (which includes in 
loop init/test/update blocks). This was pretty straightforward, the main changes 
are: 

* During PropagateScopeDependencies, we currently record scope reassignments 
based on `Identifier` object identity. In the case where a variable is 
reassigned in multiple control-flow paths of a value block, however, there can 
be multiple object identities. So we now de-dupe reassignments based on 
identifier id. 

* MergeOverlappingScopes now treats value blocks as regular blocks, allowing it 
to correctly merge scopes from the value with other scopes from the outer block. 

Otherwise this is mostly just lots of tests. Note that there is an outstanding 
todo, which is that we currently error for ternaries and logicals whose value is 
unused (eg `cond ? (x = 1) : null`). I'll address that in a follow-up.
2023-03-14 20:02:05 -07:00
Joe Savona 59cd1ca569 LeaveSSA: consistently rename identifiers even for value block phis
This PR clarifies the logic for adjust mutable ranges of phis and their operands 
during LeaveSSA. Previously we had logic in several places to determine 
whether/how to extend the ranges of each phi and its operands: this occurred 
while traversing reassignmentPhis (in 2+ places) and rewritePhis, as well as in 
rewritePlace(). 

This was kind of a band-aid to make things work, but the logic was imprecise. 
The actual rules are as follows: 

If there is a back-edge, or the phi id is unnamed, then were extend the ranges 
of the phi and its operands to min(starts) and max(ends). This ensures that the 
operands are computed as one unit, ie put into a single reactive scope. For 
loops this is necessary because...looping! For unnamed values this is necessary 
because of the way we collapse logical and ternary expressions back to a 
hierarchical ReactiveFunction — we need to make sure the final mutable range 
extends from the start of the final instruction up to the end of the 
logical/ternaries value blocks. 

Otherwise this is a phi where operands come from predecessors and are named. If 
the phi is mutated later, then we have to extend the end of each operand's range 
to account for the fact that they can be mutated later. Else, we leave the 
operands alone. 

Behavior doesn't change, but we consolidate all of the mutable range logic in 
one place.
2023-03-14 15:24:38 -07:00
Joe Savona 23e0ce02a6 Make JSX memoization optional (on by default)
Makes JSX memoized by default again, but adds an option to disable memoization 
of JSX. Also adds a new test and fixtures directory to test the opt-in 
no-jsx-memoization behavior.
2023-03-13 13:25:09 -07:00
Mofei Zhang 9465cb2100 [hir] Move Types to a separate file
--- 

Move types around in preparation for next PR #1335. No changes in impl, types, 
or functions.
2023-03-13 14:52:15 -04:00
Mofei Zhang b3086817d7 [tests][hir-typer] Add hir-tests fixture
--- 

Currently, we run type inference passes early in the pipeline and do not check 
inference output in any tests, test fixtures, or verifier passes. In fact, the 
only ways to view inferred types are (1) locally add a test fixture with`@only` 
and inspect console logs or (2) scroll to the relevant section on a playground 
example. 

However, inferred types and effects significantly affect the output of later 
passes (Alias / MutableRange analysis, InferReactiveIdentifiers, etc), and we 
have already found some bugs due to incorrect inference (e.g. #1274). 

This PR add the `typer-tests` fixture with the following goals 

1. Record relevant current compiler type + effect inference output. 

2. Have relatively stable output (with respect to changes in HIR and PrintHIR). 

- we try to achieve this by annotating the source code.
2023-03-13 14:52:15 -04:00
Mofei Zhang 03f3e541ce [tests][hir-typer] Add hir-tests fixture
--- 

Currently, we run type inference passes early in the pipeline and do not check 
inference output in any tests, test fixtures, or verifier passes. In fact, the 
only ways to view inferred types are (1) locally add a test fixture with`@only` 
and inspect console logs or (2) scroll to the relevant section on a playground 
example. 

However, inferred types and effects significantly affect the output of later 
passes (Alias / MutableRange analysis, InferReactiveIdentifiers, etc), and we 
have already found some bugs due to incorrect inference (e.g. #1274). 

This PR add the `typer-tests` fixture with the following goals 

1. Record relevant current compiler type + effect inference output. 

2. Have relatively stable output (with respect to changes in HIR and PrintHIR). 

- we try to achieve this by annotating the source code.
2023-03-13 14:49:53 -04:00
Mofei Zhang 814a21c97b [prettier] Ignore flow files in incremental formatting 2023-03-13 14:49:52 -04:00
Lauren Tan e90d058bee [babel] Remove prettier from plugin
I think we added this for easier debugging but isn't strictly needed. Remove 
since this was causing some issues internally.
2023-03-13 14:32:32 -04:00
Joe Savona d376cf1e37 Memoize arrays/objects created with destructuring spread
We were treating Destructuring as if it could never allocate and therefore 
didn't have to be memoized. That's only true if there are no rest spreads 
though. This PR teaches the compiler to treat rest spreads differently for 
scoping and memoization 

purposes, fixing the newly added test case and some existing bugs.
2023-03-09 12:44:30 -08:00
Joe Savona fcbfca69c9 More tests
More tests, in particular demonstrating that we don't memoize destructured rest 
elements propery — yet! Follow-up fixes.
2023-03-09 12:44:26 -08:00
Joe Savona 58a1cd872e Be explicit about lvalues (to distinguish their memo level later)
This is prep for #1345, which distinguishes different memoization levels per 
lvalue
2023-03-09 12:44:22 -08:00
Joe Savona 9048f59237 Prune scopes whose values dont escape
Adds a new pass that uses escape analysis and React-specific heuristics to tune 
the amount of memoization applied. Specifically, the pass ensures that we only 
memoize: 

* Values which escape (are directly returned or transitively aliased by a 
returned value) 

* ...and that are not JSX elements 

* OR values which are _dependencies_ of scopes that produce an escaping value. 

The latter case is necessary to avoid breaking memoization of an escaping value 
bc a scope happened to have a non-escaping dependency. 

## Algorithm 

1. First we build up a graph, a mapping of IdentifierId to a node describing all 
the scopes and inputs involved in creating that identifier. Individual nodes are 
marked as definitely aliased, conditionally aliased, or unaliased: 

a. Arrays, objects, function calls all produce a new value and are always marked 
as aliased 

b. Conditional and logical expressions (and a few others) are conditinally 
aliased, depending on whether their result value is aliased. 

c. JSX is always unaliased (though its props children may be) 

2. The same pass which builds the graph also stores the set of returned 
identifiers 

3. We traverse the graph starting from the returned identifiers and mark 
reachable dependencies as escaping, based on the combination of the parent 
node's type and its children (eg a conditional node with an aliased dep promotes 
to aliased). 

4. Finally we prune scopes whose outputs weren't marked.
2023-03-09 09:51:43 -08:00
Lauren Tan faa8eef0a8 [be] Fix remaining lints and enable lint in CI 2023-03-10 16:21:01 -05:00
Joe Savona 7813cfa52c Fix EliminateRedundantPhi for cascading eliminated phis
This was the actual bug. When EliminateRedundantPhis eliminates a phi, it has to 
rewrite downstream usages of the phi id to the single operand id. We were 
correctly doing that in all but one place. When we iterate _downstream phis_, we 
were looking up the operands against the rewrite table, but not updating the phi 
operands themselves to the rewritten value. 

This fixes the bug, and incidentally fixes a test that has been broken for a 
while and nagging at me.
2023-03-08 21:10:07 -08:00
Joe Savona 1fb0aed668 Fix mapInstructionOperands() visitor
Found while debugging the previous issue: `mapInstructionOperands()` should not 
look at lvalues. The previous version was causing us to create extra phi nodes, 
which interestingly weren't the actual problem behind the "SSA" bug, but sure 
looked like it at first.
2023-03-08 20:49:32 -08:00
Joe Savona ff855b1d34 Repro case for product bug
Simplified version of the example. As I eventually uncovered in #1342, the issue 
is cascading redundant phis not getting rewritten.
2023-03-08 20:49:28 -08:00
Joe Savona 6d4b0c61b3 Export ValueKind and Effect
We need to be able to reference these to teach the compiler about custom hooks 
like Relay.
2023-03-08 15:56:25 -08:00
Joe Savona f985d6cdba PropertyDelete/ComputedDelete instructions 2023-03-08 12:04:27 -08:00
Lauren Tan 0363648178 [babel] Fix export ordering
Turns out hoisting doesn't work with export declarations, so fix ordering such 
that the export always comes after the synthesized test declaration.
2023-03-08 11:42:57 -05:00
Sathya Gunasekaran 42c04fdcc7 [test] Add test to show Forget's inference is run inside lambda
JSX reads the value of `z` and does not mutate it so `z` can be independently 
memoized.
2023-03-08 00:16:00 +00:00
Sathya Gunasekaran 8695ea05c3 [test] Add test for capturing a ref before renaming
The renaming does not affect the lambda correctly.
2023-03-08 00:15:57 +00:00
Sathya Gunasekaran 80ab2f25a6 [test] Add test for capturing a renamed ref
The ref isn't renamed inside the lambda, needs to be fixed.
2023-03-08 00:15:54 +00:00
Sathya Gunasekaran d155d2dd81 [test] Add test for lambda that conditionally captures ref
Conditionally captured refs are correctly added to dependency list of the 
lambda.
2023-03-08 00:15:51 +00:00
Sathya Gunasekaran 1e8b6b51d3 [test] Add test for lambda that shadows ref
There is no ref captured as it is shadowed and the lambda does not depend on the 
ref.
2023-03-08 00:15:48 +00:00
Joe Savona 1c023263a3 Use shorthand where possible for ObjectExpression properties
During codegen, emit object properties as shorthand where possible (`{x}` 
instead of `{x: x}`)
2023-03-06 16:35:52 -08:00
Joe Savona e381aa042f Support spread elements in ArrayExpression
Similar to the previous, but for array expression: `const x = [...y]`
2023-03-06 15:32:43 -08:00
Joe Savona 93cca54aba Support spread patterns in object literals
Support ObjectExpression with spread items, eg `const x = {...y}`.
2023-03-06 15:24:43 -08:00
Joe Savona 7893a6c403 Represent ObjectExpression properties as Array<ObjectProperty>
Refactors the representation of ObjectExpression properties from a Map to an 
`Array<ObjectProperty>` to prepare for the next diff which adds spread element 
support.
2023-03-06 15:19:34 -08:00
Joe Savona f3c662845b Support string literal keys for object expressions
For ObjectExpression, we now support computed keys where the key is a string 
literal.
2023-03-06 14:51:18 -08:00
Joe Savona a9d6d2d95a [rfc] always create collections even if elements have errors
BuildHIR currently propagates UnsupportedNodes for collection types where the 
element itself can fail (for example object expressions where the key may not be 
valid). However, given that we currently abort compilation after the first 
failing pass (and will probably do so for quite a while) I think we can simplify 
and just always return the collection. Note that I already did this for 
destructuring. I'm open to leaving the code as-is if you prefer, though.
2023-03-06 14:43:58 -08:00
Joe Savona b793fe424d Separate eachLValue/eachOperand visitors
This PR starts to clean up our handling of lvalues and rvalues by adding new 
`eachInstructionLValues()` and `mapInstructionLValues()` helpers. Now, 
`eachInstructionOperand()` and `mapInstructionOperands()` only visit true 
rvalues, and the new passes must be used to visit lvalues. This allows us to 
remove the special-casing for StoreLocal and Destructure in most of the passes.
2023-03-06 14:33:52 -08:00
Sathya Gunasekaran cf03bac12a [be] Move skipped test to kitchensink 2023-03-07 16:21:34 +00:00
Sathya Gunasekaran 8477195c74 [babel] Re-export compiled functions 2023-03-07 16:08:16 +00:00
Sathya Gunasekaran 39898c1858 [babel] Add gating module support
Instead of replacing original function with compiled code, this adds an option 
to append the code and switch between the two based on an `isForgetEnabled` test 
condition that's imported from the specified gatingModule.
2023-03-06 19:54:00 +00:00
Sathya Gunasekaran 2ede182f5e [test] Enable gatingModule in tests 2023-03-06 19:53:57 +00:00
Sathya Gunasekaran c642de322c [test] Enable "use forget" directive in tests 2023-03-06 19:53:54 +00:00
Sathya Gunasekaran 8c24fc2be2 [pipeline] Type the result as t.FunctionDeclaration
A more narrower type as we only accept FunctionDeclaration as inputs
2023-03-06 19:53:52 +00:00
Sathya Gunasekaran 3cd31a11fb [babel] Add option to specify a gatingModule 2023-03-06 19:53:49 +00:00
Sathya Gunasekaran 637c2e68f1 [be][babel] Move directive checking to separate function 2023-03-06 19:53:46 +00:00
mofeiZ 0e1bba87ea [be][cleanup] Split primitive tests to individual fixtures
Followup to #1273 

No changes, just moving test functions to their own fixture files.
2023-03-06 18:25:53 -05:00
Mofei Zhang 8be45e7d4a [rhir][optim] Preserve conditional deps when propagating reactive scopes
--- 

**This PR slightly changes the semantics of ReactiveScopeDependencies**. 
Previously, reading a ReactiveScopeDependency is guaranteed to preserve the 
`nullthrows` semantics of its own declarations (not that of its inner scopes). 
This does not affect the overall correctness properties, since we already hoist 
reading of conditional dependencies (and thus may throw earlier than the 
original source). 

E.g. we already do not preserve *where* the nullthrows occurs. 

```javascript 

function Component(props) { 

// throws here, before print(x) 

const c_0 = props.a.b !== $[0]; 

let x; 

if (c_0) { 

x = {}; 

print(x); 

if (...) mutate1(x, props.a.b); 

mutate2(x, props.a.b); 

// ... 

``` 

### Summary 

This is an optimization, not a correctness property. 

When propagating reactive dependencies of an inner scope up to its parent, we 
want to *retain information about conditional dependencies* -- not the derived 
unconditional dependencies. This helps us produce more granular dependencies in 
the parent scope. 

Current implementation: 

```javascript 

const innerScopeDeps = innerScope.depTree.deriveMinimalUnconditionalDeps(); 

for (const dep of innerScopeDeps) { 

currentScope.depTree.addDep(dep); 

} 

``` 

New implementation: 

```javascript 

// union of a tree takes union of each node 

currentScope.depTree = currentScope.depTree.union(innerScope.depTree); 

``` 

### Example 

In the below example: 

- `scope @1` has a conditional dependency of `props.a.b`, but that reduces to 
the unconditional dependency `props` 

- `scope @0` itself has a unconditional dependency of `props.a.b` 

- Currently, Forget joins the derived / reduced dependencies of inner scopes, 
which adds `props` as unconditional dependency of `scope @0` 

- With this change, Forget joins the property trees and retains info about 
conditional deps, which adds `props.a.b` as a conditional dep of `scope @0`. 

```javascript 

// scope @0 (deps=[???] decls=[x, y]) 

let y = {}; 

// scope @1 (deps=[props] decls=[x]) 

let x = {}; 

if (foo) mutate1(x, props.a.b); 

mutate2(y, props.a.b); 

``` 

### Followup 

We currently keep track of properties unconditionally accessed per 
ReactiveBlock. Eventually we want to keep track of properties unconditionally 
accessed across blocks (as according to control flow). 

Consider the following code, in which sibling scopes 0 and 1 are sequentially 
executed. In this case, we can safely add props.a.b as a dependency of scope 1. 

```javascript 

// scope@0 (deps=[props.a.b], decls=[x]) 

let x = { a: foo(props.a.b) }; 

// scope@1 (deps=[???], decls=[y]) 

let y = {}; 

if (...) { 

mutate(y, props.a.b); 

} 

```
2023-03-06 16:33:08 -05:00
Mofei Zhang e2698a3124 [rhir] Add printDeps for debugging reactive dependency trees 2023-03-06 16:33:07 -05:00
Mofei Zhang e68ea941e6 [rhir] Promote conditional dependencies accessed by every cfg path
--- 

Implementation details summarized in comments. 

Overall, we want to calculate a `ReactiveDependencyTree` for every conditional 
block. If we know that conditional blocks are exhaustive (e.g. all CFG paths 
calculates a tree), we can take `intersection(depsFromEachBlock)` and add this 
to the parent Reactive + conditional scope `parentDeps = union(parentDeps, 
intersection(...))`. 

We use trees instead of individual deps here because we can still derive 
unconditional accesses. 

e.g. 

``` 

let x = {}; 

// props.a is an unconditional access here 

if (foo(other)) { 

x.a = props.a.b; 

} else { 

x.b = props.a.c; 

} 

```
2023-03-06 16:33:07 -05:00