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// =============================================================================557558type Temporaries = FxHashMap<DeclarationId, Option<ExpressionOrJsxText>>;559560#[derive(Clone)]561enum ExpressionOrJsxText {562 Expression(Expression),563 JsxText(JSXText),564}565566struct Context<'env> {567 env: &'env mut Environment,568 #[allow(dead_code)]569 fn_name: String,570 next_cache_index: u32,571 declarations: FxHashSet<DeclarationId>,572 temp: Temporaries,573 object_methods: FxHashMap<574 IdentifierId,575 (576 InstructionValue,577 Option<react_compiler_diagnostics::SourceLocation>,578 ),579 >,580 unique_identifiers: FxHashSet<String>,581 fbt_operands: FxHashSet<IdentifierId>,582 synthesized_names: FxHashMap<String, String>,583}584585impl<'env> Context<'env> {586 fn new(587 env: &'env mut Environment,588 fn_name: String,589 unique_identifiers: FxHashSet<String>,590 fbt_operands: FxHashSet<IdentifierId>,591 ) -> Self {592 Context {593 env,594 fn_name,595 next_cache_index: 0,596 declarations: FxHashSet::default(),597 temp: FxHashMap::default(),598 object_methods: FxHashMap::default(),599 unique_identifiers,600 fbt_operands,601 synthesized_names: FxHashMap::default(),602 }603 }604605 fn alloc_cache_index(&mut self) -> u32 {606 let idx = self.next_cache_index;607 self.next_cache_index += 1;608 idx609 }610611 fn declare(&mut self, identifier_id: IdentifierId) {612 let ident = &self.env.identifiers[identifier_id.0 as usize];613 self.declarations.insert(ident.declaration_id);614 }615616 fn has_declared(&self, identifier_id: IdentifierId) -> bool {617 let ident = &self.env.identifiers[identifier_id.0 as usize];618 self.declarations.contains(&ident.declaration_id)619 }620621 fn synthesize_name(&mut self, name: &str) -> String {622 if let Some(prev) = self.synthesized_names.get(name) {623 return prev.clone();624 }625 let mut validated = name.to_string();626 let mut index = 0u32;627 while self.unique_identifiers.contains(&validated) {628 validated = format!("{name}{index}");629 index += 1;630 }631 self.unique_identifiers.insert(validated.clone());632 self.synthesized_names633 .insert(name.to_string(), validated.clone());634 validated635 }636637 fn record_error(&mut self, detail: CompilerErrorDetail) -> Result<(), CompilerError> {638 self.env.record_error(detail)639 }640}641642// =============================================================================643// Core codegen functions644// =============================================================================645646fn codegen_reactive_function(647 cx: &mut Context,648 func: &ReactiveFunction,649) -> Result<CodegenFunction, CompilerError> {650 // Register parameters651 for param in &func.params {652 let place = match param {653 ParamPattern::Place(p) => p,654 ParamPattern::Spread(sp) => &sp.place,655 };656 let ident = &cx.env.identifiers[place.identifier.0 as usize];657 cx.temp.insert(ident.declaration_id, None);658 cx.declare(place.identifier);659 }660661 let params: Vec<PatternLike> = func662 .params663 .iter()664 .map(|p| convert_parameter(p, cx.env))665 .collect::<Result<_, _>>()?;666 let mut body = codegen_block(cx, &func.body)?;667668 // Add directives669 body.directives = func670 .directives671 .iter()672 .map(|d| Directive {673 base: BaseNode::typed("Directive"),674 value: DirectiveLiteral {675 base: BaseNode::typed("DirectiveLiteral"),676 value: d.clone(),677 },678 })679 .collect();680681 // Remove trailing `return undefined`682 if let Some(last) = body.body.last() {683 if matches!(last, Statement::ReturnStatement(ret) if ret.argument.is_none()) {684 body.body.pop();685 }686 }687688 // Count memo blocks689 let (memo_blocks, memo_values, pruned_memo_blocks, pruned_memo_values) =690 count_memo_blocks(func, cx.env);691692 Ok(CodegenFunction {693 loc: func.loc,694 id: func.id.as_ref().map(|name| make_identifier(name)),695 name_hint: func.name_hint.clone(),696 params,697 body,698 generator: func.generator,699 is_async: func.is_async,700 memo_slots_used: cx.next_cache_index,701 memo_blocks,702 memo_values,703 pruned_memo_blocks,704 pruned_memo_values,705 outlined: Vec::new(),706 })707}708709fn convert_parameter(710 param: &ParamPattern,711 env: &Environment,712) -> Result<PatternLike, CompilerError> {713 match param {714 ParamPattern::Place(place) => Ok(PatternLike::Identifier(convert_identifier(715 place.identifier,716 env,717 )?)),718 ParamPattern::Spread(spread) => Ok(PatternLike::RestElement(RestElement {719 base: BaseNode::typed("RestElement"),720 argument: Box::new(PatternLike::Identifier(convert_identifier(721 spread.place.identifier,722 env,723 )?)),724 type_annotation: None,725 decorators: None,726 })),727 }728}729730// =============================================================================731// Block codegen732// =============================================================================733734fn codegen_block(cx: &mut Context, block: &ReactiveBlock) -> Result<BlockStatement, CompilerError> {735 let temp_snapshot: Temporaries = cx.temp.clone();736 let result = codegen_block_no_reset(cx, block)?;737 cx.temp = temp_snapshot;738 Ok(result)739}740741fn codegen_block_no_reset(742 cx: &mut Context,743 block: &ReactiveBlock,744) -> Result<BlockStatement, CompilerError> {745 let mut statements: Vec<Statement> = Vec::new();746 for item in block {747 match item {748 ReactiveStatement::Instruction(instr) => {749 if let Some(stmt) = codegen_instruction_nullable(cx, instr)? {750 statements.push(stmt);751 }752 }753 ReactiveStatement::PrunedScope(PrunedReactiveScopeBlock { instructions, .. }) => {754 let scope_block = codegen_block_no_reset(cx, instructions)?;755 statements.extend(scope_block.body);756 }757 ReactiveStatement::Scope(ReactiveScopeBlock {758 scope,759 instructions,760 }) => {761 let temp_snapshot = cx.temp.clone();762 codegen_reactive_scope(cx, &mut statements, *scope, instructions)?;763 cx.temp = temp_snapshot;764 }765 ReactiveStatement::Terminal(term_stmt) => {766 let stmt = codegen_terminal(cx, &term_stmt.terminal)?;767 let Some(stmt) = stmt else {768 continue;769 };770 if let Some(ref label) = term_stmt.label {771 if !label.implicit {772 let inner = if let Statement::BlockStatement(bs) = &stmt {773 if bs.body.len() == 1 {774 bs.body[0].clone()775 } else {776 stmt777 }778 } else {779 stmt780 };781 statements.push(Statement::LabeledStatement(LabeledStatement {782 base: BaseNode::typed("LabeledStatement"),783 label: make_identifier(&codegen_label(label.id)),784 body: Box::new(inner),785 }));786 } else if let Statement::BlockStatement(bs) = stmt {787 statements.extend(bs.body);788 } else {789 statements.push(stmt);790 }791 } else if let Statement::BlockStatement(bs) = stmt {792 statements.extend(bs.body);793 } else {794 statements.push(stmt);795 }796 }797 }798 }799 Ok(BlockStatement {800 base: BaseNode::typed("BlockStatement"),801 body: statements,802 directives: Vec::new(),803 })804}805806// =============================================================================807// Reactive scope codegen (memoization)808// =============================================================================809810fn codegen_reactive_scope(811 cx: &mut Context,812 statements: &mut Vec<Statement>,813 scope_id: ScopeId,814 block: &ReactiveBlock,815) -> Result<(), CompilerError> {816 // Clone scope data upfront to avoid holding a borrow on cx.env817 let scope_deps = cx.env.scopes[scope_id.0 as usize].dependencies.clone();818 let scope_decls = cx.env.scopes[scope_id.0 as usize].declarations.clone();819 let scope_reassignments = cx.env.scopes[scope_id.0 as usize].reassignments.clone();820821 let mut cache_store_stmts: Vec<Statement> = Vec::new();822 let mut cache_load_stmts: Vec<Statement> = Vec::new();823 let mut cache_loads: Vec<(AstIdentifier, u32, Expression)> = Vec::new();824 let mut change_exprs: Vec<Expression> = Vec::new();825826 // Sort dependencies827 let mut deps = scope_deps;828 deps.sort_by(|a, b| compare_scope_dependency(a, b, cx.env));829830 for dep in &deps {831 let index = cx.alloc_cache_index();832 let cache_name = cx.synthesize_name("$");833 let comparison = Expression::BinaryExpression(ast_expr::BinaryExpression {834 base: BaseNode::typed("BinaryExpression"),835 operator: AstBinaryOperator::StrictNeq,836 left: Box::new(Expression::MemberExpression(ast_expr::MemberExpression {837 base: BaseNode::typed("MemberExpression"),838 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),839 property: Box::new(Expression::NumericLiteral(NumericLiteral {840 base: BaseNode::typed("NumericLiteral"),841 value: index as f64,842 extra: None,843 })),844 computed: true,845 })),846 right: Box::new(codegen_dependency(cx, dep)?),847 });848 change_exprs.push(comparison);849850 // Store dependency value into cache851 let dep_value = codegen_dependency(cx, dep)?;852 cache_store_stmts.push(Statement::ExpressionStatement(ExpressionStatement {853 base: BaseNode::typed("ExpressionStatement"),854 expression: Box::new(Expression::AssignmentExpression(855 ast_expr::AssignmentExpression {856 base: BaseNode::typed("AssignmentExpression"),857 operator: AssignmentOperator::Assign,858 left: Box::new(PatternLike::MemberExpression(ast_expr::MemberExpression {859 base: BaseNode::typed("MemberExpression"),860 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),861 property: Box::new(Expression::NumericLiteral(NumericLiteral {862 base: BaseNode::typed("NumericLiteral"),863 value: index as f64,864 extra: None,865 })),866 computed: true,867 })),868 right: Box::new(dep_value),869 },870 )),871 }));872 }873874 let mut first_output_index: Option<u32> = None;875876 // Sort declarations877 let mut decls = scope_decls;878 decls.sort_by(|(_id_a, a), (_id_b, b)| compare_scope_declaration(a, b, cx.env));879880 for (_ident_id, decl) in &decls {881 let index = cx.alloc_cache_index();882 if first_output_index.is_none() {883 first_output_index = Some(index);884 }885886 let ident = &cx.env.identifiers[decl.identifier.0 as usize];887 invariant(888 ident.name.is_some(),889 &format!(890 "Expected scope declaration identifier to be named, id={}",891 decl.identifier.0892 ),893 None,894 )?;895896 let name = convert_identifier(decl.identifier, cx.env)?;897 if !cx.has_declared(decl.identifier) {898 statements.push(Statement::VariableDeclaration(VariableDeclaration {899 base: BaseNode::typed("VariableDeclaration"),900 declarations: vec![make_var_declarator(901 PatternLike::Identifier(name.clone()),902 None,903 )],904 kind: VariableDeclarationKind::Let,905 declare: None,906 }));907 }908 cache_loads.push((name.clone(), index, Expression::Identifier(name.clone())));909 cx.declare(decl.identifier);910 }911912 for reassignment_id in scope_reassignments {913 let index = cx.alloc_cache_index();914 if first_output_index.is_none() {915 first_output_index = Some(index);916 }917 let name = convert_identifier(reassignment_id, cx.env)?;918 cache_loads.push((name.clone(), index, Expression::Identifier(name)));919 }920921 // Build test condition922 let test_condition = if change_exprs.is_empty() {923 let first_idx = first_output_index.ok_or_else(|| {924 invariant_err("Expected scope to have at least one declaration", None)925 })?;926 let cache_name = cx.synthesize_name("$");927 Expression::BinaryExpression(ast_expr::BinaryExpression {928 base: BaseNode::typed("BinaryExpression"),929 operator: AstBinaryOperator::StrictEq,930 left: Box::new(Expression::MemberExpression(ast_expr::MemberExpression {931 base: BaseNode::typed("MemberExpression"),932 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),933 property: Box::new(Expression::NumericLiteral(NumericLiteral {934 base: BaseNode::typed("NumericLiteral"),935 value: first_idx as f64,936 extra: None,937 })),938 computed: true,939 })),940 right: Box::new(symbol_for(MEMO_CACHE_SENTINEL)),941 })942 } else {943 change_exprs944 .into_iter()945 .reduce(|acc, expr| {946 Expression::LogicalExpression(ast_expr::LogicalExpression {947 base: BaseNode::typed("LogicalExpression"),948 operator: AstLogicalOperator::Or,949 left: Box::new(acc),950 right: Box::new(expr),951 })952 })953 .unwrap()954 };955956 let mut computation_block = codegen_block(cx, block)?;957958 // Build cache store and load statements for declarations959 for (name, index, value) in &cache_loads {960 let cache_name = cx.synthesize_name("$");961 cache_store_stmts.push(Statement::ExpressionStatement(ExpressionStatement {962 base: BaseNode::typed("ExpressionStatement"),963 expression: Box::new(Expression::AssignmentExpression(964 ast_expr::AssignmentExpression {965 base: BaseNode::typed("AssignmentExpression"),966 operator: AssignmentOperator::Assign,967 left: Box::new(PatternLike::MemberExpression(ast_expr::MemberExpression {968 base: BaseNode::typed("MemberExpression"),969 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),970 property: Box::new(Expression::NumericLiteral(NumericLiteral {971 base: BaseNode::typed("NumericLiteral"),972 value: *index as f64,973 extra: None,974 })),975 computed: true,976 })),977 right: Box::new(value.clone()),978 },979 )),980 }));981 cache_load_stmts.push(Statement::ExpressionStatement(ExpressionStatement {982 base: BaseNode::typed("ExpressionStatement"),983 expression: Box::new(Expression::AssignmentExpression(984 ast_expr::AssignmentExpression {985 base: BaseNode::typed("AssignmentExpression"),986 operator: AssignmentOperator::Assign,987 left: Box::new(PatternLike::Identifier(name.clone())),988 right: Box::new(Expression::MemberExpression(ast_expr::MemberExpression {989 base: BaseNode::typed("MemberExpression"),990 object: Box::new(Expression::Identifier(make_identifier(&cache_name))),991 property: Box::new(Expression::NumericLiteral(NumericLiteral {992 base: BaseNode::typed("NumericLiteral"),993 value: *index as f64,994 extra: None,995 })),996 computed: true,997 })),998 },999 )),1000 }));1001 }10021003 computation_block.body.extend(cache_store_stmts);10041005 let memo_stmt = Statement::IfStatement(IfStatement {1006 base: BaseNode::typed("IfStatement"),1007 test: Box::new(test_condition),1008 consequent: Box::new(Statement::BlockStatement(computation_block)),1009 alternate: Some(Box::new(Statement::BlockStatement(BlockStatement {1010 base: BaseNode::typed("BlockStatement"),1011 body: cache_load_stmts,1012 directives: Vec::new(),1013 }))),1014 });1015 statements.push(memo_stmt);10161017 // Handle early return1018 let early_return_value = cx.env.scopes[scope_id.0 as usize]1019 .early_return_value1020 .clone();1021 if let Some(ref early_return) = early_return_value {1022 let early_ident = &cx.env.identifiers[early_return.value.0 as usize];1023 let name = match &early_ident.name {1024 Some(react_compiler_hir::IdentifierName::Named(n)) => n.clone(),1025 Some(react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(),1026 None => {1027 return Err(invariant_err(1028 "Expected early return value to be promoted to a named variable",1029 early_return.loc,1030 ));1031 }1032 };1033 statements.push(Statement::IfStatement(IfStatement {1034 base: BaseNode::typed("IfStatement"),1035 test: Box::new(Expression::BinaryExpression(ast_expr::BinaryExpression {1036 base: BaseNode::typed("BinaryExpression"),1037 operator: AstBinaryOperator::StrictNeq,1038 left: Box::new(Expression::Identifier(make_identifier(&name))),1039 right: Box::new(symbol_for(EARLY_RETURN_SENTINEL)),1040 })),1041 consequent: Box::new(Statement::BlockStatement(BlockStatement {1042 base: BaseNode::typed("BlockStatement"),1043 body: vec![Statement::ReturnStatement(ReturnStatement {1044 base: BaseNode::typed("ReturnStatement"),1045 argument: Some(Box::new(Expression::Identifier(make_identifier(&name)))),1046 })],1047 directives: Vec::new(),1048 })),1049 alternate: None,1050 }));1051 }10521053 Ok(())1054}10551056// =============================================================================1057// Terminal codegen1058// =============================================================================10591060fn codegen_terminal(1061 cx: &mut Context,1062 terminal: &ReactiveTerminal,1063) -> Result<Option<Statement>, CompilerError> {1064 match terminal {1065 ReactiveTerminal::Break {1066 target,1067 target_kind,1068 loc,1069 ..1070 } => {1071 if *target_kind == ReactiveTerminalTargetKind::Implicit {1072 return Ok(None);1073 }1074 Ok(Some(Statement::BreakStatement(BreakStatement {1075 base: base_node_with_loc("BreakStatement", *loc),1076 label: if *target_kind == ReactiveTerminalTargetKind::Labeled {1077 Some(make_identifier(&codegen_label(*target)))1078 } else {1079 None1080 },1081 })))1082 }1083 ReactiveTerminal::Continue {1084 target,1085 target_kind,1086 loc,1087 ..1088 } => {1089 if *target_kind == ReactiveTerminalTargetKind::Implicit {1090 return Ok(None);1091 }1092 Ok(Some(Statement::ContinueStatement(ContinueStatement {1093 base: base_node_with_loc("ContinueStatement", *loc),1094 label: if *target_kind == ReactiveTerminalTargetKind::Labeled {1095 Some(make_identifier(&codegen_label(*target)))1096 } else {1097 None1098 },1099 })))1100 }1101 ReactiveTerminal::Return { value, loc, .. } => {1102 let expr = codegen_place_to_expression(cx, value)?;1103 if let Expression::Identifier(ref ident) = expr {1104 if ident.name == "undefined" {1105 return Ok(Some(Statement::ReturnStatement(ReturnStatement {1106 base: base_node_with_loc("ReturnStatement", *loc),1107 argument: None,1108 })));1109 }1110 }1111 Ok(Some(Statement::ReturnStatement(ReturnStatement {1112 base: base_node_with_loc("ReturnStatement", *loc),1113 argument: Some(Box::new(expr)),1114 })))1115 }1116 ReactiveTerminal::Throw { value, loc, .. } => {1117 let expr = codegen_place_to_expression(cx, value)?;1118 Ok(Some(Statement::ThrowStatement(ThrowStatement {1119 base: base_node_with_loc("ThrowStatement", *loc),1120 argument: Box::new(expr),1121 })))1122 }1123 ReactiveTerminal::If {1124 test,1125 consequent,1126 alternate,1127 loc,1128 ..1129 } => {1130 let test_expr = codegen_place_to_expression(cx, test)?;1131 let consequent_block = codegen_block(cx, consequent)?;1132 let alternate_stmt = if let Some(alt) = alternate {1133 let block = codegen_block(cx, alt)?;1134 if block.body.is_empty() {1135 None1136 } else {1137 Some(Box::new(Statement::BlockStatement(block)))1138 }1139 } else {1140 None1141 };1142 Ok(Some(Statement::IfStatement(IfStatement {1143 base: base_node_with_loc("IfStatement", *loc),1144 test: Box::new(test_expr),1145 consequent: Box::new(Statement::BlockStatement(consequent_block)),1146 alternate: alternate_stmt,1147 })))1148 }1149 ReactiveTerminal::Switch {1150 test, cases, loc, ..1151 } => {1152 let test_expr = codegen_place_to_expression(cx, test)?;1153 let switch_cases: Vec<SwitchCase> = cases1154 .iter()1155 .map(|case| {1156 let test = case1157 .test1158 .as_ref()1159 .map(|t| codegen_place_to_expression(cx, t))1160 .transpose()?;1161 let block = case1162 .block1163 .as_ref()1164 .map(|b| codegen_block(cx, b))1165 .transpose()?;1166 let consequent = match block {1167 Some(b) if b.body.is_empty() => Vec::new(),1168 Some(b) => vec![Statement::BlockStatement(b)],1169 None => Vec::new(),1170 };1171 Ok(SwitchCase {1172 base: BaseNode::typed("SwitchCase"),1173 test: test.map(Box::new),1174 consequent,1175 })1176 })1177 .collect::<Result<_, CompilerError>>()?;1178 Ok(Some(Statement::SwitchStatement(SwitchStatement {1179 base: base_node_with_loc("SwitchStatement", *loc),1180 discriminant: Box::new(test_expr),1181 cases: switch_cases,1182 })))1183 }1184 ReactiveTerminal::DoWhile {1185 loop_block,1186 test,1187 loc,1188 ..1189 } => {1190 let test_expr = codegen_instruction_value_to_expression(cx, test)?;1191 let body = codegen_block(cx, loop_block)?;1192 Ok(Some(Statement::DoWhileStatement(DoWhileStatement {1193 base: base_node_with_loc("DoWhileStatement", *loc),1194 test: Box::new(test_expr),1195 body: Box::new(Statement::BlockStatement(body)),1196 })))1197 }1198 ReactiveTerminal::While {1199 test,1200 loop_block,1201 loc,1202 ..1203 } => {1204 let test_expr = codegen_instruction_value_to_expression(cx, test)?;1205 let body = codegen_block(cx, loop_block)?;1206 Ok(Some(Statement::WhileStatement(WhileStatement {1207 base: base_node_with_loc("WhileStatement", *loc),1208 test: Box::new(test_expr),1209 body: Box::new(Statement::BlockStatement(body)),1210 })))1211 }1212 ReactiveTerminal::For {1213 init,1214 test,1215 update,1216 loop_block,1217 loc,1218 ..1219 } => {1220 let init_val = codegen_for_init(cx, init)?;1221 let test_expr = codegen_instruction_value_to_expression(cx, test)?;1222 let update_expr = update1223 .as_ref()1224 .map(|u| codegen_instruction_value_to_expression(cx, u))1225 .transpose()?;1226 let body = codegen_block(cx, loop_block)?;1227 Ok(Some(Statement::ForStatement(ForStatement {1228 base: base_node_with_loc("ForStatement", *loc),1229 init: init_val.map(|v| Box::new(v)),1230 test: Some(Box::new(test_expr)),1231 update: update_expr.map(Box::new),1232 body: Box::new(Statement::BlockStatement(body)),1233 })))1234 }1235 ReactiveTerminal::ForIn {1236 init,1237 loop_block,1238 loc,1239 ..1240 } => codegen_for_in(cx, init, loop_block, *loc),1241 ReactiveTerminal::ForOf {1242 init,1243 test,1244 loop_block,1245 loc,1246 ..1247 } => codegen_for_of(cx, init, test, loop_block, *loc),1248 ReactiveTerminal::Label { block, .. } => {1249 let body = codegen_block(cx, block)?;1250 Ok(Some(Statement::BlockStatement(body)))1251 }1252 ReactiveTerminal::Try {1253 block,1254 handler_binding,1255 handler,1256 loc,1257 ..1258 } => {1259 let catch_param = match handler_binding.as_ref() {1260 Some(binding) => {1261 let ident = &cx.env.identifiers[binding.identifier.0 as usize];1262 cx.temp.insert(ident.declaration_id, None);1263 Some(PatternLike::Identifier(convert_identifier(1264 binding.identifier,1265 cx.env,1266 )?))1267 }1268 None => None,1269 };1270 let try_block = codegen_block(cx, block)?;1271 let handler_block = codegen_block(cx, handler)?;1272 Ok(Some(Statement::TryStatement(TryStatement {1273 base: base_node_with_loc("TryStatement", *loc),1274 block: try_block,1275 handler: Some(CatchClause {1276 base: BaseNode::typed("CatchClause"),1277 param: catch_param,1278 body: handler_block,1279 }),1280 finalizer: None,1281 })))1282 }1283 }1284}12851286fn codegen_for_in(1287 cx: &mut Context,1288 init: &ReactiveValue,1289 loop_block: &ReactiveBlock,1290 loc: Option<DiagSourceLocation>,1291) -> Result<Option<Statement>, CompilerError> {1292 let ReactiveValue::SequenceExpression { instructions, .. } = init else {1293 return Err(invariant_err(1294 "Expected a sequence expression init for for..in",1295 None,1296 ));1297 };1298 if instructions.len() != 2 {1299 cx.record_error(CompilerErrorDetail {1300 category: ErrorCategory::Todo,1301 reason: "Support non-trivial for..in inits".to_string(),1302 description: None,1303 loc,1304 suggestions: None,1305 })?;1306 return Ok(Some(Statement::EmptyStatement(EmptyStatement {1307 base: BaseNode::typed("EmptyStatement"),1308 })));1309 }1310 let iterable_collection = &instructions[0];1311 let iterable_item = &instructions[1];1312 let instr_value = get_instruction_value(&iterable_item.value)?;1313 let (lval, var_decl_kind) = extract_for_in_of_lval(cx, instr_value, "for..in", loc)?;1314 let right = codegen_instruction_value_to_expression(cx, &iterable_collection.value)?;1315 let body = codegen_block(cx, loop_block)?;1316 Ok(Some(Statement::ForInStatement(ForInStatement {1317 base: base_node_with_loc("ForInStatement", loc),1318 left: Box::new(1319 react_compiler_ast::statements::ForInOfLeft::VariableDeclaration(VariableDeclaration {1320 base: BaseNode::typed("VariableDeclaration"),1321 declarations: vec![VariableDeclarator {1322 base: BaseNode::typed("VariableDeclarator"),1323 id: lval,1324 init: None,1325 definite: None,1326 }],1327 kind: var_decl_kind,1328 declare: None,1329 }),1330 ),1331 right: Box::new(right),1332 body: Box::new(Statement::BlockStatement(body)),1333 })))1334}13351336fn codegen_for_of(1337 cx: &mut Context,1338 init: &ReactiveValue,1339 test: &ReactiveValue,1340 loop_block: &ReactiveBlock,1341 loc: Option<DiagSourceLocation>,1342) -> Result<Option<Statement>, CompilerError> {1343 // Validate init is SequenceExpression with single GetIterator instruction1344 let ReactiveValue::SequenceExpression {1345 instructions: init_instrs,1346 ..1347 } = init1348 else {1349 return Err(invariant_err(1350 "Expected a sequence expression init for for..of",1351 None,1352 ));1353 };1354 if init_instrs.len() != 1 {1355 return Err(invariant_err(1356 "Expected a single-expression sequence expression init for for..of",1357 None,1358 ));1359 }1360 let get_iter_value = get_instruction_value(&init_instrs[0].value)?;1361 let InstructionValue::GetIterator { collection, .. } = get_iter_value else {1362 return Err(invariant_err("Expected GetIterator in for..of init", None));1363 };13641365 let ReactiveValue::SequenceExpression {1366 instructions: test_instrs,1367 ..1368 } = test1369 else {1370 return Err(invariant_err(1371 "Expected a sequence expression test for for..of",1372 None,1373 ));1374 };1375 if test_instrs.len() != 2 {1376 cx.record_error(CompilerErrorDetail {1377 category: ErrorCategory::Todo,1378 reason: "Support non-trivial for..of inits".to_string(),1379 description: None,1380 loc,1381 suggestions: None,1382 })?;1383 return Ok(Some(Statement::EmptyStatement(EmptyStatement {1384 base: BaseNode::typed("EmptyStatement"),1385 })));1386 }1387 let iterable_item = &test_instrs[1];1388 let instr_value = get_instruction_value(&iterable_item.value)?;1389 let (lval, var_decl_kind) = extract_for_in_of_lval(cx, instr_value, "for..of", loc)?;13901391 let right = codegen_place_to_expression(cx, collection)?;1392 let body = codegen_block(cx, loop_block)?;1393 Ok(Some(Statement::ForOfStatement(ForOfStatement {1394 base: base_node_with_loc("ForOfStatement", loc),1395 left: Box::new(1396 react_compiler_ast::statements::ForInOfLeft::VariableDeclaration(VariableDeclaration {1397 base: BaseNode::typed("VariableDeclaration"),1398 declarations: vec![VariableDeclarator {1399 base: BaseNode::typed("VariableDeclarator"),1400 id: lval,1401 init: None,1402 definite: None,1403 }],1404 kind: var_decl_kind,1405 declare: None,1406 }),1407 ),1408 right: Box::new(right),1409 body: Box::new(Statement::BlockStatement(body)),1410 is_await: false,1411 })))1412}14131414/// Extract lval and declaration kind from a for-in/for-of iterable item instruction.1415fn extract_for_in_of_lval(1416 cx: &mut Context,1417 instr_value: &InstructionValue,1418 context_name: &str,1419 loc: Option<DiagSourceLocation>,1420) -> Result<(PatternLike, VariableDeclarationKind), CompilerError> {1421 let (lval, kind) = match instr_value {1422 InstructionValue::StoreLocal { lvalue, .. } => (1423 codegen_lvalue(cx, &LvalueRef::Place(&lvalue.place))?,1424 lvalue.kind,1425 ),1426 InstructionValue::Destructure { lvalue, .. } => (1427 codegen_lvalue(cx, &LvalueRef::Pattern(&lvalue.pattern))?,1428 lvalue.kind,1429 ),1430 InstructionValue::StoreContext { .. } => {1431 cx.record_error(CompilerErrorDetail {1432 category: ErrorCategory::Todo,1433 reason: format!("Support non-trivial {} inits", context_name),1434 description: None,1435 loc,1436 suggestions: None,1437 })?;1438 return Ok((1439 PatternLike::Identifier(make_identifier("_")),1440 VariableDeclarationKind::Let,1441 ));1442 }1443 _ => {1444 return Err(invariant_err(1445 &format!(1446 "Expected a StoreLocal or Destructure in {} collection, found {:?}",1447 context_name,1448 std::mem::discriminant(instr_value)1449 ),1450 None,1451 ));1452 }1453 };1454 let var_decl_kind = match kind {1455 InstructionKind::Const => VariableDeclarationKind::Const,1456 InstructionKind::Let => VariableDeclarationKind::Let,1457 _ => {1458 return Err(invariant_err(1459 &format!(1460 "Unexpected {:?} variable in {} collection",1461 kind, context_name1462 ),1463 None,1464 ));1465 }1466 };1467 Ok((lval, var_decl_kind))1468}14691470fn codegen_for_init(1471 cx: &mut Context,1472 init: &ReactiveValue,1473) -> Result<Option<ForInit>, CompilerError> {1474 if let ReactiveValue::SequenceExpression { instructions, .. } = init {1475 let block_items: Vec<ReactiveStatement> = instructions1476 .iter()1477 .map(|i| ReactiveStatement::Instruction(i.clone()))1478 .collect();1479 let body = codegen_block(cx, &block_items)?.body;1480 let mut declarators: Vec<VariableDeclarator> = Vec::new();1481 let mut kind = VariableDeclarationKind::Const;1482 for instr in body {1483 // Check if this is an assignment that can be folded into the last declarator1484 if let Statement::ExpressionStatement(ref expr_stmt) = instr {1485 if let Expression::AssignmentExpression(ref assign) = *expr_stmt.expression {1486 if matches!(assign.operator, AssignmentOperator::Assign) {1487 if let PatternLike::Identifier(ref left_ident) = *assign.left {1488 if let Some(top) = declarators.last_mut() {1489 if let PatternLike::Identifier(ref top_ident) = top.id {1490 if top_ident.name == left_ident.name && top.init.is_none() {1491 top.init = Some(assign.right.clone());1492 continue;1493 }1494 }1495 }1496 }1497 }1498 }1499 }15001501 if let Statement::VariableDeclaration(var_decl) = instr {1502 match var_decl.kind {1503 VariableDeclarationKind::Let | VariableDeclarationKind::Const => {}1504 _ => {1505 return Err(invariant_err(1506 "Expected a let or const variable declaration",1507 None,1508 ));1509 }1510 }1511 if matches!(var_decl.kind, VariableDeclarationKind::Let) {1512 kind = VariableDeclarationKind::Let;1513 }1514 declarators.extend(var_decl.declarations);1515 } else {1516 let stmt_type = get_statement_type_name(&instr);1517 let stmt_loc = get_statement_loc(&instr);1518 let reason = "Expected a variable declaration".to_string();1519 let mut err = CompilerError::new();1520 err.push_diagnostic(1521 CompilerDiagnostic::new(1522 ErrorCategory::Invariant,1523 reason.clone(),1524 Some(format!("Got {}", stmt_type)),1525 )1526 .with_detail(CompilerDiagnosticDetail::Error {1527 loc: stmt_loc,1528 message: Some(reason),1529 identifier_name: None,1530 }),1531 );1532 return Err(err);1533 }1534 }1535 if declarators.is_empty() {1536 return Err(invariant_err(1537 "Expected a variable declaration in for-init",1538 None,1539 ));1540 }1541 Ok(Some(ForInit::VariableDeclaration(VariableDeclaration {1542 base: BaseNode::typed("VariableDeclaration"),1543 declarations: declarators,1544 kind,1545 declare: None,1546 })))1547 } else {1548 let expr = codegen_instruction_value_to_expression(cx, init)?;1549 Ok(Some(ForInit::Expression(Box::new(expr))))1550 }1551}15521553// =============================================================================1554// Instruction codegen1555// =============================================================================15561557/// How statement-position codegen disposes of an `UnsupportedNode`'s1558/// `original_node`. See [`codegen_unsupported_original_node`].1559enum UnsupportedOriginalNode {1560 /// Emit this statement directly (early return).1561 Statement(Statement),1562 /// Flow through the general expression codegen path so the instruction's1563 /// lvalue temporary is bound/registered.1564 ExpressionCodegen,1565}15661567/// Discriminate an `UnsupportedNode`'s `original_node` by its `type` tag.1568///1569/// Lowering serializes typed `Expression`/`Statement`/`PatternLike` bailout1570/// nodes, plus the raw nodes of `Statement::Unknown` (whose tags are1571/// unmodeled by construction). Dispatch accordingly:1572///1573/// - Modeled statement tag: parse the typed statement and emit it directly.1574/// A parse failure here is a serialize/deserialize asymmetry, surfaced as1575/// an invariant rather than degraded.1576/// - Tag parseable as `Expression` or `PatternLike` (both enums are strict,1577/// no catch-all): expression codegen. Patterns (e.g. `ObjectPattern`1578/// destructuring targets) keep their existing placeholder fallback there.1579/// - Anything else is an unmodeled tag, producible only by the1580/// unknown-statement lowering bailout — i.e. it came from a statement1581/// position — so preserve it verbatim as `Statement::Unknown`, matching1582/// the TS codegen's `return node` for non-expressions.1583fn codegen_unsupported_original_node(1584 node: &serde_json::Value,1585) -> Result<UnsupportedOriginalNode, CompilerError> {1586 let tag = node.get("type").and_then(serde_json::Value::as_str);1587 if tag.is_some_and(is_known_statement_type) {1588 let stmt: Statement = serde_json::from_value(node.clone()).map_err(|e| {1589 invariant_err(1590 &format!("Failed to deserialize original AST node: {}", e),1591 None,1592 )1593 })?;1594 return Ok(UnsupportedOriginalNode::Statement(stmt));1595 }1596 if serde_json::from_value::<Expression>(node.clone()).is_ok()1597 || serde_json::from_value::<PatternLike>(node.clone()).is_ok()1598 {1599 return Ok(UnsupportedOriginalNode::ExpressionCodegen);1600 }1601 let unknown = UnknownStatement::from_raw(RawNode::from_value(node)).map_err(|e| {1602 invariant_err(1603 &format!("Failed to read unsupported original AST node: {}", e),1604 None,1605 )1606 })?;1607 Ok(UnsupportedOriginalNode::Statement(Statement::Unknown(1608 unknown,1609 )))1610}16111612fn codegen_instruction_nullable(1613 cx: &mut Context,1614 instr: &ReactiveInstruction,1615) -> Result<Option<Statement>, CompilerError> {1616 // Only check specific InstructionValue kinds for the base Instruction variant1617 if let ReactiveValue::Instruction(ref value) = instr.value {1618 match value {1619 InstructionValue::StoreLocal { .. }1620 | InstructionValue::StoreContext { .. }1621 | InstructionValue::Destructure { .. }1622 | InstructionValue::DeclareLocal { .. }1623 | InstructionValue::DeclareContext { .. } => {1624 return codegen_store_or_declare(cx, instr, value);1625 }1626 InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => {1627 return Ok(None);1628 }1629 InstructionValue::Debugger { .. } => {1630 return Ok(Some(Statement::DebuggerStatement(DebuggerStatement {1631 base: base_node_with_loc("DebuggerStatement", instr.loc),1632 })));1633 }1634 InstructionValue::UnsupportedNode {1635 original_node: Some(node),1636 ..1637 } => {1638 // Statement-vs-expression discrimination must be explicit by1639 // `type` tag: `Statement`'s deserializer has a tolerant1640 // `Statement::Unknown` catch-all, so "does it deserialize as1641 // a Statement?" succeeds for ANY tagged object and would1642 // emit expression nodes as raw statements, orphaning their1643 // lvalue temporaries (the regression the explicit dispatch1644 // below prevents; TS codegen's equivalent check is1645 // `if (!t.isExpression(node)) return node; value = node`).1646 match codegen_unsupported_original_node(node)? {1647 UnsupportedOriginalNode::Statement(stmt) => return Ok(Some(stmt)),1648 UnsupportedOriginalNode::ExpressionCodegen => {1649 // Expression (or pattern) node — fall through to the1650 // general codegen path which handles lvalue binding1651 // and temporary registration.1652 }1653 }1654 }1655 InstructionValue::ObjectMethod { loc, .. } => {1656 invariant(1657 instr.lvalue.is_some(),1658 "Expected object methods to have a temp lvalue",1659 None,1660 )?;1661 let lvalue = instr.lvalue.as_ref().unwrap();1662 cx.object_methods1663 .insert(lvalue.identifier, (value.clone(), *loc));1664 return Ok(None);1665 }1666 _ => {} // fall through to general codegen1667 }1668 }1669 // General case: codegen the full ReactiveValue1670 let expr_value = codegen_instruction_value(cx, &instr.value)?;1671 let stmt = codegen_instruction(cx, instr, expr_value)?;1672 if matches!(stmt, Statement::EmptyStatement(_)) {1673 Ok(None)1674 } else {1675 Ok(Some(stmt))1676 }1677}16781679fn codegen_store_or_declare(1680 cx: &mut Context,1681 instr: &ReactiveInstruction,1682 value: &InstructionValue,1683) -> Result<Option<Statement>, CompilerError> {1684 match value {1685 InstructionValue::StoreLocal {1686 lvalue, value: val, ..1687 } => {1688 let mut kind = lvalue.kind;1689 if cx.has_declared(lvalue.place.identifier) {1690 kind = InstructionKind::Reassign;1691 }1692 let rhs = codegen_place_to_expression(cx, val)?;1693 emit_store(cx, instr, kind, &LvalueRef::Place(&lvalue.place), Some(rhs))1694 }1695 InstructionValue::StoreContext {1696 lvalue, value: val, ..1697 } => {1698 let rhs = codegen_place_to_expression(cx, val)?;1699 emit_store(1700 cx,1701 instr,1702 lvalue.kind,1703 &LvalueRef::Place(&lvalue.place),1704 Some(rhs),1705 )1706 }1707 InstructionValue::DeclareLocal { lvalue, .. }1708 | InstructionValue::DeclareContext { lvalue, .. } => {1709 if cx.has_declared(lvalue.place.identifier) {1710 return Ok(None);1711 }1712 emit_store(1713 cx,1714 instr,1715 lvalue.kind,1716 &LvalueRef::Place(&lvalue.place),1717 None,1718 )1719 }1720 InstructionValue::Destructure {1721 lvalue, value: val, ..1722 } => {1723 let kind = lvalue.kind;1724 // Register temporaries for unnamed pattern operands1725 for place in react_compiler_hir::visitors::each_pattern_operand(&lvalue.pattern) {1726 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);1729 }1730 }1731 let rhs = codegen_place_to_expression(cx, val)?;1732 emit_store(1733 cx,1734 instr,1735 kind,1736 &LvalueRef::Pattern(&lvalue.pattern),1737 Some(rhs),1738 )1739 }1740 _ => unreachable!(),1741 }1742}17431744fn emit_store(1745 cx: &mut Context,1746 instr: &ReactiveInstruction,1747 kind: InstructionKind,1748 lvalue: &LvalueRef,1749 value: Option<Expression>,1750) -> Result<Option<Statement>, CompilerError> {1751 match kind {1752 InstructionKind::Const => {1753 // Invariant: Const declarations cannot also have an outer lvalue1754 // (i.e., cannot be referenced as an expression)1755 if instr.lvalue.is_some() {1756 return Err(invariant_err_with_detail_message(1757 "Const declaration cannot be referenced as an expression",1758 "this is Const",1759 instr.loc,1760 ));1761 }1762 let lval = codegen_lvalue(cx, lvalue)?;1763 Ok(Some(Statement::VariableDeclaration(VariableDeclaration {1764 base: base_node_with_loc("VariableDeclaration", instr.loc),1765 declarations: vec![make_var_declarator(lval, value)],1766 kind: VariableDeclarationKind::Const,1767 declare: None,1768 })))1769 }1770 InstructionKind::Function => {1771 let lval = codegen_lvalue(cx, lvalue)?;1772 let PatternLike::Identifier(fn_id) = lval else {1773 return Err(invariant_err(1774 "Expected an identifier as function declaration lvalue",1775 None,1776 ));1777 };1778 let Some(rhs) = value else {1779 return Err(invariant_err(1780 "Expected a function value for function declaration",1781 None,1782 ));1783 };1784 match rhs {1785 Expression::FunctionExpression(func_expr) => {1786 Ok(Some(Statement::FunctionDeclaration(FunctionDeclaration {1787 base: base_node_with_loc("FunctionDeclaration", instr.loc),1788 id: Some(fn_id),1789 params: func_expr.params,1790 body: func_expr.body,1791 generator: func_expr.generator,1792 is_async: func_expr.is_async,1793 declare: None,1794 return_type: None,1795 type_parameters: None,1796 predicate: None,1797 component_declaration: false,1798 hook_declaration: false,1799 })))1800 }1801 _ => Err(invariant_err(1802 "Expected a function expression for function declaration",1803 None,1804 )),1805 }1806 }1807 InstructionKind::Let => {1808 // Invariant: Let declarations cannot also have an outer lvalue1809 if instr.lvalue.is_some() {1810 return Err(invariant_err_with_detail_message(1811 "Const declaration cannot be referenced as an expression",1812 "this is Let",1813 instr.loc,1814 ));1815 }1816 let lval = codegen_lvalue(cx, lvalue)?;1817 Ok(Some(Statement::VariableDeclaration(VariableDeclaration {1818 base: base_node_with_loc("VariableDeclaration", instr.loc),1819 declarations: vec![make_var_declarator(lval, value)],1820 kind: VariableDeclarationKind::Let,1821 declare: None,1822 })))1823 }1824 InstructionKind::Reassign => {1825 let Some(rhs) = value else {1826 return Err(invariant_err("Expected a value for reassignment", None));1827 };1828 let lval = codegen_lvalue(cx, lvalue)?;1829 let expr = Expression::AssignmentExpression(ast_expr::AssignmentExpression {1830 base: BaseNode::typed("AssignmentExpression"),1831 operator: AssignmentOperator::Assign,1832 left: Box::new(lval),1833 right: Box::new(rhs),1834 });1835 if let Some(ref lvalue_place) = instr.lvalue {1836 let is_store_context = matches!(1837 &instr.value,1838 ReactiveValue::Instruction(InstructionValue::StoreContext { .. })1839 );1840 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 );1846 return Ok(None);1847 } else {1848 let stmt =1849 codegen_instruction(cx, instr, ExpressionOrJsxText::Expression(expr))?;1850 if matches!(stmt, Statement::EmptyStatement(_)) {1851 return Ok(None);1852 }1853 return Ok(Some(stmt));1854 }1855 }1856 Ok(Some(Statement::ExpressionStatement(ExpressionStatement {1857 base: base_node_with_loc("ExpressionStatement", instr.loc),1858 expression: Box::new(expr),1859 })))1860 }1861 InstructionKind::Catch => Ok(Some(Statement::EmptyStatement(EmptyStatement {1862 base: BaseNode::typed("EmptyStatement"),1863 }))),1864 InstructionKind::HoistedLet1865 | InstructionKind::HoistedConst1866 | InstructionKind::HoistedFunction => Err(invariant_err(1867 &format!(1868 "Expected {:?} to have been pruned in PruneHoistedContexts",1869 kind1870 ),1871 None,1872 )),1873 }1874}18751876fn codegen_instruction(1877 cx: &mut Context,1878 instr: &ReactiveInstruction,1879 value: ExpressionOrJsxText,1880) -> Result<Statement, CompilerError> {1881 let Some(ref lvalue) = instr.lvalue else {1882 let expr = convert_value_to_expression(value);1883 return Ok(Statement::ExpressionStatement(ExpressionStatement {1884 base: base_node_with_loc("ExpressionStatement", instr.loc),1885 expression: Box::new(expr),1886 }));1887 };1888 let ident = &cx.env.identifiers[lvalue.identifier.0 as usize];1889 if ident.name.is_none() {1890 // temporary1891 cx.temp.insert(ident.declaration_id, Some(value));1892 return Ok(Statement::EmptyStatement(EmptyStatement {1893 base: BaseNode::typed("EmptyStatement"),1894 }));1895 }1896 let expr_value = convert_value_to_expression(value);1897 if cx.has_declared(lvalue.identifier) {1898 Ok(Statement::ExpressionStatement(ExpressionStatement {1899 base: base_node_with_loc("ExpressionStatement", instr.loc),1900 expression: Box::new(Expression::AssignmentExpression(1901 ast_expr::AssignmentExpression {1902 base: BaseNode::typed("AssignmentExpression"),1903 operator: AssignmentOperator::Assign,1904 left: Box::new(PatternLike::Identifier(convert_identifier(1905 lvalue.identifier,1906 cx.env,1907 )?)),1908 right: Box::new(expr_value),1909 },1910 )),1911 }))1912 } else {1913 Ok(Statement::VariableDeclaration(VariableDeclaration {1914 base: base_node_with_loc("VariableDeclaration", instr.loc),1915 declarations: vec![make_var_declarator(1916 PatternLike::Identifier(convert_identifier(lvalue.identifier, cx.env)?),1917 Some(expr_value),1918 )],1919 kind: VariableDeclarationKind::Const,1920 declare: None,1921 }))1922 }1923}19241925// =============================================================================1926// Instruction value codegen1927// =============================================================================19281929fn codegen_instruction_value_to_expression(1930 cx: &mut Context,1931 instr_value: &ReactiveValue,1932) -> Result<Expression, CompilerError> {1933 let value = codegen_instruction_value(cx, instr_value)?;1934 Ok(convert_value_to_expression(value))1935}19361937fn codegen_instruction_value(1938 cx: &mut Context,1939 instr_value: &ReactiveValue,1940) -> Result<ExpressionOrJsxText, CompilerError> {1941 match instr_value {1942 ReactiveValue::Instruction(iv) => {1943 let mut result = codegen_base_instruction_value(cx, iv)?;1944 // Propagate instrValue.loc to the generated expression, matching TS:1945 // if (instrValue.loc != null && instrValue.loc != GeneratedSource) {1946 // value.loc = instrValue.loc;1947 // }1948 if let Some(loc) = iv.loc() {1949 apply_loc_to_value(&mut result, *loc);1950 }1951 Ok(result)1952 }1953 ReactiveValue::LogicalExpression {1954 operator,1955 left,1956 right,1957 ..1958 } => {1959 let left_expr = codegen_instruction_value_to_expression(cx, left)?;1960 let right_expr = codegen_instruction_value_to_expression(cx, right)?;1961 Ok(ExpressionOrJsxText::Expression(1962 Expression::LogicalExpression(ast_expr::LogicalExpression {1963 base: BaseNode::typed("LogicalExpression"),1964 operator: convert_logical_operator(operator),1965 left: Box::new(left_expr),1966 right: Box::new(right_expr),1967 }),1968 ))1969 }1970 ReactiveValue::ConditionalExpression {1971 test,1972 consequent,1973 alternate,1974 ..1975 } => {1976 let test_expr = codegen_instruction_value_to_expression(cx, test)?;1977 let cons_expr = codegen_instruction_value_to_expression(cx, consequent)?;1978 let alt_expr = codegen_instruction_value_to_expression(cx, alternate)?;1979 Ok(ExpressionOrJsxText::Expression(1980 Expression::ConditionalExpression(ast_expr::ConditionalExpression {1981 base: BaseNode::typed("ConditionalExpression"),1982 test: Box::new(test_expr),1983 consequent: Box::new(cons_expr),1984 alternate: Box::new(alt_expr),1985 }),1986 ))1987 }1988 ReactiveValue::SequenceExpression {1989 instructions,1990 value,1991 ..1992 } => {1993 let block_items: Vec<ReactiveStatement> = instructions1994 .iter()1995 .map(|i| ReactiveStatement::Instruction(i.clone()))1996 .collect();1997 let body = codegen_block_no_reset(cx, &block_items)?.body;1998 let mut expressions: Vec<Expression> = Vec::new();1999 for stmt in body {2000 match stmt {
Findings
✓ No findings reported for this file.