Commit Graph

1459 Commits

Author SHA1 Message Date
Joe Savona 1d36764ca9 [be] Align tsconfig module/moduleResolution settings 2023-12-04 11:19:41 -08:00
Joe Savona 9fb3a26174 Add logging for number of memo *blocks* (in addition to slots)
Useful for understanding and comparing how much memoization Forget applies vs 
how much developers previously memoized by hand.
2023-11-30 15:20:01 -08:00
Joe Savona 3639704f2b Enable hooks validation in eslint 2023-12-04 08:22:51 -08:00
Joe Savona 0c0bedd377 New approach to hook validation
New approach to hooks validation per recent discussion. The idea is to avoid 
false positives while still preventing serious violations. See the comments in 
the file for more details about the approach. It uses a somewhat similar idea to 
InferReferenceEffects in that we track a "Kind" for each IdentifierId, and 
various instructions propagate or derive a result Kind from the operands. Kinds 
form a lattice and can be joined, allowing us to be more precise about known vs 
potential hooks, and known vs potential _sources_ of hooks.
2023-12-04 08:15:26 -08:00
Joe Savona 7da906d648 Fix Array#at and similar cases to capture if receiver is mutable
The previous PR helped me realize we weren't handling Array#at correctly. If the 
receiver is a mutable value its effect should be Capture and the lvalue effect 
needs to be Store. This PR updates the definition for Array#at to make the 
receiver Capture, and then updates inference to automatically set the lvalue 
effect to Store if _any_ argument (or the receiver) was Capture.
2023-11-29 12:05:25 -08:00
Joe Savona 75c7fdcbd0 MutableIfOperandsAreMutable flag handles mutation via capturing
There was one missing piece to the optimization from the previous PR: Array#map 
can return an alias to the receiver in its output, which means that mutations of 
the result have to be treated as mutations of the receiver. This means we need 
to use a Capture effect on the receiver. If that doesn't get downgraded to a 
Read bc the value was immutable, we then also need to make the lvalue effect a 
Store (so that InferMutableRanges actually looks at it for aliasing).
2023-11-29 10:46:44 -08:00
Joe Savona f7d16db544 [RFC] Refine memoization for Array#map with non-mutating callbacks
Improves memoization for cases such as #2409: 

```javascript 

const x = []; 

useEffect(...); 

return <div>{x.map(item => <span>{item}</span>)}</div>; 

``` 

We previously thought that the `x.map(...)` call mutated `x` since its kind was 
Mutable. However, in this case we can determine that the map call cannot mutate 
`x` (or anything else): the lambda does not mutate any free variables and does 
not mutate its arguments. 

This PR adds a new flag to function signatures, used for method calls only, that 
checks for such cases. The idea is that if the receiver is the only thing that 
is mutable — including that there are no args which are function expressions 
which mutate their parameters — then we can infer the effect as a read. See 
tests which confirm that function expressions which capture or mutate their 
params bypass the optimization.
2023-11-29 10:46:43 -08:00
Joe Savona 65c54e2d70 Repro for unmemoized array due to mutation surrounding hook
Distilled repro of an internal example we found. Forget determines a mutable 
range for the array, but that mutable range spans a hook call, so the reactive 
scope gets pruned. That's all working as expected. 

What isn't ideal though is that if we know `x` is an array and `f` can't mutate 
its arguments, then `x.map(f)` shouldn't count as a mutation of `x`, since 
Array.prototype.map can only mutate the receiver via the callback (if the 
callback mutates its args). 

Improving on this example requires a) we have to know it's an Array, via type 
information or bc we saw an array literal and b) being precise about which 
functions could possibly mutate their parameters, which is tricky because of 
indirect mutations via stores, etc.
2023-11-29 10:46:42 -08:00
Joe Savona b952bc3d87 [be] Consistently use known returnValueKind from signatures
We were using `returnValueKind` from function signatures for CallExpression but 
not MethodCall; this PR changes to use this signature information for both 
instruction kinds.
2023-11-29 10:46:42 -08:00
Mofei Zhang 359b9b1589 [ez] Patch unsound array destructuring
--- 

