Commit Graph

1567 Commits

Author SHA1 Message Date
Mofei Zhang 707d9643dd [logger] Log todo when encountering "use no forget"
--- 

This change simply logs on every function we encounter with a `use no forget` 
directive. A few nuances -- `compilationMode: "infer"` only compiles functions 
we infer to be 'react functions'. 

```js 

// `add` would not be compiled, as it has no jsx, no hook calls, 

// and is not named as a component or hook 

function add(a, b) { 

return a + b; 

} 

``` 

With this PR, we would report todos for functions that Forget wouldn't 
ordinarily try to compile. 

```js 

// Todo: Skipped due to "use no forget" directive. 

function add(a, b) { 

"use no forget"; 

return a + b; 

} 

``` 

This seems fine to me as (1) it's a bit nonsensical to have a `use no forget` 
direction on a non-react function, and (2) we're goalling on getting `use no 
forget`s down to 0.
2024-02-05 16:05:09 -05:00
Joe Savona 195b5e5a24 Fix sprout 2024-02-05 21:51:18 -08:00
Joe Savona b79a5289fc Resolve type aliases
The goal of this PR is to move towards a uniform representation for all type
declarations, whether they are named type aliases, function declarations, or
inline annotations. We now assign every non-primitive type declaration (named or
anonymous) a unique DeclarationId. In the next PR, we'll also re-map inline
annotations back to this declaration id when encountering them.

This PR is extremely gross and my intent is to refactor a bunch of things in the
HIR to allow this to be less gross. Challenges:

* Babel name resolution requires using scopes but i really want to just work
with plain nodes, since NodePath and TypeScript do _not_ get along. So here, i
find all identifiers and store a mapping of identifier -> scope, so that i can
later look them up if necessary.

* HIR doesn't have a notion of a declaration id, and in general we don't want
to extend HIR. So i end up with a whole bunch of side table information and
indirection. For example, a function doesn't know it's own declaration id.
So we have to look it up. Function params don't track their Forest type, so
we have to look them up on the function declaration. Etc.
2024-02-05 21:51:13 -08:00
Joe Savona 292a247cc4 Add type to DeclareLocal 2024-02-05 21:51:11 -08:00
Joe Savona 76aaf32c55 HIR StoreLocal.type uses babel type 2024-02-05 21:51:09 -08:00
Joe Savona 9ec9331def Emit function param and return type annotations 2024-02-05 21:51:09 -08:00
Sathya Gunasekaran 29cb62ad6c Add type information for jsx/runtime 2024-02-02 10:56:44 -05:00
Lauren Tan 7bce302421 Make Other mutation validation message more generic
The current error message "This mutates a global or a variable after it 

was passed to React" no longer makes sense since we now have more 

specific error messages for different kinds of Effect.Mutate or 

Effect.Stores. This replaces the fallthrough "Other" case with a 

more generic message. It's not perfect, but it's a little more accurate 

than what is currently emitted 

The proper fix might be to treat functions as mutable objects and allow 

the mutation, or special case `Function.displayName`. For now though 

this PR just updates the message in the meantime so it's less 

confusing.
2024-02-01 11:02:30 -05:00
Lauren Tan a0aa66ca8f Add test for function property mutation
There were no previous test paths that tested it, so I'm adding the example from 
https://github.com/facebookexternal/forget-feedback/issues/32 as a fixture
2024-02-01 11:02:30 -05:00
Joe Savona d55420c430 Allow prefixed hooks for compiling bundled code
We're doing some internal benchmarking using a lightweight bundler that @pieterv 
wrote for experimentation purposes. It's designed to fully preserve Flow type 
annotations so we can experiment with type-driven compilation and test out what 
benefits we might get from "cross-module" compilation more easily (ie by just 
bundling together a few modules so we can see them all as one). 

However, the bundler renames local variables and imports, so that a reference to 
`useMemo()` might end up as `React$useMemo()` or similar. This PR adds a flag to 
tell the compiler that builtin hooks might be prefixed and resolve them 
appropriately.
2024-01-30 22:11:17 -05:00
Mofei Zhang d0bb1fed61 [be] Explicit todo diagnostics for hoisting
--- 

Currently, we error on non-hoisted identifiers in EnterSSA with a somewhat 
cryptic message. This PR changes `BuildHIR` hoisting logic to find ALL hoistable 
bindings, then error when we try to lower hoisting for unsupported declaration 
types. 

Two benefits to this refactoring: 

- Dedups "unhandled identifier declaration" logic (previous to #2552 and this 
PR, we did this check in three places). 

- More explicit todo diagnostic messages when we cannot hoist a declaration
2024-01-31 10:59:25 -05:00
Mofei Zhang 69733c5ad8 [hoisting][patch] use Babel identifier apis in BuildHIR hoisting logic
--- 

Three functional changes: 

- Instead of visiting all identifier references, explicitly traverse only 
function decls/exprs. This avoids bugs like accidentally hoisting inline 
references 

```js 

