Commit Graph
1664 Commits
Author SHA1 Message Date
Joe Savona d70b2c2c4e Repro for unexpected label as value block terminal
This was one of the last invariants firing internally, this PR adds a minimal 
repro and the next PR makes it a todo.
2024-03-13 22:20:56 -07:00
Joe Savona 459679f91e Support type alias syntax
"Support" in the sense of dropping these on the floor and compiling, rather than 
bailing out with a todo. 

We already don't make any guarantees about which type annotations we'll preserve 
through to the output, so it seems fine for now to just drop type aliases.
2024-03-13 22:17:19 -07:00
Joe Savona 1b5ae0638e Fix block scoping of declarations with early return
I addressed some of the cases that lead to this invariant but there were still 
more. In this case, we have scopes like this: 

``` 

scope @1 declarations=[t$0] { 

let t$0 = ArrayExpression [] 

if (...) { 

return null; 

} 

} 

scope @2 deps=[t$0] declarations=[t$1] { 

let t$1 = Jsx children=[t$0] ... 

} 

``` 

Because scope 1 has an early return, PropagateEarlyReturns wraps its contents in 
a label and converts the returns to breaks: 

``` 

scope @1 declarations=[t$0] earlyReturn={t$2} { 

let t$2 

bb0: { 

let t$0 = ArrayExpression [] 

if (...) { 

t$2 = null; 

break bb0; 

} 

} 

} 

scope @2 deps=[t$0] declarations=[t$1] { 

let t$1 = Jsx children=[t$0] ... 

} 

``` 

But then MergeReactiveScopesThatInvalidateTogether smushes them together: 

``` 

scope @1 declarations=[t$1] earlyReturn={t$2} { 

let t$2 

bb0: { 

let t$0 = ArrayExpression [] // <--- Oops! We're inside a block now 

if (...) { 

t$2 = null; 

break bb0; 

} 

} 

let t$1 = Jsx children=[t$0] ... 

} 

``` 

Note that the `t$0` binding is now created inside the labeled block, so it's no 
longer accessible to the Jsx instruction which follows the labeled block. This 
isn't an issue with promoting temporaries or propagating outputs, but a simple 
issue of the labeled block (used for early return) introducing a new block 
scope. The solution here is to simply reorder the passes so that we transform 
for early returns after other optimizations. This means the jsx element will 
basically move inside the labeled block, solving the scoping issue: 

``` 

scope @1 declarations=[t$1] earlyReturn={t$2} { 

let t$2 

bb0: { 

let t$0 = ArrayExpression [] // ok, same block scope as its use 

if (...) { 

t$2 = null; 

break bb0; 

} 

let t$1 = Jsx children=[t$0] // note this moved inside the labeled block 

} 

} 

```
2024-03-13 21:29:58 -07:00
Joe Savona f7ca10a0c7 Repro for "no value for temporary"
I addressed some of the cases that lead to this invariant but there were still 
more. In this case, we have scopes like this: 

``` 

scope @1 declarations=[t$0] { 

let t$0 = ArrayExpression [] 

if (...) { 

return null; 

} 

} 

scope @2 deps=[t$0] declarations=[t$1] { 

let t$1 = Jsx children=[t$0] ... 

} 

``` 

Because scope 1 has an early return, PropagateEarlyReturns wraps its contents in 
a label and converts the returns to breaks: 

``` 

scope @1 declarations=[t$0] earlyReturn={t$2} { 

let t$2 

bb0: { 

let t$0 = ArrayExpression [] 

if (...) { 

t$2 = null; 

break bb0; 

} 

} 

} 

scope @2 deps=[t$0] declarations=[t$1] { 

let t$1 = Jsx children=[t$0] ... 

} 

``` 

But then MergeReactiveScopesThatInvalidateTogether smushes them together: 

``` 

scope @1 declarations=[t$1] earlyReturn={t$2} { 

let t$2 

bb0: { 

let t$0 = ArrayExpression [] // <--- Oops! We're inside a block now 

if (...) { 

t$2 = null; 

break bb0; 

} 

} 

let t$1 = Jsx children=[t$0] ... 

} 

``` 

