Commit Graph
1708 Commits
Author SHA1 Message Date
Mofei Zhang 25bfe728aa [rhir] move DeriveMinimalDependencies to its own file
--- 

Small refactor of reactive dependency logic, no behavioral change. 

- Moves ReactiveDependencyTree logic into `DeriveMinimalDependencies`. 

- this moves hides most helper functions + types 🥳 

- made `ReactiveDependencyTree` a class 

- Changes `#dependencies` type: `Set<ReactiveScopeDep>` -> 
`ReactiveDependencyTree` 

- instead of collecting all dependencies into a tree in the end, we now eagerly 
join dependencies into the tree on `visitDep` 

- this is needed for the next PR in the stack, which relies on incremental 
merging
2023-03-06 16:33:06 -05:00
Joe Savona 5b11372901 Optimize DCE for destructuring
In #1287 i implemented basic handling for destructuring in DCE: if any of the 
pattern values are used, we retained the whole instruction as-is. However, 
ideally we could prune out unused elements from the pattern. There are pretty 
simple rules: 

* ArrayPattern we can eliminate unused elements from the end. 

* ObjectPattern we can eliminate any unused element, but only if there is no 
rest element.
2023-03-03 17:16:39 -08:00
Joe Savona 8233e8f95a All calls to lowerExpression lower to a unique temporary
This is more followup toward deleting Instruction.lvalue. The previous PR 
ensured that all Instruction.lvalue identifiers are only ever assigned to once. 
Now we ensure that `lowerExpression()` is _only_ called via 
`lowerExpressionToTemporary()`, ie we now always lower every single expression 
to a temporary. 

This will make it easier to make lvalue a part of the InstructionValue instead 
of the instruction itself, in follow-up PRs.
2023-03-03 17:09:27 -08:00
Joe Savona fe2d179a61 Value block reassignment uses StoreLocal
As part of removing Instruction.lvalue we need to ensure that it is only used to 
represent that instruction's value — the InstructionKind should always be Const. 
The one place where we violated this was for value blocks, specifically 
ConditionalExpression and LogicalExpression. For both of those, we generate a 
single temporary place to represent the expression result. Then the consequent 
and alternate branch ended in a `LoadLocal` that reassigned that temporary (in 
the lvalue) to the result of that branch. 

This PR changes to use StoreLocal instead, and updates the recently added 
validation pass to ensure that all identifiers that appear in an 
Instruction.lvalue are only ever assigned once.
2023-03-03 17:09:26 -08:00
Joe Savona ca9d49090a New destructuring representation modeled on StoreLocal
Changes to explicitly model destructuring (array and object patterns), expanding 
support to include rest elements and preserving destructuring through the 
output. The new "Destructure" instruction is similar to "StoreLocal" but has a 
pattern instead of a place. For now each level of nested array/object patterns 
creates a separate destructure instruction, which ensures we have a temporary 
Place to talk about the intermediate array/object and its type/effects etc. 
Example: 

``` 

// INPUT 

const [x, {y}, ...z] = a; // yay rest elements work now! 

// HIR 

[1] <unknown> $2 = LoadLocal a$1 

[2] <unknown> $6 = Destructure Const [ <unknown> x$3, <unknown> $4, ...<unknown> 
z$5 ] = <unknown> $2 

[3] <unknown> $8 = Destructure Const { y: <unknown> y$8 } = <unknown> $4 

// OUTPUT 

const [x, t0, ...z] = a; 

const {y} = t0; 

``` 

Note that we can still collapse to a single destructure statement during 
codegen, independently of whether we have separate instructions internally. For 
now i'm going w the simple approach of emitting multiple statements in codegen 
(the code will very likely get further rewritten by downstream babel passes 
anyway). 

