use std::collections::{BTreeSet, HashMap, HashSet};
use indexmap::IndexMap;
use react_compiler_hir::environment::Environment;
use react_compiler_hir::{
BasicBlock, BlockId, DeclarationId, DependencyPathEntry, EvaluationOrder,
FunctionId, GotoVariant, HirFunction, IdentifierId, Instruction, InstructionId,
InstructionKind, InstructionValue, MutableRange, ParamPattern,
Place, PlaceOrSpread, PropertyLiteral, ReactFunctionType, ReactiveScopeDependency,
ScopeId, Terminal, Type, visitors,
};
use react_compiler_hir::visitors::{ScopeBlockTraversal, ScopeBlockInfo};
pub fn propagate_scope_dependencies_hir(func: &mut HirFunction, env: &mut Environment) {
let used_outside_declaring_scope = find_temporaries_used_outside_declaring_scope(func, env);
let temporaries = collect_temporaries_sidemap(func, env, &used_outside_declaring_scope);
let OptionalChainSidemap {
temporaries_read_in_optional,
processed_instrs_in_optional,
hoistable_objects,
} = collect_optional_chain_sidemap(func, env);
let hoistable_property_loads = {
let (working, registry) = collect_hoistable_and_propagate(func, env, &temporaries, &hoistable_objects);
let mut keyed: HashMap<ScopeId, Vec<ReactiveScopeDependency>> = HashMap::new();
for (_block_id, block) in &func.body.blocks {
if let Terminal::Scope { scope, block: inner_block, .. } = &block.terminal {
if let Some(node_indices) = working.get(inner_block) {
let deps: Vec<ReactiveScopeDependency> = node_indices
.iter()
.map(|&idx| registry.nodes[idx].full_path.clone())
.collect();
keyed.insert(*scope, deps);
}
}
}
keyed
};
let mut merged_temporaries = temporaries;
for (k, v) in temporaries_read_in_optional {
merged_temporaries.insert(k, v);
}
let scope_deps = collect_dependencies(
func,
env,
&used_outside_declaring_scope,
&merged_temporaries,
&processed_instrs_in_optional,
);
for (scope_id, deps) in &scope_deps {
if deps.is_empty() {
continue;
}
let hoistables = hoistable_property_loads.get(scope_id);
let hoistables = hoistables.expect(
"[PropagateScopeDependencies] Scope not found in tracked blocks",
);
let mut tree = ReactiveScopeDependencyTreeHIR::new(
hoistables.iter(),
env,
);
for dep in deps {
tree.add_dependency(dep.clone(), env);
}
let candidates = tree.derive_minimal_dependencies(env);
let scope = &mut env.scopes[scope_id.0 as usize];
for candidate_dep in candidates {
let already_exists = scope.dependencies.iter().any(|existing_dep| {
let existing_decl_id = env.identifiers[existing_dep.identifier.0 as usize].declaration_id;
let candidate_decl_id = env.identifiers[candidate_dep.identifier.0 as usize].declaration_id;
existing_decl_id == candidate_decl_id
&& are_equal_paths(&existing_dep.path, &candidate_dep.path)
});
if !already_exists {
scope.dependencies.push(candidate_dep);
}
}
}
}
fn are_equal_paths(a: &[DependencyPathEntry], b: &[DependencyPathEntry]) -> bool {
a.len() == b.len()
&& a.iter().zip(b.iter()).all(|(ai, bi)| {
ai.property == bi.property && ai.optional == bi.optional
})
}
fn find_temporaries_used_outside_declaring_scope(
func: &HirFunction,
env: &Environment,
) -> HashSet<DeclarationId> {
let mut declarations: HashMap<DeclarationId, ScopeId> = HashMap::new();
let mut pruned_scopes: HashSet<ScopeId> = HashSet::new();
let mut traversal = ScopeBlockTraversal::new();
let mut used_outside_declaring_scope: HashSet<DeclarationId> = HashSet::new();
let handle_place = |place_id: IdentifierId,
declarations: &HashMap<DeclarationId, ScopeId>,
traversal: &ScopeBlockTraversal,
pruned_scopes: &HashSet<ScopeId>,
used_outside: &mut HashSet<DeclarationId>,
env: &Environment| {
let decl_id = env.identifiers[place_id.0 as usize].declaration_id;
if let Some(&declaring_scope) = declarations.get(&decl_id) {
if !traversal.is_scope_active(declaring_scope) && !pruned_scopes.contains(&declaring_scope) {
used_outside.insert(decl_id);
}
}
};
for (block_id, block) in &func.body.blocks {
traversal.record_scopes(block);
let scope_start_info = traversal.block_infos.get(block_id);
if let Some(ScopeBlockInfo::Begin { scope, pruned: true, .. }) = scope_start_info {
pruned_scopes.insert(*scope);
}
for &instr_id in &block.instructions {
let instr = &func.instructions[instr_id.0 as usize];
for op_id in visitors::each_instruction_operand(instr, env).into_iter().map(|p| p.identifier).collect::<Vec<_>>() {
handle_place(
op_id,
&declarations,
&traversal,
&pruned_scopes,
&mut used_outside_declaring_scope,
env,
);
}
let current_scope = traversal.current_scope();
if let Some(scope) = current_scope {
if !pruned_scopes.contains(&scope) {
match &instr.value {
InstructionValue::LoadLocal { .. }
| InstructionValue::LoadContext { .. }
| InstructionValue::PropertyLoad { .. } => {
let decl_id = env.identifiers[instr.lvalue.identifier.0 as usize].declaration_id;
declarations.insert(decl_id, scope);
}
_ => {}
}
}
}
}
for op_id in visitors::each_terminal_operand(&block.terminal).into_iter().map(|p| p.identifier).collect::<Vec<_>>() {
handle_place(
op_id,
&declarations,
&traversal,
&pruned_scopes,
&mut used_outside_declaring_scope,
env,
);
}
}
used_outside_declaring_scope
}
fn collect_temporaries_sidemap(
func: &HirFunction,
env: &Environment,
used_outside_declaring_scope: &HashSet<DeclarationId>,
) -> HashMap<IdentifierId, ReactiveScopeDependency> {
let mut temporaries = HashMap::new();
collect_temporaries_sidemap_impl(
func,
env,
used_outside_declaring_scope,
&mut temporaries,
None,
);
temporaries
}
fn is_load_context_mutable(
value: &InstructionValue,
id: EvaluationOrder,
env: &Environment,
) -> bool {
if let InstructionValue::LoadContext { place, .. } = value {
if let Some(scope_id) = env.identifiers[place.identifier.0 as usize].scope {
let scope_range = &env.scopes[scope_id.0 as usize].range;
return id >= scope_range.end;
}
}
false
}
fn convert_hoisted_lvalue_kind(kind: InstructionKind) -> Option<InstructionKind> {
match kind {
InstructionKind::HoistedLet => Some(InstructionKind::Let),
InstructionKind::HoistedConst => Some(InstructionKind::Const),
InstructionKind::HoistedFunction => Some(InstructionKind::Function),
_ => None,
}
}
fn collect_temporaries_sidemap_impl(
func: &HirFunction,
env: &Environment,
used_outside_declaring_scope: &HashSet<DeclarationId>,
temporaries: &mut HashMap<IdentifierId, ReactiveScopeDependency>,
inner_fn_context: Option<EvaluationOrder>,
) {
for (_block_id, block) in &func.body.blocks {
for &instr_id in &block.instructions {
let instr = &func.instructions[instr_id.0 as usize];
let instr_eval_order = if let Some(outer_id) = inner_fn_context {
outer_id
} else {
instr.id
};
let lvalue_decl_id = env.identifiers[instr.lvalue.identifier.0 as usize].declaration_id;
let used_outside = used_outside_declaring_scope.contains(&lvalue_decl_id);
match &instr.value {
InstructionValue::PropertyLoad {
object, property, loc, ..
} if !used_outside => {
if inner_fn_context.is_none()
|| temporaries.contains_key(&object.identifier)
{
let prop = get_property(object, property, false, *loc, temporaries, env);
temporaries.insert(instr.lvalue.identifier, prop);
}
}
InstructionValue::LoadLocal { place, loc, .. }
if env.identifiers[instr.lvalue.identifier.0 as usize].name.is_none()
&& env.identifiers[place.identifier.0 as usize].name.is_some()
&& !used_outside =>
{
if inner_fn_context.is_none()
|| func
.context
.iter()
.any(|ctx| ctx.identifier == place.identifier)
{
temporaries.insert(
instr.lvalue.identifier,
ReactiveScopeDependency {
identifier: place.identifier,
reactive: place.reactive,
path: vec![],
loc: *loc,
},
);
}
}
value @ InstructionValue::LoadContext { place, loc, .. }
if is_load_context_mutable(value, instr_eval_order, env)
&& env.identifiers[instr.lvalue.identifier.0 as usize].name.is_none()
&& env.identifiers[place.identifier.0 as usize].name.is_some()
&& !used_outside =>
{
if inner_fn_context.is_none()
|| func
.context
.iter()
.any(|ctx| ctx.identifier == place.identifier)
{
temporaries.insert(
instr.lvalue.identifier,
ReactiveScopeDependency {
identifier: place.identifier,
reactive: place.reactive,
path: vec![],
loc: *loc,
},
);
}
}
InstructionValue::FunctionExpression { lowered_func, .. }
| InstructionValue::ObjectMethod { lowered_func, .. } => {
let inner_func = &env.functions[lowered_func.func.0 as usize];
let ctx = inner_fn_context.unwrap_or(instr.id);
collect_temporaries_sidemap_impl(
inner_func,
env,
used_outside_declaring_scope,
temporaries,
Some(ctx),
);
}
_ => {}
}
}
}
}
fn get_property(
object: &Place,
property_name: &PropertyLiteral,
optional: bool,
loc: Option<react_compiler_hir::SourceLocation>,
temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
_env: &Environment,
) -> ReactiveScopeDependency {
let resolved = temporaries.get(&object.identifier);
if let Some(resolved) = resolved {
let mut path = resolved.path.clone();
path.push(DependencyPathEntry {
property: property_name.clone(),
optional,
loc,
});
ReactiveScopeDependency {
identifier: resolved.identifier,
reactive: resolved.reactive,
path,
loc,
}
} else {
ReactiveScopeDependency {
identifier: object.identifier,
reactive: object.reactive,
path: vec![DependencyPathEntry {
property: property_name.clone(),
optional,
loc,
}],
loc,
}
}
}
struct OptionalChainSidemap {
temporaries_read_in_optional: HashMap<IdentifierId, ReactiveScopeDependency>,
processed_instrs_in_optional: HashSet<ProcessedInstr>,
hoistable_objects: HashMap<BlockId, ReactiveScopeDependency>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum ProcessedInstr {
Instruction(IdentifierId),
Terminal(BlockId),
}
fn collect_optional_chain_sidemap(
func: &HirFunction,
env: &Environment,
) -> OptionalChainSidemap {
let mut ctx = OptionalTraversalContext {
seen_optionals: HashSet::new(),
processed_instrs_in_optional: HashSet::new(),
temporaries_read_in_optional: HashMap::new(),
hoistable_objects: HashMap::new(),
};
traverse_function_optional(func, env, &mut ctx);
OptionalChainSidemap {
temporaries_read_in_optional: ctx.temporaries_read_in_optional,
processed_instrs_in_optional: ctx.processed_instrs_in_optional,
hoistable_objects: ctx.hoistable_objects,
}
}
struct OptionalTraversalContext {
seen_optionals: HashSet<BlockId>,
processed_instrs_in_optional: HashSet<ProcessedInstr>,
temporaries_read_in_optional: HashMap<IdentifierId, ReactiveScopeDependency>,
hoistable_objects: HashMap<BlockId, ReactiveScopeDependency>,
}
fn traverse_function_optional(
func: &HirFunction,
env: &Environment,
ctx: &mut OptionalTraversalContext,
) {
for (_block_id, block) in &func.body.blocks {
for &instr_id in &block.instructions {
let instr = &func.instructions[instr_id.0 as usize];
match &instr.value {
InstructionValue::FunctionExpression { lowered_func, .. }
| InstructionValue::ObjectMethod { lowered_func, .. } => {
let inner_func = &env.functions[lowered_func.func.0 as usize];
traverse_function_optional(inner_func, env, ctx);
}
_ => {}
}
}
if let Terminal::Optional { .. } = &block.terminal {
if !ctx.seen_optionals.contains(&block.id) {
traverse_optional_block(block, func, env, ctx, None);
}
}
}
}
struct MatchConsequentResult {
consequent_id: IdentifierId,
property: PropertyLiteral,
property_id: IdentifierId,
store_local_lvalue_id: IdentifierId,
consequent_goto: BlockId,
property_load_loc: Option<react_compiler_hir::SourceLocation>,
}
fn match_optional_test_block(
test: &Terminal,
func: &HirFunction,
_env: &Environment,
) -> Option<MatchConsequentResult> {
let (test_place, consequent_block_id, alternate_block_id) = match test {
Terminal::Branch {
test,
consequent,
alternate,
..
} => (test, *consequent, *alternate),
_ => return None,
};
let consequent_block = func.body.blocks.get(&consequent_block_id)?;
if consequent_block.instructions.len() != 2 {
return None;
}
let instr0 = &func.instructions[consequent_block.instructions[0].0 as usize];
let instr1 = &func.instructions[consequent_block.instructions[1].0 as usize];
let (property_load_object, property, property_load_loc) = match &instr0.value {
InstructionValue::PropertyLoad {
object,
property,
loc,
} => (object, property, loc),
_ => return None,
};
let store_local_value = match &instr1.value {
InstructionValue::StoreLocal { value, lvalue, .. } => {
if value.identifier != instr0.lvalue.identifier {
return None;
}
&lvalue.place
}
_ => return None,
};
if property_load_object.identifier != test_place.identifier {
return None;
}
match &consequent_block.terminal {
Terminal::Goto {
variant: GotoVariant::Break,
block: goto_block,
..
} => {
let alternate_block = func.body.blocks.get(&alternate_block_id)?;
if alternate_block.instructions.len() != 2 {
return None;
}
let alt_instr0 = &func.instructions[alternate_block.instructions[0].0 as usize];
let alt_instr1 = &func.instructions[alternate_block.instructions[1].0 as usize];
match (&alt_instr0.value, &alt_instr1.value) {
(InstructionValue::Primitive { .. }, InstructionValue::StoreLocal { .. }) => {}
_ => return None,
}
Some(MatchConsequentResult {
consequent_id: store_local_value.identifier,
property: property.clone(),
property_id: instr0.lvalue.identifier,
store_local_lvalue_id: instr1.lvalue.identifier,
consequent_goto: *goto_block,
property_load_loc: *property_load_loc,
})
}
_ => None,
}
}
fn traverse_optional_block(
optional_block: &BasicBlock,
func: &HirFunction,
env: &Environment,
ctx: &mut OptionalTraversalContext,
outer_alternate: Option<BlockId>,
) -> Option<IdentifierId> {
ctx.seen_optionals.insert(optional_block.id);
let (test_block_id, is_optional, fallthrough_block_id) = match &optional_block.terminal {
Terminal::Optional {
test,
optional,
fallthrough,
..
} => (*test, *optional, *fallthrough),
_ => return None,
};
let maybe_test_block = func.body.blocks.get(&test_block_id)?;
let (test_terminal, base_object) = match &maybe_test_block.terminal {
Terminal::Branch { .. } => {
if !is_optional {
return None;
}
if maybe_test_block.instructions.is_empty() {
return None;
}
let first_instr = &func.instructions[maybe_test_block.instructions[0].0 as usize];
if !matches!(&first_instr.value, InstructionValue::LoadLocal { .. }) {
return None;
}
let mut path: Vec<DependencyPathEntry> = Vec::new();
for i in 1..maybe_test_block.instructions.len() {
let curr_instr = &func.instructions[maybe_test_block.instructions[i].0 as usize];
let prev_instr =
&func.instructions[maybe_test_block.instructions[i - 1].0 as usize];
match &curr_instr.value {
InstructionValue::PropertyLoad {
object, property, loc, ..
} if object.identifier == prev_instr.lvalue.identifier => {
path.push(DependencyPathEntry {
property: property.clone(),
optional: false,
loc: *loc,
});
}
_ => return None,
}
}
let last_instr_id = *maybe_test_block.instructions.last().unwrap();
let last_instr = &func.instructions[last_instr_id.0 as usize];
let test_ident = match &maybe_test_block.terminal {
Terminal::Branch { test, .. } => test.identifier,
_ => return None,
};
if test_ident != last_instr.lvalue.identifier {
return None;
}
let first_place = match &first_instr.value {
InstructionValue::LoadLocal { place, .. } => place,
_ => return None,
};
let base = ReactiveScopeDependency {
identifier: first_place.identifier,
reactive: first_place.reactive,
path,
loc: first_place.loc,
};
(&maybe_test_block.terminal, base)
}
Terminal::Optional {
fallthrough: inner_fallthrough,
optional: _inner_optional,
..
} => {
let test_block = func.body.blocks.get(inner_fallthrough)?;
if !matches!(&test_block.terminal, Terminal::Branch { .. }) {
return None;
}
let inner_alternate = match &test_block.terminal {
Terminal::Branch { alternate, .. } => Some(*alternate),
_ => None,
};
let inner_optional_result =
traverse_optional_block(maybe_test_block, func, env, ctx, inner_alternate);
let inner_optional_id = inner_optional_result?;
let test_ident = match &test_block.terminal {
Terminal::Branch { test, .. } => test.identifier,
_ => return None,
};
if test_ident != inner_optional_id {
return None;
}
if !is_optional {
if let Some(inner_dep) = ctx.temporaries_read_in_optional.get(&inner_optional_id) {
ctx.hoistable_objects
.insert(optional_block.id, inner_dep.clone());
}
}
let base = ctx
.temporaries_read_in_optional
.get(&inner_optional_id)?
.clone();
(&test_block.terminal, base)
}
_ => return None,
};
if let Some(outer_alt) = outer_alternate {
let test_alternate = match test_terminal {
Terminal::Branch { alternate, .. } => *alternate,
_ => return None,
};
if test_alternate == outer_alt {
if !optional_block.instructions.is_empty() {
return None;
}
}
}
let match_result = match_optional_test_block(test_terminal, func, env)?;
if match_result.consequent_goto != fallthrough_block_id {
return None;
}
let load = ReactiveScopeDependency {
identifier: base_object.identifier,
reactive: base_object.reactive,
path: {
let mut p = base_object.path.clone();
p.push(DependencyPathEntry {
property: match_result.property.clone(),
optional: is_optional,
loc: match_result.property_load_loc,
});
p
},
loc: match_result.property_load_loc,
};
ctx.processed_instrs_in_optional
.insert(ProcessedInstr::Instruction(match_result.store_local_lvalue_id));
ctx.processed_instrs_in_optional
.insert(ProcessedInstr::Terminal(match &test_terminal {
Terminal::Branch { .. } => {
let mut found_block = BlockId(0);
for (bid, blk) in &func.body.blocks {
if std::ptr::eq(&blk.terminal, test_terminal) {
found_block = *bid;
break;
}
}
found_block
}
_ => BlockId(0),
}));
ctx.temporaries_read_in_optional
.insert(match_result.consequent_id, load.clone());
ctx.temporaries_read_in_optional
.insert(match_result.property_id, load);
Some(match_result.consequent_id)
}
#[derive(Debug, Clone)]
struct PropertyPathNode {
properties: HashMap<PropertyLiteral, usize>, optional_properties: HashMap<PropertyLiteral, usize>, #[allow(dead_code)]
parent: Option<usize>,
full_path: ReactiveScopeDependency,
has_optional: bool,
#[allow(dead_code)]
root: Option<IdentifierId>,
}
struct PropertyPathRegistry {
nodes: Vec<PropertyPathNode>,
roots: HashMap<IdentifierId, usize>,
}
impl PropertyPathRegistry {
fn new() -> Self {
Self {
nodes: Vec::new(),
roots: HashMap::new(),
}
}
fn get_or_create_identifier(
&mut self,
identifier_id: IdentifierId,
reactive: bool,
loc: Option<react_compiler_hir::SourceLocation>,
) -> usize {
if let Some(&idx) = self.roots.get(&identifier_id) {
return idx;
}
let idx = self.nodes.len();
self.nodes.push(PropertyPathNode {
properties: HashMap::new(),
optional_properties: HashMap::new(),
parent: None,
full_path: ReactiveScopeDependency {
identifier: identifier_id,
reactive,
path: vec![],
loc,
},
has_optional: false,
root: Some(identifier_id),
});
self.roots.insert(identifier_id, idx);
idx
}
fn get_or_create_property_entry(
&mut self,
parent_idx: usize,
entry: &DependencyPathEntry,
) -> usize {
let map_key = entry.property.clone();
let existing = if entry.optional {
self.nodes[parent_idx].optional_properties.get(&map_key).copied()
} else {
self.nodes[parent_idx].properties.get(&map_key).copied()
};
if let Some(idx) = existing {
return idx;
}
let parent_full_path = self.nodes[parent_idx].full_path.clone();
let parent_has_optional = self.nodes[parent_idx].has_optional;
let idx = self.nodes.len();
let mut new_path = parent_full_path.path.clone();
new_path.push(entry.clone());
self.nodes.push(PropertyPathNode {
properties: HashMap::new(),
optional_properties: HashMap::new(),
parent: Some(parent_idx),
full_path: ReactiveScopeDependency {
identifier: parent_full_path.identifier,
reactive: parent_full_path.reactive,
path: new_path,
loc: entry.loc,
},
has_optional: parent_has_optional || entry.optional,
root: None,
});
if entry.optional {
self.nodes[parent_idx]
.optional_properties
.insert(map_key, idx);
} else {
self.nodes[parent_idx].properties.insert(map_key, idx);
}
idx
}
fn get_or_create_property(&mut self, dep: &ReactiveScopeDependency) -> usize {
let mut curr = self.get_or_create_identifier(dep.identifier, dep.reactive, dep.loc);
for entry in &dep.path {
curr = self.get_or_create_property_entry(curr, entry);
}
curr
}
}
fn reduce_maybe_optional_chains(
nodes: &mut BTreeSet<usize>,
registry: &mut PropertyPathRegistry,
) {
let mut optional_chain_nodes: BTreeSet<usize> = nodes
.iter()
.copied()
.filter(|&idx| registry.nodes[idx].has_optional)
.collect();
if optional_chain_nodes.is_empty() {
return;
}
loop {
let mut changed = false;
let to_process: Vec<usize> = optional_chain_nodes.iter().copied().collect();
for original_idx in to_process {
let full_path = registry.nodes[original_idx].full_path.clone();
let mut curr_node = registry.get_or_create_identifier(
full_path.identifier,
full_path.reactive,
full_path.loc,
);
for entry in &full_path.path {
let next_entry = if entry.optional && nodes.contains(&curr_node) {
DependencyPathEntry {
property: entry.property.clone(),
optional: false,
loc: entry.loc,
}
} else {
entry.clone()
};
curr_node = registry.get_or_create_property_entry(curr_node, &next_entry);
}
if curr_node != original_idx {
changed = true;
optional_chain_nodes.remove(&original_idx);
optional_chain_nodes.insert(curr_node);
nodes.remove(&original_idx);
nodes.insert(curr_node);
}
}
if !changed {
break;
}
}
}
#[derive(Debug, Clone)]
struct BlockInfo {
assumed_non_null_objects: BTreeSet<usize>, }
#[allow(dead_code)]
fn collect_hoistable_property_loads(
func: &HirFunction,
env: &Environment,
temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
hoistable_from_optionals: &HashMap<BlockId, ReactiveScopeDependency>,
) -> HashMap<BlockId, BlockInfo> {
let mut registry = PropertyPathRegistry::new();
let known_immutable_identifiers: HashSet<IdentifierId> = if func.fn_type == ReactFunctionType::Component
|| func.fn_type == ReactFunctionType::Hook
{
func.params
.iter()
.filter_map(|p| match p {
ParamPattern::Place(place) => Some(place.identifier),
_ => None,
})
.collect()
} else {
HashSet::new()
};
let assumed_invoked_fns = get_assumed_invoked_functions(func, env);
let ctx = CollectHoistableContext {
temporaries,
known_immutable_identifiers: &known_immutable_identifiers,
hoistable_from_optionals,
nested_fn_immutable_context: None,
assumed_invoked_fns: &assumed_invoked_fns,
};
collect_hoistable_property_loads_impl(func, env, &ctx, &mut registry)
}
struct CollectHoistableContext<'a> {
temporaries: &'a HashMap<IdentifierId, ReactiveScopeDependency>,
known_immutable_identifiers: &'a HashSet<IdentifierId>,
hoistable_from_optionals: &'a HashMap<BlockId, ReactiveScopeDependency>,
nested_fn_immutable_context: Option<&'a HashSet<IdentifierId>>,
assumed_invoked_fns: &'a HashSet<FunctionId>,
}
fn is_immutable_at_instr(
identifier_id: IdentifierId,
instr_id: EvaluationOrder,
env: &Environment,
ctx: &CollectHoistableContext,
) -> bool {
if let Some(nested_ctx) = ctx.nested_fn_immutable_context {
return nested_ctx.contains(&identifier_id);
}
let ident = &env.identifiers[identifier_id.0 as usize];
let mutable_at_instr = ident.mutable_range.end > EvaluationOrder(ident.mutable_range.start.0 + 1)
&& ident.scope.is_some()
&& {
let scope = &env.scopes[ident.scope.unwrap().0 as usize];
in_range(instr_id, &scope.range)
};
!mutable_at_instr || ctx.known_immutable_identifiers.contains(&identifier_id)
}
fn in_range(id: EvaluationOrder, range: &MutableRange) -> bool {
id >= range.start && id < range.end
}
fn get_maybe_non_null_in_instruction(
value: &InstructionValue,
temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
) -> Option<ReactiveScopeDependency> {
match value {
InstructionValue::PropertyLoad { object, .. } => {
Some(
temporaries
.get(&object.identifier)
.cloned()
.unwrap_or_else(|| ReactiveScopeDependency {
identifier: object.identifier,
reactive: object.reactive,
path: vec![],
loc: object.loc,
}),
)
}
InstructionValue::Destructure { value: val, .. } => {
temporaries.get(&val.identifier).cloned()
}
InstructionValue::ComputedLoad { object, .. } => {
temporaries.get(&object.identifier).cloned()
}
_ => None,
}
}
#[allow(dead_code)]
fn collect_hoistable_property_loads_impl(
func: &HirFunction,
env: &Environment,
ctx: &CollectHoistableContext,
registry: &mut PropertyPathRegistry,
) -> HashMap<BlockId, BlockInfo> {
let nodes = collect_non_nulls_in_blocks(func, env, ctx, registry);
let working = propagate_non_null(func, &nodes, registry);
working
.into_iter()
.map(|(k, v)| (k, BlockInfo { assumed_non_null_objects: v }))
.collect()
}
fn get_assumed_invoked_functions(
func: &HirFunction,
env: &Environment,
) -> HashSet<FunctionId> {
let mut temporaries: HashMap<IdentifierId, (FunctionId, HashSet<FunctionId>)> = HashMap::new();
get_assumed_invoked_functions_impl(func, env, &mut temporaries)
}
fn get_assumed_invoked_functions_impl(
func: &HirFunction,
env: &Environment,
temporaries: &mut HashMap<IdentifierId, (FunctionId, HashSet<FunctionId>)>,
) -> HashSet<FunctionId> {
let mut hoistable: HashSet<FunctionId> = HashSet::new();
for (_block_id, block) in &func.body.blocks {
for &instr_id in &block.instructions {
let instr = &func.instructions[instr_id.0 as usize];
match &instr.value {
InstructionValue::FunctionExpression { lowered_func, .. } => {
temporaries.insert(
instr.lvalue.identifier,
(lowered_func.func, HashSet::new()),
);
}
InstructionValue::StoreLocal { value: val, lvalue, .. } => {
if let Some(entry) = temporaries.get(&val.identifier).cloned() {
temporaries.insert(lvalue.place.identifier, entry);
}
}
InstructionValue::LoadLocal { place, .. } => {
if let Some(entry) = temporaries.get(&place.identifier).cloned() {
temporaries.insert(instr.lvalue.identifier, entry);
}
}
_ => {}
}
}
}
for (_block_id, block) in &func.body.blocks {
for &instr_id in &block.instructions {
let instr = &func.instructions[instr_id.0 as usize];
match &instr.value {
InstructionValue::CallExpression { callee, args, .. } => {
let callee_ty = &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
let maybe_hook = env.get_hook_kind_for_type(callee_ty).ok().flatten();
if let Some(entry) = temporaries.get(&callee.identifier) {
hoistable.insert(entry.0);
} else if maybe_hook.is_some() {
for arg in args {
if let PlaceOrSpread::Place(p) = arg {
if let Some(entry) = temporaries.get(&p.identifier) {
hoistable.insert(entry.0);
}
}
}
}
}
InstructionValue::JsxExpression { props, children, .. } => {
for prop in props {
if let react_compiler_hir::JsxAttribute::Attribute { place, .. } = prop {
if let Some(entry) = temporaries.get(&place.identifier) {
hoistable.insert(entry.0);
}
}
}
if let Some(children) = children {
for child in children {
if let Some(entry) = temporaries.get(&child.identifier) {
hoistable.insert(entry.0);
}
}
}
}
InstructionValue::JsxFragment { children, .. } => {
for child in children {
if let Some(entry) = temporaries.get(&child.identifier) {
hoistable.insert(entry.0);
}
}
}
InstructionValue::FunctionExpression { lowered_func, .. } => {
let inner_func = &env.functions[lowered_func.func.0 as usize];
let lambdas_called = get_assumed_invoked_functions_impl(inner_func, env, temporaries);
if let Some(entry) = temporaries.get_mut(&instr.lvalue.identifier) {
for called in lambdas_called {
entry.1.insert(called);
}
}
}
_ => {}
}
}
if let Terminal::Return { value, .. } = &block.terminal {
if let Some(entry) = temporaries.get(&value.identifier) {
hoistable.insert(entry.0);
}
}
}
let mut changed = true;
while changed {
changed = false;
let mut to_add = Vec::new();
for (_, (func_id, may_invoke)) in temporaries.iter() {
if hoistable.contains(func_id) {
for &called in may_invoke {
if !hoistable.contains(&called) {
to_add.push(called);
}
}
}
}
for id in to_add {
changed = true;
hoistable.insert(id);
}
if !changed { break; }
}
hoistable
}
fn collect_non_nulls_in_blocks(
func: &HirFunction,
env: &Environment,
ctx: &CollectHoistableContext,
registry: &mut PropertyPathRegistry,
) -> HashMap<BlockId, BlockInfo> {
let mut known_non_null: BTreeSet<usize> = BTreeSet::new();
if func.fn_type == ReactFunctionType::Component
&& !func.params.is_empty()
{
if let ParamPattern::Place(place) = &func.params[0] {
let node_idx = registry.get_or_create_identifier(
place.identifier,
true,
place.loc,
);
known_non_null.insert(node_idx);
}
}
let mut nodes: HashMap<BlockId, BlockInfo> = HashMap::new();
for (block_id, block) in &func.body.blocks {
let mut assumed = known_non_null.clone();
if let Some(optional_chain) = ctx.hoistable_from_optionals.get(block_id) {
let node_idx = registry.get_or_create_property(optional_chain);
assumed.insert(node_idx);
}
for &instr_id in &block.instructions {
let instr = &func.instructions[instr_id.0 as usize];
if let Some(path) = get_maybe_non_null_in_instruction(&instr.value, ctx.temporaries) {
let path_ident = path.identifier;
if is_immutable_at_instr(path_ident, instr.id, env, ctx) {
let node_idx = registry.get_or_create_property(&path);
assumed.insert(node_idx);
}
}
if env.enable_preserve_existing_memoization_guarantees {
if let InstructionValue::StartMemoize { deps: Some(deps), .. } = &instr.value {
for dep in deps {
if let react_compiler_hir::ManualMemoDependencyRoot::NamedLocal { value: val, .. } = &dep.root {
if !is_immutable_at_instr(val.identifier, instr.id, env, ctx) {
continue;
}
for i in 0..dep.path.len() {
if dep.path[i].optional {
break;
}
let sub_dep = ReactiveScopeDependency {
identifier: val.identifier,
reactive: val.reactive,
path: dep.path[..i].to_vec(),
loc: dep.loc,
};
let node_idx = registry.get_or_create_property(&sub_dep);
assumed.insert(node_idx);
}
}
}
}
}
if let InstructionValue::FunctionExpression { lowered_func, .. } = &instr.value {
if ctx.assumed_invoked_fns.contains(&lowered_func.func) {
let inner_func = &env.functions[lowered_func.func.0 as usize];
let nested_fn_immutable_context: HashSet<IdentifierId> = if ctx.nested_fn_immutable_context.is_some() {
ctx.nested_fn_immutable_context.unwrap().clone()
} else {
inner_func
.context
.iter()
.filter(|place| is_immutable_at_instr(place.identifier, instr.id, env, ctx))
.map(|place| place.identifier)
.collect()
};
let inner_assumed = get_assumed_invoked_functions(inner_func, env);
let inner_ctx = CollectHoistableContext {
temporaries: ctx.temporaries,
known_immutable_identifiers: &HashSet::new(),
hoistable_from_optionals: ctx.hoistable_from_optionals,
nested_fn_immutable_context: Some(&nested_fn_immutable_context),
assumed_invoked_fns: &inner_assumed,
};
let inner_nodes = collect_non_nulls_in_blocks(inner_func, env, &inner_ctx, registry);
let inner_working = propagate_non_null(inner_func, &inner_nodes, registry);
let inner_entry = inner_func.body.entry;
if let Some(inner_set) = inner_working.get(&inner_entry) {
for &node_idx in inner_set {
assumed.insert(node_idx);
}
}
}
}
}
nodes.insert(
*block_id,
BlockInfo {
assumed_non_null_objects: assumed,
},
);
}
nodes
}
fn propagate_non_null(
func: &HirFunction,
nodes: &HashMap<BlockId, BlockInfo>,
registry: &mut PropertyPathRegistry,
) -> HashMap<BlockId, BTreeSet<usize>> {
let mut block_successors: HashMap<BlockId, BTreeSet<BlockId>> = HashMap::new();
for (block_id, block) in &func.body.blocks {
for pred in &block.preds {
block_successors
.entry(*pred)
.or_default()
.insert(*block_id);
}
}
let mut working: HashMap<BlockId, BTreeSet<usize>> = nodes
.iter()
.map(|(k, v)| (*k, v.assumed_non_null_objects.clone()))
.collect();
let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
let mut reversed_block_ids = block_ids.clone();
reversed_block_ids.reverse();
for _ in 0..100 {
let mut changed = false;
let mut traversal_state: HashMap<BlockId, TraversalState> = HashMap::new();
for &block_id in &block_ids {
let block_changed = recursively_propagate_non_null(
block_id,
PropagationDirection::Forward,
&mut traversal_state,
&mut working,
func,
&block_successors,
registry,
);
changed |= block_changed;
}
traversal_state.clear();
for &block_id in &reversed_block_ids {
let block_changed = recursively_propagate_non_null(
block_id,
PropagationDirection::Backward,
&mut traversal_state,
&mut working,
func,
&block_successors,
registry,
);
changed |= block_changed;
}
if !changed {
break;
}
}
working
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TraversalState {
Active,
Done,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PropagationDirection {
Forward,
Backward,
}
fn recursively_propagate_non_null(
node_id: BlockId,
direction: PropagationDirection,
traversal_state: &mut HashMap<BlockId, TraversalState>,
working: &mut HashMap<BlockId, BTreeSet<usize>>,
func: &HirFunction,
block_successors: &HashMap<BlockId, BTreeSet<BlockId>>,
registry: &mut PropertyPathRegistry,
) -> bool {
if traversal_state.contains_key(&node_id) {
return false;
}
traversal_state.insert(node_id, TraversalState::Active);
let neighbors: Vec<BlockId> = match direction {
PropagationDirection::Backward => {
block_successors
.get(&node_id)
.map(|s| s.iter().copied().collect())
.unwrap_or_default()
}
PropagationDirection::Forward => {
func.body
.blocks
.get(&node_id)
.map(|b| b.preds.iter().copied().collect())
.unwrap_or_default()
}
};
let mut changed = false;
for &neighbor in &neighbors {
if !traversal_state.contains_key(&neighbor) {
let neighbor_changed = recursively_propagate_non_null(
neighbor,
direction,
traversal_state,
working,
func,
block_successors,
registry,
);
changed |= neighbor_changed;
}
}
let done_neighbor_sets: Vec<BTreeSet<usize>> = neighbors
.iter()
.filter(|n| traversal_state.get(n) == Some(&TraversalState::Done))
.filter_map(|n| working.get(n).cloned())
.collect();
let neighbor_intersection = if done_neighbor_sets.is_empty() {
BTreeSet::new()
} else {
let mut iter = done_neighbor_sets.into_iter();
let first = iter.next().unwrap();
iter.fold(first, |acc, s| acc.intersection(&s).copied().collect())
};
let prev_objects = working.get(&node_id).cloned().unwrap_or_default();
let mut merged: BTreeSet<usize> = prev_objects.union(&neighbor_intersection).copied().collect();
reduce_maybe_optional_chains(&mut merged, registry);
working.insert(node_id, merged.clone());
traversal_state.insert(node_id, TraversalState::Done);
changed |= prev_objects != merged;
changed
}
fn collect_hoistable_and_propagate(
func: &HirFunction,
env: &Environment,
temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
hoistable_from_optionals: &HashMap<BlockId, ReactiveScopeDependency>,
) -> (HashMap<BlockId, BTreeSet<usize>>, PropertyPathRegistry) {
let mut registry = PropertyPathRegistry::new();
let assumed_invoked_fns = get_assumed_invoked_functions(func, env);
let known_immutable_identifiers: HashSet<IdentifierId> = if func.fn_type == ReactFunctionType::Component
|| func.fn_type == ReactFunctionType::Hook
{
func.params
.iter()
.filter_map(|p| match p {
ParamPattern::Place(place) => Some(place.identifier),
_ => None,
})
.collect()
} else {
HashSet::new()
};
let ctx = CollectHoistableContext {
temporaries,
known_immutable_identifiers: &known_immutable_identifiers,
hoistable_from_optionals,
nested_fn_immutable_context: None,
assumed_invoked_fns: &assumed_invoked_fns,
};
let nodes = collect_non_nulls_in_blocks(func, env, &ctx, &mut registry);
let working = propagate_non_null(func, &nodes, &mut registry);
(working, registry)
}
#[allow(dead_code)]
fn key_by_scope_id(
func: &HirFunction,
block_keyed: &HashMap<BlockId, BlockInfo>,
) -> HashMap<ScopeId, BlockInfo> {
let mut keyed: HashMap<ScopeId, BlockInfo> = HashMap::new();
for (_block_id, block) in &func.body.blocks {
if let Terminal::Scope {
scope, block: inner_block, ..
} = &block.terminal
{
if let Some(info) = block_keyed.get(inner_block) {
keyed.insert(*scope, info.clone());
}
}
}
keyed
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PropertyAccessType {
OptionalAccess,
UnconditionalAccess,
OptionalDependency,
UnconditionalDependency,
}
fn is_optional_access(access: PropertyAccessType) -> bool {
matches!(
access,
PropertyAccessType::OptionalAccess | PropertyAccessType::OptionalDependency
)
}
fn is_dependency_access(access: PropertyAccessType) -> bool {
matches!(
access,
PropertyAccessType::OptionalDependency | PropertyAccessType::UnconditionalDependency
)
}
fn merge_access(a: PropertyAccessType, b: PropertyAccessType) -> PropertyAccessType {
let is_unconditional = !(is_optional_access(a) && is_optional_access(b));
let is_dep = is_dependency_access(a) || is_dependency_access(b);
match (is_unconditional, is_dep) {
(true, true) => PropertyAccessType::UnconditionalDependency,
(true, false) => PropertyAccessType::UnconditionalAccess,
(false, true) => PropertyAccessType::OptionalDependency,
(false, false) => PropertyAccessType::OptionalAccess,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HoistableAccessType {
Optional,
NonNull,
}
struct HoistableNode {
properties: HashMap<PropertyLiteral, Box<HoistableNodeEntry>>,
access_type: HoistableAccessType,
}
struct HoistableNodeEntry {
node: HoistableNode,
}
struct DependencyNode {
properties: IndexMap<PropertyLiteral, Box<DependencyNodeEntry>>,
access_type: PropertyAccessType,
loc: Option<react_compiler_hir::SourceLocation>,
}
struct DependencyNodeEntry {
node: DependencyNode,
}
struct ReactiveScopeDependencyTreeHIR {
hoistable_roots: HashMap<IdentifierId, (HoistableNode, bool)>, dep_roots: IndexMap<IdentifierId, (DependencyNode, bool)>, }
impl ReactiveScopeDependencyTreeHIR {
fn new<'a>(
hoistable_objects: impl Iterator<Item = &'a ReactiveScopeDependency>,
_env: &Environment,
) -> Self {
let mut hoistable_roots: HashMap<IdentifierId, (HoistableNode, bool)> = HashMap::new();
let mut sorted_deps: Vec<&ReactiveScopeDependency> = hoistable_objects.collect();
sorted_deps.sort_by(|a, b| {
let a_optional = !a.path.is_empty() && a.path[0].optional;
let b_optional = !b.path.is_empty() && b.path[0].optional;
b_optional.cmp(&a_optional)
});
for dep in sorted_deps {
let root = hoistable_roots
.entry(dep.identifier)
.or_insert_with(|| {
let access_type = if !dep.path.is_empty() && dep.path[0].optional {
HoistableAccessType::Optional
} else {
HoistableAccessType::NonNull
};
(
HoistableNode {
properties: HashMap::new(),
access_type,
},
dep.reactive,
)
});
let mut curr = &mut root.0;
for i in 0..dep.path.len() {
let access_type = if i + 1 < dep.path.len() && dep.path[i + 1].optional {
HoistableAccessType::Optional
} else {
HoistableAccessType::NonNull
};
let entry = curr
.properties
.entry(dep.path[i].property.clone())
.or_insert_with(|| {
Box::new(HoistableNodeEntry {
node: HoistableNode {
properties: HashMap::new(),
access_type,
},
})
});
curr = &mut entry.node;
}
}
Self {
hoistable_roots,
dep_roots: IndexMap::new(),
}
}
fn add_dependency(&mut self, dep: ReactiveScopeDependency, _env: &Environment) {
let root = self
.dep_roots
.entry(dep.identifier)
.or_insert_with(|| {
(
DependencyNode {
properties: IndexMap::new(),
access_type: PropertyAccessType::UnconditionalAccess,
loc: dep.loc,
},
dep.reactive,
)
});
let mut dep_cursor = &mut root.0;
let hoistable_cursor_root = self.hoistable_roots.get(&dep.identifier);
let mut hoistable_ptr: Option<&HoistableNode> = hoistable_cursor_root.map(|(n, _)| n);
for entry in &dep.path {
let next_hoistable: Option<&HoistableNode>;
let access_type: PropertyAccessType;
if entry.optional {
next_hoistable = hoistable_ptr.and_then(|h| {
h.properties.get(&entry.property).map(|e| &e.node)
});
if hoistable_ptr.is_some()
&& hoistable_ptr.unwrap().access_type == HoistableAccessType::NonNull
{
access_type = PropertyAccessType::UnconditionalAccess;
} else {
access_type = PropertyAccessType::OptionalAccess;
}
} else if hoistable_ptr.is_some()
&& hoistable_ptr.unwrap().access_type == HoistableAccessType::NonNull
{
next_hoistable = hoistable_ptr.and_then(|h| {
h.properties.get(&entry.property).map(|e| &e.node)
});
access_type = PropertyAccessType::UnconditionalAccess;
} else {
break;
}
let child = dep_cursor
.properties
.entry(entry.property.clone())
.or_insert_with(|| {
Box::new(DependencyNodeEntry {
node: DependencyNode {
properties: IndexMap::new(),
access_type,
loc: entry.loc,
},
})
});
child.node.access_type = merge_access(child.node.access_type, access_type);
dep_cursor = &mut child.node;
hoistable_ptr = next_hoistable;
}
dep_cursor.access_type =
merge_access(dep_cursor.access_type, PropertyAccessType::OptionalDependency);
}
fn derive_minimal_dependencies(&self, _env: &Environment) -> Vec<ReactiveScopeDependency> {
let mut results = Vec::new();
for (&root_id, (root_node, reactive)) in &self.dep_roots {
collect_minimal_deps_in_subtree(
root_node,
*reactive,
root_id,
&[],
&mut results,
);
}
results
}
}
fn collect_minimal_deps_in_subtree(
node: &DependencyNode,
reactive: bool,
root_id: IdentifierId,
path: &[DependencyPathEntry],
results: &mut Vec<ReactiveScopeDependency>,
) {
if is_dependency_access(node.access_type) {
results.push(ReactiveScopeDependency {
identifier: root_id,
reactive,
path: path.to_vec(),
loc: node.loc,
});
} else {
for (child_name, child_entry) in &node.properties {
let mut new_path = path.to_vec();
new_path.push(DependencyPathEntry {
property: child_name.clone(),
optional: is_optional_access(child_entry.node.access_type),
loc: child_entry.node.loc,
});
collect_minimal_deps_in_subtree(
&child_entry.node,
reactive,
root_id,
&new_path,
results,
);
}
}
}
#[derive(Clone)]
struct Decl {
id: EvaluationOrder,
scope_stack: Vec<ScopeId>, }
struct DependencyCollectionContext<'a> {
declarations: HashMap<DeclarationId, Decl>,
reassignments: HashMap<IdentifierId, Decl>,
scope_stack: Vec<ScopeId>,
dep_stack: Vec<Vec<ReactiveScopeDependency>>,
deps: IndexMap<ScopeId, Vec<ReactiveScopeDependency>>,
temporaries: &'a HashMap<IdentifierId, ReactiveScopeDependency>,
#[allow(dead_code)]
temporaries_used_outside_scope: &'a HashSet<DeclarationId>,
processed_instrs_in_optional: &'a HashSet<ProcessedInstr>,
inner_fn_context: Option<EvaluationOrder>,
}
impl<'a> DependencyCollectionContext<'a> {
fn new(
temporaries_used_outside_scope: &'a HashSet<DeclarationId>,
temporaries: &'a HashMap<IdentifierId, ReactiveScopeDependency>,
processed_instrs_in_optional: &'a HashSet<ProcessedInstr>,
) -> Self {
Self {
declarations: HashMap::new(),
reassignments: HashMap::new(),
scope_stack: Vec::new(),
dep_stack: Vec::new(),
deps: IndexMap::new(),
temporaries,
temporaries_used_outside_scope,
processed_instrs_in_optional,
inner_fn_context: None,
}
}
fn enter_scope(&mut self, scope_id: ScopeId) {
self.dep_stack.push(Vec::new());
self.scope_stack.push(scope_id);
}
fn exit_scope(&mut self, scope_id: ScopeId, pruned: bool, env: &mut Environment) {
let scoped_deps = self.dep_stack.pop().expect(
"[PropagateScopeDeps]: Unexpected scope mismatch",
);
self.scope_stack.pop();
for dep in &scoped_deps {
if self.check_valid_dependency(dep, env) {
if let Some(top) = self.dep_stack.last_mut() {
top.push(dep.clone());
}
}
}
if !pruned {
self.deps.insert(scope_id, scoped_deps);
}
}
fn current_scope(&self) -> Option<ScopeId> {
self.scope_stack.last().copied()
}
fn declare(&mut self, identifier_id: IdentifierId, decl: Decl, env: &Environment) {
if self.inner_fn_context.is_some() {
return;
}
let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id;
if !self.declarations.contains_key(&decl_id) {
self.declarations.insert(decl_id, decl.clone());
}
self.reassignments.insert(identifier_id, decl);
}
fn has_declared(&self, identifier_id: IdentifierId, env: &Environment) -> bool {
let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id;
self.declarations.contains_key(&decl_id)
}
fn check_valid_dependency(&self, dep: &ReactiveScopeDependency, env: &Environment) -> bool {
let ty = &env.types[env.identifiers[dep.identifier.0 as usize].type_.0 as usize];
if react_compiler_hir::is_ref_value_type(ty) {
return false;
}
if matches!(ty, Type::ObjectMethod) {
return false;
}
let ident = &env.identifiers[dep.identifier.0 as usize];
let current_declaration = self
.reassignments
.get(&dep.identifier)
.or_else(|| self.declarations.get(&ident.declaration_id));
if let Some(current_scope) = self.current_scope() {
if let Some(decl) = current_declaration {
let scope_range_start = env.scopes[current_scope.0 as usize].range.start;
return decl.id < scope_range_start;
}
}
false
}
fn visit_operand(&mut self, place: &Place, env: &mut Environment) {
let dep = self
.temporaries
.get(&place.identifier)
.cloned()
.unwrap_or_else(|| ReactiveScopeDependency {
identifier: place.identifier,
reactive: place.reactive,
path: vec![],
loc: place.loc,
});
self.visit_dependency(dep, env);
}
fn visit_property(
&mut self,
object: &Place,
property: &PropertyLiteral,
optional: bool,
loc: Option<react_compiler_hir::SourceLocation>,
env: &mut Environment,
) {
let dep = get_property(object, property, optional, loc, self.temporaries, env);
self.visit_dependency(dep, env);
}
fn visit_dependency(&mut self, dep: ReactiveScopeDependency, env: &mut Environment) {
let ident = &env.identifiers[dep.identifier.0 as usize];
let decl_id = ident.declaration_id;
if let Some(original_decl) = self.declarations.get(&decl_id) {
if !original_decl.scope_stack.is_empty() {
let orig_scope_stack = original_decl.scope_stack.clone();
for &scope_id in &orig_scope_stack {
if !self.scope_stack.contains(&scope_id) {
let scope = &env.scopes[scope_id.0 as usize];
let already_declared = scope.declarations.iter().any(|(_, d)| {
env.identifiers[d.identifier.0 as usize].declaration_id == decl_id
});
if !already_declared {
let orig_scope_id = *orig_scope_stack.last().unwrap();
let new_decl = react_compiler_hir::ReactiveScopeDeclaration {
identifier: dep.identifier,
scope: orig_scope_id,
};
env.scopes[scope_id.0 as usize]
.declarations
.push((dep.identifier, new_decl));
}
}
}
}
}
let dep = if react_compiler_hir::is_use_ref_type(
&env.types[env.identifiers[dep.identifier.0 as usize].type_.0 as usize],
) && dep
.path
.first()
.map(|p| p.property == PropertyLiteral::String("current".to_string()))
.unwrap_or(false)
{
ReactiveScopeDependency {
identifier: dep.identifier,
reactive: dep.reactive,
path: vec![],
loc: dep.loc,
}
} else {
dep
};
if self.check_valid_dependency(&dep, env) {
if let Some(top) = self.dep_stack.last_mut() {
top.push(dep);
}
}
}
fn visit_reassignment(&mut self, place: &Place, env: &mut Environment) {
if let Some(current_scope) = self.current_scope() {
let scope = &env.scopes[current_scope.0 as usize];
let already = scope.reassignments.iter().any(|id| {
env.identifiers[id.0 as usize].declaration_id
== env.identifiers[place.identifier.0 as usize].declaration_id
});
if !already
&& self.check_valid_dependency(
&ReactiveScopeDependency {
identifier: place.identifier,
reactive: place.reactive,
path: vec![],
loc: place.loc,
},
env,
)
{
env.scopes[current_scope.0 as usize]
.reassignments
.push(place.identifier);
}
}
}
fn is_deferred_dependency_instr(&self, instr: &Instruction) -> bool {
self.processed_instrs_in_optional
.contains(&ProcessedInstr::Instruction(instr.lvalue.identifier))
|| self.temporaries.contains_key(&instr.lvalue.identifier)
}
fn is_deferred_dependency_terminal(&self, block_id: BlockId) -> bool {
self.processed_instrs_in_optional
.contains(&ProcessedInstr::Terminal(block_id))
}
}
fn visit_inner_function_blocks(
func_id: FunctionId,
ctx: &mut DependencyCollectionContext,
env: &mut Environment,
) {
let inner_instrs: Vec<Instruction> = env.functions[func_id.0 as usize]
.instructions
.clone();
let inner_blocks: Vec<(BlockId, Vec<InstructionId>, Vec<(BlockId, IdentifierId)>, Terminal)> =
env.functions[func_id.0 as usize]
.body
.blocks
.iter()
.map(|(bid, blk)| {
let phi_ops: Vec<(BlockId, IdentifierId)> = blk
.phis
.iter()
.flat_map(|phi| {
phi.operands
.iter()
.map(|(pred, place)| (*pred, place.identifier))
})
.collect();
(*bid, blk.instructions.clone(), phi_ops, blk.terminal.clone())
})
.collect();
for (inner_bid, inner_instr_ids, inner_phis, inner_terminal) in &inner_blocks {
for &(_pred_id, op_id) in inner_phis {
if let Some(maybe_optional) = ctx.temporaries.get(&op_id) {
ctx.visit_dependency(maybe_optional.clone(), env);
}
}
for &iid in inner_instr_ids {
let inner_instr = &inner_instrs[iid.0 as usize];
match &inner_instr.value {
InstructionValue::FunctionExpression { lowered_func, .. }
| InstructionValue::ObjectMethod { lowered_func, .. } => {
let scope_stack_copy = ctx.scope_stack.clone();
ctx.declare(
inner_instr.lvalue.identifier,
Decl {
id: inner_instr.id,
scope_stack: scope_stack_copy,
},
env,
);
visit_inner_function_blocks(lowered_func.func, ctx, env);
}
_ => {
handle_instruction(inner_instr, ctx, env);
}
}
}
if !ctx.is_deferred_dependency_terminal(*inner_bid) {
let terminal_ops = visitors::each_terminal_operand(inner_terminal);
for op in &terminal_ops {
ctx.visit_operand(op, env);
}
}
}
}
fn handle_instruction(
instr: &Instruction,
ctx: &mut DependencyCollectionContext,
env: &mut Environment,
) {
let id = instr.id;
let scope_stack_copy = ctx.scope_stack.clone();
ctx.declare(
instr.lvalue.identifier,
Decl {
id,
scope_stack: scope_stack_copy,
},
env,
);
if ctx.is_deferred_dependency_instr(instr) {
return;
}
match &instr.value {
InstructionValue::PropertyLoad {
object,
property,
loc,
..
} => {
ctx.visit_property(object, property, false, *loc, env);
}
InstructionValue::StoreLocal {
value: val,
lvalue,
..
} => {
ctx.visit_operand(val, env);
if lvalue.kind == InstructionKind::Reassign {
ctx.visit_reassignment(&lvalue.place, env);
}
let scope_stack_copy = ctx.scope_stack.clone();
ctx.declare(
lvalue.place.identifier,
Decl {
id,
scope_stack: scope_stack_copy,
},
env,
);
}
InstructionValue::DeclareLocal { lvalue, .. }
| InstructionValue::DeclareContext { lvalue, .. } => {
if convert_hoisted_lvalue_kind(lvalue.kind).is_none() {
let scope_stack_copy = ctx.scope_stack.clone();
ctx.declare(
lvalue.place.identifier,
Decl {
id,
scope_stack: scope_stack_copy,
},
env,
);
}
}
InstructionValue::Destructure {
value: val,
lvalue,
..
} => {
ctx.visit_operand(val, env);
let pattern_places = visitors::each_pattern_operand(&lvalue.pattern);
for place in &pattern_places {
if lvalue.kind == InstructionKind::Reassign {
ctx.visit_reassignment(place, env);
}
let scope_stack_copy = ctx.scope_stack.clone();
ctx.declare(
place.identifier,
Decl {
id,
scope_stack: scope_stack_copy,
},
env,
);
}
}
InstructionValue::StoreContext {
lvalue,
value: val,
..
} => {
if !ctx.has_declared(lvalue.place.identifier, env)
|| lvalue.kind != InstructionKind::Reassign
{
let scope_stack_copy = ctx.scope_stack.clone();
ctx.declare(
lvalue.place.identifier,
Decl {
id,
scope_stack: scope_stack_copy,
},
env,
);
}
ctx.visit_operand(&lvalue.place, env);
ctx.visit_operand(val, env);
}
_ => {
let operands = visitors::each_instruction_value_operand(&instr.value, env);
for operand in &operands {
ctx.visit_operand(operand, env);
}
}
}
}
fn collect_dependencies(
func: &HirFunction,
env: &mut Environment,
used_outside_declaring_scope: &HashSet<DeclarationId>,
temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
processed_instrs_in_optional: &HashSet<ProcessedInstr>,
) -> IndexMap<ScopeId, Vec<ReactiveScopeDependency>> {
let mut ctx = DependencyCollectionContext::new(
used_outside_declaring_scope,
temporaries,
processed_instrs_in_optional,
);
for param in &func.params {
match param {
ParamPattern::Place(place) => {
ctx.declare(
place.identifier,
Decl {
id: EvaluationOrder(0),
scope_stack: vec![],
},
env,
);
}
ParamPattern::Spread(spread) => {
ctx.declare(
spread.place.identifier,
Decl {
id: EvaluationOrder(0),
scope_stack: vec![],
},
env,
);
}
}
}
let mut traversal = ScopeBlockTraversal::new();
handle_function_deps(func, env, &mut ctx, &mut traversal);
ctx.deps
}
fn handle_function_deps(
func: &HirFunction,
env: &mut Environment,
ctx: &mut DependencyCollectionContext,
traversal: &mut ScopeBlockTraversal,
) {
for (block_id, block) in &func.body.blocks {
traversal.record_scopes(block);
let scope_block_info = traversal.block_infos.get(block_id).cloned();
match &scope_block_info {
Some(ScopeBlockInfo::Begin { scope, .. }) => {
ctx.enter_scope(*scope);
}
Some(ScopeBlockInfo::End { scope, pruned, .. }) => {
ctx.exit_scope(*scope, *pruned, env);
}
None => {}
}
for phi in &block.phis {
for (_pred_id, operand) in &phi.operands {
if let Some(maybe_optional_chain) = ctx.temporaries.get(&operand.identifier) {
ctx.visit_dependency(maybe_optional_chain.clone(), env);
}
}
}
for &instr_id in &block.instructions {
let instr = &func.instructions[instr_id.0 as usize];
match &instr.value {
InstructionValue::FunctionExpression { lowered_func, .. }
| InstructionValue::ObjectMethod { lowered_func, .. } => {
let scope_stack_copy = ctx.scope_stack.clone();
ctx.declare(
instr.lvalue.identifier,
Decl {
id: instr.id,
scope_stack: scope_stack_copy,
},
env,
);
let inner_func_id = lowered_func.func;
let prev_inner = ctx.inner_fn_context;
if ctx.inner_fn_context.is_none() {
ctx.inner_fn_context = Some(instr.id);
}
visit_inner_function_blocks(inner_func_id, ctx, env);
ctx.inner_fn_context = prev_inner;
}
_ => {
handle_instruction(instr, ctx, env);
}
}
}
if !ctx.is_deferred_dependency_terminal(*block_id) {
let terminal_ops = visitors::each_terminal_operand(&block.terminal);
for op in &terminal_ops {
ctx.visit_operand(op, env);
}
}
}
}