Note that the `t$0` binding is now created inside the labeled block, so it's no 
longer accessible to the Jsx instruction which follows the labeled block. This 
isn't an issue with promoting temporaries or propagating outputs, but a simple 
issue of the labeled block (used for early return) introducing a new block 
scope. The solution (in the next PR) is to simply reorder the passes so that we 
transform for early returns after other optimizations. This means the jsx 
element will basically move inside the labeled block, solving the scoping issue: 

``` 

scope @1 declarations=[t$1] earlyReturn={t$2} { 

let t$2 

bb0: { 

let t$0 = ArrayExpression [] // ok, same block scope as its use 

if (...) { 

t$2 = null; 

break bb0; 

} 

let t$1 = Jsx children=[t$0] // note this moved inside the labeled block 

} 

} 

```
2024-03-13 21:29:54 -07:00
Jan Kassens 72d374e978 [linter] rename ReactForgetDiagnostics to ReactCompilerRule
[linter] rename ReactForgetDiagnostics to ReactCompilerRule
2024-03-14 11:53:32 -04:00
Joe Savona ca8e0d4527 Support multiple declarations in for init
This was an oversight in codegen. The entire pipeline supports multiple values 
in a for initializer, but codegen was dropping all but the first initializer.
2024-03-13 15:45:50 -07:00
Joe Savona fe29f46de8 Repro for multiple declarations in for initializer codegen issue 2024-03-13 15:31:51 -07:00
Joe Savona 2940440dfb Fix for mutable ranges ended early with interleaving
Fixes T180504437. In MergeOverlappingReactiveScopes we track the active scopes 
and mark them as "ended" when reaching the first instruction after their mutable 
range. However, in cases of interleaving that will be merged, we could 
previously mark a scope as complete when it's original range was completed, even 
though the range would get extended post-merge. The fix here detects 
interleaving earlier, and eagerly updates the mutable ranges of the merged 
scopes to ensure that neither is "ended" earlier than it should. 

The repro here fails without this change.
2024-03-13 13:52:29 -07:00
Joe Savona 79e3fc0acb Fix for method call not memoizing in same scope as outer call
Fixes T175282980. InferReactiveScopeVariables had logic to force assigning a 
scope to MethodCall property lookups with the idea of forcing the method call 
lookup to be in the same scope as the method call itself. But this doesn't work 
if we never assign a scope to the method call! That can happen if we're able to 
infer that the method call produces a primitive and doesn't need memoization. 

This PR changes things so that: 

* InferReactiveScopeVariables no longer assumes that MethodCall property values 
need a scope 

* We run a separate pass that ensures that _if_ a MethodCall has a scope, that 
it's property is in the scope, and that otherwise its property doesn't get a 
scope. This is similar to the existing passes that force a single scope for 
related instructions like ObjectMethod+ObjectExpression and fbt operands/calls.
2024-03-13 13:52:29 -07:00
Joe Savona dc7ed065c0 Fix missing declaration invariant for "for" within try/catch
Fixes T180509722. What happened is that the logic in LeaveSSA to find 
declarations within for initializers wasn't working with try/catch because the 
initializer block gets broken up with a maybe-throw after every instruction that 
can throw. These maybe-throws can then get turned into gotos by 
PruneMaybeThrows, so LeaveSSA has to handle both cases. 

