Commit Graph
874 Commits
Author SHA1 Message Date
Joe Savona a5a86c302a Validate against ref access in render 2023-05-31 09:38:28 -07:00
Joe Savona af94b075c0 Option to disable memoization
Adds a `removeAllMemoization` flag that runs the entire compiler pipeline but 
strips out all memoization. The intent is to be able to compare (in limited 
use-cases) the performance of an existing app with all memoization removed, vs 
the performance with manual memoization, vs the performance with Forget enabled. 

In terms of how this works: we already strip out useMemo/useCallback since 
Forget is more accurate. The new option adds an extra pass that strips out all 
reactive scopes. Collectively this leaves ~zero memoization within components 
(this does leave React.memo, but close enough).
2023-05-31 08:03:36 -07:00
Sathya Gunasekaran 793b69243f [hir] Track context refs when inferring reactive ids 2023-05-31 15:37:39 +01:00
Sathya Gunasekaran b447c93edc [test] Add failing test for lambda turning value reactive 2023-05-31 15:37:35 +01:00
Sathya Gunasekaran 2c8f6888e6 [hir] Failing test for adding ref as dep incorrectly 2023-05-31 13:55:00 +01:00
Sathya Gunasekaran 91f41f5ee3 [hir] Don't track ref value as a valid dep 2023-05-31 13:54:59 +01:00
Sathya Gunasekaran bedf0cc6c5 [hir] Don't track ref.current as a valid dep 2023-05-31 13:54:58 +01:00
Sathya Gunasekaran 1d536af4f7 [hir] Refactor checkValidDependencyId to accept a dep 2023-05-30 13:44:30 +01:00
Mofei Zhang fc13557d30 [tests] remove jest fixture tests
--- 

Remove jest fixture tests in favor of snap runner. Main reasons: 

- maintaining feature flags and compatible behavior required syncing all changes 
to 3 files (`generateTestsFromFixtures`, `compiler-test`, and `compiler-worker`) 

- jest snapshot test file causes rebase conflicts on most rebases 

- speed 🙌 

$ time yarn test compiler-test 

(the extra test here is `has a consistent extension for input fixtures`) 

``` 

Test Suites: 1 passed, 1 total 

Tests:       37 skipped, 480 passed, 517 total 

Snapshots:   479 passed, 479 total 

Time:        27.668 s 

Ran all test suites matching /compiler-test/i. 

✨  Done in 43.18s. 

yarn test compiler-test  57.05s user 3.85s system 139% cpu 43.546 total 

``` 

$ time yarn snap 

``` 

478 Tests, 478 Passed, 0 Failed 

✨  Done in 13.12s. 

yarn snap  53.96s user 9.35s system 468% cpu 13.518 total 

``` 

Jest and snap should have the same set of features: 

- report test failures via exit status (used by Git Actions) 

- watch mode 

- breakpoints + `debugger` statements 

- note that `--sync` is not required for this 

- skip `todo.` prefixed fixtures 

- fixtures in nested directories e.g. `rules-of-hooks/testname.js` 

- filter mode (via editing `testfilter.txt`) 

- filter + debug mode 

(1) edit `testfilter.txt` to filter out all but one test 

(2) add `@debug` pragma to the first line of the test 

testfilter.txt 

```js 

// @only 

testfixture_basename1 

testfixture_basename2 

```
2023-05-26 13:18:38 -04:00
Mofei Zhang 71c9743e38 [snap] Fix vscode debugger attaching to forked proc
Turns out I just forgot to forward `process.env` when forking 😅 

`debugger` statements and breakpoints should work with both `--sync` and 
`--no-sync` (default) modes
2023-05-26 13:18:37 -04:00
Joe Savona 99257aa430 Enable more flags on playground 2023-05-23 16:55:10 -07:00
Joe Savona 91dcf24e67 Add test case for invalid lambdas
This is the example we discussed in our design sync. 

```javascript 

function Component(props) { 

const [x, setX] = useState({ value: "" }); 

const onChange = (e) => { 

// INVALID! should use copy-on-write and pass the new value 

x.value = e.target.value; 

setX(x); 

}; 

return <input value={x.value} onChange={onChange} />; 

} 

``` 

