compiler/crates/react_compiler_inference/src/build_reactive_scope_terminals_hir.rs RUST 425 lines View on github.com → Search inside
1// Copyright (c) Meta Platforms, Inc. and affiliates.2//3// This source code is licensed under the MIT license found in the4// LICENSE file in the root directory of this source tree.56//! Builds reactive scope terminals in the HIR.7//!8//! Given a function whose reactive scope ranges have been correctly aligned and9//! merged, this pass rewrites blocks to introduce ReactiveScopeTerminals and10//! their fallthrough blocks.11//!12//! Ported from TypeScript `src/HIR/BuildReactiveScopeTerminalsHIR.ts`.1314use indexmap::{IndexMap, IndexSet};15use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};1617use react_compiler_hir::BasicBlock;18use react_compiler_hir::BlockId;19use react_compiler_hir::EvaluationOrder;20use react_compiler_hir::GotoVariant;21use react_compiler_hir::HirFunction;22use react_compiler_hir::IdentifierId;23use react_compiler_hir::ScopeId;24use react_compiler_hir::Terminal;25use react_compiler_hir::environment::Environment;26use react_compiler_hir::visitors::each_instruction_lvalue_ids;27use react_compiler_hir::visitors::each_instruction_operand_ids;28use react_compiler_hir::visitors::each_terminal_operand_ids;29use react_compiler_lowering::get_reverse_postordered_blocks;30use react_compiler_lowering::mark_instruction_ids;31use react_compiler_lowering::mark_predecessors;3233// =============================================================================34// getScopes35// =============================================================================3637/// Collect all unique scopes from places in the function that have non-empty ranges.38/// Corresponds to TS `getScopes(fn)`.39fn get_scopes(func: &HirFunction, env: &Environment) -> Vec<ScopeId> {40    let mut scope_ids: FxHashSet<ScopeId> = FxHashSet::default();4142    let mut visit_place = |identifier_id: IdentifierId| {43        if let Some(scope_id) = env.identifiers[identifier_id.0 as usize].scope {44            let range = &env.scopes[scope_id.0 as usize].range;45            if range.start != range.end {46                scope_ids.insert(scope_id);47            }48        }49    };5051    for (_block_id, block) in &func.body.blocks {52        for &instr_id in &block.instructions {53            let instr = &func.instructions[instr_id.0 as usize];54            // lvalues55            for id in each_instruction_lvalue_ids(instr) {56                visit_place(id);57            }58            // operands59            for id in each_instruction_operand_ids(instr, env) {60                visit_place(id);61            }62        }63        // terminal operands64        for id in each_terminal_operand_ids(&block.terminal) {65            visit_place(id);66        }67    }6869    scope_ids.into_iter().collect()70}7172// =============================================================================73// TerminalRewriteInfo74// =============================================================================7576enum TerminalRewriteInfo {77    StartScope {78        block_id: BlockId,79        fallthrough_id: BlockId,80        instr_id: EvaluationOrder,81        scope_id: ScopeId,82    },83    EndScope {84        instr_id: EvaluationOrder,85        fallthrough_id: BlockId,86    },87}8889impl TerminalRewriteInfo {90    fn instr_id(&self) -> EvaluationOrder {91        match self {92            TerminalRewriteInfo::StartScope { instr_id, .. } => *instr_id,93            TerminalRewriteInfo::EndScope { instr_id, .. } => *instr_id,94        }95    }96}9798// =============================================================================99// collectScopeRewrites100// =============================================================================101102/// Collect all scope rewrites by traversing scopes in pre-order.103fn collect_scope_rewrites(func: &HirFunction, env: &mut Environment) -> Vec<TerminalRewriteInfo> {104    let scope_ids = get_scopes(func, env);105106    // Sort: ascending by start, descending by end for ties107    let mut items: Vec<ScopeId> = scope_ids;108    items.sort_by(|a, b| {109        let a_range = &env.scopes[a.0 as usize].range;110        let b_range = &env.scopes[b.0 as usize].range;111        let start_diff = a_range.start.0.cmp(&b_range.start.0);112        if start_diff != std::cmp::Ordering::Equal {113            return start_diff;114        }115        b_range.end.0.cmp(&a_range.end.0)116    });117118    let mut rewrites: Vec<TerminalRewriteInfo> = Vec::new();119    let mut fallthroughs: FxHashMap<ScopeId, BlockId> = FxHashMap::default();120    let mut active_items: Vec<ScopeId> = Vec::new();121122    for i in 0..items.len() {123        let curr = items[i];124        let curr_start = env.scopes[curr.0 as usize].range.start;125        let curr_end = env.scopes[curr.0 as usize].range.end;126127        // Pop active items that are disjoint with current128        let mut j = active_items.len();129        while j > 0 {130            j -= 1;131            let maybe_parent = active_items[j];132            let parent_end = env.scopes[maybe_parent.0 as usize].range.end;133            let disjoint = curr_start >= parent_end;134            let nested = curr_end <= parent_end;135            assert!(136                disjoint || nested,137                "Invalid nesting in program blocks or scopes"138            );139            if disjoint {140                // Exit this scope141                let fallthrough_id = *fallthroughs142                    .get(&maybe_parent)143                    .expect("Expected scope to exist");144                let end_instr_id = env.scopes[maybe_parent.0 as usize].range.end;145                rewrites.push(TerminalRewriteInfo::EndScope {146                    instr_id: end_instr_id,147                    fallthrough_id,148                });149                active_items.truncate(j);150            } else {151                break;152            }153        }154155        // Enter scope156        let block_id = env.next_block_id();157        let fallthrough_id = env.next_block_id();158        let start_instr_id = env.scopes[curr.0 as usize].range.start;159        rewrites.push(TerminalRewriteInfo::StartScope {160            block_id,161            fallthrough_id,162            instr_id: start_instr_id,163            scope_id: curr,164        });165        fallthroughs.insert(curr, fallthrough_id);166        active_items.push(curr);167    }168169    // Exit remaining active items170    while let Some(curr) = active_items.pop() {171        let fallthrough_id = *fallthroughs.get(&curr).expect("Expected scope to exist");172        let end_instr_id = env.scopes[curr.0 as usize].range.end;173        rewrites.push(TerminalRewriteInfo::EndScope {174            instr_id: end_instr_id,175            fallthrough_id,176        });177    }178179    rewrites180}181182// =============================================================================183// handleRewrite184// =============================================================================185186struct RewriteContext {187    next_block_id: BlockId,188    next_preds: Vec<BlockId>,189    instr_slice_idx: usize,190    rewrites: Vec<BasicBlock>,191}192193fn handle_rewrite(194    terminal_info: &TerminalRewriteInfo,195    idx: usize,196    source_block: &BasicBlock,197    context: &mut RewriteContext,198) {199    let terminal: Terminal = match terminal_info {200        TerminalRewriteInfo::StartScope {201            block_id,202            fallthrough_id,203            instr_id,204            scope_id,205        } => Terminal::Scope {206            fallthrough: *fallthrough_id,207            block: *block_id,208            scope: *scope_id,209            id: *instr_id,210            loc: None,211        },212        TerminalRewriteInfo::EndScope {213            instr_id,214            fallthrough_id,215        } => Terminal::Goto {216            variant: GotoVariant::Break,217            block: *fallthrough_id,218            id: *instr_id,219            loc: None,220        },221    };222223    let curr_block_id = context.next_block_id;224    let mut preds = IndexSet::default();225    for &p in &context.next_preds {226        preds.insert(p);227    }228229    context.rewrites.push(BasicBlock {230        kind: source_block.kind,231        id: curr_block_id,232        instructions: source_block.instructions[context.instr_slice_idx..idx].to_vec(),233        preds,234        // Only the first rewrite should reuse source block phis235        phis: if context.rewrites.is_empty() {236            source_block.phis.clone()237        } else {238            Vec::new()239        },240        terminal,241    });242243    context.next_preds = vec![curr_block_id];244    context.next_block_id = match terminal_info {245        TerminalRewriteInfo::StartScope { block_id, .. } => *block_id,246        TerminalRewriteInfo::EndScope { fallthrough_id, .. } => *fallthrough_id,247    };248    context.instr_slice_idx = idx;249}250251// =============================================================================252// Public API253// =============================================================================254255/// Builds reactive scope terminals in the HIR.256///257/// This pass assumes that all program blocks are properly nested with respect258/// to fallthroughs. Given a function whose reactive scope ranges have been259/// correctly aligned and merged, this pass rewrites blocks to introduce260/// ReactiveScopeTerminals and their fallthrough blocks.261pub fn build_reactive_scope_terminals_hir(func: &mut HirFunction, env: &mut Environment) {262    // Step 1: Collect rewrites263    let mut queued_rewrites = collect_scope_rewrites(func, env);264265    // Step 2: Apply rewrites by splitting blocks266    let mut rewritten_final_blocks: FxHashMap<BlockId, BlockId> = FxHashMap::default();267    let mut next_blocks: IndexMap<BlockId, BasicBlock, FxBuildHasher> = IndexMap::default();268269    // Reverse so we can pop from the end while traversing in ascending order270    queued_rewrites.reverse();271272    for (_block_id, block) in &func.body.blocks {273        let preds_vec: Vec<BlockId> = block.preds.iter().copied().collect();274        let mut context = RewriteContext {275            next_block_id: block.id,276            rewrites: Vec::new(),277            next_preds: preds_vec,278            instr_slice_idx: 0,279        };280281        // Handle queued terminal rewrites at their nearest instruction ID282        for i in 0..block.instructions.len() + 1 {283            let instr_id = if i < block.instructions.len() {284                let instr_idx = block.instructions[i];285                func.instructions[instr_idx.0 as usize].id286            } else {287                block.terminal.evaluation_order()288            };289290            while let Some(rewrite) = queued_rewrites.last() {291                if rewrite.instr_id() <= instr_id {292                    // Need to pop before calling handle_rewrite293                    let rewrite = queued_rewrites.pop().unwrap();294                    handle_rewrite(&rewrite, i, block, &mut context);295                } else {296                    break;297                }298            }299        }300301        if !context.rewrites.is_empty() {302            let mut final_preds = IndexSet::default();303            for &p in &context.next_preds {304                final_preds.insert(p);305            }306            let final_block = BasicBlock {307                id: context.next_block_id,308                kind: block.kind,309                preds: final_preds,310                terminal: block.terminal.clone(),311                instructions: block.instructions[context.instr_slice_idx..].to_vec(),312                phis: Vec::new(),313            };314            let final_block_id = final_block.id;315            context.rewrites.push(final_block);316            for b in context.rewrites {317                next_blocks.insert(b.id, b);318            }319            rewritten_final_blocks.insert(block.id, final_block_id);320        } else {321            next_blocks.insert(block.id, block.clone());322        }323    }324325    func.body.blocks = next_blocks;326327    // Step 3: Repoint phis when they refer to a rewritten block328    for block in func.body.blocks.values_mut() {329        for phi in &mut block.phis {330            let updates: Vec<(BlockId, BlockId)> = phi331                .operands332                .keys()333                .filter_map(|original_id| {334                    rewritten_final_blocks335                        .get(original_id)336                        .map(|new_id| (*original_id, *new_id))337                })338                .collect();339            for (old_id, new_id) in updates {340                if let Some(value) = phi.operands.shift_remove(&old_id) {341                    phi.operands.insert(new_id, value);342                }343            }344        }345    }346347    // Step 4: Fixup HIR to restore RPO, correct predecessors, renumber instructions348    func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions);349    mark_predecessors(&mut func.body);350    mark_instruction_ids(&mut func.body, &mut func.instructions);351352    // Step 5: Fix scope and identifier ranges to account for renumbered instructions353    fix_scope_and_identifier_ranges(func, env);354}355356/// Fix scope ranges after instruction renumbering.357/// Scope ranges should always align to start at the 'scope' terminal358/// and end at the first instruction of the fallthrough block.359///360/// In TS, `identifier.mutableRange` and `scope.range` are the same object361/// reference (after InferReactiveScopeVariables). When scope.range is updated,362/// all identifiers with that scope automatically see the new range.363/// BUT: after MergeOverlappingReactiveScopesHIR, repointed identifiers have364/// mutableRange pointing to the OLD scope's range, NOT the root scope's range.365/// So only identifiers whose mutableRange matches their scope's pre-renumbering366/// range should be updated.367///368/// Corresponds to TS `fixScopeAndIdentifierRanges`.369fn fix_scope_and_identifier_ranges(func: &HirFunction, env: &mut Environment) {370    // Save original scope ranges before updating them. In TS,371    // identifier.mutableRange and scope.range may or may not be the same372    // JS object. Only identifiers whose mutableRange shares the same object373    // reference as scope.range see the update automatically. We simulate374    // this by only syncing identifiers whose mutableRange matches the375    // scope's pre-update range.376    let original_scope_ranges: Vec<react_compiler_hir::MutableRange> =377        env.scopes.iter().map(|s| s.range.clone()).collect();378379    for (_block_id, block) in &func.body.blocks {380        match &block.terminal {381            Terminal::Scope {382                fallthrough,383                scope,384                id,385                ..386            }387            | Terminal::PrunedScope {388                fallthrough,389                scope,390                id,391                ..392            } => {393                let fallthrough_block = func.body.blocks.get(fallthrough).unwrap();394                let first_id = if !fallthrough_block.instructions.is_empty() {395                    func.instructions[fallthrough_block.instructions[0].0 as usize].id396                } else {397                    fallthrough_block.terminal.evaluation_order()398                };399                env.scopes[scope.0 as usize].range.start = *id;400                env.scopes[scope.0 as usize].range.end = first_id;401            }402            _ => {}403        }404    }405406    // Sync identifier mutable ranges with their scope ranges, but ONLY407    // for identifiers whose mutableRange has the same identity as their408    // scope's ORIGINAL range (before the updates above). In TS,409    // identifier.mutableRange and scope.range are only the same JS object410    // for identifiers that were the canonical representative when the scope411    // was created. After MergeOverlappingReactiveScopesHIR, repointed412    // identifiers have mutableRange pointing to the OLD scope's range,413    // not the root scope's range — so they should NOT be synced here.414    for ident in &mut env.identifiers {415        if let Some(scope_id) = ident.scope {416            let original = &original_scope_ranges[scope_id.0 as usize];417            if ident.mutable_range.same_range(original) {418                let scope_range = &env.scopes[scope_id.0 as usize].range;419                ident.mutable_range.start = scope_range.start;420                ident.mutable_range.end = scope_range.end;421            }422        }423    }424}

