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//! Converts the HIR CFG into a tree-structured ReactiveFunction.7//!8//! Corresponds to `src/ReactiveScopes/BuildReactiveFunction.ts`.910use rustc_hash::FxHashSet;1112use react_compiler_diagnostics::{13 CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation,14};15use react_compiler_hir::environment::Environment;16use react_compiler_hir::{17 BasicBlock, BlockId, EvaluationOrder, GotoVariant, HirFunction, InstructionValue, Place,18 PrunedReactiveScopeBlock, ReactiveBlock, ReactiveFunction, ReactiveInstruction, ReactiveLabel,19 ReactiveScopeBlock, ReactiveStatement, ReactiveSwitchCase, ReactiveTerminal,20 ReactiveTerminalStatement, ReactiveTerminalTargetKind, ReactiveValue, Terminal,21};2223/// Convert the HIR CFG into a tree-structured ReactiveFunction.24pub fn build_reactive_function(25 hir: &HirFunction,26 env: &Environment,27) -> Result<ReactiveFunction, CompilerDiagnostic> {28 let mut ctx = Context::new(hir);29 let mut driver = Driver {30 cx: &mut ctx,31 hir,32 env,33 };3435 let entry_block_id = hir.body.entry;36 let mut body = Vec::new();37 driver.visit_block(entry_block_id, &mut body)?;3839 Ok(ReactiveFunction {40 loc: hir.loc,41 id: hir.id.clone(),42 name_hint: hir.name_hint.clone(),43 params: hir.params.clone(),44 generator: hir.generator,45 is_async: hir.is_async,46 body,47 directives: hir.directives.clone(),48 })49}5051// =============================================================================52// ControlFlowTarget53// =============================================================================5455#[derive(Debug)]56enum ControlFlowTarget {57 If {58 block: BlockId,59 id: u32,60 },61 Switch {62 block: BlockId,63 id: u32,64 },65 Case {66 block: BlockId,67 id: u32,68 },69 Loop {70 block: BlockId,71 #[allow(dead_code)]72 owns_block: bool,73 continue_block: BlockId,74 loop_block: Option<BlockId>,75 owns_loop: bool,76 id: u32,77 },78}7980impl ControlFlowTarget {81 fn block(&self) -> BlockId {82 match self {83 ControlFlowTarget::If { block, .. }84 | ControlFlowTarget::Switch { block, .. }85 | ControlFlowTarget::Case { block, .. }86 | ControlFlowTarget::Loop { block, .. } => *block,87 }88 }8990 fn id(&self) -> u32 {91 match self {92 ControlFlowTarget::If { id, .. }93 | ControlFlowTarget::Switch { id, .. }94 | ControlFlowTarget::Case { id, .. }95 | ControlFlowTarget::Loop { id, .. } => *id,96 }97 }9899 fn is_loop(&self) -> bool {100 matches!(self, ControlFlowTarget::Loop { .. })101 }102}103104// =============================================================================105// Context106// =============================================================================107108struct Context<'a> {109 ir: &'a HirFunction,110 next_schedule_id: u32,111 emitted: FxHashSet<BlockId>,112 scope_fallthroughs: FxHashSet<BlockId>,113 scheduled: FxHashSet<BlockId>,114 catch_handlers: FxHashSet<BlockId>,115 control_flow_stack: Vec<ControlFlowTarget>,116}117118impl<'a> Context<'a> {119 fn new(ir: &'a HirFunction) -> Self {120 Self {121 ir,122 next_schedule_id: 0,123 emitted: FxHashSet::default(),124 scope_fallthroughs: FxHashSet::default(),125 scheduled: FxHashSet::default(),126 catch_handlers: FxHashSet::default(),127 control_flow_stack: Vec::new(),128 }129 }130131 fn block(&self, id: BlockId) -> &BasicBlock {132 &self.ir.body.blocks[&id]133 }134135 fn schedule_catch_handler(&mut self, block: BlockId) {136 self.catch_handlers.insert(block);137 }138139 fn reachable(&self, id: BlockId) -> bool {140 let block = self.block(id);141 !matches!(block.terminal, Terminal::Unreachable { .. })142 }143144 fn schedule(&mut self, block: BlockId, target_type: &str) -> Result<u32, CompilerDiagnostic> {145 let id = self.next_schedule_id;146 self.next_schedule_id += 1;147 if self.scheduled.contains(&block) {148 return Err(CompilerDiagnostic::new(149 ErrorCategory::Invariant,150 format!("Break block is already scheduled: bb{}", block.0),151 None,152 ));153 }154 self.scheduled.insert(block);155 let target = match target_type {156 "if" => ControlFlowTarget::If { block, id },157 "switch" => ControlFlowTarget::Switch { block, id },158 "case" => ControlFlowTarget::Case { block, id },159 _ => {160 return Err(CompilerDiagnostic::new(161 ErrorCategory::Invariant,162 format!("Unknown target type: {}", target_type),163 None,164 ));165 }166 };167 self.control_flow_stack.push(target);168 Ok(id)169 }170171 fn schedule_loop(172 &mut self,173 fallthrough_block: BlockId,174 continue_block: BlockId,175 loop_block: Option<BlockId>,176 ) -> Result<u32, CompilerDiagnostic> {177 let id = self.next_schedule_id;178 self.next_schedule_id += 1;179 let owns_block = !self.scheduled.contains(&fallthrough_block);180 self.scheduled.insert(fallthrough_block);181 if self.scheduled.contains(&continue_block) {182 return Err(CompilerDiagnostic::new(183 ErrorCategory::Invariant,184 format!(185 "Continue block is already scheduled: bb{}",186 continue_block.0187 ),188 None,189 ));190 }191 self.scheduled.insert(continue_block);192 let mut owns_loop = false;193 if let Some(lb) = loop_block {194 owns_loop = !self.scheduled.contains(&lb);195 self.scheduled.insert(lb);196 }197198 self.control_flow_stack.push(ControlFlowTarget::Loop {199 block: fallthrough_block,200 owns_block,201 continue_block,202 loop_block,203 owns_loop,204 id,205 });206 Ok(id)207 }208209 fn unschedule(&mut self, schedule_id: u32) -> Result<(), CompilerDiagnostic> {210 let last = self211 .control_flow_stack212 .pop()213 .expect("Can only unschedule the last target");214 if last.id() != schedule_id {215 return Err(CompilerDiagnostic::new(216 ErrorCategory::Invariant,217 "Can only unschedule the last target".to_string(),218 None,219 ));220 }221 match &last {222 ControlFlowTarget::Loop {223 block,224 continue_block,225 loop_block,226 owns_loop,227 ..228 } => {229 // TS: always removes block from scheduled for loops230 // (ownsBlock is boolean, so `!== null` is always true)231 self.scheduled.remove(block);232 self.scheduled.remove(continue_block);233 if *owns_loop {234 if let Some(lb) = loop_block {235 self.scheduled.remove(lb);236 }237 }238 }239 _ => {240 self.scheduled.remove(&last.block());241 }242 }243 Ok(())244 }245246 fn unschedule_all(&mut self, schedule_ids: &[u32]) -> Result<(), CompilerDiagnostic> {247 for &id in schedule_ids.iter().rev() {248 self.unschedule(id)?;249 }250 Ok(())251 }252253 fn is_scheduled(&self, block: BlockId) -> bool {254 self.scheduled.contains(&block) || self.catch_handlers.contains(&block)255 }256257 fn get_break_target(258 &self,259 block: BlockId,260 ) -> Result<(BlockId, ReactiveTerminalTargetKind), CompilerDiagnostic> {261 let mut has_preceding_loop = false;262 for i in (0..self.control_flow_stack.len()).rev() {263 let target = &self.control_flow_stack[i];264 if target.block() == block {265 let kind = if target.is_loop() {266 if has_preceding_loop {267 ReactiveTerminalTargetKind::Labeled268 } else {269 ReactiveTerminalTargetKind::Unlabeled270 }271 } else if i == self.control_flow_stack.len() - 1 {272 ReactiveTerminalTargetKind::Implicit273 } else {274 ReactiveTerminalTargetKind::Labeled275 };276 return Ok((target.block(), kind));277 }278 has_preceding_loop = has_preceding_loop || target.is_loop();279 }280 Err(CompilerDiagnostic::new(281 ErrorCategory::Invariant,282 format!("Expected a break target for bb{}", block.0),283 None,284 ))285 }286287 fn get_continue_target(&self, block: BlockId) -> Option<(BlockId, ReactiveTerminalTargetKind)> {288 let mut has_preceding_loop = false;289 for i in (0..self.control_flow_stack.len()).rev() {290 let target = &self.control_flow_stack[i];291 if let ControlFlowTarget::Loop {292 block: fallthrough_block,293 continue_block,294 ..295 } = target296 {297 if *continue_block == block {298 let kind = if has_preceding_loop {299 ReactiveTerminalTargetKind::Labeled300 } else if i == self.control_flow_stack.len() - 1 {301 ReactiveTerminalTargetKind::Implicit302 } else {303 ReactiveTerminalTargetKind::Unlabeled304 };305 return Some((*fallthrough_block, kind));306 }307 }308 has_preceding_loop = has_preceding_loop || target.is_loop();309 }310 None311 }312}313314// =============================================================================315// Driver316// =============================================================================317318struct Driver<'a, 'b> {319 cx: &'b mut Context<'a>,320 hir: &'a HirFunction,321 #[allow(dead_code)]322 env: &'a Environment,323}324325impl<'a, 'b> Driver<'a, 'b> {326 fn traverse_block(&mut self, block_id: BlockId) -> Result<ReactiveBlock, CompilerDiagnostic> {327 let mut block_value = Vec::new();328 self.visit_block(block_id, &mut block_value)?;329 Ok(block_value)330 }331332 fn visit_block(333 &mut self,334 mut block_id: BlockId,335 block_value: &mut ReactiveBlock,336 ) -> Result<(), CompilerDiagnostic> {337 // Use a loop to avoid deep recursion for fallthrough chains.338 // Each terminal that would tail-call visit_block(fallthrough, block_value)339 // instead sets next_block and continues the loop.340 loop {341 // Extract data from block before any mutable operations342 let block = &self.hir.body.blocks[&block_id];343 let block_id_val = block.id;344 let instructions: Vec<_> = block.instructions.clone();345 let terminal = block.terminal.clone();346347 if !self.cx.emitted.insert(block_id_val) {348 return Err(CompilerDiagnostic::new(349 ErrorCategory::Invariant,350 format!("Block bb{} was already emitted", block_id_val.0),351 None,352 ));353 }354355 // Emit instructions356 for instr_id in &instructions {357 let instr = &self.hir.instructions[instr_id.0 as usize];358 block_value.push(ReactiveStatement::Instruction(ReactiveInstruction {359 id: instr.id,360 lvalue: Some(instr.lvalue.clone()),361 value: ReactiveValue::Instruction(instr.value.clone()),362 effects: instr.effects.clone(),363 loc: instr.loc,364 }));365 }366367 // Process terminal368 let mut schedule_ids: Vec<u32> = Vec::new();369 let mut next_block: Option<BlockId> = None;370371 match &terminal {372 Terminal::If {373 test,374 consequent,375 alternate,376 fallthrough,377 id,378 loc,379 } => {380 // TS: reachable(fallthrough) && !isScheduled(fallthrough)381 let fallthrough_id =382 if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) {383 Some(*fallthrough)384 } else {385 None386 };387 // TS: alternate !== fallthrough ? alternate : null388 let alternate_id = if *alternate != *fallthrough {389 Some(*alternate)390 } else {391 None392 };393394 if let Some(ft) = fallthrough_id {395 schedule_ids.push(self.cx.schedule(ft, "if")?);396 }397398 let consequent_block = if self.cx.is_scheduled(*consequent) {399 return Err(CompilerDiagnostic::new(400 ErrorCategory::Invariant,401 format!(402 "Unexpected 'if' where consequent is already scheduled (bb{})",403 consequent.0404 ),405 None,406 ));407 } else {408 self.traverse_block(*consequent)?409 };410411 let alternate_block = if let Some(alt) = alternate_id {412 if self.cx.is_scheduled(alt) {413 return Err(CompilerDiagnostic::new(414 ErrorCategory::Invariant,415 format!(416 "Unexpected 'if' where the alternate is already scheduled (bb{})",417 alt.0418 ),419 None,420 ));421 } else {422 Some(self.traverse_block(alt)?)423 }424 } else {425 None426 };427428 self.cx.unschedule_all(&schedule_ids)?;429 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {430 terminal: ReactiveTerminal::If {431 test: test.clone(),432 consequent: consequent_block,433 alternate: alternate_block,434 id: *id,435 loc: *loc,436 },437 label: fallthrough_id.map(|ft| ReactiveLabel {438 id: ft,439 implicit: false,440 }),441 }));442443 next_block = fallthrough_id;444 }445446 Terminal::Switch {447 test,448 cases,449 fallthrough,450 id,451 loc,452 } => {453 // TS: reachable(fallthrough) && !isScheduled(fallthrough)454 let fallthrough_id =455 if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) {456 Some(*fallthrough)457 } else {458 None459 };460 if let Some(ft) = fallthrough_id {461 schedule_ids.push(self.cx.schedule(ft, "switch")?);462 }463464 // TS processes cases in reverse order, then reverses the result.465 // This ensures that later cases are scheduled when earlier cases466 // are traversed, matching fallthrough semantics.467 let mut reactive_cases = Vec::new();468 for case in cases.iter().rev() {469 let case_block_id = case.block;470471 if self.cx.is_scheduled(case_block_id) {472 // TS: asserts case.block === fallthrough, then skips (return)473 if case_block_id != *fallthrough {474 return Err(CompilerDiagnostic::new(475 ErrorCategory::Invariant,476 "Unexpected 'switch' where a case is already scheduled and block is not the fallthrough".to_string(),477 None,478 ));479 }480 continue;481 }482483 let consequent = self.traverse_block(case_block_id)?;484 let case_schedule_id = self.cx.schedule(case_block_id, "case")?;485 schedule_ids.push(case_schedule_id);486487 reactive_cases.push(ReactiveSwitchCase {488 test: case.test.clone(),489 block: Some(consequent),490 });491 }492 reactive_cases.reverse();493494 self.cx.unschedule_all(&schedule_ids)?;495 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {496 terminal: ReactiveTerminal::Switch {497 test: test.clone(),498 cases: reactive_cases,499 id: *id,500 loc: *loc,501 },502 label: fallthrough_id.map(|ft| ReactiveLabel {503 id: ft,504 implicit: false,505 }),506 }));507508 next_block = fallthrough_id;509 }510511 Terminal::DoWhile {512 loop_block,513 test,514 fallthrough,515 id,516 loc,517 } => {518 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {519 Some(*fallthrough)520 } else {521 None522 };523 let loop_id =524 if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough {525 Some(*loop_block)526 } else {527 None528 };529530 schedule_ids.push(self.cx.schedule_loop(531 *fallthrough,532 *test,533 Some(*loop_block),534 )?);535536 let loop_body = if let Some(lid) = loop_id {537 self.traverse_block(lid)?538 } else {539 return Err(CompilerDiagnostic::new(540 ErrorCategory::Invariant,541 "Unexpected 'do-while' where the loop is already scheduled",542 None,543 ));544 };545 let test_result = self.visit_value_block(*test, *loc, None)?;546547 self.cx.unschedule_all(&schedule_ids)?;548 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {549 terminal: ReactiveTerminal::DoWhile {550 loop_block: loop_body,551 test: test_result.value,552 id: *id,553 loc: *loc,554 },555 label: fallthrough_id.map(|ft| ReactiveLabel {556 id: ft,557 implicit: false,558 }),559 }));560561 next_block = fallthrough_id;562 }563564 Terminal::While {565 test,566 loop_block,567 fallthrough,568 id,569 loc,570 } => {571 // TS: reachable(fallthrough) && !isScheduled(fallthrough)572 let fallthrough_id =573 if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) {574 Some(*fallthrough)575 } else {576 None577 };578 let loop_id =579 if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough {580 Some(*loop_block)581 } else {582 None583 };584585 schedule_ids.push(self.cx.schedule_loop(586 *fallthrough,587 *test,588 Some(*loop_block),589 )?);590591 let test_result = self.visit_value_block(*test, *loc, None)?;592593 let loop_body = if let Some(lid) = loop_id {594 self.traverse_block(lid)?595 } else {596 return Err(CompilerDiagnostic::new(597 ErrorCategory::Invariant,598 "Unexpected 'while' where the loop is already scheduled",599 None,600 ));601 };602603 self.cx.unschedule_all(&schedule_ids)?;604 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {605 terminal: ReactiveTerminal::While {606 test: test_result.value,607 loop_block: loop_body,608 id: *id,609 loc: *loc,610 },611 label: fallthrough_id.map(|ft| ReactiveLabel {612 id: ft,613 implicit: false,614 }),615 }));616617 next_block = fallthrough_id;618 }619620 Terminal::For {621 init,622 test,623 update,624 loop_block,625 fallthrough,626 id,627 loc,628 } => {629 let loop_id =630 if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough {631 Some(*loop_block)632 } else {633 None634 };635636 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {637 Some(*fallthrough)638 } else {639 None640 };641642 // Continue block is update (if present) or test643 let continue_block = update.unwrap_or(*test);644 schedule_ids.push(self.cx.schedule_loop(645 *fallthrough,646 continue_block,647 Some(*loop_block),648 )?);649650 let init_result = self.visit_value_block(*init, *loc, None)?;651 let init_value = self.value_block_result_to_sequence(init_result, *loc);652653 let test_result = self.visit_value_block(*test, *loc, None)?;654655 let update_result = match update {656 Some(u) => Some(self.visit_value_block(*u, *loc, None)?),657 None => None,658 };659660 let loop_body = if let Some(lid) = loop_id {661 self.traverse_block(lid)?662 } else {663 return Err(CompilerDiagnostic::new(664 ErrorCategory::Invariant,665 "Unexpected 'for' where the loop is already scheduled",666 None,667 ));668 };669670 self.cx.unschedule_all(&schedule_ids)?;671 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {672 terminal: ReactiveTerminal::For {673 init: init_value,674 test: test_result.value,675 update: update_result.map(|r| r.value),676 loop_block: loop_body,677 id: *id,678 loc: *loc,679 },680 label: fallthrough_id.map(|ft| ReactiveLabel {681 id: ft,682 implicit: false,683 }),684 }));685686 next_block = fallthrough_id;687 }688689 Terminal::ForOf {690 init,691 test,692 loop_block,693 fallthrough,694 id,695 loc,696 } => {697 let loop_id =698 if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough {699 Some(*loop_block)700 } else {701 None702 };703704 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {705 Some(*fallthrough)706 } else {707 None708 };709710 // TS: scheduleLoop(fallthrough, init, loop)711 schedule_ids.push(self.cx.schedule_loop(712 *fallthrough,713 *init,714 Some(*loop_block),715 )?);716717 let init_result = self.visit_value_block(*init, *loc, None)?;718 let init_value = self.value_block_result_to_sequence(init_result, *loc);719720 let test_result = self.visit_value_block(*test, *loc, None)?;721 let test_value = self.value_block_result_to_sequence(test_result, *loc);722723 let loop_body = if let Some(lid) = loop_id {724 self.traverse_block(lid)?725 } else {726 return Err(CompilerDiagnostic::new(727 ErrorCategory::Invariant,728 "Unexpected 'for-of' where the loop is already scheduled",729 None,730 ));731 };732733 self.cx.unschedule_all(&schedule_ids)?;734 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {735 terminal: ReactiveTerminal::ForOf {736 init: init_value,737 test: test_value,738 loop_block: loop_body,739 id: *id,740 loc: *loc,741 },742 label: fallthrough_id.map(|ft| ReactiveLabel {743 id: ft,744 implicit: false,745 }),746 }));747748 next_block = fallthrough_id;749 }750751 Terminal::ForIn {752 init,753 loop_block,754 fallthrough,755 id,756 loc,757 } => {758 let loop_id =759 if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough {760 Some(*loop_block)761 } else {762 None763 };764765 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {766 Some(*fallthrough)767 } else {768 None769 };770771 schedule_ids.push(self.cx.schedule_loop(772 *fallthrough,773 *init,774 Some(*loop_block),775 )?);776777 let init_result = self.visit_value_block(*init, *loc, None)?;778 let init_value = self.value_block_result_to_sequence(init_result, *loc);779780 let loop_body = if let Some(lid) = loop_id {781 self.traverse_block(lid)?782 } else {783 return Err(CompilerDiagnostic::new(784 ErrorCategory::Invariant,785 "Unexpected 'for-in' where the loop is already scheduled",786 None,787 ));788 };789790 self.cx.unschedule_all(&schedule_ids)?;791 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {792 terminal: ReactiveTerminal::ForIn {793 init: init_value,794 loop_block: loop_body,795 id: *id,796 loc: *loc,797 },798 label: fallthrough_id.map(|ft| ReactiveLabel {799 id: ft,800 implicit: false,801 }),802 }));803804 next_block = fallthrough_id;805 }806807 Terminal::Label {808 block: label_block,809 fallthrough,810 id,811 loc,812 } => {813 // TS: reachable(fallthrough) && !isScheduled(fallthrough)814 let fallthrough_id =815 if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) {816 Some(*fallthrough)817 } else {818 None819 };820 if let Some(ft) = fallthrough_id {821 schedule_ids.push(self.cx.schedule(ft, "if")?);822 }823824 if self.cx.is_scheduled(*label_block) {825 return Err(CompilerDiagnostic::new(826 ErrorCategory::Invariant,827 "Unexpected 'label' where the block is already scheduled".to_string(),828 None,829 ));830 }831 let label_body = self.traverse_block(*label_block)?;832833 self.cx.unschedule_all(&schedule_ids)?;834 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {835 terminal: ReactiveTerminal::Label {836 block: label_body,837 id: *id,838 loc: *loc,839 },840 label: fallthrough_id.map(|ft| ReactiveLabel {841 id: ft,842 implicit: false,843 }),844 }));845846 next_block = fallthrough_id;847 }848849 Terminal::Sequence { .. }850 | Terminal::Optional { .. }851 | Terminal::Ternary { .. }852 | Terminal::Logical { .. } => {853 let fallthrough = match &terminal {854 Terminal::Sequence { fallthrough, .. }855 | Terminal::Optional { fallthrough, .. }856 | Terminal::Ternary { fallthrough, .. }857 | Terminal::Logical { fallthrough, .. } => *fallthrough,858 _ => unreachable!(),859 };860 let fallthrough_id = if !self.cx.is_scheduled(fallthrough) {861 Some(fallthrough)862 } else {863 None864 };865 if let Some(ft) = fallthrough_id {866 schedule_ids.push(self.cx.schedule(ft, "if")?);867 }868869 let result = self.visit_value_block_terminal(&terminal)?;870 self.cx.unschedule_all(&schedule_ids)?;871 block_value.push(ReactiveStatement::Instruction(ReactiveInstruction {872 id: result.id,873 lvalue: Some(result.place),874 value: result.value,875 effects: None,876 loc: *terminal_loc(&terminal),877 }));878879 next_block = fallthrough_id;880 }881882 Terminal::Goto {883 block: goto_block,884 variant,885 id,886 loc,887 } => {888 match variant {889 GotoVariant::Break => {890 if let Some(stmt) = self.visit_break(*goto_block, *id, *loc)? {891 block_value.push(stmt);892 }893 }894 GotoVariant::Continue => {895 let stmt = self.visit_continue(*goto_block, *id, *loc)?;896 block_value.push(stmt);897 }898 GotoVariant::Try => {899 // noop900 }901 }902 }903904 Terminal::MaybeThrow { continuation, .. } => {905 if !self.cx.is_scheduled(*continuation) {906 next_block = Some(*continuation);907 }908 }909910 Terminal::Try {911 block: try_block,912 handler_binding,913 handler,914 fallthrough,915 id,916 loc,917 } => {918 let fallthrough_id =919 if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) {920 Some(*fallthrough)921 } else {922 None923 };924 if let Some(ft) = fallthrough_id {925 schedule_ids.push(self.cx.schedule(ft, "if")?);926 }927 self.cx.schedule_catch_handler(*handler);928929 let try_body = self.traverse_block(*try_block)?;930 let handler_body = self.traverse_block(*handler)?;931932 self.cx.unschedule_all(&schedule_ids)?;933 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {934 terminal: ReactiveTerminal::Try {935 block: try_body,936 handler_binding: handler_binding.clone(),937 handler: handler_body,938 id: *id,939 loc: *loc,940 },941 label: fallthrough_id.map(|ft| ReactiveLabel {942 id: ft,943 implicit: false,944 }),945 }));946947 next_block = fallthrough_id;948 }949950 Terminal::Scope {951 fallthrough,952 block: scope_block,953 scope,954 ..955 } => {956 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {957 Some(*fallthrough)958 } else {959 None960 };961 if let Some(ft) = fallthrough_id {962 schedule_ids.push(self.cx.schedule(ft, "if")?);963 self.cx.scope_fallthroughs.insert(ft);964 }965966 if self.cx.is_scheduled(*scope_block) {967 return Err(CompilerDiagnostic::new(968 ErrorCategory::Invariant,969 "Unexpected 'scope' where the block is already scheduled".to_string(),970 None,971 ));972 }973 let scope_body = self.traverse_block(*scope_block)?;974975 self.cx.unschedule_all(&schedule_ids)?;976 block_value.push(ReactiveStatement::Scope(ReactiveScopeBlock {977 scope: *scope,978 instructions: scope_body,979 }));980981 next_block = fallthrough_id;982 }983984 Terminal::PrunedScope {985 fallthrough,986 block: scope_block,987 scope,988 ..989 } => {990 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {991 Some(*fallthrough)992 } else {993 None994 };995 if let Some(ft) = fallthrough_id {996 schedule_ids.push(self.cx.schedule(ft, "if")?);997 self.cx.scope_fallthroughs.insert(ft);998 }9991000 if self.cx.is_scheduled(*scope_block) {1001 return Err(CompilerDiagnostic::new(1002 ErrorCategory::Invariant,1003 "Unexpected 'scope' where the block is already scheduled".to_string(),1004 None,1005 ));1006 }1007 let scope_body = self.traverse_block(*scope_block)?;10081009 self.cx.unschedule_all(&schedule_ids)?;1010 block_value.push(ReactiveStatement::PrunedScope(PrunedReactiveScopeBlock {1011 scope: *scope,1012 instructions: scope_body,1013 }));10141015 next_block = fallthrough_id;1016 }10171018 Terminal::Return { value, id, loc, .. } => {1019 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {1020 terminal: ReactiveTerminal::Return {1021 value: value.clone(),1022 id: *id,1023 loc: *loc,1024 },1025 label: None,1026 }));1027 }10281029 Terminal::Throw { value, id, loc } => {1030 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {1031 terminal: ReactiveTerminal::Throw {1032 value: value.clone(),1033 id: *id,1034 loc: *loc,1035 },1036 label: None,1037 }));1038 }10391040 Terminal::Unreachable { .. } => {1041 // noop1042 }10431044 Terminal::Unsupported { .. } => {1045 return Err(CompilerDiagnostic::new(1046 ErrorCategory::Invariant,1047 "Unexpected unsupported terminal",1048 None,1049 ));1050 }10511052 Terminal::Branch {1053 test,1054 consequent,1055 alternate,1056 id,1057 loc,1058 ..1059 } => {1060 let consequent_block = if self.cx.is_scheduled(*consequent) {1061 if let Some(stmt) = self.visit_break(*consequent, *id, *loc)? {1062 vec![stmt]1063 } else {1064 Vec::new()1065 }1066 } else {1067 self.traverse_block(*consequent)?1068 };10691070 if self.cx.is_scheduled(*alternate) {1071 return Err(CompilerDiagnostic::new(1072 ErrorCategory::Invariant,1073 "Unexpected 'branch' where the alternate is already scheduled"1074 .to_string(),1075 None,1076 ));1077 }1078 let alternate_block = self.traverse_block(*alternate)?;10791080 block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement {1081 terminal: ReactiveTerminal::If {1082 test: test.clone(),1083 consequent: consequent_block,1084 alternate: Some(alternate_block),1085 id: *id,1086 loc: *loc,1087 },1088 label: None,1089 }));1090 }1091 }1092 match next_block {1093 Some(nb) => block_id = nb,1094 None => return Ok(()),1095 }1096 } // end loop1097 }10981099 // =========================================================================1100 // Value block processing1101 // =========================================================================11021103 fn visit_value_block(1104 &mut self,1105 block_id: BlockId,1106 loc: Option<SourceLocation>,1107 fallthrough: Option<BlockId>,1108 ) -> Result<ValueBlockResult, CompilerDiagnostic> {1109 let block = &self.hir.body.blocks[&block_id];1110 let block_id_val = block.id;1111 let terminal = block.terminal.clone();1112 let instructions: Vec<_> = block.instructions.clone();11131114 // If we've reached the fallthrough, stop1115 if let Some(ft) = fallthrough {1116 if block_id == ft {1117 return Err(CompilerDiagnostic::new(1118 ErrorCategory::Invariant,1119 format!(1120 "Did not expect to reach the fallthrough of a value block (bb{})",1121 block_id.01122 ),1123 None,1124 ));1125 }1126 }11271128 match &terminal {1129 Terminal::Branch {1130 test, id: term_id, ..1131 } => {1132 if instructions.is_empty() {1133 Ok(ValueBlockResult {1134 block: block_id_val,1135 place: test.clone(),1136 value: ReactiveValue::Instruction(InstructionValue::LoadLocal {1137 place: test.clone(),1138 loc: test.loc,1139 }),1140 id: *term_id,1141 })1142 } else {1143 Ok(self.extract_value_block_result(&instructions, block_id_val, loc))1144 }1145 }1146 Terminal::Goto { .. } => {1147 if instructions.is_empty() {1148 return Err(CompilerDiagnostic::new(1149 ErrorCategory::Invariant,1150 "Unexpected empty block with `goto` terminal",1151 Some(format!("Block bb{} is empty", block_id.0)),1152 )1153 .with_detail(CompilerDiagnosticDetail::Error {1154 loc,1155 message: Some("Unexpected empty block with `goto` terminal".to_string()),1156 identifier_name: None,1157 }));1158 }1159 Ok(self.extract_value_block_result(&instructions, block_id_val, loc))1160 }1161 Terminal::MaybeThrow { continuation, .. } => {1162 let continuation_id = *continuation;1163 let continuation_block = self.cx.block(continuation_id);1164 let cont_instructions_empty = continuation_block.instructions.is_empty();1165 let cont_is_goto = matches!(continuation_block.terminal, Terminal::Goto { .. });1166 let cont_block_id = continuation_block.id;11671168 if cont_instructions_empty && cont_is_goto {1169 Ok(self.extract_value_block_result(&instructions, cont_block_id, loc))1170 } else {1171 let continuation = self.visit_value_block(continuation_id, loc, fallthrough)?;1172 Ok(self.wrap_with_sequence(&instructions, continuation, loc))1173 }1174 }1175 _ => {1176 // Value block ended in a value terminal, recurse to get the value1177 // of that terminal and stitch them together in a sequence.1178 // TS: visitValueBlock(init.fallthrough, loc) — does NOT propagate fallthrough1179 let init = self.visit_value_block_terminal(&terminal)?;1180 let init_fallthrough = init.fallthrough;1181 let init_instr = ReactiveInstruction {1182 id: init.id,1183 lvalue: Some(init.place),1184 value: init.value,1185 effects: None,1186 loc,1187 };1188 let final_result = self.visit_value_block(init_fallthrough, loc, None)?;11891190 // Combine block instructions + init instruction, then wrap1191 let mut all_instrs: Vec<ReactiveInstruction> = instructions1192 .iter()1193 .map(|iid| {1194 let instr = &self.hir.instructions[iid.0 as usize];1195 ReactiveInstruction {1196 id: instr.id,1197 lvalue: Some(instr.lvalue.clone()),1198 value: ReactiveValue::Instruction(instr.value.clone()),1199 effects: instr.effects.clone(),1200 loc: instr.loc,1201 }1202 })1203 .collect();1204 all_instrs.push(init_instr);12051206 if all_instrs.is_empty() {1207 Ok(final_result)1208 } else {1209 Ok(ValueBlockResult {1210 block: final_result.block,1211 place: final_result.place.clone(),1212 value: ReactiveValue::SequenceExpression {1213 instructions: all_instrs,1214 id: final_result.id,1215 value: Box::new(final_result.value),1216 loc,1217 },1218 id: final_result.id,1219 })1220 }1221 }1222 }1223 }12241225 fn visit_test_block(1226 &mut self,1227 test_block_id: BlockId,1228 loc: Option<SourceLocation>,1229 terminal_kind: &str,1230 ) -> Result<TestBlockResult, CompilerDiagnostic> {1231 let test = self.visit_value_block(test_block_id, loc, None)?;1232 let test_block = &self.hir.body.blocks[&test.block];1233 match &test_block.terminal {1234 Terminal::Branch {1235 consequent,1236 alternate,1237 loc: branch_loc,1238 ..1239 } => Ok(TestBlockResult {1240 test,1241 consequent: *consequent,1242 alternate: *alternate,1243 branch_loc: *branch_loc,1244 }),1245 other => Err(CompilerDiagnostic::new(1246 ErrorCategory::Invariant,1247 format!(1248 "Expected a branch terminal for {} test block, got {:?}",1249 terminal_kind,1250 std::mem::discriminant(other)1251 ),1252 None,1253 )),1254 }1255 }12561257 fn visit_value_block_terminal(1258 &mut self,1259 terminal: &Terminal,1260 ) -> Result<ValueTerminalResult, CompilerDiagnostic> {1261 match terminal {1262 Terminal::Sequence {1263 block,1264 fallthrough,1265 id,1266 loc,1267 } => {1268 let block_result = self.visit_value_block(*block, *loc, Some(*fallthrough))?;1269 Ok(ValueTerminalResult {1270 value: block_result.value,1271 place: block_result.place,1272 fallthrough: *fallthrough,1273 id: *id,1274 })1275 }1276 Terminal::Optional {1277 optional,1278 test,1279 fallthrough,1280 id,1281 loc,1282 } => {1283 let test_result = self.visit_test_block(*test, *loc, "optional")?;1284 let consequent =1285 self.visit_value_block(test_result.consequent, *loc, Some(*fallthrough))?;1286 let call = ReactiveValue::SequenceExpression {1287 instructions: vec![ReactiveInstruction {1288 id: test_result.test.id,1289 lvalue: Some(test_result.test.place.clone()),1290 value: test_result.test.value,1291 effects: None,1292 loc: test_result.branch_loc,1293 }],1294 id: consequent.id,1295 value: Box::new(consequent.value),1296 loc: *loc,1297 };1298 Ok(ValueTerminalResult {1299 place: consequent.place,1300 value: ReactiveValue::OptionalExpression {1301 optional: *optional,1302 value: Box::new(call),1303 id: *id,1304 loc: *loc,1305 },1306 fallthrough: *fallthrough,1307 id: *id,1308 })1309 }1310 Terminal::Logical {1311 operator,1312 test,1313 fallthrough,1314 id,1315 loc,1316 } => {1317 let test_result = self.visit_test_block(*test, *loc, "logical")?;1318 let left_final =1319 self.visit_value_block(test_result.consequent, *loc, Some(*fallthrough))?;1320 let left = ReactiveValue::SequenceExpression {1321 instructions: vec![ReactiveInstruction {1322 id: test_result.test.id,1323 lvalue: Some(test_result.test.place.clone()),1324 value: test_result.test.value,1325 effects: None,1326 loc: *loc,1327 }],1328 id: left_final.id,1329 value: Box::new(left_final.value),1330 loc: *loc,1331 };1332 let right =1333 self.visit_value_block(test_result.alternate, *loc, Some(*fallthrough))?;1334 Ok(ValueTerminalResult {1335 place: left_final.place,1336 value: ReactiveValue::LogicalExpression {1337 operator: *operator,1338 left: Box::new(left),1339 right: Box::new(right.value),1340 loc: *loc,1341 },1342 fallthrough: *fallthrough,1343 id: *id,1344 })1345 }1346 Terminal::Ternary {1347 test,1348 fallthrough,1349 id,1350 loc,1351 } => {1352 let test_result = self.visit_test_block(*test, *loc, "ternary")?;1353 let consequent =1354 self.visit_value_block(test_result.consequent, *loc, Some(*fallthrough))?;1355 let alternate =1356 self.visit_value_block(test_result.alternate, *loc, Some(*fallthrough))?;1357 Ok(ValueTerminalResult {1358 place: consequent.place,1359 value: ReactiveValue::ConditionalExpression {1360 test: Box::new(test_result.test.value),1361 consequent: Box::new(consequent.value),1362 alternate: Box::new(alternate.value),1363 loc: *loc,1364 },1365 fallthrough: *fallthrough,1366 id: *id,1367 })1368 }1369 Terminal::MaybeThrow { .. } => Err(CompilerDiagnostic::new(1370 ErrorCategory::Invariant,1371 "Unexpected maybe-throw in visit_value_block_terminal",1372 None,1373 )),1374 Terminal::Label { .. } => Err(CompilerDiagnostic::new(1375 ErrorCategory::Todo,1376 "Support labeled statements combined with value blocks is not yet implemented",1377 None,1378 )),1379 _ => Err(CompilerDiagnostic::new(1380 ErrorCategory::Invariant,1381 "Unsupported terminal kind in value block",1382 None,1383 )),1384 }1385 }13861387 fn extract_value_block_result(1388 &self,1389 instructions: &[react_compiler_hir::InstructionId],1390 block_id: BlockId,1391 loc: Option<SourceLocation>,1392 ) -> ValueBlockResult {1393 let last_id = instructions1394 .last()1395 .expect("Expected non-empty instructions");1396 let last_instr = &self.hir.instructions[last_id.0 as usize];13971398 let remaining: Vec<ReactiveInstruction> = instructions[..instructions.len() - 1]1399 .iter()1400 .map(|iid| {1401 let instr = &self.hir.instructions[iid.0 as usize];1402 ReactiveInstruction {1403 id: instr.id,1404 lvalue: Some(instr.lvalue.clone()),1405 value: ReactiveValue::Instruction(instr.value.clone()),1406 effects: instr.effects.clone(),1407 loc: instr.loc,1408 }1409 })1410 .collect();14111412 // If the last instruction is a StoreLocal to a temporary (unnamed identifier),1413 // convert it to a LoadLocal of the value being stored, matching the TS behavior.1414 let (value, place) = match &last_instr.value {1415 InstructionValue::StoreLocal {1416 lvalue,1417 value: store_value,1418 ..1419 } => {1420 let ident = &self.env.identifiers[lvalue.place.identifier.0 as usize];1421 if ident.name.is_none() {1422 (1423 ReactiveValue::Instruction(InstructionValue::LoadLocal {1424 place: store_value.clone(),1425 loc: store_value.loc,1426 }),1427 lvalue.place.clone(),1428 )1429 } else {1430 (1431 ReactiveValue::Instruction(last_instr.value.clone()),1432 last_instr.lvalue.clone(),1433 )1434 }1435 }1436 _ => (1437 ReactiveValue::Instruction(last_instr.value.clone()),1438 last_instr.lvalue.clone(),1439 ),1440 };1441 let id = last_instr.id;14421443 if remaining.is_empty() {1444 ValueBlockResult {1445 block: block_id,1446 place,1447 value,1448 id,1449 }1450 } else {1451 ValueBlockResult {1452 block: block_id,1453 place: place.clone(),1454 value: ReactiveValue::SequenceExpression {1455 instructions: remaining,1456 id,1457 value: Box::new(value),1458 loc,1459 },1460 id,1461 }1462 }1463 }14641465 fn wrap_with_sequence(1466 &self,1467 instructions: &[react_compiler_hir::InstructionId],1468 continuation: ValueBlockResult,1469 loc: Option<SourceLocation>,1470 ) -> ValueBlockResult {1471 if instructions.is_empty() {1472 return continuation;1473 }14741475 let reactive_instrs: Vec<ReactiveInstruction> = instructions1476 .iter()1477 .map(|iid| {1478 let instr = &self.hir.instructions[iid.0 as usize];1479 ReactiveInstruction {1480 id: instr.id,1481 lvalue: Some(instr.lvalue.clone()),1482 value: ReactiveValue::Instruction(instr.value.clone()),1483 effects: instr.effects.clone(),1484 loc: instr.loc,1485 }1486 })1487 .collect();14881489 ValueBlockResult {1490 block: continuation.block,1491 place: continuation.place.clone(),1492 value: ReactiveValue::SequenceExpression {1493 instructions: reactive_instrs,1494 id: continuation.id,1495 value: Box::new(continuation.value),1496 loc,1497 },1498 id: continuation.id,1499 }1500 }15011502 /// Converts the result of visit_value_block into a SequenceExpression that includes1503 /// the instruction with its lvalue. This is needed for for/for-of/for-in init/test1504 /// blocks where the instruction's lvalue assignment must be preserved.1505 ///1506 /// This also flattens nested SequenceExpressions that can occur from MaybeThrow1507 /// handling in try-catch blocks.1508 ///1509 /// TS: valueBlockResultToSequence()1510 fn value_block_result_to_sequence(1511 &self,1512 result: ValueBlockResult,1513 loc: Option<SourceLocation>,1514 ) -> ReactiveValue {1515 // Collect all instructions from potentially nested SequenceExpressions1516 let mut instructions: Vec<ReactiveInstruction> = Vec::new();1517 let mut inner_value = result.value;15181519 // Flatten nested SequenceExpressions1520 loop {1521 match inner_value {1522 ReactiveValue::SequenceExpression {1523 instructions: seq_instrs,1524 value,1525 ..1526 } => {1527 instructions.extend(seq_instrs);1528 inner_value = *value;1529 }1530 _ => break,1531 }1532 }15331534 // Only add the final instruction if the innermost value is not just a LoadLocal1535 // of the same place we're storing to (which would be a no-op).1536 let is_load_of_same_place = match &inner_value {1537 ReactiveValue::Instruction(InstructionValue::LoadLocal { place, .. }) => {1538 place.identifier == result.place.identifier1539 }1540 _ => false,1541 };15421543 if !is_load_of_same_place {1544 instructions.push(ReactiveInstruction {1545 id: result.id,1546 lvalue: Some(result.place),1547 value: inner_value,1548 effects: None,1549 loc,1550 });1551 }15521553 ReactiveValue::SequenceExpression {1554 instructions,1555 id: result.id,1556 value: Box::new(ReactiveValue::Instruction(InstructionValue::Primitive {1557 value: react_compiler_hir::PrimitiveValue::Undefined,1558 loc,1559 })),1560 loc,1561 }1562 }15631564 fn visit_break(1565 &self,1566 block: BlockId,1567 id: EvaluationOrder,1568 loc: Option<SourceLocation>,1569 ) -> Result<Option<ReactiveStatement>, CompilerDiagnostic> {1570 let (target_block, target_kind) = self.cx.get_break_target(block)?;1571 if self.cx.scope_fallthroughs.contains(&target_block) {1572 if target_kind != ReactiveTerminalTargetKind::Implicit {1573 return Err(CompilerDiagnostic::new(1574 ErrorCategory::Invariant,1575 "Expected reactive scope to implicitly break to fallthrough".to_string(),1576 None,1577 ));1578 }1579 return Ok(None);1580 }1581 Ok(Some(ReactiveStatement::Terminal(1582 ReactiveTerminalStatement {1583 terminal: ReactiveTerminal::Break {1584 target: target_block,1585 id,1586 target_kind,1587 loc,1588 },1589 label: None,1590 },1591 )))1592 }15931594 fn visit_continue(1595 &self,1596 block: BlockId,1597 id: EvaluationOrder,1598 loc: Option<SourceLocation>,1599 ) -> Result<ReactiveStatement, CompilerDiagnostic> {1600 let (target_block, target_kind) = match self.cx.get_continue_target(block) {1601 Some(result) => result,1602 None => {1603 return Err(CompilerDiagnostic::new(1604 ErrorCategory::Invariant,1605 format!("Expected continue target to be scheduled for bb{}", block.0),1606 None,1607 ));1608 }1609 };16101611 Ok(ReactiveStatement::Terminal(ReactiveTerminalStatement {1612 terminal: ReactiveTerminal::Continue {1613 target: target_block,1614 id,1615 target_kind,1616 loc,1617 },1618 label: None,1619 }))1620 }1621}16221623// =============================================================================1624// Helper types1625// =============================================================================16261627struct ValueBlockResult {1628 block: BlockId,1629 place: Place,1630 value: ReactiveValue,1631 id: EvaluationOrder,1632}16331634struct TestBlockResult {1635 test: ValueBlockResult,1636 consequent: BlockId,1637 alternate: BlockId,1638 branch_loc: Option<SourceLocation>,1639}16401641struct ValueTerminalResult {1642 value: ReactiveValue,1643 place: Place,1644 fallthrough: BlockId,1645 id: EvaluationOrder,1646}16471648/// Helper to get loc from a terminal1649fn terminal_loc(terminal: &Terminal) -> &Option<SourceLocation> {1650 match terminal {1651 Terminal::If { loc, .. }1652 | Terminal::Branch { loc, .. }1653 | Terminal::Logical { loc, .. }1654 | Terminal::Ternary { loc, .. }1655 | Terminal::Optional { loc, .. }1656 | Terminal::Throw { loc, .. }1657 | Terminal::Return { loc, .. }1658 | Terminal::Goto { loc, .. }1659 | Terminal::Switch { loc, .. }1660 | Terminal::DoWhile { loc, .. }1661 | Terminal::While { loc, .. }1662 | Terminal::For { loc, .. }1663 | Terminal::ForOf { loc, .. }1664 | Terminal::ForIn { loc, .. }1665 | Terminal::Label { loc, .. }1666 | Terminal::Sequence { loc, .. }1667 | Terminal::Unreachable { loc, .. }1668 | Terminal::Unsupported { loc, .. }1669 | Terminal::MaybeThrow { loc, .. }1670 | Terminal::Scope { loc, .. }1671 | Terminal::PrunedScope { loc, .. }1672 | Terminal::Try { loc, .. } => loc,1673 }1674}
Findings
✓ No findings reported for this file.