Files
react/compiler/packages/sprout
Joe Savona 72c27b6893 Feature flag for transitively freezing values
This PR adds a feature flag to model a potential new-in-practice rule in React: 
that freezing a function expression also freezes its closed-over values, 
transitively. For example, in the following code `data` is frozen when the 
lambda that captures it is is passed to useEffect: 

```javascript 

const data = []; 

// useEffect freezes its argument (the function expr), which transitively 
freezes its captured value data 

useEffect(() => { 

foo(data); 

}, [data]); 

data.push(true); // ERROR: mutating a frozen value 

mutate(data); // we conservatively assume this doesn't mutate but could be wrong 

``` 

Note that this rule has never been written down or enforced. It is theoretically 
equivalent to the rule (already implemented in Forget) that values captured by 
JSX are frozen: 

```javascript 

const style = {...}; 

<div style={style}>...</div> 

style.width = 10; // ERROR: mutating a frozen value 

mutate(style); // we conservatively assume this doesn't mutate but could be 
wrong 

``` 

However, JSX is typically constructed toward the very end of a render function. 
Thus in practice there isn't much subsequent code that could even modify such a 
captured value. But for the useEffect case (and other hooks that take closures 
as arguments), they tend to occur much earlier in a render function. There's 
more code that can run later and still modify the captured values, without 
causing issues in practice. The _practical_ rule today is that you can't modify 
values captured by frozen lambdas _after the component returns_: it's fine in 
practice to modify captured values between calling eg useEffect and returning 
from render. 

Thus this feature flag is fairly likely to break some percent of real product 
code. I'm adding this so that we can experiment and see how unsafe it actually 
is.
2023-11-06 08:33:32 -08:00
..

Sprout 🌱

React Forget test framework that executes compiler fixtures.

Currently, Sprout runs each fixture with a known set of inputs and annotations. Sprout compares execution outputs (i.e. return values and console logs) of original source code and the corresponding Forget-transformed version. We hope to add fuzzing capabilities to Sprout, synthesizing sets of program inputs based on type and/or effect annotations.

Sprout is now enabled for all fixtures! If Sprout cannot execute your fixture due to some technical limitations, add your fixture to SproutTodoFilter.ts with a comment explaining why.

Sprout CLI

Sprout can be run from packages/babel-plugin-react-forget. When adding new fixtures to Sprout, please check that fixture outputs look reasonable with yarn sprout --filter --verbose.

# rebuild when sprout or babel-plugin-react-forget changes
$ yarn sprout:build

# evaluate all test fixtures not listed in SproutTodoFilter
$ yarn sprout

# show all sprout options
$ yarn sprout --help
Options:
  --sync     Run compiler in main thread.
                          [boolean] [default: false]
  --filter   Evaluate fixtures in filter mode.
                          [boolean] [default: false]
  --verbose  Print all fixture outputs and logs.
                          [boolean] [default: false]

Sprout can be run in filter mode with yarn sprout --filter. Just like Snap, Sprout expects babel-plugin-react-forget/testfilter.txt be formatted as such.

  • first line: // @only or // @skip
  • all other lines: a test fixture name, i.e. the relative path from the compiler fixtures dir, without a .js or .expect.md extension.

Example:

// @only
console-readonly
constant-propagate-global-phis
dce-loop

Adding fixtures to Sprout

1. Annotate fixtures.

Each fixture test executed by Sprout needs to export const FIXTURE_ENTRYPOINT object with the following type signature.

type FixtureEntrypoint<T> = {
  // function to be invoked
  fn: ((...params: Array<T>) => any),
  // params to pass to fn
  // (if `fn` is a react component, this should be an array
  // with exactly one element -- props)
  params: Array<T>,
}

Example:

// test.js
function MyComponent(props) {
  return <div>{props.a + props.b}</div>;
}
export const FIXTURE_ENTRYPOINT = {
  fn: MyComponent,
  params: [{a: "hello ", b: "world"}],
};

2. Import / define helper functions.

  • Prefer importing helper functions for readability and simplicity.
  • Fixtures that require helper functions with specific types or mutability can define their own within the same fixture file.
// test.js
import { addOne } from 'shared-runtime';

function customHelper(val1, val2) {
  // This directive is important, as helper functions don't
  // always follow the rules of React.
  "use no forget";
  // ...
}

// ...

Notes

  • If your fixture needs to import from an external module, we currently only support importing from react (see Milestones todo list).

  • Any fixture can use React hooks, but they need to be first imported. We may later enforce that only isComponent: true fixtures can use React hooks.

    import {useState} from 'react';
    
  • If your fixture wants to export multiple functions to Sprout to run, please split up the fixture into multiple files (e.g. test-case-1, test-case-2, etc).

  • Sprout currently runs each fixture in an iife to prevent variable collisions, but it does not run fixtures in isolation. Please do not mutate any external state in fixtures.

  • Sprout does not run fixtures listed in SproutTodoFilter.ts, even in filter mode.

Milestones:

  • [] Render fixtures with React runtime / testing-library/react.
  • [] Make Sprout CLI -runnable and report results in process exit code.
  • [] Enable Sprout by default and run it in the Github Actions pipeline.
  • [🚧] Make all existing test fixtures Sprout compatible (see SproutTodoFilter.ts). This involves each fixture being annotated with FIXTURE_ENTRYPOINT and using shared functions and/or defining its own helpers.
    • 77 done, ~410 to go
  • (optional) Store Sprout output as snapshot files. i.e. each fixture could have a fixture.js, fixture.snap.md, and fixture.sprout.md.
  • Add support for fbt.