Here `onChange` is a mutable lambda, and it should be invalid to pass a mutable 
lambda where a frozen value is expected. This is because unlike other value 
types, you cannot freeze a lambda — the only choice is to not call it at all. 

Note that there is a harder case to catch: 

```js 

function Component(props) { 

const [x, setX] = useState({ value: "" }); 

const onChange = (e) => { 

// INVALID! should use copy-on-write and pass the new value 

x.value = e.target.value; 

setX(x); 

}; 

const x = constructAValueThatMaybeAliasesItsInput(onChange); 

return <input value={x.value} onChange={x.maybeGetTheLambdaBack()} />; 

} 

``` 

This case demonstrates how mutable lambdas can be captured and then accessed 
later — the analysis to catch this case is more sophisticated bc it involves 
inferring that `x` aliases a mutable lambda. But we also can't be sure that `x` 
does alias the lambda, so disallowing this code could prevent a lot of valid 
code from compiling. My hypothesis is that we should start with at least 
validating the example at the top, while allowing the second case for now.
2023-05-25 16:08:10 -07:00
Joe Savona edbb6e2bdb Test case for Array.push on frozen array
Just making sure that we reject this
2023-05-25 16:00:35 -07:00
Joe Savona 3381ce7ea8 Array.prototype.join
Trivial
2023-05-25 15:57:09 -07:00
Joe Savona 8be5423e3b Type Array.prototype.map/filter
We can now type `Array.prototype.{map,filter}`: 

* The callee is ConditionallyMutable because, although the array itself is not 
modified, its items flow into the lambda and may be modified there. 

* The argument is ConditionallyMutable because it accepts both mutable and 
immutable lambdas. Mutate would disallow immutable lambdas (wrong), while Read 
would be incorrect for mutable lambdas since calling them triggers mutation.
2023-05-25 14:50:14 -07:00
Joe Savona 75a1972584 More tests for effect enforcement
Adds test cases to ensure we're correctly inferring mutative builtin operations 
— property store, computed property store, property deletion, and computed 
property deletion — as definite mutation and that we're rejecting inputs where 
these operations are used on immutable/frozen values.
2023-05-25 14:33:02 -07:00
Joe Savona 9766e19805 Add Effect.Mutate for known mutation
Adds back `Effect.Mutate`, and changes so that `Effect.ConditionallyMutate` 
never rejects frozen/immutable values, while `Effect.Mutate` _always_ rejects 
frozen/immutable values.
2023-05-25 13:54:35 -07:00
Joe Savona 40cf400f72 Rename Effect.Mutate -> ConditionallyMutate
We currently use `Effect.Mutate` both for places that _may_ mutate (ie untyped 
function calls) and for places that have known mutation (typed function calls, 
or operations like `delete x.y`). We then use a separate mechanism to decide 
whether to reject the input, with some call paths checking the effect and others 
not. 

This stack refactors this logic in InferReferenceEffects per our discussion, so 
that `Effect.ConditionallyMutate` is for "may or may not mutate" either because 
we're not 100% sure (untyped function) or because the mutation depends on the 
operand (ie, a callback arg that will be invoked and thus will mutate if the 
lambda is mutable, not mutate if the lambda is immutable). Later diffs add back 
`Effect.Mutate` as "definitely 100% mutating".
2023-05-25 13:41:09 -07:00
Joe Savona 37d044763b Define types for useState/useRef
Defines 4 new types: 

* Return type of `useState()`, which has properties "0" and "1" to allow us to 
infer the types when destructuring 

* Type of useState() set state function 

* Return type of `useRef()` so we know what is a ref 

* Type of ref.current, so we know what is a ref *value* 

Example: 

<img width="1670" alt="Screenshot 2023-05-24 at 9 59 37 AM" 
src="https://github.com/facebook/react-forget/assets/6425824/3ee7d04a-fda3-4b7b-89b7-d205d9a6fd0d">
2023-05-24 09:54:10 -07:00
Joe Savona c87a1916f4 Infer property types for destructuring 2023-05-24 09:54:07 -07:00
Mofei Zhang ecc730c587 [FeatureFlag] Toggle enableTreatHooksAsFunctions default to true
--- 

