555
// Context
556
// =============================================================================
557
558
-type Temporaries = FxHashMap<DeclarationId, Option<ExpressionOrJsxText>>;
559
-
558
#[derive(Clone)]
559
enum ExpressionOrJsxText {
560
Expression(Expression),
561
JsxText(JSXText),
562
}
563
564
+/// The entry a write to [`Temporaries`] displaced, kept so the write can be
565
+/// undone.
566
+///
567
+/// The expression is boxed because `ExpressionOrJsxText` is ~900 bytes (it
568
+/// inlines an `Expression`). Unboxed, the undo log would be a `Vec` of
569
+/// ~900-byte slots that are almost always `Absent`, costing more peak heap than
570
+/// the copy it replaces on shallow functions. Boxed, an entry is 16 bytes and
571
+/// only allocates when a write actually displaces a buffered expression.
572
+enum Displaced {
573
+ /// The key was not present before the write.
574
+ Absent,
575
+ /// The key was present as a declared temporary with no buffered value.
576
+ Empty,
577
+ /// The key was present with this buffered value.
578
+ Value(Box<ExpressionOrJsxText>),
579
+}
580
+
581
+/// A position in a [`Temporaries`] undo log, produced by [`Temporaries::mark`].
582
+#[derive(Clone, Copy)]
583
+struct TempMark(usize);
584
+
585
+/// Expressions buffered for temporaries that have not been emitted yet, plus an
586
+/// undo log allowing a nested block or scope to be codegen'd and its additions
587
+/// discarded.
588
+///
589
+/// The TypeScript implementation snapshots this with `new Map(cx.temp)`, which
590
+/// is a *shallow* copy: it duplicates references, not the AST nodes behind them.
591
+/// The equivalent Rust `.clone()` deep-copies every buffered `Expression` tree,
592
+/// which made codegen quadratic in component size and dominated both allocation
593
+/// volume and peak heap.
594
+///
595
+/// `TS CodegenReactiveFunction.codegenBlock` asserts that pre-existing entries
596
+/// are never mutated ("Expected temporary value to be unchanged"), so a
597
+/// snapshot's only job is to discard entries added by the nested block. Because
598
+/// entries are only ever inserted (never removed, nor mutated in place),
599
+/// rewinding an insert log restores the map exactly, with no copying.
600
+///
601
+/// All writes go through [`Temporaries::set`] so the log cannot drift out of
602
+/// sync with the map.
603
+#[derive(Default)]
604
+struct Temporaries {
605
+ values: FxHashMap<DeclarationId, Option<ExpressionOrJsxText>>,
606
+ journal: Vec<(DeclarationId, Displaced)>,
607
+}
608
+
609
+impl Temporaries {
610
+ fn get(&self, declaration_id: DeclarationId) -> Option<&Option<ExpressionOrJsxText>> {
611
+ self.values.get(&declaration_id)
612
+ }
613
+
614
+ fn contains_key(&self, declaration_id: DeclarationId) -> bool {
615
+ self.values.contains_key(&declaration_id)
616
+ }
617
+
618
+ /// Buffers `value` for `declaration_id`, journaling the displaced entry.
619
+ /// `HashMap::insert` returns that entry by move, so journaling costs no
620
+ /// clones.
621
+ fn set(&mut self, declaration_id: DeclarationId, value: Option<ExpressionOrJsxText>) {
622
+ let displaced = match self.values.insert(declaration_id, value) {
623
+ None => Displaced::Absent,
624
+ Some(None) => Displaced::Empty,
625
+ Some(Some(previous)) => Displaced::Value(Box::new(previous)),
626
+ };
627
+ self.journal.push((declaration_id, displaced));
628
+ }
629
+
630
+ /// Marks the current state, for a later [`Temporaries::rewind`].
631
+ fn mark(&self) -> TempMark {
632
+ TempMark(self.journal.len())
633
+ }
634
+
635
+ /// Restores the state captured by `mark`, discarding every write since.
636
+ fn rewind(&mut self, mark: TempMark) {
637
+ while self.journal.len() > mark.0 {
638
+ let (declaration_id, displaced) = self.journal.pop().unwrap();
639
+ match displaced {
640
+ Displaced::Absent => {
641
+ self.values.remove(&declaration_id);
642
+ }
643
+ Displaced::Empty => {
644
+ self.values.insert(declaration_id, None);
645
+ }
646
+ Displaced::Value(previous) => {
647
+ self.values.insert(declaration_id, Some(*previous));
648
+ }
649
+ }
650
+ }
651
+ }
652
+
653
+ /// Hands the buffered expressions to a nested function's context, which may
654
+ /// read them but must not leak its own additions back out.
655
+ ///
656
+ /// The borrower gets a fresh log, so [`Temporaries::reclaim`] can undo
657
+ /// exactly the borrower's writes rather than the lender's whole history.
658
+ fn lend(&mut self) -> Temporaries {
659
+ Temporaries {
660
+ values: std::mem::take(&mut self.values),
661
+ journal: Vec::new(),
662
+ }
663
+ }
664
+
665
+ /// Takes back expressions handed out by [`Temporaries::lend`], discarding
666
+ /// every write the borrower made.
667
+ fn reclaim(&mut self, mut lent: Temporaries) {
668
+ lent.rewind(TempMark(0));
669
+ self.values = lent.values;
670
+ }
671
+}
672
+
673
struct Context<'env> {
674
env: &'env mut Environment,
675
#[allow(dead_code)]
701
fn_name,
702
next_cache_index: 0,
703
declarations: FxHashSet::default(),
597
- temp: FxHashMap::default(),
704
+ temp: Temporaries::default(),
705
object_methods: FxHashMap::default(),
706
unique_identifiers,
707
fbt_operands,
760
ParamPattern::Place(p) => p,
761
ParamPattern::Spread(sp) => &sp.place,
762
};
656
- let ident = &cx.env.identifiers[place.identifier.0 as usize];
657
- cx.temp.insert(ident.declaration_id, None);
763
+ let declaration_id = cx.env.identifiers[place.identifier.0 as usize].declaration_id;
764
+ cx.temp.set(declaration_id, None);
765
cx.declare(place.identifier);
766
}
767
839
// =============================================================================
840
841
fn codegen_block(cx: &mut Context, block: &ReactiveBlock) -> Result<BlockStatement, CompilerError> {
735
- let temp_snapshot: Temporaries = cx.temp.clone();
842
+ let mark = cx.temp.mark();
843
let result = codegen_block_no_reset(cx, block)?;
737
- cx.temp = temp_snapshot;
844
+ cx.temp.rewind(mark);
845
Ok(result)
846
}
847
865
scope,
866
instructions,
867
}) => {
761
- let temp_snapshot = cx.temp.clone();
868
+ let mark = cx.temp.mark();
869
codegen_reactive_scope(cx, &mut statements, *scope, instructions)?;
763
- cx.temp = temp_snapshot;
870
+ cx.temp.rewind(mark);
871
}
872
ReactiveStatement::Terminal(term_stmt) => {
873
let stmt = codegen_terminal(cx, &term_stmt.terminal)?;
1365
} => {
1366
let catch_param = match handler_binding.as_ref() {
1367
Some(binding) => {
1261
- let ident = &cx.env.identifiers[binding.identifier.0 as usize];
1262
- cx.temp.insert(ident.declaration_id, None);
1368
+ let declaration_id =
1369
+ cx.env.identifiers[binding.identifier.0 as usize].declaration_id;
1370
+ cx.temp.set(declaration_id, None);
1371
Some(PatternLike::Identifier(convert_identifier(
1372
binding.identifier,
1373
cx.env,
1832
// Register temporaries for unnamed pattern operands
1833
for place in react_compiler_hir::visitors::each_pattern_operand(&lvalue.pattern) {
1834
let ident = &cx.env.identifiers[place.identifier.0 as usize];
1727
- if kind != InstructionKind::Reassign && ident.name.is_none() {
1728
- cx.temp.insert(ident.declaration_id, None);
1835
+ let declaration_id = ident.declaration_id;
1836
+ let is_unnamed = ident.name.is_none();
1837
+ if kind != InstructionKind::Reassign && is_unnamed {
1838
+ cx.temp.set(declaration_id, None);
1839
}
1840
}
1841
let rhs = codegen_place_to_expression(cx, val)?;
1948
ReactiveValue::Instruction(InstructionValue::StoreContext { .. })
1949
);
1950
if !is_store_context {
1841
- let ident = &cx.env.identifiers[lvalue_place.identifier.0 as usize];
1842
- cx.temp.insert(
1843
- ident.declaration_id,
1844
- Some(ExpressionOrJsxText::Expression(expr)),
1845
- );
1951
+ let declaration_id =
1952
+ cx.env.identifiers[lvalue_place.identifier.0 as usize].declaration_id;
1953
+ cx.temp
1954
+ .set(declaration_id, Some(ExpressionOrJsxText::Expression(expr)));
1955
return Ok(None);
1956
} else {
1957
let stmt =
1995
}));
1996
};
1997
let ident = &cx.env.identifiers[lvalue.identifier.0 as usize];
1998
+ let declaration_id = ident.declaration_id;
1999
if ident.name.is_none() {
2000
// temporary
1891
- cx.temp.insert(ident.declaration_id, Some(value));
2001
+ cx.temp.set(declaration_id, Some(value));
2002
return Ok(Statement::EmptyStatement(EmptyStatement {
2003
base: BaseNode::typed("EmptyStatement"),
2004
}));
2773
cx.unique_identifiers.clone(),
2774
cx.fbt_operands.clone(),
2775
);
2666
- inner_cx.temp = cx.temp.clone();
2776
+ // The inner function reads the enclosing temporaries but must not leak its
2777
+ // own back out. Lend the map to `inner_cx` and rewind its writes on the way
2778
+ // out, rather than deep-cloning every buffered expression tree. The map is
2779
+ // restored on the error path too, so `cx` is never left empty.
2780
+ inner_cx.temp = cx.temp.lend();
2781
2668
- let fn_result = codegen_reactive_function(&mut inner_cx, &reactive_fn_mut)?;
2782
+ let fn_result = codegen_reactive_function(&mut inner_cx, &reactive_fn_mut);
2783
+
2784
+ cx.temp.reclaim(std::mem::take(&mut inner_cx.temp));
2785
+ let fn_result = fn_result?;
2786
2787
let value = match expr_type {
2788
FunctionExpressionType::ArrowFunctionExpression => {
2915
cx.unique_identifiers.clone(),
2916
cx.fbt_operands.clone(),
2917
);
2801
- inner_cx.temp = cx.temp.clone();
2918
+ inner_cx.temp = cx.temp.lend();
2919
+
2920
+ let fn_result = codegen_reactive_function(&mut inner_cx, &reactive_fn_mut);
2921
2803
- let fn_result = codegen_reactive_function(&mut inner_cx, &reactive_fn_mut)?;
2922
+ cx.temp.reclaim(std::mem::take(&mut inner_cx.temp));
2923
+ let fn_result = fn_result?;
2924
2925
ast_properties.push(ast_expr::ObjectExpressionProperty::ObjectMethod(
2926
ast_expr::ObjectMethod {
3429
3430
fn codegen_place(cx: &mut Context, place: &Place) -> Result<ExpressionOrJsxText, CompilerError> {
3431
let ident = &cx.env.identifiers[place.identifier.0 as usize];
3312
- if let Some(tmp) = cx.temp.get(&ident.declaration_id) {
3432
+ if let Some(tmp) = cx.temp.get(ident.declaration_id) {
3433
if let Some(val) = tmp {
3434
return Ok(val.clone());
3435
}
3436
// tmp is None — means declared but no temp value, fall through
3437
}
3438
// Check if it's an unnamed identifier without a temp
3319
- if ident.name.is_none() && !cx.temp.contains_key(&ident.declaration_id) {
3439
+ if ident.name.is_none() && !cx.temp.contains_key(ident.declaration_id) {
3440
return Err(invariant_err(
3441
&format!(
3442
"[Codegen] No value found for temporary, identifier id={}",