Commit Graph
906 Commits
Author SHA1 Message Date
Joe Savona bf631a0bb6 Change rules for context variable promotion
This PR updates the conditions for which variables are promoted to context 
variables. 

The previous rule was to promote any variable where the variable was reassigned 
in some function expression other than the function that declared the variable. 
Notably, this meant that we did not use context variables for variables which 
were captured in a function expression, but reassigned _outside_ a function 
expression. 

The new rule is more consistent: we promote any variable which is a) reassigned 
somewhere and b) referenced in some function expression outside of their 
declaring function. The implementation builds two sets of identifiers, one for 
each criteria, then takes the union of these two sets. 

## Motivation 

The motivation for this change is to unblock additional validations and 
optimizations of function expressions. It's currently difficult to translate 
metadata that we infer about identifiers outside of a function expression into 
metadata about the identifiers within a function expression — for example to 
infer types within function expression bodies based on type information outside, 
propagate constants into functions, infer reference effects, etc. 

After this change, the only free variables inside function expressions will be 
variables that are effectively `const` - never reassigned anywhere. Thus it will 
be safe to renumber those identifiers to match the outer context (during 
EnterSSA), making it trivial to map metadata from outside the function into the 
function. 

This change also more closely models the runtime representation — any variable 
referenced in a function, and reassigned somewhere, would have to be compiled 
(ie in a JS engine) to use a context variable.
2023-06-05 22:11:08 -04:00
Lauren Tan 4e22fa6451 Try to fix test262 in ci 2023-06-06 15:25:20 -04:00
Lauren Tan 5ebb7f1a8e Try to compile with Forget in eslint plugin
Nothing interesting for now, but it does seem to successfully run the compiler
2023-06-06 10:56:21 -04:00
Lauren Tan 359df9e85c Enable TypeScript in eslint-plugin-react-forget 2023-06-06 10:56:21 -04:00
Lauren Tan 5c1faa901e Add workspace command to run all tests 2023-06-06 10:56:20 -04:00
Lauren Tan cd56c55e87 Basic noop rule for ReactForgetDiagnostics 2023-06-06 10:56:20 -04:00
Lauren Tan f82de29e73 Bootstrap new eslint plugin
Just scaffolds a new eslint plugin
2023-06-06 10:56:20 -04:00
Joe Savona 04d6bad1ae [ez] Update completed todo comment
lol at how out of date this comment is :-)
2023-06-05 16:46:38 -04:00
Joe Savona 39d837d918 Update fixtures from workspace rebase
I updated these yesterday, looks like they got added back in the workspace PR. 
Rebases are hard, all good!
2023-06-05 14:16:14 -04:00
Joe Savona 66ca1c75cb Fix dropped constant declaration bug 2023-06-05 14:08:29 -04:00
Joe Savona ad32811b64 [RFC] Improve validation against ref access in render
The goal of this PR is to improve ValidateNoRefAccessInRender to find function 
expressions which a) access refs and b) may be called during render. Currently 
we always allow ref access in any function expression, but that's obviously 
optimistic. 

For the approach, the observation is that we already have a system that tells us 
whether a function may get called — mutable range inference. So long as we 
consider a function "mutable", we'll infer a range for it, but the problem is 
that we don't currently view functions which depend on refs to be mutable. So 
here I'm doing ~~sort of~~ a hack to force function deps on refs to be treated 
as Effect.Capture. This is enough for the function to be considered mutable, for 
a mutable range to be assigned, and for us to detect that during ref validation. 

I don't love the hack, i'm open to other ideas!
2023-06-05 14:08:26 -04:00
Mofei Zhang b160ee8522 [tests] Tests and patch for context variables
--- 

- [patch] Find context variables within FunctionDeclarations (previously 
missing) 

- [todo] Need to fix non-allocating values / variables being DCE'd 

One approach is to label everything referenced by a lambda as context variables. 
I couldn't come up with other examples that break without this change, so I 
wonder if something lighter / hackier works just as well. (My only hesitation is 
that we may end up losing out on potential optimizations for everything aliased 
to these variables).
2023-06-05 15:04:46 -04:00
Mofei Zhang 6d1d05ff91 [runtime] Update makeReadOnly runtime
--- 

