Commit Graph

1737 Commits

Author SHA1 Message Date
Mofei Zhang 5656d5f078 [config] Change compiler defaults to prepare for oss 2024-03-29 09:31:09 -04:00
Lauren Tan 9d20aa2819 Don't compile node_modules on playground 2024-03-28 10:46:56 -04:00
Lauren Tan 7748ce8f3f Update compiler naming in remaining error text 2024-03-28 10:40:05 -04:00
Lauren Tan df5ff9753a Fix jest script 2024-03-28 10:40:05 -04:00
Lauren Tan f4229cdb7f Rename error type 2024-03-28 10:40:04 -04:00
Lauren Tan 638f8e3ddc Remove [ReactForget] prefix from error messages 2024-03-28 10:40:03 -04:00
Mofei Zhang 5166869204 [patch] Fix control flow bug in PropagateScopeDeps
A dependency D from either an instruction or scope is poisoned if there may be a 
(non-linear) jump instruction between it and the start of its immediate parent 
scope. Poisoned dependencies are added as conditional dependencies to their 
parent scope. 

(done: reduce false positives in scopes that begin after return/throw) (done: 
fix bugs in recording and joining exhaustive conditional deps) (done: flesh out 
commit message, clean up PR, add more fixtures) 

--- \## Bug details: 

Take a simple example: ```js target: {   instrA;   if (...) {     instrB;     
break target;   } else {     instrC;   }   instrD;   // ... } instrE; // ... ``` 

This diagram shows how we represent this program in the reactive IR. - Blocks 
are represented as a list of nodes. - Green nodes show instructions and value 
blocks (simplified as a single instruction). - Pink nodes show terminals, which 
transfer control to a subtree of nodes. <img width="450" alt="image" 
src="https://github.com/facebook/react-forget/assets/34200447/930789f2-39cd-4ea8-b12a-530042807b46"> 

Prior to this PR, PropagateReactiveScopeDeps was incorrect because it assumed 
that a block's instructions are evaluated unconditionally (which is how HIR 
basic blocks work). E.g. if a reactive scope enclosed `block 1`, we assume that 
`instrA` and `instrD` both will evaluate unconditionally. 

This failed to account for `jump` instructions like break, continue, return, and 
throw. This may result in invalid hoisting of PropertyLoads (i.e. Forget output 
may throw when source does not throw). Note that other terminals (e.g. if and 
loops) are not affected as they are self contained subtrees that evaluate 
sequentially. 

With the changes in this PR, we mark `block 1` as poisoned upon encountering the 
`break` instruction. While `block 1` is active and poisoned, it will determine 
how visited dependencies are added. 

Here, added solid lines show unconditional dependencies, dashed lines show 
conditionally accessed dependencies: - dependencies from `instrB, instrC` are 
conditional because they are within conditional subtrees - dependencies from 
`instrD` are conditional because it is within a poisoned block within its parent 
scope. 

<img width="450" alt="image" 
src="https://github.com/facebook/react-forget/assets/34200447/81980f68-7e65-4bd7-ba94-3f0c26550e5c"> 

--- Recapping an offline discussion with @josephsavona: this pass would really 
benefit from operating on HIR. The minimal work needed for this pass to run on 
HIR is to rewrite and reorder  `AlignReactiveScopesToBlockScopes` to operate on 
HIR. 

The following diagram shows what HIR blocks look like for the same code. 
Evaluating hoistable PropertyLoad dependencies for a scope enclosing 
`instr{A-D}` is much simpler:  just evaluate whether the PropertyLoad evaluates 
for every path between `bb0` and `bb4`. <img width="250" alt="image" 
src="https://github.com/facebook/react-forget/assets/34200447/44b38939-defb-4b29-878d-4445ec6ccc06"> 

---
2024-03-27 20:26:18 -04:00
Mofei Zhang b98b569017 [DeriveMinimalDeps] Account for conditional / poisoned accesses within
conditionals 

This change is needed for #2752. To minimize renaming `error.fixture` -> 
`fixture` files, I'm reordering this PR to earlier in the stack. 

Prior to the fix in #2752, we only expected unconditional accesses within 
`depsInCurrentConditional`, which records instructions directly within a 
conditional block (not including nested conditional blocks).
2024-03-27 20:26:18 -04:00
Mofei Zhang 2782fa2664 [rhir] Preserve block labels + target blockIds for break/continue terminals
RFC: we can either retain break/continue target ids (instead of pruning them in 
`buildReactiveFunction`) or re-implement the same logic in `propagateScopeDeps` 
(ignore implicit breaks; match unlabeled break / continues to their closest loop 
/ while parent terminal). 

