Commit Graph
729 Commits
Author SHA1 Message Date
Sathya Gunasekaran 7c5e5eb30c [hir] Make return.value non nullable 2023-04-17 21:11:44 +01:00
Sathya Gunasekaran 42459f579f [hir] Return undefined explicitly if there's no return value
Future passes like inlineUseMemo assume there's a return value so let's create 
one instead of implicitly returning undefined.
2023-04-17 21:11:43 +01:00
Sathya Gunasekaran 0e34c39c58 [test] Add failing test for empty return in useMemo callback 2023-04-17 21:11:42 +01:00
Sathya Gunasekaran 00d4a69459 [hir] Refactor useMemo inlining
Previously useMemo inlining created a new StoreLocal assignment (not 
reassignment!) instruction for every return value. This breaks when the return 
is inside a block (like an if-block) as the  scope is tied to the block. 

For example: ``` let x = useMemo(() => {   if (...) {     return { ... };   } }) 
``` would become: ``` if (...) {   const temp = { ... }; } const x = temp; ``` 

This PR instead changes the inlining to declare a temporary in the function 
prologue and then reassign values to it when replacing return statements. 

``` let x = useMemo(() => {   if (...) {     return { ... };   } }) ``` 

becomes 

``` let temp; if (...) {   temp = { ... }; } const x = temp; ```
2023-04-17 20:59:35 +01:00
Sathya Gunasekaran 31f6dd8070 [test] Add failing test showing useMemo bug 2023-04-17 20:59:35 +01:00
Sathya Gunasekaran 34bca247d6 [hir] Terminate function within BuildHIR
All the block building logic is encapsulated in BuildHIR.
2023-04-11 15:40:08 +01:00
Sathya Gunasekaran c42cd3f558 [hir] Refactor HIRBuilder.build to use HIRBuilder.terminate
It's a bit weird  that HIRBuilder is generating terminals, a follow up PR will 
remove this entirely from HIRBuilder. This is a first step towards that.
2023-04-11 15:37:20 +01:00
Sathya Gunasekaran 7678a52c9c [hir] Add invariant to check if useMemo callback is async or a generator 2023-04-11 15:06:53 +01:00
Sathya Gunasekaran fd252e42ab [hir] Add invariant to check if useMemo callback accepts args 2023-04-11 15:00:30 +01:00
Sathya Gunasekaran 74f5e26e00 [be][test] Remove hir-test 2023-04-11 13:41:33 +01:00
Sathya Gunasekaran 2d8b36467c [hir] Put useMemo inlining behind a flag
It's still a WIP so disable behind a flag for now.
2023-04-11 13:41:33 +01:00
Joe Savona 67b0cf8a8c Comments from #1484 2023-04-10 11:06:10 -07:00
Joe Savona 601eb2a23a Inline useMemo callbacks to allow improved memoization
This is a simplified version of #1454. The goal of this PR is to inline the 
contents of `useMemo()` callbacks, rather than just immediately invoke the 
lambda. Turning useMemo() into an IIFE works, but it means that we can't 
optimize within the lambda block. Our investigations showed that there's a lot 
of room to optimize at a finer granularity than manually written useMemo calls. 
For example, one product instance had a useMemo that created a list of child JSX 
elements. Most of those elements only relied on a single variable (`a`), but a 
few relied on a second variable (`b). Thus _all_ elements were invalidated 
whenever `b` changed. If Forget retains the original lambda, we have no choice 
but to keep that (coarse) granularity for memoization. When we inline, we can 
optimize to make e.g. individual JSX elements depend on their precise 
dependencies. 

The rough idea is: 

* Keep track of all function expressions 

* When we find a useMemo, lookup its function expression, and add its CFG to the 
main function (the previous PR ensures that BlockIds won't collide) 

* Replace any return statements with a StoreLocal to save the result and a Goto 
to the code following the useMemo call. 

* Then we run the usual set of passes to patch the HIR back up again. 

Example: 

```javascript 

// Before 

function Component(props) { 

const x = useMemo(() => { 

if (props.cond) { 

return null; 

} 

return foo(props.x); 

}, [props.x]); 

return x + props.y; 

} 

// Intended - **before** memoization 

function Component(props) { 

let x; 

if (props.cond) { 

x = null; 

} else { 

x = foo(props.x); 

} 

return x + props.y; 

} 

```
2023-04-07 16:34:52 -07:00
Joe Savona f7f7a88e19 Example suboptimal memoization with non-inlined useMemo 2023-04-07 16:34:51 -07:00
Joe Savona 6122f21393 Definition for useContext 2023-04-06 11:46:57 -07:00
Joe Savona f51e2a6bf4 Enforce terminal has a .loc (and .id)
This PR ensures (via a static assertion function) that all terminal variants 
have a SourceLocation, and adds locations to the variants which didn't have it 
before. This also adds a static assertion that terminals have an InstructionId, 
though we already relied on that so it was checked via usage.
2023-04-06 15:50:43 -07:00
Joe Savona 5622c0ee91 Use single name resolver for nested functions
This is a prerequisite to inlining `useMemo()` lambdas so that we can better 
optimize them. Nested functions are evaluated with a fresh HIRBuilder, which 
means that they currently have their own `bindings` object for mapping 
identifier instances to IdentifierIds. This means that identifier ids in a 
closure are _always_ different that those outside the closure, even when they 
refer to the same identifier: 

``` 

function Component(props) { 

props; // becomes e.g. props$1 

const onClick = () => { 

props // becomes e.g. props$2 

}; 

} 

``` 

For useMemo inlining this is problematic because we've lost the association that 
these identifiers actually refer to the same thing. This PR changes that, 
sharing the name resolution data structure between the top-level function and 
any nested function expressions.
2023-04-06 15:25:30 -07:00
Lauren Tan fcfb66914a [Babel] Skip files that contain one or more disables of React eslint rules
To unblock internal experimentation, for now let's just skip over compiling any 
file that contains one or more disables of React's eslint rules, and log that. 
This is a little coarse in the sense that we could skip over just functions that 
contain the comments, but Babel doesn't provide an easy way to traverse comments 
afaict so this is the simplest solution. I did check our internal repo and noted 
that there was only one disable of exhaustive-hooks in that entire directory in 
one file, so this should be fine. 

Notably we are not throwing any errors if we detect these violations as we don't 
want to fail the build, we just want to skip them for now.
2023-04-06 19:14:08 -04:00
Lauren Tan 2eae7dc6b9 [be] Simplify yarn test
Updates our scripts to only hash and clean up`dist` when bundling for Meta, 
since we don't need to do that for tests.
2023-04-06 19:10:21 -04:00
Joe Savona 9c1f8a962c Make CompilerError.reason a static string
While running the latest Forget build on www I noticed that a lot of the 
bailouts were special-cases where we used interpolation in the error `reason` 
string to provide more context for debugging. This is a pretty cool result, 
because it means that we actually support nearly all the common syntax (at least 
based on a sample of the codebase). But it makes our tools for aggregating 
errors break down a bit. 

This PR adds a new, nullable `description` property to CompilerErrorDetail, and 
manually updates to ensure that we always pass a static `reason` and only use 
interpolation in the `description`. This will allow our aggregation tools to 
group by the reason.
2023-04-06 08:51:31 -07:00
Joe Savona 702f43eb1e Optimize away extraneous blocks
In codegen we can flatten away extraneous BlockStatements if they turn out not 
to need a label.
2023-04-05 16:37:26 -07:00
Joe Savona d243e748d0 Build -> Codegen for LabelTerminal
NOTE: See background in #1476. 

Updates BuildHIR to use the new LabelTerminal for LabeledStatements, and adds 
support for HIR->ReactiveFunction transformation and codegen. Note that we 
sometimes produce an extraneous block wrapper if it turns out the label wasn't 
necessary, that seems...fine?
2023-04-05 16:26:40 -07:00
Joe Savona b087635e5d LabelTerminal scaffolding
Adds a new `LabelTerminal` which will be used to represent LabeledStatements 
that contain a statement other than a loop. What we do for these cases is 
basically break the containing block in two, with a goto after the inner 
statement to the fallthrough. This allows us to model the label, and any `break` 
to it, in the HIR. However this fails in codegen because we can't find the 
fallthrough branch — we need a high level terminal that knows about this 
structure. 

Hence LabelTerminal. Now, instead of just a continuation block and a goto, we 
have a structured terminal. The LabelTerminal expresses the block for the 
labeled statement and the continuation, and we can use this to put it back 
together when constructing a ReactiveFunction. Note that this PR is just the 
scaffolding for LabelTerminal, the next PR is the interesting bits.
2023-04-05 16:26:36 -07:00
Joe Savona 592b911e7f fix main 2023-04-05 15:47:40 -07:00
Joe Savona 1650d8fef2 Ensure blank line after copyright header 2023-04-05 13:42:27 -07:00
Joe Savona 801e4258cf Add missing copyright headers 2023-04-05 12:21:51 -07:00
Joe Savona fd91191c94 Script to add/fix copyright
Adds a script to automate adding/updating the copyright header to all 
appropriate files. For now i've excluded fixture inputs, just because it would 
impact fixture outputs too, but we can add them in a later PR if we want.
2023-04-05 12:21:47 -07:00
mofeiZ 9d97015236 [globals] Remove global shape for Array.from
Type inference currently assumes that a `FunctionSignature`'s effects have no 
false positives. If a `mutate` effect is observed on a read-only place, Forget 
currently assumes this is an user error and 
[throws](https://github.com/facebook/react-forget/blob/207595e04e2be08b8f62bf21dac9d846b9651e43/forget/src/Inference/InferReferenceEffects.ts#L275-L281). 

Array.from is polymorphic -- its effects are dependent on the type of its 
parameters
2023-04-05 12:53:39 -04:00
Joe Savona 4505218911 Ensure unique BlockId for nested functions
This PR ensures that we use a single id space for the `BlockId`s in both 
top-level functions as well as any nested FunctionExpressions (note, we already 
do this for `IdentifierId`). This will make it easier for follow-ups to merge 
the CFG of nested functions (ie useMemo bodies) with the parent without block id 
collisions.
2023-04-05 09:01:08 -07:00
Joe Savona a3dfc7e3a2 Use CompilerError.invariant in BabelPlugin
This means we get more information when we hit these invariants (ie the source 
location)
2023-04-04 17:56:11 -07:00
Joe Savona 1e2dbf7fdb Fix JSX form of fbt
Fixes `<fbt>`. This required a bunk of yak shaving to work through several 
issues: 

* First, there was a bug in codegen for JsxNamedspacedName. I added handling for 
it for identifiers, but JsxNamespacedName gets converted to a Primitive. The 
output looked correct because Babel happily creates invalid Jsx identifiers! 

* Next, I needed to add locations to JSX nodes. It took me a while to pinpoint 
which specific node needed the location, so I ended up just adding locations to 
all the parts of a Jsx element. 

* That uncovered the fact that FBT was expecting the `<fbt:param>`'s `name` 
attribute value to be a StringLiteral, not a StringLiteral wrapped in a 
JsxExpressionContainer. So now we special-case JsxAttribute and emit raw 
StringLiteral (either is allowed per the spec) 

And with that, voila, `<fbt>` works.
2023-04-04 14:37:30 -07:00
Joe Savona 3c46984e09 Repro of fbt issues
Adds the two FBT (https://facebook.github.io/fbt/) plugins to our test setup so 
that we can verify Forget plays well with FBT. Unfortunately FBT's plugins are a 
bit finicky, and things that are technically allowed per the JSX spec (such as 
wrapping string attribute values in a JsxExpressionContainer) aren't supported 
by FBT's plugin. This PR is just to add the fbt plugins and highlight some cases 
that fail; these are fixed in later PRs in the stack. 

For example, the `fbt-params.js` fixture fails on this PR: 

Input 

```error.fbt-params.js 

import fbt from "fbt"; 

function Component(props) { 

return ( 

<fbt desc={"Dialog to show to user"}> 

Hello <fbt:param name="user name">{props.name}</fbt:param> 

</fbt> 

); 

} 

``` 

Output 

``` 

React Forget › __tests__/fixtures/compiler › fixtures › fbt-params 

Expected fixture 'fbt-params' to succeed but it failed with error: 

/Users/joesavona/github/react-forget/forget/fbt-params: fbt: unsupported babel 
node: MemberExpression 

--- 

props.name 

--- 

``` 

See fixes later in the stack.
2023-04-04 14:32:21 -07:00
Joe Savona a97c55dc5d Support for with empty update expression
Adds support for `for` statements with an empty or unreachable update 
expression. In both cases, reversePostorderBlocks() will remove the 
empty/unreachable update block, leaving the ForTerminal.update pointing to a 
non-existent block. We explicitly rewrite this (much like we null out 
unreachable fallthroughs after shrink). When transforming to ReactiveFunction, 
we emit the update block as null if it was the same as the test block.
2023-04-04 13:29:04 -07:00
Joe Savona 1d77016026 Make ReactiveForTerminal.update nullable
Makes ReactiveForTerminal.update nullable, allowing the update clause to be 
omitted. Follow-up diffs null out the update clause in some circumstances.
2023-04-04 13:29:03 -07:00
Joe Savona d660f37bf2 Validator pass that terminal successors all exist 2023-04-04 13:29:02 -07:00
Joe Savona 6d62d9f505 Infer closures as frozen if they dont capture mutable values
Fix for the previous issue, suggested by @gsathya: when we run 
InferReferenceEffects on the outer function we check each closure to see if it 
actually captured any mutable values. If it didn't, we can mark the closure as 
readonly and memoize it independently.
2023-04-04 12:30:12 -07:00
Joe Savona 939582dae0 Repro for unmemoized readonly callback
Repro of a closure that we currently treat as readonly because it captures a 
possibly-mutable value, but which we later realize is not mutable. Specifically, 
when we check `exit()` we think `dispatch()` is mutable and therefore consider 
it captured, which means we can't independently memoize `exit`.
2023-04-04 12:30:11 -07:00
Joe Savona 2e3aa3954c Memoize hook args (treat as escaping)
Updates `PruneNonEscapingScopes` to consider hook arguments as potentially 
escaping. This is because hook inputs are "owned" by React — for example, 
closures passed to `useEffect`, or a value that is passed to a custom hook and 
which then becomes a memoized input.
2023-04-04 12:30:10 -07:00
Joe Savona e32ea49a0e Special-case fbt() function call form
Similar to what we did for `<fbt>` jsx elements, this PR ensures that `fbt()` 
calls have their operands memoized in the same scope to honor the limited 
contract for what's allowed as an argument of an fbt() call expression.
2023-04-03 11:47:45 -07:00
Sathya Gunasekaran 0cb10446b3 [test] Failing test for returning from a for-loop 2023-04-01 09:22:34 +01:00
Lauren Tan b19555573f [babel] Make gating option a pair of module and project
There are some internal restrictions in Metro that only allow us to specify one 
gating module as an injected dependency. To allow multiple projects, this PR 
updates the Babel plugin to take a gating options config specifiying a project 
name. The project name is used as a suffix for the generated import; for 
example: 

```js 

const options = { 

// ... 

gating: { 

module: "ReactForgetFeatureFlag", 

importSpecifierName: "isForgetEnabled_Secret", 

}; 

// generates 

import {isForgetEnabled_Secret} from "ReactForgetFeatureFlag"; // a module that 
exports multiple flags 

// ... 

```
2023-04-03 12:33:23 -04:00
Sathya Gunasekaran 1aec5ef523 [hir] Skip computed part of the captured ref 2023-03-31 17:37:06 +01:00
Lauren Tan 55793bd96f Also visit phis in fallthroughs for DoWhile in LeaveSSA
Oops, forgot about this previously
2023-03-31 17:55:07 -04:00
Lauren Tan 61462a43bc Add support for ForOf statements
Teaches Forget to compile simple ForOf statements, where the init comprises of a 
variable declaration with an identifier or destructure.
2023-03-31 17:55:04 -04:00
Joe Savona 1e2df5bdbb Remove console.log from testing sync script changes
oooooops
2023-03-31 14:43:31 -07:00
Joe Savona 28e6a974fa JSX: children which are jsx elements dont need expr container 2023-03-31 12:32:46 -07:00
Joe Savona 4e2ef92779 Ensure <fbt> children are not independently memod
This is a Meta-ism, but adding it for now to unblock. We special-case the 
`<fbt>` element for translation purposes, and have a transform that requires the 
children of this element to be a limited subset of nodes. Notably, any dynamic 
translation values must appear as `<fbt:param>` children — we disallow 
identifiers as children of `<fbt>` nodes. 

This PR adds a new pass which finds `<fbt>` nodes and ensures their immediate 
operands are not independently memoized. Note that this still allows the values 
of `<fbt:param>` to be independently memoized, as demonstrated in the unit test.
2023-03-31 12:32:45 -07:00
Mofei Zhang 8dfe06ddba [hir] Allow reorderable exprs in optional computed load 2023-03-31 10:58:28 -04:00
Mofei Zhang c77c1b1644 [hir] Disallow unconditional load from optional memberexpr 2023-03-30 19:23:23 -04:00
Mofei Zhang c00e7a2af2 [test] Test case for optional chaining in codegen
```js 

// here, `a?.b.c` is a single optional chain 

// (evaluates to undefined if a is nullish) 

a?.b.c; 

// here, 'a?.b` is an optional chain, and `.c` is an unconditional load 

// (nullthrows if a is nullish) 

(a?.b).c; 

``` 

--- 

Next PR in stack will add a bailout for `(a?.b).c`. 

(If we want to properly handle `(a?.b).c`, we might want to model optional 
chains explicitly in the HIR. We currently assume that any `PropertyLoad` whose 
lhs is an optional property load is read conditionally.)
2023-03-30 18:43:55 -04:00