Commit Graph
1708 Commits
Author SHA1 Message Date
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
Joe Savona 7dedc6cc72 Fix test for hook name to match lint rule 2023-05-08 12:14:30 -07:00
Joe Savona fd007be0d0 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-08 12:10:43 -07:00
Joe Savona 6bbe3111c0 ValidateUnconditionalHooks pass using dominators
See the code comments for more, but the basic idea here is that we use the post 
dominator tree to find the set of basic blocks which are guaranteed reachable in 
each function. Those are the only blocks where it is safe to call hooks, and we 
error for hook calls in any other blocks.
2023-05-08 08:55:14 -07:00
Joe Savona 2ddd00dbb1 Implement dominator/post-dominator tree calculation
Implements an efficient algorithm for computing the dominator (or post 
dominator) tree of a CFG, following 
https://www.cs.rice.edu/~keith/Embed/dom.pdf. This is used/tested in the next PR 
to validate that hooks are called unconditionally. 

note: I clean up the implementation quite a bit late in the stack in #1584
2023-05-08 08:55:13 -07:00
Joe Savona 5b0f566941 Example of "unnecessary" memoization of lambdas
I noticed this while demoing Forget to React Org alum Christoph Nakazawa — in 
array.map calls (and other APIs that take a lambda as input) we sometimes end up 
memoizing the lambda. It's technically correct since the function _could_ return 
the lambda, and then we'd need it to be memoized. It's tricky because array.map 
is often called on nested objects, where even if we had type inference on the 
outer value we wouldn't know for sure that the inner property is an Array and 
not some other data type with a custom .map. For example in 
`data.feedback.comments.edges.map(edge => ...)`, even if we knew that `data` was 
an Object, we wouldn't know that data.feedback.comments.edges is an Array 
without cross-file type knowledge. 

But it's definitely wasteful to memoize these lambdas, so we should brainstorm 
options. One option that stands out right away: if the lambda has zero 
dependencies, then we could lift it out to module scope and refer to it by name.
2023-05-10 23:09:17 -06:00
Mofei Zhang 11cee99968 [snap runner][easy] reverse diff order 2023-05-08 14:58:32 -04:00
Joe Savona 2332155161 ValidateHooksUsage
Adds a validation pass to check that the only thing you can do with hooks is 
call them. A follow-up PR (still early WIP) will check the other aspect of the 
rules of hooks, that they are not called conditionally. That's a more involved 
algorithm.
2023-05-04 13:33:43 -07:00
Joe Savona f75cf6fe38 Fix fbt (again) 2023-05-04 15:19:41 -07:00
Mofei Zhang 407e7b0c62 [snap runner] debug mode 2023-05-03 14:50:27 -04:00
Mofei Zhang 7e990419c1 [snap tester] Handle unexpected errors by failing fixture 2023-05-03 14:50:25 -04:00
Joe Savona 253b05f28d Remove restriction on optional nesting
We previously disallowed OptionalMemberExpression inside a normal 
MemberExpression, eg `(a?.b).c`. The new representation handles this case 
correctly so we can remove the restriction.
2023-05-03 17:10:31 -07:00
Joe Savona 7ecee6a091 Support complex computed properties in OptionalMemberExpression
Our previous lowering for OptionalMemberExpression reordered the evaluation of 
properties, such that we had to restrict the allowed properties to those that 
were safe for reordering. With the new representation we preserve order of 
evaluation, so we can relax the restriction. This unblocks a few cases in an 
internal product.
2023-05-03 17:10:28 -07:00
Joe Savona e192930cd4 Remove unnecessary optional property on loads
Now that _all_ optional expression types use the new representation, the 
optionality of all PropertyLoad and ComputedLoad is modeled via control flow (in 
HIR) and the structure of OptionalExpression (in ReactiveFunction). Thus we no 
longer need the `optional` properties on these load instructions — they're 
optional if they're part of an OptionalExpression.
2023-05-03 17:10:27 -07:00
Joe Savona a43fc2bcf3 Consistently use new lowering for OptionalMemberExpression
Earlier PRs in the stack change the way we lower OptionalMemberExpression, but 
only when they ultimately appear inside some OptionalCallExpression. This PR 
ensures that _all_ OptionalMemberExpressions get the new lowering. Note that one 
test case has what is arguably a regression, but the new behavior is also 
reasonable: if we see both `a.b?.c` and `a.b.c.` as dependencies of a scope, we 
previously inferred `a.b.c` as the dependency, but we now infer `a.b` as the 
dependency. This isn't as optimal as what we had before, but it also seems good 
enough for now. Also note that some cases are improved: `foo(a.b?.c)` would 
previously have taken `a.b` as a dependency, we now take the full value of 
`a.b?.c` as a dependency - more precise. 