If we go ahead with this approach, I'll clean up this PR (add relevant types and 
comments)
2024-03-27 20:26:17 -04:00
Mofei Zhang 877c490fa3 [visitors] Visit loop body before update 2024-03-27 20:26:16 -04:00
Mofei Zhang 1515cb3218 [be][sprout] Add ErrorBoundary to test exceptions in sequentialRenders 2024-03-27 20:26:15 -04:00
Mofei Zhang 4d37a599e3 [be][tests] Move PropagateScopeDep fixtures to separate directory 2024-03-27 20:26:14 -04:00
Mofei Zhang a37f242f59 [be][DeriveMinimalDeps] Check current assumptions for conditional control flow 2024-03-27 20:26:13 -04:00
Sathya Gunasekaran fd3ae0ca0a [type] Conservatively type return values from reanimated
Typing them as mixed read only seems safe because the fields are primitives but 
lets be conservative and type as Poly.
2024-03-28 12:32:25 +00:00
Sathya Gunasekaran 0a3d1c3d31 Add type defs for reanimated
The reanimated babel plugin specifically looks for args to their hooks that are 
callbacks, then it workletizes the body of that callback so it can run on the 
main thread. 

But, forget extracts that callback into a temporary variable and then replaces 
the previously inlined callback as an identifier, so that breaks reanimated's 
babel plugin. so what happens is some of the previously workletized functions no 
longer do after forget runs, which throws a runtime error about a non-worklet 
function running on the main thread. 

Reanimated expects this: ``` const animatedGProps = useAnimatedProp(function () 
{ ... }) ``` 

But forget does this: ``` const t0 =function () { ... } const animatedGProps = 
useAnimatedProp(t0) ``` 

With the type definitions, Forget no longer assumes the args to reanimated APIs 
escape so Forget does not memoize and they stay as is.
2024-03-28 12:26:22 +00:00
Sathya Gunasekaran a6c6855dd1 [be] Remove unused properties arg from addHook
This is never used by any caller.
2024-03-28 12:26:22 +00:00
Joe Savona 91f7bc8be7 Ensure valid mutable ranges for all scopes; fix ranges for context vars
Our current validation fails to detect some invalid cases of mutable ranges — 
namely, ranges that are fully or partially uninitialized, with start or 
start+end still set to zero. 

This PR fixes these cases, starting by validating that the ranges for _all_ 
reactive scopes are valid: start >= 1, and end <= (last instr id + 1). This 
exposed the invalid cases, which are also fixed here: 

* During AnalyzeFunctions, we need to reset identifier ranges and scopes when 
exiting an inner function. This has to happen *after* the effects have been 
translated to the function deps/context operands, in order for 
InferRefenceEffects to continue working on the outer function. Previously I did 
this at the start of InferReactiveScopeVariables, but that's insufficient bc the 
incorrect ranges could also influence InferMutableRanges. AnalyzeFunctions is 
the point at which we compute the ranges for identifiers in the inner function, 
so it's the most ideal place to clean those up so they don't influence the outer 
function. 

* In InferMutableLifetimes, we need to ensure that context variable identifiers 
end up with a mutable range starting where they are declared, and ending with 
their last assignment. We now track declarations and extend their mutable range 
to account for each reassignment.
2024-03-26 13:29:24 -07:00
Joe Savona fbd68eef7f Fix for invalid mutable range in phi with backedge
Fixes the repro added in 947832009997bf9149e88e583c46cc39f6a6136c - previously 
when computing mutable ranges of phis, we didn't check that all operands had 
been visited. This meant that a backedge could allow a phi's mutable range to 
start at 0. Then in PropagateScopeDeps, we might see reject dependencies of a 
scope since they appeared to start after a scope — only because the scope's 
start was incorrectly too early. 

The fix here is to initially set phi.id mutable ranges based on only on operands 
that are already visited. Then, during/after the fixpoint iteration of 
InferMutableRanges, we start account for all operands since we know they've been 
visited at least once and have a real range.
2024-03-25 21:36:33 -07:00
Joe Savona 48ecaf92d5 Reset scopes from AnalyzeFunctions in InferReactiveScopeVariables
Updates InferReactiveScopeVariables to first prune scopes attached during 
AnalyzeFunctions. This ensures that after this pass the only scopes that exist 
on identifiers in the outer program are those that the pass explicitly inferred, 
and not accidentally leftover.
2024-03-25 15:31:23 -07:00
Joe Savona 29ab0f835e Repro for scopes from AnalyzeFunction influencing outer compilation
We share identifiers between outer functions and inner function expressions, 
which means that scopes inferred during AnalyzeFunctions can be retained on 
Identifiers in the outer function. If InferReactiveScopeVariables doesn't happen 
to visit an identifier we currently retain that scope information and use it 
when constructing scopes, even if there doesn't technically need to be a scope 
for that value. 