// input 

const x = identity(y); 

const y = 2; 

// lowered HIR before this PR (simplified) 

[0] DeclareContext HoistedConst y$0 

[1] LoadContext y$0 

[2] StoreLocal Const x$5 = identity([1]) 

``` 

- Rely on `isReferencedIdentifier()` instead of manually checking member 
properties / assignments, which is error prone 

```js 

// added fixture hoisting-repro-variable-used-in-assignment 

const callbk = () => { 

// before this PR, we skip hoisting x because it's part of a declaration 

const copy = x; 

return copy; 

}; 

const x = 2; 

return callbk(); 

``` 

- Visit lvalues after rvalues. This allows for recursive self-references (e.g. 
factorial) 

From the Babel side, this change relies heavily on babel's scope binding 
resolution logic. My understanding is: 

- Babel guarantees node objects are uniqued (`node1 === node2` <--> node1 and 
node2 are the same node in the ast) 

- Each binding has exactly one `bindingIdentifier` (`binding.identifier`, 
`getBindingIdentifier`, etc) which is identifier node @ its declaration site 

```js 

// x is a binding identifier 

const x = 2; 

// foo is a binding identifier 

function foo() { 

} 

// param is a binding identifier 

(param) => {...} 

// this bar is a binding identifier 

let bar; 

// but not this bar 

bar = 2; 

```
2024-01-31 10:59:25 -05:00
Mofei Zhang c3e9cba0bb [patch] PruneHoistedContexts should traverse lambdas 2024-01-29 17:45:44 -05:00
Mofei Zhang 74d8a18637 [optim] All nested properties in refs are ref values
Forget currently removes memoization of callbacks that have `mutate` effects on 
`ref` inner properties. @gsathya pointed out that our existing compiler behavior 
is to (1) NOT extend mutable ranges for functions that mutate `ref.current` and 
(2) extend mutable ranges for functions that mutate `ref.current.inner`. 

```js 

// input 

function Component() { 

const ref = useRef({ text: null }); 

const handleChange = useCallback((e) => { 

ref.current.text = e.target.value; 

}); 

return <input onChange={handleChange} />; 

} 

// output 