Also, I don't love the "if StoreLocal/Destructure else ..." pattern that the 
StoreLocal created and that this PR entrenches. As discussed w @gsathya offline, 
the long-term direction will be to add a separate visitor, roughly 
`eachLValue()` and `eachOperand()` so that we can treat all instructions the 
same. Existing Instruction.lvalue will go away and become a property of the 
other types of instructions.
2023-03-03 17:09:25 -08:00
Sathya Gunasekaran d24f3fdad9 [test] Add failing test of incorrect typing of captured ref
The typer doesn't understand that it needs to ignore captured references ("read" 
refs are ok, but not "mutated" refs) to be conservative.
2023-03-03 17:33:44 +00:00
Sathya Gunasekaran ec2a179572 [be] Move phi aliasing to InferAliasForPhis
InferMutableRangesForAlias is about extending the mutable ranges, not for 
updating the alias sets. Let's refactor this into a separate pass. 

InferMutableRangesForAlias was iterating over alias sets and not the HIR so this 
refactor isn't costing us any additional perf cost (in terms of an extra 
iteration over the HIR).
2023-03-03 13:54:52 +00:00
Sathya Gunasekaran 1fffc842bc [hir] Don't redefine context refs in EnterSSA
Context references are special since reassignments need to update the original 
reference, not define a new place.
2023-03-03 13:35:04 +00:00
Sathya Gunasekaran 1e5e75746d [hir] Handle LHS of AssignmentExpression specially
Babel has a bug where it doesn't visit the LHS of an AssignmentExpression if 
it's an Identifier. Work around it by explicitly visiting it.
2023-03-03 13:35:04 +00:00
Sathya Gunasekaran 482ae7aa7c [hir] Refactor Babel expression visitor to separate function
We'll need to recursively call it in the future.
2023-03-03 13:35:03 +00:00
Sathya Gunasekaran 99eb48db07 [hir] Skip only MemberExpressions
Identifiers don't need skipping anyways, so this doesn't affect the existing 
behavior. 

In the future, we will special case handling of LHS of AssignmentExpression 
which will require us to not skip the RHS.
2023-03-03 13:35:03 +00:00
Sathya Gunasekaran f17f4dcf0b [hir] Treat values escaping into a contextref as Mutate 2023-03-03 13:35:02 +00:00
Sathya Gunasekaran f002682322 [hir] Treat reassignment as mutation for context refs
The mutable range difference for assignment is just 1 which is something we 
usually don't track as we care about mutation and not assignment. 

But this isn't true for context refs whose (re) assignment is actually a 
mutation.
2023-03-03 13:35:02 +00:00
Sathya Gunasekaran 24fbeddcea [test] Fix test to have initial value
Having undefined as the initial value makes this a primitive. There's a separate 
bug where we need to remove type inference for captured refs that get mutated 
but that's secondary -- we're currently not even marking the Identifier LHS as a 
captured ref. Fix the test to repro this bug for now.
2023-03-03 13:35:01 +00:00
Sathya Gunasekaran 9683771861 [packages] Add Forget version of benchmark 2023-03-02 11:06:00 +00:00
Sathya Gunasekaran 63fd00ab27 [hir] Remove unreachable fallthroughs after merging blocks
Fixes https://github.com/facebook/react-forget/issues/1141
2023-03-03 14:08:52 +00:00
Lauren Tan 0ad0f8cf43 [rhir] Visit DoWhile's test as a conditional dependency
Missed this in the prior PR!
2023-03-03 00:31:37 -05:00
Tianyu Yao de6444b4f5 Add a logger option for logging compiler errors from Babel
Add a `logger` option so compiler errors are surfaced  in our metrics collection 
pipeline. 

