main
rs 1,674 lines 61.7 KB
Raw
1 // Copyright (c) Meta Platforms, Inc. and affiliates.
2 //
3 // This source code is licensed under the MIT license found in the
4 // LICENSE file in the root directory of this source tree.
5
6 //! Converts the HIR CFG into a tree-structured ReactiveFunction.
7 //!
8 //! Corresponds to `src/ReactiveScopes/BuildReactiveFunction.ts`.
9
10 use rustc_hash::FxHashSet;
11
12 use react_compiler_diagnostics::{
13 CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation,
14 };
15 use react_compiler_hir::environment::Environment;
16 use 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 };
22
23 /// Convert the HIR CFG into a tree-structured ReactiveFunction.
24 pub 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 };
34
35 let entry_block_id = hir.body.entry;
36 let mut body = Vec::new();
37 driver.visit_block(entry_block_id, &mut body)?;
38
39 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 }
50
51 // =============================================================================
52 // ControlFlowTarget
53 // =============================================================================
54
55 #[derive(Debug)]
56 enum 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 }
79
80 impl 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 }
89
90 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 }
98
99 fn is_loop(&self) -> bool {
100 matches!(self, ControlFlowTarget::Loop { .. })
101 }
102 }
103
104 // =============================================================================
105 // Context
106 // =============================================================================
107
108 struct 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 }
117
118 impl<'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 }
130
131 fn block(&self, id: BlockId) -> &BasicBlock {
132 &self.ir.body.blocks[&id]
133 }
134
135 fn schedule_catch_handler(&mut self, block: BlockId) {
136 self.catch_handlers.insert(block);
137 }
138
139 fn reachable(&self, id: BlockId) -> bool {
140 let block = self.block(id);
141 !matches!(block.terminal, Terminal::Unreachable { .. })
142 }
143
144 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 }
170
171 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.0
187 ),
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 }
197
198 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 }
208
209 fn unschedule(&mut self, schedule_id: u32) -> Result<(), CompilerDiagnostic> {
210 let last = self
211 .control_flow_stack
212 .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 loops
230 // (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 }
245
246 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 }
252
253 fn is_scheduled(&self, block: BlockId) -> bool {
254 self.scheduled.contains(&block) || self.catch_handlers.contains(&block)
255 }
256
257 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::Labeled
268 } else {
269 ReactiveTerminalTargetKind::Unlabeled
270 }
271 } else if i == self.control_flow_stack.len() - 1 {
272 ReactiveTerminalTargetKind::Implicit
273 } else {
274 ReactiveTerminalTargetKind::Labeled
275 };
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 }
286
287 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 } = target
296 {
297 if *continue_block == block {
298 let kind = if has_preceding_loop {
299 ReactiveTerminalTargetKind::Labeled
300 } else if i == self.control_flow_stack.len() - 1 {
301 ReactiveTerminalTargetKind::Implicit
302 } else {
303 ReactiveTerminalTargetKind::Unlabeled
304 };
305 return Some((*fallthrough_block, kind));
306 }
307 }
308 has_preceding_loop = has_preceding_loop || target.is_loop();
309 }
310 None
311 }
312 }
313
314 // =============================================================================
315 // Driver
316 // =============================================================================
317
318 struct Driver<'a, 'b> {
319 cx: &'b mut Context<'a>,
320 hir: &'a HirFunction,
321 #[allow(dead_code)]
322 env: &'a Environment,
323 }
324
325 impl<'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 }
331
332 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 operations
342 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();
346
347 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 }
354
355 // Emit instructions
356 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 }
366
367 // Process terminal
368 let mut schedule_ids: Vec<u32> = Vec::new();
369 let mut next_block: Option<BlockId> = None;
370
371 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 None
386 };
387 // TS: alternate !== fallthrough ? alternate : null
388 let alternate_id = if *alternate != *fallthrough {
389 Some(*alternate)
390 } else {
391 None
392 };
393
394 if let Some(ft) = fallthrough_id {
395 schedule_ids.push(self.cx.schedule(ft, "if")?);
396 }
397
398 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.0
404 ),
405 None,
406 ));
407 } else {
408 self.traverse_block(*consequent)?
409 };
410
411 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.0
418 ),
419 None,
420 ));
421 } else {
422 Some(self.traverse_block(alt)?)
423 }
424 } else {
425 None
426 };
427
428 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 }));
442
443 next_block = fallthrough_id;
444 }
445
446 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 None
459 };
460 if let Some(ft) = fallthrough_id {
461 schedule_ids.push(self.cx.schedule(ft, "switch")?);
462 }
463
464 // TS processes cases in reverse order, then reverses the result.
465 // This ensures that later cases are scheduled when earlier cases
466 // 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;
470
471 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 }
482
483 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);
486
487 reactive_cases.push(ReactiveSwitchCase {
488 test: case.test.clone(),
489 block: Some(consequent),
490 });
491 }
492 reactive_cases.reverse();
493
494 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 }));
507
508 next_block = fallthrough_id;
509 }
510
511 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 None
522 };
523 let loop_id =
524 if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough {
525 Some(*loop_block)
526 } else {
527 None
528 };
529
530 schedule_ids.push(self.cx.schedule_loop(
531 *fallthrough,
532 *test,
533 Some(*loop_block),
534 )?);
535
536 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)?;
546
547 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 }));
560
561 next_block = fallthrough_id;
562 }
563
564 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 None
577 };
578 let loop_id =
579 if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough {
580 Some(*loop_block)
581 } else {
582 None
583 };
584
585 schedule_ids.push(self.cx.schedule_loop(
586 *fallthrough,
587 *test,
588 Some(*loop_block),
589 )?);
590
591 let test_result = self.visit_value_block(*test, *loc, None)?;
592
593 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 };
602
603 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 }));
616
617 next_block = fallthrough_id;
618 }
619
620 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 None
634 };
635
636 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {
637 Some(*fallthrough)
638 } else {
639 None
640 };
641
642 // Continue block is update (if present) or test
643 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 )?);
649
650 let init_result = self.visit_value_block(*init, *loc, None)?;
651 let init_value = self.value_block_result_to_sequence(init_result, *loc);
652
653 let test_result = self.visit_value_block(*test, *loc, None)?;
654
655 let update_result = match update {
656 Some(u) => Some(self.visit_value_block(*u, *loc, None)?),
657 None => None,
658 };
659
660 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 };
669
670 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 }));
685
686 next_block = fallthrough_id;
687 }
688
689 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 None
702 };
703
704 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {
705 Some(*fallthrough)
706 } else {
707 None
708 };
709
710 // TS: scheduleLoop(fallthrough, init, loop)
711 schedule_ids.push(self.cx.schedule_loop(
712 *fallthrough,
713 *init,
714 Some(*loop_block),
715 )?);
716
717 let init_result = self.visit_value_block(*init, *loc, None)?;
718 let init_value = self.value_block_result_to_sequence(init_result, *loc);
719
720 let test_result = self.visit_value_block(*test, *loc, None)?;
721 let test_value = self.value_block_result_to_sequence(test_result, *loc);
722
723 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 };
732
733 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 }));
747
748 next_block = fallthrough_id;
749 }
750
751 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 None
763 };
764
765 let fallthrough_id = if !self.cx.is_scheduled(*fallthrough) {
766 Some(*fallthrough)
767 } else {
768 None
769 };
770
771 schedule_ids.push(self.cx.schedule_loop(
772 *fallthrough,
773 *init,
774 Some(*loop_block),
775 )?);
776
777 let init_result = self.visit_value_block(*init, *loc, None)?;
778 let init_value = self.value_block_result_to_sequence(init_result, *loc);
779
780 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 };
789
790 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 }));
803
804 next_block = fallthrough_id;
805 }
806
807 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 None
819 };
820 if let Some(ft) = fallthrough_id {
821 schedule_ids.push(self.cx.schedule(ft, "if")?);
822 }
823
824 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)?;
832
833 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 }));
845
846 next_block = fallthrough_id;
847 }
848
849 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 None
864 };
865 if let Some(ft) = fallthrough_id {
866 schedule_ids.push(self.cx.schedule(ft, "if")?);
867 }
868
869 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 }));
878
879 next_block = fallthrough_id;
880 }
881
882 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 // noop
900 }
901 }
902 }
903
904 Terminal::MaybeThrow { continuation, .. } => {
905 if !self.cx.is_scheduled(*continuation) {
906 next_block = Some(*continuation);
907 }
908 }
909
910 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 None
923 };
924 if let Some(ft) = fallthrough_id {
925 schedule_ids.push(self.cx.schedule(ft, "if")?);
926 }
927 self.cx.schedule_catch_handler(*handler);
928
929 let try_body = self.traverse_block(*try_block)?;
930 let handler_body = self.traverse_block(*handler)?;
931
932 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 }));
946
947 next_block = fallthrough_id;
948 }
949
950 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 None
960 };
961 if let Some(ft) = fallthrough_id {
962 schedule_ids.push(self.cx.schedule(ft, "if")?);
963 self.cx.scope_fallthroughs.insert(ft);
964 }
965
966 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)?;
974
975 self.cx.unschedule_all(&schedule_ids)?;
976 block_value.push(ReactiveStatement::Scope(ReactiveScopeBlock {
977 scope: *scope,
978 instructions: scope_body,
979 }));
980
981 next_block = fallthrough_id;
982 }
983
984 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 None
994 };
995 if let Some(ft) = fallthrough_id {
996 schedule_ids.push(self.cx.schedule(ft, "if")?);
997 self.cx.scope_fallthroughs.insert(ft);
998 }
999
1000 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)?;
1008
1009 self.cx.unschedule_all(&schedule_ids)?;
1010 block_value.push(ReactiveStatement::PrunedScope(PrunedReactiveScopeBlock {
1011 scope: *scope,
1012 instructions: scope_body,
1013 }));
1014
1015 next_block = fallthrough_id;
1016 }
1017
1018 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 }
1028
1029 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 }
1039
1040 Terminal::Unreachable { .. } => {
1041 // noop
1042 }
1043
1044 Terminal::Unsupported { .. } => {
1045 return Err(CompilerDiagnostic::new(
1046 ErrorCategory::Invariant,
1047 "Unexpected unsupported terminal",
1048 None,
1049 ));
1050 }
1051
1052 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 };
1069
1070 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)?;
1079
1080 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 loop
1097 }
1098
1099 // =========================================================================
1100 // Value block processing
1101 // =========================================================================
1102
1103 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();
1113
1114 // If we've reached the fallthrough, stop
1115 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.0
1122 ),
1123 None,
1124 ));
1125 }
1126 }
1127
1128 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;
1167
1168 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 value
1177 // of that terminal and stitch them together in a sequence.
1178 // TS: visitValueBlock(init.fallthrough, loc) — does NOT propagate fallthrough
1179 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)?;
1189
1190 // Combine block instructions + init instruction, then wrap
1191 let mut all_instrs: Vec<ReactiveInstruction> = instructions
1192 .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);
1205
1206 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 }
1224
1225 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 }
1256
1257 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 }
1386
1387 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 = instructions
1394 .last()
1395 .expect("Expected non-empty instructions");
1396 let last_instr = &self.hir.instructions[last_id.0 as usize];
1397
1398 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();
1411
1412 // 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;
1442
1443 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 }
1464
1465 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 }
1474
1475 let reactive_instrs: Vec<ReactiveInstruction> = instructions
1476 .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();
1488
1489 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 }
1501
1502 /// Converts the result of visit_value_block into a SequenceExpression that includes
1503 /// the instruction with its lvalue. This is needed for for/for-of/for-in init/test
1504 /// blocks where the instruction's lvalue assignment must be preserved.
1505 ///
1506 /// This also flattens nested SequenceExpressions that can occur from MaybeThrow
1507 /// 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 SequenceExpressions
1516 let mut instructions: Vec<ReactiveInstruction> = Vec::new();
1517 let mut inner_value = result.value;
1518
1519 // Flatten nested SequenceExpressions
1520 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 }
1533
1534 // Only add the final instruction if the innermost value is not just a LoadLocal
1535 // 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.identifier
1539 }
1540 _ => false,
1541 };
1542
1543 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 }
1552
1553 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 }
1563
1564 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 }
1593
1594 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 };
1610
1611 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 }
1622
1623 // =============================================================================
1624 // Helper types
1625 // =============================================================================
1626
1627 struct ValueBlockResult {
1628 block: BlockId,
1629 place: Place,
1630 value: ReactiveValue,
1631 id: EvaluationOrder,
1632 }
1633
1634 struct TestBlockResult {
1635 test: ValueBlockResult,
1636 consequent: BlockId,
1637 alternate: BlockId,
1638 branch_loc: Option<SourceLocation>,
1639 }
1640
1641 struct ValueTerminalResult {
1642 value: ReactiveValue,
1643 place: Place,
1644 fallthrough: BlockId,
1645 id: EvaluationOrder,
1646 }
1647
1648 /// Helper to get loc from a terminal
1649 fn 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 }