(wip, waiting for feedback on workplace post) 

- remove calls to `isInROMode`, as we want to log all mutations after 'freezing' 
a value (both within and outside of render cycle) 

- add `source` parameter -- this is the function name of the parent component / 
hook
2023-06-05 14:46:59 -04:00
Mofei Zhang 5ba0a03e00 [config] Clean up codegen imports and gating
- Gating checks for debugging / profiling can be moved to within the 
instrumentation or makeReadOnly functions. This simplifies codegen and reduces 
code bloat. 

- Warn if importing conflicting identifiers 

- Aggregate imports from the same source
2023-06-05 14:46:59 -04:00
Mofei Zhang a65e9cd1f1 [devx] emit calls to makeReadOnly for debugging
--- 

Emit calls to makeReadOnly for memoized values. 

```js 

function MyComponent() { 

let x; 

if (c_0) { 

x = // ... (recompute x) 

$[0] = __DEV__ ? makeReadOnly(x, "MyComponent") : x; 

} else { 

x = $[0] 

} 

} 

``` 

- import source / specifier should be configurable, as 

- we'll likely want to add gk gating to `makeReadOnly` itself to reduce codesize 
bloat 

- each Forget project needs different logging and filter configurations 

- codegen function name as an argument for easier debugging 

- only freeze memoized outputs
2023-06-05 14:46:58 -04:00
Lauren Tan 40d1459115 Scaffold workspaces
![image](https://github.com/facebook/react-forget/assets/1390709/bb27e33f-6a88-4b71-8ce2-aeb8db0372f7) 

This is an attempt to scaffold yarn workspaces into our repo with as minimal as 
possible changes to our current setup and directory structure. Essentially this 
PR moves current `src` into `packages/babel-plugin-react-forget` as a first step 
to keep everything working. Later on when we're ready we can split out a 
`react-compiler` package that is decoupled from Babel, but it's too early for 
that now
2023-06-05 13:35:39 -04:00
Joe Savona 3744930728 Fix last(?) order of evaluation bug
Not to get ahead of myself (sorry i had to), but i think this is the last order 
of evaluation bug. At least it's the last one we know of[1]. Per the previous 
PR, the issue is that constant propagation can copy the last value of a sequence 
expression to where the sequence is used, leaving the original sequence 
expression out of order after other instructions are moved around. We fix that 
here by explicitly skipping constant propagation for the last value of a 
sequence block. 

[1] There are some places where we _would_ have evaluation order bugs if we 
allowed arbitrary expressions, but we explicitly limit the expressions we allow 
in those places. For the curious: switch test case values and destructuring 
default values.
2023-06-04 21:41:27 -04:00
Joe Savona 1e0eb25cd0 Use sequence terminal, fix most remaining order-of-evaluation bugs
Changes the lowering for sequence expressions to use the new terminal. When 
converting to a ReactiveFunction, we convert these terminals into 
ReactiveSequenceValues, which nests the instructions and preserves order of 
evaluation in the output. 

The only catch is constant propagation — constant propagation breaks 
order-of-evaluation because it can effectively copy the final value of a 
sequence elsewhere, leaving the original sequence in the wrong place. I'll 
address that in a follow-up.
2023-06-04 21:26:01 -04:00
Joe Savona cd115ec128 Scaffold for sequence terminal
I realized that we can use our value block system to fix _most_ of the remaining 
order-of-evaluation issues we had with sequence expressions. This PR adds a new 
SequenceTerminal to HIR; there is already a ReactiveFunction equivalent 
(ReactiveSequenceValue) that the next PR will convert this terminal into.
2023-06-04 21:25:59 -04:00
Joe Savona e410815aeb Infer more primitive types
Handles three more cases: 

* Template literals 

* deletion (property/computed) 

* type casts 

Only the latter has an observable impact, though i added tests for deletion just 
in case and found a bug. For type casts, they're reasonably common internally 
for fixmes, so this PR will help to ensure we don't drop type information just 
because of a cast.
2023-06-04 20:27:24 -04:00
Joe Savona 084edadaa1 Distinguish error fixtures for invalid code
Renames some error fixtures for clarity, "error.invalid-*" are fixtures that are 
expected to fail for invalid input, where other "error.*" fixtures are basically 
todos. While i was here i clarified the error messages for invalid useMemo 
callbacks, and changes the error severity from Invariant to InvalidInput.
2023-06-04 11:41:42 -07:00
Joe Savona e41a8d6a1e Validate destructuring assignment to globals
Fixes one more category of bug. For assignment expressions, we validating 
against redeclaring a global variable when the assignment target was an 
identifier, but not when the global was reassigned via destructuring. This PR 
adds a `lowerIdentifierForAssignment()` helper and uses it for assignment of all 
identifier variants, including destructuring.
2023-06-04 11:29:55 -07:00
Joe Savona 2ae3592d29 Fix/review bug repros, mark fixed
I reviewed the test cases we have marked as bugs ("_bug.*") and realized that 
several of them are already fixed — woohoo! Then one wasn't fixed _yet_: our 
type inference loses track of refs if you stash them inside an object/array. But 
that's why I added the ValidateNoRefAccessInRender pass, which i've updated to 
detect and reject these invalid cases. There are now only a few bugs left (more 
fixes coming).
2023-06-04 11:08:54 -07:00
Lauren Tan 6d6518161a Cleanup packages directory
Prepare to move to a yarn workspaces setup
2023-06-02 13:06:36 -04:00
Lauren Tan 479ef4045b Move entrypoint into its own directory 2023-06-02 13:06:35 -04:00
Lauren Tan 2eded946f8 Make CompilerEntrypoint a little more generic
These are still quite Babel specific, but the interface is slightly more 
generic: CompilerEntrypoint takes a non-Babel specific CompilerPass as an 
argument instead of passing Babel's PluginPass directly. 

In the future we can consider lowering the whole Program into HIR but that 
involves a significant lift in our representation and a small amount of new 
syntax to support (eg import statements), so this PR is the extent of this stack 
for now
2023-06-02 13:06:35 -04:00
Lauren Tan b98ee34f45 Delegate BabelPlugin to CompilerEntrypoint
Moves the existing logic in BabelPlugin to its own files
2023-06-02 13:06:34 -04:00
Lauren Tan 9077ac1f27 Copy files from src/Babel to src/ 2023-06-02 13:06:34 -04:00
Joe Savona 7fddf42d06 [ez] Hooks dont escape, their args do 2023-05-31 16:05:07 -07:00
Joe Savona 0ae37f1568 Treat setState as non-reactive
Updates PruneNonReactiveDependencies to treat setState functions as 
non-reactive, since we know they have a stable identity. This is based on type 
inference and our recently added definitions for useState and its return type, 
so it's conservative and will only work when our inference can prove that the 
scope dependency has the SetState type. 

Note that this approach is simple and has limitations, notably the fact that the 
setState is non-reactive doesn't propagate. But it's simple, trivially correct, 
and already improves codegen somewhat, so i figured it's worth landing for now. 

## Test Plan 

Tested on internal app
2023-05-31 14:29:31 -07:00
Joe Savona 9c453ad26f Validate frozen lambdas are actually frozen
Adds validation to reject freezing mutable lambdas, since lambdas cannot _be_ 
frozen, they're either frozen or not. Example invalid code: 

```javascript 

function Component(props) { 

const x = {value: ""}; 

const onChange = (e) => { 

// MUTATION!!!!! 

x.value = e.target.value; 

setX(x); 

}; 

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

} 

``` 

Note that there is a separate issue in which we are not detecting lambdas that 
would definitely modify immutable values. We may need to distinguish 
ConditionallyCapture (captures if the value is mutable, otherwise readonly) from 
Capture (definitely mutates) in order to make that case work. But already this 
validation helps prevent some invalid code.
2023-05-31 14:03:02 -07:00
Joe Savona 8e341294d0 [be] Upgrade to TypeScript 5.1
The functional changes in 5.1 don't seem that compelling but there are some perf 
improvements, supposedly.
2023-06-01 13:23:09 -07:00
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