Fixed in the next PR.
2024-03-25 15:31:22 -07:00
Lauren Tan e6ce5e4922 [eslint] Update devDep to match types 2024-03-25 15:00:34 -04:00
Lauren Tan 15abd38911 Cleanup unused eslint-browser and js-fuzzer packages
These were never used, so let's clean them up
2024-03-25 15:00:31 -04:00
Sathya Gunasekaran 70302048a0 Update yarn.lock 2024-03-25 16:41:13 +00:00
Joe Savona 2ce112dbf2 Repro for missing dep with while/if using externally declared "index" variable
What happens here is that the phi node for `i` has its mutable range set to 
start at 0, because it has a back edge and we haven't initialized the mutable 
ranges of all its operands yet when we iterate the operands and set range.start 
= min(start of operand starts). 

Then the corresponding scope has its range set to start at 0 too. When 
PropagateScopeDeps runs it sees that `b` is from instruction 1, which is after 
the start of the scope (0), so it thinks `b` isn't a valid dependency. 

The fix is in InferMutableRanges, where we need to make sure that phis ignore 
their operand's ranges until those ranges are initialized.
2024-03-22 21:49:39 -07:00
Sathya Gunasekaran db7e7c7fae Support method call when validating useEffect
Treat MethodCall similarly to CallExpression when validation useEffect
2024-03-22 23:42:27 +00:00
Lauren Tan 1f45b13fda Update yarn.lock 2024-03-22 18:59:44 -04:00
Joe Savona 200d2c379d Fix for loops with value block index initial value 2024-03-22 14:51:36 -07:00
Joe Savona f4ff1a28e7 Fix for destructuring with partial context variables 2024-03-22 14:32:50 -07:00
Joe Savona 73fa712108 Repro for destructuring with partial context variables 2024-03-22 14:32:47 -07:00
Jan Kassens d7d82e7bee Correct eslint-plugin-react-compiler dependencies
Correct eslint-plugin-react-compiler dependencies 

- The eslint plugin doesn't actually depend on the babel plugin as it compiles 
in the dependencies. - `zod` and `zod-validation-error` were missing, but 
required to run the plugin. - Update to the `hermes-parser` dependency just to 
keep it updated.
2024-03-22 17:03:30 -04:00
Joe Savona 4575c8a5f6 Detect hoisting where the reference is a reassignment
Our logic to detect hoisting relies on Babel's `isReferencedIdentifier()` to 
determine whether a reference to an identifier is a reference or a declaration. 
The idea is that we want to find references to variables that may be hoistable, 
before the declaration — the definition of hoisting. But due to the bug in 
isReferencedIdentifier, we skipped over reassignments of hoisted variables. The 
hack here checks if an identifier is a direct child of an AssignmentExpression, 
ensuring we visit reassignments.
2024-03-22 13:49:50 -07:00
Joe Savona fb0f7e5c39 Repro for closures reassigning hoisted let variables
Adds examples for closures that reassign hoisted let bindings. We currently 
compile these as context variables without hoisting, so we hit an invariant in 
InferReferenceEffects when the variable is referenced before being declared.
2024-03-22 11:53:29 -07:00
Joe Savona e7d0c81d13 Add todo for for loops with context iterator variable
Detect the previous case — for loops where the iterator is a context variables — 
and throw a todo rather than hitting the invariant.
2024-03-22 11:46:28 -07:00
Joe Savona bc516409a6 Repro of for loop with context variable iterator variable
`let` bindings of context variables are lowered to a DeclareContext + 
StoreContext, which breaks codegen for `for` loops which expect that all 
statements of the init block will lower to variable declarations. The two 
instructions produce a variable declaration and a reassignment.
2024-03-22 09:15:29 -07:00
Joe Savona 7dc08d79be PropagateScopeDeps treats switch w only default as unconditional
If we have a switch with only a default case, then that code will be executed 
unconditionally. PropagateScopeDeps can take advantage of this to record 
dependencies in these cases as unconditional, which avoids the issue seen in the 
previous PR.
2024-03-22 09:02:56 -07:00
Joe Savona 3229302222 Repro for "expect trees to be >=2 elements long" invariant
These cases have a switch with only a default, so we create a "conditional" 
child dependency tree and no other trees which hits the invariant.
2024-03-22 08:53:30 -07:00
Mofei Zhang ee93b62f51 [ez] Patch BuildHIR to match unlabeled breaks to switch and loops 2024-03-22 16:34:09 -04:00
Mofei Zhang f6472522da [repro] Fixture for incorrect lowering for break (label within loop)
--- 

