A dependency D from either an instruction or scope is poisoned if there may be a
(non-linear) jump instruction between it and the start of its immediate parent
scope. Poisoned dependencies are added as conditional dependencies to their
parent scope.
(done: reduce false positives in scopes that begin after return/throw) (done:
fix bugs in recording and joining exhaustive conditional deps) (done: flesh out
commit message, clean up PR, add more fixtures)
--- \## Bug details:
Take a simple example: ```js target: { instrA; if (...) { instrB;
break target; } else { instrC; } instrD; // ... } instrE; // ... ```
This diagram shows how we represent this program in the reactive IR. - Blocks
are represented as a list of nodes. - Green nodes show instructions and value
blocks (simplified as a single instruction). - Pink nodes show terminals, which
transfer control to a subtree of nodes. <img width="450" alt="image"
src="https://github.com/facebook/react-forget/assets/34200447/930789f2-39cd-4ea8-b12a-530042807b46">
Prior to this PR, PropagateReactiveScopeDeps was incorrect because it assumed
that a block's instructions are evaluated unconditionally (which is how HIR
basic blocks work). E.g. if a reactive scope enclosed `block 1`, we assume that
`instrA` and `instrD` both will evaluate unconditionally.
This failed to account for `jump` instructions like break, continue, return, and
throw. This may result in invalid hoisting of PropertyLoads (i.e. Forget output
may throw when source does not throw). Note that other terminals (e.g. if and
loops) are not affected as they are self contained subtrees that evaluate
sequentially.
With the changes in this PR, we mark `block 1` as poisoned upon encountering the
`break` instruction. While `block 1` is active and poisoned, it will determine
how visited dependencies are added.
Here, added solid lines show unconditional dependencies, dashed lines show
conditionally accessed dependencies: - dependencies from `instrB, instrC` are
conditional because they are within conditional subtrees - dependencies from
`instrD` are conditional because it is within a poisoned block within its parent
scope.
<img width="450" alt="image"
src="https://github.com/facebook/react-forget/assets/34200447/81980f68-7e65-4bd7-ba94-3f0c26550e5c">
--- Recapping an offline discussion with @josephsavona: this pass would really
benefit from operating on HIR. The minimal work needed for this pass to run on
HIR is to rewrite and reorder `AlignReactiveScopesToBlockScopes` to operate on
HIR.
The following diagram shows what HIR blocks look like for the same code.
Evaluating hoistable PropertyLoad dependencies for a scope enclosing
`instr{A-D}` is much simpler: just evaluate whether the PropertyLoad evaluates
for every path between `bb0` and `bb4`. <img width="250" alt="image"
src="https://github.com/facebook/react-forget/assets/34200447/44b38939-defb-4b29-878d-4445ec6ccc06">
---
React Forget
React Forget is an experimental Babel plugin to automatically memoize React Hooks and Components.
Development
# tsc --watch
$ yarn dev
# in another terminal window
$ yarn test --watch
Notes
An overview of the implementation can be found in the Architecture Overview.
This transform
- needs plugin-syntax-jsx as a dependency to inherit the syntax from.
- should be run before plugin-transform-react-jsx
- assume the enforcement of rules of hooks, i.e.
- only call hooks from React functions
- only call hooks at the top level
- https://www.npmjs.com/package/eslint-plugin-react-hooks
Scaffolding
- https://github.com/facebook/flow/tree/master/packages/babel-plugin-transform-flow-enums
- https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts
Reference
Rust Development
First-Time Setup
- Install Rust using
rustup. See the guide at https://www.rust-lang.org/tools/install. - Install Visual Studio Code from https://code.visualstudio.com/. Note to Meta employees: install the stock version from that website, not the pre-installed version.
- Install the Rust Analyzer VSCode extension through the VSCode marketplace. See instructions at https://rust-analyzer.github.io/manual.html#vs-code.
- Install
cargo editwhich extends cargo with commands to manage dependencies. See https://github.com/killercup/cargo-edit#installation - Install
cargo instawhich extens cargo with a command to manage snapshots. See https://insta.rs/docs/cli/
Workspace Hygiene
Adding Dependencies
To add a dependency, add it to the top-level Cargo.toml
// Cargo.toml
[workspace.dependencies]
...
new_dep = { version = "x.y.z" }
...
Then reference it from your crate as follows:
// crates/forget_foo/Cargo.toml
[dependencies]
...
new_dep = { workspace = true }
...
Adding new crates
Rust's compilation strategy is largely based on parallelizing at the granularity of crates, so builds can be faster when projects have more but smaller crates. Where possible it helps to structure crates to minimize dependencies. For example, our various compiler passes depend on each other in the sense that they often must run in a certain order. However, they often don't need to call each other, so they can generally be split into crates of similar types of passes, so that those crates can compile in parallel.
As a rule of thumb, add crates at roughly the granularity of our existing top-level folds. If you have some one-off utility code that
doesn't fit neatly in a crate, add it to forget_utils rather than add a one-off crate for it.
Running Tests
Run all tests with the following from the root directory:
cargo test
The majority of our tests will (should) live in the forget_fixtures crate, which is a test-only crate that runs compilation end-to-end with snapshot
tests. To run just these tests use:
# quiet version
cargo test -p forget_fixtures
# without suppressing stdout/stderr output
cargo test -p forget_fixtures -- --nocapture
Another hint is that VSCode will show a "Run test" option if you hover over a test in the source code, this lets you run a single test easily. The command line will also give you the CLI command to run just that one test.
Updating Snapshots
The above tests make frequent use of snapshot tests. If snapshots do not match the tests will fail with a diff, if the new output is correct you can accept the changes with:
cargo insta accept
If this command fails, see the note in "first-time setup" about installing cargo insta.
CI Configuration
GitHub CI is configured in .github/workflows/rust.yml.