[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); } ```