The new logic scans from the start of the init block until reaching the end, and 
creates declarations for all StoreLocals. Note that we don't yet support 
maybe-throw in value blocks — that's already a todo — so the change here simply 
allows us to compile farther until reaching that other todo. But i've 
double-checked the HIR and it looks correct for this case, so it should just 
work once we fix that todo. I've also added a comment to help us remember (and 
of course, we'd have to add a snap fixture too)
2024-03-13 13:52:28 -07:00
Jan Kassens b666bd1637 [lint] do not report issues when a matching flow suppression is present
Based on implementation of a similar case in D54776832.
2024-03-13 11:40:35 -04:00
Lauren Tan 56d96ba203 [forget-runtime] Remove invariant dep
Reduces a dep needed to be installed by users of the polyfill
2024-03-11 16:31:46 -04:00
Mofei Zhang e09c631aff [be][snap] Make testfilter file recognize glob patterns, remove @skip 2024-03-11 13:55:24 -04:00
Mofei Zhang c84b817972 [be][snap] Warm up workers when in watch + filter mode 2024-03-11 13:55:22 -04:00
Mofei Zhang 2fc910d580 [optim] NewExpressions do not mutate the caller (class type)
``` 

function Foo() { 

const MyClass = getClass(); 

// following line is not expected to change MyClass 

return new MyClass(); 

} 

```
2024-03-11 13:03:45 -04:00
Mofei Zhang df84152780 [hir] Attach fnType to HIRFunction
--- 

(This came out of running a sync and observing hundreds of bailouts due from 
this validation) 

Reading `fnType` from environment overgeneralizes, as inner functions are 
usually not the type of the outer react function. 

``` 

// Component type 

function Component() { 

// not Component type 

const helper = () => {...}; 

} 

``` 

Let's attach fnType to `HIRFunction` and use that for our inference + 
validations
2024-03-08 17:06:37 -05:00
Joe Savona 16852386a5 Support more cases of reassignment within value blocks
Fixes T175283039 — it's totally fine to have a StoreLocal as an instruction in a 
value block, so long as its a reassignment.
2024-03-08 13:59:22 -08:00
Joe Savona 5f3ed3f724 Fixture for reassignment within value block
Fixture from T175283039, a reassignment within an expression can sometimes 
generate a StoreLocal within a value block. Depending on the case this can end 
up as the last instruction of the block, which then hits an invariant.
2024-03-08 13:59:21 -08:00
Joe Savona 052f5fe973 Todo for "unreachable" possibly-hoisted functions
Adds a todo in HIRBuilder, before we prune unreachable code we check if there 
were any function expressions. Realistically that's only going to occur for 
hoisted functions, so this lets us target a todo rather than hit an invariant.
2024-03-08 13:59:20 -08:00
Joe Savona 7605e89a50 Repro for "unreachable" hoisted function
For T175282529. We already detect hoisted functions and have a specific todo for 
them, but in this case the function is in unreachable code that gets pruned 
during BuildHIR. The later check for hoisted functions doesn't find it.
2024-03-08 13:59:20 -08:00
Joe Savona fe91bcefd2 Fix promotion of catch bindings w/in function expressions
One of our visitors wasn't visiting TryTerminal's handlerBinding, which meant 
that we missed renaming those identifiers in RenameVariables. I also updated the 
printers to print this binding.
2024-03-07 09:21:33 -08:00
Joe Savona 449aa70f99 Fix promotion of locals referenced outside of scopes 2024-03-06 16:19:28 -08:00
Joe Savona 854a810f05 Turn value block within try/catch into todo (from invariant)
Within a try/catch, every instruction is followed by a maybe-throw terminal. 
This currently breaks the logic in BuildReactiveFunction which tries to 
reassemble the value block, since it isn't expecting the maybe-throw. 
Conceptually the logic should just ignore it — we could even flatten away 
maybe-throw terminals before this pass — but for now since this pattern is rare 
we can just make it a todo.
2024-03-06 15:28:36 -08:00
Joe Savona 52875a72fe Invariant if codegen tries to emit a temporary as an identifier
For T181507827 — adds an invariant in codegen when emitting identifiers to 
ensure that we only create babel Identifier nodes for nodes that the compiler 
has explicitly promoted to valid, named identifiers. This means that we'll fail 
for unnamed temporaries (previously caught), as well as promoted temporaries 
that somehow didn't get renamed by RenameVariables (newly caught).
2024-03-06 21:50:03 -08:00
Joe Savona ebbada309d MemoizeFbtOperandsInSameScope operates on HIR
Moves this pass to operate against HIRFunction instead of ReactiveFunction, no 
logic changes.
2024-03-06 14:02:18 -08:00
Joe Savona e7fcc4e6a8 Rename promoteTemporary helpers
Renames the helpers for promoting temporaries to named identifiers, per feedback 
earlier in the stack.
2024-03-06 11:16:44 -08:00
Joe Savona b7026ede39 Avoid generating conflicts w global names
Adds a visitor to collect all the globals that are referenced within the 
function, and then uses this list to avoid synthesizing variables with 
conflicting names. This is used in both RenameVariables (for promoted 
temporaries) and Codegen (for `$` and change variables only, so far, but this 
can be extended in follow-ups).
2024-03-06 11:07:11 -08:00
Joe Savona 8faed2af4c Avoid conflicting names for reactive scope codegen
This is a key part of avoiding generating conflicting names in our output. To 
start, RenameVariables now returns a Set of the unique identifier names that 
exist in the function. Codegen uses this to avoid generating duplicate names for 
change variables and for the `$` useMemoCache variable. Rather than always emit 
`$` or `c_N`, codegen checks that this name would not conflict and appends an 
incrementing suffix until it finds a unique name. 