So overall i'm inclined to land and follow-up on the one regression, since the 
overall model is more cohesive.
2023-05-03 17:10:26 -07:00
Sathya Gunasekaran e5c07ad3e1 [babel] Skip compilation if there's no function name 2023-05-03 14:32:58 +01:00
Joe Savona 404f627c2c Improve conditional dependency tracking for optional member expr inside optional
call 

When we traverse an OptionalExpression in PropagateScopeDependencies, we 
previously considered the entire value to be optional. With the changes in this 
stack to more accurately model OptionalMemberExpression, the `object` portion of 
an OptionalMemberExpression is now evaluated within the OptionalExpression. This 
PR refines the handling of OptionalExpression accordingly, so that we only treat 
the optional portion as conditional.
2023-05-02 16:49:54 -07:00
Joe Savona a1902b263d Rename OptionalCall -> Optional for clarity
The previous OptionalCall terminal and reactive value kinds are now used not 
just for optional calls, but for optional member expressions that appear within 
an optional call. This PR renames those data types to OptionalTerminal and 
OptionalExpression for clarity.
2023-05-02 16:31:31 -07:00
Joe Savona c46157b00b More accurately model OptionalMemberExpression
Extends the new modeling of the previous diff to OptionalMemberExpression. In an 
example such as `a?.b?.c`, we now model that not only is the `.c` conditional, 
but our control flow graph accurately reflects the fact that the `.c` is only 
evaluated if `a.b` exists. Previously we knew it was conditional but the CFG 
allowed a path from a being null through to evaluation of `.c`.
2023-05-02 15:44:06 -07:00
Joe Savona 73a203c199 More accurately model nested OptionalCallExpression
More accurately models nested OptionalCallExpression. Consider: 

```javascript 

a?.(b)?.(c) 

``` 

Our previous representation modeled it such that we treated the second function 
call as if it would be called regardless of whether `a` existed or not. We knew 
that the second call was conditional, so our test output was correct, but the 
control-flow graph didn't faithfully model the semantics. That bothered me. 

The new representation correctly models the control flow, and the fact that if 
`a` is null/undefined execution immediately aborts (not reaching the second call 
at all, nor the evaluation of its args), and evaluates the whole outer 
OptionalCallExpression to `undefined`. 

Note that nested optional member expressions still have the previous model — 
that's next to address.
2023-05-02 15:00:12 -07:00
Joe Savona 9a12e8ba58 Support OptionalCallExpression as LHS of LogicalExpression
A common idiom is to map over some possibly-missing list of items from a data 
payload and fall back to an empty array: 

```javascript 

const renderedItems = data?.items?.map(renderItem) ?? []; 

``` 

The way we were lowering OptionalCallExpression meant that in this case, we'd 
end up with an OptionalCallTerminal as the terminal of the logical expression's 
test block, which violates our internal invariant. Logical test blocks must end 
in a Branch! This PR fixes the immediate issue, which is that the callee - in 
this case `data?.items?.map` — was being lowered prior to the 
OptionalCallTerminal instead of inside its test block. Changing that fixes the 
shape of the IR and makes this example work. 

As part of investigating this I realized that the way I originally handled 
lowering of optional call isn't quite right. The difference isn't observable 
unless we did more sophisticated DCE but we don't correctly model the fact that 
if `data.items` is null that the `map()` call won't occur. That is technically 
fine bc we do model the fact that the `map()` call is conditional, and notably 
its arguments are only conditional dependencies. So it's good enough. But in a 
follow-up I'll change to model the fact that `data.items` is null, that the map 
call isn't reachable at all.
2023-05-02 15:00:11 -07:00
Joe Savona 8292c65d84 Add missing traverseValue()
I don't think this one matters but just in case, we should always call 
traverseValue() when overriding visitValue().
2023-05-02 11:07:12 -07:00
Joe Savona dd6a9d6a03 Dont memoize hook calls in logical expressions
Fix the previous bug — this was a simple oversight, where FlattenScopesWithHooks 
overrode `visitValue()` but failed to call `traverseValue()`. This meant that 
when we reached compound expressions such as LogicalExpressions that we didn't 
traverse into their nested values, and didn't see the hooks hidden there.
2023-05-02 11:01:37 -07:00
Joe Savona bfb84eed3b Repro for memoized hook within logical expression
Repro of a bug in which we incorrect memoize hook calls that are inside logical 
expressions (though the bug could occur for ternaries, optional calls, and 
sequence expressions too).
2023-05-02 10:47:48 -07:00
Joe Savona c0b0119356 Start of architecture doc
This is an attempt to get down some of the principles and goals that we've had 
partially written down, partially just thoroughly discussed amongst the team. 
It's rough draft quality but better than nothing, and gives us someplace to add 
to.
2023-05-02 09:33:19 -07:00
Joe Savona 5196fbd92b Visit switch test case as scope dependency
This can't be tested yet - we only support simple, safely re-orderable values as 
case test values - but it will easy to overlook later so i'm adding now.
2023-05-01 08:51:58 -07:00
Joe Savona 41d99ffcb9 PropagateScopeDeps uses visitor infra
PropagateScopeDependencies is one of the few places we don't use the new visitor 
infra for traversing ReactiveFunction. Or rather it _was_! 

