Commit Graph
855 Commits
Author SHA1 Message Date
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
Sathya Gunasekaran f57ed15a33 [Babel] Log errors to stdout, not stderr
Writing to stderr causes buck to fail compilation.. which is not we want. Let's 
log to stdout only for now.
2023-05-15 14:45:26 +01:00
Sathya Gunasekaran 41a897797d [Babel] Format errors for console logging 2023-05-15 14:45:26 +01:00
Sathya Gunasekaran 5c8195aec7 [prettier] Fix linting 2023-05-15 14:43:39 +01:00
Mofei Zhang dda3e61347 [test cases] Add failing / bug tests for destructuring assignment 2023-05-12 12:48:18 -04:00
Mofei Zhang fe95b5df90 [hir] add DeclareContext (2/n)
--- 

In `lower`, we now ensure that all context variables are declared by a 
`DeclareContext` instruction. `DeclareContext` always produces a `let` 
declaration, and `StoreContext` is always a reassign. There are a few reasons we 
need `DeclareContext`: 

- DeclareLocal assumes it is storing to a SSA-fied identifier (which always 
stores an immutable primitive). This does not fit context variables. 

- Without DeclareContext, we need custom logic in some passes to initialize 
identifier / context state (e.g. `MutableRange`, ValueKind, etc) for the 
`StoreContext` that declares the context. 

This PR stack models context variables as concrete identifiers (with references 
to context variables modeled by `Place` referencing the context variable 
identifier). @josephsavona pointed out that this is abusing the notion of 
Identifier/Place, as context variables are essentially interior properties of a 
ContextEnvironment. Since we are not modeling `ContextEnvironment` implicitly or 
explicitly, all inference for context variables is essentially pointer analysis.
2023-05-12 12:48:17 -04:00
Mofei Zhang 314a5cfca5 [hir] Add Load/StoreContext (1/n)
--- 

This PR adds LoadContext and StoreContext to handle reading and writing to 
context variables. 

A context variable is any variable that is declared within a Forget-compiled 
function and reassigned within a closure. Conceptually, we want to treat these 
variables as attributes of a `EnvironmentContext` variable (as most javascript 
VMs do). 

- context variables currently do not participate in type inference (i.e. we do 
not produce type equations for loads from context variables). In the future, we 
can try typing this as `Phi(assignment1Type, assignment2Type, ...)`. 

- context variables are always treated as `Effect.Mutable`. 

- context variables do not participate in SSA, or certain optimizing passes 
(e.g. dead code elimination, constant propagation, etc). 

There is some still follow ups: 

- From my understanding, we should introduce a `DeclareContext` instruction. 

- currently, declaring a context variable (without initializing it) is broken. 
This is because the declaration lowers to `DeclareLocal`, which assumes it is 
storing to a SSA-fied identifier. 

```js 

let x; 

x = 4; 

() => { x = {}; }; 

``` 

- DeclareContext will also make some initialization logic easier. In this PR, I 
added some hack-y code to handle initializing effects / mutable ranges / other 
inference state for the first StoreContext. 

- Handle or bail on stores to context variables through destructuring assignment 

- 

~~Next PR:~~ 

- ~~Change closures to track reassigned identifiers (to extend mutable range of 
primitives)~~
2023-05-12 12:17:05 -04:00
Joe Savona f5bdf462b2 Enable hooks validation on playground, make deguggable
Enables hooks validation in playground. Also adds a tab to show the output of 
validation (in case it passes) with the inferred post dominator tree. We can use 
this to debug the dominator in case of false negatives. 

<img width="1724" alt="Screenshot 2023-05-11 at 11 07 08 AM" 
src="https://github.com/facebook/react-forget/assets/6425824/8f7ae472-8415-4899-aedf-c8f26094ebfe">
2023-05-11 13:39:59 -06:00
Joe Savona e649bf6429 Fixtures from ESLint rule
Incorporates the fixtures from eslint-plugin-react-hooks using a script, so that 
we can easily update them in the future. For each fixture we run the compiler 
with and without hooks validation first so that we know if the fixture is 
expected to pass — we have some false positives and false negatives that i can 
work through. For example we accidentally think that `userFetch()` is a hook, 
oops. Fixtures that should pass but error, or that should error but pass, are 
marked as `todo.<name>` or `todo.error.<name>`. 

While i was here i added the ability to have fixtures in subdirectories for 
grouping purposes.
2023-05-11 13:39:58 -06:00
Mofei Zhang d1e044b81a [deps] Add DeclareLocal to ReactiveScope decls
--- 

Try to fix bug from #1589: 

> If a declaration for an immutable identifier (i.e. one that is not later 
re-assigned, since undefined is a primitive) is sandwiched between mutations, we 
currently do not record it as an output or hoist it out of the reactive scope. 

