Skip to content

Commit 6e67447

Browse files
author
Joe Savona
committed
[rust-compiler] Port FlattenScopesWithHooksOrUseHIR pass
Ported FlattenScopesWithHooksOrUseHIR (react#30) from TypeScript — flattens reactive scopes containing hook calls or use() calls since hooks must be called unconditionally. Converts affected scopes to PrunedScope or Label. Zero regressions.
1 parent 49ae06d commit 6e67447

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

compiler/crates/react_compiler/src/entrypoint/pipeline.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,11 @@ pub fn compile_fn(
379379
let debug_flatten_loops = debug_print::debug_hir(&hir, &env);
380380
context.log_debug(DebugLogEntry::new("FlattenReactiveLoopsHIR", debug_flatten_loops));
381381

382+
react_compiler_inference::flatten_scopes_with_hooks_or_use_hir(&mut hir, &env);
383+
384+
let debug_flatten_hooks = debug_print::debug_hir(&hir, &env);
385+
context.log_debug(DebugLogEntry::new("FlattenScopesWithHooksOrUseHIR", debug_flatten_hooks));
386+
382387
// Check for accumulated errors at the end of the pipeline
383388
// (matches TS Pipeline.ts: env.hasErrors() → Err at the end)
384389
if env.has_errors() {
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
//
3+
// This source code is licensed under the MIT license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
//! For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are
7+
//! different from regular functions in two key ways:
8+
//! - They can introduce reactivity even when their arguments are non-reactive (accounted for in
9+
//! InferReactivePlaces)
10+
//! - They cannot be called conditionally
11+
//!
12+
//! The `use` operator is similar:
13+
//! - It can access context, and therefore introduce reactivity
14+
//! - It can be called conditionally, but _it must be called if the component needs the return value_.
15+
//! This is because React uses the fact that use was called to remember that the component needs the
16+
//! value, and that changes to the input should invalidate the component itself.
17+
//!
18+
//! This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the
19+
//! reasoning is slightly different for each, the result is that we can't memoize scopes that call
20+
//! hooks or use since this would make them called conditionally in the output.
21+
//!
22+
//! The pass finds and removes any scopes that transitively contain a hook or use call. By running all
23+
//! the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately
24+
//! describe the set of values which "construct together", and remove _all_ that memoization in order
25+
//! to ensure the hook call does not inadvertently become conditional.
26+
//!
27+
//! Analogous to TS `ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts`.
28+
29+
use react_compiler_hir::environment::Environment;
30+
use react_compiler_hir::{BlockId, HirFunction, InstructionValue, Terminal, Type};
31+
32+
/// Flattens reactive scopes that contain hook calls or `use()` calls.
33+
///
34+
/// Hooks and `use` must be called unconditionally, so any reactive scope containing
35+
/// such a call must be flattened to avoid making the call conditional.
36+
pub fn flatten_scopes_with_hooks_or_use_hir(func: &mut HirFunction, env: &Environment) {
37+
let mut active_scopes: Vec<ActiveScope> = Vec::new();
38+
let mut prune: Vec<BlockId> = Vec::new();
39+
40+
// Collect block ids to allow mutation during iteration
41+
let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
42+
43+
for block_id in &block_ids {
44+
// Remove scopes whose fallthrough matches this block
45+
active_scopes.retain(|scope| scope.fallthrough != *block_id);
46+
47+
let block = &func.body.blocks[block_id];
48+
49+
// Check instructions for hook or use calls
50+
for instr_id in &block.instructions {
51+
let instr = &func.instructions[instr_id.0 as usize];
52+
match &instr.value {
53+
InstructionValue::CallExpression { callee, .. } => {
54+
let callee_ty = &env.types
55+
[env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
56+
if is_hook_or_use(env, callee_ty) {
57+
// All active scopes must be pruned
58+
prune.extend(active_scopes.iter().map(|s| s.block));
59+
active_scopes.clear();
60+
}
61+
}
62+
InstructionValue::MethodCall { property, .. } => {
63+
let property_ty = &env.types
64+
[env.identifiers[property.identifier.0 as usize].type_.0 as usize];
65+
if is_hook_or_use(env, property_ty) {
66+
prune.extend(active_scopes.iter().map(|s| s.block));
67+
active_scopes.clear();
68+
}
69+
}
70+
_ => {}
71+
}
72+
}
73+
74+
// Track scope terminals
75+
if let Terminal::Scope {
76+
fallthrough, ..
77+
} = &block.terminal
78+
{
79+
active_scopes.push(ActiveScope {
80+
block: *block_id,
81+
fallthrough: *fallthrough,
82+
});
83+
}
84+
}
85+
86+
// Apply pruning: convert Scope terminals to Label or PrunedScope
87+
for id in prune {
88+
let block = &func.body.blocks[&id];
89+
let terminal = &block.terminal;
90+
91+
let (scope_block, fallthrough, eval_id, loc, scope) = match terminal {
92+
Terminal::Scope {
93+
block,
94+
fallthrough,
95+
id,
96+
loc,
97+
scope,
98+
} => (*block, *fallthrough, *id, *loc, *scope),
99+
_ => panic!(
100+
"Expected block bb{} to end in a scope terminal",
101+
id.0
102+
),
103+
};
104+
105+
// Check if the scope body is a single-instruction block that goes directly
106+
// to fallthrough — if so, use Label instead of PrunedScope
107+
let body = &func.body.blocks[&scope_block];
108+
let new_terminal = if body.instructions.len() == 1
109+
&& matches!(&body.terminal, Terminal::Goto { block, .. } if *block == fallthrough)
110+
{
111+
// This was a scope just for a hook call, which doesn't need memoization.
112+
// Flatten it away. We rely on PruneUnusedLabels to do the actual flattening.
113+
Terminal::Label {
114+
block: scope_block,
115+
fallthrough,
116+
id: eval_id,
117+
loc,
118+
}
119+
} else {
120+
Terminal::PrunedScope {
121+
block: scope_block,
122+
fallthrough,
123+
scope,
124+
id: eval_id,
125+
loc,
126+
}
127+
};
128+
129+
let block_mut = func.body.blocks.get_mut(&id).unwrap();
130+
block_mut.terminal = new_terminal;
131+
}
132+
}
133+
134+
struct ActiveScope {
135+
block: BlockId,
136+
fallthrough: BlockId,
137+
}
138+
139+
fn is_hook_or_use(env: &Environment, ty: &Type) -> bool {
140+
env.get_hook_kind_for_type(ty).is_some() || is_use_operator_type(ty)
141+
}
142+
143+
fn is_use_operator_type(ty: &Type) -> bool {
144+
matches!(
145+
ty,
146+
Type::Function { shape_id: Some(id), .. }
147+
if id == react_compiler_hir::object_shape::BUILT_IN_USE_OPERATOR_ID
148+
)
149+
}

compiler/crates/react_compiler_inference/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod align_object_method_scopes;
33
pub mod align_reactive_scopes_to_block_scopes_hir;
44
pub mod build_reactive_scope_terminals_hir;
55
pub mod flatten_reactive_loops_hir;
6+
pub mod flatten_scopes_with_hooks_or_use_hir;
67
pub mod analyse_functions;
78
pub mod infer_mutation_aliasing_effects;
89
pub mod infer_mutation_aliasing_ranges;
@@ -16,6 +17,7 @@ pub use align_object_method_scopes::align_object_method_scopes;
1617
pub use align_reactive_scopes_to_block_scopes_hir::align_reactive_scopes_to_block_scopes_hir;
1718
pub use build_reactive_scope_terminals_hir::build_reactive_scope_terminals_hir;
1819
pub use flatten_reactive_loops_hir::flatten_reactive_loops_hir;
20+
pub use flatten_scopes_with_hooks_or_use_hir::flatten_scopes_with_hooks_or_use_hir;
1921
pub use analyse_functions::analyse_functions;
2022
pub use infer_mutation_aliasing_effects::infer_mutation_aliasing_effects;
2123
pub use infer_mutation_aliasing_ranges::infer_mutation_aliasing_ranges;

0 commit comments

Comments
 (0)