dillon1000/react

Commit

[rhir][optim] Preserve conditional deps when propagating reactive scopes

--- 

**This PR slightly changes the semantics of ReactiveScopeDependencies**. 
Previously, reading a ReactiveScopeDependency is guaranteed to preserve the 
`nullthrows` semantics of its own declarations (not that of its inner scopes). 
This does not affect the overall correctness properties, since we already hoist 
reading of conditional dependencies (and thus may throw earlier than the 
original source). 

E.g. we already do not preserve *where* the nullthrows occurs. 

```javascript 

function Component(props) { 

// throws here, before print(x) 

const c_0 = props.a.b !== $[0]; 

let x; 

if (c_0) { 

x = {}; 

print(x); 

if (...) mutate1(x, props.a.b); 

mutate2(x, props.a.b); 

// ... 

``` 

### Summary 

This is an optimization, not a correctness property. 

When propagating reactive dependencies of an inner scope up to its parent, we 
want to *retain information about conditional dependencies* -- not the derived 
unconditional dependencies. This helps us produce more granular dependencies in 
the parent scope. 

Current implementation: 

```javascript 

const innerScopeDeps = innerScope.depTree.deriveMinimalUnconditionalDeps(); 

for (const dep of innerScopeDeps) { 

currentScope.depTree.addDep(dep); 

} 

``` 

New implementation: 

```javascript 

// union of a tree takes union of each node 

currentScope.depTree = currentScope.depTree.union(innerScope.depTree); 

``` 

### Example 

In the below example: 

- `scope @1` has a conditional dependency of `props.a.b`, but that reduces to 
the unconditional dependency `props` 

- `scope @0` itself has a unconditional dependency of `props.a.b` 

- Currently, Forget joins the derived / reduced dependencies of inner scopes, 
which adds `props` as unconditional dependency of `scope @0` 

- With this change, Forget joins the property trees and retains info about 
conditional deps, which adds `props.a.b` as a conditional dep of `scope @0`. 

```javascript 

// scope @0 (deps=[???] decls=[x, y]) 

let y = {}; 

// scope @1 (deps=[props] decls=[x]) 

let x = {}; 

if (foo) mutate1(x, props.a.b); 

mutate2(y, props.a.b); 

``` 

### Followup 

We currently keep track of properties unconditionally accessed per 
ReactiveBlock. Eventually we want to keep track of properties unconditionally 
accessed across blocks (as according to control flow). 

Consider the following code, in which sibling scopes 0 and 1 are sequentially 
executed. In this case, we can safely add props.a.b as a dependency of scope 1. 

```javascript 

// scope@0 (deps=[props.a.b], decls=[x]) 

let x = { a: foo(props.a.b) }; 

// scope@1 (deps=[???], decls=[y]) 

let y = {}; 

if (...) { 

mutate(y, props.a.b); 

} 

```
Browse files
Changed paths4 files
First-parent comparison
M compiler/forget/src/ReactiveScopes/DeriveMinimalDependencies.ts ModifiedM compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts ModifiedA compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-deps-join-uncond-scopes-cond-deps.expect.md AddedA compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-deps-join-uncond-scopes-cond-deps.js Added
Patch

Files changed

Rendering syntax-highlighted changes…