Toggle default to true, since this should be a no-op refactor. 

Tests: 

- test fixtures 

- ran on Store + - and saw no difference in compiled output 

- [diff](P744101621) with 
`enableTreatHooksAsFunctions=false` 

- [diff](P744105565) with 
`enableTreatHooksAsFunctions=true`
2023-05-23 16:11:38 -04:00
Mofei Zhang 08a8891245 Test: unknown hooks should not assert effects 2023-05-23 13:55:12 -04:00
Mofei Zhang b76ee1ab32 [types] Consolidate Hook and Function types: Hooks are functions 2023-05-23 13:55:10 -04:00
Mofei Zhang 47e9a2b8a5 [FeatureFlag] add enableTreatHooksAsFunctions 2023-05-22 19:37:53 -04:00
Mofei Zhang 19cf39f50b [effects] Track return ValueKind for function signatures 2023-05-22 19:37:50 -04:00
Joe Savona d456e4a78c Memoize based on inferred return type, not signature 2023-05-22 09:20:16 -07:00
Joe Savona 4ce22ebcfe Option to assume hooks follow the rules
Adds a new feature flag which tells the compiler to assume that hooks follow the 
Rules of React. Specifically, the idea that since any hook could be wrapped in a 
giant `useMemo()` call, all arguments to hooks have to be treated as if they're 
owned by React — and therefore become immutable — and that the return value of 
the hook is immutable. 

Our default is to assume that hooks break the rules, but in practice nearly 
every component follows them.
2023-05-19 11:05:04 -07:00
Joe Savona 7e10c51532 Put optimizations in this stack behind a feature flag
Wraps all the optimizations from this stack behind a feature flag so we can 
quickly disable if there are problems when integrating.
2023-05-18 16:20:28 -07:00
Joe Savona 94e2ae5893 Dont memoize calls that produce primitive values
Teaches InferReactiveScopeVariables that calls can produce primitives that don't 
need a scope.
2023-05-18 16:05:32 -07:00
Joe Savona 5008448d74 Rename function for clarity
Cleanup now that we use this for two things
2023-05-18 15:52:51 -07:00
Joe Savona 0322e4dbd4 InferReferenceEffects uses function types for CallExpression
Updates the InferReferenceEffects logic for CallExpression to work similarly to 
MethodCall, where we take into account the function signature (if present) when 
inferring the effects and return kind.
2023-05-18 15:52:11 -07:00
Joe Savona c0a5d86902 Reorder similar cases in InferReferenceEffects
Moves the CallExpression case next to MethodCall to make it easier to compare
2023-05-18 15:52:07 -07:00
Joe Savona a325eed472 Define Boolean/Number/String globals
Defines the `Boolean`, `String`, and `Number` global functions. This will be 
useful for allowing developers to wrap statements that produce a primitive in a 
way that Forget knows about in order to optimize better.
2023-05-18 15:52:03 -07:00
Joe Savona 46417a4ed7 Fix bug with partially memoized destructuring 2023-05-18 09:25:28 -07:00
Joe Savona a933d5e307 Repro for bug with partially memoized destructuring 2023-05-18 09:25:24 -07:00
Joe Savona 3603ee00a6 Retain original structure of labeled blocks
Handles some edge-cases where we previously flattened away some of the structure 
of a labeled block, instead ensuring that we retain the original shape. See the 
output. 

## Test Plan 

Tested on the internal app we're focused on (w useMemo inlining enabled), it 
works fine.
2023-05-17 15:55:54 -07:00
Joe Savona 7831ac2189 Enable InlineUseMemo in playground 2023-05-17 15:55:54 -07:00
Joe Savona f0b8cfbf25 More concise output for labeled blocks 2023-05-17 15:55:53 -07:00
Joe Savona f30041b0f0 More useMemo inlining test cases
More test cases for useMemo inlining, including the problematic case we found 
internally and some similar tricky cases with labels.
2023-05-17 15:55:52 -07:00
Joe Savona 329809de81 Dramatically simplify InlineUseMemo
I realized a wayyyy simpler approach to inlining a lambda: wrap it in a labeled 
block. The transformation is roughly as follows: 