Note that it's still possible for us to generate conflicts with global 
variables, both during RenameVariable and Codegen. The next step will be to 
avoid conflicts with globals.
2024-03-06 11:07:11 -08:00
Joe Savona 2ae0f36543 Promote and rename within nested functions
Another title for this PR could be "Yet another reason for HIR-everywhere" 

ReactiveFunctionVisitor doesn't traverse into HIRFunctions from 
FunctionExpression and ObjectMethod values. This means that 
PromoteUsedTemporaries and RenameVariables also weren't traversing into such 
functions, and those values weren't getting promoted and renamed correctly. 

This PR updates ReactiveFunctionVisitor with a method that can optionally be 
invoked to traverse an HIRFunction and call the appropriate visitor methods. 
PromoteUsedTemporaries and RenameVariables invoke this to ensure they visit all 
places, even in nested HIRFunctions.
2024-03-06 11:07:10 -08:00
Joe Savona 99b9da4b00 Move remaining promotion of temporaries out of codegen
I realized that codegen still had a fallback for generating identifier nodes for 
unnamed temporaries. This PR updates codegen to throw if it needs to generate an 
identifier for a temporary, and updates earlier passes to promote temporaries to 
named values in all the cases that were missed: 

* BuildHIR needs to promote temporaries for temporaries in destructuring 
bindings and catch clause bindings 

* PromoteUsedTemporaries has to promote temporaries for destructured function 
parameters or function params that are context variables.
2024-03-06 11:07:09 -08:00
Joe Savona 31e128a441 Use opaque type for Identifier.name for improved correctness
Uses an enum for Identifier.name to distinguish originally named identifiers vs 
promoted temporaries. An opaque type for the named identifier variant makes it 
hard to accidentally create that type.
2024-03-06 11:07:08 -08:00
Joe Savona 329bb555a0 [RFC] Stabilize naming of promoted temporaries
When the compiler promotes temporary values to named variables, we currently 
eagerly assign a name using the temporary's IdentifierId. This means that we're 
sort of stuck with this name later in compilation, and RenameVariables can't be 
100% sure whether a 't0' variable is a temporary or not. As a result, the names 
of these promoted temporaries is influenced by how many temporaries we happened 
to create during compilation (and what the next available identifier id was), 
making them fluctuate more as we iterate on the compiler. 

This is an RFC for showing how we can stabilize these names. The key elements: 

* Distinguish promoted temporaries from other named identifiers. Here we use a 
hack, naming them starting with '#t' or '#T', since '#' isn't a valid identifier 
starting point. This lets us keep all of our logic that looks for non-null 
identifiers names to distinguish named/unnamed, while also distinguishing real 
names from generated names (if this was Rust, we'd use an Enum and have a 
"isNamed()" method on it that was true for real/temporary names and false 
otherwise) 

