Commit Graph

1529 Commits

Author SHA1 Message Date
Jan Kassens fb0cf4f833 Make collapsed playground tabs more compact
Make collapsed playground tabs more compact 

We have a lot of steps creating a lot of tabs by now. This makes them visually a 
lot more compact without a full redesign. 

Makes it a bit harder to read and less modern looking, but I think usability was 
a bit bad with the wide tabs. 

**Before:** 


![image](https://github.com/facebook/react-forget/assets/11849/a87c8f7e-d60c-43c1-aaea-1a1f77e082d0) 

**After:** 


![image](https://github.com/facebook/react-forget/assets/11849/227ebf1e-055f-439a-bfc9-e69bae091f6a)
2024-01-16 13:10:32 -05:00
Sathya Gunasekaran 38d3423970 Treat function expression deps as conditional 2024-01-15 12:44:22 +00:00
Sathya Gunasekaran 121e72a342 Add flag for treating function deps as conditional 2024-01-15 12:44:22 +00:00
Sathya Gunasekaran 6d133111a9 Add test for function deps not treated as conditional
In the test, unconditionally reading props.bar.length will throw when props.bar 
is null.
2024-01-15 12:44:22 +00:00
Sathya Gunasekaran 25ec9fcea4 Add .git-blame-ignore-revs
Ignore commits that just change formatting and file structures
2024-01-15 12:44:22 +00:00
Joe Savona 90348cc873 [housekeeping] Remove disabled test262 setup
I sincerely appreciate the effort to get test262 up and running. This was my 
idea, it seemed like a really good way to test our correctness on edge cases of 
JS. Unfortunately test262 relies heavily on a few specific features that we 
don't support, like classes and `var`, which has meant that we never actually 
use this as a test suite. 

In the meantime we've created a pretty extensive test suite and have tools like 
Sprout to test actual memoization behavior at runtime, which is the right place 
to invest our energy. Let's remove?
2024-01-12 12:26:30 -08:00
Joe Savona bcbbbec1f5 [housekeeping] Remove unused test fixtures
We don't use these fixtures, let's just clean them up.
2024-01-12 12:22:23 -08:00
Joe Savona 33118be835 [housekeeping] Remove fixtures/
Do we still use these? I'm happy to close this PR if we still want this but it 
feels like these may have served their purpose and no longer be necessary.
2024-01-12 12:22:19 -08:00
Joe Savona 653373141a Extra fixture for validating preserved memoization of non-escaping callbacks 2024-01-12 14:32:35 -08:00
Joe Savona 0c866672b0 Fix false positive on preserving memo of non-escaping values
Fixes the false positive in the previous PR. When we prune a scope because it's 
values are non-escaping, we now also remove any `Memoize` instructions for that 
scope. The intuition being that we're actively removing unnecessary memoization, 
so we don't need to check that the memoization occurred anymore.
2024-01-11 17:13:41 -08:00
Joe Savona 8e4d2fb69d Repro for false positive in validatePreserveMemoization on non-escaping value
This demonstrates a false positive in validatePreserveExistingManualMemoization. 
We prune memoization of non-escaping values, but the validation pass just sees 
that the value "should" have a scope and that scope doesn't exist, and thinks we 
failed to preserve memoization.
2024-01-11 16:52:57 -08:00
Joe Savona c3a947643f Update hoisting error message to allow error aggregation
Interpolating values into the `reason` field of an error breaks our error 
aggregation. This PR moves the offending function name into the `description` 
field which isn't used for aggregation.
2024-01-11 15:54:55 -08:00
Jan Kassens f6f042d747 Switch test262 submodule to https url
I had trouble checking out the repo using Sapling because the submodule couldn't 
clone properly. I got 

> Error: Permission denied (publickey) 

In my branch I tested that the https URL format seems to work okay with Sapling.
2024-01-12 15:07:24 -05:00
Jan Kassens 8a634bc1c0 Add frozen reason for props and hook arguments
Add frozen reason for props and hook arguments 

Improves the error message when mutating props or hook arguments. 

Previously, this would print a generic error about mutating global variables.
2024-01-12 14:51:59 -05:00
Joe Savona c80f0f022c useContext returns frozen values
This was an oversight in the original definition of useContext (oops my bad). 
Context values are owned by React and should not be modified. I found this 
because some cases of existing useMemo were not preserved (tested via the 
validatePreserveExistingManualMemo flag) due to function calls referencing 
context being assumed to mutate. 

This change will allow more memoization, it's also just more correct for the 
rules of React. Note the new ValueReason variant so that we can provide a 
precise error message about mutating context values.
2024-01-10 16:12:17 -08:00
Joe Savona 7ca3b004ae Early branch with new type inference foundation
It's starting to get complex just with a couple of extra
passes — we either need to substantially extend the HIR or (as i've done so far)
pass information from early passes to later ones. This PR changes things so that
very early in the babel plugin we fork into a separate mode. Forest has
its own `compileProgram()` equivalent, its own pipeline, its own codegen, etc.
2024-01-03 10:47:47 -08:00
Joe Savona bd37fbe06a [wip] Fix phi inference, expose InferMutableRange issue
> Update: this is now passing all tests. The approach is likely wrong, and even 
if it's fine it needs some cleanup. Putting up for review as folks (esp 
@gsathya) have time. 

## Background 

InferTypes was intended to infer types for phi identifiers, but by accident we 
ended up storing the inferred type on `phi.type` instead of `phi.id.type`, which 
is the type that usages of the phi will reference. Because of this, we weren't 
actually inferring types for several cases, for example if both if/else branches 
assign `x` to an array literal, we'd ideally like the corresponding phi id to be 
typed as a BuiltInArray: 

```javascript 

let x; 

let y = { ... }; 

if (cond) { 

x = []; 

} else { 

x = []; 

} 

// x should be BuiltnArray here. We inferred that on Phi.type but the x here 
wouldn't get that type previously 

x.push(y); 

``` 

## Circular Types 

I started by removing the `Phi.type` property and updating inference to store 
the result of phi unification on `phi.id.type` — but this revealed other issues. 

First was this can create circular types when there are loops. The solution is 
to basically allow circular types _for phis only_, and when we detect them we 
remove the cycle. Basically whenever we have a situation where we have some type 
variable X, and a type Y that is a (nested) phi type one of whose transitive 
operands contains X, we remove X from the transitive type and attempt to 
collapse the phi type upwards if all of its remaining operands are the same: 

``` 

X=Type(1) 

Y=Phi [ 

Type(2), 

Type(3) = Phi [ 

Type(1), // <-- cycle but we can prune this 

Type(2), 

Type(2), 

] 

] 

=> 

X=Type(1) 

Y=Phi [ 

Type(2), 

Type(3) = Phi [ // all remaining operands are the same, we can prune this 

Type(2), 

Type(2), 

] 

] 

=> 

X=Type(1) 

Y=Phi [ // all remaining operands are the same, we can prune this 

Type(2), 

Type(2), 

] 

=> 

X=Type(1) 

Y=Type(2) 

``` 

We have to do this not just doing unify(), but also in `get()` since there are 
cases where we don't know yet which type variables we can remove from a phi. 
Without also doing the pruning in get, we get an infinite loop. 

## Reactive Scope Alignment 

The above fixed the circular types, but exposed some new cases that can occur in 
terms of mutable ranges and ast structures: it wasn't possible before to have a 
Store on a phi node in practice, since that relied on type information which we 
didn't have for phis. 

The new validation that all instructions for a scope are part of that scope 
caught a couple issues, which were basically like this: 

``` 

[1] Sequence 

... 

[9] StoreLocal x@0[9:28] 

[10] ... 

``` 

Note that scope 0 starts at instruction 9, but that instruction is not at the 
block scope level. The first instruction at the block scope level that is within 
the range of scope 0 is instruction 10, which is after the scope should have 
started! So I also had to update AlignScopesToBlockScopes to handle the case of 
logical, conditional, and sequence expressions: we sometime need to adjust a 
scope start earlier in case they contain instructions that should start a scope.
2024-01-02 15:31:57 -08:00
Joe Savona bf859705b5 Enable early return support by default
This PR enables the new feature flag by default, but keeps the flag around so 
that we can quickly turn it off if there are issues.
2023-12-20 13:52:43 -08:00
Joe Savona fcc2182641 Handle scopes with only early return and no decls/deps/reassigns
Fixes the case from the previous PR by using a different sentinel for 
uninitialized cache values and early returns. I confirmed with console.log that 
the reactive scope for `x` only evaluates on the first execution, after which we 
figure out that we don't need to execute it again.
2023-12-20 13:52:42 -08:00
Joe Savona a753a326ad Fixture for only early return without decls/deps/reassigns 2023-12-20 13:52:41 -08:00
Joe Savona ad57e661f4 Sprout support for rendering multiple times w different props
RFC. This is a quick sketch of adding support to Sprout to render the same 
component instance multiple times with different props. This doesn't test 
memoization (though it forms a basis for testing it, more below), but does allow 
us to test that the code properly reacts to inputs and doesn't get "stuck" 
always returning the same output even when inputs change. 

Possible extensions: 

- Support calling non-component functions multiple times 

- Test memoization by having the `toJSON()` helper track objects it has 
encountered before, assign each object a unique id, and then emit subsequent 
references to the same value as the id instead of the printed form of the 
object. 

For example if we call a memoized function with the same input twice in a row, 
today we might get output like: 

``` 

[{a: 1}], 

[{a: 1}], 

``` 

Which doesn't tell us if the object is equal. Instead we could emit output like: 

``` 

[{a: 1}] #0, 

#0, 

``` 

Which allows verifying that memoization actually happened. Or we could automate 
this and just assert that anything structurally equal has to be referentially 
equal — though there are cases with conditionals that break this.
2023-12-20 13:52:41 -08:00
Joe Savona a86d279c46 Initial (flagged) support for reactive scopes with early return
Adds support for early returns within reactive scopes, behind a new feature 
flag. The flag is off by default, where this case continues to throw a Todo 
bailout. 

Since implementing a sketch of the codegen in the previous PR I realized that 
it's easy enough to implement the more optimal output, so i've updated that 
here. Rather than both the if and else branch of the reactive scope having an 
"if the return value was not a sentinel return it" check, we instead make the 
return temporary a proper declaration of the reactive scope. Then, since it's 
actually an output it's available in the outer block scope, and we could do a 
single if-return after the reactive scope. 

Edit: see comment below for thoughts on test cases.
2023-12-20 13:52:40 -08:00
Joe Savona 3175056935 Codegen for early returns in reactive scopes
Implements codegen for reactive scopes with early returns, though we don't ever 
construct such a case yet. See comments in the code. There is a slightly more 
optimal output that would require a larger refactor (also described in code 
comments), for now i'm starting with the simpler approach since this is 
relatively rare so we don't need to optimize code size / runtime as much.
2023-12-20 13:52:39 -08:00
Joe Savona ed9d6a2ca1 Scaffolding for early return from reactive scopes
Adds a new `earlyReturnValue` property on ReactiveScope which will be set if the 
scope had one or more early returns, with information about the temporary 
identifier that the early return value will be assigned to, as well as the label 
to be used for breaking (to simulate the early-return). The next PR shows the 
intended codegen.
2023-12-20 13:52:38 -08:00
Joe Savona 4c68da2e60 Todo for early return within reactive scopes
Adds a new compiler pass that will eventually actually handle early returns 
within reactive scopes. For now it just detects them and throws a Todo error.
2023-12-20 13:52:37 -08:00
Joe Savona f504eaa16e Add back transitive freeze functions option
Adds back a mode to transitively freeze function expressions, independently from 
the mode to preserve existing manual memoization. This lets us experiment with a 
few variants: 

* Preserve existing memoization 

* Validate existing memoization with: 

* `enableAssumeHooksFollowRulesOfReact` && 
`enableTransitivelyFreezeFunctionExpressions` 

* `enableAssumeHooksFollowRulesOfReact` only 

* neither of those flags 

Note that `enableTransitivelyFreezeFunctionExpressions` alone probably doesn't 
make sense, it's more aggressive than 

`enableAssumeHooksFollowRulesOfReact` so we might as well try them together.
2023-12-18 15:33:02 -08:00
Joe Savona c77acb3ac6 Separate mode to validate preserving manual memoization
Adds a new mode which validates that existing manual memoization is preserved 
_without_ using information from the manual memoization to affect compilation. 
This gives us a way to try out the more aggressive version of Forget — ignoring 
manual memoization — first and see how much code bails out and what patterns 
cause this. 

We can then proceed to enable the mode to actually _preserve_ existing memo 
guarantees only where necessary.
2023-12-15 17:12:06 -08:00
Joe Savona 3e79c38604 More useCallback with ref fixtures 2023-12-15 16:59:19 -08:00
Joe Savona af1aa8d0d3 Validation that useMemo/useCallback is preserved in the output
Extends `@enablePreserveExistingMemoization` to validate that all of the 
original values were actually memoized. This works nearly identically to how we 
validate effect deps are memoized. We look for Memoize instructions whose values 
need memoization but whose range extends past the memoize instruction, or where 
the value isn't memoized at all.
2023-12-15 16:22:17 -08:00
Joe Savona 0b32173d09 More fixtures for useCallback with refs
Extra fixture confirming that some usage of refs can break memoization, both in 
normal mode and in preserve-existing-memo mode.
2023-12-15 16:22:13 -08:00
Sathya Gunasekaran 721b6a4f91 [patch] Compile hooks with any number of args in infer mode 2023-12-18 13:35:56 +00:00
Joe Savona c6200d1a2b Fix comments, extend fixtures 2023-12-15 15:19:41 -08:00
Joe Savona 6747d4e33c PreserveMemo for useCallback transitively freezes function exprs
Merges `@enableTransitivelyFreezeFunctionExpressions` into the new 
`@enablePreserveExistingMemoizationGuarantees` mode, since they are both 
motivated by the same use case of preserving effect behavior by preserving 
existing memoization behavior. 

The idea is that `useCallback` has an implicit assumption: that the variables 
captured by the callback aren't subsequently modified. Previous PRs treated the 
values directly captured by the callback as frozen. But if those variables were 
themselves another function expression, and that expression captured a mutable 
value, then we wouldn't consider the freeze to be transitive: 

```javascript 

const object = makeObject(); 

useHook(); // oops, hook call inside `object`'s mutable range, can't memoize 
object, log, or onClick! 

const log = () => { console.log(object) }; 

const onClick = useCallback(() => { log() }); 

maybeMutate(object); 

``` 

However, the assumption of such code is that it _doesn't_ modify such 
transitively captured values. So here we merge 
`@enableTransitivelyFreezeFunctionExpressions` mode into the 
memoization-preserving mode. Now, the memoize instructions emitted for 
useCallback (and useMemo) will transitively freeze captured function 
expressions, allowing us to memoize. 

The flip side of this is that some code may be violating these rules. We'll rely 
on runtime validation to detect such cases.
2023-12-15 15:19:40 -08:00
Joe Savona 08e92a3a8d More test cases for useCallback
Adds test cases per the previous PR for useCallback: 

* callback that references another callback, which in turn references a 
possibly-mutated value 

* callback that references a ref
2023-12-15 15:19:39 -08:00
Joe Savona 5bfd70ac6f Preserve memoization guarantees for useCallback
Improves `@enablePreserveExistingMemoizationGuarantees` for the useCallback 
case. Similar to useMemo, we add an explicit `Memoize` instruction for the 
callback function itself _and_ for its dependencies. This means we'll assume the 
callback doesn't mutate any captured variables. 

TODO: check this with cases involving refs (should be allowed, but also not 
accidentally freeze the ref) and reassignment of locals (should be disallowed, 
though that might just be a validation we're missing today)
2023-12-15 13:47:24 -08:00
Joe Savona 86e2edfa87 Prune memoize instructions in codegen
The previous PR introduced `memoize` instructions whose lvalues aren't used, but 
which can't be pruned by DCE due to pipeline ordering. Here we change to make 
memoize an instruction intended for its side effects only, and prune during 
codegen.
2023-12-15 13:47:23 -08:00
Joe Savona 723b616c67 enablePreserveMemo treats memo deps as frozen
See discussion on #2448 for full context. In the new 
`@enablePreserveExistingMemoizationGuarantees` mode, the goal is to preserve the 
existing referential equality guarantees from the original code. #2448 lays the 
groundwork by explicitly marking the _output_ of each useMemo block as memoized, 
hinting to the compiler that the value cannot subsequently change. This ensures 
the mutable range doesn't extend _later_, possibly overlapping a hook call and 
causing memoization to gett pruned. 

This PR fixes the other direction. There are cases where free variables 
referenced in the useMemo block could have been inferred as mutated, which could 
then extend the _start_ of the range earlier past a hook: 

```javascript 

const foo = createObject(); 

useBar(); 

const baz = useMemo(() => { 

const baz = createObject(); 

maybeMutate(foo, baz); 

return baz; 

}, [foo]); 

``` 

Here the compiler would infer that both `baz` and `foo` are mutable at the 
`maybeMutate()` call, grouping them in the same scope. But that scope would span 
the `useBar()` call, and be pruned, meaning that `baz` went unmemoized. 

However, useMemo blocks shouldn't be mutating free variables. Only variables 
newly created within the useMemo block should be mutable. So this PR extends the 
feature to treat all free variables referenced in a useMemo block as frozen as 
of the block itself.
2023-12-15 13:47:22 -08:00
Joe Savona ec27708024 [be] Remove unnecessary check for SetState type in analyzefunctions
This was used as part of the previous ValidateNoSetStateInRender, but i rewrote 
that to not rely on the mutable range of function expressions.
2023-12-15 13:47:21 -08:00
Joe Savona 2abd439b43 Option to preserve existing memoization guarantees
Adds an option to preserve existing memoization guarantees for values produced 
with useMemo and useCallback. We still discard the calls to these hooks, but we 
preserve the information that the value is frozen at that point in the program. 
Because these values are produced solely within the useMemo/useCallback 
callback, their mutation cannot have any interspersed hook calls. This means 
that the values mutable range will never span a hook and end at the point of the 
useMemo, ensuring that they are memoized at the same point. 

The main things that can change (relative to the orignal code) are: 

* Forget will infer a precise set of dependencies, ignoring the user-provided 
values. In practice this should only occur if the original code had a lint 
violation, which Forget would bail out on. So in practice this shouldn't happen 
unless the code doesn't use the React linter. 

* Forget may start the memoization block earlier than the developer did if other 
values are mutated along with the value being produced. This can cause 
memoization to fail, but only in situations where it would have failed 
previously: 

```javascript 

const a = []; 

useFoo(); 

const b = useMemo(() => { 

const c = a; 

c.push(1); 

return c; 

}, [a]); 

``` 

In this example (sans Forget) the useMemo will invalidate on every render 
because `a` will always be a new array and its listed as a dependency of the 
useMemo. Forget would correctly determine that the memoization would have to 
work as follows: 

```javascript 

let c; 

if (...) { 

const a = [] 

useFoo(); // OOPS we made a hook call conditional 

const t0 = a; 

t0.push(1); 

c = t0; 

... 

} else { 

c = $[...] 

} 

``` 

Because this is invalid, Forget would (later in the pipeline) strip out this 
memoization block and (as with the original) leave `c` un-memoized. 

In this same example, removing the hook would cause Forget to be able to memoize 
a value that wasn't memoized before: 

```javascript 

const a = []; 

const b = useMemo(() => { 

const c = a; 

c.push(1); 

return c; 

}, [a]); 

``` 

This invalidates every render without Forget, but would memoize correctly with 
Forget (it would expand the memoization block to include the declaration of 
`a`).
2023-12-15 13:47:20 -08:00
Joe Savona fc36043019 Fixture for pruning unmemoized nonreactive deps
Adds a fixture for our existing behavior that reactive scope dependencies 
exclude values which are non-reactive. The idea is that regardless of whether 
the value may actually get recreated over time or not, a "nonreactive" value 
cannot semantically change and therefore we can ignore changes in its pointer 
address.
2023-12-14 12:05:25 -08:00
Joe Savona 873a286029 Convert value block terminal invariant to todo 2023-12-12 16:52:24 -08:00
Joe Savona a6e65e83e8 Prefer reporting conditional hook violations over "hook as value" violations
After running the latest hook validation internally, I found some cases where 
there was a violation but the error message was not ideal. For example on this 
code: 

```javascript 

usePossiblyNullHook?.(); 

``` 

We reported a "hooks can't be used as normal values" violation, when we'd 
ideally report a "hooks can't be called conditionally" violation. The solution 
in this PR is to track errors by source location, and upgrade the former 
violation to the latter, more serious violation. See fixtures for examples.
2023-12-12 16:42:51 -08:00
Jan Kassens afbaa8d3ca Add a reason to ValueKind for better error messages (#2447) 2023-12-15 10:20:14 -05:00
Mofei Zhang 06376b906d [patch] Generate hook guards as function expressions
--- 

Arrow functions are not compatible with es3, which we sometimes target. This 
helps us make babel transform logic less complex
2023-12-14 19:42:24 -05:00
Sathya Gunasekaran 651d3448ac [vscode] update settings for v1.85
See https://code.visualstudio.com/updates/v1_85#_code-actions-on-save-and-auto
2023-12-14 15:47:38 +00:00
Sathya Gunasekaran a3c48def1c Add Babel plugin to annotate react components
This can be used to figure out all the components in the bundle by grepping the 
bundle for the typeof check.
2023-12-14 15:52:17 +00:00
Sathya Gunasekaran 201b46d947 [eslint] Make the plugin configurable
As part of this PR, we remove the custom defined logger and use the logger from 
the plugin options.
2023-12-14 15:37:41 +00:00
Mofei Zhang 12de013aa4 [patch] ObjectMethods should have the same scope as their parent
ObjectExpressions 

--- 

Currently, we're removing all reactive scopes containing object methods. This 
could produce incorrect output as object method instructions may still be 
included in other reactive scopes (and will lose their dependencies).
2023-12-12 17:44:01 -05:00
Mofei Zhang 0966480d16 [be] More tests for object methods 2023-12-12 17:44:01 -05:00
Mofei Zhang 4c2b89288f [be][fixtures] Pass message directly instead of overwriting Error object
--- 

Started getting crashes in watch mode from trying to overwrite `error.message` 
(invalid setter or frozen object). This should fix
2023-12-11 22:59:33 -05:00