We can probably later merge the global `log` function with it.
2023-03-02 16:59:09 -08:00
Joe Savona 36d1fa2569 [storelocal] Instruction lvalue is just a Place 2023-03-02 14:19:18 -08:00
Joe Savona f4abf0a9ee [storelocal] lvalues are always const 2023-03-02 14:19:17 -08:00
Joe Savona 487786f7d4 StoreLocal instruction
Adds a new `StoreLocal <kind> <place> = <value>` instruction which stores 
<value> into <place>. With this change, Instruction.lvalue is _always_ a `const` 
temporary, and never a named identifier (there's a new validation pass to assert 
this). StoreLocal is the only way to declare or update a named identifier: the 
instructionKind property says whether it's a const/let declaration or a 
reassignment. Naturally a _lot_ of passes had to be updated to make this work, 
but the existing Effect.Store variant that @gsathya added made this overall 
straightforward. 

Note that as of this PR several passes still have code to handle the possibility 
of an instruction lvalue being something other than a temporary. When we clean 
that up in a follow-up, there will be a lot less of the duplication that appears 
here. For example, CodegenReactiveFunction has two places to handle variable 
declarations in this PR. However, one of them is to handle lvalues, which should 
now _always_ be temporaries and never emit a regular variable declaration. 
Similarly, several passes have to build up a table of identifier -> identifier 
(because of LoadLocal). Longer-term, we should update the Place abstraction so 
that it directly specifies the instruction which created that temporary, so we 
can look it up on demand instead of needing an extra mapping.
2023-03-02 14:19:17 -08:00
Lauren Tan 440fd1f24a Add support for DoWhile statements
Adds support for DoWhileStatements. It's pretty similar to how we handle While, 
except in the case where a test block is unreachable (for example, an early 
unconditional `break` within the loop body). In this scenario we eliminate the 
terminal altogether and replace it with a goto to the loop block.
2023-03-01 19:45:02 -05:00
Joe Savona 71db40c6ff LoadLocal instruction
Changes InstructionValue::Place to InstructionValue::LoadLocal for clarity, this 
is intended as the only instruction where a variable can appear as an operand. 
All other instructions operands will be temporaries.
2023-03-01 16:27:34 -08:00
Sathya Gunasekaran 08c1eda7ff [hir] Simplify depRoot map
Use Identifier as a key, which lets us simplify lookup to no longer require an 
index access.
2023-03-01 16:48:54 +00:00
Lauren Tan 7a6a0e5f72 [be] Fix various eslints 2023-02-28 19:19:38 -05:00
Lauren Tan f28eef3dca [eslint] Disable no-constant-condition and no-fallthrough 2023-02-28 19:19:37 -05:00
Lauren Tan 323a70d181 [playground] Fix compiler errors being cut off 2023-02-28 19:19:36 -05:00
Mofei Zhang 1fdbcfe162 [rhir] Add dependencies produced by active (incomplete) scopes
--- 

> If this operand is used in a scope, has a dynamic value, and was defined 
before this scope, then its a dependency of the scope. 

> (from current comments in PropagateScopeDependencies::visitDependency) 

A reactive scope can take a dependency from a definition produced by an 
incomplete parent scope. Our tests previously did not cover this, since most 
object types aliased together and remained mutable throughout a ReactiveScope. 

e.g. our tests did not have 

``` 

scope @0 (deps=...,  declarations=[x, y]) { 

x = {}; 

// define a reactive, immutable value that is not aliased to become mutable 

const immutableVal = ...; 

scope @1 (deps=immutableVal, declarations=[y]) { 

y = read(immutableVal) 

} 

mutateX(x, ...); 

} 

``` 

We should not add a dependency if it is produced in exactly the same scope as 
the one it is used. It is safe (and correct) to depend on values produced by a 
parent scope. 

--- 