Going to hold off on landing until after codefreeze, it's not urgent as we 
already fixed playground in #2404. All other internal pipelines do error 
handling through Entrypoint, which catches and creates UnexpectedErrors as 
needed.
2023-11-28 17:48:22 -05:00
Sathya Gunasekaran 0a6d3b31de Add compilation failure test for Logger 2023-11-28 15:35:48 +00:00
Sathya Gunasekaran e53e944580 [test] Add test for Logger 2023-11-28 15:35:46 +00:00
Sathya Gunasekaran 18d4913406 [babel] Don't use source location for hoisting check
Instead of using the source location to check for hoisting, just stop checking 
for a given component after we reach it's declaration. 

By definition all references to it before are (potential) hoisting errors. 

Note that there could be false positives but that's ok.
2023-11-28 14:18:24 +00:00
Sathya Gunasekaran 42fa01ec87 [test] Add failing test for component syntax with refs
The desugaring of Component syntax with refs is not compatible with our gating 
lowering.
2023-11-28 14:18:24 +00:00
Sathya Gunasekaran be03439687 [yarn] Update hermes-parser
Need new version for component syntax updates
2023-11-28 14:18:24 +00:00
Sathya Gunasekaran 1ebb7ae376 [eslint] Assume external modules are non side-effecting
This lets us tree shake out the `chalk` module entirely
2023-11-28 14:10:48 +00:00
Sathya Gunasekaran 0ba3065812 [eslint] Don't rollup zod 2023-11-28 14:10:48 +00:00
Joe Savona 445baf9ae4 Extend effect dep validation to handle pruned memoization
Extends the validation that effect deps are memoized to handle an additional 
case that @gsathya  pointed out: when a dependency has a reactive scope but that 
scope ends up being pruned. We track reactive scopes which actually exist in the 
ReactiveFunction, and reject useEffect deps that have an associated reactive 
scope but where that scope does not exist (bc it got pruned).
2023-11-27 12:31:27 -08:00
Joe Savona d7db416167 [RFC] useEffect dependency memoization check
This is one approach to testing whether useEffect dependencies are memoized. The 
idea is based off the observation that the only reason dependencies wouldn't be 
memoized (other than compiler bugs) is that they are mutated later. If they're 
mutated later, then the dep array will have a mutable range which encompasses 
the InstructionId of the useEffect call. So we look for that pattern and throw a 
validation error. 

The downside of this approach is that we might reject code that happens to be 
valid: specifically, that the lack of memoization isn't a problem in practice 
because the effect won't trigger a loop. But (per test plan) this doesn't seem 
to introduce that many new bailouts on www. Rather than implement a complex 
validation that checks whether we un-memoized something that was memoized in the 
input, it seems more practical to: 

1. Enable this more comprehensive validation against any form of un-memo'd 
effect dependency 

2. Flip the default for hooks (to assume they follow the rules), which will fix 
the primary cause of Forget pessimistically not memoizing dependencies. 

## Test Plan 

Synced to www and checked output via the upgrade script: a few components stop 
getting memoized bc they have un-memoized effect dependencies. Let's chat!
2023-11-27 10:20:43 -08:00
Mofei Zhang 5b8e94c6d8 [ez][wip] Make playground more resilient to crashes
--- 

<img width="1031" alt="image" 
src="https://github.com/facebook/react-forget/assets/34200447/8e475472-45c3-4ef0-aa4b-d187e72b999c"> 

Behold! No instacrash on `useMemo()`
2023-11-21 09:30:29 -06:00
Mofei Zhang 6a6ef0ab59 [repro] Forget outputs invalid code when we bailout
Sprout output: 

