compiler/crates/react_compiler_reactive_scopes/src/codegen_reactive_function.rs RUST 4,462 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 4,462.
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.