* In RenameVariables, detect generated names and fall back to generating the 
next available `tN`-style name (or `TN` for JSX tags). 

* To reduce thrash overall, RenameVariables no longer keeps a global "next id" 
value that uses to distinguish all conflicting identifiers, instead we restart 
at 0 whenever we find a conflict, and keep bumping until we find a free name. 
Thus if both `foo` and `bar` had conflicts, we previously would end up with 
`foo$0` and `bar$1` as the deduped names, but now will end up with `foo$0` and 
`bar$0`. 

## RFC 

I'm open to feedback on the approach. Two main questions: 

* How to annotate promoted temporaries. The most type-safe option is to change 
`Identifier.name` to be a union of `{kind: 'named', value: string} | `{kind: 
'promoted', value: string} | `{kind: 'temporary'}` though TS then wouldn't allow 
`identifier.name.value` (even as nullable) since it doesn't exist on one of the 
variants. Maybe we could type the temporary one as `{kind: 'temporary', value?: 
null}` so the value has to be null but you can always access that property? 

* ?? Other concerns about the approach? We could keep the global 
auto-incrementing id rather than attempting to reset to 0 for each conflict.
2024-03-06 11:07:08 -08:00
Joe Savona d1d6310f25 Conditional assignment of unmemoized value doesnt invalidate later scopes
The previous implementation used IdentifierId, but since this pass operates 
after LeaveSSA the identifier ids are no longer distinct for different SSA 
instances. Instead we use the Identifier instance, which preserves SSA 
information (even ever LeaveSSA) and allows distinguishing between variables 
whose value always changes vs variables that may be reassigned such that they 
don't always invalidate. 

In the future when we use HIR everywhere, this pass should use the HIR CFG to 
understand that phi nodes whose operands all will always invalidate can also be 
treated as always invalidating. 

## Test Plan 

Synced to www, 91 files have output changes 
(https://fburl.com/everpaste/3e3hjpjs). I spot checked these and confirmed that 
they are all from cases where there was already missing memoization of earlier 
values, where we now can prune later reactive scopes that depend on the 
un-memoized values.
2024-03-06 11:07:07 -08:00
Joe Savona 4af3ff1e75 Fixture for transitive invalidation of effect dep
This fixture shows that this optimization partially improves transitive checking 
for validateMemoizedEffectDependencies.
2024-03-06 09:28:03 -08:00
Joe Savona f4455ef51a Prune scopes whose deps always invalidate
Implements the optimization described in the previous PR: if we know that a 
scope's dependency will _always_ invalidate (it is not memoized and it is 
guaranteed to be a new object if the instruction executes, such as an array or 
object literal), then we can prune that scope. The invalidation is transitive: 
we track always-invalidating types from within scopes, and if their scope gets 
invalidated we prune downstream scope that depend on them. 

## Test Plan 