``` 

$ sprout --filter --verbose 

FAIL: bug-invalid-code-when-bailout 

Difference in forget and non-forget results. 

Expected result: { 

"kind": "ok", 

"value": "{}", 

"logs": [] 

} 

Found: { 

"kind": "exception", 

"value": "Cannot access 'bar' before initialization", 

"logs": [ 

"'The above error occurred in one of your React components:\\n' +\n  '\\n' +\n  
'    at globalThis.WrapperTestComponent 
(/Users/feifei0/fb/react-forget/packages/sprout/dist/runner-evaluator.js:30:26)\\n' 
+\n  '\\n' +\n  'Consider adding an error boundary to your tree to customize 
error handling behavior.\\n' +\n  'Visit 
https://reactjs.org/link/error-boundaries to learn more about error 
boundaries.'" 

] 

} 

```
2023-11-21 09:06:15 -06:00
Mofei Zhang bfefbf3fce [repro] dce bug for JSX memberexpr tags in lambda 2023-11-21 09:06:14 -06:00
Joe Savona 8176ebb546 BuildHIR lowers FunctionDecl instead of rewriting to FuncExpr
We were modifying the Babel AST as a shortcut to lowering function declarations, 
instead we can explicitly lower them equivalently to a `let <id> = 
<function-expression>`.
2023-11-16 15:41:19 -08:00
Lauren Tan a1e3891189 [rfc][babel] InvalidConfig always throws
When you have your panic threshold set to "NONE" as we recommend, it's easy to 
miss that your config is wrong (which makes everything not compile) because 
those errors were being silenced. This made debugging FluentUI and the 
forget-feedback testapp pretty difficult to figure out at first, and defeats the 
purpose of having config validation in the first place. 

This pr makes it so InvalidConfig errors always throw, regardless of the panic 
threshold set. In general our plugin should never throw at build time due to 
component bailouts, but because an InvalidConfig would bailout everything from 
being compiled at all, it seems reasonable to throw here
2023-11-17 12:01:20 -05:00
Lauren Tan 6d101435d4 [Babel] Fix up eslint suppression logic
Babel doesn't attach Comment nodes to anything, so they dangle off of 

the Program node while only specifying a range. This meant that 

previously we first had to traverse all of the Program's comments to 

find an eslint suppression of the rules of React, then during traversal 

of the individual functions, we would check if there were any *global* 

eslint suppressions, then bailout all components. 

This PR updates our logic to determine if individual functions are 

affected by an eslint suppression range: 

- If an eslint suppression range falls within its body; or 

- If an eslint suppression wraps the function
2023-11-17 10:22:46 -05:00
Lauren Tan fbcc21c37a Add repro for bug with 'use no forget'
For some reason, when there are other hooks/components defined in the file, the 
'use no forget' directive stops working
2023-11-17 10:22:45 -05:00
Mofei Zhang 96f10e058b [be][tests] Change fixtures to evaluate successfully instead of throwing
--- 

Not dependent on changes from #2366, but now the fix is easy to review
2023-11-16 18:12:00 -05:00
Mofei Zhang 66748b00f2 [be][sprout] Snapshot files for sprout with shared utils
--- 

16 out of ~150 recently added sprout fixtures have exceptions that reflect 
easy-to-miss mistakes in the fixture input, like forgetting to import 
`useState`. 

This PR adds snapshot files for sprout to prevent these mistakes (or catch them 
at diff review time). This describes what it implements, but happy to take other 
suggestions as well! 

1. One sprout snapshot file for each input. 

This significantly increases the number of files we have, but makes it clear 
what the fixture is testing. I was hoping to expand on these snapshots to record 
the result of multiple re-renders, or mounts (with different parameters), but 
the tradeoff is that adding / changing fixtures will now require running `yarn 
sprout --mode update`. 

Some alternatives I've considered: 