Note that there's a bit less value here than in other places since we have to 
handle each terminal variant with custom logic, but at least it's more 
consistent with the rest of the codebase now.
2023-04-28 17:14:02 -07:00
Joe Savona 1df89e992a Remove unnecessary temporary in tests
We previously didn't support ternaries whose value was unused, so we had an 
extraneous temporary and console.log call to ensure the value counted as used. 
We now special-case ternary/conditional expressions which are in an 
ExpressionStatement to not prune them, so the temporary and log are now 
unnecessary.
2023-04-27 10:43:43 -07:00
Lauren Tan d2f33dd524 Retain locations when using babel cloneNode in lambdas
It turns out the third parameter to `cloneNode` is ["If the third parameter is 
true, the cloned nodes exclude location 
properties."](https://github.com/babel/babel/blob/c060e5e3d538a174a017edb7e75b07e80ccb55b7/packages/babel-types/src/clone/cloneNode.ts#L35-L39) 
strips away locations if its true, so to fix simply change this to false
2023-04-27 16:25:41 -04:00
Lauren Tan d5dee80933 Failing test for fbt issue
Specifically, the fbt plugin would error on a `null` loc as it [relies on it in 
one of its 
utils](https://github.com/facebook/fbt/blob/6a23a5374a757a49654d8304a3efea8eb9548eea/packages/babel-plugin-fbt/src/getNamespacedArgs.js#L82-L84), 
implying a missing `loc` somewhere in our pipeline after codegen
2023-04-27 16:25:41 -04:00
Lauren Tan 3eed52b49b Only debug with @debug pragma
You don't always want to debug all the passes when you use @only, so split it 
out into its own pragma
2023-04-27 16:16:50 -04:00
Joe Savona b69d70664c Console methods are readonly
Defines common `console` methods to tell the compiler that they take readonly 
args. This ensures that things like `console.log()` aren't accidentally viewed 
as a mutation. Previously the pattern of "build object, then log it after 
mutation is done" would have grouped the console.log as part of the mutation and 
the log only would fire if the value got reconstructed. Now we know the log 
isn't mutating, and the log will happen regardless of whether the value is 
rebuilt or cached.
2023-04-27 10:34:31 -07:00
Joe Savona 2ddbbd4735 Exhaustive switches for some terminal handling
Updates two points in the compiler that were easy to miss when adding new 
terminals: 

* HIRBuilder's `removeUnreachableFallthroughs()` nulls out unreachable 
fallthroughs, but this had a non-exhaustive `if` statement. It now uses a helper 
function which internally has an exhaustive switch. 

* LeaveSSA needs to schedule block fallthroughs, but had a non-exhaustive `if` 
statement. It also uses a helper function which internally has an exhaustive 
switch. 

cc @poteto since you ran into this (ie the compiler not alerting you to update 
these places) w your diffs.
2023-04-26 16:34:26 -07:00
Joe Savona afe2f5c810 Support useMemo w named functions
Supports useMemo invocations where the function is not an inline function, such 
as `useMemo(someFunction, [])`.
2023-04-26 18:16:28 -07:00
Sathya Gunasekaran 1cccd6a815 [hir] Clone babel nodes rather than reusing them
There's state such as scope info sticking on to these nodes so reusing them can 
cause issues if a pass later on just uses scopes these directly.
2023-04-27 17:35:30 +01:00
Lauren Tan 4a8c90a77c [babel] Remove unsafe calls to path.stop()
Discovered this in a recent attempt at syncing Forget to Meta, it seems 

that calling path.stop() is unsafe as it appears to have strange 

behavior in plugins that come after. This resulted in `import type 

{...}` not being compiled away in the post-babel output which isn't 

valid JS syntax. Removing the `stop()` calls fixes it 

Test plan: made these changes locally, synced my local changes to Meta and reran 
- in simulator and observe that it now runs and doesn't throw a syntax error
2023-04-26 17:16:34 -04:00
Lauren Tan b1eaf88c61 [babel] Ensure only adding import specifier to non-namespace
Missed this in the previous PR 

Test plan: P706162189 (some babel errors) before this PR, P706255523 has no 
errors
2023-04-26 15:16:21 -04:00
Joe Savona e5f4b3008f Rename things for clarity in PropagateScopeDeps 2023-04-26 11:34:47 -07:00
Joe Savona 2aa3a28481 test case for debugger stmt in reactive scope 2023-04-26 11:34:46 -07:00
Joe Savona 5b9992075c Driveby optimization of DCE
Lift a constant evaluation out of a loop
2023-04-26 11:27:41 -07:00
Joe Savona d4b8df5441 Address code review 2023-04-26 11:27:40 -07:00
Joe Savona e98823f933 Generalize #1521 for PropertyLoad
This is a more general version of the change from #1521. That PR ensured that 
LoadLocal temporaries accessed outside the instruction's scope are correctly 
promoted. However, we have a similar pattern with PropertyLoad. 

This PR adds a general mechanism for handling these type of indirections: any 
LoadLocal/PropertyLoad temporary accessed when it's defining scope is not active 
will be promoted to a declaration of the defining scope. Notably, we do this in 
a way that ensures that the dependencies are preserved, ie that we correctly 
view the operand of LoadLocal/PropertyLoad as a dependency of the current scope.
2023-04-26 11:27:39 -07:00
Joe Savona dd5069162a This page left intentionally blank
Supports EmptyStatement nodes by ignoring them. Note no tests because 
format-on-save clears away any empty statements and there is, rather 
frustratingly, no way to tell VSCode not to format on save for a specific file 
via the file contents itself or project configuration (at least, not that i can 
find).
2023-04-26 11:27:38 -07:00
Joe Savona 8e7b68506f You get a debugger and you get a debugger and you get a
Adds support for DebuggerStatement.
2023-04-26 11:27:37 -07:00
Joe Savona 1f51b10ac9 Fix unused logical/condition via ExpressionStatement instr
Uses the new ExpressionStatement instruction to ensure that logical and 
conditional expressions are never pruned. This addresses an issue where we were 
unable to construct a ReactiveFunction for unused logical/conditional bc there 
wasn't a single Identifier assigned in both branches. The ExpressionStatement 
ensures that the result is used, that we don't prune the phi, and that both 
branches have a single assignment target. 

In theory we could be more sophisticated with DCE and still prune these 
instructions if their operands are also safe to prune, but in practice you're 
only like to have a logical/conditional as an expression statement (in the 
source) if it's for side effects.
2023-04-26 11:27:36 -07:00
Joe Savona 2cb1c72305 Scaffolding for ExpressionStatement instruction kind
Adds an `ExpressionStatement` instruction variant to model values that are 
otherwise "unused" but which we don't want to remove. The next diff changes 
BuildHIR to use this where appropriate.
2023-04-26 11:27:35 -07:00
Joe Savona 894cf6e37b First-class representation of builtin jsx tags
We previously represented JsxExpressions using builtin tags - `<div>`, `<b>` etc 
- by lowering the tag name to a Primitive with the string name of the tag. 
However, by lowering into an independent value, it was possible that the lowered 
tag name could be grouped into a different memo slot, such that we ended up with 
output like: 

```javascript 

let t0; 

if (c_1) { 

... 

t0 = "div" 

... 

} else { ... } 

return <t0>{children}</t0> 

``` 

This is obviously wrong. It's also wrong to rename `t0` -> `T0`, because React 
treats that as a custom component, not a builtin. The right thing is to 
explicitly model builtin components, which this PR does by making 
`JsxExpression.tag` be a union of Place | BuiltinTag.
2023-04-26 11:27:35 -07:00
Joe Savona 9aea37ff5a Promote temporaries used in JSX to uppercase names
Ensures that temporaries used in JsxExpression tags are named with a capital 
letter so that they are treated as custom components rather than builtins.
2023-04-26 11:27:34 -07:00