Tested via #2639 - see https://fburl.com/everpaste/3e3hjpjs. 91 files change 
output due to reactive scopes which would always invalidate due to always 
invalidating dependencies.
2024-03-06 09:28:02 -08:00
Joe Savona 48e08c42be Fixtures where unmemoized value invalidates later scopes
These fixtures demonstrate how currently, even if the dependency of a scope 
doesn't get memoized (ie the scope gets pruned), we don't remove later scopes 
that depend on that value. Those later scopes will always invalidate, so we 
might as well remove them.
2024-03-06 09:28:01 -08:00
Michael Vitousek 22ea72d4e9 Allow global mutation within useEffect (#2646)
Summary: Currently Forget bails on mutations to globals within any callback function. However, callbacks passed to useEffect should not bail and are not subject to the rules of react in the same way.

We allow this by instead of immediately raising errors when we see illegal writes, storing the error as part of the function. When the function is called, or passed to a position that could call it during rendering, we bail as before; but if it's passed to `useEffect`, we don't raise the errors.
2024-03-05 11:54:29 -08:00
Jordan Brown f5d99baf8e Add config option to ignore 'use no forget' directives
This will let us test without taking into account the existing 'use no forget' 
directives to better understand what validations we may need to build. 

There are two other files that look for this directive that do not seem to take 
compiler options: 

1. 
https://github.com/facebook/react-forget/blob/408617ec8a5caa815f61d4204cb3fec0775593ca/react/scripts/babel/transform-forget.js#L25 

2. 
https://github.com/facebook/react-forget/blob/408617ec8a5caa815f61d4204cb3fec0775593ca/packages/babel-plugin-react-forget/scripts/jest/makeTransform.ts#L119 

Do I need to do anything for those?
2024-03-04 19:00:48 -05:00
Sathya Gunasekaran 7f994241b0 Infer type of ref argument
Extend type inference to infer second argument of component as a ref type
2024-02-29 14:47:52 -08:00
Sathya Gunasekaran ad8f19675c Infer type of React function
Infer if a function is a component or hook when we're deciding to compile a 
function and store that in the environment. 

This is used in passes like InferReferenceEffects rather than having to re-parse 
the name in each pass.
2024-02-29 14:47:51 -08:00
Sathya Gunasekaran 19e44a4a69 Allow ref argument to be mutated
Previously, Forget would throw if _any_ of the arguments to a component are 
modified. This isn't quite right as a ref argument can be modified. 

This PR assumes the second argument of a component to be a ref and allows it to 
be mutable. 

A future PR will add types to this argument so the validateRefAccessDuringRender 
can catch if ref is mutated in render. This PR contains a todo test for this.
2024-02-29 14:47:49 -08:00
Mofei Zhang 9d25ced018 [instrument] Optional globalGating means no global gating 2024-02-29 10:04:37 -08:00
Mofei Zhang 01454d1414 [other] Make global gating (e.g. DEV) check configurable 2024-02-29 09:44:28 -08:00
Joe Savona 6dbec55791 Support prefixed useState due to pre-bundling 2024-02-28 13:12:56 -08:00
Joe Savona 9d5a01359f Move creation of scopes for primitives into a separate pass
Rather than force scopes to be created for primitives within
InferReactiveScopeVariables, here we move the creation of scopes for these
instructions to a later pass. Later in the pipeline we have more context, such
as whether e.g. a primitive or propertyload is being accessed within a scope or
not, and whether it therefore needs its own scope or not.
2024-02-27 12:01:32 -08:00
Joe Savona 45216dc3d9 Track next scope id on Environment
Currently we allocate all reactive scopes during a single pass, 
InferReactiveScopeVariables, using a local incrementing number to assign 
ScopeIds. This means we can't easily create additional scopes later since we 
don't know the next available scope id. 

Here we add `Environment.nextScopeId` and use that to synthesize scope ids.
2024-02-27 11:39:22 -08:00
Sathya Gunasekaran 130b809ac2 Allow property loads from hook
Looking up certain properties on a hook is a common pattern for logging. 

It's non-ideal but it's not a bug to do this. 

This updates Forget to not error on this pattern.
2024-02-28 16:19:43 -08:00
Sathya Gunasekaran 8879a2dd49 Remove sprout from package.json 2024-02-28 15:22:13 -08:00
Mofei Zhang a06ded902a [other] Change instrumentation to use an optional gating identifier; record
filepath 

Internal rollout currently has a good number of test failures. 
`enableEmitInstrumentForget` can help developers understand which functions / 
files they should look at: 

``` 

// input 

function Foo() { 

userCode(); 

// ... 

} 

// output 

function Foo() { 

if (__DEV__ && inE2eTestMode) { 

logRender("Foo", "/path/to/filename.js"); 

} 

const $ = useMemoCache(...); 

userCode(); 

} 

```
2024-02-27 22:06:36 -08:00
Joe Savona 5acfc13575 Repro for invariant on value block in try/catch
The code for value block handling assumes a small set of terminal kinds, but 
try/catch causes the entire body to get wrapped in MaybeThrow terminals. We need 
to skip over these and delegate to the inner content.
2024-02-26 16:21:56 -08:00