unlabeled breaks must goto the nearest iteration or switch boundary (not labeled 
block) 

--- 

Sprout fails with the following: 

``` 

FAIL: bug-unlabeled-break-within-label-loop 

>> Unexpected error during test: 

Found differences in evaluator results 

Non-forget (expected): 

(kind: ok) ["0 @A","0 @B","0 @C","1 @A"] 

Forget: 

(kind: ok) ["0 @A","0 @B","0 @C","1 @A","1 @C"] 

```
2024-03-22 16:14:35 -04:00
Mofei Zhang e047db8d03 [repro] Repro for control flow bug in PropagateScopeDependency
--- 

Thanks to @josephsavona for finding this bug. This is another example of why we 
really want hir-everywhere. 

Forget output currently nullthrows because we believe `obj.a` is run 
unconditionally in source (missing the break/returns out of this scope)
2024-03-21 17:44:13 -04:00
Joe Savona 3a6a2e7e4a Repro for "context variables are always mutable" error w callbacks
I haven't debugged to understand exactly why this pattern fails, but there are a 
few instances of this internally. It's especially weird because 

```javascript 

// @enableAssumeHooksFollowRulesOfReact 
@enableTransitivelyFreezeFunctionExpressions 

function Component(props) { 

const [_state, setState] = useState(); 

const a = () => { 

return b(); 

}; 

const b = () => { 

return ( 

<> 

<div onClick={() => onClick(true)} /> 

<div onClick={() => onClick(false)} /> // <---- only repros if there's a second 
call! 

</> 

); 

}; 

const onClick = (value) => { 

setState(value); 

}; 

return <div>{a()}</div>; 

} 

``` 

Here, if `b()` only had one nested function expression that called `onClick` it 
would work. Also, if we disable `@enableTransitivelyFreezeFunctionExpressions` 
then it works. 

But the combination of multiple calls plus that mode causes "context variables 
are always mutable". I'm guessing we're freezing `onClick` twice and the second 
time reports an error since it calls `setState`.
2024-03-21 21:40:02 -07:00
Joe Savona f18a01bf59 Add todo for destructuring to context variables
We don't have `DestructureContext` yet, this adds a todo for the cases where we 
should use one.
2024-03-21 17:12:09 -07:00
Joe Savona e8073ff16c Repro for destructure declaration of context variable
We don't have a `DestructureContext` equivalent of `StoreContext`, so variables 
that are declared via destructuring and later reassigned trigger the invariant 
that all mentions of a variable must be consistently local or context. The next 
PR adds a todo for this case.
2024-03-21 17:12:09 -07:00
Joe Savona 231f29f6f3 Fix using context variable as JSX element tag
Updates the lowering for JSX element tag names to check if the identifier is a 
context or local variable and use the appropriate instruction kind.
2024-03-21 17:12:08 -07:00
Joe Savona 21bb8f9e75 Repro for inconsistent context/local variable reference
We currently assume JSX element tags are always locals, and need to check which 
load kind to use. Fixed in the next PR.
2024-03-21 17:12:08 -07:00
Joe Savona 0e72e2a00e Repro for nested method calls causing promoted property load invariant 2024-03-21 14:11:07 -07:00
Joe Savona 1b6afa1879 Repro for uninitialized value due to shadowed function name 2024-03-21 14:11:06 -07:00
Joe Savona a6b3adbe68 Repro for unsupported recursive function expressions 2024-03-21 14:11:06 -07:00
Joe Savona d31e10b406 Fix for type aliases in function inferring as function deps 2024-03-21 14:11:05 -07:00
Joe Savona 4bc676d3db Repro for undefined "hoisted" variable from type alias
We inadvertently think the type annotation on the function expression param is 
an identifier and create a LoadLocal for it, which fails. This happens to trip 
up on the InferReferenceEffects initialization check, which we had assumed would 
only fire for invalid hoisting cases (hence the specific error message).
2024-03-21 14:11:04 -07:00
Joe Savona 241b35463c Fix PruneMaybeThrows to update phi operand predecessor ids
When PruneMaybeThrows removes maybe-throw terminals, it's possible that the 
block in question reassigned a value s.t. it appears as a later phi operand. 
That phi has to be rewritten to reflect the updated predecessor block. 

Here we track these rewrites (transitively) and rewrite phi operands 
accordingly.
2024-03-21 14:11:04 -07:00