```javascript 

// Before 

const x = useMemo(() => { 

if (a) { 

return b; 

} 

return c; 

}, [a, b, c]); 

return x; 

// After 

let x; 

label: { 

if (a) { 

x = b; 

break label; 

} 

x = c; 

break label; 

} 

return x; 

``` 

The key to making this work is fixing up some edge cases in labeled blocks, 
hence the previous PRs.
2023-05-17 15:55:51 -07:00
Joe Savona 5ee23af1a6 Elide empty if alternate blocks in codegen 2023-05-17 15:55:51 -07:00
Joe Savona 84a356274a Stop shrinking HIR to preserve fallthrough nodes
This is part of a stack to fix some edge cases in inlining of useMemo closures. 
In this first step, I'm disabling `shrink()` in order to retain more information 
about the data flow. For example, 

``` 

label: if (cond) { 

break label; 

} 

return foo; 

``` 

Would previously have shrunk away the if body, making the IfTerminal.consequent 
point directly to the fallthrough block (w the return). Now we retain a separate 
block.
2023-05-17 15:55:50 -07:00
Mofei Zhang f070cab870 [snap] add memoizeJSX flag
--- 

#1603 for snap
2023-05-17 14:39:29 -04:00
Mofei Zhang 8b38651986 [snap] Add jest test capabilities to snap
- delete output files when we detect input files are deleted 

- enable test fixtures in nested directories 

- exit with error code when we detect failures 

Note that the test failure on this PR is expected and will be fixed by #1608 (or 
happy to abandon that PR and fold the changes)
2023-05-17 14:39:28 -04:00
Mofei Zhang 7ba638ef8d [tests] delete stale .expect files
--- 

Looks like we delete `.js` files but missed `.expect` files. Jest probably 
didn't catch this because the basename of the fixtures had duplicates (in 
rule-of-hooks)
2023-05-17 14:39:28 -04:00
Mofei Zhang 0b697d1903 [babel plugin] Add codegen for useRenderCounter 2023-05-17 14:39:27 -04:00
Mofei Zhang bceb786c5a [runtime] useRenderCounter
--- 

(`react-forget-runtime` package seems to be synced to -.) 

RFC: useRenderCounter hook: 

- tracks # renders (increments on render path) 

- exposes a global renderCounterRegistry (counting # renders in alive / mounted 
components) 

Next PR will modify BabelPlugin to add codegen 

```js 

// Similar to how we're currently importing `isForgetEnabled` 

import {isInstrumentForgetEnabled_Secret} from "ReactForgetFeatureFlag"; 

// ... 

function Component_uncompiled(props) { 

if (isInstrumentForgetEnabled_Secret) { 

useRenderCounter(); 

} 

// ... 

} 

function Component_forget(props) { 

if (isInstrumentForgetEnabled_Secret) { 

useRenderCounter(); 

} 

// ... 

} 

```
2023-05-17 14:39:26 -04:00
Sathya Gunasekaran dc603bb2fa [babel] Add isDev flag and put logging behind it 2023-05-17 18:38:08 +01:00
Joe Savona 2a80ba544e Move DisableJsxMemoization-test into main compiler test w pragma
I originally created a separate test for the mode with JSX memoization disabled, 
but we can merge this into the main compiler-test and enable the feature with a 
pragma.
2023-05-15 12:12:53 -07:00
mofeiZ 8bf1ec97a4 Add flag back for inlineUseMemo
Reverts #1502, but flips test flags (e.g. `inlineUseMemo` by default, unless a 
test specifies `@inlineUseMemo false`. I figured this add less thrash for test 
fixtures, but happy to just do a clean revert (or remove the pragma altogether 
and always pass `inlineUseMemo: true`)
2023-05-15 14:27:26 -04:00