Note that we still should check for whether a defining scope is active to 
determine whether it should be added as a output of that scope 
([src](https://github.com/facebook/react-forget/blob/b608ab20d57229b528deeffa19f1ee08a4bad37a/forget/src/ReactiveScopes/PropagateScopeDependencies.ts#L469-L478)). 

Access of an identifier produced by a parent scope (i.e. adding a variable 
defined by a scope's parent as its own dependency) does not require adding that 
identifier to the parent's `declarations`, since that identifier is already 
valid to access via identifier binding rules.
2023-02-28 16:36:01 -05:00
Mofei Zhang b532465ce2 [inference] Capturing an immutable value should be a read
--- 

Following #1216: 

If a value is known to be immutable, then it doesn't need to be considered 
'captured' since no mutation should occur. 

Couldn't figure out a unit test in which this specific fix matters, but we need 
this to fix test output of #1273 

cc. @gsathya, would love some feedback / eyes on this. This makes sense for 
Primitives in particular (which are always read / copied in rval position), but 
I'm not as familiar with edge cases for other immutable values especially around 
lambdas.
2023-02-28 16:36:00 -05:00
Mofei Zhang 474c38c573 [rhir][tests] Added tests for primitives as dependencies
--- 

Our current compiler has specific logic for determining what can be a reactive 
value / reactive dependency. 

Currently, all of the following affect whether an identifier is a reactive: 

- **alias analysis** (applicable to objects) 

- **data + control flow** (whether any other reactive identifiers is used in 
determining it) 

- **reactive scopes** (we generalize and say anything produced by a block with 
reactive dependencies must be non-stable and reactive) 

- this is not true in the case of const primitives, but an overestimate is safe 

- whether the **scope that declares this identifier** is ~~currently active~~ 
the same scope in which it is used (fixed by #1275) 

(since a scope cannot be dependent on itself) 

These conditions are complex. We end up inferring most identifiers as `mutable` 
and `object` types, which have different stability and aliasing properties from 
primitives. As a result, we're missing some cases in our existing test coverage. 

Test case output is fixed by #1274 and #1275 

--- 

(This can be separated from the stack below, which implements conditional 
dependencies. Happy to merge that first and open this as a new stack if that 
produces a significantly better Git PR history.)
2023-02-28 16:35:59 -05:00
Mofei Zhang 6b129b59ed [rhir] Refactor ReactiveScopeDependency, conditional dependencies (2/2)
--- 

See comment block in `PropagateScopeDependencies` and added test case 
`reduce-reactive-conditional-dependencies` for correctness properties / 
dependency merging logic.
2023-02-28 16:35:59 -05:00
Mofei Zhang 4005f862bd [rhir] Refactor ReactiveScopeDependency, unconditional dependencies (1/2)
--- 

See comment block in `PropagateScopeDependencies` and added test case 
`reduce-reactive-unconditional-dependencies` for correctness properties / 
dependency merging logic.
2023-02-27 13:38:23 -05:00
Mofei Zhang 9ca41f8a2e [rhir] small: ReactiveDependency uses Identifier instead of Place
--- 

We never use the `Place` of a ReactiveScopeDependency, except for when we want 
to access its identifier. Later PRs in this stack will convert 
`ReactiveScopeDependency` to property access trees (and traverse over the tree). 
This usually involves merging multiple Dependencies into trees (where each root 
is a unique identifier). We then traverse over each tree to extract its 
dependencies (e.g. unconditional leaves). 

``` 

{place: {loc: 1, identifier: 'props'}, path: ['a', 'b']} 

{place: {loc: 2, identifier: 'props'}, path: ['a']} 

// merges into a single tree root, which should represent a single identifier 

``` 

The `place` of each individual `ReactiveScopeDependency` will be lost during the 
tree traversal, and it doesn't really make sense to recreate them using the 
`Place` attached to the tree root.
2023-02-27 13:38:02 -05:00
Mofei Zhang f335e7d4d9 [rhir] Patch: ordering of overlapping input dependencies does not matter
--- 

Patch and simplify logic around merging overlapping reactive dependencies. 

Added `reduce-reactive-unconditional-deps` test fixtures, which tries to cover 
all cases of merging unconditional dependencies (to a minimal dependencies set). 
Please let me know if I missed any
2023-02-27 13:38:02 -05:00
Lauren Tan 8bc8d67aee [be] Fix unused vars in BuildHIR 2023-02-27 17:12:41 -05:00
Lauren Tan e4e1af7236 [eslint] Ignore _ prefixed unused variables 2023-02-27 17:12:38 -05:00
mofeiZ 60137a8b64 [rhir][cleanup] remove DeclKind (replaced by pruneNonReactiveDependencies)
DeclKind is no longer read / needed due to pruneNonReactiveDependencies pass.
2023-02-27 11:04:38 -05:00
Joe Savona 1dfaf8a94b Lower all operands to temporaries
This PR changes BuildHIR to lower all operands to temporaries. Example: 

```javascript 

// Input 

a + b; 

// Previous Lowering 

Const t0 = BinaryOperation Place(a) "+" Place(b) 

// New Lowering 

Const t0 = Place(a); 

Const t1 = Place(b); 

BinaryOperation Place(t0) "+" Place(t1) 

``` 

This is necessary to ensure we're always referring to the correct version of a 
variable, even in the case of reassignment mid-expression. For example, we 
previously evaluated `let x=1; x + (x = 2) + x` incorrectly to 6 because we 
lowered the `x = 2` prior to the binary operators. We now lowers each instance 
of x to a temporary, ensuring they refer to the correct SSA version of the 
variable, and produce the correct result (5). 

Note that with this change, the _only_ place a variable can appear as an 
operator is when the InstructionValue is a raw identifier. This was already the 
case for globals (as of the LoadGlobal instruction). All other instruction value 
variants will only ever receive temporaries as arguments. 

This necessitated a few changes to our inference: 

* The logic to extend the range of phi operands (if the phi is mutated) was 
previously in LeaveSSA, but that was actually too late. The introduction of 
lowering to temporaries help discover failing cases, which I fixed earlier in 
the stack by moving the logic to extend the range of phi operands into the 
InferMutableRanges fixpoint loop. 

* PropagateScopeDependencies now has to track variable reassignments in addition 
to tracking property accesses 

* AnalyzeFunctions now has to track variable reassignments in addition to 
tracking property accesses 

* InferReactiveIdentifiers now needs a fixpoint iteration, because identifiers 
don't directly appear together in the same instruction anymore (such that we can 
directly propagate the reactivity between them). Instead, we'll first see that 
the temporaries are reactive, and have to propagate that back to the identifiers 
the temporaries were loaded from. 

Overall while this does introduce a bit more complexity, it also makes the 
compiler more robust. As with the phi example illustrates, there are legitimate 
inputs that can create similar indirections to that introduced by lowering 
identifiers to temporaries. 

Note that there’s a theme to the changes here: several analysis passes need to 
map an operand back to its identifier value. Ideally our HIR structure would 
directly support looking up the value for a temporary. For example, if operands 
were references to eg the index of the instruction that produced them. Because 
we don’t have such a representation yet (it would fall out naturally if we were 
writing in Rust), we have to do some bookkeeping. The key takeaway here is that 
this bookkeeping is incidental complexity given our current representation, not 
fundamental complexity of the algorithm.
2023-02-22 15:53:23 -08:00
Joe Savona 256071460d [destructuring] Cleanup InstructionValue type definition 2023-02-17 15:30:42 -08:00
Joe Savona 1d0aa64b24 Make it easier to debug lambdas 2023-02-22 14:07:33 -08:00
Joe Savona 8da443b673 Print HIR of function expressions for debugging
Example output: 

``` 

bb0 (block): 

[1] Const mutate $9$ = read a$8 

[2] Const store x$10:TObject$ = Object { a: read $9 } 

[3] Const mutate $11:TObject$ = capture x$10:TObject 

[4] Const store $12[4:6]:TFunction$ = Function @deps[read $11:TObject]: 

bb0 (block): 

[1] Const mutate q$7$ = capture x$6 

[2] Const mutate $8$ = capture q$7 

[3] Const mutate $9$ = PropertyLoad read $8.b 

[4] Const store $10[4:6]:TFunction$ = Function @deps[read $9]: 

bb0 (block): 

[1] Const mutate $7:TPrimitive$ = 1 

[2] Const mutate $8[2:4]$ = capture q$6[0:4] 

[3] Const store $9$ = PropertyStore mutate $8[2:4].b = read $7:TPrimitive 

[4] Return 

[5] Const mutate $11$ = Call mutate $10[4:6]:TFunction() 

[6] Return 

[5] Const mutate $13$ = Call mutate $12[4:6]:TFunction() 

[6] Const mutate $14:TObject$ = capture x$10:TObject 

[7] Return freeze $14:TObject 

```
2023-02-22 13:03:00 -08:00
Joe Savona bae2cb5f89 Remove now-unnecessary mutable range extension in LeaveSSA
This is no longer necessary now that we extend phi operands' ranges in 
InferMutableRanges
2023-02-21 15:29:30 -08:00
Joe Savona 9a25b5a12a Fix previous examples, alias across phis
Within the InferMutableRanges fixpoint iteration, we need to alias phi operands 
with the phi id if the phi id is later mutated.
2023-02-21 15:29:30 -08:00
Joe Savona 79eb250187 Bug repro for unobserved aliased mutation w phi
I found this while working to ensure that we always lower all operands to 
temporaries. This works: 

```javascript 

// the whole computation of x is memoized in one block, bc of the mutation after 
the phi 

let x; 

if (cond) { 

x = someObj(); 

} else { 

x = someObj(); 

} 

mutate(x); 

``` 

However, if you alias either of the operands, we lose the mutation: 

```javascript 

let x; 

if (cond) { 

const y = someObj(); // OOPS this gets independently memoized 

x = y; 

} else { 

x = someObj(); 

} 

mutate(x); 

``` 

The core issue is that InferMutableRanges does not take into account mutation of 
phis. ~~My first thought is that we need an additional, outer fixpoint iteration 
loop to flow mutation back "up" to phi operands~~ 

edit: there was a much easier fix, we need to alias phi operands and phi id 
within the existing fixpoint iteration. See follow-up PR which fixes.
2023-02-21 08:29:37 -08:00
Joe Savona fcdcd6038f Constant propagation converts computed access to static property where possible 2023-02-17 12:32:30 -08:00
Joe Savona 2b3902f043 Bailout on assigning to module-scope variables 2023-02-17 12:18:17 -08:00
Joe Savona a2ebea4cde Bailout on assigning to globals 2023-02-17 10:02:56 -08:00
Joe Savona ac9212f24e Make globals configurable; populate a reasonable default list
This is a precursor to validating that all identifiers are defined - we need to 
know about gobals and module declarations, so this PR adds the ability to 
configure a Set<string> of defined globals. The default list is inspired by the 
globals that prepack defines, which just comes from the spec definition.
2023-02-17 09:47:59 -08:00
Joe Savona 6b654f306c Construct LoadGlobal; consume hook info from types
Updates BuildHIR to produce LoadGlobal instructions for references to globals. 
Note that this breaks our previous strategy of finding hook calls: that relied 
on looking at the callee of a CallExpression and checking its name, which relied 
on the callee not being lowered to a temporary. By lowering the name (eg 
`useState`) to a temporary first, we now no longer see the name at the callsite. 

Thankfully @gsathya solved this for us already by teaching type inference about 
hooks, and more generally implementing type inference. I updated this so that we 
infer the type of a LoadGlobal if the name is a hook: the type inference picks 
this up and propagates the type forward correctly. So now, all places that 
needed to check for a hook can just look at the type and everything works. 

This is much more robust than before - you can now reassign a hook to a local 
variable and we'll still detect that when you call it, you're calling a hook.
2023-02-17 09:47:58 -08:00
Joe Savona 326e8c13f7 Scaffolding for LoadGlobal instruction
Adds a new `LoadGlobal` InstructionValue variant which will be used to represent 
identifiers that refer to globals. We don't construct this value type yet.
2023-02-16 15:17:30 -08:00