One simple fix is to add all declared (and later referenced) identifiers as 
declarations of a reactive scope. This has some undesired effects (e.g. 
additional instructions + memo cache slots), but in practice, this shouldn't be 
happening often. 

Alternatively, we could 1.) add a pass to hoist declarations, 2.) account for 
this in constant propagation, or 3.) add a bailout
2023-05-11 16:24:37 -04:00
Mofei Zhang a2169d894a [test case] Test for DeclareLocal instruction
--- 

Record incorrect output. 

If a declaration for an immutable identifier (i.e. one that is not later 
re-assigned, since `undefined` is a primitive) is sandwiched between mutations, 
we currently do not record it as an output or hoist it out of the reactive 
scope.
2023-05-11 16:24:36 -04:00
mofeiZ 77c535ce25 [snap] Make watch mode go fast(er)
While optimizing per @josephsavona's suggestions in #1592, I noticed that we 
were clearing quite a few require cache entries. 

As of this PR, `Object.keys(require.cache)` holds 

- 1258 entries total 

- 67 files compiled from Forget source code (this is what 
`ts.createWatchCompilerHost` modifies) 

- 1120 babel source files (from node_modules) 

When working on watch mode, I'm almost always making changes to Forget source or 
test fixture files. It's a bit faster to just clear those entries (assuming that 
babel has no global state we need to invalidate). 

On my computer, re-running tests in watch mode (triggered by source code 
changes) takes: 

|   |  All tests  | One test (filter) | 

|-- |--------|----------| 

| current | 4.7s  | 1.8s  | 

| this PR  |  1.8s  | 0.1s  |
2023-05-11 14:58:51 -04:00
Mofei Zhang 5e80821aa2 [effects] Allow capture-store effects in rvals
--- 

Some typed functions need to annotate callees or arguments as `Effect.Store`. 
This PR modifies alias analysis (`InferAliasForStores`) to account for this
2023-05-10 21:55:56 -04:00
Mofei Zhang f3d4d16918 [lower] Bail out on computed lvalues in destructure
--- 

Currently, we're silently producing incorrect output - 

```js 

// source 

function Component(props) { 

const computedKey = props.key; 

const { [computedKey]: x } = props.val; 

return x; 

} 

// compiles to 

function Component(props) { 

const { computedKey: x } = props.val; 

return x; 

} 

```
2023-05-09 14:38:15 -04:00
mofeiZ 04cee98166 [snap] patch bug in clearing require cache
Snap currently has a bug in which the require cache is not correctly cleared 
when running in filter mode (#tests < 2 * #workers). 

- We're currently clearing all entries in the require cache of worker threads, 
including `jest-worker` and `snap/dist/...` files 

- jest-worker seems to `require` these files on every dispatch (i.e. 
`worker.compile` seems to call `require(`compiler-worker`).compile`)
2023-05-11 13:53:06 -04:00
Joe Savona 5de02b881b Support unused conditional/logical expressions assigned to variable
I noticed some instances of this error when running forget on an internal 
product. I previously fixed the case if a logical/conditional used only for side 
effects (not assigned to a variable) but the new cases were assigned to an 
unused variable. I double-checked and we’ve actually fixed all the steps after 
these invariants so we can just remove them and support these cases.
2023-05-09 10:58:51 -07:00
Joe Savona 50c3bd9d98 Fix capturedRefs collection for lambdas
When we calculate the dependencies of a FunctionExpression we were only adding 
new items if the binding identifier had not been seen yet. That is correct for 
`capturedIds` since its the set of identifiers, but incorrect for `capturedRefs` 
since its an array of all the distinct places. This meant that if a function 
expression referenced multiple properties of the same binding, we'd only record 
the first one. We now correctly record all of them.
2023-05-09 10:38:56 -07:00
Joe Savona 90109b3572 Clean up dominator/post-dominator impl
Tidies up the implementation a bit, splitting the single function and class into 
distinct computeDominatorTree() and computePostDominatorTree() functions and 
helper classes.
2023-05-08 15:35:48 -07:00
Joe Savona 597e70d69f Improve eslint derived fixtures 2023-05-08 14:10:07 -07:00
Joe Savona 8920fc0b2f Dont count throw as an exit node (React semantics)
React will retry or abort components that throw (depending on a few conditions), 
so from React's perspective a `throw` statement is not a normal exit node. Thus 
the Rules of Hooks really have a caveat: the set of hooks that are called _in an 
execution that returns successfully_ must be consistent. Examples such as the 
following are therefore allowed: 

```javascript 

function Component(props) { 

if (props.cond) { 

throw new Error(...); 

} 

useHook(); 

} 

``` 

By modeling `throw` as an exit node, we rejected cases such as this. This diff 
changes to not model throws as exit nodes. #1584  changes this to make it an 
option, since some cases will want to consider throw as an exit node.
2023-05-08 13:58:46 -07:00