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//! Code generation pass: converts a `ReactiveFunction` tree back into a Babel-compatible7//! AST with memoization (useMemoCache) wired in.8//!9//! This is the final pass in the compilation pipeline.10//!11//! Corresponds to `src/ReactiveScopes/CodegenReactiveFunction.ts` in the TS compiler.1213use rustc_hash::{FxHashMap, FxHashSet};1415use react_compiler_ast::common::BaseNode;16use react_compiler_ast::common::Position as AstPosition;17use react_compiler_ast::common::RawNode;18use react_compiler_ast::common::SourceLocation as AstSourceLocation;19use react_compiler_ast::expressions::ArrowFunctionBody;20use react_compiler_ast::expressions::Expression;21use react_compiler_ast::expressions::Identifier as AstIdentifier;22use react_compiler_ast::expressions::{self as ast_expr};23use react_compiler_ast::jsx::JSXAttribute as AstJSXAttribute;24use react_compiler_ast::jsx::JSXAttributeItem;25use react_compiler_ast::jsx::JSXAttributeName;26use react_compiler_ast::jsx::JSXAttributeValue;27use react_compiler_ast::jsx::JSXChild;28use react_compiler_ast::jsx::JSXClosingElement;29use react_compiler_ast::jsx::JSXClosingFragment;30use react_compiler_ast::jsx::JSXElement;31use react_compiler_ast::jsx::JSXElementName;32use react_compiler_ast::jsx::JSXExpressionContainer;33use react_compiler_ast::jsx::JSXExpressionContainerExpr;34use react_compiler_ast::jsx::JSXFragment;35use react_compiler_ast::jsx::JSXIdentifier;36use react_compiler_ast::jsx::JSXMemberExprObject;37use react_compiler_ast::jsx::JSXMemberExpression;38use react_compiler_ast::jsx::JSXNamespacedName;39use react_compiler_ast::jsx::JSXOpeningElement;40use react_compiler_ast::jsx::JSXOpeningFragment;41use react_compiler_ast::jsx::JSXSpreadAttribute;42use react_compiler_ast::jsx::JSXText;43use react_compiler_ast::literals::BooleanLiteral;44use react_compiler_ast::literals::NullLiteral;45use react_compiler_ast::literals::NumericLiteral;46use react_compiler_ast::literals::RegExpLiteral as AstRegExpLiteral;47use react_compiler_ast::literals::StringLiteral;48use react_compiler_ast::literals::TemplateElement;49use react_compiler_ast::literals::TemplateElementValue;50use react_compiler_ast::operators::AssignmentOperator;51use react_compiler_ast::operators::BinaryOperator as AstBinaryOperator;52use react_compiler_ast::operators::LogicalOperator as AstLogicalOperator;53use react_compiler_ast::operators::UnaryOperator as AstUnaryOperator;54use react_compiler_ast::operators::UpdateOperator as AstUpdateOperator;55use react_compiler_ast::patterns::ArrayPattern as AstArrayPattern;56use react_compiler_ast::patterns::ObjectPatternProp;57use react_compiler_ast::patterns::ObjectPatternProperty;58use react_compiler_ast::patterns::PatternLike;59use react_compiler_ast::patterns::RestElement;60use react_compiler_ast::statements::BlockStatement;61use react_compiler_ast::statements::BreakStatement;62use react_compiler_ast::statements::CatchClause;63use react_compiler_ast::statements::ContinueStatement;64use react_compiler_ast::statements::DebuggerStatement;65use react_compiler_ast::statements::Directive;66use react_compiler_ast::statements::DirectiveLiteral;67use react_compiler_ast::statements::DoWhileStatement;68use react_compiler_ast::statements::EmptyStatement;69use react_compiler_ast::statements::ExpressionStatement;70use react_compiler_ast::statements::ForInStatement;71use react_compiler_ast::statements::ForInit;72use react_compiler_ast::statements::ForOfStatement;73use react_compiler_ast::statements::ForStatement;74use react_compiler_ast::statements::FunctionDeclaration;75use react_compiler_ast::statements::IfStatement;76use react_compiler_ast::statements::LabeledStatement;77use react_compiler_ast::statements::ReturnStatement;78use react_compiler_ast::statements::Statement;79use react_compiler_ast::statements::SwitchCase;80use react_compiler_ast::statements::SwitchStatement;81use react_compiler_ast::statements::ThrowStatement;82use react_compiler_ast::statements::TryStatement;83use react_compiler_ast::statements::UnknownStatement;84use react_compiler_ast::statements::VariableDeclaration;85use react_compiler_ast::statements::VariableDeclarationKind;86use react_compiler_ast::statements::VariableDeclarator;87use react_compiler_ast::statements::WhileStatement;88use react_compiler_ast::statements::is_known_statement_type;89use react_compiler_diagnostics::CompilerDiagnostic;90use react_compiler_diagnostics::CompilerDiagnosticDetail;91use react_compiler_diagnostics::CompilerError;92use react_compiler_diagnostics::CompilerErrorDetail;93use react_compiler_diagnostics::ErrorCategory;94use react_compiler_diagnostics::SourceLocation as DiagSourceLocation;95use react_compiler_hir::ArrayElement;96use react_compiler_hir::ArrayPattern;97use react_compiler_hir::BlockId;98use react_compiler_hir::DeclarationId;99use react_compiler_hir::FunctionExpressionType;100use react_compiler_hir::IdentifierId;101use react_compiler_hir::InstructionKind;102use react_compiler_hir::InstructionValue;103use react_compiler_hir::JsxAttribute;104use react_compiler_hir::JsxTag;105use react_compiler_hir::LogicalOperator;106use react_compiler_hir::ObjectPattern;107use react_compiler_hir::ObjectPropertyKey;108use react_compiler_hir::ObjectPropertyOrSpread;109use react_compiler_hir::ObjectPropertyType;110use react_compiler_hir::ParamPattern;111use react_compiler_hir::Pattern;112use react_compiler_hir::Place;113use react_compiler_hir::PlaceOrSpread;114use react_compiler_hir::PrimitiveValue;115use react_compiler_hir::PropertyLiteral;116use react_compiler_hir::ScopeId;117use react_compiler_hir::SpreadPattern;118use react_compiler_hir::environment::Environment;119use react_compiler_hir::reactive::PrunedReactiveScopeBlock;120use react_compiler_hir::reactive::ReactiveBlock;121use react_compiler_hir::reactive::ReactiveFunction;122use react_compiler_hir::reactive::ReactiveInstruction;123use react_compiler_hir::reactive::ReactiveScopeBlock;124use react_compiler_hir::reactive::ReactiveStatement;125use react_compiler_hir::reactive::ReactiveTerminal;126use react_compiler_hir::reactive::ReactiveTerminalTargetKind;127use react_compiler_hir::reactive::ReactiveValue;128129use crate::build_reactive_function::build_reactive_function;130use crate::prune_hoisted_contexts::prune_hoisted_contexts;131use crate::prune_unused_labels::prune_unused_labels;132use crate::prune_unused_lvalues::prune_unused_lvalues;133use crate::rename_variables::rename_variables;134use crate::visitors::ReactiveFunctionVisitor;135use crate::visitors::visit_reactive_function;136137// =============================================================================138// Public API139// =============================================================================140141pub const MEMO_CACHE_SENTINEL: &str = "react.memo_cache_sentinel";142pub const EARLY_RETURN_SENTINEL: &str = "react.early_return_sentinel";143144/// FBT tags whose children get special codegen treatment.145const SINGLE_CHILD_FBT_TAGS: &[&str] = &["fbt:param", "fbs:param"];146147/// Result of code generation for a single function.148pub struct CodegenFunction {149 pub loc: Option<DiagSourceLocation>,150 pub id: Option<AstIdentifier>,151 pub name_hint: Option<String>,152 pub params: Vec<PatternLike>,153 pub body: BlockStatement,154 pub generator: bool,155 pub is_async: bool,156 pub memo_slots_used: u32,157 pub memo_blocks: u32,158 pub memo_values: u32,159 pub pruned_memo_blocks: u32,160 pub pruned_memo_values: u32,161 pub outlined: Vec<OutlinedFunction>,162}163164impl std::fmt::Debug for CodegenFunction {165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {166 f.debug_struct("CodegenFunction")167 .field("memo_slots_used", &self.memo_slots_used)168 .field("memo_blocks", &self.memo_blocks)169 .field("memo_values", &self.memo_values)170 .field("pruned_memo_blocks", &self.pruned_memo_blocks)171 .field("pruned_memo_values", &self.pruned_memo_values)172 .finish()173 }174}175176/// An outlined function extracted during compilation.177pub struct OutlinedFunction {178 pub func: CodegenFunction,179 pub fn_type: Option<react_compiler_hir::ReactFunctionType>,180}181182/// Top-level entry point: generates code for a reactive function.183/// Computes the Fast Refresh source hash used to bust the memo cache when the184/// source file changes. Matches the TS compiler's185/// `createHmac('sha256', code).digest('hex')`: an HMAC-SHA256 keyed by the186/// source code, hashing empty data.187fn source_file_hash(code: &str) -> String {188 hmac_sha256::HMAC::mac(b"", code.as_bytes())189 .iter()190 .map(|b| format!("{b:02x}"))191 .collect()192}193194pub fn codegen_function(195 func: &ReactiveFunction,196 env: &mut Environment,197 unique_identifiers: FxHashSet<String>,198 fbt_operands: FxHashSet<IdentifierId>,199) -> Result<CodegenFunction, CompilerError> {200 let fn_name = func.id.as_deref().unwrap_or("[[ anonymous ]]");201 let mut cx = Context::new(env, fn_name.to_string(), unique_identifiers, fbt_operands);202203 // Fast Refresh: compute source hash and reserve a cache slot if enabled204 let fast_refresh_state: Option<(u32, String)> =205 if cx.env.config.enable_reset_cache_on_source_file_changes == Some(true) {206 if let Some(ref code) = cx.env.code {207 let hash = source_file_hash(code);208 let cache_index = cx.alloc_cache_index(); // Reserve slot 0 for the hash check209 Some((cache_index, hash))210 } else {211 None212 }213 } else {214 None215 };216217 let mut compiled = codegen_reactive_function(&mut cx, func)?;218219 // enableEmitHookGuards: wrap entire function body in try/finally with220 // $dispatcherGuard(PushHookGuard=0) / $dispatcherGuard(PopHookGuard=1).221 // Per-hook-call wrapping is done inline during codegen (CallExpression/MethodCall).222 if cx.env.hook_guard_name.is_some()223 && cx.env.output_mode == react_compiler_hir::environment::OutputMode::Client224 {225 let guard_name = cx.env.hook_guard_name.as_ref().unwrap().clone();226 let body_stmts = std::mem::replace(&mut compiled.body.body, Vec::new());227 compiled.body.body = vec![create_function_body_hook_guard(228 &guard_name,229 body_stmts,230 0,231 1,232 )];233 }234235 let cache_count = compiled.memo_slots_used;236 if cache_count != 0 {237 let mut preface: Vec<Statement> = Vec::new();238 let cache_name = cx.synthesize_name("$");239240 // const $ = useMemoCache(N)241 preface.push(Statement::VariableDeclaration(VariableDeclaration {242 base: BaseNode::typed("VariableDeclaration"),243 declarations: vec![VariableDeclarator {244 base: BaseNode::typed("VariableDeclarator"),245 id: PatternLike::Identifier(make_identifier(&cache_name)),246 init: Some(Box::new(Expression::CallExpression(247 ast_expr::CallExpression {248 base: BaseNode::typed("CallExpression"),249 callee: Box::new(Expression::Identifier(make_identifier("useMemoCache"))),250 arguments: vec![Expression::NumericLiteral(NumericLiteral {251 base: BaseNode::typed("NumericLiteral"),252 value: cache_count as f64,253 extra: None,254 })],255 type_parameters: None,256 type_arguments: None,257 optional: None,258 },259 ))),260 definite: None,261 }],262 kind: VariableDeclarationKind::Const,263 declare: None,264 }));265266 // Fast Refresh: emit cache invalidation check after useMemoCache267 if let Some((cache_index, ref hash)) = fast_refresh_state {268 let index_var = cx.synthesize_name("$i");269 // if ($[cacheIndex] !== "hash") { for (let $i = 0; $i < N; $i += 1) { $[$i] = Symbol.for("react.memo_cache_sentinel"); } $[cacheIndex] = "hash"; }270 preface.push(Statement::IfStatement(IfStatement {271 base: BaseNode::typed("IfStatement"),272 test: Box::new(Expression::BinaryExpression(ast_expr::BinaryExpression {273 base: BaseNode::typed("BinaryExpression"),274 operator: AstBinaryOperator::StrictNeq,275 left: Box::new(Expression::MemberExpression(ast_expr::MemberExpression {276 base: BaseNode::typed("MemberExpression"),277 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),278 property: Box::new(Expression::NumericLiteral(NumericLiteral {279 base: BaseNode::typed("NumericLiteral"),280 value: cache_index as f64,281 extra: None,282 })),283 computed: true,284 })),285 right: Box::new(Expression::StringLiteral(StringLiteral {286 base: BaseNode::typed("StringLiteral"),287 value: hash.clone().into(),288 })),289 })),290 consequent: Box::new(Statement::BlockStatement(BlockStatement {291 base: BaseNode::typed("BlockStatement"),292 body: vec![293 // for (let $i = 0; $i < N; $i += 1) { $[$i] = Symbol.for("react.memo_cache_sentinel"); }294 Statement::ForStatement(ForStatement {295 base: BaseNode::typed("ForStatement"),296 init: Some(Box::new(ForInit::VariableDeclaration(297 VariableDeclaration {298 base: BaseNode::typed("VariableDeclaration"),299 declarations: vec![VariableDeclarator {300 base: BaseNode::typed("VariableDeclarator"),301 id: PatternLike::Identifier(make_identifier(&index_var)),302 init: Some(Box::new(Expression::NumericLiteral(303 NumericLiteral {304 base: BaseNode::typed("NumericLiteral"),305 value: 0.0,306 extra: None,307 },308 ))),309 definite: None,310 }],311 kind: VariableDeclarationKind::Let,312 declare: None,313 },314 ))),315 test: Some(Box::new(Expression::BinaryExpression(316 ast_expr::BinaryExpression {317 base: BaseNode::typed("BinaryExpression"),318 operator: AstBinaryOperator::Lt,319 left: Box::new(Expression::Identifier(make_identifier(320 &index_var,321 ))),322 right: Box::new(Expression::NumericLiteral(NumericLiteral {323 base: BaseNode::typed("NumericLiteral"),324 value: cache_count as f64,325 extra: None,326 })),327 },328 ))),329 update: Some(Box::new(Expression::AssignmentExpression(330 ast_expr::AssignmentExpression {331 base: BaseNode::typed("AssignmentExpression"),332 operator: AssignmentOperator::AddAssign,333 left: Box::new(PatternLike::Identifier(make_identifier(334 &index_var,335 ))),336 right: Box::new(Expression::NumericLiteral(NumericLiteral {337 base: BaseNode::typed("NumericLiteral"),338 value: 1.0,339 extra: None,340 })),341 },342 ))),343 body: Box::new(Statement::BlockStatement(BlockStatement {344 base: BaseNode::typed("BlockStatement"),345 body: vec![Statement::ExpressionStatement(ExpressionStatement {346 base: BaseNode::typed("ExpressionStatement"),347 expression: Box::new(Expression::AssignmentExpression(348 ast_expr::AssignmentExpression {349 base: BaseNode::typed("AssignmentExpression"),350 operator: AssignmentOperator::Assign,351 left: Box::new(PatternLike::MemberExpression(352 ast_expr::MemberExpression {353 base: BaseNode::typed("MemberExpression"),354 object: Box::new(Expression::Identifier(355 make_identifier(&cache_name),356 )),357 property: Box::new(Expression::Identifier(358 make_identifier(&index_var),359 )),360 computed: true,361 },362 )),363 right: Box::new(Expression::CallExpression(364 ast_expr::CallExpression {365 base: BaseNode::typed("CallExpression"),366 callee: Box::new(Expression::MemberExpression(367 ast_expr::MemberExpression {368 base: BaseNode::typed(369 "MemberExpression",370 ),371 object: Box::new(372 Expression::Identifier(373 make_identifier("Symbol"),374 ),375 ),376 property: Box::new(377 Expression::Identifier(378 make_identifier("for"),379 ),380 ),381 computed: false,382 },383 )),384 arguments: vec![Expression::StringLiteral(385 StringLiteral {386 base: BaseNode::typed("StringLiteral"),387 value: MEMO_CACHE_SENTINEL388 .to_string()389 .into(),390 },391 )],392 type_parameters: None,393 type_arguments: None,394 optional: None,395 },396 )),397 },398 )),399 })],400 directives: Vec::new(),401 })),402 }),403 // $[cacheIndex] = "hash"404 Statement::ExpressionStatement(ExpressionStatement {405 base: BaseNode::typed("ExpressionStatement"),406 expression: Box::new(Expression::AssignmentExpression(407 ast_expr::AssignmentExpression {408 base: BaseNode::typed("AssignmentExpression"),409 operator: AssignmentOperator::Assign,410 left: Box::new(PatternLike::MemberExpression(411 ast_expr::MemberExpression {412 base: BaseNode::typed("MemberExpression"),413 object: Box::new(Expression::Identifier(414 make_identifier(&cache_name),415 )),416 property: Box::new(Expression::NumericLiteral(417 NumericLiteral {418 base: BaseNode::typed("NumericLiteral"),419 value: cache_index as f64,420 extra: None,421 },422 )),423 computed: true,424 },425 )),426 right: Box::new(Expression::StringLiteral(StringLiteral {427 base: BaseNode::typed("StringLiteral"),428 value: hash.clone().into(),429 })),430 },431 )),432 }),433 ],434 directives: Vec::new(),435 })),436 alternate: None,437 }));438 }439440 // Insert preface at the beginning of the body441 let mut new_body = preface;442 new_body.append(&mut compiled.body.body);443 compiled.body.body = new_body;444 }445446 // Instrument forget: emit instrumentation call at the top of the function body447 let emit_instrument_forget = cx.env.config.enable_emit_instrument_forget.clone();448 if let Some(ref instrument_config) = emit_instrument_forget {449 if func.id.is_some()450 && cx.env.output_mode == react_compiler_hir::environment::OutputMode::Client451 {452 // Use pre-resolved import names from environment (set by program-level code)453 let instrument_fn_local = cx454 .env455 .instrument_fn_name456 .clone()457 .unwrap_or_else(|| instrument_config.fn_.import_specifier_name.clone());458 let instrument_gating_local = cx.env.instrument_gating_name.clone();459460 // Build the gating condition461 let gating_expr: Option<Expression> =462 instrument_gating_local.map(|name| Expression::Identifier(make_identifier(&name)));463 let global_gating_expr: Option<Expression> = instrument_config464 .global_gating465 .as_ref()466 .map(|g| Expression::Identifier(make_identifier(g)));467468 let if_test = match (gating_expr, global_gating_expr) {469 (Some(gating), Some(global)) => {470 Expression::LogicalExpression(ast_expr::LogicalExpression {471 base: BaseNode::typed("LogicalExpression"),472 operator: AstLogicalOperator::And,473 left: Box::new(global),474 right: Box::new(gating),475 })476 }477 (Some(gating), None) => gating,478 (None, Some(global)) => global,479 (None, None) => unreachable!(480 "InstrumentationConfig requires at least one of gating or globalGating"481 ),482 };483484 let fn_name_str = func.id.as_deref().unwrap_or("");485 let filename_str = cx.env.filename.as_deref().unwrap_or("");486487 let instrument_call = Statement::IfStatement(IfStatement {488 base: BaseNode::typed("IfStatement"),489 test: Box::new(if_test),490 consequent: Box::new(Statement::ExpressionStatement(ExpressionStatement {491 base: BaseNode::typed("ExpressionStatement"),492 expression: Box::new(Expression::CallExpression(ast_expr::CallExpression {493 base: BaseNode::typed("CallExpression"),494 callee: Box::new(Expression::Identifier(make_identifier(495 &instrument_fn_local,496 ))),497 arguments: vec![498 Expression::StringLiteral(StringLiteral {499 base: BaseNode::typed("StringLiteral"),500 value: fn_name_str.to_string().into(),501 }),502 Expression::StringLiteral(StringLiteral {503 base: BaseNode::typed("StringLiteral"),504 value: filename_str.to_string().into(),505 }),506 ],507 type_parameters: None,508 type_arguments: None,509 optional: None,510 })),511 })),512 alternate: None,513 });514 compiled.body.body.insert(0, instrument_call);515 }516 }517518 // Process outlined functions.519 // Use clone (not take) to match TS behavior: getOutlinedFunctions() returns520 // a reference, so outlined functions persist on the environment and are also521 // available to the parent function's codegen. The inner function codegen522 // processes them here, and the parent/top-level codegen processes them again.523 let outlined_entries = cx.env.get_outlined_functions().to_vec();524 let mut outlined: Vec<OutlinedFunction> = Vec::new();525 for entry in outlined_entries {526 let reactive_fn = build_reactive_function(&entry.func, cx.env)?;527 let mut reactive_fn_mut = reactive_fn;528 prune_unused_labels(&mut reactive_fn_mut, cx.env)?;529 prune_unused_lvalues(&mut reactive_fn_mut, cx.env);530 prune_hoisted_contexts(&mut reactive_fn_mut, cx.env)?;531532 let identifiers = rename_variables(&mut reactive_fn_mut, cx.env);533 let mut outlined_cx = Context::new(534 cx.env,535 reactive_fn_mut536 .id537 .as_deref()538 .unwrap_or("[[ anonymous ]]")539 .to_string(),540 identifiers,541 cx.fbt_operands.clone(),542 );543 let codegen = codegen_reactive_function(&mut outlined_cx, &reactive_fn_mut)?;544 outlined.push(OutlinedFunction {545 func: codegen,546 fn_type: entry.fn_type,547 });548 }549 compiled.outlined = outlined;550551 Ok(compiled)552}553554// =============================================================================555// Context556// =============================================================================557558#[derive(Clone)]559enum ExpressionOrJsxText {560 Expression(Expression),561 JsxText(JSXText),562}563564/// The entry a write to [`Temporaries`] displaced, kept so the write can be565/// undone.566///567/// The expression is boxed because `ExpressionOrJsxText` is ~900 bytes (it568/// inlines an `Expression`). Unboxed, the undo log would be a `Vec` of569/// ~900-byte slots that are almost always `Absent`, costing more peak heap than570/// the copy it replaces on shallow functions. Boxed, an entry is 16 bytes and571/// only allocates when a write actually displaces a buffered expression.572enum 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}580581/// A position in a [`Temporaries`] undo log, produced by [`Temporaries::mark`].582#[derive(Clone, Copy)]583struct TempMark(usize);584585/// Expressions buffered for temporaries that have not been emitted yet, plus an586/// undo log allowing a nested block or scope to be codegen'd and its additions587/// discarded.588///589/// The TypeScript implementation snapshots this with `new Map(cx.temp)`, which590/// 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 allocation593/// volume and peak heap.594///595/// `TS CodegenReactiveFunction.codegenBlock` asserts that pre-existing entries596/// are never mutated ("Expected temporary value to be unchanged"), so a597/// snapshot's only job is to discard entries added by the nested block. Because598/// 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 of602/// sync with the map.603#[derive(Default)]604struct Temporaries {605 values: FxHashMap<DeclarationId, Option<ExpressionOrJsxText>>,606 journal: Vec<(DeclarationId, Displaced)>,607}608609impl Temporaries {610 fn get(&self, declaration_id: DeclarationId) -> Option<&Option<ExpressionOrJsxText>> {611 self.values.get(&declaration_id)612 }613614 fn contains_key(&self, declaration_id: DeclarationId) -> bool {615 self.values.contains_key(&declaration_id)616 }617618 /// Buffers `value` for `declaration_id`, journaling the displaced entry.619 /// `HashMap::insert` returns that entry by move, so journaling costs no620 /// 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 }629630 /// Marks the current state, for a later [`Temporaries::rewind`].631 fn mark(&self) -> TempMark {632 TempMark(self.journal.len())633 }634635 /// 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 }652653 /// Hands the buffered expressions to a nested function's context, which may654 /// read them but must not leak its own additions back out.655 ///656 /// The borrower gets a fresh log, so [`Temporaries::reclaim`] can undo657 /// 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 }664665 /// Takes back expressions handed out by [`Temporaries::lend`], discarding666 /// 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}672673struct Context<'env> {674 env: &'env mut Environment,675 #[allow(dead_code)]676 fn_name: String,677 next_cache_index: u32,678 declarations: FxHashSet<DeclarationId>,679 temp: Temporaries,680 object_methods: FxHashMap<681 IdentifierId,682 (683 InstructionValue,684 Option<react_compiler_diagnostics::SourceLocation>,685 ),686 >,687 unique_identifiers: FxHashSet<String>,688 fbt_operands: FxHashSet<IdentifierId>,689 synthesized_names: FxHashMap<String, String>,690}691692impl<'env> Context<'env> {693 fn new(694 env: &'env mut Environment,695 fn_name: String,696 unique_identifiers: FxHashSet<String>,697 fbt_operands: FxHashSet<IdentifierId>,698 ) -> Self {699 Context {700 env,701 fn_name,702 next_cache_index: 0,703 declarations: FxHashSet::default(),704 temp: Temporaries::default(),705 object_methods: FxHashMap::default(),706 unique_identifiers,707 fbt_operands,708 synthesized_names: FxHashMap::default(),709 }710 }711712 fn alloc_cache_index(&mut self) -> u32 {713 let idx = self.next_cache_index;714 self.next_cache_index += 1;715 idx716 }717718 fn declare(&mut self, identifier_id: IdentifierId) {719 let ident = &self.env.identifiers[identifier_id.0 as usize];720 self.declarations.insert(ident.declaration_id);721 }722723 fn has_declared(&self, identifier_id: IdentifierId) -> bool {724 let ident = &self.env.identifiers[identifier_id.0 as usize];725 self.declarations.contains(&ident.declaration_id)726 }727728 fn synthesize_name(&mut self, name: &str) -> String {729 if let Some(prev) = self.synthesized_names.get(name) {730 return prev.clone();731 }732 let mut validated = name.to_string();733 let mut index = 0u32;734 while self.unique_identifiers.contains(&validated) {735 validated = format!("{name}{index}");736 index += 1;737 }738 self.unique_identifiers.insert(validated.clone());739 self.synthesized_names740 .insert(name.to_string(), validated.clone());741 validated742 }743744 fn record_error(&mut self, detail: CompilerErrorDetail) -> Result<(), CompilerError> {745 self.env.record_error(detail)746 }747}748749// =============================================================================750// Core codegen functions751// =============================================================================752753fn codegen_reactive_function(754 cx: &mut Context,755 func: &ReactiveFunction,756) -> Result<CodegenFunction, CompilerError> {757 // Register parameters758 for param in &func.params {759 let place = match param {760 ParamPattern::Place(p) => p,761 ParamPattern::Spread(sp) => &sp.place,762 };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 }767768 let params: Vec<PatternLike> = func769 .params770 .iter()771 .map(|p| convert_parameter(p, cx.env))772 .collect::<Result<_, _>>()?;773 let mut body = codegen_block(cx, &func.body)?;774775 // Add directives776 body.directives = func777 .directives778 .iter()779 .map(|d| Directive {780 base: BaseNode::typed("Directive"),781 value: DirectiveLiteral {782 base: BaseNode::typed("DirectiveLiteral"),783 value: d.clone(),784 },785 })786 .collect();787788 // Remove trailing `return undefined`789 if let Some(last) = body.body.last() {790 if matches!(last, Statement::ReturnStatement(ret) if ret.argument.is_none()) {791 body.body.pop();792 }793 }794795 // Count memo blocks796 let (memo_blocks, memo_values, pruned_memo_blocks, pruned_memo_values) =797 count_memo_blocks(func, cx.env);798799 Ok(CodegenFunction {800 loc: func.loc,801 id: func.id.as_ref().map(|name| make_identifier(name)),802 name_hint: func.name_hint.clone(),803 params,804 body,805 generator: func.generator,806 is_async: func.is_async,807 memo_slots_used: cx.next_cache_index,808 memo_blocks,809 memo_values,810 pruned_memo_blocks,811 pruned_memo_values,812 outlined: Vec::new(),813 })814}815816fn convert_parameter(817 param: &ParamPattern,818 env: &Environment,819) -> Result<PatternLike, CompilerError> {820 match param {821 ParamPattern::Place(place) => Ok(PatternLike::Identifier(convert_identifier(822 place.identifier,823 env,824 )?)),825 ParamPattern::Spread(spread) => Ok(PatternLike::RestElement(RestElement {826 base: BaseNode::typed("RestElement"),827 argument: Box::new(PatternLike::Identifier(convert_identifier(828 spread.place.identifier,829 env,830 )?)),831 type_annotation: None,832 decorators: None,833 })),834 }835}836837// =============================================================================838// Block codegen839// =============================================================================840841fn codegen_block(cx: &mut Context, block: &ReactiveBlock) -> Result<BlockStatement, CompilerError> {842 let mark = cx.temp.mark();843 let result = codegen_block_no_reset(cx, block)?;844 cx.temp.rewind(mark);845 Ok(result)846}847848fn codegen_block_no_reset(849 cx: &mut Context,850 block: &ReactiveBlock,851) -> Result<BlockStatement, CompilerError> {852 let mut statements: Vec<Statement> = Vec::new();853 for item in block {854 match item {855 ReactiveStatement::Instruction(instr) => {856 if let Some(stmt) = codegen_instruction_nullable(cx, instr)? {857 statements.push(stmt);858 }859 }860 ReactiveStatement::PrunedScope(PrunedReactiveScopeBlock { instructions, .. }) => {861 let scope_block = codegen_block_no_reset(cx, instructions)?;862 statements.extend(scope_block.body);863 }864 ReactiveStatement::Scope(ReactiveScopeBlock {865 scope,866 instructions,867 }) => {868 let mark = cx.temp.mark();869 codegen_reactive_scope(cx, &mut statements, *scope, instructions)?;870 cx.temp.rewind(mark);871 }872 ReactiveStatement::Terminal(term_stmt) => {873 let stmt = codegen_terminal(cx, &term_stmt.terminal)?;874 let Some(stmt) = stmt else {875 continue;876 };877 if let Some(ref label) = term_stmt.label {878 if !label.implicit {879 let inner = if let Statement::BlockStatement(bs) = &stmt {880 if bs.body.len() == 1 {881 bs.body[0].clone()882 } else {883 stmt884 }885 } else {886 stmt887 };888 statements.push(Statement::LabeledStatement(LabeledStatement {889 base: BaseNode::typed("LabeledStatement"),890 label: make_identifier(&codegen_label(label.id)),891 body: Box::new(inner),892 }));893 } else if let Statement::BlockStatement(bs) = stmt {894 statements.extend(bs.body);895 } else {896 statements.push(stmt);897 }898 } else if let Statement::BlockStatement(bs) = stmt {899 statements.extend(bs.body);900 } else {901 statements.push(stmt);902 }903 }904 }905 }906 Ok(BlockStatement {907 base: BaseNode::typed("BlockStatement"),908 body: statements,909 directives: Vec::new(),910 })911}912913// =============================================================================914// Reactive scope codegen (memoization)915// =============================================================================916917fn codegen_reactive_scope(918 cx: &mut Context,919 statements: &mut Vec<Statement>,920 scope_id: ScopeId,921 block: &ReactiveBlock,922) -> Result<(), CompilerError> {923 // Clone scope data upfront to avoid holding a borrow on cx.env924 let scope_deps = cx.env.scopes[scope_id.0 as usize].dependencies.clone();925 let scope_decls = cx.env.scopes[scope_id.0 as usize].declarations.clone();926 let scope_reassignments = cx.env.scopes[scope_id.0 as usize].reassignments.clone();927928 let mut cache_store_stmts: Vec<Statement> = Vec::new();929 let mut cache_load_stmts: Vec<Statement> = Vec::new();930 let mut cache_loads: Vec<(AstIdentifier, u32, Expression)> = Vec::new();931 let mut change_exprs: Vec<Expression> = Vec::new();932933 // Sort dependencies934 let mut deps = scope_deps;935 deps.sort_by(|a, b| compare_scope_dependency(a, b, cx.env));936937 for dep in &deps {938 let index = cx.alloc_cache_index();939 let cache_name = cx.synthesize_name("$");940 let comparison = Expression::BinaryExpression(ast_expr::BinaryExpression {941 base: BaseNode::typed("BinaryExpression"),942 operator: AstBinaryOperator::StrictNeq,943 left: Box::new(Expression::MemberExpression(ast_expr::MemberExpression {944 base: BaseNode::typed("MemberExpression"),945 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),946 property: Box::new(Expression::NumericLiteral(NumericLiteral {947 base: BaseNode::typed("NumericLiteral"),948 value: index as f64,949 extra: None,950 })),951 computed: true,952 })),953 right: Box::new(codegen_dependency(cx, dep)?),954 });955 change_exprs.push(comparison);956957 // Store dependency value into cache958 let dep_value = codegen_dependency(cx, dep)?;959 cache_store_stmts.push(Statement::ExpressionStatement(ExpressionStatement {960 base: BaseNode::typed("ExpressionStatement"),961 expression: Box::new(Expression::AssignmentExpression(962 ast_expr::AssignmentExpression {963 base: BaseNode::typed("AssignmentExpression"),964 operator: AssignmentOperator::Assign,965 left: Box::new(PatternLike::MemberExpression(ast_expr::MemberExpression {966 base: BaseNode::typed("MemberExpression"),967 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),968 property: Box::new(Expression::NumericLiteral(NumericLiteral {969 base: BaseNode::typed("NumericLiteral"),970 value: index as f64,971 extra: None,972 })),973 computed: true,974 })),975 right: Box::new(dep_value),976 },977 )),978 }));979 }980981 let mut first_output_index: Option<u32> = None;982983 // Sort declarations984 let mut decls = scope_decls;985 decls.sort_by(|(_id_a, a), (_id_b, b)| compare_scope_declaration(a, b, cx.env));986987 for (_ident_id, decl) in &decls {988 let index = cx.alloc_cache_index();989 if first_output_index.is_none() {990 first_output_index = Some(index);991 }992993 let ident = &cx.env.identifiers[decl.identifier.0 as usize];994 invariant(995 ident.name.is_some(),996 &format!(997 "Expected scope declaration identifier to be named, id={}",998 decl.identifier.0999 ),1000 None,1001 )?;10021003 let name = convert_identifier(decl.identifier, cx.env)?;1004 if !cx.has_declared(decl.identifier) {1005 statements.push(Statement::VariableDeclaration(VariableDeclaration {1006 base: BaseNode::typed("VariableDeclaration"),1007 declarations: vec![make_var_declarator(1008 PatternLike::Identifier(name.clone()),1009 None,1010 )],1011 kind: VariableDeclarationKind::Let,1012 declare: None,1013 }));1014 }1015 cache_loads.push((name.clone(), index, Expression::Identifier(name.clone())));1016 cx.declare(decl.identifier);1017 }10181019 for reassignment_id in scope_reassignments {1020 let index = cx.alloc_cache_index();1021 if first_output_index.is_none() {1022 first_output_index = Some(index);1023 }1024 let name = convert_identifier(reassignment_id, cx.env)?;1025 cache_loads.push((name.clone(), index, Expression::Identifier(name)));1026 }10271028 // Build test condition1029 let test_condition = if change_exprs.is_empty() {1030 let first_idx = first_output_index.ok_or_else(|| {1031 invariant_err("Expected scope to have at least one declaration", None)1032 })?;1033 let cache_name = cx.synthesize_name("$");1034 Expression::BinaryExpression(ast_expr::BinaryExpression {1035 base: BaseNode::typed("BinaryExpression"),1036 operator: AstBinaryOperator::StrictEq,1037 left: Box::new(Expression::MemberExpression(ast_expr::MemberExpression {1038 base: BaseNode::typed("MemberExpression"),1039 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),1040 property: Box::new(Expression::NumericLiteral(NumericLiteral {1041 base: BaseNode::typed("NumericLiteral"),1042 value: first_idx as f64,1043 extra: None,1044 })),1045 computed: true,1046 })),1047 right: Box::new(symbol_for(MEMO_CACHE_SENTINEL)),1048 })1049 } else {1050 change_exprs1051 .into_iter()1052 .reduce(|acc, expr| {1053 Expression::LogicalExpression(ast_expr::LogicalExpression {1054 base: BaseNode::typed("LogicalExpression"),1055 operator: AstLogicalOperator::Or,1056 left: Box::new(acc),1057 right: Box::new(expr),1058 })1059 })1060 .unwrap()1061 };10621063 let mut computation_block = codegen_block(cx, block)?;10641065 // Build cache store and load statements for declarations1066 for (name, index, value) in &cache_loads {1067 let cache_name = cx.synthesize_name("$");1068 cache_store_stmts.push(Statement::ExpressionStatement(ExpressionStatement {1069 base: BaseNode::typed("ExpressionStatement"),1070 expression: Box::new(Expression::AssignmentExpression(1071 ast_expr::AssignmentExpression {1072 base: BaseNode::typed("AssignmentExpression"),1073 operator: AssignmentOperator::Assign,1074 left: Box::new(PatternLike::MemberExpression(ast_expr::MemberExpression {1075 base: BaseNode::typed("MemberExpression"),1076 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),1077 property: Box::new(Expression::NumericLiteral(NumericLiteral {1078 base: BaseNode::typed("NumericLiteral"),1079 value: *index as f64,1080 extra: None,1081 })),1082 computed: true,1083 })),1084 right: Box::new(value.clone()),1085 },1086 )),1087 }));1088 cache_load_stmts.push(Statement::ExpressionStatement(ExpressionStatement {1089 base: BaseNode::typed("ExpressionStatement"),1090 expression: Box::new(Expression::AssignmentExpression(1091 ast_expr::AssignmentExpression {1092 base: BaseNode::typed("AssignmentExpression"),1093 operator: AssignmentOperator::Assign,1094 left: Box::new(PatternLike::Identifier(name.clone())),1095 right: Box::new(Expression::MemberExpression(ast_expr::MemberExpression {1096 base: BaseNode::typed("MemberExpression"),1097 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),1098 property: Box::new(Expression::NumericLiteral(NumericLiteral {1099 base: BaseNode::typed("NumericLiteral"),1100 value: *index as f64,1101 extra: None,1102 })),1103 computed: true,1104 })),1105 },1106 )),1107 }));1108 }11091110 computation_block.body.extend(cache_store_stmts);11111112 let memo_stmt = Statement::IfStatement(IfStatement {1113 base: BaseNode::typed("IfStatement"),1114 test: Box::new(test_condition),1115 consequent: Box::new(Statement::BlockStatement(computation_block)),1116 alternate: Some(Box::new(Statement::BlockStatement(BlockStatement {1117 base: BaseNode::typed("BlockStatement"),1118 body: cache_load_stmts,1119 directives: Vec::new(),1120 }))),1121 });1122 statements.push(memo_stmt);11231124 // Handle early return1125 let early_return_value = cx.env.scopes[scope_id.0 as usize]1126 .early_return_value1127 .clone();1128 if let Some(ref early_return) = early_return_value {1129 let early_ident = &cx.env.identifiers[early_return.value.0 as usize];1130 let name = match &early_ident.name {1131 Some(react_compiler_hir::IdentifierName::Named(n)) => n.clone(),1132 Some(react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(),1133 None => {1134 return Err(invariant_err(1135 "Expected early return value to be promoted to a named variable",1136 early_return.loc,1137 ));1138 }1139 };1140 statements.push(Statement::IfStatement(IfStatement {1141 base: BaseNode::typed("IfStatement"),1142 test: Box::new(Expression::BinaryExpression(ast_expr::BinaryExpression {1143 base: BaseNode::typed("BinaryExpression"),1144 operator: AstBinaryOperator::StrictNeq,1145 left: Box::new(Expression::Identifier(make_identifier(&name))),1146 right: Box::new(symbol_for(EARLY_RETURN_SENTINEL)),1147 })),1148 consequent: Box::new(Statement::BlockStatement(BlockStatement {1149 base: BaseNode::typed("BlockStatement"),1150 body: vec![Statement::ReturnStatement(ReturnStatement {1151 base: BaseNode::typed("ReturnStatement"),1152 argument: Some(Box::new(Expression::Identifier(make_identifier(&name)))),1153 })],1154 directives: Vec::new(),1155 })),1156 alternate: None,1157 }));1158 }11591160 Ok(())1161}11621163// =============================================================================1164// Terminal codegen1165// =============================================================================11661167fn codegen_terminal(1168 cx: &mut Context,1169 terminal: &ReactiveTerminal,1170) -> Result<Option<Statement>, CompilerError> {1171 match terminal {1172 ReactiveTerminal::Break {1173 target,1174 target_kind,1175 loc,1176 ..1177 } => {1178 if *target_kind == ReactiveTerminalTargetKind::Implicit {1179 return Ok(None);1180 }1181 Ok(Some(Statement::BreakStatement(BreakStatement {1182 base: base_node_with_loc("BreakStatement", *loc),1183 label: if *target_kind == ReactiveTerminalTargetKind::Labeled {1184 Some(make_identifier(&codegen_label(*target)))1185 } else {1186 None1187 },1188 })))1189 }1190 ReactiveTerminal::Continue {1191 target,1192 target_kind,1193 loc,1194 ..1195 } => {1196 if *target_kind == ReactiveTerminalTargetKind::Implicit {1197 return Ok(None);1198 }1199 Ok(Some(Statement::ContinueStatement(ContinueStatement {1200 base: base_node_with_loc("ContinueStatement", *loc),1201 label: if *target_kind == ReactiveTerminalTargetKind::Labeled {1202 Some(make_identifier(&codegen_label(*target)))1203 } else {1204 None1205 },1206 })))1207 }1208 ReactiveTerminal::Return { value, loc, .. } => {1209 let expr = codegen_place_to_expression(cx, value)?;1210 if let Expression::Identifier(ref ident) = expr {1211 if ident.name == "undefined" {1212 return Ok(Some(Statement::ReturnStatement(ReturnStatement {1213 base: base_node_with_loc("ReturnStatement", *loc),1214 argument: None,1215 })));1216 }1217 }1218 Ok(Some(Statement::ReturnStatement(ReturnStatement {1219 base: base_node_with_loc("ReturnStatement", *loc),1220 argument: Some(Box::new(expr)),1221 })))1222 }1223 ReactiveTerminal::Throw { value, loc, .. } => {1224 let expr = codegen_place_to_expression(cx, value)?;1225 Ok(Some(Statement::ThrowStatement(ThrowStatement {1226 base: base_node_with_loc("ThrowStatement", *loc),1227 argument: Box::new(expr),1228 })))1229 }1230 ReactiveTerminal::If {1231 test,1232 consequent,1233 alternate,1234 loc,1235 ..1236 } => {1237 let test_expr = codegen_place_to_expression(cx, test)?;1238 let consequent_block = codegen_block(cx, consequent)?;1239 let alternate_stmt = if let Some(alt) = alternate {1240 let block = codegen_block(cx, alt)?;1241 if block.body.is_empty() {1242 None1243 } else {1244 Some(Box::new(Statement::BlockStatement(block)))1245 }1246 } else {1247 None1248 };1249 Ok(Some(Statement::IfStatement(IfStatement {1250 base: base_node_with_loc("IfStatement", *loc),1251 test: Box::new(test_expr),1252 consequent: Box::new(Statement::BlockStatement(consequent_block)),1253 alternate: alternate_stmt,1254 })))1255 }1256 ReactiveTerminal::Switch {1257 test, cases, loc, ..1258 } => {1259 let test_expr = codegen_place_to_expression(cx, test)?;1260 let switch_cases: Vec<SwitchCase> = cases1261 .iter()1262 .map(|case| {1263 let test = case1264 .test1265 .as_ref()1266 .map(|t| codegen_place_to_expression(cx, t))1267 .transpose()?;1268 let block = case1269 .block1270 .as_ref()1271 .map(|b| codegen_block(cx, b))1272 .transpose()?;1273 let consequent = match block {1274 Some(b) if b.body.is_empty() => Vec::new(),1275 Some(b) => vec![Statement::BlockStatement(b)],1276 None => Vec::new(),1277 };1278 Ok(SwitchCase {1279 base: BaseNode::typed("SwitchCase"),1280 test: test.map(Box::new),1281 consequent,1282 })1283 })1284 .collect::<Result<_, CompilerError>>()?;1285 Ok(Some(Statement::SwitchStatement(SwitchStatement {1286 base: base_node_with_loc("SwitchStatement", *loc),1287 discriminant: Box::new(test_expr),1288 cases: switch_cases,1289 })))1290 }1291 ReactiveTerminal::DoWhile {1292 loop_block,1293 test,1294 loc,1295 ..1296 } => {1297 let test_expr = codegen_instruction_value_to_expression(cx, test)?;1298 let body = codegen_block(cx, loop_block)?;1299 Ok(Some(Statement::DoWhileStatement(DoWhileStatement {1300 base: base_node_with_loc("DoWhileStatement", *loc),1301 test: Box::new(test_expr),1302 body: Box::new(Statement::BlockStatement(body)),1303 })))1304 }1305 ReactiveTerminal::While {1306 test,1307 loop_block,1308 loc,1309 ..1310 } => {1311 let test_expr = codegen_instruction_value_to_expression(cx, test)?;1312 let body = codegen_block(cx, loop_block)?;1313 Ok(Some(Statement::WhileStatement(WhileStatement {1314 base: base_node_with_loc("WhileStatement", *loc),1315 test: Box::new(test_expr),1316 body: Box::new(Statement::BlockStatement(body)),1317 })))1318 }1319 ReactiveTerminal::For {1320 init,1321 test,1322 update,1323 loop_block,1324 loc,1325 ..1326 } => {1327 let init_val = codegen_for_init(cx, init)?;1328 let test_expr = codegen_instruction_value_to_expression(cx, test)?;1329 let update_expr = update1330 .as_ref()1331 .map(|u| codegen_instruction_value_to_expression(cx, u))1332 .transpose()?;1333 let body = codegen_block(cx, loop_block)?;1334 Ok(Some(Statement::ForStatement(ForStatement {1335 base: base_node_with_loc("ForStatement", *loc),1336 init: init_val.map(|v| Box::new(v)),1337 test: Some(Box::new(test_expr)),1338 update: update_expr.map(Box::new),1339 body: Box::new(Statement::BlockStatement(body)),1340 })))1341 }1342 ReactiveTerminal::ForIn {1343 init,1344 loop_block,1345 loc,1346 ..1347 } => codegen_for_in(cx, init, loop_block, *loc),1348 ReactiveTerminal::ForOf {1349 init,1350 test,1351 loop_block,1352 loc,1353 ..1354 } => codegen_for_of(cx, init, test, loop_block, *loc),1355 ReactiveTerminal::Label { block, .. } => {1356 let body = codegen_block(cx, block)?;1357 Ok(Some(Statement::BlockStatement(body)))1358 }1359 ReactiveTerminal::Try {1360 block,1361 handler_binding,1362 handler,1363 loc,1364 ..1365 } => {1366 let catch_param = match handler_binding.as_ref() {1367 Some(binding) => {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,1374 )?))1375 }1376 None => None,1377 };1378 let try_block = codegen_block(cx, block)?;1379 let handler_block = codegen_block(cx, handler)?;1380 Ok(Some(Statement::TryStatement(TryStatement {1381 base: base_node_with_loc("TryStatement", *loc),1382 block: try_block,1383 handler: Some(CatchClause {1384 base: BaseNode::typed("CatchClause"),1385 param: catch_param,1386 body: handler_block,1387 }),1388 finalizer: None,1389 })))1390 }1391 }1392}13931394fn codegen_for_in(1395 cx: &mut Context,1396 init: &ReactiveValue,1397 loop_block: &ReactiveBlock,1398 loc: Option<DiagSourceLocation>,1399) -> Result<Option<Statement>, CompilerError> {1400 let ReactiveValue::SequenceExpression { instructions, .. } = init else {1401 return Err(invariant_err(1402 "Expected a sequence expression init for for..in",1403 None,1404 ));1405 };1406 if instructions.len() != 2 {1407 cx.record_error(CompilerErrorDetail {1408 category: ErrorCategory::Todo,1409 reason: "Support non-trivial for..in inits".to_string(),1410 description: None,1411 loc,1412 suggestions: None,1413 })?;1414 return Ok(Some(Statement::EmptyStatement(EmptyStatement {1415 base: BaseNode::typed("EmptyStatement"),1416 })));1417 }1418 let iterable_collection = &instructions[0];1419 let iterable_item = &instructions[1];1420 let instr_value = get_instruction_value(&iterable_item.value)?;1421 let (lval, var_decl_kind) = extract_for_in_of_lval(cx, instr_value, "for..in", loc)?;1422 let right = codegen_instruction_value_to_expression(cx, &iterable_collection.value)?;1423 let body = codegen_block(cx, loop_block)?;1424 Ok(Some(Statement::ForInStatement(ForInStatement {1425 base: base_node_with_loc("ForInStatement", loc),1426 left: Box::new(1427 react_compiler_ast::statements::ForInOfLeft::VariableDeclaration(VariableDeclaration {1428 base: BaseNode::typed("VariableDeclaration"),1429 declarations: vec![VariableDeclarator {1430 base: BaseNode::typed("VariableDeclarator"),1431 id: lval,1432 init: None,1433 definite: None,1434 }],1435 kind: var_decl_kind,1436 declare: None,1437 }),1438 ),1439 right: Box::new(right),1440 body: Box::new(Statement::BlockStatement(body)),1441 })))1442}14431444fn codegen_for_of(1445 cx: &mut Context,1446 init: &ReactiveValue,1447 test: &ReactiveValue,1448 loop_block: &ReactiveBlock,1449 loc: Option<DiagSourceLocation>,1450) -> Result<Option<Statement>, CompilerError> {1451 // Validate init is SequenceExpression with single GetIterator instruction1452 let ReactiveValue::SequenceExpression {1453 instructions: init_instrs,1454 ..1455 } = init1456 else {1457 return Err(invariant_err(1458 "Expected a sequence expression init for for..of",1459 None,1460 ));1461 };1462 if init_instrs.len() != 1 {1463 return Err(invariant_err(1464 "Expected a single-expression sequence expression init for for..of",1465 None,1466 ));1467 }1468 let get_iter_value = get_instruction_value(&init_instrs[0].value)?;1469 let InstructionValue::GetIterator { collection, .. } = get_iter_value else {1470 return Err(invariant_err("Expected GetIterator in for..of init", None));1471 };14721473 let ReactiveValue::SequenceExpression {1474 instructions: test_instrs,1475 ..1476 } = test1477 else {1478 return Err(invariant_err(1479 "Expected a sequence expression test for for..of",1480 None,1481 ));1482 };1483 if test_instrs.len() != 2 {1484 cx.record_error(CompilerErrorDetail {1485 category: ErrorCategory::Todo,1486 reason: "Support non-trivial for..of inits".to_string(),1487 description: None,1488 loc,1489 suggestions: None,1490 })?;1491 return Ok(Some(Statement::EmptyStatement(EmptyStatement {1492 base: BaseNode::typed("EmptyStatement"),1493 })));1494 }1495 let iterable_item = &test_instrs[1];1496 let instr_value = get_instruction_value(&iterable_item.value)?;1497 let (lval, var_decl_kind) = extract_for_in_of_lval(cx, instr_value, "for..of", loc)?;14981499 let right = codegen_place_to_expression(cx, collection)?;1500 let body = codegen_block(cx, loop_block)?;1501 Ok(Some(Statement::ForOfStatement(ForOfStatement {1502 base: base_node_with_loc("ForOfStatement", loc),1503 left: Box::new(1504 react_compiler_ast::statements::ForInOfLeft::VariableDeclaration(VariableDeclaration {1505 base: BaseNode::typed("VariableDeclaration"),1506 declarations: vec![VariableDeclarator {1507 base: BaseNode::typed("VariableDeclarator"),1508 id: lval,1509 init: None,1510 definite: None,1511 }],1512 kind: var_decl_kind,1513 declare: None,1514 }),1515 ),1516 right: Box::new(right),1517 body: Box::new(Statement::BlockStatement(body)),1518 is_await: false,1519 })))1520}15211522/// Extract lval and declaration kind from a for-in/for-of iterable item instruction.1523fn extract_for_in_of_lval(1524 cx: &mut Context,1525 instr_value: &InstructionValue,1526 context_name: &str,1527 loc: Option<DiagSourceLocation>,1528) -> Result<(PatternLike, VariableDeclarationKind), CompilerError> {1529 let (lval, kind) = match instr_value {1530 InstructionValue::StoreLocal { lvalue, .. } => (1531 codegen_lvalue(cx, &LvalueRef::Place(&lvalue.place))?,1532 lvalue.kind,1533 ),1534 InstructionValue::Destructure { lvalue, .. } => (1535 codegen_lvalue(cx, &LvalueRef::Pattern(&lvalue.pattern))?,1536 lvalue.kind,1537 ),1538 InstructionValue::StoreContext { .. } => {1539 cx.record_error(CompilerErrorDetail {1540 category: ErrorCategory::Todo,1541 reason: format!("Support non-trivial {} inits", context_name),1542 description: None,1543 loc,1544 suggestions: None,1545 })?;1546 return Ok((1547 PatternLike::Identifier(make_identifier("_")),1548 VariableDeclarationKind::Let,1549 ));1550 }1551 _ => {1552 return Err(invariant_err(1553 &format!(1554 "Expected a StoreLocal or Destructure in {} collection, found {:?}",1555 context_name,1556 std::mem::discriminant(instr_value)1557 ),1558 None,1559 ));1560 }1561 };1562 let var_decl_kind = match kind {1563 InstructionKind::Const => VariableDeclarationKind::Const,1564 InstructionKind::Let => VariableDeclarationKind::Let,1565 _ => {1566 return Err(invariant_err(1567 &format!(1568 "Unexpected {:?} variable in {} collection",1569 kind, context_name1570 ),1571 None,1572 ));1573 }1574 };1575 Ok((lval, var_decl_kind))1576}15771578fn codegen_for_init(1579 cx: &mut Context,1580 init: &ReactiveValue,1581) -> Result<Option<ForInit>, CompilerError> {1582 if let ReactiveValue::SequenceExpression { instructions, .. } = init {1583 let block_items: Vec<ReactiveStatement> = instructions1584 .iter()1585 .map(|i| ReactiveStatement::Instruction(i.clone()))1586 .collect();1587 let body = codegen_block(cx, &block_items)?.body;1588 let mut declarators: Vec<VariableDeclarator> = Vec::new();1589 let mut kind = VariableDeclarationKind::Const;1590 for instr in body {1591 // Check if this is an assignment that can be folded into the last declarator1592 if let Statement::ExpressionStatement(ref expr_stmt) = instr {1593 if let Expression::AssignmentExpression(ref assign) = *expr_stmt.expression {1594 if matches!(assign.operator, AssignmentOperator::Assign) {1595 if let PatternLike::Identifier(ref left_ident) = *assign.left {1596 if let Some(top) = declarators.last_mut() {1597 if let PatternLike::Identifier(ref top_ident) = top.id {1598 if top_ident.name == left_ident.name && top.init.is_none() {1599 top.init = Some(assign.right.clone());1600 continue;1601 }1602 }1603 }1604 }1605 }1606 }1607 }16081609 if let Statement::VariableDeclaration(var_decl) = instr {1610 match var_decl.kind {1611 VariableDeclarationKind::Let | VariableDeclarationKind::Const => {}1612 _ => {1613 return Err(invariant_err(1614 "Expected a let or const variable declaration",1615 None,1616 ));1617 }1618 }1619 if matches!(var_decl.kind, VariableDeclarationKind::Let) {1620 kind = VariableDeclarationKind::Let;1621 }1622 declarators.extend(var_decl.declarations);1623 } else {1624 let stmt_type = get_statement_type_name(&instr);1625 let stmt_loc = get_statement_loc(&instr);1626 let reason = "Expected a variable declaration".to_string();1627 let mut err = CompilerError::new();1628 err.push_diagnostic(1629 CompilerDiagnostic::new(1630 ErrorCategory::Invariant,1631 reason.clone(),1632 Some(format!("Got {}", stmt_type)),1633 )1634 .with_detail(CompilerDiagnosticDetail::Error {1635 loc: stmt_loc,1636 message: Some(reason),1637 identifier_name: None,1638 }),1639 );1640 return Err(err);1641 }1642 }1643 if declarators.is_empty() {1644 return Err(invariant_err(1645 "Expected a variable declaration in for-init",1646 None,1647 ));1648 }1649 Ok(Some(ForInit::VariableDeclaration(VariableDeclaration {1650 base: BaseNode::typed("VariableDeclaration"),1651 declarations: declarators,1652 kind,1653 declare: None,1654 })))1655 } else {1656 let expr = codegen_instruction_value_to_expression(cx, init)?;1657 Ok(Some(ForInit::Expression(Box::new(expr))))1658 }1659}16601661// =============================================================================1662// Instruction codegen1663// =============================================================================16641665/// How statement-position codegen disposes of an `UnsupportedNode`'s1666/// `original_node`. See [`codegen_unsupported_original_node`].1667enum UnsupportedOriginalNode {1668 /// Emit this statement directly (early return).1669 Statement(Statement),1670 /// Flow through the general expression codegen path so the instruction's1671 /// lvalue temporary is bound/registered.1672 ExpressionCodegen,1673}16741675/// Discriminate an `UnsupportedNode`'s `original_node` by its `type` tag.1676///1677/// Lowering serializes typed `Expression`/`Statement`/`PatternLike` bailout1678/// nodes, plus the raw nodes of `Statement::Unknown` (whose tags are1679/// unmodeled by construction). Dispatch accordingly:1680///1681/// - Modeled statement tag: parse the typed statement and emit it directly.1682/// A parse failure here is a serialize/deserialize asymmetry, surfaced as1683/// an invariant rather than degraded.1684/// - Tag parseable as `Expression` or `PatternLike` (both enums are strict,1685/// no catch-all): expression codegen. Patterns (e.g. `ObjectPattern`1686/// destructuring targets) keep their existing placeholder fallback there.1687/// - Anything else is an unmodeled tag, producible only by the1688/// unknown-statement lowering bailout — i.e. it came from a statement1689/// position — so preserve it verbatim as `Statement::Unknown`, matching1690/// the TS codegen's `return node` for non-expressions.1691fn codegen_unsupported_original_node(1692 node: &serde_json::Value,1693) -> Result<UnsupportedOriginalNode, CompilerError> {1694 let tag = node.get("type").and_then(serde_json::Value::as_str);1695 if tag.is_some_and(is_known_statement_type) {1696 let stmt: Statement = serde_json::from_value(node.clone()).map_err(|e| {1697 invariant_err(1698 &format!("Failed to deserialize original AST node: {}", e),1699 None,1700 )1701 })?;1702 return Ok(UnsupportedOriginalNode::Statement(stmt));1703 }1704 if serde_json::from_value::<Expression>(node.clone()).is_ok()1705 || serde_json::from_value::<PatternLike>(node.clone()).is_ok()1706 {1707 return Ok(UnsupportedOriginalNode::ExpressionCodegen);1708 }1709 let unknown = UnknownStatement::from_raw(RawNode::from_value(node)).map_err(|e| {1710 invariant_err(1711 &format!("Failed to read unsupported original AST node: {}", e),1712 None,1713 )1714 })?;1715 Ok(UnsupportedOriginalNode::Statement(Statement::Unknown(1716 unknown,1717 )))1718}17191720fn codegen_instruction_nullable(1721 cx: &mut Context,1722 instr: &ReactiveInstruction,1723) -> Result<Option<Statement>, CompilerError> {1724 // Only check specific InstructionValue kinds for the base Instruction variant1725 if let ReactiveValue::Instruction(ref value) = instr.value {1726 match value {1727 InstructionValue::StoreLocal { .. }1728 | InstructionValue::StoreContext { .. }1729 | InstructionValue::Destructure { .. }1730 | InstructionValue::DeclareLocal { .. }1731 | InstructionValue::DeclareContext { .. } => {1732 return codegen_store_or_declare(cx, instr, value);1733 }1734 InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => {1735 return Ok(None);1736 }1737 InstructionValue::Debugger { .. } => {1738 return Ok(Some(Statement::DebuggerStatement(DebuggerStatement {1739 base: base_node_with_loc("DebuggerStatement", instr.loc),1740 })));1741 }1742 InstructionValue::UnsupportedNode {1743 original_node: Some(node),1744 ..1745 } => {1746 // Statement-vs-expression discrimination must be explicit by1747 // `type` tag: `Statement`'s deserializer has a tolerant1748 // `Statement::Unknown` catch-all, so "does it deserialize as1749 // a Statement?" succeeds for ANY tagged object and would1750 // emit expression nodes as raw statements, orphaning their1751 // lvalue temporaries (the regression the explicit dispatch1752 // below prevents; TS codegen's equivalent check is1753 // `if (!t.isExpression(node)) return node; value = node`).1754 match codegen_unsupported_original_node(node)? {1755 UnsupportedOriginalNode::Statement(stmt) => return Ok(Some(stmt)),1756 UnsupportedOriginalNode::ExpressionCodegen => {1757 // Expression (or pattern) node — fall through to the1758 // general codegen path which handles lvalue binding1759 // and temporary registration.1760 }1761 }1762 }1763 InstructionValue::ObjectMethod { loc, .. } => {1764 invariant(1765 instr.lvalue.is_some(),1766 "Expected object methods to have a temp lvalue",1767 None,1768 )?;1769 let lvalue = instr.lvalue.as_ref().unwrap();1770 cx.object_methods1771 .insert(lvalue.identifier, (value.clone(), *loc));1772 return Ok(None);1773 }1774 _ => {} // fall through to general codegen1775 }1776 }1777 // General case: codegen the full ReactiveValue1778 let expr_value = codegen_instruction_value(cx, &instr.value)?;1779 let stmt = codegen_instruction(cx, instr, expr_value)?;1780 if matches!(stmt, Statement::EmptyStatement(_)) {1781 Ok(None)1782 } else {1783 Ok(Some(stmt))1784 }1785}17861787fn codegen_store_or_declare(1788 cx: &mut Context,1789 instr: &ReactiveInstruction,1790 value: &InstructionValue,1791) -> Result<Option<Statement>, CompilerError> {1792 match value {1793 InstructionValue::StoreLocal {1794 lvalue, value: val, ..1795 } => {1796 let mut kind = lvalue.kind;1797 if cx.has_declared(lvalue.place.identifier) {1798 kind = InstructionKind::Reassign;1799 }1800 let rhs = codegen_place_to_expression(cx, val)?;1801 emit_store(cx, instr, kind, &LvalueRef::Place(&lvalue.place), Some(rhs))1802 }1803 InstructionValue::StoreContext {1804 lvalue, value: val, ..1805 } => {1806 let rhs = codegen_place_to_expression(cx, val)?;1807 emit_store(1808 cx,1809 instr,1810 lvalue.kind,1811 &LvalueRef::Place(&lvalue.place),1812 Some(rhs),1813 )1814 }1815 InstructionValue::DeclareLocal { lvalue, .. }1816 | InstructionValue::DeclareContext { lvalue, .. } => {1817 if cx.has_declared(lvalue.place.identifier) {1818 return Ok(None);1819 }1820 emit_store(1821 cx,1822 instr,1823 lvalue.kind,1824 &LvalueRef::Place(&lvalue.place),1825 None,1826 )1827 }1828 InstructionValue::Destructure {1829 lvalue, value: val, ..1830 } => {1831 let kind = lvalue.kind;1832 // Register temporaries for unnamed pattern operands1833 for place in react_compiler_hir::visitors::each_pattern_operand(&lvalue.pattern) {1834 let ident = &cx.env.identifiers[place.identifier.0 as usize];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)?;1842 emit_store(1843 cx,1844 instr,1845 kind,1846 &LvalueRef::Pattern(&lvalue.pattern),1847 Some(rhs),1848 )1849 }1850 _ => unreachable!(),1851 }1852}18531854fn emit_store(1855 cx: &mut Context,1856 instr: &ReactiveInstruction,1857 kind: InstructionKind,1858 lvalue: &LvalueRef,1859 value: Option<Expression>,1860) -> Result<Option<Statement>, CompilerError> {1861 match kind {1862 InstructionKind::Const => {1863 // Invariant: Const declarations cannot also have an outer lvalue1864 // (i.e., cannot be referenced as an expression)1865 if instr.lvalue.is_some() {1866 return Err(invariant_err_with_detail_message(1867 "Const declaration cannot be referenced as an expression",1868 "this is Const",1869 instr.loc,1870 ));1871 }1872 let lval = codegen_lvalue(cx, lvalue)?;1873 Ok(Some(Statement::VariableDeclaration(VariableDeclaration {1874 base: base_node_with_loc("VariableDeclaration", instr.loc),1875 declarations: vec![make_var_declarator(lval, value)],1876 kind: VariableDeclarationKind::Const,1877 declare: None,1878 })))1879 }1880 InstructionKind::Function => {1881 let lval = codegen_lvalue(cx, lvalue)?;1882 let PatternLike::Identifier(fn_id) = lval else {1883 return Err(invariant_err(1884 "Expected an identifier as function declaration lvalue",1885 None,1886 ));1887 };1888 let Some(rhs) = value else {1889 return Err(invariant_err(1890 "Expected a function value for function declaration",1891 None,1892 ));1893 };1894 match rhs {1895 Expression::FunctionExpression(func_expr) => {1896 Ok(Some(Statement::FunctionDeclaration(FunctionDeclaration {1897 base: base_node_with_loc("FunctionDeclaration", instr.loc),1898 id: Some(fn_id),1899 params: func_expr.params,1900 body: func_expr.body,1901 generator: func_expr.generator,1902 is_async: func_expr.is_async,1903 declare: None,1904 return_type: None,1905 type_parameters: None,1906 predicate: None,1907 component_declaration: false,1908 hook_declaration: false,1909 })))1910 }1911 _ => Err(invariant_err(1912 "Expected a function expression for function declaration",1913 None,1914 )),1915 }1916 }1917 InstructionKind::Let => {1918 // Invariant: Let declarations cannot also have an outer lvalue1919 if instr.lvalue.is_some() {1920 return Err(invariant_err_with_detail_message(1921 "Const declaration cannot be referenced as an expression",1922 "this is Let",1923 instr.loc,1924 ));1925 }1926 let lval = codegen_lvalue(cx, lvalue)?;1927 Ok(Some(Statement::VariableDeclaration(VariableDeclaration {1928 base: base_node_with_loc("VariableDeclaration", instr.loc),1929 declarations: vec![make_var_declarator(lval, value)],1930 kind: VariableDeclarationKind::Let,1931 declare: None,1932 })))1933 }1934 InstructionKind::Reassign => {1935 let Some(rhs) = value else {1936 return Err(invariant_err("Expected a value for reassignment", None));1937 };1938 let lval = codegen_lvalue(cx, lvalue)?;1939 let expr = Expression::AssignmentExpression(ast_expr::AssignmentExpression {1940 base: BaseNode::typed("AssignmentExpression"),1941 operator: AssignmentOperator::Assign,1942 left: Box::new(lval),1943 right: Box::new(rhs),1944 });1945 if let Some(ref lvalue_place) = instr.lvalue {1946 let is_store_context = matches!(1947 &instr.value,1948 ReactiveValue::Instruction(InstructionValue::StoreContext { .. })1949 );1950 if !is_store_context {1951 let declaration_id =1952 cx.env.identifiers[lvalue_place.identifier.0 as usize].declaration_id;1953 cx.temp1954 .set(declaration_id, Some(ExpressionOrJsxText::Expression(expr)));1955 return Ok(None);1956 } else {1957 let stmt =1958 codegen_instruction(cx, instr, ExpressionOrJsxText::Expression(expr))?;1959 if matches!(stmt, Statement::EmptyStatement(_)) {1960 return Ok(None);1961 }1962 return Ok(Some(stmt));1963 }1964 }1965 Ok(Some(Statement::ExpressionStatement(ExpressionStatement {1966 base: base_node_with_loc("ExpressionStatement", instr.loc),1967 expression: Box::new(expr),1968 })))1969 }1970 InstructionKind::Catch => Ok(Some(Statement::EmptyStatement(EmptyStatement {1971 base: BaseNode::typed("EmptyStatement"),1972 }))),1973 InstructionKind::HoistedLet1974 | InstructionKind::HoistedConst1975 | InstructionKind::HoistedFunction => Err(invariant_err(1976 &format!(1977 "Expected {:?} to have been pruned in PruneHoistedContexts",1978 kind1979 ),1980 None,1981 )),1982 }1983}19841985fn codegen_instruction(1986 cx: &mut Context,1987 instr: &ReactiveInstruction,1988 value: ExpressionOrJsxText,1989) -> Result<Statement, CompilerError> {1990 let Some(ref lvalue) = instr.lvalue else {1991 let expr = convert_value_to_expression(value);1992 return Ok(Statement::ExpressionStatement(ExpressionStatement {1993 base: base_node_with_loc("ExpressionStatement", instr.loc),1994 expression: Box::new(expr),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
Findings
✓ No findings reported for this file.