function Component() { 

const ref = useRef({ text: null }); 

// now unmemoized! 

const handleChange = (e) => { 

ref.current.text = e.target.value; 

};
2024-01-29 17:04:52 -05:00
Mofei Zhang 788182f709 [patch] Patch edge case: RenameVariables should visit lvalues
--- 

This is likely a rare edge case, but it does produce a parse error. 

RenameVariables visits all identifier references to ensure we don't end up 
producing conflicting variable declarations, using a stack of block scopes to 
check "in scope variables". 

This pass is currently built to be conservative -- we explicitly rename shadowed 
variables, and visit all rvalue references. The issue is for this IR: 

``` 

{ 

1.  decl t0; 

2.  scope 0 { 

3.    reassign t0 = ... 

4.    read(t0) 

5.  } 

6.  let t0 = ... 

7.  read(t0); 

} 

``` 

We currently visit t0 only on line 4 and 7 (and never rename t0). Instead we 
should visit lvalues (declaration sites) which occur earlier than rvalues 
(visiting lines 1 and 6 will show conflicting declarations)
2024-01-26 15:29:09 -05:00
Lauren Tan 4ec05660a2 [be] Update to node 20 actions
Node.js 16 which various github actions (v3) were using is no longer supported 
by github and was spewing a bunch of warnings
2024-01-30 15:46:19 -05:00
Joe Savona bc145f6f1f Support customizable eslint suppressions
The compiler bails out of compiling code that contains suppressions of the 
official React ESLint rules. However, some apps may use additional rules that 
they want to trigger bailouts for, or use the official rules under a different 
name (we do this at Meta). This PR adds a compiler flag to specify a custom set 
of line rule names, suppression of which should trigger a bailout.
2024-01-29 16:58:49 -05:00
Lauren Tan 742a6f09bf [ez] Get rid of caniuse warning 2024-01-30 14:54:04 -05:00
Lauren Tan 6b49c4f2d5 [eslint-plugin] Cleanup package.json
Removes some unused packages and unused package.json fields
2024-01-25 10:29:32 -05:00
Joe Savona c92ad38375 Fix block scoping issues from MergeConsecutiveBlocks
Fixes the issues from the previous PR. It's a simple fix — we don't merge 
consecutive blocks if the successor block is some terminal's fallthrough.
2024-01-23 11:41:00 -08:00
Joe Savona b2f44c103b Fixtures demonstrating incorrect block scoping due to MergeConsecutiveBlocks
Fixtures from T173102122 and T173101739 demonstrating cases where 
MergeConsecutiveBlocks can move code out of its correct block scope, changing 
behavior or breaking the program, in cases where a control flow structure (such 
as switch) only has one non-returning control flow path. In these cases, the 
non-returning path gets merged with the fallthrough, effectively lifting that 
code out of the control flow structure and moving it into the outer scope. This 
can create dead code or just invalid code (with references to variables that are 
not in scope). 

Sprout fails on both of these fixtures: 

<img width="812" alt="Screenshot 2024-01-23 at 11 25 36 AM" 
src="https://github.com/facebook/react-forget/assets/6425824/d397ea22-3fa3-436e-b655-09a45781274b">
2024-01-23 11:40:56 -08:00
Joe Savona f9f084087f Fixture for reactively-controlled context variables
Mofei considered this case, it works thanks to the handling for function 
expressions earlier in the stack.
2024-01-23 08:59:21 -08:00
Joe Savona a023a2da72 Fixtures for control values that become reactive due to interleaving
See the previous PR, interleaved mutation can cause values that were not 
reactive to become reactive. I swear I had a case where this was observable, but 
I came up with it before reordering the PRs in this stack. I think my repro 
relied on an immutable reference to a mutable value, which is now handled in 
InferReactivePlaces. So here i'm just adding fixtures, and allowing this case 
since it's unobservable.
2024-01-23 08:59:20 -08:00
Joe Savona 4d84bee172 Propagate reactive scope dependencies transitively
During PruneNonReactiveDependencies, we sometimes need to promote a value from 
non-reactive to reactive if it ended up being grouped in the same reactive scope 
as some other reactive value. This generally happens due to interleaving 
mutations. 

In this case all downstream usage of the promoted value need to also be 
considered reactive. Fully propagating the reactivity requires re-running 
InferReactivePlaces, to account for things like control reactivity. We can't yet 
reuse that pass here though, because we haven't unified the pipeline on HIR yet. 

For now, we propagate the reactivity through local variables and downstream 
reactive scopes. See test fixtures for some examples that now correctly 
propagate reactivity and some that need the full reactivity inference to run 
correctly. The latter cases are handled in the next PR.
2024-01-22 15:35:56 -08:00
Joe Savona 57163f0a52 InferReactivePlaces account for immutable aliases of mutably aliased values
I found this by adding logic to reject inputs where reactivity gets newly 
propagated in PruneNonReactiveDependencies. It's possible to create a readonly 
alias to a mutable value such that we don't know the value is reactive yet when 
the alias is created. Thus we need to do a fixpoint iteration even if there are 
no loops in order to be able to revisit such aliases and reflow the reactivity 
forward. Example: 

```javascript 

const x = []; 

const y = x; 

const z = [y]; // y isn't reactive yet when we first visit this, so z is 
initially non-reactive 

y.push(props.value); // then we realize y is reactive. we need a fixpoint to 
propagate this back to z 

const a = [z]; // need an indirection to get past the partial propagation in 
PruneNonReactiveDependencies 

let b = 0; 

if (a[0][0]) { 

b = 1; 

} 

return [b]; 

``` 

Existing fixtures don't change because the basic reactivity propagation in 
PruneNonReactiveDependencies is enough to make common cases work. I confirmed 
that the new fixture does not work on previous PR in the stack.
2024-01-22 15:35:55 -08:00
Joe Savona 0894e35d94 More fixtures for reactivity and mutable aliasing (property load case) 2024-01-19 19:12:58 -08:00
Joe Savona 8be56418d3 InferReactivePlaces accounts for mutable aliasing
Fixes T175227223. When inferring reactivity, mutation of a value with a reactive 
input marks the mutable value as reactive. However, we also need to account for 
aliases: 

```javascript 

const x = []; 

const y = x; 

y.push(props.value); 

``` 

Previously we would have only considered `y` reactive here, but `x` also becomes 
reactive. 

The implementation extracts out a helper from InferReactiveScopeVariables that 
builds a `DisjointSet<Identifier>` of disjoint sets of mutably aliased values. 
InferReactivePlaces then treats all instances of each mutable alias group as 
equivalent for reactivity purposes.
2024-01-19 16:03:58 -08:00
Joe Savona a272cf9b0c Reactive control fixtures use multipass evaluation
Updates all of the reactive control dependency fixtures to use multipass 
evaluation in sprout.
2024-01-19 11:04:18 -08:00
Joe Savona c8323f3b42 Mutation within a reactively controlled block propagates reactivity
In InferReactivePlaces, we already account for reactively controlled values: 
where a value is never assigned a non-reactive value, but _which_ value is 
assigned is based on a reactive condition (the test conditions of an if, switch, 
loop, etc). 

This PR extends that reactively-controlled inference to mutation that is 
conditioned upon a reactive value. From the test case: 

```javascript 

let x = []; 

if (props.cond) { 

// This mutation has no reactive inputs. 

// *But* the mutation conditionally occurs based on props.cond which is reactive 

x.push(1); 

} 

let y = false; 

if (x[0]) { // therefore the value observed here is reactive 

y = true; 

} 

// so the value of y here is reactive via the reactive control dependency x[0] 

return [y]; 

```
2024-01-19 10:03:51 -08:00
Mofei Zhang 34c89458f2 [entrypoint] Allow ref params to component functions 2024-01-19 14:59:52 -05:00
Mofei Zhang 4aa60d32b9 [patch][dce] Patch dce to have separate mark and sweep phases
--- 

Previously, our logic was something like: 

```js 

fixed-point-loop { 

foreach instruction { 

mark referenced identifiers 

// assume that usages are always visited before declarations 

if (instruction is decl) { 

prune(instruction); 

} 

} 

foreach instruction { 

if not referenced { 

delete(instruction); 

} 

} 

``` 

This contained a bug, as not all usages of a variable are guaranteed to be 
visited before its declaration. 

```js 

// input 

let x = 0; 

while(x < 10) { 

x += 2; 

} 

return x; 

// hir 

entry: 

x$0 = 0 

goto loop-test 

loop-test: 

x$1 = phi(x$0, x$2) 

if ... goto loop-body else goto fallthrough 

loop-body: 

x$2 = x$1 ... 

goto loop-test 

fallthrough: 

return x$1 

``` 

In this example,`x$2` is defined by `loop-body` and used by `loop-test`. 
Similarly, `x$1` is defined by `loop-test` and used by `loop-body`. 

--- 

TODO: trying to come up with more test fixtures
2024-01-18 18:29:19 -05:00
Mofei Zhang a0e90065c6 [patch][babel] check babel identifier before lowering to HoistedConst
Same babel identifier issue as #2510 but for HoistedConst 

Not sure how we should best test this -- one possibility is using constant prop. 
Currently, we have false positives for HoistedConst that prevent constant 
propagation. I don't want to over-rotate on babel apis tests in our fixtures 
(instead of semantically interesting ones) 

```js 

// input 

function Component() { 

{ x: 4 }; 

const x = 2; 

return x; 

} 

// output 

function Component() { 

const $ = useMemoCache(1); 

let x; 

if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 

x = 2; 

$[0] = x; 

} else { 

x = $[0]; 

} 

return x; 

} 

```
2024-01-18 18:29:19 -05:00
Mofei Zhang f88e7fe412 [repro] add fixture repro for destructuring bug 2024-01-18 18:29:18 -05:00
Mofei Zhang 241a615732 [babel][contextvar] Patch context identifier babel logic; only use referenced
identifiers 

--- 

A few fixes for finding context identifiers: 

Previously, we counted every babel identifier as a reference. This is 
problematic because babel counts every string symbol as an identifier. 

```js 

print(x);  // x is an identifier as expected 

obj.x      // x is.. also an identifier here 

{x: 2}     // x is also an identifier here 

``` 

This PR adds a check for `isReferencedIdentifier`. Note that only non-lval 
references pass this check 

```js 

print(x);  // isReferencedIdentifier(x) -> true 

obj.x      // isReferencedIdentifier(x) -> false 

{x: 2}     // isReferencedIdentifier(x) -> false 

x = 2      // isReferencedIdentifier(x) -> false 

``` 

Which brings us to change #2. 

Previously, we counted assignments as references due to the identifier visiting 
+ checking logic. The logic was roughly the following (from #1691) 

```js 

contextVars = intersection(reassigned, referencedByInnerFn); 

``` 

Now that assignments (lvals) and references (rvals) are tracked separately, the 
equivalent logic is this. Note that assignment to a context variable does not 
need to be modeled as a read (`console.log(x = 5)` always will evaluates and 
prints 5, regardless of the previous value of x). 

``` 

contextVars = union(reassignedByInnerFn, intersection(reassigned, 
referencedByInnerFn)) 

``` 

--- 

Note that variables that are never read do not need to be modeled as context 
variables, but this is unlikely to be a common pattern. 

```js 

function fn() { 

let x = 2; 

const inner = () => { 

x = 3; 

} 

} 

```
2024-01-18 18:29:18 -05:00
Mofei Zhang 8f18b8233f [patch][contextvars] Patch for variables reassigned after objectmethod
definitions
2024-01-18 18:29:18 -05:00
Mofei Zhang f0ef0b7d0b Edit .git-blame-ignore-revs 2024-01-18 18:29:17 -05:00
Jan Kassens da7c466f07 Rename eslint-plugin-react-forget to eslint-plugin-react-compiler
Rename eslint-plugin-react-forget to eslint-plugin-react-compiler
2024-01-16 17:36:37 -05:00
Jan Kassens 9b6605d313 Remove [ReactForget] prefix from eslint messages
Remove [ReactForget] prefix from eslint messages 

No other lint warnings have a prefix, removing this is cleaner.
2024-01-16 13:47:20 -05:00
Jan Kassens fb0cf4f833 Make collapsed playground tabs more compact
Make collapsed playground tabs more compact 

We have a lot of steps creating a lot of tabs by now. This makes them visually a 
lot more compact without a full redesign. 

Makes it a bit harder to read and less modern looking, but I think usability was 
a bit bad with the wide tabs. 

**Before:** 


![image](https://github.com/facebook/react-forget/assets/11849/a87c8f7e-d60c-43c1-aaea-1a1f77e082d0) 

**After:** 


![image](https://github.com/facebook/react-forget/assets/11849/227ebf1e-055f-439a-bfc9-e69bae091f6a)
2024-01-16 13:10:32 -05:00
Sathya Gunasekaran 38d3423970 Treat function expression deps as conditional 2024-01-15 12:44:22 +00:00
Sathya Gunasekaran 121e72a342 Add flag for treating function deps as conditional 2024-01-15 12:44:22 +00:00
Sathya Gunasekaran 6d133111a9 Add test for function deps not treated as conditional
In the test, unconditionally reading props.bar.length will throw when props.bar 
is null.
2024-01-15 12:44:22 +00:00
Sathya Gunasekaran 25ec9fcea4 Add .git-blame-ignore-revs
Ignore commits that just change formatting and file structures
2024-01-15 12:44:22 +00:00
Joe Savona 90348cc873 [housekeeping] Remove disabled test262 setup
I sincerely appreciate the effort to get test262 up and running. This was my 
idea, it seemed like a really good way to test our correctness on edge cases of 
JS. Unfortunately test262 relies heavily on a few specific features that we 
don't support, like classes and `var`, which has meant that we never actually 
use this as a test suite. 

In the meantime we've created a pretty extensive test suite and have tools like 
Sprout to test actual memoization behavior at runtime, which is the right place 
to invest our energy. Let's remove?
2024-01-12 12:26:30 -08:00
Joe Savona bcbbbec1f5 [housekeeping] Remove unused test fixtures
We don't use these fixtures, let's just clean them up.
2024-01-12 12:22:23 -08:00
Joe Savona 33118be835 [housekeeping] Remove fixtures/
Do we still use these? I'm happy to close this PR if we still want this but it 
feels like these may have served their purpose and no longer be necessary.
2024-01-12 12:22:19 -08:00
Joe Savona 653373141a Extra fixture for validating preserved memoization of non-escaping callbacks 2024-01-12 14:32:35 -08:00
Joe Savona 0c866672b0 Fix false positive on preserving memo of non-escaping values
Fixes the false positive in the previous PR. When we prune a scope because it's 
values are non-escaping, we now also remove any `Memoize` instructions for that 
scope. The intuition being that we're actively removing unnecessary memoization, 
so we don't need to check that the memoization occurred anymore.
2024-01-11 17:13:41 -08:00
Joe Savona 8e4d2fb69d Repro for false positive in validatePreserveMemoization on non-escaping value
This demonstrates a false positive in validatePreserveExistingManualMemoization. 
We prune memoization of non-escaping values, but the validation pass just sees 
that the value "should" have a scope and that scope doesn't exist, and thinks we 
failed to preserve memoization.
2024-01-11 16:52:57 -08:00
Joe Savona c3a947643f Update hoisting error message to allow error aggregation
Interpolating values into the `reason` field of an error breaks our error 
aggregation. This PR moves the offending function name into the `description` 
field which isn't used for aggregation.
2024-01-11 15:54:55 -08:00