Code quality findings 31

Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
if let Some(scope_id) = env.identifiers[identifier_id.0 as usize].scope {
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let range = &env.scopes[scope_id.0 as usize].range;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let instr = &func.instructions[instr_id.0 as usize];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let a_range = &env.scopes[a.0 as usize].range;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let b_range = &env.scopes[b.0 as usize].range;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let curr = items[i];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let curr_start = env.scopes[curr.0 as usize].range.start;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let curr_end = env.scopes[curr.0 as usize].range.end;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let maybe_parent = active_items[j];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let parent_end = env.scopes[maybe_parent.0 as usize].range.end;
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
.expect("Expected scope to exist");
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let end_instr_id = env.scopes[maybe_parent.0 as usize].range.end;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let start_instr_id = env.scopes[curr.0 as usize].range.start;
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
let fallthrough_id = *fallthroughs.get(&curr).expect("Expected scope to exist");
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let end_instr_id = env.scopes[curr.0 as usize].range.end;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
instructions: source_block.instructions[context.instr_slice_idx..idx].to_vec(),
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let rewrite = queued_rewrites.pop().unwrap();
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
instructions: block.instructions[context.instr_slice_idx..].to_vec(),
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let fallthrough_block = func.body.blocks.get(fallthrough).unwrap();
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
func.instructions[fallthrough_block.instructions[0].0 as usize].id
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
env.scopes[scope.0 as usize].range.start = *id;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
env.scopes[scope.0 as usize].range.end = first_id;
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let original = &original_scope_ranges[scope_id.0 as usize];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
let scope_range = &env.scopes[scope_id.0 as usize].range;
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
active_items.push(curr);
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
rewrites.push(TerminalRewriteInfo::EndScope {
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
context.rewrites.push(BasicBlock {
Performance Info: Frequent cloning, especially of Strings, Vecs, or other heap-allocated types inside loops, can be expensive. Consider using references/borrowing where possible.
info performance clone-in-loop
terminal: block.terminal.clone(),
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
context.rewrites.push(final_block);
Performance Info: Frequent cloning, especially of Strings, Vecs, or other heap-allocated types inside loops, can be expensive. Consider using references/borrowing where possible.
info performance clone-in-loop
next_blocks.insert(block.id, block.clone());
Performance Info: Frequent cloning, especially of Strings, Vecs, or other heap-allocated types inside loops, can be expensive. Consider using references/borrowing where possible.
info performance clone-in-loop
env.scopes.iter().map(|s| s.range.clone()).collect();

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.