Skip to main content

react_compiler_lowering/
hir_builder.rs

1use indexmap::IndexMap;
2use indexmap::IndexSet;
3use react_compiler_ast::scope::BindingId;
4use react_compiler_ast::scope::ImportBindingKind;
5use react_compiler_ast::scope::ScopeId;
6use react_compiler_ast::scope::ScopeInfo;
7use react_compiler_diagnostics::CompilerDiagnostic;
8use react_compiler_diagnostics::CompilerDiagnosticDetail;
9use react_compiler_diagnostics::CompilerError;
10use react_compiler_diagnostics::CompilerErrorDetail;
11use react_compiler_diagnostics::ErrorCategory;
12use react_compiler_hir::environment::Environment;
13use react_compiler_hir::visitors::each_terminal_successor;
14use react_compiler_hir::visitors::terminal_fallthrough;
15use react_compiler_hir::*;
16
17use crate::identifier_loc_index::IdentifierLocIndex;
18
19// ---------------------------------------------------------------------------
20// Reserved word check (matches TS isReservedWord)
21// ---------------------------------------------------------------------------
22
23pub(crate) fn is_always_reserved_word(s: &str) -> bool {
24    matches!(
25        s,
26        "break"
27            | "case"
28            | "catch"
29            | "continue"
30            | "debugger"
31            | "default"
32            | "do"
33            | "else"
34            | "finally"
35            | "for"
36            | "function"
37            | "if"
38            | "in"
39            | "instanceof"
40            | "new"
41            | "return"
42            | "switch"
43            | "this"
44            | "throw"
45            | "try"
46            | "typeof"
47            | "var"
48            | "void"
49            | "while"
50            | "with"
51            | "class"
52            | "const"
53            | "enum"
54            | "export"
55            | "extends"
56            | "import"
57            | "super"
58            | "null"
59            | "true"
60            | "false"
61            | "delete"
62    )
63}
64
65pub(crate) fn reserved_identifier_diagnostic(name: &str) -> CompilerDiagnostic {
66    CompilerDiagnostic::new(
67        ErrorCategory::Syntax,
68        "Expected a non-reserved identifier name",
69        Some(format!(
70            "`{}` is a reserved word in JavaScript and cannot be used as an identifier name",
71            name
72        )),
73    )
74    .with_detail(CompilerDiagnosticDetail::Error {
75        loc: None, // GeneratedSource in TS
76        message: Some("reserved word".to_string()),
77        identifier_name: None,
78    })
79}
80
81// ---------------------------------------------------------------------------
82// Scope types for tracking break/continue targets
83// ---------------------------------------------------------------------------
84
85enum Scope {
86    Loop {
87        label: Option<String>,
88        continue_block: BlockId,
89        break_block: BlockId,
90    },
91    Label {
92        label: String,
93        break_block: BlockId,
94    },
95    Switch {
96        label: Option<String>,
97        break_block: BlockId,
98    },
99}
100
101impl Scope {
102    fn label(&self) -> Option<&str> {
103        match self {
104            Scope::Loop { label, .. } => label.as_deref(),
105            Scope::Label { label, .. } => Some(label.as_str()),
106            Scope::Switch { label, .. } => label.as_deref(),
107        }
108    }
109
110    fn break_block(&self) -> BlockId {
111        match self {
112            Scope::Loop { break_block, .. } => *break_block,
113            Scope::Label { break_block, .. } => *break_block,
114            Scope::Switch { break_block, .. } => *break_block,
115        }
116    }
117}
118
119// ---------------------------------------------------------------------------
120// WipBlock: a block under construction that does not yet have a terminal
121// ---------------------------------------------------------------------------
122
123pub struct WipBlock {
124    pub id: BlockId,
125    pub instructions: Vec<InstructionId>,
126    pub kind: BlockKind,
127}
128
129fn new_block(id: BlockId, kind: BlockKind) -> WipBlock {
130    WipBlock {
131        id,
132        kind,
133        instructions: Vec::new(),
134    }
135}
136
137// ---------------------------------------------------------------------------
138// HirBuilder: helper struct for constructing a CFG
139// ---------------------------------------------------------------------------
140
141pub struct HirBuilder<'a> {
142    completed: IndexMap<BlockId, BasicBlock>,
143    current: WipBlock,
144    entry: BlockId,
145    scopes: Vec<Scope>,
146    /// Context identifiers: variables captured from an outer scope.
147    /// Maps the outer scope's BindingId to the source location where it was referenced.
148    context: IndexMap<BindingId, Option<SourceLocation>>,
149    /// Resolved bindings: maps a BindingId to the HIR IdentifierId created for it.
150    bindings: IndexMap<BindingId, IdentifierId>,
151    /// Names already used by bindings, for collision avoidance.
152    /// Maps name string -> how many times it has been used (for appending _0, _1, ...).
153    used_names: IndexMap<String, BindingId>,
154    env: &'a mut Environment,
155    scope_info: &'a ScopeInfo,
156    exception_handler_stack: Vec<BlockId>,
157    /// Flat instruction table being built up.
158    instruction_table: Vec<Instruction>,
159    /// Traversal context: counts the number of `fbt` tag parents
160    /// of the current babel node.
161    pub fbt_depth: u32,
162    /// The scope of the function being compiled (for context identifier checks).
163    function_scope: ScopeId,
164    /// The scope of the outermost component/hook function (for gather_captured_context).
165    component_scope: ScopeId,
166    /// Set of BindingIds for variables declared in scopes between component_scope
167    /// and any inner function scope, that are referenced from an inner function scope.
168    /// These need StoreContext/LoadContext instead of StoreLocal/LoadLocal.
169    context_identifiers: std::collections::HashSet<BindingId>,
170    /// Set of ScopeIds that have been matched to synthetic blocks/functions.
171    /// Prevents the same scope from being reused for different synthetic nodes.
172    claimed_synthetic_scopes: std::collections::HashSet<ScopeId>,
173    /// Index mapping identifier byte offsets to source locations and JSX status.
174    identifier_locs: &'a IdentifierLocIndex,
175}
176
177impl<'a> HirBuilder<'a> {
178    // -----------------------------------------------------------------------
179    // M2: Core methods
180    // -----------------------------------------------------------------------
181
182    /// Create a new HirBuilder.
183    ///
184    /// - `env`: the shared environment (counters, arenas, error accumulator)
185    /// - `scope_info`: the scope information from the AST
186    /// - `function_scope`: the ScopeId of the function being compiled
187    /// - `bindings`: optional pre-existing bindings (e.g., from a parent function)
188    /// - `context`: optional pre-existing captured context map
189    /// - `entry_block_kind`: the kind of the entry block (defaults to `Block`)
190    pub fn new(
191        env: &'a mut Environment,
192        scope_info: &'a ScopeInfo,
193        function_scope: ScopeId,
194        component_scope: ScopeId,
195        context_identifiers: std::collections::HashSet<BindingId>,
196        bindings: Option<IndexMap<BindingId, IdentifierId>>,
197        context: Option<IndexMap<BindingId, Option<SourceLocation>>>,
198        entry_block_kind: Option<BlockKind>,
199        used_names: Option<IndexMap<String, BindingId>>,
200        identifier_locs: &'a IdentifierLocIndex,
201    ) -> Self {
202        let entry = env.next_block_id();
203        let kind = entry_block_kind.unwrap_or(BlockKind::Block);
204        HirBuilder {
205            completed: IndexMap::new(),
206            current: new_block(entry, kind),
207            entry,
208            scopes: Vec::new(),
209            context: context.unwrap_or_default(),
210            bindings: bindings.unwrap_or_default(),
211            used_names: used_names.unwrap_or_default(),
212            env,
213            scope_info,
214            exception_handler_stack: Vec::new(),
215            instruction_table: Vec::new(),
216            fbt_depth: 0,
217            function_scope,
218            component_scope,
219            context_identifiers,
220            claimed_synthetic_scopes: std::collections::HashSet::new(),
221            identifier_locs,
222        }
223    }
224
225    /// Check if a scope is the component scope or a descendant of it.
226    /// Used to determine whether a binding is local to the compiled function
227    /// or belongs to an ancestor function scope (e.g., a factory function
228    /// wrapping a nested component declaration).
229    /// Uses component_scope (the outermost compiled function's scope) rather
230    /// than function_scope because inner function expressions within the
231    /// compiled function have their own function_scope but still consider
232    /// the outer component's variables as local.
233    fn is_scope_within_compiled_function(&self, scope_id: ScopeId) -> bool {
234        let mut current = Some(scope_id);
235        while let Some(id) = current {
236            if id == self.component_scope {
237                return true;
238            }
239            current = self.scope_info.scopes[id.0 as usize].parent;
240        }
241        false
242    }
243
244    /// Access the environment.
245    pub fn environment(&self) -> &Environment {
246        self.env
247    }
248
249    /// Access the environment mutably.
250    pub fn environment_mut(&mut self) -> &mut Environment {
251        self.env
252    }
253
254    /// Create a new unique TypeVar type, allocated from the environment's type arena
255    /// so that TypeIds are consistent with identifier type slots.
256    pub fn make_type(&mut self) -> Type {
257        let type_id = self.env.make_type();
258        Type::TypeVar { id: type_id }
259    }
260
261    /// Access the scope info.
262    pub fn scope_info(&self) -> &ScopeInfo {
263        self.scope_info
264    }
265
266    /// Look up the source location of an identifier by its node_id.
267    pub fn get_identifier_loc(&self, node_id: u32) -> Option<SourceLocation> {
268        self.identifier_locs
269            .get(&node_id)
270            .map(|entry| entry.loc.clone())
271    }
272
273    /// Check whether a reference at the given byte offset corresponds to a
274    /// JSXIdentifier. Scans the node_id-keyed index for an entry whose stored
275    /// `start` matches the offset.
276    pub fn is_jsx_identifier_at_pos(&self, offset: u32) -> bool {
277        self.identifier_locs
278            .values()
279            .any(|entry| entry.start == offset && entry.is_jsx)
280    }
281
282    /// Access the function scope (the scope of the function being compiled).
283    pub fn function_scope(&self) -> ScopeId {
284        self.function_scope
285    }
286
287    /// Access the component scope.
288    pub fn component_scope(&self) -> ScopeId {
289        self.component_scope
290    }
291
292    /// Access the context map.
293    pub fn context(&self) -> &IndexMap<BindingId, Option<SourceLocation>> {
294        &self.context
295    }
296
297    /// Access the pre-computed context identifiers set.
298    pub fn context_identifiers(&self) -> &std::collections::HashSet<BindingId> {
299        &self.context_identifiers
300    }
301
302    /// Add a binding to the context identifiers set (used by hoisting).
303    pub fn add_context_identifier(&mut self, binding_id: BindingId) {
304        self.context_identifiers.insert(binding_id);
305    }
306
307    pub fn claim_synthetic_scope(&mut self, scope_id: ScopeId) {
308        self.claimed_synthetic_scopes.insert(scope_id);
309    }
310
311    pub fn is_synthetic_scope_claimed(&self, scope_id: ScopeId) -> bool {
312        self.claimed_synthetic_scopes.contains(&scope_id)
313    }
314
315    /// Access scope_info and environment mutably at the same time.
316    /// This is safe because they are disjoint fields, but Rust's borrow checker
317    /// can't prove this through method calls alone.
318    pub fn scope_info_and_env_mut(&mut self) -> (&ScopeInfo, &mut Environment) {
319        (self.scope_info, self.env)
320    }
321
322    /// Access the identifier location index.
323    /// Returns the 'a reference to avoid conflicts with mutable borrows on self.
324    pub fn identifier_locs(&self) -> &'a IdentifierLocIndex {
325        self.identifier_locs
326    }
327
328    /// Access the bindings map.
329    pub fn bindings(&self) -> &IndexMap<BindingId, IdentifierId> {
330        &self.bindings
331    }
332
333    /// Access the used names map.
334    pub fn used_names(&self) -> &IndexMap<String, BindingId> {
335        &self.used_names
336    }
337
338    /// Merge used names from a child builder back into this builder.
339    /// This ensures name deduplication works across function scopes.
340    pub fn merge_used_names(&mut self, child_used_names: IndexMap<String, BindingId>) {
341        for (name, binding_id) in child_used_names {
342            self.used_names.entry(name).or_insert(binding_id);
343        }
344    }
345
346    /// Merge bindings (binding_id -> IdentifierId) from a child builder back into this builder.
347    /// This matches TS behavior where parent and child share the same #bindings map by reference,
348    /// so bindings resolved by the child are automatically visible to the parent.
349    pub fn merge_bindings(&mut self, child_bindings: IndexMap<BindingId, IdentifierId>) {
350        for (binding_id, identifier_id) in child_bindings {
351            self.bindings.entry(binding_id).or_insert(identifier_id);
352        }
353    }
354
355    /// Push an instruction onto the current block.
356    ///
357    /// Adds the instruction to the flat instruction table and records
358    /// its InstructionId in the current block's instruction list.
359    ///
360    /// If an exception handler is active, also emits a MaybeThrow terminal
361    /// after the instruction to model potential control flow to the handler,
362    /// then continues in a new block.
363    pub fn push(&mut self, instruction: Instruction) {
364        let loc = instruction.loc.clone();
365        let instr_id = InstructionId(self.instruction_table.len() as u32);
366        self.instruction_table.push(instruction);
367        self.current.instructions.push(instr_id);
368
369        if let Some(&handler) = self.exception_handler_stack.last() {
370            let continuation = self.reserve(self.current_block_kind());
371            self.terminate_with_continuation(
372                Terminal::MaybeThrow {
373                    continuation: continuation.id,
374                    handler: Some(handler),
375                    id: EvaluationOrder(0),
376                    loc,
377                    effects: None,
378                },
379                continuation,
380            );
381        }
382    }
383
384    /// Terminate the current block with the given terminal and start a new block.
385    ///
386    /// If `next_block_kind` is `Some`, a new current block is created with that kind.
387    /// Returns the BlockId of the completed block.
388    pub fn terminate(&mut self, terminal: Terminal, next_block_kind: Option<BlockKind>) -> BlockId {
389        // The placeholder block created here (BlockId(u32::MAX)) is only used when
390        // next_block_kind is None, meaning this is the final terminate() call.
391        // It will never be read or completed because build() consumes self
392        // immediately after, and no further operations should occur on the builder.
393        let wip = std::mem::replace(
394            &mut self.current,
395            new_block(BlockId(u32::MAX), BlockKind::Block),
396        );
397        let block_id = wip.id;
398
399        self.completed.insert(
400            block_id,
401            BasicBlock {
402                kind: wip.kind,
403                id: block_id,
404                instructions: wip.instructions,
405                terminal,
406                preds: IndexSet::new(),
407                phis: Vec::new(),
408            },
409        );
410
411        if let Some(kind) = next_block_kind {
412            let next_id = self.env.next_block_id();
413            self.current = new_block(next_id, kind);
414        }
415        block_id
416    }
417
418    /// Terminate the current block with the given terminal, and set
419    /// a previously reserved block as the new current block.
420    pub fn terminate_with_continuation(&mut self, terminal: Terminal, continuation: WipBlock) {
421        let wip = std::mem::replace(&mut self.current, continuation);
422        let block_id = wip.id;
423        self.completed.insert(
424            block_id,
425            BasicBlock {
426                kind: wip.kind,
427                id: block_id,
428                instructions: wip.instructions,
429                terminal,
430                preds: IndexSet::new(),
431                phis: Vec::new(),
432            },
433        );
434    }
435
436    /// Reserve a new block so it can be referenced before construction.
437    /// Use `terminate_with_continuation()` to make it current, or `complete()` to
438    /// save it directly.
439    pub fn reserve(&mut self, kind: BlockKind) -> WipBlock {
440        let id = self.env.next_block_id();
441        new_block(id, kind)
442    }
443
444    /// Save a previously reserved block as completed with the given terminal.
445    pub fn complete(&mut self, block: WipBlock, terminal: Terminal) {
446        let block_id = block.id;
447        self.completed.insert(
448            block_id,
449            BasicBlock {
450                kind: block.kind,
451                id: block_id,
452                instructions: block.instructions,
453                terminal,
454                preds: IndexSet::new(),
455                phis: Vec::new(),
456            },
457        );
458    }
459
460    /// Sets the given wip block as current, executes the closure to populate
461    /// it and obtain its terminal, then completes the block and restores the
462    /// previous current block.
463    pub fn enter_reserved(&mut self, wip: WipBlock, f: impl FnOnce(&mut Self) -> Terminal) {
464        let prev = std::mem::replace(&mut self.current, wip);
465        let terminal = f(self);
466        let completed_wip = std::mem::replace(&mut self.current, prev);
467        self.completed.insert(
468            completed_wip.id,
469            BasicBlock {
470                kind: completed_wip.kind,
471                id: completed_wip.id,
472                instructions: completed_wip.instructions,
473                terminal,
474                preds: IndexSet::new(),
475                phis: Vec::new(),
476            },
477        );
478    }
479
480    /// Like `enter_reserved`, but the closure returns a `Result<Terminal, CompilerDiagnostic>`.
481    pub fn try_enter_reserved(
482        &mut self,
483        wip: WipBlock,
484        f: impl FnOnce(&mut Self) -> Result<Terminal, CompilerDiagnostic>,
485    ) -> Result<(), CompilerDiagnostic> {
486        let prev = std::mem::replace(&mut self.current, wip);
487        let terminal = f(self)?;
488        let completed_wip = std::mem::replace(&mut self.current, prev);
489        self.completed.insert(
490            completed_wip.id,
491            BasicBlock {
492                kind: completed_wip.kind,
493                id: completed_wip.id,
494                instructions: completed_wip.instructions,
495                terminal,
496                preds: IndexSet::new(),
497                phis: Vec::new(),
498            },
499        );
500        Ok(())
501    }
502
503    /// Create a new block, set it as current, run the closure to populate it
504    /// and obtain its terminal, complete the block, and restore the previous
505    /// current block. Returns the new block's BlockId.
506    pub fn enter(
507        &mut self,
508        kind: BlockKind,
509        f: impl FnOnce(&mut Self, BlockId) -> Terminal,
510    ) -> BlockId {
511        let wip = self.reserve(kind);
512        let wip_id = wip.id;
513        self.enter_reserved(wip, |this| f(this, wip_id));
514        wip_id
515    }
516
517    /// Like `enter`, but the closure returns a `Result<Terminal, CompilerDiagnostic>`.
518    pub fn try_enter(
519        &mut self,
520        kind: BlockKind,
521        f: impl FnOnce(&mut Self, BlockId) -> Result<Terminal, CompilerDiagnostic>,
522    ) -> Result<BlockId, CompilerDiagnostic> {
523        let wip = self.reserve(kind);
524        let wip_id = wip.id;
525        self.try_enter_reserved(wip, |this| f(this, wip_id))?;
526        Ok(wip_id)
527    }
528
529    /// Push an exception handler, run the closure, then pop the handler.
530    pub fn enter_try_catch(&mut self, handler: BlockId, f: impl FnOnce(&mut Self)) {
531        self.exception_handler_stack.push(handler);
532        f(self);
533        self.exception_handler_stack.pop();
534    }
535
536    /// Like `enter_try_catch`, but the closure returns a `Result`.
537    pub fn try_enter_try_catch(
538        &mut self,
539        handler: BlockId,
540        f: impl FnOnce(&mut Self) -> Result<(), CompilerDiagnostic>,
541    ) -> Result<(), CompilerDiagnostic> {
542        self.exception_handler_stack.push(handler);
543        let result = f(self);
544        self.exception_handler_stack.pop();
545        result
546    }
547
548    /// Return the top of the exception handler stack, or None.
549    pub fn resolve_throw_handler(&self) -> Option<BlockId> {
550        self.exception_handler_stack.last().copied()
551    }
552
553    /// Push a Loop scope, run the closure, pop and verify.
554    pub fn loop_scope<T>(
555        &mut self,
556        label: Option<String>,
557        continue_block: BlockId,
558        break_block: BlockId,
559        f: impl FnOnce(&mut Self) -> Result<T, CompilerDiagnostic>,
560    ) -> Result<T, CompilerDiagnostic> {
561        self.scopes.push(Scope::Loop {
562            label: label.clone(),
563            continue_block,
564            break_block,
565        });
566        let value = f(self)?;
567        let last = self
568            .scopes
569            .pop()
570            .expect("Mismatched loop scope: stack empty");
571        match &last {
572            Scope::Loop {
573                label: l,
574                continue_block: c,
575                break_block: b,
576            } => {
577                assert!(
578                    *l == label && *c == continue_block && *b == break_block,
579                    "Mismatched loop scope"
580                );
581            }
582            _ => {
583                return Err(CompilerDiagnostic::new(
584                    ErrorCategory::Invariant,
585                    "Mismatched loop scope: expected Loop, got other",
586                    None,
587                ));
588            }
589        }
590        Ok(value)
591    }
592
593    /// Push a Label scope, run the closure, pop and verify.
594    pub fn label_scope<T>(
595        &mut self,
596        label: String,
597        break_block: BlockId,
598        f: impl FnOnce(&mut Self) -> Result<T, CompilerDiagnostic>,
599    ) -> Result<T, CompilerDiagnostic> {
600        self.scopes.push(Scope::Label {
601            label: label.clone(),
602            break_block,
603        });
604        let value = f(self)?;
605        let last = self
606            .scopes
607            .pop()
608            .expect("Mismatched label scope: stack empty");
609        match &last {
610            Scope::Label {
611                label: l,
612                break_block: b,
613            } => {
614                assert!(*l == label && *b == break_block, "Mismatched label scope");
615            }
616            _ => {
617                return Err(CompilerDiagnostic::new(
618                    ErrorCategory::Invariant,
619                    "Mismatched label scope: expected Label, got other",
620                    None,
621                ));
622            }
623        }
624        Ok(value)
625    }
626
627    /// Push a Switch scope, run the closure, pop and verify.
628    pub fn switch_scope<T>(
629        &mut self,
630        label: Option<String>,
631        break_block: BlockId,
632        f: impl FnOnce(&mut Self) -> Result<T, CompilerDiagnostic>,
633    ) -> Result<T, CompilerDiagnostic> {
634        self.scopes.push(Scope::Switch {
635            label: label.clone(),
636            break_block,
637        });
638        let value = f(self)?;
639        let last = self
640            .scopes
641            .pop()
642            .expect("Mismatched switch scope: stack empty");
643        match &last {
644            Scope::Switch {
645                label: l,
646                break_block: b,
647            } => {
648                assert!(*l == label && *b == break_block, "Mismatched switch scope");
649            }
650            _ => {
651                return Err(CompilerDiagnostic::new(
652                    ErrorCategory::Invariant,
653                    "Mismatched switch scope: expected Switch, got other",
654                    None,
655                ));
656            }
657        }
658        Ok(value)
659    }
660
661    /// Look up the break target for the given label (or the innermost
662    /// loop/switch if label is None).
663    pub fn lookup_break(&self, label: Option<&str>) -> Result<BlockId, CompilerDiagnostic> {
664        for scope in self.scopes.iter().rev() {
665            match scope {
666                Scope::Loop { .. } | Scope::Switch { .. } if label.is_none() => {
667                    return Ok(scope.break_block());
668                }
669                _ if label.is_some() && scope.label() == label => {
670                    return Ok(scope.break_block());
671                }
672                _ => continue,
673            }
674        }
675        Err(CompilerDiagnostic::new(
676            ErrorCategory::Invariant,
677            "Expected a loop or switch to be in scope for break",
678            None,
679        ))
680    }
681
682    /// Look up the continue target for the given label (or the innermost
683    /// loop if label is None). Only loops support continue.
684    pub fn lookup_continue(&self, label: Option<&str>) -> Result<BlockId, CompilerDiagnostic> {
685        for scope in self.scopes.iter().rev() {
686            match scope {
687                Scope::Loop {
688                    label: scope_label,
689                    continue_block,
690                    ..
691                } => {
692                    if label.is_none() || label == scope_label.as_deref() {
693                        return Ok(*continue_block);
694                    }
695                }
696                _ => {
697                    if label.is_some() && scope.label() == label {
698                        return Err(CompilerDiagnostic::new(
699                            ErrorCategory::Invariant,
700                            "Continue may only refer to a labeled loop",
701                            None,
702                        ));
703                    }
704                }
705            }
706        }
707        Err(CompilerDiagnostic::new(
708            ErrorCategory::Invariant,
709            "Expected a loop to be in scope for continue",
710            None,
711        ))
712    }
713
714    /// Create a temporary identifier with a fresh id, returning its IdentifierId.
715    pub fn make_temporary(&mut self, loc: Option<SourceLocation>) -> IdentifierId {
716        let id = self.env.next_identifier_id();
717        // Update the loc on the allocated identifier
718        self.env.identifiers[id.0 as usize].loc = loc;
719        id
720    }
721
722    /// Set the source location for an identifier.
723    pub fn set_identifier_loc(&mut self, id: IdentifierId, loc: Option<SourceLocation>) {
724        self.env.identifiers[id.0 as usize].loc = loc;
725    }
726
727    /// Record an error on the environment.
728    /// Returns `Err` for Invariant errors (matching TS throw behavior).
729    pub fn record_error(&mut self, error: CompilerErrorDetail) -> Result<(), CompilerError> {
730        self.env.record_error(error)
731    }
732
733    /// Record a diagnostic on the environment.
734    pub fn record_diagnostic(&mut self, diagnostic: CompilerDiagnostic) {
735        self.env.record_diagnostic(diagnostic);
736    }
737
738    /// Check if a name has a local binding (non-module-level).
739    /// This is used for checking if fbt/fbs JSX tags are local bindings
740    /// (which is not supported).
741    pub fn has_local_binding(&self, name: &str) -> bool {
742        if let Some(binding) = self
743            .scope_info
744            .find_binding_in_descendants(name, self.component_scope)
745        {
746            return binding.scope != self.scope_info.program_scope;
747        }
748        false
749    }
750
751    /// Return the kind of the current block.
752    pub fn current_block_kind(&self) -> BlockKind {
753        self.current.kind
754    }
755
756    /// Construct the final HIR and instruction table from the completed blocks.
757    ///
758    /// Performs these post-build passes:
759    /// 1. Reverse-postorder sort + unreachable block removal
760    /// 2. Check for unreachable blocks containing FunctionExpression instructions
761    /// 3. Remove unreachable for-loop updates
762    /// 4. Remove dead do-while statements
763    /// 5. Remove unnecessary try-catch
764    /// 6. Number all instructions and terminals
765    /// 7. Mark predecessor blocks
766    pub fn build(
767        mut self,
768    ) -> Result<
769        (
770            HIR,
771            Vec<Instruction>,
772            IndexMap<String, BindingId>,
773            IndexMap<BindingId, IdentifierId>,
774        ),
775        CompilerError,
776    > {
777        let mut hir = HIR {
778            blocks: std::mem::take(&mut self.completed),
779            entry: self.entry,
780        };
781
782        let mut instructions = std::mem::take(&mut self.instruction_table);
783
784        let rpo_blocks = get_reverse_postordered_blocks(&hir, &instructions);
785
786        // Check for unreachable blocks that contain FunctionExpression instructions.
787        // These could contain hoisted declarations that we can't safely remove.
788        for (id, block) in &hir.blocks {
789            if !rpo_blocks.contains_key(id) {
790                let has_function_expr = block.instructions.iter().any(|&instr_id| {
791                    matches!(
792                        instructions[instr_id.0 as usize].value,
793                        InstructionValue::FunctionExpression { .. }
794                    )
795                });
796                if has_function_expr {
797                    let loc = block
798                        .instructions
799                        .first()
800                        .and_then(|&i| instructions[i.0 as usize].loc.clone())
801                        .or_else(|| block.terminal.loc().copied());
802                    self.env.record_error(CompilerErrorDetail {
803                        category: ErrorCategory::Todo,
804                        reason: "Support functions with unreachable code that may contain hoisted declarations".to_string(),
805                        description: None,
806                        loc,
807                        suggestions: None,
808                    })?;
809                }
810            }
811        }
812
813        hir.blocks = rpo_blocks;
814
815        remove_unreachable_for_updates(&mut hir);
816        remove_dead_do_while_statements(&mut hir);
817        remove_unnecessary_try_catch(&mut hir);
818        mark_instruction_ids(&mut hir, &mut instructions);
819        mark_predecessors(&mut hir);
820
821        let used_names = self.used_names;
822        let bindings = self.bindings;
823        Ok((hir, instructions, used_names, bindings))
824    }
825
826    // -----------------------------------------------------------------------
827    // M3: Binding resolution methods
828    // -----------------------------------------------------------------------
829
830    /// Map a BindingId to an HIR IdentifierId.
831    ///
832    /// On first encounter, creates a new Identifier with the given name and a fresh id.
833    /// On subsequent encounters, returns the cached IdentifierId.
834    /// Handles name collisions by appending `_0`, `_1`, etc.
835    ///
836    /// Records errors for variables named 'fbt' or 'this'.
837    pub fn resolve_binding(
838        &mut self,
839        name: &str,
840        binding_id: BindingId,
841    ) -> Result<IdentifierId, CompilerError> {
842        self.resolve_binding_with_loc(name, binding_id, None)
843    }
844
845    /// Map a BindingId to an HIR IdentifierId, with an optional source location.
846    pub fn resolve_binding_with_loc(
847        &mut self,
848        name: &str,
849        binding_id: BindingId,
850        loc: Option<SourceLocation>,
851    ) -> Result<IdentifierId, CompilerError> {
852        // Check for unsupported names BEFORE the cache check.
853        // In TS, resolveBinding records fbt errors when node.name === 'fbt'. After a name collision
854        // causes a rename (e.g., "fbt" -> "fbt_0"), TS's scope.rename changes the AST node's name,
855        // preventing subsequent fbt error recording. We simulate this by checking whether the
856        // resolved name for this binding is still "fbt" (not renamed to "fbt_0" etc.).
857        if name == "fbt" {
858            // Check if this binding was previously resolved to a renamed version
859            let should_record_fbt_error =
860                if let Some(&identifier_id) = self.bindings.get(&binding_id) {
861                    // Already resolved - check if the resolved name is still "fbt"
862                    match &self.env.identifiers[identifier_id.0 as usize].name {
863                        Some(IdentifierName::Named(resolved_name)) => resolved_name == "fbt",
864                        _ => false,
865                    }
866                } else {
867                    // First resolution - always record
868                    true
869                };
870            if should_record_fbt_error {
871                let error_loc = self.scope_info.bindings[binding_id.0 as usize]
872                    .declaration_node_id
873                    .and_then(|nid| self.get_identifier_loc(nid))
874                    .or_else(|| loc.clone());
875                self.env.record_error(CompilerErrorDetail {
876                    category: ErrorCategory::Todo,
877                    reason: "Support local variables named `fbt`".to_string(),
878                    description: Some(
879                        "Local variables named `fbt` may conflict with the fbt plugin and are not yet supported".to_string(),
880                    ),
881                    loc: error_loc,
882                    suggestions: None,
883                })?;
884            }
885        }
886
887        // If we've already resolved this binding, return the cached IdentifierId
888        if let Some(&identifier_id) = self.bindings.get(&binding_id) {
889            return Ok(identifier_id);
890        }
891
892        if is_always_reserved_word(name) {
893            // Match TS behavior: makeIdentifierName throws for reserved words.
894            return Err(CompilerError::from(reserved_identifier_diagnostic(name)));
895        }
896
897        // Find a unique name: start with the original name, then try name_0, name_1, ...
898        let mut candidate = name.to_string();
899        let mut index = 0u32;
900        loop {
901            if let Some(&existing_binding_id) = self.used_names.get(&candidate) {
902                if existing_binding_id == binding_id {
903                    // Same binding, use this name
904                    break;
905                }
906                // Name collision with a different binding, try the next suffix
907                candidate = format!("{}_{}", name, index);
908                index += 1;
909            } else {
910                // Name is available
911                break;
912            }
913        }
914
915        // Record rename if the candidate differs from the original name
916        if candidate != name {
917            let binding = &self.scope_info.bindings[binding_id.0 as usize];
918            if let Some(decl_start) = binding.declaration_start {
919                self.env
920                    .renames
921                    .push(react_compiler_hir::environment::BindingRename {
922                        original: name.to_string(),
923                        renamed: candidate.clone(),
924                        declaration_start: decl_start,
925                    });
926            }
927        }
928
929        // Allocate identifier in the arena
930        let id = self.env.next_identifier_id();
931        // Update the name and loc on the allocated identifier
932        self.env.identifiers[id.0 as usize].name = Some(IdentifierName::Named(candidate.clone()));
933        // Prefer the binding's declaration loc over the reference loc.
934        // This matches TS behavior where Babel's resolveBinding returns the
935        // binding identifier's original loc (the declaration site).
936        let binding = &self.scope_info.bindings[binding_id.0 as usize];
937        let decl_loc = binding
938            .declaration_node_id
939            .and_then(|nid| self.get_identifier_loc(nid));
940        if let Some(ref dl) = decl_loc {
941            self.env.identifiers[id.0 as usize].loc = Some(dl.clone());
942        } else if let Some(ref loc) = loc {
943            self.env.identifiers[id.0 as usize].loc = Some(loc.clone());
944        }
945
946        self.used_names.insert(candidate, binding_id);
947        self.bindings.insert(binding_id, id);
948        Ok(id)
949    }
950
951    /// Set the loc on an identifier to the declaration-site loc.
952    /// This overrides any previously-set loc (which may have come from a reference site).
953    pub fn set_identifier_declaration_loc(
954        &mut self,
955        id: IdentifierId,
956        loc: &Option<SourceLocation>,
957    ) {
958        if let Some(loc_val) = loc {
959            self.env.identifiers[id.0 as usize].loc = Some(loc_val.clone());
960        }
961    }
962
963    /// Resolve an identifier reference to a VariableBinding.
964    ///
965    /// Uses ScopeInfo to determine whether the reference is:
966    /// - Global (no binding found)
967    /// - ImportDefault, ImportSpecifier, ImportNamespace (program-scope import binding)
968    /// - ModuleLocal (program-scope non-import binding)
969    /// - Identifier (local binding, resolved via resolve_binding)
970    pub fn resolve_identifier(
971        &mut self,
972        name: &str,
973        _start_offset: u32,
974        loc: Option<SourceLocation>,
975        node_id: Option<u32>,
976    ) -> Result<VariableBinding, CompilerError> {
977        let binding_data = self.scope_info.resolve_reference_for_node(node_id);
978
979        match binding_data {
980            None => {
981                // No binding found: this is a global
982                Ok(VariableBinding::Global {
983                    name: name.to_string(),
984                })
985            }
986            Some(binding) => {
987                // Treat type-only declarations as globals so the compiler
988                // doesn't try to create/initialize HIR bindings for them.
989                // TSEnumDeclaration is included because enums inside function
990                // bodies are lowered as UnsupportedNode and their binding
991                // is never initialized in HIR.
992                if matches!(
993                    binding.declaration_type.as_str(),
994                    "TSTypeAliasDeclaration"
995                        | "TSInterfaceDeclaration"
996                        | "TSEnumDeclaration"
997                        | "TSModuleDeclaration"
998                ) {
999                    return Ok(VariableBinding::Global {
1000                        name: name.to_string(),
1001                    });
1002                }
1003                if binding.scope == self.scope_info.program_scope {
1004                    // Module-level binding: check import info
1005                    Ok(match &binding.import {
1006                        Some(import_info) => match import_info.kind {
1007                            ImportBindingKind::Default => VariableBinding::ImportDefault {
1008                                name: name.to_string(),
1009                                module: import_info.source.clone(),
1010                            },
1011                            ImportBindingKind::Named => VariableBinding::ImportSpecifier {
1012                                name: name.to_string(),
1013                                module: import_info.source.clone(),
1014                                imported: import_info
1015                                    .imported
1016                                    .clone()
1017                                    .unwrap_or_else(|| name.to_string()),
1018                            },
1019                            ImportBindingKind::Namespace => VariableBinding::ImportNamespace {
1020                                name: name.to_string(),
1021                                module: import_info.source.clone(),
1022                            },
1023                        },
1024                        None => VariableBinding::ModuleLocal {
1025                            name: name.to_string(),
1026                        },
1027                    })
1028                } else if !self.is_scope_within_compiled_function(binding.scope) {
1029                    Ok(VariableBinding::ModuleLocal {
1030                        name: name.to_string(),
1031                    })
1032                } else {
1033                    let binding_id = binding.id;
1034                    let binding_kind = crate::convert_binding_kind(&binding.kind);
1035                    let identifier_id = self.resolve_binding_with_loc(name, binding_id, loc)?;
1036                    Ok(VariableBinding::Identifier {
1037                        identifier: identifier_id,
1038                        binding_kind,
1039                    })
1040                }
1041            }
1042        }
1043    }
1044
1045    /// Check if an identifier reference resolves to a context identifier.
1046    ///
1047    /// A context identifier is a variable declared in an ancestor scope of the
1048    /// current function's scope, but NOT in the program scope itself and NOT
1049    /// in the function's own scope. These are "captured" variables from an
1050    /// enclosing function.
1051    pub fn is_context_identifier(
1052        &self,
1053        _name: &str,
1054        _start_offset: u32,
1055        node_id: Option<u32>,
1056    ) -> bool {
1057        let binding = self.scope_info.resolve_reference_for_node(node_id);
1058
1059        match binding {
1060            None => false,
1061            Some(binding_data) => {
1062                if binding_data.scope == self.scope_info.program_scope {
1063                    return false;
1064                }
1065                self.context_identifiers.contains(&binding_data.id)
1066            }
1067        }
1068    }
1069
1070    /// Like `is_context_identifier`, for callers that already resolved a
1071    /// BindingId instead of going through a reference node.
1072    pub fn is_context_binding(&self, binding_id: BindingId) -> bool {
1073        let binding = &self.scope_info.bindings[binding_id.0 as usize];
1074        if binding.scope == self.scope_info.program_scope {
1075            return false;
1076        }
1077        self.context_identifiers.contains(&binding_id)
1078    }
1079
1080    /// Resolve the binding for a function declaration's id the way TS does:
1081    /// Babel's `path.scope.getBinding(name)` starts at the function's OWN
1082    /// scope, so a body-level local (or parameter) that shadows the function's
1083    /// name resolves to that inner binding rather than to the function's
1084    /// hoisted binding in the parent scope.
1085    ///
1086    /// Babel's `scope.rename` re-keys a scope's bindings when the TS builder
1087    /// renames a shadowed binding (e.g. `init` -> `init_0`), so a binding only
1088    /// matches if its *current* name — the resolved HIR identifier name once
1089    /// resolved — still equals `name`. A binding renamed *to* `name` overwrites
1090    /// the original key in Babel and takes precedence over an unresolved
1091    /// binding with that original name.
1092    ///
1093    /// Returns None when the walk resolves outside the compiled function
1094    /// (degraded scope info); callers should fall back to node-based
1095    /// resolution in that case.
1096    pub fn get_function_declaration_binding(
1097        &self,
1098        function_scope: ScopeId,
1099        name: &str,
1100    ) -> Option<BindingId> {
1101        // None = unresolved binding; Some(matches) = resolved, current name comparison
1102        let resolved_name_matches = |bid: BindingId| -> Option<bool> {
1103            let &identifier_id = self.bindings.get(&bid)?;
1104            match &self.env.identifiers[identifier_id.0 as usize].name {
1105                Some(IdentifierName::Named(n)) => Some(n == name),
1106                _ => Some(false),
1107            }
1108        };
1109        let mut current = Some(function_scope);
1110        while let Some(id) = current {
1111            let scope = &self.scope_info.scopes[id.0 as usize];
1112            let mut found = scope
1113                .bindings
1114                .values()
1115                .copied()
1116                .find(|&bid| resolved_name_matches(bid) == Some(true));
1117            if found.is_none() {
1118                if let Some(&bid) = scope.bindings.get(name) {
1119                    // Skip bindings that were renamed away from `name`.
1120                    if resolved_name_matches(bid) != Some(false) {
1121                        found = Some(bid);
1122                    }
1123                }
1124            }
1125            if let Some(bid) = found {
1126                let binding_scope = self.scope_info.bindings[bid.0 as usize].scope;
1127                if !self.is_scope_within_compiled_function(binding_scope) {
1128                    return None;
1129                }
1130                return Some(bid);
1131            }
1132            current = scope.parent;
1133        }
1134        None
1135    }
1136}
1137
1138// ---------------------------------------------------------------------------
1139// Post-build helper functions
1140// ---------------------------------------------------------------------------
1141
1142/// Compute a reverse-postorder of blocks reachable from the entry.
1143///
1144/// Visits successors in reverse order so that when the postorder list is
1145/// reversed, sibling edges appear in program order.
1146///
1147/// Blocks not reachable through successors are removed. Blocks that are
1148/// only reachable as fallthroughs (not through real successor edges) are
1149/// replaced with empty blocks that have an Unreachable terminal.
1150pub fn get_reverse_postordered_blocks(
1151    hir: &HIR,
1152    _instructions: &[Instruction],
1153) -> IndexMap<BlockId, BasicBlock> {
1154    let mut visited: IndexSet<BlockId> = IndexSet::new();
1155    let mut used: IndexSet<BlockId> = IndexSet::new();
1156    let mut used_fallthroughs: IndexSet<BlockId> = IndexSet::new();
1157    let mut postorder: Vec<BlockId> = Vec::new();
1158
1159    fn visit(
1160        hir: &HIR,
1161        block_id: BlockId,
1162        is_used: bool,
1163        visited: &mut IndexSet<BlockId>,
1164        used: &mut IndexSet<BlockId>,
1165        used_fallthroughs: &mut IndexSet<BlockId>,
1166        postorder: &mut Vec<BlockId>,
1167    ) {
1168        let was_used = used.contains(&block_id);
1169        let was_visited = visited.contains(&block_id);
1170        visited.insert(block_id);
1171        if is_used {
1172            used.insert(block_id);
1173        }
1174        if was_visited && (was_used || !is_used) {
1175            return;
1176        }
1177
1178        let block = hir
1179            .blocks
1180            .get(&block_id)
1181            .unwrap_or_else(|| panic!("[HIRBuilder] expected block {:?} to exist", block_id));
1182
1183        // Visit successors in reverse order so that when we reverse the
1184        // postorder list, sibling edges come out in program order.
1185        let mut successors = each_terminal_successor(&block.terminal);
1186        successors.reverse();
1187
1188        let fallthrough = terminal_fallthrough(&block.terminal);
1189
1190        // Visit fallthrough first (marking as not-yet-used) to ensure its
1191        // block ID is emitted in the correct position.
1192        if let Some(ft) = fallthrough {
1193            if is_used {
1194                used_fallthroughs.insert(ft);
1195            }
1196            visit(hir, ft, false, visited, used, used_fallthroughs, postorder);
1197        }
1198        for successor in successors {
1199            visit(
1200                hir,
1201                successor,
1202                is_used,
1203                visited,
1204                used,
1205                used_fallthroughs,
1206                postorder,
1207            );
1208        }
1209
1210        if !was_visited {
1211            postorder.push(block_id);
1212        }
1213    }
1214
1215    visit(
1216        hir,
1217        hir.entry,
1218        true,
1219        &mut visited,
1220        &mut used,
1221        &mut used_fallthroughs,
1222        &mut postorder,
1223    );
1224
1225    let mut blocks = IndexMap::new();
1226    for block_id in postorder.into_iter().rev() {
1227        let block = hir.blocks.get(&block_id).unwrap();
1228        if used.contains(&block_id) {
1229            blocks.insert(block_id, block.clone());
1230        } else if used_fallthroughs.contains(&block_id) {
1231            blocks.insert(
1232                block_id,
1233                BasicBlock {
1234                    kind: block.kind,
1235                    id: block_id,
1236                    instructions: Vec::new(),
1237                    terminal: Terminal::Unreachable {
1238                        id: block.terminal.evaluation_order(),
1239                        loc: block.terminal.loc().copied(),
1240                    },
1241                    preds: block.preds.clone(),
1242                    phis: Vec::new(),
1243                },
1244            );
1245        }
1246        // otherwise this block is unreachable and is dropped
1247    }
1248
1249    blocks
1250}
1251
1252/// For each block with a `For` terminal whose update block is not in the
1253/// blocks map, set update to None.
1254pub fn remove_unreachable_for_updates(hir: &mut HIR) {
1255    let block_ids: IndexSet<BlockId> = hir.blocks.keys().copied().collect();
1256    for block in hir.blocks.values_mut() {
1257        if let Terminal::For { update, .. } = &mut block.terminal {
1258            if let Some(update_id) = *update {
1259                if !block_ids.contains(&update_id) {
1260                    *update = None;
1261                }
1262            }
1263        }
1264    }
1265}
1266
1267/// For each block with a `DoWhile` terminal whose test block is not in
1268/// the blocks map, replace the terminal with a Goto to the loop block.
1269pub fn remove_dead_do_while_statements(hir: &mut HIR) {
1270    let block_ids: IndexSet<BlockId> = hir.blocks.keys().copied().collect();
1271    for block in hir.blocks.values_mut() {
1272        let should_replace = if let Terminal::DoWhile { test, .. } = &block.terminal {
1273            !block_ids.contains(test)
1274        } else {
1275            false
1276        };
1277        if should_replace {
1278            if let Terminal::DoWhile {
1279                loop_block,
1280                id,
1281                loc,
1282                ..
1283            } = std::mem::replace(
1284                &mut block.terminal,
1285                Terminal::Unreachable {
1286                    id: EvaluationOrder(0),
1287                    loc: None,
1288                },
1289            ) {
1290                block.terminal = Terminal::Goto {
1291                    block: loop_block,
1292                    variant: GotoVariant::Break,
1293                    id,
1294                    loc,
1295                };
1296            }
1297        }
1298    }
1299}
1300
1301/// For each block with a `Try` terminal whose handler block is not in
1302/// the blocks map, replace the terminal with a Goto to the try block.
1303///
1304/// Also cleans up the fallthrough block's predecessors if the handler
1305/// was the only path to it.
1306pub fn remove_unnecessary_try_catch(hir: &mut HIR) {
1307    let block_ids: IndexSet<BlockId> = hir.blocks.keys().copied().collect();
1308
1309    // Collect the blocks that need replacement and their associated data
1310    let replacements: Vec<(BlockId, BlockId, BlockId, BlockId, Option<SourceLocation>)> = hir
1311        .blocks
1312        .iter()
1313        .filter_map(|(&block_id, block)| {
1314            if let Terminal::Try {
1315                block: try_block,
1316                handler,
1317                fallthrough,
1318                loc,
1319                ..
1320            } = &block.terminal
1321            {
1322                if !block_ids.contains(handler) {
1323                    return Some((block_id, *try_block, *handler, *fallthrough, loc.clone()));
1324                }
1325            }
1326            None
1327        })
1328        .collect();
1329
1330    for (block_id, try_block, handler_id, fallthrough_id, loc) in replacements {
1331        // Replace the terminal
1332        if let Some(block) = hir.blocks.get_mut(&block_id) {
1333            block.terminal = Terminal::Goto {
1334                block: try_block,
1335                id: EvaluationOrder(0),
1336                loc,
1337                variant: GotoVariant::Break,
1338            };
1339        }
1340
1341        // Clean up fallthrough predecessor info
1342        if let Some(fallthrough) = hir.blocks.get_mut(&fallthrough_id) {
1343            if fallthrough.preds.len() == 1 && fallthrough.preds.contains(&handler_id) {
1344                // The handler was the only predecessor: remove the fallthrough block
1345                hir.blocks.shift_remove(&fallthrough_id);
1346            } else {
1347                fallthrough.preds.shift_remove(&handler_id);
1348            }
1349        }
1350    }
1351}
1352
1353/// Sequentially number all instructions and terminals starting from 1.
1354pub fn mark_instruction_ids(hir: &mut HIR, instructions: &mut [Instruction]) {
1355    let mut order: u32 = 0;
1356    for block in hir.blocks.values_mut() {
1357        for &instr_id in &block.instructions {
1358            order += 1;
1359            instructions[instr_id.0 as usize].id = EvaluationOrder(order);
1360        }
1361        order += 1;
1362        block.terminal.set_evaluation_order(EvaluationOrder(order));
1363    }
1364}
1365
1366/// DFS from entry, for each successor add the predecessor's id to
1367/// the successor's preds set.
1368///
1369/// Note: This only visits direct successors (via `each_terminal_successor`),
1370/// not fallthrough blocks. Fallthrough blocks are reached indirectly via
1371/// Goto terminals from within branching blocks, matching the TypeScript
1372/// `markPredecessors` behavior.
1373pub fn mark_predecessors(hir: &mut HIR) {
1374    // Clear all preds first
1375    for block in hir.blocks.values_mut() {
1376        block.preds.clear();
1377    }
1378
1379    let mut visited: IndexSet<BlockId> = IndexSet::new();
1380
1381    fn visit(
1382        hir: &mut HIR,
1383        block_id: BlockId,
1384        prev_block_id: Option<BlockId>,
1385        visited: &mut IndexSet<BlockId>,
1386    ) {
1387        // Add predecessor
1388        if let Some(prev_id) = prev_block_id {
1389            if let Some(block) = hir.blocks.get_mut(&block_id) {
1390                block.preds.insert(prev_id);
1391            } else {
1392                return;
1393            }
1394        }
1395
1396        if visited.contains(&block_id) {
1397            return;
1398        }
1399        visited.insert(block_id);
1400
1401        // Get successors before mutating
1402        let successors = if let Some(block) = hir.blocks.get(&block_id) {
1403            each_terminal_successor(&block.terminal)
1404        } else {
1405            return;
1406        };
1407
1408        for successor in successors {
1409            visit(hir, successor, Some(block_id), visited);
1410        }
1411    }
1412
1413    visit(hir, hir.entry, None, &mut visited);
1414}
1415
1416// ---------------------------------------------------------------------------
1417// Public helper functions
1418// ---------------------------------------------------------------------------
1419
1420/// Create a temporary Place with a fresh identifier allocated in the arena.
1421pub fn create_temporary_place(env: &mut Environment, loc: Option<SourceLocation>) -> Place {
1422    let id = env.next_identifier_id();
1423    // Update the loc on the allocated identifier
1424    env.identifiers[id.0 as usize].loc = loc;
1425    Place {
1426        identifier: id,
1427        reactive: false,
1428        effect: Effect::Unknown,
1429        loc: None,
1430    }
1431}