- Only record snapshots for fixtures that error (`result.kind == "exception"`). 
This change would be pretty easy. This doesn't help sanity check sprout results 
for general mistakes though ("am I testing what I want to test?) 

- Warn loudly for fixtures that error -- seems like these are easy to ignore, 
but.. worth it for the convenience? 

- Some github action that runs and comments on your PR with sprout fixture 
changes; i.e. never manually update sprout files? 

2. Sprout and snap share a common snapshot file, with some parsing hackery. 

Previously, the implementation added new files to a separate sprout snapshot 
directory, assuming that devs don't need to read these often. 

After feedback from @josephsavona, I updated to have snap and sprout write to 
the same file for ease of reviewing / debugging (to be able to see the input / 
output side by side). 

- `snap --mode update` will update snap's part of the file. 

- `sprout --mode update` will update sprout's part 

- `snap` and `sprout` will both compare oldSnapshot (read from the file) with 
`newSnapshot = merge(oldSnapshot, newData)` to determine if a test pass. 

3. Some hacks 😅 Absolute paths to project directory (e.g. stack traces) are 
replaced with `<project_root>` in a mocked `console.log`
2023-11-16 18:12:00 -05:00
Mofei Zhang fa07eb0b3b [be] make evaluator and worker easier to debug in sprout
--- 

jsdom and other libraries seem to cause jest workers to exit with 
`forceExit:true`. Not sure what option I set (or global I've overwritten) but 
console logs aren't flushed as a result, making debugging a bit confusing. 

This PR: 

- Moves some code from `eval(...)` to a typechecked real js function. I always 
had trouble debugging the `eval`ed code, so smaller code snippet is better here. 

- waits for jest workers to end before exiting
2023-11-16 18:12:00 -05:00
Joe Savona 94348be2d9 Enable ValidateNoSetStateInRender in ESLint plugin
Same as previous: this rule is working accurately with no false positives, let's 
enable it.
2023-11-16 07:52:13 -08:00
Joe Savona 7b6b370638 Consolidate ValidateNoSetStateInRender flags
I ran the plugin with the extended version of ValidateNoSetStateInRender enabled 
(incl. function expressions) and there are no false positives. Let's remove the 
flag for the function expression case since the whole rule is working 
accurately.
2023-11-15 13:05:24 -08:00
Joe Savona 6a1343c82b Repro of undefined expressioncontainer expression
Repro from T169063835. This works, but since it came up it seems good to add a 
test case for it just in case there's something funny here that we could regress 
on w/o realizing it.
2023-11-15 14:23:53 -08:00
Joe Savona df42058237 Fix variable-resolution hoisting issues
Fixes one category of bugs with const hoisting. The algorithm finds all consts 
that need to be hoisted, then looks through the statements of a block to find 
the first statement which references that const, delaying the emission of the 
HoistedConst instruction until its actually used. To determine if a statement 
references a const we find every identifier in the statement and check if its 
binding is one of the hoisted bindings. 

There's a very small bug here: when we resolve the binding of each identifier, 
we need to resolve it in its own scope. We're currently resolving these 
identifiers agains the outer block statement's scope, which can cause us to 
misattribute identifiers when there is shadowing: 

``` 

const items = props.items.map(x => x); // we scan this statement, resolve 'x' in 
the block statement scope, and mis-attribute it to the outer x. 

const x = 42; // (1) this x is a candidate for hoisting, so the binding is in 
the set of hoisted consts 

```
2023-11-15 16:55:01 -08:00
Joe Savona b55ccb1b84 Repro for hoisting bug
I had added a repro for this earlier but hadn't realized it was due to const 
hoisting. Renaming this test to clarify what's causing the problem and to make 
it easier to find.
2023-11-15 16:34:28 -08:00
Sathya Gunasekaran eacf189eca [playground] Remove button from error message
We show the entire error in panel, no need to click to console.log 

This makes it easier to copy the error now. 

Before: 


https://github.com/facebook/react-forget/assets/565765/4c13abfe-a06d-4580-b3d7-b02792f53e57 

After: 


https://github.com/facebook/react-forget/assets/565765/0dff95cc-5207-48a5-a0b6-2055cadcb780
2023-11-16 12:01:59 +00:00
Sathya Gunasekaran 369c315ac4 [hir] Update error message to say global
This is non ideal but at least it's a step in the right direction. 

Getting the correct error requires us to track every identifier and global, 
which seems a bit excessive for now. 

We can revisit and improve this error if this is starting to confuse folks.
2023-11-15 17:03:55 +00:00
Sathya Gunasekaran 392a4fd9da [test] Add tests for mutating a global
The error is thrown correctly but the error message is incorrect.
2023-11-15 17:03:51 +00:00
Sathya Gunasekaran 306fe03e78 [printer] Print function name if available 2023-11-15 09:36:32 +00:00
Sathya Gunasekaran 58947d9075 [babel] Rename isReactFunction to isReactAPI
Disambiguates better between this and the existing `isReactFunctionLike` 
function.
2023-11-15 09:36:31 +00:00
Joe Savona 696434fed8 Fix ValidateNoSetStateInRender for loops 2023-11-14 11:33:39 -08:00
Mofei Zhang c434ef4b70 [validation] Patch false positives for hook calls after loops
--- 

We were throwing `InvalidReact` errors on valid inputs. 

```js 

// Input 

function Foo() { 

log("block0"); 

for (const _ of foo) { 

log("loop"); 

} 

useBar(); 

} 

// IR 

bb0: 

// log("block0"); 

ForOf init=bb2 loop=bb3 fallthrough=bb1 

bb2: 

// init 

Branch: then:bb3 else:bb1 

bb3: 

// log("loop") 

Goto(Continue) bb2 

bb1: 

// useBar(); 

Return 

``` 

We correctly compute post dominators here. 

```js 

// In validateUnconditionalHooks 

console.log(dominators.debug()); 

/* Output: 

(read x => y as x is the post-dominator for y (all paths from x to the exit must 
go through y)) 

"bb4" => "bb4", 

"bb1" => "bb4", 

"bb2" => "bb1", 

"bb3" => "bb2", 

"bb0" => "bb2", 

*/ 

``` 

However, `findBlocksWithBackEdges` prevented us from adding `bb1` to the 
`unconditionalBlocks` set as `bb2` (its post dominator) has a back edge. I'm not 
sure what the `findBlocksWithBackEdges` was doing previously, so I replaced it 
with an invariant asserting that the loop terminates.
2023-11-14 13:04:31 -05:00
Mofei Zhang 00abc5acf5 [be][tests] Run todo fixtures
--- 

Snap should compile all fixtures to record changes in results, even `todo` 
prefixed ones. Previously, they were skipped as we noted the correlation of `// 
@skip` pragmas and file naming. 

Now, no fixture should be skipped as our compiler pipeline should be able to 
handle every kind of error.
2023-11-14 13:04:28 -05:00
Sathya Gunasekaran 2efd85f145 Convert CompilationModeScham to zod type 2023-11-14 11:46:26 +00:00
Sathya Gunasekaran 084e4325b9 Convert PanicThresholdOptions to zod type 2023-11-14 11:46:23 +00:00
Sathya Gunasekaran 0821a57fa8 [playground] Capture Babel parsing errors
Before: 

<img width="1416" alt="Screenshot 2023-11-14 at 9 09 53 AM" 
src="https://github.com/facebook/react-forget/assets/565765/f53daf34-080a-4804-9a61-77c141c05d21"> 

After: 

<img width="1444" alt="Screenshot 2023-11-14 at 9 09 45 AM" 
src="https://github.com/facebook/react-forget/assets/565765/8861c266-9a49-4ff7-99cd-7240ca1750b1">
2023-11-14 18:24:38 +00:00
Sathya Gunasekaran 07d6c6d8aa [playground] Show error when compiling unsupported functions
Before: 

<img width="1117" alt="Screenshot 2023-11-14 at 9 11 28 AM" 
src="https://github.com/facebook/react-forget/assets/565765/d1ec2b5f-1bc5-4d29-9811-effcd39581e4"> 

After: 

<img width="1115" alt="Screenshot 2023-11-14 at 9 11 18 AM" 
src="https://github.com/facebook/react-forget/assets/565765/9a41889c-cf7b-4f31-8b6e-e26bfb204883">
2023-11-14 09:10:31 +00:00
Sathya Gunasekaran 0794aacdbf Turn off validateNoSetStateInRender in babel-plugin
There's false positives that we found in the Eslint plugin
2023-11-14 16:59:10 +00:00
Sathya Gunasekaran 1641f94a86 [eslint] Turn off validations
We're seeing false positives
2023-11-14 16:59:09 +00:00
Sathya Gunasekaran 3da32312e7 [globals] type useEffect return value as primitive
useEffect returns undefined
2023-11-13 10:51:08 +00:00
Lauren Tan 480c11bdb1 [hoisting] Make hoisting related errors consolidatable
Noticed from our paste that we weren't correctly rolling up hoisting related 
errors due to specific information being in the error title, so this PR moves 
them into description instead.
2023-11-13 16:54:54 -05:00