diff --git a/.typos.toml b/.typos.toml index d467cc0bb7a1c..0daf92ea56d2f 100644 --- a/.typos.toml +++ b/.typos.toml @@ -18,6 +18,10 @@ extend-exclude = [ "crates/oxc_linter/src/rules/jsx_a11y/img_redundant_alt.rs", "crates/oxc_linter/src/rules/react/no_unknown_property.rs", "crates/oxc_parser/src/lexer/byte_handlers.rs", + # Vendored React Compiler core (kept close to upstream; legit short identifiers + # like `pn`/`oce`/`ome`/`froms` trip typos). The hand-written conversion code + # (`convert_*`, `lib`, ...) is not matched by this glob and stays checked. + "crates/oxc_react_compiler/src/react_compiler*", "crates/oxc_syntax/src/xml_entities.rs", "patches/**", "pnpm-lock.yaml", diff --git a/Cargo.lock b/Cargo.lock index 98bfe3975f000..afacf5ed5874a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,179 +734,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "forked_react_compiler" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2369cf719891033c9b17a8093c7cf83dbad40113d4a24513fe3d42e37050ea" -dependencies = [ - "forked_react_compiler_ast", - "forked_react_compiler_diagnostics", - "forked_react_compiler_hir", - "forked_react_compiler_inference", - "forked_react_compiler_lowering", - "forked_react_compiler_optimization", - "forked_react_compiler_reactive_scopes", - "forked_react_compiler_ssa", - "forked_react_compiler_typeinference", - "forked_react_compiler_utils", - "forked_react_compiler_validation", - "indexmap", - "rustc-hash", - "serde", - "serde_json", -] - -[[package]] -name = "forked_react_compiler_ast" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daccd93f89b1bc808a485767928b80456711e2141e54722e478795c84e0d5c7b" -dependencies = [ - "forked_react_compiler_diagnostics", - "forked_react_compiler_utils", - "indexmap", - "rustc-hash", - "serde", - "serde-transcode", - "serde_json", -] - -[[package]] -name = "forked_react_compiler_diagnostics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3d5b1d76f01cd1cc64d016880380cada17c2e7e3fc2337ffd2db690cdb3b4e" -dependencies = [ - "rustc-hash", - "serde", -] - -[[package]] -name = "forked_react_compiler_hir" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4668644f0b802b21e0a547e74cbf01dccb8f854c894800f9f1115fa5d0816383" -dependencies = [ - "forked_react_compiler_ast", - "forked_react_compiler_diagnostics", - "forked_react_compiler_utils", - "indexmap", - "rustc-hash", - "serde", - "serde_json", -] - -[[package]] -name = "forked_react_compiler_inference" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17f5d24f016fe5fab0b15e36fcaa54ef377a341dbcd747f1e4371e29acb3198" -dependencies = [ - "forked_react_compiler_diagnostics", - "forked_react_compiler_hir", - "forked_react_compiler_lowering", - "forked_react_compiler_optimization", - "forked_react_compiler_ssa", - "forked_react_compiler_utils", - "indexmap", - "rustc-hash", -] - -[[package]] -name = "forked_react_compiler_lowering" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dc9d291addca587d64bc90b7d6e58fcd4ebc255aa9a97751e0058ba2a4c1dab" -dependencies = [ - "forked_react_compiler_ast", - "forked_react_compiler_diagnostics", - "forked_react_compiler_hir", - "forked_react_compiler_utils", - "indexmap", - "rustc-hash", - "serde_json", -] - -[[package]] -name = "forked_react_compiler_optimization" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "717defb246a678ab851a46210d65e0c7604cc7944395d380338a3062924965c9" -dependencies = [ - "forked_react_compiler_diagnostics", - "forked_react_compiler_hir", - "forked_react_compiler_lowering", - "forked_react_compiler_ssa", - "forked_react_compiler_utils", - "indexmap", - "rustc-hash", -] - -[[package]] -name = "forked_react_compiler_reactive_scopes" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928d6ae8021a1c8f5a32b20cf2ef4b20f76cf997ae753c4e958e896bcf64ae90" -dependencies = [ - "forked_react_compiler_ast", - "forked_react_compiler_diagnostics", - "forked_react_compiler_hir", - "forked_react_compiler_utils", - "hmac-sha256", - "indexmap", - "rustc-hash", - "serde_json", -] - -[[package]] -name = "forked_react_compiler_ssa" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd26c931f5a799188a2d7bb5960cfde048c91348a074876e49c9f7d6e8581d93" -dependencies = [ - "forked_react_compiler_diagnostics", - "forked_react_compiler_hir", - "forked_react_compiler_utils", - "indexmap", - "rustc-hash", -] - -[[package]] -name = "forked_react_compiler_typeinference" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f603c0f89520b55e9c34fc6264df6f29bba60d7e94269f433dd13ba76c4d686" -dependencies = [ - "forked_react_compiler_diagnostics", - "forked_react_compiler_hir", - "forked_react_compiler_ssa", - "rustc-hash", -] - -[[package]] -name = "forked_react_compiler_utils" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5390b90b48cf2aaab7e511009e10e6b8c2c5b750a8c873881b4014e4295cb128" -dependencies = [ - "indexmap", - "rustc-hash", -] - -[[package]] -name = "forked_react_compiler_validation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7801bd3359181d53a99a06c43f76943f56736814f3dead0ce5fac9118391f18b" -dependencies = [ - "forked_react_compiler_diagnostics", - "forked_react_compiler_hir", - "forked_react_compiler_utils", - "indexmap", - "rustc-hash", -] - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1150,12 +977,6 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823" -[[package]] -name = "hmac-sha256" -version = "1.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" - [[package]] name = "http" version = "1.3.1" @@ -2499,9 +2320,6 @@ dependencies = [ name = "oxc_react_compiler" version = "0.137.0" dependencies = [ - "forked_react_compiler", - "forked_react_compiler_ast", - "forked_react_compiler_hir", "indexmap", "oxc_allocator", "oxc_ast", @@ -2513,7 +2331,6 @@ dependencies = [ "oxc_span", "oxc_syntax", "rustc-hash", - "serde_json", ] [[package]] @@ -2722,6 +2539,7 @@ dependencies = [ "napi-derive", "oxc", "oxc_napi", + "oxc_react_compiler", "oxc_sourcemap", "rustc-hash", ] @@ -3421,15 +3239,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-transcode" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590c0e25c2a5bb6e85bf5c1bce768ceb86b316e7a01bdf07d2cb4ec2271990e2" -dependencies = [ - "serde", -] - [[package]] name = "serde_core" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index 6ae026b3743a8..f5062ba6a350b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "3" members = ["apps/*", "crates/*", "napi/*", "tasks/*"] -exclude = ["apps/shared", "tasks/lint_rules", "tasks/e2e"] +exclude = ["apps/shared", "tasks/lint_rules", "tasks/e2e", "tasks/react_compiler_compare"] [workspace.package] authors = ["Boshen ", "Oxc contributors"] diff --git a/crates/oxc_linter/src/context/host.rs b/crates/oxc_linter/src/context/host.rs index 54ed959cb00a6..9722d24a87864 100644 --- a/crates/oxc_linter/src/context/host.rs +++ b/crates/oxc_linter/src/context/host.rs @@ -167,6 +167,11 @@ pub struct ContextHost<'a> { pub(super) config: Arc, /// Front-end frameworks that might be in use in the target file. pub(super) frameworks: FrameworkFlags, + /// The arena that owns the parsed AST for this file. Rules that need to + /// allocate scratch AST nodes (e.g. the React Compiler lint rule, which runs + /// the compiler read-only) can reuse it instead of deep-cloning the program + /// into a private arena. + allocator: &'a oxc_allocator::Allocator, } impl std::fmt::Debug for ContextHost<'_> { @@ -183,6 +188,7 @@ impl<'a> ContextHost<'a> { sub_hosts: Vec>, options: LintOptions, config: Arc, + allocator: &'a oxc_allocator::Allocator, ) -> Self { const DIAGNOSTICS_INITIAL_CAPACITY: usize = 512; @@ -203,6 +209,7 @@ impl<'a> ContextHost<'a> { file_extension, config, frameworks: options.framework_hints, + allocator, } .sniff_for_frameworks() } @@ -228,6 +235,12 @@ impl<'a> ContextHost<'a> { &self.current_sub_host().semantic } + /// The arena that owns this file's parsed AST. + #[inline] + pub fn allocator(&self) -> &'a oxc_allocator::Allocator { + self.allocator + } + /// Mutable reference to the [`Semantic`] analysis of current script block. #[inline] pub fn semantic_mut(&mut self) -> &mut Semantic<'a> { diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index 593c0c43136eb..dd937cea9d82b 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -113,6 +113,14 @@ impl<'a> LintContext<'a> { self.parent.module_record() } + /// The arena that owns this file's parsed AST. Rules that need scratch AST + /// allocations (e.g. the React Compiler rule) can reuse it instead of cloning + /// the program into a private arena. + #[inline] + pub fn allocator(&self) -> &'a oxc_allocator::Allocator { + self.parent.allocator() + } + /// Get the control flow graph for the current program. #[inline] pub fn cfg(&self) -> &ControlFlowGraph { diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index 24f7eef7561e9..95673d6b31111 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -367,7 +367,8 @@ impl Linter { let ResolvedLinterState { rules, config, external_rules } = self.config.resolve(path); let mut timing_recorder = TIMINGS.then(|| RuleTimingRecorder::with_capacity(rules.len())); - let mut ctx_host = Rc::new(ContextHost::new(path, context_sub_hosts, self.options, config)); + let mut ctx_host = + Rc::new(ContextHost::new(path, context_sub_hosts, self.options, config, allocator)); #[cfg(debug_assertions)] let mut current_diagnostic_index = 0; diff --git a/crates/oxc_linter/src/rules/react/react_compiler.rs b/crates/oxc_linter/src/rules/react/react_compiler.rs index d5be713d7ca43..5d97662869d75 100644 --- a/crates/oxc_linter/src/rules/react/react_compiler.rs +++ b/crates/oxc_linter/src/rules/react/react_compiler.rs @@ -124,7 +124,7 @@ impl Rule for ReactCompiler { let program = ctx.nodes().program(); let options = react_compiler_options(ctx.file_path().to_str().map(ToString::to_string)); - let result = oxc_react_compiler::lint(program, options); + let result = oxc_react_compiler::lint(program, ctx.allocator(), options); let diagnostics = result.diagnostics.into_vec(); let diagnostics = if self.report_all_bailouts { diff --git a/crates/oxc_linter/src/utils/jest.rs b/crates/oxc_linter/src/utils/jest.rs index 678dbcb22be44..3bb39dec5f6eb 100644 --- a/crates/oxc_linter/src/utils/jest.rs +++ b/crates/oxc_linter/src/utils/jest.rs @@ -379,6 +379,7 @@ mod test { )], LintOptions::default(), Arc::default(), + &allocator, )) .spawn_for_test() }; diff --git a/crates/oxc_linter/src/utils/regex.rs b/crates/oxc_linter/src/utils/regex.rs index 6fa225723afbc..bab206677a929 100644 --- a/crates/oxc_linter/src/utils/regex.rs +++ b/crates/oxc_linter/src/utils/regex.rs @@ -274,6 +274,7 @@ mod test { )], LintOptions::default(), Arc::default(), + &allocator, )) .spawn_for_test(); diff --git a/crates/oxc_react_compiler/Cargo.toml b/crates/oxc_react_compiler/Cargo.toml index 4b83b979a3004..fe0974adf2aee 100644 --- a/crates/oxc_react_compiler/Cargo.toml +++ b/crates/oxc_react_compiler/Cargo.toml @@ -3,11 +3,12 @@ # Native oxc integration for the Rust port of the React Compiler: # https://github.com/react/react/tree/main/compiler # -# This crate owns the oxc <-> react_compiler_ast conversion layer. The React -# Compiler *core* crates are frontend-agnostic (they depend on serde/indexmap -# only, never on oxc), so we consume them from crates.io (a published MIT fork of -# the React Compiler) and keep the AST/scope conversion on our side, written -# against the live workspace oxc AST. +# This crate runs the React Compiler directly over the live workspace oxc AST. +# The React Compiler *core* (a Rust port of the MIT React Compiler) is vendored +# in `src/react_compiler*` — frontend-agnostic modules with no oxc dependency. +# The scope conversion (`convert_scope.rs`) lives alongside it, written against +# the oxc AST. TS types and unmodeled nodes round-trip by re-parsing their source +# span, so the compiler carries no JSON/serde layer. [package] name = "oxc_react_compiler" @@ -26,6 +27,12 @@ description = "oxc integration for the Rust port of React Compiler" [lib] doctest = false +[features] +# Off by default. Gates the heavy HIR / reactive-function debug (IR-dump) +# printers, which only run when `PluginOptions.debug` is set. Excluding them from +# the default build trims ~100 KiB of code with no effect on transform/lint. +debug = [] + [dependencies] oxc_allocator = { workspace = true } oxc_ast = { workspace = true } @@ -37,15 +44,8 @@ oxc_semantic = { workspace = true } oxc_span = { workspace = true } oxc_syntax = { workspace = true } -# React Compiler core crates — frontend-agnostic (no oxc deps). Published fork of -# the React Compiler (MIT). `package` keeps the in-code name `react_compiler*`. -react_compiler = { package = "forked_react_compiler", version = "0.2.0" } -react_compiler_ast = { package = "forked_react_compiler_ast", version = "0.2.0" } -react_compiler_hir = { package = "forked_react_compiler_hir", version = "0.2.0" } - -indexmap = { workspace = true, features = ["serde"] } +indexmap = { workspace = true } rustc-hash = { workspace = true } -serde_json = { workspace = true, features = ["raw_value"] } # The `src/` conversion modules are vendored close to upstream `react_compiler_oxc` # (kept near byte-for-byte to ease re-syncing), so relax the handful of lints they @@ -54,7 +54,7 @@ serde_json = { workspace = true, features = ["raw_value"] } # lints (`todo!`/`unimplemented!`/...) off the vendored code while clippy's default # correctness and perf checks still apply. [lints.clippy] -disallowed_types = "allow" # `react_compiler_ast`'s `ScopeInfo`/`ScopeData` fields are std `HashMap` +disallowed_types = "allow" # `ScopeInfo`/`ScopeData` fields are std `HashMap` collapsible_if = "allow" needless_return = "allow" unnecessary_unwrap = "allow" diff --git a/crates/oxc_react_compiler/README.md b/crates/oxc_react_compiler/README.md index f463c9f5324a1..00683317f69c6 100644 --- a/crates/oxc_react_compiler/README.md +++ b/crates/oxc_react_compiler/README.md @@ -4,10 +4,9 @@ oxc integration for the Rust port of the [React Compiler](https://github.com/rea ## Overview -This crate owns the oxc ↔ `react_compiler_ast` (Babel) conversion layer and runs the React -Compiler over an oxc AST, memoizing React components and hooks. The compiler _core_ crates are -front-end agnostic (they never depend on oxc), so they are consumed from crates.io as a published -fork; the AST and scope conversion lives here, written against the live oxc AST. +This crate runs the React Compiler directly over an oxc AST, memoizing React components and hooks. +The compiler _core_ modules are front-end agnostic (they never depend on oxc) and are vendored under +`src/react_compiler*`; the scope conversion lives here, written against the live oxc AST. ## API diff --git a/crates/oxc_react_compiler/examples/react_compiler.rs b/crates/oxc_react_compiler/examples/react_compiler.rs index 2da714948da32..920eae9eea4d3 100644 --- a/crates/oxc_react_compiler/examples/react_compiler.rs +++ b/crates/oxc_react_compiler/examples/react_compiler.rs @@ -36,9 +36,9 @@ fn main() { println!("{source_text}"); let allocator = Allocator::default(); - let program = Parser::new(&allocator, &source_text, source_type).parse().program; + let mut program = Parser::new(&allocator, &source_text, source_type).parse().program; - let result = transform(&program, &allocator, default_plugin_options()); + let result = transform(&mut program, &allocator, default_plugin_options()); if !result.diagnostics.is_empty() { println!("Diagnostics:\n"); @@ -48,14 +48,12 @@ fn main() { println!(); } - match result.program { - Some(compiled) => { - let output = Codegen::new().build(&compiled).code; - println!("Compiled:\n"); - println!("{output}"); - } - None => { - println!("No changes: no React component or hook found (or compilation bailed out)."); - } + if result.changed { + // The compiler spliced the memoized functions into `program` in place. + let output = Codegen::new().build(&program).code; + println!("Compiled:\n"); + println!("{output}"); + } else { + println!("No changes: no React component or hook found (or compilation bailed out)."); } } diff --git a/crates/oxc_react_compiler/examples/react_compiler_debug.rs b/crates/oxc_react_compiler/examples/react_compiler_debug.rs new file mode 100644 index 0000000000000..4c27085938ec0 --- /dev/null +++ b/crates/oxc_react_compiler/examples/react_compiler_debug.rs @@ -0,0 +1,74 @@ +//! Dump the React Compiler's internal IR after each pipeline pass. +//! +//! Enables `options.debug`, runs the pipeline, and prints every debug log entry +//! (the compiler emits an HIR/reactive-function dump after most passes). +//! +//! Usage: +//! cargo run -p oxc_react_compiler --example react_compiler_debug -- [pass_name] +//! +//! With no `pass_name`, prints every pass. With one, prints only passes whose +//! name exactly matches (e.g. `HIR`, `SSA`, `InferTypes`). + +use std::path::Path; + +use oxc_allocator::Allocator; +use oxc_parser::Parser; +use oxc_semantic::SemanticBuilder; +use oxc_span::SourceType; + +use oxc_ast::AstBuilder; + +use oxc_react_compiler::convert_scope::convert_scope_info; +use oxc_react_compiler::default_plugin_options; +use oxc_react_compiler::react_compiler::entrypoint::compile_result::{ + CompileResult, OrderedLogItem, +}; +use oxc_react_compiler::react_compiler::entrypoint::program::compile_program_and_finalize; + +fn main() { + let mut args = std::env::args().skip(1); + let name = args.next().unwrap_or_else(|| { + eprintln!("usage: react_compiler_debug [pass_name]"); + std::process::exit(1); + }); + let filter = args.next(); + + let path = Path::new(&name); + let source_text = std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{name}: {e}")); + let source_type = SourceType::from_path(path).unwrap_or_else(|_| SourceType::tsx()); + + let allocator = Allocator::default(); + let mut program = Parser::new(&allocator, &source_text, source_type).parse().program; + let scope_info = { + let semantic = SemanticBuilder::new().with_build_nodes(true).build(&program).semantic; + convert_scope_info(&semantic, &program) + }; + + let mut options = default_plugin_options(); + options.debug = true; + + let ast_builder = AstBuilder::new(&allocator); + let result = compile_program_and_finalize(&ast_builder, &mut program, scope_info, options); + let ordered_log = match &result { + CompileResult::Success { ordered_log, .. } | CompileResult::Error { ordered_log, .. } => { + ordered_log + } + }; + + let mut printed = 0; + for item in ordered_log { + if let OrderedLogItem::Debug { entry } = item { + if let Some(f) = &filter { + if &entry.name != f { + continue; + } + } + println!("\x1b[1;36m===== after pass: {} =====\x1b[0m", entry.name); + println!("{}", entry.value); + printed += 1; + } + } + if printed == 0 { + eprintln!("(no debug entries — no React component/hook found, or pass name didn't match)"); + } +} diff --git a/crates/oxc_react_compiler/src/convert_ast.rs b/crates/oxc_react_compiler/src/convert_ast.rs deleted file mode 100644 index fd41a487e6311..0000000000000 --- a/crates/oxc_react_compiler/src/convert_ast.rs +++ /dev/null @@ -1,3142 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -use oxc_ast::ast as oxc; -use oxc_span::GetSpan; -use oxc_span::Span; -use react_compiler_ast::File; -use react_compiler_ast::InterpreterDirective; -use react_compiler_ast::Program; -use react_compiler_ast::SourceType; -use react_compiler_ast::common::BaseNode; -use react_compiler_ast::common::Comment; -use react_compiler_ast::common::CommentData; -use react_compiler_ast::common::Position; -use react_compiler_ast::common::RawNode; -use react_compiler_ast::common::SourceLocation; -use react_compiler_ast::declarations::*; -use react_compiler_ast::expressions::*; -use react_compiler_ast::jsx::*; -use react_compiler_ast::literals::*; -use react_compiler_ast::operators::*; -use react_compiler_ast::patterns::*; -use react_compiler_ast::statements::*; - -/// Decode XML/HTML entities in JSX text (`&` → `&`, `>` → `>`, `{` -/// → `{`, `😀` → emoji, …) so the Babel `JSXText.value` is the decoded -/// text, matching Babel's parser. oxc keeps JSX text raw in the AST, so the -/// bridge to the Babel AST must decode here. Mirrors `oxc_transformer`'s JSX -/// entity decoder; unrecognized `&…;` sequences are kept verbatim. -fn decode_jsx_entities(s: &str) -> String { - if !s.contains('&') { - return s.to_string(); - } - let mut out = String::with_capacity(s.len()); - let mut chars = s.char_indices(); - let mut prev = 0; - while let Some((i, c)) = chars.next() { - if c != '&' { - continue; - } - let mut start = i; - let mut end = None; - for (j, c) in chars.by_ref() { - if c == ';' { - end = Some(j); - break; - } else if c == '&' { - start = j; - } - } - let Some(end) = end else { break }; - out.push_str(&s[prev..start]); - prev = end + 1; - let word = &s[start + 1..end]; - let decoded = if let Some(num) = word.strip_prefix('#') { - if let Some(hex) = num.strip_prefix(['x', 'X']) { - u32::from_str_radix(hex, 16).ok().and_then(char::from_u32) - } else { - num.parse::().ok().and_then(char::from_u32) - } - } else { - oxc_syntax::xml_entities::XML_ENTITIES.get(word).copied() - }; - match decoded { - Some(c) => out.push(c), - // Not a recognized entity — keep the `&…;` literal. - None => { - out.push('&'); - out.push_str(word); - out.push(';'); - } - } - } - out.push_str(&s[prev..]); - out -} - -/// Converts an OXC AST to the React compiler's Babel-compatible AST. -pub fn convert_program(program: &oxc::Program, source_text: &str) -> File { - let ctx = ConvertCtx::new(source_text); - let base = ctx.make_base_node(program.span); - - let mut body = Vec::new(); - for stmt in &program.body { - body.push(ctx.convert_statement(stmt)); - } - - let directives = program.directives.iter().map(|d| ctx.convert_directive(d)).collect(); - - let source_type = match program.source_type.is_module() { - true => SourceType::Module, - false => SourceType::Script, - }; - - // Convert OXC comments - let comments = ctx.convert_comments(&program.comments); - - File { - base: ctx.make_base_node(program.span), - program: Program { - base, - body, - directives, - source_type, - interpreter: program.hashbang.as_ref().map(|hashbang| InterpreterDirective { - base: ctx.make_base_node(hashbang.span), - value: hashbang.value.to_string(), - }), - source_file: None, - }, - comments, - errors: vec![], - } -} - -struct ConvertCtx<'a> { - source_text: &'a str, - line_offsets: Vec, -} - -impl<'a> ConvertCtx<'a> { - fn new(source_text: &'a str) -> Self { - let mut line_offsets = vec![0]; - for (i, ch) in source_text.char_indices() { - if ch == '\n' { - line_offsets.push((i + 1) as u32); - } - } - Self { source_text, line_offsets } - } - - fn make_base_node(&self, span: Span) -> BaseNode { - BaseNode { - node_type: None, - start: Some(span.start), - end: Some(span.end), - loc: Some(self.source_location(span)), - range: None, - extra: None, - node_id: Some(span.start), - leading_comments: None, - inner_comments: None, - trailing_comments: None, - } - } - - fn make_typed_json_base( - &self, - type_name: &str, - span: Span, - ) -> serde_json::Map { - let mut obj = serde_json::Map::new(); - obj.insert("type".to_string(), serde_json::Value::String(type_name.to_string())); - obj.insert("start".to_string(), serde_json::Value::from(span.start)); - obj.insert("end".to_string(), serde_json::Value::from(span.end)); - obj.insert( - "loc".to_string(), - serde_json::to_value(self.source_location(span)).unwrap_or(serde_json::Value::Null), - ); - obj.insert("_nodeId".to_string(), serde_json::Value::from(span.start)); - obj - } - - fn convert_ts_type_annotation_json(&self, type_annotation: &oxc::TSTypeAnnotation) -> RawNode { - let mut obj = self.make_typed_json_base("TSTypeAnnotation", type_annotation.span); - obj.insert( - "typeAnnotation".to_string(), - self.convert_ts_type_json_value(&type_annotation.type_annotation), - ); - RawNode::from_value(&serde_json::Value::Object(obj)) - } - - fn convert_ts_type_parameter_instantiation_json( - &self, - type_arguments: &oxc::TSTypeParameterInstantiation, - ) -> RawNode { - let mut obj = - self.make_typed_json_base("TSTypeParameterInstantiation", type_arguments.span); - obj.insert( - "params".to_string(), - serde_json::Value::Array( - type_arguments - .params - .iter() - .map(|ty| self.convert_ts_type_json_value(ty)) - .collect(), - ), - ); - RawNode::from_value(&serde_json::Value::Object(obj)) - } - - fn convert_ts_type_json(&self, ty: &oxc::TSType) -> RawNode { - RawNode::from_value(&self.convert_ts_type_json_value(ty)) - } - - fn convert_ts_type_json_value(&self, ty: &oxc::TSType) -> serde_json::Value { - let type_name = match ty { - oxc::TSType::TSAnyKeyword(_) => "TSAnyKeyword", - oxc::TSType::TSBigIntKeyword(_) => "TSBigIntKeyword", - oxc::TSType::TSBooleanKeyword(_) => "TSBooleanKeyword", - oxc::TSType::TSIntrinsicKeyword(_) => "TSIntrinsicKeyword", - oxc::TSType::TSNeverKeyword(_) => "TSNeverKeyword", - oxc::TSType::TSNullKeyword(_) => "TSNullKeyword", - oxc::TSType::TSNumberKeyword(_) => "TSNumberKeyword", - oxc::TSType::TSObjectKeyword(_) => "TSObjectKeyword", - oxc::TSType::TSStringKeyword(_) => "TSStringKeyword", - oxc::TSType::TSSymbolKeyword(_) => "TSSymbolKeyword", - oxc::TSType::TSThisType(_) => "TSThisType", - oxc::TSType::TSUndefinedKeyword(_) => "TSUndefinedKeyword", - oxc::TSType::TSUnknownKeyword(_) => "TSUnknownKeyword", - oxc::TSType::TSVoidKeyword(_) => "TSVoidKeyword", - oxc::TSType::TSArrayType(_) => "TSArrayType", - oxc::TSType::TSUnionType(_) => "TSUnionType", - oxc::TSType::TSParenthesizedType(_) => "TSParenthesizedType", - oxc::TSType::TSLiteralType(_) => "TSLiteralType", - oxc::TSType::TSTypeReference(_) => "TSTypeReference", - oxc::TSType::TSTypeOperatorType(_) => "TSTypeOperator", - oxc::TSType::TSTupleType(_) => "TSTupleType", - oxc::TSType::TSIntersectionType(_) => "TSIntersectionType", - oxc::TSType::TSTypeLiteral(_) => "TSTypeLiteral", - oxc::TSType::TSTypeQuery(_) => "TSTypeQuery", - oxc::TSType::TSFunctionType(_) => "TSFunctionType", - oxc::TSType::TSConstructorType(_) => "TSConstructorType", - oxc::TSType::TSConditionalType(_) => "TSConditionalType", - oxc::TSType::TSIndexedAccessType(_) => "TSIndexedAccessType", - oxc::TSType::TSInferType(_) => "TSInferType", - oxc::TSType::TSImportType(_) => "TSImportType", - oxc::TSType::TSMappedType(_) => "TSMappedType", - oxc::TSType::TSNamedTupleMember(_) => "TSNamedTupleMember", - oxc::TSType::TSTemplateLiteralType(_) => "TSTemplateLiteralType", - oxc::TSType::TSTypePredicate(_) => "TSTypePredicate", - oxc::TSType::JSDocNullableType(_) => "JSDocNullableType", - oxc::TSType::JSDocNonNullableType(_) => "JSDocNonNullableType", - oxc::TSType::JSDocUnknownType(_) => "JSDocUnknownType", - }; - - let mut obj = self.make_typed_json_base(type_name, ty.span()); - match ty { - oxc::TSType::TSArrayType(array) => { - obj.insert( - "elementType".to_string(), - self.convert_ts_type_json_value(&array.element_type), - ); - } - oxc::TSType::TSUnionType(union) => { - obj.insert( - "types".to_string(), - serde_json::Value::Array( - union.types.iter().map(|ty| self.convert_ts_type_json_value(ty)).collect(), - ), - ); - } - oxc::TSType::TSParenthesizedType(parenthesized) => { - obj.insert( - "typeAnnotation".to_string(), - self.convert_ts_type_json_value(&parenthesized.type_annotation), - ); - } - oxc::TSType::TSTypeReference(reference) => { - obj.insert( - "typeName".to_string(), - self.convert_ts_type_name_json_value(&reference.type_name), - ); - if let Some(type_arguments) = &reference.type_arguments { - obj.insert( - "typeParameters".to_string(), - self.convert_ts_type_parameter_instantiation_json(type_arguments) - .parse_value(), - ); - } - } - oxc::TSType::TSTypeQuery(query) => { - obj.insert( - "exprName".to_string(), - self.convert_ts_type_query_expr_name_json_value(&query.expr_name), - ); - if let Some(type_arguments) = &query.type_arguments { - obj.insert( - "typeParameters".to_string(), - self.convert_ts_type_parameter_instantiation_json(type_arguments) - .parse_value(), - ); - } - } - oxc::TSType::TSIndexedAccessType(indexed) => { - obj.insert( - "objectType".to_string(), - self.convert_ts_type_json_value(&indexed.object_type), - ); - obj.insert( - "indexType".to_string(), - self.convert_ts_type_json_value(&indexed.index_type), - ); - } - oxc::TSType::TSTypeOperatorType(operator) => { - let operator_name = match operator.operator { - oxc::TSTypeOperatorOperator::Keyof => "keyof", - oxc::TSTypeOperatorOperator::Unique => "unique", - oxc::TSTypeOperatorOperator::Readonly => "readonly", - }; - obj.insert( - "operator".to_string(), - serde_json::Value::String(operator_name.to_string()), - ); - obj.insert( - "typeAnnotation".to_string(), - self.convert_ts_type_json_value(&operator.type_annotation), - ); - } - oxc::TSType::TSLiteralType(literal) => { - obj.insert( - "literal".to_string(), - self.convert_ts_literal_json_value(&literal.literal), - ); - } - _ => {} - } - serde_json::Value::Object(obj) - } - - fn convert_ts_type_name_json_value(&self, type_name: &oxc::TSTypeName) -> serde_json::Value { - match type_name { - oxc::TSTypeName::IdentifierReference(identifier) => { - let mut obj = self.make_typed_json_base("Identifier", identifier.span); - obj.insert( - "name".to_string(), - serde_json::Value::String(identifier.name.to_string()), - ); - serde_json::Value::Object(obj) - } - oxc::TSTypeName::QualifiedName(qualified) => { - let mut obj = self.make_typed_json_base("TSQualifiedName", qualified.span); - obj.insert( - "left".to_string(), - self.convert_ts_type_name_json_value(&qualified.left), - ); - let mut right = self.make_typed_json_base("Identifier", qualified.right.span); - right.insert( - "name".to_string(), - serde_json::Value::String(qualified.right.name.to_string()), - ); - obj.insert("right".to_string(), serde_json::Value::Object(right)); - serde_json::Value::Object(obj) - } - oxc::TSTypeName::ThisExpression(this) => { - serde_json::Value::Object(self.make_typed_json_base("TSThisType", this.span)) - } - } - } - - fn convert_ts_type_query_expr_name_json_value( - &self, - expr_name: &oxc::TSTypeQueryExprName, - ) -> serde_json::Value { - match expr_name { - oxc::TSTypeQueryExprName::IdentifierReference(identifier) => { - let mut obj = self.make_typed_json_base("Identifier", identifier.span); - obj.insert( - "name".to_string(), - serde_json::Value::String(identifier.name.to_string()), - ); - serde_json::Value::Object(obj) - } - oxc::TSTypeQueryExprName::QualifiedName(qualified) => { - let mut obj = self.make_typed_json_base("TSQualifiedName", qualified.span); - obj.insert( - "left".to_string(), - self.convert_ts_type_name_json_value(&qualified.left), - ); - let mut right = self.make_typed_json_base("Identifier", qualified.right.span); - right.insert( - "name".to_string(), - serde_json::Value::String(qualified.right.name.to_string()), - ); - obj.insert("right".to_string(), serde_json::Value::Object(right)); - serde_json::Value::Object(obj) - } - oxc::TSTypeQueryExprName::ThisExpression(this) => { - serde_json::Value::Object(self.make_typed_json_base("TSThisType", this.span)) - } - oxc::TSTypeQueryExprName::TSImportType(import) => { - serde_json::Value::Object(self.make_typed_json_base("TSImportType", import.span)) - } - } - } - - fn convert_ts_literal_json_value(&self, literal: &oxc::TSLiteral) -> serde_json::Value { - match literal { - oxc::TSLiteral::BooleanLiteral(literal) => { - let mut obj = self.make_typed_json_base("BooleanLiteral", literal.span); - obj.insert("value".to_string(), serde_json::Value::Bool(literal.value)); - serde_json::Value::Object(obj) - } - oxc::TSLiteral::NumericLiteral(literal) => { - let mut obj = self.make_typed_json_base("NumericLiteral", literal.span); - obj.insert("value".to_string(), serde_json::Value::from(literal.value)); - serde_json::Value::Object(obj) - } - oxc::TSLiteral::StringLiteral(literal) => { - let mut obj = self.make_typed_json_base("StringLiteral", literal.span); - obj.insert( - "value".to_string(), - serde_json::Value::String(literal.value.to_string()), - ); - serde_json::Value::Object(obj) - } - oxc::TSLiteral::BigIntLiteral(literal) => { - let mut obj = self.make_typed_json_base("BigIntLiteral", literal.span); - obj.insert( - "value".to_string(), - serde_json::Value::String(literal.value.to_string()), - ); - serde_json::Value::Object(obj) - } - other => { - serde_json::Value::Object(self.make_typed_json_base("TSLiteral", other.span())) - } - } - } - - fn position(&self, offset: u32) -> Position { - let line_idx = match self.line_offsets.binary_search(&offset) { - Ok(idx) => idx, - Err(idx) => idx.saturating_sub(1), - }; - let line_start = self.line_offsets[line_idx]; - Position { line: (line_idx + 1) as u32, column: offset - line_start, index: Some(offset) } - } - - fn source_location(&self, span: Span) -> SourceLocation { - SourceLocation { - start: self.position(span.start), - end: self.position(span.end), - filename: None, - identifier_name: None, - } - } - - fn convert_comments(&self, comments: &[oxc::Comment]) -> Vec { - comments - .iter() - .map(|comment| { - let base = self.make_base_node(comment.span); - // OXC comment spans include delimiters (// or /* */), so we need - // to strip them to get the content-only value that the compiler expects. - let raw = &self.source_text[comment.span.start as usize..comment.span.end as usize]; - let value = match comment.kind { - oxc::CommentKind::Line => { - // Strip leading // - raw.strip_prefix("//").unwrap_or(raw).trim().to_string() - } - oxc::CommentKind::SingleLineBlock | oxc::CommentKind::MultiLineBlock => { - // Strip leading /* and trailing */ - let stripped = - raw.strip_prefix("/*").unwrap_or(raw).strip_suffix("*/").unwrap_or(raw); - stripped.trim().to_string() - } - }; - let comment_data = - CommentData { value, start: base.start, end: base.end, loc: base.loc.clone() }; - match comment.kind { - oxc::CommentKind::Line => Comment::CommentLine(comment_data), - oxc::CommentKind::SingleLineBlock | oxc::CommentKind::MultiLineBlock => { - Comment::CommentBlock(comment_data) - } - } - }) - .collect() - } - - fn convert_directive(&self, directive: &oxc::Directive) -> Directive { - let base = self.make_base_node(directive.span); - Directive { - base, - value: DirectiveLiteral { - base: self.make_base_node(directive.expression.span), - value: directive.expression.value.to_string(), - }, - } - } - - fn convert_statement(&self, stmt: &oxc::Statement) -> Statement { - match stmt { - oxc::Statement::BlockStatement(s) => { - Statement::BlockStatement(self.convert_block_statement(s)) - } - oxc::Statement::ReturnStatement(s) => { - Statement::ReturnStatement(self.convert_return_statement(s)) - } - oxc::Statement::IfStatement(s) => Statement::IfStatement(self.convert_if_statement(s)), - oxc::Statement::ForStatement(s) => { - Statement::ForStatement(self.convert_for_statement(s)) - } - oxc::Statement::WhileStatement(s) => { - Statement::WhileStatement(self.convert_while_statement(s)) - } - oxc::Statement::DoWhileStatement(s) => { - Statement::DoWhileStatement(self.convert_do_while_statement(s)) - } - oxc::Statement::ForInStatement(s) => { - Statement::ForInStatement(self.convert_for_in_statement(s)) - } - oxc::Statement::ForOfStatement(s) => { - Statement::ForOfStatement(self.convert_for_of_statement(s)) - } - oxc::Statement::SwitchStatement(s) => { - Statement::SwitchStatement(self.convert_switch_statement(s)) - } - oxc::Statement::ThrowStatement(s) => { - Statement::ThrowStatement(self.convert_throw_statement(s)) - } - oxc::Statement::TryStatement(s) => { - Statement::TryStatement(self.convert_try_statement(s)) - } - oxc::Statement::BreakStatement(s) => { - Statement::BreakStatement(self.convert_break_statement(s)) - } - oxc::Statement::ContinueStatement(s) => { - Statement::ContinueStatement(self.convert_continue_statement(s)) - } - oxc::Statement::LabeledStatement(s) => { - Statement::LabeledStatement(self.convert_labeled_statement(s)) - } - oxc::Statement::ExpressionStatement(s) => { - Statement::ExpressionStatement(self.convert_expression_statement(s)) - } - oxc::Statement::EmptyStatement(s) => { - Statement::EmptyStatement(EmptyStatement { base: self.make_base_node(s.span) }) - } - oxc::Statement::DebuggerStatement(s) => { - Statement::DebuggerStatement(DebuggerStatement { - base: self.make_base_node(s.span), - }) - } - oxc::Statement::WithStatement(s) => { - Statement::WithStatement(self.convert_with_statement(s)) - } - // Declaration variants (inherited) - oxc::Statement::VariableDeclaration(v) => { - Statement::VariableDeclaration(self.convert_variable_declaration(v)) - } - oxc::Statement::FunctionDeclaration(f) => { - if f.body.is_none() { - Statement::TSDeclareFunction(self.convert_ts_declare_function(f)) - } else { - Statement::FunctionDeclaration(self.convert_function_declaration(f)) - } - } - oxc::Statement::ClassDeclaration(c) => { - Statement::ClassDeclaration(self.convert_class_declaration(c)) - } - oxc::Statement::TSTypeAliasDeclaration(t) => { - Statement::TSTypeAliasDeclaration(self.convert_ts_type_alias_declaration(t)) - } - oxc::Statement::TSInterfaceDeclaration(t) => { - Statement::TSInterfaceDeclaration(self.convert_ts_interface_declaration(t)) - } - oxc::Statement::TSEnumDeclaration(t) => { - Statement::TSEnumDeclaration(self.convert_ts_enum_declaration(t)) - } - oxc::Statement::TSModuleDeclaration(t) => { - Statement::TSModuleDeclaration(self.convert_ts_module_declaration(t)) - } - oxc::Statement::TSImportEqualsDeclaration(t) => { - // `import x = require(...)` / `import x = ns` — TS-only, no - // dedicated Babel node. Round-trips via source re-parse. - Statement::TSModuleDeclaration( - self.convert_ts_declaration_passthrough(t.span, false), - ) - } - // ModuleDeclaration variants (inherited directly into Statement) - oxc::Statement::ImportDeclaration(i) => { - Statement::ImportDeclaration(self.convert_import_declaration(i)) - } - oxc::Statement::ExportAllDeclaration(e) => { - Statement::ExportAllDeclaration(self.convert_export_all_declaration(e)) - } - oxc::Statement::ExportDefaultDeclaration(e) => { - Statement::ExportDefaultDeclaration(self.convert_export_default_declaration(e)) - } - oxc::Statement::ExportNamedDeclaration(e) => { - Statement::ExportNamedDeclaration(self.convert_export_named_declaration(e)) - } - oxc::Statement::TSExportAssignment(t) => { - // `export = expr` — TS-only. Round-trips via source re-parse. - Statement::TSModuleDeclaration( - self.convert_ts_declaration_passthrough(t.span, false), - ) - } - oxc::Statement::TSNamespaceExportDeclaration(t) => { - // `export as namespace X` — TS-only. Round-trips via source re-parse. - Statement::TSModuleDeclaration( - self.convert_ts_declaration_passthrough(t.span, false), - ) - } - oxc::Statement::TSGlobalDeclaration(t) => { - // `declare global { ... }` — modeled as a `global` TSModuleDeclaration. - Statement::TSModuleDeclaration( - self.convert_ts_declaration_passthrough(t.span, true), - ) - } - } - } - - fn convert_block_statement(&self, block: &oxc::BlockStatement) -> BlockStatement { - let base = self.make_base_node(block.span); - let body = block.body.iter().map(|s| self.convert_statement(s)).collect(); - BlockStatement { base, body, directives: vec![] } - } - - fn convert_return_statement(&self, ret: &oxc::ReturnStatement) -> ReturnStatement { - ReturnStatement { - base: self.make_base_node(ret.span), - argument: ret.argument.as_ref().map(|e| Box::new(self.convert_expression(e))), - } - } - - fn convert_if_statement(&self, if_stmt: &oxc::IfStatement) -> IfStatement { - IfStatement { - base: self.make_base_node(if_stmt.span), - test: Box::new(self.convert_expression(&if_stmt.test)), - consequent: Box::new(self.convert_statement(&if_stmt.consequent)), - alternate: if_stmt.alternate.as_ref().map(|a| Box::new(self.convert_statement(a))), - } - } - - fn convert_for_statement(&self, for_stmt: &oxc::ForStatement) -> ForStatement { - ForStatement { - base: self.make_base_node(for_stmt.span), - init: for_stmt.init.as_ref().map(|init| { - Box::new(match init { - oxc::ForStatementInit::VariableDeclaration(v) => { - ForInit::VariableDeclaration(self.convert_variable_declaration(v)) - } - _ => ForInit::Expression(Box::new(self.convert_expression_like(init))), - }) - }), - test: for_stmt.test.as_ref().map(|t| Box::new(self.convert_expression(t))), - update: for_stmt.update.as_ref().map(|u| Box::new(self.convert_expression(u))), - body: Box::new(self.convert_statement(&for_stmt.body)), - } - } - - fn convert_while_statement(&self, while_stmt: &oxc::WhileStatement) -> WhileStatement { - WhileStatement { - base: self.make_base_node(while_stmt.span), - test: Box::new(self.convert_expression(&while_stmt.test)), - body: Box::new(self.convert_statement(&while_stmt.body)), - } - } - - fn convert_do_while_statement(&self, do_while: &oxc::DoWhileStatement) -> DoWhileStatement { - DoWhileStatement { - base: self.make_base_node(do_while.span), - test: Box::new(self.convert_expression(&do_while.test)), - body: Box::new(self.convert_statement(&do_while.body)), - } - } - - fn convert_for_in_statement(&self, for_in: &oxc::ForInStatement) -> ForInStatement { - ForInStatement { - base: self.make_base_node(for_in.span), - left: Box::new(self.convert_for_in_of_left(&for_in.left)), - right: Box::new(self.convert_expression(&for_in.right)), - body: Box::new(self.convert_statement(&for_in.body)), - } - } - - fn convert_for_of_statement(&self, for_of: &oxc::ForOfStatement) -> ForOfStatement { - ForOfStatement { - base: self.make_base_node(for_of.span), - left: Box::new(self.convert_for_in_of_left(&for_of.left)), - right: Box::new(self.convert_expression(&for_of.right)), - body: Box::new(self.convert_statement(&for_of.body)), - is_await: for_of.r#await, - } - } - - fn convert_for_in_of_left(&self, left: &oxc::ForStatementLeft) -> ForInOfLeft { - match left { - oxc::ForStatementLeft::VariableDeclaration(v) => { - ForInOfLeft::VariableDeclaration(self.convert_variable_declaration(v)) - } - oxc::ForStatementLeft::AssignmentTargetIdentifier(i) => { - ForInOfLeft::Pattern(Box::new(PatternLike::Identifier(Identifier { - base: self.make_base_node(i.span), - name: i.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }))) - } - oxc::ForStatementLeft::ArrayAssignmentTarget(a) => { - ForInOfLeft::Pattern(Box::new(self.convert_array_assignment_target(a))) - } - oxc::ForStatementLeft::ObjectAssignmentTarget(o) => { - ForInOfLeft::Pattern(Box::new(self.convert_object_assignment_target(o))) - } - oxc::ForStatementLeft::ComputedMemberExpression(m) => { - let mem = MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(self.convert_expression(&m.expression)), - computed: true, - }; - ForInOfLeft::Pattern(Box::new(PatternLike::MemberExpression(mem))) - } - oxc::ForStatementLeft::StaticMemberExpression(m) => { - let mem = MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(Expression::Identifier( - self.convert_identifier_name(&m.property), - )), - computed: false, - }; - ForInOfLeft::Pattern(Box::new(PatternLike::MemberExpression(mem))) - } - oxc::ForStatementLeft::PrivateFieldExpression(p) => { - let mem = MemberExpression { - base: self.make_base_node(p.span), - object: Box::new(self.convert_expression(&p.object)), - property: Box::new(Expression::PrivateName(PrivateName { - base: self.make_base_node(p.field.span), - id: Identifier { - base: self.make_base_node(p.field.span), - name: p.field.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - })), - computed: false, - }; - ForInOfLeft::Pattern(Box::new(PatternLike::MemberExpression(mem))) - } - oxc::ForStatementLeft::TSAsExpression(t) => ForInOfLeft::Pattern(Box::new( - PatternLike::TSAsExpression(self.convert_ts_as_expression(t)), - )), - oxc::ForStatementLeft::TSSatisfiesExpression(t) => ForInOfLeft::Pattern(Box::new( - PatternLike::TSSatisfiesExpression(self.convert_ts_satisfies_expression(t)), - )), - oxc::ForStatementLeft::TSNonNullExpression(t) => ForInOfLeft::Pattern(Box::new( - PatternLike::TSNonNullExpression(self.convert_ts_non_null_expression(t)), - )), - oxc::ForStatementLeft::TSTypeAssertion(t) => ForInOfLeft::Pattern(Box::new( - PatternLike::TSTypeAssertion(self.convert_ts_type_assertion(t)), - )), - } - } - - fn convert_switch_statement(&self, switch: &oxc::SwitchStatement) -> SwitchStatement { - SwitchStatement { - base: self.make_base_node(switch.span), - discriminant: Box::new(self.convert_expression(&switch.discriminant)), - cases: switch.cases.iter().map(|c| self.convert_switch_case(c)).collect(), - } - } - - fn convert_switch_case(&self, case: &oxc::SwitchCase) -> SwitchCase { - SwitchCase { - base: self.make_base_node(case.span), - test: case.test.as_ref().map(|t| Box::new(self.convert_expression(t))), - consequent: case.consequent.iter().map(|s| self.convert_statement(s)).collect(), - } - } - - fn convert_throw_statement(&self, throw: &oxc::ThrowStatement) -> ThrowStatement { - ThrowStatement { - base: self.make_base_node(throw.span), - argument: Box::new(self.convert_expression(&throw.argument)), - } - } - - fn convert_try_statement(&self, try_stmt: &oxc::TryStatement) -> TryStatement { - TryStatement { - base: self.make_base_node(try_stmt.span), - block: self.convert_block_statement(&try_stmt.block), - handler: try_stmt.handler.as_ref().map(|h| self.convert_catch_clause(h)), - finalizer: try_stmt.finalizer.as_ref().map(|f| self.convert_block_statement(f)), - } - } - - fn convert_catch_clause(&self, catch: &oxc::CatchClause) -> CatchClause { - CatchClause { - base: self.make_base_node(catch.span), - param: catch.param.as_ref().map(|p| { - let mut pattern = self.convert_binding_pattern(&p.pattern); - if let Some(type_annotation) = &p.type_annotation { - Self::set_pattern_type_annotation( - &mut pattern, - self.convert_ts_type_annotation_json(type_annotation), - ); - } - pattern - }), - body: self.convert_block_statement(&catch.body), - } - } - - fn convert_break_statement(&self, brk: &oxc::BreakStatement) -> BreakStatement { - BreakStatement { - base: self.make_base_node(brk.span), - label: brk.label.as_ref().map(|l| self.convert_label_identifier(l)), - } - } - - fn convert_continue_statement(&self, cont: &oxc::ContinueStatement) -> ContinueStatement { - ContinueStatement { - base: self.make_base_node(cont.span), - label: cont.label.as_ref().map(|l| self.convert_label_identifier(l)), - } - } - - fn convert_labeled_statement(&self, labeled: &oxc::LabeledStatement) -> LabeledStatement { - LabeledStatement { - base: self.make_base_node(labeled.span), - label: self.convert_label_identifier(&labeled.label), - body: Box::new(self.convert_statement(&labeled.body)), - } - } - - fn convert_expression_statement( - &self, - expr_stmt: &oxc::ExpressionStatement, - ) -> ExpressionStatement { - ExpressionStatement { - base: self.make_base_node(expr_stmt.span), - expression: Box::new(self.convert_expression(&expr_stmt.expression)), - } - } - - fn convert_with_statement(&self, with: &oxc::WithStatement) -> WithStatement { - WithStatement { - base: self.make_base_node(with.span), - object: Box::new(self.convert_expression(&with.object)), - body: Box::new(self.convert_statement(&with.body)), - } - } - - fn convert_variable_declaration(&self, var: &oxc::VariableDeclaration) -> VariableDeclaration { - VariableDeclaration { - base: self.make_base_node(var.span), - declarations: var - .declarations - .iter() - .map(|d| self.convert_variable_declarator(d)) - .collect(), - kind: match var.kind { - oxc::VariableDeclarationKind::Var => VariableDeclarationKind::Var, - oxc::VariableDeclarationKind::Let => VariableDeclarationKind::Let, - oxc::VariableDeclarationKind::Const => VariableDeclarationKind::Const, - oxc::VariableDeclarationKind::Using => VariableDeclarationKind::Using, - oxc::VariableDeclarationKind::AwaitUsing => { - // Map to Using for now - VariableDeclarationKind::Using - } - }, - declare: if var.declare { Some(true) } else { None }, - } - } - - fn convert_variable_declarator( - &self, - declarator: &oxc::VariableDeclarator, - ) -> VariableDeclarator { - VariableDeclarator { - base: self.make_base_node(declarator.span), - id: self.convert_binding_pattern(&declarator.id), - init: declarator.init.as_ref().map(|i| Box::new(self.convert_expression(i))), - definite: if declarator.definite { Some(true) } else { None }, - } - } - - fn convert_function_declaration(&self, func: &oxc::Function) -> FunctionDeclaration { - FunctionDeclaration { - base: self.make_base_node(func.span), - id: func.id.as_ref().map(|id| self.convert_binding_identifier(id)), - params: self.convert_formal_parameters(&func.params), - body: self.convert_optional_function_body(func.body.as_deref(), func.span), - generator: func.generator, - is_async: func.r#async, - declare: if func.declare { Some(true) } else { None }, - return_type: func.return_type.as_ref().map(|t| self.convert_ts_type_annotation_json(t)), - type_parameters: func.type_parameters.as_ref().map(|_t| RawNode::null()), - predicate: None, - component_declaration: false, - hook_declaration: false, - } - } - - fn convert_ts_declare_function(&self, func: &oxc::Function) -> TSDeclareFunction { - TSDeclareFunction { - base: self.make_base_node(func.span), - id: func.id.as_ref().map(|id| self.convert_binding_identifier(id)), - params: self - .convert_formal_parameters(&func.params) - .into_iter() - .map(|param| { - RawNode::from_value( - &serde_json::to_value(param).unwrap_or(serde_json::Value::Null), - ) - }) - .collect(), - is_async: if func.r#async { Some(true) } else { None }, - declare: if func.declare { Some(true) } else { None }, - generator: if func.generator { Some(true) } else { None }, - return_type: func.return_type.as_ref().map(|t| self.convert_ts_type_annotation_json(t)), - type_parameters: func.type_parameters.as_ref().map(|_t| RawNode::null()), - } - } - - fn convert_class_declaration(&self, class: &oxc::Class) -> ClassDeclaration { - ClassDeclaration { - base: self.make_base_node(class.span), - id: class.id.as_ref().map(|id| self.convert_binding_identifier(id)), - super_class: class.super_class.as_ref().map(|s| Box::new(self.convert_expression(s))), - body: ClassBody { - base: self.make_base_node(class.body.span), - body: class.body.body.iter().map(|_item| RawNode::null()).collect(), - }, - decorators: if class.decorators.is_empty() { - None - } else { - Some(class.decorators.iter().map(|_d| RawNode::null()).collect()) - }, - is_abstract: if class.r#abstract { Some(true) } else { None }, - declare: if class.declare { Some(true) } else { None }, - implements: if class.implements.is_empty() { - None - } else { - Some(class.implements.iter().map(|_i| RawNode::null()).collect()) - }, - super_type_parameters: class.super_type_arguments.as_ref().map(|_t| RawNode::null()), - type_parameters: class.type_parameters.as_ref().map(|_t| RawNode::null()), - mixins: None, - } - } - - fn convert_import_declaration(&self, import: &oxc::ImportDeclaration) -> ImportDeclaration { - ImportDeclaration { - base: self.make_base_node(import.span), - specifiers: import - .specifiers - .as_ref() - .map(|specs| { - specs - .iter() - .flat_map(|s| self.convert_import_declaration_specifier(s)) - .collect() - }) - .unwrap_or_default(), - source: StringLiteral { - base: self.make_base_node(import.source.span), - value: import.source.value.to_string().into(), - }, - import_kind: match import.import_kind { - oxc::ImportOrExportKind::Value => None, - oxc::ImportOrExportKind::Type => Some(ImportKind::Type), - }, - assertions: None, - attributes: if import.with_clause.is_some() { - Some( - import - .with_clause - .as_ref() - .unwrap() - .with_entries - .iter() - .map(|e| self.convert_import_attribute(e)) - .collect(), - ) - } else { - None - }, - } - } - - fn convert_import_declaration_specifier( - &self, - spec: &oxc::ImportDeclarationSpecifier, - ) -> Option { - match spec { - oxc::ImportDeclarationSpecifier::ImportSpecifier(s) => { - Some(ImportSpecifier::ImportSpecifier(ImportSpecifierData { - base: self.make_base_node(s.span), - local: self.convert_binding_identifier(&s.local), - imported: self.convert_module_export_name(&s.imported), - import_kind: match s.import_kind { - oxc::ImportOrExportKind::Value => None, - oxc::ImportOrExportKind::Type => Some(ImportKind::Type), - }, - })) - } - oxc::ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { - Some(ImportSpecifier::ImportDefaultSpecifier(ImportDefaultSpecifierData { - base: self.make_base_node(s.span), - local: self.convert_binding_identifier(&s.local), - })) - } - oxc::ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => { - Some(ImportSpecifier::ImportNamespaceSpecifier(ImportNamespaceSpecifierData { - base: self.make_base_node(s.span), - local: self.convert_binding_identifier(&s.local), - })) - } - } - } - - fn convert_import_attribute(&self, attr: &oxc::ImportAttribute) -> ImportAttribute { - ImportAttribute { - base: self.make_base_node(attr.span), - key: self.convert_import_attribute_key(&attr.key), - value: StringLiteral { - base: self.make_base_node(attr.value.span), - value: attr.value.value.to_string().into(), - }, - } - } - - fn convert_import_attribute_key(&self, key: &oxc::ImportAttributeKey) -> Identifier { - match key { - oxc::ImportAttributeKey::Identifier(id) => Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - oxc::ImportAttributeKey::StringLiteral(s) => Identifier { - base: self.make_base_node(s.span), - name: s.value.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - } - } - - fn convert_module_export_name(&self, name: &oxc::ModuleExportName) -> ModuleExportName { - match name { - oxc::ModuleExportName::IdentifierName(id) => { - ModuleExportName::Identifier(self.convert_identifier_name(id)) - } - oxc::ModuleExportName::IdentifierReference(id) => { - ModuleExportName::Identifier(self.convert_identifier_reference(id)) - } - oxc::ModuleExportName::StringLiteral(s) => { - ModuleExportName::StringLiteral(StringLiteral { - base: self.make_base_node(s.span), - value: s.value.to_string().into(), - }) - } - } - } - - fn convert_export_all_declaration( - &self, - export: &oxc::ExportAllDeclaration, - ) -> ExportAllDeclaration { - ExportAllDeclaration { - base: self.make_base_node(export.span), - source: StringLiteral { - base: self.make_base_node(export.source.span), - value: export.source.value.to_string().into(), - }, - export_kind: match export.export_kind { - oxc::ImportOrExportKind::Value => None, - oxc::ImportOrExportKind::Type => Some(ExportKind::Type), - }, - assertions: None, - attributes: if export.with_clause.is_some() { - Some( - export - .with_clause - .as_ref() - .unwrap() - .with_entries - .iter() - .map(|e| self.convert_import_attribute(e)) - .collect(), - ) - } else { - None - }, - } - } - - fn convert_export_default_declaration( - &self, - export: &oxc::ExportDefaultDeclaration, - ) -> ExportDefaultDeclaration { - let declaration = match &export.declaration { - oxc::ExportDefaultDeclarationKind::FunctionDeclaration(f) => { - ExportDefaultDecl::FunctionDeclaration(self.convert_function_declaration(f)) - } - oxc::ExportDefaultDeclarationKind::ClassDeclaration(c) => { - ExportDefaultDecl::ClassDeclaration(self.convert_class_declaration(c)) - } - oxc::ExportDefaultDeclarationKind::TSInterfaceDeclaration(i) => { - // `export default interface X {}` is type-only and has no Babel - // representation here. The compiler never analyzes it; the - // reverse converter recognizes this placeholder by source span - // and re-parses the original type-only export. - ExportDefaultDecl::Expression(Box::new(Expression::NullLiteral(NullLiteral { - base: self.make_base_node(i.span), - }))) - } - _ => { - // All expression variants - ExportDefaultDecl::Expression(Box::new( - self.convert_export_default_expr(&export.declaration), - )) - } - }; - - ExportDefaultDeclaration { - base: self.make_base_node(export.span), - declaration: Box::new(declaration), - export_kind: None, - } - } - - fn convert_export_default_expr(&self, kind: &oxc::ExportDefaultDeclarationKind) -> Expression { - match kind { - oxc::ExportDefaultDeclarationKind::FunctionDeclaration(_) - | oxc::ExportDefaultDeclarationKind::ClassDeclaration(_) - | oxc::ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => { - panic!("Should be handled separately") - } - other => self.convert_expression_from_export_default(other), - } - } - - fn convert_export_named_declaration( - &self, - export: &oxc::ExportNamedDeclaration, - ) -> ExportNamedDeclaration { - ExportNamedDeclaration { - base: self.make_base_node(export.span), - declaration: export.declaration.as_ref().map(|d| { - Box::new(match d { - oxc::Declaration::VariableDeclaration(v) => { - Declaration::VariableDeclaration(self.convert_variable_declaration(v)) - } - oxc::Declaration::FunctionDeclaration(f) => { - if f.body.is_none() { - Declaration::TSDeclareFunction(self.convert_ts_declare_function(f)) - } else { - Declaration::FunctionDeclaration(self.convert_function_declaration(f)) - } - } - oxc::Declaration::ClassDeclaration(c) => { - Declaration::ClassDeclaration(self.convert_class_declaration(c)) - } - oxc::Declaration::TSTypeAliasDeclaration(t) => { - Declaration::TSTypeAliasDeclaration( - self.convert_ts_type_alias_declaration(t), - ) - } - oxc::Declaration::TSInterfaceDeclaration(t) => { - Declaration::TSInterfaceDeclaration( - self.convert_ts_interface_declaration(t), - ) - } - oxc::Declaration::TSEnumDeclaration(t) => { - Declaration::TSEnumDeclaration(self.convert_ts_enum_declaration(t)) - } - oxc::Declaration::TSModuleDeclaration(t) => { - Declaration::TSModuleDeclaration(self.convert_ts_module_declaration(t)) - } - oxc::Declaration::TSImportEqualsDeclaration(t) => { - // `export import x = require(...)` — TS-only. - Declaration::TSModuleDeclaration( - self.convert_ts_declaration_passthrough(t.span, false), - ) - } - oxc::Declaration::TSGlobalDeclaration(t) => { - // `export declare global { ... }` — TS-only. - Declaration::TSModuleDeclaration( - self.convert_ts_declaration_passthrough(t.span, true), - ) - } - }) - }), - specifiers: export - .specifiers - .iter() - .map(|s| self.convert_export_specifier(s)) - .collect(), - source: export.source.as_ref().map(|s| StringLiteral { - base: self.make_base_node(s.span), - value: s.value.to_string().into(), - }), - export_kind: match export.export_kind { - oxc::ImportOrExportKind::Value => None, - oxc::ImportOrExportKind::Type => Some(ExportKind::Type), - }, - assertions: None, - attributes: if export.with_clause.is_some() { - Some( - export - .with_clause - .as_ref() - .unwrap() - .with_entries - .iter() - .map(|e| self.convert_import_attribute(e)) - .collect(), - ) - } else { - None - }, - } - } - - fn convert_export_specifier(&self, spec: &oxc::ExportSpecifier) -> ExportSpecifier { - // ExportSpecifier is now a struct in OXC v0.121, not an enum - ExportSpecifier::ExportSpecifier(ExportSpecifierData { - base: self.make_base_node(spec.span), - local: self.convert_module_export_name(&spec.local), - exported: self.convert_module_export_name(&spec.exported), - export_kind: match spec.export_kind { - oxc::ImportOrExportKind::Value => None, - oxc::ImportOrExportKind::Type => Some(ExportKind::Type), - }, - }) - } - - fn convert_ts_type_alias_declaration( - &self, - type_alias: &oxc::TSTypeAliasDeclaration, - ) -> TSTypeAliasDeclaration { - TSTypeAliasDeclaration { - base: self.make_base_node(type_alias.span), - id: self.convert_binding_identifier(&type_alias.id), - type_annotation: self.convert_ts_type_json(&type_alias.type_annotation), - type_parameters: type_alias.type_parameters.as_ref().map(|_t| RawNode::null()), - declare: if type_alias.declare { Some(true) } else { None }, - } - } - - fn convert_ts_interface_declaration( - &self, - interface: &oxc::TSInterfaceDeclaration, - ) -> TSInterfaceDeclaration { - TSInterfaceDeclaration { - base: self.make_base_node(interface.span), - id: self.convert_binding_identifier(&interface.id), - body: RawNode::null(), - type_parameters: interface.type_parameters.as_ref().map(|_t| RawNode::null()), - extends: if interface.extends.is_empty() { - None - } else { - Some(interface.extends.iter().map(|_e| RawNode::null()).collect()) - }, - declare: if interface.declare { Some(true) } else { None }, - } - } - - fn convert_ts_enum_declaration(&self, ts_enum: &oxc::TSEnumDeclaration) -> TSEnumDeclaration { - TSEnumDeclaration { - base: self.make_base_node(ts_enum.span), - id: self.convert_binding_identifier(&ts_enum.id), - members: ts_enum.body.members.iter().map(|_m| RawNode::null()).collect(), - declare: if ts_enum.declare { Some(true) } else { None }, - is_const: if ts_enum.r#const { Some(true) } else { None }, - } - } - - fn convert_ts_module_declaration( - &self, - module: &oxc::TSModuleDeclaration, - ) -> TSModuleDeclaration { - TSModuleDeclaration { - base: self.make_base_node(module.span), - id: RawNode::null(), - body: RawNode::null(), - declare: if module.declare { Some(true) } else { None }, - global: None, - } - } - - /// Build a placeholder `TSModuleDeclaration` carrying only the original - /// span. The React compiler does not analyze type-level TypeScript - /// constructs, and `react_compiler_ast` has no dedicated Babel node for - /// several of them (`declare global`, `export =`, `export as namespace`, - /// `import x = require(...)`). They only need to round-trip back to OXC, - /// which the reverse converter does by re-parsing the original source slice - /// at `[start, end)`. Reusing the `TSModuleDeclaration` variant keeps these - /// statements routed through that source-extraction path. - fn convert_ts_declaration_passthrough(&self, span: Span, global: bool) -> TSModuleDeclaration { - TSModuleDeclaration { - base: self.make_base_node(span), - id: RawNode::null(), - body: RawNode::null(), - declare: None, - global: if global { Some(true) } else { None }, - } - } - - fn convert_expression(&self, expr: &oxc::Expression) -> Expression { - match expr { - oxc::Expression::BooleanLiteral(b) => Expression::BooleanLiteral(BooleanLiteral { - base: self.make_base_node(b.span), - value: b.value, - }), - oxc::Expression::NullLiteral(n) => { - Expression::NullLiteral(NullLiteral { base: self.make_base_node(n.span) }) - } - oxc::Expression::NumericLiteral(n) => Expression::NumericLiteral(NumericLiteral { - base: self.make_base_node(n.span), - value: n.value, - extra: None, - }), - oxc::Expression::BigIntLiteral(b) => { - Expression::BigIntLiteral(self.convert_big_int_literal(b)) - } - oxc::Expression::RegExpLiteral(r) => Expression::RegExpLiteral(RegExpLiteral { - base: self.make_base_node(r.span), - pattern: r.regex.pattern.text.to_string(), - flags: r.regex.flags.to_string(), - }), - oxc::Expression::StringLiteral(s) => Expression::StringLiteral(StringLiteral { - base: self.make_base_node(s.span), - value: s.value.to_string().into(), - }), - oxc::Expression::TemplateLiteral(t) => { - Expression::TemplateLiteral(self.convert_template_literal(t)) - } - oxc::Expression::Identifier(id) => { - Expression::Identifier(self.convert_identifier_reference(id)) - } - oxc::Expression::MetaProperty(m) => { - Expression::MetaProperty(self.convert_meta_property(m)) - } - oxc::Expression::Super(s) => { - Expression::Super(Super { base: self.make_base_node(s.span) }) - } - oxc::Expression::ArrayExpression(a) => { - Expression::ArrayExpression(self.convert_array_expression(a)) - } - oxc::Expression::ArrowFunctionExpression(a) => { - Expression::ArrowFunctionExpression(self.convert_arrow_function_expression(a)) - } - oxc::Expression::AssignmentExpression(a) => { - Expression::AssignmentExpression(self.convert_assignment_expression(a)) - } - oxc::Expression::AwaitExpression(a) => { - Expression::AwaitExpression(self.convert_await_expression(a)) - } - oxc::Expression::BinaryExpression(b) => { - Expression::BinaryExpression(self.convert_binary_expression(b)) - } - oxc::Expression::CallExpression(c) => { - Expression::CallExpression(self.convert_call_expression(c)) - } - oxc::Expression::ChainExpression(c) => self.convert_chain_expression(c), - oxc::Expression::ClassExpression(c) => { - Expression::ClassExpression(self.convert_class_expression(c)) - } - oxc::Expression::ConditionalExpression(c) => { - Expression::ConditionalExpression(self.convert_conditional_expression(c)) - } - oxc::Expression::FunctionExpression(f) => { - Expression::FunctionExpression(self.convert_function_expression(f)) - } - oxc::Expression::ImportExpression(i) => { - // OXC models `import('foo')` as ImportExpression { source, options }. - // Babel/compiler AST models it as CallExpression { callee: Import, arguments: [source] }. - let mut args = vec![self.convert_expression(&i.source)]; - if let Some(ref opts) = i.options { - args.push(self.convert_expression(opts)); - } - Expression::CallExpression(CallExpression { - base: self.make_base_node(i.span), - callee: Box::new(Expression::Import(Import { - base: self.make_base_node(i.span), - })), - arguments: args, - optional: None, - type_arguments: None, - type_parameters: None, - }) - } - oxc::Expression::LogicalExpression(l) => { - Expression::LogicalExpression(self.convert_logical_expression(l)) - } - oxc::Expression::NewExpression(n) => { - Expression::NewExpression(self.convert_new_expression(n)) - } - oxc::Expression::ObjectExpression(o) => { - Expression::ObjectExpression(self.convert_object_expression(o)) - } - oxc::Expression::ParenthesizedExpression(p) => { - Expression::ParenthesizedExpression(self.convert_parenthesized_expression(p)) - } - oxc::Expression::SequenceExpression(s) => { - Expression::SequenceExpression(self.convert_sequence_expression(s)) - } - oxc::Expression::TaggedTemplateExpression(t) => { - Expression::TaggedTemplateExpression(self.convert_tagged_template_expression(t)) - } - oxc::Expression::ThisExpression(t) => { - Expression::ThisExpression(ThisExpression { base: self.make_base_node(t.span) }) - } - oxc::Expression::UnaryExpression(u) => { - Expression::UnaryExpression(self.convert_unary_expression(u)) - } - oxc::Expression::UpdateExpression(u) => { - Expression::UpdateExpression(self.convert_update_expression(u)) - } - oxc::Expression::YieldExpression(y) => { - Expression::YieldExpression(self.convert_yield_expression(y)) - } - oxc::Expression::PrivateInExpression(p) => { - Expression::BinaryExpression(self.convert_private_in_expression(p)) - } - oxc::Expression::JSXElement(j) => { - Expression::JSXElement(Box::new(self.convert_jsx_element(j))) - } - oxc::Expression::JSXFragment(j) => { - Expression::JSXFragment(self.convert_jsx_fragment(j)) - } - oxc::Expression::TSAsExpression(t) => { - Expression::TSAsExpression(self.convert_ts_as_expression(t)) - } - oxc::Expression::TSSatisfiesExpression(t) => { - Expression::TSSatisfiesExpression(self.convert_ts_satisfies_expression(t)) - } - oxc::Expression::TSTypeAssertion(t) => { - Expression::TSTypeAssertion(self.convert_ts_type_assertion(t)) - } - oxc::Expression::TSNonNullExpression(t) => { - Expression::TSNonNullExpression(self.convert_ts_non_null_expression(t)) - } - oxc::Expression::TSInstantiationExpression(t) => { - Expression::TSInstantiationExpression(self.convert_ts_instantiation_expression(t)) - } - oxc::Expression::ComputedMemberExpression(m) => { - Expression::MemberExpression(MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(self.convert_expression(&m.expression)), - computed: true, - }) - } - oxc::Expression::StaticMemberExpression(m) => { - Expression::MemberExpression(MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(Expression::Identifier( - self.convert_identifier_name(&m.property), - )), - computed: false, - }) - } - oxc::Expression::PrivateFieldExpression(p) => { - Expression::MemberExpression(MemberExpression { - base: self.make_base_node(p.span), - object: Box::new(self.convert_expression(&p.object)), - property: Box::new(Expression::PrivateName(PrivateName { - base: self.make_base_node(p.field.span), - id: Identifier { - base: self.make_base_node(p.field.span), - name: p.field.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - })), - computed: false, - }) - } - oxc::Expression::V8IntrinsicExpression(_) => { - // `%DebugPrint(x)` etc. OXC only parses these when the - // `allow_v8_intrinsics` ParseOptions flag is set, which the - // React compiler never enables (it parses with defaults). There - // is no Babel node to map them to, so they are unreachable here. - unreachable!( - "V8IntrinsicExpression: oxc does not emit this without ParseOptions::allow_v8_intrinsics" - ) - } - } - } - - fn convert_template_literal(&self, template: &oxc::TemplateLiteral) -> TemplateLiteral { - TemplateLiteral { - base: self.make_base_node(template.span), - quasis: template.quasis.iter().map(|q| self.convert_template_element(q)).collect(), - expressions: template.expressions.iter().map(|e| self.convert_expression(e)).collect(), - } - } - - fn convert_template_element(&self, element: &oxc::TemplateElement) -> TemplateElement { - TemplateElement { - base: self.make_base_node(element.span), - value: TemplateElementValue { - raw: element.value.raw.to_string(), - cooked: element.value.cooked.as_ref().map(|s| s.to_string()), - }, - tail: element.tail, - } - } - - fn convert_meta_property(&self, meta: &oxc::MetaProperty) -> MetaProperty { - MetaProperty { - base: self.make_base_node(meta.span), - meta: self.convert_identifier_name(&meta.meta), - property: self.convert_identifier_name(&meta.property), - } - } - - fn convert_array_expression(&self, array: &oxc::ArrayExpression) -> ArrayExpression { - ArrayExpression { - base: self.make_base_node(array.span), - elements: array - .elements - .iter() - .map(|e| match e { - oxc::ArrayExpressionElement::SpreadElement(s) => { - Some(Expression::SpreadElement(SpreadElement { - base: self.make_base_node(s.span), - argument: Box::new(self.convert_expression(&s.argument)), - })) - } - oxc::ArrayExpressionElement::Elision(_) => None, - other => Some(self.convert_expression_from_array_element(other)), - }) - .collect(), - } - } - - fn convert_arrow_function_expression( - &self, - arrow: &oxc::ArrowFunctionExpression, - ) -> ArrowFunctionExpression { - let body = if arrow.expression { - // When expression is true, the body contains a single expression statement - let expr = match &arrow.body.statements[0] { - oxc::Statement::ExpressionStatement(es) => self.convert_expression(&es.expression), - _ => panic!("Expected ExpressionStatement in arrow expression body"), - }; - ArrowFunctionBody::Expression(Box::new(expr)) - } else { - ArrowFunctionBody::BlockStatement(self.convert_function_body(&arrow.body)) - }; - - ArrowFunctionExpression { - base: self.make_base_node(arrow.span), - params: self.convert_formal_parameters(&arrow.params), - body: Box::new(body), - id: None, - generator: false, - is_async: arrow.r#async, - expression: if arrow.expression { Some(true) } else { None }, - return_type: arrow - .return_type - .as_ref() - .map(|t| self.convert_ts_type_annotation_json(t)), - type_parameters: arrow.type_parameters.as_ref().map(|_t| RawNode::null()), - predicate: None, - } - } - - fn convert_assignment_expression( - &self, - assign: &oxc::AssignmentExpression, - ) -> AssignmentExpression { - AssignmentExpression { - base: self.make_base_node(assign.span), - operator: self.convert_assignment_operator(assign.operator), - left: Box::new(self.convert_assignment_target(&assign.left)), - right: Box::new(self.convert_expression(&assign.right)), - } - } - - fn convert_assignment_operator(&self, op: oxc::AssignmentOperator) -> AssignmentOperator { - match op { - oxc::AssignmentOperator::Assign => AssignmentOperator::Assign, - oxc::AssignmentOperator::Addition => AssignmentOperator::AddAssign, - oxc::AssignmentOperator::Subtraction => AssignmentOperator::SubAssign, - oxc::AssignmentOperator::Multiplication => AssignmentOperator::MulAssign, - oxc::AssignmentOperator::Division => AssignmentOperator::DivAssign, - oxc::AssignmentOperator::Remainder => AssignmentOperator::RemAssign, - oxc::AssignmentOperator::ShiftLeft => AssignmentOperator::ShlAssign, - oxc::AssignmentOperator::ShiftRight => AssignmentOperator::ShrAssign, - oxc::AssignmentOperator::ShiftRightZeroFill => AssignmentOperator::UShrAssign, - oxc::AssignmentOperator::BitwiseOR => AssignmentOperator::BitOrAssign, - oxc::AssignmentOperator::BitwiseXOR => AssignmentOperator::BitXorAssign, - oxc::AssignmentOperator::BitwiseAnd => AssignmentOperator::BitAndAssign, - oxc::AssignmentOperator::LogicalAnd => AssignmentOperator::AndAssign, - oxc::AssignmentOperator::LogicalOr => AssignmentOperator::OrAssign, - oxc::AssignmentOperator::LogicalNullish => AssignmentOperator::NullishAssign, - oxc::AssignmentOperator::Exponential => AssignmentOperator::ExpAssign, - } - } - - fn convert_assignment_target(&self, target: &oxc::AssignmentTarget) -> PatternLike { - match target { - oxc::AssignmentTarget::AssignmentTargetIdentifier(id) => { - PatternLike::Identifier(Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }) - } - oxc::AssignmentTarget::ComputedMemberExpression(m) => { - PatternLike::MemberExpression(MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(self.convert_expression(&m.expression)), - computed: true, - }) - } - oxc::AssignmentTarget::StaticMemberExpression(m) => { - PatternLike::MemberExpression(MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(Expression::Identifier( - self.convert_identifier_name(&m.property), - )), - computed: false, - }) - } - oxc::AssignmentTarget::PrivateFieldExpression(p) => { - PatternLike::MemberExpression(MemberExpression { - base: self.make_base_node(p.span), - object: Box::new(self.convert_expression(&p.object)), - property: Box::new(Expression::PrivateName(PrivateName { - base: self.make_base_node(p.field.span), - id: Identifier { - base: self.make_base_node(p.field.span), - name: p.field.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - })), - computed: false, - }) - } - oxc::AssignmentTarget::ArrayAssignmentTarget(a) => { - self.convert_array_assignment_target(a) - } - oxc::AssignmentTarget::ObjectAssignmentTarget(o) => { - self.convert_object_assignment_target(o) - } - oxc::AssignmentTarget::TSAsExpression(t) => { - PatternLike::TSAsExpression(self.convert_ts_as_expression(t)) - } - oxc::AssignmentTarget::TSSatisfiesExpression(t) => { - PatternLike::TSSatisfiesExpression(self.convert_ts_satisfies_expression(t)) - } - oxc::AssignmentTarget::TSNonNullExpression(t) => { - PatternLike::TSNonNullExpression(self.convert_ts_non_null_expression(t)) - } - oxc::AssignmentTarget::TSTypeAssertion(t) => { - PatternLike::TSTypeAssertion(self.convert_ts_type_assertion(t)) - } - } - } - - fn convert_array_assignment_target(&self, arr: &oxc::ArrayAssignmentTarget) -> PatternLike { - PatternLike::ArrayPattern(ArrayPattern { - base: self.make_base_node(arr.span), - elements: arr - .elements - .iter() - .map(|e| match e { - Some(oxc::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(d)) => { - Some(PatternLike::AssignmentPattern(AssignmentPattern { - base: self.make_base_node(d.span), - left: Box::new(self.convert_assignment_target(&d.binding)), - right: Box::new(self.convert_expression(&d.init)), - type_annotation: None, - decorators: None, - })) - } - Some(other) => { - Some(self.convert_assignment_target_maybe_default_as_target(other)) - } - None => None, - }) - .collect(), - type_annotation: None, - decorators: None, - }) - } - - /// Convert an AssignmentTargetMaybeDefault that is NOT an AssignmentTargetWithDefault - /// to a PatternLike by extracting the underlying AssignmentTarget - fn convert_assignment_target_maybe_default_as_target( - &self, - target: &oxc::AssignmentTargetMaybeDefault, - ) -> PatternLike { - match target { - oxc::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(_) => { - unreachable!("handled separately") - } - oxc::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(id) => { - PatternLike::Identifier(Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }) - } - oxc::AssignmentTargetMaybeDefault::ComputedMemberExpression(m) => { - PatternLike::MemberExpression(MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(self.convert_expression(&m.expression)), - computed: true, - }) - } - oxc::AssignmentTargetMaybeDefault::StaticMemberExpression(m) => { - PatternLike::MemberExpression(MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(Expression::Identifier( - self.convert_identifier_name(&m.property), - )), - computed: false, - }) - } - oxc::AssignmentTargetMaybeDefault::PrivateFieldExpression(p) => { - PatternLike::MemberExpression(MemberExpression { - base: self.make_base_node(p.span), - object: Box::new(self.convert_expression(&p.object)), - property: Box::new(Expression::PrivateName(PrivateName { - base: self.make_base_node(p.field.span), - id: Identifier { - base: self.make_base_node(p.field.span), - name: p.field.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - })), - computed: false, - }) - } - oxc::AssignmentTargetMaybeDefault::ArrayAssignmentTarget(a) => { - self.convert_array_assignment_target(a) - } - oxc::AssignmentTargetMaybeDefault::ObjectAssignmentTarget(o) => { - self.convert_object_assignment_target(o) - } - oxc::AssignmentTargetMaybeDefault::TSAsExpression(t) => { - PatternLike::TSAsExpression(self.convert_ts_as_expression(t)) - } - oxc::AssignmentTargetMaybeDefault::TSSatisfiesExpression(t) => { - PatternLike::TSSatisfiesExpression(self.convert_ts_satisfies_expression(t)) - } - oxc::AssignmentTargetMaybeDefault::TSNonNullExpression(t) => { - PatternLike::TSNonNullExpression(self.convert_ts_non_null_expression(t)) - } - oxc::AssignmentTargetMaybeDefault::TSTypeAssertion(t) => { - PatternLike::TSTypeAssertion(self.convert_ts_type_assertion(t)) - } - } - } - - fn convert_object_assignment_target(&self, obj: &oxc::ObjectAssignmentTarget) -> PatternLike { - let mut properties: Vec = obj - .properties - .iter() - .map(|p| match p { - oxc::AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(id) => { - let ident = PatternLike::Identifier(Identifier { - base: self.make_base_node(id.binding.span), - name: id.binding.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }); - let value = if let Some(init) = &id.init { - Box::new(PatternLike::AssignmentPattern(AssignmentPattern { - base: self.make_base_node(id.span), - left: Box::new(ident), - right: Box::new(self.convert_expression(init)), - type_annotation: None, - decorators: None, - })) - } else { - Box::new(ident) - }; - ObjectPatternProperty::ObjectProperty(ObjectPatternProp { - base: self.make_base_node(id.span), - key: Box::new(Expression::Identifier(Identifier { - base: self.make_base_node(id.binding.span), - name: id.binding.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - })), - value, - computed: false, - shorthand: true, - decorators: None, - method: None, - }) - } - oxc::AssignmentTargetProperty::AssignmentTargetPropertyProperty(prop) => { - let value = match &prop.binding { - oxc::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(d) => { - Box::new(PatternLike::AssignmentPattern(AssignmentPattern { - base: self.make_base_node(d.span), - left: Box::new(self.convert_assignment_target(&d.binding)), - right: Box::new(self.convert_expression(&d.init)), - type_annotation: None, - decorators: None, - })) - } - other => { - Box::new(self.convert_assignment_target_maybe_default_as_target(other)) - } - }; - ObjectPatternProperty::ObjectProperty(ObjectPatternProp { - base: self.make_base_node(prop.span), - key: Box::new(self.convert_property_key(&prop.name)), - value, - computed: prop.computed, - shorthand: false, - decorators: None, - method: None, - }) - } - }) - .collect(); - - // Handle rest element separately (it's now a separate field) - if let Some(rest) = &obj.rest { - properties.push(ObjectPatternProperty::RestElement(RestElement { - base: self.make_base_node(rest.span), - argument: Box::new(self.convert_assignment_target(&rest.target)), - type_annotation: None, - decorators: None, - })); - } - - PatternLike::ObjectPattern(ObjectPattern { - base: self.make_base_node(obj.span), - properties, - type_annotation: None, - decorators: None, - }) - } - - fn convert_await_expression(&self, await_expr: &oxc::AwaitExpression) -> AwaitExpression { - AwaitExpression { - base: self.make_base_node(await_expr.span), - argument: Box::new(self.convert_expression(&await_expr.argument)), - } - } - - fn convert_binary_expression(&self, binary: &oxc::BinaryExpression) -> BinaryExpression { - BinaryExpression { - base: self.make_base_node(binary.span), - operator: self.convert_binary_operator(binary.operator), - left: Box::new(self.convert_expression(&binary.left)), - right: Box::new(self.convert_expression(&binary.right)), - } - } - - /// Convert `#field in obj`. Babel models this as a `BinaryExpression` with - /// the `in` operator and a `PrivateName` left operand. - fn convert_private_in_expression( - &self, - private_in: &oxc::PrivateInExpression, - ) -> BinaryExpression { - BinaryExpression { - base: self.make_base_node(private_in.span), - operator: BinaryOperator::In, - left: Box::new(Expression::PrivateName(PrivateName { - base: self.make_base_node(private_in.left.span), - id: Identifier { - base: self.make_base_node(private_in.left.span), - name: private_in.left.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - })), - right: Box::new(self.convert_expression(&private_in.right)), - } - } - - fn convert_binary_operator(&self, op: oxc::BinaryOperator) -> BinaryOperator { - match op { - oxc::BinaryOperator::Equality => BinaryOperator::Eq, - oxc::BinaryOperator::Inequality => BinaryOperator::Neq, - oxc::BinaryOperator::StrictEquality => BinaryOperator::StrictEq, - oxc::BinaryOperator::StrictInequality => BinaryOperator::StrictNeq, - oxc::BinaryOperator::LessThan => BinaryOperator::Lt, - oxc::BinaryOperator::LessEqualThan => BinaryOperator::Lte, - oxc::BinaryOperator::GreaterThan => BinaryOperator::Gt, - oxc::BinaryOperator::GreaterEqualThan => BinaryOperator::Gte, - oxc::BinaryOperator::ShiftLeft => BinaryOperator::Shl, - oxc::BinaryOperator::ShiftRight => BinaryOperator::Shr, - oxc::BinaryOperator::ShiftRightZeroFill => BinaryOperator::UShr, - oxc::BinaryOperator::Addition => BinaryOperator::Add, - oxc::BinaryOperator::Subtraction => BinaryOperator::Sub, - oxc::BinaryOperator::Multiplication => BinaryOperator::Mul, - oxc::BinaryOperator::Division => BinaryOperator::Div, - oxc::BinaryOperator::Remainder => BinaryOperator::Rem, - oxc::BinaryOperator::BitwiseOR => BinaryOperator::BitOr, - oxc::BinaryOperator::BitwiseXOR => BinaryOperator::BitXor, - oxc::BinaryOperator::BitwiseAnd => BinaryOperator::BitAnd, - oxc::BinaryOperator::In => BinaryOperator::In, - oxc::BinaryOperator::Instanceof => BinaryOperator::Instanceof, - oxc::BinaryOperator::Exponential => BinaryOperator::Exp, - } - } - - fn convert_call_expression(&self, call: &oxc::CallExpression) -> CallExpression { - CallExpression { - base: self.make_base_node(call.span), - callee: Box::new(self.convert_expression(&call.callee)), - arguments: call.arguments.iter().map(|a| self.convert_argument(a)).collect(), - type_parameters: call.type_arguments.as_ref().map(|_t| RawNode::null()), - type_arguments: None, - optional: if call.optional { Some(true) } else { None }, - } - } - - fn convert_argument(&self, arg: &oxc::Argument) -> Expression { - match arg { - oxc::Argument::SpreadElement(s) => Expression::SpreadElement(SpreadElement { - base: self.make_base_node(s.span), - argument: Box::new(self.convert_expression(&s.argument)), - }), - other => self.convert_expression_from_argument(other), - } - } - - fn convert_chain_expression(&self, chain: &oxc::ChainExpression) -> Expression { - // ChainExpression wraps optional call/member expressions in Babel - match &chain.expression { - oxc::ChainElement::CallExpression(c) => { - Expression::OptionalCallExpression(OptionalCallExpression { - base: self.make_base_node(c.span), - callee: Box::new(self.convert_expression_in_chain(&c.callee)), - arguments: c.arguments.iter().map(|a| self.convert_argument(a)).collect(), - optional: c.optional, - type_parameters: c.type_arguments.as_ref().map(|_t| RawNode::null()), - type_arguments: None, - }) - } - oxc::ChainElement::ComputedMemberExpression(m) => { - Expression::OptionalMemberExpression(OptionalMemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression_in_chain(&m.object)), - property: Box::new(self.convert_expression(&m.expression)), - computed: true, - optional: m.optional, - }) - } - oxc::ChainElement::StaticMemberExpression(m) => { - Expression::OptionalMemberExpression(OptionalMemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression_in_chain(&m.object)), - property: Box::new(Expression::Identifier( - self.convert_identifier_name(&m.property), - )), - computed: false, - optional: m.optional, - }) - } - oxc::ChainElement::PrivateFieldExpression(p) => { - Expression::OptionalMemberExpression(OptionalMemberExpression { - base: self.make_base_node(p.span), - object: Box::new(self.convert_expression_in_chain(&p.object)), - property: Box::new(Expression::PrivateName(PrivateName { - base: self.make_base_node(p.field.span), - id: Identifier { - base: self.make_base_node(p.field.span), - name: p.field.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - })), - computed: false, - optional: p.optional, - }) - } - oxc::ChainElement::TSNonNullExpression(t) => { - // `foo?.bar!` — the non-null assertion is the outermost chain - // element. Wrap the (chain-aware) inner expression in a Babel - // `TSNonNullExpression`. - Expression::TSNonNullExpression(TSNonNullExpression { - base: self.make_base_node(t.span), - expression: Box::new(self.convert_expression_in_chain(&t.expression)), - }) - } - } - } - - /// Check if an expression within a chain contains any optional access. - fn expr_contains_optional(expr: &oxc::Expression) -> bool { - match expr { - oxc::Expression::CallExpression(c) => { - c.optional || Self::expr_contains_optional(&c.callee) - } - oxc::Expression::StaticMemberExpression(m) => { - m.optional || Self::expr_contains_optional(&m.object) - } - oxc::Expression::ComputedMemberExpression(m) => { - m.optional || Self::expr_contains_optional(&m.object) - } - oxc::Expression::PrivateFieldExpression(p) => { - p.optional || Self::expr_contains_optional(&p.object) - } - _ => false, - } - } - - /// Convert an expression that appears as callee/object inside a ChainExpression. - /// In OXC, `a?.b?.c` is a single ChainExpression with nested CallExpression/ - /// MemberExpression nodes that have `optional: true`. In Babel, each optional - /// node is an OptionalCallExpression/OptionalMemberExpression. Non-optional - /// nodes that appear AFTER the first `?.` become OptionalMember/Call with - /// `optional: false`, while non-optional nodes BEFORE the first `?.` are - /// regular MemberExpression/CallExpression nodes. - fn convert_expression_in_chain(&self, expr: &oxc::Expression) -> Expression { - match expr { - oxc::Expression::CallExpression(c) if c.optional => { - Expression::OptionalCallExpression(OptionalCallExpression { - base: self.make_base_node(c.span), - callee: Box::new(self.convert_expression_in_chain(&c.callee)), - arguments: c.arguments.iter().map(|a| self.convert_argument(a)).collect(), - optional: true, - type_parameters: c.type_arguments.as_ref().map(|_t| RawNode::null()), - type_arguments: None, - }) - } - oxc::Expression::StaticMemberExpression(m) if m.optional => { - Expression::OptionalMemberExpression(OptionalMemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression_in_chain(&m.object)), - property: Box::new(Expression::Identifier( - self.convert_identifier_name(&m.property), - )), - computed: false, - optional: true, - }) - } - oxc::Expression::ComputedMemberExpression(m) if m.optional => { - Expression::OptionalMemberExpression(OptionalMemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression_in_chain(&m.object)), - property: Box::new(self.convert_expression(&m.expression)), - computed: true, - optional: true, - }) - } - // Non-optional expressions inside chains: need to check if the - // object/callee still contains optional parts. If so, this node - // is AFTER the first `?.` and should be OptionalMember/Call with - // optional: false. If not, it's BEFORE the first `?.` and should - // be a regular MemberExpression/CallExpression. - oxc::Expression::CallExpression(c) if Self::expr_contains_optional(&c.callee) => { - Expression::OptionalCallExpression(OptionalCallExpression { - base: self.make_base_node(c.span), - callee: Box::new(self.convert_expression_in_chain(&c.callee)), - arguments: c.arguments.iter().map(|a| self.convert_argument(a)).collect(), - optional: false, - type_parameters: c.type_arguments.as_ref().map(|_t| RawNode::null()), - type_arguments: None, - }) - } - oxc::Expression::StaticMemberExpression(m) - if Self::expr_contains_optional(&m.object) => - { - Expression::OptionalMemberExpression(OptionalMemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression_in_chain(&m.object)), - property: Box::new(Expression::Identifier( - self.convert_identifier_name(&m.property), - )), - computed: false, - optional: false, - }) - } - oxc::Expression::ComputedMemberExpression(m) - if Self::expr_contains_optional(&m.object) => - { - Expression::OptionalMemberExpression(OptionalMemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression_in_chain(&m.object)), - property: Box::new(self.convert_expression(&m.expression)), - computed: true, - optional: false, - }) - } - // No optional in the sub-tree — this is the base receiver before - // the first `?.`. Convert as a regular expression. - _ => self.convert_expression(expr), - } - } - - fn convert_class_expression(&self, class: &oxc::Class) -> ClassExpression { - ClassExpression { - base: self.make_base_node(class.span), - id: class.id.as_ref().map(|id| self.convert_binding_identifier(id)), - super_class: class.super_class.as_ref().map(|s| Box::new(self.convert_expression(s))), - body: ClassBody { - base: self.make_base_node(class.body.span), - body: class.body.body.iter().map(|_item| RawNode::null()).collect(), - }, - decorators: if class.decorators.is_empty() { - None - } else { - Some(class.decorators.iter().map(|_d| RawNode::null()).collect()) - }, - implements: if class.implements.is_empty() { - None - } else { - Some(class.implements.iter().map(|_i| RawNode::null()).collect()) - }, - super_type_parameters: class.super_type_arguments.as_ref().map(|_t| RawNode::null()), - type_parameters: class.type_parameters.as_ref().map(|_t| RawNode::null()), - } - } - - fn convert_big_int_literal(&self, lit: &oxc::BigIntLiteral) -> BigIntLiteral { - BigIntLiteral { base: self.make_base_node(lit.span), value: lit.value.to_string() } - } - - fn convert_conditional_expression( - &self, - cond: &oxc::ConditionalExpression, - ) -> ConditionalExpression { - ConditionalExpression { - base: self.make_base_node(cond.span), - test: Box::new(self.convert_expression(&cond.test)), - consequent: Box::new(self.convert_expression(&cond.consequent)), - alternate: Box::new(self.convert_expression(&cond.alternate)), - } - } - - fn convert_function_expression(&self, func: &oxc::Function) -> FunctionExpression { - FunctionExpression { - base: self.make_base_node(func.span), - id: func.id.as_ref().map(|id| self.convert_binding_identifier(id)), - params: self.convert_formal_parameters(&func.params), - body: self.convert_optional_function_body(func.body.as_deref(), func.span), - generator: func.generator, - is_async: func.r#async, - return_type: func.return_type.as_ref().map(|t| self.convert_ts_type_annotation_json(t)), - type_parameters: func.type_parameters.as_ref().map(|_t| RawNode::null()), - predicate: None, - } - } - - fn convert_logical_expression(&self, logical: &oxc::LogicalExpression) -> LogicalExpression { - LogicalExpression { - base: self.make_base_node(logical.span), - operator: self.convert_logical_operator(logical.operator), - left: Box::new(self.convert_expression(&logical.left)), - right: Box::new(self.convert_expression(&logical.right)), - } - } - - fn convert_logical_operator(&self, op: oxc::LogicalOperator) -> LogicalOperator { - match op { - oxc::LogicalOperator::Or => LogicalOperator::Or, - oxc::LogicalOperator::And => LogicalOperator::And, - oxc::LogicalOperator::Coalesce => LogicalOperator::NullishCoalescing, - } - } - - fn convert_new_expression(&self, new: &oxc::NewExpression) -> NewExpression { - NewExpression { - base: self.make_base_node(new.span), - callee: Box::new(self.convert_expression(&new.callee)), - arguments: new.arguments.iter().map(|a| self.convert_argument(a)).collect(), - type_parameters: new.type_arguments.as_ref().map(|_t| RawNode::null()), - type_arguments: None, - } - } - - fn convert_object_expression(&self, obj: &oxc::ObjectExpression) -> ObjectExpression { - ObjectExpression { - base: self.make_base_node(obj.span), - properties: obj - .properties - .iter() - .map(|p| self.convert_object_property_kind(p)) - .collect(), - } - } - - fn convert_object_property_kind( - &self, - prop: &oxc::ObjectPropertyKind, - ) -> ObjectExpressionProperty { - match prop { - oxc::ObjectPropertyKind::ObjectProperty(p) => { - // When method is true or kind is Get/Set, convert to ObjectMethod - // to match Babel's AST representation of method shorthand. - if p.method || matches!(p.kind, oxc::PropertyKind::Get | oxc::PropertyKind::Set) { - if let oxc::Expression::FunctionExpression(func) = &p.value { - let method_kind = match p.kind { - oxc::PropertyKind::Get => ObjectMethodKind::Get, - oxc::PropertyKind::Set => ObjectMethodKind::Set, - _ => ObjectMethodKind::Method, - }; - let params: Vec = self.convert_formal_parameters(&func.params); - let body = func - .body - .as_ref() - .map(|b| self.convert_function_body(b)) - .unwrap_or_else(|| BlockStatement { - base: BaseNode::default(), - body: vec![], - directives: vec![], - }); - return ObjectExpressionProperty::ObjectMethod(ObjectMethod { - base: self.make_base_node(p.span), - method: p.method, - kind: method_kind, - key: Box::new(self.convert_property_key(&p.key)), - params, - body, - computed: p.computed, - id: func.id.as_ref().map(|id| self.convert_binding_identifier(id)), - generator: func.generator, - is_async: func.r#async, - decorators: None, - return_type: func - .return_type - .as_ref() - .map(|t| self.convert_ts_type_annotation_json(t)), - type_parameters: func.type_parameters.as_ref().map(|_| RawNode::null()), - predicate: None, - }); - } - } - ObjectExpressionProperty::ObjectProperty(self.convert_object_property(p)) - } - oxc::ObjectPropertyKind::SpreadProperty(s) => { - ObjectExpressionProperty::SpreadElement(SpreadElement { - base: self.make_base_node(s.span), - argument: Box::new(self.convert_expression(&s.argument)), - }) - } - } - } - - fn convert_object_property(&self, prop: &oxc::ObjectProperty) -> ObjectProperty { - ObjectProperty { - base: self.make_base_node(prop.span), - key: Box::new(self.convert_property_key(&prop.key)), - value: Box::new(self.convert_expression(&prop.value)), - computed: prop.computed, - shorthand: prop.shorthand, - decorators: None, - method: if prop.method { Some(true) } else { None }, - } - } - - fn convert_property_key(&self, key: &oxc::PropertyKey) -> Expression { - match key { - oxc::PropertyKey::StaticIdentifier(id) => { - Expression::Identifier(self.convert_identifier_name(id)) - } - oxc::PropertyKey::PrivateIdentifier(id) => Expression::PrivateName(PrivateName { - base: self.make_base_node(id.span), - id: Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - }), - other => self.convert_expression_from_property_key(other), - } - } - - fn convert_parenthesized_expression( - &self, - paren: &oxc::ParenthesizedExpression, - ) -> ParenthesizedExpression { - ParenthesizedExpression { - base: self.make_base_node(paren.span), - expression: Box::new(self.convert_expression(&paren.expression)), - } - } - - fn convert_sequence_expression(&self, seq: &oxc::SequenceExpression) -> SequenceExpression { - SequenceExpression { - base: self.make_base_node(seq.span), - expressions: seq.expressions.iter().map(|e| self.convert_expression(e)).collect(), - } - } - - fn convert_tagged_template_expression( - &self, - tagged: &oxc::TaggedTemplateExpression, - ) -> TaggedTemplateExpression { - TaggedTemplateExpression { - base: self.make_base_node(tagged.span), - tag: Box::new(self.convert_expression(&tagged.tag)), - quasi: self.convert_template_literal(&tagged.quasi), - type_parameters: tagged.type_arguments.as_ref().map(|_t| RawNode::null()), - } - } - - fn convert_unary_expression(&self, unary: &oxc::UnaryExpression) -> UnaryExpression { - UnaryExpression { - base: self.make_base_node(unary.span), - operator: self.convert_unary_operator(unary.operator), - prefix: true, - argument: Box::new(self.convert_expression(&unary.argument)), - } - } - - fn convert_unary_operator(&self, op: oxc::UnaryOperator) -> UnaryOperator { - match op { - oxc::UnaryOperator::UnaryNegation => UnaryOperator::Neg, - oxc::UnaryOperator::UnaryPlus => UnaryOperator::Plus, - oxc::UnaryOperator::LogicalNot => UnaryOperator::Not, - oxc::UnaryOperator::BitwiseNot => UnaryOperator::BitNot, - oxc::UnaryOperator::Typeof => UnaryOperator::TypeOf, - oxc::UnaryOperator::Void => UnaryOperator::Void, - oxc::UnaryOperator::Delete => UnaryOperator::Delete, - } - } - - fn convert_update_expression(&self, update: &oxc::UpdateExpression) -> UpdateExpression { - UpdateExpression { - base: self.make_base_node(update.span), - operator: self.convert_update_operator(update.operator), - argument: Box::new( - self.convert_simple_assignment_target_as_expression(&update.argument), - ), - prefix: update.prefix, - } - } - - fn convert_simple_assignment_target_as_expression( - &self, - target: &oxc::SimpleAssignmentTarget, - ) -> Expression { - match target { - oxc::SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => { - Expression::Identifier(Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }) - } - oxc::SimpleAssignmentTarget::ComputedMemberExpression(m) => { - Expression::MemberExpression(MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(self.convert_expression(&m.expression)), - computed: true, - }) - } - oxc::SimpleAssignmentTarget::StaticMemberExpression(m) => { - Expression::MemberExpression(MemberExpression { - base: self.make_base_node(m.span), - object: Box::new(self.convert_expression(&m.object)), - property: Box::new(Expression::Identifier( - self.convert_identifier_name(&m.property), - )), - computed: false, - }) - } - oxc::SimpleAssignmentTarget::PrivateFieldExpression(p) => { - Expression::MemberExpression(MemberExpression { - base: self.make_base_node(p.span), - object: Box::new(self.convert_expression(&p.object)), - property: Box::new(Expression::PrivateName(PrivateName { - base: self.make_base_node(p.field.span), - id: Identifier { - base: self.make_base_node(p.field.span), - name: p.field.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - })), - computed: false, - }) - } - oxc::SimpleAssignmentTarget::TSAsExpression(t) => { - self.convert_expression(&t.expression) - } - oxc::SimpleAssignmentTarget::TSSatisfiesExpression(t) => { - self.convert_expression(&t.expression) - } - oxc::SimpleAssignmentTarget::TSNonNullExpression(t) => { - self.convert_expression(&t.expression) - } - oxc::SimpleAssignmentTarget::TSTypeAssertion(t) => { - self.convert_expression(&t.expression) - } - } - } - - fn convert_update_operator(&self, op: oxc::UpdateOperator) -> UpdateOperator { - match op { - oxc::UpdateOperator::Increment => UpdateOperator::Increment, - oxc::UpdateOperator::Decrement => UpdateOperator::Decrement, - } - } - - fn convert_yield_expression(&self, yield_expr: &oxc::YieldExpression) -> YieldExpression { - YieldExpression { - base: self.make_base_node(yield_expr.span), - argument: yield_expr.argument.as_ref().map(|a| Box::new(self.convert_expression(a))), - delegate: yield_expr.delegate, - } - } - - fn convert_ts_as_expression(&self, ts_as: &oxc::TSAsExpression) -> TSAsExpression { - TSAsExpression { - base: self.make_base_node(ts_as.span), - expression: Box::new(self.convert_expression(&ts_as.expression)), - type_annotation: self.convert_ts_type_json(&ts_as.type_annotation), - } - } - - fn convert_ts_satisfies_expression( - &self, - ts_sat: &oxc::TSSatisfiesExpression, - ) -> TSSatisfiesExpression { - TSSatisfiesExpression { - base: self.make_base_node(ts_sat.span), - expression: Box::new(self.convert_expression(&ts_sat.expression)), - type_annotation: self.convert_ts_type_json(&ts_sat.type_annotation), - } - } - - fn convert_ts_type_assertion(&self, ts_assert: &oxc::TSTypeAssertion) -> TSTypeAssertion { - TSTypeAssertion { - base: self.make_base_node(ts_assert.span), - expression: Box::new(self.convert_expression(&ts_assert.expression)), - type_annotation: self.convert_ts_type_json(&ts_assert.type_annotation), - } - } - - fn convert_ts_non_null_expression( - &self, - ts_non_null: &oxc::TSNonNullExpression, - ) -> TSNonNullExpression { - TSNonNullExpression { - base: self.make_base_node(ts_non_null.span), - expression: Box::new(self.convert_expression(&ts_non_null.expression)), - } - } - - fn convert_ts_instantiation_expression( - &self, - ts_inst: &oxc::TSInstantiationExpression, - ) -> TSInstantiationExpression { - TSInstantiationExpression { - base: self.make_base_node(ts_inst.span), - expression: Box::new(self.convert_expression(&ts_inst.expression)), - type_parameters: self - .convert_ts_type_parameter_instantiation_json(&ts_inst.type_arguments), - } - } - - fn convert_jsx_element(&self, jsx: &oxc::JSXElement) -> JSXElement { - JSXElement { - base: self.make_base_node(jsx.span), - opening_element: self.convert_jsx_opening_element(&jsx.opening_element), - closing_element: jsx - .closing_element - .as_ref() - .map(|c| self.convert_jsx_closing_element(c)), - children: jsx.children.iter().map(|c| self.convert_jsx_child(c)).collect(), - self_closing: None, - } - } - - fn convert_jsx_fragment(&self, jsx: &oxc::JSXFragment) -> JSXFragment { - JSXFragment { - base: self.make_base_node(jsx.span), - opening_fragment: JSXOpeningFragment { - base: self.make_base_node(jsx.opening_fragment.span), - }, - closing_fragment: JSXClosingFragment { - base: self.make_base_node(jsx.closing_fragment.span), - }, - children: jsx.children.iter().map(|c| self.convert_jsx_child(c)).collect(), - } - } - - fn convert_jsx_opening_element(&self, opening: &oxc::JSXOpeningElement) -> JSXOpeningElement { - // In OXC v0.121, self_closing is computed (not a field), and type_parameters -> type_arguments - // Determine self_closing from absence of closing element (handled at JSXElement level) - JSXOpeningElement { - base: self.make_base_node(opening.span), - name: self.convert_jsx_element_name(&opening.name), - attributes: opening - .attributes - .iter() - .map(|a| self.convert_jsx_attribute_item(a)) - .collect(), - self_closing: false, // Will be set properly at JSXElement level if needed - type_parameters: opening.type_arguments.as_ref().map(|_t| RawNode::null()), - } - } - - fn convert_jsx_closing_element(&self, closing: &oxc::JSXClosingElement) -> JSXClosingElement { - JSXClosingElement { - base: self.make_base_node(closing.span), - name: self.convert_jsx_element_name(&closing.name), - } - } - - fn convert_jsx_element_name(&self, name: &oxc::JSXElementName) -> JSXElementName { - match name { - oxc::JSXElementName::Identifier(id) => JSXElementName::JSXIdentifier(JSXIdentifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - }), - oxc::JSXElementName::IdentifierReference(id) => { - JSXElementName::JSXIdentifier(JSXIdentifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - }) - } - oxc::JSXElementName::NamespacedName(ns) => { - JSXElementName::JSXNamespacedName(JSXNamespacedName { - base: self.make_base_node(ns.span), - namespace: JSXIdentifier { - base: self.make_base_node(ns.namespace.span), - name: ns.namespace.name.to_string(), - }, - name: JSXIdentifier { - base: self.make_base_node(ns.name.span), - name: ns.name.name.to_string(), - }, - }) - } - oxc::JSXElementName::MemberExpression(mem) => { - JSXElementName::JSXMemberExpression(self.convert_jsx_member_expression(mem)) - } - oxc::JSXElementName::ThisExpression(t) => { - JSXElementName::JSXIdentifier(JSXIdentifier { - base: self.make_base_node(t.span), - name: "this".to_string(), - }) - } - } - } - - fn convert_jsx_member_expression(&self, mem: &oxc::JSXMemberExpression) -> JSXMemberExpression { - JSXMemberExpression { - base: self.make_base_node(mem.span), - object: Box::new(self.convert_jsx_member_expression_object(&mem.object)), - property: JSXIdentifier { - base: self.make_base_node(mem.property.span), - name: mem.property.name.to_string(), - }, - } - } - - fn convert_jsx_member_expression_object( - &self, - obj: &oxc::JSXMemberExpressionObject, - ) -> JSXMemberExprObject { - match obj { - oxc::JSXMemberExpressionObject::IdentifierReference(id) => { - JSXMemberExprObject::JSXIdentifier(JSXIdentifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - }) - } - oxc::JSXMemberExpressionObject::MemberExpression(mem) => { - JSXMemberExprObject::JSXMemberExpression(Box::new( - self.convert_jsx_member_expression(mem), - )) - } - oxc::JSXMemberExpressionObject::ThisExpression(t) => { - // JSX `` - convert to identifier - JSXMemberExprObject::JSXIdentifier(JSXIdentifier { - base: self.make_base_node(t.span), - name: "this".to_string(), - }) - } - } - } - - fn convert_jsx_attribute_item(&self, attr: &oxc::JSXAttributeItem) -> JSXAttributeItem { - match attr { - oxc::JSXAttributeItem::Attribute(a) => { - JSXAttributeItem::JSXAttribute(self.convert_jsx_attribute(a)) - } - oxc::JSXAttributeItem::SpreadAttribute(s) => { - JSXAttributeItem::JSXSpreadAttribute(JSXSpreadAttribute { - base: self.make_base_node(s.span), - argument: Box::new(self.convert_expression(&s.argument)), - }) - } - } - } - - fn convert_jsx_attribute(&self, attr: &oxc::JSXAttribute) -> JSXAttribute { - JSXAttribute { - base: self.make_base_node(attr.span), - name: self.convert_jsx_attribute_name(&attr.name), - value: attr.value.as_ref().map(|v| self.convert_jsx_attribute_value(v)), - } - } - - fn convert_jsx_attribute_name(&self, name: &oxc::JSXAttributeName) -> JSXAttributeName { - match name { - oxc::JSXAttributeName::Identifier(id) => { - JSXAttributeName::JSXIdentifier(JSXIdentifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - }) - } - oxc::JSXAttributeName::NamespacedName(ns) => { - JSXAttributeName::JSXNamespacedName(JSXNamespacedName { - base: self.make_base_node(ns.span), - namespace: JSXIdentifier { - base: self.make_base_node(ns.namespace.span), - name: ns.namespace.name.to_string(), - }, - name: JSXIdentifier { - base: self.make_base_node(ns.name.span), - name: ns.name.name.to_string(), - }, - }) - } - } - } - - fn convert_jsx_attribute_value(&self, value: &oxc::JSXAttributeValue) -> JSXAttributeValue { - match value { - oxc::JSXAttributeValue::StringLiteral(s) => { - JSXAttributeValue::StringLiteral(StringLiteral { - base: self.make_base_node(s.span), - value: decode_jsx_entities(s.value.as_str()).into(), - }) - } - oxc::JSXAttributeValue::ExpressionContainer(e) => { - JSXAttributeValue::JSXExpressionContainer(self.convert_jsx_expression_container(e)) - } - oxc::JSXAttributeValue::Element(e) => { - JSXAttributeValue::JSXElement(Box::new(self.convert_jsx_element(e))) - } - oxc::JSXAttributeValue::Fragment(f) => { - JSXAttributeValue::JSXFragment(self.convert_jsx_fragment(f)) - } - } - } - - fn convert_jsx_expression_container( - &self, - container: &oxc::JSXExpressionContainer, - ) -> JSXExpressionContainer { - JSXExpressionContainer { - base: self.make_base_node(container.span), - expression: match &container.expression { - oxc::JSXExpression::EmptyExpression(e) => { - JSXExpressionContainerExpr::JSXEmptyExpression(JSXEmptyExpression { - base: self.make_base_node(e.span), - }) - } - other => JSXExpressionContainerExpr::Expression(Box::new( - self.convert_expression_from_jsx_expression(other), - )), - }, - } - } - - fn convert_jsx_child(&self, child: &oxc::JSXChild) -> JSXChild { - match child { - oxc::JSXChild::Element(e) => { - JSXChild::JSXElement(Box::new(self.convert_jsx_element(e))) - } - oxc::JSXChild::Fragment(f) => JSXChild::JSXFragment(self.convert_jsx_fragment(f)), - oxc::JSXChild::ExpressionContainer(e) => { - JSXChild::JSXExpressionContainer(self.convert_jsx_expression_container(e)) - } - oxc::JSXChild::Spread(s) => JSXChild::JSXSpreadChild(JSXSpreadChild { - base: self.make_base_node(s.span), - expression: Box::new(self.convert_expression(&s.expression)), - }), - oxc::JSXChild::Text(t) => JSXChild::JSXText(JSXText { - base: self.make_base_node(t.span), - value: decode_jsx_entities(t.value.as_str()), - }), - } - } - - fn convert_binding_pattern(&self, pattern: &oxc::BindingPattern) -> PatternLike { - match pattern { - oxc::BindingPattern::BindingIdentifier(id) => { - PatternLike::Identifier(self.convert_binding_identifier(id)) - } - oxc::BindingPattern::ObjectPattern(obj) => { - PatternLike::ObjectPattern(self.convert_object_pattern(obj)) - } - oxc::BindingPattern::ArrayPattern(arr) => { - PatternLike::ArrayPattern(self.convert_array_pattern(arr)) - } - oxc::BindingPattern::AssignmentPattern(assign) => { - PatternLike::AssignmentPattern(self.convert_assignment_pattern(assign)) - } - } - } - - fn convert_binding_identifier(&self, id: &oxc::BindingIdentifier) -> Identifier { - Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - } - } - - fn convert_identifier_name(&self, id: &oxc::IdentifierName) -> Identifier { - Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - } - } - - fn convert_label_identifier(&self, id: &oxc::LabelIdentifier) -> Identifier { - Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - } - } - - fn convert_identifier_reference(&self, id: &oxc::IdentifierReference) -> Identifier { - Identifier { - base: self.make_base_node(id.span), - name: id.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - } - } - - fn convert_object_pattern(&self, obj: &oxc::ObjectPattern) -> ObjectPattern { - let mut properties: Vec = - obj.properties.iter().map(|p| self.convert_binding_property(p)).collect(); - - // Handle rest element (separate field in OXC v0.121) - if let Some(rest) = &obj.rest { - properties - .push(ObjectPatternProperty::RestElement(self.convert_binding_rest_element(rest))); - } - - ObjectPattern { - base: self.make_base_node(obj.span), - properties, - type_annotation: None, - decorators: None, - } - } - - fn convert_binding_property(&self, prop: &oxc::BindingProperty) -> ObjectPatternProperty { - // BindingProperty is now a struct (not an enum) in OXC v0.121 - ObjectPatternProperty::ObjectProperty(ObjectPatternProp { - base: self.make_base_node(prop.span), - key: Box::new(self.convert_property_key(&prop.key)), - value: Box::new(self.convert_binding_pattern(&prop.value)), - computed: prop.computed, - shorthand: prop.shorthand, - decorators: None, - method: None, - }) - } - - fn convert_array_pattern(&self, arr: &oxc::ArrayPattern) -> ArrayPattern { - let mut elements: Vec> = arr - .elements - .iter() - .map(|e| e.as_ref().map(|p| self.convert_binding_pattern(p))) - .collect(); - - // Handle rest element (separate field in OXC v0.121) - if let Some(rest) = &arr.rest { - elements.push(Some(PatternLike::RestElement(self.convert_binding_rest_element(rest)))); - } - - ArrayPattern { - base: self.make_base_node(arr.span), - elements, - type_annotation: None, - decorators: None, - } - } - - fn convert_assignment_pattern(&self, assign: &oxc::AssignmentPattern) -> AssignmentPattern { - AssignmentPattern { - base: self.make_base_node(assign.span), - left: Box::new(self.convert_binding_pattern(&assign.left)), - right: Box::new(self.convert_expression(&assign.right)), - type_annotation: None, - decorators: None, - } - } - - fn convert_binding_rest_element(&self, rest: &oxc::BindingRestElement) -> RestElement { - RestElement { - base: self.make_base_node(rest.span), - argument: Box::new(self.convert_binding_pattern(&rest.argument)), - type_annotation: None, - decorators: None, - } - } - - /// Convert FormalParameters (items + optional rest) to a Vec. - /// OXC stores rest parameters separately from items, but Babel includes them - /// in the same params array. - fn convert_formal_parameters(&self, params: &oxc::FormalParameters) -> Vec { - let mut result: Vec = - params.items.iter().map(|p| self.convert_formal_parameter(p)).collect(); - if let Some(rest) = ¶ms.rest { - result.push(PatternLike::RestElement(RestElement { - base: self.make_base_node(rest.rest.span), - argument: Box::new(self.convert_binding_pattern(&rest.rest.argument)), - type_annotation: rest - .type_annotation - .as_ref() - .map(|type_annotation| self.convert_ts_type_annotation_json(type_annotation)), - decorators: None, - })); - } - result - } - - fn convert_formal_parameter(&self, param: &oxc::FormalParameter) -> PatternLike { - let mut pattern = self.convert_binding_pattern(¶m.pattern); - - if param.optional - && let PatternLike::Identifier(id) = &mut pattern - { - id.optional = Some(true); - } - - // Add type annotation if present (now on FormalParameter, not BindingPattern) - if let Some(type_annotation) = ¶m.type_annotation { - let type_json = self.convert_ts_type_annotation_json(type_annotation); - Self::set_pattern_type_annotation(&mut pattern, type_json); - } - - // Handle default parameter values: OXC stores default values in - // FormalParameter.initializer rather than using BindingPattern::AssignmentPattern. - // Babel/TS expects them as AssignmentPattern in the params array. - if let Some(ref initializer) = param.initializer { - let right = self.convert_expression(initializer); - pattern = PatternLike::AssignmentPattern(AssignmentPattern { - base: self.make_base_node(param.span), - left: Box::new(pattern), - right: Box::new(right), - type_annotation: None, - decorators: None, - }); - } - - pattern - } - - fn set_pattern_type_annotation(pattern: &mut PatternLike, type_annotation: RawNode) { - match pattern { - PatternLike::Identifier(id) => { - id.type_annotation = Some(type_annotation); - } - PatternLike::ObjectPattern(obj) => { - obj.type_annotation = Some(type_annotation); - } - PatternLike::ArrayPattern(arr) => { - arr.type_annotation = Some(type_annotation); - } - PatternLike::AssignmentPattern(assign) => { - assign.type_annotation = Some(type_annotation); - } - PatternLike::RestElement(rest) => { - rest.type_annotation = Some(type_annotation); - } - PatternLike::MemberExpression(_) - | PatternLike::TSAsExpression(_) - | PatternLike::TSSatisfiesExpression(_) - | PatternLike::TSNonNullExpression(_) - | PatternLike::TSTypeAssertion(_) - | PatternLike::TypeCastExpression(_) => {} - } - } - - fn convert_function_body(&self, body: &oxc::FunctionBody) -> BlockStatement { - BlockStatement { - base: self.make_base_node(body.span), - body: body.statements.iter().map(|s| self.convert_statement(s)).collect(), - directives: body.directives.iter().map(|d| self.convert_directive(d)).collect(), - } - } - - /// Convert an optional function body. TypeScript overload signatures and - /// ambient (`declare`) function declarations have no body, but Babel's - /// `FunctionDeclaration`/`FunctionExpression` require one. These are - /// type-level constructs the compiler never analyzes, so emit an empty - /// block as a placeholder spanning the function. - fn convert_optional_function_body( - &self, - body: Option<&oxc::FunctionBody>, - fallback_span: Span, - ) -> BlockStatement { - match body { - Some(body) => self.convert_function_body(body), - None => BlockStatement { - base: self.make_base_node(fallback_span), - body: vec![], - directives: vec![], - }, - } - } - - // ============================================================ - // Helper methods for converting Expression-inheriting enums - // These handle the case where OXC enums inherit from Expression - // via @inherit and each variant has a differently-typed Box. - // ============================================================ - - /// Convert Argument expression variants (not SpreadElement) to Expression - fn convert_expression_from_argument(&self, arg: &oxc::Argument) -> Expression { - self.convert_expression_like(arg) - } - - /// Convert ArrayExpressionElement expression variants to Expression - fn convert_expression_from_array_element( - &self, - elem: &oxc::ArrayExpressionElement, - ) -> Expression { - self.convert_expression_like(elem) - } - - /// Convert ExportDefaultDeclarationKind expression variants to Expression - fn convert_expression_from_export_default( - &self, - kind: &oxc::ExportDefaultDeclarationKind, - ) -> Expression { - self.convert_expression_like(kind) - } - - /// Convert PropertyKey expression variants to Expression - fn convert_expression_from_property_key(&self, key: &oxc::PropertyKey) -> Expression { - self.convert_expression_like(key) - } - - /// Convert JSXExpression expression variants to Expression - fn convert_expression_from_jsx_expression(&self, expr: &oxc::JSXExpression) -> Expression { - self.convert_expression_like(expr) - } - - /// Generic helper to convert any enum that inherits Expression variants. - /// Uses the ExpressionLike trait. - fn convert_expression_like(&self, value: &T) -> Expression { - value.convert_with(self) - } -} - -/// Trait for enums that inherit Expression variants. -/// Each implementing type matches its Expression-inherited variants and -/// delegates to ConvertCtx::convert_expression by constructing the equivalent -/// Expression variant. -trait ExpressionLike { - fn convert_with(&self, ctx: &ConvertCtx) -> Expression; -} - -/// Macro to implement ExpressionLike for enums that @inherit Expression. -/// Each variant name matches the Expression variant name, so we can -/// deref the inner Box and call the appropriate convert method. -macro_rules! impl_expression_like { - ($enum_ty:ty, [$($non_expr_variant:pat => $non_expr_handler:expr),*]) => { - impl<'a> ExpressionLike for $enum_ty { - fn convert_with(&self, ctx: &ConvertCtx) -> Expression { - match self { - $($non_expr_variant => $non_expr_handler,)* - // Expression-inherited variants - Self::BooleanLiteral(e) => Expression::BooleanLiteral(BooleanLiteral { - base: ctx.make_base_node(e.span), - value: e.value, - }), - Self::NullLiteral(e) => Expression::NullLiteral(NullLiteral { - base: ctx.make_base_node(e.span), - }), - Self::NumericLiteral(e) => Expression::NumericLiteral(NumericLiteral { - base: ctx.make_base_node(e.span), - value: e.value, - extra: None, - }), - Self::BigIntLiteral(e) => { - Expression::BigIntLiteral(ctx.convert_big_int_literal(e)) - }, - Self::RegExpLiteral(e) => Expression::RegExpLiteral(RegExpLiteral { - base: ctx.make_base_node(e.span), - pattern: e.regex.pattern.text.to_string(), - flags: e.regex.flags.to_string(), - }), - Self::StringLiteral(e) => Expression::StringLiteral(StringLiteral { - base: ctx.make_base_node(e.span), - value: e.value.to_string().into(), - }), - Self::TemplateLiteral(e) => Expression::TemplateLiteral(ctx.convert_template_literal(e)), - Self::Identifier(e) => Expression::Identifier(ctx.convert_identifier_reference(e)), - Self::MetaProperty(e) => Expression::MetaProperty(ctx.convert_meta_property(e)), - Self::Super(e) => Expression::Super(Super { base: ctx.make_base_node(e.span) }), - Self::ArrayExpression(e) => Expression::ArrayExpression(ctx.convert_array_expression(e)), - Self::ArrowFunctionExpression(e) => Expression::ArrowFunctionExpression(ctx.convert_arrow_function_expression(e)), - Self::AssignmentExpression(e) => Expression::AssignmentExpression(ctx.convert_assignment_expression(e)), - Self::AwaitExpression(e) => Expression::AwaitExpression(ctx.convert_await_expression(e)), - Self::BinaryExpression(e) => Expression::BinaryExpression(ctx.convert_binary_expression(e)), - Self::CallExpression(e) => Expression::CallExpression(ctx.convert_call_expression(e)), - Self::ChainExpression(e) => ctx.convert_chain_expression(e), - Self::ClassExpression(e) => Expression::ClassExpression(ctx.convert_class_expression(e)), - Self::ConditionalExpression(e) => Expression::ConditionalExpression(ctx.convert_conditional_expression(e)), - Self::FunctionExpression(e) => Expression::FunctionExpression(ctx.convert_function_expression(e)), - Self::ImportExpression(i) => { - let mut args = vec![ctx.convert_expression(&i.source)]; - if let Some(ref opts) = i.options { - args.push(ctx.convert_expression(opts)); - } - Expression::CallExpression(CallExpression { - base: ctx.make_base_node(i.span), - callee: Box::new(Expression::Import(Import { - base: ctx.make_base_node(i.span), - })), - arguments: args, - optional: None, - type_arguments: None, - type_parameters: None, - }) - } - Self::LogicalExpression(e) => Expression::LogicalExpression(ctx.convert_logical_expression(e)), - Self::NewExpression(e) => Expression::NewExpression(ctx.convert_new_expression(e)), - Self::ObjectExpression(e) => Expression::ObjectExpression(ctx.convert_object_expression(e)), - Self::ParenthesizedExpression(e) => Expression::ParenthesizedExpression(ctx.convert_parenthesized_expression(e)), - Self::SequenceExpression(e) => Expression::SequenceExpression(ctx.convert_sequence_expression(e)), - Self::TaggedTemplateExpression(e) => Expression::TaggedTemplateExpression(ctx.convert_tagged_template_expression(e)), - Self::ThisExpression(e) => Expression::ThisExpression(ThisExpression { base: ctx.make_base_node(e.span) }), - Self::UnaryExpression(e) => Expression::UnaryExpression(ctx.convert_unary_expression(e)), - Self::UpdateExpression(e) => Expression::UpdateExpression(ctx.convert_update_expression(e)), - Self::YieldExpression(e) => Expression::YieldExpression(ctx.convert_yield_expression(e)), - Self::PrivateInExpression(e) => Expression::BinaryExpression(ctx.convert_private_in_expression(e)), - Self::JSXElement(e) => Expression::JSXElement(Box::new(ctx.convert_jsx_element(e))), - Self::JSXFragment(e) => Expression::JSXFragment(ctx.convert_jsx_fragment(e)), - Self::TSAsExpression(e) => Expression::TSAsExpression(ctx.convert_ts_as_expression(e)), - Self::TSSatisfiesExpression(e) => Expression::TSSatisfiesExpression(ctx.convert_ts_satisfies_expression(e)), - Self::TSTypeAssertion(e) => Expression::TSTypeAssertion(ctx.convert_ts_type_assertion(e)), - Self::TSNonNullExpression(e) => Expression::TSNonNullExpression(ctx.convert_ts_non_null_expression(e)), - Self::TSInstantiationExpression(e) => Expression::TSInstantiationExpression(ctx.convert_ts_instantiation_expression(e)), - Self::ComputedMemberExpression(e) => Expression::MemberExpression(MemberExpression { - base: ctx.make_base_node(e.span), - object: Box::new(ctx.convert_expression(&e.object)), - property: Box::new(ctx.convert_expression(&e.expression)), - computed: true, - }), - Self::StaticMemberExpression(e) => Expression::MemberExpression(MemberExpression { - base: ctx.make_base_node(e.span), - object: Box::new(ctx.convert_expression(&e.object)), - property: Box::new(Expression::Identifier(ctx.convert_identifier_name(&e.property))), - computed: false, - }), - Self::PrivateFieldExpression(e) => Expression::MemberExpression(MemberExpression { - base: ctx.make_base_node(e.span), - object: Box::new(ctx.convert_expression(&e.object)), - property: Box::new(Expression::PrivateName(PrivateName { - base: ctx.make_base_node(e.field.span), - id: Identifier { - base: ctx.make_base_node(e.field.span), - name: e.field.name.to_string(), - type_annotation: None, - optional: None, - decorators: None, - }, - })), - computed: false, - }), - Self::V8IntrinsicExpression(_) => unreachable!( - "V8IntrinsicExpression: oxc does not emit this without ParseOptions::allow_v8_intrinsics" - ), - } - } - } - }; -} - -// ForStatementInit: VariableDeclaration + @inherit Expression -impl_expression_like!(oxc::ForStatementInit<'a>, [ - Self::VariableDeclaration(_) => unreachable!("handled separately") -]); - -// Argument: SpreadElement + @inherit Expression -impl_expression_like!(oxc::Argument<'a>, [ - Self::SpreadElement(_) => unreachable!("handled separately") -]); - -// ArrayExpressionElement: SpreadElement + Elision + @inherit Expression -impl_expression_like!(oxc::ArrayExpressionElement<'a>, [ - Self::SpreadElement(_) => unreachable!("handled separately"), - Self::Elision(_) => unreachable!("handled separately") -]); - -// ExportDefaultDeclarationKind: FunctionDeclaration + ClassDeclaration + TSInterfaceDeclaration + @inherit Expression -impl_expression_like!(oxc::ExportDefaultDeclarationKind<'a>, [ - Self::FunctionDeclaration(_) => unreachable!("handled separately"), - Self::ClassDeclaration(_) => unreachable!("handled separately"), - Self::TSInterfaceDeclaration(_) => unreachable!("handled separately") -]); - -// PropertyKey: StaticIdentifier + PrivateIdentifier + @inherit Expression -impl_expression_like!(oxc::PropertyKey<'a>, [ - Self::StaticIdentifier(_) => unreachable!("handled separately"), - Self::PrivateIdentifier(_) => unreachable!("handled separately") -]); - -// JSXExpression: EmptyExpression + @inherit Expression -impl_expression_like!(oxc::JSXExpression<'a>, [ - Self::EmptyExpression(_) => unreachable!("handled separately") -]); diff --git a/crates/oxc_react_compiler/src/convert_ast_reverse.rs b/crates/oxc_react_compiler/src/convert_ast_reverse.rs deleted file mode 100644 index b2060518f08a7..0000000000000 --- a/crates/oxc_react_compiler/src/convert_ast_reverse.rs +++ /dev/null @@ -1,3141 +0,0 @@ -// Copyright (c) Meta Platforms, Inc. and affiliates. -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. - -//! Reverse AST converter: react_compiler_ast (Babel format) → OXC AST. -//! -//! This is the inverse of `convert_ast.rs`. It takes a `react_compiler_ast::File` -//! (which represents the compiler's Babel-compatible output) and produces OXC AST -//! nodes allocated in an OXC arena, suitable for code generation via `oxc_codegen`. - -use oxc_allocator::{Allocator, Box as ArenaBox}; -use oxc_ast::ast as oxc; -use oxc_ast_visit::VisitMut; -use oxc_span::SPAN; -use oxc_span::Span; -use oxc_syntax::identifier::is_identifier_name; -use react_compiler_ast::common::BaseNode; -use react_compiler_ast::declarations::*; -use react_compiler_ast::expressions::*; -use react_compiler_ast::jsx::*; -use react_compiler_ast::operators::*; -use react_compiler_ast::patterns::*; -use react_compiler_ast::statements::*; - -fn set_statement_span(stmt: &mut oxc::Statement<'_>, span: Span) { - use oxc_span::GetSpanMut; - match stmt { - oxc::Statement::ImportDeclaration(d) => *d.span_mut() = span, - oxc::Statement::VariableDeclaration(d) => *d.span_mut() = span, - oxc::Statement::FunctionDeclaration(d) => *d.span_mut() = span, - oxc::Statement::ExportNamedDeclaration(d) => *d.span_mut() = span, - oxc::Statement::ExportDefaultDeclaration(d) => *d.span_mut() = span, - oxc::Statement::ExportAllDeclaration(d) => *d.span_mut() = span, - oxc::Statement::ExpressionStatement(s) => *s.span_mut() = span, - oxc::Statement::IfStatement(s) => *s.span_mut() = span, - oxc::Statement::ForStatement(s) => *s.span_mut() = span, - oxc::Statement::WhileStatement(s) => *s.span_mut() = span, - oxc::Statement::DoWhileStatement(s) => *s.span_mut() = span, - oxc::Statement::ForInStatement(s) => *s.span_mut() = span, - oxc::Statement::ForOfStatement(s) => *s.span_mut() = span, - oxc::Statement::SwitchStatement(s) => *s.span_mut() = span, - oxc::Statement::ThrowStatement(s) => *s.span_mut() = span, - oxc::Statement::TryStatement(s) => *s.span_mut() = span, - oxc::Statement::BreakStatement(s) => *s.span_mut() = span, - oxc::Statement::ContinueStatement(s) => *s.span_mut() = span, - oxc::Statement::LabeledStatement(s) => *s.span_mut() = span, - oxc::Statement::BlockStatement(s) => *s.span_mut() = span, - oxc::Statement::ReturnStatement(s) => *s.span_mut() = span, - oxc::Statement::WithStatement(s) => *s.span_mut() = span, - oxc::Statement::EmptyStatement(s) => *s.span_mut() = span, - oxc::Statement::DebuggerStatement(s) => *s.span_mut() = span, - _ => {} // ClassDeclaration etc. - leave as-is - } -} - -fn encode_jsx_text(raw: &str) -> String { - let mut escaped = String::with_capacity(raw.len()); - for ch in raw.chars() { - match ch { - '&' => escaped.push_str("&"), - '<' => escaped.push_str("<"), - '>' => escaped.push_str(">"), - '{' => escaped.push_str("{"), - '}' => escaped.push_str("}"), - _ => escaped.push(ch), - } - } - escaped -} - -struct SpanShift { - offset: u32, -} - -impl VisitMut<'_> for SpanShift { - fn visit_span(&mut self, span: &mut Span) { - span.start = span.start.saturating_add(self.offset); - span.end = span.end.saturating_add(self.offset); - } -} - -/// Convert a `react_compiler_ast::File` into an OXC `Program` allocated in the given arena. -pub fn convert_program_to_oxc<'a>( - file: &react_compiler_ast::File, - allocator: &'a Allocator, -) -> oxc::Program<'a> { - let ctx = ReverseCtx::new(allocator, None); - ctx.convert_program(&file.program) -} - -/// Convert with source text available for extracting TS declarations. -pub fn convert_program_to_oxc_with_source<'a>( - file: &react_compiler_ast::File, - allocator: &'a Allocator, - source_text: &str, -) -> oxc::Program<'a> { - let ctx = ReverseCtx::new(allocator, Some(source_text.to_string())); - ctx.convert_program(&file.program) -} - -struct ReverseCtx<'a> { - allocator: &'a Allocator, - builder: oxc_ast::AstBuilder<'a>, - source_text: Option, -} - -impl<'a> ReverseCtx<'a> { - fn new(allocator: &'a Allocator, source_text: Option) -> Self { - Self { allocator, builder: oxc_ast::AstBuilder::new(allocator), source_text } - } - - /// Extract a statement from the original source text using the base node's - /// start/end positions. Re-parses the snippet with OXC to get a proper AST node. - fn extract_source_stmt(&self, base: &BaseNode) -> Option> { - let text = self.source_text_for_base(base)?; - self.parse_source_stmt_text_at(text, base.start? as usize) - } - - fn parse_source_stmt_text_at( - &self, - text: &str, - original_start: usize, - ) -> Option> { - let text_ref = self.copy_source_text_to_allocator(text); - let parsed = - oxc_parser::Parser::new(self.allocator, text_ref, oxc_span::SourceType::tsx()).parse(); - if parsed.panicked || parsed.program.body.is_empty() { - return None; - } - let mut stmt = parsed.program.body.into_iter().next()?; - if original_start > 0 { - let mut shifter = SpanShift { offset: original_start as u32 }; - shifter.visit_statement(&mut stmt); - } - Some(stmt) - } - - /// Extract an expression from the original source text using the base - /// node's start/end positions. - fn extract_source_expr(&self, base: &BaseNode) -> Option> { - let text = self.source_text_for_base(base)?; - self.parse_source_expr_text_at(text, base.start? as usize) - } - - fn parse_source_expr_text_at( - &self, - text: &str, - original_start: usize, - ) -> Option> { - let text_ref = self.copy_source_text_to_allocator(text); - let mut expr = - oxc_parser::Parser::new(self.allocator, text_ref, oxc_span::SourceType::tsx()) - .parse_expression() - .ok()?; - if original_start > 0 { - let mut shifter = SpanShift { offset: original_start as u32 }; - shifter.visit_expression(&mut expr); - } - Some(expr) - } - - fn parse_source_ts_type_text_at( - &self, - text: &str, - original_start: usize, - ) -> Option> { - const PREFIX: &str = "let __oxc_type = __oxc_value as "; - let wrapped = format!("{PREFIX}{text};"); - let stmt = - self.parse_source_stmt_text_at(&wrapped, original_start.saturating_sub(PREFIX.len()))?; - let oxc::Statement::VariableDeclaration(decl) = stmt else { return None }; - let decl = decl.unbox(); - let init = decl.declarations.into_iter().next()?.init?; - let oxc::Expression::TSAsExpression(ts_as) = init else { return None }; - Some(ts_as.unbox().type_annotation) - } - - fn copy_source_text_to_allocator(&self, text: &str) -> &'a str { - oxc_allocator::StringBuilder::from_str_in(text, self.allocator).into_str() - } - - fn extract_source_class_expression( - &self, - class: &react_compiler_ast::expressions::ClassExpression, - ) -> Option> { - let expr = self.extract_source_expr(&class.base)?; - if matches!(expr, oxc::Expression::ClassExpression(_)) { Some(expr) } else { None } - } - - fn extract_source_call_type_arguments( - &self, - base: &BaseNode, - ) -> Option>> { - match self.extract_source_expr(base)? { - oxc::Expression::CallExpression(call) => call.unbox().type_arguments, - oxc::Expression::ChainExpression(chain) => match chain.unbox().expression { - oxc::ChainElement::CallExpression(call) => call.unbox().type_arguments, - _ => None, - }, - _ => None, - } - } - - fn extract_source_call_arguments(&self, base: &BaseNode) -> Option>> { - match self.extract_source_expr(base)? { - oxc::Expression::CallExpression(call) => { - Some(call.unbox().arguments.into_iter().collect()) - } - oxc::Expression::ChainExpression(chain) => match chain.unbox().expression { - oxc::ChainElement::CallExpression(call) => { - Some(call.unbox().arguments.into_iter().collect()) - } - _ => None, - }, - _ => None, - } - } - - fn extract_source_new_type_arguments( - &self, - base: &BaseNode, - ) -> Option>> { - match self.extract_source_expr(base)? { - oxc::Expression::NewExpression(new) => new.unbox().type_arguments, - _ => None, - } - } - - fn extract_source_new_arguments(&self, base: &BaseNode) -> Option>> { - match self.extract_source_expr(base)? { - oxc::Expression::NewExpression(new) => { - Some(new.unbox().arguments.into_iter().collect()) - } - _ => None, - } - } - - fn extract_source_tagged_template_type_arguments( - &self, - base: &BaseNode, - ) -> Option>> { - match self.extract_source_expr(base)? { - oxc::Expression::TaggedTemplateExpression(tagged) => tagged.unbox().type_arguments, - _ => None, - } - } - - fn extract_source_jsx_type_arguments( - &self, - base: &BaseNode, - ) -> Option>> { - match self.extract_source_expr(base)? { - oxc::Expression::JSXElement(element) => { - element.unbox().opening_element.unbox().type_arguments - } - _ => None, - } - } - - fn extract_source_ts_as_type(&self, base: &BaseNode) -> Option> { - match self.extract_source_expr(base)? { - oxc::Expression::TSAsExpression(expr) => Some(expr.unbox().type_annotation), - _ => None, - } - } - - fn extract_source_ts_type_from_json( - &self, - value: &serde_json::Value, - ) -> Option> { - let value = - if value.get("type").and_then(serde_json::Value::as_str) == Some("TSTypeAnnotation") { - value.get("typeAnnotation")? - } else { - value - }; - let source = self.source_text.as_deref()?; - let start = value.get("start")?.as_u64()? as usize; - let end = value.get("end")?.as_u64()? as usize; - if start >= source.len() || end > source.len() || start >= end { - return None; - } - self.parse_source_ts_type_text_at(&source[start..end], start) - } - - fn span_from_json_value(&self, value: &serde_json::Value) -> Span { - let start = value.get("start").and_then(serde_json::Value::as_u64); - let end = value.get("end").and_then(serde_json::Value::as_u64); - match (start, end) { - (Some(start), Some(end)) => Span::new(start as u32, end as u32), - (Some(start), None) => Span::new(start as u32, start as u32), - _ => SPAN, - } - } - - fn convert_ts_type_annotation_from_json( - &self, - value: &serde_json::Value, - ) -> Option>> { - let ty = - if value.get("type").and_then(serde_json::Value::as_str) == Some("TSTypeAnnotation") { - let type_annotation = value.get("typeAnnotation")?; - self.convert_ts_type_from_json(type_annotation).or_else(|| { - if Self::ts_type_json_contains_type_query(type_annotation) { - None - } else { - self.extract_source_ts_type_from_json(value) - } - })? - } else { - self.convert_ts_type_from_json(value)? - }; - Some(self.builder.alloc_ts_type_annotation(SPAN, ty)) - } - - fn convert_ts_type_from_json(&self, value: &serde_json::Value) -> Option> { - if !Self::ts_type_json_contains_type_query(value) - && let Some(ty) = self.extract_source_ts_type_from_json(value) - { - return Some(ty); - } - - match value.get("type")?.as_str()? { - "TSAnyKeyword" => Some(self.builder.ts_type_any_keyword(SPAN)), - "TSBigIntKeyword" => Some(self.builder.ts_type_big_int_keyword(SPAN)), - "TSBooleanKeyword" => Some(self.builder.ts_type_boolean_keyword(SPAN)), - "TSIntrinsicKeyword" => Some(self.builder.ts_type_intrinsic_keyword(SPAN)), - "TSNeverKeyword" => Some(self.builder.ts_type_never_keyword(SPAN)), - "TSNullKeyword" => Some(self.builder.ts_type_null_keyword(SPAN)), - "TSNumberKeyword" => Some(self.builder.ts_type_number_keyword(SPAN)), - "TSObjectKeyword" => Some(self.builder.ts_type_object_keyword(SPAN)), - "TSStringKeyword" => Some(self.builder.ts_type_string_keyword(SPAN)), - "TSSymbolKeyword" => Some(self.builder.ts_type_symbol_keyword(SPAN)), - "TSThisType" => Some(self.builder.ts_type_this_type(SPAN)), - "TSUndefinedKeyword" => Some(self.builder.ts_type_undefined_keyword(SPAN)), - "TSUnknownKeyword" => Some(self.builder.ts_type_unknown_keyword(SPAN)), - "TSVoidKeyword" => Some(self.builder.ts_type_void_keyword(SPAN)), - "TSArrayType" => { - let element_type = self.convert_ts_type_from_json(value.get("elementType")?)?; - Some(self.builder.ts_type_array_type(SPAN, element_type)) - } - "TSUnionType" => { - let types = value.get("types")?.as_array()?; - let types = self.builder.vec_from_iter( - types.iter().filter_map(|ty| self.convert_ts_type_from_json(ty)), - ); - Some(self.builder.ts_type_union_type(SPAN, types)) - } - "TSParenthesizedType" => { - let type_annotation = - self.convert_ts_type_from_json(value.get("typeAnnotation")?)?; - Some(self.builder.ts_type_parenthesized_type(SPAN, type_annotation)) - } - "TSTypeOperator" => { - let operator = match value.get("operator")?.as_str()? { - "keyof" => oxc::TSTypeOperatorOperator::Keyof, - "unique" => oxc::TSTypeOperatorOperator::Unique, - "readonly" => oxc::TSTypeOperatorOperator::Readonly, - _ => return None, - }; - let type_annotation = - self.convert_ts_type_from_json(value.get("typeAnnotation")?)?; - Some(self.builder.ts_type_type_operator_type(SPAN, operator, type_annotation)) - } - "TSTypeReference" => { - let type_name = self.convert_ts_type_name_from_json(value.get("typeName")?)?; - let type_arguments = - value.get("typeParameters").or_else(|| value.get("typeArguments")).and_then( - |value| self.convert_ts_type_parameter_instantiation_from_json(value), - ); - Some(self.builder.ts_type_type_reference(SPAN, type_name, type_arguments)) - } - "TSTypeQuery" => { - let expr_name = - self.convert_ts_type_query_expr_name_from_json(value.get("exprName")?)?; - let type_arguments = - value.get("typeParameters").or_else(|| value.get("typeArguments")).and_then( - |value| self.convert_ts_type_parameter_instantiation_from_json(value), - ); - Some(self.builder.ts_type_type_query( - self.span_from_json_value(value), - expr_name, - type_arguments, - )) - } - "TSIndexedAccessType" => { - let object_type = self.convert_ts_type_from_json(value.get("objectType")?)?; - let index_type = self.convert_ts_type_from_json(value.get("indexType")?)?; - Some(self.builder.ts_type_indexed_access_type( - self.span_from_json_value(value), - object_type, - index_type, - )) - } - "TSLiteralType" => { - let literal = self.convert_ts_literal_from_json(value.get("literal")?)?; - Some(self.builder.ts_type_literal_type(SPAN, literal)) - } - _ => None, - } - } - - fn convert_ts_type_name_from_json( - &self, - value: &serde_json::Value, - ) -> Option> { - match value.get("type")?.as_str()? { - "Identifier" => Some(self.builder.ts_type_name_identifier_reference( - self.span_from_json_value(value), - self.atom(value.get("name")?.as_str()?), - )), - "TSQualifiedName" => { - let left = self.convert_ts_type_name_from_json(value.get("left")?)?; - let right_value = value.get("right")?; - let right = self.builder.identifier_name( - self.span_from_json_value(right_value), - self.atom(right_value.get("name")?.as_str()?), - ); - Some(self.builder.ts_type_name_qualified_name( - self.span_from_json_value(value), - left, - right, - )) - } - "TSThisType" | "ThisExpression" => { - Some(self.builder.ts_type_name_this_expression(self.span_from_json_value(value))) - } - _ => None, - } - } - - fn convert_ts_type_query_expr_name_from_json( - &self, - value: &serde_json::Value, - ) -> Option> { - match self.convert_ts_type_name_from_json(value)? { - oxc::TSTypeName::IdentifierReference(identifier) => { - Some(oxc::TSTypeQueryExprName::IdentifierReference(identifier)) - } - oxc::TSTypeName::QualifiedName(qualified) => { - Some(oxc::TSTypeQueryExprName::QualifiedName(qualified)) - } - oxc::TSTypeName::ThisExpression(this) => { - Some(oxc::TSTypeQueryExprName::ThisExpression(this)) - } - } - } - - fn convert_ts_type_parameter_instantiation_from_json( - &self, - value: &serde_json::Value, - ) -> Option>> { - let params = value.get("params")?.as_array()?; - let params = self - .builder - .vec_from_iter(params.iter().filter_map(|ty| self.convert_ts_type_from_json(ty))); - Some(self.builder.alloc_ts_type_parameter_instantiation(SPAN, params)) - } - - fn convert_ts_literal_from_json( - &self, - value: &serde_json::Value, - ) -> Option> { - match value.get("type")?.as_str()? { - "BooleanLiteral" => { - Some(self.builder.ts_literal_boolean_literal(SPAN, value.get("value")?.as_bool()?)) - } - "NumericLiteral" => Some(self.builder.ts_literal_numeric_literal( - SPAN, - value.get("value")?.as_f64()?, - None, - oxc::NumberBase::Decimal, - )), - "StringLiteral" => Some(self.builder.ts_literal_string_literal( - SPAN, - self.atom(value.get("value")?.as_str()?), - None, - )), - "BigIntLiteral" => Some(self.builder.ts_literal_big_int_literal( - SPAN, - self.atom(value.get("value")?.as_str()?), - None, - oxc::BigintBase::Decimal, - )), - _ => None, - } - } - - fn ts_type_json_contains_type_query(value: &serde_json::Value) -> bool { - match value { - serde_json::Value::Object(obj) => { - let is_rename_sensitive_type_query = - obj.get("type").and_then(serde_json::Value::as_str) == Some("TSTypeQuery") - && obj - .get("exprName") - .and_then(|expr| expr.get("type")) - .and_then(serde_json::Value::as_str) - != Some("TSImportType"); - is_rename_sensitive_type_query - || obj.values().any(Self::ts_type_json_contains_type_query) - } - serde_json::Value::Array(values) => { - values.iter().any(Self::ts_type_json_contains_type_query) - } - _ => false, - } - } - - fn ts_type_contains_type_query(ty: &oxc::TSType<'a>) -> bool { - match ty { - oxc::TSType::TSTypeQuery(_) => true, - oxc::TSType::TSArrayType(ty) => Self::ts_type_contains_type_query(&ty.element_type), - oxc::TSType::TSUnionType(ty) => ty.types.iter().any(Self::ts_type_contains_type_query), - oxc::TSType::TSIntersectionType(ty) => { - ty.types.iter().any(Self::ts_type_contains_type_query) - } - oxc::TSType::TSParenthesizedType(ty) => { - Self::ts_type_contains_type_query(&ty.type_annotation) - } - oxc::TSType::TSTypeOperatorType(ty) => { - Self::ts_type_contains_type_query(&ty.type_annotation) - } - oxc::TSType::TSIndexedAccessType(ty) => { - Self::ts_type_contains_type_query(&ty.object_type) - || Self::ts_type_contains_type_query(&ty.index_type) - } - oxc::TSType::TSTypeReference(ty) => { - ty.type_arguments.as_ref().is_some_and(|type_arguments| { - Self::ts_type_arguments_contain_type_query(type_arguments) - }) - } - _ => false, - } - } - - fn ts_type_arguments_contain_type_query( - type_arguments: &oxc::TSTypeParameterInstantiation<'a>, - ) -> bool { - type_arguments.params.iter().any(Self::ts_type_contains_type_query) - } - - fn extract_source_ts_satisfies_type(&self, base: &BaseNode) -> Option> { - match self.extract_source_expr(base)? { - oxc::Expression::TSSatisfiesExpression(expr) => Some(expr.unbox().type_annotation), - _ => None, - } - } - - fn extract_source_ts_type_assertion_type(&self, base: &BaseNode) -> Option> { - match self.extract_source_expr(base)? { - oxc::Expression::TSTypeAssertion(expr) => Some(expr.unbox().type_annotation), - _ => None, - } - } - - fn extract_source_ts_instantiation_type_arguments( - &self, - base: &BaseNode, - ) -> Option>> { - match self.extract_source_expr(base)? { - oxc::Expression::TSInstantiationExpression(expr) => Some(expr.unbox().type_arguments), - _ => None, - } - } - - fn wrap_expression_with_source_ts( - &self, - base: &BaseNode, - expression: oxc::Expression<'a>, - ) -> Result, oxc::Expression<'a>> { - let Some(text) = self.source_text_for_base(base) else { - return Err(expression); - }; - if !text.contains(" as ") && !text.contains(" satisfies ") && !text.contains('<') { - return Err(expression); - } - - let Some(start) = base.start else { - return Err(expression); - }; - match self.parse_source_expr_text_at(text, start as usize) { - Some(source_expr) => self.wrap_expression_with_source_ts_expr(source_expr, expression), - None => Err(expression), - } - } - - fn wrap_expression_with_source_ts_expr( - &self, - source_expr: oxc::Expression<'a>, - expression: oxc::Expression<'a>, - ) -> Result, oxc::Expression<'a>> { - if Self::is_ts_expression_wrapper(&expression) { - return Err(expression); - } - match source_expr { - oxc::Expression::TSAsExpression(expr) => { - let expr = expr.unbox(); - if Self::ts_type_contains_type_query(&expr.type_annotation) { - return Err(expression); - } - Ok(self.builder.expression_ts_as(SPAN, expression, expr.type_annotation)) - } - oxc::Expression::TSSatisfiesExpression(expr) => { - let expr = expr.unbox(); - if Self::ts_type_contains_type_query(&expr.type_annotation) { - return Err(expression); - } - Ok(self.builder.expression_ts_satisfies(SPAN, expression, expr.type_annotation)) - } - oxc::Expression::TSTypeAssertion(expr) => { - let expr = expr.unbox(); - if Self::ts_type_contains_type_query(&expr.type_annotation) { - return Err(expression); - } - Ok(self.builder.expression_ts_type_assertion( - SPAN, - expr.type_annotation, - expression, - )) - } - oxc::Expression::TSInstantiationExpression(expr) => { - let expr = expr.unbox(); - if Self::ts_type_arguments_contain_type_query(&expr.type_arguments) { - return Err(expression); - } - Ok(self.builder.expression_ts_instantiation(SPAN, expression, expr.type_arguments)) - } - _ => Err(expression), - } - } - - fn wrap_expression_with_source_argument_ts( - &self, - source_arg: oxc::Argument<'a>, - expression: oxc::Expression<'a>, - ) -> Result, oxc::Expression<'a>> { - if Self::is_ts_expression_wrapper(&expression) { - return Err(expression); - } - match source_arg { - oxc::Argument::TSAsExpression(expr) => { - let expr = expr.unbox(); - if Self::ts_type_contains_type_query(&expr.type_annotation) { - return Err(expression); - } - Ok(self.builder.expression_ts_as(SPAN, expression, expr.type_annotation)) - } - oxc::Argument::TSSatisfiesExpression(expr) => { - let expr = expr.unbox(); - if Self::ts_type_contains_type_query(&expr.type_annotation) { - return Err(expression); - } - Ok(self.builder.expression_ts_satisfies(SPAN, expression, expr.type_annotation)) - } - oxc::Argument::TSTypeAssertion(expr) => { - let expr = expr.unbox(); - if Self::ts_type_contains_type_query(&expr.type_annotation) { - return Err(expression); - } - Ok(self.builder.expression_ts_type_assertion( - SPAN, - expr.type_annotation, - expression, - )) - } - oxc::Argument::TSInstantiationExpression(expr) => { - let expr = expr.unbox(); - if Self::ts_type_arguments_contain_type_query(&expr.type_arguments) { - return Err(expression); - } - Ok(self.builder.expression_ts_instantiation(SPAN, expression, expr.type_arguments)) - } - _ => Err(expression), - } - } - - fn is_ts_expression_wrapper(expression: &oxc::Expression<'a>) -> bool { - matches!( - expression, - oxc::Expression::TSAsExpression(_) - | oxc::Expression::TSSatisfiesExpression(_) - | oxc::Expression::TSTypeAssertion(_) - | oxc::Expression::TSInstantiationExpression(_) - ) - } - - fn expression_base(expr: &Expression) -> Option<&BaseNode> { - match expr { - Expression::Identifier(expr) => Some(&expr.base), - Expression::StringLiteral(expr) => Some(&expr.base), - Expression::NumericLiteral(expr) => Some(&expr.base), - Expression::BooleanLiteral(expr) => Some(&expr.base), - Expression::NullLiteral(expr) => Some(&expr.base), - Expression::BigIntLiteral(expr) => Some(&expr.base), - Expression::RegExpLiteral(expr) => Some(&expr.base), - Expression::CallExpression(expr) => Some(&expr.base), - Expression::MemberExpression(expr) => Some(&expr.base), - Expression::OptionalCallExpression(expr) => Some(&expr.base), - Expression::OptionalMemberExpression(expr) => Some(&expr.base), - Expression::BinaryExpression(expr) => Some(&expr.base), - Expression::LogicalExpression(expr) => Some(&expr.base), - Expression::UnaryExpression(expr) => Some(&expr.base), - Expression::UpdateExpression(expr) => Some(&expr.base), - Expression::ConditionalExpression(expr) => Some(&expr.base), - Expression::AssignmentExpression(expr) => Some(&expr.base), - Expression::SequenceExpression(expr) => Some(&expr.base), - Expression::ArrowFunctionExpression(expr) => Some(&expr.base), - Expression::FunctionExpression(expr) => Some(&expr.base), - Expression::ObjectExpression(expr) => Some(&expr.base), - Expression::ArrayExpression(expr) => Some(&expr.base), - Expression::NewExpression(expr) => Some(&expr.base), - Expression::TemplateLiteral(expr) => Some(&expr.base), - Expression::TaggedTemplateExpression(expr) => Some(&expr.base), - Expression::AwaitExpression(expr) => Some(&expr.base), - Expression::YieldExpression(expr) => Some(&expr.base), - Expression::SpreadElement(expr) => Some(&expr.base), - Expression::MetaProperty(expr) => Some(&expr.base), - Expression::ClassExpression(expr) => Some(&expr.base), - Expression::PrivateName(expr) => Some(&expr.base), - Expression::Super(expr) => Some(&expr.base), - Expression::Import(expr) => Some(&expr.base), - Expression::ThisExpression(expr) => Some(&expr.base), - Expression::ParenthesizedExpression(expr) => Some(&expr.base), - Expression::JSXElement(expr) => Some(&expr.base), - Expression::JSXFragment(expr) => Some(&expr.base), - Expression::AssignmentPattern(expr) => Some(&expr.base), - Expression::TSAsExpression(_) - | Expression::TSSatisfiesExpression(_) - | Expression::TSNonNullExpression(_) - | Expression::TSTypeAssertion(_) - | Expression::TSInstantiationExpression(_) - | Expression::TypeCastExpression(_) => None, - } - } - - fn extract_source_variable_declarator( - &self, - base: &BaseNode, - kind: oxc::VariableDeclarationKind, - ) -> Option> { - let text = self.source_text_for_base(base)?; - if !text.contains(':') && !text.contains('!') { - return None; - } - - let keyword = match kind { - oxc::VariableDeclarationKind::Var => "var", - oxc::VariableDeclarationKind::Let => "let", - oxc::VariableDeclarationKind::Const => "const", - oxc::VariableDeclarationKind::Using | oxc::VariableDeclarationKind::AwaitUsing => { - "using" - } - }; - let wrapped = format!("{keyword} {text};"); - let stmt = self.parse_source_stmt_text_at( - &wrapped, - (base.start? as usize).saturating_sub(keyword.len() + 1), - )?; - let oxc::Statement::VariableDeclaration(decl) = stmt else { return None }; - decl.unbox().declarations.into_iter().next() - } - - fn extract_source_object_property_value(&self, base: &BaseNode) -> Option> { - let text = self.source_text_for_base(base)?; - let wrapped = format!("({{{text}}})"); - let expr = - self.parse_source_expr_text_at(&wrapped, (base.start? as usize).saturating_sub(2))?; - let oxc::Expression::ObjectExpression(obj) = expr else { return None }; - let mut properties = obj.unbox().properties.into_iter(); - let Some(oxc::ObjectPropertyKind::ObjectProperty(prop)) = properties.next() else { - return None; - }; - Some(prop.unbox().value) - } - - fn extract_source_function_declaration(&self, base: &BaseNode) -> Option> { - let stmt = self.extract_source_stmt(base)?; - let oxc::Statement::FunctionDeclaration(func) = stmt else { return None }; - Some(func.unbox()) - } - - fn extract_source_function_expression(&self, base: &BaseNode) -> Option> { - match self.extract_source_expr(base)? { - oxc::Expression::FunctionExpression(func) => Some(func.unbox()), - _ => None, - } - } - - fn extract_source_arrow_function( - &self, - base: &BaseNode, - ) -> Option> { - match self.extract_source_expr(base)? { - oxc::Expression::ArrowFunctionExpression(arrow) => Some(arrow.unbox()), - _ => None, - } - } - - fn extract_source_object_method_function(&self, base: &BaseNode) -> Option> { - let text = self.source_text_for_base(base)?; - let wrapped = format!("({{{text}}})"); - let expr = - self.parse_source_expr_text_at(&wrapped, (base.start? as usize).saturating_sub(2))?; - let oxc::Expression::ObjectExpression(obj) = expr else { return None }; - let mut properties = obj.unbox().properties.into_iter(); - let Some(oxc::ObjectPropertyKind::ObjectProperty(prop)) = properties.next() else { - return None; - }; - let prop = prop.unbox(); - if !prop.method { - return None; - } - let oxc::Expression::FunctionExpression(func) = prop.value else { return None }; - Some(func.unbox()) - } - - fn apply_function_signature_from_source( - &self, - func: &mut oxc::Function<'a>, - source_func: oxc::Function<'a>, - ) { - func.type_parameters = source_func.type_parameters; - func.this_param = source_func.this_param; - func.return_type = source_func.return_type; - self.apply_formal_parameters_ts_from_source(&mut func.params, source_func.params); - } - - fn apply_arrow_signature_from_source( - &self, - arrow: &mut oxc::ArrowFunctionExpression<'a>, - source_arrow: oxc::ArrowFunctionExpression<'a>, - ) { - arrow.type_parameters = source_arrow.type_parameters; - arrow.return_type = source_arrow.return_type; - self.apply_formal_parameters_ts_from_source(&mut arrow.params, source_arrow.params); - } - - fn apply_formal_parameters_ts_from_source( - &self, - params: &mut ArenaBox<'a, oxc::FormalParameters<'a>>, - source_params: ArenaBox<'a, oxc::FormalParameters<'a>>, - ) { - let source_params = source_params.unbox(); - for (param, source_param) in params.items.iter_mut().zip(source_params.items) { - param.decorators = source_param.decorators; - param.type_annotation = source_param.type_annotation; - param.optional = source_param.optional; - param.accessibility = source_param.accessibility; - param.readonly = source_param.readonly; - param.r#override = source_param.r#override; - } - - if let (Some(rest), Some(source_rest)) = (&mut params.rest, source_params.rest) { - let source_rest = source_rest.unbox(); - rest.decorators = source_rest.decorators; - rest.type_annotation = source_rest.type_annotation; - } - } - - fn block_initializes_react_cache(&self, block: &BlockStatement) -> bool { - block.body.iter().any(|stmt| { - let Statement::VariableDeclaration(decl) = stmt else { return false }; - decl.declarations.iter().any(|declarator| { - declarator.init.as_deref().is_some_and(Self::is_react_cache_call_expression) - }) - }) - } - - fn is_react_cache_call_expression(expr: &Expression) -> bool { - let Expression::CallExpression(call) = expr else { return false }; - matches!(call.callee.as_ref(), Expression::Identifier(id) if id.name == "_c") - } - - fn source_text_for_base(&self, base: &BaseNode) -> Option<&str> { - let source = self.source_text.as_deref()?; - let start = base.start? as usize; - let end = base.end? as usize; - if start >= source.len() || end > source.len() || start >= end { - return None; - } - Some(&source[start..end]) - } - - fn import_declaration_has_empty_named_specifiers(&self, decl: &ImportDeclaration) -> bool { - if let Some(text) = self.source_text_for_base(&decl.base) { - let Some(mut rest) = text.trim_start().strip_prefix("import").map(str::trim_start) - else { - return false; - }; - if let Some(after_type) = rest.strip_prefix("type") - && after_type.chars().next().is_some_and(char::is_whitespace) - { - rest = after_type.trim_start(); - } - return rest.starts_with("{}"); - } - - matches!(decl.import_kind, Some(ImportKind::Type | ImportKind::Typeof)) - } - - fn export_named_needs_source_stmt(&self, decl: &ExportNamedDeclaration) -> bool { - matches!( - decl.declaration.as_deref(), - Some( - Declaration::ClassDeclaration(_) - | Declaration::TSTypeAliasDeclaration(_) - | Declaration::TSInterfaceDeclaration(_) - | Declaration::TSEnumDeclaration(_) - | Declaration::TSModuleDeclaration(_) - | Declaration::TSDeclareFunction(_) - | Declaration::TypeAlias(_) - | Declaration::OpaqueType(_) - | Declaration::InterfaceDeclaration(_) - | Declaration::EnumDeclaration(_) - ) - ) - } - - fn export_default_needs_source_stmt(&self, decl: &ExportDefaultDeclaration) -> bool { - match decl.declaration.as_ref() { - ExportDefaultDecl::ClassDeclaration(_) => true, - ExportDefaultDecl::Expression(expr) => self.is_export_default_interface(expr, decl), - ExportDefaultDecl::FunctionDeclaration(_) | ExportDefaultDecl::EnumDeclaration(_) => { - false - } - } - } - - fn is_export_default_interface( - &self, - expr: &Expression, - decl: &ExportDefaultDeclaration, - ) -> bool { - let Expression::NullLiteral(null) = expr else { return false }; - self.source_text_for_base(&decl.base) - .is_some_and(|text| text.trim_start().starts_with("export default interface")) - || self - .source_text_for_base(&null.base) - .is_some_and(|text| text.trim_start().starts_with("interface")) - } - - /// Allocate a string in the arena and return a `&str` with lifetime 'a. - /// - /// The returned `&'a str` converts into both `Ident` and `Str` (identifier - /// and string-literal name types), so it feeds every `AstBuilder` method. - fn atom(&self, s: &str) -> &'a str { - oxc_allocator::StringBuilder::from_str_in(s, self.allocator).into_str() - } - - /// Convert a BaseNode's start/end into an OXC Span. - /// Returns SPAN (0,0) if the base has no position info. - fn span_from_base(&self, base: &BaseNode) -> Span { - match (base.start, base.end) { - (Some(start), Some(end)) => Span::new(start, end), - (Some(start), None) => Span::new(start, start), - _ => SPAN, - } - } - - // ===== Program ===== - - fn convert_program(&self, program: &react_compiler_ast::Program) -> oxc::Program<'a> { - let source_type = match program.source_type { - react_compiler_ast::SourceType::Module => oxc_span::SourceType::mjs(), - react_compiler_ast::SourceType::Script => oxc_span::SourceType::cjs(), - }; - - // Use convert_statements_with_spans for the top-level body so that - // original source positions are preserved. This allows comments from - // the original source to be correctly attached to statements. - let body = self.convert_statements_with_spans(&program.body); - let directives = self.convert_directives(&program.directives); - let hashbang = program.interpreter.as_ref().map(|interpreter| { - self.builder - .hashbang(self.span_from_base(&interpreter.base), self.atom(&interpreter.value)) - }); - let comments = self.builder.vec(); - - self.builder.program(SPAN, source_type, "", comments, hashbang, directives, body) - } - - // ===== Directives ===== - - fn convert_directives( - &self, - directives: &[Directive], - ) -> oxc_allocator::Vec<'a, oxc::Directive<'a>> { - self.builder.vec_from_iter(directives.iter().map(|d| self.convert_directive(d))) - } - - fn convert_directive(&self, d: &Directive) -> oxc::Directive<'a> { - let expression = self.builder.string_literal(SPAN, self.atom(&d.value.value), None); - self.builder.directive(SPAN, expression, self.atom(&d.value.value)) - } - - // ===== Statements ===== - - /// Convert statements preserving span info from the Babel AST. - /// This is used for top-level program body where span positions - /// are needed for comment attachment. - fn convert_statements_with_spans( - &self, - stmts: &[Statement], - ) -> oxc_allocator::Vec<'a, oxc::Statement<'a>> { - self.builder.vec_from_iter(stmts.iter().map(|s| { - let span = self.get_statement_span(s); - let mut oxc_stmt = self.convert_statement(s); - if span != SPAN { - set_statement_span(&mut oxc_stmt, span); - } - oxc_stmt - })) - } - - /// Extract the span from a Babel AST Statement's base node. - fn get_statement_span(&self, stmt: &Statement) -> Span { - let base = match stmt { - Statement::BlockStatement(s) => &s.base, - Statement::ReturnStatement(s) => &s.base, - Statement::ExpressionStatement(s) => &s.base, - Statement::IfStatement(s) => &s.base, - Statement::ForStatement(s) => &s.base, - Statement::WhileStatement(s) => &s.base, - Statement::DoWhileStatement(s) => &s.base, - Statement::ForInStatement(s) => &s.base, - Statement::ForOfStatement(s) => &s.base, - Statement::SwitchStatement(s) => &s.base, - Statement::ThrowStatement(s) => &s.base, - Statement::TryStatement(s) => &s.base, - Statement::BreakStatement(s) => &s.base, - Statement::ContinueStatement(s) => &s.base, - Statement::LabeledStatement(s) => &s.base, - Statement::EmptyStatement(s) => &s.base, - Statement::DebuggerStatement(s) => &s.base, - Statement::WithStatement(s) => &s.base, - Statement::VariableDeclaration(d) => &d.base, - Statement::FunctionDeclaration(f) => &f.base, - Statement::ClassDeclaration(c) => &c.base, - Statement::ImportDeclaration(d) => &d.base, - Statement::ExportNamedDeclaration(d) => &d.base, - Statement::ExportDefaultDeclaration(d) => &d.base, - Statement::ExportAllDeclaration(d) => &d.base, - _ => return SPAN, - }; - self.span_from_base(base) - } - - fn convert_statement(&self, stmt: &Statement) -> oxc::Statement<'a> { - match stmt { - Statement::BlockStatement(s) => { - self.builder.statement_block(SPAN, self.convert_statement_vec(&s.body)) - } - Statement::ReturnStatement(s) => self - .builder - .statement_return(SPAN, s.argument.as_ref().map(|a| self.convert_expression(a))), - Statement::ExpressionStatement(s) => { - self.builder.statement_expression(SPAN, self.convert_expression(&s.expression)) - } - Statement::IfStatement(s) => self.builder.statement_if( - SPAN, - self.convert_expression(&s.test), - self.convert_statement(&s.consequent), - s.alternate.as_ref().map(|a| self.convert_statement(a)), - ), - Statement::ForStatement(s) => { - let init = s.init.as_ref().map(|i| self.convert_for_init(i)); - let test = s.test.as_ref().map(|t| self.convert_expression(t)); - let update = s.update.as_ref().map(|u| self.convert_expression(u)); - let body = self.convert_statement(&s.body); - self.builder.statement_for(SPAN, init, test, update, body) - } - Statement::WhileStatement(s) => self.builder.statement_while( - SPAN, - self.convert_expression(&s.test), - self.convert_statement(&s.body), - ), - Statement::DoWhileStatement(s) => self.builder.statement_do_while( - SPAN, - self.convert_statement(&s.body), - self.convert_expression(&s.test), - ), - Statement::ForInStatement(s) => self.builder.statement_for_in( - SPAN, - self.convert_for_in_of_left(&s.left), - self.convert_expression(&s.right), - self.convert_statement(&s.body), - ), - Statement::ForOfStatement(s) => self.builder.statement_for_of( - SPAN, - s.is_await, - self.convert_for_in_of_left(&s.left), - self.convert_expression(&s.right), - self.convert_statement(&s.body), - ), - Statement::SwitchStatement(s) => { - let cases = self.builder.vec_from_iter(s.cases.iter().map(|c| { - self.builder.switch_case( - SPAN, - c.test.as_ref().map(|t| self.convert_expression(t)), - self.convert_statement_vec(&c.consequent), - ) - })); - self.builder.statement_switch(SPAN, self.convert_expression(&s.discriminant), cases) - } - Statement::ThrowStatement(s) => { - self.builder.statement_throw(SPAN, self.convert_expression(&s.argument)) - } - Statement::TryStatement(s) => { - let block = self.convert_block_statement(&s.block); - let handler = s.handler.as_ref().map(|h| self.convert_catch_clause(h)); - let finalizer = s.finalizer.as_ref().map(|f| self.convert_block_statement(f)); - self.builder.statement_try(SPAN, block, handler, finalizer) - } - Statement::BreakStatement(s) => { - let label = s - .label - .as_ref() - .map(|l| self.builder.label_identifier(SPAN, self.atom(&l.name))); - self.builder.statement_break(SPAN, label) - } - Statement::ContinueStatement(s) => { - let label = s - .label - .as_ref() - .map(|l| self.builder.label_identifier(SPAN, self.atom(&l.name))); - self.builder.statement_continue(SPAN, label) - } - Statement::LabeledStatement(s) => { - let label = self.builder.label_identifier(SPAN, self.atom(&s.label.name)); - self.builder.statement_labeled(SPAN, label, self.convert_statement(&s.body)) - } - Statement::EmptyStatement(_) => self.builder.statement_empty(SPAN), - Statement::DebuggerStatement(_) => self.builder.statement_debugger(SPAN), - Statement::WithStatement(s) => self.builder.statement_with( - SPAN, - self.convert_expression(&s.object), - self.convert_statement(&s.body), - ), - Statement::VariableDeclaration(d) => { - let decl = self.convert_variable_declaration(d); - oxc::Statement::VariableDeclaration(self.builder.alloc(decl)) - } - Statement::FunctionDeclaration(f) => { - let func = self.convert_function_decl(f, oxc::FunctionType::FunctionDeclaration); - oxc::Statement::FunctionDeclaration(self.builder.alloc(func)) - } - // The compiler never compiles classes, so the converter stubs out the - // class body (members -> null forward, empty reverse). Re-parse the - // original class from source to preserve its members; fall back to the - // structural (member-less) conversion only if the slice is unavailable. - Statement::ClassDeclaration(c) => { - self.extract_source_stmt(&c.base).unwrap_or_else(|| { - oxc::Statement::ClassDeclaration( - self.builder.alloc(self.convert_class_declaration(c)), - ) - }) - } - Statement::ImportDeclaration(d) => { - if d.specifiers.is_empty() - && let Some(stmt) = self.extract_source_stmt(&d.base) - { - return stmt; - } - let decl = self.convert_import_declaration(d); - oxc::Statement::ImportDeclaration(self.builder.alloc(decl)) - } - Statement::ExportNamedDeclaration(d) => { - if self.export_named_needs_source_stmt(d) { - if let Some(stmt) = self.extract_source_stmt(&d.base) { - return stmt; - } - } - let decl = self.convert_export_named_declaration(d); - oxc::Statement::ExportNamedDeclaration(self.builder.alloc(decl)) - } - Statement::ExportDefaultDeclaration(d) => { - if self.export_default_needs_source_stmt(d) { - return self.extract_source_stmt(&d.base).unwrap_or_else(|| { - if matches!(d.declaration.as_ref(), ExportDefaultDecl::Expression(_)) { - self.builder.statement_empty(SPAN) - } else { - let decl = self.convert_export_default_declaration(d); - oxc::Statement::ExportDefaultDeclaration(self.builder.alloc(decl)) - } - }); - } - let decl = self.convert_export_default_declaration(d); - oxc::Statement::ExportDefaultDeclaration(self.builder.alloc(decl)) - } - Statement::ExportAllDeclaration(d) => { - if let Some(stmt) = self.extract_source_stmt(&d.base) { - return stmt; - } - let decl = self.convert_export_all_declaration(d); - oxc::Statement::ExportAllDeclaration(self.builder.alloc(decl)) - } - // TS/Flow declarations - try to extract from source text, fall back to empty - Statement::TSTypeAliasDeclaration(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::TSInterfaceDeclaration(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::TSEnumDeclaration(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::TSModuleDeclaration(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::TSDeclareFunction(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::TypeAlias(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::OpaqueType(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::InterfaceDeclaration(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::EnumDeclaration(d) => self - .extract_source_stmt(&d.base) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - Statement::DeclareVariable(_) - | Statement::DeclareFunction(_) - | Statement::DeclareClass(_) - | Statement::DeclareModule(_) - | Statement::DeclareModuleExports(_) - | Statement::DeclareExportDeclaration(_) - | Statement::DeclareExportAllDeclaration(_) - | Statement::DeclareInterface(_) - | Statement::DeclareTypeAlias(_) - | Statement::DeclareOpaqueType(_) => self.builder.statement_empty(SPAN), - Statement::Unknown(s) => self - .extract_source_stmt(s.base()) - .unwrap_or_else(|| self.builder.statement_empty(SPAN)), - } - } - - fn convert_statement_vec( - &self, - stmts: &[Statement], - ) -> oxc_allocator::Vec<'a, oxc::Statement<'a>> { - self.builder.vec_from_iter(stmts.iter().map(|s| self.convert_statement(s))) - } - - fn convert_block_statement(&self, block: &BlockStatement) -> oxc::BlockStatement<'a> { - self.builder.block_statement(SPAN, self.convert_statement_vec(&block.body)) - } - - fn convert_catch_clause(&self, clause: &CatchClause) -> oxc::CatchClause<'a> { - let param = clause.param.as_ref().map(|p| { - let pattern = self.convert_pattern_to_binding_pattern(p); - let type_annotation = self.pattern_type_annotation(p); - self.builder.catch_parameter(SPAN, pattern, type_annotation) - }); - self.builder.catch_clause(SPAN, param, self.convert_block_statement(&clause.body)) - } - - fn convert_for_init(&self, init: &ForInit) -> oxc::ForStatementInit<'a> { - match init { - ForInit::VariableDeclaration(v) => { - let decl = self.convert_variable_declaration(v); - oxc::ForStatementInit::VariableDeclaration(self.builder.alloc(decl)) - } - ForInit::Expression(e) => oxc::ForStatementInit::from(self.convert_expression(e)), - } - } - - fn convert_for_in_of_left(&self, left: &ForInOfLeft) -> oxc::ForStatementLeft<'a> { - match left { - ForInOfLeft::VariableDeclaration(v) => { - let decl = self.convert_variable_declaration(v); - oxc::ForStatementLeft::VariableDeclaration(self.builder.alloc(decl)) - } - ForInOfLeft::Pattern(p) => { - let target = self.convert_pattern_to_assignment_target(p); - oxc::ForStatementLeft::from(target) - } - } - } - - fn convert_variable_declaration( - &self, - decl: &VariableDeclaration, - ) -> oxc::VariableDeclaration<'a> { - let kind = match decl.kind { - VariableDeclarationKind::Var => oxc::VariableDeclarationKind::Var, - VariableDeclarationKind::Let => oxc::VariableDeclarationKind::Let, - VariableDeclarationKind::Const => oxc::VariableDeclarationKind::Const, - VariableDeclarationKind::Using => oxc::VariableDeclarationKind::Using, - }; - let declarators = self.builder.vec_from_iter( - decl.declarations.iter().map(|d| self.convert_variable_declarator(d, kind)), - ); - let declare = decl.declare.unwrap_or(false); - self.builder.variable_declaration(SPAN, kind, declarators, declare) - } - - fn convert_variable_declarator( - &self, - d: &VariableDeclarator, - kind: oxc::VariableDeclarationKind, - ) -> oxc::VariableDeclarator<'a> { - let id = self.convert_pattern_to_binding_pattern(&d.id); - let definite = d.definite.unwrap_or(false); - let source_declarator = self.extract_source_variable_declarator(&d.base, kind); - let (type_annotation, source_init) = source_declarator.map_or((None, None), |d| { - let oxc::VariableDeclarator { type_annotation, init, .. } = d; - (type_annotation, init) - }); - let init = d.init.as_ref().map(|e| { - let converted = self.convert_expression(e); - match source_init { - Some(source_init) => { - match self.wrap_expression_with_source_ts_expr(source_init, converted) { - Ok(wrapped) => wrapped, - Err(converted) => converted, - } - } - None => converted, - } - }); - self.builder.variable_declarator(SPAN, kind, id, type_annotation, init, definite) - } - - // ===== Expressions ===== - - fn convert_expression(&self, expr: &Expression) -> oxc::Expression<'a> { - let converted = match expr { - Expression::Identifier(id) => { - self.builder.expression_identifier(SPAN, self.atom(&id.name)) - } - Expression::StringLiteral(lit) => self.builder.expression_string_literal( - SPAN, - self.atom(&lit.value.to_string_lossy()), - None, - ), - Expression::NumericLiteral(lit) => self.builder.expression_numeric_literal( - SPAN, - lit.value, - None, - oxc::NumberBase::Decimal, - ), - Expression::BooleanLiteral(lit) => { - self.builder.expression_boolean_literal(SPAN, lit.value) - } - Expression::NullLiteral(_) => self.builder.expression_null_literal(SPAN), - Expression::BigIntLiteral(lit) => self.builder.expression_big_int_literal( - SPAN, - self.atom(lit.value.strip_suffix('n').unwrap_or(&lit.value)), - None, - oxc::BigintBase::Decimal, - ), - Expression::RegExpLiteral(lit) => { - let flags = self.parse_regexp_flags(&lit.flags); - let pattern = - oxc::RegExpPattern { text: self.atom(&lit.pattern).into(), pattern: None }; - let regex = oxc::RegExp { pattern, flags }; - self.builder.expression_reg_exp_literal(SPAN, regex, None) - } - Expression::CallExpression(call) => { - if let Some(import_expr) = self.convert_dynamic_import_call(call) { - return import_expr; - } - let callee = self.convert_expression(&call.callee); - let args = self.convert_arguments_with_source( - &call.arguments, - self.extract_source_call_arguments(&call.base), - ); - let type_arguments = - if call.type_parameters.is_some() || call.type_arguments.is_some() { - self.extract_source_call_type_arguments(&call.base) - } else { - None - }; - self.builder.expression_call(SPAN, callee, type_arguments, args, false) - } - Expression::MemberExpression(m) => self.convert_member_expression(m), - Expression::OptionalCallExpression(call) => { - let callee = self.convert_expression_for_chain(&call.callee); - let args = self.convert_arguments_with_source( - &call.arguments, - self.extract_source_call_arguments(&call.base), - ); - let type_arguments = - if call.type_parameters.is_some() || call.type_arguments.is_some() { - self.extract_source_call_type_arguments(&call.base) - } else { - None - }; - let chain_call = self.builder.chain_element_call_expression( - SPAN, - callee, - type_arguments, - args, - call.optional, - ); - self.builder.expression_chain(SPAN, chain_call) - } - Expression::OptionalMemberExpression(m) => { - let chain_elem = self.convert_optional_member_to_chain_element(m); - self.builder.expression_chain(SPAN, chain_elem) - } - Expression::BinaryExpression(bin) => { - let op = self.convert_binary_operator(&bin.operator); - self.builder.expression_binary( - SPAN, - self.convert_expression(&bin.left), - op, - self.convert_expression(&bin.right), - ) - } - Expression::LogicalExpression(log) => { - let op = self.convert_logical_operator(&log.operator); - self.builder.expression_logical( - SPAN, - self.convert_expression(&log.left), - op, - self.convert_expression(&log.right), - ) - } - Expression::UnaryExpression(un) => { - let op = self.convert_unary_operator(&un.operator); - self.builder.expression_unary(SPAN, op, self.convert_expression(&un.argument)) - } - Expression::UpdateExpression(up) => { - let op = self.convert_update_operator(&up.operator); - let arg = self.convert_expression_to_simple_assignment_target(&up.argument); - self.builder.expression_update(SPAN, op, up.prefix, arg) - } - Expression::ConditionalExpression(cond) => self.builder.expression_conditional( - SPAN, - self.convert_expression(&cond.test), - self.convert_expression(&cond.consequent), - self.convert_expression(&cond.alternate), - ), - Expression::AssignmentExpression(assign) => { - let op = self.convert_assignment_operator(&assign.operator); - let left = self.convert_pattern_to_assignment_target(&assign.left); - self.builder.expression_assignment( - SPAN, - op, - left, - self.convert_expression(&assign.right), - ) - } - Expression::SequenceExpression(seq) => { - let exprs = self - .builder - .vec_from_iter(seq.expressions.iter().map(|e| self.convert_expression(e))); - self.builder.expression_sequence(SPAN, exprs) - } - Expression::ArrowFunctionExpression(arrow) => self.convert_arrow_function(arrow), - Expression::FunctionExpression(func) => { - let f = self.convert_function_expr(func); - oxc::Expression::FunctionExpression(self.builder.alloc(f)) - } - Expression::ObjectExpression(obj) => { - let properties = self.builder.vec_from_iter( - obj.properties.iter().map(|p| self.convert_object_expression_property(p)), - ); - self.builder.expression_object(SPAN, properties) - } - Expression::ArrayExpression(arr) => { - let elements = self - .builder - .vec_from_iter(arr.elements.iter().map(|e| self.convert_array_element(e))); - self.builder.expression_array(SPAN, elements) - } - Expression::NewExpression(n) => { - let callee = self.convert_expression(&n.callee); - let args = self.convert_arguments_with_source( - &n.arguments, - self.extract_source_new_arguments(&n.base), - ); - let type_arguments = if n.type_parameters.is_some() || n.type_arguments.is_some() { - self.extract_source_new_type_arguments(&n.base) - } else { - None - }; - self.builder.expression_new(SPAN, callee, type_arguments, args) - } - Expression::TemplateLiteral(tl) => { - let template = self.convert_template_literal(tl); - oxc::Expression::TemplateLiteral(self.builder.alloc(template)) - } - Expression::TaggedTemplateExpression(tag) => { - let t = self.convert_expression(&tag.tag); - let quasi = self.convert_template_literal(&tag.quasi); - let type_arguments = if tag.type_parameters.is_some() { - self.extract_source_tagged_template_type_arguments(&tag.base) - } else { - None - }; - self.builder.expression_tagged_template(SPAN, t, type_arguments, quasi) - } - Expression::AwaitExpression(a) => { - self.builder.expression_await(SPAN, self.convert_expression(&a.argument)) - } - Expression::YieldExpression(y) => self.builder.expression_yield( - SPAN, - y.delegate, - y.argument.as_ref().map(|a| self.convert_expression(a)), - ), - Expression::SpreadElement(s) => { - // SpreadElement can't be a standalone expression in OXC. - // Return the argument directly as a fallback. - self.convert_expression(&s.argument) - } - Expression::MetaProperty(mp) => { - let meta = self.builder.identifier_name(SPAN, self.atom(&mp.meta.name)); - let property = self.builder.identifier_name(SPAN, self.atom(&mp.property.name)); - self.builder.expression_meta_property(SPAN, meta, property) - } - Expression::ClassExpression(c) => { - if let Some(expr) = self.extract_source_class_expression(c) { - return expr; - } - let class = self.convert_class_to_oxc(c, oxc::ClassType::ClassExpression); - oxc::Expression::ClassExpression(self.builder.alloc(class)) - } - Expression::PrivateName(_) => { - self.builder.expression_identifier(SPAN, self.atom("__private__")) - } - Expression::Super(_) => self.builder.expression_super(SPAN), - Expression::Import(_) => { - self.builder.expression_identifier(SPAN, self.atom("__import__")) - } - Expression::ThisExpression(_) => self.builder.expression_this(SPAN), - Expression::ParenthesizedExpression(p) => { - self.builder.expression_parenthesized(SPAN, self.convert_expression(&p.expression)) - } - Expression::JSXElement(el) => { - let element = self.convert_jsx_element(el); - oxc::Expression::JSXElement(self.builder.alloc(element)) - } - Expression::JSXFragment(frag) => { - let fragment = self.convert_jsx_fragment(frag); - oxc::Expression::JSXFragment(self.builder.alloc(fragment)) - } - // TS expressions carry their actual type AST as `null` through the - // React AST bridge. Rebuild the wrapper with the converted child - // expression and recover only the type from the original source. - Expression::TSAsExpression(e) => { - let expression = self.convert_expression(&e.expression); - let parsed_type = e.type_annotation.parse_value(); - let type_annotation = self.convert_ts_type_from_json(&parsed_type).or_else(|| { - if Self::ts_type_json_contains_type_query(&parsed_type) { - None - } else { - self.extract_source_ts_as_type(&e.base) - } - }); - if let Some(type_annotation) = type_annotation { - self.builder.expression_ts_as(SPAN, expression, type_annotation) - } else { - expression - } - } - Expression::TSSatisfiesExpression(e) => { - let expression = self.convert_expression(&e.expression); - let parsed_type = e.type_annotation.parse_value(); - let type_annotation = self.convert_ts_type_from_json(&parsed_type).or_else(|| { - if Self::ts_type_json_contains_type_query(&parsed_type) { - None - } else { - self.extract_source_ts_satisfies_type(&e.base) - } - }); - if let Some(type_annotation) = type_annotation { - self.builder.expression_ts_satisfies(SPAN, expression, type_annotation) - } else { - expression - } - } - Expression::TSNonNullExpression(e) => { - self.builder.expression_ts_non_null(SPAN, self.convert_expression(&e.expression)) - } - Expression::TSTypeAssertion(e) => { - let expression = self.convert_expression(&e.expression); - let parsed_type = e.type_annotation.parse_value(); - let type_annotation = self.convert_ts_type_from_json(&parsed_type).or_else(|| { - if Self::ts_type_json_contains_type_query(&parsed_type) { - None - } else { - self.extract_source_ts_type_assertion_type(&e.base) - } - }); - if let Some(type_annotation) = type_annotation { - self.builder.expression_ts_type_assertion(SPAN, type_annotation, expression) - } else { - expression - } - } - Expression::TSInstantiationExpression(e) => { - let expression = self.convert_expression(&e.expression); - if let Some(type_arguments) = - self.extract_source_ts_instantiation_type_arguments(&e.base) - { - self.builder.expression_ts_instantiation(SPAN, expression, type_arguments) - } else { - expression - } - } - Expression::TypeCastExpression(e) => self.convert_expression(&e.expression), - Expression::AssignmentPattern(p) => { - let left = self.convert_pattern_to_assignment_target(&p.left); - self.builder.expression_assignment( - SPAN, - oxc_syntax::operator::AssignmentOperator::Assign, - left, - self.convert_expression(&p.right), - ) - } - }; - - if let Some(base) = Self::expression_base(expr) { - match self.wrap_expression_with_source_ts(base, converted) { - Ok(wrapped) => wrapped, - Err(converted) => converted, - } - } else { - converted - } - } - - fn convert_dynamic_import_call(&self, call: &CallExpression) -> Option> { - if !matches!(call.callee.as_ref(), Expression::Import(_)) { - return None; - } - let source = call.arguments.first().map(|arg| self.convert_expression(arg))?; - let options = call.arguments.get(1).map(|arg| self.convert_expression(arg)); - Some(self.builder.expression_import(SPAN, source, options, None)) - } - - /// Convert an expression that may be used inside a chain (optional chaining). - fn convert_expression_for_chain(&self, expr: &Expression) -> oxc::Expression<'a> { - match expr { - Expression::OptionalMemberExpression(m) => { - self.convert_optional_member_to_expression(m) - } - Expression::OptionalCallExpression(call) => { - let callee = self.convert_expression_for_chain(&call.callee); - let args = self.convert_arguments_with_source( - &call.arguments, - self.extract_source_call_arguments(&call.base), - ); - let type_arguments = - if call.type_parameters.is_some() || call.type_arguments.is_some() { - self.extract_source_call_type_arguments(&call.base) - } else { - None - }; - let call_expr = - self.builder.call_expression(SPAN, callee, type_arguments, args, call.optional); - oxc::Expression::CallExpression(self.builder.alloc(call_expr)) - } - _ => self.convert_expression(expr), - } - } - - fn convert_member_expression(&self, m: &MemberExpression) -> oxc::Expression<'a> { - let object = self.convert_expression(&m.object); - if m.computed { - let property = self.convert_expression(&m.property); - oxc::Expression::ComputedMemberExpression( - self.builder - .alloc(self.builder.computed_member_expression(SPAN, object, property, false)), - ) - } else { - let prop_name = self.expression_to_identifier_name(&m.property); - oxc::Expression::StaticMemberExpression( - self.builder - .alloc(self.builder.static_member_expression(SPAN, object, prop_name, false)), - ) - } - } - - fn convert_optional_member_to_chain_element( - &self, - m: &OptionalMemberExpression, - ) -> oxc::ChainElement<'a> { - let object = self.convert_expression_for_chain(&m.object); - if m.computed { - let property = self.convert_expression(&m.property); - oxc::ChainElement::ComputedMemberExpression( - self.builder.alloc( - self.builder.computed_member_expression(SPAN, object, property, m.optional), - ), - ) - } else { - let prop_name = self.expression_to_identifier_name(&m.property); - oxc::ChainElement::StaticMemberExpression( - self.builder.alloc( - self.builder.static_member_expression(SPAN, object, prop_name, m.optional), - ), - ) - } - } - - fn convert_optional_member_to_expression( - &self, - m: &OptionalMemberExpression, - ) -> oxc::Expression<'a> { - let object = self.convert_expression_for_chain(&m.object); - if m.computed { - let property = self.convert_expression(&m.property); - oxc::Expression::ComputedMemberExpression( - self.builder.alloc( - self.builder.computed_member_expression(SPAN, object, property, m.optional), - ), - ) - } else { - let prop_name = self.expression_to_identifier_name(&m.property); - oxc::Expression::StaticMemberExpression( - self.builder.alloc( - self.builder.static_member_expression(SPAN, object, prop_name, m.optional), - ), - ) - } - } - - fn expression_to_identifier_name(&self, expr: &Expression) -> oxc::IdentifierName<'a> { - match expr { - Expression::Identifier(id) => self.builder.identifier_name(SPAN, self.atom(&id.name)), - _ => self.builder.identifier_name(SPAN, self.atom("__unknown__")), - } - } - - fn convert_arguments_with_source( - &self, - args: &[Expression], - source_args: Option>>, - ) -> oxc_allocator::Vec<'a, oxc::Argument<'a>> { - let mut source_args = source_args.map(Vec::into_iter); - self.builder.vec_from_iter(args.iter().map(|arg| { - let source_arg = source_args.as_mut().and_then(Iterator::next); - self.convert_argument_with_source(arg, source_arg) - })) - } - - fn convert_argument_with_source( - &self, - arg: &Expression, - source_arg: Option>, - ) -> oxc::Argument<'a> { - match arg { - Expression::SpreadElement(s) => { - let converted = self.convert_expression(&s.argument); - let converted = match source_arg { - Some(oxc::Argument::SpreadElement(source_spread)) => { - match self.wrap_expression_with_source_ts_expr( - source_spread.unbox().argument, - converted, - ) { - Ok(wrapped) => wrapped, - Err(converted) => converted, - } - } - Some(source_arg) => { - match self.wrap_expression_with_source_argument_ts(source_arg, converted) { - Ok(wrapped) => wrapped, - Err(converted) => converted, - } - } - None => converted, - }; - self.builder.argument_spread_element(SPAN, converted) - } - _ => { - let converted = self.convert_expression(arg); - let converted = match source_arg { - Some(source_arg) => { - match self.wrap_expression_with_source_argument_ts(source_arg, converted) { - Ok(wrapped) => wrapped, - Err(converted) => converted, - } - } - None => converted, - }; - oxc::Argument::from(converted) - } - } - } - - fn convert_array_element(&self, elem: &Option) -> oxc::ArrayExpressionElement<'a> { - match elem { - None => self.builder.array_expression_element_elision(SPAN), - Some(Expression::SpreadElement(s)) => { - self.builder.array_expression_element_spread_element( - SPAN, - self.convert_expression(&s.argument), - ) - } - Some(e) => oxc::ArrayExpressionElement::from(self.convert_expression(e)), - } - } - - fn convert_object_expression_property( - &self, - prop: &ObjectExpressionProperty, - ) -> oxc::ObjectPropertyKind<'a> { - match prop { - ObjectExpressionProperty::ObjectProperty(p) => { - let key = self.convert_expression_to_property_key(&p.key, p.computed); - let value = self.convert_expression(&p.value); - let value = match self.extract_source_object_property_value(&p.base) { - Some(source_value) => { - match self.wrap_expression_with_source_ts_expr(source_value, value) { - Ok(wrapped) => wrapped, - Err(value) => value, - } - } - None => value, - }; - let method = p.method.unwrap_or(false); - let obj_prop = self.builder.object_property( - SPAN, - oxc::PropertyKind::Init, - key, - value, - method, - p.shorthand, - p.computed, - ); - oxc::ObjectPropertyKind::ObjectProperty(self.builder.alloc(obj_prop)) - } - ObjectExpressionProperty::ObjectMethod(m) => { - let kind = match m.kind { - ObjectMethodKind::Method => oxc::PropertyKind::Init, - ObjectMethodKind::Get => oxc::PropertyKind::Get, - ObjectMethodKind::Set => oxc::PropertyKind::Set, - }; - let key = self.convert_expression_to_property_key(&m.key, m.computed); - let func = self.convert_object_method_to_function(m); - let func_expr = oxc::Expression::FunctionExpression(self.builder.alloc(func)); - let obj_prop = self.builder.object_property( - SPAN, kind, key, func_expr, m.method, false, // shorthand - m.computed, - ); - oxc::ObjectPropertyKind::ObjectProperty(self.builder.alloc(obj_prop)) - } - ObjectExpressionProperty::SpreadElement(s) => { - let spread = - self.builder.spread_element(SPAN, self.convert_expression(&s.argument)); - oxc::ObjectPropertyKind::SpreadProperty(self.builder.alloc(spread)) - } - } - } - - fn convert_expression_to_property_key( - &self, - expr: &Expression, - computed: bool, - ) -> oxc::PropertyKey<'a> { - // A computed key (`{ [expr]: … }`) is an arbitrary expression evaluated at - // runtime, so its identifiers are *references* — `{ [CONST]: x }` reads the - // variable `CONST`. Build it from the expression so semantic analysis links - // those references; otherwise an imported `CONST` looks unused and import - // elision drops it, leaving the emitted `[CONST]` dangling. Only a - // non-computed key is a static property name or literal. - if computed { - return oxc::PropertyKey::from(self.convert_expression(expr)); - } - match expr { - Expression::Identifier(id) => { - self.builder.property_key_static_identifier(SPAN, self.atom(&id.name)) - } - Expression::StringLiteral(s) => { - let lit = - self.builder.string_literal(SPAN, self.atom(&s.value.to_string_lossy()), None); - oxc::PropertyKey::StringLiteral(self.builder.alloc(lit)) - } - Expression::NumericLiteral(n) => { - let lit = - self.builder.numeric_literal(SPAN, n.value, None, oxc::NumberBase::Decimal); - oxc::PropertyKey::NumericLiteral(self.builder.alloc(lit)) - } - Expression::PrivateName(p) => { - self.builder.property_key_private_identifier(SPAN, self.atom(&p.id.name)) - } - _ => oxc::PropertyKey::from(self.convert_expression(expr)), - } - } - - fn convert_template_literal( - &self, - tl: &react_compiler_ast::expressions::TemplateLiteral, - ) -> oxc::TemplateLiteral<'a> { - let quasis = self.builder.vec_from_iter(tl.quasis.iter().map(|q| { - let raw = self.atom(&q.value.raw).into(); - let cooked = q.value.cooked.as_ref().map(|c| self.atom(c).into()); - let value = oxc::TemplateElementValue { raw, cooked }; - self.builder.template_element(SPAN, value, q.tail) - })); - let expressions = - self.builder.vec_from_iter(tl.expressions.iter().map(|e| self.convert_expression(e))); - self.builder.template_literal(SPAN, quasis, expressions) - } - - // ===== Functions ===== - - fn convert_function_decl( - &self, - f: &FunctionDeclaration, - fn_type: oxc::FunctionType, - ) -> oxc::Function<'a> { - let id = f.id.as_ref().map(|id| self.builder.binding_identifier(SPAN, self.atom(&id.name))); - let params = self.convert_params_to_formal_parameters(&f.params); - let body = self.convert_block_to_function_body(&f.body); - let mut func = self.builder.function( - SPAN, - fn_type, - id, - f.generator, - f.is_async, - f.declare.unwrap_or(false), - None::>>, - None::>>, - params, - None::>>, - Some(body), - ); - if !self.block_initializes_react_cache(&f.body) - && let Some(source_func) = self.extract_source_function_declaration(&f.base) - { - let source_has_no_body = source_func.body.is_none(); - self.apply_function_signature_from_source(&mut func, source_func); - if source_has_no_body { - func.body = None; - } - } - func - } - - fn convert_class_declaration(&self, c: &ClassDeclaration) -> oxc::Class<'a> { - let id = c.id.as_ref().map(|id| self.builder.binding_identifier(SPAN, self.atom(&id.name))); - let super_class = c.super_class.as_ref().map(|s| self.convert_expression(s)); - let body = self.builder.class_body(SPAN, self.builder.vec()); - self.builder.class( - SPAN, - oxc::ClassType::ClassDeclaration, - self.builder.vec(), // decorators - id, - None::>>, - super_class, - None::>>, - self.builder.vec(), // implements - body, - c.is_abstract.unwrap_or(false), - c.declare.unwrap_or(false), - ) - } - - fn convert_class_to_oxc( - &self, - c: &react_compiler_ast::expressions::ClassExpression, - class_type: oxc::ClassType, - ) -> oxc::Class<'a> { - let id = c.id.as_ref().map(|id| self.builder.binding_identifier(SPAN, self.atom(&id.name))); - let super_class = c.super_class.as_ref().map(|s| self.convert_expression(s)); - let body = self.builder.class_body(SPAN, self.builder.vec()); - self.builder.class( - SPAN, - class_type, - self.builder.vec(), // decorators - id, - None::>>, - super_class, - None::>>, - self.builder.vec(), // implements - body, - false, // is_abstract - false, // declare - ) - } - - fn convert_function_expr(&self, f: &FunctionExpression) -> oxc::Function<'a> { - let id = f.id.as_ref().map(|id| self.builder.binding_identifier(SPAN, self.atom(&id.name))); - let params = self.convert_params_to_formal_parameters(&f.params); - let body = self.convert_block_to_function_body(&f.body); - let initializes_react_cache = self.block_initializes_react_cache(&f.body); - let return_type = if initializes_react_cache { - None - } else { - f.return_type - .as_ref() - .and_then(|value| self.convert_ts_type_annotation_from_json(&value.parse_value())) - }; - let mut func = self.builder.function( - SPAN, - oxc::FunctionType::FunctionExpression, - id, - f.generator, - f.is_async, - false, - None::>>, - None::>>, - params, - return_type, - Some(body), - ); - if !initializes_react_cache - && let Some(source_func) = self.extract_source_function_expression(&f.base) - { - self.apply_function_signature_from_source(&mut func, source_func); - } - func - } - - fn convert_object_method_to_function(&self, m: &ObjectMethod) -> oxc::Function<'a> { - let params = self.convert_params_to_formal_parameters(&m.params); - let body = self.convert_block_to_function_body(&m.body); - let initializes_react_cache = self.block_initializes_react_cache(&m.body); - let return_type = if initializes_react_cache { - None - } else { - m.return_type - .as_ref() - .and_then(|value| self.convert_ts_type_annotation_from_json(&value.parse_value())) - }; - let mut func = self.builder.function( - SPAN, - oxc::FunctionType::FunctionExpression, - None, - m.generator, - m.is_async, - false, - None::>>, - None::>>, - params, - return_type, - Some(body), - ); - if !initializes_react_cache - && let Some(source_func) = self.extract_source_object_method_function(&m.base) - { - self.apply_function_signature_from_source(&mut func, source_func); - } - func - } - - fn convert_arrow_function(&self, arrow: &ArrowFunctionExpression) -> oxc::Expression<'a> { - let is_expression = arrow.expression.unwrap_or(false); - let params = self.convert_params_to_formal_parameters(&arrow.params); - let arrow_initializes_react_cache = match &*arrow.body { - ArrowFunctionBody::BlockStatement(block) => self.block_initializes_react_cache(block), - ArrowFunctionBody::Expression(_) => false, - }; - - let body = match &*arrow.body { - ArrowFunctionBody::BlockStatement(block) => self.convert_block_to_function_body(block), - ArrowFunctionBody::Expression(expr) => { - let oxc_expr = self.convert_expression(expr); - let stmt = self.builder.statement_expression(SPAN, oxc_expr); - let stmts = self.builder.vec_from_iter(std::iter::once(stmt)); - self.builder.function_body(SPAN, self.builder.vec(), stmts) - } - }; - - let mut arrow_expr = self.builder.arrow_function_expression( - SPAN, - is_expression, - arrow.is_async, - None::>>, - params, - if arrow_initializes_react_cache { - None - } else { - arrow.return_type.as_ref().and_then(|value| { - self.convert_ts_type_annotation_from_json(&value.parse_value()) - }) - }, - body, - ); - if !arrow_initializes_react_cache - && let Some(source_arrow) = self.extract_source_arrow_function(&arrow.base) - { - self.apply_arrow_signature_from_source(&mut arrow_expr, source_arrow); - } - oxc::Expression::ArrowFunctionExpression(self.builder.alloc(arrow_expr)) - } - - fn convert_block_to_function_body(&self, block: &BlockStatement) -> oxc::FunctionBody<'a> { - let stmts = self.convert_statement_vec(&block.body); - let directives = self.convert_directives(&block.directives); - self.builder.function_body(SPAN, directives, stmts) - } - - fn convert_params_to_formal_parameters( - &self, - params: &[PatternLike], - ) -> oxc::FormalParameters<'a> { - let mut items: Vec> = Vec::new(); - let mut rest: Option> = None; - - for param in params { - match param { - PatternLike::RestElement(r) => { - let arg = self.convert_pattern_to_binding_pattern(&r.argument); - let rest_elem = self.builder.binding_rest_element(SPAN, arg); - let type_annotation = r.type_annotation.as_ref().and_then(|value| { - self.convert_ts_type_annotation_from_json(&value.parse_value()) - }); - rest = Some(self.builder.formal_parameter_rest( - SPAN, - self.builder.vec(), - rest_elem, - type_annotation, - )); - } - PatternLike::AssignmentPattern(ap) => { - // OXC stores default parameter values in FormalParameter.initializer - // rather than using BindingPattern::AssignmentPattern (which OXC considers - // invalid in FormalParameter position). - let left = self.convert_pattern_to_binding_pattern(&ap.left); - let right = self.convert_expression(&ap.right); - let initializer = Some(oxc_allocator::Box::new_in(right, self.allocator)); - let type_annotation = self - .pattern_type_annotation(param) - .or_else(|| self.pattern_type_annotation(&ap.left)); - let optional = self.pattern_optional(param) || self.pattern_optional(&ap.left); - let fp = self.builder.formal_parameter( - SPAN, - self.builder.vec(), // decorators - left, - type_annotation, - initializer, - optional, - None, // accessibility - false, // readonly - false, // override - ); - items.push(fp); - } - _ => { - let pattern = self.convert_pattern_to_binding_pattern(param); - let type_annotation = self.pattern_type_annotation(param); - let optional = self.pattern_optional(param); - let fp = self.builder.formal_parameter( - SPAN, - self.builder.vec(), // decorators - pattern, - type_annotation, - None::>>, - optional, - None, // accessibility - false, // readonly - false, // override - ); - items.push(fp); - } - } - } - - let items_vec = self.builder.vec_from_iter(items); - self.builder.formal_parameters( - SPAN, - oxc::FormalParameterKind::FormalParameter, - items_vec, - rest, - ) - } - - fn pattern_optional(&self, pattern: &PatternLike) -> bool { - match pattern { - PatternLike::Identifier(id) => id.optional.unwrap_or(false), - PatternLike::AssignmentPattern(assign) => self.pattern_optional(&assign.left), - PatternLike::RestElement(rest) => self.pattern_optional(&rest.argument), - PatternLike::ObjectPattern(_) - | PatternLike::ArrayPattern(_) - | PatternLike::MemberExpression(_) - | PatternLike::TSAsExpression(_) - | PatternLike::TSSatisfiesExpression(_) - | PatternLike::TSNonNullExpression(_) - | PatternLike::TSTypeAssertion(_) - | PatternLike::TypeCastExpression(_) => false, - } - } - - fn pattern_type_annotation( - &self, - pattern: &PatternLike, - ) -> Option>> { - let value = match pattern { - PatternLike::Identifier(id) => id.type_annotation.as_ref(), - PatternLike::ObjectPattern(obj) => obj.type_annotation.as_ref(), - PatternLike::ArrayPattern(arr) => arr.type_annotation.as_ref(), - PatternLike::AssignmentPattern(assign) => assign.type_annotation.as_ref(), - PatternLike::RestElement(rest) => rest.type_annotation.as_ref(), - PatternLike::MemberExpression(_) - | PatternLike::TSAsExpression(_) - | PatternLike::TSSatisfiesExpression(_) - | PatternLike::TSNonNullExpression(_) - | PatternLike::TSTypeAssertion(_) - | PatternLike::TypeCastExpression(_) => None, - }?; - self.convert_ts_type_annotation_from_json(&value.parse_value()) - } - - // ===== Patterns → BindingPattern ===== - - fn convert_pattern_to_binding_pattern(&self, pattern: &PatternLike) -> oxc::BindingPattern<'a> { - match pattern { - PatternLike::Identifier(id) => { - self.builder.binding_pattern_binding_identifier(SPAN, self.atom(&id.name)) - } - PatternLike::ObjectPattern(obj) => { - let mut properties: Vec> = Vec::new(); - let mut rest: Option> = None; - - for prop in &obj.properties { - match prop { - ObjectPatternProperty::ObjectProperty(p) => { - let key = self.convert_expression_to_property_key(&p.key, p.computed); - let value = self.convert_pattern_to_binding_pattern(&p.value); - let bp = self.builder.binding_property( - SPAN, - key, - value, - p.shorthand, - p.computed, - ); - properties.push(bp); - } - ObjectPatternProperty::RestElement(r) => { - let arg = self.convert_pattern_to_binding_pattern(&r.argument); - rest = Some(self.builder.binding_rest_element(SPAN, arg)); - } - } - } - - let props_vec = self.builder.vec_from_iter(properties); - self.builder.binding_pattern_object_pattern(SPAN, props_vec, rest) - } - PatternLike::ArrayPattern(arr) => { - let mut elements: Vec>> = Vec::new(); - let mut rest: Option> = None; - - for elem in &arr.elements { - match elem { - None => elements.push(None), - Some(PatternLike::RestElement(r)) => { - let arg = self.convert_pattern_to_binding_pattern(&r.argument); - rest = Some(self.builder.binding_rest_element(SPAN, arg)); - } - Some(p) => { - elements.push(Some(self.convert_pattern_to_binding_pattern(p))); - } - } - } - - let elems_vec = self.builder.vec_from_iter(elements); - self.builder.binding_pattern_array_pattern(SPAN, elems_vec, rest) - } - PatternLike::AssignmentPattern(ap) => { - let left = self.convert_pattern_to_binding_pattern(&ap.left); - let right = self.convert_expression(&ap.right); - self.builder.binding_pattern_assignment_pattern(SPAN, left, right) - } - PatternLike::RestElement(r) => self.convert_pattern_to_binding_pattern(&r.argument), - PatternLike::MemberExpression(_) - | PatternLike::TSAsExpression(_) - | PatternLike::TSSatisfiesExpression(_) - | PatternLike::TSNonNullExpression(_) - | PatternLike::TSTypeAssertion(_) - | PatternLike::TypeCastExpression(_) => self - .builder - .binding_pattern_binding_identifier(SPAN, self.atom("__member_pattern__")), - } - } - - // ===== Patterns → AssignmentTarget ===== - - fn convert_pattern_to_assignment_target( - &self, - pattern: &PatternLike, - ) -> oxc::AssignmentTarget<'a> { - match pattern { - PatternLike::Identifier(id) => self - .builder - .simple_assignment_target_assignment_target_identifier(SPAN, self.atom(&id.name)) - .into(), - PatternLike::MemberExpression(m) => { - let object = self.convert_expression(&m.object); - if m.computed { - let property = self.convert_expression(&m.property); - let mem = - self.builder.computed_member_expression(SPAN, object, property, false); - oxc::AssignmentTarget::ComputedMemberExpression(self.builder.alloc(mem)) - } else { - let prop_name = self.expression_to_identifier_name(&m.property); - let mem = self.builder.static_member_expression(SPAN, object, prop_name, false); - oxc::AssignmentTarget::StaticMemberExpression(self.builder.alloc(mem)) - } - } - PatternLike::ObjectPattern(obj) => { - let mut properties: Vec> = Vec::new(); - let mut rest: Option> = None; - - for prop in &obj.properties { - match prop { - ObjectPatternProperty::ObjectProperty(p) => { - if p.shorthand { - // Shorthand: { x } means { x: x } - // Use AssignmentTargetPropertyIdentifier - if let Expression::Identifier(id) = &*p.key { - let binding = self - .builder - .identifier_reference(SPAN, self.atom(&id.name)); - let init = match &*p.value { - PatternLike::AssignmentPattern(ap) => { - Some(self.convert_expression(&ap.right)) - } - _ => None, - }; - let atp = self - .builder - .assignment_target_property_assignment_target_property_identifier( - SPAN, binding, init, - ); - properties.push(atp); - } else { - // Fallback to non-shorthand - let key = - self.convert_expression_to_property_key(&p.key, p.computed); - let binding = self - .convert_pattern_to_assignment_target_maybe_default( - &p.value, - ); - let atp = self - .builder - .assignment_target_property_assignment_target_property_property( - SPAN, key, binding, p.computed, - ); - properties.push(atp); - } - } else { - let key = - self.convert_expression_to_property_key(&p.key, p.computed); - let binding = self - .convert_pattern_to_assignment_target_maybe_default(&p.value); - let atp = self - .builder - .assignment_target_property_assignment_target_property_property( - SPAN, key, binding, p.computed, - ); - properties.push(atp); - } - } - ObjectPatternProperty::RestElement(r) => { - let target = self.convert_pattern_to_assignment_target(&r.argument); - rest = Some(self.builder.assignment_target_rest(SPAN, target)); - } - } - } - - let props_vec = self.builder.vec_from_iter(properties); - self.builder - .assignment_target_pattern_object_assignment_target(SPAN, props_vec, rest) - .into() - } - PatternLike::ArrayPattern(arr) => { - let mut elements: Vec>> = Vec::new(); - let mut rest: Option> = None; - - for elem in &arr.elements { - match elem { - None => elements.push(None), - Some(PatternLike::RestElement(r)) => { - let target = self.convert_pattern_to_assignment_target(&r.argument); - rest = Some(self.builder.assignment_target_rest(SPAN, target)); - } - Some(p) => { - elements.push(Some( - self.convert_pattern_to_assignment_target_maybe_default(p), - )); - } - } - } - - let elems_vec = self.builder.vec_from_iter(elements); - self.builder - .assignment_target_pattern_array_assignment_target(SPAN, elems_vec, rest) - .into() - } - PatternLike::AssignmentPattern(ap) => { - // For assignment LHS, use the left side - self.convert_pattern_to_assignment_target(&ap.left) - } - PatternLike::RestElement(r) => self.convert_pattern_to_assignment_target(&r.argument), - PatternLike::TSAsExpression(e) => { - self.convert_ts_as_expression_to_simple_assignment_target(e).into() - } - PatternLike::TSSatisfiesExpression(e) => { - self.convert_ts_satisfies_expression_to_simple_assignment_target(e).into() - } - PatternLike::TSNonNullExpression(e) => { - self.convert_ts_non_null_expression_to_simple_assignment_target(e).into() - } - PatternLike::TSTypeAssertion(e) => { - self.convert_ts_type_assertion_to_simple_assignment_target(e).into() - } - PatternLike::TypeCastExpression(_) => self - .builder - .simple_assignment_target_assignment_target_identifier( - SPAN, - self.atom("__unknown__"), - ) - .into(), - } - } - - fn convert_pattern_to_assignment_target_maybe_default( - &self, - pattern: &PatternLike, - ) -> oxc::AssignmentTargetMaybeDefault<'a> { - match pattern { - PatternLike::AssignmentPattern(ap) => { - let binding = self.convert_pattern_to_assignment_target(&ap.left); - let init = self.convert_expression(&ap.right); - self.builder.assignment_target_maybe_default_assignment_target_with_default( - SPAN, binding, init, - ) - } - _ => { - let target = self.convert_pattern_to_assignment_target(pattern); - oxc::AssignmentTargetMaybeDefault::from(target) - } - } - } - - fn convert_expression_to_simple_assignment_target( - &self, - expr: &Expression, - ) -> oxc::SimpleAssignmentTarget<'a> { - match expr { - Expression::Identifier(id) => self - .builder - .simple_assignment_target_assignment_target_identifier(SPAN, self.atom(&id.name)), - Expression::MemberExpression(m) => { - let object = self.convert_expression(&m.object); - if m.computed { - let property = self.convert_expression(&m.property); - let mem = - self.builder.computed_member_expression(SPAN, object, property, false); - oxc::SimpleAssignmentTarget::ComputedMemberExpression(self.builder.alloc(mem)) - } else { - let prop_name = self.expression_to_identifier_name(&m.property); - let mem = self.builder.static_member_expression(SPAN, object, prop_name, false); - oxc::SimpleAssignmentTarget::StaticMemberExpression(self.builder.alloc(mem)) - } - } - Expression::TSAsExpression(e) => { - self.convert_ts_as_expression_to_simple_assignment_target(e) - } - Expression::TSSatisfiesExpression(e) => { - self.convert_ts_satisfies_expression_to_simple_assignment_target(e) - } - Expression::TSNonNullExpression(e) => { - self.convert_ts_non_null_expression_to_simple_assignment_target(e) - } - Expression::TSTypeAssertion(e) => { - self.convert_ts_type_assertion_to_simple_assignment_target(e) - } - _ => self.builder.simple_assignment_target_assignment_target_identifier( - SPAN, - self.atom("__unknown__"), - ), - } - } - - fn convert_ts_as_expression_to_simple_assignment_target( - &self, - expr: &TSAsExpression, - ) -> oxc::SimpleAssignmentTarget<'a> { - let expression = self.convert_expression(&expr.expression); - let parsed_type = expr.type_annotation.parse_value(); - if let Some(type_annotation) = self.convert_ts_type_from_json(&parsed_type).or_else(|| { - if Self::ts_type_json_contains_type_query(&parsed_type) { - None - } else { - self.extract_source_ts_as_type(&expr.base) - } - }) { - self.builder.simple_assignment_target_ts_as_expression( - SPAN, - expression, - type_annotation, - ) - } else { - self.convert_expression_to_simple_assignment_target(&expr.expression) - } - } - - fn convert_ts_satisfies_expression_to_simple_assignment_target( - &self, - expr: &TSSatisfiesExpression, - ) -> oxc::SimpleAssignmentTarget<'a> { - let expression = self.convert_expression(&expr.expression); - let parsed_type = expr.type_annotation.parse_value(); - if let Some(type_annotation) = self.convert_ts_type_from_json(&parsed_type).or_else(|| { - if Self::ts_type_json_contains_type_query(&parsed_type) { - None - } else { - self.extract_source_ts_satisfies_type(&expr.base) - } - }) { - self.builder.simple_assignment_target_ts_satisfies_expression( - SPAN, - expression, - type_annotation, - ) - } else { - self.convert_expression_to_simple_assignment_target(&expr.expression) - } - } - - fn convert_ts_non_null_expression_to_simple_assignment_target( - &self, - expr: &TSNonNullExpression, - ) -> oxc::SimpleAssignmentTarget<'a> { - let expression = self.convert_expression(&expr.expression); - self.builder.simple_assignment_target_ts_non_null_expression(SPAN, expression) - } - - fn convert_ts_type_assertion_to_simple_assignment_target( - &self, - expr: &TSTypeAssertion, - ) -> oxc::SimpleAssignmentTarget<'a> { - let expression = self.convert_expression(&expr.expression); - let parsed_type = expr.type_annotation.parse_value(); - if let Some(type_annotation) = self.convert_ts_type_from_json(&parsed_type).or_else(|| { - if Self::ts_type_json_contains_type_query(&parsed_type) { - None - } else { - self.extract_source_ts_type_assertion_type(&expr.base) - } - }) { - self.builder.simple_assignment_target_ts_type_assertion( - SPAN, - type_annotation, - expression, - ) - } else { - self.convert_expression_to_simple_assignment_target(&expr.expression) - } - } - - // ===== JSX ===== - - fn convert_jsx_element(&self, el: &JSXElement) -> oxc::JSXElement<'a> { - let opening = self.convert_jsx_opening_element(&el.opening_element, Some(&el.base)); - let children = - self.builder.vec_from_iter(el.children.iter().map(|c| self.convert_jsx_child(c))); - let closing = el.closing_element.as_ref().map(|c| self.convert_jsx_closing_element(c)); - self.builder.jsx_element(SPAN, opening, children, closing) - } - - fn convert_jsx_opening_element( - &self, - el: &JSXOpeningElement, - element_base: Option<&BaseNode>, - ) -> oxc::JSXOpeningElement<'a> { - let name = self.convert_jsx_element_name(&el.name); - let attrs = self - .builder - .vec_from_iter(el.attributes.iter().map(|a| self.convert_jsx_attribute_item(a))); - let type_arguments = self - .extract_source_jsx_type_arguments(&el.base) - .or_else(|| element_base.and_then(|base| self.extract_source_jsx_type_arguments(base))); - self.builder.jsx_opening_element(SPAN, name, type_arguments, attrs) - } - - fn convert_jsx_closing_element(&self, el: &JSXClosingElement) -> oxc::JSXClosingElement<'a> { - let name = self.convert_jsx_element_name(&el.name); - self.builder.jsx_closing_element(SPAN, name) - } - - fn convert_jsx_element_name(&self, name: &JSXElementName) -> oxc::JSXElementName<'a> { - match name { - JSXElementName::JSXIdentifier(id) => { - let first_char = id.name.chars().next().unwrap_or('a'); - if first_char.is_uppercase() || id.name.contains('.') { - self.builder.jsx_element_name_identifier_reference(SPAN, self.atom(&id.name)) - } else { - self.builder.jsx_element_name_identifier(SPAN, self.atom(&id.name)) - } - } - JSXElementName::JSXMemberExpression(m) => { - let member = self.convert_jsx_member_expression(m); - self.builder.jsx_element_name_member_expression( - SPAN, - member.object, - member.property, - ) - } - JSXElementName::JSXNamespacedName(ns) => { - let namespace = self.builder.jsx_identifier(SPAN, self.atom(&ns.namespace.name)); - let name = self.builder.jsx_identifier(SPAN, self.atom(&ns.name.name)); - self.builder.jsx_element_name_namespaced_name(SPAN, namespace, name) - } - } - } - - fn convert_jsx_member_expression( - &self, - m: &JSXMemberExpression, - ) -> oxc::JSXMemberExpression<'a> { - let object = self.convert_jsx_member_expression_object(&m.object); - let property = self.builder.jsx_identifier(SPAN, self.atom(&m.property.name)); - self.builder.jsx_member_expression(SPAN, object, property) - } - - fn convert_jsx_member_expression_object( - &self, - obj: &JSXMemberExprObject, - ) -> oxc::JSXMemberExpressionObject<'a> { - match obj { - JSXMemberExprObject::JSXIdentifier(id) => self - .builder - .jsx_member_expression_object_identifier_reference(SPAN, self.atom(&id.name)), - JSXMemberExprObject::JSXMemberExpression(m) => { - let member = self.convert_jsx_member_expression(m); - self.builder.jsx_member_expression_object_member_expression( - SPAN, - member.object, - member.property, - ) - } - } - } - - fn convert_jsx_attribute_item(&self, item: &JSXAttributeItem) -> oxc::JSXAttributeItem<'a> { - match item { - JSXAttributeItem::JSXAttribute(attr) => { - let name = self.convert_jsx_attribute_name(&attr.name); - let value = attr.value.as_ref().map(|v| self.convert_jsx_attribute_value(v)); - self.builder.jsx_attribute_item_attribute(SPAN, name, value) - } - JSXAttributeItem::JSXSpreadAttribute(s) => self - .builder - .jsx_attribute_item_spread_attribute(SPAN, self.convert_expression(&s.argument)), - } - } - - fn convert_jsx_attribute_name(&self, name: &JSXAttributeName) -> oxc::JSXAttributeName<'a> { - match name { - JSXAttributeName::JSXIdentifier(id) => { - self.builder.jsx_attribute_name_identifier(SPAN, self.atom(&id.name)) - } - JSXAttributeName::JSXNamespacedName(ns) => { - let namespace = self.builder.jsx_identifier(SPAN, self.atom(&ns.namespace.name)); - let name = self.builder.jsx_identifier(SPAN, self.atom(&ns.name.name)); - self.builder.jsx_attribute_name_namespaced_name(SPAN, namespace, name) - } - } - } - - fn convert_jsx_attribute_value(&self, value: &JSXAttributeValue) -> oxc::JSXAttributeValue<'a> { - match value { - JSXAttributeValue::StringLiteral(s) => self.builder.jsx_attribute_value_string_literal( - SPAN, - self.atom(&s.value.to_string_lossy()), - None, - ), - JSXAttributeValue::JSXExpressionContainer(ec) => { - let expr = self.convert_jsx_expression_container_expr(&ec.expression); - self.builder.jsx_attribute_value_expression_container(SPAN, expr) - } - JSXAttributeValue::JSXElement(el) => { - let element = self.convert_jsx_element(el); - let opening = element.opening_element; - let closing = element.closing_element; - self.builder.jsx_attribute_value_element(SPAN, opening, element.children, closing) - } - JSXAttributeValue::JSXFragment(frag) => { - let fragment = self.convert_jsx_fragment(frag); - self.builder.jsx_attribute_value_fragment( - SPAN, - fragment.opening_fragment, - fragment.children, - fragment.closing_fragment, - ) - } - } - } - - fn convert_jsx_expression_container_expr( - &self, - expr: &JSXExpressionContainerExpr, - ) -> oxc::JSXExpression<'a> { - match expr { - JSXExpressionContainerExpr::JSXEmptyExpression(_) => { - self.builder.jsx_expression_empty_expression(SPAN) - } - JSXExpressionContainerExpr::Expression(e) => { - oxc::JSXExpression::from(self.convert_expression(e)) - } - } - } - - fn convert_jsx_child(&self, child: &JSXChild) -> oxc::JSXChild<'a> { - match child { - JSXChild::JSXText(t) => { - let value = encode_jsx_text(&t.value); - self.builder.jsx_child_text(SPAN, self.atom(&value), None) - } - JSXChild::JSXElement(el) => { - let element = self.convert_jsx_element(el); - let opening = element.opening_element; - let closing = element.closing_element; - self.builder.jsx_child_element(SPAN, opening, element.children, closing) - } - JSXChild::JSXFragment(frag) => { - let fragment = self.convert_jsx_fragment(frag); - self.builder.jsx_child_fragment( - SPAN, - fragment.opening_fragment, - fragment.children, - fragment.closing_fragment, - ) - } - JSXChild::JSXExpressionContainer(ec) => { - let expr = self.convert_jsx_expression_container_expr(&ec.expression); - self.builder.jsx_child_expression_container(SPAN, expr) - } - JSXChild::JSXSpreadChild(s) => { - self.builder.jsx_child_spread(SPAN, self.convert_expression(&s.expression)) - } - } - } - - fn convert_jsx_fragment(&self, frag: &JSXFragment) -> oxc::JSXFragment<'a> { - let opening = self.builder.jsx_opening_fragment(SPAN); - let closing = self.builder.jsx_closing_fragment(SPAN); - let children = - self.builder.vec_from_iter(frag.children.iter().map(|c| self.convert_jsx_child(c))); - self.builder.jsx_fragment(SPAN, opening, children, closing) - } - - // ===== Import/Export ===== - - fn convert_import_declaration(&self, decl: &ImportDeclaration) -> oxc::ImportDeclaration<'a> { - let specifiers = self - .builder - .vec_from_iter(decl.specifiers.iter().map(|s| self.convert_import_specifier(s))); - let specifiers = - if specifiers.is_empty() && !self.import_declaration_has_empty_named_specifiers(decl) { - None - } else { - Some(specifiers) - }; - let source = self.builder.string_literal( - SPAN, - self.atom(&decl.source.value.to_string_lossy()), - None, - ); - let import_kind = match decl.import_kind.as_ref() { - Some(ImportKind::Type) => oxc::ImportOrExportKind::Type, - _ => oxc::ImportOrExportKind::Value, - }; - let with_clause = - self.convert_with_clause(decl.attributes.as_deref().or(decl.assertions.as_deref())); - self.builder.import_declaration( - SPAN, - specifiers, - source, - None, // phase - with_clause, - import_kind, - ) - } - - fn convert_with_clause( - &self, - attributes: Option<&[ImportAttribute]>, - ) -> Option>> { - attributes.map(|attributes| { - let with_entries = self - .builder - .vec_from_iter(attributes.iter().map(|attr| self.convert_import_attribute(attr))); - self.builder.alloc_with_clause(SPAN, oxc::WithClauseKeyword::With, with_entries) - }) - } - - fn convert_import_attribute(&self, attr: &ImportAttribute) -> oxc::ImportAttribute<'a> { - let key_was_quoted = self - .source_text_for_base(&attr.key.base) - .is_some_and(|text| matches!(text.trim_start().as_bytes().first(), Some(b'"' | b'\''))); - let key = if key_was_quoted || !is_identifier_name(&attr.key.name) { - self.builder.import_attribute_key_string_literal(SPAN, self.atom(&attr.key.name), None) - } else { - self.builder.import_attribute_key_identifier(SPAN, self.atom(&attr.key.name)) - }; - let value = - self.builder.string_literal(SPAN, self.atom(&attr.value.value.to_string_lossy()), None); - self.builder.import_attribute(SPAN, key, value) - } - - fn convert_import_specifier( - &self, - spec: &react_compiler_ast::declarations::ImportSpecifier, - ) -> oxc::ImportDeclarationSpecifier<'a> { - match spec { - react_compiler_ast::declarations::ImportSpecifier::ImportSpecifier(s) => { - let local = self.builder.binding_identifier(SPAN, self.atom(&s.local.name)); - let imported = self.convert_module_export_name(&s.imported); - let import_kind = match s.import_kind.as_ref() { - Some(ImportKind::Type) => oxc::ImportOrExportKind::Type, - _ => oxc::ImportOrExportKind::Value, - }; - let is = self.builder.import_specifier(SPAN, imported, local, import_kind); - oxc::ImportDeclarationSpecifier::ImportSpecifier(self.builder.alloc(is)) - } - react_compiler_ast::declarations::ImportSpecifier::ImportDefaultSpecifier(s) => { - let local = self.builder.binding_identifier(SPAN, self.atom(&s.local.name)); - let ids = self.builder.import_default_specifier(SPAN, local); - oxc::ImportDeclarationSpecifier::ImportDefaultSpecifier(self.builder.alloc(ids)) - } - react_compiler_ast::declarations::ImportSpecifier::ImportNamespaceSpecifier(s) => { - let local = self.builder.binding_identifier(SPAN, self.atom(&s.local.name)); - let ins = self.builder.import_namespace_specifier(SPAN, local); - oxc::ImportDeclarationSpecifier::ImportNamespaceSpecifier(self.builder.alloc(ins)) - } - } - } - - fn convert_module_export_name( - &self, - name: &react_compiler_ast::declarations::ModuleExportName, - ) -> oxc::ModuleExportName<'a> { - match name { - react_compiler_ast::declarations::ModuleExportName::Identifier(id) => { - oxc::ModuleExportName::IdentifierName( - self.builder.identifier_name(SPAN, self.atom(&id.name)), - ) - } - react_compiler_ast::declarations::ModuleExportName::StringLiteral(s) => { - oxc::ModuleExportName::StringLiteral(self.builder.string_literal( - SPAN, - self.atom(&s.value.to_string_lossy()), - None, - )) - } - } - } - - /// Like [`Self::convert_module_export_name`], but builds an identifier `local` - /// of a local export specifier as an `IdentifierReference` (not a bare - /// `IdentifierName`) so semantic analysis links it to the exported binding. A - /// string-literal local is only valid with a `source`, so it falls back to the - /// plain name. - fn convert_module_export_name_local_ref( - &self, - name: &react_compiler_ast::declarations::ModuleExportName, - ) -> oxc::ModuleExportName<'a> { - match name { - react_compiler_ast::declarations::ModuleExportName::Identifier(id) => { - oxc::ModuleExportName::IdentifierReference( - self.builder.identifier_reference(SPAN, self.atom(&id.name)), - ) - } - react_compiler_ast::declarations::ModuleExportName::StringLiteral(_) => { - self.convert_module_export_name(name) - } - } - } - - fn convert_export_named_declaration( - &self, - decl: &ExportNamedDeclaration, - ) -> oxc::ExportNamedDeclaration<'a> { - let declaration = decl.declaration.as_ref().map(|d| self.convert_declaration(d)); - // For a local export (`export { x }`, no `source`) the specifier `local` - // refers to a binding in this module, so it must be an `IdentifierReference` - // for semantic analysis to link it (and thus keep its import alive through - // TypeScript import elision). Re-exports (`export { x } from`) keep an - // `IdentifierName`, since `local` names an export of the other module. This - // mirrors the parser (`parse_export_named_specifiers`). - let local_is_reference = decl.source.is_none(); - let specifiers = self.builder.vec_from_iter( - decl.specifiers.iter().map(|s| self.convert_export_specifier(s, local_is_reference)), - ); - let source = decl.source.as_ref().map(|s| { - self.builder.string_literal(SPAN, self.atom(&s.value.to_string_lossy()), None) - }); - let export_kind = match decl.export_kind.as_ref() { - Some(ExportKind::Type) => oxc::ImportOrExportKind::Type, - _ => oxc::ImportOrExportKind::Value, - }; - let with_clause = - self.convert_with_clause(decl.attributes.as_deref().or(decl.assertions.as_deref())); - self.builder.export_named_declaration( - SPAN, - declaration, - specifiers, - source, - export_kind, - with_clause, - ) - } - - fn convert_declaration(&self, decl: &Declaration) -> oxc::Declaration<'a> { - match decl { - Declaration::FunctionDeclaration(f) => { - let func = self.convert_function_decl(f, oxc::FunctionType::FunctionDeclaration); - oxc::Declaration::FunctionDeclaration(self.builder.alloc(func)) - } - Declaration::VariableDeclaration(v) => { - let d = self.convert_variable_declaration(v); - oxc::Declaration::VariableDeclaration(self.builder.alloc(d)) - } - Declaration::ClassDeclaration(c) => { - let class = self.convert_class_declaration(c); - oxc::Declaration::ClassDeclaration(self.builder.alloc(class)) - } - _ => { - let d = self.builder.variable_declaration( - SPAN, - oxc::VariableDeclarationKind::Const, - self.builder.vec(), - true, - ); - oxc::Declaration::VariableDeclaration(self.builder.alloc(d)) - } - } - } - - fn convert_export_specifier( - &self, - spec: &react_compiler_ast::declarations::ExportSpecifier, - local_is_reference: bool, - ) -> oxc::ExportSpecifier<'a> { - match spec { - react_compiler_ast::declarations::ExportSpecifier::ExportSpecifier(s) => { - let local = if local_is_reference { - self.convert_module_export_name_local_ref(&s.local) - } else { - self.convert_module_export_name(&s.local) - }; - let exported = self.convert_module_export_name(&s.exported); - let export_kind = match s.export_kind.as_ref() { - Some(ExportKind::Type) => oxc::ImportOrExportKind::Type, - _ => oxc::ImportOrExportKind::Value, - }; - self.builder.export_specifier(SPAN, local, exported, export_kind) - } - react_compiler_ast::declarations::ExportSpecifier::ExportDefaultSpecifier(s) => { - let name = oxc::ModuleExportName::IdentifierName( - self.builder.identifier_name(SPAN, self.atom(&s.exported.name)), - ); - let default_name = oxc::ModuleExportName::IdentifierName( - self.builder.identifier_name(SPAN, self.atom("default")), - ); - self.builder.export_specifier( - SPAN, - name, - default_name, - oxc::ImportOrExportKind::Value, - ) - } - react_compiler_ast::declarations::ExportSpecifier::ExportNamespaceSpecifier(s) => { - let exported = self.convert_module_export_name(&s.exported); - let star = oxc::ModuleExportName::IdentifierName( - self.builder.identifier_name(SPAN, self.atom("*")), - ); - self.builder.export_specifier(SPAN, star, exported, oxc::ImportOrExportKind::Value) - } - } - } - - fn convert_export_default_declaration( - &self, - decl: &ExportDefaultDeclaration, - ) -> oxc::ExportDefaultDeclaration<'a> { - let declaration = self.convert_export_default_decl(&decl.declaration); - self.builder.export_default_declaration(SPAN, declaration) - } - - fn convert_export_default_decl( - &self, - decl: &ExportDefaultDecl, - ) -> oxc::ExportDefaultDeclarationKind<'a> { - match decl { - ExportDefaultDecl::FunctionDeclaration(f) => { - let func = self.convert_function_decl(f, oxc::FunctionType::FunctionDeclaration); - oxc::ExportDefaultDeclarationKind::FunctionDeclaration(self.builder.alloc(func)) - } - ExportDefaultDecl::ClassDeclaration(c) => { - let class = self.convert_class_declaration(c); - oxc::ExportDefaultDeclarationKind::ClassDeclaration(self.builder.alloc(class)) - } - ExportDefaultDecl::EnumDeclaration(_) => { - // Flow enum declarations cannot be represented in OXC AST; - // emit a null placeholder to preserve the export shape. - oxc::ExportDefaultDeclarationKind::from( - self.builder.expression_null_literal(oxc::Span::default()), - ) - } - ExportDefaultDecl::Expression(e) => { - oxc::ExportDefaultDeclarationKind::from(self.convert_expression(e)) - } - } - } - - fn convert_export_all_declaration( - &self, - decl: &ExportAllDeclaration, - ) -> oxc::ExportAllDeclaration<'a> { - let source = self.builder.string_literal( - SPAN, - self.atom(&decl.source.value.to_string_lossy()), - None, - ); - let export_kind = match decl.export_kind.as_ref() { - Some(ExportKind::Type) => oxc::ImportOrExportKind::Type, - _ => oxc::ImportOrExportKind::Value, - }; - let with_clause = - self.convert_with_clause(decl.attributes.as_deref().or(decl.assertions.as_deref())); - self.builder.export_all_declaration( - SPAN, - None, // exported - source, - with_clause, - export_kind, - ) - } - - // ===== Operators ===== - - fn convert_binary_operator(&self, op: &BinaryOperator) -> oxc_syntax::operator::BinaryOperator { - use oxc_syntax::operator::BinaryOperator as OxcBinOp; - match op { - BinaryOperator::Add => OxcBinOp::Addition, - BinaryOperator::Sub => OxcBinOp::Subtraction, - BinaryOperator::Mul => OxcBinOp::Multiplication, - BinaryOperator::Div => OxcBinOp::Division, - BinaryOperator::Rem => OxcBinOp::Remainder, - BinaryOperator::Exp => OxcBinOp::Exponential, - BinaryOperator::Eq => OxcBinOp::Equality, - BinaryOperator::StrictEq => OxcBinOp::StrictEquality, - BinaryOperator::Neq => OxcBinOp::Inequality, - BinaryOperator::StrictNeq => OxcBinOp::StrictInequality, - BinaryOperator::Lt => OxcBinOp::LessThan, - BinaryOperator::Lte => OxcBinOp::LessEqualThan, - BinaryOperator::Gt => OxcBinOp::GreaterThan, - BinaryOperator::Gte => OxcBinOp::GreaterEqualThan, - BinaryOperator::Shl => OxcBinOp::ShiftLeft, - BinaryOperator::Shr => OxcBinOp::ShiftRight, - BinaryOperator::UShr => OxcBinOp::ShiftRightZeroFill, - BinaryOperator::BitOr => OxcBinOp::BitwiseOR, - BinaryOperator::BitXor => OxcBinOp::BitwiseXOR, - BinaryOperator::BitAnd => OxcBinOp::BitwiseAnd, - BinaryOperator::In => OxcBinOp::In, - BinaryOperator::Instanceof => OxcBinOp::Instanceof, - BinaryOperator::Pipeline => OxcBinOp::BitwiseOR, // no pipeline in OXC - } - } - - fn convert_logical_operator( - &self, - op: &LogicalOperator, - ) -> oxc_syntax::operator::LogicalOperator { - use oxc_syntax::operator::LogicalOperator as OxcLogOp; - match op { - LogicalOperator::Or => OxcLogOp::Or, - LogicalOperator::And => OxcLogOp::And, - LogicalOperator::NullishCoalescing => OxcLogOp::Coalesce, - } - } - - fn convert_unary_operator(&self, op: &UnaryOperator) -> oxc_syntax::operator::UnaryOperator { - use oxc_syntax::operator::UnaryOperator as OxcUnOp; - match op { - UnaryOperator::Neg => OxcUnOp::UnaryNegation, - UnaryOperator::Plus => OxcUnOp::UnaryPlus, - UnaryOperator::Not => OxcUnOp::LogicalNot, - UnaryOperator::BitNot => OxcUnOp::BitwiseNot, - UnaryOperator::TypeOf => OxcUnOp::Typeof, - UnaryOperator::Void => OxcUnOp::Void, - UnaryOperator::Delete => OxcUnOp::Delete, - UnaryOperator::Throw => OxcUnOp::Void, // no throw-as-unary in OXC - } - } - - fn convert_update_operator(&self, op: &UpdateOperator) -> oxc_syntax::operator::UpdateOperator { - use oxc_syntax::operator::UpdateOperator as OxcUpOp; - match op { - UpdateOperator::Increment => OxcUpOp::Increment, - UpdateOperator::Decrement => OxcUpOp::Decrement, - } - } - - fn convert_assignment_operator( - &self, - op: &AssignmentOperator, - ) -> oxc_syntax::operator::AssignmentOperator { - use oxc_syntax::operator::AssignmentOperator as OxcAssOp; - match op { - AssignmentOperator::Assign => OxcAssOp::Assign, - AssignmentOperator::AddAssign => OxcAssOp::Addition, - AssignmentOperator::SubAssign => OxcAssOp::Subtraction, - AssignmentOperator::MulAssign => OxcAssOp::Multiplication, - AssignmentOperator::DivAssign => OxcAssOp::Division, - AssignmentOperator::RemAssign => OxcAssOp::Remainder, - AssignmentOperator::ExpAssign => OxcAssOp::Exponential, - AssignmentOperator::ShlAssign => OxcAssOp::ShiftLeft, - AssignmentOperator::ShrAssign => OxcAssOp::ShiftRight, - AssignmentOperator::UShrAssign => OxcAssOp::ShiftRightZeroFill, - AssignmentOperator::BitOrAssign => OxcAssOp::BitwiseOR, - AssignmentOperator::BitXorAssign => OxcAssOp::BitwiseXOR, - AssignmentOperator::BitAndAssign => OxcAssOp::BitwiseAnd, - AssignmentOperator::OrAssign => OxcAssOp::LogicalOr, - AssignmentOperator::AndAssign => OxcAssOp::LogicalAnd, - AssignmentOperator::NullishAssign => OxcAssOp::LogicalNullish, - } - } - - fn parse_regexp_flags(&self, flags_str: &str) -> oxc::RegExpFlags { - let mut flags = oxc::RegExpFlags::empty(); - for ch in flags_str.chars() { - match ch { - 'd' => flags |= oxc::RegExpFlags::D, - 'g' => flags |= oxc::RegExpFlags::G, - 'i' => flags |= oxc::RegExpFlags::I, - 'm' => flags |= oxc::RegExpFlags::M, - 's' => flags |= oxc::RegExpFlags::S, - 'u' => flags |= oxc::RegExpFlags::U, - 'v' => flags |= oxc::RegExpFlags::V, - 'y' => flags |= oxc::RegExpFlags::Y, - _ => {} - } - } - flags - } -} diff --git a/crates/oxc_react_compiler/src/convert_scope.rs b/crates/oxc_react_compiler/src/convert_scope.rs index bec1f90dc214e..bf3f69ef73109 100644 --- a/crates/oxc_react_compiler/src/convert_scope.rs +++ b/crates/oxc_react_compiler/src/convert_scope.rs @@ -3,17 +3,17 @@ // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. +use crate::scope::*; use indexmap::IndexMap; use oxc_ast::AstKind; use oxc_ast::ast::Program; use oxc_semantic::Semantic; use oxc_span::GetSpan; use oxc_syntax::symbol::SymbolFlags; -use react_compiler_ast::scope::*; -use rustc_hash::{FxBuildHasher, FxHashMap}; +use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; /// `IndexMap` keyed with the deterministic Fx hasher, matching the `FxIndexMap` -/// used by `react_compiler_ast::scope` fields (`react_compiler_utils::FxIndexMap`). +/// used by `crate::scope` fields (`crate::react_compiler_utils::FxIndexMap`). type FxIndexMap = IndexMap; /// Convert OXC's semantic analysis into React Compiler's ScopeInfo. @@ -136,6 +136,46 @@ pub fn convert_scope_info(semantic: &Semantic, _program: &Program) -> ScopeInfo let reference = scoping.get_reference(ref_id); let ref_node = nodes.get_node(reference.node_id()); let start = ref_node.kind().span().start; + // The old Babel scope analysis did not record pure type-position + // references that live in a *variable-declarator* type annotation + // (`const v: T`), so they must not enter the scope stream (else they + // drive the hoisting scan to treat a type parameter as a hoistable + // "Unknown" binding and bail). Param/return annotations and + // `as`/`satisfies` casts ARE recorded by the old path, so only the + // declarator-annotation case is skipped. Walk to the structural host + // of the reference: the first non-type ancestor decides. + if reference.is_type() && !reference.is_value() { + let mut cur = reference.node_id(); + let skip = loop { + let parent = nodes.parent_id(cur); + if parent == cur { + break false; + } + match nodes.get_node(parent).kind() { + // Positions the old Babel scope analysis did NOT record: + // variable-declarator annotations (`const v: T`) and type + // arguments on calls/news (`foo.get()`, `new Foo()`). + AstKind::VariableDeclarator(_) + | AstKind::CallExpression(_) + | AstKind::NewExpression(_) + | AstKind::JSXOpeningElement(_) => break true, + // Positions it DID record: param/return annotations and + // `as`/`satisfies` casts. These are reached first when the + // ref is nested inside them, so they win over the skips. + AstKind::FormalParameter(_) + | AstKind::FormalParameters(_) + | AstKind::TSAsExpression(_) + | AstKind::TSSatisfiesExpression(_) + | AstKind::Function(_) + | AstKind::ArrowFunctionExpression(_) => break false, + _ => {} + } + cur = parent; + }; + if skip { + continue; + } + } ref_node_id_to_binding.insert(start, binding_id); } } @@ -212,6 +252,36 @@ pub fn convert_scope_info(semantic: &Semantic, _program: &Program) -> ScopeInfo let program_scope = ScopeId(scoping.root_scope_id().index() as u32); + // Build the child-scope adjacency once from the parent links so descendant + // queries don't rescan every scope per call. + let mut children: Vec> = vec![Vec::new(); scopes.len()]; + for (i, scope) in scopes.iter().enumerate() { + if let Some(parent) = scope.parent { + children[parent.0 as usize].push(ScopeId(i as u32)); + } + } + + // Candidate scopes for TS `this`-parameter validation: those declaring a + // `this` binding (usually none). Kept in `node_to_scope` iteration order so + // the validation visits them in the same order as before. + let this_binding_scopes: Vec<(u32, ScopeId)> = node_to_scope + .iter() + .filter(|(_, sid)| { + scopes.get(sid.0 as usize).is_some_and(|s| s.bindings.contains_key("this")) + }) + .map(|(&start, &sid)| (start, sid)) + .collect(); + + // Reference node-ids that are actually a binding's own declaration site. + // Program-wide, so build once here instead of per function in lowering. + let declaration_node_ids: FxHashSet<(BindingId, u32)> = ref_node_id_to_binding + .iter() + .filter_map(|(&ref_nid, &binding_id)| { + let binding = bindings.get(binding_id.0 as usize)?; + (binding.declaration_node_id == Some(ref_nid)).then_some((binding_id, ref_nid)) + }) + .collect(); + ScopeInfo { scopes, bindings, @@ -221,6 +291,9 @@ pub fn convert_scope_info(semantic: &Semantic, _program: &Program) -> ScopeInfo ref_node_id_to_binding, node_id_to_scope, program_scope, + children, + this_binding_scopes, + declaration_node_ids, } } diff --git a/crates/oxc_react_compiler/src/diagnostics.rs b/crates/oxc_react_compiler/src/diagnostics.rs index f73ecf073145e..02b79a02037dc 100644 --- a/crates/oxc_react_compiler/src/diagnostics.rs +++ b/crates/oxc_react_compiler/src/diagnostics.rs @@ -6,7 +6,7 @@ use oxc_diagnostics::{Diagnostics, LabeledSpan, OxcDiagnostic}; use oxc_span::Span; -use react_compiler::entrypoint::compile_result::{ +use crate::react_compiler::entrypoint::compile_result::{ CompileResult, CompilerErrorDetailInfo, CompilerErrorInfo, LoggerEvent, LoggerSourceLocation, }; diff --git a/crates/oxc_react_compiler/src/lib.rs b/crates/oxc_react_compiler/src/lib.rs index d539dc2748427..3ea0c7e79e7b7 100644 --- a/crates/oxc_react_compiler/src/lib.rs +++ b/crates/oxc_react_compiler/src/lib.rs @@ -1,20 +1,49 @@ -pub mod convert_ast; -pub mod convert_ast_reverse; +// React Compiler core, vendored from the upstream multi-crate workspace and +// collapsed into modules of this crate. The former cross-crate `react_compiler_x::` +// paths are now `crate::react_compiler_x::`. These modules are frontend-agnostic +// (no oxc deps); the oxc <-> AST/scope conversion lives in the modules below. +// +// The vendored source is kept byte-for-byte with upstream to ease re-syncing, so +// clippy is relaxed on these modules here rather than by editing the code. The +// hand-written conversion modules (`convert_*`, `prefilter`, ...) stay fully linted. +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_diagnostics; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_hir; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_inference; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_lowering; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_optimization; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_reactive_scopes; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_ssa; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_typeinference; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_utils; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::disallowed_methods)] +pub mod react_compiler_validation; + pub mod convert_scope; pub mod diagnostics; pub mod prefilter; +pub mod scope; -use convert_ast::convert_program; +use crate::react_compiler::entrypoint::compile_result::LoggerEvent; use convert_scope::convert_scope_info; use diagnostics::compile_result_to_diagnostics; use prefilter::{has_react_like_functions, has_resource_management_declarations}; -use react_compiler::entrypoint::compile_result::LoggerEvent; // Re-exported so integrations needn't depend on the upstream `react_compiler` crates. -pub use react_compiler::entrypoint::plugin_options::{ +pub use crate::react_compiler::entrypoint::plugin_options::{ CompilerTarget, DynamicGatingConfig, GatingConfig, PluginOptions, }; -pub use react_compiler_hir::environment_config::EnvironmentConfig; +pub use crate::react_compiler_hir::environment_config::EnvironmentConfig; use rustc_hash::FxHashSet; @@ -45,9 +74,11 @@ pub fn default_plugin_options() -> PluginOptions { } #[derive(Default)] -pub struct TransformResult<'a> { - /// Compiled, ready-to-codegen OXC AST; `None` if the compiler made no changes. - pub program: Option>, +pub struct TransformResult { + /// `true` if the compiler memoized at least one function, in which case the + /// caller-owned `&mut Program` was mutated in place; `false` if no changes + /// were made. + pub changed: bool, /// Errors and warnings produced by the compile. Errors (e.g. Rules of Hooks /// violations) are hard problems in the source; the program is still left /// valid. Warnings include bail-outs where the compiler declined to optimize. @@ -64,15 +95,16 @@ pub struct LintResult { } /// Run the React Compiler on a pre-parsed program, building the semantic model -/// internally and returning the result. `program` in the result is `None` when -/// nothing was compiled (no React-like functions, a bail-out, or no changes). +/// internally. When at least one function is memoized the compiled functions are +/// spliced into `program` in place and `result.changed` is `true`; otherwise the +/// program is left untouched (no React-like functions, a bail-out, or no changes). /// /// Must run **first**, on the pristine AST, before any other transform. pub fn transform<'a>( - program: &oxc_ast::ast::Program<'a>, + program: &mut oxc_ast::ast::Program<'a>, allocator: &'a oxc_allocator::Allocator, options: PluginOptions, -) -> TransformResult<'a> { +) -> TransformResult { let source_text = program.source_text; // Skip files with no React-like functions, unless the mode compiles everything. @@ -87,48 +119,89 @@ pub fn transform<'a>( return TransformResult::default(); } - let semantic = - oxc_semantic::SemanticBuilder::new().with_build_nodes(true).build(program).semantic; + // The HIR lowering computes `SourceLocation` line/column from a line-offset + // table built off `context.code` (see `pipeline::compile_fn`). In the + // original Babel front-end the locations were carried on the AST nodes + // (`base.loc`) computed from the source during `convert_ast`; the oxc + // front-end instead derives them on demand from the source text, so the + // source must be threaded through. Without it, every loc collapses to + // `line = 1, column = byte_offset`, which surfaces as wrong `(line:col)` + // suffixes in diagnostics. + // + // This clones the whole source, so it must come *after* the early-bail + // checks above — otherwise every skipped file (no React functions) pays a + // full-source copy for nothing (a large per-file regression on big files + // like `binder.ts` that compile to nothing). + let mut options = options; + if options.source_code.is_none() { + options.source_code = Some(source_text.to_string()); + } - let file = convert_program(program, source_text); - let scope_info = convert_scope_info(&semantic, program); - let result = react_compiler::entrypoint::program::compile_program(file, scope_info, options); + // Build the semantic model and scope info in a scoped block so the immutable + // borrow of `program` (held by `semantic`) is released before the in-place + // splice needs `&mut program`. + let scope_info = { + let semantic = + oxc_semantic::SemanticBuilder::new().with_build_nodes(true).build(&*program).semantic; + convert_scope_info(&semantic, &*program) + }; + + // The back-end splices compiled oxc functions into `program` in place (see + // `codegen_function` / `ox_splice_program`). Thread the arena's `AstBuilder` + // and the program in. Function discovery and lowering walk it via reborrow. + let ast_builder = oxc_ast::AstBuilder::new(allocator); + let output = crate::react_compiler::entrypoint::program::compile_program( + &ast_builder, + &*program, + scope_info, + options, + ); + let result = match output { + crate::react_compiler::entrypoint::program::CompileOutput::Final(result) => result, + crate::react_compiler::entrypoint::program::CompileOutput::Splice { + replacements, + context, + } => crate::react_compiler::entrypoint::program::splice_and_finalize( + &ast_builder, + program, + replacements, + context, + ), + }; let diagnostics = compile_result_to_diagnostics(&result); - let (program_ast, events) = match result { - react_compiler::entrypoint::compile_result::CompileResult::Success { - ast, events, .. - } => (ast, events), - react_compiler::entrypoint::compile_result::CompileResult::Error { events, .. } => { - (None, events) - } + let (changed, events) = match result { + crate::react_compiler::entrypoint::compile_result::CompileResult::Success { + changed, + events, + .. + } => (changed, events), + crate::react_compiler::entrypoint::compile_result::CompileResult::Error { + events, .. + } => (false, events), }; - let compiled_program = program_ast.map(|file: react_compiler_ast::File| { - let mut compiled = - convert_ast_reverse::convert_program_to_oxc_with_source(&file, allocator, source_text); - compiled.source_type = program.source_type; - preserve_comments(&mut compiled, program, allocator); - compiled - }); + if changed { + preserve_comments(program, allocator); + } - TransformResult { program: compiled_program, diagnostics, events } + TransformResult { changed, diagnostics, events } } -/// Carry over the comments attached to top-level statements of the compiled -/// program, so codegen can re-emit them. The `react_compiler_ast` roundtrip -/// drops comments, so we reuse the ones from the original `source` program -/// (already parsed) rather than re-parsing the source. +/// Drop comments that no longer attach to a top-level statement, so codegen +/// doesn't re-emit stale inner comments of replaced function bodies. The in-place +/// splice swaps compiled functions in for their originals but leaves the original +/// program's comment list intact; comments attached inside a replaced body now +/// point at code that's gone, so filter the list down to top-level comments. fn preserve_comments<'a>( - compiled: &mut oxc_ast::ast::Program<'a>, - source: &oxc_ast::ast::Program<'a>, + program: &mut oxc_ast::ast::Program<'a>, allocator: &'a oxc_allocator::Allocator, ) { // Keep only comments attached to a top-level statement; inner comments have // `attached_to` positions that match no top-level statement. let mut top_level_starts = FxHashSet::default(); top_level_starts.insert(0u32); - for stmt in &compiled.body { + for stmt in &program.body { use oxc_span::GetSpan; let start = stmt.span().start; if start > 0 { @@ -137,39 +210,93 @@ fn preserve_comments<'a>( } // Copy only comments attached to top-level statements. - let mut comments = oxc_allocator::Vec::with_capacity_in(source.comments.len(), allocator); - for comment in &source.comments { + let mut comments = oxc_allocator::Vec::with_capacity_in(program.comments.len(), allocator); + for comment in &program.comments { if top_level_starts.contains(&comment.attached_to) { comments.push(*comment); } } - compiled.comments = comments; - - // Codegen reads comment content from `source_text` via span offsets, so the - // compiled program must point at the same source as the original. - compiled.source_text = source.source_text; + program.comments = comments; } /// Convenience wrapper — parses source text, runs semantic analysis, then transforms. +/// +/// The compiler now mutates the program in place, so the (possibly mutated) +/// program is returned alongside the result — `Some` when `result.changed`, so +/// callers can codegen it. `None` means no changes were made. pub fn transform_source<'a>( source_text: &'a str, source_type: oxc_span::SourceType, allocator: &'a oxc_allocator::Allocator, options: PluginOptions, -) -> TransformResult<'a> { - let parsed = oxc_parser::Parser::new(allocator, source_text, source_type).parse(); - transform(&parsed.program, allocator, options) +) -> (Option>, TransformResult) { + let mut parsed = oxc_parser::Parser::new(allocator, source_text, source_type).parse(); + let result = transform(&mut parsed.program, allocator, options); + let program = result.changed.then_some(parsed.program); + (program, result) } /// Lint a pre-parsed program — like [`transform`] but only collects diagnostics. -pub fn lint(program: &oxc_ast::ast::Program, options: PluginOptions) -> LintResult { +/// +/// Takes a shared `&Program` (the linter only ever has shared access). `transform` +/// now mutates a `&mut Program` in place, so lint runs the analysis on a throwaway +/// clone in a local arena. `no_emit` (lint mode) compiles no functions, so nothing +/// is spliced — the clone is only to satisfy the in-place signature. The +/// react_compiler lint rule is opt-in and off the hot transform path. +/// Lint a pre-parsed program — like [`transform`] but only collects diagnostics +/// and never mutates the program. +/// +/// In lint mode (`no_emit`) the compiler runs the full analysis (which produces +/// the Rules-of-React diagnostics) but never splices anything back, so the +/// program is only ever read. We therefore run directly on the borrowed +/// `program` instead of deep-cloning it into a private arena. `allocator` is the +/// arena that owns `program`; codegen still runs (so diagnostics are identical to +/// the emit path) and allocates its throwaway output into that same arena, which +/// is reset by the caller after the file is linted. +pub fn lint<'a>( + program: &oxc_ast::ast::Program<'a>, + allocator: &'a oxc_allocator::Allocator, + options: PluginOptions, +) -> LintResult { let mut opts = options; opts.no_emit = true; - // `no_emit` yields `program: None`; a local arena for the conversion suffices. - let allocator = oxc_allocator::Allocator::default(); - let result = transform(program, &allocator, opts); - LintResult { diagnostics: result.diagnostics } + // Mirror `transform`'s early bail-outs (no clone, so they're just early returns). + if !matches!(opts.compilation_mode.as_str(), "all" | "annotation") + && !has_react_like_functions(program) + { + return LintResult { diagnostics: oxc_diagnostics::Diagnostics::default() }; + } + if has_resource_management_declarations(program) { + return LintResult { diagnostics: oxc_diagnostics::Diagnostics::default() }; + } + if opts.source_code.is_none() { + opts.source_code = Some(program.source_text.to_string()); + } + + let scope_info = { + let semantic = + oxc_semantic::SemanticBuilder::new().with_build_nodes(true).build(program).semantic; + convert_scope_info(&semantic, program) + }; + + let ast_builder = oxc_ast::AstBuilder::new(allocator); + let output = crate::react_compiler::entrypoint::program::compile_program( + &ast_builder, + program, + scope_info, + opts, + ); + // Lint mode never produces replacements (every function returns `Ok(None)`), + // so this is always `Final`; the `Splice` arm is a defensive no-op fallback. + let result = match output { + crate::react_compiler::entrypoint::program::CompileOutput::Final(result) => result, + crate::react_compiler::entrypoint::program::CompileOutput::Splice { context, .. } => { + crate::react_compiler::entrypoint::program::finalize_without_splice(context) + } + }; + + LintResult { diagnostics: compile_result_to_diagnostics(&result) } } /// Convenience wrapper — parses source text, runs semantic analysis, then lints. @@ -179,15 +306,18 @@ pub fn lint_source( options: PluginOptions, ) -> LintResult { let allocator = oxc_allocator::Allocator::default(); - let parsed = oxc_parser::Parser::new(&allocator, source_text, source_type).parse(); - lint(&parsed.program, options) + let mut parsed = oxc_parser::Parser::new(&allocator, source_text, source_type).parse(); + let mut opts = options; + opts.no_emit = true; + let result = transform(&mut parsed.program, &allocator, opts); + LintResult { diagnostics: result.diagnostics } } // End-to-end smoke tests: oxc parse + semantic -> convert -> compile -> convert // back -> codegen. #[cfg(test)] mod tests { - use react_compiler::entrypoint::plugin_options::PluginOptions; + use crate::react_compiler::entrypoint::plugin_options::PluginOptions; use super::transform_source; @@ -200,13 +330,17 @@ mod tests { } } + // Stage 2 scaffold: codegen emission is stubbed (the back-end builds oxc but + // not the memoized body yet), so the compiled-output assertions below don't + // hold. Re-enable once the per-instruction emission is ported. #[test] fn memoizes_a_component_end_to_end() { let source = "function Component(props) {\n \ return
props.onClick()}>{props.text}
;\n}\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let (program, result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); assert!(!result.diagnostics.has_errors(), "unexpected errors: {:?}", result.diagnostics); assert!( @@ -214,7 +348,7 @@ mod tests { "unexpected warnings: {:?}", result.diagnostics ); - let program = result.program.expect("React Compiler should have transformed the component"); + let program = program.expect("React Compiler should have transformed the component"); let output = oxc_codegen::Codegen::new().build(&program).code; @@ -232,8 +366,9 @@ mod tests { fn skips_non_react_code() { let source = "function add(a, b) {\n return a + b;\n}\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - assert!(result.program.is_none(), "non-React code must not be transformed"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + assert!(program.is_none(), "non-React code must not be transformed"); } /// TypeScript-only constructs (`declare global`, `import =`, `export =`, @@ -253,8 +388,9 @@ function Component(props) {\n return
{props.text}
;\n}\n\ export = legacy;\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.unwrap_or_else(|| { + let (program, result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.unwrap_or_else(|| { panic!("component should be compiled; diagnostics: {:?}", result.diagnostics) }); let output = oxc_codegen::Codegen::new().build(&program).code; @@ -301,8 +437,9 @@ export = legacy;\n"; class Store {\n count = 0;\n increment() {\n this.count++;\n }\n}\n\ function Component(props) {\n return
{props.text}
;\n}\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.expect("component should be compiled"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.expect("component should be compiled"); let output = oxc_codegen::Codegen::new().build(&program).code; assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}"); assert!(output.contains("count = 0"), "class field lost:\n{output}"); @@ -326,8 +463,9 @@ function Component(props) {\n\ return
{props.text}
;\n\ }\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.expect("component should be compiled"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.expect("component should be compiled"); let output = oxc_codegen::Codegen::new().build(&program).code; assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}"); @@ -379,8 +517,9 @@ function Component(props: Props): JSX.Element {\n\ }\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.unwrap_or_else(|| { + let (program, result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.unwrap_or_else(|| { panic!("component should compile; diagnostics: {:?}", result.diagnostics) }); let output = oxc_codegen::Codegen::new().build(&program).code; @@ -414,6 +553,7 @@ function Component(props: Props): JSX.Element {\n\ } #[test] + #[ignore = "codegen value-emission (outlined fns / type-query rename) not yet ported"] fn type_query_casts_are_renamed_with_value_bindings() { let source = "\ type Field = { value?: string; optionsInputs?: Record };\n\ @@ -428,8 +568,9 @@ function Component({ fields }: { fields: Field[] }) {\n\ }\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.expect("component should be compiled"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.expect("component should be compiled"); let output = oxc_codegen::Codegen::new().build(&program).code; assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}"); @@ -451,8 +592,9 @@ function Component(props) {\n\ }\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.expect("component should be compiled"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.expect("component should be compiled"); let output = oxc_codegen::Codegen::new().build(&program).code; assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}"); @@ -477,10 +619,10 @@ async function Component(props) {\n await using x = sideEffect();\n return {E.A}{N.value}{props.text};\n}\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.expect("component should be compiled"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.expect("component should be compiled"); let output = oxc_codegen::Codegen::new().build(&program).code; assert!(output.contains("export enum E"), "exported enum lost:\n{output}"); @@ -523,8 +666,9 @@ import { Foo } from './foo';\n\ export { Foo };\n\ function Component(props) {\n return
{props.text}
;\n}\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.expect("component should be compiled"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.expect("component should be compiled"); let export = program .body @@ -559,8 +703,9 @@ function Component(props) {\n return
{props.text}
;\n}\n"; fn memo_wrapped_component_compiles() { let source = "React.memo((props) => {\n return
{props.text}
;\n});\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.expect("memo-wrapped component should compile"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.expect("memo-wrapped component should compile"); let output = oxc_codegen::Codegen::new().build(&program).code; assert!(output.contains("react/compiler-runtime"), "should memoize:\n{output}"); assert!(output.contains("_c("), "expected memo cache reads:\n{output}"); @@ -572,7 +717,8 @@ function Component(props) {\n return
{props.text}
;\n}\n"; // A Rules of Hooks violation is an `Error`-severity diagnostic. let source = "function Component(props) {\n if (props.cond) {\n useState(0);\n }\n return
{props.text}
;\n}\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let (_program, result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); assert!( result.diagnostics.has_errors(), "Rules of Hooks violation should be reported as an error: {:?}", @@ -582,7 +728,8 @@ function Component(props) {\n return
{props.text}
;\n}\n"; // A local named `fbt` is an unsupported-syntax bail-out — a warning, not an error. let source = "function Component() {\n const fbt = \"span\";\n return Hello;\n}\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let (_program, result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); assert!( result.diagnostics.has_warnings(), "fbt bail-out should be reported as a warning: {:?}", @@ -595,8 +742,8 @@ function Component(props) {\n return
{props.text}
;\n}\n"; ); } - /// Comments are dropped by the `react_compiler_ast` roundtrip, so - /// `preserve_comments` carries top-level comments over from the original + /// Comments are dropped when the compile pipeline rebuilds the program AST, + /// so `preserve_comments` carries top-level comments over from the original /// program. Comments inside a compiled function are not recovered. #[test] fn top_level_comments_are_preserved() { @@ -612,8 +759,9 @@ function Component(props) {\n\ export default Component;\n"; let allocator = oxc_allocator::Allocator::default(); - let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); - let program = result.program.expect("component should be compiled"); + let (program, _result) = + transform_source(source, oxc_span::SourceType::tsx(), &allocator, options()); + let program = program.expect("component should be compiled"); let output = oxc_codegen::Codegen::new().build(&program).code; assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}"); diff --git a/crates/oxc_react_compiler/src/prefilter.rs b/crates/oxc_react_compiler/src/prefilter.rs index 2be30ed1c9262..45dfe43619bf0 100644 --- a/crates/oxc_react_compiler/src/prefilter.rs +++ b/crates/oxc_react_compiler/src/prefilter.rs @@ -21,7 +21,7 @@ pub fn has_resource_management_declarations(program: &Program) -> bool { visitor.found } -use react_compiler_hir::environment::is_react_like_name; +use crate::react_compiler_hir::environment::is_react_like_name; /// `memo`/`forwardRef` (optionally `React.`-qualified): their callback is a /// component even when anonymous, so such files must not be skipped. diff --git a/crates/oxc_react_compiler/src/react_compiler/debug_print.rs b/crates/oxc_react_compiler/src/react_compiler/debug_print.rs new file mode 100644 index 0000000000000..65933da98e10a --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/debug_print.rs @@ -0,0 +1,610 @@ +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::print::{self, PrintFormatter}; +use crate::react_compiler_hir::{ + BasicBlock, BlockId, HirFunction, Instruction, ParamPattern, Place, Terminal, +}; + +// ============================================================================= +// DebugPrinter struct — thin wrapper around PrintFormatter for HIR-specific logic +// ============================================================================= + +struct DebugPrinter<'a, 'h> { + fmt: PrintFormatter<'a, 'h>, +} + +impl<'a, 'h> DebugPrinter<'a, 'h> { + fn new(env: &'a Environment<'h>) -> Self { + Self { fmt: PrintFormatter::new(env) } + } + + // ========================================================================= + // Function + // ========================================================================= + + fn format_function(&mut self, func: &HirFunction<'h>) { + self.fmt.indent(); + self.fmt.line(&format!( + "id: {}", + match &func.id { + Some(id) => format!("\"{}\"", id), + None => "null".to_string(), + } + )); + self.fmt.line(&format!( + "name_hint: {}", + match &func.name_hint { + Some(h) => format!("\"{}\"", h), + None => "null".to_string(), + } + )); + self.fmt.line(&format!("fn_type: {:?}", func.fn_type)); + self.fmt.line(&format!("generator: {}", func.generator)); + self.fmt.line(&format!("is_async: {}", func.is_async)); + self.fmt.line(&format!("loc: {}", print::format_loc(&func.loc))); + + // params + self.fmt.line("params:"); + self.fmt.indent(); + for (i, param) in func.params.iter().enumerate() { + match param { + ParamPattern::Place(place) => { + self.fmt.format_place_field(&format!("[{}]", i), place); + } + ParamPattern::Spread(spread) => { + self.fmt.line(&format!("[{}] Spread:", i)); + self.fmt.indent(); + self.fmt.format_place_field("place", &spread.place); + self.fmt.dedent(); + } + } + } + self.fmt.dedent(); + + // returns + self.fmt.line("returns:"); + self.fmt.indent(); + self.fmt.format_place_field("value", &func.returns); + self.fmt.dedent(); + + // context + self.fmt.line("context:"); + self.fmt.indent(); + for (i, place) in func.context.iter().enumerate() { + self.fmt.format_place_field(&format!("[{}]", i), place); + } + self.fmt.dedent(); + + // aliasing_effects + match &func.aliasing_effects { + Some(effects) => { + self.fmt.line("aliasingEffects:"); + self.fmt.indent(); + for (i, eff) in effects.iter().enumerate() { + self.fmt.line(&format!("[{}] {}", i, self.fmt.format_effect(eff))); + } + self.fmt.dedent(); + } + None => self.fmt.line("aliasingEffects: null"), + } + + // directives + self.fmt.line("directives:"); + self.fmt.indent(); + for (i, d) in func.directives.iter().enumerate() { + self.fmt.line(&format!("[{}] \"{}\"", i, d)); + } + self.fmt.dedent(); + + // return_type_annotation + self.fmt.line(&format!( + "returnTypeAnnotation: {}", + match &func.return_type_annotation { + Some(ann) => ann.clone(), + None => "null".to_string(), + } + )); + + self.fmt.line(""); + self.fmt.line("Blocks:"); + self.fmt.indent(); + for (block_id, block) in &func.body.blocks { + self.format_block(block_id, block, &func.instructions); + } + self.fmt.dedent(); + self.fmt.dedent(); + } + + // ========================================================================= + // Block + // ========================================================================= + + fn format_block( + &mut self, + block_id: &BlockId, + block: &BasicBlock, + instructions: &[Instruction<'h>], + ) { + self.fmt.line(&format!("bb{} ({}):", block_id.0, block.kind)); + self.fmt.indent(); + + // preds + let preds: Vec = block.preds.iter().map(|p| format!("bb{}", p.0)).collect(); + self.fmt.line(&format!("preds: [{}]", preds.join(", "))); + + // phis + self.fmt.line("phis:"); + self.fmt.indent(); + for phi in &block.phis { + self.format_phi(phi); + } + self.fmt.dedent(); + + // instructions + self.fmt.line("instructions:"); + self.fmt.indent(); + for (index, instr_id) in block.instructions.iter().enumerate() { + let instr = &instructions[instr_id.0 as usize]; + self.format_instruction(instr, index); + } + self.fmt.dedent(); + + // terminal + self.fmt.line("terminal:"); + self.fmt.indent(); + self.format_terminal(&block.terminal); + self.fmt.dedent(); + + self.fmt.dedent(); + } + + // ========================================================================= + // Phi + // ========================================================================= + + fn format_phi(&mut self, phi: &crate::react_compiler_hir::Phi) { + self.fmt.line("Phi {"); + self.fmt.indent(); + self.fmt.format_place_field("place", &phi.place); + self.fmt.line("operands:"); + self.fmt.indent(); + for (block_id, place) in &phi.operands { + self.fmt.line(&format!("bb{}:", block_id.0)); + self.fmt.indent(); + self.fmt.format_place_field("value", place); + self.fmt.dedent(); + } + self.fmt.dedent(); + self.fmt.dedent(); + self.fmt.line("}"); + } + + // ========================================================================= + // Instruction + // ========================================================================= + + fn format_instruction(&mut self, instr: &Instruction<'h>, index: usize) { + self.fmt.line(&format!("[{}] Instruction {{", index)); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", instr.id.0)); + self.fmt.format_place_field("lvalue", &instr.lvalue); + self.fmt.line("value:"); + self.fmt.indent(); + // For the HIR printer, inner functions are formatted via format_function + self.fmt.format_instruction_value( + &instr.value, + Some(&|fmt: &mut PrintFormatter<'_, 'h>, func: &HirFunction<'h>| { + // We need to recursively format the inner function + // Use a temporary DebugPrinter that shares the formatter state + let mut inner = DebugPrinter { + fmt: PrintFormatter { + env: fmt.env, + seen_identifiers: std::mem::take(&mut fmt.seen_identifiers), + seen_scopes: std::mem::take(&mut fmt.seen_scopes), + output: Vec::new(), + indent_level: fmt.indent_level, + }, + }; + inner.format_function(func); + // Write the output lines into the parent formatter + for line in &inner.fmt.output { + fmt.line_raw(line); + } + // Copy back the seen state + fmt.seen_identifiers = inner.fmt.seen_identifiers; + fmt.seen_scopes = inner.fmt.seen_scopes; + }), + ); + self.fmt.dedent(); + match &instr.effects { + Some(effects) => { + self.fmt.line("effects:"); + self.fmt.indent(); + for (i, eff) in effects.iter().enumerate() { + self.fmt.line(&format!("[{}] {}", i, self.fmt.format_effect(eff))); + } + self.fmt.dedent(); + } + None => self.fmt.line("effects: null"), + } + self.fmt.line(&format!("loc: {}", print::format_loc(&instr.loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + + // ========================================================================= + // Terminal + // ========================================================================= + + fn format_terminal(&mut self, terminal: &Terminal) { + match terminal { + Terminal::If { test, consequent, alternate, fallthrough, id, loc } => { + self.fmt.line("If {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.format_place_field("test", test); + self.fmt.line(&format!("consequent: bb{}", consequent.0)); + self.fmt.line(&format!("alternate: bb{}", alternate.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Branch { test, consequent, alternate, fallthrough, id, loc } => { + self.fmt.line("Branch {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.format_place_field("test", test); + self.fmt.line(&format!("consequent: bb{}", consequent.0)); + self.fmt.line(&format!("alternate: bb{}", alternate.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Logical { operator, test, fallthrough, id, loc } => { + self.fmt.line("Logical {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("operator: \"{}\"", operator)); + self.fmt.line(&format!("test: bb{}", test.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Ternary { test, fallthrough, id, loc } => { + self.fmt.line("Ternary {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("test: bb{}", test.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Optional { optional, test, fallthrough, id, loc } => { + self.fmt.line("Optional {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("optional: {}", optional)); + self.fmt.line(&format!("test: bb{}", test.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Throw { value, id, loc } => { + self.fmt.line("Throw {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.format_place_field("value", value); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Return { value, return_variant, id, loc, effects } => { + self.fmt.line("Return {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("returnVariant: {:?}", return_variant)); + self.fmt.format_place_field("value", value); + match effects { + Some(e) => { + self.fmt.line("effects:"); + self.fmt.indent(); + for (i, eff) in e.iter().enumerate() { + self.fmt.line(&format!("[{}] {}", i, self.fmt.format_effect(eff))); + } + self.fmt.dedent(); + } + None => self.fmt.line("effects: null"), + } + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Goto { block, variant, id, loc } => { + self.fmt.line("Goto {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("block: bb{}", block.0)); + self.fmt.line(&format!("variant: {:?}", variant)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Switch { test, cases, fallthrough, id, loc } => { + self.fmt.line("Switch {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.format_place_field("test", test); + self.fmt.line("cases:"); + self.fmt.indent(); + for (i, case) in cases.iter().enumerate() { + match &case.test { + Some(p) => { + self.fmt.line(&format!("[{}] Case {{", i)); + self.fmt.indent(); + self.fmt.format_place_field("test", p); + self.fmt.line(&format!("block: bb{}", case.block.0)); + self.fmt.dedent(); + self.fmt.line("}"); + } + None => { + self.fmt + .line(&format!("[{}] Default {{ block: bb{} }}", i, case.block.0)); + } + } + } + self.fmt.dedent(); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::DoWhile { loop_block, test, fallthrough, id, loc } => { + self.fmt.line("DoWhile {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loop: bb{}", loop_block.0)); + self.fmt.line(&format!("test: bb{}", test.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::While { test, loop_block, fallthrough, id, loc } => { + self.fmt.line("While {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("test: bb{}", test.0)); + self.fmt.line(&format!("loop: bb{}", loop_block.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::For { init, test, update, loop_block, fallthrough, id, loc } => { + self.fmt.line("For {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("init: bb{}", init.0)); + self.fmt.line(&format!("test: bb{}", test.0)); + self.fmt.line(&format!( + "update: {}", + match update { + Some(u) => format!("bb{}", u.0), + None => "null".to_string(), + } + )); + self.fmt.line(&format!("loop: bb{}", loop_block.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::ForOf { init, test, loop_block, fallthrough, id, loc } => { + self.fmt.line("ForOf {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("init: bb{}", init.0)); + self.fmt.line(&format!("test: bb{}", test.0)); + self.fmt.line(&format!("loop: bb{}", loop_block.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::ForIn { init, loop_block, fallthrough, id, loc } => { + self.fmt.line("ForIn {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("init: bb{}", init.0)); + self.fmt.line(&format!("loop: bb{}", loop_block.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Label { block, fallthrough, id, loc } => { + self.fmt.line("Label {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("block: bb{}", block.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Sequence { block, fallthrough, id, loc } => { + self.fmt.line("Sequence {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("block: bb{}", block.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Unreachable { id, loc } => { + self.fmt.line(&format!( + "Unreachable {{ id: {}, loc: {} }}", + id.0, + print::format_loc(loc) + )); + } + Terminal::Unsupported { id, loc } => { + self.fmt.line(&format!( + "Unsupported {{ id: {}, loc: {} }}", + id.0, + print::format_loc(loc) + )); + } + Terminal::MaybeThrow { continuation, handler, id, loc, effects } => { + self.fmt.line("MaybeThrow {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("continuation: bb{}", continuation.0)); + self.fmt.line(&format!( + "handler: {}", + match handler { + Some(h) => format!("bb{}", h.0), + None => "null".to_string(), + } + )); + match effects { + Some(e) => { + self.fmt.line("effects:"); + self.fmt.indent(); + for (i, eff) in e.iter().enumerate() { + self.fmt.line(&format!("[{}] {}", i, self.fmt.format_effect(eff))); + } + self.fmt.dedent(); + } + None => self.fmt.line("effects: null"), + } + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Scope { fallthrough, block, scope, id, loc } => { + self.fmt.line("Scope {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.format_scope_field("scope", *scope); + self.fmt.line(&format!("block: bb{}", block.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::PrunedScope { fallthrough, block, scope, id, loc } => { + self.fmt.line("PrunedScope {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.format_scope_field("scope", *scope); + self.fmt.line(&format!("block: bb{}", block.0)); + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + Terminal::Try { block, handler_binding, handler, fallthrough, id, loc } => { + self.fmt.line("Try {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("block: bb{}", block.0)); + self.fmt.line(&format!("handler: bb{}", handler.0)); + match handler_binding { + Some(p) => self.fmt.format_place_field("handlerBinding", p), + None => self.fmt.line("handlerBinding: null"), + } + self.fmt.line(&format!("fallthrough: bb{}", fallthrough.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + } + } +} + +// ============================================================================= +// Entry point +// ============================================================================= + +pub fn debug_hir<'h>(hir: &HirFunction<'h>, env: &Environment<'h>) -> String { + let mut printer = DebugPrinter::new(env); + printer.format_function(hir); + + // Print outlined functions (matches TS DebugPrintHIR.ts: printDebugHIR) + for outlined in env.get_outlined_functions() { + printer.fmt.line(""); + printer.format_function(&outlined.func); + } + + printer.fmt.line(""); + printer.fmt.line("Environment:"); + printer.fmt.indent(); + printer.fmt.format_errors(&env.errors); + printer.fmt.dedent(); + + printer.fmt.to_string_output() +} + +// ============================================================================= +// Error formatting (kept for backward compatibility) +// ============================================================================= + +pub fn format_errors(error: &CompilerError) -> String { + let env = Environment::new(); + let mut fmt = PrintFormatter::new(&env); + fmt.format_errors(error); + fmt.to_string_output() +} + +/// Format an HIR function into a reactive PrintFormatter. +/// This bridges the two debug printers so inner functions in FunctionExpression/ObjectMethod +/// can be printed within the reactive function output. +pub fn format_hir_function_into<'h>( + reactive_fmt: &mut PrintFormatter<'_, 'h>, + func: &HirFunction<'h>, +) { + // Create a temporary DebugPrinter that shares the same environment + let mut printer = DebugPrinter { + fmt: PrintFormatter { + env: reactive_fmt.env, + seen_identifiers: std::mem::take(&mut reactive_fmt.seen_identifiers), + seen_scopes: std::mem::take(&mut reactive_fmt.seen_scopes), + output: Vec::new(), + indent_level: reactive_fmt.indent_level, + }, + }; + printer.format_function(func); + + // Write the output lines into the reactive formatter + for line in &printer.fmt.output { + reactive_fmt.line_raw(line); + } + // Copy back the seen state + reactive_fmt.seen_identifiers = printer.fmt.seen_identifiers; + reactive_fmt.seen_scopes = printer.fmt.seen_scopes; +} + +// ============================================================================= +// Helpers for effect formatting (kept for backward compatibility) +// ============================================================================= + +#[allow(dead_code)] +fn format_place_short(place: &Place, env: &Environment) -> String { + let ident = &env.identifiers[place.identifier.0 as usize]; + let name = match &ident.name { + Some(name) => name.value().to_string(), + None => String::new(), + }; + let scope = match ident.scope { + Some(scope_id) => format!(":{}", scope_id.0), + None => String::new(), + }; + format!("{}${}{}", name, place.identifier.0, scope) +} diff --git a/crates/oxc_react_compiler/src/react_compiler/entrypoint/compile_result.rs b/crates/oxc_react_compiler/src/react_compiler/entrypoint/compile_result.rs new file mode 100644 index 0000000000000..0517c0559e6b2 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/entrypoint/compile_result.rs @@ -0,0 +1,240 @@ +use crate::react_compiler_diagnostics::SourceLocation; +use crate::react_compiler_hir::ReactFunctionType; + +use crate::react_compiler::timing::TimingEntry; + +/// Source location with index and filename fields for logger event serialization. +/// Matches the Babel SourceLocation format that the TS compiler emits in logger events. +#[derive(Debug, Clone)] +pub struct LoggerSourceLocation { + pub start: LoggerPosition, + pub end: LoggerPosition, + pub filename: Option, + pub identifier_name: Option, +} + +#[derive(Debug, Clone)] +pub struct LoggerPosition { + pub line: u32, + pub column: u32, + pub index: Option, +} + +impl LoggerSourceLocation { + /// Create from a diagnostics SourceLocation, adding index and filename. + pub fn from_loc( + loc: &SourceLocation, + filename: Option<&str>, + start_index: Option, + end_index: Option, + ) -> Self { + Self { + start: LoggerPosition { + line: loc.start.line, + column: loc.start.column, + index: start_index, + }, + end: LoggerPosition { line: loc.end.line, column: loc.end.column, index: end_index }, + filename: filename.map(|s| s.to_string()), + identifier_name: None, + } + } + + /// Create from a diagnostics SourceLocation without index or filename. + pub fn from_loc_simple(loc: &SourceLocation) -> Self { + Self { + start: LoggerPosition { line: loc.start.line, column: loc.start.column, index: None }, + end: LoggerPosition { line: loc.end.line, column: loc.end.column, index: None }, + filename: None, + identifier_name: None, + } + } +} + +/// A variable rename from lowering, serialized for the JS shim. +#[derive(Debug, Clone)] +pub struct BindingRenameInfo { + pub original: String, + pub renamed: String, + pub declaration_start: u32, +} + +/// Main result type returned by the compile function. +/// +/// The compiler splices the compiled functions into the caller-owned +/// `&mut Program` in place (see `compile_program`); `changed` reports whether +/// any splice happened. +#[derive(Debug)] +pub enum CompileResult { + /// Compilation succeeded (or no functions needed compilation). + /// `changed` is false if no changes were made to the program. + Success { + changed: bool, + events: Vec, + /// Unified ordered log interleaving events and debug entries. + /// Items appear in the order they were emitted during compilation. + /// The JS side uses this as the single source of truth (preferred over + /// separate events/debugLogs arrays). + ordered_log: Vec, + /// Variable renames from lowering, for applying back to the Babel AST. + /// Each entry maps an original binding name to its renamed version, + /// identified by the binding's declaration start position in the source. + renames: Vec, + /// Timing data for profiling. Only populated when __profiling is enabled. + timing: Vec, + }, + /// A fatal error occurred and panicThreshold dictates it should throw. + Error { + error: CompilerErrorInfo, + events: Vec, + ordered_log: Vec, + /// Timing data for profiling. Only populated when __profiling is enabled. + timing: Vec, + }, +} + +/// An item in the ordered log, which can be either a logger event or a debug entry. +#[derive(Debug, Clone)] +pub enum OrderedLogItem { + Event { event: LoggerEvent }, + Debug { entry: DebugLogEntry }, +} + +/// Structured error information for the JS shim. +#[derive(Debug, Clone)] +pub struct CompilerErrorInfo { + pub reason: String, + pub description: Option, + pub details: Vec, + /// When set, the JS shim should throw an Error with this exact message + /// instead of formatting through formatCompilerError(). This is used + /// for simulated unknown exceptions (throwUnknownException__testonly) + /// which in the TS compiler are plain Error objects, not CompilerErrors. + pub raw_message: Option, + /// Pre-formatted error message produced by Rust, matching the JS + /// formatCompilerError() output. When present, the JS shim uses this + /// directly instead of calling formatCompilerError() on the JS side. + pub formatted_message: Option, +} + +/// Serializable error detail — flat plain object matching the TS +/// `formatDetailForLogging()` output. All fields are direct properties. +#[derive(Debug, Clone)] +pub struct CompilerErrorDetailInfo { + pub category: String, + pub reason: String, + pub description: Option, + pub severity: String, + pub suggestions: Option>, + pub details: Option>, + pub loc: Option, +} + +/// Serializable suggestion info for logger events. +#[derive(Debug, Clone)] +pub struct LoggerSuggestionInfo { + pub description: String, + pub op: LoggerSuggestionOp, + pub range: (usize, usize), + pub text: Option, +} + +/// Numeric enum matching TS `CompilerSuggestionOperation`. +#[derive(Debug, Clone, Copy)] +pub enum LoggerSuggestionOp { + InsertBefore = 0, + InsertAfter = 1, + Remove = 2, + Replace = 3, +} + +/// Individual error or hint item within a CompilerErrorDetailInfo. +#[derive(Debug, Clone)] +pub struct CompilerErrorItemInfo { + pub kind: String, + pub loc: Option, + /// Serialized as `null` when None (not omitted), matching TS behavior. + pub message: Option, +} + +/// Debug log entry for debugLogIRs support. +/// Currently only supports the 'debug' variant (string values). +#[derive(Debug, Clone)] +pub struct DebugLogEntry { + pub kind: &'static str, + pub name: String, + pub value: String, +} + +impl DebugLogEntry { + pub fn new(name: impl Into, value: impl Into) -> Self { + Self { kind: "debug", name: name.into(), value: value.into() } + } +} + +/// Codegen output for a single compiled function. +/// +/// Stage 2: the generated AST fields are now arena-allocated oxc nodes (lifetime +/// `'a`) instead of the former Babel-shaped `Identifier`/`PatternLike`/ +/// `BlockStatement`. This is the type the back-end (`codegen_function`) produces +/// and the pipeline threads up to `compile_program`. +#[derive(Debug)] +pub struct CodegenFunction<'a> { + pub loc: Option, + pub id: Option>, + pub name_hint: Option, + pub params: oxc_allocator::Box<'a, oxc_ast::ast::FormalParameters<'a>>, + pub body: oxc_allocator::Box<'a, oxc_ast::ast::FunctionBody<'a>>, + pub generator: bool, + pub is_async: bool, + pub memo_slots_used: u32, + pub memo_blocks: u32, + pub memo_values: u32, + pub pruned_memo_blocks: u32, + pub pruned_memo_values: u32, + pub outlined: Vec>, +} + +/// An outlined function extracted during compilation. +#[derive(Debug)] +pub struct OutlinedFunction<'a> { + pub func: CodegenFunction<'a>, + pub fn_type: Option, +} + +/// Logger events emitted during compilation. +/// These are returned to JS for the logger callback. +#[derive(Debug, Clone)] +pub enum LoggerEvent { + CompileSuccess { + fn_loc: Option, + fn_name: Option, + memo_slots: u32, + memo_blocks: u32, + memo_values: u32, + pruned_memo_blocks: u32, + pruned_memo_values: u32, + }, + CompileError { + detail: CompilerErrorDetailInfo, + fn_loc: Option, + }, + /// Same as CompileError but serializes fnLoc before detail (matching TS program.ts output) + CompileErrorWithLoc { + fn_loc: LoggerSourceLocation, + detail: CompilerErrorDetailInfo, + }, + CompileSkip { + fn_loc: Option, + reason: String, + loc: Option, + }, + CompileUnexpectedThrow { + fn_loc: Option, + data: String, + }, + PipelineError { + fn_loc: Option, + data: String, + }, +} diff --git a/crates/oxc_react_compiler/src/react_compiler/entrypoint/imports.rs b/crates/oxc_react_compiler/src/react_compiler/entrypoint/imports.rs new file mode 100644 index 0000000000000..4a5c05f87fec6 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/entrypoint/imports.rs @@ -0,0 +1,321 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{CompilerError, CompilerErrorDetail, ErrorCategory}; +use crate::scope::ScopeInfo; + +use super::compile_result::{DebugLogEntry, LoggerEvent, OrderedLogItem}; +use super::plugin_options::{CompilerTarget, PluginOptions}; +use super::suppression::SuppressionRange; +use crate::react_compiler::timing::TimingData; + +/// An import specifier tracked by ProgramContext. +/// Corresponds to NonLocalImportSpecifier in the TS compiler. +#[derive(Debug, Clone)] +pub struct NonLocalImportSpecifier { + pub name: String, + pub module: String, + pub imported: String, +} + +/// Context for the program being compiled. +/// Tracks compiled functions, generated names, and import requirements. +/// Equivalent to ProgramContext class in Imports.ts. +pub struct ProgramContext { + pub opts: PluginOptions, + pub filename: Option, + /// The source filename from the parser's sourceFilename option. + /// This is the filename stored on AST node `loc.filename` fields, + /// which may differ from `filename` (e.g., no path prefix). + source_filename: Option, + pub code: Option, + pub react_runtime_module: String, + pub suppressions: Vec, + pub has_module_scope_opt_out: bool, + pub events: Vec, + /// Unified ordered log that interleaves events and debug entries + /// in the order they were emitted during compilation. + pub ordered_log: Vec, + + // Pre-resolved import local names for codegen + pub instrument_fn_name: Option, + pub instrument_gating_name: Option, + pub hook_guard_name: Option, + + // Variable renames from lowering, to be applied back to the Babel AST + pub renames: Vec, + + /// Timing data for profiling. Accumulates across all function compilations. + pub timing: TimingData, + + /// Line-offset index over the whole source, built once. The HIR front-end + /// resolves byte spans to `(line, column)` via this table; building it is an + /// O(source) scan, so it must be shared across every per-function `lower` + /// call rather than rebuilt for each function. + pub line_offsets: crate::react_compiler_lowering::source_loc::LineOffsets, + + /// Node IDs of identifiers that are actual references to bindings (the keys of + /// `ScopeInfo::ref_node_id_to_binding`). Whole-program and read-only, built + /// once from the scope info and shared (via `Rc`) into every per-function + /// `Environment` instead of being rebuilt for each function. + pub reference_node_ids: std::rc::Rc>, + + /// Whether debug logging is enabled (HIR formatting after each pass). + pub debug_enabled: bool, + + // Internal state + already_compiled: FxHashSet, + known_referenced_names: FxHashSet, + imports: FxHashMap>, +} + +impl ProgramContext { + pub fn new( + opts: PluginOptions, + filename: Option, + code: Option, + suppressions: Vec, + has_module_scope_opt_out: bool, + ) -> Self { + let react_runtime_module = get_react_compiler_runtime_module(&opts.target); + let profiling = opts.profiling; + let debug_enabled = opts.debug; + let line_offsets = crate::react_compiler_lowering::source_loc::LineOffsets::new( + code.as_deref().unwrap_or(""), + ); + Self { + opts, + filename, + source_filename: None, + code, + react_runtime_module, + suppressions, + has_module_scope_opt_out, + events: Vec::new(), + ordered_log: Vec::new(), + instrument_fn_name: None, + instrument_gating_name: None, + hook_guard_name: None, + renames: Vec::new(), + timing: TimingData::new(profiling), + line_offsets, + reference_node_ids: std::rc::Rc::new(FxHashSet::default()), + debug_enabled, + already_compiled: FxHashSet::default(), + known_referenced_names: FxHashSet::default(), + imports: FxHashMap::default(), + } + } + + /// Set the source filename (from AST node loc.filename). + pub fn set_source_filename(&mut self, filename: Option) { + if self.source_filename.is_none() { + self.source_filename = filename; + } + } + + /// Get the source filename for logger events. + pub fn source_filename(&self) -> Option { + self.source_filename.clone() + } + + /// Check if a function at the given start position has already been compiled. + /// This is a workaround for Babel not consistently respecting skip(). + pub fn is_already_compiled(&self, start: u32) -> bool { + self.already_compiled.contains(&start) + } + + /// Mark a function at the given start position as compiled. + pub fn mark_compiled(&mut self, start: u32) { + self.already_compiled.insert(start); + } + + /// Initialize known referenced names from scope bindings. + /// Call this after construction to seed conflict detection with program scope bindings. + pub fn init_from_scope(&mut self, scope: &ScopeInfo) { + // Register ALL bindings (not just program-scope) so that UID generation + // avoids name conflicts with any binding in the file. This matches + // Babel's generateUid() which checks all scopes. + for binding in &scope.bindings { + self.known_referenced_names.insert(binding.name.clone()); + } + // Build the whole-program reference-node-id set once, here, so each + // per-function `Environment` can share it (see the field doc). + self.reference_node_ids = + std::rc::Rc::new(scope.ref_node_id_to_binding.keys().copied().collect()); + } + + /// Check if a name conflicts with known references. + pub fn has_reference(&self, name: &str) -> bool { + self.known_referenced_names.contains(name) + } + + /// Generate a unique identifier name that doesn't conflict with existing bindings. + /// + /// For hook names (use*), preserves the original name to avoid breaking + /// hook-name-based type inference. For other names, prefixes with underscore + /// similar to Babel's generateUid. + pub fn new_uid(&mut self, name: &str) -> String { + if is_hook_name(name) { + // Don't prefix hooks with underscore, since InferTypes might + // type HookKind based on callee naming convention. + let mut uid = name.to_string(); + let mut i = 0; + while self.has_reference(&uid) { + uid = format!("{}_{}", name, i); + i += 1; + } + self.known_referenced_names.insert(uid.clone()); + uid + } else if !self.has_reference(name) { + self.known_referenced_names.insert(name.to_string()); + name.to_string() + } else { + // Generate unique name with underscore prefix (similar to Babel's generateUid). + // Babel strips leading underscores before prefixing, so: + // generateUid("_c") → strips to "c" → generates "_c", "_c2", "_c3", ... + // generateUid("foo") → generates "_foo", "_foo2", "_foo3", ... + let base = name.trim_start_matches('_'); + let mut uid = format!("_{}", base); + let mut i = 2; + while self.has_reference(&uid) { + uid = format!("_{}{}", base, i); + i += 1; + } + self.known_referenced_names.insert(uid.clone()); + uid + } + } + + /// Add the memo cache import (the `c` function from the compiler runtime). + pub fn add_memo_cache_import(&mut self) -> NonLocalImportSpecifier { + let module = self.react_runtime_module.clone(); + self.add_import_specifier(&module, "c", Some("_c")) + } + + /// Add an import specifier, reusing an existing one if it was already added. + /// + /// If `name_hint` is provided, it will be used as the basis for the local + /// name; otherwise `specifier` is used. + pub fn add_import_specifier( + &mut self, + module: &str, + specifier: &str, + name_hint: Option<&str>, + ) -> NonLocalImportSpecifier { + // Check if already imported + if let Some(module_imports) = self.imports.get(module) { + if let Some(existing) = module_imports.get(specifier) { + return existing.clone(); + } + } + + let name = self.new_uid(name_hint.unwrap_or(specifier)); + let binding = NonLocalImportSpecifier { + name, + module: module.to_string(), + imported: specifier.to_string(), + }; + + self.imports + .entry(module.to_string()) + .or_default() + .insert(specifier.to_string(), binding.clone()); + + binding + } + + /// Register a name as referenced so future uid generation avoids it. + pub fn add_new_reference(&mut self, name: String) { + self.known_referenced_names.insert(name); + } + + /// Get the set of known referenced names for seeding per-function Environment UID generation. + pub fn known_referenced_names(&self) -> &FxHashSet { + &self.known_referenced_names + } + + /// Merge UID names generated during a function compilation back into the program context, + /// so subsequent function compilations avoid collisions. + pub fn merge_uid_known_names(&mut self, names: &FxHashSet) { + self.known_referenced_names.extend(names.iter().cloned()); + } + + /// Log a compilation event. + pub fn log_event(&mut self, event: LoggerEvent) { + self.ordered_log.push(OrderedLogItem::Event { event: event.clone() }); + self.events.push(event); + } + + /// Log a debug entry (for debugLogIRs support). + pub fn log_debug(&mut self, entry: DebugLogEntry) { + self.ordered_log.push(OrderedLogItem::Debug { entry }); + } + + /// Check if there are any pending imports to add to the program. + pub fn has_pending_imports(&self) -> bool { + !self.imports.is_empty() + } + + /// Get an immutable view of the generated imports. + pub fn imports(&self) -> &FxHashMap> { + &self.imports + } +} + +/// Check for blocklisted import modules. +/// Returns a CompilerError if any blocklisted imports are found. +pub fn validate_restricted_imports( + program: &oxc_ast::ast::Program, + blocklisted: &Option>, +) -> Option { + let blocklisted = match blocklisted { + Some(b) if !b.is_empty() => b, + _ => return None, + }; + let restricted: FxHashSet<&str> = blocklisted.iter().map(|s| s.as_str()).collect(); + let mut error = CompilerError::new(); + + for stmt in &program.body { + if let oxc_ast::ast::Statement::ImportDeclaration(import) = stmt { + if restricted.contains(import.source.value.as_str()) { + let detail = CompilerErrorDetail::new( + ErrorCategory::Todo, + "Bailing out due to blocklisted import", + ) + .with_description(format!("Import from module {}", import.source.value)); + error.push_error_detail(detail); + } + } + } + + if error.has_any_errors() { Some(error) } else { None } +} + +/// Check if a name follows the React hook naming convention (use[A-Z0-9]...). +fn is_hook_name(name: &str) -> bool { + let bytes = name.as_bytes(); + bytes.len() >= 4 + && bytes[0] == b'u' + && bytes[1] == b's' + && bytes[2] == b'e' + && bytes.get(3).map_or(false, |c| c.is_ascii_uppercase() || c.is_ascii_digit()) +} + +/// Get the runtime module name based on the compiler target. +pub fn get_react_compiler_runtime_module(target: &CompilerTarget) -> String { + match target { + CompilerTarget::Version(v) if v == "19" => "react/compiler-runtime".to_string(), + CompilerTarget::Version(v) if v == "17" || v == "18" => { + "react-compiler-runtime".to_string() + } + CompilerTarget::MetaInternal { runtime_module, .. } => runtime_module.clone(), + // Default to React 19 runtime for unrecognized versions + CompilerTarget::Version(_) => "react/compiler-runtime".to_string(), + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler/entrypoint/mod.rs b/crates/oxc_react_compiler/src/react_compiler/entrypoint/mod.rs new file mode 100644 index 0000000000000..0a6eb70e5687d --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/entrypoint/mod.rs @@ -0,0 +1,10 @@ +pub mod compile_result; +pub mod imports; +pub mod pipeline; +pub mod plugin_options; +pub mod program; +pub mod suppression; + +pub use compile_result::*; +pub use plugin_options::*; +pub use program::*; diff --git a/crates/oxc_react_compiler/src/react_compiler/entrypoint/pipeline.rs b/crates/oxc_react_compiler/src/react_compiler/entrypoint/pipeline.rs new file mode 100644 index 0000000000000..ff82763611321 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/entrypoint/pipeline.rs @@ -0,0 +1,1296 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Compilation pipeline for a single function. +//! +//! Analogous to TS `Pipeline.ts` (`compileFn` → `run` → `runWithEnvironment`). +//! Currently runs BuildHIR (lowering) and PruneMaybeThrows. + +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_hir::ReactFunctionType; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::environment::OutputMode; +use crate::react_compiler_hir::environment_config::EnvironmentConfig; +use crate::react_compiler_lowering::FunctionNode; +use crate::scope::ScopeInfo; + +use super::compile_result::CodegenFunction; +use super::compile_result::CompilerErrorDetailInfo; +use super::compile_result::CompilerErrorItemInfo; +use super::compile_result::DebugLogEntry; +use super::compile_result::LoggerPosition; +use super::compile_result::LoggerSourceLocation; +use super::compile_result::OutlinedFunction; +use super::imports::ProgramContext; +use super::plugin_options::CompilerOutputMode; +use crate::react_compiler::debug_print; + +/// Run the compilation pipeline on a single function. +/// +/// Currently: creates an Environment, runs BuildHIR (lowering), and produces +/// debug output via the context. Returns a CodegenFunction with zeroed memo +/// stats on success (codegen is not yet implemented). +pub fn compile_fn<'a>( + ast: &oxc_ast::AstBuilder<'a>, + func: &FunctionNode<'_>, + fn_name: Option<&str>, + scope_info: &ScopeInfo, + fn_type: ReactFunctionType, + mode: CompilerOutputMode, + env_config: &EnvironmentConfig, + context: &mut ProgramContext, +) -> Result, CompilerError> { + let mut env = Environment::with_config(env_config.clone()); + env.fn_type = fn_type; + env.output_mode = match mode { + CompilerOutputMode::Ssr => OutputMode::Ssr, + CompilerOutputMode::Client => OutputMode::Client, + CompilerOutputMode::Lint => OutputMode::Lint, + }; + env.code = context.code.clone(); + env.filename = context.filename.clone(); + env.instrument_fn_name = context.instrument_fn_name.clone(); + env.instrument_gating_name = context.instrument_gating_name.clone(); + env.hook_guard_name = context.hook_guard_name.clone(); + env.seed_uid_known_names(&context.known_referenced_names()); + + env.reference_node_ids = std::rc::Rc::clone(&context.reference_node_ids); + + context.timing.start("lower"); + let mut hir = crate::react_compiler_lowering::lower( + func, + fn_name, + scope_info, + &mut env, + &context.line_offsets, + )?; + context.timing.stop(); + + // Copy renames from lowering to context (keep on env for codegen to apply to type annotations) + if !env.renames.is_empty() { + context.renames.extend(env.renames.iter().cloned()); + } + + // Check for Invariant errors after lowering, before logging HIR. + // In TS, Invariant errors throw from recordError(), aborting lower() before + // the HIR entry is logged. The thrown error contains ONLY the Invariant error, + // not other recorded (non-Invariant) errors. + if env.has_invariant_errors() { + return Err(env.take_invariant_errors()); + } + + if context.debug_enabled { + context.timing.start("debug_print:HIR"); + let debug_hir = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("HIR", debug_hir)); + context.timing.stop(); + } + + context.timing.start("PruneMaybeThrows"); + crate::react_compiler_optimization::prune_maybe_throws(&mut hir, &mut env.functions)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneMaybeThrows"); + let debug_prune = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("PruneMaybeThrows", debug_prune)); + context.timing.stop(); + } + + context.timing.start("ValidateContextVariableLValues"); + crate::react_compiler_validation::validate_context_variable_lvalues(&hir, &mut env)?; + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("ValidateContextVariableLValues", "ok".to_string())); + } + context.timing.stop(); + + context.timing.start("ValidateUseMemo"); + let void_memo_errors = crate::react_compiler_validation::validate_use_memo(&hir, &mut env); + log_errors_as_events(&void_memo_errors, context); + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("ValidateUseMemo", "ok".to_string())); + } + context.timing.stop(); + + context.timing.start("DropManualMemoization"); + crate::react_compiler_optimization::drop_manual_memoization(&mut hir, &mut env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:DropManualMemoization"); + let debug_drop_memo = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("DropManualMemoization", debug_drop_memo)); + context.timing.stop(); + } + + context.timing.start("InlineImmediatelyInvokedFunctionExpressions"); + crate::react_compiler_optimization::inline_immediately_invoked_function_expressions( + &mut hir, &mut env, + ); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:InlineImmediatelyInvokedFunctionExpressions"); + let debug_inline_iifes = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new( + "InlineImmediatelyInvokedFunctionExpressions", + debug_inline_iifes, + )); + context.timing.stop(); + } + + context.timing.start("MergeConsecutiveBlocks"); + crate::react_compiler_optimization::merge_consecutive_blocks::merge_consecutive_blocks( + &mut hir, + &mut env.functions, + ); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:MergeConsecutiveBlocks"); + let debug_merge = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("MergeConsecutiveBlocks", debug_merge)); + context.timing.stop(); + } + + // TODO: port assertConsistentIdentifiers + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("AssertConsistentIdentifiers", "ok".to_string())); + } + // TODO: port assertTerminalSuccessorsExist + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("AssertTerminalSuccessorsExist", "ok".to_string())); + } + + context.timing.start("EnterSSA"); + crate::react_compiler_ssa::enter_ssa(&mut hir, &mut env).map_err(|diag| { + let loc = diag.primary_location().cloned(); + let mut err = CompilerError::new(); + err.push_error_detail(crate::react_compiler_diagnostics::CompilerErrorDetail { + category: diag.category, + reason: diag.reason, + description: diag.description, + loc, + suggestions: diag.suggestions, + }); + err + })?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:SSA"); + let debug_ssa = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("SSA", debug_ssa)); + context.timing.stop(); + } + + context.timing.start("EliminateRedundantPhi"); + crate::react_compiler_ssa::eliminate_redundant_phi(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:EliminateRedundantPhi"); + let debug_eliminate_phi = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("EliminateRedundantPhi", debug_eliminate_phi)); + context.timing.stop(); + } + + // TODO: port assertConsistentIdentifiers + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("AssertConsistentIdentifiers", "ok".to_string())); + } + + context.timing.start("ConstantPropagation"); + crate::react_compiler_optimization::constant_propagation(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:ConstantPropagation"); + let debug_const_prop = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("ConstantPropagation", debug_const_prop)); + context.timing.stop(); + } + + context.timing.start("InferTypes"); + crate::react_compiler_typeinference::infer_types(&mut hir, &mut env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:InferTypes"); + let debug_infer_types = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("InferTypes", debug_infer_types)); + context.timing.stop(); + } + + if env.enable_validations() { + if env.config.validate_hooks_usage { + context.timing.start("ValidateHooksUsage"); + crate::react_compiler_validation::validate_hooks_usage(&hir, &mut env)?; + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("ValidateHooksUsage", "ok".to_string())); + } + context.timing.stop(); + } + + if env.config.validate_no_capitalized_calls.is_some() { + context.timing.start("ValidateNoCapitalizedCalls"); + crate::react_compiler_validation::validate_no_capitalized_calls(&hir, &mut env)?; + if context.debug_enabled { + context + .log_debug(DebugLogEntry::new("ValidateNoCapitalizedCalls", "ok".to_string())); + } + context.timing.stop(); + } + } + + context.timing.start("OptimizePropsMethodCalls"); + crate::react_compiler_optimization::optimize_props_method_calls(&mut hir, &env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:OptimizePropsMethodCalls"); + let debug_optimize_props = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("OptimizePropsMethodCalls", debug_optimize_props)); + context.timing.stop(); + } + + context.timing.start("AnalyseFunctions"); + let mut inner_logs: Vec = Vec::new(); + let debug_inner = context.debug_enabled; + let analyse_result = crate::react_compiler_inference::analyse_functions( + &mut hir, + &mut env, + &mut |inner_func, inner_env| { + if debug_inner { + inner_logs.push(debug_print::debug_hir(inner_func, inner_env)); + } + }, + ); + context.timing.stop(); + + // Always flush inner logs before propagating errors + if context.debug_enabled { + for inner_log in inner_logs { + context.log_debug(DebugLogEntry::new("AnalyseFunction (inner)", inner_log)); + } + } + + analyse_result?; + + if env.has_invariant_errors() { + return Err(env.take_invariant_errors()); + } + + if context.debug_enabled { + context.timing.start("debug_print:AnalyseFunctions"); + let debug_analyse_functions = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("AnalyseFunctions", debug_analyse_functions)); + context.timing.stop(); + } + + context.timing.start("InferMutationAliasingEffects"); + crate::react_compiler_inference::infer_mutation_aliasing_effects(&mut hir, &mut env, false)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:InferMutationAliasingEffects"); + let debug_infer_effects = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("InferMutationAliasingEffects", debug_infer_effects)); + context.timing.stop(); + } + + if env.output_mode == OutputMode::Ssr { + context.timing.start("OptimizeForSSR"); + crate::react_compiler_optimization::optimize_for_ssr(&mut hir, &env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:OptimizeForSSR"); + let debug_ssr = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("OptimizeForSSR", debug_ssr)); + context.timing.stop(); + } + } + + context.timing.start("DeadCodeElimination"); + crate::react_compiler_optimization::dead_code_elimination(&mut hir, &env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:DeadCodeElimination"); + let debug_dce = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("DeadCodeElimination", debug_dce)); + context.timing.stop(); + } + + context.timing.start("PruneMaybeThrows2"); + crate::react_compiler_optimization::prune_maybe_throws(&mut hir, &mut env.functions)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneMaybeThrows2"); + let debug_prune2 = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("PruneMaybeThrows", debug_prune2)); + context.timing.stop(); + } + + context.timing.start("InferMutationAliasingRanges"); + crate::react_compiler_inference::infer_mutation_aliasing_ranges(&mut hir, &mut env, false)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:InferMutationAliasingRanges"); + let debug_infer_ranges = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("InferMutationAliasingRanges", debug_infer_ranges)); + context.timing.stop(); + } + + if env.enable_validations() { + context.timing.start("ValidateLocalsNotReassignedAfterRender"); + crate::react_compiler_validation::validate_locals_not_reassigned_after_render( + &hir, &mut env, + ); + if context.debug_enabled { + context.log_debug(DebugLogEntry::new( + "ValidateLocalsNotReassignedAfterRender", + "ok".to_string(), + )); + } + context.timing.stop(); + + if env.config.validate_ref_access_during_render { + context.timing.start("ValidateNoRefAccessInRender"); + crate::react_compiler_validation::validate_no_ref_access_in_render(&hir, &mut env); + if context.debug_enabled { + context + .log_debug(DebugLogEntry::new("ValidateNoRefAccessInRender", "ok".to_string())); + } + context.timing.stop(); + } + + if env.config.validate_no_set_state_in_render { + context.timing.start("ValidateNoSetStateInRender"); + crate::react_compiler_validation::validate_no_set_state_in_render(&hir, &mut env)?; + if context.debug_enabled { + context + .log_debug(DebugLogEntry::new("ValidateNoSetStateInRender", "ok".to_string())); + } + context.timing.stop(); + } + + if env.config.validate_no_derived_computations_in_effects_exp + && env.output_mode == OutputMode::Lint + { + context.timing.start("ValidateNoDerivedComputationsInEffects"); + let errors = + crate::react_compiler_validation::validate_no_derived_computations_in_effects_exp( + &hir, &env, + )?; + log_errors_as_events(&errors, context); + if context.debug_enabled { + context.log_debug(DebugLogEntry::new( + "ValidateNoDerivedComputationsInEffects", + "ok".to_string(), + )); + } + context.timing.stop(); + } else if env.config.validate_no_derived_computations_in_effects { + context.timing.start("ValidateNoDerivedComputationsInEffects"); + crate::react_compiler_validation::validate_no_derived_computations_in_effects( + &hir, &mut env, + )?; + if context.debug_enabled { + context.log_debug(DebugLogEntry::new( + "ValidateNoDerivedComputationsInEffects", + "ok".to_string(), + )); + } + context.timing.stop(); + } + + if env.config.validate_no_set_state_in_effects && env.output_mode == OutputMode::Lint { + context.timing.start("ValidateNoSetStateInEffects"); + let errors = + crate::react_compiler_validation::validate_no_set_state_in_effects(&hir, &env)?; + log_errors_as_events(&errors, context); + if context.debug_enabled { + context + .log_debug(DebugLogEntry::new("ValidateNoSetStateInEffects", "ok".to_string())); + } + context.timing.stop(); + } + + if env.config.validate_no_jsx_in_try_statements && env.output_mode == OutputMode::Lint { + context.timing.start("ValidateNoJSXInTryStatement"); + let errors = crate::react_compiler_validation::validate_no_jsx_in_try_statement(&hir); + log_errors_as_events(&errors, context); + if context.debug_enabled { + context + .log_debug(DebugLogEntry::new("ValidateNoJSXInTryStatement", "ok".to_string())); + } + context.timing.stop(); + } + + context.timing.start("ValidateNoFreezingKnownMutableFunctions"); + crate::react_compiler_validation::validate_no_freezing_known_mutable_functions( + &hir, &mut env, + ); + if context.debug_enabled { + context.log_debug(DebugLogEntry::new( + "ValidateNoFreezingKnownMutableFunctions", + "ok".to_string(), + )); + } + context.timing.stop(); + } + + context.timing.start("InferReactivePlaces"); + crate::react_compiler_inference::infer_reactive_places(&mut hir, &mut env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:InferReactivePlaces"); + let debug_reactive_places = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("InferReactivePlaces", debug_reactive_places)); + context.timing.stop(); + } + + if env.enable_validations() { + context.timing.start("ValidateExhaustiveDependencies"); + crate::react_compiler_validation::validate_exhaustive_dependencies(&mut hir, &mut env)?; + if context.debug_enabled { + context + .log_debug(DebugLogEntry::new("ValidateExhaustiveDependencies", "ok".to_string())); + } + context.timing.stop(); + } + + context.timing.start("RewriteInstructionKindsBasedOnReassignment"); + crate::react_compiler_ssa::rewrite_instruction_kinds_based_on_reassignment(&mut hir, &env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:RewriteInstructionKindsBasedOnReassignment"); + let debug_rewrite = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new( + "RewriteInstructionKindsBasedOnReassignment", + debug_rewrite, + )); + context.timing.stop(); + } + + if env.enable_validations() + && env.config.validate_static_components + && env.output_mode == OutputMode::Lint + { + context.timing.start("ValidateStaticComponents"); + let errors = crate::react_compiler_validation::validate_static_components(&hir); + log_errors_as_events(&errors, context); + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("ValidateStaticComponents", "ok".to_string())); + } + context.timing.stop(); + } + + if env.enable_memoization() { + context.timing.start("InferReactiveScopeVariables"); + crate::react_compiler_inference::infer_reactive_scope_variables(&mut hir, &mut env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:InferReactiveScopeVariables"); + let debug_infer_scopes = debug_print::debug_hir(&hir, &env); + context + .log_debug(DebugLogEntry::new("InferReactiveScopeVariables", debug_infer_scopes)); + context.timing.stop(); + } + } + + context.timing.start("MemoizeFbtAndMacroOperandsInSameScope"); + let fbt_operands = + crate::react_compiler_inference::memoize_fbt_and_macro_operands_in_same_scope( + &hir, &mut env, + ); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:MemoizeFbtAndMacroOperandsInSameScope"); + let debug_fbt = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("MemoizeFbtAndMacroOperandsInSameScope", debug_fbt)); + context.timing.stop(); + } + + if env.config.enable_jsx_outlining { + context.timing.start("OutlineJsx"); + crate::react_compiler_optimization::outline_jsx(&mut hir, &mut env); + context.timing.stop(); + } + + if env.config.enable_name_anonymous_functions { + context.timing.start("NameAnonymousFunctions"); + crate::react_compiler_optimization::name_anonymous_functions(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:NameAnonymousFunctions"); + let debug_name_anon = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("NameAnonymousFunctions", debug_name_anon)); + context.timing.stop(); + } + } + + if env.config.enable_function_outlining { + context.timing.start("OutlineFunctions"); + crate::react_compiler_optimization::outline_functions(&mut hir, &mut env, &fbt_operands); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:OutlineFunctions"); + let debug_outline = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("OutlineFunctions", debug_outline)); + context.timing.stop(); + } + } + + context.timing.start("AlignMethodCallScopes"); + crate::react_compiler_inference::align_method_call_scopes(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:AlignMethodCallScopes"); + let debug_align = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("AlignMethodCallScopes", debug_align)); + context.timing.stop(); + } + + context.timing.start("AlignObjectMethodScopes"); + crate::react_compiler_inference::align_object_method_scopes(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:AlignObjectMethodScopes"); + let debug_align_obj = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("AlignObjectMethodScopes", debug_align_obj)); + context.timing.stop(); + } + + context.timing.start("PruneUnusedLabelsHIR"); + crate::react_compiler_optimization::prune_unused_labels_hir(&mut hir); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneUnusedLabelsHIR"); + let debug_prune_labels = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("PruneUnusedLabelsHIR", debug_prune_labels)); + context.timing.stop(); + } + + context.timing.start("AlignReactiveScopesToBlockScopesHIR"); + crate::react_compiler_inference::align_reactive_scopes_to_block_scopes_hir(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:AlignReactiveScopesToBlockScopesHIR"); + let debug_align_block_scopes = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new( + "AlignReactiveScopesToBlockScopesHIR", + debug_align_block_scopes, + )); + context.timing.stop(); + } + + context.timing.start("MergeOverlappingReactiveScopesHIR"); + crate::react_compiler_inference::merge_overlapping_reactive_scopes_hir(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:MergeOverlappingReactiveScopesHIR"); + let debug_merge_overlapping = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new( + "MergeOverlappingReactiveScopesHIR", + debug_merge_overlapping, + )); + context.timing.stop(); + } + + // TODO: port assertValidBlockNesting + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("AssertValidBlockNesting", "ok".to_string())); + } + + context.timing.start("BuildReactiveScopeTerminalsHIR"); + crate::react_compiler_inference::build_reactive_scope_terminals_hir(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:BuildReactiveScopeTerminalsHIR"); + let debug_build_scope_terminals = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new( + "BuildReactiveScopeTerminalsHIR", + debug_build_scope_terminals, + )); + context.timing.stop(); + } + + // TODO: port assertValidBlockNesting + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("AssertValidBlockNesting", "ok".to_string())); + } + + context.timing.start("FlattenReactiveLoopsHIR"); + crate::react_compiler_inference::flatten_reactive_loops_hir(&mut hir); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:FlattenReactiveLoopsHIR"); + let debug_flatten_loops = debug_print::debug_hir(&hir, &env); + context.log_debug(DebugLogEntry::new("FlattenReactiveLoopsHIR", debug_flatten_loops)); + context.timing.stop(); + } + + context.timing.start("FlattenScopesWithHooksOrUseHIR"); + crate::react_compiler_inference::flatten_scopes_with_hooks_or_use_hir(&mut hir, &env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:FlattenScopesWithHooksOrUseHIR"); + let debug_flatten_hooks = debug_print::debug_hir(&hir, &env); + context + .log_debug(DebugLogEntry::new("FlattenScopesWithHooksOrUseHIR", debug_flatten_hooks)); + context.timing.stop(); + } + + // TODO: port assertTerminalSuccessorsExist + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("AssertTerminalSuccessorsExist", "ok".to_string())); + } + // TODO: port assertTerminalPredsExist + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("AssertTerminalPredsExist", "ok".to_string())); + } + + context.timing.start("PropagateScopeDependenciesHIR"); + crate::react_compiler_inference::propagate_scope_dependencies_hir(&mut hir, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PropagateScopeDependenciesHIR"); + let debug_propagate_deps = debug_print::debug_hir(&hir, &env); + context + .log_debug(DebugLogEntry::new("PropagateScopeDependenciesHIR", debug_propagate_deps)); + context.timing.stop(); + } + + context.timing.start("BuildReactiveFunction"); + let mut reactive_fn = + crate::react_compiler_reactive_scopes::build_reactive_function(&hir, &env)?; + context.timing.stop(); + + fn hir_formatter<'h>( + fmt: &mut crate::react_compiler_hir::print::PrintFormatter<'_, 'h>, + func: &crate::react_compiler_hir::HirFunction<'h>, + ) { + debug_print::format_hir_function_into(fmt, func); + } + + if context.debug_enabled { + context.timing.start("debug_print:BuildReactiveFunction"); + let debug_reactive = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("BuildReactiveFunction", debug_reactive)); + context.timing.stop(); + } + + context.timing.start("AssertWellFormedBreakTargets"); + crate::react_compiler_reactive_scopes::assert_well_formed_break_targets(&reactive_fn, &env); + if context.debug_enabled { + context.log_debug(DebugLogEntry::new("AssertWellFormedBreakTargets", "ok".to_string())); + } + context.timing.stop(); + + context.timing.start("PruneUnusedLabels"); + crate::react_compiler_reactive_scopes::prune_unused_labels(&mut reactive_fn, &env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneUnusedLabels"); + let debug_prune_labels_reactive = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("PruneUnusedLabels", debug_prune_labels_reactive)); + context.timing.stop(); + } + + context.timing.start("AssertScopeInstructionsWithinScopes"); + crate::react_compiler_reactive_scopes::assert_scope_instructions_within_scopes( + &reactive_fn, + &env, + )?; + if context.debug_enabled { + context + .log_debug(DebugLogEntry::new("AssertScopeInstructionsWithinScopes", "ok".to_string())); + } + context.timing.stop(); + + context.timing.start("PruneNonEscapingScopes"); + crate::react_compiler_reactive_scopes::prune_non_escaping_scopes(&mut reactive_fn, &mut env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneNonEscapingScopes"); + let debug = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("PruneNonEscapingScopes", debug)); + context.timing.stop(); + } + + context.timing.start("PruneNonReactiveDependencies"); + crate::react_compiler_reactive_scopes::prune_non_reactive_dependencies( + &mut reactive_fn, + &mut env, + ); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneNonReactiveDependencies"); + let debug_prune_non_reactive = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new( + "PruneNonReactiveDependencies", + debug_prune_non_reactive, + )); + context.timing.stop(); + } + + context.timing.start("PruneUnusedScopes"); + crate::react_compiler_reactive_scopes::prune_unused_scopes(&mut reactive_fn, &env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneUnusedScopes"); + let debug_prune_unused_scopes = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("PruneUnusedScopes", debug_prune_unused_scopes)); + context.timing.stop(); + } + + context.timing.start("MergeReactiveScopesThatInvalidateTogether"); + crate::react_compiler_reactive_scopes::merge_reactive_scopes_that_invalidate_together( + &mut reactive_fn, + &mut env, + )?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:MergeReactiveScopesThatInvalidateTogether"); + let debug = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("MergeReactiveScopesThatInvalidateTogether", debug)); + context.timing.stop(); + } + + context.timing.start("PruneAlwaysInvalidatingScopes"); + crate::react_compiler_reactive_scopes::prune_always_invalidating_scopes( + &mut reactive_fn, + &env, + )?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneAlwaysInvalidatingScopes"); + let debug_prune_always_inv = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context + .log_debug(DebugLogEntry::new("PruneAlwaysInvalidatingScopes", debug_prune_always_inv)); + context.timing.stop(); + } + + context.timing.start("PropagateEarlyReturns"); + crate::react_compiler_reactive_scopes::propagate_early_returns(&mut reactive_fn, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PropagateEarlyReturns"); + let debug = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("PropagateEarlyReturns", debug)); + context.timing.stop(); + } + + context.timing.start("PruneUnusedLValues"); + crate::react_compiler_reactive_scopes::prune_unused_lvalues(&mut reactive_fn, &env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneUnusedLValues"); + let debug_prune_lvalues = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("PruneUnusedLValues", debug_prune_lvalues)); + context.timing.stop(); + } + + context.timing.start("PromoteUsedTemporaries"); + crate::react_compiler_reactive_scopes::promote_used_temporaries(&mut reactive_fn, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PromoteUsedTemporaries"); + let debug = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("PromoteUsedTemporaries", debug)); + context.timing.stop(); + } + + context.timing.start("ExtractScopeDeclarationsFromDestructuring"); + crate::react_compiler_reactive_scopes::extract_scope_declarations_from_destructuring( + &mut reactive_fn, + &mut env, + )?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:ExtractScopeDeclarationsFromDestructuring"); + let debug = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("ExtractScopeDeclarationsFromDestructuring", debug)); + context.timing.stop(); + } + + context.timing.start("StabilizeBlockIds"); + crate::react_compiler_reactive_scopes::stabilize_block_ids(&mut reactive_fn, &mut env); + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:StabilizeBlockIds"); + let debug_stabilize = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("StabilizeBlockIds", debug_stabilize)); + context.timing.stop(); + } + + context.timing.start("RenameVariables"); + let unique_identifiers = + crate::react_compiler_reactive_scopes::rename_variables(&mut reactive_fn, &mut env); + context.timing.stop(); + + for name in &unique_identifiers { + context.add_new_reference(name.clone()); + } + + if context.debug_enabled { + context.timing.start("debug_print:RenameVariables"); + let debug = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("RenameVariables", debug)); + context.timing.stop(); + } + + context.timing.start("PruneHoistedContexts"); + crate::react_compiler_reactive_scopes::prune_hoisted_contexts(&mut reactive_fn, &mut env)?; + context.timing.stop(); + + if context.debug_enabled { + context.timing.start("debug_print:PruneHoistedContexts"); + let debug = crate::react_compiler_reactive_scopes::print_reactive_function::debug_reactive_function_with_formatter( + &reactive_fn, &env, Some(&hir_formatter), + ); + context.log_debug(DebugLogEntry::new("PruneHoistedContexts", debug)); + context.timing.stop(); + } + + if env.config.enable_preserve_existing_memoization_guarantees + || env.config.validate_preserve_existing_memoization_guarantees + { + context.timing.start("ValidatePreservedManualMemoization"); + crate::react_compiler_validation::validate_preserved_manual_memoization( + &reactive_fn, + &mut env, + ); + if context.debug_enabled { + context.log_debug(DebugLogEntry::new( + "ValidatePreservedManualMemoization", + "ok".to_string(), + )); + } + context.timing.stop(); + } + + context.timing.start("codegen"); + let codegen_result = crate::react_compiler_reactive_scopes::codegen_function( + ast, + &reactive_fn, + &mut env, + unique_identifiers, + fbt_operands, + )?; + context.timing.stop(); + + // NOTE: we intentionally do NOT register the memo cache import here. + // The import is registered in apply_compiled_functions() only for functions + // that are actually applied to the output. Registering it here would cause + // a spurious `import { c as _c }` when a function compiles with memo slots + // but is later discarded (e.g., due to "use no memo" opt-out or errors), + // while other functions in the same file compile to 0 memo slots. + + // Simulate unexpected exception for testing (matches TS Pipeline.ts) + if env.config.throw_unknown_exception_testonly { + let mut err = CompilerError::new(); + err.push_error_detail(crate::react_compiler_diagnostics::CompilerErrorDetail { + category: crate::react_compiler_diagnostics::ErrorCategory::Invariant, + reason: "unexpected error".to_string(), + description: None, + loc: None, + suggestions: None, + }); + return Err(err); + } + + // Check for accumulated errors at the end of the pipeline + // (matches TS Pipeline.ts: env.hasErrors() → Err at the end) + if env.has_errors() { + // Merge UIDs even on error: in TS, Babel's scope.generateUid() permanently + // registers names in the scope's `uids` map regardless of whether the function + // compilation succeeds or fails. Without this merge, failed compilations would + // "leak" _temp names that subsequent successful compilations wouldn't see, + // causing numbering mismatches vs TS. + if let Some(uid_names) = env.take_uid_known_names() { + context.merge_uid_known_names(&uid_names); + } + return Err(env.take_errors()); + } + + // Re-compile outlined functions through the full pipeline. + // This mirrors TS behavior where outlined functions from JSX outlining + // are pushed back onto the compilation queue and compiled as components. + // With emission stubbed, codegen produces no outlined functions, so this loop + // is effectively inert; kept for when the oxc emission is ported. + let mut codegen_result = codegen_result; + let outlined = std::mem::take(&mut codegen_result.outlined); + let mut compiled_outlined: Vec> = Vec::new(); + for o in outlined { + let mut outlined_codegen = o.func; + outlined_codegen.outlined = Vec::new(); + if let Some(fn_type) = o.fn_type { + let fn_name = outlined_codegen.id.as_ref().map(|id| id.name.to_string()); + match compile_outlined_fn( + ast, + outlined_codegen, + fn_name.as_deref(), + fn_type, + mode, + env_config, + context, + ) { + Ok(compiled) => { + compiled_outlined + .push(OutlinedFunction { func: compiled, fn_type: Some(fn_type) }); + } + Err(_err) => { + // If re-compilation fails, skip the outlined function + } + } + } else { + compiled_outlined.push(OutlinedFunction { func: outlined_codegen, fn_type: o.fn_type }); + } + } + + if let Some(uid_names) = env.take_uid_known_names() { + context.merge_uid_known_names(&uid_names); + } + + codegen_result.outlined = compiled_outlined; + Ok(codegen_result) +} + +/// Compile an outlined function's codegen AST through the full pipeline. +/// +/// Creates a fresh Environment, builds a synthetic ScopeInfo with unique fake +/// positions for identifier resolution, lowers from AST to HIR, then runs +/// the full compilation pipeline. This mirrors the TS behavior where outlined +/// functions are inserted into the program AST and re-compiled from scratch. +pub fn compile_outlined_fn<'a>( + ast: &oxc_ast::AstBuilder<'a>, + codegen_fn: CodegenFunction<'a>, + fn_name: Option<&str>, + fn_type: ReactFunctionType, + mode: CompilerOutputMode, + env_config: &EnvironmentConfig, + context: &mut ProgramContext, +) -> Result, CompilerError> { + // Outlining synthesizes a function and re-lowers it. With the current codegen + // no functions are outlined, so this stays a passthrough until outlining is + // re-wired to synthesize an oxc function. + let _ = (ast, fn_name, fn_type, mode, env_config, context); + Ok(codegen_fn) +} + +/// Run the compilation pipeline passes on an HIR function (everything after lowering). +/// +/// This is extracted from `compile_fn` to allow reuse for outlined functions. +/// Returns the compiled CodegenFunction on success. +/// +/// Currently unused (kept for the outlined-function port); threads the oxc +/// `AstBuilder` like `compile_fn`. +#[allow(dead_code)] +fn run_pipeline_passes<'a, 'b>( + ast: &oxc_ast::AstBuilder<'a>, + hir: &mut crate::react_compiler_hir::HirFunction<'b>, + env: &mut Environment<'b>, + context: &mut ProgramContext, +) -> Result, CompilerError> { + crate::react_compiler_optimization::prune_maybe_throws(hir, &mut env.functions)?; + + crate::react_compiler_optimization::drop_manual_memoization(hir, env)?; + + crate::react_compiler_optimization::inline_immediately_invoked_function_expressions(hir, env); + + crate::react_compiler_optimization::merge_consecutive_blocks::merge_consecutive_blocks( + hir, + &mut env.functions, + ); + + crate::react_compiler_ssa::enter_ssa(hir, env).map_err(|diag| { + let loc = diag.primary_location().cloned(); + let mut err = CompilerError::new(); + err.push_error_detail(crate::react_compiler_diagnostics::CompilerErrorDetail { + category: diag.category, + reason: diag.reason, + description: diag.description, + loc, + suggestions: diag.suggestions, + }); + err + })?; + + crate::react_compiler_ssa::eliminate_redundant_phi(hir, env); + + crate::react_compiler_optimization::constant_propagation(hir, env); + + crate::react_compiler_typeinference::infer_types(hir, env)?; + + if env.enable_validations() { + if env.config.validate_hooks_usage { + crate::react_compiler_validation::validate_hooks_usage(hir, env)?; + } + } + + crate::react_compiler_optimization::optimize_props_method_calls(hir, env); + + crate::react_compiler_inference::analyse_functions( + hir, + env, + &mut |_inner_func, _inner_env| {}, + )?; + + if env.has_invariant_errors() { + return Err(env.take_invariant_errors()); + } + + crate::react_compiler_inference::infer_mutation_aliasing_effects(hir, env, false)?; + + if env.output_mode == OutputMode::Ssr { + crate::react_compiler_optimization::optimize_for_ssr(hir, env); + } + + crate::react_compiler_optimization::dead_code_elimination(hir, env); + + crate::react_compiler_optimization::prune_maybe_throws(hir, &mut env.functions)?; + + crate::react_compiler_inference::infer_mutation_aliasing_ranges(hir, env, false)?; + + if env.enable_validations() { + crate::react_compiler_validation::validate_locals_not_reassigned_after_render(hir, env); + + if env.config.validate_ref_access_during_render { + crate::react_compiler_validation::validate_no_ref_access_in_render(hir, env); + } + + if env.config.validate_no_set_state_in_render { + crate::react_compiler_validation::validate_no_set_state_in_render(hir, env)?; + } + + crate::react_compiler_validation::validate_no_freezing_known_mutable_functions(hir, env); + } + + crate::react_compiler_inference::infer_reactive_places(hir, env)?; + + if env.enable_validations() { + crate::react_compiler_validation::validate_exhaustive_dependencies(hir, env)?; + } + + crate::react_compiler_ssa::rewrite_instruction_kinds_based_on_reassignment(hir, env)?; + + if env.enable_memoization() { + crate::react_compiler_inference::infer_reactive_scope_variables(hir, env)?; + } + + let fbt_operands = + crate::react_compiler_inference::memoize_fbt_and_macro_operands_in_same_scope(hir, env); + + // Don't run outline_jsx on outlined functions (they're already outlined) + + if env.config.enable_name_anonymous_functions { + crate::react_compiler_optimization::name_anonymous_functions(hir, env); + } + + if env.config.enable_function_outlining { + crate::react_compiler_optimization::outline_functions(hir, env, &fbt_operands); + } + + crate::react_compiler_inference::align_method_call_scopes(hir, env); + crate::react_compiler_inference::align_object_method_scopes(hir, env); + + crate::react_compiler_optimization::prune_unused_labels_hir(hir); + + crate::react_compiler_inference::align_reactive_scopes_to_block_scopes_hir(hir, env); + crate::react_compiler_inference::merge_overlapping_reactive_scopes_hir(hir, env); + + crate::react_compiler_inference::build_reactive_scope_terminals_hir(hir, env); + crate::react_compiler_inference::flatten_reactive_loops_hir(hir); + crate::react_compiler_inference::flatten_scopes_with_hooks_or_use_hir(hir, env)?; + crate::react_compiler_inference::propagate_scope_dependencies_hir(hir, env); + let mut reactive_fn = crate::react_compiler_reactive_scopes::build_reactive_function(hir, env)?; + + crate::react_compiler_reactive_scopes::assert_well_formed_break_targets(&reactive_fn, env); + + crate::react_compiler_reactive_scopes::prune_unused_labels(&mut reactive_fn, env)?; + + crate::react_compiler_reactive_scopes::assert_scope_instructions_within_scopes( + &reactive_fn, + env, + )?; + + crate::react_compiler_reactive_scopes::prune_non_escaping_scopes(&mut reactive_fn, env)?; + crate::react_compiler_reactive_scopes::prune_non_reactive_dependencies(&mut reactive_fn, env); + crate::react_compiler_reactive_scopes::prune_unused_scopes(&mut reactive_fn, env)?; + crate::react_compiler_reactive_scopes::merge_reactive_scopes_that_invalidate_together( + &mut reactive_fn, + env, + )?; + crate::react_compiler_reactive_scopes::prune_always_invalidating_scopes(&mut reactive_fn, env)?; + crate::react_compiler_reactive_scopes::propagate_early_returns(&mut reactive_fn, env); + crate::react_compiler_reactive_scopes::prune_unused_lvalues(&mut reactive_fn, env); + crate::react_compiler_reactive_scopes::promote_used_temporaries(&mut reactive_fn, env); + crate::react_compiler_reactive_scopes::extract_scope_declarations_from_destructuring( + &mut reactive_fn, + env, + )?; + crate::react_compiler_reactive_scopes::stabilize_block_ids(&mut reactive_fn, env); + + let unique_identifiers = + crate::react_compiler_reactive_scopes::rename_variables(&mut reactive_fn, env); + for name in &unique_identifiers { + context.add_new_reference(name.clone()); + } + + crate::react_compiler_reactive_scopes::prune_hoisted_contexts(&mut reactive_fn, env)?; + + if env.config.enable_preserve_existing_memoization_guarantees + || env.config.validate_preserve_existing_memoization_guarantees + { + crate::react_compiler_validation::validate_preserved_manual_memoization(&reactive_fn, env); + } + + // `codegen_function` already returns the oxc-shaped `CodegenFunction<'a>`. + let codegen_result = crate::react_compiler_reactive_scopes::codegen_function( + ast, + &reactive_fn, + env, + unique_identifiers, + fbt_operands, + )?; + + Ok(codegen_result) +} + +/// Log CompilerError diagnostics as CompileError events, matching TS `env.logErrors()` behavior. +/// These are logged for telemetry/lint output but not accumulated as compile errors. +fn log_errors_as_events(errors: &CompilerError, context: &mut ProgramContext) { + // Use the source_filename from the AST (set by parser's sourceFilename option). + // This is stored on the Environment during lowering. + let source_filename = context.source_filename(); + for detail in &errors.details { + let detail_info = match detail { + crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => { + let items: Option> = { + let v: Vec = d + .details + .iter() + .map(|item| { + match item { + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc, + message, + identifier_name, + } => CompilerErrorItemInfo { + kind: "error".to_string(), + loc: loc.as_ref().map(|l| LoggerSourceLocation { + start: LoggerPosition { + line: l.start.line, + column: l.start.column, + index: l.start.index, + }, + end: LoggerPosition { + line: l.end.line, + column: l.end.column, + index: l.end.index, + }, + filename: source_filename.clone(), + identifier_name: identifier_name.clone(), + }), + message: message.clone(), + }, + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Hint { + message, + } => CompilerErrorItemInfo { + kind: "hint".to_string(), + loc: None, + message: Some(message.clone()), + }, + } + }) + .collect(); + if v.is_empty() { None } else { Some(v) } + }; + CompilerErrorDetailInfo { + category: format!("{:?}", d.category), + reason: d.reason.clone(), + description: d.description.clone(), + severity: format!("{:?}", d.logged_severity()), + suggestions: None, + details: items, + loc: None, + } + } + crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => { + CompilerErrorDetailInfo { + category: format!("{:?}", d.category), + reason: d.reason.clone(), + description: d.description.clone(), + severity: format!("{:?}", d.logged_severity()), + suggestions: None, + details: None, + loc: None, + } + } + }; + context.log_event(super::compile_result::LoggerEvent::CompileError { + fn_loc: None, + detail: detail_info, + }); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler/entrypoint/plugin_options.rs b/crates/oxc_react_compiler/src/react_compiler/entrypoint/plugin_options.rs new file mode 100644 index 0000000000000..e4b1325547ba2 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/entrypoint/plugin_options.rs @@ -0,0 +1,82 @@ +use crate::react_compiler_hir::environment_config::EnvironmentConfig; + +/// Target configuration for the compiler +#[derive(Debug, Clone)] +pub enum CompilerTarget { + /// Standard React version target + Version(String), // "17", "18", "19" + /// Meta-internal target with custom runtime module + MetaInternal { + kind: String, // "donotuse_meta_internal" + runtime_module: String, + }, +} + +/// Gating configuration +#[derive(Debug, Clone)] +pub struct GatingConfig { + pub source: String, + pub import_specifier_name: String, +} + +/// Dynamic gating configuration +#[derive(Debug, Clone)] +pub struct DynamicGatingConfig { + pub source: String, +} + +/// Serializable plugin options, pre-resolved by the JS shim. +/// JS-only values (sources function, logger, etc.) are resolved before +/// being sent to Rust. +#[derive(Debug, Clone)] +pub struct PluginOptions { + // Pre-resolved by JS + pub should_compile: bool, + pub enable_reanimated: bool, + pub is_dev: bool, + pub filename: Option, + + // Pass-through options + pub compilation_mode: String, + pub panic_threshold: String, + pub target: CompilerTarget, + pub gating: Option, + pub dynamic_gating: Option, + pub no_emit: bool, + pub output_mode: Option, + pub eslint_suppression_rules: Option>, + pub flow_suppressions: bool, + pub ignore_use_no_forget: bool, + pub custom_opt_out_directives: Option>, + pub environment: EnvironmentConfig, + + /// Source code of the file being compiled (passed from Babel plugin for fast refresh hash). + pub source_code: Option, + + /// Enable profiling timing data collection. + pub profiling: bool, + + /// Enable debug logging (HIR formatting after each pass). + /// Only set to true when a logger with debugLogIRs is configured on the JS side. + pub debug: bool, +} + +/// Output mode for the compiler, derived from PluginOptions. +/// Matches the TS `compilerOutputMode` logic in Program.ts. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompilerOutputMode { + Ssr, + Client, + Lint, +} + +impl CompilerOutputMode { + pub fn from_opts(opts: &PluginOptions) -> Self { + match opts.output_mode.as_deref() { + Some("ssr") => Self::Ssr, + Some("lint") => Self::Lint, + _ if opts.no_emit => Self::Lint, + _ => Self::Client, + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler/entrypoint/program.rs b/crates/oxc_react_compiler/src/react_compiler/entrypoint/program.rs new file mode 100644 index 0000000000000..c99044f1b6f9e --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/entrypoint/program.rs @@ -0,0 +1,3627 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Main entrypoint for the React Compiler. +//! +//! This module is a port of Program.ts from the TypeScript compiler. It orchestrates +//! the compilation of a program by: +//! 1. Checking if compilation should be skipped +//! 2. Validating restricted imports +//! 3. Finding program-level suppressions +//! 4. Discovering functions to compile (components, hooks) +//! 5. Processing each function through the compilation pipeline +//! 6. Applying compiled functions back to the AST + +use oxc_ast::ast as oxc; +use oxc_span::Span; +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_diagnostics::CompilerErrorDetail; +use crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic; +use crate::react_compiler_diagnostics::ErrorCategory; +use crate::react_compiler_diagnostics::SourceLocation; +use crate::react_compiler_hir::ReactFunctionType; +use crate::react_compiler_hir::environment_config::EnvironmentConfig; +use crate::react_compiler_lowering::FunctionNode; +use crate::scope::ScopeId; +use crate::scope::ScopeInfo; + +use super::compile_result::BindingRenameInfo; +use super::compile_result::CodegenFunction; +use super::compile_result::CompileResult; +use super::compile_result::CompilerErrorDetailInfo; +use super::compile_result::CompilerErrorInfo; +use super::compile_result::CompilerErrorItemInfo; +use super::compile_result::DebugLogEntry; +use super::compile_result::LoggerEvent; +use super::compile_result::LoggerPosition; +use super::compile_result::LoggerSourceLocation; +use super::compile_result::LoggerSuggestionInfo; +use super::compile_result::LoggerSuggestionOp; +use super::compile_result::OrderedLogItem; +use super::imports::ProgramContext; +use super::imports::get_react_compiler_runtime_module; +use super::imports::validate_restricted_imports; +use super::pipeline; +use super::plugin_options::CompilerOutputMode; +use super::plugin_options::GatingConfig; +use super::plugin_options::PluginOptions; +use super::suppression::SuppressionRange; +use super::suppression::filter_suppressions_that_affect_function; +use super::suppression::find_program_suppressions; +use super::suppression::suppressions_to_compiler_error; + +// ----------------------------------------------------------------------- +// Constants +// ----------------------------------------------------------------------- + +const DEFAULT_ESLINT_SUPPRESSIONS: &[&str] = + &["react-hooks/exhaustive-deps", "react-hooks/rules-of-hooks"]; + +/// Directives that opt a function into memoization +const OPT_IN_DIRECTIVES: &[&str] = &["use forget", "use memo"]; + +/// Directives that opt a function out of memoization +const OPT_OUT_DIRECTIVES: &[&str] = &["use no forget", "use no memo"]; + +// ----------------------------------------------------------------------- +// Internal types +// ----------------------------------------------------------------------- + +/// A source location for a discovered function, used only to feed logger +/// events. The former Babel front-end carried this on `BaseNode.loc`; the oxc +/// front-end synthesizes it from the function span (only the byte `index` is +/// load-bearing — line/column/filename never reach the compiled output). +#[derive(Debug, Clone)] +struct FnSourceLoc { + start: FnSourcePos, + end: FnSourcePos, + filename: Option, + identifier_name: Option, +} + +#[derive(Debug, Clone)] +struct FnSourcePos { + line: u32, + column: u32, + index: Option, +} + +/// A function found in the program that should be compiled. +/// +/// `'a` is the arena lifetime of the discovered oxc function node. +#[allow(dead_code)] +struct CompileSource<'a> { + kind: CompileSourceKind, + original_kind: OriginalFnKind, + fn_name: Option, + /// Original AST source location for logger events. The example front-end + /// discards logger locations, and diagnostics carry their own byte spans, so + /// this is left `None` (it never affects compiled output). + fn_ast_loc: Option, + fn_start: Option, + fn_end: Option, + fn_node_id: Option, + fn_type: ReactFunctionType, + /// The discovered oxc function node, handed straight to lowering. + fn_node: FunctionNode<'a>, + /// Directive values from the function body (for opt-in/opt-out checks) + body_directives: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CompileSourceKind { + Original, + #[allow(dead_code)] + Outlined, +} + +// ----------------------------------------------------------------------- +// Directive helpers +// ----------------------------------------------------------------------- + +/// Check if any opt-in directive is present in the given directives. +/// Returns the first matching directive value, or None. +/// +/// Also checks for dynamic gating directives (`use memo if(...)`) +fn try_find_directive_enabling_memoization<'a>( + directives: &'a [String], + opts: &PluginOptions, +) -> Result, CompilerError> { + // Check standard opt-in directives + let opt_in = directives.iter().find(|d| OPT_IN_DIRECTIVES.contains(&d.as_str())); + if let Some(directive) = opt_in { + return Ok(Some(directive)); + } + + // Check dynamic gating directives + match find_directives_dynamic_gating(directives, opts) { + Ok(Some(result)) => Ok(Some(result.directive)), + Ok(None) => Ok(None), + Err(e) => Err(e), + } +} + +/// Check if any opt-out directive is present in the given directives. +fn find_directive_disabling_memoization<'a>( + directives: &'a [String], + opts: &PluginOptions, +) -> Option<&'a String> { + if let Some(ref custom_directives) = opts.custom_opt_out_directives { + directives.iter().find(|d| custom_directives.contains(d)) + } else { + directives.iter().find(|d| OPT_OUT_DIRECTIVES.contains(&d.as_str())) + } +} + +/// Result of a dynamic gating directive parse. +struct DynamicGatingResult<'a> { + #[allow(dead_code)] + directive: &'a String, + gating: GatingConfig, +} + +/// Check for dynamic gating directives like `use memo if(identifier)`. +/// Returns the directive and gating config if found, or an error if malformed. +fn find_directives_dynamic_gating<'a>( + directives: &'a [String], + opts: &PluginOptions, +) -> Result>, CompilerError> { + let dynamic_gating = match &opts.dynamic_gating { + Some(dg) => dg, + None => return Ok(None), + }; + + let mut errors: Vec = Vec::new(); + let mut matches: Vec<(&'a String, String)> = Vec::new(); + + for directive in directives { + if let Some(ident) = parse_dynamic_gating_directive(directive) { + if is_valid_identifier(ident) { + matches.push((directive, ident.to_string())); + } else { + let detail = CompilerErrorDetail::new( + ErrorCategory::Gating, + "Dynamic gating directive is not a valid JavaScript identifier", + ) + .with_description(format!("Found '{directive}'")); + errors.push(detail); + } + } + } + + if !errors.is_empty() { + let mut err = CompilerError::new(); + for e in errors { + err.push_error_detail(e); + } + return Err(err); + } + + if matches.len() > 1 { + let names: Vec = matches.iter().map(|(d, _)| (*d).clone()).collect(); + let mut err = CompilerError::new(); + let detail = CompilerErrorDetail::new( + ErrorCategory::Gating, + "Multiple dynamic gating directives found", + ) + .with_description(format!("Expected a single directive but found [{}]", names.join(", "))); + err.push_error_detail(detail); + return Err(err); + } + + if matches.len() == 1 { + Ok(Some(DynamicGatingResult { + directive: matches[0].0, + gating: GatingConfig { + source: dynamic_gating.source.clone(), + import_specifier_name: matches[0].1.clone(), + }, + })) + } else { + Ok(None) + } +} + +/// Parse a `use memo if()` directive, returning the condition. +/// Exact equivalent of the TS DYNAMIC_GATING_DIRECTIVE regex +/// `^use memo if\(([^\)]*)\)$`: the condition may not contain `)` and the +/// directive must end at the closing paren. +fn parse_dynamic_gating_directive(value: &str) -> Option<&str> { + let condition = value.strip_prefix("use memo if(")?.strip_suffix(')')?; + if condition.contains(')') { + return None; + } + Some(condition) +} + +/// Simple check for valid JavaScript identifier (alphanumeric + underscore + $, starting with letter/$/_ ) +/// Also rejects reserved words like `true`, `false`, `null`, etc. +fn is_valid_identifier(s: &str) -> bool { + if s.is_empty() { + return false; + } + let mut chars = s.chars(); + let first = chars.next().unwrap(); + if !first.is_alphabetic() && first != '_' && first != '$' { + return false; + } + if !chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$') { + return false; + } + // Check for reserved words (matching Babel's t.isValidIdentifier) + !matches!( + s, + "break" + | "case" + | "catch" + | "continue" + | "debugger" + | "default" + | "do" + | "else" + | "finally" + | "for" + | "function" + | "if" + | "in" + | "instanceof" + | "new" + | "return" + | "switch" + | "this" + | "throw" + | "try" + | "typeof" + | "var" + | "void" + | "while" + | "with" + | "class" + | "const" + | "enum" + | "export" + | "extends" + | "import" + | "super" + | "implements" + | "interface" + | "let" + | "package" + | "private" + | "protected" + | "public" + | "static" + | "yield" + | "null" + | "true" + | "false" + | "delete" + ) +} + +// ----------------------------------------------------------------------- +// Name helpers +// ----------------------------------------------------------------------- + +/// Check if a string follows the React hook naming convention (use[A-Z0-9]...). +fn is_hook_name(s: &str) -> bool { + let bytes = s.as_bytes(); + bytes.len() >= 4 + && bytes[0] == b'u' + && bytes[1] == b's' + && bytes[2] == b'e' + && bytes.get(3).map_or(false, |c| c.is_ascii_uppercase() || c.is_ascii_digit()) +} + +/// Check if a name looks like a React component (starts with uppercase letter). +fn is_component_name(name: &str) -> bool { + name.chars().next().map_or(false, |c| c.is_ascii_uppercase()) +} + +/// Check if an expression is a hook call (identifier with hook name, or +/// member expression `PascalCase.useHook`). +fn expr_is_hook(expr: &oxc::Expression) -> bool { + match expr { + oxc::Expression::Identifier(id) => is_hook_name(&id.name), + oxc::Expression::StaticMemberExpression(member) => { + // Property must be a hook name + if !is_hook_name(&member.property.name) { + return false; + } + // Object must be a PascalCase identifier + if let oxc::Expression::Identifier(obj) = &member.object { + obj.name.chars().next().map_or(false, |c| c.is_ascii_uppercase()) + } else { + false + } + } + _ => false, + } +} + +/// Whether an expression's sub-tree contains any optional chaining link. Mirrors +/// `convert_ast::ConvertCtx::expr_contains_optional`, used to decide whether a +/// `CallExpression` inside a `ChainExpression` was a regular call (before the +/// first `?.`, hook-checkable) or an optional call (not a hook). +fn expr_contains_optional(expr: &oxc::Expression) -> bool { + match expr { + oxc::Expression::CallExpression(c) => c.optional || expr_contains_optional(&c.callee), + oxc::Expression::StaticMemberExpression(m) => { + m.optional || expr_contains_optional(&m.object) + } + oxc::Expression::ComputedMemberExpression(m) => { + m.optional || expr_contains_optional(&m.object) + } + oxc::Expression::PrivateFieldExpression(p) => { + p.optional || expr_contains_optional(&p.object) + } + _ => false, + } +} + +/// Whether a `CallExpression` is a "regular" (non-optional) call. In Babel such a +/// call was a `CallExpression` (hook-checkable); optional / post-`?.` calls were +/// `OptionalCallExpression` (never treated as hooks). Matches the Babel-bridge +/// chain flattening exactly. +fn is_regular_call(call: &oxc::CallExpression) -> bool { + !call.optional && !expr_contains_optional(&call.callee) +} + +/// Get the inferred function name from a function's context. +/// +/// For FunctionDeclaration: uses the `id` field. +/// For FunctionExpression/ArrowFunctionExpression: infers from parent context +/// (VariableDeclarator, etc.) which is passed explicitly since we don't have Babel paths. +fn get_function_name_from_id(id: Option<&oxc::BindingIdentifier>) -> Option { + id.map(|id| id.name.to_string()) +} + +// ----------------------------------------------------------------------- +// AST traversal helpers +// ----------------------------------------------------------------------- + +/// Check if an expression is a "non-node" return value (indicating the function +/// is not a React component). This matches the TS `isNonNode` function. +fn is_non_node(expr: &oxc::Expression) -> bool { + matches!( + expr, + oxc::Expression::ObjectExpression(_) + | oxc::Expression::ArrowFunctionExpression(_) + | oxc::Expression::FunctionExpression(_) + | oxc::Expression::BigIntLiteral(_) + | oxc::Expression::ClassExpression(_) + | oxc::Expression::NewExpression(_) + ) +} + +/// Recursively check if a function body returns a non-React-node value. +/// Walks all return statements in the function (not in nested functions). +/// The last return statement visited (in DFS order) determines the result, +/// rather than short-circuiting on the first non-node return. +fn returns_non_node_in_stmts(stmts: &[oxc::Statement]) -> bool { + let mut result = false; + for stmt in stmts { + returns_non_node_in_stmt(stmt, &mut result); + } + result +} + +fn returns_non_node_in_stmt(stmt: &oxc::Statement, result: &mut bool) { + match stmt { + oxc::Statement::ReturnStatement(ret) => { + *result = match &ret.argument { + Some(arg) => is_non_node(arg), + None => true, // bare `return;` with no argument is a non-node value + }; + } + oxc::Statement::BlockStatement(block) => { + for s in &block.body { + returns_non_node_in_stmt(s, result); + } + } + oxc::Statement::IfStatement(if_stmt) => { + returns_non_node_in_stmt(&if_stmt.consequent, result); + if let Some(ref alt) = if_stmt.alternate { + returns_non_node_in_stmt(alt, result); + } + } + oxc::Statement::ForStatement(for_stmt) => returns_non_node_in_stmt(&for_stmt.body, result), + oxc::Statement::WhileStatement(while_stmt) => { + returns_non_node_in_stmt(&while_stmt.body, result) + } + oxc::Statement::DoWhileStatement(do_while) => { + returns_non_node_in_stmt(&do_while.body, result) + } + oxc::Statement::ForInStatement(for_in) => returns_non_node_in_stmt(&for_in.body, result), + oxc::Statement::ForOfStatement(for_of) => returns_non_node_in_stmt(&for_of.body, result), + oxc::Statement::SwitchStatement(switch) => { + for case in &switch.cases { + for s in &case.consequent { + returns_non_node_in_stmt(s, result); + } + } + } + oxc::Statement::TryStatement(try_stmt) => { + for s in &try_stmt.block.body { + returns_non_node_in_stmt(s, result); + } + if let Some(ref handler) = try_stmt.handler { + for s in &handler.body.body { + returns_non_node_in_stmt(s, result); + } + } + if let Some(ref finalizer) = try_stmt.finalizer { + for s in &finalizer.body { + returns_non_node_in_stmt(s, result); + } + } + } + oxc::Statement::LabeledStatement(labeled) => { + returns_non_node_in_stmt(&labeled.body, result) + } + oxc::Statement::WithStatement(with) => returns_non_node_in_stmt(&with.body, result), + // Skip nested function/class declarations -- they have their own returns. + // All other statements (incl. TS-only declarations) are opaque here. + _ => {} + } +} + +/// Check if a function returns non-node values. +/// For arrow functions with expression body, checks the expression directly. +/// For block bodies, walks the statements. +fn returns_non_node_fn(params: &oxc::FormalParameters, body: &FunctionBody) -> bool { + let _ = params; + match body { + FunctionBody::Block(block) => returns_non_node_in_stmts(&block.statements), + FunctionBody::Expression(expr) => is_non_node(expr), + } +} + +/// Check if a function body calls hooks or creates JSX. +/// Traverses the function body (not nested functions) looking for: +/// - CallExpression where callee is a hook +/// - JSXElement or JSXFragment +fn calls_hooks_or_creates_jsx_in_stmts(stmts: &[oxc::Statement]) -> bool { + for stmt in stmts { + if calls_hooks_or_creates_jsx_in_stmt(stmt) { + return true; + } + } + false +} + +fn calls_hooks_or_creates_jsx_in_stmt(stmt: &oxc::Statement) -> bool { + match stmt { + oxc::Statement::ExpressionStatement(expr_stmt) => { + calls_hooks_or_creates_jsx_in_expr(&expr_stmt.expression) + } + oxc::Statement::ReturnStatement(ret) => { + if let Some(ref arg) = ret.argument { + calls_hooks_or_creates_jsx_in_expr(arg) + } else { + false + } + } + oxc::Statement::VariableDeclaration(var_decl) => { + for decl in &var_decl.declarations { + if let Some(ref init) = decl.init { + if calls_hooks_or_creates_jsx_in_expr(init) { + return true; + } + } + } + false + } + oxc::Statement::BlockStatement(block) => calls_hooks_or_creates_jsx_in_stmts(&block.body), + oxc::Statement::IfStatement(if_stmt) => { + calls_hooks_or_creates_jsx_in_expr(&if_stmt.test) + || calls_hooks_or_creates_jsx_in_stmt(&if_stmt.consequent) + || if_stmt + .alternate + .as_ref() + .map_or(false, |alt| calls_hooks_or_creates_jsx_in_stmt(alt)) + } + oxc::Statement::ForStatement(for_stmt) => { + if let Some(ref init) = for_stmt.init { + match init { + oxc::ForStatementInit::VariableDeclaration(var_decl) => { + for decl in &var_decl.declarations { + if let Some(ref init) = decl.init { + if calls_hooks_or_creates_jsx_in_expr(init) { + return true; + } + } + } + } + // An expression `ForStatementInit` is an `Expression` (the + // enum inherits the Expression variants). + expr => { + if let Some(expr) = expr.as_expression() { + if calls_hooks_or_creates_jsx_in_expr(expr) { + return true; + } + } + } + } + } + if let Some(ref test) = for_stmt.test { + if calls_hooks_or_creates_jsx_in_expr(test) { + return true; + } + } + if let Some(ref update) = for_stmt.update { + if calls_hooks_or_creates_jsx_in_expr(update) { + return true; + } + } + calls_hooks_or_creates_jsx_in_stmt(&for_stmt.body) + } + oxc::Statement::WhileStatement(while_stmt) => { + calls_hooks_or_creates_jsx_in_expr(&while_stmt.test) + || calls_hooks_or_creates_jsx_in_stmt(&while_stmt.body) + } + oxc::Statement::DoWhileStatement(do_while) => { + calls_hooks_or_creates_jsx_in_stmt(&do_while.body) + || calls_hooks_or_creates_jsx_in_expr(&do_while.test) + } + oxc::Statement::ForInStatement(for_in) => { + calls_hooks_or_creates_jsx_in_expr(&for_in.right) + || calls_hooks_or_creates_jsx_in_stmt(&for_in.body) + } + oxc::Statement::ForOfStatement(for_of) => { + calls_hooks_or_creates_jsx_in_expr(&for_of.right) + || calls_hooks_or_creates_jsx_in_stmt(&for_of.body) + } + oxc::Statement::SwitchStatement(switch) => { + if calls_hooks_or_creates_jsx_in_expr(&switch.discriminant) { + return true; + } + for case in &switch.cases { + if let Some(ref test) = case.test { + if calls_hooks_or_creates_jsx_in_expr(test) { + return true; + } + } + if calls_hooks_or_creates_jsx_in_stmts(&case.consequent) { + return true; + } + } + false + } + oxc::Statement::ThrowStatement(throw) => { + calls_hooks_or_creates_jsx_in_expr(&throw.argument) + } + oxc::Statement::TryStatement(try_stmt) => { + if calls_hooks_or_creates_jsx_in_stmts(&try_stmt.block.body) { + return true; + } + if let Some(ref handler) = try_stmt.handler { + if calls_hooks_or_creates_jsx_in_stmts(&handler.body.body) { + return true; + } + } + if let Some(ref finalizer) = try_stmt.finalizer { + if calls_hooks_or_creates_jsx_in_stmts(&finalizer.body) { + return true; + } + } + false + } + oxc::Statement::LabeledStatement(labeled) => { + calls_hooks_or_creates_jsx_in_stmt(&labeled.body) + } + oxc::Statement::WithStatement(with) => { + calls_hooks_or_creates_jsx_in_expr(&with.object) + || calls_hooks_or_creates_jsx_in_stmt(&with.body) + } + // Nested function declarations have their own returns; class bodies in the + // Babel bridge carried no extracted hook/JSX metadata (always `false`), so + // class declarations never contributed here. All other statements (incl. + // TS-only declarations) are opaque. + _ => false, + } +} + +fn calls_hooks_or_creates_jsx_in_expr(expr: &oxc::Expression) -> bool { + /// Whether an argument expression is a nested function (skipped by the walk). + fn is_nested_fn(arg: &oxc::Expression) -> bool { + matches!( + arg, + oxc::Expression::ArrowFunctionExpression(_) | oxc::Expression::FunctionExpression(_) + ) + } + + match expr { + // JSX creates + oxc::Expression::JSXElement(_) | oxc::Expression::JSXFragment(_) => true, + + // Hook calls. Only a "regular" (non-optional) call's callee is hook-checked; + // optional calls (Babel `OptionalCallExpression`) never count as hooks, but + // their callee/args are still searched. + oxc::Expression::CallExpression(call) => { + if is_regular_call(call) && expr_is_hook(&call.callee) { + return true; + } + // Also check arguments for JSX/hooks (but not nested functions) + if calls_hooks_or_creates_jsx_in_expr(&call.callee) { + return true; + } + for arg in &call.arguments { + // Skip function arguments -- they are nested functions + if let Some(arg) = arg.as_expression() { + if is_nested_fn(arg) { + continue; + } + if calls_hooks_or_creates_jsx_in_expr(arg) { + return true; + } + } else if let oxc::Argument::SpreadElement(s) = arg { + if calls_hooks_or_creates_jsx_in_expr(&s.argument) { + return true; + } + } + } + false + } + // Optional chaining (`a?.b`, `a?.()`): Babel modeled these as + // Optional{Member,Call}Expression. Recurse into the inner element, where + // per-call hook checks honor the optional flag via `is_regular_call`. + oxc::Expression::ChainExpression(chain) => { + calls_hooks_or_creates_jsx_in_chain_element(&chain.expression) + } + + // Binary/logical + oxc::Expression::BinaryExpression(bin) => { + calls_hooks_or_creates_jsx_in_expr(&bin.left) + || calls_hooks_or_creates_jsx_in_expr(&bin.right) + } + oxc::Expression::LogicalExpression(log) => { + calls_hooks_or_creates_jsx_in_expr(&log.left) + || calls_hooks_or_creates_jsx_in_expr(&log.right) + } + oxc::Expression::ConditionalExpression(cond) => { + calls_hooks_or_creates_jsx_in_expr(&cond.test) + || calls_hooks_or_creates_jsx_in_expr(&cond.consequent) + || calls_hooks_or_creates_jsx_in_expr(&cond.alternate) + } + oxc::Expression::AssignmentExpression(assign) => { + calls_hooks_or_creates_jsx_in_expr(&assign.right) + } + oxc::Expression::SequenceExpression(seq) => { + seq.expressions.iter().any(calls_hooks_or_creates_jsx_in_expr) + } + oxc::Expression::UnaryExpression(unary) => { + calls_hooks_or_creates_jsx_in_expr(&unary.argument) + } + oxc::Expression::UpdateExpression(update) => match &update.argument { + oxc::SimpleAssignmentTarget::AssignmentTargetIdentifier(_) => false, + target => { + target.as_member_expression().map_or(false, calls_hooks_or_creates_jsx_in_member) + } + }, + oxc::Expression::StaticMemberExpression(member) => { + calls_hooks_or_creates_jsx_in_expr(&member.object) + } + oxc::Expression::ComputedMemberExpression(member) => { + calls_hooks_or_creates_jsx_in_expr(&member.object) + || calls_hooks_or_creates_jsx_in_expr(&member.expression) + } + oxc::Expression::PrivateFieldExpression(member) => { + calls_hooks_or_creates_jsx_in_expr(&member.object) + } + oxc::Expression::AwaitExpression(await_expr) => { + calls_hooks_or_creates_jsx_in_expr(&await_expr.argument) + } + oxc::Expression::YieldExpression(yield_expr) => yield_expr + .argument + .as_ref() + .map_or(false, |arg| calls_hooks_or_creates_jsx_in_expr(arg)), + oxc::Expression::TaggedTemplateExpression(tagged) => { + calls_hooks_or_creates_jsx_in_expr(&tagged.tag) + || tagged.quasi.expressions.iter().any(calls_hooks_or_creates_jsx_in_expr) + } + oxc::Expression::TemplateLiteral(tl) => { + tl.expressions.iter().any(calls_hooks_or_creates_jsx_in_expr) + } + oxc::Expression::ArrayExpression(arr) => arr.elements.iter().any(|e| match e { + oxc::ArrayExpressionElement::SpreadElement(s) => { + calls_hooks_or_creates_jsx_in_expr(&s.argument) + } + oxc::ArrayExpressionElement::Elision(_) => false, + other => other.as_expression().map_or(false, calls_hooks_or_creates_jsx_in_expr), + }), + oxc::Expression::ObjectExpression(obj) => obj.properties.iter().any(|prop| match prop { + oxc::ObjectPropertyKind::SpreadProperty(s) => { + calls_hooks_or_creates_jsx_in_expr(&s.argument) + } + // Object methods (`{ foo() {} }`, getters/setters): Babel modeled these + // as `ObjectMethod` and traversed their body statements. Regular + // properties traverse their value (nested functions are skipped). + oxc::ObjectPropertyKind::ObjectProperty(p) => { + if p.method || matches!(p.kind, oxc::PropertyKind::Get | oxc::PropertyKind::Set) { + if let oxc::Expression::FunctionExpression(func) = &p.value { + if let Some(body) = &func.body { + return calls_hooks_or_creates_jsx_in_stmts(&body.statements); + } + } + false + } else { + calls_hooks_or_creates_jsx_in_expr(&p.value) + } + } + }), + oxc::Expression::ParenthesizedExpression(paren) => { + calls_hooks_or_creates_jsx_in_expr(&paren.expression) + } + oxc::Expression::TSAsExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression), + oxc::Expression::TSSatisfiesExpression(ts) => { + calls_hooks_or_creates_jsx_in_expr(&ts.expression) + } + oxc::Expression::TSNonNullExpression(ts) => { + calls_hooks_or_creates_jsx_in_expr(&ts.expression) + } + oxc::Expression::TSTypeAssertion(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression), + oxc::Expression::TSInstantiationExpression(ts) => { + calls_hooks_or_creates_jsx_in_expr(&ts.expression) + } + oxc::Expression::NewExpression(new) => { + if calls_hooks_or_creates_jsx_in_expr(&new.callee) { + return true; + } + new.arguments.iter().any(|a| { + if let Some(a) = a.as_expression() { + if is_nested_fn(a) { + return false; + } + calls_hooks_or_creates_jsx_in_expr(a) + } else if let oxc::Argument::SpreadElement(s) = a { + calls_hooks_or_creates_jsx_in_expr(&s.argument) + } else { + false + } + }) + } + + // Nested functions are skipped; class expressions carried no extracted + // hook/JSX metadata in the Babel bridge (always `false`). Leaf expressions + // fall through to `false`. + _ => false, + } +} + +/// Search a `ChainElement` (`a?.b`, `a?.()`, ...) for hook calls / JSX, mirroring +/// the Babel `Optional{Member,Call}Expression` traversal. Optional calls never +/// count as hooks, but their callee/args are searched. +fn calls_hooks_or_creates_jsx_in_chain_element(element: &oxc::ChainElement) -> bool { + match element { + oxc::ChainElement::CallExpression(call) => { + if is_regular_call(call) && expr_is_hook(&call.callee) { + return true; + } + if calls_hooks_or_creates_jsx_in_expr(&call.callee) { + return true; + } + call.arguments.iter().any(|arg| { + if let Some(arg) = arg.as_expression() { + !matches!( + arg, + oxc::Expression::ArrowFunctionExpression(_) + | oxc::Expression::FunctionExpression(_) + ) && calls_hooks_or_creates_jsx_in_expr(arg) + } else if let oxc::Argument::SpreadElement(s) = arg { + calls_hooks_or_creates_jsx_in_expr(&s.argument) + } else { + false + } + }) + } + oxc::ChainElement::StaticMemberExpression(m) => { + calls_hooks_or_creates_jsx_in_expr(&m.object) + } + oxc::ChainElement::ComputedMemberExpression(m) => { + calls_hooks_or_creates_jsx_in_expr(&m.object) + || calls_hooks_or_creates_jsx_in_expr(&m.expression) + } + oxc::ChainElement::PrivateFieldExpression(m) => { + calls_hooks_or_creates_jsx_in_expr(&m.object) + } + oxc::ChainElement::TSNonNullExpression(t) => { + calls_hooks_or_creates_jsx_in_expr(&t.expression) + } + } +} + +/// Search a member expression (object side) for hook calls / JSX. +fn calls_hooks_or_creates_jsx_in_member(member: &oxc::MemberExpression) -> bool { + match member { + oxc::MemberExpression::StaticMemberExpression(m) => { + calls_hooks_or_creates_jsx_in_expr(&m.object) + } + oxc::MemberExpression::ComputedMemberExpression(m) => { + calls_hooks_or_creates_jsx_in_expr(&m.object) + || calls_hooks_or_creates_jsx_in_expr(&m.expression) + } + oxc::MemberExpression::PrivateFieldExpression(m) => { + calls_hooks_or_creates_jsx_in_expr(&m.object) + } + } +} + +/// Check if a function body calls hooks or creates JSX. +fn calls_hooks_or_creates_jsx(params: &oxc::FormalParameters, body: &FunctionBody) -> bool { + // Check default param values (TS traverses the whole function node including params) + if calls_hooks_or_creates_jsx_in_params(params) { + return true; + } + match body { + FunctionBody::Block(block) => calls_hooks_or_creates_jsx_in_stmts(&block.statements), + FunctionBody::Expression(expr) => calls_hooks_or_creates_jsx_in_expr(expr), + } +} + +/// Check if any parameter default values contain hooks or JSX. +/// +/// Babel traversed the whole function node including params; default values live +/// on `FormalParameter.initializer`, and the binding pattern may itself contain +/// nested defaults. +fn calls_hooks_or_creates_jsx_in_params(params: &oxc::FormalParameters) -> bool { + for param in ¶ms.items { + if let Some(init) = ¶m.initializer { + if calls_hooks_or_creates_jsx_in_expr(init) { + return true; + } + } + if calls_hooks_or_creates_jsx_in_binding(¶m.pattern) { + return true; + } + } + if let Some(rest) = ¶ms.rest { + if calls_hooks_or_creates_jsx_in_binding(&rest.rest.argument) { + return true; + } + } + false +} + +fn calls_hooks_or_creates_jsx_in_binding(pattern: &oxc::BindingPattern) -> bool { + match pattern { + oxc::BindingPattern::BindingIdentifier(_) => false, + oxc::BindingPattern::ObjectPattern(obj) => { + obj.properties.iter().any(|p| calls_hooks_or_creates_jsx_in_binding(&p.value)) + || obj + .rest + .as_ref() + .map_or(false, |r| calls_hooks_or_creates_jsx_in_binding(&r.argument)) + } + oxc::BindingPattern::ArrayPattern(arr) => { + arr.elements + .iter() + .any(|e| e.as_ref().map_or(false, calls_hooks_or_creates_jsx_in_binding)) + || arr + .rest + .as_ref() + .map_or(false, |r| calls_hooks_or_creates_jsx_in_binding(&r.argument)) + } + oxc::BindingPattern::AssignmentPattern(assign) => { + calls_hooks_or_creates_jsx_in_expr(&assign.right) + || calls_hooks_or_creates_jsx_in_binding(&assign.left) + } + } +} + +/// Check if a parameter's type annotation is valid for a React component prop. +/// Returns false for primitive type annotations that indicate this is NOT a component. +fn is_valid_props_annotation(type_annotation: Option<&oxc::TSTypeAnnotation>) -> bool { + let Some(annotation) = type_annotation else { + return true; // No annotation = valid + }; + // Mirrors the Babel-bridge `babel_ts_type_name` of the param's annotation; the + // disallowed TS keyword/type set. (Flow types never appear in the oxc AST.) + !matches!( + &annotation.type_annotation, + oxc::TSType::TSArrayType(_) + | oxc::TSType::TSBigIntKeyword(_) + | oxc::TSType::TSBooleanKeyword(_) + | oxc::TSType::TSConstructorType(_) + | oxc::TSType::TSFunctionType(_) + | oxc::TSType::TSLiteralType(_) + | oxc::TSType::TSNeverKeyword(_) + | oxc::TSType::TSNumberKeyword(_) + | oxc::TSType::TSStringKeyword(_) + | oxc::TSType::TSSymbolKeyword(_) + | oxc::TSType::TSTupleType(_) + ) +} + +/// Check if the function parameters are valid for a React component. +/// Components can have 0 params, 1 param (props), or 2 params (props + ref). +/// +/// The Babel reference treated the rest parameter as a trailing `RestElement` in +/// the flat params list; in oxc the rest lives in `params.rest`. So the logical +/// param count is `items.len() + rest.is_some()`. +fn is_valid_component_params(params: &oxc::FormalParameters) -> bool { + let logical_len = params.items.len() + usize::from(params.rest.is_some()); + if logical_len == 0 { + return true; + } + if logical_len > 2 { + return false; + } + // First param cannot be a rest element. If there are no `items`, the only param + // is the rest -> invalid. + let Some(first) = params.items.first() else { + return false; + }; + // Check type annotation on first param. + if !is_valid_props_annotation(first.type_annotation.as_deref()) { + return false; + } + if logical_len == 1 { + return true; + } + // Second param: either a second `item` or the rest. The rest is not an + // identifier, so it never looks like a ref. + match params.items.get(1) { + Some(second) => match &second.pattern { + oxc::BindingPattern::BindingIdentifier(id) => { + id.name.contains("ref") || id.name.contains("Ref") + } + _ => false, + }, + None => false, + } +} + +// ----------------------------------------------------------------------- +// Unified function body type for traversal +// ----------------------------------------------------------------------- + +/// Abstraction over function body types to simplify traversal code +enum FunctionBody<'a> { + Block(&'a oxc::FunctionBody<'a>), + Expression(&'a oxc::Expression<'a>), +} + +// ----------------------------------------------------------------------- +// Function type detection +// ----------------------------------------------------------------------- + +/// Determine the React function type for a function, given the compilation mode +/// and the function's name and context. +/// +/// This is the Rust equivalent of `getReactFunctionType` in Program.ts. +fn get_react_function_type( + name: Option<&str>, + params: &oxc::FormalParameters, + body: &FunctionBody, + body_directives: &[String], + is_declaration: bool, + parent_callee_name: Option<&str>, + opts: &PluginOptions, + is_component_declaration: bool, + is_hook_declaration: bool, +) -> Option { + // Check for opt-in directives in the function body + if let FunctionBody::Block(_) = body { + let opt_in = try_find_directive_enabling_memoization(body_directives, opts); + if let Ok(Some(_)) = opt_in { + // If there's an opt-in directive, use name heuristics but fall back to Other + return Some( + get_component_or_hook_like(name, params, body, parent_callee_name) + .unwrap_or(ReactFunctionType::Other), + ); + } + } + + // Component and hook declarations are known components/hooks + // (Flow `component Foo() { ... }` and `hook useFoo() { ... }` syntax, + // detected via __componentDeclaration / __hookDeclaration from the Hermes parser) + let component_syntax_type = if is_declaration { + if is_component_declaration { + Some(ReactFunctionType::Component) + } else if is_hook_declaration { + Some(ReactFunctionType::Hook) + } else { + None + } + } else { + None + }; + + match opts.compilation_mode.as_str() { + "annotation" => { + // opt-ins were checked above + None + } + "infer" => { + // Check if this is a component or hook-like function + component_syntax_type + .or_else(|| get_component_or_hook_like(name, params, body, parent_callee_name)) + } + "syntax" => { + // In syntax mode, only compile declared components/hooks + component_syntax_type + } + "all" => Some( + get_component_or_hook_like(name, params, body, parent_callee_name) + .unwrap_or(ReactFunctionType::Other), + ), + _ => None, + } +} + +/// Determine if a function looks like a React component or hook based on +/// naming conventions and code patterns. +/// +/// Adapted from the ESLint rule at +/// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js +fn get_component_or_hook_like( + name: Option<&str>, + params: &oxc::FormalParameters, + body: &FunctionBody, + parent_callee_name: Option<&str>, +) -> Option { + if let Some(fn_name) = name { + if is_component_name(fn_name) { + // Check if it actually looks like a component + let is_component = calls_hooks_or_creates_jsx(params, body) + && is_valid_component_params(params) + && !returns_non_node_fn(params, body); + return if is_component { Some(ReactFunctionType::Component) } else { None }; + } else if is_hook_name(fn_name) { + // Hooks have hook invocations or JSX, but can take any # of arguments + return if calls_hooks_or_creates_jsx(params, body) { + Some(ReactFunctionType::Hook) + } else { + None + }; + } + } + + // For unnamed functions, check if they are forwardRef/memo callbacks + if let Some(callee_name) = parent_callee_name { + if callee_name == "forwardRef" || callee_name == "memo" { + return if calls_hooks_or_creates_jsx(params, body) { + Some(ReactFunctionType::Component) + } else { + None + }; + } + } + + None +} + +/// Extract the callee name from a CallExpression if it's a React API call +/// (forwardRef, memo, React.forwardRef, React.memo). +fn get_callee_name_if_react_api<'e>(callee: &'e oxc::Expression) -> Option<&'e str> { + match callee { + oxc::Expression::Identifier(id) => { + if id.name == "forwardRef" || id.name == "memo" { + Some(id.name.as_str()) + } else { + None + } + } + oxc::Expression::StaticMemberExpression(member) => { + if let oxc::Expression::Identifier(obj) = &member.object { + if obj.name == "React" + && (member.property.name == "forwardRef" || member.property.name == "memo") + { + return Some(member.property.name.as_str()); + } + } + None + } + _ => None, + } +} + +// ----------------------------------------------------------------------- +// Error handling +// ----------------------------------------------------------------------- + +/// Convert CompilerDiagnostic details into serializable CompilerErrorItemInfo items. +fn diagnostic_details_to_items( + d: &crate::react_compiler_diagnostics::CompilerDiagnostic, + filename: Option<&str>, +) -> Option> { + let items: Vec = d + .details + .iter() + .map(|item| match item { + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc, + message, + identifier_name, + } => CompilerErrorItemInfo { + kind: "error".to_string(), + loc: loc.as_ref().map(|l| { + let mut logger_loc = diag_loc_to_logger_loc(l, filename); + logger_loc.identifier_name = identifier_name.clone(); + logger_loc + }), + message: message.clone(), + }, + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Hint { message } => { + CompilerErrorItemInfo { + kind: "hint".to_string(), + loc: None, + message: Some(message.clone()), + } + } + }) + .collect(); + if items.is_empty() { None } else { Some(items) } +} + +/// Convert an optional AST SourceLocation to a LoggerSourceLocation with filename. +fn to_logger_loc( + ast_loc: Option<&FnSourceLoc>, + filename: Option<&str>, +) -> Option { + ast_loc.map(|loc| LoggerSourceLocation { + start: LoggerPosition { + line: loc.start.line, + column: loc.start.column, + index: loc.start.index, + }, + end: LoggerPosition { line: loc.end.line, column: loc.end.column, index: loc.end.index }, + filename: filename.map(|s| s.to_string()), + identifier_name: loc.identifier_name.clone(), + }) +} + +/// Convert a diagnostics SourceLocation to a LoggerSourceLocation with filename. +fn diag_loc_to_logger_loc(loc: &SourceLocation, filename: Option<&str>) -> LoggerSourceLocation { + LoggerSourceLocation { + start: LoggerPosition { + line: loc.start.line, + column: loc.start.column, + index: loc.start.index, + }, + end: LoggerPosition { line: loc.end.line, column: loc.end.column, index: loc.end.index }, + filename: filename.map(|s| s.to_string()), + identifier_name: None, + } +} + +/// Convert diagnostic suggestions to logger suggestion infos. +fn suggestions_to_logger( + suggestions: &Option>, +) -> Option> { + suggestions.as_ref().map(|suggestions| { + suggestions + .iter() + .map(|s| { + let op = match s.op { + crate::react_compiler_diagnostics::CompilerSuggestionOperation::InsertBefore => { + LoggerSuggestionOp::InsertBefore + } + crate::react_compiler_diagnostics::CompilerSuggestionOperation::InsertAfter => { + LoggerSuggestionOp::InsertAfter + } + crate::react_compiler_diagnostics::CompilerSuggestionOperation::Remove => { + LoggerSuggestionOp::Remove + } + crate::react_compiler_diagnostics::CompilerSuggestionOperation::Replace => { + LoggerSuggestionOp::Replace + } + }; + LoggerSuggestionInfo { + description: s.description.clone(), + op, + range: s.range, + text: s.text.clone(), + } + }) + .collect() + }) +} + +/// Log an error as LoggerEvent(s) directly onto the ProgramContext. +fn log_error(err: &CompilerError, fn_ast_loc: Option<&FnSourceLoc>, context: &mut ProgramContext) { + // Use the filename from the AST node's loc (set by parser's sourceFilename option), + // not from plugin options (which may have a different prefix like '/'). + let source_filename = fn_ast_loc.and_then(|loc| loc.filename.as_deref()); + let fn_loc = to_logger_loc(fn_ast_loc, source_filename); + + // Detect simulated unknown exception (throwUnknownException__testonly). + // In TS, non-CompilerError exceptions are logged as PipelineError with the + // error message as data. Emit the same event shape. + let is_simulated_unknown = err.details.len() == 1 + && err.details.iter().all(|d| match d { + CompilerErrorOrDiagnostic::ErrorDetail(d) => { + d.category == ErrorCategory::Invariant && d.reason == "unexpected error" + } + _ => false, + }); + if is_simulated_unknown { + context.log_event(LoggerEvent::PipelineError { + fn_loc: fn_loc.clone(), + data: "Error: unexpected error".to_string(), + }); + return; + } + + for detail in &err.details { + let detail_info = match detail { + CompilerErrorOrDiagnostic::Diagnostic(d) => CompilerErrorDetailInfo { + category: format!("{:?}", d.category), + reason: d.reason.clone(), + description: d.description.clone(), + severity: format!("{:?}", d.logged_severity()), + suggestions: suggestions_to_logger(&d.suggestions), + details: diagnostic_details_to_items(d, source_filename), + loc: None, + }, + CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerErrorDetailInfo { + category: format!("{:?}", d.category), + reason: d.reason.clone(), + description: d.description.clone(), + severity: format!("{:?}", d.logged_severity()), + suggestions: suggestions_to_logger(&d.suggestions), + details: None, + loc: d.loc.as_ref().map(|l| diag_loc_to_logger_loc(l, source_filename)), + }, + }; + // Use CompileErrorWithLoc when fn_loc is present to match TS field ordering + if let Some(ref loc) = fn_loc { + context.log_event(LoggerEvent::CompileErrorWithLoc { + fn_loc: loc.clone(), + detail: detail_info, + }); + } else { + context.log_event(LoggerEvent::CompileError { fn_loc: None, detail: detail_info }); + } + } +} + +/// Handle an error according to the panicThreshold setting. +/// Returns Some(CompileResult::Error) if the error should be surfaced as fatal, +/// otherwise returns None (error was logged only). +fn handle_error( + err: &CompilerError, + fn_ast_loc: Option<&FnSourceLoc>, + context: &mut ProgramContext, +) -> Option { + // Log the error + log_error(err, fn_ast_loc, context); + + let should_panic = match context.opts.panic_threshold.as_str() { + "all_errors" => true, + "critical_errors" => err.has_errors(), + _ => false, + }; + + // Config errors always cause a panic + let is_config_error = err.details.iter().any(|d| match d { + CompilerErrorOrDiagnostic::Diagnostic(d) => d.category == ErrorCategory::Config, + CompilerErrorOrDiagnostic::ErrorDetail(d) => d.category == ErrorCategory::Config, + }); + + if should_panic || is_config_error { + let source_fn = context.source_filename(); + let mut error_info = compiler_error_to_info(err, source_fn.as_deref()); + + // Detect simulated unknown exception (throwUnknownException__testonly). + // In the TS compiler, this throws a plain Error('unexpected error'), not + // a CompilerError. Set rawMessage so the JS side throws with the raw + // message instead of formatting through formatCompilerError(). + let is_simulated_unknown = err.details.len() == 1 + && err.details.iter().all(|d| match d { + CompilerErrorOrDiagnostic::ErrorDetail(d) => { + d.category == ErrorCategory::Invariant && d.reason == "unexpected error" + } + _ => false, + }); + if is_simulated_unknown { + error_info.raw_message = Some("unexpected error".to_string()); + } + + // Pre-format the error message in Rust when possible, so the JS + // shim can use it directly instead of calling formatCompilerError(). + if error_info.raw_message.is_none() { + if let Some(ref source) = context.code { + error_info.formatted_message = + Some(crate::react_compiler_diagnostics::code_frame::format_compiler_error( + err, + source, + source_fn.as_deref(), + )); + } + } + + Some(CompileResult::Error { + error: error_info, + events: context.events.clone(), + ordered_log: context.ordered_log.clone(), + timing: Vec::new(), + }) + } else { + None + } +} + +/// Convert a diagnostics CompilerError to a serializable CompilerErrorInfo. +fn compiler_error_to_info(err: &CompilerError, filename: Option<&str>) -> CompilerErrorInfo { + let details: Vec = err + .details + .iter() + .map(|d| match d { + CompilerErrorOrDiagnostic::Diagnostic(d) => CompilerErrorDetailInfo { + category: format!("{:?}", d.category), + reason: d.reason.clone(), + description: d.description.clone(), + severity: format!("{:?}", d.severity()), + suggestions: suggestions_to_logger(&d.suggestions), + details: diagnostic_details_to_items(d, filename), + loc: None, + }, + CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerErrorDetailInfo { + category: format!("{:?}", d.category), + reason: d.reason.clone(), + description: d.description.clone(), + severity: format!("{:?}", d.severity()), + suggestions: suggestions_to_logger(&d.suggestions), + details: None, + loc: d.loc.as_ref().map(|l| diag_loc_to_logger_loc(l, filename)), + }, + }) + .collect(); + + let (reason, description) = details + .first() + .map(|d| (d.reason.clone(), d.description.clone())) + .unwrap_or_else(|| ("Unknown error".to_string(), None)); + + CompilerErrorInfo { reason, description, details, raw_message: None, formatted_message: None } +} + +// ----------------------------------------------------------------------- +// Compilation pipeline stubs +// ----------------------------------------------------------------------- + +/// Attempt to compile a single function. +/// +/// Returns `CodegenFunction` on success or `CompilerError` on failure. +/// Debug log entries are accumulated on `context.debug_logs`. +fn try_compile_function<'a>( + ast: &oxc_ast::AstBuilder<'a>, + source: &CompileSource, + scope_info: &ScopeInfo, + output_mode: CompilerOutputMode, + env_config: &EnvironmentConfig, + context: &mut ProgramContext, +) -> Result, CompilerError> { + // Check for suppressions that affect this function + if let (Some(start), Some(end)) = (source.fn_start, source.fn_end) { + let affecting = filter_suppressions_that_affect_function(&context.suppressions, start, end); + if !affecting.is_empty() { + let owned: Vec = affecting.into_iter().cloned().collect(); + let mut err = suppressions_to_compiler_error(&owned); + // Suppression errors are returned (not thrown), so they should NOT + // trigger CompileUnexpectedThrow. + err.is_thrown = false; + return Err(err); + } + } + + // Run the compilation pipeline directly on the oxc function node discovered + // during the program walk. + pipeline::compile_fn( + ast, + &source.fn_node, + source.fn_name.as_deref(), + scope_info, + source.fn_type, + output_mode, + env_config, + context, + ) +} + +/// Process a single function: check directives, attempt compilation, handle results. +/// +/// Returns `Ok(Some(codegen_fn))` when the function was compiled and should be applied, +/// `Ok(None)` when the function was skipped or lint-only, +/// or `Err(CompileResult)` if a fatal error should short-circuit the program. +fn process_fn<'a>( + ast: &oxc_ast::AstBuilder<'a>, + source: &CompileSource, + scope_info: &ScopeInfo, + output_mode: CompilerOutputMode, + env_config: &EnvironmentConfig, + context: &mut ProgramContext, +) -> Result>, CompileResult> { + // Parse directives from the function body + let opt_in_result = + try_find_directive_enabling_memoization(&source.body_directives, &context.opts); + let opt_out = find_directive_disabling_memoization(&source.body_directives, &context.opts); + + // If parsing opt-in directive fails, handle the error and skip + let opt_in = match opt_in_result { + Ok(d) => d, + Err(err) => { + // Apply panic threshold logic (same as compilation errors) + if let Some(result) = handle_error(&err, source.fn_ast_loc.as_ref(), context) { + return Err(result); + } + return Ok(None); + } + }; + + // Attempt compilation + let compile_result = + try_compile_function(ast, source, scope_info, output_mode, env_config, context); + + match compile_result { + Err(err) => { + // Emit CompileUnexpectedThrow for errors that were "thrown" from a pass + // (not accumulated via env.record_error) and have all non-Invariant details. + // Matches TS tryCompileFunction() catch block behavior. + if err.is_thrown && err.is_all_non_invariant() { + let source_filename = + source.fn_ast_loc.as_ref().and_then(|loc| loc.filename.as_deref()); + context.log_event(LoggerEvent::CompileUnexpectedThrow { + fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename), + data: err.to_string_for_event(), + }); + } + + if opt_out.is_some() { + // If there's an opt-out, just log the error (don't escalate) + log_error(&err, source.fn_ast_loc.as_ref(), context); + } else { + // Apply panic threshold logic + if let Some(result) = handle_error(&err, source.fn_ast_loc.as_ref(), context) { + return Err(result); + } + } + Ok(None) + } + Ok(codegen_fn) => { + // Check opt-out + if !context.opts.ignore_use_no_forget && opt_out.is_some() { + let opt_out_value = opt_out.unwrap(); + let source_filename = + source.fn_ast_loc.as_ref().and_then(|loc| loc.filename.as_deref()); + context.log_event(LoggerEvent::CompileSkip { + fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename), + reason: format!("Skipped due to '{opt_out_value}' directive."), + // The directive's own source location only fed logger events, + // which the example front-end discards. + loc: None, + }); + // The function is skipped due to opt-out. Do NOT register the memo + // cache import here — it will be registered in apply_compiled_functions() + // only for functions that are actually applied to the output. + return Ok(None); + } + + // Log success with memo stats from CodegenFunction + let source_filename = + source.fn_ast_loc.as_ref().and_then(|loc| loc.filename.as_deref()); + context.log_event(LoggerEvent::CompileSuccess { + fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename), + fn_name: codegen_fn.id.as_ref().map(|id| id.name.to_string()), + memo_slots: codegen_fn.memo_slots_used, + memo_blocks: codegen_fn.memo_blocks, + memo_values: codegen_fn.memo_values, + pruned_memo_blocks: codegen_fn.pruned_memo_blocks, + pruned_memo_values: codegen_fn.pruned_memo_values, + }); + + // Check module scope opt-out + if context.has_module_scope_opt_out { + return Ok(None); + } + + // Check output mode — lint mode doesn't apply compiled functions + if output_mode == CompilerOutputMode::Lint { + return Ok(None); + } + + // Check annotation mode + if context.opts.compilation_mode == "annotation" && opt_in.is_none() { + return Ok(None); + } + + Ok(Some(codegen_fn)) + } + } +} + +// ----------------------------------------------------------------------- +// Import checking +// ----------------------------------------------------------------------- + +/// Check if the program already has a `c` import from the React Compiler runtime module. +/// If so, the file was already compiled and should be skipped. +fn has_memo_cache_function_import(program: &oxc::Program, module_name: &str) -> bool { + for stmt in &program.body { + if let oxc::Statement::ImportDeclaration(import) = stmt { + if import.source.value == module_name { + if let Some(specifiers) = &import.specifiers { + for specifier in specifiers { + if let oxc::ImportDeclarationSpecifier::ImportSpecifier(data) = specifier { + let imported_name = match &data.imported { + oxc::ModuleExportName::IdentifierName(id) => Some(id.name.as_str()), + oxc::ModuleExportName::IdentifierReference(id) => { + Some(id.name.as_str()) + } + oxc::ModuleExportName::StringLiteral(s) => Some(s.value.as_str()), + }; + if imported_name == Some("c") { + return true; + } + } + } + } + } + } + } + false +} + +/// Check if compilation should be skipped for this program. +fn should_skip_compilation(program: &oxc::Program, options: &PluginOptions) -> bool { + let runtime_module = get_react_compiler_runtime_module(&options.target); + has_memo_cache_function_import(program, &runtime_module) +} + +// ----------------------------------------------------------------------- +// Function discovery +// ----------------------------------------------------------------------- + +/// Collect the directive value strings of a function body (for opt-in/opt-out +/// checks). Matches the Babel-bridge directive values (`d.expression.value`). +fn body_directive_values(body: &oxc::FunctionBody) -> Vec { + body.directives.iter().map(|d| d.expression.value.to_string()).collect() +} + +/// Try to create a `CompileSource` from a discovered oxc function node. +/// +/// `fn_node` is the oxc function node, `span` its byte range (`span.start` is the +/// node id), `name` the inferred function name, `original_kind` the syntactic kind, +/// and `parent_callee_name` the enclosing forwardRef/memo callee (if any). +fn try_make_compile_source<'a>( + fn_node: FunctionNode<'a>, + name: Option, + original_kind: OriginalFnKind, + parent_callee_name: Option, + opts: &PluginOptions, + context: &mut ProgramContext, +) -> Option> { + let (params, body, span, body_directives) = match fn_node { + FunctionNode::Function(f) => { + // Bodyless functions (`declare function`, overload signatures, + // `TSDeclareFunction`) are never compiled; Babel modeled these as + // separate, non-traversed statement kinds. + let block = f.body.as_deref()?; + (&f.params, FunctionBody::Block(block), f.span, body_directive_values(block)) + } + FunctionNode::Arrow(a) => { + let (body, directives) = if a.expression { + // Expression-bodied arrow: the single body statement is an + // `ExpressionStatement` wrapping the expression. + let expr = a.get_expression().expect("expression-bodied arrow has an expression"); + (FunctionBody::Expression(expr), Vec::new()) + } else { + (FunctionBody::Block(&a.body), body_directive_values(&a.body)) + }; + (&a.params, body, a.span, directives) + } + }; + + let node_id = span.start; + + // Skip if already compiled (identified by node_id). + if context.is_already_compiled(node_id) { + return None; + } + + let fn_type = get_react_function_type( + name.as_deref(), + params, + &body, + &body_directives, + // Flow `component`/`hook` declaration syntax never appears in the oxc AST. + false, + parent_callee_name.as_deref(), + opts, + false, + false, + )?; + + context.mark_compiled(node_id); + + Some(CompileSource { + kind: CompileSourceKind::Original, + original_kind, + fn_name: name, + // The function source location flows into compile-error diagnostics as the + // fallback labeled span (offset/length). Only the byte `index` is + // load-bearing; line/column/filename never reach the example's output. + fn_ast_loc: Some(FnSourceLoc { + start: FnSourcePos { line: 0, column: 0, index: Some(span.start) }, + end: FnSourcePos { line: 0, column: 0, index: Some(span.end) }, + filename: None, + identifier_name: None, + }), + fn_start: Some(span.start), + fn_end: Some(span.end), + fn_node_id: Some(node_id), + fn_type, + fn_node, + body_directives, + }) +} + +/// Get the variable declarator name (for inferring function names from +/// `const Foo = () => {}`). +fn get_declarator_name(decl: &oxc::VariableDeclarator) -> Option { + match &decl.id { + oxc::BindingPattern::BindingIdentifier(id) => Some(id.name.to_string()), + _ => None, + } +} + +// ----------------------------------------------------------------------- +// Discovery walker +// ----------------------------------------------------------------------- + +/// Walks the oxc `Program` to find compilable functions, mirroring the +/// TypeScript compiler's Babel `program.traverse` behavior one-for-one. +/// +/// Replicates the former Babel `AstWalker` + `FunctionDiscoveryVisitor`: +/// - scope tracking via `ScopeInfo` (`node_id_to_scope`, keyed by `span.start`); +/// - `loop_expression_depth` for loop test/right positions (while.test, +/// do-while.test, for-in.right, for-of.right), which Babel treats as +/// non-program-scope in 'all' mode; +/// - parent context: `current_declarator_name` for `const Foo = () => {}` and +/// `parent_callee_stack` for forwardRef/memo wrappers; +/// - in 'all' mode, rejects functions whose scope depth `> 2` (program + +/// function) or that sit in a loop expression position. +/// +/// Compiled functions have their bodies skipped (Babel's `fn.skip()`); other +/// functions are descended to find nested component/hook declarations. Classes +/// are entered only structurally (their bodies carry no compilable functions for +/// discovery — matching the Babel bridge, which extracted no metadata for them). +struct DiscoveryWalker<'a, 'ast> { + scope_info: &'a ScopeInfo, + opts: &'a PluginOptions, + context: &'a mut ProgramContext, + queue: Vec>, + scope_stack: Vec, + loop_expression_depth: usize, + current_declarator_name: Option, + parent_callee_stack: Vec>, +} + +impl<'a, 'ast> DiscoveryWalker<'a, 'ast> { + fn new( + scope_info: &'a ScopeInfo, + opts: &'a PluginOptions, + context: &'a mut ProgramContext, + ) -> Self { + Self { + scope_info, + opts, + context, + queue: Vec::new(), + scope_stack: Vec::new(), + loop_expression_depth: 0, + current_declarator_name: None, + parent_callee_stack: Vec::new(), + } + } + + /// Try to push the scope a node creates. Returns whether one was pushed. + fn try_push_scope(&mut self, span: Span) -> bool { + if let Some(scope_id) = self.scope_info.resolve_scope_for_node(Some(span.start)) { + self.scope_stack.push(scope_id); + true + } else { + false + } + } + + /// In 'all' mode, reject functions that are not at program scope. The + /// function's own scope is on the stack, so a top-level function has + /// `len == 2` (program + function); deeper means a nested scope. + fn is_rejected_by_scope_check(&self) -> bool { + self.opts.compilation_mode == "all" + && (self.scope_stack.len() > 2 || self.loop_expression_depth > 0) + } + + fn current_parent_callee(&self) -> Option { + self.parent_callee_stack.last().and_then(|opt| opt.clone()) + } + + fn walk_program(&mut self, program: &'ast oxc::Program<'ast>) { + let pushed = self.try_push_scope(program.span); + for stmt in &program.body { + self.walk_statement(stmt); + } + if pushed { + self.scope_stack.pop(); + } + } + + fn walk_block(&mut self, block: &'ast oxc::BlockStatement<'ast>) { + let pushed = self.try_push_scope(block.span); + for stmt in &block.body { + self.walk_statement(stmt); + } + if pushed { + self.scope_stack.pop(); + } + } + + fn walk_function_body_block(&mut self, body: &'ast oxc::FunctionBody<'ast>) { + // A function body BlockStatement shares the function's scope in the Babel + // model and never gets its own scope entry, so do not push a scope here. + for stmt in &body.statements { + self.walk_statement(stmt); + } + } + + fn walk_statement(&mut self, stmt: &'ast oxc::Statement<'ast>) { + match stmt { + oxc::Statement::BlockStatement(node) => self.walk_block(node), + oxc::Statement::ReturnStatement(node) => { + if let Some(arg) = &node.argument { + self.walk_expression(arg); + } + } + oxc::Statement::ExpressionStatement(node) => self.walk_expression(&node.expression), + oxc::Statement::IfStatement(node) => { + self.walk_expression(&node.test); + self.walk_statement(&node.consequent); + if let Some(alt) = &node.alternate { + self.walk_statement(alt); + } + } + oxc::Statement::ForStatement(node) => { + let pushed = self.try_push_scope(node.span); + if let Some(init) = &node.init { + match init { + oxc::ForStatementInit::VariableDeclaration(decl) => { + self.walk_variable_declaration(decl); + } + expr => { + if let Some(expr) = expr.as_expression() { + self.walk_expression(expr); + } + } + } + } + if let Some(test) = &node.test { + self.walk_expression(test); + } + if let Some(update) = &node.update { + self.walk_expression(update); + } + self.walk_statement(&node.body); + if pushed { + self.scope_stack.pop(); + } + } + oxc::Statement::WhileStatement(node) => { + self.loop_expression_depth += 1; + self.walk_expression(&node.test); + self.loop_expression_depth -= 1; + self.walk_statement(&node.body); + } + oxc::Statement::DoWhileStatement(node) => { + self.walk_statement(&node.body); + self.loop_expression_depth += 1; + self.walk_expression(&node.test); + self.loop_expression_depth -= 1; + } + oxc::Statement::ForInStatement(node) => { + let pushed = self.try_push_scope(node.span); + self.walk_for_left(&node.left); + self.loop_expression_depth += 1; + self.walk_expression(&node.right); + self.loop_expression_depth -= 1; + self.walk_statement(&node.body); + if pushed { + self.scope_stack.pop(); + } + } + oxc::Statement::ForOfStatement(node) => { + let pushed = self.try_push_scope(node.span); + self.walk_for_left(&node.left); + self.loop_expression_depth += 1; + self.walk_expression(&node.right); + self.loop_expression_depth -= 1; + self.walk_statement(&node.body); + if pushed { + self.scope_stack.pop(); + } + } + oxc::Statement::SwitchStatement(node) => { + let pushed = self.try_push_scope(node.span); + self.walk_expression(&node.discriminant); + for case in &node.cases { + if let Some(test) = &case.test { + self.walk_expression(test); + } + for consequent in &case.consequent { + self.walk_statement(consequent); + } + } + if pushed { + self.scope_stack.pop(); + } + } + oxc::Statement::ThrowStatement(node) => self.walk_expression(&node.argument), + oxc::Statement::TryStatement(node) => { + self.walk_block(&node.block); + if let Some(handler) = &node.handler { + let pushed = self.try_push_scope(handler.span); + self.walk_block(&handler.body); + if pushed { + self.scope_stack.pop(); + } + } + if let Some(finalizer) = &node.finalizer { + self.walk_block(finalizer); + } + } + oxc::Statement::LabeledStatement(node) => self.walk_statement(&node.body), + oxc::Statement::VariableDeclaration(node) => self.walk_variable_declaration(node), + oxc::Statement::FunctionDeclaration(node) => self.walk_function(node, None), + oxc::Statement::WithStatement(node) => { + self.walk_expression(&node.object); + self.walk_statement(&node.body); + } + oxc::Statement::ExportNamedDeclaration(node) => { + if let Some(decl) = &node.declaration { + self.walk_declaration(decl); + } + } + oxc::Statement::ExportDefaultDeclaration(node) => { + self.walk_export_default(&node.declaration); + } + // Classes are not descended into (their bodies hold no functions for + // discovery); all remaining statements (TS-only declarations, imports, + // break/continue, etc.) carry no compilable functions. + _ => {} + } + } + + fn walk_for_left(&mut self, left: &'ast oxc::ForStatementLeft<'ast>) { + if let oxc::ForStatementLeft::VariableDeclaration(decl) = left { + self.walk_variable_declaration(decl); + } + // Assignment-target lefts contain no functions to discover. + } + + fn walk_variable_declaration(&mut self, decl: &'ast oxc::VariableDeclaration<'ast>) { + for declarator in &decl.declarations { + // Only infer the declarator name when the init is a direct function + // expression, arrow, or call expression (for forwardRef/memo wrappers). + if let Some(init) = &declarator.init { + if matches!( + init, + oxc::Expression::FunctionExpression(_) + | oxc::Expression::ArrowFunctionExpression(_) + | oxc::Expression::CallExpression(_) + ) { + self.current_declarator_name = get_declarator_name(declarator); + } + self.walk_expression(init); + } + self.current_declarator_name = None; + } + } + + fn walk_declaration(&mut self, decl: &'ast oxc::Declaration<'ast>) { + match decl { + oxc::Declaration::FunctionDeclaration(node) => self.walk_function(node, None), + oxc::Declaration::VariableDeclaration(node) => self.walk_variable_declaration(node), + // TS-only declarations have no runtime expressions / functions. + _ => {} + } + } + + fn walk_export_default(&mut self, decl: &'ast oxc::ExportDefaultDeclarationKind<'ast>) { + match decl { + oxc::ExportDefaultDeclarationKind::FunctionDeclaration(node) => { + self.walk_function(node, None) + } + other => { + if let Some(expr) = other.as_expression() { + self.walk_expression(expr); + } + } + } + } + + /// Walk an oxc `Function` node (declaration or expression). `inferred_name`, + /// when `Some`, supplies the name from the enclosing variable declarator (for + /// function expressions); `None` means use the function's own id. + fn walk_function(&mut self, func: &'ast oxc::Function<'ast>, inferred_name: Option) { + let pushed = self.try_push_scope(func.span); + + let original_kind = match func.r#type { + oxc::FunctionType::FunctionDeclaration | oxc::FunctionType::TSDeclareFunction => { + OriginalFnKind::FunctionDeclaration + } + _ => OriginalFnKind::FunctionExpression, + }; + + let skip_body = if self.is_rejected_by_scope_check() { + false + } else { + // TS `getFunctionName` for FunctionDeclaration uses the node's own id; + // for FunctionExpression it uses only the parent context (declarator). + let name = match original_kind { + OriginalFnKind::FunctionDeclaration => get_function_name_from_id(func.id.as_ref()), + _ => inferred_name, + }; + let parent_callee = self.current_parent_callee(); + if let Some(source) = try_make_compile_source( + FunctionNode::Function(func), + name, + original_kind, + parent_callee, + self.opts, + self.context, + ) { + self.queue.push(source); + true + } else { + false + } + }; + + if !skip_body { + // Babel `fn.skip()` is only called for compiled functions; other + // functions are descended to find nested declarations. + if let Some(body) = &func.body { + self.walk_function_body_block(body); + } + } + + if pushed { + self.scope_stack.pop(); + } + } + + fn walk_arrow( + &mut self, + arrow: &'ast oxc::ArrowFunctionExpression<'ast>, + inferred_name: Option, + ) { + let pushed = self.try_push_scope(arrow.span); + + let skip_body = if self.is_rejected_by_scope_check() { + false + } else { + let parent_callee = self.current_parent_callee(); + if let Some(source) = try_make_compile_source( + FunctionNode::Arrow(arrow), + inferred_name, + OriginalFnKind::ArrowFunctionExpression, + parent_callee, + self.opts, + self.context, + ) { + self.queue.push(source); + true + } else { + false + } + }; + + if !skip_body { + for stmt in &arrow.body.statements { + self.walk_statement(stmt); + } + } + + if pushed { + self.scope_stack.pop(); + } + } + + fn walk_expression(&mut self, expr: &'ast oxc::Expression<'ast>) { + match expr { + oxc::Expression::FunctionExpression(node) => { + // The declarator name flows into the function expression and is + // consumed here (so siblings don't inherit it). + let name = self.current_declarator_name.take(); + self.walk_function(node, name); + } + oxc::Expression::ArrowFunctionExpression(node) => { + let name = self.current_declarator_name.take(); + self.walk_arrow(node, name); + } + oxc::Expression::CallExpression(node) => { + let callee_name = get_callee_name_if_react_api(&node.callee).map(|s| s.to_string()); + // The declarator name only flows through forwardRef/memo calls; for + // any other call, clear it so nested functions don't inherit it. + if callee_name.is_none() { + self.current_declarator_name = None; + } + self.parent_callee_stack.push(callee_name); + self.walk_expression(&node.callee); + for arg in &node.arguments { + self.walk_argument(arg); + } + let was_react_api = self.parent_callee_stack.pop().flatten().is_some(); + if was_react_api { + self.current_declarator_name = None; + } + } + oxc::Expression::ChainExpression(node) => self.walk_chain_element(&node.expression), + oxc::Expression::StaticMemberExpression(node) => self.walk_expression(&node.object), + oxc::Expression::ComputedMemberExpression(node) => { + self.walk_expression(&node.object); + self.walk_expression(&node.expression); + } + oxc::Expression::PrivateFieldExpression(node) => self.walk_expression(&node.object), + oxc::Expression::BinaryExpression(node) => { + self.walk_expression(&node.left); + self.walk_expression(&node.right); + } + oxc::Expression::LogicalExpression(node) => { + self.walk_expression(&node.left); + self.walk_expression(&node.right); + } + oxc::Expression::UnaryExpression(node) => self.walk_expression(&node.argument), + oxc::Expression::UpdateExpression(node) => { + if let Some(member) = node.argument.as_member_expression() { + self.walk_member_expression(member); + } + } + oxc::Expression::ConditionalExpression(node) => { + self.walk_expression(&node.test); + self.walk_expression(&node.consequent); + self.walk_expression(&node.alternate); + } + oxc::Expression::AssignmentExpression(node) => self.walk_expression(&node.right), + oxc::Expression::SequenceExpression(node) => { + for e in &node.expressions { + self.walk_expression(e); + } + } + oxc::Expression::ObjectExpression(node) => { + for prop in &node.properties { + self.walk_object_property(prop); + } + } + oxc::Expression::ArrayExpression(node) => { + for el in &node.elements { + match el { + oxc::ArrayExpressionElement::SpreadElement(s) => { + self.walk_expression(&s.argument) + } + oxc::ArrayExpressionElement::Elision(_) => {} + other => { + if let Some(e) = other.as_expression() { + self.walk_expression(e); + } + } + } + } + } + oxc::Expression::NewExpression(node) => { + self.walk_expression(&node.callee); + for arg in &node.arguments { + self.walk_argument(arg); + } + } + oxc::Expression::TemplateLiteral(node) => { + for e in &node.expressions { + self.walk_expression(e); + } + } + oxc::Expression::TaggedTemplateExpression(node) => { + self.walk_expression(&node.tag); + for e in &node.quasi.expressions { + self.walk_expression(e); + } + } + oxc::Expression::AwaitExpression(node) => self.walk_expression(&node.argument), + oxc::Expression::YieldExpression(node) => { + if let Some(arg) = &node.argument { + self.walk_expression(arg); + } + } + oxc::Expression::ParenthesizedExpression(node) => { + self.walk_expression(&node.expression) + } + oxc::Expression::JSXElement(node) => self.walk_jsx_element(node), + oxc::Expression::JSXFragment(node) => self.walk_jsx_children(&node.children), + oxc::Expression::TSAsExpression(node) => self.walk_expression(&node.expression), + oxc::Expression::TSSatisfiesExpression(node) => self.walk_expression(&node.expression), + oxc::Expression::TSNonNullExpression(node) => self.walk_expression(&node.expression), + oxc::Expression::TSTypeAssertion(node) => self.walk_expression(&node.expression), + oxc::Expression::TSInstantiationExpression(node) => { + self.walk_expression(&node.expression) + } + // ClassExpression bodies and leaf expressions are not descended. + _ => {} + } + } + + fn walk_argument(&mut self, arg: &'ast oxc::Argument<'ast>) { + if let Some(expr) = arg.as_expression() { + self.walk_expression(expr); + } else if let oxc::Argument::SpreadElement(s) = arg { + self.walk_expression(&s.argument); + } + } + + fn walk_member_expression(&mut self, member: &'ast oxc::MemberExpression<'ast>) { + match member { + oxc::MemberExpression::StaticMemberExpression(m) => self.walk_expression(&m.object), + oxc::MemberExpression::ComputedMemberExpression(m) => { + self.walk_expression(&m.object); + self.walk_expression(&m.expression); + } + oxc::MemberExpression::PrivateFieldExpression(m) => self.walk_expression(&m.object), + } + } + + fn walk_chain_element(&mut self, element: &'ast oxc::ChainElement<'ast>) { + match element { + oxc::ChainElement::CallExpression(node) => { + self.walk_expression(&node.callee); + for arg in &node.arguments { + self.walk_argument(arg); + } + } + oxc::ChainElement::StaticMemberExpression(m) => self.walk_expression(&m.object), + oxc::ChainElement::ComputedMemberExpression(m) => { + self.walk_expression(&m.object); + self.walk_expression(&m.expression); + } + oxc::ChainElement::PrivateFieldExpression(m) => self.walk_expression(&m.object), + oxc::ChainElement::TSNonNullExpression(t) => self.walk_expression(&t.expression), + } + } + + fn walk_object_property(&mut self, prop: &'ast oxc::ObjectPropertyKind<'ast>) { + match prop { + oxc::ObjectPropertyKind::SpreadProperty(s) => self.walk_expression(&s.argument), + oxc::ObjectPropertyKind::ObjectProperty(p) => { + if p.computed { + self.walk_property_key(&p.key); + } + // Object methods (`{ foo() {} }`, getters/setters): Babel modeled + // these as `ObjectMethod` and walked the body without queuing the + // method itself; the body is the value FunctionExpression's body. + let is_method = + p.method || matches!(p.kind, oxc::PropertyKind::Get | oxc::PropertyKind::Set); + if is_method { + if let oxc::Expression::FunctionExpression(func) = &p.value { + let pushed = self.try_push_scope(func.span); + if let Some(body) = &func.body { + self.walk_function_body_block(body); + } + if pushed { + self.scope_stack.pop(); + } + } + } else { + self.walk_expression(&p.value); + } + } + } + } + + fn walk_property_key(&mut self, key: &'ast oxc::PropertyKey<'ast>) { + if let Some(expr) = key.as_expression() { + self.walk_expression(expr); + } + } + + fn walk_jsx_element(&mut self, node: &'ast oxc::JSXElement<'ast>) { + for attr in &node.opening_element.attributes { + match attr { + oxc::JSXAttributeItem::Attribute(a) => { + if let Some(value) = &a.value { + match value { + oxc::JSXAttributeValue::ExpressionContainer(c) => { + self.walk_jsx_container(c) + } + oxc::JSXAttributeValue::Element(el) => self.walk_jsx_element(el), + oxc::JSXAttributeValue::Fragment(f) => { + self.walk_jsx_children(&f.children) + } + oxc::JSXAttributeValue::StringLiteral(_) => {} + } + } + } + oxc::JSXAttributeItem::SpreadAttribute(a) => self.walk_expression(&a.argument), + } + } + self.walk_jsx_children(&node.children); + } + + fn walk_jsx_children(&mut self, children: &'ast oxc_allocator::Vec<'ast, oxc::JSXChild<'ast>>) { + for child in children { + match child { + oxc::JSXChild::Element(el) => self.walk_jsx_element(el), + oxc::JSXChild::Fragment(f) => self.walk_jsx_children(&f.children), + oxc::JSXChild::ExpressionContainer(c) => self.walk_jsx_container(c), + oxc::JSXChild::Spread(s) => self.walk_expression(&s.expression), + oxc::JSXChild::Text(_) => {} + } + } + } + + fn walk_jsx_container(&mut self, container: &'ast oxc::JSXExpressionContainer<'ast>) { + if let Some(expr) = container.expression.as_expression() { + self.walk_expression(expr); + } + } +} + +/// Find all functions in the program that should be compiled by walking the oxc +/// `Program` directly. See [`DiscoveryWalker`] for the traversal semantics. +fn find_functions_to_compile<'ast>( + program: &'ast oxc::Program<'ast>, + opts: &PluginOptions, + context: &mut ProgramContext, + scope: &ScopeInfo, +) -> Vec> { + let mut walker = DiscoveryWalker::new(scope, opts, context); + walker.walk_program(program); + walker.queue +} + +// ----------------------------------------------------------------------- +// Main entry point +// ----------------------------------------------------------------------- + +/// A successfully compiled function, ready to be applied to the AST. +/// +/// `'a` is the arena lifetime of the compiled oxc nodes; `'s` borrows the +/// discovery's `CompileSource`, itself parameterized by the program-borrow `'p`. +struct CompiledFunction<'a, 'p, 's> { + #[allow(dead_code)] + kind: CompileSourceKind, + #[allow(dead_code)] + source: &'s CompileSource<'p>, + #[allow(dead_code)] + codegen_fn: CodegenFunction<'a>, +} + +/// The type of the original function node, used to determine what kind of +/// replacement node to create. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OriginalFnKind { + FunctionDeclaration, + FunctionExpression, + ArrowFunctionExpression, +} + +// ============================================================================= +// oxc splice +// +// Builds the final oxc `Program` by substituting each compiled oxc function (from +// codegen) for its original — matched by `span.start == fn_node_id` — applying +// gating, inserting outlined functions, and adding the memo-cache / gating imports. +// ============================================================================= + +/// An owned, oxc-shaped compiled function ready to splice into the program. +pub(crate) struct OxcReplacement<'a> { + fn_node_id: Option, + original_kind: OriginalFnKind, + codegen_fn: CodegenFunction<'a>, + gating: Option, +} + +/// Copy the TS metadata (type annotation, decorators, optional/modifier flags) +/// from a function's original parameters onto the compiled replacement parameters, +/// matched positionally. Mirrors the Babel reference's signature restoration for +/// functions that are not memoized: the parameter bindings are unchanged, so their +/// types carry through. The compiled params (from codegen) never carry types. +fn copy_param_ts_metadata<'a>( + allocator: &'a oxc_allocator::Allocator, + new_params: &mut oxc_ast::ast::FormalParameters<'a>, + source_params: &oxc_ast::ast::FormalParameters<'a>, +) { + use oxc_allocator::CloneIn; + for (param, source) in new_params.items.iter_mut().zip(source_params.items.iter()) { + param.decorators = source.decorators.clone_in(allocator); + param.type_annotation = source.type_annotation.clone_in(allocator); + param.optional = source.optional; + param.accessibility = source.accessibility; + param.readonly = source.readonly; + param.r#override = source.r#override; + } + if let (Some(rest), Some(source_rest)) = (&mut new_params.rest, &source_params.rest) { + rest.decorators = source_rest.decorators.clone_in(allocator); + rest.type_annotation = source_rest.type_annotation.clone_in(allocator); + } +} + +/// Build an oxc `Function` from a compiled codegen function. `r#type` selects +/// declaration vs expression. Mirrors the Babel `ReplaceFnVisitor` field copy. +fn ox_build_function<'a>( + ast: &oxc_ast::AstBuilder<'a>, + codegen: &CodegenFunction<'a>, + fn_type: oxc_ast::ast::FunctionType, +) -> oxc_allocator::Box<'a, oxc_ast::ast::Function<'a>> { + use oxc_allocator::CloneIn; + use oxc_span::SPAN; + ast.alloc_function( + SPAN, + fn_type, + codegen.id.clone_in(ast.allocator), + codegen.generator, + codegen.is_async, + false, + None::>, + None::>, + codegen.params.clone_in(ast.allocator), + None::>, + Some(codegen.body.clone_in(ast.allocator)), + ) +} + +/// Build the compiled replacement as an `Expression`, matching the original node +/// kind (arrow vs function expression). Mirrors `build_compiled_expression_matching_kind`. +fn ox_build_compiled_expression<'a>( + ast: &oxc_ast::AstBuilder<'a>, + codegen: &CodegenFunction<'a>, + original_kind: OriginalFnKind, +) -> oxc_ast::ast::Expression<'a> { + use oxc_allocator::CloneIn; + use oxc_span::SPAN; + match original_kind { + OriginalFnKind::ArrowFunctionExpression => { + oxc_ast::ast::Expression::ArrowFunctionExpression(ast.alloc_arrow_function_expression( + SPAN, + false, + codegen.is_async, + None::>, + codegen.params.clone_in(ast.allocator), + None::>, + codegen.body.clone_in(ast.allocator), + )) + } + _ => oxc_ast::ast::Expression::FunctionExpression(ox_build_function( + ast, + codegen, + oxc_ast::ast::FunctionType::FunctionExpression, + )), + } +} + +/// Visitor that replaces a compiled function in the oxc AST by matching `span.start`. +/// Mirrors the Babel `ReplaceFnVisitor`. +struct OxcReplaceFnVisitor<'a, 'b> { + ast: &'b oxc_ast::AstBuilder<'a>, + node_id: u32, + codegen: &'b CodegenFunction<'a>, + done: bool, +} + +impl<'a, 'b> oxc_ast_visit::VisitMut<'a> for OxcReplaceFnVisitor<'a, 'b> { + fn visit_function( + &mut self, + func: &mut oxc_ast::ast::Function<'a>, + flags: oxc_syntax::scope::ScopeFlags, + ) { + if self.done { + return; + } + if func.span.start == self.node_id { + use oxc_allocator::CloneIn; + // When the compiled function does not initialize a memo cache, the body is + // left essentially intact, so the original TS signature (type parameters, + // `this` parameter, return type, and per-parameter type annotations) is + // preserved. Functions that memoize drop these types, mirroring Babel. + let keep_types = self.codegen.memo_slots_used == 0; + let mut params = self.codegen.params.clone_in(self.ast.allocator); + if keep_types { + copy_param_ts_metadata(self.ast.allocator, &mut params, &func.params); + } else { + func.type_parameters = None; + func.return_type = None; + func.this_param = None; + } + func.id = self.codegen.id.clone_in(self.ast.allocator); + func.params = params; + func.body = Some(self.codegen.body.clone_in(self.ast.allocator)); + func.generator = self.codegen.generator; + func.r#async = self.codegen.is_async; + func.declare = false; + self.done = true; + return; + } + oxc_ast_visit::walk_mut::walk_function(self, func, flags); + } + + fn visit_arrow_function_expression( + &mut self, + arrow: &mut oxc_ast::ast::ArrowFunctionExpression<'a>, + ) { + if self.done { + return; + } + if arrow.span.start == self.node_id { + use oxc_allocator::CloneIn; + let keep_types = self.codegen.memo_slots_used == 0; + let mut params = self.codegen.params.clone_in(self.ast.allocator); + if keep_types { + copy_param_ts_metadata(self.ast.allocator, &mut params, &arrow.params); + } else { + arrow.type_parameters = None; + arrow.return_type = None; + } + arrow.params = params; + arrow.body = self.codegen.body.clone_in(self.ast.allocator); + arrow.r#async = self.codegen.is_async; + arrow.expression = false; + self.done = true; + return; + } + oxc_ast_visit::walk_mut::walk_arrow_function_expression(self, arrow); + } +} + +/// Visitor that replaces a function (matched by `span.start`) with a gated +/// conditional expression. Mirrors the Babel `ReplaceWithGatedVisitor`. +struct OxcReplaceWithGatedVisitor<'a, 'b> { + ast: &'b oxc_ast::AstBuilder<'a>, + node_id: u32, + gating_expression: &'b oxc_ast::ast::Expression<'a>, + /// Pending `export default Name;` to insert after a named export-default fn. + export_default_name: Option, + done: bool, +} + +impl<'a, 'b> OxcReplaceWithGatedVisitor<'a, 'b> { + /// Build `const = ;` + fn build_const_decl(&self, name: &str) -> oxc_ast::ast::Statement<'a> { + use oxc_allocator::CloneIn; + use oxc_span::SPAN; + let declarator = self.ast.variable_declarator( + SPAN, + oxc_ast::ast::VariableDeclarationKind::Const, + self.ast.binding_pattern_binding_identifier(SPAN, ox_atom(self.ast, name)), + None::>, + Some(self.gating_expression.clone_in(self.ast.allocator)), + false, + ); + oxc_ast::ast::Statement::VariableDeclaration(self.ast.alloc_variable_declaration( + SPAN, + oxc_ast::ast::VariableDeclarationKind::Const, + self.ast.vec1(declarator), + false, + )) + } +} + +impl<'a, 'b> oxc_ast_visit::VisitMut<'a> for OxcReplaceWithGatedVisitor<'a, 'b> { + fn visit_statements( + &mut self, + stmts: &mut oxc_allocator::Vec<'a, oxc_ast::ast::Statement<'a>>, + ) { + use oxc_ast::ast::Statement; + let mut i = 0; + while i < stmts.len() { + if self.done { + break; + } + // FunctionDeclaration → `const Foo = gating() ? ... : ...;` + let replace_name: Option> = match &stmts[i] { + Statement::FunctionDeclaration(f) if f.span.start == self.node_id => { + Some(f.id.as_ref().map(|id| id.name.to_string())) + } + Statement::ExportNamedDeclaration(e) => match &e.declaration { + Some(oxc_ast::ast::Declaration::FunctionDeclaration(f)) + if f.span.start == self.node_id => + { + Some(f.id.as_ref().map(|id| id.name.to_string())) + } + _ => None, + }, + _ => None, + }; + if let Some(name) = replace_name { + let name = name.unwrap_or_else(|| "anonymous".to_string()); + let is_export = matches!(stmts[i], Statement::ExportNamedDeclaration(_)); + let const_decl = self.build_const_decl(&name); + if is_export { + use oxc_span::SPAN; + let decl = match const_decl { + Statement::VariableDeclaration(d) => { + oxc_ast::ast::Declaration::VariableDeclaration(d) + } + _ => unreachable!(), + }; + stmts[i] = oxc_ast::ast::Statement::ExportNamedDeclaration( + self.ast.alloc_export_named_declaration( + SPAN, + Some(decl), + self.ast.vec(), + None, + oxc_ast::ast::ImportOrExportKind::Value, + None::>, + ), + ); + } else { + stmts[i] = const_decl; + } + self.done = true; + break; + } + // ExportDefaultDeclaration with FunctionDeclaration + if let Statement::ExportDefaultDeclaration(e) = &stmts[i] { + if let oxc_ast::ast::ExportDefaultDeclarationKind::FunctionDeclaration(f) = + &e.declaration + { + if f.span.start == self.node_id { + if let Some(id) = f.id.as_ref().map(|id| id.name.to_string()) { + stmts[i] = self.build_const_decl(&id); + self.export_default_name = Some(id); + } else { + use oxc_allocator::CloneIn; + use oxc_span::SPAN; + stmts[i] = oxc_ast::ast::Statement::ExportDefaultDeclaration( + self.ast.alloc_export_default_declaration( + SPAN, + oxc_ast::ast::ExportDefaultDeclarationKind::from( + self.gating_expression.clone_in(self.ast.allocator), + ), + ), + ); + } + self.done = true; + break; + } + } + } + self.visit_statement(&mut stmts[i]); + i += 1; + } + + // Insert `export default Name;` right after the replaced declaration. + if let Some(name) = self.export_default_name.take() { + use oxc_span::SPAN; + let ident = self.ast.expression_identifier(SPAN, ox_atom(self.ast, &name)); + let export = oxc_ast::ast::Statement::ExportDefaultDeclaration( + self.ast.alloc_export_default_declaration( + SPAN, + oxc_ast::ast::ExportDefaultDeclarationKind::from(ident), + ), + ); + // Find the const decl we just inserted (it has name `name`); insert after. + let pos = stmts.iter().position(|s| { + matches!(s, oxc_ast::ast::Statement::VariableDeclaration(d) + if d.declarations.first().is_some_and(|decl| matches!(&decl.id, + oxc_ast::ast::BindingPattern::BindingIdentifier(b) if b.name.as_str() == name))) + }); + if let Some(pos) = pos { + stmts.insert(pos + 1, export); + } else { + stmts.push(export); + } + } + } + + fn visit_expression(&mut self, expr: &mut oxc_ast::ast::Expression<'a>) { + if self.done { + return; + } + let matched = match expr { + oxc_ast::ast::Expression::FunctionExpression(f) => f.span.start == self.node_id, + oxc_ast::ast::Expression::ArrowFunctionExpression(f) => f.span.start == self.node_id, + _ => false, + }; + if matched { + use oxc_allocator::CloneIn; + *expr = self.gating_expression.clone_in(self.ast.allocator); + self.done = true; + return; + } + oxc_ast_visit::walk_mut::walk_expression(self, expr); + } +} + +/// Allocate a `&'a str` in the arena (satisfies the builders' `Into` / +/// `IntoIn` slots; convert to `Atom` via `.into()` where a bare `Atom` is needed). +fn ox_atom<'a>(ast: &oxc_ast::AstBuilder<'a>, s: &str) -> &'a str { + oxc_allocator::StringBuilder::from_str_in(s, ast.allocator).into_str() +} + +/// Build `()` as an oxc call expression. +fn ox_gating_call<'a>( + ast: &oxc_ast::AstBuilder<'a>, + callee_name: &str, +) -> oxc_ast::ast::Expression<'a> { + use oxc_span::SPAN; + ast.expression_call( + SPAN, + ast.expression_identifier(SPAN, ox_atom(ast, callee_name)), + None::>, + ast.vec(), + false, + ) +} + +/// Apply the conditional gating pattern to the oxc program. Mirrors +/// `apply_gated_function_conditional`. +fn ox_apply_gated_conditional<'a>( + ast: &oxc_ast::AstBuilder<'a>, + program: &mut oxc_ast::ast::Program<'a>, + replacement: &OxcReplacement<'a>, + gating_config: &GatingConfig, + context: &mut ProgramContext, +) { + let node_id = match replacement.fn_node_id { + Some(nid) => nid, + None => return, + }; + + let gating_import = context.add_import_specifier( + &gating_config.source, + &gating_config.import_specifier_name, + None, + ); + let gating_callee_name = gating_import.name; + + // Clone the original function (matched by node_id) as the fallback expression + // BEFORE replacing it. + let original_expr = ox_clone_original_fn_as_expression(ast, program, node_id); + let original_expr = match original_expr { + Some(e) => e, + None => return, + }; + + let compiled_expr = + ox_build_compiled_expression(ast, &replacement.codegen_fn, replacement.original_kind); + + // gating() ? compiled : original + use oxc_span::SPAN; + let gating_expression = ast.expression_conditional( + SPAN, + ox_gating_call(ast, &gating_callee_name), + compiled_expr, + original_expr, + ); + + let mut visitor = OxcReplaceWithGatedVisitor { + ast, + node_id, + gating_expression: &gating_expression, + export_default_name: None, + done: false, + }; + oxc_ast_visit::VisitMut::visit_program(&mut visitor, program); +} + +/// Clone the original function at `node_id` as an `Expression` (FunctionDeclaration +/// becomes a FunctionExpression). Mirrors `clone_original_fn_as_expression`. +fn ox_clone_original_fn_as_expression<'a>( + ast: &oxc_ast::AstBuilder<'a>, + program: &oxc_ast::ast::Program<'a>, + node_id: u32, +) -> Option> { + use oxc_allocator::CloneIn; + use oxc_span::SPAN; + + struct Finder<'a, 'b> { + ast: &'b oxc_ast::AstBuilder<'a>, + node_id: u32, + found: Option>, + } + impl<'a, 'b> oxc_ast_visit::Visit<'a> for Finder<'a, 'b> { + fn visit_function( + &mut self, + func: &oxc_ast::ast::Function<'a>, + flags: oxc_syntax::scope::ScopeFlags, + ) { + if self.found.is_some() { + return; + } + if func.span.start == self.node_id { + use oxc_allocator::CloneIn; + use oxc_span::SPAN; + let f = self.ast.alloc_function( + SPAN, + oxc_ast::ast::FunctionType::FunctionExpression, + func.id.clone_in(self.ast.allocator), + func.generator, + func.r#async, + false, + None::>, + None::>, + func.params.clone_in(self.ast.allocator), + None::>, + func.body.clone_in(self.ast.allocator), + ); + self.found = Some(oxc_ast::ast::Expression::FunctionExpression(f)); + return; + } + oxc_ast_visit::walk::walk_function(self, func, flags); + } + fn visit_arrow_function_expression( + &mut self, + arrow: &oxc_ast::ast::ArrowFunctionExpression<'a>, + ) { + if self.found.is_some() { + return; + } + if arrow.span.start == self.node_id { + self.found = Some(oxc_ast::ast::Expression::ArrowFunctionExpression( + self.ast.alloc(arrow.clone_in(self.ast.allocator)), + )); + return; + } + oxc_ast_visit::walk::walk_arrow_function_expression(self, arrow); + } + } + let _ = (SPAN, ast.allocator); + let mut finder = Finder { ast, node_id, found: None }; + oxc_ast_visit::Visit::visit_program(&mut finder, program); + finder.found.map(|e| e.clone_in(ast.allocator)) +} + +/// Splice every compiled oxc function into a clone of the original oxc program and +/// add the required imports. Returns the final memoized program. +/// Applies all passed-through-code codegen fixups in ONE traversal. These rewrites +/// target disjoint node kinds, so a single walk is identical to running them as +/// three separate whole-program passes (which is what the splice used to do): +/// - clear the parser's `pife` hint on functions/arrows (so codegen doesn't keep +/// source-only parens — matching `@babel/generator`); +/// - re-apply the JSX-text entity decode→encode round-trip on passed-through text +/// (e.g. `>e;` → `&gte;`); +/// - re-wrap a non-null over an optional chain (`a?.b!`, parsed as +/// `ChainExpression(TSNonNull(member))`) into `TSNonNull(Paren(Chain(member)))` +/// so codegen prints `(a?.b)!`, matching the original Babel round-trip. +/// - (when `rename` is set) rename identifier references — used to rewrite the +/// codegen placeholder `useMemoCache` to the actual memo-cache import name. +/// Runs in the same pass since it walks the same program after splicing. +struct OxcPassthroughFixupVisitor<'a, 'b> { + ast: oxc_ast::AstBuilder<'a>, + rename: Option<(&'b str, &'b str)>, +} + +impl<'a, 'b> oxc_ast_visit::VisitMut<'a> for OxcPassthroughFixupVisitor<'a, 'b> { + fn visit_identifier_reference(&mut self, ident: &mut oxc_ast::ast::IdentifierReference<'a>) { + if let Some((old, new)) = self.rename { + if ident.name == old { + ident.name = ox_atom(&self.ast, new).into(); + } + } + } + + fn visit_function( + &mut self, + it: &mut oxc_ast::ast::Function<'a>, + flags: oxc_syntax::scope::ScopeFlags, + ) { + it.pife = false; + oxc_ast_visit::walk_mut::walk_function(self, it, flags); + } + + fn visit_arrow_function_expression( + &mut self, + it: &mut oxc_ast::ast::ArrowFunctionExpression<'a>, + ) { + it.pife = false; + oxc_ast_visit::walk_mut::walk_arrow_function_expression(self, it); + } + + fn visit_jsx_text(&mut self, it: &mut oxc_ast::ast::JSXText<'a>) { + let decoded = + crate::react_compiler_lowering::build_hir::decode_jsx_entities(it.value.as_str()); + let encoded = + crate::react_compiler_reactive_scopes::codegen_reactive_function::ox_encode_jsx_text( + &decoded, + ); + it.value = self.ast.str(&encoded); + } + + fn visit_expression(&mut self, it: &mut oxc_ast::ast::Expression<'a>) { + use oxc_ast::ast::{ChainElement, Expression}; + oxc_ast_visit::walk_mut::walk_expression(self, it); + // Only a `ChainExpression` whose head element is a `TSNonNullExpression`. + let is_target = matches!( + it, + Expression::ChainExpression(c) if matches!(c.expression, ChainElement::TSNonNullExpression(_)) + ); + if !is_target { + return; + } + let taken = std::mem::replace(it, oxc_allocator::Dummy::dummy(self.ast.allocator)); + let Expression::ChainExpression(chain) = taken else { unreachable!() }; + let ChainElement::TSNonNullExpression(ts) = chain.unbox().expression else { + unreachable!() + }; + // Re-wrap the non-null operand (a member/call) as a chain element so the + // optional chain is reconstructed inside the parens. + let member_elem: ChainElement<'a> = match ts.unbox().expression { + Expression::StaticMemberExpression(b) => ChainElement::StaticMemberExpression(b), + Expression::ComputedMemberExpression(b) => ChainElement::ComputedMemberExpression(b), + Expression::PrivateFieldExpression(b) => ChainElement::PrivateFieldExpression(b), + Expression::CallExpression(b) => ChainElement::CallExpression(b), + // Not a member/call operand — emit a plain non-null without the chain. + other => { + *it = self.ast.expression_ts_non_null(oxc_span::SPAN, other); + return; + } + }; + let new_chain = self.ast.expression_chain(oxc_span::SPAN, member_elem); + let paren = self.ast.expression_parenthesized(oxc_span::SPAN, new_chain); + *it = self.ast.expression_ts_non_null(oxc_span::SPAN, paren); + } +} + +fn ox_splice_program<'a>( + ast: &oxc_ast::AstBuilder<'a>, + program: &mut oxc_ast::ast::Program<'a>, + replacements: &[OxcReplacement<'a>], + context: &mut ProgramContext, +) { + // Outlined function declarations are placed differently depending on the + // original function's syntactic kind, mirroring `insertNewOutlinedFunctionNode` + // in TS `Program.ts`: + // - FunctionDeclaration originals: inserted as a sibling immediately after the + // original function (Babel `insertAfter`). + // - (Arrow)FunctionExpression originals: appended at the end of the program + // body (Babel `pushContainer('body', ...)`), since inserting as a sibling + // would corrupt the parent expression. + let mut appended_outlined_decls: Vec> = Vec::new(); + + for replacement in replacements { + let mut sibling_outlined_decls: Vec> = Vec::new(); + let insert_as_sibling = replacement.original_kind == OriginalFnKind::FunctionDeclaration; + for outlined in &replacement.codegen_fn.outlined { + let func = ox_build_function( + ast, + &outlined.func, + oxc_ast::ast::FunctionType::FunctionDeclaration, + ); + let stmt = oxc_ast::ast::Statement::FunctionDeclaration(func); + if insert_as_sibling { + sibling_outlined_decls.push(stmt); + } else { + appended_outlined_decls.push(stmt); + } + } + + if let Some(ref gating_config) = replacement.gating { + ox_apply_gated_conditional(ast, program, replacement, gating_config, context); + } else if let Some(node_id) = replacement.fn_node_id { + let mut visitor = + OxcReplaceFnVisitor { ast, node_id, codegen: &replacement.codegen_fn, done: false }; + oxc_ast_visit::VisitMut::visit_program(&mut visitor, program); + } + + if !sibling_outlined_decls.is_empty() { + if let Some(node_id) = replacement.fn_node_id { + ox_insert_outlined_after(program, node_id, sibling_outlined_decls); + } + } + } + + // Append outlined function declarations (from expression-parented originals) at + // the top level. + program.body.extend(appended_outlined_decls); + + // Register the memo-cache import (if any function memoized), then run the + // passed-through-code codegen fixups over the final program in a SINGLE + // traversal, folding the `useMemoCache` -> import-name rename into the same + // walk. The fixups target disjoint node kinds and are no-ops/idempotent on the + // freshly compiled bodies, so running them once after splicing is equivalent to + // running them before, and saves two whole-program walks (the separate fixup + // pass and the separate rename pass). The fixups: + // - clear `pife` on functions/arrows (the parser set it on parenthesized + // function/arrow expressions; passed-through code would otherwise emit + // callee parens the original Babel path never produced); + // - normalize JSX-text entities (the original decoded on parse + re-encoded + // on codegen; do the same decode→encode for passed-through JSX text); + // - re-wrap a non-null over an optional chain (`a?.b!`) as `(a?.b)!` to match + // the original Babel round-trip's output. + let needs_memo_import = replacements.iter().any(|r| r.codegen_fn.memo_slots_used > 0); + let memo_local_name = needs_memo_import.then(|| context.add_memo_cache_import().name); + let rename = memo_local_name.as_deref().map(|new| ("useMemoCache", new)); + oxc_ast_visit::VisitMut::visit_program( + &mut OxcPassthroughFixupVisitor { ast: *ast, rename }, + program, + ); + + ox_add_imports_to_program(ast, program, context); +} + +/// Insert outlined function declarations immediately after the statement that +/// declares the function identified by `node_id`. Mirrors Babel's +/// `originalFn.insertAfter(...)` for `FunctionDeclaration` originals. The original +/// may be nested (e.g. a component declared inside a `describe(() => { ... })` +/// callback), so the search descends into every statement list — function bodies, +/// blocks, and the bodies of arrow/function arguments — not just `program.body`. +fn ox_insert_outlined_after<'a>( + program: &mut oxc_ast::ast::Program<'a>, + node_id: u32, + outlined_decls: Vec>, +) { + let mut visitor = OxcInsertOutlinedVisitor { node_id, decls: outlined_decls, done: false }; + oxc_ast_visit::VisitMut::visit_program(&mut visitor, program); + // The original function should always be found; if not (defensive), append at + // the top level rather than dropping the outlined declarations. + if !visitor.done { + program.body.extend(visitor.decls); + } +} + +/// Finds the statement list containing the original function (by `node_id`) and +/// inserts the outlined declarations right after it, wherever it is nested. +struct OxcInsertOutlinedVisitor<'a> { + node_id: u32, + decls: Vec>, + done: bool, +} + +impl<'a> oxc_ast_visit::VisitMut<'a> for OxcInsertOutlinedVisitor<'a> { + fn visit_statements( + &mut self, + stmts: &mut oxc_allocator::Vec<'a, oxc_ast::ast::Statement<'a>>, + ) { + use oxc_ast::ast::{Declaration, ExportDefaultDeclarationKind, Statement}; + if !self.done { + let node_id = self.node_id; + let pos = stmts.iter().position(|stmt| match stmt { + Statement::FunctionDeclaration(f) => f.span.start == node_id, + Statement::ExportNamedDeclaration(e) => { + matches!(&e.declaration, Some(Declaration::FunctionDeclaration(f)) if f.span.start == node_id) + } + Statement::ExportDefaultDeclaration(e) => { + matches!(&e.declaration, ExportDefaultDeclarationKind::FunctionDeclaration(f) if f.span.start == node_id) + } + _ => false, + }); + if let Some(idx) = pos { + // Babel anchors each `insertAfter` at the same original node, so + // repeated insertions reverse the emitted order. Insert each at + // `idx + 1` to reproduce that. + for stmt in std::mem::take(&mut self.decls) { + stmts.insert(idx + 1, stmt); + } + self.done = true; + } + } + oxc_ast_visit::walk_mut::walk_statements(self, stmts); + } +} + +/// Insert import declarations into the oxc program. Mirrors `add_imports_to_program` +/// in imports.rs but builds oxc nodes. Handles ESM imports, CommonJS require, and +/// merging into an existing non-namespaced import of the same module. +fn ox_add_imports_to_program<'a>( + ast: &oxc_ast::AstBuilder<'a>, + program: &mut oxc_ast::ast::Program<'a>, + context: &ProgramContext, +) { + use oxc_span::SPAN; + if !context.has_pending_imports() { + return; + } + let imports = context.imports(); + + // Existing non-namespaced value imports, by module name. + let mut existing_import_indices: FxHashMap = FxHashMap::default(); + for (idx, stmt) in program.body.iter().enumerate() { + if let oxc_ast::ast::Statement::ImportDeclaration(import) = stmt { + if ox_is_non_namespaced_import(import) { + existing_import_indices.entry(import.source.value.to_string()).or_insert(idx); + } + } + } + + let mut sorted_modules: Vec<_> = imports.iter().collect(); + sorted_modules.sort_by(|(a, _), (b, _)| a.to_lowercase().cmp(&b.to_lowercase())); + + let is_module = matches!(program.source_type.module_kind(), oxc_span::ModuleKind::Module); + + let mut new_stmts: Vec> = Vec::new(); + + for (module_name, imports_map) in sorted_modules { + let mut sorted_imports: Vec<_> = imports_map.values().collect(); + sorted_imports.sort_by(|a, b| a.imported.cmp(&b.imported)); + + if let Some(&idx) = existing_import_indices.get(module_name) { + // Merge into the existing import declaration. + if let oxc_ast::ast::Statement::ImportDeclaration(import) = &mut program.body[idx] { + let specifiers = import.specifiers.get_or_insert_with(|| ast.vec()); + for spec in &sorted_imports { + specifiers.push(ox_make_import_specifier(ast, spec)); + } + } + } else if is_module { + // ESM: import { imported as local, ... } from 'module' + let mut specifiers = ast.vec(); + for spec in &sorted_imports { + specifiers.push(ox_make_import_specifier(ast, spec)); + } + let source = ast.string_literal(SPAN, ox_atom(ast, module_name), None); + let import = ast.alloc_import_declaration( + SPAN, + Some(specifiers), + source, + None, + None::>, + oxc_ast::ast::ImportOrExportKind::Value, + ); + new_stmts.push(oxc_ast::ast::Statement::ImportDeclaration(import)); + } else { + // CommonJS: const { imported: local, ... } = require('module') + let mut props = ast.vec(); + for spec in &sorted_imports { + let key = ast.property_key_static_identifier(SPAN, ox_atom(ast, &spec.imported)); + let value = ast.binding_pattern_binding_identifier(SPAN, ox_atom(ast, &spec.name)); + props.push(ast.binding_property(SPAN, key, value, false, false)); + } + let object_pattern = ast.binding_pattern_object_pattern( + SPAN, + props, + None::>, + ); + let require_call = ast.expression_call( + SPAN, + ast.expression_identifier(SPAN, "require"), + None::>, + ast.vec1(oxc_ast::ast::Argument::from(ast.expression_string_literal( + SPAN, + ox_atom(ast, module_name), + None, + ))), + false, + ); + let declarator = ast.variable_declarator( + SPAN, + oxc_ast::ast::VariableDeclarationKind::Const, + object_pattern, + None::>, + Some(require_call), + false, + ); + let decl = ast.alloc_variable_declaration( + SPAN, + oxc_ast::ast::VariableDeclarationKind::Const, + ast.vec1(declarator), + false, + ); + new_stmts.push(oxc_ast::ast::Statement::VariableDeclaration(decl)); + } + } + + if !new_stmts.is_empty() { + let old_body = std::mem::replace(&mut program.body, ast.vec()); + program.body.extend(new_stmts); + program.body.extend(old_body); + } +} + +/// Build an oxc named import specifier `imported as local`. Mirrors `make_import_specifier`. +fn ox_make_import_specifier<'a>( + ast: &oxc_ast::AstBuilder<'a>, + spec: &super::imports::NonLocalImportSpecifier, +) -> oxc_ast::ast::ImportDeclarationSpecifier<'a> { + use oxc_span::SPAN; + let imported = oxc_ast::ast::ModuleExportName::IdentifierName( + ast.identifier_name(SPAN, ox_atom(ast, &spec.imported)), + ); + let local = ast.binding_identifier(SPAN, ox_atom(ast, &spec.name)); + oxc_ast::ast::ImportDeclarationSpecifier::ImportSpecifier(ast.alloc_import_specifier( + SPAN, + imported, + local, + oxc_ast::ast::ImportOrExportKind::Value, + )) +} + +/// Whether an import declaration is a non-namespaced value import. Mirrors +/// `is_non_namespaced_import`. +fn ox_is_non_namespaced_import(import: &oxc_ast::ast::ImportDeclaration) -> bool { + if !matches!(import.import_kind, oxc_ast::ast::ImportOrExportKind::Value) { + return false; + } + match &import.specifiers { + None => true, + Some(specifiers) => specifiers + .iter() + .all(|s| matches!(s, oxc_ast::ast::ImportDeclarationSpecifier::ImportSpecifier(_))), + } +} + +/// Main entry point for the React Compiler. +/// +/// Receives a full program AST, scope information (unused for now), and resolved options. +/// Returns a CompileResult indicating whether the AST was modified, +/// along with any logger events. +/// +/// This function implements the logic from the TS entrypoint (Program.ts): +/// - shouldSkipCompilation: check for existing runtime imports +/// - validateRestrictedImports: check for blocklisted imports +/// - findProgramSuppressions: find eslint/flow suppression comments +/// - findFunctionsToCompile: traverse program to find components and hooks +/// - processFn: per-function compilation with directive and suppression handling +/// - applyCompiledFunctions: replace original functions with compiled versions +pub(crate) fn compile_program<'a>( + ast: &oxc_ast::AstBuilder<'a>, + program: &oxc_ast::ast::Program<'a>, + scope: ScopeInfo, + options: PluginOptions, +) -> CompileOutput<'a> { + // Compute output mode once, up front + let output_mode = CompilerOutputMode::from_opts(&options); + + // Create a temporary context for early-return paths (before full context is set up) + let early_events: Vec = Vec::new(); + let mut early_ordered_log: Vec = Vec::new(); + + // Log environment config for debugLogIRs + if options.debug { + early_ordered_log.push(OrderedLogItem::Debug { + entry: DebugLogEntry::new("EnvironmentConfig", format!("{:#?}", options.environment)), + }); + } + + // Check if we should compile this file at all (pre-resolved by JS shim) + if !options.should_compile { + return CompileOutput::Final(CompileResult::Success { + changed: false, + events: early_events, + ordered_log: early_ordered_log, + renames: Vec::new(), + timing: Vec::new(), + }); + } + + // Check for existing runtime imports (file already compiled) + if should_skip_compilation(program, &options) { + return CompileOutput::Final(CompileResult::Success { + changed: false, + events: early_events, + ordered_log: early_ordered_log, + renames: Vec::new(), + timing: Vec::new(), + }); + } + + // Validate restricted imports from the environment config + let restricted_imports = options.environment.validate_blocklisted_imports.clone(); + + // Determine if we should check for eslint suppressions + let validate_exhaustive = options.environment.validate_exhaustive_memoization_dependencies; + let validate_hooks = options.environment.validate_hooks_usage; + + let eslint_rules: Option> = + if validate_exhaustive && validate_hooks { + // Don't check for ESLint suppressions if both validations are enabled + None + } else { + Some(options.eslint_suppression_rules.clone().unwrap_or_else(|| { + DEFAULT_ESLINT_SUPPRESSIONS.iter().map(|s| s.to_string()).collect() + })) + }; + + // Find program-level suppressions from comments + let suppressions = find_program_suppressions( + &program.comments, + program.source_text, + eslint_rules.as_deref(), + options.flow_suppressions, + ); + + // Check for module-scope opt-out directive + let module_directives: Vec = + program.directives.iter().map(|d| d.expression.value.to_string()).collect(); + let has_module_scope_opt_out = + find_directive_disabling_memoization(&module_directives, &options).is_some(); + + // Create program context + let mut context = ProgramContext::new( + options.clone(), + options.filename.clone(), + // Pass the source code for fast refresh hash computation. + options.source_code.clone(), + suppressions, + has_module_scope_opt_out, + ); + + // The source filename only fed logger event source locations. The oxc AST + // carries no per-node filename (the Babel bridge set it to `None` too), so + // leave it unset; it never affects compiled output or diagnostics. + context.set_source_filename(None); + + // Initialize known referenced names from scope bindings for UID collision detection + context.init_from_scope(&scope); + + // Seed context with early ordered log entries + context.ordered_log.extend(early_ordered_log); + + // Validate restricted imports (needs context for handle_error) + if let Some(err) = validate_restricted_imports(program, &restricted_imports) { + if let Some(result) = handle_error(&err, None, &mut context) { + return CompileOutput::Final(result); + } + return CompileOutput::Final(CompileResult::Success { + changed: false, + events: context.events, + ordered_log: context.ordered_log, + renames: convert_renames(&context.renames), + timing: Vec::new(), + }); + } + + // Pre-register instrumentation imports to get stable local names. + // These are needed before compilation so codegen can use the correct names. + let instrument_fn_name: Option; + let instrument_gating_name: Option; + let hook_guard_name: Option; + + if let Some(ref instrument_config) = options.environment.enable_emit_instrument_forget { + let fn_spec = context.add_import_specifier( + &instrument_config.fn_.source, + &instrument_config.fn_.import_specifier_name, + None, + ); + instrument_fn_name = Some(fn_spec.name.clone()); + instrument_gating_name = instrument_config.gating.as_ref().map(|g| { + let spec = context.add_import_specifier(&g.source, &g.import_specifier_name, None); + spec.name.clone() + }); + } else { + instrument_fn_name = None; + instrument_gating_name = None; + } + + if let Some(ref hook_guard_config) = options.environment.enable_emit_hook_guards { + let spec = context.add_import_specifier( + &hook_guard_config.source, + &hook_guard_config.import_specifier_name, + None, + ); + hook_guard_name = Some(spec.name.clone()); + } else { + hook_guard_name = None; + } + + // Store pre-resolved names on context for pipeline access + context.instrument_fn_name = instrument_fn_name; + context.instrument_gating_name = instrument_gating_name; + context.hook_guard_name = hook_guard_name; + + // Find all functions to compile + let queue = find_functions_to_compile(program, &options, &mut context, &scope); + + // Clone env_config once for all function compilations (avoids per-function clone + // while satisfying the borrow checker — compile_fn needs &mut context + &env_config) + let env_config = options.environment.clone(); + + // Process each function and collect compiled results + let mut compiled_fns: Vec> = Vec::new(); + + for source in &queue { + match process_fn(ast, source, &scope, output_mode, &env_config, &mut context) { + Ok(Some(codegen_fn)) => { + compiled_fns.push(CompiledFunction { kind: source.kind, source, codegen_fn }); + } + Ok(None) => { + // Function was skipped or lint-only + } + Err(fatal_result) => { + return CompileOutput::Final(fatal_result); + } + } + } + + // Emit CompileSuccess events for JSX-outlined functions (fn_type.is_some()). + // In TS, outlined functions from outlineJSX are appended to the compilation queue + // and processed after all original functions, so their events appear at the end. + // Regular outlined functions (from OutlineFunctions pass) don't get separate events. + for compiled in &compiled_fns { + for outlined in &compiled.codegen_fn.outlined { + if outlined.fn_type.is_some() { + context.log_event(LoggerEvent::CompileSuccess { + fn_loc: None, + fn_name: outlined.func.id.as_ref().map(|id| id.name.to_string()), + memo_slots: outlined.func.memo_slots_used, + memo_blocks: outlined.func.memo_blocks, + memo_values: outlined.func.memo_values, + pruned_memo_blocks: outlined.func.pruned_memo_blocks, + pruned_memo_values: outlined.func.pruned_memo_values, + }); + } + } + } + + // TS invariant: if there's a module scope opt-out, no functions should have been compiled + if has_module_scope_opt_out { + if !compiled_fns.is_empty() { + let mut err = CompilerError::new(); + err.push_error_detail(CompilerErrorDetail::new( + ErrorCategory::Invariant, + "Unexpected compiled functions when module scope opt-out is present", + )); + handle_error(&err, None, &mut context); + } + return CompileOutput::Final(CompileResult::Success { + changed: false, + events: context.events, + ordered_log: context.ordered_log, + renames: convert_renames(&context.renames), + timing: Vec::new(), + }); + } + + // Convert compiled functions to owned oxc replacements (dropping the borrows of + // `file.program`), resolving per-function gating. Dynamic gating from directives + // (`use memo if(identifier)`) takes precedence over plugin-level gating; gating + // only applies to 'original' (not 'outlined') functions. Mirrors the Babel path. + let function_gating_config = options.gating.clone(); + let replacements: Vec> = compiled_fns + .into_iter() + .map(|cf| { + let gating = if cf.kind == CompileSourceKind::Original { + let dynamic_gating = + find_directives_dynamic_gating(&cf.source.body_directives, &options) + .ok() + .flatten() + .map(|r| r.gating); + dynamic_gating.or_else(|| function_gating_config.clone()) + } else { + None + }; + OxcReplacement { + fn_node_id: cf.source.fn_node_id, + original_kind: cf.source.original_kind, + codegen_fn: cf.codegen_fn, + gating, + } + }) + .collect(); + + // Drop the discovery results (and their borrows of `file.program`). + drop(queue); + + if replacements.is_empty() { + // No functions to replace. Return renames for the Babel plugin to apply + // (e.g., variable shadowing renames in lint mode). Imports are NOT added + // when there are no replacements — matching TS behavior where + // addImportsToProgram is only called when compiledFns.length > 0. + return CompileOutput::Final(CompileResult::Success { + changed: false, + events: context.events, + ordered_log: context.ordered_log, + renames: convert_renames(&context.renames), + timing: Vec::new(), + }); + } + + // Functions were compiled. The actual splice into the program needs `&mut + // Program`, which the analysis above never required (it only reads `program`). + // Hand the replacements back to the caller — which owns the `&mut Program` — + // so the lint path (read-only, no emit) can run this whole analysis without a + // mutable program (and therefore without cloning the program into a fresh + // arena). The emit path finalizes via `splice_and_finalize`. + CompileOutput::Splice { replacements, context } +} + +/// Outcome of [`compile_program`]. The analysis phase never mutates the program; +/// when functions were compiled it returns [`CompileOutput::Splice`] so the +/// caller (the emit path, which owns `&mut Program`) can perform the only +/// mutating step. The lint path never produces replacements, so it only ever +/// sees [`CompileOutput::Final`]. +pub(crate) enum CompileOutput<'a> { + /// No splice needed — file skipped, lint mode, or nothing compiled. + Final(CompileResult), + /// Compiled functions to splice into a `&mut Program`. Finalize with + /// [`splice_and_finalize`]. + Splice { replacements: Vec>, context: ProgramContext }, +} + +/// Run the analysis and splice in one call on a `&mut Program`, returning the +/// finalized result. This is the convenience entrypoint for tools and examples; +/// production callers use [`crate::transform`] (which also preserves comments) or +/// [`crate::lint`] (read-only). +pub fn compile_program_and_finalize<'a>( + ast: &oxc_ast::AstBuilder<'a>, + program: &mut oxc_ast::ast::Program<'a>, + scope: ScopeInfo, + options: PluginOptions, +) -> CompileResult { + match compile_program(ast, program, scope, options) { + CompileOutput::Final(result) => result, + CompileOutput::Splice { replacements, context } => { + splice_and_finalize(ast, program, replacements, context) + } + } +} + +/// Splice the compiled functions into `program` (the only step needing `&mut +/// Program`) and finalize the [`CompileResult`]. Called by the emit path. +pub(crate) fn splice_and_finalize<'a>( + ast: &oxc_ast::AstBuilder<'a>, + program: &mut oxc_ast::ast::Program<'a>, + replacements: Vec>, + mut context: ProgramContext, +) -> CompileResult { + // Splice each compiled oxc function into the program in place (matched by + // `span.start == fn_node_id`), apply gating, insert outlined functions, and add + // the memo-cache / gating imports. + ox_splice_program(ast, program, &replacements, &mut context); + + let timing_entries = context.timing.into_entries(); + + CompileResult::Success { + changed: true, + events: context.events, + ordered_log: context.ordered_log, + renames: convert_renames(&context.renames), + timing: timing_entries, + } +} + +/// Finalize a [`CompileOutput::Splice`] without splicing — used only as a +/// defensive fallback by the lint path, which never actually produces +/// replacements (every function returns `Ok(None)` in lint mode). +pub(crate) fn finalize_without_splice(context: ProgramContext) -> CompileResult { + CompileResult::Success { + changed: false, + events: context.events, + ordered_log: context.ordered_log, + renames: convert_renames(&context.renames), + timing: Vec::new(), + } +} + +/// Convert internal BindingRename structs to the serializable BindingRenameInfo format. +fn convert_renames( + renames: &[crate::react_compiler_hir::environment::BindingRename], +) -> Vec { + renames + .iter() + .map(|r| BindingRenameInfo { + original: r.original.clone(), + renamed: r.renamed.clone(), + declaration_start: r.declaration_start, + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_hook_name() { + assert!(is_hook_name("useState")); + assert!(is_hook_name("useEffect")); + assert!(is_hook_name("use0Something")); + assert!(!is_hook_name("use")); + assert!(!is_hook_name("useless")); // lowercase after use + assert!(!is_hook_name("foo")); + assert!(!is_hook_name("")); + } + + #[test] + fn test_is_component_name() { + assert!(is_component_name("MyComponent")); + assert!(is_component_name("App")); + assert!(!is_component_name("myComponent")); + assert!(!is_component_name("app")); + assert!(!is_component_name("")); + } + + #[test] + fn test_is_valid_identifier() { + assert!(is_valid_identifier("foo")); + assert!(is_valid_identifier("_bar")); + assert!(is_valid_identifier("$baz")); + assert!(is_valid_identifier("foo123")); + assert!(!is_valid_identifier("")); + assert!(!is_valid_identifier("123foo")); + assert!(!is_valid_identifier("foo bar")); + } + + /// Build an `oxc::FormalParameters` of plain identifier params from names, + /// for the component-param validity tests. + fn ident_params<'a>( + ast: &oxc_ast::AstBuilder<'a>, + names: &[&str], + ) -> oxc_ast::ast::FormalParameters<'a> { + use oxc_span::SPAN; + let mut items = ast.vec(); + for name in names { + let name = ox_atom(ast, name); + let pattern = ast.binding_pattern_binding_identifier(SPAN, name); + items.push(ast.formal_parameter( + SPAN, + ast.vec(), + pattern, + None::>, + None::, + false, + None, + false, + false, + )); + } + ast.formal_parameters( + SPAN, + oxc_ast::ast::FormalParameterKind::FormalParameter, + items, + None::>, + ) + } + + #[test] + fn test_is_valid_component_params_empty() { + let allocator = oxc_allocator::Allocator::default(); + let ast = oxc_ast::AstBuilder::new(&allocator); + assert!(is_valid_component_params(&ident_params(&ast, &[]))); + } + + #[test] + fn test_is_valid_component_params_one_identifier() { + let allocator = oxc_allocator::Allocator::default(); + let ast = oxc_ast::AstBuilder::new(&allocator); + assert!(is_valid_component_params(&ident_params(&ast, &["props"]))); + } + + #[test] + fn test_is_valid_component_params_too_many() { + let allocator = oxc_allocator::Allocator::default(); + let ast = oxc_ast::AstBuilder::new(&allocator); + assert!(!is_valid_component_params(&ident_params(&ast, &["a", "b", "c"]))); + } + + #[test] + fn test_is_valid_component_params_with_ref() { + let allocator = oxc_allocator::Allocator::default(); + let ast = oxc_ast::AstBuilder::new(&allocator); + assert!(is_valid_component_params(&ident_params(&ast, &["props", "ref"]))); + } + + #[test] + fn test_should_skip_compilation_no_import() { + let allocator = oxc_allocator::Allocator::default(); + let parsed = oxc_parser::Parser::new( + &allocator, + "function Component() {}\n", + oxc_span::SourceType::tsx(), + ) + .parse(); + let options = PluginOptions { + should_compile: true, + enable_reanimated: false, + is_dev: false, + filename: None, + compilation_mode: "infer".to_string(), + panic_threshold: "none".to_string(), + target: super::super::plugin_options::CompilerTarget::Version("19".to_string()), + gating: None, + dynamic_gating: None, + no_emit: false, + output_mode: None, + eslint_suppression_rules: None, + flow_suppressions: true, + ignore_use_no_forget: false, + custom_opt_out_directives: None, + environment: EnvironmentConfig::default(), + source_code: None, + profiling: false, + debug: false, + }; + assert!(!should_skip_compilation(&parsed.program, &options)); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler/entrypoint/suppression.rs b/crates/oxc_react_compiler/src/react_compiler/entrypoint/suppression.rs new file mode 100644 index 0000000000000..1d0110e178107 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/entrypoint/suppression.rs @@ -0,0 +1,320 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, CompilerSuggestion, + CompilerSuggestionOperation, ErrorCategory, +}; + +/// A comment's text and byte range, plus the byte-offset loc that surfaces as the +/// labeled span on a suppression diagnostic. The former Babel front-end carried +/// this on `CommentData`; the oxc front-end builds it directly from oxc comments. +#[derive(Debug, Clone)] +pub struct CommentData { + pub value: String, + pub start: Option, + pub end: Option, + pub loc: Option, +} + +#[derive(Debug, Clone)] +pub struct CommentLoc { + pub start_index: Option, + pub end_index: Option, +} + +#[derive(Debug, Clone)] +pub enum SuppressionSource { + Eslint, + Flow, +} + +/// Captures the start and end range of a pair of eslint-disable ... eslint-enable comments. +/// In the case of a CommentLine or a relevant Flow suppression, both the disable and enable +/// point to the same comment. +/// +/// The enable comment can be missing in the case where only a disable block is present, +/// ie the rest of the file has potential React violations. +#[derive(Debug, Clone)] +pub struct SuppressionRange { + pub disable_comment: CommentData, + pub enable_comment: Option, + pub source: SuppressionSource, +} + +/// Build a Babel-shaped [`CommentData`] from an oxc comment, stripping the `//` +/// or `/* */` delimiters from the value exactly as the former Babel bridge did. +fn comment_data(comment: &oxc_ast::ast::Comment, source_text: &str) -> CommentData { + let raw = &source_text[comment.span.start as usize..comment.span.end as usize]; + let value = if matches!(comment.kind, oxc_ast::ast::CommentKind::Line) { + raw.strip_prefix("//").unwrap_or(raw).trim().to_string() + } else { + raw.strip_prefix("/*").unwrap_or(raw).strip_suffix("*/").unwrap_or(raw).trim().to_string() + }; + CommentData { + value, + start: Some(comment.span.start), + end: Some(comment.span.end), + // Only the byte `index` is load-bearing here: it surfaces as the labeled + // span offset/length on the suppression diagnostic. Line/column are unused + // by downstream consumers of this loc. + loc: Some(CommentLoc { + start_index: Some(comment.span.start), + end_index: Some(comment.span.end), + }), + } +} + +/// Check if a comment value matches `eslint-disable-next-line ` for any rule in `rule_names`. +fn matches_eslint_disable_next_line(value: &str, rule_names: &[String]) -> bool { + if let Some(rest) = value.strip_prefix("eslint-disable-next-line ") { + return rule_names.iter().any(|name| rest.starts_with(name.as_str())); + } + // Also check with leading space (comment values often have leading whitespace) + let trimmed = value.trim_start(); + if let Some(rest) = trimmed.strip_prefix("eslint-disable-next-line ") { + return rule_names.iter().any(|name| rest.starts_with(name.as_str())); + } + false +} + +/// Check if a comment value matches `eslint-disable ` for any rule in `rule_names`. +fn matches_eslint_disable(value: &str, rule_names: &[String]) -> bool { + if let Some(rest) = value.strip_prefix("eslint-disable ") { + return rule_names.iter().any(|name| rest.starts_with(name.as_str())); + } + let trimmed = value.trim_start(); + if let Some(rest) = trimmed.strip_prefix("eslint-disable ") { + return rule_names.iter().any(|name| rest.starts_with(name.as_str())); + } + false +} + +/// Check if a comment value matches `eslint-enable ` for any rule in `rule_names`. +fn matches_eslint_enable(value: &str, rule_names: &[String]) -> bool { + if let Some(rest) = value.strip_prefix("eslint-enable ") { + return rule_names.iter().any(|name| rest.starts_with(name.as_str())); + } + let trimmed = value.trim_start(); + if let Some(rest) = trimmed.strip_prefix("eslint-enable ") { + return rule_names.iter().any(|name| rest.starts_with(name.as_str())); + } + false +} + +/// Check if a comment value matches a Flow suppression pattern. +/// Matches: $FlowFixMe[react-rule, $FlowFixMe_xxx[react-rule, +/// $FlowExpectedError[react-rule, $FlowIssue[react-rule +fn matches_flow_suppression(value: &str) -> bool { + // Find "$Flow" anywhere in the value + let Some(idx) = value.find("$Flow") else { + return false; + }; + let after_dollar_flow = &value[idx + "$Flow".len()..]; + + // Match FlowFixMe (with optional word chars), FlowExpectedError, or FlowIssue + let after_kind = if after_dollar_flow.starts_with("FixMe") { + // Skip "FixMe" + any word characters + let rest = &after_dollar_flow["FixMe".len()..]; + let word_end = rest.find(|c: char| !c.is_alphanumeric() && c != '_').unwrap_or(rest.len()); + &rest[word_end..] + } else if after_dollar_flow.starts_with("ExpectedError") { + &after_dollar_flow["ExpectedError".len()..] + } else if after_dollar_flow.starts_with("Issue") { + &after_dollar_flow["Issue".len()..] + } else { + return false; + }; + + // Must be followed by "[react-rule" + after_kind.starts_with("[react-rule") +} + +/// Parse eslint-disable/enable and Flow suppression comments from program comments. +/// Equivalent to findProgramSuppressions in Suppression.ts +pub fn find_program_suppressions( + comments: &[oxc_ast::ast::Comment], + source_text: &str, + rule_names: Option<&[String]>, + flow_suppressions: bool, +) -> Vec { + let mut suppression_ranges: Vec = Vec::new(); + let mut disable_comment: Option = None; + let mut enable_comment: Option = None; + let mut source: Option = None; + + let has_rules = matches!(rule_names, Some(names) if !names.is_empty()); + + for comment in comments { + let data = comment_data(comment, source_text); + + if data.start.is_none() || data.end.is_none() { + continue; + } + + // Check for eslint-disable-next-line (only if not already within a block) + if disable_comment.is_none() && has_rules { + if let Some(names) = rule_names { + if matches_eslint_disable_next_line(&data.value, names) { + disable_comment = Some(data.clone()); + enable_comment = Some(data.clone()); + source = Some(SuppressionSource::Eslint); + } + } + } + + // Check for Flow suppression (only if not already within a block) + if flow_suppressions && disable_comment.is_none() && matches_flow_suppression(&data.value) { + disable_comment = Some(data.clone()); + enable_comment = Some(data.clone()); + source = Some(SuppressionSource::Flow); + } + + // Check for eslint-disable (block start) + if has_rules { + if let Some(names) = rule_names { + if matches_eslint_disable(&data.value, names) { + disable_comment = Some(data.clone()); + source = Some(SuppressionSource::Eslint); + } + } + } + + // Check for eslint-enable (block end) + if has_rules { + if let Some(names) = rule_names { + if matches_eslint_enable(&data.value, names) { + if matches!(source, Some(SuppressionSource::Eslint)) { + enable_comment = Some(data.clone()); + } + } + } + } + + // If we have a complete suppression, push it + if disable_comment.is_some() && source.is_some() { + suppression_ranges.push(SuppressionRange { + disable_comment: disable_comment.take().unwrap(), + enable_comment: enable_comment.take(), + source: source.take().unwrap(), + }); + } + } + + suppression_ranges +} + +/// Check if suppression ranges overlap with a function's source range. +/// A suppression affects a function if: +/// 1. The suppression is within the function's body +/// 2. The suppression wraps the function +pub fn filter_suppressions_that_affect_function( + suppressions: &[SuppressionRange], + fn_start: u32, + fn_end: u32, +) -> Vec<&SuppressionRange> { + let mut suppressions_in_scope: Vec<&SuppressionRange> = Vec::new(); + + for suppression in suppressions { + let disable_start = match suppression.disable_comment.start { + Some(s) => s, + None => continue, + }; + + // The suppression is within the function + if disable_start > fn_start + && (suppression.enable_comment.is_none() + || suppression + .enable_comment + .as_ref() + .and_then(|c| c.end) + .map_or(false, |end| end < fn_end)) + { + suppressions_in_scope.push(suppression); + } + + // The suppression wraps the function + if disable_start < fn_start + && (suppression.enable_comment.is_none() + || suppression + .enable_comment + .as_ref() + .and_then(|c| c.end) + .map_or(false, |end| end > fn_end)) + { + suppressions_in_scope.push(suppression); + } + } + + suppressions_in_scope +} + +/// Convert suppression ranges to a CompilerError. +pub fn suppressions_to_compiler_error(suppressions: &[SuppressionRange]) -> CompilerError { + assert!(!suppressions.is_empty(), "Expected at least one suppression comment source range"); + + let mut error = CompilerError::new(); + + for suppression in suppressions { + let (disable_start, disable_end) = + match (suppression.disable_comment.start, suppression.disable_comment.end) { + (Some(s), Some(e)) => (s, e), + _ => continue, + }; + + let (reason, suggestion) = match suppression.source { + SuppressionSource::Eslint => ( + "React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled", + "Remove the ESLint suppression and address the React error", + ), + SuppressionSource::Flow => ( + "React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow", + "Remove the Flow suppression and address the React error", + ), + }; + + let description = format!( + "React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `{}`", + suppression.disable_comment.value.trim() + ); + + let mut diagnostic = + CompilerDiagnostic::new(ErrorCategory::Suppression, reason, Some(description)); + + diagnostic.suggestions = Some(vec![CompilerSuggestion { + description: suggestion.to_string(), + range: (disable_start as usize, disable_end as usize), + op: CompilerSuggestionOperation::Remove, + text: None, + }]); + + // Add error detail with location info + let loc = suppression.disable_comment.loc.as_ref().map(|l| { + crate::react_compiler_diagnostics::SourceLocation { + start: crate::react_compiler_diagnostics::Position { + line: 0, + column: 0, + index: l.start_index, + }, + end: crate::react_compiler_diagnostics::Position { + line: 0, + column: 0, + index: l.end_index, + }, + } + }); + + diagnostic = diagnostic.with_detail(CompilerDiagnosticDetail::Error { + loc, + message: Some("Found React rule suppression".to_string()), + identifier_name: None, + }); + + error.push_diagnostic(diagnostic); + } + + error +} diff --git a/crates/oxc_react_compiler/src/react_compiler/mod.rs b/crates/oxc_react_compiler/src/react_compiler/mod.rs new file mode 100644 index 0000000000000..8baba87e359c8 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/mod.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "debug")] +pub mod debug_print; +/// Stub when the `debug` feature is off: the pipeline still calls these in its +/// `if debug_enabled` blocks, so keep the signatures but drop the IR printing. +#[cfg(not(feature = "debug"))] +pub mod debug_print { + use crate::react_compiler_hir::HirFunction; + use crate::react_compiler_hir::environment::Environment; + use crate::react_compiler_hir::print::PrintFormatter; + + pub fn debug_hir<'h>(_hir: &HirFunction<'h>, _env: &Environment<'h>) -> String { + String::new() + } + + pub fn format_hir_function_into<'h>( + _fmt: &mut PrintFormatter<'_, 'h>, + _func: &HirFunction<'h>, + ) { + } +} +pub mod entrypoint; +pub mod timing; + +// Re-export from new crates for backwards compatibility +pub use crate::react_compiler_diagnostics; +pub use crate::react_compiler_hir; +pub use crate::react_compiler_hir as hir; +pub use crate::react_compiler_hir::environment; +pub use crate::react_compiler_lowering::lower; diff --git a/crates/oxc_react_compiler/src/react_compiler/timing.rs b/crates/oxc_react_compiler/src/react_compiler/timing.rs new file mode 100644 index 0000000000000..d37a81f44b550 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler/timing.rs @@ -0,0 +1,66 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Simple timing accumulator for profiling compiler passes. +//! +//! Uses `std::time::Instant` unconditionally (cheap when not storing results). +//! Controlled by the `__profiling` flag in plugin options. + +use std::time::{Duration, Instant}; + +/// A single timing entry recording how long a named phase took. +#[derive(Debug, Clone)] +pub struct TimingEntry { + pub name: String, + pub duration_us: u64, +} + +/// Accumulates timing data for compiler passes. +pub struct TimingData { + enabled: bool, + entries: Vec<(String, Duration)>, + current_name: Option, + current_start: Option, +} + +impl TimingData { + /// Create a new TimingData. If `enabled` is false, all operations are no-ops. + pub fn new(enabled: bool) -> Self { + Self { enabled, entries: Vec::new(), current_name: None, current_start: None } + } + + /// Start timing a named phase. Stops any currently running phase first. + pub fn start(&mut self, name: &str) { + if !self.enabled { + return; + } + // Stop any currently running phase + if self.current_start.is_some() { + self.stop(); + } + self.current_name = Some(name.to_string()); + self.current_start = Some(Instant::now()); + } + + /// Stop the currently running phase and record its duration. + pub fn stop(&mut self) { + if !self.enabled { + return; + } + if let (Some(name), Some(start)) = (self.current_name.take(), self.current_start.take()) { + self.entries.push((name, start.elapsed())); + } + } + + /// Consume this TimingData and return the collected entries. + pub fn into_entries(mut self) -> Vec { + // Stop any still-running phase + self.stop(); + self.entries + .into_iter() + .map(|(name, duration)| TimingEntry { name, duration_us: duration.as_micros() as u64 }) + .collect() + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_diagnostics/code_frame.rs b/crates/oxc_react_compiler/src/react_compiler_diagnostics/code_frame.rs new file mode 100644 index 0000000000000..d90ca3e103456 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_diagnostics/code_frame.rs @@ -0,0 +1,418 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +use crate::react_compiler_diagnostics::{ + CompilerDiagnosticDetail, CompilerError, CompilerErrorOrDiagnostic, +}; + +const CODEFRAME_LINES_ABOVE: u32 = 2; +const CODEFRAME_LINES_BELOW: u32 = 3; +const CODEFRAME_MAX_LINES: u32 = 10; +const CODEFRAME_ABBREVIATED_SOURCE_LINES: usize = 5; + +/// Split source text on newlines, matching Babel's NEWLINE regex: /\r\n|[\n\r\u2028\u2029]/ +fn split_lines(source: &str) -> Vec<&str> { + let mut lines = Vec::new(); + let mut start = 0; + let bytes = source.as_bytes(); + let len = bytes.len(); + let mut i = 0; + while i < len { + let ch = bytes[i]; + if ch == b'\r' { + lines.push(&source[start..i]); + if i + 1 < len && bytes[i + 1] == b'\n' { + i += 2; + } else { + i += 1; + } + start = i; + } else if ch == b'\n' { + lines.push(&source[start..i]); + i += 1; + start = i; + } else { + // Check for Unicode line separators U+2028 and U+2029 + // These are encoded as E2 80 A8 and E2 80 A9 in UTF-8 + if ch == 0xE2 + && i + 2 < len + && bytes[i + 1] == 0x80 + && (bytes[i + 2] == 0xA8 || bytes[i + 2] == 0xA9) + { + lines.push(&source[start..i]); + i += 3; + start = i; + } else { + i += 1; + } + } + } + lines.push(&source[start..]); + lines +} + +/// Represents a marker line entry: either mark the whole line (true) or a [column, length] range. +#[derive(Clone, Debug)] +enum MarkerEntry { + WholeLine, + Range(usize, usize), // (start_column_1based, length) +} + +/// Compute marker lines matching Babel's getMarkerLines(). +/// All column values here are 1-based (Babel convention). +fn get_marker_lines( + start_line: u32, + start_column: u32, // 1-based + end_line: u32, + end_column: u32, // 1-based + source_line_count: usize, + lines_above: u32, + lines_below: u32, +) -> (usize, usize, Vec<(usize, MarkerEntry)>) { + let start_line = start_line as usize; + let end_line = end_line as usize; + let start_column = start_column as usize; + let end_column = end_column as usize; + + // Compute display range + let start = if start_line > (lines_above as usize + 1) { + start_line - (lines_above as usize + 1) + } else { + 0 + }; + let end = std::cmp::min(source_line_count, end_line + lines_below as usize); + + let line_diff = end_line - start_line; + let mut marker_lines: Vec<(usize, MarkerEntry)> = Vec::new(); + + if line_diff > 0 { + // Multi-line error + for i in 0..=line_diff { + let line_number = i + start_line; + if start_column == 0 { + marker_lines.push((line_number, MarkerEntry::WholeLine)); + } else if i == 0 { + // First line: from start_column to end of source line + // source[lineNumber - 1] gives us the source line (0-indexed array, 1-indexed line numbers) + // But we don't have access to source lines here, so we pass the length through. + // Actually, Babel accesses source[lineNumber - 1].length. We need to thread source lines. + // For now, this is handled in code_frame_columns where we have access to source lines. + // We use a placeholder that will be filled in later. + marker_lines.push((line_number, MarkerEntry::Range(start_column, 0))); // 0 = placeholder + } else if i == line_diff { + marker_lines.push((line_number, MarkerEntry::Range(0, end_column))); + } else { + marker_lines.push((line_number, MarkerEntry::Range(0, 0))); // 0 = placeholder for full line + } + } + } else { + // Single-line error + if start_column == end_column { + if start_column != 0 { + marker_lines.push((start_line, MarkerEntry::Range(start_column, 0))); + } else { + marker_lines.push((start_line, MarkerEntry::WholeLine)); + } + } else { + marker_lines + .push((start_line, MarkerEntry::Range(start_column, end_column - start_column))); + } + } + + (start, end, marker_lines) +} + +/// Produce a code frame matching @babel/code-frame's codeFrameColumns() in non-highlighted mode. +/// +/// Columns are 0-based (matching the Rust/AST convention). They are converted to 1-based +/// internally to match Babel's convention (the JS caller already does column + 1). +pub fn code_frame_columns( + source: &str, + start_line: u32, + start_col: u32, + end_line: u32, + end_col: u32, + message: &str, +) -> String { + // Convert 0-based columns to 1-based (Babel convention) + let start_column_1 = start_col + 1; + let end_column_1 = end_col + 1; + + let lines = split_lines(source); + let source_line_count = lines.len(); + + let (start, end, marker_lines_raw) = get_marker_lines( + start_line, + start_column_1, + end_line, + end_column_1, + source_line_count, + CODEFRAME_LINES_ABOVE, + CODEFRAME_LINES_BELOW, + ); + + let has_columns = start_column_1 > 0; + let number_max_width = format!("{}", end).len(); + + // Build a lookup map for marker lines + let mut marker_map: rustc_hash::FxHashMap = + rustc_hash::FxHashMap::default(); + let line_diff = end_line as usize - start_line as usize; + for (line_number, entry) in marker_lines_raw { + // Resolve placeholder lengths using actual source lines + let resolved = match &entry { + MarkerEntry::Range(col, len) => { + if line_diff > 0 { + let i = line_number - start_line as usize; + if i == 0 && *len == 0 { + // First line of multi-line: from start_column to end of line + let source_length = if line_number >= 1 && line_number <= lines.len() { + lines[line_number - 1].len() + } else { + 0 + }; + MarkerEntry::Range(*col, source_length.saturating_sub(*col) + 1) + } else if i > 0 && i < line_diff && *col == 0 && *len == 0 { + // Middle line of multi-line: Babel uses source[lineNumber - i].length + // which evaluates to source[startLine] (0-indexed array, 1-indexed line number). + // This means all middle lines use the length of source[startLine], + // which is the line at 0-indexed position startLine in the source array. + let source_length = if (start_line as usize) < lines.len() { + lines[start_line as usize].len() + } else { + 0 + }; + MarkerEntry::Range(0, source_length) + } else { + entry + } + } else { + entry + } + } + _ => entry, + }; + marker_map.insert(line_number, resolved); + } + + // Build frame lines + let mut frame_parts: Vec = Vec::new(); + let display_lines = &lines[start..end]; + + for (index, line) in display_lines.iter().enumerate() { + let number = start + 1 + index; + // Right-align the line number: ` ${number}`.slice(-numberMaxWidth) + let number_str = format!("{}", number); + let padded_number = if number_str.len() >= number_max_width { + number_str + } else { + let padding = " ".repeat(number_max_width - number_str.len()); + format!("{}{}", padding, number_str) + }; + let gutter = format!(" {} |", padded_number); + + let has_marker = marker_map.get(&number); + let has_next_marker = marker_map.contains_key(&(number + 1)); + let last_marker_line = has_marker.is_some() && !has_next_marker; + + if let Some(marker_entry) = has_marker { + // This is a marked line + let line_content = if line.is_empty() { String::new() } else { format!(" {}", line) }; + + let marker_line_str = match marker_entry { + MarkerEntry::Range(col, len) => { + // Build marker spacing: replace non-tab chars with spaces + let max_col = if *col > 0 { col - 1 } else { 0 }; + let byte_end = std::cmp::min(max_col, line.len()); + // Ensure we don't slice in the middle of a multi-byte UTF-8 character + let safe_end = if byte_end < line.len() && !line.is_char_boundary(byte_end) { + line.floor_char_boundary(byte_end) + } else { + byte_end + }; + let prefix = &line[..safe_end]; + let marker_spacing: String = + prefix.chars().map(|c| if c == '\t' { '\t' } else { ' ' }).collect(); + let number_of_markers = if *len == 0 { 1 } else { *len }; + let carets = "^".repeat(number_of_markers); + let gutter_spaces = gutter.replace(|c: char| c.is_ascii_digit(), " "); + let mut marker_str = + format!("\n {} {}{}", gutter_spaces, marker_spacing, carets); + if last_marker_line && !message.is_empty() { + marker_str.push(' '); + marker_str.push_str(message); + } + marker_str + } + MarkerEntry::WholeLine => String::new(), + }; + + frame_parts.push(format!(">{}{}{}", gutter, line_content, marker_line_str)); + } else { + // Non-marked line + let line_content = if line.is_empty() { String::new() } else { format!(" {}", line) }; + frame_parts.push(format!(" {}{}", gutter, line_content)); + } + } + + let mut frame = frame_parts.join("\n"); + + // If message is set but no columns, prepend the message + if !message.is_empty() && !has_columns { + frame = format!("{}{}\n{}", " ".repeat(number_max_width + 1), message, frame); + } + + frame +} + +/// Format a code frame with abbreviation for long spans, +/// matching the JS printCodeFrame() function. +pub fn print_code_frame( + source: &str, + start_line: u32, + start_col: u32, + end_line: u32, + end_col: u32, + message: &str, +) -> String { + let printed = code_frame_columns(source, start_line, start_col, end_line, end_col, message); + + if end_line - start_line < CODEFRAME_MAX_LINES { + return printed; + } + + // Abbreviate: truncate middle + let lines: Vec<&str> = printed.split('\n').collect(); + let head_count = CODEFRAME_LINES_ABOVE as usize + CODEFRAME_ABBREVIATED_SOURCE_LINES; + let tail_count = CODEFRAME_LINES_BELOW as usize + CODEFRAME_ABBREVIATED_SOURCE_LINES; + + if lines.len() <= head_count + tail_count { + return printed; + } + + // Find the pipe index from the first line + let pipe_index = lines[0].find('|').unwrap_or(0); + let tail_start = lines.len() - tail_count; + + let mut parts: Vec = Vec::new(); + for line in &lines[..head_count] { + parts.push(line.to_string()); + } + parts.push(format!("{}\u{2026}", " ".repeat(pipe_index))); + for line in &lines[tail_start..] { + parts.push(line.to_string()); + } + parts.join("\n") +} + +use crate::react_compiler_diagnostics::format_category_heading; + +/// Format a CompilerError into a message string matching the TS compiler's +/// CompilerError.printErrorMessage() / formatCompilerError() format. +/// +/// The source parameter is the full source code of the file being compiled. +/// The filename parameter is the source filename (e.g., "foo.ts") used in +/// location displays. +pub fn format_compiler_error(err: &CompilerError, source: &str, filename: Option<&str>) -> String { + let detail_messages: Vec = + err.details.iter().map(|d| format_error_detail(d, source, filename)).collect(); + + let count = err.details.len(); + let plural = if count == 1 { "" } else { "s" }; + let header = format!("Found {} error{}:\n\n", count, plural); + + let trimmed: Vec = detail_messages.iter().map(|m| m.trim().to_string()).collect(); + format!("{}{}", header, trimmed.join("\n\n")) +} + +/// Format a single error detail (either Diagnostic or ErrorDetail). +fn format_error_detail( + detail: &CompilerErrorOrDiagnostic, + source: &str, + filename: Option<&str>, +) -> String { + match detail { + CompilerErrorOrDiagnostic::Diagnostic(d) => { + let heading = format_category_heading(d.category); + let mut buffer = vec![format!("{}: {}", heading, d.reason)]; + + if let Some(ref description) = d.description { + buffer.push(format!("\n\n{}.", description)); + } + for item in &d.details { + match item { + CompilerDiagnosticDetail::Error { loc, message, .. } => { + if let Some(loc) = loc { + let frame = print_code_frame( + source, + loc.start.line, + loc.start.column, + loc.end.line, + loc.end.column, + message.as_deref().unwrap_or(""), + ); + buffer.push("\n\n".to_string()); + if let Some(fname) = filename { + buffer.push(format!( + "{}:{}:{}\n", + fname, loc.start.line, loc.start.column + )); + } + buffer.push(frame); + } + } + CompilerDiagnosticDetail::Hint { message } => { + buffer.push("\n\n".to_string()); + buffer.push(message.clone()); + } + } + } + + buffer.join("") + } + CompilerErrorOrDiagnostic::ErrorDetail(d) => { + let heading = format_category_heading(d.category); + let mut buffer = vec![format!("{}: {}", heading, d.reason)]; + + if let Some(ref description) = d.description { + buffer.push(format!("\n\n{}.", description)); + if let Some(ref loc) = d.loc { + let frame = print_code_frame( + source, + loc.start.line, + loc.start.column, + loc.end.line, + loc.end.column, + &d.reason, + ); + buffer.push("\n\n".to_string()); + if let Some(fname) = filename { + buffer.push(format!("{}:{}:{}\n", fname, loc.start.line, loc.start.column)); + } + buffer.push(frame); + buffer.push("\n\n".to_string()); + } + } else if let Some(ref loc) = d.loc { + let frame = print_code_frame( + source, + loc.start.line, + loc.start.column, + loc.end.line, + loc.end.column, + &d.reason, + ); + buffer.push("\n\n".to_string()); + if let Some(fname) = filename { + buffer.push(format!("{}:{}:{}\n", fname, loc.start.line, loc.start.column)); + } + buffer.push(frame); + buffer.push("\n\n".to_string()); + } + + buffer.join("") + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_diagnostics/js_string.rs b/crates/oxc_react_compiler/src/react_compiler_diagnostics/js_string.rs new file mode 100644 index 0000000000000..81721397a01ec --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_diagnostics/js_string.rs @@ -0,0 +1,313 @@ +//! A JavaScript string value. JS strings are sequences of UTF-16 code units +//! with no validity requirement, so a value can contain unpaired surrogate +//! halves that Rust's `String` cannot represent. `JsString` keeps the common +//! valid case as UTF-8 and falls back to code units only when the value is +//! ill-formed, so the compiler computes on true program values instead of +//! replacement characters or escape hatches. +//! +//! Wire format: the babel bridge transports lone surrogates as +//! `__SURROGATE_XXXX__` markers (see `sanitizeJsonSurrogates` in bridge.ts), +//! because serde_json can neither parse nor emit a lone `\uXXXX` escape. +//! Serde for `JsString` decodes and re-emits that marker form, which keeps the +//! JS side of the bridge unchanged. + +use std::fmt; + +/// Invariant: `Repr::Utf8` holds every well-formed value and `Repr::Wtf16` +/// only ill-formed ones (at least one unpaired surrogate). The derived +/// `PartialEq`/`Hash` are only sound under this invariant: a well-formed +/// value smuggled into `Wtf16` would compare unequal to its `Utf8` twin. The +/// representation is private so the invariant holds by construction; match on +/// [`JsString::as_ref`] to branch on well-formedness. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct JsString(Repr); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Repr { + /// A well-formed string (no unpaired surrogates), stored as UTF-8. + Utf8(String), + /// An ill-formed string, stored as UTF-16 code units. + Wtf16(Vec), +} + +/// Borrowed view of a [`JsString`] for callers that need to branch on +/// well-formedness. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JsStringRef<'a> { + Utf8(&'a str), + Wtf16(&'a [u16]), +} + +impl JsString { + /// Build from UTF-16 code units, normalizing to UTF-8 when well-formed. + pub fn from_code_units(units: Vec) -> Self { + match String::from_utf16(&units) { + Ok(s) => JsString(Repr::Utf8(s)), + Err(_) => JsString(Repr::Wtf16(units)), + } + } + + pub fn as_ref(&self) -> JsStringRef<'_> { + match &self.0 { + Repr::Utf8(s) => JsStringRef::Utf8(s), + Repr::Wtf16(units) => JsStringRef::Wtf16(units), + } + } + + /// The UTF-8 view, when the value is well-formed. + pub fn as_str(&self) -> Option<&str> { + match &self.0 { + Repr::Utf8(s) => Some(s), + Repr::Wtf16(_) => None, + } + } + + pub fn code_units(&self) -> Vec { + match &self.0 { + Repr::Utf8(s) => s.encode_utf16().collect(), + Repr::Wtf16(units) => units.clone(), + } + } + + /// Length in UTF-16 code units (JS `String.prototype.length`). + pub fn len_utf16(&self) -> usize { + match &self.0 { + Repr::Utf8(s) => s.encode_utf16().count(), + Repr::Wtf16(units) => units.len(), + } + } + + /// The value with unpaired surrogates replaced by U+FFFD, for consumers + /// whose string type cannot represent ill-formed values. + pub fn to_string_lossy(&self) -> String { + match &self.0 { + Repr::Utf8(s) => s.clone(), + Repr::Wtf16(units) => String::from_utf16_lossy(units), + } + } + + /// Decode the bridge wire form: a UTF-8 string in which lone surrogates + /// appear as `__SURROGATE_XXXX__` markers (uppercase hex, mirroring what + /// `sanitizeJsonSurrogates` emits and `restoreJsonSurrogates` accepts). + /// + /// All scanning is byte-wise: a marker is 18 ASCII bytes, so byte-slice + /// comparisons cannot land on a UTF-8 char boundary the way `str` range + /// indexing can when multibyte text follows the prefix. + pub fn from_marker_string(s: &str) -> Self { + const PREFIX: &[u8] = b"__SURROGATE_"; + const MARKER_LEN: usize = 18; + if !s.contains("__SURROGATE_") { + return JsString(Repr::Utf8(s.to_string())); + } + let bytes = s.as_bytes(); + let mut units: Vec = Vec::with_capacity(s.len()); + let mut pos = 0; + let mut segment_start = 0; + while let Some(found) = s[pos..].find("__SURROGATE_") { + let idx = pos + found; + let tail = &bytes[idx..]; + let well_formed = tail.len() >= MARKER_LEN + && &tail[MARKER_LEN - 2..MARKER_LEN] == b"__" + && tail[PREFIX.len()..PREFIX.len() + 4] + .iter() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_lowercase()); + if well_formed { + let hex = std::str::from_utf8(&tail[PREFIX.len()..PREFIX.len() + 4]) + .expect("ascii hex is valid utf8"); + let unit = u16::from_str_radix(hex, 16).expect("validated hex digits"); + units.extend(s[segment_start..idx].encode_utf16()); + units.push(unit); + pos = idx + MARKER_LEN; + segment_start = pos; + } else { + // Not a well-formed marker: keep the literal text and continue + // scanning after the prefix. + pos = idx + PREFIX.len(); + } + } + units.extend(s[segment_start..].encode_utf16()); + JsString::from_code_units(units) + } + + /// Encode to the bridge wire form (markers for unpaired surrogates). + pub fn to_marker_string(&self) -> String { + match &self.0 { + Repr::Utf8(s) => s.clone(), + Repr::Wtf16(units) => { + let mut out = String::with_capacity(units.len() * 2); + let mut iter = units.iter().copied().peekable(); + while let Some(unit) = iter.next() { + match unit { + 0xD800..=0xDBFF => { + if let Some(&next) = iter.peek() { + if (0xDC00..=0xDFFF).contains(&next) { + iter.next(); + let cp = 0x10000 + + ((unit as u32 - 0xD800) << 10) + + (next as u32 - 0xDC00); + out.push(char::from_u32(cp).expect("valid supplementary")); + continue; + } + } + out.push_str(&format!("__SURROGATE_{unit:04X}__")); + } + 0xDC00..=0xDFFF => { + out.push_str(&format!("__SURROGATE_{unit:04X}__")); + } + _ => { + out.push( + char::from_u32(unit as u32).expect("BMP non-surrogate is a char"), + ); + } + } + } + out + } + } + } + + /// Render as JS-source-style escaped text, matching the form TS's debug + /// printer produces via JSON.stringify: unpaired surrogates print as + /// lowercase `\udXXX` escapes inside the otherwise UTF-8 text. + pub fn to_escaped_string(&self) -> String { + match &self.0 { + Repr::Utf8(s) => s.clone(), + Repr::Wtf16(units) => { + let mut out = String::with_capacity(units.len() * 2); + let mut iter = units.iter().copied().peekable(); + while let Some(unit) = iter.next() { + match unit { + 0xD800..=0xDBFF => { + if let Some(&next) = iter.peek() { + if (0xDC00..=0xDFFF).contains(&next) { + iter.next(); + let cp = 0x10000 + + ((unit as u32 - 0xD800) << 10) + + (next as u32 - 0xDC00); + out.push(char::from_u32(cp).expect("valid supplementary")); + continue; + } + } + out.push_str(&format!("\\u{unit:04x}")); + } + 0xDC00..=0xDFFF => { + out.push_str(&format!("\\u{unit:04x}")); + } + _ => { + out.push( + char::from_u32(unit as u32).expect("BMP non-surrogate is a char"), + ); + } + } + } + out + } + } + } +} + +impl From for JsString { + fn from(s: String) -> Self { + // A Rust String is valid UTF-8 and so cannot contain an unpaired + // surrogate; constructing Utf8 directly preserves the invariant. + JsString(Repr::Utf8(s)) + } +} + +impl From<&str> for JsString { + fn from(s: &str) -> Self { + JsString(Repr::Utf8(s.to_string())) + } +} + +impl PartialEq for JsString { + fn eq(&self, other: &str) -> bool { + self.as_str() == Some(other) + } +} + +impl PartialEq<&str> for JsString { + fn eq(&self, other: &&str) -> bool { + self.as_str() == Some(*other) + } +} + +impl fmt::Display for JsString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_escaped_string()) + } +} + +#[cfg(test)] +mod tests { + use super::JsString; + use super::JsStringRef; + + #[test] + fn as_ref_views_match_well_formedness() { + assert!(matches!(JsString::from("plain").as_ref(), JsStringRef::Utf8("plain"))); + assert!(matches!( + JsString::from_code_units(vec![0xD83E]).as_ref(), + JsStringRef::Wtf16(&[0xD83E]) + )); + // Well-formed code units normalize to the Utf8 representation, so + // equal logical strings are equal values regardless of how they + // were constructed. + assert_eq!( + JsString::from_code_units("plain".encode_utf16().collect()), + JsString::from("plain") + ); + } + + #[test] + fn marker_round_trip_preserves_lone_surrogates() { + let js = JsString::from_marker_string("__SURROGATE_D83E__"); + assert_eq!(js.code_units(), vec![0xD83E]); + assert_eq!(js.to_marker_string(), "__SURROGATE_D83E__"); + assert_eq!(js.to_escaped_string(), "\\ud83e"); + } + + #[test] + fn paired_halves_render_as_the_supplementary_character() { + let js = JsString::from_code_units(vec![0xD83E, 0xDD21]); + assert_eq!(js.as_str(), Some("\u{1F921}")); + } + + #[test] + fn plain_strings_stay_utf8_and_compare_with_str() { + let js = JsString::from("use memo"); + assert!(js == "use memo"); + assert_eq!(js.to_marker_string(), "use memo"); + } + + #[test] + fn malformed_marker_text_is_kept_literally() { + let js = JsString::from_marker_string("__SURROGATE_XYZ__"); + assert_eq!(js.as_str(), Some("__SURROGATE_XYZ__")); + } + + #[test] + fn multibyte_text_after_marker_prefix_does_not_panic() { + let input = "__SURROGATE_\u{20AC}\u{20AC}"; + let js = JsString::from_marker_string(input); + assert_eq!(js.as_str(), Some(input)); + + let truncated = "__SURROGATE_D8"; + assert_eq!(JsString::from_marker_string(truncated).as_str(), Some(truncated)); + + let mixed = "a\u{20AC}__SURROGATE_D83E__b\u{20AC}"; + let js = JsString::from_marker_string(mixed); + let mut expected: Vec = "a\u{20AC}".encode_utf16().collect(); + expected.push(0xD83E); + expected.extend("b\u{20AC}".encode_utf16()); + assert_eq!(js.code_units(), expected); + } + + #[test] + fn lowercase_hex_markers_are_not_decoded() { + // The bridge emits uppercase hex only; lowercase marker-shaped text is + // user text and must survive verbatim. + let input = "__SURROGATE_d83e__"; + assert_eq!(JsString::from_marker_string(input).as_str(), Some(input)); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_diagnostics/mod.rs b/crates/oxc_react_compiler/src/react_compiler_diagnostics/mod.rs new file mode 100644 index 0000000000000..aaba5bafafadc --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_diagnostics/mod.rs @@ -0,0 +1,438 @@ +pub mod code_frame; +pub mod js_string; + +pub use js_string::JsString; + +/// Error categories matching the TS ErrorCategory enum +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCategory { + Hooks, + CapitalizedCalls, + StaticComponents, + UseMemo, + VoidUseMemo, + PreserveManualMemo, + MemoDependencies, + IncompatibleLibrary, + Immutability, + Globals, + Refs, + EffectDependencies, + EffectExhaustiveDependencies, + EffectSetState, + EffectDerivationsOfState, + ErrorBoundaries, + Purity, + RenderSetState, + Invariant, + Todo, + Syntax, + UnsupportedSyntax, + Config, + Gating, + Suppression, + FBT, +} + +/// Error severity levels +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorSeverity { + Error, + Warning, + Hint, + Off, +} + +impl ErrorCategory { + pub fn severity(&self) -> ErrorSeverity { + match self { + // These map to "Compilation Skipped" (Warning severity) + ErrorCategory::EffectDependencies + | ErrorCategory::IncompatibleLibrary + | ErrorCategory::PreserveManualMemo + | ErrorCategory::UnsupportedSyntax => ErrorSeverity::Warning, + + // Todo is Hint + ErrorCategory::Todo => ErrorSeverity::Hint, + + // Invariant and all others are Error severity + _ => ErrorSeverity::Error, + } + } + + /// The severity to use in logged output, matching the TS compiler's + /// `getRuleForCategory()`. This may differ from the internal `severity()` + /// used for panicThreshold logic. In particular, `PreserveManualMemo` is + /// `Warning` internally (so it doesn't trigger panicThreshold throws) but + /// `Error` in logged output (matching TS behavior). + pub fn logged_severity(&self) -> ErrorSeverity { + match self { + ErrorCategory::PreserveManualMemo => ErrorSeverity::Error, + _ => self.severity(), + } + } +} + +/// Suggestion operations for auto-fixes +#[derive(Debug, Clone)] +pub enum CompilerSuggestionOperation { + InsertBefore, + InsertAfter, + Remove, + Replace, +} + +/// A compiler suggestion for fixing an error +#[derive(Debug, Clone)] +pub struct CompilerSuggestion { + pub op: CompilerSuggestionOperation, + pub range: (usize, usize), + pub description: String, + pub text: Option, // None for Remove operations +} + +/// Source location (matches Babel's SourceLocation format) +/// This is the HIR source location, separate from AST's BaseNode location. +/// GeneratedSource is represented as None. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SourceLocation { + pub start: Position, + pub end: Position, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Position { + pub line: u32, + pub column: u32, + /// Byte offset in the source file. Preserved for logger event serialization. + pub index: Option, +} + +/// Sentinel value for generated/synthetic source locations +pub const GENERATED_SOURCE: Option = None; + +/// Detail for a diagnostic +#[derive(Debug, Clone)] +pub enum CompilerDiagnosticDetail { + Error { + loc: Option, + message: Option, + /// The identifier name from the AST source location, if this error + /// points to an identifier node. Preserved for logger event serialization + /// to match Babel's SourceLocation.identifierName field. + identifier_name: Option, + }, + Hint { + message: String, + }, +} + +/// A single compiler diagnostic (new-style) +#[derive(Debug, Clone)] +pub struct CompilerDiagnostic { + pub category: ErrorCategory, + pub reason: String, + pub description: Option, + pub details: Vec, + pub suggestions: Option>, +} + +impl CompilerDiagnostic { + pub fn new( + category: ErrorCategory, + reason: impl Into, + description: Option, + ) -> Self { + Self { + category, + reason: reason.into(), + description, + details: Vec::new(), + suggestions: None, + } + } + + pub fn severity(&self) -> ErrorSeverity { + self.category.severity() + } + + pub fn logged_severity(&self) -> ErrorSeverity { + self.category.logged_severity() + } + + pub fn with_detail(mut self, detail: CompilerDiagnosticDetail) -> Self { + self.details.push(detail); + self + } + + /// Create a Todo diagnostic (matches TS `CompilerError.throwTodo()`). + pub fn todo(reason: impl Into, loc: Option) -> Self { + let reason = reason.into(); + let mut diag = Self::new(ErrorCategory::Todo, reason.clone(), None); + diag.details.push(CompilerDiagnosticDetail::Error { + loc, + message: Some(reason), + identifier_name: None, + }); + diag + } + + /// Create a diagnostic from a CompilerErrorDetail. + pub fn from_detail(detail: CompilerErrorDetail) -> Self { + Self::new(detail.category, detail.reason.clone(), detail.description.clone()).with_detail( + CompilerDiagnosticDetail::Error { + loc: detail.loc, + message: Some(detail.reason), + identifier_name: None, + }, + ) + } + + pub fn primary_location(&self) -> Option<&SourceLocation> { + self.details.iter().find_map(|d| match d { + CompilerDiagnosticDetail::Error { loc, .. } => loc.as_ref(), // identifier_name covered by .. + _ => None, + }) + } +} + +/// Legacy-style error detail (matches CompilerErrorDetail in TS) +#[derive(Debug, Clone)] +pub struct CompilerErrorDetail { + pub category: ErrorCategory, + pub reason: String, + pub description: Option, + pub loc: Option, + pub suggestions: Option>, +} + +impl CompilerErrorDetail { + pub fn new(category: ErrorCategory, reason: impl Into) -> Self { + Self { category, reason: reason.into(), description: None, loc: None, suggestions: None } + } + + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn with_loc(mut self, loc: Option) -> Self { + self.loc = loc; + self + } + + pub fn severity(&self) -> ErrorSeverity { + self.category.severity() + } + + pub fn logged_severity(&self) -> ErrorSeverity { + self.category.logged_severity() + } +} + +/// Aggregate compiler error - can contain multiple diagnostics. +/// This is the main error type thrown/returned by the compiler. +#[derive(Debug, Clone)] +pub struct CompilerError { + pub details: Vec, + /// When false, this error was accumulated on the Environment via + /// `record_error()` / `record_diagnostic()` and returned at the end + /// of the pipeline. In TS, `CompileUnexpectedThrow` is only emitted + /// for errors that are **thrown** (not accumulated). Defaults to `true` + /// because errors created directly (e.g., via `?` from a pass) are + /// analogous to thrown errors in the TS code. + pub is_thrown: bool, +} + +/// Either a new-style diagnostic or legacy error detail +#[derive(Debug, Clone)] +pub enum CompilerErrorOrDiagnostic { + Diagnostic(CompilerDiagnostic), + ErrorDetail(CompilerErrorDetail), +} + +impl CompilerErrorOrDiagnostic { + pub fn severity(&self) -> ErrorSeverity { + match self { + Self::Diagnostic(d) => d.severity(), + Self::ErrorDetail(d) => d.severity(), + } + } + + pub fn logged_severity(&self) -> ErrorSeverity { + match self { + Self::Diagnostic(d) => d.logged_severity(), + Self::ErrorDetail(d) => d.logged_severity(), + } + } +} + +impl CompilerError { + pub fn new() -> Self { + Self { details: Vec::new(), is_thrown: true } + } + + pub fn push_diagnostic(&mut self, diagnostic: CompilerDiagnostic) { + if diagnostic.severity() != ErrorSeverity::Off { + self.details.push(CompilerErrorOrDiagnostic::Diagnostic(diagnostic)); + } + } + + pub fn push_error_detail(&mut self, detail: CompilerErrorDetail) { + if detail.severity() != ErrorSeverity::Off { + self.details.push(CompilerErrorOrDiagnostic::ErrorDetail(detail)); + } + } + + pub fn has_errors(&self) -> bool { + self.details.iter().any(|d| d.severity() == ErrorSeverity::Error) + } + + pub fn has_any_errors(&self) -> bool { + !self.details.is_empty() + } + + /// Check if any error detail has Invariant category. + pub fn has_invariant_errors(&self) -> bool { + self.details.iter().any(|d| { + let cat = match d { + CompilerErrorOrDiagnostic::Diagnostic(d) => d.category, + CompilerErrorOrDiagnostic::ErrorDetail(d) => d.category, + }; + cat == ErrorCategory::Invariant + }) + } + + pub fn merge(&mut self, other: CompilerError) { + self.details.extend(other.details); + } + + /// Check if all error details are non-invariant. + /// In TS, this is used to determine if an error thrown during compilation + /// should be logged as CompileUnexpectedThrow. + pub fn is_all_non_invariant(&self) -> bool { + self.details.iter().all(|d| { + let cat = match d { + CompilerErrorOrDiagnostic::Diagnostic(d) => d.category, + CompilerErrorOrDiagnostic::ErrorDetail(d) => d.category, + }; + cat != ErrorCategory::Invariant + }) + } + + /// Format as a string matching the TS `CompilerError.toString()` output. + /// Used for the `data` field of `CompileUnexpectedThrow` events. + /// + /// Format per detail: `"Category: reason. Description. (line:column)"` + /// Multiple details are joined with `"\n\n"`. + pub fn to_string_for_event(&self) -> String { + self.details + .iter() + .map(|d| { + let (category, reason, description, loc) = match d { + CompilerErrorOrDiagnostic::Diagnostic(d) => { + let loc = d.primary_location().cloned(); + (d.category, &d.reason, &d.description, loc) + } + CompilerErrorOrDiagnostic::ErrorDetail(d) => { + (d.category, &d.reason, &d.description, d.loc) + } + }; + let mut buf = format!("{}: {}", format_category_heading(category), reason); + if let Some(desc) = description { + buf.push_str(&format!(". {}.", desc)); + } + if let Some(loc) = loc { + buf.push_str(&format!(" ({}:{})", loc.start.line, loc.start.column)); + } + buf + }) + .collect::>() + .join("\n\n") + } +} + +impl Default for CompilerError { + fn default() -> Self { + Self::new() + } +} + +/// Allow `?` to convert a `CompilerError` into a `CompilerDiagnostic` +/// when the enclosing function returns `Result`. +/// +/// This typically happens when `record_error()` returns `Err(CompilerError)` +/// for an Invariant error, and the calling function already returns +/// `Result`. The conversion extracts the first +/// error detail from the aggregate error. +impl From for CompilerDiagnostic { + fn from(err: CompilerError) -> Self { + if let Some(first) = err.details.into_iter().next() { + match first { + CompilerErrorOrDiagnostic::Diagnostic(d) => d, + CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerDiagnostic::from_detail(d), + } + } else { + CompilerDiagnostic::new(ErrorCategory::Invariant, "Unknown compiler error", None) + } + } +} + +impl From for CompilerError { + fn from(diagnostic: CompilerDiagnostic) -> Self { + let mut error = CompilerError::new(); + // Todo diagnostics should produce ErrorDetail (flat loc format), matching + // the TS behavior where CompilerError.throwTodo() creates a CompilerErrorDetail + // with loc directly on it, not a CompilerDiagnostic with sub-details. + if diagnostic.category == ErrorCategory::Todo { + let loc = diagnostic.primary_location().cloned(); + error.push_error_detail(CompilerErrorDetail { + category: diagnostic.category, + reason: diagnostic.reason, + description: diagnostic.description, + loc, + suggestions: diagnostic.suggestions, + }); + } else { + error.push_diagnostic(diagnostic); + } + error + } +} + +impl std::fmt::Display for CompilerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for detail in &self.details { + match detail { + CompilerErrorOrDiagnostic::Diagnostic(d) => { + write!(f, "{}: {}", format_category_heading(d.category), d.reason)?; + if let Some(desc) = &d.description { + write!(f, ". {}.", desc)?; + } + } + CompilerErrorOrDiagnostic::ErrorDetail(d) => { + write!(f, "{}: {}", format_category_heading(d.category), d.reason)?; + if let Some(desc) = &d.description { + write!(f, ". {}.", desc)?; + } + } + } + writeln!(f)?; + } + Ok(()) + } +} + +impl std::error::Error for CompilerError {} + +pub fn format_category_heading(category: ErrorCategory) -> &'static str { + match category { + ErrorCategory::EffectDependencies + | ErrorCategory::IncompatibleLibrary + | ErrorCategory::PreserveManualMemo + | ErrorCategory::UnsupportedSyntax => "Compilation Skipped", + ErrorCategory::Invariant => "Invariant", + ErrorCategory::Todo => "Todo", + _ => "Error", + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/default_module_type_provider.rs b/crates/oxc_react_compiler/src/react_compiler_hir/default_module_type_provider.rs new file mode 100644 index 0000000000000..b5e04ed3eca67 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/default_module_type_provider.rs @@ -0,0 +1,100 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Default module type provider, ported from DefaultModuleTypeProvider.ts. +//! +//! Provides hardcoded type overrides for known-incompatible third-party libraries. + +use crate::react_compiler_utils::FxIndexMap; + +use crate::react_compiler_hir::Effect; +use crate::react_compiler_hir::type_config::{ + BuiltInTypeRef, FunctionTypeConfig, HookTypeConfig, ObjectTypeConfig, TypeConfig, + TypeReferenceConfig, ValueKind, +}; + +/// Returns type configuration for known third-party modules that are +/// incompatible with memoization. Ported from TS `defaultModuleTypeProvider`. +pub fn default_module_type_provider(module_name: &str) -> Option { + match module_name { + "react-hook-form" => Some(TypeConfig::Object(ObjectTypeConfig { + properties: Some(FxIndexMap::from_iter([( + "useForm".to_string(), + TypeConfig::Hook(HookTypeConfig { + return_type: Box::new(TypeConfig::Object(ObjectTypeConfig { + properties: Some(FxIndexMap::from_iter([( + "watch".to_string(), + TypeConfig::Function(FunctionTypeConfig { + positional_params: Vec::new(), + rest_param: Some(Effect::Read), + callee_effect: Effect::Read, + return_type: Box::new(TypeConfig::TypeReference( + TypeReferenceConfig { + name: BuiltInTypeRef::Any, + }, + )), + return_value_kind: ValueKind::Mutable, + no_alias: None, + mutable_only_if_operands_are_mutable: None, + impure: None, + canonical_name: None, + aliasing: None, + known_incompatible: Some( + "React Hook Form's `useForm()` API returns a `watch()` function which cannot be memoized safely.".to_string(), + ), + }), + )])), + })), + positional_params: None, + rest_param: None, + return_value_kind: None, + no_alias: None, + aliasing: None, + known_incompatible: None, + }), + )])), + })), + + "@tanstack/react-table" => Some(TypeConfig::Object(ObjectTypeConfig { + properties: Some(FxIndexMap::from_iter([( + "useReactTable".to_string(), + TypeConfig::Hook(HookTypeConfig { + positional_params: Some(Vec::new()), + rest_param: Some(Effect::Read), + return_type: Box::new(TypeConfig::TypeReference(TypeReferenceConfig { + name: BuiltInTypeRef::Any, + })), + return_value_kind: None, + no_alias: None, + aliasing: None, + known_incompatible: Some( + "TanStack Table's `useReactTable()` API returns functions that cannot be memoized safely".to_string(), + ), + }), + )])), + })), + + "@tanstack/react-virtual" => Some(TypeConfig::Object(ObjectTypeConfig { + properties: Some(FxIndexMap::from_iter([( + "useVirtualizer".to_string(), + TypeConfig::Hook(HookTypeConfig { + positional_params: Some(Vec::new()), + rest_param: Some(Effect::Read), + return_type: Box::new(TypeConfig::TypeReference(TypeReferenceConfig { + name: BuiltInTypeRef::Any, + })), + return_value_kind: None, + no_alias: None, + aliasing: None, + known_incompatible: Some( + "TanStack Virtual's `useVirtualizer()` API returns functions that cannot be memoized safely".to_string(), + ), + }), + )])), + })), + + _ => None, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/dominator.rs b/crates/oxc_react_compiler/src/react_compiler_hir/dominator.rs new file mode 100644 index 0000000000000..f041c00f6f974 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/dominator.rs @@ -0,0 +1,334 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Dominator and post-dominator tree computation. +//! +//! Port of Dominator.ts and ComputeUnconditionalBlocks.ts. +//! Uses the Cooper/Harvey/Kennedy algorithm from +//! https://www.cs.rice.edu/~keith/Embed/dom.pdf + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; + +use crate::react_compiler_hir::visitors::each_terminal_successor; +use crate::react_compiler_hir::{BlockId, HirFunction, Terminal}; + +// ============================================================================= +// Public types +// ============================================================================= + +/// Stores the immediate post-dominator for each block. +pub struct PostDominator { + /// The exit node (synthetic node representing function exit). + pub exit: BlockId, + nodes: FxHashMap, +} + +impl PostDominator { + /// Returns the immediate post-dominator of the given block, or None if + /// the block post-dominates itself (i.e., it is the exit node). + pub fn get(&self, id: BlockId) -> Option { + let dominator = self.nodes.get(&id).expect("Unknown node in post-dominator tree"); + if *dominator == id { None } else { Some(*dominator) } + } +} + +// ============================================================================= +// Graph representation +// ============================================================================= + +struct Node { + id: BlockId, + index: usize, + preds: FxHashSet, + succs: FxHashSet, +} + +struct Graph { + entry: BlockId, + /// Nodes stored in iteration order (RPO for reverse graph). + nodes: Vec, + /// Map from BlockId to index in the nodes vec. + node_index: FxHashMap, +} + +impl Graph { + fn get_node(&self, id: BlockId) -> &Node { + let idx = self.node_index[&id]; + &self.nodes[idx] + } +} + +// ============================================================================= +// Post-dominator tree computation +// ============================================================================= + +/// Compute the post-dominator tree for a function. +/// +/// If `include_throws_as_exit_node` is true, throw terminals are treated as +/// exit nodes (like return). Otherwise, only return terminals feed into exit. +pub fn compute_post_dominator_tree( + func: &HirFunction, + next_block_id_counter: u32, + include_throws_as_exit_node: bool, +) -> Result { + let graph = build_reverse_graph(func, next_block_id_counter, include_throws_as_exit_node); + let mut nodes = compute_immediate_dominators(&graph)?; + + // When include_throws_as_exit_node is false, nodes that flow into a throw + // terminal and don't reach the exit won't be in the node map. Add them + // with themselves as dominator. + if !include_throws_as_exit_node { + for (id, _) in &func.body.blocks { + nodes.entry(*id).or_insert(*id); + } + } + + Ok(PostDominator { exit: graph.entry, nodes }) +} + +/// Build the reverse graph from the HIR function. +/// +/// Reverses all edges and adds a synthetic exit node that receives edges from +/// return (and optionally throw) terminals. The result is put into RPO order. +fn build_reverse_graph( + func: &HirFunction, + next_block_id_counter: u32, + include_throws_as_exit_node: bool, +) -> Graph { + let exit_id = BlockId(next_block_id_counter); + + // Build initial nodes with reversed edges + let mut raw_nodes: FxHashMap = FxHashMap::default(); + + // Create exit node + raw_nodes.insert( + exit_id, + Node { id: exit_id, index: 0, preds: FxHashSet::default(), succs: FxHashSet::default() }, + ); + + for (id, block) in &func.body.blocks { + let successors = each_terminal_successor(&block.terminal); + let mut preds_set: FxHashSet = successors.into_iter().collect(); + let succs_set: FxHashSet = block.preds.iter().copied().collect(); + + let is_return = matches!(&block.terminal, Terminal::Return { .. }); + let is_throw = matches!(&block.terminal, Terminal::Throw { .. }); + + if is_return || (is_throw && include_throws_as_exit_node) { + preds_set.insert(exit_id); + raw_nodes.get_mut(&exit_id).unwrap().succs.insert(*id); + } + + raw_nodes.insert(*id, Node { id: *id, index: 0, preds: preds_set, succs: succs_set }); + } + + // DFS from exit to compute RPO + let mut visited = FxHashSet::default(); + let mut postorder = Vec::new(); + dfs_postorder(exit_id, &raw_nodes, &mut visited, &mut postorder); + + // Reverse postorder + postorder.reverse(); + + let mut nodes = Vec::with_capacity(postorder.len()); + let mut node_index = FxHashMap::default(); + for (idx, id) in postorder.into_iter().enumerate() { + let mut node = raw_nodes.remove(&id).unwrap(); + node.index = idx; + node_index.insert(id, idx); + nodes.push(node); + } + + Graph { entry: exit_id, nodes, node_index } +} + +fn dfs_postorder( + id: BlockId, + nodes: &FxHashMap, + visited: &mut FxHashSet, + postorder: &mut Vec, +) { + if !visited.insert(id) { + return; + } + if let Some(node) = nodes.get(&id) { + for &succ in &node.succs { + dfs_postorder(succ, nodes, visited, postorder); + } + } + postorder.push(id); +} + +// ============================================================================= +// Dominator fixpoint (Cooper/Harvey/Kennedy) +// ============================================================================= + +fn compute_immediate_dominators( + graph: &Graph, +) -> Result, CompilerDiagnostic> { + let mut doms: FxHashMap = FxHashMap::default(); + doms.insert(graph.entry, graph.entry); + + let mut changed = true; + while changed { + changed = false; + for node in &graph.nodes { + if node.id == graph.entry { + continue; + } + + // Find first processed predecessor + let mut new_idom: Option = None; + for &pred in &node.preds { + if doms.contains_key(&pred) { + new_idom = Some(pred); + break; + } + } + let mut new_idom = match new_idom { + Some(idom) => idom, + None => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!( + "At least one predecessor must have been visited for block {:?}", + node.id + ), + None, + )); + } + }; + + // Intersect with other processed predecessors + for &pred in &node.preds { + if pred == new_idom { + continue; + } + if doms.contains_key(&pred) { + new_idom = intersect(pred, new_idom, graph, &doms); + } + } + + if doms.get(&node.id) != Some(&new_idom) { + doms.insert(node.id, new_idom); + changed = true; + } + } + } + Ok(doms) +} + +fn intersect(a: BlockId, b: BlockId, graph: &Graph, doms: &FxHashMap) -> BlockId { + let mut block1 = graph.get_node(a); + let mut block2 = graph.get_node(b); + while block1.id != block2.id { + while block1.index > block2.index { + let dom = doms[&block1.id]; + block1 = graph.get_node(dom); + } + while block2.index > block1.index { + let dom = doms[&block2.id]; + block2 = graph.get_node(dom); + } + } + block1.id +} + +// ============================================================================= +// Post-dominator frontier +// ============================================================================= + +/// Computes the post-dominator frontier of `target_id`. These are immediate +/// predecessors of nodes that post-dominate `target_id` from which execution may +/// not reach `target_id`. Intuitively, these are the earliest blocks from which +/// execution branches such that it may or may not reach the target block. +pub fn post_dominator_frontier( + func: &HirFunction, + post_dominators: &PostDominator, + target_id: BlockId, +) -> FxHashSet { + let target_post_dominators = post_dominators_of(func, post_dominators, target_id); + let mut visited = FxHashSet::default(); + let mut frontier = FxHashSet::default(); + + let mut to_visit: Vec = target_post_dominators.iter().copied().collect(); + to_visit.push(target_id); + + for block_id in to_visit { + if !visited.insert(block_id) { + continue; + } + if let Some(block) = func.body.blocks.get(&block_id) { + for &pred in &block.preds { + if !target_post_dominators.contains(&pred) { + frontier.insert(pred); + } + } + } + } + frontier +} + +/// Walks up the post-dominator tree to collect all blocks that post-dominate `target_id`. +pub fn post_dominators_of( + func: &HirFunction, + post_dominators: &PostDominator, + target_id: BlockId, +) -> FxHashSet { + let mut result = FxHashSet::default(); + let mut visited = FxHashSet::default(); + let mut queue = vec![target_id]; + + while let Some(current_id) = queue.pop() { + if !visited.insert(current_id) { + continue; + } + if let Some(block) = func.body.blocks.get(¤t_id) { + for &pred in &block.preds { + let pred_post_dom = post_dominators.get(pred).unwrap_or(pred); + if pred_post_dom == target_id || result.contains(&pred_post_dom) { + result.insert(pred); + } + queue.push(pred); + } + } + } + result +} + +// ============================================================================= +// Unconditional blocks +// ============================================================================= + +/// Compute the set of blocks that are unconditionally executed from the entry. +/// +/// Port of ComputeUnconditionalBlocks.ts. Walks the immediate post-dominator +/// chain starting from the function entry. A block is unconditional if it lies +/// on this chain (meaning every path through the function must pass through it). +pub fn compute_unconditional_blocks( + func: &HirFunction, + next_block_id_counter: u32, +) -> Result, CompilerDiagnostic> { + let mut unconditional = FxHashSet::default(); + let dominators = compute_post_dominator_tree(func, next_block_id_counter, false)?; + let exit = dominators.exit; + let mut current: Option = Some(func.body.entry); + + while let Some(block_id) = current { + if block_id == exit { + break; + } + assert!( + !unconditional.contains(&block_id), + "Internal error: non-terminating loop in ComputeUnconditionalBlocks" + ); + unconditional.insert(block_id); + current = dominators.get(block_id); + } + + Ok(unconditional) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/environment.rs b/crates/oxc_react_compiler/src/react_compiler_hir/environment.rs new file mode 100644 index 0000000000000..853525f31f89c --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/environment.rs @@ -0,0 +1,1102 @@ +use std::rc::Rc; + +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::CompilerDiagnostic; +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_diagnostics::CompilerErrorDetail; +use crate::react_compiler_diagnostics::ErrorCategory; + +use crate::react_compiler_hir::default_module_type_provider::default_module_type_provider; +use crate::react_compiler_hir::environment_config::EnvironmentConfig; +use crate::react_compiler_hir::globals::Global; +use crate::react_compiler_hir::globals::GlobalRegistry; +use crate::react_compiler_hir::globals::{self}; +use crate::react_compiler_hir::object_shape::BUILT_IN_MIXED_READONLY_ID; +use crate::react_compiler_hir::object_shape::FunctionSignature; +use crate::react_compiler_hir::object_shape::HookKind; +use crate::react_compiler_hir::object_shape::HookSignatureBuilder; +use crate::react_compiler_hir::object_shape::ShapeRegistry; +use crate::react_compiler_hir::object_shape::add_hook; +use crate::react_compiler_hir::object_shape::default_mutating_hook; +use crate::react_compiler_hir::object_shape::default_nonmutating_hook; +use crate::react_compiler_hir::*; + +/// A variable rename from lowering: the binding at `declaration_start` position +/// was renamed from `original` to `renamed`. +#[derive(Debug, Clone)] +pub struct BindingRename { + pub original: String, + pub renamed: String, + pub declaration_start: u32, +} + +/// Output mode for the compiler, mirrored from the entrypoint's CompilerOutputMode. +/// Stored on Environment so pipeline passes can access it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputMode { + Ssr, + Client, + Lint, +} + +pub struct Environment<'a> { + // Counters + pub next_block_id_counter: u32, + pub next_scope_id_counter: u32, + next_mutable_range_id_counter: u32, + + // Arenas (use direct field access for sliced borrows) + pub identifiers: Vec, + pub types: Vec, + pub scopes: Vec, + pub functions: Vec>, + + // Error accumulation + pub errors: CompilerError, + + // Function type classification (Component, Hook, Other) + pub fn_type: ReactFunctionType, + + // Output mode (Client, Ssr, Lint) + pub output_mode: OutputMode, + + // Source file code (for fast refresh hash computation) + pub code: Option, + + // Source file name (for instrumentation) + pub filename: Option, + + // Pre-resolved import local names for instrumentation/hook guards. + // Set by the program-level code before compilation. + pub instrument_fn_name: Option, + pub instrument_gating_name: Option, + pub hook_guard_name: Option, + + // Renames: tracks variable renames from lowering (original_name → new_name) + // keyed by binding declaration position, for applying back to the Babel AST. + pub renames: Vec, + + // Node IDs of identifiers that are actual references to bindings. + // Used by codegen to filter type annotation renames — only rename identifiers + // whose node_id is in this set (type labels like ObjectTypeIndexer params + // are NOT in this set and should keep their original names). + // + // Whole-program and read-only during compilation, so it is built once and + // shared (via `Rc`) across every per-function `Environment` rather than + // rebuilt for each function. + pub reference_node_ids: Rc>, + + // Hoisted identifiers: tracks which bindings have already been hoisted + // via DeclareContext to avoid duplicate hoisting. Keyed by node_id (u32). + hoisted_identifiers: FxHashSet, + + // Config flags for validation passes (kept for backwards compat with existing pipeline code) + pub validate_preserve_existing_memoization_guarantees: bool, + pub validate_no_set_state_in_render: bool, + pub enable_preserve_existing_memoization_guarantees: bool, + + // Type system registries + globals: GlobalRegistry, + pub shapes: ShapeRegistry, + module_types: FxHashMap>, + module_type_errors: FxHashMap>, + + // Environment configuration (feature flags, custom hooks, etc.) + pub config: EnvironmentConfig, + + // Cached default hook types (lazily initialized) + default_nonmutating_hook: Option, + default_mutating_hook: Option, + + // Outlined functions: functions extracted from the component during outlining passes + outlined_functions: Vec>, + + // Known names for collision-aware UID generation. Lazily populated from + // identifiers on first use, then updated with each generated name. + // Matches Babel's generateUid behavior of checking hasBinding/hasReference. + uid_known_names: Option>, +} + +/// An outlined function entry, stored on Environment during compilation. +/// Corresponds to TS `{ fn: HIRFunction, type: ReactFunctionType | null }`. +#[derive(Debug, Clone)] +pub struct OutlinedFunctionEntry<'a> { + pub func: HirFunction<'a>, + pub fn_type: Option, +} + +impl<'a> Environment<'a> { + pub fn new() -> Self { + Self::with_config(EnvironmentConfig::default()) + } + + /// Create a new Environment with the given configuration. + /// + /// Initializes the shape and global registries, registers custom hooks, + /// and sets up the module type cache. + pub fn with_config(config: EnvironmentConfig) -> Self { + let mut shapes = ShapeRegistry::with_base(globals::base_shapes()); + let mut global_registry = GlobalRegistry::with_base(globals::base_globals()); + + // Register custom hooks from config + for (hook_name, hook) in &config.custom_hooks { + // Don't overwrite existing globals (matches TS invariant) + if global_registry.contains_key(hook_name) { + continue; + } + let return_type = if hook.transitive_mixed_data { + Type::Object { shape_id: Some(BUILT_IN_MIXED_READONLY_ID.to_string()) } + } else { + Type::Poly + }; + let hook_type = add_hook( + &mut shapes, + HookSignatureBuilder { + rest_param: Some(hook.effect_kind), + return_type, + return_value_kind: hook.value_kind, + hook_kind: HookKind::Custom, + no_alias: hook.no_alias, + ..Default::default() + }, + None, + ); + global_registry.insert(hook_name.clone(), hook_type); + } + + // Register reanimated module type when enabled + let mut module_types: FxHashMap> = FxHashMap::default(); + if config.enable_custom_type_definition_for_reanimated { + let reanimated_module_type = globals::get_reanimated_module_type(&mut shapes); + module_types + .insert("react-native-reanimated".to_string(), Some(reanimated_module_type)); + } + + Self { + next_block_id_counter: 0, + next_scope_id_counter: 0, + next_mutable_range_id_counter: 0, + identifiers: Vec::new(), + types: Vec::new(), + scopes: Vec::new(), + functions: Vec::new(), + errors: CompilerError::new(), + fn_type: ReactFunctionType::Other, + output_mode: OutputMode::Client, + code: None, + filename: None, + instrument_fn_name: None, + instrument_gating_name: None, + hook_guard_name: None, + renames: Vec::new(), + reference_node_ids: Rc::new(FxHashSet::default()), + hoisted_identifiers: FxHashSet::default(), + validate_preserve_existing_memoization_guarantees: config + .validate_preserve_existing_memoization_guarantees, + validate_no_set_state_in_render: config.validate_no_set_state_in_render, + enable_preserve_existing_memoization_guarantees: config + .enable_preserve_existing_memoization_guarantees, + globals: global_registry, + shapes, + module_types, + module_type_errors: FxHashMap::default(), + default_nonmutating_hook: None, + default_mutating_hook: None, + outlined_functions: Vec::new(), + uid_known_names: None, + config, + } + } + + /// Create a child Environment for compiling an outlined function. + /// + /// The child shares the same config, globals, and shapes, and receives copies of + /// all arenas (identifiers, types, scopes, functions) so that references from + /// the outlined HIR remain valid. Block/scope counters start past the cloned + /// data to avoid ID conflicts. + pub fn for_outlined_fn(&self, fn_type: ReactFunctionType) -> Self { + Self { + // Start block counter past any existing blocks in the outlined function. + // The outlined function has BlockId(0), parent may have more. Use parent's + // counter which is guaranteed to be > any block ID in the outlined function. + next_block_id_counter: self.next_block_id_counter, + // Scope counter must be consistent with scopes vec length + next_scope_id_counter: self.scopes.len() as u32, + next_mutable_range_id_counter: self.next_mutable_range_id_counter, + identifiers: self.identifiers.clone(), + types: self.types.clone(), + scopes: self.scopes.clone(), + functions: self.functions.clone(), + errors: CompilerError::new(), + fn_type, + output_mode: self.output_mode, + code: self.code.clone(), + filename: self.filename.clone(), + instrument_fn_name: self.instrument_fn_name.clone(), + instrument_gating_name: self.instrument_gating_name.clone(), + hook_guard_name: self.hook_guard_name.clone(), + renames: Vec::new(), + reference_node_ids: Rc::new(FxHashSet::default()), + hoisted_identifiers: FxHashSet::default(), + validate_preserve_existing_memoization_guarantees: self + .validate_preserve_existing_memoization_guarantees, + validate_no_set_state_in_render: self.validate_no_set_state_in_render, + enable_preserve_existing_memoization_guarantees: self + .enable_preserve_existing_memoization_guarantees, + globals: self.globals.clone(), + shapes: self.shapes.clone(), + module_types: self.module_types.clone(), + module_type_errors: self.module_type_errors.clone(), + config: self.config.clone(), + default_nonmutating_hook: self.default_nonmutating_hook.clone(), + default_mutating_hook: self.default_mutating_hook.clone(), + outlined_functions: Vec::new(), + uid_known_names: self.uid_known_names.clone(), + } + } + + pub fn next_block_id(&mut self) -> BlockId { + let id = BlockId(self.next_block_id_counter); + self.next_block_id_counter += 1; + id + } + + /// Create a new MutableRange with a unique ID. + /// Use this when creating a logically new range (not copying an existing one). + /// To copy a range preserving its identity, use `.clone()` instead. + pub fn new_mutable_range( + &mut self, + start: EvaluationOrder, + end: EvaluationOrder, + ) -> MutableRange { + let id = MutableRangeId(self.next_mutable_range_id_counter); + self.next_mutable_range_id_counter += 1; + MutableRange { id, start, end } + } + + /// Allocate a new Identifier in the arena with default values, + /// returns its IdentifierId. + pub fn next_identifier_id(&mut self) -> IdentifierId { + let id = IdentifierId(self.identifiers.len() as u32); + let type_id = self.make_type(); + let mutable_range = self.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0)); + self.identifiers.push(Identifier { + id, + declaration_id: DeclarationId(id.0), + name: None, + mutable_range, + scope: None, + type_: type_id, + loc: None, + }); + id + } + + /// Allocate a new ReactiveScope in the arena, returns its ScopeId. + pub fn next_scope_id(&mut self) -> ScopeId { + let id = ScopeId(self.next_scope_id_counter); + self.next_scope_id_counter += 1; + let range = self.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0)); + self.scopes.push(ReactiveScope { + id, + range, + dependencies: Vec::new(), + declarations: Vec::new(), + reassignments: Vec::new(), + early_return_value: None, + merged: Vec::new(), + loc: None, + }); + id + } + + /// Allocate a new Type in the arena, returns its TypeId. + pub fn next_type_id(&mut self) -> TypeId { + let id = TypeId(self.types.len() as u32); + self.types.push(Type::TypeVar { id }); + id + } + + /// Allocate a new Type (TypeVar) in the arena, returns its TypeId. + pub fn make_type(&mut self) -> TypeId { + self.next_type_id() + } + + pub fn add_function(&mut self, func: HirFunction<'a>) -> FunctionId { + let id = FunctionId(self.functions.len() as u32); + self.functions.push(func); + id + } + + pub fn record_error(&mut self, detail: CompilerErrorDetail) -> Result<(), CompilerError> { + if detail.category == ErrorCategory::Invariant { + let detail_clone = detail.clone(); + self.errors.push_error_detail(detail); + let mut err = CompilerError::new(); + err.push_error_detail(detail_clone); + return Err(err); + } + self.errors.push_error_detail(detail); + Ok(()) + } + + pub fn record_diagnostic(&mut self, diagnostic: CompilerDiagnostic) { + self.errors.push_diagnostic(diagnostic); + } + + pub fn has_errors(&self) -> bool { + self.errors.has_any_errors() + } + + pub fn error_count(&self) -> usize { + self.errors.details.len() + } + + /// Check if any recorded errors have Invariant category. + /// In TS, Invariant errors throw immediately from recordError(), + /// which aborts the current operation. + pub fn has_invariant_errors(&self) -> bool { + self.errors.has_invariant_errors() + } + + pub fn errors(&self) -> &CompilerError { + &self.errors + } + + pub fn take_errors(&mut self) -> CompilerError { + let mut errors = std::mem::take(&mut self.errors); + // Mark as not thrown — these are accumulated errors returned at the end + // of the pipeline, not errors thrown by a pass. + errors.is_thrown = false; + errors + } + + /// Take errors added after position `since_count`, leaving earlier errors in place. + /// Used to detect new errors added by a specific pass. + pub fn take_errors_since(&mut self, since_count: usize) -> CompilerError { + let mut taken = CompilerError::new(); + if self.errors.details.len() > since_count { + taken.details = self.errors.details.split_off(since_count); + } + taken + } + + /// Take only the Invariant errors, leaving non-Invariant errors in place. + /// In TS, Invariant errors throw as a separate CompilerError, so only + /// the Invariant error is surfaced. + pub fn take_invariant_errors(&mut self) -> CompilerError { + let mut invariant = CompilerError::new(); + let mut remaining = CompilerError::new(); + let old = std::mem::take(&mut self.errors); + for detail in old.details { + let is_invariant = match &detail { + crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => { + d.category == crate::react_compiler_diagnostics::ErrorCategory::Invariant + } + crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => { + d.category == crate::react_compiler_diagnostics::ErrorCategory::Invariant + } + }; + if is_invariant { + invariant.details.push(detail); + } else { + remaining.details.push(detail); + } + } + self.errors = remaining; + invariant + } + + /// Check if any recorded errors have Todo category. + /// In TS, Todo errors throw immediately via CompilerError.throwTodo(). + pub fn has_todo_errors(&self) -> bool { + self.errors.details.iter().any(|d| match d { + crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => { + d.category == crate::react_compiler_diagnostics::ErrorCategory::Todo + } + crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => { + d.category == crate::react_compiler_diagnostics::ErrorCategory::Todo + } + }) + } + + /// Take errors that would have been thrown in TS (Invariant and Todo), + /// leaving other accumulated errors in place. + pub fn take_thrown_errors(&mut self) -> CompilerError { + let mut thrown = CompilerError::new(); + let mut remaining = CompilerError::new(); + let old = std::mem::take(&mut self.errors); + for detail in old.details { + let is_thrown = match &detail { + crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => { + d.category == crate::react_compiler_diagnostics::ErrorCategory::Invariant + || d.category == crate::react_compiler_diagnostics::ErrorCategory::Todo + } + crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => { + d.category == crate::react_compiler_diagnostics::ErrorCategory::Invariant + || d.category == crate::react_compiler_diagnostics::ErrorCategory::Todo + } + }; + if is_thrown { + thrown.details.push(detail); + } else { + remaining.details.push(detail); + } + } + self.errors = remaining; + thrown + } + + /// Check if a binding has been hoisted (via DeclareContext) already. + pub fn is_hoisted_identifier(&self, binding_id: u32) -> bool { + self.hoisted_identifiers.contains(&binding_id) + } + + /// Mark a binding as hoisted. + pub fn add_hoisted_identifier(&mut self, binding_id: u32) { + self.hoisted_identifiers.insert(binding_id); + } + + // ========================================================================= + // Type resolution methods (ported from Environment.ts) + // ========================================================================= + + /// Resolve a non-local binding to its type. Ported from TS `getGlobalDeclaration`. + /// + /// The `loc` parameter is used for error diagnostics when validating module type + /// configurations. Pass `None` if no source location is available. + pub fn get_global_declaration( + &mut self, + binding: &NonLocalBinding, + loc: Option, + ) -> Result, CompilerError> { + match binding { + NonLocalBinding::ModuleLocal { name, .. } => { + if is_hook_name(name) { + Ok(Some(self.get_custom_hook_type())) + } else { + Ok(None) + } + } + NonLocalBinding::Global { name, .. } => { + if let Some(ty) = self.globals.get(name) { + return Ok(Some(ty.clone())); + } + if is_hook_name(name) { Ok(Some(self.get_custom_hook_type())) } else { Ok(None) } + } + NonLocalBinding::ImportSpecifier { name, module, imported } => { + if self.is_known_react_module(module) { + if let Some(ty) = self.globals.get(imported) { + return Ok(Some(ty.clone())); + } + if is_hook_name(imported) || is_hook_name(name) { + return Ok(Some(self.get_custom_hook_type())); + } + return Ok(None); + } + + // Try module type provider. We resolve first, then do property + // lookup on the cloned result to avoid double-borrow of self. + let module_type = self.resolve_module_type(module); + + // Check for module type validation errors (hook-name vs hook-type mismatches) + if let Some(errors) = self.module_type_errors.remove(module.as_str()) { + if let Some(first_error) = errors.into_iter().next() { + self.record_error( + CompilerErrorDetail::new( + ErrorCategory::Config, + "Invalid type configuration for module", + ) + .with_description(format!("{}", first_error)) + .with_loc(loc), + )?; + } + } + + if let Some(module_type) = module_type { + if let Some(imported_type) = + Self::get_property_type_from_shapes(&self.shapes, &module_type, imported) + { + return Ok(Some(imported_type)); + } + } + + if is_hook_name(imported) || is_hook_name(name) { + Ok(Some(self.get_custom_hook_type())) + } else { + Ok(None) + } + } + NonLocalBinding::ImportDefault { name, module } + | NonLocalBinding::ImportNamespace { name, module } => { + let is_default = matches!(binding, NonLocalBinding::ImportDefault { .. }); + + if self.is_known_react_module(module) { + if let Some(ty) = self.globals.get(name) { + return Ok(Some(ty.clone())); + } + if is_hook_name(name) { + return Ok(Some(self.get_custom_hook_type())); + } + return Ok(None); + } + + let module_type = self.resolve_module_type(module); + + // Check for module type validation errors (hook-name vs hook-type mismatches) + if let Some(errors) = self.module_type_errors.remove(module.as_str()) { + if let Some(first_error) = errors.into_iter().next() { + self.record_error( + CompilerErrorDetail::new( + ErrorCategory::Config, + "Invalid type configuration for module", + ) + .with_description(format!("{}", first_error)) + .with_loc(loc), + )?; + } + } + + if let Some(module_type) = module_type { + let imported_type = if is_default { + Self::get_property_type_from_shapes(&self.shapes, &module_type, "default") + } else { + Some(module_type) + }; + if let Some(imported_type) = imported_type { + // Validate hook-name vs hook-type consistency for module name + let expect_hook = is_hook_name(module); + let is_hook = + self.get_hook_kind_for_type(&imported_type).ok().flatten().is_some(); + if expect_hook != is_hook { + self.record_error( + CompilerErrorDetail::new( + ErrorCategory::Config, + "Invalid type configuration for module", + ) + .with_description(format!( + "Expected type for `import ... from '{}'` {} based on the module name", + module, + if expect_hook { "to be a hook" } else { "not to be a hook" } + )) + .with_loc(loc), + )?; + } + return Ok(Some(imported_type)); + } + } + + if is_hook_name(name) { Ok(Some(self.get_custom_hook_type())) } else { Ok(None) } + } + } + } + + /// Static helper: resolve a property type using only the shapes registry. + /// Used internally to avoid double-borrow of `self`. Includes hook-name + /// fallback matching TS `getPropertyType`. + fn get_property_type_from_shapes( + shapes: &ShapeRegistry, + receiver: &Type, + property: &str, + ) -> Option { + let shape_id = match receiver { + Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), + _ => None, + }; + if let Some(shape_id) = shape_id { + let shape = shapes.get(shape_id)?; + if let Some(ty) = shape.properties.get(property) { + return Some(ty.clone()); + } + if let Some(ty) = shape.properties.get("*") { + return Some(ty.clone()); + } + // Hook-name fallback: callers that need the custom hook type + // check is_hook_name after this returns None, which produces + // the same result as the TS getPropertyType hook-name fallback. + } + None + } + + /// Get the type of a named property on a receiver type. + /// Ported from TS `getPropertyType`. + pub fn get_property_type( + &mut self, + receiver: &Type, + property: &str, + ) -> Result, CompilerDiagnostic> { + let shape_id = match receiver { + Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), + _ => None, + }; + if let Some(shape_id) = shape_id { + let shape = self.shapes.get(shape_id).ok_or_else(|| { + CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("[HIR] Forget internal error: cannot resolve shape {}", shape_id), + None, + ) + })?; + if let Some(ty) = shape.properties.get(property) { + return Ok(Some(ty.clone())); + } + // Fall through to wildcard + if let Some(ty) = shape.properties.get("*") { + return Ok(Some(ty.clone())); + } + // If property name looks like a hook, return custom hook type + if is_hook_name(property) { + return Ok(Some(self.get_custom_hook_type())); + } + return Ok(None); + } + // No shape ID — if property looks like a hook, return custom hook type + if is_hook_name(property) { + return Ok(Some(self.get_custom_hook_type())); + } + Ok(None) + } + + /// Get the type of a numeric property on a receiver type. + /// Ported from the numeric branch of TS `getPropertyType`. + pub fn get_property_type_numeric( + &self, + receiver: &Type, + ) -> Result, CompilerDiagnostic> { + let shape_id = match receiver { + Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), + _ => None, + }; + if let Some(shape_id) = shape_id { + let shape = self.shapes.get(shape_id).ok_or_else(|| { + CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("[HIR] Forget internal error: cannot resolve shape {}", shape_id), + None, + ) + })?; + return Ok(shape.properties.get("*").cloned()); + } + Ok(None) + } + + /// Get the fallthrough (wildcard `*`) property type for computed property access. + /// Ported from TS `getFallthroughPropertyType`. + pub fn get_fallthrough_property_type( + &self, + receiver: &Type, + ) -> Result, CompilerDiagnostic> { + let shape_id = match receiver { + Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), + _ => None, + }; + if let Some(shape_id) = shape_id { + let shape = self.shapes.get(shape_id).ok_or_else(|| { + CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("[HIR] Forget internal error: cannot resolve shape {}", shape_id), + None, + ) + })?; + return Ok(shape.properties.get("*").cloned()); + } + Ok(None) + } + + /// Get the function signature for a function type. + /// Ported from TS `getFunctionSignature`. + pub fn get_function_signature( + &self, + ty: &Type, + ) -> Result, CompilerDiagnostic> { + let shape_id = match ty { + Type::Function { shape_id, .. } => shape_id.as_deref(), + _ => return Ok(None), + }; + if let Some(shape_id) = shape_id { + let shape = self.shapes.get(shape_id).ok_or_else(|| { + CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("[HIR] Forget internal error: cannot resolve shape {}", shape_id), + None, + ) + })?; + return Ok(shape.function_type.as_ref()); + } + Ok(None) + } + + /// Get the hook kind for a type, if it represents a hook. + /// Ported from TS `getHookKindForType` in HIR.ts. + pub fn get_hook_kind_for_type( + &self, + ty: &Type, + ) -> Result, CompilerDiagnostic> { + Ok(self.get_function_signature(ty)?.and_then(|sig| sig.hook_kind.as_ref())) + } + + /// Resolve the module type provider for a given module name. + /// Caches results. Checks pre-resolved provider results first, then falls + /// back to `defaultModuleTypeProvider` (hardcoded). + fn resolve_module_type(&mut self, module_name: &str) -> Option { + if let Some(cached) = self.module_types.get(module_name) { + return cached.clone(); + } + + // Check pre-resolved provider results first, then fall back to default + let module_config = self + .config + .module_type_provider + .as_ref() + .and_then(|map| map.get(module_name).cloned()) + .or_else(|| default_module_type_provider(module_name)); + + let module_type = module_config.map(|config| { + let mut type_errors: Vec = Vec::new(); + let ty = globals::install_type_config_with_errors( + &mut self.globals, + &mut self.shapes, + &config, + module_name, + (), + &mut type_errors, + ); + // Store errors for later reporting when the import is actually used + for err in type_errors { + self.module_type_errors.entry(module_name.to_string()).or_default().push(err); + } + ty + }); + self.module_types.insert(module_name.to_string(), module_type.clone()); + module_type + } + + fn is_known_react_module(&self, module_name: &str) -> bool { + let lower = module_name.to_lowercase(); + lower == "react" || lower == "react-dom" + } + + fn get_custom_hook_type(&mut self) -> Global { + if self.config.enable_assume_hooks_follow_rules_of_react { + if self.default_nonmutating_hook.is_none() { + self.default_nonmutating_hook = Some(default_nonmutating_hook(&mut self.shapes)); + } + self.default_nonmutating_hook.clone().unwrap() + } else { + if self.default_mutating_hook.is_none() { + self.default_mutating_hook = Some(default_mutating_hook(&mut self.shapes)); + } + self.default_mutating_hook.clone().unwrap() + } + } + + /// Public accessor for the custom hook type, used by InferTypes for + /// property resolution fallback when a property name looks like a hook. + pub fn get_custom_hook_type_opt(&mut self) -> Option { + Some(self.get_custom_hook_type()) + } + + /// Get a reference to the shapes registry. + pub fn shapes(&self) -> &ShapeRegistry { + &self.shapes + } + + /// Get a reference to the globals registry. + pub fn globals(&self) -> &GlobalRegistry { + &self.globals + } + + /// Generate a globally unique identifier name, analogous to TS + /// `generateGloballyUniqueIdentifierName` which delegates to Babel's + /// `scope.generateUidIdentifier`. Matches Babel's naming convention: + /// first name is `_`, subsequent are `_2`, `_3`, etc. + /// Also applies Babel's `toIdentifier` sanitization on the input name. + /// + /// Like Babel's `generateUid`, checks for collisions against existing + /// bindings (source-level identifier names) and previously generated UIDs, + /// rather than using a blind counter. + pub fn generate_globally_unique_identifier_name(&mut self, name: Option<&str>) -> String { + let base = name.unwrap_or("temp"); + // Apply Babel's toIdentifier sanitization: + // 1. Replace non-identifier chars with '-' + // 2. Strip leading '-' and digits + // 3. CamelCase: replace '-' sequences + optional following char with uppercase of that char + let mut dashed = String::new(); + for c in base.chars() { + if c.is_ascii_alphanumeric() || c == '_' || c == '$' { + dashed.push(c); + } else { + dashed.push('-'); + } + } + // Strip leading dashes and digits + let trimmed = dashed.trim_start_matches(|c: char| c == '-' || c.is_ascii_digit()); + // CamelCase conversion: replace sequences of '-' followed by optional char with uppercase + let mut camel = String::new(); + let mut chars = trimmed.chars().peekable(); + while let Some(c) = chars.next() { + if c == '-' { + while chars.peek() == Some(&'-') { + chars.next(); + } + if let Some(next) = chars.next() { + for uc in next.to_uppercase() { + camel.push(uc); + } + } + } else { + camel.push(c); + } + } + if camel.is_empty() { + camel = "temp".to_string(); + } + // Strip leading '_' and trailing digits (Babel's generateUid behavior) + let stripped = camel.trim_start_matches('_'); + let stripped = stripped.trim_end_matches(|c: char| c.is_ascii_digit()); + let uid_base = if stripped.is_empty() { "temp" } else { stripped }; + + // Lazily build the set of known names from existing identifiers. + // This approximates Babel's hasBinding/hasGlobal/hasReference checks. + if self.uid_known_names.is_none() { + let mut known = FxHashSet::default(); + for id in &self.identifiers { + if let Some(name) = &id.name { + known.insert(name.value().to_string()); + } + } + self.uid_known_names = Some(known); + } + + // Find a name that doesn't collide, matching Babel's generateUid loop + let mut i = 1u32; + let uid = loop { + let candidate = + if i == 1 { format!("_{}", uid_base) } else { format!("_{}{}", uid_base, i) }; + i += 1; + if !self.uid_known_names.as_ref().unwrap().contains(&candidate) { + break candidate; + } + }; + + // Register the generated name so subsequent calls see it + self.uid_known_names.as_mut().unwrap().insert(uid.clone()); + + uid + } + + /// Seed the UID known names set with external names (e.g. from ProgramContext). + /// This ensures UID generation avoids names generated by previous function compilations, + /// matching Babel's behavior where the program scope accumulates all generated UIDs. + pub fn seed_uid_known_names(&mut self, names: &FxHashSet) { + match &mut self.uid_known_names { + Some(existing) => existing.extend(names.iter().cloned()), + None => self.uid_known_names = Some(names.clone()), + } + } + + /// Return the UID known names accumulated during this compilation. + pub fn take_uid_known_names(&mut self) -> Option> { + self.uid_known_names.take() + } + + /// Record an outlined function (extracted during outlineFunctions or outlineJSX). + /// Corresponds to TS `env.outlineFunction(fn, type)`. + pub fn outline_function(&mut self, func: HirFunction<'a>, fn_type: Option) { + self.outlined_functions.push(OutlinedFunctionEntry { func, fn_type }); + } + + /// Get the outlined functions accumulated during compilation. + pub fn get_outlined_functions(&self) -> &[OutlinedFunctionEntry<'a>] { + &self.outlined_functions + } + + /// Take the outlined functions, leaving the vec empty. + pub fn take_outlined_functions(&mut self) -> Vec> { + std::mem::take(&mut self.outlined_functions) + } + + /// Whether memoization is enabled for this compilation. + /// Ported from TS `get enableMemoization()` in Environment.ts. + /// Returns true for client/lint modes, false for SSR. + pub fn enable_memoization(&self) -> bool { + match self.output_mode { + OutputMode::Client | OutputMode::Lint => true, + OutputMode::Ssr => false, + } + } + + /// Whether validations are enabled for this compilation. + /// Ported from TS `get enableValidations()` in Environment.ts. + pub fn enable_validations(&self) -> bool { + match self.output_mode { + OutputMode::Client | OutputMode::Lint | OutputMode::Ssr => true, + } + } + + // ========================================================================= + // Name resolution helpers + // ========================================================================= + + /// Get the user-visible name for an identifier. + /// + /// First checks the identifier's own name. If None, looks for another + /// identifier with the same `declaration_id` that has a name. This handles + /// SSA identifiers that don't carry names but share a declaration_id with + /// the original named identifier from lowering. + /// + /// This is analogous to `identifierName` on Babel's SourceLocation, + /// which the parser sets on every identifier node. + pub fn identifier_name_for_id(&self, id: IdentifierId) -> Option { + let ident = &self.identifiers[id.0 as usize]; + if let Some(name) = &ident.name { + return Some(name.value().to_string()); + } + // Fall back: find another identifier with the same declaration_id that has a Named name + let decl_id = ident.declaration_id; + for other in &self.identifiers { + if other.declaration_id == decl_id { + if let Some(IdentifierName::Named(name)) = &other.name { + return Some(name.clone()); + } + } + } + None + } + + // ========================================================================= + // ID-based type helper methods + // ========================================================================= + + /// Check whether the function type for an identifier has a noAlias signature. + /// Looks up the identifier's type and checks its function signature. + pub fn has_no_alias_signature(&self, identifier_id: IdentifierId) -> bool { + let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize]; + self.get_function_signature(ty).ok().flatten().map_or(false, |sig| sig.no_alias) + } + + /// Get the hook kind for an identifier, if its type represents a hook. + /// Looks up the identifier's type and delegates to `get_hook_kind_for_type`. + pub fn get_hook_kind_for_id( + &self, + identifier_id: IdentifierId, + ) -> Result, CompilerDiagnostic> { + let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize]; + self.get_hook_kind_for_type(ty) + } +} + +impl Default for Environment<'_> { + fn default() -> Self { + Self::new() + } +} + +/// Check if a name matches the React hook naming convention: `use[A-Z0-9]`. +/// Ported from TS `isHookName` in Environment.ts. +pub fn is_hook_name(name: &str) -> bool { + if name.len() < 4 { + return false; + } + if !name.starts_with("use") { + return false; + } + let fourth_char = name.as_bytes()[3]; + fourth_char.is_ascii_uppercase() || fourth_char.is_ascii_digit() +} + +/// Returns true if the name follows React naming conventions (component or hook). +/// Components start with an uppercase letter; hooks match `use[A-Z0-9]`. +pub fn is_react_like_name(name: &str) -> bool { + if name.is_empty() { + return false; + } + let first_char = name.as_bytes()[0]; + if first_char.is_ascii_uppercase() { + return true; + } + is_hook_name(name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_hook_name() { + assert!(is_hook_name("useState")); + assert!(is_hook_name("useEffect")); + assert!(is_hook_name("useMyHook")); + assert!(is_hook_name("use3rdParty")); + assert!(!is_hook_name("use")); + assert!(!is_hook_name("used")); + assert!(!is_hook_name("useless")); + assert!(!is_hook_name("User")); + assert!(!is_hook_name("foo")); + } + + #[test] + fn test_environment_has_globals() { + let env = Environment::new(); + assert!(env.globals().contains_key("useState")); + assert!(env.globals().contains_key("useEffect")); + assert!(env.globals().contains_key("useRef")); + assert!(env.globals().contains_key("Math")); + assert!(env.globals().contains_key("console")); + assert!(env.globals().contains_key("Array")); + assert!(env.globals().contains_key("Object")); + } + + #[test] + fn test_get_property_type_array() { + let mut env = Environment::new(); + let array_type = Type::Object { shape_id: Some("BuiltInArray".to_string()) }; + let map_type = env.get_property_type(&array_type, "map").unwrap(); + assert!(map_type.is_some()); + let push_type = env.get_property_type(&array_type, "push").unwrap(); + assert!(push_type.is_some()); + let nonexistent = env.get_property_type(&array_type, "nonExistentMethod").unwrap(); + assert!(nonexistent.is_none()); + } + + #[test] + fn test_get_function_signature() { + let env = Environment::new(); + let use_state_type = env.globals().get("useState").unwrap(); + let sig = env.get_function_signature(use_state_type).unwrap(); + assert!(sig.is_some()); + let sig = sig.unwrap(); + assert!(sig.hook_kind.is_some()); + assert_eq!(sig.hook_kind.as_ref().unwrap(), &HookKind::UseState); + } + + #[test] + fn test_get_global_declaration() { + let mut env = Environment::new(); + // Global binding + let binding = NonLocalBinding::Global { name: "Math".to_string() }; + let result = env.get_global_declaration(&binding, None).unwrap(); + assert!(result.is_some()); + + // Import from react + let binding = NonLocalBinding::ImportSpecifier { + name: "useState".to_string(), + module: "react".to_string(), + imported: "useState".to_string(), + }; + let result = env.get_global_declaration(&binding, None).unwrap(); + assert!(result.is_some()); + + // Unknown global + let binding = NonLocalBinding::Global { name: "unknownThing".to_string() }; + let result = env.get_global_declaration(&binding, None).unwrap(); + assert!(result.is_none()); + + // Hook-like name gets default hook type + let binding = NonLocalBinding::Global { name: "useCustom".to_string() }; + let result = env.get_global_declaration(&binding, None).unwrap(); + assert!(result.is_some()); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/environment_config.rs b/crates/oxc_react_compiler/src/react_compiler_hir/environment_config.rs new file mode 100644 index 0000000000000..1d3b1e6f174fd --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/environment_config.rs @@ -0,0 +1,168 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Environment configuration, ported from EnvironmentConfigSchema in Environment.ts. +//! +//! Contains feature flags and custom hook definitions that control compiler behavior. + +use crate::react_compiler_utils::FxIndexMap; +use rustc_hash::FxHashMap; + +use crate::react_compiler_hir::Effect; +use crate::react_compiler_hir::type_config::{TypeConfig, ValueKind}; + +/// External function reference (source module + import name). +/// Corresponds to TS `ExternalFunction`. +#[derive(Debug, Clone)] +pub struct ExternalFunctionConfig { + pub source: String, + pub import_specifier_name: String, +} + +/// Instrumentation configuration. +/// Corresponds to TS `InstrumentationSchema`. +#[derive(Debug, Clone)] +pub struct InstrumentationConfig { + pub fn_: ExternalFunctionConfig, + pub gating: Option, + pub global_gating: Option, +} + +/// Custom hook configuration, ported from TS `HookSchema`. +#[derive(Debug, Clone)] +pub struct HookConfig { + pub effect_kind: Effect, + pub value_kind: ValueKind, + pub no_alias: bool, + pub transitive_mixed_data: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExhaustiveEffectDepsMode { + Off, + All, + MissingOnly, + ExtraOnly, +} + +impl Default for ExhaustiveEffectDepsMode { + fn default() -> Self { + Self::Off + } +} + +/// Compiler environment configuration. Contains feature flags and settings. +/// +/// Fields that would require passing JS functions across the JS/Rust boundary +/// are omitted with TODO comments. The Rust port uses hardcoded defaults for +/// these (e.g., `defaultModuleTypeProvider`). +#[derive(Debug, Clone)] +pub struct EnvironmentConfig { + /// Custom hook type definitions, keyed by hook name. + pub custom_hooks: FxHashMap, + + /// Pre-resolved module type provider results. + /// Map from module name to TypeConfig, computed by the JS shim. + pub module_type_provider: Option>, + + /// Custom macro-like function names that should have their operands + /// memoized in the same scope (similar to fbt). + pub custom_macros: Option>, + + /// If true, emit code to reset the memo cache on source file changes (HMR/fast refresh). + /// If null (None), HMR detection is conditionally enabled based on NODE_ENV/__DEV__. + pub enable_reset_cache_on_source_file_changes: Option, + + pub enable_preserve_existing_memoization_guarantees: bool, + pub validate_preserve_existing_memoization_guarantees: bool, + pub validate_exhaustive_memoization_dependencies: bool, + pub validate_exhaustive_effect_dependencies: ExhaustiveEffectDepsMode, + + // TODO: flowTypeProvider — requires JS function callback. + pub enable_optional_dependencies: bool, + pub enable_name_anonymous_functions: bool, + pub validate_hooks_usage: bool, + pub validate_ref_access_during_render: bool, + pub validate_no_set_state_in_render: bool, + pub enable_use_keyed_state: bool, + pub validate_no_set_state_in_effects: bool, + pub validate_no_derived_computations_in_effects: bool, + pub validate_no_derived_computations_in_effects_exp: bool, + pub validate_no_jsx_in_try_statements: bool, + pub validate_static_components: bool, + pub validate_no_capitalized_calls: Option>, + pub validate_blocklisted_imports: Option>, + pub validate_source_locations: bool, + pub validate_no_impure_functions_in_render: bool, + pub validate_no_freezing_known_mutable_functions: bool, + pub enable_assume_hooks_follow_rules_of_react: bool, + pub enable_transitively_freeze_function_expressions: bool, + + /// Hook guard configuration. When set, wraps hook calls with dispatcher guard calls. + pub enable_emit_hook_guards: Option, + + /// Instrumentation configuration. When set, emits calls to instrument functions. + pub enable_emit_instrument_forget: Option, + + pub enable_function_outlining: bool, + pub enable_jsx_outlining: bool, + pub assert_valid_mutable_ranges: bool, + pub throw_unknown_exception_testonly: bool, + pub enable_custom_type_definition_for_reanimated: bool, + pub enable_treat_ref_like_identifiers_as_refs: bool, + pub enable_treat_set_identifiers_as_state_setters: bool, + pub validate_no_void_use_memo: bool, + pub enable_allow_set_state_from_refs_in_effects: bool, + pub enable_verbose_no_set_state_in_effect: bool, + + // 🌲 + pub enable_forest: bool, +} + +impl Default for EnvironmentConfig { + fn default() -> Self { + Self { + custom_hooks: FxHashMap::default(), + enable_reset_cache_on_source_file_changes: None, + module_type_provider: None, + enable_preserve_existing_memoization_guarantees: true, + validate_preserve_existing_memoization_guarantees: true, + validate_exhaustive_memoization_dependencies: true, + validate_exhaustive_effect_dependencies: ExhaustiveEffectDepsMode::Off, + enable_optional_dependencies: true, + enable_name_anonymous_functions: false, + validate_hooks_usage: true, + validate_ref_access_during_render: true, + validate_no_set_state_in_render: true, + enable_use_keyed_state: false, + validate_no_set_state_in_effects: false, + validate_no_derived_computations_in_effects: false, + validate_no_derived_computations_in_effects_exp: false, + validate_no_jsx_in_try_statements: false, + validate_static_components: false, + validate_no_capitalized_calls: None, + validate_blocklisted_imports: None, + validate_source_locations: false, + validate_no_impure_functions_in_render: false, + validate_no_freezing_known_mutable_functions: false, + enable_assume_hooks_follow_rules_of_react: true, + enable_transitively_freeze_function_expressions: true, + enable_emit_hook_guards: None, + enable_emit_instrument_forget: None, + enable_function_outlining: true, + enable_jsx_outlining: false, + assert_valid_mutable_ranges: false, + throw_unknown_exception_testonly: false, + enable_custom_type_definition_for_reanimated: false, + enable_treat_ref_like_identifiers_as_refs: true, + enable_treat_set_identifiers_as_state_setters: false, + validate_no_void_use_memo: true, + enable_allow_set_state_from_refs_in_effects: true, + enable_verbose_no_set_state_in_effect: false, + enable_forest: false, + custom_macros: None, + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/globals.rs b/crates/oxc_react_compiler/src/react_compiler_hir/globals.rs new file mode 100644 index 0000000000000..09db8fe78950c --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/globals.rs @@ -0,0 +1,2224 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Global type registry and built-in shape definitions, ported from Globals.ts. +//! +//! Provides `DEFAULT_SHAPES` (built-in object shapes) and `DEFAULT_GLOBALS` +//! (global variable types including React hooks and JS built-ins). + +use rustc_hash::FxHashMap; +use std::sync::LazyLock; + +use crate::react_compiler_hir::Effect; +use crate::react_compiler_hir::Type; +use crate::react_compiler_hir::object_shape::*; +use crate::react_compiler_hir::type_config::AliasingEffectConfig; +use crate::react_compiler_hir::type_config::AliasingSignatureConfig; +use crate::react_compiler_hir::type_config::ApplyArgConfig; +use crate::react_compiler_hir::type_config::ApplyArgHoleKind; +use crate::react_compiler_hir::type_config::BuiltInTypeRef; +use crate::react_compiler_hir::type_config::TypeConfig; +use crate::react_compiler_hir::type_config::TypeReferenceConfig; +use crate::react_compiler_hir::type_config::ValueKind; +use crate::react_compiler_hir::type_config::ValueReason; + +/// Type alias matching TS `Global = BuiltInType | PolyType`. +/// In the Rust port, both map to our `Type` enum. +pub type Global = Type; + +/// Registry mapping global names to their types. +/// +/// Supports two modes: +/// - **Builder mode** (`base=None`): wraps a single FxHashMap, used during +/// `build_default_globals` to construct the static base. +/// - **Overlay mode** (`base=Some`): holds a `&'static FxHashMap` base plus a small +/// extras FxHashMap. Lookups check extras first, then base. Inserts go into extras. +/// Cloning only copies the extras map (the base pointer is shared). +pub struct GlobalRegistry { + base: Option<&'static FxHashMap>, + entries: FxHashMap, +} + +impl GlobalRegistry { + /// Create an empty builder-mode registry. + pub fn new() -> Self { + Self { base: None, entries: FxHashMap::default() } + } + + /// Create an overlay-mode registry backed by a static base. + pub fn with_base(base: &'static FxHashMap) -> Self { + Self { base: Some(base), entries: FxHashMap::default() } + } + + pub fn get(&self, key: &str) -> Option<&Global> { + self.entries.get(key).or_else(|| self.base.and_then(|b| b.get(key))) + } + + pub fn insert(&mut self, key: String, value: Global) { + self.entries.insert(key, value); + } + + pub fn contains_key(&self, key: &str) -> bool { + self.entries.contains_key(key) || self.base.map_or(false, |b| b.contains_key(key)) + } + + /// Iterate over all keys in the registry (base + extras). + /// Keys in extras that shadow base keys appear only once. + pub fn keys(&self) -> impl Iterator { + let base_keys = self + .base + .into_iter() + .flat_map(|b| b.keys()) + .filter(|k| !self.entries.contains_key(k.as_str())); + self.entries.keys().chain(base_keys) + } + + /// Consume the registry and return the inner FxHashMap. + /// Only valid in builder mode (no base). + pub fn into_inner(self) -> FxHashMap { + debug_assert!(self.base.is_none(), "into_inner() called on overlay-mode GlobalRegistry"); + self.entries + } +} + +impl Clone for GlobalRegistry { + fn clone(&self) -> Self { + Self { base: self.base, entries: self.entries.clone() } + } +} + +// ============================================================================= +// Static base registries (initialized once, shared across all Environments) +// ============================================================================= + +struct BaseRegistries { + shapes: FxHashMap, + globals: FxHashMap, +} + +static BASE: LazyLock = LazyLock::new(|| { + let mut shapes = build_builtin_shapes(); + let globals = build_default_globals(&mut shapes); + BaseRegistries { shapes: shapes.into_inner(), globals: globals.into_inner() } +}); + +/// Get a reference to the static base shapes registry. +pub fn base_shapes() -> &'static FxHashMap { + &BASE.shapes +} + +/// Get a reference to the static base globals registry. +pub fn base_globals() -> &'static FxHashMap { + &BASE.globals +} + +// ============================================================================= +// installTypeConfig — converts TypeConfig to internal Type +// ============================================================================= + +/// Convert a user-provided TypeConfig into an internal Type, registering shapes +/// as needed. Ported from TS `installTypeConfig` in Globals.ts. +/// If `errors` is provided, hook-name vs hook-type consistency validation +/// errors are collected there. +pub fn install_type_config( + _globals: &mut GlobalRegistry, + shapes: &mut ShapeRegistry, + type_config: &TypeConfig, + module_name: &str, + _loc: (), +) -> Global { + install_type_config_inner(_globals, shapes, type_config, module_name, _loc, &mut None) +} + +/// Like `install_type_config` but collects validation errors. +pub fn install_type_config_with_errors( + _globals: &mut GlobalRegistry, + shapes: &mut ShapeRegistry, + type_config: &TypeConfig, + module_name: &str, + _loc: (), + errors: &mut Vec, +) -> Global { + install_type_config_inner(_globals, shapes, type_config, module_name, _loc, &mut Some(errors)) +} + +fn install_type_config_inner( + _globals: &mut GlobalRegistry, + shapes: &mut ShapeRegistry, + type_config: &TypeConfig, + module_name: &str, + _loc: (), + errors: &mut Option<&mut Vec>, +) -> Global { + match type_config { + TypeConfig::TypeReference(TypeReferenceConfig { name }) => match name { + BuiltInTypeRef::Array => Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + BuiltInTypeRef::MixedReadonly => { + Type::Object { shape_id: Some(owned(BUILT_IN_MIXED_READONLY_ID)) } + } + BuiltInTypeRef::Primitive => Type::Primitive, + BuiltInTypeRef::Ref => Type::Object { shape_id: Some(owned(BUILT_IN_USE_REF_ID)) }, + BuiltInTypeRef::Any => Type::Poly, + }, + TypeConfig::Function(func_config) => { + // Compute return type first to avoid double-borrow of shapes + let return_type = install_type_config_inner( + _globals, + shapes, + &func_config.return_type, + module_name, + (), + errors, + ); + add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: func_config.positional_params.clone(), + rest_param: func_config.rest_param, + callee_effect: func_config.callee_effect, + return_type, + return_value_kind: func_config.return_value_kind, + no_alias: func_config.no_alias.unwrap_or(false), + mutable_only_if_operands_are_mutable: func_config + .mutable_only_if_operands_are_mutable + .unwrap_or(false), + impure: func_config.impure.unwrap_or(false), + canonical_name: func_config.canonical_name.clone(), + aliasing: func_config.aliasing.clone(), + known_incompatible: func_config.known_incompatible.clone(), + ..Default::default() + }, + None, + false, + ) + } + TypeConfig::Hook(hook_config) => { + // Compute return type first to avoid double-borrow of shapes + let return_type = install_type_config_inner( + _globals, + shapes, + &hook_config.return_type, + module_name, + (), + errors, + ); + add_hook( + shapes, + HookSignatureBuilder { + hook_kind: HookKind::Custom, + positional_params: hook_config.positional_params.clone().unwrap_or_default(), + rest_param: hook_config.rest_param.or(Some(Effect::Freeze)), + callee_effect: Effect::Read, + return_type, + return_value_kind: hook_config.return_value_kind.unwrap_or(ValueKind::Frozen), + no_alias: hook_config.no_alias.unwrap_or(false), + aliasing: hook_config.aliasing.clone(), + known_incompatible: hook_config.known_incompatible.clone(), + ..Default::default() + }, + None, + ) + } + TypeConfig::Object(obj_config) => { + let properties: Vec<(String, Type)> = obj_config + .properties + .as_ref() + .map(|props| { + props + .iter() + .map(|(key, value)| { + let ty = install_type_config_inner( + _globals, + shapes, + value, + module_name, + (), + errors, + ); + // Validate hook-name vs hook-type consistency (matching TS installTypeConfig) + if let Some(errs) = errors { + let expect_hook = crate::react_compiler_hir::environment::is_hook_name(key); + let is_hook = match &ty { + Type::Function { shape_id: Some(id), .. } => { + shapes.get(id) + .and_then(|shape| shape.function_type.as_ref()) + .and_then(|ft| ft.hook_kind.as_ref()) + .is_some() + } + _ => false, + }; + if expect_hook != is_hook { + errs.push(format!( + "Expected type for object property '{}' from module '{}' {} based on the property name", + key, + module_name, + if expect_hook { "to be a hook" } else { "not to be a hook" } + )); + } + } + (key.clone(), ty) + }) + .collect() + }) + .unwrap_or_default(); + add_object(shapes, None, properties) + } + } +} + +// ============================================================================= +// Build built-in shapes (BUILTIN_SHAPES from ObjectShape.ts) +// ============================================================================= + +/// Allocate an owned `String` from a static string. The builtin shape/global +/// tables build hundreds of `String`s from string literals and `&'static str` +/// consts; routing them through one out-of-line helper keeps the allocation +/// sequence from being inlined at every call site (it's run-once init code, so +/// the extra call is free), shrinking the generated code for these tables. +#[inline(never)] +fn owned(s: &str) -> String { + s.to_string() +} + +/// Build the built-in shapes registry. This corresponds to TS `BUILTIN_SHAPES` +/// defined at module level in ObjectShape.ts. +pub fn build_builtin_shapes() -> ShapeRegistry { + let mut shapes = ShapeRegistry::new(); + + // BuiltInProps: { ref: UseRefType } + add_object( + &mut shapes, + Some(BUILT_IN_PROPS_ID), + vec![(owned("ref"), Type::Object { shape_id: Some(owned(BUILT_IN_USE_REF_ID)) })], + ); + + build_array_shape(&mut shapes); + build_set_shape(&mut shapes); + build_map_shape(&mut shapes); + build_weak_set_shape(&mut shapes); + build_weak_map_shape(&mut shapes); + build_object_shape(&mut shapes); + build_ref_shapes(&mut shapes); + build_state_shapes(&mut shapes); + build_hook_shapes(&mut shapes); + build_misc_shapes(&mut shapes); + + shapes +} + +fn simple_function( + shapes: &mut ShapeRegistry, + positional_params: Vec, + rest_param: Option, + return_type: Type, + return_value_kind: ValueKind, +) -> Type { + add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params, + rest_param, + return_type, + return_value_kind, + ..Default::default() + }, + None, + false, + ) +} + +/// Shorthand for a pure function returning Primitive. +fn pure_primitive_fn(shapes: &mut ShapeRegistry) -> Type { + simple_function(shapes, Vec::new(), Some(Effect::Read), Type::Primitive, ValueKind::Primitive) +} + +fn build_array_shape(shapes: &mut ShapeRegistry) { + let index_of = pure_primitive_fn(shapes); + let includes = pure_primitive_fn(shapes); + let pop = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Store, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let at = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let concat = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Capture), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + callee_effect: Effect::Capture, + ..Default::default() + }, + None, + false, + ); + let join = pure_primitive_fn(shapes); + let slice = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + callee_effect: Effect::Capture, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let map = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + aliasing: Some(AliasingSignatureConfig { + receiver: owned("@receiver"), + params: vec![owned("@callback")], + rest: None, + returns: owned("@returns"), + temporaries: vec![owned("@item"), owned("@callbackReturn"), owned("@thisArg")], + effects: vec![ + // Map creates a new mutable array + AliasingEffectConfig::Create { + into: owned("@returns"), + value: ValueKind::Mutable, + reason: ValueReason::KnownReturnSignature, + }, + // The first arg to the callback is an item extracted from the receiver array + AliasingEffectConfig::CreateFrom { + from: owned("@receiver"), + into: owned("@item"), + }, + // The undefined this for the callback + AliasingEffectConfig::Create { + into: owned("@thisArg"), + value: ValueKind::Primitive, + reason: ValueReason::KnownReturnSignature, + }, + // Calls the callback, returning the result into a temporary + AliasingEffectConfig::Apply { + receiver: owned("@thisArg"), + function: owned("@callback"), + mutates_function: false, + args: vec![ + ApplyArgConfig::Place(owned("@item")), + ApplyArgConfig::Hole { kind: ApplyArgHoleKind::Hole }, + ApplyArgConfig::Place(owned("@receiver")), + ], + into: owned("@callbackReturn"), + }, + // Captures the result of the callback into the return array + AliasingEffectConfig::Capture { + from: owned("@callbackReturn"), + into: owned("@returns"), + }, + ], + }), + ..Default::default() + }, + None, + false, + ); + let filter = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let find = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let find_index = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let every = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let some = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let flat_map = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let length = Type::Primitive; + let push = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Capture), + callee_effect: Effect::Store, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + aliasing: Some(AliasingSignatureConfig { + receiver: owned("@receiver"), + params: Vec::new(), + rest: Some(owned("@rest")), + returns: owned("@returns"), + temporaries: Vec::new(), + effects: vec![ + // Push directly mutates the array itself + AliasingEffectConfig::Mutate { value: owned("@receiver") }, + // The arguments are captured into the array + AliasingEffectConfig::Capture { + from: owned("@rest"), + into: owned("@receiver"), + }, + // Returns the new length, a primitive + AliasingEffectConfig::Create { + into: owned("@returns"), + value: ValueKind::Primitive, + reason: ValueReason::KnownReturnSignature, + }, + ], + }), + ..Default::default() + }, + None, + false, + ); + + add_object( + shapes, + Some(BUILT_IN_ARRAY_ID), + vec![ + (owned("indexOf"), index_of), + (owned("includes"), includes), + (owned("pop"), pop), + (owned("at"), at), + (owned("concat"), concat), + (owned("length"), length), + (owned("push"), push), + (owned("slice"), slice), + (owned("map"), map), + (owned("flatMap"), flat_map), + (owned("filter"), filter), + (owned("every"), every), + (owned("some"), some), + (owned("find"), find), + (owned("findIndex"), find_index), + (owned("join"), join), + // TODO: rest of Array properties + ], + ); +} + +fn build_set_shape(shapes: &mut ShapeRegistry) { + let has = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let add = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture], + callee_effect: Effect::Store, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_SET_ID)) }, + return_value_kind: ValueKind::Mutable, + aliasing: Some(AliasingSignatureConfig { + receiver: owned("@receiver"), + params: Vec::new(), + rest: Some(owned("@rest")), + returns: owned("@returns"), + temporaries: Vec::new(), + effects: vec![ + // Set.add returns the receiver Set + AliasingEffectConfig::Assign { + from: owned("@receiver"), + into: owned("@returns"), + }, + // Set.add mutates the set itself + AliasingEffectConfig::Mutate { value: owned("@receiver") }, + // Captures the rest params into the set + AliasingEffectConfig::Capture { + from: owned("@rest"), + into: owned("@receiver"), + }, + ], + }), + ..Default::default() + }, + None, + false, + ); + let clear = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Store, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let delete = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Store, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let size = Type::Primitive; + let difference = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture], + callee_effect: Effect::Capture, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_SET_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let union = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture], + callee_effect: Effect::Capture, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_SET_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let symmetrical_difference = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture], + callee_effect: Effect::Capture, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_SET_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let is_subset_of = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Read, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let is_superset_of = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Read, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let for_each = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let values = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let keys = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let entries = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + + add_object( + shapes, + Some(BUILT_IN_SET_ID), + vec![ + (owned("add"), add), + (owned("clear"), clear), + (owned("delete"), delete), + (owned("has"), has), + (owned("size"), size), + (owned("difference"), difference), + (owned("union"), union), + (owned("symmetricalDifference"), symmetrical_difference), + (owned("isSubsetOf"), is_subset_of), + (owned("isSupersetOf"), is_superset_of), + (owned("forEach"), for_each), + (owned("values"), values), + (owned("keys"), keys), + (owned("entries"), entries), + ], + ); +} + +fn build_map_shape(shapes: &mut ShapeRegistry) { + let has = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let get = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let clear = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Store, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let set = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture, Effect::Capture], + callee_effect: Effect::Store, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_MAP_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let delete = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Store, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let size = Type::Primitive; + let for_each = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let values = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let keys = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let entries = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + + add_object( + shapes, + Some(BUILT_IN_MAP_ID), + vec![ + (owned("has"), has), + (owned("get"), get), + (owned("set"), set), + (owned("clear"), clear), + (owned("delete"), delete), + (owned("size"), size), + (owned("forEach"), for_each), + (owned("values"), values), + (owned("keys"), keys), + (owned("entries"), entries), + ], + ); +} + +fn build_weak_set_shape(shapes: &mut ShapeRegistry) { + let has = pure_primitive_fn(shapes); + let add = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture], + callee_effect: Effect::Store, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_WEAK_SET_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let delete = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Store, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + + add_object( + shapes, + Some(BUILT_IN_WEAK_SET_ID), + vec![(owned("has"), has), (owned("add"), add), (owned("delete"), delete)], + ); +} + +fn build_weak_map_shape(shapes: &mut ShapeRegistry) { + let has = pure_primitive_fn(shapes); + let get = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Capture, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let set = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture, Effect::Capture], + callee_effect: Effect::Store, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_WEAK_MAP_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let delete = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + callee_effect: Effect::Store, + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + + add_object( + shapes, + Some(BUILT_IN_WEAK_MAP_ID), + vec![ + (owned("has"), has), + (owned("get"), get), + (owned("set"), set), + (owned("delete"), delete), + ], + ); +} + +fn build_object_shape(shapes: &mut ShapeRegistry) { + // BuiltInObject: has toString() returning Primitive (matches TS BuiltInObjectId shape) + let to_string = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + add_object(shapes, Some(BUILT_IN_OBJECT_ID), vec![(owned("toString"), to_string)]); + // BuiltInFunction: empty shape + add_object(shapes, Some(BUILT_IN_FUNCTION_ID), Vec::new()); + // BuiltInJsx: empty shape + add_object(shapes, Some(BUILT_IN_JSX_ID), Vec::new()); + // BuiltInMixedReadonly: has explicit method types + wildcard returning MixedReadonly + // (matches TS BuiltInMixedReadonlyId shape) + let mixed_to_string = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let mixed_index_of = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let mixed_includes = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let mixed_at = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_MIXED_READONLY_ID)) }, + callee_effect: Effect::Capture, + return_value_kind: ValueKind::Frozen, + ..Default::default() + }, + None, + false, + ); + let mixed_map = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + callee_effect: Effect::ConditionallyMutate, + return_value_kind: ValueKind::Mutable, + no_alias: true, + ..Default::default() + }, + None, + false, + ); + let mixed_flat_map = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + callee_effect: Effect::ConditionallyMutate, + return_value_kind: ValueKind::Mutable, + no_alias: true, + ..Default::default() + }, + None, + false, + ); + let mixed_filter = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + callee_effect: Effect::ConditionallyMutate, + return_value_kind: ValueKind::Mutable, + no_alias: true, + ..Default::default() + }, + None, + false, + ); + let mixed_concat = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Capture), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + callee_effect: Effect::Capture, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let mixed_slice = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + callee_effect: Effect::Capture, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let mixed_every = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + return_type: Type::Primitive, + callee_effect: Effect::ConditionallyMutate, + return_value_kind: ValueKind::Primitive, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let mixed_some = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + return_type: Type::Primitive, + callee_effect: Effect::ConditionallyMutate, + return_value_kind: ValueKind::Primitive, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let mixed_find = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_MIXED_READONLY_ID)) }, + callee_effect: Effect::ConditionallyMutate, + return_value_kind: ValueKind::Frozen, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let mixed_find_index = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + return_type: Type::Primitive, + callee_effect: Effect::ConditionallyMutate, + return_value_kind: ValueKind::Primitive, + no_alias: true, + mutable_only_if_operands_are_mutable: true, + ..Default::default() + }, + None, + false, + ); + let mixed_join = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let mut mixed_props = FxHashMap::default(); + mixed_props.insert(owned("toString"), mixed_to_string); + mixed_props.insert(owned("indexOf"), mixed_index_of); + mixed_props.insert(owned("includes"), mixed_includes); + mixed_props.insert(owned("at"), mixed_at); + mixed_props.insert(owned("map"), mixed_map); + mixed_props.insert(owned("flatMap"), mixed_flat_map); + mixed_props.insert(owned("filter"), mixed_filter); + mixed_props.insert(owned("concat"), mixed_concat); + mixed_props.insert(owned("slice"), mixed_slice); + mixed_props.insert(owned("every"), mixed_every); + mixed_props.insert(owned("some"), mixed_some); + mixed_props.insert(owned("find"), mixed_find); + mixed_props.insert(owned("findIndex"), mixed_find_index); + mixed_props.insert(owned("join"), mixed_join); + mixed_props + .insert(owned("*"), Type::Object { shape_id: Some(owned(BUILT_IN_MIXED_READONLY_ID)) }); + shapes.insert( + owned(BUILT_IN_MIXED_READONLY_ID), + ObjectShape { properties: mixed_props, function_type: None }, + ); +} + +fn build_ref_shapes(shapes: &mut ShapeRegistry) { + // BuiltInUseRefId: { current: Object { shapeId: BuiltInRefValue } } + add_object( + shapes, + Some(BUILT_IN_USE_REF_ID), + vec![(owned("current"), Type::Object { shape_id: Some(owned(BUILT_IN_REF_VALUE_ID)) })], + ); + // BuiltInRefValue: { *: Object { shapeId: BuiltInRefValue } } (self-referencing) + add_object( + shapes, + Some(BUILT_IN_REF_VALUE_ID), + vec![(owned("*"), Type::Object { shape_id: Some(owned(BUILT_IN_REF_VALUE_ID)) })], + ); +} + +fn build_state_shapes(shapes: &mut ShapeRegistry) { + // BuiltInSetState: function that freezes its argument + let set_state = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + Some(BUILT_IN_SET_STATE_ID), + false, + ); + + // BuiltInUseState: object with [0] = Poly (state), [1] = setState function + add_object( + shapes, + Some(BUILT_IN_USE_STATE_ID), + vec![(owned("0"), Type::Poly), (owned("1"), set_state)], + ); + + // BuiltInSetActionState + let set_action_state = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + Some(BUILT_IN_SET_ACTION_STATE_ID), + false, + ); + + // BuiltInUseActionState: [0] = Poly, [1] = setActionState function + add_object( + shapes, + Some(BUILT_IN_USE_ACTION_STATE_ID), + vec![(owned("0"), Type::Poly), (owned("1"), set_action_state)], + ); + + // BuiltInDispatch + let dispatch = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + Some(BUILT_IN_DISPATCH_ID), + false, + ); + + // BuiltInUseReducer: [0] = Poly, [1] = dispatch function + add_object( + shapes, + Some(BUILT_IN_USE_REDUCER_ID), + vec![(owned("0"), Type::Poly), (owned("1"), dispatch)], + ); + + // BuiltInStartTransition + let start_transition = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + // Note: TS uses restParam: null for startTransition + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + Some(BUILT_IN_START_TRANSITION_ID), + false, + ); + + // BuiltInUseTransition: [0] = Primitive (isPending), [1] = startTransition function + add_object( + shapes, + Some(BUILT_IN_USE_TRANSITION_ID), + vec![(owned("0"), Type::Primitive), (owned("1"), start_transition)], + ); + + // BuiltInSetOptimistic + let set_optimistic = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + Some(BUILT_IN_SET_OPTIMISTIC_ID), + false, + ); + + // BuiltInUseOptimistic: [0] = Poly, [1] = setOptimistic function + add_object( + shapes, + Some(BUILT_IN_USE_OPTIMISTIC_ID), + vec![(owned("0"), Type::Poly), (owned("1"), set_optimistic)], + ); +} + +fn build_hook_shapes(shapes: &mut ShapeRegistry) { + // BuiltInEffectEvent function shape (the return value of useEffectEvent) + add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + callee_effect: Effect::ConditionallyMutate, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + Some(BUILT_IN_EFFECT_EVENT_ID), + false, + ); +} + +fn build_misc_shapes(shapes: &mut ShapeRegistry) { + // ReanimatedSharedValue: empty properties (matching TS) + add_object(shapes, Some(REANIMATED_SHARED_VALUE_ID), Vec::new()); +} + +/// Build the reanimated module type. Ported from TS `getReanimatedModuleType`. +pub fn get_reanimated_module_type(shapes: &mut ShapeRegistry) -> Type { + let mut reanimated_type: Vec<(String, Type)> = Vec::new(); + + // hooks that freeze args and return frozen value + let frozen_hooks = [ + "useFrameCallback", + "useAnimatedStyle", + "useAnimatedProps", + "useAnimatedScrollHandler", + "useAnimatedReaction", + "useWorkletCallback", + ]; + for hook in &frozen_hooks { + let hook_type = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + no_alias: true, + hook_kind: HookKind::Custom, + ..Default::default() + }, + None, + ); + reanimated_type.push((hook.to_string(), hook_type)); + } + + // hooks that return a mutable value (modelled as shared value) + let mutable_hooks = ["useSharedValue", "useDerivedValue"]; + for hook in &mutable_hooks { + let hook_type = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Object { + shape_id: Some(REANIMATED_SHARED_VALUE_ID.to_string()), + }, + return_value_kind: ValueKind::Mutable, + no_alias: true, + hook_kind: HookKind::Custom, + ..Default::default() + }, + None, + ); + reanimated_type.push((hook.to_string(), hook_type)); + } + + // functions that return mutable value + let funcs = [ + "withTiming", + "withSpring", + "createAnimatedPropAdapter", + "withDecay", + "withRepeat", + "runOnUI", + "executeOnUIRuntimeSync", + ]; + for func_name in &funcs { + let func_type = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + no_alias: true, + ..Default::default() + }, + None, + false, + ); + reanimated_type.push((func_name.to_string(), func_type)); + } + + add_object(shapes, None, reanimated_type) +} + +// ============================================================================= +// Build default globals (DEFAULT_GLOBALS from Globals.ts) +// ============================================================================= + +/// Build the default globals registry. This corresponds to TS `DEFAULT_GLOBALS`. +/// +/// Requires a mutable reference to the shapes registry because some globals +/// (like Object.keys, Array.isArray) register new shapes. +pub fn build_default_globals(shapes: &mut ShapeRegistry) -> GlobalRegistry { + let mut globals = GlobalRegistry::new(); + + // React APIs — returns the list so we can reuse them for the React namespace + let react_apis = build_react_apis(shapes, &mut globals); + + // Untyped globals (treated as Poly) — must come before typed globals + // so typed definitions take priority (matching TS ordering) + for name in UNTYPED_GLOBALS { + globals.insert(name.to_string(), Type::Poly); + } + + // Typed JS globals (overwrites Poly entries from UNTYPED_GLOBALS). + // Returns the list of typed globals for use as globalThis/global properties. + let typed_globals = build_typed_globals(shapes, &mut globals, react_apis); + + // globalThis and global — populated with all typed globals as properties + // (matching TS: `addObject(DEFAULT_SHAPES, 'globalThis', TYPED_GLOBALS)`) + globals + .insert(owned("globalThis"), add_object(shapes, Some("globalThis"), typed_globals.clone())); + globals.insert(owned("global"), add_object(shapes, Some("global"), typed_globals)); + + globals +} + +const UNTYPED_GLOBALS: &[&str] = &[ + "Object", + "Function", + "RegExp", + "Date", + "Error", + "TypeError", + "RangeError", + "ReferenceError", + "SyntaxError", + "URIError", + "EvalError", + "DataView", + "Float32Array", + "Float64Array", + "Int8Array", + "Int16Array", + "Int32Array", + "WeakMap", + "Uint8Array", + "Uint8ClampedArray", + "Uint16Array", + "Uint32Array", + "ArrayBuffer", + "JSON", + "console", + "eval", +]; + +/// Build the React API types (REACT_APIS from TS). Returns the list of (name, type) pairs +/// so they can be reused as properties of the React namespace object (matching TS behavior +/// where the SAME type objects are used in both DEFAULT_GLOBALS and the React namespace). +fn build_react_apis( + shapes: &mut ShapeRegistry, + globals: &mut GlobalRegistry, +) -> Vec<(String, Type)> { + let mut react_apis: Vec<(String, Type)> = Vec::new(); + + // useContext + let use_context = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + return_value_reason: Some(ValueReason::Context), + hook_kind: HookKind::UseContext, + ..Default::default() + }, + Some(BUILT_IN_USE_CONTEXT_HOOK_ID), + ); + react_apis.push((owned("useContext"), use_context)); + + // useState + let use_state = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_USE_STATE_ID)) }, + return_value_kind: ValueKind::Frozen, + return_value_reason: Some(ValueReason::State), + hook_kind: HookKind::UseState, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useState"), use_state)); + + // useActionState + let use_action_state = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_USE_ACTION_STATE_ID)) }, + return_value_kind: ValueKind::Frozen, + return_value_reason: Some(ValueReason::State), + hook_kind: HookKind::UseActionState, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useActionState"), use_action_state)); + + // useReducer + let use_reducer = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_USE_REDUCER_ID)) }, + return_value_kind: ValueKind::Frozen, + return_value_reason: Some(ValueReason::ReducerState), + hook_kind: HookKind::UseReducer, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useReducer"), use_reducer)); + + // useRef + let use_ref = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Capture), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_USE_REF_ID)) }, + return_value_kind: ValueKind::Mutable, + hook_kind: HookKind::UseRef, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useRef"), use_ref)); + + // useImperativeHandle + let use_imperative_handle = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Primitive, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::UseImperativeHandle, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useImperativeHandle"), use_imperative_handle)); + + // useMemo + let use_memo = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::UseMemo, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useMemo"), use_memo)); + + // useCallback + let use_callback = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::UseCallback, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useCallback"), use_callback)); + + // useEffect (with aliasing signature) + let use_effect = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Primitive, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::UseEffect, + aliasing: Some(AliasingSignatureConfig { + receiver: owned("@receiver"), + params: Vec::new(), + rest: Some(owned("@rest")), + returns: owned("@returns"), + temporaries: vec![owned("@effect")], + effects: vec![ + AliasingEffectConfig::Freeze { + value: owned("@rest"), + reason: ValueReason::Effect, + }, + AliasingEffectConfig::Create { + into: owned("@effect"), + value: ValueKind::Frozen, + reason: ValueReason::KnownReturnSignature, + }, + AliasingEffectConfig::Capture { from: owned("@rest"), into: owned("@effect") }, + AliasingEffectConfig::Create { + into: owned("@returns"), + value: ValueKind::Primitive, + reason: ValueReason::KnownReturnSignature, + }, + ], + }), + ..Default::default() + }, + Some(BUILT_IN_USE_EFFECT_HOOK_ID), + ); + react_apis.push((owned("useEffect"), use_effect)); + + // useLayoutEffect + let use_layout_effect = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::UseLayoutEffect, + ..Default::default() + }, + Some(BUILT_IN_USE_LAYOUT_EFFECT_HOOK_ID), + ); + react_apis.push((owned("useLayoutEffect"), use_layout_effect)); + + // useInsertionEffect + let use_insertion_effect = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::UseInsertionEffect, + ..Default::default() + }, + Some(BUILT_IN_USE_INSERTION_EFFECT_HOOK_ID), + ); + react_apis.push((owned("useInsertionEffect"), use_insertion_effect)); + + // useTransition + let use_transition = add_hook( + shapes, + HookSignatureBuilder { + rest_param: None, + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_USE_TRANSITION_ID)) }, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::UseTransition, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useTransition"), use_transition)); + + // useOptimistic + let use_optimistic = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_USE_OPTIMISTIC_ID)) }, + return_value_kind: ValueKind::Frozen, + return_value_reason: Some(ValueReason::State), + hook_kind: HookKind::UseOptimistic, + ..Default::default() + }, + None, + ); + react_apis.push((owned("useOptimistic"), use_optimistic)); + + // use (not a hook, it's a function) + let use_fn = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + ..Default::default() + }, + Some(BUILT_IN_USE_OPERATOR_ID), + false, + ); + react_apis.push((owned("use"), use_fn)); + + // useEffectEvent + let use_effect_event = add_hook( + shapes, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Function { + shape_id: Some(owned(BUILT_IN_EFFECT_EVENT_ID)), + return_type: Box::new(Type::Poly), + is_constructor: false, + }, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::UseEffectEvent, + ..Default::default() + }, + Some(BUILT_IN_USE_EFFECT_EVENT_ID), + ); + react_apis.push((owned("useEffectEvent"), use_effect_event)); + + // Insert all React APIs as standalone globals + for (name, ty) in &react_apis { + globals.insert(name.clone(), ty.clone()); + } + + react_apis +} + +/// Build typed globals and return them as a list for use as globalThis/global properties. +fn build_typed_globals( + shapes: &mut ShapeRegistry, + globals: &mut GlobalRegistry, + react_apis: Vec<(String, Type)>, +) -> Vec<(String, Type)> { + let mut typed_globals: Vec<(String, Type)> = Vec::new(); + // Object + let obj_keys = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + aliasing: Some(AliasingSignatureConfig { + receiver: owned("@receiver"), + params: vec![owned("@object")], + rest: None, + returns: owned("@returns"), + temporaries: Vec::new(), + effects: vec![ + AliasingEffectConfig::Create { + into: owned("@returns"), + value: ValueKind::Mutable, + reason: ValueReason::KnownReturnSignature, + }, + // Only keys are captured, and keys are immutable + AliasingEffectConfig::ImmutableCapture { + from: owned("@object"), + into: owned("@returns"), + }, + ], + }), + ..Default::default() + }, + None, + false, + ); + let obj_from_entries = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::ConditionallyMutate], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_OBJECT_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let obj_entries = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + aliasing: Some(AliasingSignatureConfig { + receiver: owned("@receiver"), + params: vec![owned("@object")], + rest: None, + returns: owned("@returns"), + temporaries: Vec::new(), + effects: vec![ + AliasingEffectConfig::Create { + into: owned("@returns"), + value: ValueKind::Mutable, + reason: ValueReason::KnownReturnSignature, + }, + // Object values are captured into the return + AliasingEffectConfig::Capture { + from: owned("@object"), + into: owned("@returns"), + }, + ], + }), + ..Default::default() + }, + None, + false, + ); + let obj_values = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Capture], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + aliasing: Some(AliasingSignatureConfig { + receiver: owned("@receiver"), + params: vec![owned("@object")], + rest: None, + returns: owned("@returns"), + temporaries: Vec::new(), + effects: vec![ + AliasingEffectConfig::Create { + into: owned("@returns"), + value: ValueKind::Mutable, + reason: ValueReason::KnownReturnSignature, + }, + // Object values are captured into the return + AliasingEffectConfig::Capture { + from: owned("@object"), + into: owned("@returns"), + }, + ], + }), + ..Default::default() + }, + None, + false, + ); + let object_global = add_object( + shapes, + Some("Object"), + vec![ + (owned("keys"), obj_keys), + (owned("fromEntries"), obj_from_entries), + (owned("entries"), obj_entries), + (owned("values"), obj_values), + ], + ); + typed_globals.push((owned("Object"), object_global.clone())); + globals.insert(owned("Object"), object_global); + + // Array + let array_is_array = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::Read], + return_type: Type::Primitive, + return_value_kind: ValueKind::Primitive, + ..Default::default() + }, + None, + false, + ); + let array_from = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![ + Effect::ConditionallyMutateIterator, + Effect::ConditionallyMutate, + Effect::ConditionallyMutate, + ], + rest_param: Some(Effect::Read), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let array_of = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_ARRAY_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + let array_global = add_object( + shapes, + Some("Array"), + vec![ + (owned("isArray"), array_is_array), + (owned("from"), array_from), + (owned("of"), array_of), + ], + ); + typed_globals.push((owned("Array"), array_global.clone())); + globals.insert(owned("Array"), array_global); + + // Math + let math_fns: Vec<(String, Type)> = ["max", "min", "trunc", "ceil", "floor", "pow"] + .iter() + .map(|name| (name.to_string(), pure_primitive_fn(shapes))) + .collect(); + let mut math_props = math_fns; + math_props.push((owned("PI"), Type::Primitive)); + // Math.random is impure + let math_random = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + impure: true, + canonical_name: Some(owned("Math.random")), + ..Default::default() + }, + None, + false, + ); + math_props.push((owned("random"), math_random)); + let math_global = add_object(shapes, Some("Math"), math_props); + typed_globals.push((owned("Math"), math_global.clone())); + globals.insert(owned("Math"), math_global); + + // performance + let perf_now = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + impure: true, + canonical_name: Some(owned("performance.now")), + ..Default::default() + }, + None, + false, + ); + let perf_global = add_object(shapes, Some("performance"), vec![(owned("now"), perf_now)]); + typed_globals.push((owned("performance"), perf_global.clone())); + globals.insert(owned("performance"), perf_global); + + // Date + let date_now = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Read), + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + impure: true, + canonical_name: Some(owned("Date.now")), + ..Default::default() + }, + None, + false, + ); + let date_global = add_object(shapes, Some("Date"), vec![(owned("now"), date_now)]); + typed_globals.push((owned("Date"), date_global.clone())); + globals.insert(owned("Date"), date_global); + + // console + let console_methods: Vec<(String, Type)> = ["error", "info", "log", "table", "trace", "warn"] + .iter() + .map(|name| (name.to_string(), pure_primitive_fn(shapes))) + .collect(); + let console_global = add_object(shapes, Some("console"), console_methods); + typed_globals.push((owned("console"), console_global.clone())); + globals.insert(owned("console"), console_global); + + // Simple global functions returning Primitive + for name in &[ + "Boolean", + "Number", + "String", + "parseInt", + "parseFloat", + "isNaN", + "isFinite", + "encodeURI", + "encodeURIComponent", + "decodeURI", + "decodeURIComponent", + ] { + let f = pure_primitive_fn(shapes); + typed_globals.push((name.to_string(), f.clone())); + globals.insert(name.to_string(), f); + } + + // Primitive globals + typed_globals.push((owned("Infinity"), Type::Primitive)); + globals.insert(owned("Infinity"), Type::Primitive); + typed_globals.push((owned("NaN"), Type::Primitive)); + globals.insert(owned("NaN"), Type::Primitive); + + // Map, Set, WeakMap, WeakSet constructors + let map_ctor = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::ConditionallyMutateIterator], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_MAP_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + true, + ); + typed_globals.push((owned("Map"), map_ctor.clone())); + globals.insert(owned("Map"), map_ctor); + + let set_ctor = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::ConditionallyMutateIterator], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_SET_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + true, + ); + typed_globals.push((owned("Set"), set_ctor.clone())); + globals.insert(owned("Set"), set_ctor); + + let weak_map_ctor = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::ConditionallyMutateIterator], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_WEAK_MAP_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + true, + ); + typed_globals.push((owned("WeakMap"), weak_map_ctor.clone())); + globals.insert(owned("WeakMap"), weak_map_ctor); + + let weak_set_ctor = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + positional_params: vec![Effect::ConditionallyMutateIterator], + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_WEAK_SET_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + true, + ); + typed_globals.push((owned("WeakSet"), weak_set_ctor.clone())); + globals.insert(owned("WeakSet"), weak_set_ctor); + + // React global object — reuses the same REACT_APIS types (matching TS behavior + // where the same type objects are used as both standalone globals and React.* properties) + let react_create_element = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + ..Default::default() + }, + None, + false, + ); + let react_clone_element = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + ..Default::default() + }, + None, + false, + ); + let react_create_ref = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Capture), + return_type: Type::Object { shape_id: Some(owned(BUILT_IN_USE_REF_ID)) }, + return_value_kind: ValueKind::Mutable, + ..Default::default() + }, + None, + false, + ); + + // Build React namespace properties from react_apis + React-specific functions + let mut react_props: Vec<(String, Type)> = react_apis; + react_props.push((owned("createElement"), react_create_element)); + react_props.push((owned("cloneElement"), react_clone_element)); + react_props.push((owned("createRef"), react_create_ref)); + + let react_global = add_object(shapes, None, react_props); + typed_globals.push((owned("React"), react_global.clone())); + globals.insert(owned("React"), react_global); + + // _jsx (used by JSX transform) + let jsx_fn = add_function( + shapes, + Vec::new(), + FunctionSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + ..Default::default() + }, + None, + false, + ); + typed_globals.push((owned("_jsx"), jsx_fn.clone())); + globals.insert(owned("_jsx"), jsx_fn); + + typed_globals +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/mod.rs b/crates/oxc_react_compiler/src/react_compiler_hir/mod.rs new file mode 100644 index 0000000000000..c9524ecefea8a --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/mod.rs @@ -0,0 +1,1573 @@ +pub mod default_module_type_provider; +pub mod dominator; +pub mod environment; +pub mod environment_config; +pub mod globals; +pub mod object_shape; +#[cfg(feature = "debug")] +pub mod print; +pub mod raw; +/// Stub when the `debug` feature is off: keeps only the `PrintFormatter` type +/// that the pipeline's debug closures name; the IR printer itself is excluded. +#[cfg(not(feature = "debug"))] +pub mod print { + use super::environment::Environment; + use std::marker::PhantomData; + + pub struct PrintFormatter<'a, 'h>(PhantomData<(&'a (), &'h ())>, PhantomData>); +} +pub mod reactive; +pub mod type_config; +pub mod visitors; + +pub use crate::react_compiler_diagnostics::CompilerDiagnostic; +pub use crate::react_compiler_diagnostics::ErrorCategory; +pub use crate::react_compiler_diagnostics::GENERATED_SOURCE; +pub use crate::react_compiler_diagnostics::Position; +pub use crate::react_compiler_diagnostics::SourceLocation; +use crate::react_compiler_utils::FxIndexMap; +use crate::react_compiler_utils::FxIndexSet; +use oxc_ast::ast as oxc; +pub use raw::{RawIdent, RawNode, RawTypeCategory}; +pub use reactive::*; + +// ============================================================================= +// ID newtypes +// ============================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct BlockId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct IdentifierId(pub u32); + +/// Index into the flat instruction table on HirFunction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct InstructionId(pub u32); + +/// Evaluation order assigned to instructions and terminals during numbering. +/// This was previously called InstructionId in the TypeScript compiler. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct EvaluationOrder(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct DeclarationId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ScopeId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct TypeId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct FunctionId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MutableRangeId(pub u32); + +// ============================================================================= +// FloatValue wrapper +// ============================================================================= + +/// Wrapper around f64 that stores raw bytes for deterministic equality and hashing. +/// This allows use in FxHashMap keys and ensures NaN == NaN (bitwise comparison). +#[derive(Debug, Clone, Copy)] +pub struct FloatValue(u64); + +impl FloatValue { + pub fn new(value: f64) -> Self { + FloatValue(value.to_bits()) + } + + pub fn value(self) -> f64 { + f64::from_bits(self.0) + } +} + +impl From for FloatValue { + fn from(value: f64) -> Self { + FloatValue::new(value) + } +} + +impl From for f64 { + fn from(value: FloatValue) -> Self { + value.value() + } +} + +impl PartialEq for FloatValue { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for FloatValue {} + +impl std::hash::Hash for FloatValue { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl std::fmt::Display for FloatValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", format_js_number(self.value())) + } +} + +/// Format an f64 the way JavaScript's `Number.prototype.toString()` does. +/// +/// Key differences from Rust's default `Display`: +/// - Uses scientific notation for |x| >= 1e21 (e.g. `1e+21`, `2.18739127891275e+22`) +/// - Uses scientific notation for 0 < |x| < 1e-6 (e.g. `1e-7`, `1.5e-8`) +/// - Uses minimal significant digits that round-trip to the same f64 +/// - Formats -0 as "0" +pub fn format_js_number(n: f64) -> String { + if n.is_nan() { + return "NaN".to_string(); + } + if n.is_infinite() { + return if n > 0.0 { "Infinity".to_string() } else { "-Infinity".to_string() }; + } + if n == 0.0 { + return "0".to_string(); + } + + let abs = n.abs(); + let sign = if n < 0.0 { "-" } else { "" }; + + if abs >= 1e21 || (abs > 0.0 && abs < 1e-6) { + // Use scientific notation matching JS format: coefficient + "e+" or "e-" + exponent + // Rust's {:e} uses "e" (lowercase) like JS, but formats as e.g. "1.5e21" not "1.5e+21" + let formatted = format!("{:e}", abs); + // Split into coefficient and exponent parts + let (coeff, exp_str) = formatted.split_once('e').unwrap(); + let exp: i32 = exp_str.parse().unwrap(); + // JS uses e+N for positive exponents, e-N for negative + if exp >= 0 { + format!("{}{}e+{}", sign, coeff, exp) + } else { + format!("{}{}e-{}", sign, coeff, exp.unsigned_abs()) + } + } else if abs.fract() == 0.0 && abs < (i64::MAX as f64) { + // Integer that fits in i64 — format without decimal point + format!("{}{}", sign, abs as i64) + } else { + // Regular float: Rust's default Display gives us the right digits + format!("{}", n) + } +} + +// ============================================================================= +// Core HIR types +// ============================================================================= + +/// A function lowered to HIR form +#[derive(Debug, Clone)] +pub struct HirFunction<'a> { + pub loc: Option, + pub id: Option, + pub name_hint: Option, + pub fn_type: ReactFunctionType, + pub params: Vec, + pub return_type_annotation: Option, + pub returns: Place, + pub context: Vec, + pub body: HIR, + pub instructions: Vec>, + pub generator: bool, + pub is_async: bool, + pub directives: Vec, + pub aliasing_effects: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReactFunctionType { + Component, + Hook, + Other, +} + +#[derive(Debug, Clone)] +pub enum ParamPattern { + Place(Place), + Spread(SpreadPattern), +} + +/// The HIR control-flow graph +#[derive(Debug, Clone)] +pub struct HIR { + pub entry: BlockId, + pub blocks: FxIndexMap, +} + +/// Block kinds +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlockKind { + Block, + Value, + Loop, + Sequence, + Catch, +} + +impl std::fmt::Display for BlockKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BlockKind::Block => write!(f, "block"), + BlockKind::Value => write!(f, "value"), + BlockKind::Loop => write!(f, "loop"), + BlockKind::Sequence => write!(f, "sequence"), + BlockKind::Catch => write!(f, "catch"), + } + } +} + +/// A basic block in the CFG +#[derive(Debug, Clone)] +pub struct BasicBlock { + pub kind: BlockKind, + pub id: BlockId, + pub instructions: Vec, + pub terminal: Terminal, + pub preds: FxIndexSet, + pub phis: Vec, +} + +/// Phi node for SSA +#[derive(Debug, Clone)] +pub struct Phi { + pub place: Place, + pub operands: FxIndexMap, +} + +// ============================================================================= +// Terminal enum +// ============================================================================= + +#[derive(Debug, Clone)] +pub enum Terminal { + Unsupported { + id: EvaluationOrder, + loc: Option, + }, + Unreachable { + id: EvaluationOrder, + loc: Option, + }, + Throw { + value: Place, + id: EvaluationOrder, + loc: Option, + }, + Return { + value: Place, + return_variant: ReturnVariant, + id: EvaluationOrder, + loc: Option, + effects: Option>, + }, + Goto { + block: BlockId, + variant: GotoVariant, + id: EvaluationOrder, + loc: Option, + }, + If { + test: Place, + consequent: BlockId, + alternate: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + Branch { + test: Place, + consequent: BlockId, + alternate: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + Switch { + test: Place, + cases: Vec, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + DoWhile { + loop_block: BlockId, + test: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + While { + test: BlockId, + loop_block: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + For { + init: BlockId, + test: BlockId, + update: Option, + loop_block: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + ForOf { + init: BlockId, + test: BlockId, + loop_block: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + ForIn { + init: BlockId, + loop_block: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + Logical { + operator: LogicalOperator, + test: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + Ternary { + test: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + Optional { + optional: bool, + test: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + Label { + block: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + Sequence { + block: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + MaybeThrow { + continuation: BlockId, + handler: Option, + id: EvaluationOrder, + loc: Option, + effects: Option>, + }, + Try { + block: BlockId, + handler_binding: Option, + handler: BlockId, + fallthrough: BlockId, + id: EvaluationOrder, + loc: Option, + }, + Scope { + fallthrough: BlockId, + block: BlockId, + scope: ScopeId, + id: EvaluationOrder, + loc: Option, + }, + PrunedScope { + fallthrough: BlockId, + block: BlockId, + scope: ScopeId, + id: EvaluationOrder, + loc: Option, + }, +} + +impl Terminal { + /// Get the evaluation order of this terminal + pub fn evaluation_order(&self) -> EvaluationOrder { + match self { + Terminal::Unsupported { id, .. } + | Terminal::Unreachable { id, .. } + | Terminal::Throw { id, .. } + | Terminal::Return { id, .. } + | Terminal::Goto { id, .. } + | Terminal::If { id, .. } + | Terminal::Branch { id, .. } + | Terminal::Switch { id, .. } + | Terminal::DoWhile { id, .. } + | Terminal::While { id, .. } + | Terminal::For { id, .. } + | Terminal::ForOf { id, .. } + | Terminal::ForIn { id, .. } + | Terminal::Logical { id, .. } + | Terminal::Ternary { id, .. } + | Terminal::Optional { id, .. } + | Terminal::Label { id, .. } + | Terminal::Sequence { id, .. } + | Terminal::MaybeThrow { id, .. } + | Terminal::Try { id, .. } + | Terminal::Scope { id, .. } + | Terminal::PrunedScope { id, .. } => *id, + } + } + + /// Get the source location of this terminal + pub fn loc(&self) -> Option<&SourceLocation> { + match self { + Terminal::Unsupported { loc, .. } + | Terminal::Unreachable { loc, .. } + | Terminal::Throw { loc, .. } + | Terminal::Return { loc, .. } + | Terminal::Goto { loc, .. } + | Terminal::If { loc, .. } + | Terminal::Branch { loc, .. } + | Terminal::Switch { loc, .. } + | Terminal::DoWhile { loc, .. } + | Terminal::While { loc, .. } + | Terminal::For { loc, .. } + | Terminal::ForOf { loc, .. } + | Terminal::ForIn { loc, .. } + | Terminal::Logical { loc, .. } + | Terminal::Ternary { loc, .. } + | Terminal::Optional { loc, .. } + | Terminal::Label { loc, .. } + | Terminal::Sequence { loc, .. } + | Terminal::MaybeThrow { loc, .. } + | Terminal::Try { loc, .. } + | Terminal::Scope { loc, .. } + | Terminal::PrunedScope { loc, .. } => loc.as_ref(), + } + } + + /// Set the evaluation order of this terminal + pub fn set_evaluation_order(&mut self, new_id: EvaluationOrder) { + match self { + Terminal::Unsupported { id, .. } + | Terminal::Unreachable { id, .. } + | Terminal::Throw { id, .. } + | Terminal::Return { id, .. } + | Terminal::Goto { id, .. } + | Terminal::If { id, .. } + | Terminal::Branch { id, .. } + | Terminal::Switch { id, .. } + | Terminal::DoWhile { id, .. } + | Terminal::While { id, .. } + | Terminal::For { id, .. } + | Terminal::ForOf { id, .. } + | Terminal::ForIn { id, .. } + | Terminal::Logical { id, .. } + | Terminal::Ternary { id, .. } + | Terminal::Optional { id, .. } + | Terminal::Label { id, .. } + | Terminal::Sequence { id, .. } + | Terminal::MaybeThrow { id, .. } + | Terminal::Try { id, .. } + | Terminal::Scope { id, .. } + | Terminal::PrunedScope { id, .. } => *id = new_id, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReturnVariant { + Void, + Implicit, + Explicit, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GotoVariant { + Break, + Continue, + Try, +} + +#[derive(Debug, Clone)] +pub struct Case { + pub test: Option, + pub block: BlockId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogicalOperator { + And, + Or, + NullishCoalescing, +} + +impl std::fmt::Display for LogicalOperator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LogicalOperator::And => write!(f, "&&"), + LogicalOperator::Or => write!(f, "||"), + LogicalOperator::NullishCoalescing => write!(f, "??"), + } + } +} + +// ============================================================================= +// Instruction types +// ============================================================================= + +#[derive(Debug, Clone)] +pub struct Instruction<'a> { + pub id: EvaluationOrder, + pub lvalue: Place, + pub value: InstructionValue<'a>, + pub loc: Option, + pub effects: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstructionKind { + Const, + Let, + Reassign, + Catch, + HoistedConst, + HoistedLet, + HoistedFunction, + Function, +} + +#[derive(Debug, Clone)] +pub struct LValue { + pub place: Place, + pub kind: InstructionKind, +} + +#[derive(Debug, Clone)] +pub struct LValuePattern { + pub pattern: Pattern, + pub kind: InstructionKind, +} + +#[derive(Debug, Clone)] +pub enum Pattern { + Array(ArrayPattern), + Object(ObjectPattern), +} + +// ============================================================================= +// InstructionValue enum +// ============================================================================= + +#[derive(Debug, Clone)] +pub enum InstructionValue<'a> { + LoadLocal { + place: Place, + loc: Option, + }, + LoadContext { + place: Place, + loc: Option, + }, + DeclareLocal { + lvalue: LValue, + type_annotation: Option, + loc: Option, + }, + DeclareContext { + lvalue: LValue, + loc: Option, + }, + StoreLocal { + lvalue: LValue, + value: Place, + type_annotation: Option, + loc: Option, + }, + StoreContext { + lvalue: LValue, + value: Place, + loc: Option, + }, + Destructure { + lvalue: LValuePattern, + value: Place, + loc: Option, + }, + Primitive { + value: PrimitiveValue, + loc: Option, + }, + JSXText { + value: String, + loc: Option, + }, + BinaryExpression { + operator: BinaryOperator, + left: Place, + right: Place, + loc: Option, + }, + NewExpression { + callee: Place, + args: Vec, + loc: Option, + }, + CallExpression { + callee: Place, + args: Vec, + loc: Option, + }, + MethodCall { + receiver: Place, + property: Place, + args: Vec, + loc: Option, + }, + UnaryExpression { + operator: UnaryOperator, + value: Place, + loc: Option, + }, + TypeCastExpression { + value: Place, + type_: Type, + type_annotation_name: Option, + type_annotation_kind: Option, + /// The original AST type annotation subtree, preserved for codegen, which + /// re-emits it by cloning into the output allocator (and applying any + /// identifier renames recorded on the environment). + type_annotation: Option<&'a oxc::TSType<'a>>, + loc: Option, + }, + JsxExpression { + tag: JsxTag, + props: Vec, + children: Option>, + loc: Option, + opening_loc: Option, + closing_loc: Option, + }, + ObjectExpression { + properties: Vec, + loc: Option, + }, + ObjectMethod { + loc: Option, + lowered_func: LoweredFunction, + }, + ArrayExpression { + elements: Vec, + loc: Option, + }, + JsxFragment { + children: Vec, + loc: Option, + }, + RegExpLiteral { + pattern: String, + flags: String, + loc: Option, + }, + MetaProperty { + meta: String, + property: String, + loc: Option, + }, + PropertyStore { + object: Place, + property: PropertyLiteral, + value: Place, + loc: Option, + }, + PropertyLoad { + object: Place, + property: PropertyLiteral, + loc: Option, + }, + PropertyDelete { + object: Place, + property: PropertyLiteral, + loc: Option, + }, + ComputedStore { + object: Place, + property: Place, + value: Place, + loc: Option, + }, + ComputedLoad { + object: Place, + property: Place, + loc: Option, + }, + ComputedDelete { + object: Place, + property: Place, + loc: Option, + }, + LoadGlobal { + binding: NonLocalBinding, + loc: Option, + }, + StoreGlobal { + name: String, + value: Place, + loc: Option, + }, + FunctionExpression { + name: Option, + name_hint: Option, + lowered_func: LoweredFunction, + expr_type: FunctionExpressionType, + loc: Option, + }, + TaggedTemplateExpression { + tag: Place, + // Mirrors `TemplateLiteral`: `subexprs.len() == quasis.len() - 1`. + // Upstream's HIR models only a single quasi with no interpolation; the + // oxc port extends it to support `tag`-ed templates with `${...}` + // interpolations (a deliberate divergence from the TS reference). + quasis: Vec, + subexprs: Vec, + loc: Option, + }, + TemplateLiteral { + subexprs: Vec, + quasis: Vec, + loc: Option, + }, + Await { + value: Place, + loc: Option, + }, + GetIterator { + collection: Place, + loc: Option, + }, + IteratorNext { + iterator: Place, + collection: Place, + loc: Option, + }, + NextPropertyOf { + value: Place, + loc: Option, + }, + PrefixUpdate { + lvalue: Place, + operation: UpdateOperator, + value: Place, + loc: Option, + }, + PostfixUpdate { + lvalue: Place, + operation: UpdateOperator, + value: Place, + loc: Option, + }, + Debugger { + loc: Option, + }, + /// A statement preserved verbatim from the source and re-emitted at this + /// position (e.g. an inline TS `enum`, which has runtime semantics). Holds + /// the borrowed oxc node; codegen clones it into the output allocator. The + /// original captured this via `UnsupportedNode`. Value-less, like `Debugger`. + PassthroughStatement { + stmt: &'a oxc::Statement<'a>, + loc: Option, + }, + StartMemoize { + manual_memo_id: u32, + deps: Option>, + deps_loc: Option>, + has_invalid_deps: bool, + loc: Option, + }, + FinishMemoize { + manual_memo_id: u32, + decl: Place, + pruned: bool, + loc: Option, + }, +} + +impl<'a> InstructionValue<'a> { + pub fn loc(&self) -> Option<&SourceLocation> { + match self { + InstructionValue::LoadLocal { loc, .. } + | InstructionValue::LoadContext { loc, .. } + | InstructionValue::DeclareLocal { loc, .. } + | InstructionValue::DeclareContext { loc, .. } + | InstructionValue::StoreLocal { loc, .. } + | InstructionValue::StoreContext { loc, .. } + | InstructionValue::Destructure { loc, .. } + | InstructionValue::Primitive { loc, .. } + | InstructionValue::JSXText { loc, .. } + | InstructionValue::BinaryExpression { loc, .. } + | InstructionValue::NewExpression { loc, .. } + | InstructionValue::CallExpression { loc, .. } + | InstructionValue::MethodCall { loc, .. } + | InstructionValue::UnaryExpression { loc, .. } + | InstructionValue::TypeCastExpression { loc, .. } + | InstructionValue::JsxExpression { loc, .. } + | InstructionValue::ObjectExpression { loc, .. } + | InstructionValue::ObjectMethod { loc, .. } + | InstructionValue::ArrayExpression { loc, .. } + | InstructionValue::JsxFragment { loc, .. } + | InstructionValue::RegExpLiteral { loc, .. } + | InstructionValue::MetaProperty { loc, .. } + | InstructionValue::PropertyStore { loc, .. } + | InstructionValue::PropertyLoad { loc, .. } + | InstructionValue::PropertyDelete { loc, .. } + | InstructionValue::ComputedStore { loc, .. } + | InstructionValue::ComputedLoad { loc, .. } + | InstructionValue::ComputedDelete { loc, .. } + | InstructionValue::LoadGlobal { loc, .. } + | InstructionValue::StoreGlobal { loc, .. } + | InstructionValue::FunctionExpression { loc, .. } + | InstructionValue::TaggedTemplateExpression { loc, .. } + | InstructionValue::TemplateLiteral { loc, .. } + | InstructionValue::Await { loc, .. } + | InstructionValue::GetIterator { loc, .. } + | InstructionValue::IteratorNext { loc, .. } + | InstructionValue::NextPropertyOf { loc, .. } + | InstructionValue::PrefixUpdate { loc, .. } + | InstructionValue::PostfixUpdate { loc, .. } + | InstructionValue::Debugger { loc, .. } + | InstructionValue::PassthroughStatement { loc, .. } + | InstructionValue::StartMemoize { loc, .. } + | InstructionValue::FinishMemoize { loc, .. } => loc.as_ref(), + } + } +} + +// ============================================================================= +// Supporting types +// ============================================================================= + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PrimitiveValue { + Null, + Undefined, + Boolean(bool), + Number(FloatValue), + String(crate::react_compiler_diagnostics::JsString), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOperator { + Equal, + NotEqual, + StrictEqual, + StrictNotEqual, + LessThan, + LessEqual, + GreaterThan, + GreaterEqual, + ShiftLeft, + ShiftRight, + UnsignedShiftRight, + Add, + Subtract, + Multiply, + Divide, + Modulo, + Exponent, + BitwiseOr, + BitwiseXor, + BitwiseAnd, + In, + InstanceOf, +} + +impl std::fmt::Display for BinaryOperator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BinaryOperator::Equal => write!(f, "=="), + BinaryOperator::NotEqual => write!(f, "!="), + BinaryOperator::StrictEqual => write!(f, "==="), + BinaryOperator::StrictNotEqual => write!(f, "!=="), + BinaryOperator::LessThan => write!(f, "<"), + BinaryOperator::LessEqual => write!(f, "<="), + BinaryOperator::GreaterThan => write!(f, ">"), + BinaryOperator::GreaterEqual => write!(f, ">="), + BinaryOperator::ShiftLeft => write!(f, "<<"), + BinaryOperator::ShiftRight => write!(f, ">>"), + BinaryOperator::UnsignedShiftRight => write!(f, ">>>"), + BinaryOperator::Add => write!(f, "+"), + BinaryOperator::Subtract => write!(f, "-"), + BinaryOperator::Multiply => write!(f, "*"), + BinaryOperator::Divide => write!(f, "/"), + BinaryOperator::Modulo => write!(f, "%"), + BinaryOperator::Exponent => write!(f, "**"), + BinaryOperator::BitwiseOr => write!(f, "|"), + BinaryOperator::BitwiseXor => write!(f, "^"), + BinaryOperator::BitwiseAnd => write!(f, "&"), + BinaryOperator::In => write!(f, "in"), + BinaryOperator::InstanceOf => write!(f, "instanceof"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnaryOperator { + Minus, + Plus, + Not, + BitwiseNot, + TypeOf, + Void, +} + +impl std::fmt::Display for UnaryOperator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UnaryOperator::Minus => write!(f, "-"), + UnaryOperator::Plus => write!(f, "+"), + UnaryOperator::Not => write!(f, "!"), + UnaryOperator::BitwiseNot => write!(f, "~"), + UnaryOperator::TypeOf => write!(f, "typeof"), + UnaryOperator::Void => write!(f, "void"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpdateOperator { + Increment, + Decrement, +} + +impl std::fmt::Display for UpdateOperator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UpdateOperator::Increment => write!(f, "++"), + UpdateOperator::Decrement => write!(f, "--"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FunctionExpressionType { + ArrowFunctionExpression, + FunctionExpression, + FunctionDeclaration, +} + +#[derive(Debug, Clone)] +pub struct TemplateQuasi { + pub raw: String, + pub cooked: Option, +} + +#[derive(Debug, Clone)] +pub struct ManualMemoDependency { + pub root: ManualMemoDependencyRoot, + pub path: Vec, + pub loc: Option, +} + +#[derive(Debug, Clone)] +pub enum ManualMemoDependencyRoot { + NamedLocal { value: Place, constant: bool }, + Global { identifier_name: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DependencyPathEntry { + pub property: PropertyLiteral, + pub optional: bool, + pub loc: Option, +} + +// ============================================================================= +// Place, Identifier, and related types +// ============================================================================= + +#[derive(Debug, Clone)] +pub struct Place { + pub identifier: IdentifierId, + pub effect: Effect, + pub reactive: bool, + pub loc: Option, +} + +#[derive(Debug, Clone)] +pub struct Identifier { + pub id: IdentifierId, + pub declaration_id: DeclarationId, + pub name: Option, + pub mutable_range: MutableRange, + pub scope: Option, + pub type_: TypeId, + pub loc: Option, +} + +#[derive(Debug, Clone)] +pub struct MutableRange { + /// Unique identity for this logical range. Cloning preserves the ID + /// (same logical range); use `Environment::new_mutable_range()` to create + /// a range with a fresh ID. + pub id: MutableRangeId, + pub start: EvaluationOrder, + pub end: EvaluationOrder, +} + +impl MutableRange { + /// Returns true if the given evaluation order falls within this mutable range. + /// Corresponds to TS `inRange({id}, range)` / `isMutable(instr, place)`. + pub fn contains(&self, eval_order: EvaluationOrder) -> bool { + eval_order >= self.start && eval_order < self.end + } + + /// Returns true if this range has the same identity as `other`. + /// In the TS compiler, this corresponds to checking whether two mutableRange + /// references point to the same JS object (=== identity). + pub fn same_range(&self, other: &MutableRange) -> bool { + self.id == other.id + } +} + +#[derive(Debug, Clone)] +pub enum IdentifierName { + Named(String), + Promoted(String), +} + +impl IdentifierName { + pub fn value(&self) -> &str { + match self { + IdentifierName::Named(v) | IdentifierName::Promoted(v) => v, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Effect { + Unknown, + Freeze, + Read, + Capture, + ConditionallyMutateIterator, + ConditionallyMutate, + Mutate, + Store, +} + +impl Effect { + /// Returns true if this effect represents a mutable operation. + /// Mutable effects are: Capture, Store, ConditionallyMutate, + /// ConditionallyMutateIterator, and Mutate. + pub fn is_mutable(&self) -> bool { + matches!( + self, + Effect::Capture + | Effect::Store + | Effect::ConditionallyMutate + | Effect::ConditionallyMutateIterator + | Effect::Mutate + ) + } +} + +impl std::fmt::Display for Effect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Effect::Unknown => write!(f, ""), + Effect::Freeze => write!(f, "freeze"), + Effect::Read => write!(f, "read"), + Effect::Capture => write!(f, "capture"), + Effect::ConditionallyMutateIterator => write!(f, "mutate-iterator?"), + Effect::ConditionallyMutate => write!(f, "mutate?"), + Effect::Mutate => write!(f, "mutate"), + Effect::Store => write!(f, "store"), + } + } +} + +#[derive(Debug, Clone)] +pub struct SpreadPattern { + pub place: Place, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Hole { + Hole, +} + +#[derive(Debug, Clone)] +pub struct ArrayPattern { + pub items: Vec, + pub loc: Option, +} + +#[derive(Debug, Clone)] +pub enum ArrayPatternElement { + Place(Place), + Spread(SpreadPattern), + Hole, +} + +#[derive(Debug, Clone)] +pub struct ObjectPattern { + pub properties: Vec, + pub loc: Option, +} + +#[derive(Debug, Clone)] +pub enum ObjectPropertyOrSpread { + Property(ObjectProperty), + Spread(SpreadPattern), +} + +#[derive(Debug, Clone)] +pub struct ObjectProperty { + pub key: ObjectPropertyKey, + pub property_type: ObjectPropertyType, + pub place: Place, +} + +#[derive(Debug, Clone)] +pub enum ObjectPropertyKey { + String { name: String }, + Identifier { name: String }, + Computed { name: Place }, + Number { name: FloatValue }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ObjectPropertyType { + Property, + Method, +} + +impl std::fmt::Display for ObjectPropertyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ObjectPropertyType::Property => write!(f, "property"), + ObjectPropertyType::Method => write!(f, "method"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PropertyLiteral { + String(String), + Number(FloatValue), +} + +impl std::fmt::Display for PropertyLiteral { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PropertyLiteral::String(s) => write!(f, "{}", s), + PropertyLiteral::Number(n) => write!(f, "{}", n), + } + } +} + +#[derive(Debug, Clone)] +pub enum PlaceOrSpread { + Place(Place), + Spread(SpreadPattern), +} + +#[derive(Debug, Clone)] +pub enum ArrayElement { + Place(Place), + Spread(SpreadPattern), + Hole, +} + +#[derive(Debug, Clone)] +pub struct LoweredFunction { + pub func: FunctionId, +} + +#[derive(Debug, Clone)] +pub struct BuiltinTag { + pub name: String, + pub loc: Option, +} + +#[derive(Debug, Clone)] +pub enum JsxTag { + Place(Place), + Builtin(BuiltinTag), +} + +#[derive(Debug, Clone)] +pub enum JsxAttribute { + SpreadAttribute { argument: Place }, + Attribute { name: String, place: Place }, +} + +// ============================================================================= +// Variable Binding types +// ============================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BindingKind { + Var, + Let, + Const, + Param, + Module, + Hoisted, + Local, + Unknown, +} + +#[derive(Debug, Clone)] +pub enum VariableBinding { + Identifier { identifier: IdentifierId, binding_kind: BindingKind }, + Global { name: String }, + ImportDefault { name: String, module: String }, + ImportSpecifier { name: String, module: String, imported: String }, + ImportNamespace { name: String, module: String }, + ModuleLocal { name: String }, +} + +#[derive(Debug, Clone)] +pub enum NonLocalBinding { + ImportDefault { name: String, module: String }, + ImportSpecifier { name: String, module: String, imported: String }, + ImportNamespace { name: String, module: String }, + ModuleLocal { name: String }, + Global { name: String }, +} + +impl NonLocalBinding { + /// Returns the `name` field common to all variants. + pub fn name(&self) -> &str { + match self { + NonLocalBinding::ImportDefault { name, .. } + | NonLocalBinding::ImportSpecifier { name, .. } + | NonLocalBinding::ImportNamespace { name, .. } + | NonLocalBinding::ModuleLocal { name, .. } + | NonLocalBinding::Global { name, .. } => name, + } + } +} + +// ============================================================================= +// Type system (from Types.ts) +// ============================================================================= + +#[derive(Debug, Clone)] +pub enum Type { + Primitive, + Function { shape_id: Option, return_type: Box, is_constructor: bool }, + Object { shape_id: Option }, + TypeVar { id: TypeId }, + Poly, + Phi { operands: Vec }, + Property { object_type: Box, object_name: String, property_name: PropertyNameKind }, + ObjectMethod, +} + +#[derive(Debug, Clone)] +pub enum PropertyNameKind { + Literal { value: PropertyLiteral }, + Computed { value: Box }, +} + +// ============================================================================= +// ReactiveScope +// ============================================================================= + +#[derive(Debug, Clone)] +pub struct ReactiveScope { + pub id: ScopeId, + pub range: MutableRange, + + /// The inputs to this reactive scope (populated by later passes) + pub dependencies: Vec, + + /// The set of values produced by this scope (populated by later passes) + pub declarations: Vec<(IdentifierId, ReactiveScopeDeclaration)>, + + /// Identifiers which are reassigned by this scope (populated by later passes) + pub reassignments: Vec, + + /// If the scope contains an early return, this stores info about it (populated by later passes) + pub early_return_value: Option, + + /// Scopes that were merged into this one (populated by later passes) + pub merged: Vec, + + /// Source location spanning the scope + pub loc: Option, +} + +/// A dependency of a reactive scope. +#[derive(Debug, Clone)] +pub struct ReactiveScopeDependency { + pub identifier: IdentifierId, + pub reactive: bool, + pub path: Vec, + pub loc: Option, +} + +/// A declaration produced by a reactive scope. +#[derive(Debug, Clone)] +pub struct ReactiveScopeDeclaration { + pub identifier: IdentifierId, + pub scope: ScopeId, +} + +/// Early return value info for a reactive scope. +#[derive(Debug, Clone)] +pub struct ReactiveScopeEarlyReturn { + pub value: IdentifierId, + pub loc: Option, + pub label: BlockId, +} + +// ============================================================================= +// Aliasing effects (runtime types, from AliasingEffects.ts) +// ============================================================================= + +use crate::react_compiler_hir::object_shape::FunctionSignature; +use crate::react_compiler_hir::type_config::ValueKind; +use crate::react_compiler_hir::type_config::ValueReason; + +/// Reason for a mutation, used for generating hints (e.g. rename to "Ref"). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MutationReason { + AssignCurrentProperty, +} + +/// Describes the aliasing/mutation/data-flow effects of an instruction or terminal. +/// Ported from TS `AliasingEffect` in `AliasingEffects.ts`. +#[derive(Debug, Clone)] +pub enum AliasingEffect { + /// Marks the given value and its direct aliases as frozen. + Freeze { value: Place, reason: ValueReason }, + /// Mutate the value and any direct aliases. + Mutate { value: Place, reason: Option }, + /// Mutate the value conditionally (only if mutable). + MutateConditionally { value: Place }, + /// Mutate the value and transitive captures. + MutateTransitive { value: Place }, + /// Mutate the value and transitive captures conditionally. + MutateTransitiveConditionally { value: Place }, + /// Information flow from `from` to `into` (non-aliasing capture). + Capture { from: Place, into: Place }, + /// Direct aliasing: mutation of `into` implies mutation of `from`. + Alias { from: Place, into: Place }, + /// Potential aliasing relationship. + MaybeAlias { from: Place, into: Place }, + /// Direct assignment: `into = from`. + Assign { from: Place, into: Place }, + /// Creates a value of the given kind at the given place. + Create { into: Place, value: ValueKind, reason: ValueReason }, + /// Creates a new value with the same kind as the source. + CreateFrom { from: Place, into: Place }, + /// Immutable data flow (escape analysis only, no mutable range influence). + ImmutableCapture { from: Place, into: Place }, + /// Function call application. + Apply { + receiver: Place, + function: Place, + mutates_function: bool, + args: Vec, + into: Place, + signature: Option, + loc: Option, + }, + /// Function expression creation with captures. + CreateFunction { captures: Vec, function_id: FunctionId, into: Place }, + /// Mutation of a value known to be frozen (error). + MutateFrozen { place: Place, error: CompilerDiagnostic }, + /// Mutation of a global value (error). + MutateGlobal { place: Place, error: CompilerDiagnostic }, + /// Side-effect not safe during render. + Impure { place: Place, error: CompilerDiagnostic }, + /// Value is accessed during render. + Render { place: Place }, +} + +/// Combined Place/Spread/Hole for Apply args. +#[derive(Debug, Clone)] +pub enum PlaceOrSpreadOrHole { + Place(Place), + Spread(SpreadPattern), + Hole, +} + +/// Aliasing signature for function calls. +/// Ported from TS `AliasingSignature` in `AliasingEffects.ts`. +#[derive(Debug, Clone)] +pub struct AliasingSignature { + pub receiver: IdentifierId, + pub params: Vec, + pub rest: Option, + pub returns: IdentifierId, + pub effects: Vec, + pub temporaries: Vec, +} + +// ============================================================================= +// Type helper functions (ported from HIR.ts) +// ============================================================================= + +use crate::react_compiler_hir::object_shape::BUILT_IN_ARRAY_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_JSX_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_MAP_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_PROPS_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_REF_VALUE_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_SET_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_USE_OPERATOR_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_USE_REF_ID; + +/// Returns true if the type (looked up via identifier) is primitive. +pub fn is_primitive_type(ty: &Type) -> bool { + matches!(ty, Type::Primitive) +} + +/// Returns true if the type is the props object. +pub fn is_props_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == BUILT_IN_PROPS_ID) +} + +/// Returns true if the type is an array. +pub fn is_array_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == BUILT_IN_ARRAY_ID) +} + +/// Returns true if the type is a Set. +pub fn is_set_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == BUILT_IN_SET_ID) +} + +/// Returns true if the type is a Map. +pub fn is_map_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == BUILT_IN_MAP_ID) +} + +/// Returns true if the type is JSX. +pub fn is_jsx_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == BUILT_IN_JSX_ID) +} + +/// Returns true if the identifier type is a ref value. +pub fn is_ref_value_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == BUILT_IN_REF_VALUE_ID) +} + +/// Returns true if the identifier type is useRef. +pub fn is_use_ref_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == BUILT_IN_USE_REF_ID) +} + +/// Returns true if the type is a ref or ref value. +pub fn is_ref_or_ref_value(ty: &Type) -> bool { + is_use_ref_type(ty) || is_ref_value_type(ty) +} + +/// Returns true if the type is a useState result (BuiltInUseState). +pub fn is_use_state_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == object_shape::BUILT_IN_USE_STATE_ID) +} + +/// Returns true if the type is a setState function (BuiltInSetState). +pub fn is_set_state_type(ty: &Type) -> bool { + matches!(ty, Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_SET_STATE_ID) +} + +/// Returns true if the type is a useEffect hook. +pub fn is_use_effect_hook_type(ty: &Type) -> bool { + matches!(ty, Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_USE_EFFECT_HOOK_ID) +} + +/// Returns true if the type is a useLayoutEffect hook. +pub fn is_use_layout_effect_hook_type(ty: &Type) -> bool { + matches!(ty, Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_USE_LAYOUT_EFFECT_HOOK_ID) +} + +/// Returns true if the type is a useInsertionEffect hook. +pub fn is_use_insertion_effect_hook_type(ty: &Type) -> bool { + matches!(ty, Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_USE_INSERTION_EFFECT_HOOK_ID) +} + +/// Returns true if the type is a useEffectEvent function. +pub fn is_use_effect_event_type(ty: &Type) -> bool { + matches!(ty, Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_USE_EFFECT_EVENT_ID) +} + +/// Returns true if the type is a ref or ref-like mutable type (e.g. Reanimated shared values). +pub fn is_ref_or_ref_like_mutable_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } + if id == object_shape::BUILT_IN_USE_REF_ID || id == object_shape::REANIMATED_SHARED_VALUE_ID) +} + +/// Returns true if the type is the `use()` operator (React.use). +pub fn is_use_operator_type(ty: &Type) -> bool { + matches!( + ty, + Type::Function { shape_id: Some(id), .. } + if id == BUILT_IN_USE_OPERATOR_ID + ) +} + +/// Returns true if the type is a plain object (BuiltInObject). +pub fn is_plain_object_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == object_shape::BUILT_IN_OBJECT_ID) +} + +/// Returns true if the type is a startTransition function (BuiltInStartTransition). +pub fn is_start_transition_type(ty: &Type) -> bool { + matches!(ty, Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_START_TRANSITION_ID) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_js_number() { + // Scientific notation for large numbers (>= 1e21) + assert_eq!(format_js_number(1e21), "1e+21"); + assert_eq!(format_js_number(1.5e21), "1.5e+21"); + assert_eq!(format_js_number(2.18739127891275e22), "2.18739127891275e+22"); + assert_eq!(format_js_number(1e100), "1e+100"); + assert_eq!(format_js_number(-1e21), "-1e+21"); + assert_eq!(format_js_number(-1e100), "-1e+100"); + + // Scientific notation for small numbers (< 1e-6) + assert_eq!(format_js_number(1e-7), "1e-7"); + assert_eq!(format_js_number(5e-7), "5e-7"); + assert_eq!(format_js_number(1.5e-8), "1.5e-8"); + assert_eq!(format_js_number(-1.5e-8), "-1.5e-8"); + + // Non-scientific large numbers (< 1e21) + assert_eq!(format_js_number(1e20), "100000000000000000000"); + assert_eq!(format_js_number(1e-6), "0.000001"); + + // Integers + assert_eq!(format_js_number(0.0), "0"); + assert_eq!(format_js_number(-0.0), "0"); + assert_eq!(format_js_number(1.0), "1"); + assert_eq!(format_js_number(100.0), "100"); + + // Regular floats + assert_eq!(format_js_number(1.5), "1.5"); + assert_eq!(format_js_number(0.5), "0.5"); + assert_eq!(format_js_number(0.1), "0.1"); + + // Special values + assert_eq!(format_js_number(f64::NAN), "NaN"); + assert_eq!(format_js_number(f64::INFINITY), "Infinity"); + assert_eq!(format_js_number(f64::NEG_INFINITY), "-Infinity"); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/object_shape.rs b/crates/oxc_react_compiler/src/react_compiler_hir/object_shape.rs new file mode 100644 index 0000000000000..16c19f0ad2c96 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/object_shape.rs @@ -0,0 +1,411 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Object shapes and function signatures, ported from ObjectShape.ts. +//! +//! Defines the shape registry used by Environment to resolve property types +//! and function call signatures for built-in objects, hooks, and user-defined types. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_hir::Effect; +use crate::react_compiler_hir::Type; +use crate::react_compiler_hir::type_config::{ + AliasingEffectConfig, AliasingSignatureConfig, ValueKind, ValueReason, +}; + +// ============================================================================= +// Shape ID constants (matching TS ObjectShape.ts) +// ============================================================================= + +pub const BUILT_IN_PROPS_ID: &str = "BuiltInProps"; +pub const BUILT_IN_ARRAY_ID: &str = "BuiltInArray"; +pub const BUILT_IN_SET_ID: &str = "BuiltInSet"; +pub const BUILT_IN_MAP_ID: &str = "BuiltInMap"; +pub const BUILT_IN_WEAK_SET_ID: &str = "BuiltInWeakSet"; +pub const BUILT_IN_WEAK_MAP_ID: &str = "BuiltInWeakMap"; +pub const BUILT_IN_FUNCTION_ID: &str = "BuiltInFunction"; +pub const BUILT_IN_JSX_ID: &str = "BuiltInJsx"; +pub const BUILT_IN_OBJECT_ID: &str = "BuiltInObject"; +pub const BUILT_IN_USE_STATE_ID: &str = "BuiltInUseState"; +pub const BUILT_IN_SET_STATE_ID: &str = "BuiltInSetState"; +pub const BUILT_IN_USE_ACTION_STATE_ID: &str = "BuiltInUseActionState"; +pub const BUILT_IN_SET_ACTION_STATE_ID: &str = "BuiltInSetActionState"; +pub const BUILT_IN_USE_REF_ID: &str = "BuiltInUseRefId"; +pub const BUILT_IN_REF_VALUE_ID: &str = "BuiltInRefValue"; +pub const BUILT_IN_MIXED_READONLY_ID: &str = "BuiltInMixedReadonly"; +pub const BUILT_IN_USE_EFFECT_HOOK_ID: &str = "BuiltInUseEffectHook"; +pub const BUILT_IN_USE_LAYOUT_EFFECT_HOOK_ID: &str = "BuiltInUseLayoutEffectHook"; +pub const BUILT_IN_USE_INSERTION_EFFECT_HOOK_ID: &str = "BuiltInUseInsertionEffectHook"; +pub const BUILT_IN_USE_OPERATOR_ID: &str = "BuiltInUseOperator"; +pub const BUILT_IN_USE_REDUCER_ID: &str = "BuiltInUseReducer"; +pub const BUILT_IN_DISPATCH_ID: &str = "BuiltInDispatch"; +pub const BUILT_IN_USE_CONTEXT_HOOK_ID: &str = "BuiltInUseContextHook"; +pub const BUILT_IN_USE_TRANSITION_ID: &str = "BuiltInUseTransition"; +pub const BUILT_IN_USE_OPTIMISTIC_ID: &str = "BuiltInUseOptimistic"; +pub const BUILT_IN_SET_OPTIMISTIC_ID: &str = "BuiltInSetOptimistic"; +pub const BUILT_IN_START_TRANSITION_ID: &str = "BuiltInStartTransition"; +pub const BUILT_IN_USE_EFFECT_EVENT_ID: &str = "BuiltInUseEffectEvent"; +pub const BUILT_IN_EFFECT_EVENT_ID: &str = "BuiltInEffectEventFunction"; +pub const REANIMATED_SHARED_VALUE_ID: &str = "ReanimatedSharedValueId"; + +// ============================================================================= +// Core types +// ============================================================================= + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HookKind { + UseContext, + UseState, + UseActionState, + UseReducer, + UseRef, + UseEffect, + UseLayoutEffect, + UseInsertionEffect, + UseMemo, + UseCallback, + UseTransition, + UseImperativeHandle, + UseEffectEvent, + UseOptimistic, + Custom, +} + +impl std::fmt::Display for HookKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HookKind::UseContext => write!(f, "useContext"), + HookKind::UseState => write!(f, "useState"), + HookKind::UseActionState => write!(f, "useActionState"), + HookKind::UseReducer => write!(f, "useReducer"), + HookKind::UseRef => write!(f, "useRef"), + HookKind::UseEffect => write!(f, "useEffect"), + HookKind::UseLayoutEffect => write!(f, "useLayoutEffect"), + HookKind::UseInsertionEffect => write!(f, "useInsertionEffect"), + HookKind::UseMemo => write!(f, "useMemo"), + HookKind::UseCallback => write!(f, "useCallback"), + HookKind::UseTransition => write!(f, "useTransition"), + HookKind::UseImperativeHandle => write!(f, "useImperativeHandle"), + HookKind::UseEffectEvent => write!(f, "useEffectEvent"), + HookKind::UseOptimistic => write!(f, "useOptimistic"), + HookKind::Custom => write!(f, "Custom"), + } + } +} + +/// Call signature of a function, used for type and effect inference. +/// Ported from TS `FunctionSignature`. +#[derive(Debug, Clone)] +pub struct FunctionSignature { + pub positional_params: Vec, + pub rest_param: Option, + pub return_type: Type, + pub return_value_kind: ValueKind, + pub return_value_reason: Option, + pub callee_effect: Effect, + pub hook_kind: Option, + pub no_alias: bool, + pub mutable_only_if_operands_are_mutable: bool, + pub impure: bool, + pub known_incompatible: Option, + pub canonical_name: Option, + /// Aliasing signature in config form. Full parsing into AliasingSignature + /// with Place values is deferred until the aliasing effects system is ported. + pub aliasing: Option, +} + +/// Shape of an object or function type. +/// Ported from TS `ObjectShape`. +#[derive(Debug, Clone)] +pub struct ObjectShape { + pub properties: FxHashMap, + pub function_type: Option, +} + +/// Registry mapping shape IDs to their ObjectShape definitions. +/// +/// Supports two modes: +/// - **Builder mode** (`base=None`): wraps a single FxHashMap, used during +/// `build_builtin_shapes` / `build_default_globals` to construct the static base. +/// - **Overlay mode** (`base=Some`): holds a `&'static FxHashMap` base plus a small +/// extras FxHashMap. Lookups check extras first, then base. Inserts go into extras. +/// Cloning only copies the extras map (the base pointer is shared). +pub struct ShapeRegistry { + base: Option<&'static FxHashMap>, + entries: FxHashMap, +} + +impl ShapeRegistry { + /// Create an empty builder-mode registry. + pub fn new() -> Self { + Self { base: None, entries: FxHashMap::default() } + } + + /// Create an overlay-mode registry backed by a static base. + pub fn with_base(base: &'static FxHashMap) -> Self { + Self { base: Some(base), entries: FxHashMap::default() } + } + + pub fn get(&self, key: &str) -> Option<&ObjectShape> { + self.entries.get(key).or_else(|| self.base.and_then(|b| b.get(key))) + } + + pub fn insert(&mut self, key: String, value: ObjectShape) { + self.entries.insert(key, value); + } + + /// Consume the registry and return the inner FxHashMap. + /// Only valid in builder mode (no base). + pub fn into_inner(self) -> FxHashMap { + debug_assert!(self.base.is_none(), "into_inner() called on overlay-mode ShapeRegistry"); + self.entries + } +} + +impl Clone for ShapeRegistry { + fn clone(&self) -> Self { + Self { base: self.base, entries: self.entries.clone() } + } +} + +// ============================================================================= +// Counter for anonymous shape IDs +// ============================================================================= + +/// Thread-local counter for generating unique anonymous shape IDs. +/// Mirrors TS `nextAnonId` in ObjectShape.ts. +fn next_anon_id() -> String { + use std::sync::atomic::{AtomicU32, Ordering}; + static COUNTER: AtomicU32 = AtomicU32::new(0); + let id = COUNTER.fetch_add(1, Ordering::Relaxed); + format!("", id) +} + +// ============================================================================= +// Builder functions (matching TS addFunction, addHook, addObject) +// ============================================================================= + +/// Add a non-hook function to a ShapeRegistry. +/// Returns a `Type::Function` representing the added function. +pub fn add_function( + registry: &mut ShapeRegistry, + properties: Vec<(String, Type)>, + sig: FunctionSignatureBuilder, + id: Option<&str>, + is_constructor: bool, +) -> Type { + let shape_id = id.map(|s| s.to_string()).unwrap_or_else(next_anon_id); + let return_type = sig.return_type.clone(); + add_shape( + registry, + &shape_id, + properties, + Some(FunctionSignature { + positional_params: sig.positional_params, + rest_param: sig.rest_param, + return_type: sig.return_type, + return_value_kind: sig.return_value_kind, + return_value_reason: sig.return_value_reason, + callee_effect: sig.callee_effect, + hook_kind: None, + no_alias: sig.no_alias, + mutable_only_if_operands_are_mutable: sig.mutable_only_if_operands_are_mutable, + impure: sig.impure, + known_incompatible: sig.known_incompatible, + canonical_name: sig.canonical_name, + aliasing: sig.aliasing, + }), + ); + Type::Function { shape_id: Some(shape_id), return_type: Box::new(return_type), is_constructor } +} + +/// Add a hook to a ShapeRegistry. +/// Returns a `Type::Function` representing the added hook. +pub fn add_hook(registry: &mut ShapeRegistry, sig: HookSignatureBuilder, id: Option<&str>) -> Type { + let shape_id = id.map(|s| s.to_string()).unwrap_or_else(next_anon_id); + let return_type = sig.return_type.clone(); + add_shape( + registry, + &shape_id, + Vec::new(), + Some(FunctionSignature { + positional_params: sig.positional_params, + rest_param: sig.rest_param, + return_type: sig.return_type, + return_value_kind: sig.return_value_kind, + return_value_reason: sig.return_value_reason, + callee_effect: sig.callee_effect, + hook_kind: Some(sig.hook_kind), + no_alias: sig.no_alias, + mutable_only_if_operands_are_mutable: false, + impure: false, + known_incompatible: sig.known_incompatible, + canonical_name: None, + aliasing: sig.aliasing, + }), + ); + Type::Function { + shape_id: Some(shape_id), + return_type: Box::new(return_type), + is_constructor: false, + } +} + +/// Add an object to a ShapeRegistry. +/// Returns a `Type::Object` representing the added object. +pub fn add_object( + registry: &mut ShapeRegistry, + id: Option<&str>, + properties: Vec<(String, Type)>, +) -> Type { + let shape_id = id.map(|s| s.to_string()).unwrap_or_else(next_anon_id); + add_shape(registry, &shape_id, properties, None); + Type::Object { shape_id: Some(shape_id) } +} + +fn add_shape( + registry: &mut ShapeRegistry, + id: &str, + properties: Vec<(String, Type)>, + function_type: Option, +) { + let shape = ObjectShape { properties: properties.into_iter().collect(), function_type }; + // Note: TS has an invariant that the id doesn't already exist. We use + // insert which overwrites. In practice duplicates don't occur for built-in + // shapes, and for user configs we want last-write-wins behavior. + registry.insert(id.to_string(), shape); +} + +// ============================================================================= +// Builder structs (to avoid large parameter lists) +// ============================================================================= + +/// Builder for non-hook function signatures. +pub struct FunctionSignatureBuilder { + pub positional_params: Vec, + pub rest_param: Option, + pub return_type: Type, + pub return_value_kind: ValueKind, + pub return_value_reason: Option, + pub callee_effect: Effect, + pub no_alias: bool, + pub mutable_only_if_operands_are_mutable: bool, + pub impure: bool, + pub known_incompatible: Option, + pub canonical_name: Option, + pub aliasing: Option, +} + +impl Default for FunctionSignatureBuilder { + fn default() -> Self { + Self { + positional_params: Vec::new(), + rest_param: None, + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + return_value_reason: None, + callee_effect: Effect::Read, + no_alias: false, + mutable_only_if_operands_are_mutable: false, + impure: false, + known_incompatible: None, + canonical_name: None, + aliasing: None, + } + } +} + +/// Builder for hook signatures. +pub struct HookSignatureBuilder { + pub positional_params: Vec, + pub rest_param: Option, + pub return_type: Type, + pub return_value_kind: ValueKind, + pub return_value_reason: Option, + pub callee_effect: Effect, + pub hook_kind: HookKind, + pub no_alias: bool, + pub known_incompatible: Option, + pub aliasing: Option, +} + +impl Default for HookSignatureBuilder { + fn default() -> Self { + Self { + positional_params: Vec::new(), + rest_param: None, + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + return_value_reason: None, + callee_effect: Effect::Read, + hook_kind: HookKind::Custom, + no_alias: false, + known_incompatible: None, + aliasing: None, + } + } +} + +// ============================================================================= +// Default hook types used for unknown hooks +// ============================================================================= + +/// Default type for hooks when enableAssumeHooksFollowRulesOfReact is true. +/// Matches TS `DefaultNonmutatingHook`. +pub fn default_nonmutating_hook(registry: &mut ShapeRegistry) -> Type { + add_hook( + registry, + HookSignatureBuilder { + rest_param: Some(Effect::Freeze), + return_type: Type::Poly, + return_value_kind: ValueKind::Frozen, + hook_kind: HookKind::Custom, + aliasing: Some(AliasingSignatureConfig { + receiver: "@receiver".to_string(), + params: Vec::new(), + rest: Some("@rest".to_string()), + returns: "@returns".to_string(), + temporaries: Vec::new(), + effects: vec![ + // Freeze the arguments + AliasingEffectConfig::Freeze { + value: "@rest".to_string(), + reason: ValueReason::HookCaptured, + }, + // Returns a frozen value + AliasingEffectConfig::Create { + into: "@returns".to_string(), + value: ValueKind::Frozen, + reason: ValueReason::HookReturn, + }, + // May alias any arguments into the return + AliasingEffectConfig::Alias { + from: "@rest".to_string(), + into: "@returns".to_string(), + }, + ], + }), + ..Default::default() + }, + Some("DefaultNonmutatingHook"), + ) +} + +/// Default type for hooks when enableAssumeHooksFollowRulesOfReact is false. +/// Matches TS `DefaultMutatingHook`. +pub fn default_mutating_hook(registry: &mut ShapeRegistry) -> Type { + add_hook( + registry, + HookSignatureBuilder { + rest_param: Some(Effect::ConditionallyMutate), + return_type: Type::Poly, + return_value_kind: ValueKind::Mutable, + hook_kind: HookKind::Custom, + ..Default::default() + }, + Some("DefaultMutatingHook"), + ) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/print.rs b/crates/oxc_react_compiler/src/react_compiler_hir/print.rs new file mode 100644 index 0000000000000..5f31c2c64cc6c --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/print.rs @@ -0,0 +1,1405 @@ +//! Shared formatting utilities for HIR debug printing. +//! +//! This module provides `PrintFormatter` — a stateful formatter that both +//! `crate::react_compiler::debug_print` (HIR printer) and +//! `crate::react_compiler_reactive_scopes::print_reactive_function` (reactive printer) +//! delegate to for shared formatting logic. +//! +//! It also exports standalone formatting functions (format_loc, format_primitive, etc.) +//! that require no state. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_diagnostics::CompilerErrorOrDiagnostic; +use crate::react_compiler_diagnostics::SourceLocation; + +use crate::react_compiler_hir::AliasingEffect; +use crate::react_compiler_hir::HirFunction; +use crate::react_compiler_hir::IdentifierId; +use crate::react_compiler_hir::IdentifierName; +use crate::react_compiler_hir::InstructionValue; +use crate::react_compiler_hir::LValue; +use crate::react_compiler_hir::MutationReason; +use crate::react_compiler_hir::Pattern; +use crate::react_compiler_hir::Place; +use crate::react_compiler_hir::PlaceOrSpreadOrHole; +use crate::react_compiler_hir::ScopeId; +use crate::react_compiler_hir::Type; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::type_config::ValueKind; +use crate::react_compiler_hir::type_config::ValueReason; + +// ============================================================================= +// Standalone formatting functions (no state needed) +// ============================================================================= + +pub fn format_loc(loc: &Option) -> String { + match loc { + Some(l) => format_loc_value(l), + None => "generated".to_string(), + } +} + +pub fn format_loc_value(loc: &SourceLocation) -> String { + format!("{}:{}-{}:{}", loc.start.line, loc.start.column, loc.end.line, loc.end.column) +} + +/// Format a string like JS `JSON.stringify`: escape control chars and quotes +/// but preserve non-ASCII unicode (e.g. U+00A0 nbsp) as literal characters. +pub fn format_js_string(s: &str) -> String { + let mut result = String::with_capacity(s.len() + 2); + result.push('"'); + for c in s.chars() { + match c { + '"' => result.push_str("\\\""), + '\\' => result.push_str("\\\\"), + '\n' => result.push_str("\\n"), + '\r' => result.push_str("\\r"), + '\t' => result.push_str("\\t"), + '\u{0008}' => result.push_str("\\b"), + '\u{000c}' => result.push_str("\\f"), + // Only escape C0 control chars (U+0000–U+001F), matching JS JSON.stringify. + // Do NOT escape C1 controls (U+0080–U+009F) — JS outputs those as literal chars. + c if (c as u32) <= 0x1F => { + result.push_str(&format!("\\u{:04x}", c as u32)); + } + c => result.push(c), + } + } + result.push('"'); + result +} + +pub fn format_primitive(prim: &crate::react_compiler_hir::PrimitiveValue) -> String { + match prim { + crate::react_compiler_hir::PrimitiveValue::Null => "null".to_string(), + crate::react_compiler_hir::PrimitiveValue::Undefined => "undefined".to_string(), + crate::react_compiler_hir::PrimitiveValue::Boolean(b) => format!("{}", b), + crate::react_compiler_hir::PrimitiveValue::Number(n) => { + crate::react_compiler_hir::format_js_number(n.value()) + } + crate::react_compiler_hir::PrimitiveValue::String(s) => match s.as_str() { + Some(utf8) => format_js_string(utf8), + // Ill-formed strings: escape the well-formed segments exactly like + // format_js_string and render each unpaired surrogate as \uXXXX, + // matching what TS's JSON.stringify-based printer emits. + None => { + let mut result = String::new(); + result.push('"'); + let mut units = s.code_units().into_iter().peekable(); + while let Some(unit) = units.next() { + let is_lead = (0xD800..=0xDBFF).contains(&unit); + let is_trail = (0xDC00..=0xDFFF).contains(&unit); + if is_lead { + if let Some(&next) = units.peek() { + if (0xDC00..=0xDFFF).contains(&next) { + units.next(); + let cp = 0x10000 + + ((unit as u32 - 0xD800) << 10) + + (next as u32 - 0xDC00); + result.push(char::from_u32(cp).expect("valid supplementary")); + continue; + } + } + } + if is_lead || is_trail { + result.push_str(&format!("\\u{unit:04x}")); + continue; + } + let c = char::from_u32(unit as u32).expect("BMP non-surrogate is a char"); + match c { + '"' => result.push_str("\\\""), + '\\' => result.push_str("\\\\"), + '\n' => result.push_str("\\n"), + '\r' => result.push_str("\\r"), + '\t' => result.push_str("\\t"), + '\u{0008}' => result.push_str("\\b"), + '\u{000c}' => result.push_str("\\f"), + c if (c as u32) <= 0x1F => { + result.push_str(&format!("\\u{:04x}", c as u32)); + } + c => result.push(c), + } + } + result.push('"'); + result + } + }, + } +} + +pub fn format_property_literal(prop: &crate::react_compiler_hir::PropertyLiteral) -> String { + match prop { + crate::react_compiler_hir::PropertyLiteral::String(s) => s.clone(), + crate::react_compiler_hir::PropertyLiteral::Number(n) => { + crate::react_compiler_hir::format_js_number(n.value()) + } + } +} + +pub fn format_object_property_key(key: &crate::react_compiler_hir::ObjectPropertyKey) -> String { + match key { + crate::react_compiler_hir::ObjectPropertyKey::String { name } => { + format!("String(\"{}\")", name) + } + crate::react_compiler_hir::ObjectPropertyKey::Identifier { name } => { + format!("Identifier(\"{}\")", name) + } + crate::react_compiler_hir::ObjectPropertyKey::Computed { name } => { + format!("Computed({})", name.identifier.0) + } + crate::react_compiler_hir::ObjectPropertyKey::Number { name } => { + format!("Number({})", crate::react_compiler_hir::format_js_number(name.value())) + } + } +} + +pub fn format_non_local_binding(binding: &crate::react_compiler_hir::NonLocalBinding) -> String { + match binding { + crate::react_compiler_hir::NonLocalBinding::Global { name } => { + format!("Global {{ name: \"{}\" }}", name) + } + crate::react_compiler_hir::NonLocalBinding::ModuleLocal { name } => { + format!("ModuleLocal {{ name: \"{}\" }}", name) + } + crate::react_compiler_hir::NonLocalBinding::ImportDefault { name, module } => { + format!("ImportDefault {{ name: \"{}\", module: \"{}\" }}", name, module) + } + crate::react_compiler_hir::NonLocalBinding::ImportNamespace { name, module } => { + format!("ImportNamespace {{ name: \"{}\", module: \"{}\" }}", name, module) + } + crate::react_compiler_hir::NonLocalBinding::ImportSpecifier { name, module, imported } => { + format!( + "ImportSpecifier {{ name: \"{}\", module: \"{}\", imported: \"{}\" }}", + name, module, imported + ) + } + } +} + +pub fn format_value_kind(kind: ValueKind) -> &'static str { + match kind { + ValueKind::Mutable => "mutable", + ValueKind::Frozen => "frozen", + ValueKind::Primitive => "primitive", + ValueKind::MaybeFrozen => "maybe-frozen", + ValueKind::Global => "global", + ValueKind::Context => "context", + } +} + +pub fn format_value_reason(reason: ValueReason) -> &'static str { + match reason { + ValueReason::KnownReturnSignature => "known-return-signature", + ValueReason::State => "state", + ValueReason::ReducerState => "reducer-state", + ValueReason::Context => "context", + ValueReason::Effect => "effect", + ValueReason::HookCaptured => "hook-captured", + ValueReason::HookReturn => "hook-return", + ValueReason::Global => "global", + ValueReason::JsxCaptured => "jsx-captured", + ValueReason::StoreLocal => "store-local", + ValueReason::ReactiveFunctionArgument => "reactive-function-argument", + ValueReason::Other => "other", + } +} + +// ============================================================================= +// PrintFormatter — shared stateful formatter +// ============================================================================= + +/// Shared formatter state used by both HIR and reactive printers. +/// +/// Both `DebugPrinter` structs delegate to this for formatting shared constructs +/// like Places, Identifiers, Scopes, Types, InstructionValues, etc. +pub struct PrintFormatter<'a, 'h> { + pub env: &'a Environment<'h>, + pub seen_identifiers: FxHashSet, + pub seen_scopes: FxHashSet, + pub output: Vec, + pub indent_level: usize, +} + +impl<'a, 'h> PrintFormatter<'a, 'h> { + pub fn new(env: &'a Environment<'h>) -> Self { + Self { + env, + seen_identifiers: FxHashSet::default(), + seen_scopes: FxHashSet::default(), + output: Vec::new(), + indent_level: 0, + } + } + + pub fn line(&mut self, text: &str) { + let indent = " ".repeat(self.indent_level); + self.output.push(format!("{}{}", indent, text)); + } + + /// Write a line without adding indentation (used when copying pre-formatted output) + pub fn line_raw(&mut self, text: &str) { + self.output.push(text.to_string()); + } + + pub fn indent(&mut self) { + self.indent_level += 1; + } + + pub fn dedent(&mut self) { + self.indent_level -= 1; + } + + pub fn to_string_output(&self) -> String { + self.output.join("\n") + } + + // ========================================================================= + // AliasingEffect + // ========================================================================= + + pub fn format_effect(&self, effect: &AliasingEffect) -> String { + match effect { + AliasingEffect::Freeze { value, reason } => { + format!( + "Freeze {{ value: {}, reason: {} }}", + value.identifier.0, + format_value_reason(*reason) + ) + } + AliasingEffect::Mutate { value, reason } => match reason { + Some(MutationReason::AssignCurrentProperty) => { + format!( + "Mutate {{ value: {}, reason: AssignCurrentProperty }}", + value.identifier.0 + ) + } + None => format!("Mutate {{ value: {} }}", value.identifier.0), + }, + AliasingEffect::MutateConditionally { value } => { + format!("MutateConditionally {{ value: {} }}", value.identifier.0) + } + AliasingEffect::MutateTransitive { value } => { + format!("MutateTransitive {{ value: {} }}", value.identifier.0) + } + AliasingEffect::MutateTransitiveConditionally { value } => { + format!("MutateTransitiveConditionally {{ value: {} }}", value.identifier.0) + } + AliasingEffect::Capture { from, into } => { + format!("Capture {{ into: {}, from: {} }}", into.identifier.0, from.identifier.0) + } + AliasingEffect::Alias { from, into } => { + format!("Alias {{ into: {}, from: {} }}", into.identifier.0, from.identifier.0) + } + AliasingEffect::MaybeAlias { from, into } => { + format!("MaybeAlias {{ into: {}, from: {} }}", into.identifier.0, from.identifier.0) + } + AliasingEffect::Assign { from, into } => { + format!("Assign {{ into: {}, from: {} }}", into.identifier.0, from.identifier.0) + } + AliasingEffect::Create { into, value, reason } => { + format!( + "Create {{ into: {}, value: {}, reason: {} }}", + into.identifier.0, + format_value_kind(*value), + format_value_reason(*reason) + ) + } + AliasingEffect::CreateFrom { from, into } => { + format!("CreateFrom {{ into: {}, from: {} }}", into.identifier.0, from.identifier.0) + } + AliasingEffect::ImmutableCapture { from, into } => { + format!( + "ImmutableCapture {{ into: {}, from: {} }}", + into.identifier.0, from.identifier.0 + ) + } + AliasingEffect::Apply { receiver, function, mutates_function, args, into, .. } => { + let args_str: Vec = args + .iter() + .map(|a| match a { + PlaceOrSpreadOrHole::Hole => "hole".to_string(), + PlaceOrSpreadOrHole::Place(p) => p.identifier.0.to_string(), + PlaceOrSpreadOrHole::Spread(s) => format!("...{}", s.place.identifier.0), + }) + .collect(); + format!( + "Apply {{ into: {}, receiver: {}, function: {}, mutatesFunction: {}, args: [{}] }}", + into.identifier.0, + receiver.identifier.0, + function.identifier.0, + mutates_function, + args_str.join(", ") + ) + } + AliasingEffect::CreateFunction { captures, function_id: _, into } => { + let cap_str: Vec = + captures.iter().map(|p| p.identifier.0.to_string()).collect(); + format!( + "CreateFunction {{ into: {}, captures: [{}] }}", + into.identifier.0, + cap_str.join(", ") + ) + } + AliasingEffect::MutateFrozen { place, error } => { + format!( + "MutateFrozen {{ place: {}, reason: {:?} }}", + place.identifier.0, error.reason + ) + } + AliasingEffect::MutateGlobal { place, error } => { + format!( + "MutateGlobal {{ place: {}, reason: {:?} }}", + place.identifier.0, error.reason + ) + } + AliasingEffect::Impure { place, error } => { + format!("Impure {{ place: {}, reason: {:?} }}", place.identifier.0, error.reason) + } + AliasingEffect::Render { place } => { + format!("Render {{ place: {} }}", place.identifier.0) + } + } + } + + // ========================================================================= + // Place (with identifier deduplication) + // ========================================================================= + + pub fn format_place_field(&mut self, field_name: &str, place: &Place) { + let is_seen = self.seen_identifiers.contains(&place.identifier); + if is_seen { + self.line(&format!( + "{}: Place {{ identifier: Identifier({}), effect: {}, reactive: {}, loc: {} }}", + field_name, + place.identifier.0, + place.effect, + place.reactive, + format_loc(&place.loc) + )); + } else { + self.line(&format!("{}: Place {{", field_name)); + self.indent(); + self.line("identifier:"); + self.indent(); + self.format_identifier(place.identifier); + self.dedent(); + self.line(&format!("effect: {}", place.effect)); + self.line(&format!("reactive: {}", place.reactive)); + self.line(&format!("loc: {}", format_loc(&place.loc))); + self.dedent(); + self.line("}"); + } + } + + // ========================================================================= + // Identifier (first-seen expansion) + // ========================================================================= + + pub fn format_identifier(&mut self, id: IdentifierId) { + self.seen_identifiers.insert(id); + let ident = &self.env.identifiers[id.0 as usize]; + self.line("Identifier {"); + self.indent(); + self.line(&format!("id: {}", ident.id.0)); + self.line(&format!("declarationId: {}", ident.declaration_id.0)); + match &ident.name { + Some(name) => { + let (kind, value) = match name { + IdentifierName::Named(n) => ("named", n.as_str()), + IdentifierName::Promoted(n) => ("promoted", n.as_str()), + }; + self.line(&format!("name: {{ kind: \"{}\", value: \"{}\" }}", kind, value)); + } + None => self.line("name: null"), + } + // Print the identifier's mutable_range directly, matching the TS + // DebugPrintHIR which prints `identifier.mutableRange`. In TS, + // InferReactiveScopeVariables sets identifier.mutableRange = scope.range + // (shared reference), and AlignReactiveScopesToBlockScopesHIR syncs them. + // After MergeOverlappingReactiveScopesHIR repoints scopes, the TS + // identifier.mutableRange still references the OLD scope's range (stale), + // so we match by using ident.mutable_range directly (which is synced + // at the AlignReactiveScopesToBlockScopesHIR step but not re-synced + // after scope repointing in merge passes). + self.line(&format!( + "mutableRange: [{}:{}]", + ident.mutable_range.start.0, ident.mutable_range.end.0 + )); + match ident.scope { + Some(scope_id) => self.format_scope_field("scope", scope_id), + None => self.line("scope: null"), + } + self.line(&format!("type: {}", self.format_type(ident.type_))); + self.line(&format!("loc: {}", format_loc(&ident.loc))); + self.dedent(); + self.line("}"); + } + + // ========================================================================= + // Scope (with deduplication) + // ========================================================================= + + pub fn format_scope_field(&mut self, field_name: &str, scope_id: ScopeId) { + let is_seen = self.seen_scopes.contains(&scope_id); + if is_seen { + self.line(&format!("{}: Scope({})", field_name, scope_id.0)); + } else { + self.seen_scopes.insert(scope_id); + if let Some(scope) = self.env.scopes.iter().find(|s| s.id == scope_id) { + let range_start = scope.range.start.0; + let range_end = scope.range.end.0; + let dependencies = scope.dependencies.clone(); + let declarations = scope.declarations.clone(); + let reassignments = scope.reassignments.clone(); + let early_return_value = scope.early_return_value.clone(); + let merged = scope.merged.clone(); + let loc = scope.loc; + + self.line(&format!("{}: Scope {{", field_name)); + self.indent(); + self.line(&format!("id: {}", scope_id.0)); + self.line(&format!("range: [{}:{}]", range_start, range_end)); + + // dependencies + self.line("dependencies:"); + self.indent(); + for (i, dep) in dependencies.iter().enumerate() { + let path_str: String = dep + .path + .iter() + .map(|p| { + let prop = match &p.property { + crate::react_compiler_hir::PropertyLiteral::String(s) => s.clone(), + crate::react_compiler_hir::PropertyLiteral::Number(n) => { + crate::react_compiler_hir::format_js_number(n.value()) + } + }; + format!("{}{}", if p.optional { "?." } else { "." }, prop) + }) + .collect(); + self.line(&format!( + "[{}] {{ identifier: {}, reactive: {}, path: \"{}\" }}", + i, dep.identifier.0, dep.reactive, path_str + )); + } + self.dedent(); + + // declarations + self.line("declarations:"); + self.indent(); + for (ident_id, decl) in &declarations { + self.line(&format!( + "{}: {{ identifier: {}, scope: {} }}", + ident_id.0, decl.identifier.0, decl.scope.0 + )); + } + self.dedent(); + + // reassignments + self.line("reassignments:"); + self.indent(); + for ident_id in &reassignments { + self.line(&format!("{}", ident_id.0)); + } + self.dedent(); + + // earlyReturnValue + if let Some(early_return) = &early_return_value { + self.line("earlyReturnValue:"); + self.indent(); + self.line(&format!("value: {}", early_return.value.0)); + self.line(&format!("loc: {}", format_loc(&early_return.loc))); + self.line(&format!("label: bb{}", early_return.label.0)); + self.dedent(); + } else { + self.line("earlyReturnValue: null"); + } + + // merged + let merged_str: Vec = merged.iter().map(|s| s.0.to_string()).collect(); + self.line(&format!("merged: [{}]", merged_str.join(", "))); + + // loc + self.line(&format!("loc: {}", format_loc(&loc))); + + self.dedent(); + self.line("}"); + } else { + self.line(&format!("{}: Scope({})", field_name, scope_id.0)); + } + } + } + + // ========================================================================= + // Type + // ========================================================================= + + pub fn format_type(&self, type_id: crate::react_compiler_hir::TypeId) -> String { + if let Some(ty) = self.env.types.get(type_id.0 as usize) { + self.format_type_value(ty) + } else { + format!("Type({})", type_id.0) + } + } + + pub fn format_type_value(&self, ty: &Type) -> String { + match ty { + Type::Primitive => "Primitive".to_string(), + Type::Function { shape_id, return_type, is_constructor } => { + format!( + "Function {{ shapeId: {}, return: {}, isConstructor: {} }}", + match shape_id { + Some(s) => format!("\"{}\"", s), + None => "null".to_string(), + }, + self.format_type_value(return_type), + is_constructor + ) + } + Type::Object { shape_id } => { + format!( + "Object {{ shapeId: {} }}", + match shape_id { + Some(s) => format!("\"{}\"", s), + None => "null".to_string(), + } + ) + } + Type::TypeVar { id } => format!("Type({})", id.0), + Type::Poly => "Poly".to_string(), + Type::Phi { operands } => { + let ops: Vec = + operands.iter().map(|op| self.format_type_value(op)).collect(); + format!("Phi {{ operands: [{}] }}", ops.join(", ")) + } + Type::Property { object_type, object_name, property_name } => { + let prop_str = match property_name { + crate::react_compiler_hir::PropertyNameKind::Literal { value } => { + format!("\"{}\"", format_property_literal(value)) + } + crate::react_compiler_hir::PropertyNameKind::Computed { value } => { + format!("computed({})", self.format_type_value(value)) + } + }; + format!( + "Property {{ objectType: {}, objectName: \"{}\", propertyName: {} }}", + self.format_type_value(object_type), + object_name, + prop_str + ) + } + Type::ObjectMethod => "ObjectMethod".to_string(), + } + } + + // ========================================================================= + // LValue + // ========================================================================= + + pub fn format_lvalue(&mut self, field_name: &str, lv: &LValue) { + self.line(&format!("{}:", field_name)); + self.indent(); + self.line(&format!("kind: {:?}", lv.kind)); + self.format_place_field("place", &lv.place); + self.dedent(); + } + + // ========================================================================= + // Pattern + // ========================================================================= + + pub fn format_pattern(&mut self, pattern: &Pattern) { + match pattern { + Pattern::Array(arr) => { + self.line("pattern: ArrayPattern {"); + self.indent(); + self.line("items:"); + self.indent(); + for (i, item) in arr.items.iter().enumerate() { + match item { + crate::react_compiler_hir::ArrayPatternElement::Hole => { + self.line(&format!("[{}] Hole", i)); + } + crate::react_compiler_hir::ArrayPatternElement::Place(p) => { + self.format_place_field(&format!("[{}]", i), p); + } + crate::react_compiler_hir::ArrayPatternElement::Spread(s) => { + self.line(&format!("[{}] Spread:", i)); + self.indent(); + self.format_place_field("place", &s.place); + self.dedent(); + } + } + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(&arr.loc))); + self.dedent(); + self.line("}"); + } + Pattern::Object(obj) => { + self.line("pattern: ObjectPattern {"); + self.indent(); + self.line("properties:"); + self.indent(); + for (i, prop) in obj.properties.iter().enumerate() { + match prop { + crate::react_compiler_hir::ObjectPropertyOrSpread::Property(p) => { + self.line(&format!("[{}] ObjectProperty {{", i)); + self.indent(); + self.line(&format!("key: {}", format_object_property_key(&p.key))); + self.line(&format!("type: \"{}\"", p.property_type)); + self.format_place_field("place", &p.place); + self.dedent(); + self.line("}"); + } + crate::react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => { + self.line(&format!("[{}] Spread:", i)); + self.indent(); + self.format_place_field("place", &s.place); + self.dedent(); + } + } + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(&obj.loc))); + self.dedent(); + self.line("}"); + } + } + } + + // ========================================================================= + // Arguments + // ========================================================================= + + pub fn format_argument( + &mut self, + arg: &crate::react_compiler_hir::PlaceOrSpread, + index: usize, + ) { + match arg { + crate::react_compiler_hir::PlaceOrSpread::Place(p) => { + self.format_place_field(&format!("[{}]", index), p); + } + crate::react_compiler_hir::PlaceOrSpread::Spread(s) => { + self.line(&format!("[{}] Spread:", index)); + self.indent(); + self.format_place_field("place", &s.place); + self.dedent(); + } + } + } + + // ========================================================================= + // InstructionValue + // ========================================================================= + + /// Format an InstructionValue. The `inner_func_formatter` callback is invoked + /// for FunctionExpression/ObjectMethod to format the inner HirFunction. If None, + /// a placeholder is printed instead. + pub fn format_instruction_value( + &mut self, + value: &InstructionValue<'h>, + inner_func_formatter: Option<&dyn Fn(&mut PrintFormatter<'_, 'h>, &HirFunction<'h>)>, + ) { + match value { + InstructionValue::ArrayExpression { elements, loc } => { + self.line("ArrayExpression {"); + self.indent(); + self.line("elements:"); + self.indent(); + for (i, elem) in elements.iter().enumerate() { + match elem { + crate::react_compiler_hir::ArrayElement::Place(p) => { + self.format_place_field(&format!("[{}]", i), p); + } + crate::react_compiler_hir::ArrayElement::Hole => { + self.line(&format!("[{}] Hole", i)); + } + crate::react_compiler_hir::ArrayElement::Spread(s) => { + self.line(&format!("[{}] Spread:", i)); + self.indent(); + self.format_place_field("place", &s.place); + self.dedent(); + } + } + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::ObjectExpression { properties, loc } => { + self.line("ObjectExpression {"); + self.indent(); + self.line("properties:"); + self.indent(); + for (i, prop) in properties.iter().enumerate() { + match prop { + crate::react_compiler_hir::ObjectPropertyOrSpread::Property(p) => { + self.line(&format!("[{}] ObjectProperty {{", i)); + self.indent(); + self.line(&format!("key: {}", format_object_property_key(&p.key))); + self.line(&format!("type: \"{}\"", p.property_type)); + self.format_place_field("place", &p.place); + self.dedent(); + self.line("}"); + } + crate::react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => { + self.line(&format!("[{}] Spread:", i)); + self.indent(); + self.format_place_field("place", &s.place); + self.dedent(); + } + } + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::UnaryExpression { operator, value: val, loc } => { + self.line("UnaryExpression {"); + self.indent(); + self.line(&format!("operator: \"{}\"", operator)); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::BinaryExpression { operator, left, right, loc } => { + self.line("BinaryExpression {"); + self.indent(); + self.line(&format!("operator: \"{}\"", operator)); + self.format_place_field("left", left); + self.format_place_field("right", right); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::NewExpression { callee, args, loc } => { + self.line("NewExpression {"); + self.indent(); + self.format_place_field("callee", callee); + self.line("args:"); + self.indent(); + for (i, arg) in args.iter().enumerate() { + self.format_argument(arg, i); + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::CallExpression { callee, args, loc } => { + self.line("CallExpression {"); + self.indent(); + self.format_place_field("callee", callee); + self.line("args:"); + self.indent(); + for (i, arg) in args.iter().enumerate() { + self.format_argument(arg, i); + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::MethodCall { receiver, property, args, loc } => { + self.line("MethodCall {"); + self.indent(); + self.format_place_field("receiver", receiver); + self.format_place_field("property", property); + self.line("args:"); + self.indent(); + for (i, arg) in args.iter().enumerate() { + self.format_argument(arg, i); + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::JSXText { value: val, loc } => { + self.line(&format!( + "JSXText {{ value: {}, loc: {} }}", + format_js_string(val), + format_loc(loc) + )); + } + InstructionValue::Primitive { value: prim, loc } => { + self.line(&format!( + "Primitive {{ value: {}, loc: {} }}", + format_primitive(prim), + format_loc(loc) + )); + } + InstructionValue::TypeCastExpression { + value: val, + type_, + type_annotation_name, + type_annotation_kind, + type_annotation: _, + loc, + } => { + self.line("TypeCastExpression {"); + self.indent(); + self.format_place_field("value", val); + self.line(&format!("type: {}", self.format_type_value(type_))); + if let Some(annotation_name) = type_annotation_name { + self.line(&format!("typeAnnotation: {}", annotation_name)); + } + if let Some(annotation_kind) = type_annotation_kind { + self.line(&format!("typeAnnotationKind: \"{}\"", annotation_kind)); + } + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::JsxExpression { + tag, + props, + children, + loc, + opening_loc, + closing_loc, + } => { + self.line("JsxExpression {"); + self.indent(); + match tag { + crate::react_compiler_hir::JsxTag::Place(p) => { + self.format_place_field("tag", p); + } + crate::react_compiler_hir::JsxTag::Builtin(b) => { + self.line(&format!("tag: BuiltinTag(\"{}\")", b.name)); + } + } + self.line("props:"); + self.indent(); + for (i, prop) in props.iter().enumerate() { + match prop { + crate::react_compiler_hir::JsxAttribute::Attribute { name, place } => { + self.line(&format!("[{}] JsxAttribute {{", i)); + self.indent(); + self.line(&format!("name: \"{}\"", name)); + self.format_place_field("place", place); + self.dedent(); + self.line("}"); + } + crate::react_compiler_hir::JsxAttribute::SpreadAttribute { argument } => { + self.line(&format!("[{}] JsxSpreadAttribute:", i)); + self.indent(); + self.format_place_field("argument", argument); + self.dedent(); + } + } + } + self.dedent(); + match children { + Some(c) => { + self.line("children:"); + self.indent(); + for (i, child) in c.iter().enumerate() { + self.format_place_field(&format!("[{}]", i), child); + } + self.dedent(); + } + None => self.line("children: null"), + } + self.line(&format!("openingLoc: {}", format_loc(opening_loc))); + self.line(&format!("closingLoc: {}", format_loc(closing_loc))); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::JsxFragment { children, loc } => { + self.line("JsxFragment {"); + self.indent(); + self.line("children:"); + self.indent(); + for (i, child) in children.iter().enumerate() { + self.format_place_field(&format!("[{}]", i), child); + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::LoadLocal { place, loc } => { + self.line("LoadLocal {"); + self.indent(); + self.format_place_field("place", place); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::DeclareLocal { lvalue, type_annotation, loc } => { + self.line("DeclareLocal {"); + self.indent(); + self.format_lvalue("lvalue", lvalue); + self.line(&format!( + "type: {}", + match type_annotation { + Some(t) => t.clone(), + None => "null".to_string(), + } + )); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::DeclareContext { lvalue, loc } => { + self.line("DeclareContext {"); + self.indent(); + self.line("lvalue:"); + self.indent(); + self.line(&format!("kind: {:?}", lvalue.kind)); + self.format_place_field("place", &lvalue.place); + self.dedent(); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::StoreLocal { lvalue, value: val, type_annotation, loc } => { + self.line("StoreLocal {"); + self.indent(); + self.format_lvalue("lvalue", lvalue); + self.format_place_field("value", val); + self.line(&format!( + "type: {}", + match type_annotation { + Some(t) => t.clone(), + None => "null".to_string(), + } + )); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::LoadContext { place, loc } => { + self.line("LoadContext {"); + self.indent(); + self.format_place_field("place", place); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::StoreContext { lvalue, value: val, loc } => { + self.line("StoreContext {"); + self.indent(); + self.line("lvalue:"); + self.indent(); + self.line(&format!("kind: {:?}", lvalue.kind)); + self.format_place_field("place", &lvalue.place); + self.dedent(); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::Destructure { lvalue, value: val, loc } => { + self.line("Destructure {"); + self.indent(); + self.line("lvalue:"); + self.indent(); + self.line(&format!("kind: {:?}", lvalue.kind)); + self.format_pattern(&lvalue.pattern); + self.dedent(); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::PropertyLoad { object, property, loc } => { + self.line("PropertyLoad {"); + self.indent(); + self.format_place_field("object", object); + self.line(&format!("property: \"{}\"", format_property_literal(property))); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::PropertyStore { object, property, value: val, loc } => { + self.line("PropertyStore {"); + self.indent(); + self.format_place_field("object", object); + self.line(&format!("property: \"{}\"", format_property_literal(property))); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::PropertyDelete { object, property, loc } => { + self.line("PropertyDelete {"); + self.indent(); + self.format_place_field("object", object); + self.line(&format!("property: \"{}\"", format_property_literal(property))); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::ComputedLoad { object, property, loc } => { + self.line("ComputedLoad {"); + self.indent(); + self.format_place_field("object", object); + self.format_place_field("property", property); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::ComputedStore { object, property, value: val, loc } => { + self.line("ComputedStore {"); + self.indent(); + self.format_place_field("object", object); + self.format_place_field("property", property); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::ComputedDelete { object, property, loc } => { + self.line("ComputedDelete {"); + self.indent(); + self.format_place_field("object", object); + self.format_place_field("property", property); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::LoadGlobal { binding, loc } => { + self.line("LoadGlobal {"); + self.indent(); + self.line(&format!("binding: {}", format_non_local_binding(binding))); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::StoreGlobal { name, value: val, loc } => { + self.line("StoreGlobal {"); + self.indent(); + self.line(&format!("name: \"{}\"", name)); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::FunctionExpression { + name, + name_hint, + lowered_func, + expr_type, + loc, + } => { + self.line("FunctionExpression {"); + self.indent(); + self.line(&format!( + "name: {}", + match name { + Some(n) => format!("\"{}\"", n), + None => "null".to_string(), + } + )); + self.line(&format!( + "nameHint: {}", + match name_hint { + Some(h) => format!("\"{}\"", h), + None => "null".to_string(), + } + )); + self.line(&format!("type: \"{:?}\"", expr_type)); + self.line("loweredFunc:"); + let inner_func = &self.env.functions[lowered_func.func.0 as usize]; + if let Some(formatter) = inner_func_formatter { + formatter(self, inner_func); + } else { + self.line(&format!(" ", lowered_func.func.0)); + } + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::ObjectMethod { loc, lowered_func } => { + self.line("ObjectMethod {"); + self.indent(); + self.line("loweredFunc:"); + let inner_func = &self.env.functions[lowered_func.func.0 as usize]; + if let Some(formatter) = inner_func_formatter { + formatter(self, inner_func); + } else { + self.line(&format!(" ", lowered_func.func.0)); + } + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::TaggedTemplateExpression { tag, quasis, subexprs, loc } => { + self.line("TaggedTemplateExpression {"); + self.indent(); + self.format_place_field("tag", tag); + if subexprs.is_empty() && quasis.len() == 1 { + // Non-interpolation case: preserve the upstream-compatible + // single-quasi format so existing fixtures stay byte-identical. + let val = &quasis[0]; + self.line(&format!("raw: {}", format_js_string(&val.raw))); + self.line(&format!( + "cooked: {}", + match &val.cooked { + Some(c) => format_js_string(c), + None => "undefined".to_string(), + } + )); + } else { + // Interpolation case (oxc divergence): print like TemplateLiteral. + self.line("subexprs:"); + self.indent(); + for (i, sub) in subexprs.iter().enumerate() { + self.format_place_field(&format!("[{}]", i), sub); + } + self.dedent(); + self.line("quasis:"); + self.indent(); + for (i, q) in quasis.iter().enumerate() { + self.line(&format!( + "[{}] {{ raw: {}, cooked: {} }}", + i, + format_js_string(&q.raw), + match &q.cooked { + Some(c) => format_js_string(c), + None => "undefined".to_string(), + } + )); + } + self.dedent(); + } + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::TemplateLiteral { subexprs, quasis, loc } => { + self.line("TemplateLiteral {"); + self.indent(); + self.line("subexprs:"); + self.indent(); + for (i, sub) in subexprs.iter().enumerate() { + self.format_place_field(&format!("[{}]", i), sub); + } + self.dedent(); + self.line("quasis:"); + self.indent(); + for (i, q) in quasis.iter().enumerate() { + self.line(&format!( + "[{}] {{ raw: {}, cooked: {} }}", + i, + format_js_string(&q.raw), + match &q.cooked { + Some(c) => format_js_string(c), + None => "undefined".to_string(), + } + )); + } + self.dedent(); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::RegExpLiteral { pattern, flags, loc } => { + self.line(&format!( + "RegExpLiteral {{ pattern: \"{}\", flags: \"{}\", loc: {} }}", + pattern, + flags, + format_loc(loc) + )); + } + InstructionValue::MetaProperty { meta, property, loc } => { + self.line(&format!( + "MetaProperty {{ meta: \"{}\", property: \"{}\", loc: {} }}", + meta, + property, + format_loc(loc) + )); + } + InstructionValue::Await { value: val, loc } => { + self.line("Await {"); + self.indent(); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::GetIterator { collection, loc } => { + self.line("GetIterator {"); + self.indent(); + self.format_place_field("collection", collection); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::IteratorNext { iterator, collection, loc } => { + self.line("IteratorNext {"); + self.indent(); + self.format_place_field("iterator", iterator); + self.format_place_field("collection", collection); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::NextPropertyOf { value: val, loc } => { + self.line("NextPropertyOf {"); + self.indent(); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::Debugger { loc } => { + self.line(&format!("Debugger {{ loc: {} }}", format_loc(loc))); + } + InstructionValue::PassthroughStatement { loc, .. } => { + self.line(&format!("PassthroughStatement {{ loc: {} }}", format_loc(loc))); + } + InstructionValue::PostfixUpdate { lvalue, operation, value: val, loc } => { + self.line("PostfixUpdate {"); + self.indent(); + self.format_place_field("lvalue", lvalue); + self.line(&format!("operation: \"{}\"", operation)); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::PrefixUpdate { lvalue, operation, value: val, loc } => { + self.line("PrefixUpdate {"); + self.indent(); + self.format_place_field("lvalue", lvalue); + self.line(&format!("operation: \"{}\"", operation)); + self.format_place_field("value", val); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::StartMemoize { + manual_memo_id, + deps, + deps_loc: _, + has_invalid_deps: _, + loc, + } => { + self.line("StartMemoize {"); + self.indent(); + self.line(&format!("manualMemoId: {}", manual_memo_id)); + match deps { + Some(d) => { + self.line("deps:"); + self.indent(); + for (i, dep) in d.iter().enumerate() { + let root_str = match &dep.root { + crate::react_compiler_hir::ManualMemoDependencyRoot::Global { identifier_name } => { + format!("Global(\"{}\")", identifier_name) + } + crate::react_compiler_hir::ManualMemoDependencyRoot::NamedLocal { + value: val, + constant, + } => { + format!( + "NamedLocal({}, constant={})", + val.identifier.0, constant + ) + } + }; + let path_str: String = dep + .path + .iter() + .map(|p| { + format!( + "{}.{}", + if p.optional { "?" } else { "" }, + format_property_literal(&p.property) + ) + }) + .collect(); + self.line(&format!("[{}] {}{}", i, root_str, path_str)); + } + self.dedent(); + } + None => self.line("deps: null"), + } + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + InstructionValue::FinishMemoize { manual_memo_id, decl, pruned, loc } => { + self.line("FinishMemoize {"); + self.indent(); + self.line(&format!("manualMemoId: {}", manual_memo_id)); + self.format_place_field("decl", decl); + self.line(&format!("pruned: {}", pruned)); + self.line(&format!("loc: {}", format_loc(loc))); + self.dedent(); + self.line("}"); + } + } + } + + // ========================================================================= + // Errors + // ========================================================================= + + pub fn format_errors(&mut self, error: &CompilerError) { + if error.details.is_empty() { + self.line("Errors: []"); + return; + } + self.line("Errors:"); + self.indent(); + for (i, detail) in error.details.iter().enumerate() { + self.line(&format!("[{}] {{", i)); + self.indent(); + match detail { + CompilerErrorOrDiagnostic::Diagnostic(d) => { + self.line(&format!("severity: {:?}", d.severity())); + self.line(&format!("reason: {:?}", d.reason)); + self.line(&format!( + "description: {}", + match &d.description { + Some(desc) => format!("{:?}", desc), + None => "null".to_string(), + } + )); + self.line(&format!("category: {:?}", d.category)); + let loc = d.primary_location(); + self.line(&format!( + "loc: {}", + match loc { + Some(l) => format_loc_value(l), + None => "null".to_string(), + } + )); + } + CompilerErrorOrDiagnostic::ErrorDetail(d) => { + self.line(&format!("severity: {:?}", d.severity())); + self.line(&format!("reason: {:?}", d.reason)); + self.line(&format!( + "description: {}", + match &d.description { + Some(desc) => format!("{:?}", desc), + None => "null".to_string(), + } + )); + self.line(&format!("category: {:?}", d.category)); + self.line(&format!( + "loc: {}", + match &d.loc { + Some(l) => format_loc_value(l), + None => "null".to_string(), + } + )); + } + } + self.dedent(); + self.line("}"); + } + self.dedent(); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/raw.rs b/crates/oxc_react_compiler/src/react_compiler_hir/raw.rs new file mode 100644 index 0000000000000..a912f5dd9453c --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/raw.rs @@ -0,0 +1,91 @@ +//! Raw, unmodeled AST subtrees carried through the HIR. +//! +//! [`RawNode`] holds the metadata the compiler reads off type annotations, class +//! bodies and other parser extras the IR does not model with typed nodes; the +//! original text is re-parsed from source during codegen. + +use crate::react_compiler_diagnostics::SourceLocation; + +/// An AST subtree the compiler does not model with typed nodes (type +/// annotations, class bodies, parser extras). The original text is re-parsed +/// from source during codegen; only the metadata the compiler reads is kept. +#[derive(Debug, Clone, Default)] +pub struct RawNode { + /// Identifiers (incl. JSX identifiers) inside this subtree, pre-extracted + /// with node-id (== source start offset), location and flags, so the core + /// never walks JSON for the loc index, reference scans or renaming. + pub idents: Vec, + /// Whether the subtree contains a hook call or JSX (for class-body members). + pub contains_hook_or_jsx: bool, + /// The type node's `type` tag, unwrapped past any + /// `TypeAnnotation`/`TSTypeAnnotation` wrapper (e.g. `"TSTypeReference"`). + pub node_type: Option, + /// Source span of the unwrapped type, for re-parsing it from source. + pub type_start: Option, + pub type_end: Option, + /// Coarse classification of the unwrapped type, for HIR `Type` lowering. + pub type_category: RawTypeCategory, +} + +/// Coarse classification of a type annotation, mirroring the cases HIR type +/// lowering distinguishes (array / primitive / everything else). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum RawTypeCategory { + Array, + Primitive, + #[default] + Other, +} + +/// A reference to an identifier discovered inside a [`RawNode`] subtree. +#[derive(Debug, Clone)] +pub struct RawIdent { + pub name: String, + /// Babel `_nodeId`, equal to the identifier's source start offset. + pub node_id: u32, + pub start: u32, + pub loc: Option, + pub is_jsx: bool, + /// True if the identifier sits inside a type-annotation subtree. + pub in_type_annotation: bool, + /// Set by the rename pass to the new name when this identifier is renamed. + pub renamed_to: Option, +} + +impl RawNode { + /// An empty placeholder carrying no metadata (e.g. for decorators / type + /// parameters / class bodies re-emitted verbatim from source). + pub fn empty() -> Self { + RawNode::default() + } + + /// Alias for [`RawNode::empty`]. + pub fn null() -> Self { + RawNode::default() + } + + /// A RawNode for a TS type, carrying its tag, source span, classification and + /// referenced identifiers. + pub fn type_node( + node_type: Option, + type_start: Option, + type_end: Option, + type_category: RawTypeCategory, + idents: Vec, + ) -> Self { + RawNode { + idents, + contains_hook_or_jsx: false, + node_type, + type_start, + type_end, + type_category, + } + } + + /// A RawNode for an unmodeled subtree, carrying its identifiers and whether it + /// contains a hook call or JSX. + pub fn unknown(idents: Vec, contains_hook_or_jsx: bool) -> Self { + RawNode { idents, contains_hook_or_jsx, ..RawNode::default() } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/reactive.rs b/crates/oxc_react_compiler/src/react_compiler_hir/reactive.rs new file mode 100644 index 0000000000000..178648df0a1cc --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/reactive.rs @@ -0,0 +1,248 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Reactive function types — tree representation of a compiled function. +//! +//! `ReactiveFunction` is derived from the HIR CFG by `BuildReactiveFunction`. +//! Control flow constructs (if/switch/loops/try) and reactive scopes become +//! nested blocks rather than block references. +//! +//! Corresponds to the reactive types in `HIR.ts`. + +use crate::react_compiler_diagnostics::SourceLocation; + +use crate::react_compiler_hir::{ + AliasingEffect, BlockId, EvaluationOrder, InstructionValue, LogicalOperator, ParamPattern, + Place, ScopeId, +}; + +// ============================================================================= +// ReactiveFunction +// ============================================================================= + +/// Tree representation of a compiled function, converted from the CFG-based HIR. +/// TS: ReactiveFunction in HIR.ts +#[derive(Debug, Clone)] +pub struct ReactiveFunction<'a> { + pub loc: Option, + pub id: Option, + pub name_hint: Option, + pub params: Vec, + pub generator: bool, + pub is_async: bool, + pub body: ReactiveBlock<'a>, + pub directives: Vec, + // No env field — passed separately per established Rust convention +} + +// ============================================================================= +// ReactiveBlock and ReactiveStatement +// ============================================================================= + +/// TS: ReactiveBlock = Array +pub type ReactiveBlock<'a> = Vec>; + +/// TS: ReactiveStatement (discriminated union with 'kind' field) +#[derive(Debug, Clone)] +pub enum ReactiveStatement<'a> { + Instruction(ReactiveInstruction<'a>), + Terminal(ReactiveTerminalStatement<'a>), + Scope(ReactiveScopeBlock<'a>), + PrunedScope(PrunedReactiveScopeBlock<'a>), +} + +// ============================================================================= +// ReactiveInstruction and ReactiveValue +// ============================================================================= + +/// TS: ReactiveInstruction +#[derive(Debug, Clone)] +pub struct ReactiveInstruction<'a> { + pub id: EvaluationOrder, + pub lvalue: Option, + pub value: ReactiveValue<'a>, + pub effects: Option>, + pub loc: Option, +} + +/// Extends InstructionValue with compound expression types that were +/// separate blocks+terminals in HIR but become nested expressions here. +/// TS: ReactiveValue = InstructionValue | ReactiveLogicalValue | ... +#[derive(Debug, Clone)] +pub enum ReactiveValue<'a> { + /// All ~35 base instruction value kinds + Instruction(InstructionValue<'a>), + + /// TS: ReactiveLogicalValue + LogicalExpression { + operator: LogicalOperator, + left: Box>, + right: Box>, + loc: Option, + }, + + /// TS: ReactiveTernaryValue + ConditionalExpression { + test: Box>, + consequent: Box>, + alternate: Box>, + loc: Option, + }, + + /// TS: ReactiveSequenceValue + SequenceExpression { + instructions: Vec>, + id: EvaluationOrder, + value: Box>, + loc: Option, + }, + + /// TS: ReactiveOptionalCallValue + OptionalExpression { + id: EvaluationOrder, + value: Box>, + optional: bool, + loc: Option, + }, +} + +// ============================================================================= +// Terminals +// ============================================================================= + +#[derive(Debug, Clone)] +pub struct ReactiveTerminalStatement<'a> { + pub terminal: ReactiveTerminal<'a>, + pub label: Option, +} + +#[derive(Debug, Clone)] +pub struct ReactiveLabel { + pub id: BlockId, + pub implicit: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReactiveTerminalTargetKind { + Implicit, + Labeled, + Unlabeled, +} + +impl std::fmt::Display for ReactiveTerminalTargetKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReactiveTerminalTargetKind::Implicit => write!(f, "implicit"), + ReactiveTerminalTargetKind::Labeled => write!(f, "labeled"), + ReactiveTerminalTargetKind::Unlabeled => write!(f, "unlabeled"), + } + } +} + +#[derive(Debug, Clone)] +pub enum ReactiveTerminal<'a> { + Break { + target: BlockId, + id: EvaluationOrder, + target_kind: ReactiveTerminalTargetKind, + loc: Option, + }, + Continue { + target: BlockId, + id: EvaluationOrder, + target_kind: ReactiveTerminalTargetKind, + loc: Option, + }, + Return { + value: Place, + id: EvaluationOrder, + loc: Option, + }, + Throw { + value: Place, + id: EvaluationOrder, + loc: Option, + }, + Switch { + test: Place, + cases: Vec>, + id: EvaluationOrder, + loc: Option, + }, + DoWhile { + loop_block: ReactiveBlock<'a>, + test: ReactiveValue<'a>, + id: EvaluationOrder, + loc: Option, + }, + While { + test: ReactiveValue<'a>, + loop_block: ReactiveBlock<'a>, + id: EvaluationOrder, + loc: Option, + }, + For { + init: ReactiveValue<'a>, + test: ReactiveValue<'a>, + update: Option>, + loop_block: ReactiveBlock<'a>, + id: EvaluationOrder, + loc: Option, + }, + ForOf { + init: ReactiveValue<'a>, + test: ReactiveValue<'a>, + loop_block: ReactiveBlock<'a>, + id: EvaluationOrder, + loc: Option, + }, + ForIn { + init: ReactiveValue<'a>, + loop_block: ReactiveBlock<'a>, + id: EvaluationOrder, + loc: Option, + }, + If { + test: Place, + consequent: ReactiveBlock<'a>, + alternate: Option>, + id: EvaluationOrder, + loc: Option, + }, + Label { + block: ReactiveBlock<'a>, + id: EvaluationOrder, + loc: Option, + }, + Try { + block: ReactiveBlock<'a>, + handler_binding: Option, + handler: ReactiveBlock<'a>, + id: EvaluationOrder, + loc: Option, + }, +} + +#[derive(Debug, Clone)] +pub struct ReactiveSwitchCase<'a> { + pub test: Option, + pub block: Option>, +} + +// ============================================================================= +// Scope Blocks +// ============================================================================= + +#[derive(Debug, Clone)] +pub struct ReactiveScopeBlock<'a> { + pub scope: ScopeId, + pub instructions: ReactiveBlock<'a>, +} + +#[derive(Debug, Clone)] +pub struct PrunedReactiveScopeBlock<'a> { + pub scope: ScopeId, + pub instructions: ReactiveBlock<'a>, +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/type_config.rs b/crates/oxc_react_compiler/src/react_compiler_hir/type_config.rs new file mode 100644 index 0000000000000..526142a1e3c87 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/type_config.rs @@ -0,0 +1,188 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Type configuration types, ported from TypeSchema.ts. +//! +//! These are the JSON-serializable config types used by `moduleTypeProvider` +//! and `installTypeConfig` to describe module/function/hook types. + +use crate::react_compiler_utils::FxIndexMap; + +use crate::react_compiler_hir::Effect; + +/// Mirrors TS `ValueKind` enum for use in config. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValueKind { + Mutable, + Frozen, + Primitive, + MaybeFrozen, + Global, + Context, +} + +/// Mirrors TS `ValueReason` enum for use in config. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ValueReason { + KnownReturnSignature, + State, + ReducerState, + Context, + Effect, + HookCaptured, + HookReturn, + Global, + JsxCaptured, + StoreLocal, + ReactiveFunctionArgument, + Other, +} + +// ============================================================================= +// Aliasing effect config types (from TypeSchema.ts) +// ============================================================================= + +#[derive(Debug, Clone)] +pub enum AliasingEffectConfig { + Freeze { + value: String, + reason: ValueReason, + }, + Create { + into: String, + value: ValueKind, + reason: ValueReason, + }, + CreateFrom { + from: String, + into: String, + }, + Assign { + from: String, + into: String, + }, + Alias { + from: String, + into: String, + }, + Capture { + from: String, + into: String, + }, + ImmutableCapture { + from: String, + into: String, + }, + Impure { + place: String, + }, + Mutate { + value: String, + }, + MutateTransitiveConditionally { + value: String, + }, + Apply { + receiver: String, + function: String, + mutates_function: bool, + args: Vec, + into: String, + }, +} + +#[derive(Debug, Clone)] +pub enum ApplyArgConfig { + Place(String), + Spread { + #[allow(dead_code)] + kind: ApplyArgSpreadKind, + place: String, + }, + Hole { + #[allow(dead_code)] + kind: ApplyArgHoleKind, + }, +} + +/// Helper enum for tagged serde of `ApplyArgConfig::Spread`. +#[derive(Debug, Clone)] +pub enum ApplyArgSpreadKind { + Spread, +} + +/// Helper enum for tagged serde of `ApplyArgConfig::Hole`. +#[derive(Debug, Clone)] +pub enum ApplyArgHoleKind { + Hole, +} + +/// Aliasing signature config, the JSON-serializable form. +#[derive(Debug, Clone)] +pub struct AliasingSignatureConfig { + pub receiver: String, + pub params: Vec, + pub rest: Option, + pub returns: String, + pub temporaries: Vec, + pub effects: Vec, +} + +// ============================================================================= +// Type config (from TypeSchema.ts) +// ============================================================================= + +#[derive(Debug, Clone)] +pub enum TypeConfig { + Object(ObjectTypeConfig), + Function(FunctionTypeConfig), + Hook(HookTypeConfig), + TypeReference(TypeReferenceConfig), +} + +#[derive(Debug, Clone)] +pub struct ObjectTypeConfig { + pub properties: Option>, +} + +#[derive(Debug, Clone)] +pub struct FunctionTypeConfig { + pub positional_params: Vec, + pub rest_param: Option, + pub callee_effect: Effect, + pub return_type: Box, + pub return_value_kind: ValueKind, + pub no_alias: Option, + pub mutable_only_if_operands_are_mutable: Option, + pub impure: Option, + pub canonical_name: Option, + pub aliasing: Option, + pub known_incompatible: Option, +} + +#[derive(Debug, Clone)] +pub struct HookTypeConfig { + pub positional_params: Option>, + pub rest_param: Option, + pub return_type: Box, + pub return_value_kind: Option, + pub no_alias: Option, + pub aliasing: Option, + pub known_incompatible: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BuiltInTypeRef { + Any, + Ref, + Array, + Primitive, + MixedReadonly, +} + +#[derive(Debug, Clone)] +pub struct TypeReferenceConfig { + pub name: BuiltInTypeRef, +} diff --git a/crates/oxc_react_compiler/src/react_compiler_hir/visitors.rs b/crates/oxc_react_compiler/src/react_compiler_hir/visitors.rs new file mode 100644 index 0000000000000..5062fda898ca1 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_hir/visitors.rs @@ -0,0 +1,1510 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +use rustc_hash::FxHashMap; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + ArrayElement, ArrayPatternElement, BasicBlock, BlockId, HirFunction, IdentifierId, Instruction, + InstructionKind, InstructionValue, JsxAttribute, JsxTag, ManualMemoDependencyRoot, + ObjectPropertyKey, ObjectPropertyOrSpread, Pattern, Place, PlaceOrSpread, ScopeId, Terminal, +}; + +// ============================================================================= +// Iterator functions (return Vec instead of generators) +// ============================================================================= + +/// Yields `instr.lvalue` plus the value's lvalues. +/// Equivalent to TS `eachInstructionLValue`. +pub fn each_instruction_lvalue(instr: &Instruction) -> Vec { + let mut result = Vec::new(); + result.push(instr.lvalue.clone()); + result.extend(each_instruction_value_lvalue(&instr.value)); + result +} + +/// Yields lvalues from DeclareLocal/StoreLocal/DeclareContext/StoreContext/Destructure/PostfixUpdate/PrefixUpdate. +/// Equivalent to TS `eachInstructionValueLValue`. +pub fn each_instruction_value_lvalue(value: &InstructionValue) -> Vec { + let mut result = Vec::new(); + match value { + InstructionValue::DeclareContext { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } + | InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } => { + result.push(lvalue.place.clone()); + } + InstructionValue::Destructure { lvalue, .. } => { + result.extend(each_pattern_operand(&lvalue.pattern)); + } + InstructionValue::PostfixUpdate { lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue, .. } => { + result.push(lvalue.clone()); + } + // All other variants have no lvalues + InstructionValue::LoadLocal { .. } + | InstructionValue::LoadContext { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::BinaryExpression { .. } + | InstructionValue::NewExpression { .. } + | InstructionValue::CallExpression { .. } + | InstructionValue::MethodCall { .. } + | InstructionValue::UnaryExpression { .. } + | InstructionValue::TypeCastExpression { .. } + | InstructionValue::JsxExpression { .. } + | InstructionValue::ObjectExpression { .. } + | InstructionValue::ObjectMethod { .. } + | InstructionValue::ArrayExpression { .. } + | InstructionValue::JsxFragment { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::PropertyStore { .. } + | InstructionValue::PropertyLoad { .. } + | InstructionValue::PropertyDelete { .. } + | InstructionValue::ComputedStore { .. } + | InstructionValue::ComputedLoad { .. } + | InstructionValue::ComputedDelete { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::StoreGlobal { .. } + | InstructionValue::FunctionExpression { .. } + | InstructionValue::TaggedTemplateExpression { .. } + | InstructionValue::TemplateLiteral { .. } + | InstructionValue::Await { .. } + | InstructionValue::GetIterator { .. } + | InstructionValue::IteratorNext { .. } + | InstructionValue::NextPropertyOf { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::StartMemoize { .. } + | InstructionValue::FinishMemoize { .. } => {} + } + result +} + +/// Yields lvalues with their InstructionKind. +/// Equivalent to TS `eachInstructionLValueWithKind`. +pub fn each_instruction_lvalue_with_kind( + value: &InstructionValue, +) -> Vec<(Place, InstructionKind)> { + let mut result = Vec::new(); + match value { + InstructionValue::DeclareContext { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } + | InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } => { + result.push((lvalue.place.clone(), lvalue.kind)); + } + InstructionValue::Destructure { lvalue, .. } => { + let kind = lvalue.kind; + for place in each_pattern_operand(&lvalue.pattern) { + result.push((place, kind)); + } + } + InstructionValue::PostfixUpdate { lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue, .. } => { + result.push((lvalue.clone(), InstructionKind::Reassign)); + } + // All other variants have no lvalues with kind + InstructionValue::LoadLocal { .. } + | InstructionValue::LoadContext { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::BinaryExpression { .. } + | InstructionValue::NewExpression { .. } + | InstructionValue::CallExpression { .. } + | InstructionValue::MethodCall { .. } + | InstructionValue::UnaryExpression { .. } + | InstructionValue::TypeCastExpression { .. } + | InstructionValue::JsxExpression { .. } + | InstructionValue::ObjectExpression { .. } + | InstructionValue::ObjectMethod { .. } + | InstructionValue::ArrayExpression { .. } + | InstructionValue::JsxFragment { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::PropertyStore { .. } + | InstructionValue::PropertyLoad { .. } + | InstructionValue::PropertyDelete { .. } + | InstructionValue::ComputedStore { .. } + | InstructionValue::ComputedLoad { .. } + | InstructionValue::ComputedDelete { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::StoreGlobal { .. } + | InstructionValue::FunctionExpression { .. } + | InstructionValue::TaggedTemplateExpression { .. } + | InstructionValue::TemplateLiteral { .. } + | InstructionValue::Await { .. } + | InstructionValue::GetIterator { .. } + | InstructionValue::IteratorNext { .. } + | InstructionValue::NextPropertyOf { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::StartMemoize { .. } + | InstructionValue::FinishMemoize { .. } => {} + } + result +} + +/// Delegates to each_instruction_value_operand. +/// Equivalent to TS `eachInstructionOperand`. +pub fn each_instruction_operand(instr: &Instruction, env: &Environment) -> Vec { + each_instruction_value_operand(&instr.value, env) +} + +/// Like `each_instruction_operand` but takes `functions` directly instead of `env`. +/// Useful when borrow splitting prevents passing the full `Environment`. +pub fn each_instruction_operand_with_functions( + instr: &Instruction, + functions: &[HirFunction], +) -> Vec { + each_instruction_value_operand_with_functions(&instr.value, functions) +} + +/// Yields operand places from an InstructionValue. +/// Equivalent to TS `eachInstructionValueOperand`. +pub fn each_instruction_value_operand(value: &InstructionValue, env: &Environment) -> Vec { + each_instruction_value_operand_with_functions(value, &env.functions) +} + +/// Like `each_instruction_value_operand` but takes `functions` directly instead of `env`. +/// Useful when borrow splitting prevents passing the full `Environment`. +pub fn each_instruction_value_operand_with_functions( + value: &InstructionValue, + functions: &[HirFunction], +) -> Vec { + let mut result = Vec::new(); + match value { + InstructionValue::NewExpression { callee, args, .. } + | InstructionValue::CallExpression { callee, args, .. } => { + result.push(callee.clone()); + result.extend(each_call_argument(args)); + } + InstructionValue::BinaryExpression { left, right, .. } => { + result.push(left.clone()); + result.push(right.clone()); + } + InstructionValue::MethodCall { receiver, property, args, .. } => { + result.push(receiver.clone()); + result.push(property.clone()); + result.extend(each_call_argument(args)); + } + InstructionValue::DeclareContext { .. } | InstructionValue::DeclareLocal { .. } => { + // no operands + } + InstructionValue::LoadLocal { place, .. } | InstructionValue::LoadContext { place, .. } => { + result.push(place.clone()); + } + InstructionValue::StoreLocal { value: val, .. } => { + result.push(val.clone()); + } + InstructionValue::StoreContext { lvalue, value: val, .. } => { + result.push(lvalue.place.clone()); + result.push(val.clone()); + } + InstructionValue::StoreGlobal { value: val, .. } => { + result.push(val.clone()); + } + InstructionValue::Destructure { value: val, .. } => { + result.push(val.clone()); + } + InstructionValue::PropertyLoad { object, .. } => { + result.push(object.clone()); + } + InstructionValue::PropertyDelete { object, .. } => { + result.push(object.clone()); + } + InstructionValue::PropertyStore { object, value: val, .. } => { + result.push(object.clone()); + result.push(val.clone()); + } + InstructionValue::ComputedLoad { object, property, .. } => { + result.push(object.clone()); + result.push(property.clone()); + } + InstructionValue::ComputedDelete { object, property, .. } => { + result.push(object.clone()); + result.push(property.clone()); + } + InstructionValue::ComputedStore { object, property, value: val, .. } => { + result.push(object.clone()); + result.push(property.clone()); + result.push(val.clone()); + } + InstructionValue::UnaryExpression { value: val, .. } => { + result.push(val.clone()); + } + InstructionValue::JsxExpression { tag, props, children, .. } => { + if let JsxTag::Place(place) = tag { + result.push(place.clone()); + } + for attribute in props { + match attribute { + JsxAttribute::Attribute { place, .. } => { + result.push(place.clone()); + } + JsxAttribute::SpreadAttribute { argument, .. } => { + result.push(argument.clone()); + } + } + } + if let Some(children) = children { + for child in children { + result.push(child.clone()); + } + } + } + InstructionValue::JsxFragment { children, .. } => { + for child in children { + result.push(child.clone()); + } + } + InstructionValue::ObjectExpression { properties, .. } => { + for property in properties { + match property { + ObjectPropertyOrSpread::Property(prop) => { + if let ObjectPropertyKey::Computed { name } = &prop.key { + result.push(name.clone()); + } + result.push(prop.place.clone()); + } + ObjectPropertyOrSpread::Spread(spread) => { + result.push(spread.place.clone()); + } + } + } + } + InstructionValue::ArrayExpression { elements, .. } => { + for element in elements { + match element { + ArrayElement::Place(place) => { + result.push(place.clone()); + } + ArrayElement::Spread(spread) => { + result.push(spread.place.clone()); + } + ArrayElement::Hole => {} + } + } + } + InstructionValue::ObjectMethod { lowered_func, .. } + | InstructionValue::FunctionExpression { lowered_func, .. } => { + let func = &functions[lowered_func.func.0 as usize]; + for ctx_place in &func.context { + result.push(ctx_place.clone()); + } + } + InstructionValue::TaggedTemplateExpression { tag, subexprs, .. } => { + result.push(tag.clone()); + for subexpr in subexprs { + result.push(subexpr.clone()); + } + } + InstructionValue::TypeCastExpression { value: val, .. } => { + result.push(val.clone()); + } + InstructionValue::TemplateLiteral { subexprs, .. } => { + for subexpr in subexprs { + result.push(subexpr.clone()); + } + } + InstructionValue::Await { value: val, .. } => { + result.push(val.clone()); + } + InstructionValue::GetIterator { collection, .. } => { + result.push(collection.clone()); + } + InstructionValue::IteratorNext { iterator, collection, .. } => { + result.push(iterator.clone()); + result.push(collection.clone()); + } + InstructionValue::NextPropertyOf { value: val, .. } => { + result.push(val.clone()); + } + InstructionValue::PostfixUpdate { value: val, .. } + | InstructionValue::PrefixUpdate { value: val, .. } => { + result.push(val.clone()); + } + InstructionValue::StartMemoize { deps, .. } => { + if let Some(deps) = deps { + for dep in deps { + if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &dep.root { + result.push(value.clone()); + } + } + } + } + InstructionValue::FinishMemoize { decl, .. } => { + result.push(decl.clone()); + } + InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::JSXText { .. } => { + // no operands + } + } + result +} + +/// Yields each arg's place. +/// Equivalent to TS `eachCallArgument`. +pub fn each_call_argument(args: &[PlaceOrSpread]) -> Vec { + let mut result = Vec::new(); + for arg in args { + match arg { + PlaceOrSpread::Place(place) => { + result.push(place.clone()); + } + PlaceOrSpread::Spread(spread) => { + result.push(spread.place.clone()); + } + } + } + result +} + +/// Yields places from array/object patterns. +/// Equivalent to TS `eachPatternOperand`. +pub fn each_pattern_operand(pattern: &Pattern) -> Vec { + let mut result = Vec::new(); + match pattern { + Pattern::Array(arr) => { + for item in &arr.items { + match item { + ArrayPatternElement::Place(place) => { + result.push(place.clone()); + } + ArrayPatternElement::Spread(spread) => { + result.push(spread.place.clone()); + } + ArrayPatternElement::Hole => {} + } + } + } + Pattern::Object(obj) => { + for property in &obj.properties { + match property { + ObjectPropertyOrSpread::Property(prop) => { + result.push(prop.place.clone()); + } + ObjectPropertyOrSpread::Spread(spread) => { + result.push(spread.place.clone()); + } + } + } + } + } + result +} + +/// Returns true if the pattern contains a spread element. +/// Equivalent to TS `doesPatternContainSpreadElement`. +pub fn does_pattern_contain_spread_element(pattern: &Pattern) -> bool { + match pattern { + Pattern::Array(arr) => { + for item in &arr.items { + if matches!(item, ArrayPatternElement::Spread(_)) { + return true; + } + } + } + Pattern::Object(obj) => { + for property in &obj.properties { + if matches!(property, ObjectPropertyOrSpread::Spread(_)) { + return true; + } + } + } + } + false +} + +/// Yields successor block IDs (NOT fallthroughs, this is intentional). +/// Equivalent to TS `eachTerminalSuccessor`. +pub fn each_terminal_successor(terminal: &Terminal) -> Vec { + let mut result = Vec::new(); + match terminal { + Terminal::Goto { block, .. } => { + result.push(*block); + } + Terminal::If { consequent, alternate, .. } => { + result.push(*consequent); + result.push(*alternate); + } + Terminal::Branch { consequent, alternate, .. } => { + result.push(*consequent); + result.push(*alternate); + } + Terminal::Switch { cases, .. } => { + for case in cases { + result.push(case.block); + } + } + Terminal::Optional { test, .. } + | Terminal::Ternary { test, .. } + | Terminal::Logical { test, .. } => { + result.push(*test); + } + Terminal::Return { .. } => {} + Terminal::Throw { .. } => {} + Terminal::DoWhile { loop_block, .. } => { + result.push(*loop_block); + } + Terminal::While { test, .. } => { + result.push(*test); + } + Terminal::For { init, .. } => { + result.push(*init); + } + Terminal::ForOf { init, .. } => { + result.push(*init); + } + Terminal::ForIn { init, .. } => { + result.push(*init); + } + Terminal::Label { block, .. } => { + result.push(*block); + } + Terminal::Sequence { block, .. } => { + result.push(*block); + } + Terminal::MaybeThrow { continuation, handler, .. } => { + result.push(*continuation); + if let Some(handler) = handler { + result.push(*handler); + } + } + Terminal::Try { block, .. } => { + result.push(*block); + } + Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => { + result.push(*block); + } + Terminal::Unreachable { .. } | Terminal::Unsupported { .. } => {} + } + result +} + +/// Yields places used by terminal. +/// Equivalent to TS `eachTerminalOperand`. +pub fn each_terminal_operand(terminal: &Terminal) -> Vec { + let mut result = Vec::new(); + match terminal { + Terminal::If { test, .. } => { + result.push(test.clone()); + } + Terminal::Branch { test, .. } => { + result.push(test.clone()); + } + Terminal::Switch { test, cases, .. } => { + result.push(test.clone()); + for case in cases { + if let Some(test) = &case.test { + result.push(test.clone()); + } + } + } + Terminal::Return { value, .. } | Terminal::Throw { value, .. } => { + result.push(value.clone()); + } + Terminal::Try { handler_binding, .. } => { + if let Some(binding) = handler_binding { + result.push(binding.clone()); + } + } + Terminal::MaybeThrow { .. } + | Terminal::Sequence { .. } + | Terminal::Label { .. } + | Terminal::Optional { .. } + | Terminal::Ternary { .. } + | Terminal::Logical { .. } + | Terminal::DoWhile { .. } + | Terminal::While { .. } + | Terminal::For { .. } + | Terminal::ForOf { .. } + | Terminal::ForIn { .. } + | Terminal::Goto { .. } + | Terminal::Unreachable { .. } + | Terminal::Unsupported { .. } + | Terminal::Scope { .. } + | Terminal::PrunedScope { .. } => { + // no-op + } + } + result +} + +// ============================================================================= +// Mapping functions (mutate in place) +// ============================================================================= + +/// Maps the instruction's lvalue and value's lvalues. +/// Equivalent to TS `mapInstructionLValues`. +pub fn map_instruction_lvalues(instr: &mut Instruction, f: &mut impl FnMut(Place) -> Place) { + match &mut instr.value { + InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } + | InstructionValue::DeclareContext { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } => { + lvalue.place = f(lvalue.place.clone()); + } + InstructionValue::Destructure { lvalue, .. } => { + map_pattern_operands(&mut lvalue.pattern, f); + } + InstructionValue::PostfixUpdate { lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue, .. } => { + *lvalue = f(lvalue.clone()); + } + _ => {} + } + instr.lvalue = f(instr.lvalue.clone()); +} + +/// Maps operands of an instruction. +/// Equivalent to TS `mapInstructionOperands`. +pub fn map_instruction_operands( + instr: &mut Instruction, + env: &mut Environment, + f: &mut impl FnMut(Place) -> Place, +) { + map_instruction_value_operands(&mut instr.value, env, f); +} + +/// Maps operand places in an InstructionValue. +/// Equivalent to TS `mapInstructionValueOperands`. +pub fn map_instruction_value_operands( + value: &mut InstructionValue, + env: &mut Environment, + f: &mut impl FnMut(Place) -> Place, +) { + match value { + InstructionValue::BinaryExpression { left, right, .. } => { + *left = f(left.clone()); + *right = f(right.clone()); + } + InstructionValue::PropertyLoad { object, .. } => { + *object = f(object.clone()); + } + InstructionValue::PropertyDelete { object, .. } => { + *object = f(object.clone()); + } + InstructionValue::PropertyStore { object, value: val, .. } => { + *object = f(object.clone()); + *val = f(val.clone()); + } + InstructionValue::ComputedLoad { object, property, .. } => { + *object = f(object.clone()); + *property = f(property.clone()); + } + InstructionValue::ComputedDelete { object, property, .. } => { + *object = f(object.clone()); + *property = f(property.clone()); + } + InstructionValue::ComputedStore { object, property, value: val, .. } => { + *object = f(object.clone()); + *property = f(property.clone()); + *val = f(val.clone()); + } + InstructionValue::DeclareContext { .. } | InstructionValue::DeclareLocal { .. } => { + // no operands + } + InstructionValue::LoadLocal { place, .. } | InstructionValue::LoadContext { place, .. } => { + *place = f(place.clone()); + } + InstructionValue::StoreLocal { value: val, .. } => { + *val = f(val.clone()); + } + InstructionValue::StoreContext { lvalue, value: val, .. } => { + lvalue.place = f(lvalue.place.clone()); + *val = f(val.clone()); + } + InstructionValue::StoreGlobal { value: val, .. } => { + *val = f(val.clone()); + } + InstructionValue::Destructure { value: val, .. } => { + *val = f(val.clone()); + } + InstructionValue::NewExpression { callee, args, .. } + | InstructionValue::CallExpression { callee, args, .. } => { + *callee = f(callee.clone()); + map_call_arguments(args, f); + } + InstructionValue::MethodCall { receiver, property, args, .. } => { + *receiver = f(receiver.clone()); + *property = f(property.clone()); + map_call_arguments(args, f); + } + InstructionValue::UnaryExpression { value: val, .. } => { + *val = f(val.clone()); + } + InstructionValue::JsxExpression { tag, props, children, .. } => { + if let JsxTag::Place(place) = tag { + *place = f(place.clone()); + } + for attribute in props.iter_mut() { + match attribute { + JsxAttribute::Attribute { place, .. } => { + *place = f(place.clone()); + } + JsxAttribute::SpreadAttribute { argument, .. } => { + *argument = f(argument.clone()); + } + } + } + if let Some(children) = children { + *children = children.iter().map(|p| f(p.clone())).collect(); + } + } + InstructionValue::ObjectExpression { properties, .. } => { + for property in properties.iter_mut() { + match property { + ObjectPropertyOrSpread::Property(prop) => { + if let ObjectPropertyKey::Computed { name } = &mut prop.key { + *name = f(name.clone()); + } + prop.place = f(prop.place.clone()); + } + ObjectPropertyOrSpread::Spread(spread) => { + spread.place = f(spread.place.clone()); + } + } + } + } + InstructionValue::ArrayExpression { elements, .. } => { + *elements = elements + .iter() + .map(|element| match element { + ArrayElement::Place(place) => ArrayElement::Place(f(place.clone())), + ArrayElement::Spread(spread) => { + let mut spread = spread.clone(); + spread.place = f(spread.place.clone()); + ArrayElement::Spread(spread) + } + ArrayElement::Hole => ArrayElement::Hole, + }) + .collect(); + } + InstructionValue::JsxFragment { children, .. } => { + *children = children.iter().map(|e| f(e.clone())).collect(); + } + InstructionValue::ObjectMethod { lowered_func, .. } + | InstructionValue::FunctionExpression { lowered_func, .. } => { + let func = &mut env.functions[lowered_func.func.0 as usize]; + func.context = func.context.iter().map(|d| f(d.clone())).collect(); + } + InstructionValue::TaggedTemplateExpression { tag, subexprs, .. } => { + *tag = f(tag.clone()); + *subexprs = subexprs.iter().map(|s| f(s.clone())).collect(); + } + InstructionValue::TypeCastExpression { value: val, .. } => { + *val = f(val.clone()); + } + InstructionValue::TemplateLiteral { subexprs, .. } => { + *subexprs = subexprs.iter().map(|s| f(s.clone())).collect(); + } + InstructionValue::Await { value: val, .. } => { + *val = f(val.clone()); + } + InstructionValue::GetIterator { collection, .. } => { + *collection = f(collection.clone()); + } + InstructionValue::IteratorNext { iterator, collection, .. } => { + *iterator = f(iterator.clone()); + *collection = f(collection.clone()); + } + InstructionValue::NextPropertyOf { value: val, .. } => { + *val = f(val.clone()); + } + InstructionValue::PostfixUpdate { value: val, .. } + | InstructionValue::PrefixUpdate { value: val, .. } => { + *val = f(val.clone()); + } + InstructionValue::StartMemoize { deps, .. } => { + if let Some(deps) = deps { + for dep in deps.iter_mut() { + if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &mut dep.root { + *value = f(value.clone()); + } + } + } + } + InstructionValue::FinishMemoize { decl, .. } => { + *decl = f(decl.clone()); + } + InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::JSXText { .. } => { + // no operands + } + } +} + +/// Maps call arguments in place. +/// Equivalent to TS `mapCallArguments`. +pub fn map_call_arguments(args: &mut Vec, f: &mut impl FnMut(Place) -> Place) { + for arg in args.iter_mut() { + match arg { + PlaceOrSpread::Place(place) => { + *place = f(place.clone()); + } + PlaceOrSpread::Spread(spread) => { + spread.place = f(spread.place.clone()); + } + } + } +} + +/// Maps pattern operands in place. +/// Equivalent to TS `mapPatternOperands`. +pub fn map_pattern_operands(pattern: &mut Pattern, f: &mut impl FnMut(Place) -> Place) { + match pattern { + Pattern::Array(arr) => { + arr.items = arr + .items + .iter() + .map(|item| match item { + ArrayPatternElement::Place(place) => { + ArrayPatternElement::Place(f(place.clone())) + } + ArrayPatternElement::Spread(spread) => { + let mut spread = spread.clone(); + spread.place = f(spread.place.clone()); + ArrayPatternElement::Spread(spread) + } + ArrayPatternElement::Hole => ArrayPatternElement::Hole, + }) + .collect(); + } + Pattern::Object(obj) => { + for property in obj.properties.iter_mut() { + match property { + ObjectPropertyOrSpread::Property(prop) => { + prop.place = f(prop.place.clone()); + } + ObjectPropertyOrSpread::Spread(spread) => { + spread.place = f(spread.place.clone()); + } + } + } + } + } +} + +/// Maps a terminal node's block assignments in place. +/// Equivalent to TS `mapTerminalSuccessors` — but mutates in place instead of returning a new terminal. +pub fn map_terminal_successors(terminal: &mut Terminal, f: &mut impl FnMut(BlockId) -> BlockId) { + match terminal { + Terminal::Goto { block, .. } => { + *block = f(*block); + } + Terminal::If { consequent, alternate, fallthrough, .. } => { + *consequent = f(*consequent); + *alternate = f(*alternate); + *fallthrough = f(*fallthrough); + } + Terminal::Branch { consequent, alternate, fallthrough, .. } => { + *consequent = f(*consequent); + *alternate = f(*alternate); + *fallthrough = f(*fallthrough); + } + Terminal::Switch { cases, fallthrough, .. } => { + for case in cases.iter_mut() { + case.block = f(case.block); + } + *fallthrough = f(*fallthrough); + } + Terminal::Logical { test, fallthrough, .. } => { + *test = f(*test); + *fallthrough = f(*fallthrough); + } + Terminal::Ternary { test, fallthrough, .. } => { + *test = f(*test); + *fallthrough = f(*fallthrough); + } + Terminal::Optional { test, fallthrough, .. } => { + *test = f(*test); + *fallthrough = f(*fallthrough); + } + Terminal::Return { .. } => {} + Terminal::Throw { .. } => {} + Terminal::DoWhile { loop_block, test, fallthrough, .. } => { + *loop_block = f(*loop_block); + *test = f(*test); + *fallthrough = f(*fallthrough); + } + Terminal::While { test, loop_block, fallthrough, .. } => { + *test = f(*test); + *loop_block = f(*loop_block); + *fallthrough = f(*fallthrough); + } + Terminal::For { init, test, update, loop_block, fallthrough, .. } => { + *init = f(*init); + *test = f(*test); + if let Some(update) = update { + *update = f(*update); + } + *loop_block = f(*loop_block); + *fallthrough = f(*fallthrough); + } + Terminal::ForOf { init, test, loop_block, fallthrough, .. } => { + *init = f(*init); + *test = f(*test); + *loop_block = f(*loop_block); + *fallthrough = f(*fallthrough); + } + Terminal::ForIn { init, loop_block, fallthrough, .. } => { + *init = f(*init); + *loop_block = f(*loop_block); + *fallthrough = f(*fallthrough); + } + Terminal::Label { block, fallthrough, .. } => { + *block = f(*block); + *fallthrough = f(*fallthrough); + } + Terminal::Sequence { block, fallthrough, .. } => { + *block = f(*block); + *fallthrough = f(*fallthrough); + } + Terminal::MaybeThrow { continuation, handler, .. } => { + *continuation = f(*continuation); + if let Some(handler) = handler { + *handler = f(*handler); + } + } + Terminal::Try { block, handler, fallthrough, .. } => { + *block = f(*block); + *handler = f(*handler); + *fallthrough = f(*fallthrough); + } + Terminal::Scope { block, fallthrough, .. } + | Terminal::PrunedScope { block, fallthrough, .. } => { + *block = f(*block); + *fallthrough = f(*fallthrough); + } + Terminal::Unreachable { .. } | Terminal::Unsupported { .. } => {} + } +} + +/// Maps a terminal node's operand places in place. +/// Equivalent to TS `mapTerminalOperands`. +pub fn map_terminal_operands(terminal: &mut Terminal, f: &mut impl FnMut(Place) -> Place) { + match terminal { + Terminal::If { test, .. } => { + *test = f(test.clone()); + } + Terminal::Branch { test, .. } => { + *test = f(test.clone()); + } + Terminal::Switch { test, cases, .. } => { + *test = f(test.clone()); + for case in cases.iter_mut() { + if let Some(t) = &mut case.test { + *t = f(t.clone()); + } + } + } + Terminal::Return { value, .. } | Terminal::Throw { value, .. } => { + *value = f(value.clone()); + } + Terminal::Try { handler_binding, .. } => { + if let Some(binding) = handler_binding { + *binding = f(binding.clone()); + } + } + Terminal::MaybeThrow { .. } + | Terminal::Sequence { .. } + | Terminal::Label { .. } + | Terminal::Optional { .. } + | Terminal::Ternary { .. } + | Terminal::Logical { .. } + | Terminal::DoWhile { .. } + | Terminal::While { .. } + | Terminal::For { .. } + | Terminal::ForOf { .. } + | Terminal::ForIn { .. } + | Terminal::Goto { .. } + | Terminal::Unreachable { .. } + | Terminal::Unsupported { .. } + | Terminal::Scope { .. } + | Terminal::PrunedScope { .. } => { + // no-op + } + } +} + +/// Yields ALL block IDs referenced by a terminal (successors + fallthroughs + internal blocks). +/// Unlike `each_terminal_successor` which yields only standard control flow successors, +/// this function yields every block ID that `map_terminal_successors` would visit. +pub fn each_terminal_all_successors(terminal: &Terminal) -> Vec { + let mut result = Vec::new(); + match terminal { + Terminal::Goto { block, .. } => { + result.push(*block); + } + Terminal::If { consequent, alternate, fallthrough, .. } => { + result.push(*consequent); + result.push(*alternate); + result.push(*fallthrough); + } + Terminal::Branch { consequent, alternate, fallthrough, .. } => { + result.push(*consequent); + result.push(*alternate); + result.push(*fallthrough); + } + Terminal::Switch { cases, fallthrough, .. } => { + for case in cases { + result.push(case.block); + } + result.push(*fallthrough); + } + Terminal::Logical { test, fallthrough, .. } + | Terminal::Ternary { test, fallthrough, .. } + | Terminal::Optional { test, fallthrough, .. } => { + result.push(*test); + result.push(*fallthrough); + } + Terminal::Return { .. } | Terminal::Throw { .. } => {} + Terminal::DoWhile { loop_block, test, fallthrough, .. } => { + result.push(*loop_block); + result.push(*test); + result.push(*fallthrough); + } + Terminal::While { test, loop_block, fallthrough, .. } => { + result.push(*test); + result.push(*loop_block); + result.push(*fallthrough); + } + Terminal::For { init, test, update, loop_block, fallthrough, .. } => { + result.push(*init); + result.push(*test); + if let Some(update) = update { + result.push(*update); + } + result.push(*loop_block); + result.push(*fallthrough); + } + Terminal::ForOf { init, test, loop_block, fallthrough, .. } => { + result.push(*init); + result.push(*test); + result.push(*loop_block); + result.push(*fallthrough); + } + Terminal::ForIn { init, loop_block, fallthrough, .. } => { + result.push(*init); + result.push(*loop_block); + result.push(*fallthrough); + } + Terminal::Label { block, fallthrough, .. } + | Terminal::Sequence { block, fallthrough, .. } => { + result.push(*block); + result.push(*fallthrough); + } + Terminal::MaybeThrow { continuation, handler, .. } => { + result.push(*continuation); + if let Some(handler) = handler { + result.push(*handler); + } + } + Terminal::Try { block, handler, fallthrough, .. } => { + result.push(*block); + result.push(*handler); + result.push(*fallthrough); + } + Terminal::Scope { block, fallthrough, .. } + | Terminal::PrunedScope { block, fallthrough, .. } => { + result.push(*block); + result.push(*fallthrough); + } + Terminal::Unreachable { .. } | Terminal::Unsupported { .. } => {} + } + result +} + +// ============================================================================= +// Terminal fallthrough functions +// ============================================================================= + +/// Returns the fallthrough block ID for terminals that have one. +/// Equivalent to TS `terminalFallthrough`. +pub fn terminal_fallthrough(terminal: &Terminal) -> Option { + match terminal { + // These terminals do NOT have a fallthrough + Terminal::MaybeThrow { .. } + | Terminal::Goto { .. } + | Terminal::Return { .. } + | Terminal::Throw { .. } + | Terminal::Unreachable { .. } + | Terminal::Unsupported { .. } => None, + + // These terminals DO have a fallthrough + Terminal::Branch { fallthrough, .. } + | Terminal::Try { fallthrough, .. } + | Terminal::DoWhile { fallthrough, .. } + | Terminal::ForOf { fallthrough, .. } + | Terminal::ForIn { fallthrough, .. } + | Terminal::For { fallthrough, .. } + | Terminal::If { fallthrough, .. } + | Terminal::Label { fallthrough, .. } + | Terminal::Logical { fallthrough, .. } + | Terminal::Optional { fallthrough, .. } + | Terminal::Sequence { fallthrough, .. } + | Terminal::Switch { fallthrough, .. } + | Terminal::Ternary { fallthrough, .. } + | Terminal::While { fallthrough, .. } + | Terminal::Scope { fallthrough, .. } + | Terminal::PrunedScope { fallthrough, .. } => Some(*fallthrough), + } +} + +/// Returns true if the terminal has a fallthrough block. +/// Equivalent to TS `terminalHasFallthrough`. +pub fn terminal_has_fallthrough(terminal: &Terminal) -> bool { + terminal_fallthrough(terminal).is_some() +} + +// ============================================================================= +// ScopeBlockTraversal +// ============================================================================= + +/// Block info entry for ScopeBlockTraversal. +#[derive(Debug, Clone)] +pub enum ScopeBlockInfo { + Begin { scope: ScopeId, pruned: bool, fallthrough: BlockId }, + End { scope: ScopeId, pruned: bool }, +} + +/// Helper struct for traversing scope blocks in HIR-form. +/// Equivalent to TS `ScopeBlockTraversal` class. +pub struct ScopeBlockTraversal { + /// Live stack of active scopes + active_scopes: Vec, + /// Map from block ID to scope block info + pub block_infos: FxHashMap, +} + +impl ScopeBlockTraversal { + pub fn new() -> Self { + ScopeBlockTraversal { active_scopes: Vec::new(), block_infos: FxHashMap::default() } + } + + /// Record scope information for a block's terminal. + /// Equivalent to TS `recordScopes`. + pub fn record_scopes(&mut self, block: &BasicBlock) { + if let Some(block_info) = self.block_infos.get(&block.id) { + match block_info { + ScopeBlockInfo::Begin { scope, .. } => { + self.active_scopes.push(*scope); + } + ScopeBlockInfo::End { scope, .. } => { + let top = self.active_scopes.last(); + assert_eq!( + Some(scope), + top, + "Expected traversed block fallthrough to match top-most active scope" + ); + self.active_scopes.pop(); + } + } + } + + match &block.terminal { + Terminal::Scope { block: scope_block, fallthrough, scope, .. } => { + assert!( + !self.block_infos.contains_key(scope_block) + && !self.block_infos.contains_key(fallthrough), + "Expected unique scope blocks and fallthroughs" + ); + self.block_infos.insert( + *scope_block, + ScopeBlockInfo::Begin { + scope: *scope, + pruned: false, + fallthrough: *fallthrough, + }, + ); + self.block_infos + .insert(*fallthrough, ScopeBlockInfo::End { scope: *scope, pruned: false }); + } + Terminal::PrunedScope { block: scope_block, fallthrough, scope, .. } => { + assert!( + !self.block_infos.contains_key(scope_block) + && !self.block_infos.contains_key(fallthrough), + "Expected unique scope blocks and fallthroughs" + ); + self.block_infos.insert( + *scope_block, + ScopeBlockInfo::Begin { + scope: *scope, + pruned: true, + fallthrough: *fallthrough, + }, + ); + self.block_infos + .insert(*fallthrough, ScopeBlockInfo::End { scope: *scope, pruned: true }); + } + _ => {} + } + } + + /// Returns true if the given scope is currently 'active', i.e. if the scope start + /// block but not the scope fallthrough has been recorded. + pub fn is_scope_active(&self, scope_id: ScopeId) -> bool { + self.active_scopes.contains(&scope_id) + } + + /// The current, innermost active scope. + pub fn current_scope(&self) -> Option { + self.active_scopes.last().copied() + } +} + +impl Default for ScopeBlockTraversal { + fn default() -> Self { + Self::new() + } +} + +// ============================================================================= +// Convenience wrappers: extract IdentifierIds from Places +// ============================================================================= + +/// Collect all lvalue IdentifierIds from an instruction. +/// Convenience wrapper around `each_instruction_lvalue` that maps to ids. +pub fn each_instruction_lvalue_ids(instr: &Instruction) -> Vec { + each_instruction_lvalue(instr).into_iter().map(|p| p.identifier).collect() +} + +/// Collect all operand IdentifierIds from an instruction. +/// Convenience wrapper around `each_instruction_operand` that maps to ids. +pub fn each_instruction_operand_ids(instr: &Instruction, env: &Environment) -> Vec { + each_instruction_operand(instr, env).into_iter().map(|p| p.identifier).collect() +} + +/// Collect all operand IdentifierIds from an instruction value. +/// Convenience wrapper around `each_instruction_value_operand` that maps to ids. +pub fn each_instruction_value_operand_ids( + value: &InstructionValue, + env: &Environment, +) -> Vec { + each_instruction_value_operand(value, env).into_iter().map(|p| p.identifier).collect() +} + +/// Collect all operand IdentifierIds from a terminal. +/// Convenience wrapper around `each_terminal_operand` that maps to ids. +pub fn each_terminal_operand_ids(terminal: &Terminal) -> Vec { + each_terminal_operand(terminal).into_iter().map(|p| p.identifier).collect() +} + +/// Collect all IdentifierIds from a pattern. +/// Convenience wrapper around `each_pattern_operand` that maps to ids. +pub fn each_pattern_operand_ids(pattern: &Pattern) -> Vec { + each_pattern_operand(pattern).into_iter().map(|p| p.identifier).collect() +} + +// ============================================================================= +// In-place mutation variants (f(&mut Place) callbacks) +// ============================================================================= +// +// These variants use `f(&mut Place)` instead of `f(Place) -> Place`, which is +// more natural for Rust in-place mutation patterns. They do NOT handle +// FunctionExpression/ObjectMethod context (since that requires env access). +// Callers that need to process inner function context should handle it +// separately, e.g.: +// +// for_each_instruction_value_operand_mut(&mut instr.value, &mut |place| { ... }); +// if let InstructionValue::FunctionExpression { lowered_func, .. } +// | InstructionValue::ObjectMethod { lowered_func, .. } = &mut instr.value { +// let func = &mut env.functions[lowered_func.func.0 as usize]; +// for ctx in func.context.iter_mut() { ... } +// } +// + +/// In-place mutation of all operand places in an InstructionValue. +/// Does NOT handle FunctionExpression/ObjectMethod context — callers handle those separately. +pub fn for_each_instruction_value_operand_mut( + value: &mut InstructionValue, + f: &mut impl FnMut(&mut Place), +) { + match value { + InstructionValue::BinaryExpression { left, right, .. } => { + f(left); + f(right); + } + InstructionValue::PropertyLoad { object, .. } + | InstructionValue::PropertyDelete { object, .. } => { + f(object); + } + InstructionValue::PropertyStore { object, value: val, .. } => { + f(object); + f(val); + } + InstructionValue::ComputedLoad { object, property, .. } + | InstructionValue::ComputedDelete { object, property, .. } => { + f(object); + f(property); + } + InstructionValue::ComputedStore { object, property, value: val, .. } => { + f(object); + f(property); + f(val); + } + InstructionValue::DeclareContext { .. } | InstructionValue::DeclareLocal { .. } => {} + InstructionValue::LoadLocal { place, .. } | InstructionValue::LoadContext { place, .. } => { + f(place); + } + InstructionValue::StoreLocal { value: val, .. } => { + f(val); + } + InstructionValue::StoreContext { lvalue, value: val, .. } => { + f(&mut lvalue.place); + f(val); + } + InstructionValue::StoreGlobal { value: val, .. } => { + f(val); + } + InstructionValue::Destructure { value: val, .. } => { + f(val); + } + InstructionValue::NewExpression { callee, args, .. } + | InstructionValue::CallExpression { callee, args, .. } => { + f(callee); + for_each_call_argument_mut(args, f); + } + InstructionValue::MethodCall { receiver, property, args, .. } => { + f(receiver); + f(property); + for_each_call_argument_mut(args, f); + } + InstructionValue::UnaryExpression { value: val, .. } => { + f(val); + } + InstructionValue::JsxExpression { tag, props, children, .. } => { + if let JsxTag::Place(place) = tag { + f(place); + } + for attribute in props.iter_mut() { + match attribute { + JsxAttribute::Attribute { place, .. } => f(place), + JsxAttribute::SpreadAttribute { argument, .. } => f(argument), + } + } + if let Some(children) = children { + for child in children.iter_mut() { + f(child); + } + } + } + InstructionValue::ObjectExpression { properties, .. } => { + for property in properties.iter_mut() { + match property { + ObjectPropertyOrSpread::Property(prop) => { + if let ObjectPropertyKey::Computed { name } = &mut prop.key { + f(name); + } + f(&mut prop.place); + } + ObjectPropertyOrSpread::Spread(spread) => { + f(&mut spread.place); + } + } + } + } + InstructionValue::ArrayExpression { elements, .. } => { + for elem in elements.iter_mut() { + match elem { + ArrayElement::Place(p) => f(p), + ArrayElement::Spread(s) => f(&mut s.place), + ArrayElement::Hole => {} + } + } + } + InstructionValue::JsxFragment { children, .. } => { + for child in children.iter_mut() { + f(child); + } + } + InstructionValue::FunctionExpression { .. } | InstructionValue::ObjectMethod { .. } => { + // Context places require env access — callers handle separately. + } + InstructionValue::TaggedTemplateExpression { tag, subexprs, .. } => { + f(tag); + for expr in subexprs.iter_mut() { + f(expr); + } + } + InstructionValue::TypeCastExpression { value: val, .. } => { + f(val); + } + InstructionValue::TemplateLiteral { subexprs, .. } => { + for expr in subexprs.iter_mut() { + f(expr); + } + } + InstructionValue::Await { value: val, .. } => { + f(val); + } + InstructionValue::GetIterator { collection, .. } => { + f(collection); + } + InstructionValue::IteratorNext { iterator, collection, .. } => { + f(iterator); + f(collection); + } + InstructionValue::NextPropertyOf { value: val, .. } => { + f(val); + } + InstructionValue::PostfixUpdate { value: val, .. } + | InstructionValue::PrefixUpdate { value: val, .. } => { + f(val); + } + InstructionValue::StartMemoize { deps, .. } => { + if let Some(deps) = deps { + for dep in deps.iter_mut() { + if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &mut dep.root { + f(value); + } + } + } + } + InstructionValue::FinishMemoize { decl, .. } => { + f(decl); + } + InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::JSXText { .. } => {} + } +} + +/// In-place mutation of call arguments. +pub fn for_each_call_argument_mut(args: &mut [PlaceOrSpread], f: &mut impl FnMut(&mut Place)) { + for arg in args.iter_mut() { + match arg { + PlaceOrSpread::Place(place) => f(place), + PlaceOrSpread::Spread(spread) => f(&mut spread.place), + } + } +} + +/// In-place mutation of an InstructionValue's lvalues (DeclareLocal, StoreLocal, DeclareContext, +/// StoreContext, Destructure, PostfixUpdate, PrefixUpdate). Does NOT include the instruction's +/// top-level lvalue — use `for_each_instruction_lvalue_mut` for that. +pub fn for_each_instruction_value_lvalue_mut( + value: &mut InstructionValue, + f: &mut impl FnMut(&mut Place), +) { + match value { + InstructionValue::DeclareContext { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } + | InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } => { + f(&mut lvalue.place); + } + InstructionValue::Destructure { lvalue, .. } => { + for_each_pattern_operand_mut(&mut lvalue.pattern, f); + } + InstructionValue::PostfixUpdate { lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue, .. } => { + f(lvalue); + } + _ => {} + } +} + +/// In-place mutation of the instruction's lvalue and value's lvalues. +/// Matches the same variants as TS `mapInstructionLValues` (skips DeclareContext/StoreContext). +pub fn for_each_instruction_lvalue_mut(instr: &mut Instruction, f: &mut impl FnMut(&mut Place)) { + match &mut instr.value { + InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } => { + f(&mut lvalue.place); + } + InstructionValue::Destructure { lvalue, .. } => { + for_each_pattern_operand_mut(&mut lvalue.pattern, f); + } + InstructionValue::PostfixUpdate { lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue, .. } => { + f(lvalue); + } + _ => {} + } + f(&mut instr.lvalue); +} + +/// In-place mutation of pattern operands. +pub fn for_each_pattern_operand_mut(pattern: &mut Pattern, f: &mut impl FnMut(&mut Place)) { + match pattern { + Pattern::Array(arr) => { + for item in arr.items.iter_mut() { + match item { + ArrayPatternElement::Place(p) => f(p), + ArrayPatternElement::Spread(s) => f(&mut s.place), + ArrayPatternElement::Hole => {} + } + } + } + Pattern::Object(obj) => { + for property in obj.properties.iter_mut() { + match property { + ObjectPropertyOrSpread::Property(prop) => f(&mut prop.place), + ObjectPropertyOrSpread::Spread(spread) => f(&mut spread.place), + } + } + } + } +} + +/// In-place mutation of terminal operand places. +pub fn for_each_terminal_operand_mut(terminal: &mut Terminal, f: &mut impl FnMut(&mut Place)) { + match terminal { + Terminal::If { test, .. } | Terminal::Branch { test, .. } => { + f(test); + } + Terminal::Switch { test, cases, .. } => { + f(test); + for case in cases.iter_mut() { + if let Some(t) = &mut case.test { + f(t); + } + } + } + Terminal::Return { value, .. } | Terminal::Throw { value, .. } => { + f(value); + } + Terminal::Try { handler_binding, .. } => { + if let Some(binding) = handler_binding { + f(binding); + } + } + Terminal::MaybeThrow { .. } + | Terminal::Sequence { .. } + | Terminal::Label { .. } + | Terminal::Optional { .. } + | Terminal::Ternary { .. } + | Terminal::Logical { .. } + | Terminal::DoWhile { .. } + | Terminal::While { .. } + | Terminal::For { .. } + | Terminal::ForOf { .. } + | Terminal::ForIn { .. } + | Terminal::Goto { .. } + | Terminal::Unreachable { .. } + | Terminal::Unsupported { .. } + | Terminal::Scope { .. } + | Terminal::PrunedScope { .. } => {} + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/align_method_call_scopes.rs b/crates/oxc_react_compiler/src/react_compiler_inference/align_method_call_scopes.rs new file mode 100644 index 0000000000000..72372440a7ffa --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/align_method_call_scopes.rs @@ -0,0 +1,141 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Ensures that method call instructions have scopes such that either: +//! - Both the MethodCall and its property have the same scope +//! - OR neither has a scope +//! +//! Ported from TypeScript `src/ReactiveScopes/AlignMethodCallScopes.ts`. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + EvaluationOrder, HirFunction, IdentifierId, InstructionValue, ScopeId, +}; +use crate::react_compiler_utils::DisjointSet; + +// ============================================================================= +// Public API +// ============================================================================= + +/// Aligns method call scopes so that either both the MethodCall result and its +/// property operand share the same scope, or neither has a scope. +/// +/// Corresponds to TS `alignMethodCallScopes(fn: HIRFunction): void`. +pub fn align_method_call_scopes(func: &mut HirFunction, env: &mut Environment) { + // Maps an identifier to the scope it should be assigned to (or None to remove scope) + let mut scope_mapping: FxHashMap> = FxHashMap::default(); + let mut merged_scopes = DisjointSet::::new(); + + // Phase 1: Walk instructions and collect scope relationships + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::MethodCall { property, .. } => { + let lvalue_scope = env.identifiers[instr.lvalue.identifier.0 as usize].scope; + let property_scope = env.identifiers[property.identifier.0 as usize].scope; + + match (lvalue_scope, property_scope) { + (Some(lvalue_sid), Some(property_sid)) => { + // Both have a scope: merge the scopes + merged_scopes.union(&[lvalue_sid, property_sid]); + } + (Some(lvalue_sid), None) => { + // Call has a scope but not the property: + // record that this property should be in this scope + scope_mapping.insert(property.identifier, Some(lvalue_sid)); + } + (None, Some(_)) => { + // Property has a scope but call doesn't: + // this property does not need a scope + scope_mapping.insert(property.identifier, None); + } + (None, None) => { + // Neither has a scope, nothing to do + } + } + } + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + // Recurse into inner functions + let func_id = lowered_func.func; + let mut inner_func = std::mem::replace( + &mut env.functions[func_id.0 as usize], + crate::react_compiler_ssa::enter_ssa::placeholder_function(), + ); + align_method_call_scopes(&mut inner_func, env); + env.functions[func_id.0 as usize] = inner_func; + } + _ => {} + } + } + } + + // Phase 2: Merge scope ranges for unioned scopes. + // Use a FxHashMap to accumulate min/max across all scopes mapping to the same root, + // matching TS behavior where root.range is updated in-place during iteration. + let mut range_updates: FxHashMap = + FxHashMap::default(); + + merged_scopes.for_each(|scope_id, root_id| { + if scope_id == root_id { + return; + } + let scope_range = env.scopes[scope_id.0 as usize].range.clone(); + let root_range = env.scopes[root_id.0 as usize].range.clone(); + + let entry = + range_updates.entry(root_id).or_insert_with(|| (root_range.start, root_range.end)); + entry.0 = EvaluationOrder(std::cmp::min(entry.0.0, scope_range.start.0)); + entry.1 = EvaluationOrder(std::cmp::max(entry.1.0, scope_range.end.0)); + }); + + // Save original scope range IDs before updating + let original_range_ids: FxHashMap = + range_updates + .keys() + .map(|&root_id| { + let range_id = env.scopes[root_id.0 as usize].range.id; + (root_id, range_id) + }) + .collect(); + + for (root_id, (new_start, new_end)) in &range_updates { + env.scopes[root_id.0 as usize].range.start = *new_start; + env.scopes[root_id.0 as usize].range.end = *new_end; + } + + // Sync identifier mutable_ranges that shared the old scope range. + // Uses MutableRangeId for exact identity matching instead of value comparison. + for ident in &mut env.identifiers { + if let Some(scope_id) = ident.scope { + if let Some(&orig_range_id) = original_range_ids.get(&scope_id) { + if ident.mutable_range.id == orig_range_id { + let new_range = &env.scopes[scope_id.0 as usize].range; + ident.mutable_range.start = new_range.start; + ident.mutable_range.end = new_range.end; + } + } + } + } + + // Phase 3: Apply scope mappings and merged scope reassignments + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let lvalue_id = func.instructions[instr_id.0 as usize].lvalue.identifier; + + if let Some(mapped_scope) = scope_mapping.get(&lvalue_id) { + env.identifiers[lvalue_id.0 as usize].scope = *mapped_scope; + } else if let Some(current_scope) = env.identifiers[lvalue_id.0 as usize].scope { + // TS: mergedScopes.find() returns null if not in the set + if let Some(merged) = merged_scopes.find_opt(current_scope) { + env.identifiers[lvalue_id.0 as usize].scope = Some(merged); + } + } + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/align_object_method_scopes.rs b/crates/oxc_react_compiler/src/react_compiler_inference/align_object_method_scopes.rs new file mode 100644 index 0000000000000..5d9550e0226cf --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/align_object_method_scopes.rs @@ -0,0 +1,169 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Aligns scopes of object method values to that of their enclosing object expressions. +//! To produce a well-formed JS program in Codegen, object methods and object expressions +//! must be in the same ReactiveBlock as object method definitions must be inlined. +//! +//! Ported from TypeScript `src/ReactiveScopes/AlignObjectMethodScopes.ts`. + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::cmp; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + EvaluationOrder, HirFunction, IdentifierId, InstructionValue, ObjectPropertyOrSpread, ScopeId, +}; +use crate::react_compiler_utils::DisjointSet; + +// ============================================================================= +// findScopesToMerge +// ============================================================================= + +/// Identifies ObjectMethod lvalue identifiers and then finds ObjectExpression +/// instructions whose operands reference those methods. Returns a disjoint set +/// of scopes that must be merged. +fn find_scopes_to_merge(func: &HirFunction, env: &Environment) -> DisjointSet { + let mut object_method_decls: FxHashSet = FxHashSet::default(); + let mut merged_scopes = DisjointSet::::new(); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::ObjectMethod { .. } => { + object_method_decls.insert(instr.lvalue.identifier); + } + InstructionValue::ObjectExpression { properties, .. } => { + for prop_or_spread in properties { + let operand_place = match prop_or_spread { + ObjectPropertyOrSpread::Property(prop) => &prop.place, + ObjectPropertyOrSpread::Spread(spread) => &spread.place, + }; + if object_method_decls.contains(&operand_place.identifier) { + let operand_scope = + env.identifiers[operand_place.identifier.0 as usize].scope; + let lvalue_scope = + env.identifiers[instr.lvalue.identifier.0 as usize].scope; + + // TS: CompilerError.invariant(operandScope != null && lvalueScope != null, ...) + let operand_sid = operand_scope.expect( + "Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.", + ); + let lvalue_sid = lvalue_scope.expect( + "Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.", + ); + merged_scopes.union(&[operand_sid, lvalue_sid]); + } + } + } + _ => {} + } + } + } + + merged_scopes +} + +// ============================================================================= +// Public API +// ============================================================================= + +/// Aligns object method scopes so that ObjectMethod values and their enclosing +/// ObjectExpression share the same scope. +/// +/// Corresponds to TS `alignObjectMethodScopes(fn: HIRFunction): void`. +pub fn align_object_method_scopes(func: &mut HirFunction, env: &mut Environment) { + // Handle inner functions first (TS recurses before processing the outer function) + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let func_id = lowered_func.func; + let mut inner_func = std::mem::replace( + &mut env.functions[func_id.0 as usize], + crate::react_compiler_ssa::enter_ssa::placeholder_function(), + ); + align_object_method_scopes(&mut inner_func, env); + env.functions[func_id.0 as usize] = inner_func; + } + _ => {} + } + } + } + + let mut merged_scopes = find_scopes_to_merge(func, env); + + // Step 1: Merge affected scopes to their canonical root. + // Use a FxHashMap to accumulate min/max across all scopes mapping to the same root, + // matching TS behavior where root.range is updated in-place during iteration. + let mut range_updates: FxHashMap = + FxHashMap::default(); + + merged_scopes.for_each(|scope_id, root_id| { + if scope_id == root_id { + return; + } + let scope_range = env.scopes[scope_id.0 as usize].range.clone(); + let root_range = env.scopes[root_id.0 as usize].range.clone(); + + let entry = + range_updates.entry(root_id).or_insert_with(|| (root_range.start, root_range.end)); + entry.0 = EvaluationOrder(cmp::min(entry.0.0, scope_range.start.0)); + entry.1 = EvaluationOrder(cmp::max(entry.1.0, scope_range.end.0)); + }); + + // Save original scope range IDs before updating + let original_range_ids: FxHashMap = + range_updates + .keys() + .map(|&root_id| { + let range_id = env.scopes[root_id.0 as usize].range.id; + (root_id, range_id) + }) + .collect(); + + for (root_id, (new_start, new_end)) in &range_updates { + env.scopes[root_id.0 as usize].range.start = *new_start; + env.scopes[root_id.0 as usize].range.end = *new_end; + } + + // Sync identifier mutable_ranges that shared the old scope range. + // Uses MutableRangeId for exact identity matching instead of value comparison. + for ident in &mut env.identifiers { + if let Some(scope_id) = ident.scope { + if let Some(&orig_range_id) = original_range_ids.get(&scope_id) { + if ident.mutable_range.id == orig_range_id { + let new_range = &env.scopes[scope_id.0 as usize].range; + ident.mutable_range.start = new_range.start; + ident.mutable_range.end = new_range.end; + } + } + } + } + + // Step 2: Repoint identifiers whose scopes were merged + // Build a map from old scope -> root scope for quick lookup + let mut scope_remap: FxHashMap = FxHashMap::default(); + merged_scopes.for_each(|scope_id, root_id| { + if scope_id != root_id { + scope_remap.insert(scope_id, root_id); + } + }); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let lvalue_id = func.instructions[instr_id.0 as usize].lvalue.identifier; + + if let Some(current_scope) = env.identifiers[lvalue_id.0 as usize].scope { + if let Some(&root) = scope_remap.get(¤t_scope) { + env.identifiers[lvalue_id.0 as usize].scope = Some(root); + } + } + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/align_reactive_scopes_to_block_scopes_hir.rs b/crates/oxc_react_compiler/src/react_compiler_inference/align_reactive_scopes_to_block_scopes_hir.rs new file mode 100644 index 0000000000000..2df4885778a0b --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/align_reactive_scopes_to_block_scopes_hir.rs @@ -0,0 +1,315 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Aligns reactive scope boundaries to block scope boundaries in the HIR. +//! +//! Ported from TypeScript `src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts`. +//! +//! This is the 2nd of 4 passes that determine how to break a function into +//! discrete reactive scopes (independently memoizable units of code): +//! 1. InferReactiveScopeVariables (on HIR) determines operands that mutate +//! together and assigns them a unique reactive scope. +//! 2. AlignReactiveScopesToBlockScopes (this pass) aligns reactive scopes +//! to block scopes. +//! 3. MergeOverlappingReactiveScopes ensures scopes do not overlap. +//! 4. BuildReactiveBlocks groups the statements for each scope. +//! +//! Prior inference passes assign a reactive scope to each operand, but the +//! ranges of these scopes are based on specific instructions at arbitrary +//! points in the control-flow graph. However, to codegen blocks around the +//! instructions in each scope, the scopes must be aligned to block-scope +//! boundaries — we can't memoize half of a loop! + +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::BlockId; +use crate::react_compiler_hir::BlockKind; +use crate::react_compiler_hir::EvaluationOrder; +use crate::react_compiler_hir::HirFunction; +use crate::react_compiler_hir::IdentifierId; +use crate::react_compiler_hir::MutableRange; +use crate::react_compiler_hir::ScopeId; +use crate::react_compiler_hir::Terminal; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::visitors::each_instruction_lvalue_ids; +use crate::react_compiler_hir::visitors::each_instruction_value_operand_ids; +use crate::react_compiler_hir::visitors::each_terminal_operand_ids; + +// ============================================================================= +// ValueBlockNode — stores the valueRange for scope alignment in value blocks +// ============================================================================= + +/// Tracks the value range for a value block. The `children` field from the TS +/// implementation is only used for debug output and is omitted here. +#[derive(Clone)] +struct ValueBlockNode { + value_range: MutableRange, +} + +/// Returns all block IDs referenced by a terminal, including both direct +/// successors and fallthrough. +fn all_terminal_block_ids(terminal: &Terminal) -> Vec { + visitors::each_terminal_all_successors(terminal) +} + +// ============================================================================= +// Helper: get the first EvaluationOrder in a block +// ============================================================================= + +fn block_first_id(func: &HirFunction, block_id: BlockId) -> EvaluationOrder { + let block = func.body.blocks.get(&block_id).unwrap(); + if !block.instructions.is_empty() { + func.instructions[block.instructions[0].0 as usize].id + } else { + block.terminal.evaluation_order() + } +} + +// ============================================================================= +// BlockFallthroughRange +// ============================================================================= + +#[derive(Clone)] +struct BlockFallthroughRange { + fallthrough: BlockId, + range: MutableRange, +} + +// ============================================================================= +// Public API +// ============================================================================= + +/// Aligns reactive scope boundaries to block scope boundaries in the HIR. +/// +/// This pass updates reactive scope boundaries to align to control flow +/// boundaries. For example, if a scope ends partway through an if consequent, +/// the scope is extended to the end of the consequent block. +pub fn align_reactive_scopes_to_block_scopes_hir(func: &mut HirFunction, env: &mut Environment) { + // Save original scope ranges BEFORE this pass modifies them. + // In TS, identifier.mutableRange and scope.range may or may not be the same + // JS object. Only identifiers whose mutableRange IS the scope's range object + // (same reference) automatically see scope range modifications. In Rust, we + // simulate this by recording original ranges and only syncing identifiers + // whose mutableRange matches the original. + let original_scope_ranges: Vec = + env.scopes.iter().map(|s| s.range.clone()).collect(); + + let mut active_block_fallthrough_ranges: Vec = Vec::new(); + let mut active_scopes: FxHashSet = FxHashSet::default(); + let mut seen: FxHashSet = FxHashSet::default(); + let mut value_block_nodes: FxHashMap = FxHashMap::default(); + + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + + for &block_id in &block_ids { + let starting_id = block_first_id(func, block_id); + + // Retain only active scopes whose range.end > startingId + active_scopes.retain(|&scope_id| env.scopes[scope_id.0 as usize].range.end > starting_id); + + // Check if we've reached a fallthrough block + if let Some(top) = active_block_fallthrough_ranges.last().cloned() { + if top.fallthrough == block_id { + active_block_fallthrough_ranges.pop(); + // All active scopes overlap this block-fallthrough range; + // extend their start to include the range start. + for &scope_id in &active_scopes { + let scope = &mut env.scopes[scope_id.0 as usize]; + scope.range.start = std::cmp::min(scope.range.start, top.range.start); + } + } + } + + let node = value_block_nodes.get(&block_id).cloned(); + + // Visit instruction lvalues and operands + let block = func.body.blocks.get(&block_id).unwrap(); + let instr_ids: Vec = + block.instructions.iter().copied().collect(); + for &instr_id in &instr_ids { + let instr = &func.instructions[instr_id.0 as usize]; + let eval_order = instr.id; + + let lvalue_ids = each_instruction_lvalue_ids(instr); + for lvalue_id in lvalue_ids { + record_place_id(eval_order, lvalue_id, &node, env, &mut active_scopes, &mut seen); + } + + let operand_ids = each_instruction_value_operand_ids(&instr.value, env); + for operand_id in operand_ids { + record_place_id(eval_order, operand_id, &node, env, &mut active_scopes, &mut seen); + } + } + + // Visit terminal operands + let block = func.body.blocks.get(&block_id).unwrap(); + let terminal_eval_order = block.terminal.evaluation_order(); + let terminal_operand_ids = each_terminal_operand_ids(&block.terminal); + for operand_id in terminal_operand_ids { + record_place_id( + terminal_eval_order, + operand_id, + &node, + env, + &mut active_scopes, + &mut seen, + ); + } + + let block = func.body.blocks.get(&block_id).unwrap(); + let terminal = &block.terminal; + let fallthrough = visitors::terminal_fallthrough(terminal); + let is_branch = matches!(terminal, Terminal::Branch { .. }); + let is_goto = match terminal { + Terminal::Goto { block, .. } => Some(*block), + _ => None, + }; + let is_ternary_logical_optional = matches!( + terminal, + Terminal::Ternary { .. } | Terminal::Logical { .. } | Terminal::Optional { .. } + ); + let all_successors = all_terminal_block_ids(terminal); + + // Handle fallthrough logic + if let Some(ft) = fallthrough { + if !is_branch { + let next_id = block_first_id(func, ft); + + for &scope_id in &active_scopes { + let scope = &mut env.scopes[scope_id.0 as usize]; + if scope.range.end > terminal_eval_order { + scope.range.end = std::cmp::max(scope.range.end, next_id); + } + } + + active_block_fallthrough_ranges.push(BlockFallthroughRange { + fallthrough: ft, + range: env.new_mutable_range(terminal_eval_order, next_id), + }); + + assert!( + !value_block_nodes.contains_key(&ft), + "Expect hir blocks to have unique fallthroughs" + ); + if let Some(n) = &node { + value_block_nodes.insert(ft, n.clone()); + } + } + } else if let Some(goto_block) = is_goto { + // Handle goto to label + let start_pos = + active_block_fallthrough_ranges.iter().position(|r| r.fallthrough == goto_block); + let top_idx = if active_block_fallthrough_ranges.is_empty() { + None + } else { + Some(active_block_fallthrough_ranges.len() - 1) + }; + if let Some(pos) = start_pos { + if top_idx != Some(pos) { + let start_range = active_block_fallthrough_ranges[pos].clone(); + let first_id = block_first_id(func, start_range.fallthrough); + + for &scope_id in &active_scopes { + let scope = &mut env.scopes[scope_id.0 as usize]; + if scope.range.end <= terminal_eval_order { + continue; + } + scope.range.start = + std::cmp::min(start_range.range.start, scope.range.start); + scope.range.end = std::cmp::max(first_id, scope.range.end); + } + } + } + } + + // Visit all successors to set up value block nodes + for successor in all_successors { + if value_block_nodes.contains_key(&successor) { + continue; + } + + let successor_block = func.body.blocks.get(&successor).unwrap(); + if successor_block.kind == BlockKind::Block || successor_block.kind == BlockKind::Catch + { + // Block or catch kind: don't create a value block node + } else if node.is_none() || is_ternary_logical_optional { + // Create a new node when transitioning non-value -> value, + // or for ternary/logical/optional terminals. + let value_range = if node.is_none() { + // Transition from block -> value block + let ft = fallthrough.expect("Expected a fallthrough for value block"); + let next_id = block_first_id(func, ft); + env.new_mutable_range(terminal_eval_order, next_id) + } else { + // Value -> value transition (ternary/logical/optional): reuse range + node.as_ref().unwrap().value_range.clone() + }; + + value_block_nodes.insert(successor, ValueBlockNode { value_range }); + } else { + // Value -> value block transition: reuse the node + if let Some(n) = &node { + value_block_nodes.insert(successor, n.clone()); + } + } + } + } + + // Sync identifier mutable_range with their scope's range, but ONLY for + // identifiers whose mutable_range matched their scope's ORIGINAL range + // (before this pass modified it). In TS, identifier.mutableRange and + // scope.range are only the same JS object for identifiers that were the + // "canonical" representative when the scope was created. Other identifiers + // in the same scope have independent mutableRange objects that should NOT + // be updated when the scope's range changes. + for ident in &mut env.identifiers { + if let Some(scope_id) = ident.scope { + let original = &original_scope_ranges[scope_id.0 as usize]; + if ident.mutable_range.same_range(original) { + let scope_range = &env.scopes[scope_id.0 as usize].range; + ident.mutable_range.start = scope_range.start; + ident.mutable_range.end = scope_range.end; + } + } + } +} + +/// Records a place's scope as active and adjusts scope ranges for value blocks. +/// +/// Mirrors TS `recordPlace(id, place, node)`. +fn record_place_id( + id: EvaluationOrder, + identifier_id: IdentifierId, + node: &Option, + env: &mut Environment, + active_scopes: &mut FxHashSet, + seen: &mut FxHashSet, +) { + // Get the scope for this identifier, if active at this instruction + let scope_id = match env.identifiers[identifier_id.0 as usize].scope { + Some(scope_id) => { + let scope = &env.scopes[scope_id.0 as usize]; + if id >= scope.range.start && id < scope.range.end { Some(scope_id) } else { None } + } + None => None, + }; + + if let Some(scope_id) = scope_id { + active_scopes.insert(scope_id); + + if seen.contains(&scope_id) { + return; + } + seen.insert(scope_id); + + if let Some(n) = node { + let scope = &mut env.scopes[scope_id.0 as usize]; + scope.range.start = std::cmp::min(n.value_range.start, scope.range.start); + scope.range.end = std::cmp::max(n.value_range.end, scope.range.end); + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/analyse_functions.rs b/crates/oxc_react_compiler/src/react_compiler_inference/analyse_functions.rs new file mode 100644 index 0000000000000..2de5c958262c9 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/analyse_functions.rs @@ -0,0 +1,215 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Recursively analyzes nested function expressions and object methods to infer +//! their aliasing effect signatures. +//! +//! Ported from TypeScript `src/Inference/AnalyseFunctions.ts`. +//! +//! Runs inferMutationAliasingEffects, deadCodeElimination, +//! inferMutationAliasingRanges, rewriteInstructionKindsBasedOnReassignment, +//! and inferReactiveScopeVariables on each inner function. + +use crate::react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_utils::FxIndexMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::{ + AliasingEffect, BlockId, Effect, EvaluationOrder, FunctionId, HIR, HirFunction, IdentifierId, + InstructionValue, Place, ReactFunctionType, +}; + +/// Analyse all nested function expressions and object methods in `func`. +/// +/// For each inner function found, runs `lower_with_mutation_aliasing` to infer +/// its aliasing effects, then resets context variable mutable ranges. +/// +/// The optional `debug_logger` callback is invoked after processing each inner +/// function, receiving `(&HirFunction, &Environment)` so the caller can produce +/// debug output. This mirrors the TS `fn.env.logger?.debugLogIRs` call inside +/// `lowerWithMutationAliasing`. +/// +/// Corresponds to TS `analyseFunctions(func: HIRFunction): void`. +pub fn analyse_functions( + func: &mut HirFunction, + env: &mut Environment, + debug_logger: &mut F, +) -> Result<(), CompilerDiagnostic> +where + F: FnMut(&HirFunction, &Environment), +{ + // Collect FunctionIds from FunctionExpression/ObjectMethod instructions. + // We collect first to avoid borrow conflicts with env.functions. + let mut inner_func_ids: Vec = Vec::new(); + for (_block_id, block) in &func.body.blocks { + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + inner_func_ids.push(lowered_func.func); + } + _ => {} + } + } + } + + // Process each inner function + for func_id in inner_func_ids { + // Take the inner function out of the arena to avoid borrow conflicts + let mut inner_func = + std::mem::replace(&mut env.functions[func_id.0 as usize], placeholder_function()); + + lower_with_mutation_aliasing(&mut inner_func, env, debug_logger)?; + + // If an invariant error was recorded, put the function back and stop processing + if env.has_invariant_errors() { + env.functions[func_id.0 as usize] = inner_func; + return Ok(()); + } + + // Reset mutable range for outer inferMutationAliasingEffects. + // + // NOTE: inferReactiveScopeVariables makes identifiers in the scope + // point to the *same* mutableRange instance (in TS). In Rust, scopes + // are stored in an arena, so we reset both the identifier's range + // and clear its scope. + for operand in &inner_func.context { + let new_range = env.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0)); + let ident = &mut env.identifiers[operand.identifier.0 as usize]; + ident.mutable_range = new_range; + ident.scope = None; + } + + // Put the function back + env.functions[func_id.0 as usize] = inner_func; + } + + Ok(()) +} + +/// Run mutation/aliasing inference on an inner function. +/// +/// Corresponds to TS `lowerWithMutationAliasing(fn: HIRFunction): void`. +fn lower_with_mutation_aliasing( + func: &mut HirFunction, + env: &mut Environment, + debug_logger: &mut F, +) -> Result<(), CompilerDiagnostic> +where + F: FnMut(&HirFunction, &Environment), +{ + // Phase 1: Recursively analyse nested functions first (depth-first) + analyse_functions(func, env, debug_logger)?; + + // inferMutationAliasingEffects on the inner function + crate::react_compiler_inference::infer_mutation_aliasing_effects::infer_mutation_aliasing_effects(func, env, true)?; + + // Check for invariant errors (e.g., uninitialized value kind) + // In TS, these throw from within inferMutationAliasingEffects, aborting + // the rest of the function processing. + if env.has_invariant_errors() { + return Ok(()); + } + + // deadCodeElimination for inner functions + crate::react_compiler_optimization::dead_code_elimination(func, env); + + // inferMutationAliasingRanges — returns the externally-visible function effects + let function_effects = + crate::react_compiler_inference::infer_mutation_aliasing_ranges::infer_mutation_aliasing_ranges(func, env, true)?; + + // rewriteInstructionKindsBasedOnReassignment + if let Err(err) = + crate::react_compiler_ssa::rewrite_instruction_kinds_based_on_reassignment(func, env) + { + env.errors.merge(err); + return Ok(()); + } + + // inferReactiveScopeVariables on the inner function + crate::react_compiler_inference::infer_reactive_scope_variables::infer_reactive_scope_variables(func, env)?; + + func.aliasing_effects = Some(function_effects.clone()); + + // Phase 2: Populate the Effect of each context variable to use in inferring + // the outer function. Corresponds to TS Phase 2 in lowerWithMutationAliasing. + let mut captured_or_mutated: FxHashSet = FxHashSet::default(); + for effect in &function_effects { + match effect { + AliasingEffect::Assign { from, .. } + | AliasingEffect::Alias { from, .. } + | AliasingEffect::Capture { from, .. } + | AliasingEffect::CreateFrom { from, .. } + | AliasingEffect::MaybeAlias { from, .. } => { + captured_or_mutated.insert(from.identifier); + } + AliasingEffect::Mutate { value, .. } + | AliasingEffect::MutateConditionally { value } + | AliasingEffect::MutateTransitive { value } + | AliasingEffect::MutateTransitiveConditionally { value } => { + captured_or_mutated.insert(value.identifier); + } + AliasingEffect::Impure { .. } + | AliasingEffect::Render { .. } + | AliasingEffect::MutateFrozen { .. } + | AliasingEffect::MutateGlobal { .. } + | AliasingEffect::CreateFunction { .. } + | AliasingEffect::Create { .. } + | AliasingEffect::Freeze { .. } + | AliasingEffect::ImmutableCapture { .. } => { + // no-op + } + AliasingEffect::Apply { .. } => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects", + None, + )); + } + } + } + + for operand in &mut func.context { + if captured_or_mutated.contains(&operand.identifier) || operand.effect == Effect::Capture { + operand.effect = Effect::Capture; + } else { + operand.effect = Effect::Read; + } + } + + // Log the inner function's state (mirrors TS: fn.env.logger?.debugLogIRs) + debug_logger(func, env); + + Ok(()) +} + +/// Create a placeholder HirFunction for temporarily swapping an inner function +/// out of `env.functions` via `std::mem::replace`. The placeholder is never +/// read — the real function is swapped back immediately after processing. +fn placeholder_function<'a>() -> HirFunction<'a> { + HirFunction { + loc: None, + id: None, + name_hint: None, + fn_type: ReactFunctionType::Other, + params: Vec::new(), + return_type_annotation: None, + returns: Place { + identifier: IdentifierId(0), + effect: Effect::Unknown, + reactive: false, + loc: None, + }, + context: Vec::new(), + body: HIR { entry: BlockId(0), blocks: FxIndexMap::default() }, + instructions: Vec::new(), + generator: false, + is_async: false, + directives: Vec::new(), + aliasing_effects: None, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/build_reactive_scope_terminals_hir.rs b/crates/oxc_react_compiler/src/react_compiler_inference/build_reactive_scope_terminals_hir.rs new file mode 100644 index 0000000000000..c9583fb197883 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/build_reactive_scope_terminals_hir.rs @@ -0,0 +1,395 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Builds reactive scope terminals in the HIR. +//! +//! Given a function whose reactive scope ranges have been correctly aligned and +//! merged, this pass rewrites blocks to introduce ReactiveScopeTerminals and +//! their fallthrough blocks. +//! +//! Ported from TypeScript `src/HIR/BuildReactiveScopeTerminalsHIR.ts`. + +use crate::react_compiler_utils::FxIndexSet; +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::BasicBlock; +use crate::react_compiler_hir::BlockId; +use crate::react_compiler_hir::EvaluationOrder; +use crate::react_compiler_hir::GotoVariant; +use crate::react_compiler_hir::HirFunction; +use crate::react_compiler_hir::IdentifierId; +use crate::react_compiler_hir::ScopeId; +use crate::react_compiler_hir::Terminal; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::each_instruction_lvalue_ids; +use crate::react_compiler_hir::visitors::each_instruction_operand_ids; +use crate::react_compiler_hir::visitors::each_terminal_operand_ids; +use crate::react_compiler_lowering::get_reverse_postordered_blocks; +use crate::react_compiler_lowering::mark_instruction_ids; +use crate::react_compiler_lowering::mark_predecessors; +use crate::react_compiler_utils::FxIndexMap; + +// ============================================================================= +// getScopes +// ============================================================================= + +/// Collect all unique scopes from places in the function that have non-empty ranges. +/// Corresponds to TS `getScopes(fn)`. +fn get_scopes(func: &HirFunction, env: &Environment) -> Vec { + let mut scope_ids: FxHashSet = FxHashSet::default(); + + let mut visit_place = |identifier_id: IdentifierId| { + if let Some(scope_id) = env.identifiers[identifier_id.0 as usize].scope { + let range = &env.scopes[scope_id.0 as usize].range; + if range.start != range.end { + scope_ids.insert(scope_id); + } + } + }; + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + // lvalues + for id in each_instruction_lvalue_ids(instr) { + visit_place(id); + } + // operands + for id in each_instruction_operand_ids(instr, env) { + visit_place(id); + } + } + // terminal operands + for id in each_terminal_operand_ids(&block.terminal) { + visit_place(id); + } + } + + scope_ids.into_iter().collect() +} + +// ============================================================================= +// TerminalRewriteInfo +// ============================================================================= + +enum TerminalRewriteInfo { + StartScope { + block_id: BlockId, + fallthrough_id: BlockId, + instr_id: EvaluationOrder, + scope_id: ScopeId, + }, + EndScope { + instr_id: EvaluationOrder, + fallthrough_id: BlockId, + }, +} + +impl TerminalRewriteInfo { + fn instr_id(&self) -> EvaluationOrder { + match self { + TerminalRewriteInfo::StartScope { instr_id, .. } => *instr_id, + TerminalRewriteInfo::EndScope { instr_id, .. } => *instr_id, + } + } +} + +// ============================================================================= +// collectScopeRewrites +// ============================================================================= + +/// Collect all scope rewrites by traversing scopes in pre-order. +fn collect_scope_rewrites(func: &HirFunction, env: &mut Environment) -> Vec { + let scope_ids = get_scopes(func, env); + + // Sort: ascending by start, descending by end for ties + let mut items: Vec = scope_ids; + items.sort_by(|a, b| { + let a_range = &env.scopes[a.0 as usize].range; + let b_range = &env.scopes[b.0 as usize].range; + let start_diff = a_range.start.0.cmp(&b_range.start.0); + if start_diff != std::cmp::Ordering::Equal { + return start_diff; + } + b_range.end.0.cmp(&a_range.end.0) + }); + + let mut rewrites: Vec = Vec::new(); + let mut fallthroughs: FxHashMap = FxHashMap::default(); + let mut active_items: Vec = Vec::new(); + + for i in 0..items.len() { + let curr = items[i]; + let curr_start = env.scopes[curr.0 as usize].range.start; + let curr_end = env.scopes[curr.0 as usize].range.end; + + // Pop active items that are disjoint with current + let mut j = active_items.len(); + while j > 0 { + j -= 1; + let maybe_parent = active_items[j]; + let parent_end = env.scopes[maybe_parent.0 as usize].range.end; + let disjoint = curr_start >= parent_end; + let nested = curr_end <= parent_end; + assert!(disjoint || nested, "Invalid nesting in program blocks or scopes"); + if disjoint { + // Exit this scope + let fallthrough_id = + *fallthroughs.get(&maybe_parent).expect("Expected scope to exist"); + let end_instr_id = env.scopes[maybe_parent.0 as usize].range.end; + rewrites + .push(TerminalRewriteInfo::EndScope { instr_id: end_instr_id, fallthrough_id }); + active_items.truncate(j); + } else { + break; + } + } + + // Enter scope + let block_id = env.next_block_id(); + let fallthrough_id = env.next_block_id(); + let start_instr_id = env.scopes[curr.0 as usize].range.start; + rewrites.push(TerminalRewriteInfo::StartScope { + block_id, + fallthrough_id, + instr_id: start_instr_id, + scope_id: curr, + }); + fallthroughs.insert(curr, fallthrough_id); + active_items.push(curr); + } + + // Exit remaining active items + while let Some(curr) = active_items.pop() { + let fallthrough_id = *fallthroughs.get(&curr).expect("Expected scope to exist"); + let end_instr_id = env.scopes[curr.0 as usize].range.end; + rewrites.push(TerminalRewriteInfo::EndScope { instr_id: end_instr_id, fallthrough_id }); + } + + rewrites +} + +// ============================================================================= +// handleRewrite +// ============================================================================= + +struct RewriteContext { + next_block_id: BlockId, + next_preds: Vec, + instr_slice_idx: usize, + rewrites: Vec, +} + +fn handle_rewrite( + terminal_info: &TerminalRewriteInfo, + idx: usize, + source_block: &BasicBlock, + context: &mut RewriteContext, +) { + let terminal: Terminal = match terminal_info { + TerminalRewriteInfo::StartScope { block_id, fallthrough_id, instr_id, scope_id } => { + Terminal::Scope { + fallthrough: *fallthrough_id, + block: *block_id, + scope: *scope_id, + id: *instr_id, + loc: None, + } + } + TerminalRewriteInfo::EndScope { instr_id, fallthrough_id } => Terminal::Goto { + variant: GotoVariant::Break, + block: *fallthrough_id, + id: *instr_id, + loc: None, + }, + }; + + let curr_block_id = context.next_block_id; + let mut preds = FxIndexSet::default(); + for &p in &context.next_preds { + preds.insert(p); + } + + context.rewrites.push(BasicBlock { + kind: source_block.kind, + id: curr_block_id, + instructions: source_block.instructions[context.instr_slice_idx..idx].to_vec(), + preds, + // Only the first rewrite should reuse source block phis + phis: if context.rewrites.is_empty() { source_block.phis.clone() } else { Vec::new() }, + terminal, + }); + + context.next_preds = vec![curr_block_id]; + context.next_block_id = match terminal_info { + TerminalRewriteInfo::StartScope { block_id, .. } => *block_id, + TerminalRewriteInfo::EndScope { fallthrough_id, .. } => *fallthrough_id, + }; + context.instr_slice_idx = idx; +} + +// ============================================================================= +// Public API +// ============================================================================= + +/// Builds reactive scope terminals in the HIR. +/// +/// This pass assumes that all program blocks are properly nested with respect +/// to fallthroughs. Given a function whose reactive scope ranges have been +/// correctly aligned and merged, this pass rewrites blocks to introduce +/// ReactiveScopeTerminals and their fallthrough blocks. +pub fn build_reactive_scope_terminals_hir(func: &mut HirFunction, env: &mut Environment) { + // Step 1: Collect rewrites + let mut queued_rewrites = collect_scope_rewrites(func, env); + + // Step 2: Apply rewrites by splitting blocks + let mut rewritten_final_blocks: FxHashMap = FxHashMap::default(); + let mut next_blocks: FxIndexMap = FxIndexMap::default(); + + // Reverse so we can pop from the end while traversing in ascending order + queued_rewrites.reverse(); + + for (_block_id, block) in &func.body.blocks { + let preds_vec: Vec = block.preds.iter().copied().collect(); + let mut context = RewriteContext { + next_block_id: block.id, + rewrites: Vec::new(), + next_preds: preds_vec, + instr_slice_idx: 0, + }; + + // Handle queued terminal rewrites at their nearest instruction ID + for i in 0..block.instructions.len() + 1 { + let instr_id = if i < block.instructions.len() { + let instr_idx = block.instructions[i]; + func.instructions[instr_idx.0 as usize].id + } else { + block.terminal.evaluation_order() + }; + + while let Some(rewrite) = queued_rewrites.last() { + if rewrite.instr_id() <= instr_id { + // Need to pop before calling handle_rewrite + let rewrite = queued_rewrites.pop().unwrap(); + handle_rewrite(&rewrite, i, block, &mut context); + } else { + break; + } + } + } + + if !context.rewrites.is_empty() { + let mut final_preds = FxIndexSet::default(); + for &p in &context.next_preds { + final_preds.insert(p); + } + let final_block = BasicBlock { + id: context.next_block_id, + kind: block.kind, + preds: final_preds, + terminal: block.terminal.clone(), + instructions: block.instructions[context.instr_slice_idx..].to_vec(), + phis: Vec::new(), + }; + let final_block_id = final_block.id; + context.rewrites.push(final_block); + for b in context.rewrites { + next_blocks.insert(b.id, b); + } + rewritten_final_blocks.insert(block.id, final_block_id); + } else { + next_blocks.insert(block.id, block.clone()); + } + } + + func.body.blocks = next_blocks; + + // Step 3: Repoint phis when they refer to a rewritten block + for block in func.body.blocks.values_mut() { + for phi in &mut block.phis { + let updates: Vec<(BlockId, BlockId)> = phi + .operands + .keys() + .filter_map(|original_id| { + rewritten_final_blocks.get(original_id).map(|new_id| (*original_id, *new_id)) + }) + .collect(); + for (old_id, new_id) in updates { + if let Some(value) = phi.operands.shift_remove(&old_id) { + phi.operands.insert(new_id, value); + } + } + } + } + + // Step 4: Fixup HIR to restore RPO, correct predecessors, renumber instructions + func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions); + mark_predecessors(&mut func.body); + mark_instruction_ids(&mut func.body, &mut func.instructions); + + // Step 5: Fix scope and identifier ranges to account for renumbered instructions + fix_scope_and_identifier_ranges(func, env); +} + +/// Fix scope ranges after instruction renumbering. +/// Scope ranges should always align to start at the 'scope' terminal +/// and end at the first instruction of the fallthrough block. +/// +/// In TS, `identifier.mutableRange` and `scope.range` are the same object +/// reference (after InferReactiveScopeVariables). When scope.range is updated, +/// all identifiers with that scope automatically see the new range. +/// BUT: after MergeOverlappingReactiveScopesHIR, repointed identifiers have +/// mutableRange pointing to the OLD scope's range, NOT the root scope's range. +/// So only identifiers whose mutableRange matches their scope's pre-renumbering +/// range should be updated. +/// +/// Corresponds to TS `fixScopeAndIdentifierRanges`. +fn fix_scope_and_identifier_ranges(func: &HirFunction, env: &mut Environment) { + // Save original scope ranges before updating them. In TS, + // identifier.mutableRange and scope.range may or may not be the same + // JS object. Only identifiers whose mutableRange shares the same object + // reference as scope.range see the update automatically. We simulate + // this by only syncing identifiers whose mutableRange matches the + // scope's pre-update range. + let original_scope_ranges: Vec = + env.scopes.iter().map(|s| s.range.clone()).collect(); + + for (_block_id, block) in &func.body.blocks { + match &block.terminal { + Terminal::Scope { fallthrough, scope, id, .. } + | Terminal::PrunedScope { fallthrough, scope, id, .. } => { + let fallthrough_block = func.body.blocks.get(fallthrough).unwrap(); + let first_id = if !fallthrough_block.instructions.is_empty() { + func.instructions[fallthrough_block.instructions[0].0 as usize].id + } else { + fallthrough_block.terminal.evaluation_order() + }; + env.scopes[scope.0 as usize].range.start = *id; + env.scopes[scope.0 as usize].range.end = first_id; + } + _ => {} + } + } + + // Sync identifier mutable ranges with their scope ranges, but ONLY + // for identifiers whose mutableRange has the same identity as their + // scope's ORIGINAL range (before the updates above). In TS, + // identifier.mutableRange and scope.range are only the same JS object + // for identifiers that were the canonical representative when the scope + // was created. After MergeOverlappingReactiveScopesHIR, repointed + // identifiers have mutableRange pointing to the OLD scope's range, + // not the root scope's range — so they should NOT be synced here. + for ident in &mut env.identifiers { + if let Some(scope_id) = ident.scope { + let original = &original_scope_ranges[scope_id.0 as usize]; + if ident.mutable_range.same_range(original) { + let scope_range = &env.scopes[scope_id.0 as usize].range; + ident.mutable_range.start = scope_range.start; + ident.mutable_range.end = scope_range.end; + } + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/flatten_reactive_loops_hir.rs b/crates/oxc_react_compiler/src/react_compiler_inference/flatten_reactive_loops_hir.rs new file mode 100644 index 0000000000000..40e1136b2803a --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/flatten_reactive_loops_hir.rs @@ -0,0 +1,57 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Prunes any reactive scopes that are within a loop (for, while, etc). We don't yet +//! support memoization within loops because this would require an extra layer of reconciliation +//! (plus a way to identify values across runs, similar to how we use `key` in JSX for lists). +//! Eventually we may integrate more deeply into the runtime so that we can do a single level +//! of reconciliation, but for now we've found it's sufficient to memoize *around* the loop. +//! +//! Analogous to TS `ReactiveScopes/FlattenReactiveLoopsHIR.ts`. + +use crate::react_compiler_hir::{BlockId, HirFunction, Terminal}; + +/// Flattens reactive scopes that are inside loops by converting `Scope` terminals +/// to `PrunedScope` terminals. +pub fn flatten_reactive_loops_hir(func: &mut HirFunction) { + let mut active_loops: Vec = Vec::new(); + + // Collect block ids in iteration order so we can iterate while mutating terminals + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + + for block_id in block_ids { + // Remove this block from active loops (matching TS retainWhere) + active_loops.retain(|id| *id != block_id); + + let block = &func.body.blocks[&block_id]; + let terminal = &block.terminal; + + match terminal { + Terminal::DoWhile { fallthrough, .. } + | Terminal::For { fallthrough, .. } + | Terminal::ForIn { fallthrough, .. } + | Terminal::ForOf { fallthrough, .. } + | Terminal::While { fallthrough, .. } => { + active_loops.push(*fallthrough); + } + Terminal::Scope { block, fallthrough, scope, id, loc } => { + if !active_loops.is_empty() { + let new_terminal = Terminal::PrunedScope { + block: *block, + fallthrough: *fallthrough, + scope: *scope, + id: *id, + loc: *loc, + }; + // We need to drop the borrow and reborrow mutably + let block_mut = func.body.blocks.get_mut(&block_id).unwrap(); + block_mut.terminal = new_terminal; + } + } + // All other terminal kinds: no action needed + _ => {} + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/flatten_scopes_with_hooks_or_use_hir.rs b/crates/oxc_react_compiler/src/react_compiler_inference/flatten_scopes_with_hooks_or_use_hir.rs new file mode 100644 index 0000000000000..f565c767f4b2e --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/flatten_scopes_with_hooks_or_use_hir.rs @@ -0,0 +1,129 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are +//! different from regular functions in two key ways: +//! - They can introduce reactivity even when their arguments are non-reactive (accounted for in +//! InferReactivePlaces) +//! - They cannot be called conditionally +//! +//! The `use` operator is similar: +//! - It can access context, and therefore introduce reactivity +//! - It can be called conditionally, but _it must be called if the component needs the return value_. +//! This is because React uses the fact that use was called to remember that the component needs the +//! value, and that changes to the input should invalidate the component itself. +//! +//! This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the +//! reasoning is slightly different for each, the result is that we can't memoize scopes that call +//! hooks or use since this would make them called conditionally in the output. +//! +//! The pass finds and removes any scopes that transitively contain a hook or use call. By running all +//! the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately +//! describe the set of values which "construct together", and remove _all_ that memoization in order +//! to ensure the hook call does not inadvertently become conditional. +//! +//! Analogous to TS `ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts`. + +use crate::react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{BlockId, HirFunction, InstructionValue, Terminal, Type}; + +/// Flattens reactive scopes that contain hook calls or `use()` calls. +/// +/// Hooks and `use` must be called unconditionally, so any reactive scope containing +/// such a call must be flattened to avoid making the call conditional. +pub fn flatten_scopes_with_hooks_or_use_hir( + func: &mut HirFunction, + env: &Environment, +) -> Result<(), CompilerDiagnostic> { + let mut active_scopes: Vec = Vec::new(); + let mut prune: Vec = Vec::new(); + + // Collect block ids to allow mutation during iteration + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + + for block_id in &block_ids { + // Remove scopes whose fallthrough matches this block + active_scopes.retain(|scope| scope.fallthrough != *block_id); + + let block = &func.body.blocks[block_id]; + + // Check instructions for hook or use calls + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::CallExpression { callee, .. } => { + let callee_ty = + &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if is_hook_or_use(env, callee_ty)? { + // All active scopes must be pruned + prune.extend(active_scopes.iter().map(|s| s.block)); + active_scopes.clear(); + } + } + InstructionValue::MethodCall { property, .. } => { + let property_ty = &env.types + [env.identifiers[property.identifier.0 as usize].type_.0 as usize]; + if is_hook_or_use(env, property_ty)? { + prune.extend(active_scopes.iter().map(|s| s.block)); + active_scopes.clear(); + } + } + _ => {} + } + } + + // Track scope terminals + if let Terminal::Scope { fallthrough, .. } = &block.terminal { + active_scopes.push(ActiveScope { block: *block_id, fallthrough: *fallthrough }); + } + } + + // Apply pruning: convert Scope terminals to Label or PrunedScope + for id in prune { + let block = &func.body.blocks[&id]; + let terminal = &block.terminal; + + let (scope_block, fallthrough, eval_id, loc, scope) = match terminal { + Terminal::Scope { block, fallthrough, id, loc, scope } => { + (*block, *fallthrough, *id, *loc, *scope) + } + _ => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("Expected block bb{} to end in a scope terminal", id.0), + None, + )); + } + }; + + // Check if the scope body is a single-instruction block that goes directly + // to fallthrough — if so, use Label instead of PrunedScope + let body = &func.body.blocks[&scope_block]; + let new_terminal = if body.instructions.len() == 1 + && matches!(&body.terminal, Terminal::Goto { block, .. } if *block == fallthrough) + { + // This was a scope just for a hook call, which doesn't need memoization. + // Flatten it away. We rely on PruneUnusedLabels to do the actual flattening. + Terminal::Label { block: scope_block, fallthrough, id: eval_id, loc } + } else { + Terminal::PrunedScope { block: scope_block, fallthrough, scope, id: eval_id, loc } + }; + + let block_mut = func.body.blocks.get_mut(&id).unwrap(); + block_mut.terminal = new_terminal; + } + Ok(()) +} + +struct ActiveScope { + block: BlockId, + fallthrough: BlockId, +} + +fn is_hook_or_use(env: &Environment, ty: &Type) -> Result { + Ok(env.get_hook_kind_for_type(ty)?.is_some() + || crate::react_compiler_hir::is_use_operator_type(ty)) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/infer_mutation_aliasing_effects.rs b/crates/oxc_react_compiler/src/react_compiler_inference/infer_mutation_aliasing_effects.rs new file mode 100644 index 0000000000000..6a9a0edc2c585 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/infer_mutation_aliasing_effects.rs @@ -0,0 +1,3255 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Infers the mutation/aliasing effects for instructions and terminals. +//! +//! Ported from TypeScript `src/Inference/InferMutationAliasingEffects.ts`. +//! +//! This pass uses abstract interpretation to compute effects describing +//! creation, aliasing, mutation, freezing, and error conditions for each +//! instruction and terminal in the HIR. + +use crate::react_compiler_utils::FxIndexMap; +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::CompilerDiagnostic; +use crate::react_compiler_diagnostics::CompilerDiagnosticDetail; +use crate::react_compiler_diagnostics::ErrorCategory; +use crate::react_compiler_hir::AliasingEffect; +use crate::react_compiler_hir::AliasingSignature; +use crate::react_compiler_hir::BlockId; +use crate::react_compiler_hir::DeclarationId; +use crate::react_compiler_hir::Effect; +use crate::react_compiler_hir::FunctionId; +use crate::react_compiler_hir::HirFunction; +use crate::react_compiler_hir::IdentifierId; +use crate::react_compiler_hir::InstructionKind; +use crate::react_compiler_hir::InstructionValue; +use crate::react_compiler_hir::MutationReason; +use crate::react_compiler_hir::ParamPattern; +use crate::react_compiler_hir::Place; +use crate::react_compiler_hir::PlaceOrSpread; +use crate::react_compiler_hir::PlaceOrSpreadOrHole; +use crate::react_compiler_hir::ReactFunctionType; +use crate::react_compiler_hir::SourceLocation; +use crate::react_compiler_hir::Type; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::object_shape::BUILT_IN_ARRAY_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_MAP_ID; +use crate::react_compiler_hir::object_shape::BUILT_IN_SET_ID; +use crate::react_compiler_hir::object_shape::FunctionSignature; +use crate::react_compiler_hir::object_shape::HookKind; +use crate::react_compiler_hir::type_config::ValueKind; +use crate::react_compiler_hir::type_config::ValueReason; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_utils::FxIndexSet; + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Infers mutation/aliasing effects for all instructions and terminals in `func`. +/// +/// Corresponds to TS `inferMutationAliasingEffects(fn, {isFunctionExpression})`. +pub fn infer_mutation_aliasing_effects( + func: &mut HirFunction, + env: &mut Environment, + is_function_expression: bool, +) -> Result<(), CompilerDiagnostic> { + let mut initial_state = InferenceState::empty(env, is_function_expression); + + // Map of blocks to the last (merged) incoming state that was processed + let mut states_by_block: FxHashMap = FxHashMap::default(); + + // Initialize context variables + for ctx_place in &func.context { + let value_id = ValueId::new(); + initial_state.initialize( + value_id, + AbstractValue { kind: ValueKind::Context, reason: hashset_of(ValueReason::Other) }, + ); + initial_state.define(ctx_place.identifier, value_id); + } + + let param_kind: AbstractValue = if is_function_expression { + AbstractValue { kind: ValueKind::Mutable, reason: hashset_of(ValueReason::Other) } + } else { + AbstractValue { + kind: ValueKind::Frozen, + reason: hashset_of(ValueReason::ReactiveFunctionArgument), + } + }; + + if func.fn_type == ReactFunctionType::Component { + // Component: at most 2 params (props, ref) + let params_len = func.params.len(); + if params_len > 0 { + infer_param(&func.params[0], &mut initial_state, ¶m_kind); + } + if params_len > 1 { + let ref_place = match &func.params[1] { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + let value_id = ValueId::new(); + initial_state.initialize( + value_id, + AbstractValue { kind: ValueKind::Mutable, reason: hashset_of(ValueReason::Other) }, + ); + initial_state.define(ref_place.identifier, value_id); + } + } else { + for param in &func.params { + infer_param(param, &mut initial_state, ¶m_kind); + } + } + + let mut queued_states: FxIndexMap = FxIndexMap::default(); + + // Queue helper + fn queue( + queued_states: &mut FxIndexMap, + states_by_block: &FxHashMap, + block_id: BlockId, + state: InferenceState, + ) { + if let Some(queued_state) = queued_states.get(&block_id) { + let merged = queued_state.merge(&state); + let new_state = merged.unwrap_or_else(|| queued_state.clone()); + queued_states.insert(block_id, new_state); + } else { + let prev_state = states_by_block.get(&block_id); + if let Some(prev) = prev_state { + let next_state = prev.merge(&state); + if let Some(next) = next_state { + queued_states.insert(block_id, next); + } + } else { + queued_states.insert(block_id, state); + } + } + } + + queue(&mut queued_states, &states_by_block, func.body.entry, initial_state); + + let hoisted_context_declarations = find_hoisted_context_declarations(func, env); + let non_mutating_spreads = find_non_mutated_destructure_spreads(func, env); + + let mut context = Context { + interned_effects: FxHashMap::default(), + instruction_signature_cache: FxHashMap::default(), + catch_handlers: FxHashMap::default(), + is_function_expression, + hoisted_context_declarations, + non_mutating_spreads, + effect_value_id_cache: FxHashMap::default(), + function_values: FxHashMap::default(), + function_signature_cache: FxHashMap::default(), + aliasing_config_temp_cache: FxHashMap::default(), + }; + + let mut iteration_count = 0; + + while !queued_states.is_empty() { + iteration_count += 1; + if iteration_count > 100 { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "[InferMutationAliasingEffects] Potential infinite loop: \ + A value, temporary place, or effect was not cached properly", + None, + )); + } + + // Collect block IDs to process in order + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + for block_id in block_ids { + let incoming_state = match queued_states.swap_remove(&block_id) { + Some(s) => s, + None => continue, + }; + + states_by_block.insert(block_id, incoming_state.clone()); + let mut state = incoming_state.clone(); + + infer_block(&mut context, &mut state, block_id, func, env)?; + + // Check for uninitialized identifier access (matches TS invariant: + // "Expected value kind to be initialized") + if let Some((uninitialized_id, usage_loc)) = state.uninitialized_access.get() { + let ident_info = env.identifiers.get(uninitialized_id.0 as usize); + let name = ident_info + .and_then(|ident| ident.name.as_ref()) + .map(|n| n.value().to_string()) + .unwrap_or_else(|| "".to_string()); + // Use usage_loc if available, otherwise fall back to identifier's own loc + let error_loc = usage_loc.or_else(|| ident_info.and_then(|i| i.loc)); + // Match TS printPlace format: " name$id:type" + let type_str = ident_info + .map(|ident| { + let ty = &env.types[ident.type_.0 as usize]; + format_type_for_print(ty) + }) + .unwrap_or_default(); + let description = format!(" {}${}{}", name, uninitialized_id.0, type_str); + let diag = CompilerDiagnostic::new( + ErrorCategory::Invariant, + "[InferMutationAliasingEffects] Expected value kind to be initialized", + Some(description), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: error_loc, + message: Some("this is uninitialized".to_string()), + identifier_name: None, + }); + return Err(diag); + } + + // Queue successors + let successors = terminal_successors(&func.body.blocks[&block_id].terminal); + for next_block_id in successors { + queue(&mut queued_states, &states_by_block, next_block_id, state.clone()); + } + } + } + + Ok(()) +} + +// ============================================================================= +// ValueId: replaces InstructionValue identity as allocation-site key +// ============================================================================= + +/// Unique allocation-site identifier, replacing TS's object-identity on InstructionValue. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct ValueId(u32); + +use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering; +static NEXT_VALUE_ID: AtomicU32 = AtomicU32::new(1); + +impl ValueId { + fn new() -> Self { + ValueId(NEXT_VALUE_ID.fetch_add(1, Ordering::Relaxed)) + } +} + +// ============================================================================= +// AbstractValue +// ============================================================================= + +#[derive(Debug, Clone)] +struct AbstractValue { + kind: ValueKind, + reason: FxIndexSet, +} + +fn hashset_of(r: ValueReason) -> FxIndexSet { + let mut s = FxIndexSet::default(); + s.insert(r); + s +} + +// ============================================================================= +// InferenceState +// ============================================================================= + +/// The abstract state tracked during inference. +/// Uses interior mutability via a struct with direct fields (no Rc needed since +/// we always have exclusive access in the pass). +#[derive(Debug, Clone)] +struct InferenceState { + is_function_expression: bool, + /// The kind of each value, based on its allocation site + values: FxHashMap, + /// The set of values pointed to by each identifier + variables: FxHashMap>, + /// Tracks uninitialized identifier access errors (matches TS invariant). + /// Uses Cell so it can be set from `&self` methods like `kind()`. + /// Stores (IdentifierId, usage_loc) where usage_loc is the source location + /// of the Place that triggered the uninitialized access. + uninitialized_access: std::cell::Cell)>>, +} + +impl InferenceState { + fn empty(_env: &Environment, is_function_expression: bool) -> Self { + InferenceState { + is_function_expression, + values: FxHashMap::default(), + variables: FxHashMap::default(), + uninitialized_access: std::cell::Cell::new(None), + } + } + + /// Check the kind of a place, recording the usage location for error reporting. + fn kind_with_loc( + &self, + place_id: IdentifierId, + usage_loc: Option, + ) -> AbstractValue { + let values = match self.variables.get(&place_id) { + Some(v) => v, + None => { + if self.uninitialized_access.get().is_none() { + self.uninitialized_access.set(Some((place_id, usage_loc))); + } + return AbstractValue { + kind: ValueKind::Mutable, + reason: hashset_of(ValueReason::Other), + }; + } + }; + let mut merged_kind: Option = None; + for value_id in values { + let kind = match self.values.get(value_id) { + Some(k) => k, + None => continue, + }; + merged_kind = Some(match merged_kind { + Some(prev) => merge_abstract_values(&prev, kind), + None => kind.clone(), + }); + } + merged_kind.unwrap_or_else(|| AbstractValue { + kind: ValueKind::Mutable, + reason: hashset_of(ValueReason::Other), + }) + } + + fn initialize(&mut self, value_id: ValueId, kind: AbstractValue) { + self.values.insert(value_id, kind); + } + + fn define(&mut self, place_id: IdentifierId, value_id: ValueId) { + let mut set = FxHashSet::default(); + set.insert(value_id); + self.variables.insert(place_id, set); + } + + fn assign(&mut self, into: IdentifierId, from: IdentifierId) { + let values = match self.variables.get(&from) { + Some(v) => v.clone(), + None => { + // Create a stable value for uninitialized identifiers + // Use a deterministic ID based on the from identifier + let vid = ValueId(from.0 | 0x80000000); + let mut set = FxHashSet::default(); + set.insert(vid); + if !self.values.contains_key(&vid) { + self.values.insert( + vid, + AbstractValue { + kind: ValueKind::Mutable, + reason: hashset_of(ValueReason::Other), + }, + ); + } + set + } + }; + self.variables.insert(into, values); + } + + fn append_alias(&mut self, place: IdentifierId, value: IdentifierId) { + let new_values = match self.variables.get(&value) { + Some(v) => v.clone(), + None => return, + }; + let prev_values = match self.variables.get(&place) { + Some(v) => v.clone(), + None => return, + }; + let merged: FxHashSet = prev_values.union(&new_values).copied().collect(); + self.variables.insert(place, merged); + } + + fn is_defined(&self, place_id: IdentifierId) -> bool { + self.variables.contains_key(&place_id) + } + + fn values_for(&self, place_id: IdentifierId) -> Vec { + match self.variables.get(&place_id) { + Some(values) => values.iter().copied().collect(), + None => Vec::new(), + } + } + + #[allow(dead_code)] + fn kind_opt(&self, place_id: IdentifierId) -> Option { + let values = self.variables.get(&place_id)?; + let mut merged_kind: Option = None; + for value_id in values { + let kind = self.values.get(value_id)?; + merged_kind = Some(match merged_kind { + Some(prev) => merge_abstract_values(&prev, kind), + None => kind.clone(), + }); + } + merged_kind + } + + fn kind(&self, place_id: IdentifierId) -> AbstractValue { + self.kind_with_loc(place_id, None) + } + + fn freeze(&mut self, place_id: IdentifierId, reason: ValueReason) -> bool { + // Check if defined first to avoid recording uninitialized access error. + // Freeze on undefined identifiers is a no-op — this matches the TS + // behavior where freeze() is never called on undefined identifiers + // (the invariant in kind() catches this before freeze is reached). + if !self.variables.contains_key(&place_id) { + return false; + } + let value = self.kind(place_id); + match value.kind { + ValueKind::Context | ValueKind::Mutable | ValueKind::MaybeFrozen => { + let value_ids: Vec = self.values_for(place_id); + for vid in value_ids { + self.freeze_value(vid, reason); + } + true + } + ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => false, + } + } + + fn freeze_value(&mut self, value_id: ValueId, reason: ValueReason) { + self.values.insert( + value_id, + AbstractValue { kind: ValueKind::Frozen, reason: hashset_of(reason) }, + ); + // Note: In TS, this also transitively freezes FunctionExpression captures + // if enableTransitivelyFreezeFunctionExpressions is set. We skip that here + // since we don't have access to the function arena from within state. + } + + #[allow(dead_code)] + fn mutate( + &self, + variant: MutateVariant, + place_id: IdentifierId, + env: &Environment, + ) -> MutationResult { + self.mutate_with_loc(variant, place_id, env, None) + } + + fn mutate_with_loc( + &self, + variant: MutateVariant, + place_id: IdentifierId, + env: &Environment, + usage_loc: Option, + ) -> MutationResult { + let ty = &env.types[env.identifiers[place_id.0 as usize].type_.0 as usize]; + if crate::react_compiler_hir::is_ref_or_ref_value(ty) { + return MutationResult::MutateRef; + } + let kind = self.kind_with_loc(place_id, usage_loc).kind; + match variant { + MutateVariant::MutateConditionally | MutateVariant::MutateTransitiveConditionally => { + match kind { + ValueKind::Mutable | ValueKind::Context => MutationResult::Mutate, + _ => MutationResult::None, + } + } + MutateVariant::Mutate | MutateVariant::MutateTransitive => match kind { + ValueKind::Mutable | ValueKind::Context => MutationResult::Mutate, + ValueKind::Primitive => MutationResult::None, + ValueKind::Frozen | ValueKind::MaybeFrozen => MutationResult::MutateFrozen, + ValueKind::Global => MutationResult::MutateGlobal, + }, + } + } + + fn merge(&self, other: &InferenceState) -> Option { + let mut next_values: Option> = None; + let mut next_variables: Option>> = None; + + // Merge values present in both + for (id, this_value) in &self.values { + if let Some(other_value) = other.values.get(id) { + let merged = merge_abstract_values(this_value, other_value); + if merged.kind != this_value.kind + || !is_superset(&this_value.reason, &merged.reason) + { + let nv = next_values.get_or_insert_with(|| self.values.clone()); + nv.insert(*id, merged); + } + } + } + // Add values only in other + for (id, other_value) in &other.values { + if !self.values.contains_key(id) { + let nv = next_values.get_or_insert_with(|| self.values.clone()); + nv.insert(*id, other_value.clone()); + } + } + + // Merge variables present in both + for (id, this_values) in &self.variables { + if let Some(other_values) = other.variables.get(id) { + let mut has_new = false; + for ov in other_values { + if !this_values.contains(ov) { + has_new = true; + break; + } + } + if has_new { + let nvars = next_variables.get_or_insert_with(|| self.variables.clone()); + let merged: FxHashSet = + this_values.union(other_values).copied().collect(); + nvars.insert(*id, merged); + } + } + } + // Add variables only in other + for (id, other_values) in &other.variables { + if !self.variables.contains_key(id) { + let nvars = next_variables.get_or_insert_with(|| self.variables.clone()); + nvars.insert(*id, other_values.clone()); + } + } + + if next_variables.is_none() && next_values.is_none() { + None + } else { + Some(InferenceState { + is_function_expression: self.is_function_expression, + values: next_values.unwrap_or_else(|| self.values.clone()), + variables: next_variables.unwrap_or_else(|| self.variables.clone()), + uninitialized_access: std::cell::Cell::new(None), + }) + } + } + + fn infer_phi(&mut self, phi_place_id: IdentifierId, phi_operands: &FxIndexMap) { + let mut values: FxHashSet = FxHashSet::default(); + for (_, operand) in phi_operands { + if let Some(operand_values) = self.variables.get(&operand.identifier) { + for v in operand_values { + values.insert(*v); + } + } + // If not found, it's a backedge that will be handled later by merge + } + if !values.is_empty() { + self.variables.insert(phi_place_id, values); + } + } +} + +fn is_superset(a: &FxIndexSet, b: &FxIndexSet) -> bool { + b.iter().all(|x| a.contains(x)) +} + +#[derive(Debug, Clone, Copy)] +enum MutateVariant { + Mutate, + MutateConditionally, + MutateTransitive, + MutateTransitiveConditionally, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MutationResult { + None, + Mutate, + MutateFrozen, + MutateGlobal, + MutateRef, +} + +// ============================================================================= +// Context +// ============================================================================= + +struct Context { + interned_effects: FxHashMap, + instruction_signature_cache: FxHashMap, + catch_handlers: FxHashMap, + is_function_expression: bool, + hoisted_context_declarations: FxHashMap>, + non_mutating_spreads: FxHashSet, + /// Cache of ValueIds keyed by effect hash, ensuring stable allocation-site identity + /// across fixpoint iterations. Mirrors TS `effectInstructionValueCache`. + effect_value_id_cache: FxHashMap, + /// Maps ValueId to FunctionId for function expressions, so we can look up + /// locally-declared functions when processing Apply effects. + function_values: FxHashMap, + /// Cache of function expression signatures, keyed by FunctionId + function_signature_cache: FxHashMap, + /// Cache of temporary places created for aliasing signature config temporaries. + /// Keyed by (lvalue_identifier_id, temp_name) to ensure stable allocation + /// across fixpoint iterations. + aliasing_config_temp_cache: FxHashMap<(IdentifierId, String), Place>, +} + +impl Context { + fn intern_effect(&mut self, effect: AliasingEffect) -> AliasingEffect { + let hash = hash_effect(&effect); + self.interned_effects.entry(hash).or_insert(effect).clone() + } + + /// Get or create a stable ValueId for a given effect, ensuring fixpoint convergence. + fn get_or_create_value_id(&mut self, effect: &AliasingEffect) -> ValueId { + let hash = hash_effect(effect); + *self.effect_value_id_cache.entry(hash).or_insert_with(ValueId::new) + } +} + +struct InstructionSignature { + effects: Vec, +} + +// ============================================================================= +// Helper: hash_effect +// ============================================================================= + +fn hash_effect(effect: &AliasingEffect) -> String { + match effect { + AliasingEffect::Apply { receiver, function, mutates_function, args, into, .. } => { + let args_str: Vec = args + .iter() + .map(|a| match a { + PlaceOrSpreadOrHole::Hole => String::new(), + PlaceOrSpreadOrHole::Place(p) => format!("{}", p.identifier.0), + PlaceOrSpreadOrHole::Spread(s) => format!("...{}", s.place.identifier.0), + }) + .collect(); + format!( + "Apply:{}:{}:{}:{}:{}", + receiver.identifier.0, + function.identifier.0, + mutates_function, + args_str.join(","), + into.identifier.0 + ) + } + AliasingEffect::CreateFrom { from, into } => { + format!("CreateFrom:{}:{}", from.identifier.0, into.identifier.0) + } + AliasingEffect::ImmutableCapture { from, into } => { + format!("ImmutableCapture:{}:{}", from.identifier.0, into.identifier.0) + } + AliasingEffect::Assign { from, into } => { + format!("Assign:{}:{}", from.identifier.0, into.identifier.0) + } + AliasingEffect::Alias { from, into } => { + format!("Alias:{}:{}", from.identifier.0, into.identifier.0) + } + AliasingEffect::Capture { from, into } => { + format!("Capture:{}:{}", from.identifier.0, into.identifier.0) + } + AliasingEffect::MaybeAlias { from, into } => { + format!("MaybeAlias:{}:{}", from.identifier.0, into.identifier.0) + } + AliasingEffect::Create { into, value, reason } => { + format!("Create:{}:{:?}:{:?}", into.identifier.0, value, reason) + } + AliasingEffect::Freeze { value, reason } => { + format!("Freeze:{}:{:?}", value.identifier.0, reason) + } + AliasingEffect::Impure { place, .. } => format!("Impure:{}", place.identifier.0), + AliasingEffect::Render { place } => format!("Render:{}", place.identifier.0), + AliasingEffect::MutateFrozen { place, error } => { + format!("MutateFrozen:{}:{}:{:?}", place.identifier.0, error.reason, error.description) + } + AliasingEffect::MutateGlobal { place, error } => { + format!("MutateGlobal:{}:{}:{:?}", place.identifier.0, error.reason, error.description) + } + AliasingEffect::Mutate { value, .. } => format!("Mutate:{}", value.identifier.0), + AliasingEffect::MutateConditionally { value } => { + format!("MutateConditionally:{}", value.identifier.0) + } + AliasingEffect::MutateTransitive { value } => { + format!("MutateTransitive:{}", value.identifier.0) + } + AliasingEffect::MutateTransitiveConditionally { value } => { + format!("MutateTransitiveConditionally:{}", value.identifier.0) + } + AliasingEffect::CreateFunction { into, function_id, captures } => { + let cap_str: Vec = + captures.iter().map(|p| format!("{}", p.identifier.0)).collect(); + format!("CreateFunction:{}:{}:{}", into.identifier.0, function_id.0, cap_str.join(",")) + } + } +} + +// ============================================================================= +// merge helpers +// ============================================================================= + +fn merge_abstract_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue { + let kind = merge_value_kinds(a.kind, b.kind); + if kind == a.kind && kind == b.kind && is_superset(&a.reason, &b.reason) { + return a.clone(); + } + let mut reason = a.reason.clone(); + for r in &b.reason { + reason.insert(*r); + } + AbstractValue { kind, reason } +} + +fn merge_value_kinds(a: ValueKind, b: ValueKind) -> ValueKind { + if a == b { + return a; + } + if a == ValueKind::MaybeFrozen || b == ValueKind::MaybeFrozen { + return ValueKind::MaybeFrozen; + } + if a == ValueKind::Mutable || b == ValueKind::Mutable { + if a == ValueKind::Frozen || b == ValueKind::Frozen { + return ValueKind::MaybeFrozen; + } else if a == ValueKind::Context || b == ValueKind::Context { + return ValueKind::Context; + } else { + return ValueKind::Mutable; + } + } + if a == ValueKind::Context || b == ValueKind::Context { + if a == ValueKind::Frozen || b == ValueKind::Frozen { + return ValueKind::MaybeFrozen; + } else { + return ValueKind::Context; + } + } + if a == ValueKind::Frozen || b == ValueKind::Frozen { + return ValueKind::Frozen; + } + if a == ValueKind::Global || b == ValueKind::Global { + return ValueKind::Global; + } + ValueKind::Primitive +} + +// ============================================================================= +// Pre-passes +// ============================================================================= + +fn find_hoisted_context_declarations( + func: &HirFunction, + env: &Environment, +) -> FxHashMap> { + let mut hoisted: FxHashMap> = FxHashMap::default(); + + fn visit( + hoisted: &mut FxHashMap>, + place: &Place, + env: &Environment, + ) { + let decl_id = env.identifiers[place.identifier.0 as usize].declaration_id; + if hoisted.contains_key(&decl_id) && hoisted.get(&decl_id).unwrap().is_none() { + hoisted.insert(decl_id, Some(place.clone())); + } + } + + for (_block_id, block) in &func.body.blocks { + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::DeclareContext { lvalue, .. } => { + let kind = lvalue.kind; + if kind == InstructionKind::HoistedConst + || kind == InstructionKind::HoistedFunction + || kind == InstructionKind::HoistedLet + { + let decl_id = + env.identifiers[lvalue.place.identifier.0 as usize].declaration_id; + hoisted.insert(decl_id, None); + } + } + _ => { + for operand in visitors::each_instruction_value_operand(&instr.value, env) { + visit(&mut hoisted, &operand, env); + } + } + } + } + for operand in visitors::each_terminal_operand(&block.terminal) { + visit(&mut hoisted, &operand, env); + } + } + hoisted +} + +fn find_non_mutated_destructure_spreads( + func: &HirFunction, + env: &Environment, +) -> FxHashSet { + let mut known_frozen: FxHashSet = FxHashSet::default(); + if func.fn_type == ReactFunctionType::Component { + if let Some(param) = func.params.first() { + if let ParamPattern::Place(p) = param { + known_frozen.insert(p.identifier); + } + } + } else { + for param in &func.params { + if let ParamPattern::Place(p) = param { + known_frozen.insert(p.identifier); + } + } + } + + let mut candidate_non_mutating_spreads: FxHashMap = + FxHashMap::default(); + for (_block_id, block) in &func.body.blocks { + if !candidate_non_mutating_spreads.is_empty() { + for phi in &block.phis { + for (_, operand) in &phi.operands { + if let Some(spread) = + candidate_non_mutating_spreads.get(&operand.identifier).copied() + { + candidate_non_mutating_spreads.remove(&spread); + } + } + } + } + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + match &instr.value { + InstructionValue::Destructure { lvalue, value, .. } => { + if !known_frozen.contains(&value.identifier) { + continue; + } + if !(lvalue.kind == InstructionKind::Let + || lvalue.kind == InstructionKind::Const) + { + continue; + } + match &lvalue.pattern { + crate::react_compiler_hir::Pattern::Object(obj_pat) => { + for prop in &obj_pat.properties { + if let crate::react_compiler_hir::ObjectPropertyOrSpread::Spread( + s, + ) = prop + { + candidate_non_mutating_spreads + .insert(s.place.identifier, s.place.identifier); + } + } + } + _ => continue, + } + } + InstructionValue::LoadLocal { place, .. } => { + if let Some(spread) = + candidate_non_mutating_spreads.get(&place.identifier).copied() + { + candidate_non_mutating_spreads.insert(lvalue_id, spread); + } + } + InstructionValue::StoreLocal { lvalue: sl, value: sv, .. } => { + if let Some(spread) = + candidate_non_mutating_spreads.get(&sv.identifier).copied() + { + candidate_non_mutating_spreads.insert(lvalue_id, spread); + candidate_non_mutating_spreads.insert(sl.place.identifier, spread); + } + } + InstructionValue::JsxFragment { .. } | InstructionValue::JsxExpression { .. } => { + // Passing objects created with spread to jsx can't mutate them + } + InstructionValue::PropertyLoad { .. } => { + // Properties must be frozen since the original value was frozen + } + InstructionValue::CallExpression { callee, .. } + | InstructionValue::MethodCall { property: callee, .. } => { + let callee_ty = + &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if get_hook_kind_for_type(env, callee_ty).ok().flatten().is_some() { + if !is_ref_or_ref_value_for_id(env, lvalue_id) { + known_frozen.insert(lvalue_id); + } + } else if !candidate_non_mutating_spreads.is_empty() { + for operand in visitors::each_instruction_value_operand(&instr.value, env) { + if let Some(spread) = + candidate_non_mutating_spreads.get(&operand.identifier).copied() + { + candidate_non_mutating_spreads.remove(&spread); + } + } + } + } + _ => { + if !candidate_non_mutating_spreads.is_empty() { + for operand in visitors::each_instruction_value_operand(&instr.value, env) { + if let Some(spread) = + candidate_non_mutating_spreads.get(&operand.identifier).copied() + { + candidate_non_mutating_spreads.remove(&spread); + } + } + } + } + } + } + } + + let mut non_mutating: FxHashSet = FxHashSet::default(); + for (key, value) in &candidate_non_mutating_spreads { + if key == value { + non_mutating.insert(*key); + } + } + non_mutating +} + +// ============================================================================= +// inferParam +// ============================================================================= + +fn infer_param(param: &ParamPattern, state: &mut InferenceState, param_kind: &AbstractValue) { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + let value_id = ValueId::new(); + state.initialize(value_id, param_kind.clone()); + state.define(place.identifier, value_id); +} + +// ============================================================================= +// inferBlock +// ============================================================================= + +fn infer_block( + context: &mut Context, + state: &mut InferenceState, + block_id: BlockId, + func: &mut HirFunction, + env: &mut Environment, +) -> Result<(), CompilerDiagnostic> { + let block = &func.body.blocks[&block_id]; + + // Process phis + let phis: Vec<(IdentifierId, FxIndexMap)> = + block.phis.iter().map(|phi| (phi.place.identifier, phi.operands.clone())).collect(); + for (place_id, operands) in &phis { + state.infer_phi(*place_id, operands); + } + + // Process instructions + let instr_ids: Vec = block.instructions.iter().map(|id| id.0).collect(); + for instr_idx in &instr_ids { + let instr_index = *instr_idx as usize; + + // Compute signature if not cached + if !context.instruction_signature_cache.contains_key(instr_idx) { + let sig = compute_signature_for_instruction( + context, + env, + &func.instructions[instr_index], + func, + ); + context.instruction_signature_cache.insert(*instr_idx, sig); + } + + // Apply signature + let effects = apply_signature( + context, + state, + *instr_idx, + &func.instructions[instr_index], + env, + func, + )?; + func.instructions[instr_index].effects = effects; + } + + // Process terminal + // Determine what terminal action to take without holding borrows + enum TerminalAction { + Try { handler: BlockId, binding: Place }, + MaybeThrow { handler_id: BlockId }, + Return, + None, + } + let action = { + let block = &func.body.blocks[&block_id]; + match &block.terminal { + crate::react_compiler_hir::Terminal::Try { + handler, + handler_binding: Some(binding), + .. + } => TerminalAction::Try { handler: *handler, binding: binding.clone() }, + crate::react_compiler_hir::Terminal::MaybeThrow { + handler: Some(handler_id), .. + } => TerminalAction::MaybeThrow { handler_id: *handler_id }, + crate::react_compiler_hir::Terminal::Return { .. } => TerminalAction::Return, + _ => TerminalAction::None, + } + }; + + match action { + TerminalAction::Try { handler, binding } => { + context.catch_handlers.insert(handler, binding); + } + TerminalAction::MaybeThrow { handler_id } => { + if let Some(handler_param) = context.catch_handlers.get(&handler_id).cloned() { + if state.is_defined(handler_param.identifier) { + let mut terminal_effects: Vec = Vec::new(); + for instr_idx in &instr_ids { + let instr = &func.instructions[*instr_idx as usize]; + match &instr.value { + InstructionValue::CallExpression { .. } + | InstructionValue::MethodCall { .. } => { + state.append_alias( + handler_param.identifier, + instr.lvalue.identifier, + ); + let kind = state.kind(instr.lvalue.identifier).kind; + if kind == ValueKind::Mutable || kind == ValueKind::Context { + terminal_effects.push(context.intern_effect( + AliasingEffect::Alias { + from: instr.lvalue.clone(), + into: handler_param.clone(), + }, + )); + } + } + _ => {} + } + } + let block_mut = func.body.blocks.get_mut(&block_id).unwrap(); + if let crate::react_compiler_hir::Terminal::MaybeThrow { + effects: ref mut term_effects, + .. + } = block_mut.terminal + { + *term_effects = + if terminal_effects.is_empty() { None } else { Some(terminal_effects) }; + } + } + } + } + TerminalAction::Return => { + if !context.is_function_expression { + let block_mut = func.body.blocks.get_mut(&block_id).unwrap(); + if let crate::react_compiler_hir::Terminal::Return { + ref value, + effects: ref mut term_effects, + .. + } = block_mut.terminal + { + *term_effects = Some(vec![context.intern_effect(AliasingEffect::Freeze { + value: value.clone(), + reason: ValueReason::JsxCaptured, + })]); + } + } + } + TerminalAction::None => {} + } + Ok(()) +} + +// ============================================================================= +// applySignature +// ============================================================================= + +fn apply_signature( + context: &mut Context, + state: &mut InferenceState, + instr_idx: u32, + instr: &crate::react_compiler_hir::Instruction, + env: &mut Environment, + func: &HirFunction, +) -> Result>, CompilerDiagnostic> { + let mut effects: Vec = Vec::new(); + + // For function instructions, validate frozen mutation + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let inner_func = &env.functions[lowered_func.func.0 as usize]; + if let Some(ref aliasing_effects) = inner_func.aliasing_effects { + let context_ids: FxHashSet = + inner_func.context.iter().map(|p| p.identifier).collect(); + for effect in aliasing_effects { + let (mutate_value, is_mutate) = match effect { + AliasingEffect::Mutate { value, .. } => (value, true), + AliasingEffect::MutateTransitive { value } => (value, false), + _ => continue, + }; + if !context_ids.contains(&mutate_value.identifier) { + continue; + } + if !state.is_defined(mutate_value.identifier) { + continue; + } + let value_abstract = state.kind(mutate_value.identifier); + if value_abstract.kind == ValueKind::Frozen { + let reason_str = get_write_error_reason(&value_abstract); + let ident = &env.identifiers[mutate_value.identifier.0 as usize]; + let variable = match &ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => { + format!("`{}`", n) + } + _ => "value".to_string(), + }; + let mut diagnostic = CompilerDiagnostic::new( + ErrorCategory::Immutability, + "This value cannot be modified", + Some(reason_str), + ); + diagnostic.details.push( + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc: mutate_value.loc, + message: Some(format!("{} cannot be modified", variable)), + identifier_name: None, + }, + ); + if is_mutate { + if let AliasingEffect::Mutate { + reason: Some(MutationReason::AssignCurrentProperty), + .. + } = effect + { + diagnostic.details.push(crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Hint { + message: "Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in \"Ref\".".to_string() + }); + } + } + effects.push(AliasingEffect::MutateFrozen { + place: mutate_value.clone(), + error: diagnostic, + }); + } + } + } + } + _ => {} + } + + // Track which values we've already initialized + let mut initialized: FxHashSet = FxHashSet::default(); + + // Get the cached signature effects + let sig = context.instruction_signature_cache.get(&instr_idx).unwrap(); + let sig_effects: Vec = sig.effects.clone(); + + for effect in &sig_effects { + apply_effect(context, state, effect.clone(), &mut initialized, &mut effects, env, func)?; + } + + // If lvalue is not yet defined, initialize it with a default value. + // The TS version asserts this as an invariant, but the Rust port may have + // edge cases where effects don't cover the lvalue (e.g. missing signature entries). + if !state.is_defined(instr.lvalue.identifier) { + let vid = ValueId(instr.lvalue.identifier.0 | 0x80000000); + state.initialize( + vid, + AbstractValue { kind: ValueKind::Mutable, reason: hashset_of(ValueReason::Other) }, + ); + state.define(instr.lvalue.identifier, vid); + } + + Ok(if effects.is_empty() { None } else { Some(effects) }) +} + +// ============================================================================= +// Transitive freeze helper +// ============================================================================= + +/// Recursively freeze through FunctionExpression captures. If `value_id` +/// corresponds to a FunctionExpression, freeze each of its context captures +/// and recurse into any that are themselves FunctionExpressions. This matches +/// the TS `freezeValue` → `freeze` → `freezeValue` recursion chain. +fn freeze_function_captures_transitive( + state: &mut InferenceState, + context: &Context, + env: &Environment, + value_id: ValueId, + reason: ValueReason, +) { + if let Some(&func_id) = context.function_values.get(&value_id) { + let ctx_ids: Vec = + env.functions[func_id.0 as usize].context.iter().map(|p| p.identifier).collect(); + for ctx_id in ctx_ids { + // Replicate InferenceState::freeze() logic inline — + // we need to recurse with context/env which freeze() doesn't have. + if !state.variables.contains_key(&ctx_id) { + continue; + } + let kind = state.kind(ctx_id).kind; + match kind { + ValueKind::Context | ValueKind::Mutable | ValueKind::MaybeFrozen => { + let vids: Vec = state.values_for(ctx_id); + for vid in vids { + state.freeze_value(vid, reason); + // Recurse into nested function captures + freeze_function_captures_transitive(state, context, env, vid, reason); + } + } + ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => { + // Already frozen or immutable — no-op + } + } + } + } +} + +// ============================================================================= +// applyEffect +// ============================================================================= + +fn apply_effect( + context: &mut Context, + state: &mut InferenceState, + effect: AliasingEffect, + initialized: &mut FxHashSet, + effects: &mut Vec, + env: &mut Environment, + func: &HirFunction, +) -> Result<(), CompilerDiagnostic> { + let effect = context.intern_effect(effect); + match effect { + AliasingEffect::Freeze { ref value, reason } => { + let did_freeze = state.freeze(value.identifier, reason); + if did_freeze { + effects.push(effect.clone()); + // Transitively freeze FunctionExpression captures if enabled + // (matches TS freezeValue which recurses into func.context) + let enable_transitive = env.config.enable_preserve_existing_memoization_guarantees + || env.config.enable_transitively_freeze_function_expressions; + if enable_transitive { + // Recursively freeze through function captures. The TS + // freezeValue() calls freeze() on each capture, which + // calls freezeValue() again — creating a transitive + // closure through arbitrarily nested function captures. + let value_ids: Vec = state.values_for(value.identifier); + for vid in &value_ids { + freeze_function_captures_transitive(state, context, env, *vid, reason); + } + } + } + } + AliasingEffect::Create { ref into, value: kind, reason } => { + assert!( + !initialized.contains(&into.identifier), + "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction" + ); + initialized.insert(into.identifier); + let value_id = context.get_or_create_value_id(&effect); + state.initialize(value_id, AbstractValue { kind, reason: hashset_of(reason) }); + state.define(into.identifier, value_id); + effects.push(effect.clone()); + } + AliasingEffect::ImmutableCapture { ref from, .. } => { + let kind = state.kind(from.identifier).kind; + match kind { + ValueKind::Global | ValueKind::Primitive => { + // no-op: don't track data flow for copy types + } + _ => { + effects.push(effect.clone()); + } + } + } + AliasingEffect::CreateFrom { ref from, ref into } => { + assert!( + !initialized.contains(&into.identifier), + "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction" + ); + initialized.insert(into.identifier); + let from_value = state.kind(from.identifier); + let value_id = context.get_or_create_value_id(&effect); + state.initialize( + value_id, + AbstractValue { kind: from_value.kind, reason: from_value.reason.clone() }, + ); + state.define(into.identifier, value_id); + match from_value.kind { + ValueKind::Primitive | ValueKind::Global => { + let first_reason = primary_reason(&from_value.reason); + effects.push(AliasingEffect::Create { + value: from_value.kind, + into: into.clone(), + reason: first_reason, + }); + } + ValueKind::Frozen => { + let first_reason = primary_reason(&from_value.reason); + effects.push(AliasingEffect::Create { + value: from_value.kind, + into: into.clone(), + reason: first_reason, + }); + apply_effect( + context, + state, + AliasingEffect::ImmutableCapture { from: from.clone(), into: into.clone() }, + initialized, + effects, + env, + func, + )?; + } + _ => { + effects.push(effect.clone()); + } + } + } + AliasingEffect::CreateFunction { ref captures, function_id, ref into } => { + assert!( + !initialized.contains(&into.identifier), + "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction" + ); + initialized.insert(into.identifier); + effects.push(effect.clone()); + + // Check if function is mutable + let has_captures = captures.iter().any(|capture| { + if !state.is_defined(capture.identifier) { + return false; + } + let k = state.kind(capture.identifier).kind; + k == ValueKind::Context || k == ValueKind::Mutable + }); + + let inner_func = &env.functions[function_id.0 as usize]; + let has_tracked_side_effects = inner_func + .aliasing_effects + .as_ref() + .map(|effs| { + effs.iter().any(|e| { + matches!( + e, + AliasingEffect::MutateFrozen { .. } + | AliasingEffect::MutateGlobal { .. } + | AliasingEffect::Impure { .. } + ) + }) + }) + .unwrap_or(false); + + let captures_ref = inner_func + .context + .iter() + .any(|operand| is_ref_or_ref_value_for_id(env, operand.identifier)); + + let is_mutable = has_captures || has_tracked_side_effects || captures_ref; + + // Update context variable effects + let context_places: Vec = inner_func.context.clone(); + for operand in &context_places { + if operand.effect != Effect::Capture { + continue; + } + if !state.is_defined(operand.identifier) { + continue; + } + let kind = state.kind(operand.identifier).kind; + if kind == ValueKind::Primitive + || kind == ValueKind::Frozen + || kind == ValueKind::Global + { + // Downgrade to Read - we need to mutate the inner function + let inner_func_mut = &mut env.functions[function_id.0 as usize]; + for ctx in &mut inner_func_mut.context { + if ctx.identifier == operand.identifier && ctx.effect == Effect::Capture { + ctx.effect = Effect::Read; + } + } + } + } + + let value_id = context.get_or_create_value_id(&effect); + // Track this value as a function expression so Apply can look it up + context.function_values.insert(value_id, function_id); + state.initialize( + value_id, + AbstractValue { + kind: if is_mutable { ValueKind::Mutable } else { ValueKind::Frozen }, + reason: FxIndexSet::default(), + }, + ); + state.define(into.identifier, value_id); + + for capture in captures { + apply_effect( + context, + state, + AliasingEffect::Capture { from: capture.clone(), into: into.clone() }, + initialized, + effects, + env, + func, + )?; + } + } + AliasingEffect::MaybeAlias { ref from, ref into } + | AliasingEffect::Alias { ref from, ref into } + | AliasingEffect::Capture { ref from, ref into } => { + let is_capture = matches!(effect, AliasingEffect::Capture { .. }); + let is_maybe_alias = matches!(effect, AliasingEffect::MaybeAlias { .. }); + // For Alias, destination must already be initialized (Capture/MaybeAlias are exempt) + assert!( + is_capture || is_maybe_alias || initialized.contains(&into.identifier), + "[InferMutationAliasingEffects] Expected destination to already be initialized within this instruction" + ); + + // Check destination kind + let into_kind = state.kind_with_loc(into.identifier, into.loc).kind; + let destination_type = match into_kind { + ValueKind::Context => Some("context"), + ValueKind::Mutable | ValueKind::MaybeFrozen => Some("mutable"), + _ => None, + }; + + let from_kind = state.kind_with_loc(from.identifier, from.loc).kind; + let source_type = match from_kind { + ValueKind::Context => Some("context"), + ValueKind::Global | ValueKind::Primitive => None, + ValueKind::MaybeFrozen | ValueKind::Frozen => Some("frozen"), + ValueKind::Mutable => Some("mutable"), + }; + + if source_type == Some("frozen") { + apply_effect( + context, + state, + AliasingEffect::ImmutableCapture { from: from.clone(), into: into.clone() }, + initialized, + effects, + env, + func, + )?; + } else if (source_type == Some("mutable") && destination_type == Some("mutable")) + || is_maybe_alias + { + effects.push(effect.clone()); + } else if (source_type == Some("context") && destination_type.is_some()) + || (source_type == Some("mutable") && destination_type == Some("context")) + { + apply_effect( + context, + state, + AliasingEffect::MaybeAlias { from: from.clone(), into: into.clone() }, + initialized, + effects, + env, + func, + )?; + } + } + AliasingEffect::Assign { ref from, ref into } => { + assert!( + !initialized.contains(&into.identifier), + "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction" + ); + initialized.insert(into.identifier); + let from_value = state.kind_with_loc(from.identifier, from.loc); + match from_value.kind { + ValueKind::Frozen => { + apply_effect( + context, + state, + AliasingEffect::ImmutableCapture { from: from.clone(), into: into.clone() }, + initialized, + effects, + env, + func, + )?; + let cache_key = + format!("Assign_frozen:{}:{}", from.identifier.0, into.identifier.0); + let value_id = *context + .effect_value_id_cache + .entry(cache_key) + .or_insert_with(ValueId::new); + state.initialize( + value_id, + AbstractValue { kind: from_value.kind, reason: from_value.reason.clone() }, + ); + state.define(into.identifier, value_id); + } + ValueKind::Global | ValueKind::Primitive => { + let cache_key = + format!("Assign_copy:{}:{}", from.identifier.0, into.identifier.0); + let value_id = *context + .effect_value_id_cache + .entry(cache_key) + .or_insert_with(ValueId::new); + state.initialize( + value_id, + AbstractValue { kind: from_value.kind, reason: from_value.reason.clone() }, + ); + state.define(into.identifier, value_id); + } + _ => { + state.assign(into.identifier, from.identifier); + effects.push(effect.clone()); + } + } + } + AliasingEffect::Apply { + ref receiver, + ref function, + mutates_function, + ref args, + ref into, + ref signature, + ref loc, + } => { + // First, check if the callee is a locally-declared function expression + // whose aliasing effects we already know (TS lines 1016-1068) + if state.is_defined(function.identifier) { + let function_values = state.values_for(function.identifier); + if function_values.len() == 1 { + let value_id = function_values[0]; + if let Some(func_id) = context.function_values.get(&value_id).copied() { + let inner_func = &env.functions[func_id.0 as usize]; + if inner_func.aliasing_effects.is_some() { + // Build or retrieve the signature from the function expression + if !context.function_signature_cache.contains_key(&func_id) { + let sig = build_signature_from_function_expression(env, func_id); + context.function_signature_cache.insert(func_id, sig); + } + let sig = + context.function_signature_cache.get(&func_id).unwrap().clone(); + let inner_func = &env.functions[func_id.0 as usize]; + let context_places: Vec = inner_func.context.clone(); + let sig_effects = compute_effects_for_aliasing_signature( + env, + &sig, + into, + receiver, + args, + &context_places, + loc.as_ref(), + )?; + if let Some(sig_effs) = sig_effects { + // Conditionally mutate the function itself first + apply_effect( + context, + state, + AliasingEffect::MutateTransitiveConditionally { + value: function.clone(), + }, + initialized, + effects, + env, + func, + )?; + for se in sig_effs { + apply_effect( + context, + state, + se, + initialized, + effects, + env, + func, + )?; + } + return Ok(()); + } + } + } + } + } + if let Some(sig) = signature { + // Check known_incompatible (TS line 2351-2370) + if let Some(ref incompatible_msg) = sig.known_incompatible { + if env.enable_validations() { + let mut diagnostic = CompilerDiagnostic::new( + ErrorCategory::IncompatibleLibrary, + "Use of incompatible library", + Some( + "This API returns functions which cannot be memoized without leading to stale UI. \ + To prevent this, by default React Compiler will skip memoizing this component/hook. \ + However, you may see issues if values from this API are passed to other components/hooks that are \ + memoized".to_string(), + ), + ); + diagnostic.details.push(CompilerDiagnosticDetail::Error { + loc: receiver.loc, + message: Some(incompatible_msg.clone()), + identifier_name: None, + }); + // TS throws here, aborting compilation for this function + return Err(diagnostic); + } + } + + if let Some(ref aliasing) = sig.aliasing { + let sig_effects = compute_effects_for_aliasing_signature_config( + env, + aliasing, + into, + receiver, + args, + &[], + loc.as_ref(), + &mut context.aliasing_config_temp_cache, + )?; + if let Some(sig_effs) = sig_effects { + for se in sig_effs { + apply_effect(context, state, se, initialized, effects, env, func)?; + } + return Ok(()); + } + } + + // Legacy signature + let mut todo_errors: Vec = + Vec::new(); + let legacy_effects = compute_effects_for_legacy_signature( + state, + sig, + into, + receiver, + args, + loc.as_ref(), + env, + &context.function_values, + &mut todo_errors, + ); + // Todo errors should short-circuit (TS throws throwTodo) + if let Some(err_detail) = todo_errors.into_iter().next() { + return Err(CompilerDiagnostic::from_detail(err_detail)); + } + for le in legacy_effects { + apply_effect(context, state, le, initialized, effects, env, func)?; + } + } else { + // No signature: default behavior + apply_effect( + context, + state, + AliasingEffect::Create { + into: into.clone(), + value: ValueKind::Mutable, + reason: ValueReason::Other, + }, + initialized, + effects, + env, + func, + )?; + + let all_operands = build_apply_operands(receiver, function, args); + for (operand, _is_function_operand, is_spread) in &all_operands { + // In TS, the check is `operand !== effect.function || effect.mutatesFunction`. + // This compares by reference identity, so for CallExpression/NewExpression + // where receiver === function, BOTH are skipped when !mutatesFunction. + if operand.identifier == function.identifier && !mutates_function { + // Don't mutate callee for non-mutating calls + } else { + apply_effect( + context, + state, + AliasingEffect::MutateTransitiveConditionally { + value: operand.clone(), + }, + initialized, + effects, + env, + func, + )?; + } + + if *is_spread { + let ty = &env.types + [env.identifiers[operand.identifier.0 as usize].type_.0 as usize]; + if let Some(mutate_iter) = conditionally_mutate_iterator(operand, ty) { + apply_effect( + context, + state, + mutate_iter, + initialized, + effects, + env, + func, + )?; + } + } + + apply_effect( + context, + state, + AliasingEffect::MaybeAlias { from: operand.clone(), into: into.clone() }, + initialized, + effects, + env, + func, + )?; + + // In TS, `other === arg` compares the Place extracted from + // `otherArg` with the original `arg` element. For Identifier + // args, the extracted Place IS the arg, so this is a reference + // identity check. For Spread args, the extracted Place is + // `.place` which is never `===` the Spread wrapper object, + // so NO pairs are skipped when the outer arg is a Spread + // (including self-pairs, producing self-captures). + for (other, _other_is_func, _other_is_spread) in &all_operands { + if !is_spread && other.identifier == operand.identifier { + continue; + } + apply_effect( + context, + state, + AliasingEffect::Capture { from: operand.clone(), into: other.clone() }, + initialized, + effects, + env, + func, + )?; + } + } + } + } + ref eff @ (AliasingEffect::Mutate { .. } + | AliasingEffect::MutateConditionally { .. } + | AliasingEffect::MutateTransitive { .. } + | AliasingEffect::MutateTransitiveConditionally { .. }) => { + let (mutate_place, variant) = match eff { + AliasingEffect::Mutate { value, .. } => (value, MutateVariant::Mutate), + AliasingEffect::MutateConditionally { value } => { + (value, MutateVariant::MutateConditionally) + } + AliasingEffect::MutateTransitive { value } => { + (value, MutateVariant::MutateTransitive) + } + AliasingEffect::MutateTransitiveConditionally { value } => { + (value, MutateVariant::MutateTransitiveConditionally) + } + _ => unreachable!(), + }; + let value = mutate_place; + let mutation_kind = state.mutate_with_loc(variant, value.identifier, env, value.loc); + if mutation_kind == MutationResult::Mutate { + effects.push(effect.clone()); + } else if mutation_kind == MutationResult::MutateRef { + // no-op + } else if mutation_kind != MutationResult::None + && matches!(variant, MutateVariant::Mutate | MutateVariant::MutateTransitive) + { + let abstract_value = state.kind(value.identifier); + + let ident = &env.identifiers[value.identifier.0 as usize]; + let decl_id = ident.declaration_id; + + if mutation_kind == MutationResult::MutateFrozen + && context.hoisted_context_declarations.contains_key(&decl_id) + { + let variable = match &ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => { + Some(format!("`{}`", n)) + } + _ => None, + }; + let hoisted_access = + context.hoisted_context_declarations.get(&decl_id).cloned().flatten(); + let mut diagnostic = CompilerDiagnostic::new( + ErrorCategory::Immutability, + "Cannot access variable before it is declared", + Some(format!( + "{} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time", + variable.as_deref().unwrap_or("This variable") + )), + ); + if let Some(ref access) = hoisted_access { + if access.loc != value.loc { + diagnostic.details.push( + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc: access.loc, + message: Some(format!( + "{} accessed before it is declared", + variable.as_deref().unwrap_or("variable") + )), + identifier_name: None, + }, + ); + } + } + diagnostic.details.push( + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc: value.loc, + message: Some(format!( + "{} is declared here", + variable.as_deref().unwrap_or("variable") + )), + identifier_name: None, + }, + ); + apply_effect( + context, + state, + AliasingEffect::MutateFrozen { place: value.clone(), error: diagnostic }, + initialized, + effects, + env, + func, + )?; + } else { + let reason_str = get_write_error_reason(&abstract_value); + let variable = match &ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => { + format!("`{}`", n) + } + _ => "value".to_string(), + }; + let mut diagnostic = CompilerDiagnostic::new( + ErrorCategory::Immutability, + "This value cannot be modified", + Some(reason_str), + ); + diagnostic.details.push( + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc: value.loc, + message: Some(format!("{} cannot be modified", variable)), + identifier_name: None, + }, + ); + + if let AliasingEffect::Mutate { + reason: Some(MutationReason::AssignCurrentProperty), + .. + } = &effect + { + diagnostic.details.push(crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Hint { + message: "Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in \"Ref\".".to_string(), + }); + } + + let error_kind = if abstract_value.kind == ValueKind::Frozen { + AliasingEffect::MutateFrozen { place: value.clone(), error: diagnostic } + } else { + AliasingEffect::MutateGlobal { place: value.clone(), error: diagnostic } + }; + apply_effect(context, state, error_kind, initialized, effects, env, func)?; + } + } + } + AliasingEffect::Impure { .. } + | AliasingEffect::Render { .. } + | AliasingEffect::MutateFrozen { .. } + | AliasingEffect::MutateGlobal { .. } => { + effects.push(effect.clone()); + } + } + Ok(()) +} + +// ============================================================================= +// computeSignatureForInstruction +// ============================================================================= + +fn compute_signature_for_instruction( + context: &mut Context, + env: &Environment, + instr: &crate::react_compiler_hir::Instruction, + _func: &HirFunction, +) -> InstructionSignature { + let lvalue = &instr.lvalue; + let value = &instr.value; + let mut effects: Vec = Vec::new(); + + match value { + InstructionValue::ArrayExpression { elements, .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Mutable, + reason: ValueReason::Other, + }); + for element in elements { + match element { + crate::react_compiler_hir::ArrayElement::Place(p) => { + effects.push(AliasingEffect::Capture { + from: p.clone(), + into: lvalue.clone(), + }); + } + crate::react_compiler_hir::ArrayElement::Spread(s) => { + let ty = &env.types + [env.identifiers[s.place.identifier.0 as usize].type_.0 as usize]; + if let Some(mutate_iter) = conditionally_mutate_iterator(&s.place, ty) { + effects.push(mutate_iter); + } + effects.push(AliasingEffect::Capture { + from: s.place.clone(), + into: lvalue.clone(), + }); + } + crate::react_compiler_hir::ArrayElement::Hole => {} + } + } + } + InstructionValue::ObjectExpression { properties, .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Mutable, + reason: ValueReason::Other, + }); + for property in properties { + match property { + crate::react_compiler_hir::ObjectPropertyOrSpread::Property(p) => { + effects.push(AliasingEffect::Capture { + from: p.place.clone(), + into: lvalue.clone(), + }); + } + crate::react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => { + effects.push(AliasingEffect::Capture { + from: s.place.clone(), + into: lvalue.clone(), + }); + } + } + } + } + InstructionValue::Await { value: await_value, .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Mutable, + reason: ValueReason::Other, + }); + effects + .push(AliasingEffect::MutateTransitiveConditionally { value: await_value.clone() }); + effects + .push(AliasingEffect::Capture { from: await_value.clone(), into: lvalue.clone() }); + } + InstructionValue::NewExpression { callee, args, loc } => { + let sig = get_function_call_signature(env, callee.identifier).ok().flatten(); + effects.push(AliasingEffect::Apply { + receiver: callee.clone(), + function: callee.clone(), + mutates_function: false, + args: args.iter().map(place_or_spread_to_hole).collect(), + into: lvalue.clone(), + signature: sig, + loc: *loc, + }); + } + InstructionValue::CallExpression { callee, args, loc } => { + let sig = get_function_call_signature(env, callee.identifier).ok().flatten(); + effects.push(AliasingEffect::Apply { + receiver: callee.clone(), + function: callee.clone(), + mutates_function: true, + args: args.iter().map(place_or_spread_to_hole).collect(), + into: lvalue.clone(), + signature: sig, + loc: *loc, + }); + } + InstructionValue::MethodCall { receiver, property, args, loc } => { + let sig = get_function_call_signature(env, property.identifier).ok().flatten(); + effects.push(AliasingEffect::Apply { + receiver: receiver.clone(), + function: property.clone(), + mutates_function: false, + args: args.iter().map(place_or_spread_to_hole).collect(), + into: lvalue.clone(), + signature: sig, + loc: *loc, + }); + } + InstructionValue::PropertyDelete { object, .. } + | InstructionValue::ComputedDelete { object, .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + effects.push(AliasingEffect::Mutate { value: object.clone(), reason: None }); + } + InstructionValue::PropertyLoad { object, .. } + | InstructionValue::ComputedLoad { object, .. } => { + let ty = &env.types[env.identifiers[lvalue.identifier.0 as usize].type_.0 as usize]; + if crate::react_compiler_hir::is_primitive_type(ty) { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } else { + effects.push(AliasingEffect::CreateFrom { + from: object.clone(), + into: lvalue.clone(), + }); + } + } + InstructionValue::PropertyStore { object, property, value: store_value, .. } => { + let mutation_reason: Option = { + let obj_ty = + &env.types[env.identifiers[object.identifier.0 as usize].type_.0 as usize]; + if let crate::react_compiler_hir::PropertyLiteral::String(prop_name) = property { + if prop_name == "current" && matches!(obj_ty, Type::TypeVar { .. }) { + Some(MutationReason::AssignCurrentProperty) + } else { + None + } + } else { + None + } + }; + effects.push(AliasingEffect::Mutate { value: object.clone(), reason: mutation_reason }); + effects + .push(AliasingEffect::Capture { from: store_value.clone(), into: object.clone() }); + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } + InstructionValue::ComputedStore { object, value: store_value, .. } => { + effects.push(AliasingEffect::Mutate { value: object.clone(), reason: None }); + effects + .push(AliasingEffect::Capture { from: store_value.clone(), into: object.clone() }); + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let inner_func = &env.functions[lowered_func.func.0 as usize]; + let captures: Vec = inner_func + .context + .iter() + .filter(|operand| operand.effect == Effect::Capture) + .cloned() + .collect(); + effects.push(AliasingEffect::CreateFunction { + into: lvalue.clone(), + function_id: lowered_func.func, + captures, + }); + } + InstructionValue::GetIterator { collection, .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Mutable, + reason: ValueReason::Other, + }); + let ty = &env.types[env.identifiers[collection.identifier.0 as usize].type_.0 as usize]; + if is_builtin_collection_type(ty) { + effects.push(AliasingEffect::Capture { + from: collection.clone(), + into: lvalue.clone(), + }); + } else { + effects + .push(AliasingEffect::Alias { from: collection.clone(), into: lvalue.clone() }); + effects.push(AliasingEffect::MutateTransitiveConditionally { + value: collection.clone(), + }); + } + } + InstructionValue::IteratorNext { iterator, collection, .. } => { + effects.push(AliasingEffect::MutateConditionally { value: iterator.clone() }); + effects.push(AliasingEffect::CreateFrom { + from: collection.clone(), + into: lvalue.clone(), + }); + } + InstructionValue::NextPropertyOf { .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } + InstructionValue::JsxExpression { tag, props, children, .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Frozen, + reason: ValueReason::JsxCaptured, + }); + for operand in visitors::each_instruction_value_operand(value, env) { + effects.push(AliasingEffect::Freeze { + value: operand.clone(), + reason: ValueReason::JsxCaptured, + }); + effects + .push(AliasingEffect::Capture { from: operand.clone(), into: lvalue.clone() }); + } + if let JsxTag::Place(tag_place) = tag { + effects.push(AliasingEffect::Render { place: tag_place.clone() }); + } + if let Some(ch) = children { + for child in ch { + effects.push(AliasingEffect::Render { place: child.clone() }); + } + } + for prop in props { + if let crate::react_compiler_hir::JsxAttribute::Attribute { + place: prop_place, + .. + } = prop + { + let prop_ty = &env.types + [env.identifiers[prop_place.identifier.0 as usize].type_.0 as usize]; + if let Type::Function { return_type, .. } = prop_ty { + if crate::react_compiler_hir::is_jsx_type(return_type) + || is_phi_with_jsx(return_type) + { + effects.push(AliasingEffect::Render { place: prop_place.clone() }); + } + } + } + } + } + InstructionValue::JsxFragment { children: _, .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Frozen, + reason: ValueReason::JsxCaptured, + }); + for operand in visitors::each_instruction_value_operand(value, env) { + effects.push(AliasingEffect::Freeze { + value: operand.clone(), + reason: ValueReason::JsxCaptured, + }); + effects + .push(AliasingEffect::Capture { from: operand.clone(), into: lvalue.clone() }); + } + } + InstructionValue::DeclareLocal { lvalue: dl, .. } => { + effects.push(AliasingEffect::Create { + into: dl.place.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } + InstructionValue::Destructure { lvalue: dl, value: dest_value, .. } => { + for pat_item in each_pattern_items(&dl.pattern) { + match pat_item { + PatternItem::Place(place) => { + let ty = &env.types + [env.identifiers[place.identifier.0 as usize].type_.0 as usize]; + if crate::react_compiler_hir::is_primitive_type(ty) { + effects.push(AliasingEffect::Create { + into: place.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } else { + effects.push(AliasingEffect::CreateFrom { + from: dest_value.clone(), + into: place.clone(), + }); + } + } + PatternItem::Spread(place) => { + let value_kind = if context.non_mutating_spreads.contains(&place.identifier) + { + ValueKind::Frozen + } else { + ValueKind::Mutable + }; + effects.push(AliasingEffect::Create { + into: place.clone(), + reason: ValueReason::Other, + value: value_kind, + }); + effects.push(AliasingEffect::Capture { + from: dest_value.clone(), + into: place.clone(), + }); + } + } + } + effects.push(AliasingEffect::Assign { from: dest_value.clone(), into: lvalue.clone() }); + } + InstructionValue::LoadContext { place, .. } => { + effects.push(AliasingEffect::CreateFrom { from: place.clone(), into: lvalue.clone() }); + } + InstructionValue::DeclareContext { lvalue: dcl, .. } => { + let decl_id = env.identifiers[dcl.place.identifier.0 as usize].declaration_id; + let kind = dcl.kind; + if !context.hoisted_context_declarations.contains_key(&decl_id) + || kind == InstructionKind::HoistedConst + || kind == InstructionKind::HoistedFunction + || kind == InstructionKind::HoistedLet + { + effects.push(AliasingEffect::Create { + into: dcl.place.clone(), + value: ValueKind::Mutable, + reason: ValueReason::Other, + }); + } else { + effects.push(AliasingEffect::Mutate { value: dcl.place.clone(), reason: None }); + } + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } + InstructionValue::StoreContext { lvalue: scl, value: sc_value, .. } => { + let decl_id = env.identifiers[scl.place.identifier.0 as usize].declaration_id; + if scl.kind == InstructionKind::Reassign + || context.hoisted_context_declarations.contains_key(&decl_id) + { + effects.push(AliasingEffect::Mutate { value: scl.place.clone(), reason: None }); + } else { + effects.push(AliasingEffect::Create { + into: scl.place.clone(), + value: ValueKind::Mutable, + reason: ValueReason::Other, + }); + } + effects + .push(AliasingEffect::Capture { from: sc_value.clone(), into: scl.place.clone() }); + effects.push(AliasingEffect::Assign { from: sc_value.clone(), into: lvalue.clone() }); + } + InstructionValue::LoadLocal { place, .. } => { + effects.push(AliasingEffect::Assign { from: place.clone(), into: lvalue.clone() }); + } + InstructionValue::StoreLocal { lvalue: sl, value: sl_value, .. } => { + effects.push(AliasingEffect::Assign { from: sl_value.clone(), into: sl.place.clone() }); + effects.push(AliasingEffect::Assign { from: sl_value.clone(), into: lvalue.clone() }); + } + InstructionValue::PostfixUpdate { lvalue: pf_lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue: pf_lvalue, .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + effects.push(AliasingEffect::Create { + into: pf_lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } + InstructionValue::StoreGlobal { name, value: sg_value, loc: _, .. } => { + let variable = format!("`{}`", name); + let mut diagnostic = CompilerDiagnostic::new( + ErrorCategory::Globals, + "Cannot reassign variables declared outside of the component/hook", + Some(format!( + "Variable {} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)", + variable + )), + ); + diagnostic.details.push( + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc: instr.loc, + message: Some(format!("{} cannot be reassigned", variable)), + identifier_name: None, + }, + ); + effects + .push(AliasingEffect::MutateGlobal { place: sg_value.clone(), error: diagnostic }); + effects.push(AliasingEffect::Assign { from: sg_value.clone(), into: lvalue.clone() }); + } + InstructionValue::TypeCastExpression { value: tc_value, .. } => { + effects.push(AliasingEffect::Assign { from: tc_value.clone(), into: lvalue.clone() }); + } + InstructionValue::LoadGlobal { .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Global, + reason: ValueReason::Global, + }); + } + InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => { + if env.config.enable_preserve_existing_memoization_guarantees { + for operand in visitors::each_instruction_value_operand(value, env) { + effects.push(AliasingEffect::Freeze { + value: operand.clone(), + reason: ValueReason::HookCaptured, + }); + } + } + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } + // All primitive-creating instructions + InstructionValue::TaggedTemplateExpression { .. } + | InstructionValue::BinaryExpression { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::TemplateLiteral { .. } + | InstructionValue::UnaryExpression { .. } => { + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: ValueKind::Primitive, + reason: ValueReason::Other, + }); + } + } + + InstructionSignature { effects } +} + +// ============================================================================= +// Legacy signature support +// ============================================================================= + +fn compute_effects_for_legacy_signature( + state: &InferenceState, + signature: &FunctionSignature, + lvalue: &Place, + receiver: &Place, + args: &[PlaceOrSpreadOrHole], + _loc: Option<&SourceLocation>, + env: &Environment, + function_values: &FxHashMap, + todo_errors: &mut Vec, +) -> Vec { + let return_value_reason = signature.return_value_reason.unwrap_or(ValueReason::Other); + let mut effects: Vec = Vec::new(); + + effects.push(AliasingEffect::Create { + into: lvalue.clone(), + value: signature.return_value_kind, + reason: return_value_reason, + }); + + if signature.impure && env.config.validate_no_impure_functions_in_render { + let mut diagnostic = CompilerDiagnostic::new( + ErrorCategory::Purity, + "Cannot call impure function during render", + Some(format!( + "{}Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)", + if let Some(ref name) = signature.canonical_name { + format!("`{}` is an impure function. ", name) + } else { + String::new() + } + )), + ); + diagnostic.details.push( + crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc: _loc.copied(), + message: Some("Cannot call impure function".to_string()), + identifier_name: None, + }, + ); + effects.push(AliasingEffect::Impure { place: receiver.clone(), error: diagnostic }); + } + + // TODO: check signature.known_incompatible and throw (TS line 2351-2370) + // This requires threading Result through apply_effect/apply_signature. + + // If the function is mutable only if operands are mutable, and all + // arguments are immutable/non-mutating, short-circuit with simple aliasing. + if signature.mutable_only_if_operands_are_mutable + && are_arguments_immutable_and_non_mutating(state, args, env, function_values) + { + effects.push(AliasingEffect::Alias { from: receiver.clone(), into: lvalue.clone() }); + for arg in args { + match arg { + PlaceOrSpreadOrHole::Hole => continue, + PlaceOrSpreadOrHole::Place(place) + | PlaceOrSpreadOrHole::Spread(crate::react_compiler_hir::SpreadPattern { place }) => + { + effects.push(AliasingEffect::ImmutableCapture { + from: place.clone(), + into: lvalue.clone(), + }); + } + } + } + return effects; + } + + let mut stores: Vec = Vec::new(); + let mut captures: Vec = Vec::new(); + + let mut visit = |place: &Place, effect: Effect, effects: &mut Vec| match effect + { + Effect::Store => { + effects.push(AliasingEffect::Mutate { value: place.clone(), reason: None }); + stores.push(place.clone()); + } + Effect::Capture => { + captures.push(place.clone()); + } + Effect::ConditionallyMutate => { + effects.push(AliasingEffect::MutateTransitiveConditionally { value: place.clone() }); + } + Effect::ConditionallyMutateIterator => { + let ty = &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize]; + if let Some(mutate_iter) = conditionally_mutate_iterator(place, ty) { + effects.push(mutate_iter); + } + effects.push(AliasingEffect::Capture { from: place.clone(), into: lvalue.clone() }); + } + Effect::Freeze => { + effects + .push(AliasingEffect::Freeze { value: place.clone(), reason: return_value_reason }); + } + Effect::Mutate => { + effects.push(AliasingEffect::MutateTransitive { value: place.clone() }); + } + Effect::Read => { + effects.push(AliasingEffect::ImmutableCapture { + from: place.clone(), + into: lvalue.clone(), + }); + } + _ => {} + }; + + if signature.callee_effect != Effect::Capture { + effects.push(AliasingEffect::Alias { from: receiver.clone(), into: lvalue.clone() }); + } + + visit(receiver, signature.callee_effect, &mut effects); + for (i, arg) in args.iter().enumerate() { + match arg { + PlaceOrSpreadOrHole::Hole => continue, + PlaceOrSpreadOrHole::Place(place) + | PlaceOrSpreadOrHole::Spread(crate::react_compiler_hir::SpreadPattern { place }) => { + let is_spread = matches!(arg, PlaceOrSpreadOrHole::Spread(_)); + let sig_effect = if !is_spread && i < signature.positional_params.len() { + signature.positional_params[i] + } else { + signature.rest_param.unwrap_or(Effect::ConditionallyMutate) + }; + let (effect, err_detail) = get_argument_effect(sig_effect, is_spread, place.loc); + if let Some(d) = err_detail { + todo_errors.push(d); + } + visit(place, effect, &mut effects); + } + } + } + + if !captures.is_empty() { + if stores.is_empty() { + for capture in &captures { + effects.push(AliasingEffect::Alias { from: capture.clone(), into: lvalue.clone() }); + } + } else { + for capture in &captures { + for store in &stores { + effects.push(AliasingEffect::Capture { + from: capture.clone(), + into: store.clone(), + }); + } + } + } + } + + effects +} + +fn get_argument_effect( + sig_effect: Effect, + is_spread: bool, + spread_loc: Option, +) -> (Effect, Option) { + if !is_spread { + (sig_effect, None) + } else if sig_effect == Effect::Mutate || sig_effect == Effect::ConditionallyMutate { + (sig_effect, None) + } else { + // Spread with Freeze effect is unsupported for hook arguments + // (matches TS CompilerError.throwTodo) + let detail = if sig_effect == Effect::Freeze { + Some(crate::react_compiler_diagnostics::CompilerErrorDetail { + reason: "Support spread syntax for hook arguments".to_string(), + description: None, + category: ErrorCategory::Todo, + loc: spread_loc, + suggestions: None, + }) + } else { + None + }; + (Effect::ConditionallyMutateIterator, detail) + } +} + +/// Returns true if all of the arguments are both non-mutable (immutable or frozen) +/// _and_ are not functions which might mutate their arguments. +/// +/// Corresponds to TS `areArgumentsImmutableAndNonMutating`. +fn are_arguments_immutable_and_non_mutating( + state: &InferenceState, + args: &[PlaceOrSpreadOrHole], + env: &Environment, + function_values: &FxHashMap, +) -> bool { + for arg in args { + match arg { + PlaceOrSpreadOrHole::Hole => continue, + PlaceOrSpreadOrHole::Place(place) + | PlaceOrSpreadOrHole::Spread(crate::react_compiler_hir::SpreadPattern { place }) => { + // Check if it's a function type with a known signature + let is_place = matches!(arg, PlaceOrSpreadOrHole::Place(_)); + if is_place { + let ty = + &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize]; + if let Type::Function { .. } = ty { + let fn_shape = env.get_function_signature(ty).ok().flatten(); + if let Some(fn_sig) = fn_shape { + let has_mutable_param = fn_sig + .positional_params + .iter() + .any(|e| is_known_mutable_effect(*e)); + let has_mutable_rest = + fn_sig.rest_param.map_or(false, |e| is_known_mutable_effect(e)); + return !has_mutable_param && !has_mutable_rest; + } + } + } + + let kind = state.kind(place.identifier); + match kind.kind { + ValueKind::Primitive | ValueKind::Frozen => { + // Immutable values are ok, continue checking + } + _ => { + return false; + } + } + + // Check if any value for this place is a function expression + // that mutates its parameters (TS lines 2545-2557) + let value_ids = state.values_for(place.identifier); + for vid in &value_ids { + if let Some(&func_id) = function_values.get(vid) { + let inner_func = &env.functions[func_id.0 as usize]; + let mutates_params = inner_func.params.iter().any(|param| { + let param_id = match param { + ParamPattern::Place(p) => p.identifier, + ParamPattern::Spread(s) => s.place.identifier, + }; + let ident = &env.identifiers[param_id.0 as usize]; + ident.mutable_range.end.0 > ident.mutable_range.start.0 + 1 + }); + if mutates_params { + return false; + } + } + } + } + } + } + true +} + +fn is_known_mutable_effect(effect: Effect) -> bool { + matches!( + effect, + Effect::Store + | Effect::Mutate + | Effect::ConditionallyMutate + | Effect::ConditionallyMutateIterator + ) +} + +// ============================================================================= +// Aliasing signature config support (new-style signatures) +// ============================================================================= + +fn compute_effects_for_aliasing_signature_config( + env: &mut Environment, + config: &crate::react_compiler_hir::type_config::AliasingSignatureConfig, + lvalue: &Place, + receiver: &Place, + args: &[PlaceOrSpreadOrHole], + context: &[Place], + _loc: Option<&SourceLocation>, + temp_cache: &mut FxHashMap<(IdentifierId, String), Place>, +) -> Result>, CompilerDiagnostic> { + // Build substitutions from config strings to places + let mut substitutions: FxHashMap> = FxHashMap::default(); + substitutions.insert(config.receiver.clone(), vec![receiver.clone()]); + substitutions.insert(config.returns.clone(), vec![lvalue.clone()]); + + let mut mutable_spreads: FxHashSet = FxHashSet::default(); + + for (i, arg) in args.iter().enumerate() { + match arg { + PlaceOrSpreadOrHole::Hole => continue, + PlaceOrSpreadOrHole::Place(place) + | PlaceOrSpreadOrHole::Spread(crate::react_compiler_hir::SpreadPattern { place }) => { + if i < config.params.len() && !matches!(arg, PlaceOrSpreadOrHole::Spread(_)) { + substitutions.insert(config.params[i].clone(), vec![place.clone()]); + } else if let Some(ref rest) = config.rest { + substitutions.entry(rest.clone()).or_default().push(place.clone()); + } else { + return Ok(None); + } + + if matches!(arg, PlaceOrSpreadOrHole::Spread(_)) { + let ty = + &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize]; + let mutate_iterator = conditionally_mutate_iterator(place, ty); + if mutate_iterator.is_some() { + mutable_spreads.insert(place.identifier); + } + } + } + } + } + + for operand in context { + let ident = &env.identifiers[operand.identifier.0 as usize]; + if let Some(ref name) = ident.name { + substitutions.insert(format!("@{}", name.value()), vec![operand.clone()]); + } + } + + // Create temporaries (cached by lvalue + temp_name to be stable across fixpoint iterations) + for temp_name in &config.temporaries { + let cache_key = (lvalue.identifier, temp_name.clone()); + let temp_place = temp_cache + .entry(cache_key) + .or_insert_with(|| create_temp_place(env, receiver.loc)) + .clone(); + substitutions.insert(temp_name.clone(), vec![temp_place]); + } + + let mut effects: Vec = Vec::new(); + + for eff_config in &config.effects { + match eff_config { + crate::react_compiler_hir::type_config::AliasingEffectConfig::Freeze { value, reason } => { + let values = substitutions.get(value).cloned().unwrap_or_default(); + for v in values { + if mutable_spreads.contains(&v.identifier) { + return Err(CompilerDiagnostic::todo( + "Support spread syntax for hook arguments", + v.loc, + )); + } + effects.push(AliasingEffect::Freeze { value: v, reason: *reason }); + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::Create { into, value, reason } => { + let intos = substitutions.get(into).cloned().unwrap_or_default(); + for v in intos { + effects.push(AliasingEffect::Create { into: v, value: *value, reason: *reason }); + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::CreateFrom { from, into } => { + let froms = substitutions.get(from).cloned().unwrap_or_default(); + let intos = substitutions.get(into).cloned().unwrap_or_default(); + for f in &froms { + for t in &intos { + effects.push(AliasingEffect::CreateFrom { from: f.clone(), into: t.clone() }); + } + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::Assign { from, into } => { + let froms = substitutions.get(from).cloned().unwrap_or_default(); + let intos = substitutions.get(into).cloned().unwrap_or_default(); + for f in &froms { + for t in &intos { + effects.push(AliasingEffect::Assign { from: f.clone(), into: t.clone() }); + } + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::Alias { from, into } => { + let froms = substitutions.get(from).cloned().unwrap_or_default(); + let intos = substitutions.get(into).cloned().unwrap_or_default(); + for f in &froms { + for t in &intos { + effects.push(AliasingEffect::Alias { from: f.clone(), into: t.clone() }); + } + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::Capture { from, into } => { + let froms = substitutions.get(from).cloned().unwrap_or_default(); + let intos = substitutions.get(into).cloned().unwrap_or_default(); + for f in &froms { + for t in &intos { + effects.push(AliasingEffect::Capture { from: f.clone(), into: t.clone() }); + } + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::ImmutableCapture { from, into } => { + let froms = substitutions.get(from).cloned().unwrap_or_default(); + let intos = substitutions.get(into).cloned().unwrap_or_default(); + for f in &froms { + for t in &intos { + effects.push(AliasingEffect::ImmutableCapture { from: f.clone(), into: t.clone() }); + } + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::Impure { place } => { + let values = substitutions.get(place).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::Impure { + place: v, + error: CompilerDiagnostic::new(ErrorCategory::Purity, "Impure function call", None), + }); + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::Mutate { value } => { + let values = substitutions.get(value).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::Mutate { value: v, reason: None }); + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::MutateTransitiveConditionally { value } => { + let values = substitutions.get(value).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::MutateTransitiveConditionally { value: v }); + } + } + crate::react_compiler_hir::type_config::AliasingEffectConfig::Apply { receiver: r, function: f, mutates_function, args: a, into: i } => { + let recv = substitutions.get(r).and_then(|v| v.first()).cloned(); + let func = substitutions.get(f).and_then(|v| v.first()).cloned(); + let into = substitutions.get(i).and_then(|v| v.first()).cloned(); + if let (Some(recv), Some(func), Some(into)) = (recv, func, into) { + let mut apply_args: Vec = Vec::new(); + for arg in a { + match arg { + crate::react_compiler_hir::type_config::ApplyArgConfig::Hole { .. } => { + apply_args.push(PlaceOrSpreadOrHole::Hole); + } + crate::react_compiler_hir::type_config::ApplyArgConfig::Place(name) => { + if let Some(places) = substitutions.get(name) { + if let Some(p) = places.first() { + apply_args.push(PlaceOrSpreadOrHole::Place(p.clone())); + } + } + } + crate::react_compiler_hir::type_config::ApplyArgConfig::Spread { place: name, .. } => { + if let Some(places) = substitutions.get(name) { + if let Some(p) = places.first() { + apply_args.push(PlaceOrSpreadOrHole::Spread(crate::react_compiler_hir::SpreadPattern { place: p.clone() })); + } + } + } + } + } + effects.push(AliasingEffect::Apply { + receiver: recv, + function: func, + mutates_function: *mutates_function, + args: apply_args, + into, + signature: None, + loc: _loc.copied(), + }); + } else { + return Ok(None); + } + } + } + } + + Ok(Some(effects)) +} + +// ============================================================================= +// Function expression signature building +// ============================================================================= + +/// Build an AliasingSignature from a function expression's params/returns/aliasing effects. +/// Corresponds to TS `buildSignatureFromFunctionExpression`. +fn build_signature_from_function_expression( + env: &mut Environment, + func_id: FunctionId, +) -> AliasingSignature { + let inner_func = &env.functions[func_id.0 as usize]; + let mut params: Vec = Vec::new(); + let mut rest: Option = None; + for param in &inner_func.params { + match param { + ParamPattern::Place(p) => params.push(p.identifier), + ParamPattern::Spread(s) => rest = Some(s.place.identifier), + } + } + let returns = inner_func.returns.identifier; + let aliasing_effects = inner_func.aliasing_effects.clone().unwrap_or_default(); + let loc = inner_func.loc; + + if rest.is_none() { + let temp = create_temp_place(env, loc); + rest = Some(temp.identifier); + } + + AliasingSignature { + receiver: IdentifierId(0), + params, + rest, + returns, + effects: aliasing_effects, + temporaries: Vec::new(), + } +} + +/// Compute effects by substituting an AliasingSignature (IdentifierId-based) +/// with actual arguments. Corresponds to TS `computeEffectsForSignature`. +fn compute_effects_for_aliasing_signature( + env: &mut Environment, + signature: &AliasingSignature, + lvalue: &Place, + receiver: &Place, + args: &[PlaceOrSpreadOrHole], + context: &[Place], + _loc: Option<&SourceLocation>, +) -> Result>, CompilerDiagnostic> { + if signature.params.len() > args.len() + || (args.len() > signature.params.len() && signature.rest.is_none()) + { + return Ok(None); + } + + let mut mutable_spreads: FxHashSet = FxHashSet::default(); + let mut substitutions: FxHashMap> = FxHashMap::default(); + substitutions.insert(signature.receiver, vec![receiver.clone()]); + substitutions.insert(signature.returns, vec![lvalue.clone()]); + + for (i, arg) in args.iter().enumerate() { + match arg { + PlaceOrSpreadOrHole::Hole => continue, + PlaceOrSpreadOrHole::Place(place) + | PlaceOrSpreadOrHole::Spread(crate::react_compiler_hir::SpreadPattern { place }) => { + let is_spread = matches!(arg, PlaceOrSpreadOrHole::Spread(_)); + if !is_spread && i < signature.params.len() { + substitutions.insert(signature.params[i], vec![place.clone()]); + } else if let Some(rest_id) = signature.rest { + substitutions.entry(rest_id).or_default().push(place.clone()); + } else { + return Ok(None); + } + + if is_spread { + let ty = + &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize]; + let mutate_iterator = conditionally_mutate_iterator(place, ty); + if mutate_iterator.is_some() { + mutable_spreads.insert(place.identifier); + } + } + } + } + } + + // Add context variable substitutions (identity mapping) + for operand in context { + substitutions.insert(operand.identifier, vec![operand.clone()]); + } + + // Create temporaries + for temp in &signature.temporaries { + let temp_place = create_temp_place(env, receiver.loc); + substitutions.insert(temp.identifier, vec![temp_place]); + } + + let mut effects: Vec = Vec::new(); + + for eff in &signature.effects { + match eff { + AliasingEffect::MaybeAlias { from, into } + | AliasingEffect::Assign { from, into } + | AliasingEffect::ImmutableCapture { from, into } + | AliasingEffect::Alias { from, into } + | AliasingEffect::CreateFrom { from, into } + | AliasingEffect::Capture { from, into } => { + let from_places = substitutions.get(&from.identifier).cloned().unwrap_or_default(); + let to_places = substitutions.get(&into.identifier).cloned().unwrap_or_default(); + for f in &from_places { + for t in &to_places { + effects.push(match eff { + AliasingEffect::MaybeAlias { .. } => { + AliasingEffect::MaybeAlias { from: f.clone(), into: t.clone() } + } + AliasingEffect::Assign { .. } => { + AliasingEffect::Assign { from: f.clone(), into: t.clone() } + } + AliasingEffect::ImmutableCapture { .. } => { + AliasingEffect::ImmutableCapture { + from: f.clone(), + into: t.clone(), + } + } + AliasingEffect::Alias { .. } => { + AliasingEffect::Alias { from: f.clone(), into: t.clone() } + } + AliasingEffect::CreateFrom { .. } => { + AliasingEffect::CreateFrom { from: f.clone(), into: t.clone() } + } + AliasingEffect::Capture { .. } => { + AliasingEffect::Capture { from: f.clone(), into: t.clone() } + } + _ => unreachable!(), + }); + } + } + } + AliasingEffect::Impure { place, error } => { + let values = substitutions.get(&place.identifier).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::Impure { place: v, error: error.clone() }); + } + } + AliasingEffect::MutateFrozen { place, error } => { + let values = substitutions.get(&place.identifier).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::MutateFrozen { place: v, error: error.clone() }); + } + } + AliasingEffect::MutateGlobal { place, error } => { + let values = substitutions.get(&place.identifier).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::MutateGlobal { place: v, error: error.clone() }); + } + } + AliasingEffect::Render { place } => { + let values = substitutions.get(&place.identifier).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::Render { place: v }); + } + } + AliasingEffect::Mutate { value, reason } => { + let values = substitutions.get(&value.identifier).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::Mutate { value: v, reason: reason.clone() }); + } + } + AliasingEffect::MutateConditionally { value } => { + let values = substitutions.get(&value.identifier).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::MutateConditionally { value: v }); + } + } + AliasingEffect::MutateTransitive { value } => { + let values = substitutions.get(&value.identifier).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::MutateTransitive { value: v }); + } + } + AliasingEffect::MutateTransitiveConditionally { value } => { + let values = substitutions.get(&value.identifier).cloned().unwrap_or_default(); + for v in values { + effects.push(AliasingEffect::MutateTransitiveConditionally { value: v }); + } + } + AliasingEffect::Freeze { value, reason } => { + let values = substitutions.get(&value.identifier).cloned().unwrap_or_default(); + for v in values { + if mutable_spreads.contains(&v.identifier) { + return Err(CompilerDiagnostic::todo( + "Support spread syntax for hook arguments", + v.loc, + )); + } + effects.push(AliasingEffect::Freeze { value: v, reason: *reason }); + } + } + AliasingEffect::Create { into, value, reason } => { + let intos = substitutions.get(&into.identifier).cloned().unwrap_or_default(); + for v in intos { + effects.push(AliasingEffect::Create { + into: v, + value: *value, + reason: *reason, + }); + } + } + AliasingEffect::Apply { + receiver: r, + function: f, + mutates_function: mf, + args: a, + into: i, + signature: s, + loc: _l, + } => { + let recv = substitutions.get(&r.identifier).and_then(|v| v.first()).cloned(); + let func = substitutions.get(&f.identifier).and_then(|v| v.first()).cloned(); + let apply_into = substitutions.get(&i.identifier).and_then(|v| v.first()).cloned(); + if let (Some(recv), Some(func), Some(apply_into)) = (recv, func, apply_into) { + let mut apply_args: Vec = Vec::new(); + for arg in a { + match arg { + PlaceOrSpreadOrHole::Hole => apply_args.push(PlaceOrSpreadOrHole::Hole), + PlaceOrSpreadOrHole::Place(p) => { + if let Some(places) = substitutions.get(&p.identifier) { + if let Some(place) = places.first() { + apply_args.push(PlaceOrSpreadOrHole::Place(place.clone())); + } + } + } + PlaceOrSpreadOrHole::Spread(sp) => { + if let Some(places) = substitutions.get(&sp.place.identifier) { + if let Some(place) = places.first() { + apply_args.push(PlaceOrSpreadOrHole::Spread( + crate::react_compiler_hir::SpreadPattern { + place: place.clone(), + }, + )); + } + } + } + } + } + effects.push(AliasingEffect::Apply { + receiver: recv, + function: func, + mutates_function: *mf, + args: apply_args, + into: apply_into, + signature: s.clone(), + loc: _loc.copied(), + }); + } else { + return Ok(None); + } + } + AliasingEffect::CreateFunction { .. } => { + // Not supported in signature substitution + return Ok(None); + } + } + } + + Ok(Some(effects)) +} + +// ============================================================================= +// Helpers +// ============================================================================= + +/// Select the primary (most specific) reason from a set of reasons. +/// TS uses `[...set][0]` which returns the first-inserted element; +/// since the primary reason is always inserted first, this effectively +/// picks the most specific non-Other reason. We replicate this by +/// preferring any non-Other reason over Other. +fn primary_reason(reasons: &FxIndexSet) -> ValueReason { + for &r in reasons { + if r != ValueReason::Other { + return r; + } + } + ValueReason::Other +} + +fn get_write_error_reason(abstract_value: &AbstractValue) -> String { + if abstract_value.reason.contains(&ValueReason::Global) { + "Modifying a variable defined outside a component or hook is not allowed. Consider using an effect".to_string() + } else if abstract_value.reason.contains(&ValueReason::JsxCaptured) { + "Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX".to_string() + } else if abstract_value.reason.contains(&ValueReason::Context) { + "Modifying a value returned from 'useContext()' is not allowed.".to_string() + } else if abstract_value.reason.contains(&ValueReason::KnownReturnSignature) { + "Modifying a value returned from a function whose return value should not be mutated" + .to_string() + } else if abstract_value.reason.contains(&ValueReason::ReactiveFunctionArgument) { + "Modifying component props or hook arguments is not allowed. Consider using a local variable instead".to_string() + } else if abstract_value.reason.contains(&ValueReason::State) { + "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead".to_string() + } else if abstract_value.reason.contains(&ValueReason::ReducerState) { + "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead".to_string() + } else if abstract_value.reason.contains(&ValueReason::Effect) { + "Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()".to_string() + } else if abstract_value.reason.contains(&ValueReason::HookCaptured) { + "Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook".to_string() + } else if abstract_value.reason.contains(&ValueReason::HookReturn) { + "Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed".to_string() + } else { + "This modifies a variable that React considers immutable".to_string() + } +} + +fn conditionally_mutate_iterator(place: &Place, ty: &Type) -> Option { + if !is_builtin_collection_type(ty) { + Some(AliasingEffect::MutateTransitiveConditionally { value: place.clone() }) + } else { + None + } +} + +fn is_builtin_collection_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } + if id == BUILT_IN_ARRAY_ID || id == BUILT_IN_SET_ID || id == BUILT_IN_MAP_ID + ) +} + +fn get_function_call_signature( + env: &Environment, + callee_id: IdentifierId, +) -> Result, CompilerDiagnostic> { + let ty = &env.types[env.identifiers[callee_id.0 as usize].type_.0 as usize]; + Ok(env.get_function_signature(ty)?.cloned()) +} + +fn is_ref_or_ref_value_for_id(env: &Environment, id: IdentifierId) -> bool { + let ty = &env.types[env.identifiers[id.0 as usize].type_.0 as usize]; + crate::react_compiler_hir::is_ref_or_ref_value(ty) +} + +fn get_hook_kind_for_type<'a>( + env: &'a Environment, + ty: &Type, +) -> Result, CompilerDiagnostic> { + env.get_hook_kind_for_type(ty) +} + +/// Format a Type for printPlace-style output, matching TS's `printType()`. +fn format_type_for_print(ty: &Type) -> String { + match ty { + Type::Primitive => String::new(), + Type::Function { shape_id, return_type, .. } => { + if let Some(sid) = shape_id { + let ret = format_type_for_print(return_type); + if ret.is_empty() { + format!(":TFunction<{}>()", sid) + } else { + format!(":TFunction<{}>(): {}", sid, ret) + } + } else { + ":TFunction".to_string() + } + } + Type::Object { shape_id } => { + if let Some(sid) = shape_id { + format!(":TObject<{}>", sid) + } else { + ":TObject".to_string() + } + } + Type::Poly => ":TPoly".to_string(), + Type::Phi { .. } => ":TPhi".to_string(), + Type::Property { .. } => ":TProperty".to_string(), + Type::TypeVar { .. } => String::new(), + Type::ObjectMethod => ":TObjectMethod".to_string(), + } +} + +fn is_phi_with_jsx(ty: &Type) -> bool { + if let Type::Phi { operands } = ty { + operands.iter().any(|op| crate::react_compiler_hir::is_jsx_type(op)) + } else { + false + } +} + +fn place_or_spread_to_hole(pos: &PlaceOrSpread) -> PlaceOrSpreadOrHole { + match pos { + PlaceOrSpread::Place(p) => PlaceOrSpreadOrHole::Place(p.clone()), + PlaceOrSpread::Spread(s) => PlaceOrSpreadOrHole::Spread(s.clone()), + } +} + +use crate::react_compiler_hir::JsxTag; + +fn build_apply_operands( + receiver: &Place, + function: &Place, + args: &[PlaceOrSpreadOrHole], +) -> Vec<(Place, bool, bool)> { + let mut result = vec![(receiver.clone(), false, false), (function.clone(), true, false)]; + for arg in args { + match arg { + PlaceOrSpreadOrHole::Hole => continue, + PlaceOrSpreadOrHole::Place(p) => result.push((p.clone(), false, false)), + PlaceOrSpreadOrHole::Spread(s) => result.push((s.place.clone(), false, true)), + } + } + result +} + +fn create_temp_place(env: &mut Environment, loc: Option) -> Place { + let id = env.next_identifier_id(); + env.identifiers[id.0 as usize].loc = loc; + Place { identifier: id, effect: Effect::Unknown, reactive: false, loc } +} + +// ============================================================================= +// Terminal successor helper +// ============================================================================= + +/// Returns the successor blocks used for traversal in mutation/aliasing inference. +/// +/// Matches the TS `eachTerminalSuccessor` which yields standard control-flow +/// successors but NOT pseudo-successors (fallthroughs). Fallthroughs for +/// Logical/Ternary/Optional and Try/Scope/PrunedScope are reached naturally +/// via the block iteration order (blocks are stored in topological order). +fn terminal_successors(terminal: &crate::react_compiler_hir::Terminal) -> Vec { + use crate::react_compiler_hir::Terminal; + match terminal { + Terminal::Goto { block, .. } => vec![*block], + Terminal::If { consequent, alternate, .. } => vec![*consequent, *alternate], + Terminal::Branch { consequent, alternate, .. } => vec![*consequent, *alternate], + Terminal::Switch { cases, .. } => cases.iter().map(|c| c.block).collect(), + Terminal::For { init, .. } => vec![*init], + Terminal::ForOf { init, .. } | Terminal::ForIn { init, .. } => vec![*init], + Terminal::DoWhile { loop_block, .. } => vec![*loop_block], + Terminal::While { test, .. } => vec![*test], + Terminal::Return { .. } + | Terminal::Throw { .. } + | Terminal::Unreachable { .. } + | Terminal::Unsupported { .. } => vec![], + Terminal::Try { block, .. } => vec![*block], + Terminal::MaybeThrow { continuation, handler, .. } => { + let mut v = vec![*continuation]; + if let Some(h) = handler { + v.push(*h); + } + v + } + Terminal::Label { block, .. } | Terminal::Sequence { block, .. } => vec![*block], + Terminal::Logical { test, .. } | Terminal::Ternary { test, .. } => vec![*test], + Terminal::Optional { test, .. } => vec![*test], + Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => vec![*block], + } +} + +/// Pattern item helper for Destructure. +/// +/// NOTE: This cannot use `visitors::each_pattern_operand` because callers need +/// to distinguish Place from Spread elements — Spread elements get different +/// aliasing effects (Create + Capture) vs Place elements (Create or CreateFrom). +enum PatternItem<'a> { + Place(&'a Place), + Spread(&'a Place), +} + +fn each_pattern_items(pattern: &crate::react_compiler_hir::Pattern) -> Vec> { + let mut items = Vec::new(); + match pattern { + crate::react_compiler_hir::Pattern::Array(arr) => { + for el in &arr.items { + match el { + crate::react_compiler_hir::ArrayPatternElement::Place(p) => { + items.push(PatternItem::Place(p)) + } + crate::react_compiler_hir::ArrayPatternElement::Spread(s) => { + items.push(PatternItem::Spread(&s.place)) + } + crate::react_compiler_hir::ArrayPatternElement::Hole => {} + } + } + } + crate::react_compiler_hir::Pattern::Object(obj) => { + for prop in &obj.properties { + match prop { + crate::react_compiler_hir::ObjectPropertyOrSpread::Property(p) => { + items.push(PatternItem::Place(&p.place)) + } + crate::react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => { + items.push(PatternItem::Spread(&s.place)) + } + } + } + } + } + items +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/infer_mutation_aliasing_ranges.rs b/crates/oxc_react_compiler/src/react_compiler_inference/infer_mutation_aliasing_ranges.rs new file mode 100644 index 0000000000000..457c6ea87ac03 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/infer_mutation_aliasing_ranges.rs @@ -0,0 +1,1107 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Infers mutable ranges for identifiers and populates Place effects. +//! +//! Ported from TypeScript `src/Inference/InferMutationAliasingRanges.ts`. +//! +//! This pass builds an abstract model of the heap and interprets the effects of +//! the given function in order to determine: +//! - The mutable ranges of all identifiers in the function +//! - The externally-visible effects of the function (mutations of params/context +//! vars, aliasing between params/context-vars/return-value) +//! - The legacy `Effect` to store on each Place + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_utils::FxIndexMap; + +use crate::react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::type_config::{ValueKind, ValueReason}; +use crate::react_compiler_hir::visitors::{ + each_instruction_value_lvalue, for_each_instruction_value_lvalue_mut, + for_each_instruction_value_operand_mut, for_each_terminal_operand_mut, +}; +use crate::react_compiler_hir::{ + AliasingEffect, BlockId, Effect, EvaluationOrder, FunctionId, HirFunction, IdentifierId, + InstructionValue, MutationReason, Place, SourceLocation, is_jsx_type, is_primitive_type, +}; + +// ============================================================================= +// MutationKind +// ============================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[allow(dead_code)] +enum MutationKind { + None = 0, + Conditional = 1, + Definite = 2, +} + +// ============================================================================= +// Node and AliasingState +// ============================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EdgeKind { + Capture, + Alias, + MaybeAlias, +} + +#[derive(Debug, Clone)] +struct Edge { + index: usize, + node: IdentifierId, + kind: EdgeKind, +} + +#[derive(Debug, Clone)] +struct MutationInfo { + kind: MutationKind, + loc: Option, +} + +#[derive(Debug, Clone)] +enum NodeValue { + Object, + Phi, + Function { function_id: FunctionId }, +} + +#[derive(Debug, Clone)] +struct Node { + id: IdentifierId, + created_from: FxIndexMap, + captures: FxIndexMap, + aliases: FxIndexMap, + maybe_aliases: FxIndexMap, + edges: Vec, + transitive: Option, + local: Option, + last_mutated: usize, + mutation_reason: Option, + value: NodeValue, +} + +impl Node { + fn new(id: IdentifierId, value: NodeValue) -> Self { + Node { + id, + created_from: FxIndexMap::default(), + captures: FxIndexMap::default(), + aliases: FxIndexMap::default(), + maybe_aliases: FxIndexMap::default(), + edges: Vec::new(), + transitive: None, + local: None, + last_mutated: 0, + mutation_reason: None, + value, + } + } +} + +struct AliasingState { + nodes: FxIndexMap, +} + +impl AliasingState { + fn new() -> Self { + AliasingState { nodes: FxIndexMap::default() } + } + + fn create(&mut self, place: &Place, value: NodeValue) { + self.nodes.insert(place.identifier, Node::new(place.identifier, value)); + } + + fn create_from(&mut self, index: usize, from: &Place, into: &Place) { + self.create(into, NodeValue::Object); + let from_id = from.identifier; + let into_id = into.identifier; + // Add forward edge from -> into on the from node + if let Some(from_node) = self.nodes.get_mut(&from_id) { + from_node.edges.push(Edge { index, node: into_id, kind: EdgeKind::Alias }); + } + // Add created_from on the into node + if let Some(to_node) = self.nodes.get_mut(&into_id) { + to_node.created_from.entry(from_id).or_insert(index); + } + } + + fn capture(&mut self, index: usize, from: &Place, into: &Place) { + let from_id = from.identifier; + let into_id = into.identifier; + if !self.nodes.contains_key(&from_id) || !self.nodes.contains_key(&into_id) { + return; + } + self.nodes.get_mut(&from_id).unwrap().edges.push(Edge { + index, + node: into_id, + kind: EdgeKind::Capture, + }); + self.nodes.get_mut(&into_id).unwrap().captures.entry(from_id).or_insert(index); + } + + fn assign(&mut self, index: usize, from: &Place, into: &Place) { + let from_id = from.identifier; + let into_id = into.identifier; + if !self.nodes.contains_key(&from_id) || !self.nodes.contains_key(&into_id) { + return; + } + self.nodes.get_mut(&from_id).unwrap().edges.push(Edge { + index, + node: into_id, + kind: EdgeKind::Alias, + }); + self.nodes.get_mut(&into_id).unwrap().aliases.entry(from_id).or_insert(index); + } + + fn maybe_alias(&mut self, index: usize, from: &Place, into: &Place) { + let from_id = from.identifier; + let into_id = into.identifier; + if !self.nodes.contains_key(&from_id) || !self.nodes.contains_key(&into_id) { + return; + } + self.nodes.get_mut(&from_id).unwrap().edges.push(Edge { + index, + node: into_id, + kind: EdgeKind::MaybeAlias, + }); + self.nodes.get_mut(&into_id).unwrap().maybe_aliases.entry(from_id).or_insert(index); + } + + fn render(&self, index: usize, start: IdentifierId, env: &mut Environment) { + let mut seen = FxHashSet::default(); + let mut queue: Vec = vec![start]; + while let Some(current) = queue.pop() { + if !seen.insert(current) { + continue; + } + let node = match self.nodes.get(¤t) { + Some(n) => n, + None => continue, + }; + if node.transitive.is_some() || node.local.is_some() { + continue; + } + if let NodeValue::Function { function_id } = &node.value { + append_function_errors(env, *function_id); + } + for (&alias, &when) in &node.created_from { + if when >= index { + continue; + } + queue.push(alias); + } + for (&alias, &when) in &node.aliases { + if when >= index { + continue; + } + queue.push(alias); + } + for (&capture, &when) in &node.captures { + if when >= index { + continue; + } + queue.push(capture); + } + } + } + + fn mutate( + &mut self, + index: usize, + start: IdentifierId, + end: Option, // None for simulated mutations + transitive: bool, + start_kind: MutationKind, + loc: Option, + reason: Option, + env: &mut Environment, + should_record_errors: bool, + ) { + #[derive(Clone)] + struct QueueEntry { + place: IdentifierId, + transitive: bool, + direction: Direction, + kind: MutationKind, + } + #[derive(Clone, Copy, PartialEq)] + enum Direction { + Backwards, + Forwards, + } + + let mut seen: FxHashMap = FxHashMap::default(); + let mut queue: Vec = vec![QueueEntry { + place: start, + transitive, + direction: Direction::Backwards, + kind: start_kind, + }]; + + while let Some(entry) = queue.pop() { + let current = entry.place; + let previous_kind = seen.get(¤t).copied(); + if let Some(prev) = previous_kind { + if prev >= entry.kind { + continue; + } + } + seen.insert(current, entry.kind); + + let node = match self.nodes.get_mut(¤t) { + Some(n) => n, + None => continue, + }; + + if node.mutation_reason.is_none() { + node.mutation_reason = reason.clone(); + } + node.last_mutated = node.last_mutated.max(index); + + if let Some(end_val) = end { + let ident = &mut env.identifiers[node.id.0 as usize]; + ident.mutable_range.end = EvaluationOrder(ident.mutable_range.end.0.max(end_val.0)); + } + + if let NodeValue::Function { function_id } = &node.value { + if node.transitive.is_none() && node.local.is_none() { + if should_record_errors { + append_function_errors(env, *function_id); + } + } + } + + if entry.transitive { + match &node.transitive { + None => { + node.transitive = Some(MutationInfo { kind: entry.kind, loc }); + } + Some(existing) if existing.kind < entry.kind => { + node.transitive = Some(MutationInfo { kind: entry.kind, loc }); + } + _ => {} + } + } else { + match &node.local { + None => { + node.local = Some(MutationInfo { kind: entry.kind, loc }); + } + Some(existing) if existing.kind < entry.kind => { + node.local = Some(MutationInfo { kind: entry.kind, loc }); + } + _ => {} + } + } + + // Forward edges: Capture a -> b, Alias a -> b: mutate(a) => mutate(b) + // Collect edges to avoid borrow conflict + let edges: Vec = node.edges.clone(); + let node_value_kind = match &node.value { + NodeValue::Phi => "Phi", + _ => "Other", + }; + let node_aliases: Vec<(IdentifierId, usize)> = + node.aliases.iter().map(|(&k, &v)| (k, v)).collect(); + let node_maybe_aliases: Vec<(IdentifierId, usize)> = + node.maybe_aliases.iter().map(|(&k, &v)| (k, v)).collect(); + let node_captures: Vec<(IdentifierId, usize)> = + node.captures.iter().map(|(&k, &v)| (k, v)).collect(); + let node_created_from: Vec<(IdentifierId, usize)> = + node.created_from.iter().map(|(&k, &v)| (k, v)).collect(); + + for edge in &edges { + if edge.index >= index { + break; + } + queue.push(QueueEntry { + place: edge.node, + transitive: entry.transitive, + direction: Direction::Forwards, + // MaybeAlias edges downgrade to conditional mutation + kind: if edge.kind == EdgeKind::MaybeAlias { + MutationKind::Conditional + } else { + entry.kind + }, + }); + } + + for (alias, when) in &node_created_from { + if *when >= index { + continue; + } + queue.push(QueueEntry { + place: *alias, + transitive: true, + direction: Direction::Backwards, + kind: entry.kind, + }); + } + + if entry.direction == Direction::Backwards || node_value_kind != "Phi" { + // Backward alias edges + for (alias, when) in &node_aliases { + if *when >= index { + continue; + } + queue.push(QueueEntry { + place: *alias, + transitive: entry.transitive, + direction: Direction::Backwards, + kind: entry.kind, + }); + } + // MaybeAlias backward edges (downgrade to conditional) + for (alias, when) in &node_maybe_aliases { + if *when >= index { + continue; + } + queue.push(QueueEntry { + place: *alias, + transitive: entry.transitive, + direction: Direction::Backwards, + kind: MutationKind::Conditional, + }); + } + } + + // Only transitive mutations affect captures backward + if entry.transitive { + for (capture, when) in &node_captures { + if *when >= index { + continue; + } + queue.push(QueueEntry { + place: *capture, + transitive: entry.transitive, + direction: Direction::Backwards, + kind: entry.kind, + }); + } + } + } + } +} + +// ============================================================================= +// Helper: append function errors +// ============================================================================= + +fn append_function_errors(env: &mut Environment, function_id: FunctionId) { + let func = &env.functions[function_id.0 as usize]; + if let Some(ref effects) = func.aliasing_effects { + // Collect errors first to avoid borrow conflict + let errors: Vec<_> = effects + .iter() + .filter_map(|effect| match effect { + AliasingEffect::Impure { error, .. } + | AliasingEffect::MutateFrozen { error, .. } + | AliasingEffect::MutateGlobal { error, .. } => Some(error.clone()), + _ => None, + }) + .collect(); + for error in errors { + env.record_diagnostic(error); + } + } +} + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Infers mutable ranges for identifiers and populates Place effects. +/// +/// Returns the externally-visible effects of the function (mutations of +/// params/context-vars, aliasing between params/context-vars/return). +/// +/// Corresponds to TS `inferMutationAliasingRanges(fn, {isFunctionExpression})`. +pub fn infer_mutation_aliasing_ranges( + func: &mut HirFunction, + env: &mut Environment, + is_function_expression: bool, +) -> Result, CompilerDiagnostic> { + let mut function_effects: Vec = Vec::new(); + + // ========================================================================= + // Part 1: Build data flow graph and infer mutable ranges + // ========================================================================= + let mut state = AliasingState::new(); + + struct PendingPhiOperand { + from: Place, + into: Place, + index: usize, + } + let mut pending_phis: FxHashMap> = FxHashMap::default(); + + struct PendingMutation { + index: usize, + id: EvaluationOrder, + transitive: bool, + kind: MutationKind, + place: Place, + reason: Option, + } + let mut mutations: Vec = Vec::new(); + + struct PendingRender { + index: usize, + place: Place, + } + let mut renders: Vec = Vec::new(); + + let mut index: usize = 0; + + let should_record_errors = !is_function_expression && env.enable_validations(); + + // Create nodes for params, context vars, and return + for param in &func.params { + let place = match param { + crate::react_compiler_hir::ParamPattern::Place(p) => p, + crate::react_compiler_hir::ParamPattern::Spread(s) => &s.place, + }; + state.create(place, NodeValue::Object); + } + for ctx in &func.context { + state.create(ctx, NodeValue::Object); + } + state.create(&func.returns, NodeValue::Object); + + let mut seen_blocks: FxHashSet = FxHashSet::default(); + + // Collect block iteration data to avoid borrow conflicts + let block_order: Vec = func.body.blocks.keys().cloned().collect(); + + for &block_id in &block_order { + let block = &func.body.blocks[&block_id]; + + // Process phis + for phi in &block.phis { + state.create(&phi.place, NodeValue::Phi); + for (&pred, operand) in &phi.operands { + if !seen_blocks.contains(&pred) { + pending_phis.entry(pred).or_insert_with(Vec::new).push(PendingPhiOperand { + from: operand.clone(), + into: phi.place.clone(), + index, + }); + index += 1; + } else { + state.assign(index, operand, &phi.place); + index += 1; + } + } + } + seen_blocks.insert(block_id); + + // Process instruction effects + let instr_ids: Vec<_> = block.instructions.clone(); + for instr_id in &instr_ids { + let instr = &func.instructions[instr_id.0 as usize]; + let instr_eval_order = instr.id; + let effects = match &instr.effects { + Some(e) => e.clone(), + None => continue, + }; + for effect in &effects { + match effect { + AliasingEffect::Create { into, .. } => { + state.create(into, NodeValue::Object); + } + AliasingEffect::CreateFunction { into, function_id, .. } => { + state.create(into, NodeValue::Function { function_id: *function_id }); + } + AliasingEffect::CreateFrom { from, into } => { + state.create_from(index, from, into); + index += 1; + } + AliasingEffect::Assign { from, into } => { + if !state.nodes.contains_key(&into.identifier) { + state.create(into, NodeValue::Object); + } + state.assign(index, from, into); + index += 1; + } + AliasingEffect::Alias { from, into } => { + state.assign(index, from, into); + index += 1; + } + AliasingEffect::MaybeAlias { from, into } => { + state.maybe_alias(index, from, into); + index += 1; + } + AliasingEffect::Capture { from, into } => { + state.capture(index, from, into); + index += 1; + } + AliasingEffect::MutateTransitive { value } + | AliasingEffect::MutateTransitiveConditionally { value } => { + let is_transitive_conditional = + matches!(effect, AliasingEffect::MutateTransitiveConditionally { .. }); + mutations.push(PendingMutation { + index, + id: instr_eval_order, + transitive: true, + kind: if is_transitive_conditional { + MutationKind::Conditional + } else { + MutationKind::Definite + }, + reason: None, + place: value.clone(), + }); + index += 1; + } + AliasingEffect::Mutate { value, reason } => { + mutations.push(PendingMutation { + index, + id: instr_eval_order, + transitive: false, + kind: MutationKind::Definite, + reason: reason.clone(), + place: value.clone(), + }); + index += 1; + } + AliasingEffect::MutateConditionally { value } => { + mutations.push(PendingMutation { + index, + id: instr_eval_order, + transitive: false, + kind: MutationKind::Conditional, + reason: None, + place: value.clone(), + }); + index += 1; + } + AliasingEffect::MutateFrozen { .. } + | AliasingEffect::MutateGlobal { .. } + | AliasingEffect::Impure { .. } => { + if should_record_errors { + match effect { + AliasingEffect::MutateFrozen { error, .. } + | AliasingEffect::MutateGlobal { error, .. } + | AliasingEffect::Impure { error, .. } => { + env.record_diagnostic(error.clone()); + } + _ => unreachable!(), + } + } + function_effects.push(effect.clone()); + } + AliasingEffect::Render { place } => { + renders.push(PendingRender { index, place: place.clone() }); + index += 1; + function_effects.push(effect.clone()); + } + // Other effects (Freeze, ImmutableCapture, Apply) are no-ops here + _ => {} + } + } + } + + // Process pending phis for this block + let block = &func.body.blocks[&block_id]; + if let Some(block_phis) = pending_phis.remove(&block_id) { + for pending in block_phis { + state.assign(pending.index, &pending.from, &pending.into); + } + } + + // Handle return terminal + let terminal = &block.terminal; + if let crate::react_compiler_hir::Terminal::Return { value, .. } = terminal { + state.assign(index, value, &func.returns); + index += 1; + } + + // Handle terminal effects (MaybeThrow and Return) + let terminal_effects = match terminal { + crate::react_compiler_hir::Terminal::MaybeThrow { effects, .. } + | crate::react_compiler_hir::Terminal::Return { effects, .. } => effects.clone(), + _ => None, + }; + if let Some(effects) = terminal_effects { + for effect in &effects { + match effect { + AliasingEffect::Alias { from, into } => { + state.assign(index, from, into); + index += 1; + } + AliasingEffect::Freeze { .. } => { + // Expected for MaybeThrow terminals, skip + } + _ => { + // TS: CompilerError.invariant(effect.kind === 'Freeze', ...) + // We skip non-Alias, non-Freeze effects + } + } + } + } + } + + // Process mutations + for mutation in &mutations { + state.mutate( + mutation.index, + mutation.place.identifier, + Some(EvaluationOrder(mutation.id.0 + 1)), + mutation.transitive, + mutation.kind, + mutation.place.loc, + mutation.reason.clone(), + env, + should_record_errors, + ); + } + + // Process renders + for render in &renders { + if should_record_errors { + state.render(render.index, render.place.identifier, env); + } + } + + // Collect function effects for context vars and params + // NOTE: TS iterates [...fn.context, ...fn.params] — context first, then params + for ctx in &func.context { + collect_param_effects(&state, ctx, &mut function_effects); + } + for param in &func.params { + let place = match param { + crate::react_compiler_hir::ParamPattern::Place(p) => p, + crate::react_compiler_hir::ParamPattern::Spread(s) => &s.place, + }; + collect_param_effects(&state, place, &mut function_effects); + } + + // Set effect on mutated params/context vars + // We need to do this in a separate pass because we need to know which params + // were mutated before setting effects + let mut captured_params: FxHashSet = FxHashSet::default(); + for param in &func.params { + let place = match param { + crate::react_compiler_hir::ParamPattern::Place(p) => p, + crate::react_compiler_hir::ParamPattern::Spread(s) => &s.place, + }; + if let Some(node) = state.nodes.get(&place.identifier) { + if node.local.is_some() || node.transitive.is_some() { + captured_params.insert(place.identifier); + } + } + } + for ctx in &func.context { + if let Some(node) = state.nodes.get(&ctx.identifier) { + if node.local.is_some() || node.transitive.is_some() { + captured_params.insert(ctx.identifier); + } + } + } + + // Now mutate the effects on params/context in place + for param in &mut func.params { + let place = match param { + crate::react_compiler_hir::ParamPattern::Place(p) => p, + crate::react_compiler_hir::ParamPattern::Spread(s) => &mut s.place, + }; + if captured_params.contains(&place.identifier) { + place.effect = Effect::Capture; + } + } + for ctx in &mut func.context { + if captured_params.contains(&ctx.identifier) { + ctx.effect = Effect::Capture; + } + } + + // ========================================================================= + // Part 2: Add legacy operand-specific effects based on instruction effects + // and mutable ranges. Also fix up mutable range start values. + // ========================================================================= + // Part 2 loop + for &block_id in &block_order { + let block = &func.body.blocks[&block_id]; + + // Process phis + let phi_data: Vec<_> = block + .phis + .iter() + .map(|phi| { + let first_instr_id = block + .instructions + .first() + .map(|id| func.instructions[id.0 as usize].id) + .unwrap_or_else(|| block.terminal.evaluation_order()); + + let is_mutated_after_creation = + env.identifiers[phi.place.identifier.0 as usize].mutable_range.end + > first_instr_id; + + ( + phi.place.identifier, + phi.operands.values().map(|o| o.identifier).collect::>(), + is_mutated_after_creation, + first_instr_id, + ) + }) + .collect(); + + for (phi_id, _operand_ids, is_mutated_after_creation, first_instr_id) in &phi_data { + // Set phi place effect to Store + // We need to find this phi in the block and set it + let block = func.body.blocks.get_mut(&block_id).unwrap(); + for phi in &mut block.phis { + if phi.place.identifier == *phi_id { + phi.place.effect = Effect::Store; + for operand in phi.operands.values_mut() { + operand.effect = + if *is_mutated_after_creation { Effect::Capture } else { Effect::Read }; + } + break; + } + } + + if *is_mutated_after_creation { + let ident = &mut env.identifiers[phi_id.0 as usize]; + if ident.mutable_range.start == EvaluationOrder(0) { + ident.mutable_range.start = EvaluationOrder(first_instr_id.0.saturating_sub(1)); + } + } + } + + let block = &func.body.blocks[&block_id]; + let instr_ids: Vec<_> = block.instructions.clone(); + + for instr_id in &instr_ids { + let instr = &func.instructions[instr_id.0 as usize]; + let eval_order = instr.id; + + // Set lvalue effect to ConditionallyMutate and fix up mutable range + // This covers the top-level lvalue + let lvalue_id = instr.lvalue.identifier; + { + let ident = &mut env.identifiers[lvalue_id.0 as usize]; + if ident.mutable_range.start == EvaluationOrder(0) { + ident.mutable_range.start = eval_order; + } + if ident.mutable_range.end == EvaluationOrder(0) { + ident.mutable_range.end = + EvaluationOrder((eval_order.0 + 1).max(ident.mutable_range.end.0)); + } + } + func.instructions[instr_id.0 as usize].lvalue.effect = Effect::ConditionallyMutate; + + // Also handle value-level lvalues (DeclareLocal, StoreLocal, etc.) + let value_lvalue_ids: Vec = + each_instruction_value_lvalue(&func.instructions[instr_id.0 as usize].value) + .into_iter() + .map(|p| p.identifier) + .collect(); + for vlid in &value_lvalue_ids { + let ident = &mut env.identifiers[vlid.0 as usize]; + if ident.mutable_range.start == EvaluationOrder(0) { + ident.mutable_range.start = eval_order; + } + if ident.mutable_range.end == EvaluationOrder(0) { + ident.mutable_range.end = + EvaluationOrder((eval_order.0 + 1).max(ident.mutable_range.end.0)); + } + } + for_each_instruction_value_lvalue_mut( + &mut func.instructions[instr_id.0 as usize].value, + &mut |place| { + place.effect = Effect::ConditionallyMutate; + }, + ); + + // Set operand effects to Read + for_each_instruction_value_operand_mut( + &mut func.instructions[instr_id.0 as usize].value, + &mut |place| { + place.effect = Effect::Read; + }, + ); + + let instr = &func.instructions[instr_id.0 as usize]; + if instr.effects.is_none() { + continue; + } + + // Compute operand effects from instruction effects + let effects = instr.effects.as_ref().unwrap().clone(); + let mut operand_effects: FxHashMap = FxHashMap::default(); + + for effect in &effects { + match effect { + AliasingEffect::Assign { from, into, .. } + | AliasingEffect::Alias { from, into } + | AliasingEffect::Capture { from, into } + | AliasingEffect::CreateFrom { from, into } + | AliasingEffect::MaybeAlias { from, into } => { + let is_mutated_or_reassigned = + env.identifiers[into.identifier.0 as usize].mutable_range.end + > eval_order; + if is_mutated_or_reassigned { + operand_effects.insert(from.identifier, Effect::Capture); + operand_effects.insert(into.identifier, Effect::Store); + } else { + operand_effects.insert(from.identifier, Effect::Read); + operand_effects.insert(into.identifier, Effect::Store); + } + } + AliasingEffect::CreateFunction { .. } | AliasingEffect::Create { .. } => { + // no-op + } + AliasingEffect::Mutate { value, .. } => { + operand_effects.insert(value.identifier, Effect::Store); + } + AliasingEffect::Apply { .. } => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects", + None, + )); + } + AliasingEffect::MutateTransitive { value, .. } + | AliasingEffect::MutateConditionally { value } + | AliasingEffect::MutateTransitiveConditionally { value } => { + operand_effects.insert(value.identifier, Effect::ConditionallyMutate); + } + AliasingEffect::Freeze { value, .. } => { + operand_effects.insert(value.identifier, Effect::Freeze); + } + AliasingEffect::ImmutableCapture { .. } => { + // no-op, Read is the default + } + AliasingEffect::Impure { .. } + | AliasingEffect::Render { .. } + | AliasingEffect::MutateFrozen { .. } + | AliasingEffect::MutateGlobal { .. } => { + // no-op + } + } + } + + // Apply operand effects to top-level lvalue + let instr = &mut func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + if let Some(&effect) = operand_effects.get(&lvalue_id) { + instr.lvalue.effect = effect; + } + // Apply operand effects to value-level lvalues + for_each_instruction_value_lvalue_mut(&mut instr.value, &mut |place| { + if let Some(&effect) = operand_effects.get(&place.identifier) { + place.effect = effect; + } + }); + + // Apply operand effects to value operands and fix up mutable ranges + { + let mut apply = |place: &mut Place| { + // Fix up mutable range start + let ident = &env.identifiers[place.identifier.0 as usize]; + if ident.mutable_range.end > eval_order + && ident.mutable_range.start == EvaluationOrder(0) + { + env.identifiers[place.identifier.0 as usize].mutable_range.start = + eval_order; + } + // Apply effect + if let Some(&effect) = operand_effects.get(&place.identifier) { + place.effect = effect; + } + }; + for_each_instruction_value_operand_mut(&mut instr.value, &mut apply); + + // FunctionExpression/ObjectMethod context variables are operands that + // require env access (they live in env.functions[func_id].context). + if let InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } = &instr.value + { + let func_id = lowered_func.func; + let ctx_ids: Vec = env.functions[func_id.0 as usize] + .context + .iter() + .map(|c| c.identifier) + .collect(); + for ctx_id in &ctx_ids { + let ident = &env.identifiers[ctx_id.0 as usize]; + if ident.mutable_range.end > eval_order + && ident.mutable_range.start == EvaluationOrder(0) + { + env.identifiers[ctx_id.0 as usize].mutable_range.start = eval_order; + } + let effect = operand_effects.get(ctx_id).copied().unwrap_or(Effect::Read); + let inner_func = &mut env.functions[func_id.0 as usize]; + for ctx_place in &mut inner_func.context { + if ctx_place.identifier == *ctx_id { + ctx_place.effect = effect; + } + } + } + } + } + + // Handle StoreContext case: extend rvalue range if needed + let instr = &func.instructions[instr_id.0 as usize]; + if let InstructionValue::StoreContext { value, .. } = &instr.value { + let val_id = value.identifier; + let val_range_end = env.identifiers[val_id.0 as usize].mutable_range.end; + if val_range_end <= eval_order { + env.identifiers[val_id.0 as usize].mutable_range.end = + EvaluationOrder(eval_order.0 + 1); + } + } + } + + // Set terminal operand effects + let block = func.body.blocks.get_mut(&block_id).unwrap(); + match &mut block.terminal { + crate::react_compiler_hir::Terminal::Return { value, .. } => { + value.effect = if is_function_expression { Effect::Read } else { Effect::Freeze }; + } + terminal => { + for_each_terminal_operand_mut(terminal, &mut |place| { + place.effect = Effect::Read; + }); + } + } + } + + // ========================================================================= + // Part 3: Finish populating the externally visible effects + // ========================================================================= + let returns_id = func.returns.identifier; + let returns_type_id = env.identifiers[returns_id.0 as usize].type_; + let returns_type = &env.types[returns_type_id.0 as usize]; + let return_value_kind = if is_primitive_type(returns_type) { + ValueKind::Primitive + } else if is_jsx_type(returns_type) { + ValueKind::Frozen + } else { + ValueKind::Mutable + }; + + function_effects.push(AliasingEffect::Create { + into: func.returns.clone(), + value: return_value_kind, + reason: ValueReason::KnownReturnSignature, + }); + + // Determine precise data-flow effects by simulating transitive mutations + let mut tracked: Vec = Vec::new(); + for param in &func.params { + let place = match param { + crate::react_compiler_hir::ParamPattern::Place(p) => p.clone(), + crate::react_compiler_hir::ParamPattern::Spread(s) => s.place.clone(), + }; + tracked.push(place); + } + for ctx in &func.context { + tracked.push(ctx.clone()); + } + tracked.push(func.returns.clone()); + + let returns_identifier_id = func.returns.identifier; + + for i in 0..tracked.len() { + let into = tracked[i].clone(); + let mutation_index = index; + index += 1; + + state.mutate( + mutation_index, + into.identifier, + None, // simulated mutation + true, + MutationKind::Conditional, + into.loc, + None, + env, + false, // never record errors for simulated mutations + ); + + for j in 0..tracked.len() { + let from = &tracked[j]; + if from.identifier == into.identifier || from.identifier == returns_identifier_id { + continue; + } + + let from_node = state.nodes.get(&from.identifier); + assert!( + from_node.is_some(), + "Expected a node to exist for all parameters and context variables" + ); + let from_node = from_node.unwrap(); + + if from_node.last_mutated == mutation_index { + if into.identifier == returns_identifier_id { + function_effects + .push(AliasingEffect::Alias { from: from.clone(), into: into.clone() }); + } else { + function_effects + .push(AliasingEffect::Capture { from: from.clone(), into: into.clone() }); + } + } + } + } + + Ok(function_effects) +} + +// ============================================================================= +// Helper: collect param/context mutation effects +// ============================================================================= + +fn collect_param_effects( + state: &AliasingState, + place: &Place, + function_effects: &mut Vec, +) { + let node = match state.nodes.get(&place.identifier) { + Some(n) => n, + None => return, + }; + + if let Some(ref local) = node.local { + match local.kind { + MutationKind::Conditional => { + function_effects.push(AliasingEffect::MutateConditionally { + value: Place { loc: local.loc, ..place.clone() }, + }); + } + MutationKind::Definite => { + function_effects.push(AliasingEffect::Mutate { + value: Place { loc: local.loc, ..place.clone() }, + reason: node.mutation_reason.clone(), + }); + } + MutationKind::None => {} + } + } + + if let Some(ref transitive) = node.transitive { + match transitive.kind { + MutationKind::Conditional => { + function_effects.push(AliasingEffect::MutateTransitiveConditionally { + value: Place { loc: transitive.loc, ..place.clone() }, + }); + } + MutationKind::Definite => { + function_effects.push(AliasingEffect::MutateTransitive { + value: Place { loc: transitive.loc, ..place.clone() }, + }); + } + MutationKind::None => {} + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/infer_reactive_places.rs b/crates/oxc_react_compiler/src/react_compiler_inference/infer_reactive_places.rs new file mode 100644 index 0000000000000..683ea7a001fe5 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/infer_reactive_places.rs @@ -0,0 +1,773 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Infers which `Place`s are reactive. +//! +//! Ported from TypeScript `src/Inference/InferReactivePlaces.ts`. +//! +//! A place is reactive if it derives from any source of reactivity: +//! 1. Props (component parameters may change between renders) +//! 2. Hooks (can access state or context) +//! 3. `use` operator (can access context) +//! 4. Mutation with reactive operands +//! 5. Conditional assignment based on reactive control flow + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; +use crate::react_compiler_hir::dominator::post_dominator_frontier; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::object_shape::HookKind; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::{ + BlockId, Effect, FunctionId, HirFunction, IdentifierId, InstructionValue, ParamPattern, + Terminal, Type, +}; + +use crate::react_compiler_utils::DisjointSet; + +use crate::react_compiler_inference::infer_reactive_scope_variables::find_disjoint_mutable_values; + +// ============================================================================= +// Public API +// ============================================================================= + +/// Infer which places in a function are reactive. +/// +/// Corresponds to TS `inferReactivePlaces(fn: HIRFunction): void`. +pub fn infer_reactive_places( + func: &mut HirFunction, + env: &mut Environment, +) -> Result<(), CompilerDiagnostic> { + let mut aliased_identifiers = find_disjoint_mutable_values(func, env); + let mut reactive_map = ReactivityMap::new(&mut aliased_identifiers); + let mut stable_sidemap = StableSidemap::new(); + + // Mark all function parameters as reactive + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + reactive_map.mark_reactive(place.identifier); + } + + // Compute control dominators + let post_dominators = crate::react_compiler_hir::dominator::compute_post_dominator_tree( + func, + env.next_block_id().0, + false, + )?; + + // Collect block IDs for iteration + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + + // Track phi operand reactive flags during fixpoint. + // In TS, isReactive() sets place.reactive as a side effect. But when a phi + // is already reactive, the TS `continue`s and skips operand processing. + // We track which phi operand Places should be marked reactive. + // Key: (block_id, phi_idx, operand_idx), Value: should be reactive + let mut phi_operand_reactive: FxHashMap<(BlockId, usize, usize), bool> = FxHashMap::default(); + + // Fixpoint iteration — compute reactive set + loop { + for block_id in &block_ids { + let block = func.body.blocks.get(block_id).unwrap(); + let has_reactive_control = + is_reactive_controlled_block(block.id, func, &post_dominators, &mut reactive_map); + + // Process phi nodes + let block = func.body.blocks.get(block_id).unwrap(); + for (phi_idx, phi) in block.phis.iter().enumerate() { + if reactive_map.is_reactive(phi.place.identifier) { + // TS does `continue` here — skips operand isReactive calls. + // phi operand reactive flags stay as they were from last visit. + continue; + } + let mut is_phi_reactive = false; + for (op_idx, (_pred, operand)) in phi.operands.iter().enumerate() { + let op_reactive = reactive_map.is_reactive(operand.identifier); + // Record the reactive state for this operand at this point + phi_operand_reactive.insert((*block_id, phi_idx, op_idx), op_reactive); + if op_reactive { + is_phi_reactive = true; + break; // TS breaks here — remaining operands NOT visited + } + } + if is_phi_reactive { + reactive_map.mark_reactive(phi.place.identifier); + } else { + for (pred, _operand) in &phi.operands { + if is_reactive_controlled_block( + *pred, + func, + &post_dominators, + &mut reactive_map, + ) { + reactive_map.mark_reactive(phi.place.identifier); + break; + } + } + } + } + + // Process instructions + let block = func.body.blocks.get(block_id).unwrap(); + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + + // Handle stable identifier sources + stable_sidemap.handle_instruction(instr, env); + + let value = &instr.value; + + // Check if any operand is reactive + let mut has_reactive_input = false; + let operands: Vec = + visitors::each_instruction_value_operand(value, env) + .into_iter() + .map(|p| p.identifier) + .collect(); + for &op_id in &operands { + let reactive = reactive_map.is_reactive(op_id); + has_reactive_input = has_reactive_input || reactive; + } + + // Hooks and `use` operator are sources of reactivity + match value { + InstructionValue::CallExpression { callee, .. } => { + let callee_ty = &env.types + [env.identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if get_hook_kind_for_type(env, callee_ty)?.is_some() + || is_use_operator_type(callee_ty) + { + has_reactive_input = true; + } + } + InstructionValue::MethodCall { property, .. } => { + let property_ty = &env.types + [env.identifiers[property.identifier.0 as usize].type_.0 as usize]; + if get_hook_kind_for_type(env, property_ty)?.is_some() + || is_use_operator_type(property_ty) + { + has_reactive_input = true; + } + } + _ => {} + } + + if has_reactive_input { + // Mark lvalues reactive (unless stable) + let lvalue_ids: Vec = visitors::each_instruction_lvalue(instr) + .into_iter() + .map(|p| p.identifier) + .collect(); + for lvalue_id in lvalue_ids { + if stable_sidemap.is_stable(lvalue_id) { + continue; + } + reactive_map.mark_reactive(lvalue_id); + } + } + + if has_reactive_input || has_reactive_control { + // Mark mutable operands reactive + let operand_places = visitors::each_instruction_value_operand(value, env); + for op_place in &operand_places { + match op_place.effect { + Effect::Capture + | Effect::Store + | Effect::ConditionallyMutate + | Effect::ConditionallyMutateIterator + | Effect::Mutate => { + let op_range = + &env.identifiers[op_place.identifier.0 as usize].mutable_range; + if op_range.contains(instr.id) { + reactive_map.mark_reactive(op_place.identifier); + } + } + Effect::Freeze | Effect::Read => { + // no-op + } + Effect::Unknown => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + &format!("Unexpected unknown effect at {:?}", op_place.loc), + None, + )); + } + } + } + } + } + + // Process terminal operands (just to mark them reactive for output) + for op in visitors::each_terminal_operand(&block.terminal) { + reactive_map.is_reactive(op.identifier); + } + } + + if !reactive_map.snapshot() { + break; + } + } + + // Propagate reactivity to inner functions (read-only phase, just queries reactive_map) + propagate_reactivity_to_inner_functions_outer(func, env, &mut reactive_map); + + // Now apply reactive flags by replaying the traversal pattern. + apply_reactive_flags_replay( + func, + env, + &mut reactive_map, + &mut stable_sidemap, + &phi_operand_reactive, + ); + + Ok(()) +} + +// ============================================================================= +// ReactivityMap +// ============================================================================= + +struct ReactivityMap<'a> { + has_changes: bool, + reactive: FxHashSet, + aliased_identifiers: &'a mut DisjointSet, +} + +impl<'a> ReactivityMap<'a> { + fn new(aliased_identifiers: &'a mut DisjointSet) -> Self { + ReactivityMap { has_changes: false, reactive: FxHashSet::default(), aliased_identifiers } + } + + fn is_reactive(&mut self, id: IdentifierId) -> bool { + let canonical = self.aliased_identifiers.find_opt(id).unwrap_or(id); + self.reactive.contains(&canonical) + } + + fn mark_reactive(&mut self, id: IdentifierId) { + let canonical = self.aliased_identifiers.find_opt(id).unwrap_or(id); + if self.reactive.insert(canonical) { + self.has_changes = true; + } + } + + /// Reset change tracking, returns true if there were changes. + fn snapshot(&mut self) -> bool { + let had_changes = self.has_changes; + self.has_changes = false; + had_changes + } +} + +// ============================================================================= +// StableSidemap +// ============================================================================= + +struct StableSidemap { + map: FxHashMap, +} + +impl StableSidemap { + fn new() -> Self { + StableSidemap { map: FxHashMap::default() } + } + + fn handle_instruction( + &mut self, + instr: &crate::react_compiler_hir::Instruction, + env: &Environment, + ) { + let lvalue_id = instr.lvalue.identifier; + let value = &instr.value; + + match value { + InstructionValue::CallExpression { callee, .. } => { + let callee_ty = + &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if evaluates_to_stable_type_or_container(env, callee_ty) { + let lvalue_ty = + &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize]; + if is_stable_type(lvalue_ty) { + self.map.insert(lvalue_id, true); + } else { + self.map.insert(lvalue_id, false); + } + } + } + InstructionValue::MethodCall { property, .. } => { + let property_ty = + &env.types[env.identifiers[property.identifier.0 as usize].type_.0 as usize]; + if evaluates_to_stable_type_or_container(env, property_ty) { + let lvalue_ty = + &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize]; + if is_stable_type(lvalue_ty) { + self.map.insert(lvalue_id, true); + } else { + self.map.insert(lvalue_id, false); + } + } + } + InstructionValue::PropertyLoad { object, .. } => { + let source_id = object.identifier; + if self.map.contains_key(&source_id) { + let lvalue_ty = + &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize]; + if is_stable_type_container(lvalue_ty) { + self.map.insert(lvalue_id, false); + } else if is_stable_type(lvalue_ty) { + self.map.insert(lvalue_id, true); + } + } + } + InstructionValue::Destructure { value: val, .. } => { + let source_id = val.identifier; + if self.map.contains_key(&source_id) { + let lvalue_ids: Vec = visitors::each_instruction_lvalue(instr) + .into_iter() + .map(|p| p.identifier) + .collect(); + for lid in lvalue_ids { + let lid_ty = &env.types[env.identifiers[lid.0 as usize].type_.0 as usize]; + if is_stable_type_container(lid_ty) { + self.map.insert(lid, false); + } else if is_stable_type(lid_ty) { + self.map.insert(lid, true); + } + } + } + } + InstructionValue::StoreLocal { lvalue, value: val, .. } => { + if let Some(&entry) = self.map.get(&val.identifier) { + self.map.insert(lvalue_id, entry); + self.map.insert(lvalue.place.identifier, entry); + } + } + InstructionValue::LoadLocal { place, .. } => { + if let Some(&entry) = self.map.get(&place.identifier) { + self.map.insert(lvalue_id, entry); + } + } + _ => {} + } + } + + fn is_stable(&self, id: IdentifierId) -> bool { + self.map.get(&id).copied().unwrap_or(false) + } +} + +// ============================================================================= +// Control dominators (ported from ControlDominators.ts) +// ============================================================================= + +fn is_reactive_controlled_block( + block_id: BlockId, + func: &HirFunction, + post_dominators: &crate::react_compiler_hir::dominator::PostDominator, + reactive_map: &mut ReactivityMap, +) -> bool { + let frontier = post_dominator_frontier(func, post_dominators, block_id); + for frontier_block_id in &frontier { + let control_block = func.body.blocks.get(frontier_block_id).unwrap(); + match &control_block.terminal { + Terminal::If { test, .. } | Terminal::Branch { test, .. } => { + if reactive_map.is_reactive(test.identifier) { + return true; + } + } + Terminal::Switch { test, cases, .. } => { + if reactive_map.is_reactive(test.identifier) { + return true; + } + for case in cases { + if let Some(ref case_test) = case.test { + if reactive_map.is_reactive(case_test.identifier) { + return true; + } + } + } + } + _ => {} + } + } + false +} + +// ============================================================================= +// Type helpers (ported from HIR.ts) +// ============================================================================= + +use crate::react_compiler_hir::is_use_operator_type; + +fn get_hook_kind_for_type<'a>( + env: &'a Environment, + ty: &Type, +) -> Result, CompilerDiagnostic> { + env.get_hook_kind_for_type(ty) +} + +fn is_stable_type(ty: &Type) -> bool { + match ty { + Type::Function { shape_id: Some(id), .. } => { + matches!( + id.as_str(), + "BuiltInSetState" + | "BuiltInSetActionState" + | "BuiltInDispatch" + | "BuiltInStartTransition" + | "BuiltInSetOptimistic" + ) + } + Type::Object { shape_id: Some(id) } => { + matches!(id.as_str(), "BuiltInUseRefId") + } + _ => false, + } +} + +fn is_stable_type_container(ty: &Type) -> bool { + match ty { + Type::Object { shape_id: Some(id) } => { + matches!( + id.as_str(), + "BuiltInUseState" + | "BuiltInUseActionState" + | "BuiltInUseReducer" + | "BuiltInUseOptimistic" + | "BuiltInUseTransition" + ) + } + _ => false, + } +} + +fn evaluates_to_stable_type_or_container(env: &Environment, callee_ty: &Type) -> bool { + if let Some(hook_kind) = get_hook_kind_for_type(env, callee_ty).ok().flatten() { + matches!( + hook_kind, + HookKind::UseState + | HookKind::UseReducer + | HookKind::UseActionState + | HookKind::UseRef + | HookKind::UseTransition + | HookKind::UseOptimistic + ) + } else { + false + } +} + +// ============================================================================= +// Propagate reactivity to inner functions +// ============================================================================= + +fn propagate_reactivity_to_inner_functions_outer( + func: &HirFunction, + env: &Environment, + reactive_map: &mut ReactivityMap, +) { + for (_block_id, block) in &func.body.blocks { + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + propagate_reactivity_to_inner_functions_inner( + lowered_func.func, + env, + reactive_map, + ); + } + _ => {} + } + } + } +} + +fn propagate_reactivity_to_inner_functions_inner( + func_id: FunctionId, + env: &Environment, + reactive_map: &mut ReactivityMap, +) { + let inner_func = &env.functions[func_id.0 as usize]; + + for (_block_id, block) in &inner_func.body.blocks { + for instr_id in &block.instructions { + let instr = &inner_func.instructions[instr_id.0 as usize]; + + for op in visitors::each_instruction_value_operand(&instr.value, env) { + reactive_map.is_reactive(op.identifier); + } + + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + propagate_reactivity_to_inner_functions_inner( + lowered_func.func, + env, + reactive_map, + ); + } + _ => {} + } + } + + for op in visitors::each_terminal_operand(&block.terminal) { + reactive_map.is_reactive(op.identifier); + } + } +} + +// ============================================================================= +// Apply reactive flags to the HIR (replay pass) +// ============================================================================= + +fn apply_reactive_flags_replay( + func: &mut HirFunction, + env: &mut Environment, + reactive_map: &mut ReactivityMap, + stable_sidemap: &mut StableSidemap, + phi_operand_reactive: &FxHashMap<(BlockId, usize, usize), bool>, +) { + let reactive_ids = build_reactive_id_set(reactive_map); + + // 1. Mark params + for param in &mut func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &mut s.place, + }; + place.reactive = true; + } + + // 2. Walk blocks + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + + for block_id in &block_ids { + let block = func.body.blocks.get(block_id).unwrap(); + + // 2a. Phi nodes + let phi_count = block.phis.len(); + for phi_idx in 0..phi_count { + let block = func.body.blocks.get_mut(block_id).unwrap(); + let phi = &mut block.phis[phi_idx]; + + if reactive_ids.contains(&phi.place.identifier) { + phi.place.reactive = true; + } + + for (op_idx, (_pred, operand)) in phi.operands.iter_mut().enumerate() { + if let Some(&is_reactive) = phi_operand_reactive.get(&(*block_id, phi_idx, op_idx)) + { + if is_reactive { + operand.reactive = true; + } + } + } + } + + // 2b. Instructions + let block = func.body.blocks.get(block_id).unwrap(); + let instr_ids: Vec = block.instructions.clone(); + + for instr_id in &instr_ids { + let instr = &func.instructions[instr_id.0 as usize]; + + // Compute hasReactiveInput by checking value operands + let value_operand_ids: Vec = + visitors::each_instruction_value_operand(&instr.value, env) + .into_iter() + .map(|p| p.identifier) + .collect(); + let mut has_reactive_input = false; + for &op_id in &value_operand_ids { + if reactive_ids.contains(&op_id) { + has_reactive_input = true; + } + } + + // Check hooks/use + match &instr.value { + InstructionValue::CallExpression { callee, .. } => { + let callee_ty = + &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if get_hook_kind_for_type(env, callee_ty).ok().flatten().is_some() + || is_use_operator_type(callee_ty) + { + has_reactive_input = true; + } + } + InstructionValue::MethodCall { property, .. } => { + let property_ty = &env.types + [env.identifiers[property.identifier.0 as usize].type_.0 as usize]; + if get_hook_kind_for_type(env, property_ty).ok().flatten().is_some() + || is_use_operator_type(property_ty) + { + has_reactive_input = true; + } + } + _ => {} + } + + // Value operands: set reactive flag using canonical visitor + let instr = &mut func.instructions[instr_id.0 as usize]; + visitors::for_each_instruction_value_operand_mut(&mut instr.value, &mut |place| { + if reactive_ids.contains(&place.identifier) { + place.reactive = true; + } + }); + // FunctionExpression/ObjectMethod context variables require env access + if let InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } = &mut instr.value + { + let inner_func = &mut env.functions[lowered_func.func.0 as usize]; + for ctx in &mut inner_func.context { + if reactive_ids.contains(&ctx.identifier) { + ctx.reactive = true; + } + } + } + + // Lvalues: markReactive is called only when hasReactiveInput + if has_reactive_input { + let lvalue_id = instr.lvalue.identifier; + if !stable_sidemap.is_stable(lvalue_id) && reactive_ids.contains(&lvalue_id) { + instr.lvalue.reactive = true; + } + // Handle value lvalues — includes DeclareContext/StoreContext which + // for_each_instruction_lvalue_mut skips, so we use a direct match. + match &mut instr.value { + InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::DeclareContext { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } => { + let id = lvalue.place.identifier; + if !stable_sidemap.is_stable(id) && reactive_ids.contains(&id) { + lvalue.place.reactive = true; + } + } + InstructionValue::Destructure { lvalue, .. } => { + visitors::for_each_pattern_operand_mut(&mut lvalue.pattern, &mut |place| { + if !stable_sidemap.is_stable(place.identifier) + && reactive_ids.contains(&place.identifier) + { + place.reactive = true; + } + }); + } + InstructionValue::PrefixUpdate { lvalue, .. } + | InstructionValue::PostfixUpdate { lvalue, .. } => { + let id = lvalue.identifier; + if !stable_sidemap.is_stable(id) && reactive_ids.contains(&id) { + lvalue.reactive = true; + } + } + _ => {} + } + } + } + + // 2c. Terminal operands + let block = func.body.blocks.get_mut(block_id).unwrap(); + visitors::for_each_terminal_operand_mut(&mut block.terminal, &mut |place| { + if reactive_ids.contains(&place.identifier) { + place.reactive = true; + } + }); + } + + // 3. Apply to inner functions + apply_reactive_flags_to_inner_functions(func, env, &reactive_ids); +} + +fn build_reactive_id_set(reactive_map: &mut ReactivityMap) -> FxHashSet { + let mut result = FxHashSet::default(); + for &id in &reactive_map.reactive { + result.insert(id); + } + let reactive = &reactive_map.reactive; + reactive_map.aliased_identifiers.for_each(|id, canonical| { + if reactive.contains(&canonical) { + result.insert(id); + } + }); + result +} + +fn apply_reactive_flags_to_inner_functions( + func: &HirFunction, + env: &mut Environment, + reactive_ids: &FxHashSet, +) { + for (_block_id, block) in &func.body.blocks { + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + apply_reactive_flags_to_inner_func(lowered_func.func, env, reactive_ids); + } + _ => {} + } + } + } +} + +fn apply_reactive_flags_to_inner_func( + func_id: FunctionId, + env: &mut Environment, + reactive_ids: &FxHashSet, +) { + // Collect nested function IDs first to avoid borrow issues + let nested_func_ids: Vec = { + let func = &env.functions[func_id.0 as usize]; + let mut ids = Vec::new(); + for (_block_id, block) in &func.body.blocks { + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + ids.push(lowered_func.func); + } + _ => {} + } + } + } + ids + }; + + // Apply reactive flags using canonical visitors + let inner_func = &mut env.functions[func_id.0 as usize]; + for (_block_id, block) in &mut inner_func.body.blocks { + for instr_id in &block.instructions { + let instr = &mut inner_func.instructions[instr_id.0 as usize]; + visitors::for_each_instruction_value_operand_mut(&mut instr.value, &mut |place| { + if reactive_ids.contains(&place.identifier) { + place.reactive = true; + } + }); + } + visitors::for_each_terminal_operand_mut(&mut block.terminal, &mut |place| { + if reactive_ids.contains(&place.identifier) { + place.reactive = true; + } + }); + } + + // Recurse into nested functions, and set reactive on their context variables + for nested_id in nested_func_ids { + let nested_func = &mut env.functions[nested_id.0 as usize]; + for ctx in &mut nested_func.context { + if reactive_ids.contains(&ctx.identifier) { + ctx.reactive = true; + } + } + apply_reactive_flags_to_inner_func(nested_id, env, reactive_ids); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/infer_reactive_scope_variables.rs b/crates/oxc_react_compiler/src/react_compiler_inference/infer_reactive_scope_variables.rs new file mode 100644 index 0000000000000..5cbf22b8282e2 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/infer_reactive_scope_variables.rs @@ -0,0 +1,396 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Infers which variables belong to reactive scopes. +//! +//! Ported from TypeScript `src/ReactiveScopes/InferReactiveScopeVariables.ts`. +//! +//! This is the 1st of 4 passes that determine how to break a function into +//! discrete reactive scopes (independently memoizable units of code): +//! 1. InferReactiveScopeVariables (this pass, on HIR) determines operands that +//! mutate together and assigns them a unique reactive scope. +//! 2. AlignReactiveScopesToBlockScopes aligns reactive scopes to block scopes. +//! 3. MergeOverlappingReactiveScopes ensures scopes do not overlap. +//! 4. BuildReactiveBlocks groups the statements for each scope. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::{ + DeclarationId, EvaluationOrder, HirFunction, IdentifierId, InstructionValue, Pattern, Position, + SourceLocation, +}; +use crate::react_compiler_utils::DisjointSet; + +// ============================================================================= +// Public API +// ============================================================================= + +/// Infer reactive scope variables for a function. +/// +/// For each mutable variable, infers a reactive scope which will construct that +/// variable. Variables that co-mutate are assigned to the same reactive scope. +/// +/// Corresponds to TS `inferReactiveScopeVariables(fn: HIRFunction): void`. +pub fn infer_reactive_scope_variables( + func: &mut HirFunction, + env: &mut Environment, +) -> Result<(), CompilerDiagnostic> { + // Phase 1: find disjoint sets of co-mutating identifiers + let mut scope_identifiers = find_disjoint_mutable_values(func, env); + + // Phase 2: assign scopes + // Maps each group root identifier to the ScopeId assigned to that group. + let mut scopes: FxHashMap = FxHashMap::default(); + + scope_identifiers.for_each(|identifier_id, group_id| { + let ident_range = env.identifiers[identifier_id.0 as usize].mutable_range.clone(); + let ident_loc = env.identifiers[identifier_id.0 as usize].loc; + + let state = scopes.entry(group_id).or_insert_with(|| { + let scope_id = env.next_scope_id(); + // Initialize scope range from the first member + let scope = &mut env.scopes[scope_id.0 as usize]; + scope.range = ident_range.clone(); + ScopeState { scope_id, loc: ident_loc } + }); + + // Update scope range + let scope = &mut env.scopes[state.scope_id.0 as usize]; + + // If this is not the first identifier (scope was already created), merge ranges + if scope.range.start != ident_range.start || scope.range.end != ident_range.end { + if scope.range.start == EvaluationOrder(0) { + scope.range.start = ident_range.start; + } else if ident_range.start != EvaluationOrder(0) { + scope.range.start = EvaluationOrder(scope.range.start.0.min(ident_range.start.0)); + } + scope.range.end = EvaluationOrder(scope.range.end.0.max(ident_range.end.0)); + } + + // Merge location + state.loc = merge_location(state.loc, ident_loc); + + // Assign the scope to this identifier + let scope_id = state.scope_id; + env.identifiers[identifier_id.0 as usize].scope = Some(scope_id); + }); + + // Set loc on each scope + for (_group_id, state) in &scopes { + env.scopes[state.scope_id.0 as usize].loc = state.loc; + } + + // Update each identifier's mutable_range to match its scope's range + for (&_identifier_id, state) in &scopes { + let scope_range = env.scopes[state.scope_id.0 as usize].range.clone(); + // Find all identifiers with this scope and update their mutable_range + // We iterate through all identifiers and check their scope + for ident in &mut env.identifiers { + if ident.scope == Some(state.scope_id) { + ident.mutable_range = scope_range.clone(); + } + } + } + + // Validate scope ranges + let mut max_instruction = EvaluationOrder(0); + for (_block_id, block) in &func.body.blocks { + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + max_instruction = EvaluationOrder(max_instruction.0.max(instr.id.0)); + } + max_instruction = + EvaluationOrder(max_instruction.0.max(block.terminal.evaluation_order().0)); + } + + for (_group_id, state) in &scopes { + let scope = &env.scopes[state.scope_id.0 as usize]; + if scope.range.start == EvaluationOrder(0) + || scope.range.end == EvaluationOrder(0) + || max_instruction == EvaluationOrder(0) + || scope.range.end.0 > max_instruction.0 + 1 + { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + &format!( + "Invalid mutable range for scope: Scope @{} has range [{}:{}] but the valid range is [1:{}]", + scope.id.0, + scope.range.start.0, + scope.range.end.0, + max_instruction.0 + 1, + ), + None, + )); + } + } + + Ok(()) +} + +struct ScopeState { + scope_id: crate::react_compiler_hir::ScopeId, + loc: Option, +} + +/// Merge two source locations, preferring non-None values. +/// Corresponds to TS `mergeLocation`. +fn merge_location(l: Option, r: Option) -> Option { + match (l, r) { + (None, r) => r, + (l, None) => l, + (Some(l), Some(r)) => Some(SourceLocation { + start: Position { + line: l.start.line.min(r.start.line), + column: l.start.column.min(r.start.column), + index: match (l.start.index, r.start.index) { + (Some(a), Some(b)) => Some(a.min(b)), + (a, b) => a.or(b), + }, + }, + end: Position { + line: l.end.line.max(r.end.line), + column: l.end.column.max(r.end.column), + index: match (l.end.index, r.end.index) { + (Some(a), Some(b)) => Some(a.max(b)), + (a, b) => a.or(b), + }, + }, + }), + } +} + +// ============================================================================= +// is_mutable / in_range helpers +// ============================================================================= + +// ============================================================================= +// may_allocate +// ============================================================================= + +/// Check if an instruction may allocate. Corresponds to TS `mayAllocate`. +fn may_allocate(value: &InstructionValue, lvalue_type_is_primitive: bool) -> bool { + match value { + InstructionValue::Destructure { lvalue, .. } => { + visitors::does_pattern_contain_spread_element(&lvalue.pattern) + } + InstructionValue::PostfixUpdate { .. } + | InstructionValue::PrefixUpdate { .. } + | InstructionValue::Await { .. } + | InstructionValue::DeclareLocal { .. } + | InstructionValue::DeclareContext { .. } + | InstructionValue::StoreLocal { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::TypeCastExpression { .. } + | InstructionValue::LoadLocal { .. } + | InstructionValue::LoadContext { .. } + | InstructionValue::StoreContext { .. } + | InstructionValue::PropertyDelete { .. } + | InstructionValue::ComputedLoad { .. } + | InstructionValue::ComputedDelete { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::TemplateLiteral { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::GetIterator { .. } + | InstructionValue::IteratorNext { .. } + | InstructionValue::NextPropertyOf { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::StartMemoize { .. } + | InstructionValue::FinishMemoize { .. } + | InstructionValue::UnaryExpression { .. } + | InstructionValue::BinaryExpression { .. } + | InstructionValue::PropertyLoad { .. } + | InstructionValue::StoreGlobal { .. } => false, + + InstructionValue::TaggedTemplateExpression { .. } + | InstructionValue::CallExpression { .. } + | InstructionValue::MethodCall { .. } => !lvalue_type_is_primitive, + + InstructionValue::RegExpLiteral { .. } + | InstructionValue::PropertyStore { .. } + | InstructionValue::ComputedStore { .. } + | InstructionValue::ArrayExpression { .. } + | InstructionValue::JsxExpression { .. } + | InstructionValue::JsxFragment { .. } + | InstructionValue::NewExpression { .. } + | InstructionValue::ObjectExpression { .. } + | InstructionValue::ObjectMethod { .. } + | InstructionValue::FunctionExpression { .. } => true, + } +} + +// ============================================================================= +// Pattern helpers +// ============================================================================= + +/// Collect all Place identifiers from a destructure pattern. +/// Corresponds to TS `eachPatternOperand`. +fn each_pattern_operand(pattern: &Pattern) -> Vec { + visitors::each_pattern_operand(pattern).into_iter().map(|p| p.identifier).collect() +} + +/// Collect all operand identifiers from an instruction value. +/// Corresponds to TS `eachInstructionValueOperand`. +fn each_instruction_value_operand( + value: &InstructionValue, + env: &Environment, +) -> Vec { + visitors::each_instruction_value_operand(value, env).into_iter().map(|p| p.identifier).collect() +} + +// ============================================================================= +// findDisjointMutableValues +// ============================================================================= + +/// Find disjoint sets of co-mutating identifier IDs. +/// +/// Corresponds to TS `findDisjointMutableValues(fn: HIRFunction): DisjointSet`. +pub(crate) fn find_disjoint_mutable_values( + func: &HirFunction, + env: &Environment, +) -> DisjointSet { + let mut scope_identifiers = DisjointSet::::new(); + let mut declarations: FxHashMap = FxHashMap::default(); + + let enable_forest = env.config.enable_forest; + + for (_block_id, block) in &func.body.blocks { + // Handle phi nodes + for phi in &block.phis { + let phi_id = phi.place.identifier; + let phi_range = &env.identifiers[phi_id.0 as usize].mutable_range; + let phi_decl_id = env.identifiers[phi_id.0 as usize].declaration_id; + + let first_instr_id = block + .instructions + .first() + .map(|iid| func.instructions[iid.0 as usize].id) + .unwrap_or(block.terminal.evaluation_order()); + + let is_phi_mutated_after_creation = + phi_range.start.0 + 1 != phi_range.end.0 && phi_range.end > first_instr_id; + // A phi operand defined at or after the phi's block is a loop + // back-edge: the variable is reassigned within the loop (eg a + // counter `a++` or `a = a + 1`). The reassignment must count as + // the loop's scope reassigning the variable, so union the phi + // with its operands and declaration. Otherwise the variable's + // pre-loop value would become a dependency of the scope even + // though the scope changes the value as it executes, making the + // scope's dependencies unstable (the cached dependency would be + // the post-loop value, which can never match the pre-loop value + // compared at the top of the scope). + let is_loop_carried_reassignment = !is_phi_mutated_after_creation + && phi.operands.iter().any(|(_pred_id, operand)| { + env.identifiers[operand.identifier.0 as usize].mutable_range.start + >= first_instr_id + }); + if is_phi_mutated_after_creation || is_loop_carried_reassignment { + let mut operands = vec![phi_id]; + if let Some(&decl_id) = declarations.get(&phi_decl_id) { + operands.push(decl_id); + } + for (_pred_id, phi_operand) in &phi.operands { + operands.push(phi_operand.identifier); + } + scope_identifiers.union(&operands); + } else if enable_forest { + for (_pred_id, phi_operand) in &phi.operands { + scope_identifiers.union(&[phi_id, phi_operand.identifier]); + } + } + } + + // Handle instructions + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let mut operands: Vec = Vec::new(); + + let lvalue_id = instr.lvalue.identifier; + let lvalue_range = &env.identifiers[lvalue_id.0 as usize].mutable_range; + let lvalue_type = &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize]; + let lvalue_type_is_primitive = + crate::react_compiler_hir::is_primitive_type(lvalue_type); + + if lvalue_range.end.0 > lvalue_range.start.0 + 1 + || may_allocate(&instr.value, lvalue_type_is_primitive) + { + operands.push(lvalue_id); + } + + match &instr.value { + InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::DeclareContext { lvalue, .. } => { + let place_id = lvalue.place.identifier; + let decl_id = env.identifiers[place_id.0 as usize].declaration_id; + declarations.entry(decl_id).or_insert(place_id); + } + InstructionValue::StoreLocal { lvalue, value, .. } + | InstructionValue::StoreContext { lvalue, value, .. } => { + let place_id = lvalue.place.identifier; + let decl_id = env.identifiers[place_id.0 as usize].declaration_id; + declarations.entry(decl_id).or_insert(place_id); + + let place_range = &env.identifiers[place_id.0 as usize].mutable_range; + if place_range.end.0 > place_range.start.0 + 1 { + operands.push(place_id); + } + + let value_range = &env.identifiers[value.identifier.0 as usize].mutable_range; + if value_range.contains(instr.id) && value_range.start.0 > 0 { + operands.push(value.identifier); + } + } + InstructionValue::Destructure { lvalue, value, .. } => { + let pattern_places = each_pattern_operand(&lvalue.pattern); + for place_id in &pattern_places { + let decl_id = env.identifiers[place_id.0 as usize].declaration_id; + declarations.entry(decl_id).or_insert(*place_id); + + let place_range = &env.identifiers[place_id.0 as usize].mutable_range; + if place_range.end.0 > place_range.start.0 + 1 { + operands.push(*place_id); + } + } + + let value_range = &env.identifiers[value.identifier.0 as usize].mutable_range; + if value_range.contains(instr.id) && value_range.start.0 > 0 { + operands.push(value.identifier); + } + } + InstructionValue::MethodCall { property, .. } => { + // For MethodCall: include all mutable operands plus the computed property + let all_operands = each_instruction_value_operand(&instr.value, env); + for op_id in &all_operands { + let op_range = &env.identifiers[op_id.0 as usize].mutable_range; + if op_range.contains(instr.id) && op_range.start.0 > 0 { + operands.push(*op_id); + } + } + // Ensure method property is in the same scope as the call + operands.push(property.identifier); + } + _ => { + // For all other instructions: include mutable operands + let all_operands = each_instruction_value_operand(&instr.value, env); + for op_id in &all_operands { + let op_range = &env.identifiers[op_id.0 as usize].mutable_range; + if op_range.contains(instr.id) && op_range.start.0 > 0 { + operands.push(*op_id); + } + } + } + } + + if !operands.is_empty() { + scope_identifiers.union(&operands); + } + } + } + scope_identifiers +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/memoize_fbt_and_macro_operands_in_same_scope.rs b/crates/oxc_react_compiler/src/react_compiler_inference/memoize_fbt_and_macro_operands_in_same_scope.rs new file mode 100644 index 0000000000000..e8975ce6e2029 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/memoize_fbt_and_macro_operands_in_same_scope.rs @@ -0,0 +1,357 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Port of MemoizeFbtAndMacroOperandsInSameScope from TypeScript. +//! +//! Ensures that FBT (Facebook Translation) expressions and their operands +//! are memoized within the same reactive scope. Also supports user-configured +//! custom macro-like APIs via `customMacros` configuration. +//! +//! The pass has two phases: +//! 1. Forward data-flow: identify all macro tags (including property loads like `fbt.param`) +//! 2. Reverse data-flow: merge arguments of macro invocations into the same scope + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::{ + HirFunction, IdentifierId, InstructionValue, JsxTag, PrimitiveValue, PropertyLiteral, ScopeId, +}; + +/// Whether a macro requires its arguments to be transitively inlined (e.g., fbt) +/// or just avoids having the top-level values be converted to variables (e.g., fbt.param). +#[derive(Debug, Clone)] +enum InlineLevel { + Transitive, + Shallow, +} + +/// Defines how a macro and its properties should be handled. +#[derive(Debug, Clone)] +struct MacroDefinition { + level: InlineLevel, + /// Maps property names to their own MacroDefinition. `"*"` is a wildcard. + properties: Option>, +} + +fn shallow_macro() -> MacroDefinition { + MacroDefinition { level: InlineLevel::Shallow, properties: None } +} + +fn transitive_macro() -> MacroDefinition { + MacroDefinition { level: InlineLevel::Transitive, properties: None } +} + +fn fbt_macro() -> MacroDefinition { + let mut props = FxHashMap::default(); + props.insert("*".to_string(), shallow_macro()); + // fbt.enum gets FBT_MACRO (recursive/transitive) + // We'll fill this in after construction since it's self-referential. + // Instead, we use a special marker and handle it in property lookup. + let mut fbt = MacroDefinition { level: InlineLevel::Transitive, properties: Some(props) }; + // Add "enum" as a recursive reference (same as FBT_MACRO) + // Since we can't do self-referential structs, we clone the structure. + let enum_macro = MacroDefinition { + level: InlineLevel::Transitive, + properties: Some({ + let mut p = FxHashMap::default(); + p.insert("*".to_string(), shallow_macro()); + // enum's enum is also recursive, but in practice the depth is bounded + p.insert("enum".to_string(), transitive_macro()); + p + }), + }; + fbt.properties.as_mut().unwrap().insert("enum".to_string(), enum_macro); + fbt +} + +/// Built-in FBT tags and their macro definitions. +fn fbt_tags() -> FxHashMap { + let mut tags = FxHashMap::default(); + tags.insert("fbt".to_string(), fbt_macro()); + tags.insert("fbt:param".to_string(), shallow_macro()); + tags.insert("fbt:enum".to_string(), fbt_macro()); + tags.insert("fbt:plural".to_string(), shallow_macro()); + tags.insert("fbs".to_string(), fbt_macro()); + tags.insert("fbs:param".to_string(), shallow_macro()); + tags.insert("fbs:enum".to_string(), fbt_macro()); + tags.insert("fbs:plural".to_string(), shallow_macro()); + tags +} + +/// Main entry point. Returns the set of identifier IDs that are fbt/macro operands. +pub fn memoize_fbt_and_macro_operands_in_same_scope( + func: &HirFunction, + env: &mut Environment, +) -> FxHashSet { + // Phase 1: Build macro kinds map from built-in FBT tags + custom macros + let mut macro_kinds: FxHashMap = fbt_tags(); + if let Some(ref custom_macros) = env.config.custom_macros { + for name in custom_macros { + macro_kinds.insert(name.clone(), transitive_macro()); + } + } + + // Phase 2: Forward data-flow to identify all macro tags + let mut macro_tags = populate_macro_tags(func, ¯o_kinds); + + // Phase 3: Reverse data-flow to merge arguments of macro invocations + let macro_values = merge_macro_arguments(func, env, &mut macro_tags, ¯o_kinds); + + macro_values +} + +/// Forward data-flow analysis to identify all macro tags, including +/// things like `fbt.foo.bar(...)`. +fn populate_macro_tags( + func: &HirFunction, + macro_kinds: &FxHashMap, +) -> FxHashMap { + let mut macro_tags: FxHashMap = FxHashMap::default(); + + for block in func.body.blocks.values() { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + + match &instr.value { + InstructionValue::Primitive { value: PrimitiveValue::String(s), .. } => { + if let Some(macro_def) = s.as_str().and_then(|utf8| macro_kinds.get(utf8)) { + // We don't distinguish between tag names and strings, so record + // all `fbt` string literals in case they are used as a jsx tag. + macro_tags.insert(lvalue_id, macro_def.clone()); + } + } + InstructionValue::LoadGlobal { binding, .. } => { + let name = binding.name(); + if let Some(macro_def) = macro_kinds.get(name) { + macro_tags.insert(lvalue_id, macro_def.clone()); + } + } + InstructionValue::PropertyLoad { object, property, .. } => { + if let PropertyLiteral::String(prop_name) = property { + if let Some(macro_def) = macro_tags.get(&object.identifier).cloned() { + let property_macro = if let Some(ref props) = macro_def.properties { + let prop_def = + props.get(prop_name.as_str()).or_else(|| props.get("*")); + match prop_def { + Some(def) => def.clone(), + None => macro_def.clone(), + } + } else { + macro_def.clone() + }; + macro_tags.insert(lvalue_id, property_macro); + } + } + } + _ => {} + } + } + } + + macro_tags +} + +/// Reverse data-flow analysis to merge arguments to macro *invocations* +/// based on the kind of the macro. +fn merge_macro_arguments( + func: &HirFunction, + env: &mut Environment, + macro_tags: &mut FxHashMap, + macro_kinds: &FxHashMap, +) -> FxHashSet { + let mut macro_values: FxHashSet = macro_tags.keys().copied().collect(); + + // Iterate blocks in reverse order + let block_ids: Vec<_> = func.body.blocks.keys().copied().collect(); + for &block_id in block_ids.iter().rev() { + let block = &func.body.blocks[&block_id]; + + // Iterate instructions in reverse order + for &instr_id in block.instructions.iter().rev() { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + + match &instr.value { + // Instructions that never need to be merged + InstructionValue::DeclareContext { .. } + | InstructionValue::DeclareLocal { .. } + | InstructionValue::Destructure { .. } + | InstructionValue::LoadContext { .. } + | InstructionValue::LoadLocal { .. } + | InstructionValue::PostfixUpdate { .. } + | InstructionValue::PrefixUpdate { .. } + | InstructionValue::StoreContext { .. } + | InstructionValue::StoreLocal { .. } => { + // Skip these + } + + InstructionValue::CallExpression { callee, .. } + | InstructionValue::MethodCall { property: callee, .. } => { + let scope_id = match env.identifiers[lvalue_id.0 as usize].scope { + Some(s) => s, + None => continue, + }; + + let macro_def = macro_tags + .get(&callee.identifier) + .or_else(|| macro_tags.get(&lvalue_id)) + .cloned(); + + if let Some(macro_def) = macro_def { + visit_operands( + ¯o_def, + scope_id, + lvalue_id, + &instr.value, + env, + &mut macro_values, + macro_tags, + ); + } + } + + InstructionValue::JsxExpression { tag, .. } => { + let scope_id = match env.identifiers[lvalue_id.0 as usize].scope { + Some(s) => s, + None => continue, + }; + + let macro_def = match tag { + JsxTag::Place(place) => macro_tags.get(&place.identifier).cloned(), + JsxTag::Builtin(builtin) => macro_kinds.get(builtin.name.as_str()).cloned(), + }; + + let macro_def = macro_def.or_else(|| macro_tags.get(&lvalue_id).cloned()); + + if let Some(macro_def) = macro_def { + visit_operands( + ¯o_def, + scope_id, + lvalue_id, + &instr.value, + env, + &mut macro_values, + macro_tags, + ); + } + } + + // Default case: check if lvalue is a macro tag + _ => { + let scope_id = match env.identifiers[lvalue_id.0 as usize].scope { + Some(s) => s, + None => continue, + }; + + let macro_def = macro_tags.get(&lvalue_id).cloned(); + if let Some(macro_def) = macro_def { + visit_operands( + ¯o_def, + scope_id, + lvalue_id, + &instr.value, + env, + &mut macro_values, + macro_tags, + ); + } + } + } + } + + // Handle phis + let block = &func.body.blocks[&block_id]; + for phi in &block.phis { + let scope_id = match env.identifiers[phi.place.identifier.0 as usize].scope { + Some(s) => s, + None => continue, + }; + + let macro_def = match macro_tags.get(&phi.place.identifier).cloned() { + Some(def) => def, + None => continue, + }; + + if matches!(macro_def.level, InlineLevel::Shallow) { + continue; + } + + macro_values.insert(phi.place.identifier); + + // Collect operand updates to avoid borrow issues + let operand_updates: Vec<(IdentifierId, MacroDefinition)> = phi + .operands + .values() + .map(|operand| (operand.identifier, macro_def.clone())) + .collect(); + + for (operand_id, def) in operand_updates { + env.identifiers[operand_id.0 as usize].scope = Some(scope_id); + expand_fbt_scope_range(env, scope_id, operand_id); + macro_tags.insert(operand_id, def); + macro_values.insert(operand_id); + } + } + } + + macro_values +} + +/// Expand the scope range on the environment, reading from identifier's mutable_range. +/// Also syncs mutable_range for all identifiers in this scope whose range matched +/// the old scope range. In TS, identifier.mutableRange shares the same object as +/// scope.range, so mutations are automatically visible; in Rust we must propagate. +/// Equivalent to TS `expandFbtScopeRange`. +fn expand_fbt_scope_range(env: &mut Environment, scope_id: ScopeId, operand_id: IdentifierId) { + let extend_start = env.identifiers[operand_id.0 as usize].mutable_range.start; + if extend_start.0 == 0 { + return; + } + let old_range_id = env.scopes[scope_id.0 as usize].range.id; + let old_start = env.scopes[scope_id.0 as usize].range.start; + let new_start = old_start.0.min(extend_start.0); + if new_start == old_start.0 { + return; + } + env.scopes[scope_id.0 as usize].range.start.0 = new_start; + let new_range = env.scopes[scope_id.0 as usize].range.clone(); + for ident in &mut env.identifiers { + if ident.scope == Some(scope_id) && ident.mutable_range.id == old_range_id { + ident.mutable_range = new_range.clone(); + } + } +} + +/// Visit operands for an instruction value, merging them into the same scope +/// if the macro definition requires transitive inlining. +fn visit_operands( + macro_def: &MacroDefinition, + scope_id: ScopeId, + lvalue_id: IdentifierId, + value: &InstructionValue, + env: &mut Environment, + macro_values: &mut FxHashSet, + macro_tags: &mut FxHashMap, +) { + macro_values.insert(lvalue_id); + + let operand_ids: Vec = + visitors::each_instruction_value_operand_with_functions(value, &env.functions) + .into_iter() + .map(|p| p.identifier) + .collect(); + + for operand_id in operand_ids { + if matches!(macro_def.level, InlineLevel::Transitive) { + env.identifiers[operand_id.0 as usize].scope = Some(scope_id); + expand_fbt_scope_range(env, scope_id, operand_id); + macro_tags.insert(operand_id, macro_def.clone()); + } + macro_values.insert(operand_id); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/merge_overlapping_reactive_scopes_hir.rs b/crates/oxc_react_compiler/src/react_compiler_inference/merge_overlapping_reactive_scopes_hir.rs new file mode 100644 index 0000000000000..8a89b164a2b24 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/merge_overlapping_reactive_scopes_hir.rs @@ -0,0 +1,397 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Merges reactive scopes that have overlapping ranges. +//! +//! While previous passes ensure that reactive scopes span valid sets of program +//! blocks, pairs of reactive scopes may still be inconsistent with respect to +//! each other. Two scopes must either be entirely disjoint or one must be nested +//! within the other. This pass detects overlapping scopes and merges them. +//! +//! Additionally, if an instruction mutates an outer scope while a different +//! scope is active, those scopes are merged. +//! +//! Ported from TypeScript `src/HIR/MergeOverlappingReactiveScopesHIR.ts`. + +use rustc_hash::FxHashMap; +use std::cmp; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::visitors::{each_instruction_lvalue_ids, each_terminal_operand_ids}; +use crate::react_compiler_hir::{ + EvaluationOrder, HirFunction, IdentifierId, InstructionValue, ScopeId, Type, +}; +use crate::react_compiler_utils::DisjointSet; + +// ============================================================================= +// ScopeInfo +// ============================================================================= + +struct ScopeStartEntry { + id: EvaluationOrder, + scopes: Vec, +} + +struct ScopeEndEntry { + id: EvaluationOrder, + scopes: Vec, +} + +struct ScopeInfo { + /// Sorted descending by id (so we can pop from the end for smallest) + scope_starts: Vec, + /// Sorted descending by id (so we can pop from the end for smallest) + scope_ends: Vec, + /// Maps IdentifierId -> ScopeId for all places that have a scope + place_scopes: FxHashMap, +} + +// ============================================================================= +// TraversalState +// ============================================================================= + +struct TraversalState { + joined: DisjointSet, + active_scopes: Vec, +} + +// ============================================================================= +// Helper functions +// ============================================================================= + +/// Check if a scope is active at the given instruction id. +/// Corresponds to TS `isScopeActive(scope, id)`. +fn is_scope_active(env: &Environment, scope_id: ScopeId, id: EvaluationOrder) -> bool { + env.scopes[scope_id.0 as usize].range.contains(id) +} + +/// Get the scope for a place if it's active at the given instruction. +/// Corresponds to TS `getPlaceScope(id, place)`. +fn get_place_scope( + env: &Environment, + id: EvaluationOrder, + identifier_id: IdentifierId, +) -> Option { + let scope_id = env.identifiers[identifier_id.0 as usize].scope?; + if is_scope_active(env, scope_id, id) { Some(scope_id) } else { None } +} + +/// Check if a place is mutable at the given instruction. +/// Corresponds to TS `isMutable({id}, place)`. +fn is_mutable(env: &Environment, id: EvaluationOrder, identifier_id: IdentifierId) -> bool { + let range = &env.identifiers[identifier_id.0 as usize].mutable_range; + range.contains(id) +} + +// ============================================================================= +// collectScopeInfo +// ============================================================================= + +fn collect_scope_info(func: &HirFunction, env: &Environment) -> ScopeInfo { + let mut scope_starts_map: FxHashMap> = FxHashMap::default(); + let mut scope_ends_map: FxHashMap> = FxHashMap::default(); + let mut place_scopes: FxHashMap = FxHashMap::default(); + + let mut collect_place_scope = |identifier_id: IdentifierId, env: &Environment| { + let scope_id = match env.identifiers[identifier_id.0 as usize].scope { + Some(s) => s, + None => return, + }; + place_scopes.insert(identifier_id, scope_id); + let range = &env.scopes[scope_id.0 as usize].range; + if range.start != range.end { + scope_starts_map.entry(range.start).or_default().push(scope_id); + scope_ends_map.entry(range.end).or_default().push(scope_id); + } + }; + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + // lvalues + let lvalue_ids = each_instruction_lvalue_ids(instr); + for id in lvalue_ids { + collect_place_scope(id, env); + } + // operands + let operand_ids: Vec = visitors::each_instruction_operand(instr, env) + .into_iter() + .map(|p| p.identifier) + .collect(); + for id in operand_ids { + collect_place_scope(id, env); + } + } + // terminal operands + let terminal_op_ids = each_terminal_operand_ids(&block.terminal); + for id in terminal_op_ids { + collect_place_scope(id, env); + } + } + + // Deduplicate scope IDs in each entry, preserving insertion order. + // The TS uses Set which preserves insertion order and deduplicates. + // We must NOT sort by ScopeId here — the insertion order determines which scope + // becomes the root in the disjoint set union. + fn dedup_preserve_order(scopes: &mut Vec) { + let mut seen = rustc_hash::FxHashSet::default(); + scopes.retain(|s| seen.insert(*s)); + } + for scopes in scope_starts_map.values_mut() { + dedup_preserve_order(scopes); + } + for scopes in scope_ends_map.values_mut() { + dedup_preserve_order(scopes); + } + + // Convert to sorted vecs (descending by id for pop-from-end) + let mut scope_starts: Vec = + scope_starts_map.into_iter().map(|(id, scopes)| ScopeStartEntry { id, scopes }).collect(); + scope_starts.sort_by(|a, b| b.id.cmp(&a.id)); + + let mut scope_ends: Vec = + scope_ends_map.into_iter().map(|(id, scopes)| ScopeEndEntry { id, scopes }).collect(); + scope_ends.sort_by(|a, b| b.id.cmp(&a.id)); + + ScopeInfo { scope_starts, scope_ends, place_scopes } +} + +// ============================================================================= +// visitInstructionId +// ============================================================================= + +fn visit_instruction_id( + id: EvaluationOrder, + scope_info: &mut ScopeInfo, + state: &mut TraversalState, + env: &Environment, +) { + // Handle all scopes that end at this instruction + if let Some(top) = scope_info.scope_ends.last() { + if top.id <= id { + let scope_end_entry = scope_info.scope_ends.pop().unwrap(); + + // Sort scopes by start descending (matching active_scopes order) + let mut scopes_sorted = scope_end_entry.scopes; + scopes_sorted.sort_by(|a, b| { + let a_start = env.scopes[a.0 as usize].range.start; + let b_start = env.scopes[b.0 as usize].range.start; + b_start.cmp(&a_start) + }); + + for scope in &scopes_sorted { + let idx = state.active_scopes.iter().position(|s| s == scope); + if let Some(idx) = idx { + // Detect and merge all overlapping scopes + if idx != state.active_scopes.len() - 1 { + let mut to_union: Vec = vec![*scope]; + to_union.extend_from_slice(&state.active_scopes[idx + 1..]); + state.joined.union(&to_union); + } + state.active_scopes.remove(idx); + } + } + } + } + + // Handle all scopes that begin at this instruction + if let Some(top) = scope_info.scope_starts.last() { + if top.id <= id { + let scope_start_entry = scope_info.scope_starts.pop().unwrap(); + + // Sort by end descending + let mut scopes_sorted = scope_start_entry.scopes; + scopes_sorted.sort_by(|a, b| { + let a_end = env.scopes[a.0 as usize].range.end; + let b_end = env.scopes[b.0 as usize].range.end; + b_end.cmp(&a_end) + }); + + state.active_scopes.extend_from_slice(&scopes_sorted); + + // Merge all identical scopes (same start and end) + for i in 1..scopes_sorted.len() { + let prev = scopes_sorted[i - 1]; + let curr = scopes_sorted[i]; + if env.scopes[prev.0 as usize].range.end == env.scopes[curr.0 as usize].range.end { + state.joined.union(&[prev, curr]); + } + } + } + } +} + +// ============================================================================= +// visitPlace +// ============================================================================= + +fn visit_place( + id: EvaluationOrder, + identifier_id: IdentifierId, + state: &mut TraversalState, + env: &Environment, +) { + // If an instruction mutates an outer scope, flatten all scopes from top + // of the stack to the mutated outer scope + let place_scope = get_place_scope(env, id, identifier_id); + if let Some(scope_id) = place_scope { + if is_mutable(env, id, identifier_id) { + let place_scope_idx = state.active_scopes.iter().position(|s| *s == scope_id); + if let Some(idx) = place_scope_idx { + if idx != state.active_scopes.len() - 1 { + let mut to_union: Vec = vec![scope_id]; + to_union.extend_from_slice(&state.active_scopes[idx + 1..]); + state.joined.union(&to_union); + } + } + } + } +} + +// ============================================================================= +// getOverlappingReactiveScopes +// ============================================================================= + +fn get_overlapping_reactive_scopes( + func: &HirFunction, + env: &Environment, + mut scope_info: ScopeInfo, +) -> DisjointSet { + let mut state = + TraversalState { joined: DisjointSet::::new(), active_scopes: Vec::new() }; + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + visit_instruction_id(instr.id, &mut scope_info, &mut state, env); + + // Visit operands + let is_func_or_method = matches!( + &instr.value, + InstructionValue::FunctionExpression { .. } | InstructionValue::ObjectMethod { .. } + ); + let operand_ids = each_instruction_operand_ids_with_types(instr, env); + for (op_id, type_) in &operand_ids { + if is_func_or_method && matches!(type_, Type::Primitive) { + continue; + } + visit_place(instr.id, *op_id, &mut state, env); + } + + // Visit lvalues + let lvalue_ids = each_instruction_lvalue_ids(instr); + for lvalue_id in lvalue_ids { + visit_place(instr.id, lvalue_id, &mut state, env); + } + } + + let terminal_id = block.terminal.evaluation_order(); + visit_instruction_id(terminal_id, &mut scope_info, &mut state, env); + + let terminal_op_ids = each_terminal_operand_ids(&block.terminal); + for op_id in terminal_op_ids { + visit_place(terminal_id, op_id, &mut state, env); + } + } + + state.joined +} + +// ============================================================================= +// Public API +// ============================================================================= + +/// Merges reactive scopes that have overlapping ranges. +/// +/// Corresponds to TS `mergeOverlappingReactiveScopesHIR(fn: HIRFunction): void`. +pub fn merge_overlapping_reactive_scopes_hir(func: &mut HirFunction, env: &mut Environment) { + // Collect scope info + let scope_info = collect_scope_info(func, env); + + // Save place_scopes before moving scope_info + let place_scopes = scope_info.place_scopes.clone(); + + // Find overlapping scopes + let mut joined_scopes = get_overlapping_reactive_scopes(func, env, scope_info); + + // Merge scope ranges: collect all (scope, root) pairs, then update root ranges + // by accumulating min start / max end from all members of each group. + // This matches TS behavior where groupScope.range is updated in-place during iteration. + let mut scope_groups: Vec<(ScopeId, ScopeId)> = Vec::new(); + joined_scopes.for_each(|scope_id, root_id| { + if scope_id != root_id { + scope_groups.push((scope_id, root_id)); + } + }); + // Collect root scopes' ORIGINAL range IDs BEFORE updating them. + // In TS, identifier.mutableRange shares the same object reference as scope.range. + // When scope.range is updated, ALL identifiers referencing that range object + // automatically see the new values. We use MutableRangeId to identify which + // identifiers share the same logical range as a root scope. + let mut original_root_range_ids: FxHashMap = + FxHashMap::default(); + for (_, root_id) in &scope_groups { + if !original_root_range_ids.contains_key(root_id) { + let range_id = env.scopes[root_id.0 as usize].range.id; + original_root_range_ids.insert(*root_id, range_id); + } + } + + // Update root scope ranges + for (scope_id, root_id) in &scope_groups { + let scope_start = env.scopes[scope_id.0 as usize].range.start; + let scope_end = env.scopes[scope_id.0 as usize].range.end; + let root_range = &mut env.scopes[root_id.0 as usize].range; + root_range.start = EvaluationOrder(cmp::min(root_range.start.0, scope_start.0)); + root_range.end = EvaluationOrder(cmp::max(root_range.end.0, scope_end.0)); + } + // Sync mutable_range for ALL identifiers whose mutable_range has the same + // identity as a root scope's original range. In TS, identifier.mutableRange + // shares the same object reference as scope.range, so when scope.range is + // updated, all identifiers referencing that range object automatically see + // the new values. We use MutableRangeId for exact identity matching. + for ident in &mut env.identifiers { + for (root_id, orig_range_id) in &original_root_range_ids { + if ident.mutable_range.id == *orig_range_id { + let new_range = &env.scopes[root_id.0 as usize].range; + ident.mutable_range.start = new_range.start; + ident.mutable_range.end = new_range.end; + break; + } + } + } + + // Rewrite all references: for each place that had a scope, point to the merged root. + // Note: we intentionally do NOT update mutable_range for repointed identifiers, + // matching TS behavior where identifier.mutableRange still references the old scope's + // range object after scope repointing. + for (identifier_id, original_scope) in &place_scopes { + let next_scope = joined_scopes.find(*original_scope); + if next_scope != *original_scope { + env.identifiers[identifier_id.0 as usize].scope = Some(next_scope); + } + } +} + +// ============================================================================= +// Instruction visitor helpers (delegating to canonical visitors) +// ============================================================================= + +/// Collect operand IdentifierIds with their types from an instruction value. +/// Used to check for Primitive type on FunctionExpression/ObjectMethod operands. +fn each_instruction_operand_ids_with_types( + instr: &crate::react_compiler_hir::Instruction, + env: &Environment, +) -> Vec<(IdentifierId, Type)> { + visitors::each_instruction_operand(instr, env) + .into_iter() + .map(|p| { + let type_ = + env.types[env.identifiers[p.identifier.0 as usize].type_.0 as usize].clone(); + (p.identifier, type_) + }) + .collect() +} diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/mod.rs b/crates/oxc_react_compiler/src/react_compiler_inference/mod.rs new file mode 100644 index 0000000000000..45021a25e1a6a --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/mod.rs @@ -0,0 +1,29 @@ +pub mod align_method_call_scopes; +pub mod align_object_method_scopes; +pub mod align_reactive_scopes_to_block_scopes_hir; +pub mod analyse_functions; +pub mod build_reactive_scope_terminals_hir; +pub mod flatten_reactive_loops_hir; +pub mod flatten_scopes_with_hooks_or_use_hir; +pub mod infer_mutation_aliasing_effects; +pub mod infer_mutation_aliasing_ranges; +pub mod infer_reactive_places; +pub mod infer_reactive_scope_variables; +pub mod memoize_fbt_and_macro_operands_in_same_scope; +pub mod merge_overlapping_reactive_scopes_hir; +pub mod propagate_scope_dependencies_hir; + +pub use align_method_call_scopes::align_method_call_scopes; +pub use align_object_method_scopes::align_object_method_scopes; +pub use align_reactive_scopes_to_block_scopes_hir::align_reactive_scopes_to_block_scopes_hir; +pub use analyse_functions::analyse_functions; +pub use build_reactive_scope_terminals_hir::build_reactive_scope_terminals_hir; +pub use flatten_reactive_loops_hir::flatten_reactive_loops_hir; +pub use flatten_scopes_with_hooks_or_use_hir::flatten_scopes_with_hooks_or_use_hir; +pub use infer_mutation_aliasing_effects::infer_mutation_aliasing_effects; +pub use infer_mutation_aliasing_ranges::infer_mutation_aliasing_ranges; +pub use infer_reactive_places::infer_reactive_places; +pub use infer_reactive_scope_variables::infer_reactive_scope_variables; +pub use memoize_fbt_and_macro_operands_in_same_scope::memoize_fbt_and_macro_operands_in_same_scope; +pub use merge_overlapping_reactive_scopes_hir::merge_overlapping_reactive_scopes_hir; +pub use propagate_scope_dependencies_hir::propagate_scope_dependencies_hir; diff --git a/crates/oxc_react_compiler/src/react_compiler_inference/propagate_scope_dependencies_hir.rs b/crates/oxc_react_compiler/src/react_compiler_inference/propagate_scope_dependencies_hir.rs new file mode 100644 index 0000000000000..bd26fd1704c0d --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_inference/propagate_scope_dependencies_hir.rs @@ -0,0 +1,2141 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Propagates scope dependencies through the HIR, computing which values each +//! reactive scope depends on. +//! +//! Ported from TypeScript: +//! - `src/HIR/PropagateScopeDependenciesHIR.ts` +//! - `src/HIR/CollectOptionalChainDependencies.ts` +//! - `src/HIR/CollectHoistablePropertyLoads.ts` +//! - `src/HIR/DeriveMinimalDependenciesHIR.ts` + +use crate::react_compiler_utils::FxIndexMap; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::collections::BTreeSet; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::{ScopeBlockInfo, ScopeBlockTraversal}; +use crate::react_compiler_hir::{ + BasicBlock, BlockId, DeclarationId, DependencyPathEntry, EvaluationOrder, FunctionId, + GotoVariant, HirFunction, IdentifierId, Instruction, InstructionId, InstructionKind, + InstructionValue, MutableRange, ParamPattern, Place, PlaceOrSpread, PropertyLiteral, + ReactFunctionType, ReactiveScopeDependency, ScopeId, Terminal, Type, visitors, +}; + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Main entry point: propagate scope dependencies through the HIR. +/// Corresponds to TS `propagateScopeDependenciesHIR(fn)`. +pub fn propagate_scope_dependencies_hir(func: &mut HirFunction, env: &mut Environment) { + let used_outside_declaring_scope = find_temporaries_used_outside_declaring_scope(func, env); + let temporaries = collect_temporaries_sidemap(func, env, &used_outside_declaring_scope); + + let OptionalChainSidemap { + temporaries_read_in_optional, + processed_instrs_in_optional, + hoistable_objects, + } = collect_optional_chain_sidemap(func, env); + + let hoistable_property_loads = { + let (working, registry) = + collect_hoistable_and_propagate(func, env, &temporaries, &hoistable_objects); + // Convert to scope-keyed map with full dependency paths + let mut keyed: FxHashMap> = FxHashMap::default(); + for (_block_id, block) in &func.body.blocks { + if let Terminal::Scope { scope, block: inner_block, .. } = &block.terminal { + if let Some(node_indices) = working.get(inner_block) { + let deps: Vec = node_indices + .iter() + .map(|&idx| registry.nodes[idx].full_path.clone()) + .collect(); + keyed.insert(*scope, deps); + } + } + } + keyed + }; + + // Merge temporaries + temporariesReadInOptional + let mut merged_temporaries = temporaries; + for (k, v) in temporaries_read_in_optional { + merged_temporaries.insert(k, v); + } + + let scope_deps = collect_dependencies( + func, + env, + &used_outside_declaring_scope, + &merged_temporaries, + &processed_instrs_in_optional, + ); + + // Derive the minimal set of hoistable dependencies for each scope. + for (scope_id, deps) in &scope_deps { + if deps.is_empty() { + continue; + } + + let hoistables = hoistable_property_loads.get(scope_id); + let hoistables = + hoistables.expect("[PropagateScopeDependencies] Scope not found in tracked blocks"); + + // Step 2: Calculate hoistable dependencies using the tree. + let mut tree = ReactiveScopeDependencyTreeHIR::new(hoistables.iter(), env); + for dep in deps { + tree.add_dependency(dep.clone(), env); + } + + // Step 3: Reduce dependencies to a minimal set. + let candidates = tree.derive_minimal_dependencies(env); + let scope = &mut env.scopes[scope_id.0 as usize]; + for candidate_dep in candidates { + let already_exists = scope.dependencies.iter().any(|existing_dep| { + let existing_decl_id = + env.identifiers[existing_dep.identifier.0 as usize].declaration_id; + let candidate_decl_id = + env.identifiers[candidate_dep.identifier.0 as usize].declaration_id; + existing_decl_id == candidate_decl_id + && are_equal_paths(&existing_dep.path, &candidate_dep.path) + }); + if !already_exists { + scope.dependencies.push(candidate_dep); + } + } + } +} + +fn are_equal_paths(a: &[DependencyPathEntry], b: &[DependencyPathEntry]) -> bool { + a.len() == b.len() + && a.iter() + .zip(b.iter()) + .all(|(ai, bi)| ai.property == bi.property && ai.optional == bi.optional) +} + +// ============================================================================= +// findTemporariesUsedOutsideDeclaringScope +// ============================================================================= + +/// Corresponds to TS `findTemporariesUsedOutsideDeclaringScope`. +fn find_temporaries_used_outside_declaring_scope( + func: &HirFunction, + env: &Environment, +) -> FxHashSet { + let mut declarations: FxHashMap = FxHashMap::default(); + let mut pruned_scopes: FxHashSet = FxHashSet::default(); + let mut traversal = ScopeBlockTraversal::new(); + let mut used_outside_declaring_scope: FxHashSet = FxHashSet::default(); + + let handle_place = |place_id: IdentifierId, + declarations: &FxHashMap, + traversal: &ScopeBlockTraversal, + pruned_scopes: &FxHashSet, + used_outside: &mut FxHashSet, + env: &Environment| { + let decl_id = env.identifiers[place_id.0 as usize].declaration_id; + if let Some(&declaring_scope) = declarations.get(&decl_id) { + if !traversal.is_scope_active(declaring_scope) + && !pruned_scopes.contains(&declaring_scope) + { + used_outside.insert(decl_id); + } + } + }; + + for (block_id, block) in &func.body.blocks { + // recordScopes + traversal.record_scopes(block); + + let scope_start_info = traversal.block_infos.get(block_id); + if let Some(ScopeBlockInfo::Begin { scope, pruned: true, .. }) = scope_start_info { + pruned_scopes.insert(*scope); + } + + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + // Handle operands + for op_id in visitors::each_instruction_operand(instr, env) + .into_iter() + .map(|p| p.identifier) + .collect::>() + { + handle_place( + op_id, + &declarations, + &traversal, + &pruned_scopes, + &mut used_outside_declaring_scope, + env, + ); + } + // Handle instruction (track declarations) + let current_scope = traversal.current_scope(); + if let Some(scope) = current_scope { + if !pruned_scopes.contains(&scope) { + match &instr.value { + InstructionValue::LoadLocal { .. } + | InstructionValue::LoadContext { .. } + | InstructionValue::PropertyLoad { .. } => { + let decl_id = + env.identifiers[instr.lvalue.identifier.0 as usize].declaration_id; + declarations.insert(decl_id, scope); + } + _ => {} + } + } + } + } + + // Terminal operands + for op_id in visitors::each_terminal_operand(&block.terminal) + .into_iter() + .map(|p| p.identifier) + .collect::>() + { + handle_place( + op_id, + &declarations, + &traversal, + &pruned_scopes, + &mut used_outside_declaring_scope, + env, + ); + } + } + + used_outside_declaring_scope +} + +// ============================================================================= +// collectTemporariesSidemap +// ============================================================================= + +/// Corresponds to TS `collectTemporariesSidemap`. +fn collect_temporaries_sidemap( + func: &HirFunction, + env: &Environment, + used_outside_declaring_scope: &FxHashSet, +) -> FxHashMap { + let mut temporaries = FxHashMap::default(); + collect_temporaries_sidemap_impl( + func, + env, + used_outside_declaring_scope, + &mut temporaries, + None, + ); + temporaries +} + +/// Corresponds to TS `isLoadContextMutable`. +fn is_load_context_mutable( + value: &InstructionValue, + id: EvaluationOrder, + env: &Environment, +) -> bool { + if let InstructionValue::LoadContext { place, .. } = value { + if let Some(scope_id) = env.identifiers[place.identifier.0 as usize].scope { + let scope_range = &env.scopes[scope_id.0 as usize].range; + return id >= scope_range.end; + } + } + false +} + +/// Corresponds to TS `convertHoistedLValueKind` — returns None for non-hoisted kinds. +fn convert_hoisted_lvalue_kind(kind: InstructionKind) -> Option { + match kind { + InstructionKind::HoistedLet => Some(InstructionKind::Let), + InstructionKind::HoistedConst => Some(InstructionKind::Const), + InstructionKind::HoistedFunction => Some(InstructionKind::Function), + _ => None, + } +} + +/// Recursive implementation. Corresponds to TS `collectTemporariesSidemapImpl`. +fn collect_temporaries_sidemap_impl( + func: &HirFunction, + env: &Environment, + used_outside_declaring_scope: &FxHashSet, + temporaries: &mut FxHashMap, + inner_fn_context: Option, +) { + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let instr_eval_order = + if let Some(outer_id) = inner_fn_context { outer_id } else { instr.id }; + let lvalue_decl_id = env.identifiers[instr.lvalue.identifier.0 as usize].declaration_id; + let used_outside = used_outside_declaring_scope.contains(&lvalue_decl_id); + + match &instr.value { + InstructionValue::PropertyLoad { object, property, loc, .. } if !used_outside => { + if inner_fn_context.is_none() || temporaries.contains_key(&object.identifier) { + let prop = get_property(object, property, false, *loc, temporaries, env); + temporaries.insert(instr.lvalue.identifier, prop); + } + } + InstructionValue::LoadLocal { place, loc, .. } + if env.identifiers[instr.lvalue.identifier.0 as usize].name.is_none() + && env.identifiers[place.identifier.0 as usize].name.is_some() + && !used_outside => + { + if inner_fn_context.is_none() + || func.context.iter().any(|ctx| ctx.identifier == place.identifier) + { + temporaries.insert( + instr.lvalue.identifier, + ReactiveScopeDependency { + identifier: place.identifier, + reactive: place.reactive, + path: vec![], + loc: *loc, + }, + ); + } + } + value @ InstructionValue::LoadContext { place, loc, .. } + if is_load_context_mutable(value, instr_eval_order, env) + && env.identifiers[instr.lvalue.identifier.0 as usize].name.is_none() + && env.identifiers[place.identifier.0 as usize].name.is_some() + && !used_outside => + { + if inner_fn_context.is_none() + || func.context.iter().any(|ctx| ctx.identifier == place.identifier) + { + temporaries.insert( + instr.lvalue.identifier, + ReactiveScopeDependency { + identifier: place.identifier, + reactive: place.reactive, + path: vec![], + loc: *loc, + }, + ); + } + } + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let inner_func = &env.functions[lowered_func.func.0 as usize]; + let ctx = inner_fn_context.unwrap_or(instr.id); + collect_temporaries_sidemap_impl( + inner_func, + env, + used_outside_declaring_scope, + temporaries, + Some(ctx), + ); + } + _ => {} + } + } + } +} + +/// Corresponds to TS `getProperty`. +fn get_property( + object: &Place, + property_name: &PropertyLiteral, + optional: bool, + loc: Option, + temporaries: &FxHashMap, + _env: &Environment, +) -> ReactiveScopeDependency { + let resolved = temporaries.get(&object.identifier); + if let Some(resolved) = resolved { + let mut path = resolved.path.clone(); + path.push(DependencyPathEntry { property: property_name.clone(), optional, loc }); + ReactiveScopeDependency { + identifier: resolved.identifier, + reactive: resolved.reactive, + path, + loc, + } + } else { + ReactiveScopeDependency { + identifier: object.identifier, + reactive: object.reactive, + path: vec![DependencyPathEntry { property: property_name.clone(), optional, loc }], + loc, + } + } +} + +// ============================================================================= +// CollectOptionalChainDependencies +// ============================================================================= + +struct OptionalChainSidemap { + temporaries_read_in_optional: FxHashMap, + processed_instrs_in_optional: FxHashSet, + hoistable_objects: FxHashMap, +} + +/// We track processed instructions/terminals by their lvalue IdentifierId + block id. +/// In TS this uses reference identity (Set). +/// We use IdentifierId for instructions (globally unique across functions) and +/// BlockId for terminals. Note: EvaluationOrder (instruction id) is NOT unique +/// across functions, so we cannot use it here. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ProcessedInstr { + Instruction(IdentifierId), + Terminal(BlockId), +} + +fn collect_optional_chain_sidemap(func: &HirFunction, env: &Environment) -> OptionalChainSidemap { + let mut ctx = OptionalTraversalContext { + seen_optionals: FxHashSet::default(), + processed_instrs_in_optional: FxHashSet::default(), + temporaries_read_in_optional: FxHashMap::default(), + hoistable_objects: FxHashMap::default(), + }; + + traverse_function_optional(func, env, &mut ctx); + + OptionalChainSidemap { + temporaries_read_in_optional: ctx.temporaries_read_in_optional, + processed_instrs_in_optional: ctx.processed_instrs_in_optional, + hoistable_objects: ctx.hoistable_objects, + } +} + +struct OptionalTraversalContext { + seen_optionals: FxHashSet, + processed_instrs_in_optional: FxHashSet, + temporaries_read_in_optional: FxHashMap, + hoistable_objects: FxHashMap, +} + +fn traverse_function_optional( + func: &HirFunction, + env: &Environment, + ctx: &mut OptionalTraversalContext, +) { + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let inner_func = &env.functions[lowered_func.func.0 as usize]; + traverse_function_optional(inner_func, env, ctx); + } + _ => {} + } + } + if let Terminal::Optional { .. } = &block.terminal { + if !ctx.seen_optionals.contains(&block.id) { + traverse_optional_block(block, func, env, ctx, None); + } + } + } +} + +struct MatchConsequentResult { + consequent_id: IdentifierId, + property: PropertyLiteral, + property_id: IdentifierId, + store_local_lvalue_id: IdentifierId, + consequent_goto: BlockId, + property_load_loc: Option, +} + +fn match_optional_test_block( + test: &Terminal, + func: &HirFunction, + _env: &Environment, +) -> Option { + let (test_place, consequent_block_id, alternate_block_id) = match test { + Terminal::Branch { test, consequent, alternate, .. } => (test, *consequent, *alternate), + _ => return None, + }; + + let consequent_block = func.body.blocks.get(&consequent_block_id)?; + if consequent_block.instructions.len() != 2 { + return None; + } + + let instr0 = &func.instructions[consequent_block.instructions[0].0 as usize]; + let instr1 = &func.instructions[consequent_block.instructions[1].0 as usize]; + + let (property_load_object, property, property_load_loc) = match &instr0.value { + InstructionValue::PropertyLoad { object, property, loc } => (object, property, loc), + _ => return None, + }; + + let store_local_value = match &instr1.value { + InstructionValue::StoreLocal { value, lvalue, .. } => { + // Verify the store local's value matches the property load's lvalue + if value.identifier != instr0.lvalue.identifier { + return None; + } + &lvalue.place + } + _ => return None, + }; + + // Verify property load's object matches the test + if property_load_object.identifier != test_place.identifier { + return None; + } + + // Check consequent block terminal is goto break + match &consequent_block.terminal { + Terminal::Goto { variant: GotoVariant::Break, block: goto_block, .. } => { + // Verify alternate block structure + let alternate_block = func.body.blocks.get(&alternate_block_id)?; + if alternate_block.instructions.len() != 2 { + return None; + } + let alt_instr0 = &func.instructions[alternate_block.instructions[0].0 as usize]; + let alt_instr1 = &func.instructions[alternate_block.instructions[1].0 as usize]; + match (&alt_instr0.value, &alt_instr1.value) { + (InstructionValue::Primitive { .. }, InstructionValue::StoreLocal { .. }) => {} + _ => return None, + } + + Some(MatchConsequentResult { + consequent_id: store_local_value.identifier, + property: property.clone(), + property_id: instr0.lvalue.identifier, + store_local_lvalue_id: instr1.lvalue.identifier, + consequent_goto: *goto_block, + property_load_loc: *property_load_loc, + }) + } + _ => None, + } +} + +fn traverse_optional_block( + optional_block: &BasicBlock, + func: &HirFunction, + env: &Environment, + ctx: &mut OptionalTraversalContext, + outer_alternate: Option, +) -> Option { + ctx.seen_optionals.insert(optional_block.id); + + let (test_block_id, is_optional, fallthrough_block_id) = match &optional_block.terminal { + Terminal::Optional { test, optional, fallthrough, .. } => (*test, *optional, *fallthrough), + _ => return None, + }; + + let maybe_test_block = func.body.blocks.get(&test_block_id)?; + + let (test_terminal, base_object) = match &maybe_test_block.terminal { + Terminal::Branch { .. } => { + // Base case: optional must be true + if !is_optional { + return None; + } + // Match base expression that is straightforward PropertyLoad chain + if maybe_test_block.instructions.is_empty() { + return None; + } + let first_instr = &func.instructions[maybe_test_block.instructions[0].0 as usize]; + if !matches!(&first_instr.value, InstructionValue::LoadLocal { .. }) { + return None; + } + + let mut path: Vec = Vec::new(); + for i in 1..maybe_test_block.instructions.len() { + let curr_instr = &func.instructions[maybe_test_block.instructions[i].0 as usize]; + let prev_instr = + &func.instructions[maybe_test_block.instructions[i - 1].0 as usize]; + match &curr_instr.value { + InstructionValue::PropertyLoad { object, property, loc, .. } + if object.identifier == prev_instr.lvalue.identifier => + { + path.push(DependencyPathEntry { + property: property.clone(), + optional: false, + loc: *loc, + }); + } + _ => return None, + } + } + + // Verify test expression matches last instruction's lvalue + let last_instr_id = *maybe_test_block.instructions.last().unwrap(); + let last_instr = &func.instructions[last_instr_id.0 as usize]; + let test_ident = match &maybe_test_block.terminal { + Terminal::Branch { test, .. } => test.identifier, + _ => return None, + }; + if test_ident != last_instr.lvalue.identifier { + return None; + } + + let first_place = match &first_instr.value { + InstructionValue::LoadLocal { place, .. } => place, + _ => return None, + }; + + let base = ReactiveScopeDependency { + identifier: first_place.identifier, + reactive: first_place.reactive, + path, + loc: first_place.loc, + }; + (&maybe_test_block.terminal, base) + } + Terminal::Optional { + fallthrough: inner_fallthrough, optional: _inner_optional, .. + } => { + let test_block = func.body.blocks.get(inner_fallthrough)?; + if !matches!(&test_block.terminal, Terminal::Branch { .. }) { + return None; + } + + // Recurse into inner optional + let inner_alternate = match &test_block.terminal { + Terminal::Branch { alternate, .. } => Some(*alternate), + _ => None, + }; + let inner_optional_result = + traverse_optional_block(maybe_test_block, func, env, ctx, inner_alternate); + let inner_optional_id = inner_optional_result?; + + // Check that inner optional is part of the same chain + let test_ident = match &test_block.terminal { + Terminal::Branch { test, .. } => test.identifier, + _ => return None, + }; + if test_ident != inner_optional_id { + return None; + } + + if !is_optional { + // Non-optional load: record that PropertyLoads from inner optional are hoistable + if let Some(inner_dep) = ctx.temporaries_read_in_optional.get(&inner_optional_id) { + ctx.hoistable_objects.insert(optional_block.id, inner_dep.clone()); + } + } + + let base = ctx.temporaries_read_in_optional.get(&inner_optional_id)?.clone(); + (&test_block.terminal, base) + } + _ => return None, + }; + + // Verify alternate matches outer_alternate if present + if let Some(outer_alt) = outer_alternate { + let test_alternate = match test_terminal { + Terminal::Branch { alternate, .. } => *alternate, + _ => return None, + }; + if test_alternate == outer_alt { + // Verify optional block has no instructions + if !optional_block.instructions.is_empty() { + return None; + } + } + } + + let match_result = match_optional_test_block(test_terminal, func, env)?; + + // Verify consequent goto matches optional fallthrough + if match_result.consequent_goto != fallthrough_block_id { + return None; + } + + let load = ReactiveScopeDependency { + identifier: base_object.identifier, + reactive: base_object.reactive, + path: { + let mut p = base_object.path.clone(); + p.push(DependencyPathEntry { + property: match_result.property.clone(), + optional: is_optional, + loc: match_result.property_load_loc, + }); + p + }, + loc: match_result.property_load_loc, + }; + + ctx.processed_instrs_in_optional + .insert(ProcessedInstr::Instruction(match_result.store_local_lvalue_id)); + ctx.processed_instrs_in_optional.insert(ProcessedInstr::Terminal(match &test_terminal { + Terminal::Branch { .. } => { + // Find the block ID for this terminal + // The terminal belongs to either maybe_test_block or the fallthrough block of inner optional + // We need to identify which block this terminal belongs to. + // For the base case, it's test_block_id. + // For nested optional, it's the fallthrough block. + // We'll use the block_id approach based on what we know. + // Actually, we tracked the terminal by its block, so we need to find which block + // contains this terminal. Let's use a pragmatic approach: + // The test terminal we matched was from maybe_test_block or from the inner fallthrough block. + // We'll search for it. + + // For the base case (Branch terminal at maybe_test_block), block_id = test_block_id + // For the nested case, the test terminal is at the fallthrough block of inner optional + // In either case, we stored the terminal as test_terminal which comes from a known block. + // We need to find the block that owns this terminal. + + // Let's take a simpler approach: find the block whose terminal matches + // This is the block we got test_terminal from. + // In the first branch of the match, test_terminal = &maybe_test_block.terminal + // and maybe_test_block.id = test_block_id + // In the second branch, test_terminal = &test_block.terminal + // and test_block = func.body.blocks.get(inner_fallthrough) + // We can't easily tell which case we're in here since we're past the match. + + // Actually, since test_terminal is a reference to a terminal in a block, + // we can just look up which block it belongs to by finding blocks whose terminal + // pointer matches. But that's expensive. Instead, let's use the block approach + // and find the block from the terminal's properties. + + // For simplicity, use a sentinel approach: just check all blocks. + // This is O(n) but only happens for optional chains. + let mut found_block = BlockId(0); + for (bid, blk) in &func.body.blocks { + if std::ptr::eq(&blk.terminal, test_terminal) { + found_block = *bid; + break; + } + } + found_block + } + _ => BlockId(0), + })); + ctx.temporaries_read_in_optional.insert(match_result.consequent_id, load.clone()); + ctx.temporaries_read_in_optional.insert(match_result.property_id, load); + + Some(match_result.consequent_id) +} + +// ============================================================================= +// CollectHoistablePropertyLoads +// ============================================================================= + +#[derive(Debug, Clone)] +struct PropertyPathNode { + properties: FxHashMap, // index into registry + optional_properties: FxHashMap, // index into registry + #[allow(dead_code)] + parent: Option, + full_path: ReactiveScopeDependency, + has_optional: bool, + #[allow(dead_code)] + root: Option, +} + +struct PropertyPathRegistry { + nodes: Vec, + roots: FxHashMap, +} + +impl PropertyPathRegistry { + fn new() -> Self { + Self { nodes: Vec::new(), roots: FxHashMap::default() } + } + + fn get_or_create_identifier( + &mut self, + identifier_id: IdentifierId, + reactive: bool, + loc: Option, + ) -> usize { + if let Some(&idx) = self.roots.get(&identifier_id) { + return idx; + } + let idx = self.nodes.len(); + self.nodes.push(PropertyPathNode { + properties: FxHashMap::default(), + optional_properties: FxHashMap::default(), + parent: None, + full_path: ReactiveScopeDependency { + identifier: identifier_id, + reactive, + path: vec![], + loc, + }, + has_optional: false, + root: Some(identifier_id), + }); + self.roots.insert(identifier_id, idx); + idx + } + + fn get_or_create_property_entry( + &mut self, + parent_idx: usize, + entry: &DependencyPathEntry, + ) -> usize { + let map_key = entry.property.clone(); + let existing = if entry.optional { + self.nodes[parent_idx].optional_properties.get(&map_key).copied() + } else { + self.nodes[parent_idx].properties.get(&map_key).copied() + }; + if let Some(idx) = existing { + return idx; + } + let parent_full_path = self.nodes[parent_idx].full_path.clone(); + let parent_has_optional = self.nodes[parent_idx].has_optional; + let idx = self.nodes.len(); + let mut new_path = parent_full_path.path.clone(); + new_path.push(entry.clone()); + self.nodes.push(PropertyPathNode { + properties: FxHashMap::default(), + optional_properties: FxHashMap::default(), + parent: Some(parent_idx), + full_path: ReactiveScopeDependency { + identifier: parent_full_path.identifier, + reactive: parent_full_path.reactive, + path: new_path, + loc: entry.loc, + }, + has_optional: parent_has_optional || entry.optional, + root: None, + }); + if entry.optional { + self.nodes[parent_idx].optional_properties.insert(map_key, idx); + } else { + self.nodes[parent_idx].properties.insert(map_key, idx); + } + idx + } + + fn get_or_create_property(&mut self, dep: &ReactiveScopeDependency) -> usize { + let mut curr = self.get_or_create_identifier(dep.identifier, dep.reactive, dep.loc); + for entry in &dep.path { + curr = self.get_or_create_property_entry(curr, entry); + } + curr + } +} + +/// Reduces optional chains in a set of property path nodes. +/// +/// Any two optional chains with different operations (`.` vs `?.`) but the same set +/// of property string paths de-duplicates. If unconditional reads from `` are +/// hoistable (i.e., `` is in the set), we replace `?.PROPERTY` with +/// `.PROPERTY`. +/// +/// Port of `reduceMaybeOptionalChains` from CollectHoistablePropertyLoads.ts. +fn reduce_maybe_optional_chains(nodes: &mut BTreeSet, registry: &mut PropertyPathRegistry) { + // Collect indices of nodes that have optional in their path + let mut optional_chain_nodes: BTreeSet = + nodes.iter().copied().filter(|&idx| registry.nodes[idx].has_optional).collect(); + + if optional_chain_nodes.is_empty() { + return; + } + + loop { + let mut changed = false; + + // Collect the indices to process (snapshot to avoid borrow issues) + let to_process: Vec = optional_chain_nodes.iter().copied().collect(); + + for original_idx in to_process { + let full_path = registry.nodes[original_idx].full_path.clone(); + + let mut curr_node = registry.get_or_create_identifier( + full_path.identifier, + full_path.reactive, + full_path.loc, + ); + + for entry in &full_path.path { + // If the base is known to be non-null (in the set), replace optional with non-optional + let next_entry = if entry.optional && nodes.contains(&curr_node) { + DependencyPathEntry { + property: entry.property.clone(), + optional: false, + loc: entry.loc, + } + } else { + entry.clone() + }; + curr_node = registry.get_or_create_property_entry(curr_node, &next_entry); + } + + if curr_node != original_idx { + changed = true; + optional_chain_nodes.remove(&original_idx); + optional_chain_nodes.insert(curr_node); + nodes.remove(&original_idx); + nodes.insert(curr_node); + } + } + + if !changed { + break; + } + } +} + +#[derive(Debug, Clone)] +struct BlockInfo { + assumed_non_null_objects: BTreeSet, // indices into PropertyPathRegistry +} + +#[allow(dead_code)] +fn collect_hoistable_property_loads( + func: &HirFunction, + env: &Environment, + temporaries: &FxHashMap, + hoistable_from_optionals: &FxHashMap, +) -> FxHashMap { + let mut registry = PropertyPathRegistry::new(); + let known_immutable_identifiers: FxHashSet = if func.fn_type + == ReactFunctionType::Component + || func.fn_type == ReactFunctionType::Hook + { + func.params + .iter() + .filter_map(|p| match p { + ParamPattern::Place(place) => Some(place.identifier), + _ => None, + }) + .collect() + } else { + FxHashSet::default() + }; + + let assumed_invoked_fns = get_assumed_invoked_functions(func, env); + let ctx = CollectHoistableContext { + temporaries, + known_immutable_identifiers: &known_immutable_identifiers, + hoistable_from_optionals, + nested_fn_immutable_context: None, + assumed_invoked_fns: &assumed_invoked_fns, + }; + + collect_hoistable_property_loads_impl(func, env, &ctx, &mut registry) +} + +struct CollectHoistableContext<'a> { + temporaries: &'a FxHashMap, + known_immutable_identifiers: &'a FxHashSet, + hoistable_from_optionals: &'a FxHashMap, + nested_fn_immutable_context: Option<&'a FxHashSet>, + assumed_invoked_fns: &'a FxHashSet, +} + +fn is_immutable_at_instr( + identifier_id: IdentifierId, + instr_id: EvaluationOrder, + env: &Environment, + ctx: &CollectHoistableContext, +) -> bool { + if let Some(nested_ctx) = ctx.nested_fn_immutable_context { + return nested_ctx.contains(&identifier_id); + } + let ident = &env.identifiers[identifier_id.0 as usize]; + let mutable_at_instr = ident.mutable_range.end + > EvaluationOrder(ident.mutable_range.start.0 + 1) + && ident.scope.is_some() + && { + let scope = &env.scopes[ident.scope.unwrap().0 as usize]; + in_range(instr_id, &scope.range) + }; + !mutable_at_instr || ctx.known_immutable_identifiers.contains(&identifier_id) +} + +fn in_range(id: EvaluationOrder, range: &MutableRange) -> bool { + id >= range.start && id < range.end +} + +fn get_maybe_non_null_in_instruction( + value: &InstructionValue, + temporaries: &FxHashMap, +) -> Option { + match value { + InstructionValue::PropertyLoad { object, .. } => { + Some(temporaries.get(&object.identifier).cloned().unwrap_or_else(|| { + ReactiveScopeDependency { + identifier: object.identifier, + reactive: object.reactive, + path: vec![], + loc: object.loc, + } + })) + } + InstructionValue::Destructure { value: val, .. } => { + temporaries.get(&val.identifier).cloned() + } + InstructionValue::ComputedLoad { object, .. } => { + temporaries.get(&object.identifier).cloned() + } + _ => None, + } +} + +#[allow(dead_code)] +fn collect_hoistable_property_loads_impl( + func: &HirFunction, + env: &Environment, + ctx: &CollectHoistableContext, + registry: &mut PropertyPathRegistry, +) -> FxHashMap { + let nodes = collect_non_nulls_in_blocks(func, env, ctx, registry); + let working = propagate_non_null(func, &nodes, registry); + // Return the propagated results, converting FxHashSet back to BlockInfo + working.into_iter().map(|(k, v)| (k, BlockInfo { assumed_non_null_objects: v })).collect() +} + +/// Corresponds to TS `getAssumedInvokedFunctions`. +/// Returns the set of LoweredFunction FunctionIds that are assumed to be invoked. +/// The `temporaries` map is shared across recursive calls (matching TS behavior where +/// the same Map is passed to recursive invocations for inner functions). +fn get_assumed_invoked_functions(func: &HirFunction, env: &Environment) -> FxHashSet { + let mut temporaries: FxHashMap)> = + FxHashMap::default(); + get_assumed_invoked_functions_impl(func, env, &mut temporaries) +} + +fn get_assumed_invoked_functions_impl( + func: &HirFunction, + env: &Environment, + temporaries: &mut FxHashMap)>, +) -> FxHashSet { + let mut hoistable: FxHashSet = FxHashSet::default(); + + // Step 1: Collect identifier to function expression mappings + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } => { + temporaries + .insert(instr.lvalue.identifier, (lowered_func.func, FxHashSet::default())); + } + InstructionValue::StoreLocal { value: val, lvalue, .. } => { + if let Some(entry) = temporaries.get(&val.identifier).cloned() { + temporaries.insert(lvalue.place.identifier, entry); + } + } + InstructionValue::LoadLocal { place, .. } => { + if let Some(entry) = temporaries.get(&place.identifier).cloned() { + temporaries.insert(instr.lvalue.identifier, entry); + } + } + _ => {} + } + } + } + + // Step 2: Forward pass to analyze assumed function calls + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::CallExpression { callee, args, .. } => { + let callee_ty = + &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize]; + let maybe_hook = env.get_hook_kind_for_type(callee_ty).ok().flatten(); + if let Some(entry) = temporaries.get(&callee.identifier) { + // Direct calls + hoistable.insert(entry.0); + } else if maybe_hook.is_some() { + // Assume arguments to all hooks are safe to invoke + for arg in args { + if let PlaceOrSpread::Place(p) = arg { + if let Some(entry) = temporaries.get(&p.identifier) { + hoistable.insert(entry.0); + } + } + } + } + } + InstructionValue::JsxExpression { props, children, .. } => { + // Assume JSX attributes and children are safe to invoke + for prop in props { + if let crate::react_compiler_hir::JsxAttribute::Attribute { + place, .. + } = prop + { + if let Some(entry) = temporaries.get(&place.identifier) { + hoistable.insert(entry.0); + } + } + } + if let Some(children) = children { + for child in children { + if let Some(entry) = temporaries.get(&child.identifier) { + hoistable.insert(entry.0); + } + } + } + } + InstructionValue::JsxFragment { children, .. } => { + for child in children { + if let Some(entry) = temporaries.get(&child.identifier) { + hoistable.insert(entry.0); + } + } + } + InstructionValue::FunctionExpression { lowered_func, .. } => { + // Recursively traverse into other function expressions + // TS passes the shared temporaries map to the recursive call + let inner_func = &env.functions[lowered_func.func.0 as usize]; + let lambdas_called = + get_assumed_invoked_functions_impl(inner_func, env, temporaries); + if let Some(entry) = temporaries.get_mut(&instr.lvalue.identifier) { + for called in lambdas_called { + entry.1.insert(called); + } + } + } + _ => {} + } + } + + // Assume directly returned functions are safe to call + if let Terminal::Return { value, .. } = &block.terminal { + if let Some(entry) = temporaries.get(&value.identifier) { + hoistable.insert(entry.0); + } + } + } + + // Step 3: Propagate assumed-invoked status through mayInvoke chains + let mut changed = true; + while changed { + changed = false; + // Two-phase: collect then insert + let mut to_add = Vec::new(); + for (_, (func_id, may_invoke)) in temporaries.iter() { + if hoistable.contains(func_id) { + for &called in may_invoke { + if !hoistable.contains(&called) { + to_add.push(called); + } + } + } + } + for id in to_add { + changed = true; + hoistable.insert(id); + } + if !changed { + break; + } + } + + hoistable +} + +fn collect_non_nulls_in_blocks( + func: &HirFunction, + env: &Environment, + ctx: &CollectHoistableContext, + registry: &mut PropertyPathRegistry, +) -> FxHashMap { + // Known non-null identifiers (e.g. component props) + let mut known_non_null: BTreeSet = BTreeSet::new(); + if func.fn_type == ReactFunctionType::Component && !func.params.is_empty() { + if let ParamPattern::Place(place) = &func.params[0] { + let node_idx = registry.get_or_create_identifier(place.identifier, true, place.loc); + known_non_null.insert(node_idx); + } + } + + let mut nodes: FxHashMap = FxHashMap::default(); + + for (block_id, block) in &func.body.blocks { + let mut assumed = known_non_null.clone(); + + // Check hoistable from optionals + if let Some(optional_chain) = ctx.hoistable_from_optionals.get(block_id) { + let node_idx = registry.get_or_create_property(optional_chain); + assumed.insert(node_idx); + } + + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + if let Some(path) = get_maybe_non_null_in_instruction(&instr.value, ctx.temporaries) { + let path_ident = path.identifier; + if is_immutable_at_instr(path_ident, instr.id, env, ctx) { + let node_idx = registry.get_or_create_property(&path); + assumed.insert(node_idx); + } + } + + // Handle StartMemoize deps for enablePreserveExistingMemoizationGuarantees + if env.enable_preserve_existing_memoization_guarantees { + if let InstructionValue::StartMemoize { deps: Some(deps), .. } = &instr.value { + for dep in deps { + if let crate::react_compiler_hir::ManualMemoDependencyRoot::NamedLocal { + value: val, + .. + } = &dep.root + { + if !is_immutable_at_instr(val.identifier, instr.id, env, ctx) { + continue; + } + for i in 0..dep.path.len() { + if dep.path[i].optional { + break; + } + let sub_dep = ReactiveScopeDependency { + identifier: val.identifier, + reactive: val.reactive, + path: dep.path[..i].to_vec(), + loc: dep.loc, + }; + let node_idx = registry.get_or_create_property(&sub_dep); + assumed.insert(node_idx); + } + } + } + } + } + + // Handle assumed-invoked inner functions + if let InstructionValue::FunctionExpression { lowered_func, .. } = &instr.value { + if ctx.assumed_invoked_fns.contains(&lowered_func.func) { + let inner_func = &env.functions[lowered_func.func.0 as usize]; + // Build nested fn immutable context + let nested_fn_immutable_context: FxHashSet = + if ctx.nested_fn_immutable_context.is_some() { + // Already in a nested fn context, use existing + ctx.nested_fn_immutable_context.unwrap().clone() + } else { + inner_func + .context + .iter() + .filter(|place| { + is_immutable_at_instr(place.identifier, instr.id, env, ctx) + }) + .map(|place| place.identifier) + .collect() + }; + let inner_assumed = get_assumed_invoked_functions(inner_func, env); + let inner_ctx = CollectHoistableContext { + temporaries: ctx.temporaries, + known_immutable_identifiers: &FxHashSet::default(), + hoistable_from_optionals: ctx.hoistable_from_optionals, + nested_fn_immutable_context: Some(&nested_fn_immutable_context), + assumed_invoked_fns: &inner_assumed, + }; + let inner_nodes = + collect_non_nulls_in_blocks(inner_func, env, &inner_ctx, registry); + // Propagate non-null from inner function + let inner_working = propagate_non_null(inner_func, &inner_nodes, registry); + // Get hoistables from inner function's entry block (after propagation) + let inner_entry = inner_func.body.entry; + if let Some(inner_set) = inner_working.get(&inner_entry) { + for &node_idx in inner_set { + assumed.insert(node_idx); + } + } + } + } + } + + nodes.insert(*block_id, BlockInfo { assumed_non_null_objects: assumed }); + } + + nodes +} + +/// Recursive DFS propagation of non-null information through the CFG. +/// Uses 'active'/'done' state tracking to correctly handle cycles (backedges in loops). +/// +/// Port of TS `propagateNonNull` which uses `recursivelyPropagateNonNull`. +/// Key insight: when computing the intersection of neighbor sets, only include +/// neighbors that are 'done' (not 'active'). Active neighbors are part of a cycle +/// and should be filtered out, allowing non-null info to propagate through non-cyclic paths. +fn propagate_non_null( + func: &HirFunction, + nodes: &FxHashMap, + registry: &mut PropertyPathRegistry, +) -> FxHashMap> { + // Build successor map. Use BTreeSet to iterate successors in sorted BlockId + // order, matching the TS Set insertion order (blocks are created in + // ascending BlockId order). + let mut block_successors: FxHashMap> = FxHashMap::default(); + for (block_id, block) in &func.body.blocks { + for pred in &block.preds { + block_successors.entry(*pred).or_default().insert(*block_id); + } + } + + // Clone nodes into mutable working set + let mut working: FxHashMap> = + nodes.iter().map(|(k, v)| (*k, v.assumed_non_null_objects.clone())).collect(); + + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + let mut reversed_block_ids = block_ids.clone(); + reversed_block_ids.reverse(); + + for _ in 0..100 { + let mut changed = false; + + // Forward pass (using predecessors) + let mut traversal_state: FxHashMap = FxHashMap::default(); + for &block_id in &block_ids { + let block_changed = recursively_propagate_non_null( + block_id, + PropagationDirection::Forward, + &mut traversal_state, + &mut working, + func, + &block_successors, + registry, + ); + changed |= block_changed; + } + + // Backward pass (using successors) + traversal_state.clear(); + for &block_id in &reversed_block_ids { + let block_changed = recursively_propagate_non_null( + block_id, + PropagationDirection::Backward, + &mut traversal_state, + &mut working, + func, + &block_successors, + registry, + ); + changed |= block_changed; + } + + if !changed { + break; + } + } + + working +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TraversalState { + Active, + Done, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PropagationDirection { + Forward, + Backward, +} + +fn recursively_propagate_non_null( + node_id: BlockId, + direction: PropagationDirection, + traversal_state: &mut FxHashMap, + working: &mut FxHashMap>, + func: &HirFunction, + block_successors: &FxHashMap>, + registry: &mut PropertyPathRegistry, +) -> bool { + // Avoid re-visiting computed or currently active nodes + if traversal_state.contains_key(&node_id) { + return false; + } + traversal_state.insert(node_id, TraversalState::Active); + + let neighbors: Vec = match direction { + PropagationDirection::Backward => { + block_successors.get(&node_id).map(|s| s.iter().copied().collect()).unwrap_or_default() + } + PropagationDirection::Forward => func + .body + .blocks + .get(&node_id) + .map(|b| b.preds.iter().copied().collect()) + .unwrap_or_default(), + }; + + let mut changed = false; + for &neighbor in &neighbors { + if !traversal_state.contains_key(&neighbor) { + let neighbor_changed = recursively_propagate_non_null( + neighbor, + direction, + traversal_state, + working, + func, + block_successors, + registry, + ); + changed |= neighbor_changed; + } + } + + // Compute intersection of 'done' neighbors only (filter out 'active' = cycle nodes) + let done_neighbor_sets: Vec> = neighbors + .iter() + .filter(|n| traversal_state.get(n) == Some(&TraversalState::Done)) + .filter_map(|n| working.get(n).cloned()) + .collect(); + + let neighbor_intersection = if done_neighbor_sets.is_empty() { + BTreeSet::new() + } else { + let mut iter = done_neighbor_sets.into_iter(); + let first = iter.next().unwrap(); + iter.fold(first, |acc, s| acc.intersection(&s).copied().collect()) + }; + + let prev_objects = working.get(&node_id).cloned().unwrap_or_default(); + let mut merged: BTreeSet = prev_objects.union(&neighbor_intersection).copied().collect(); + reduce_maybe_optional_chains(&mut merged, registry); + + working.insert(node_id, merged.clone()); + traversal_state.insert(node_id, TraversalState::Done); + + // Compare with previous value — can't just check size due to reduce_maybe_optional_chains + changed |= prev_objects != merged; + changed +} + +fn collect_hoistable_and_propagate( + func: &HirFunction, + env: &Environment, + temporaries: &FxHashMap, + hoistable_from_optionals: &FxHashMap, +) -> (FxHashMap>, PropertyPathRegistry) { + let mut registry = PropertyPathRegistry::new(); + let assumed_invoked_fns = get_assumed_invoked_functions(func, env); + let known_immutable_identifiers: FxHashSet = if func.fn_type + == ReactFunctionType::Component + || func.fn_type == ReactFunctionType::Hook + { + func.params + .iter() + .filter_map(|p| match p { + ParamPattern::Place(place) => Some(place.identifier), + _ => None, + }) + .collect() + } else { + FxHashSet::default() + }; + + let ctx = CollectHoistableContext { + temporaries, + known_immutable_identifiers: &known_immutable_identifiers, + hoistable_from_optionals, + nested_fn_immutable_context: None, + assumed_invoked_fns: &assumed_invoked_fns, + }; + + let nodes = collect_non_nulls_in_blocks(func, env, &ctx, &mut registry); + let working = propagate_non_null(func, &nodes, &mut registry); + + (working, registry) +} + +// Restructured version used by the main entry point +#[allow(dead_code)] +fn key_by_scope_id( + func: &HirFunction, + block_keyed: &FxHashMap, +) -> FxHashMap { + let mut keyed: FxHashMap = FxHashMap::default(); + for (_block_id, block) in &func.body.blocks { + if let Terminal::Scope { scope, block: inner_block, .. } = &block.terminal { + if let Some(info) = block_keyed.get(inner_block) { + keyed.insert(*scope, info.clone()); + } + } + } + keyed +} + +// ============================================================================= +// DeriveMinimalDependenciesHIR +// ============================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PropertyAccessType { + OptionalAccess, + UnconditionalAccess, + OptionalDependency, + UnconditionalDependency, +} + +fn is_optional_access(access: PropertyAccessType) -> bool { + matches!(access, PropertyAccessType::OptionalAccess | PropertyAccessType::OptionalDependency) +} + +fn is_dependency_access(access: PropertyAccessType) -> bool { + matches!( + access, + PropertyAccessType::OptionalDependency | PropertyAccessType::UnconditionalDependency + ) +} + +fn merge_access(a: PropertyAccessType, b: PropertyAccessType) -> PropertyAccessType { + let is_unconditional = !(is_optional_access(a) && is_optional_access(b)); + let is_dep = is_dependency_access(a) || is_dependency_access(b); + match (is_unconditional, is_dep) { + (true, true) => PropertyAccessType::UnconditionalDependency, + (true, false) => PropertyAccessType::UnconditionalAccess, + (false, true) => PropertyAccessType::OptionalDependency, + (false, false) => PropertyAccessType::OptionalAccess, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HoistableAccessType { + Optional, + NonNull, +} + +struct HoistableNode { + properties: FxHashMap>, + access_type: HoistableAccessType, +} + +struct HoistableNodeEntry { + node: HoistableNode, +} + +struct DependencyNode { + properties: FxIndexMap>, + access_type: PropertyAccessType, + loc: Option, +} + +struct DependencyNodeEntry { + node: DependencyNode, +} + +struct ReactiveScopeDependencyTreeHIR { + hoistable_roots: FxHashMap, // node + reactive + dep_roots: FxIndexMap, // node + reactive (preserves insertion order like JS Map) +} + +impl ReactiveScopeDependencyTreeHIR { + fn new<'a>( + hoistable_objects: impl Iterator, + _env: &Environment, + ) -> Self { + let mut hoistable_roots: FxHashMap = + FxHashMap::default(); + + // Sort hoistable objects so that entries with optional first path come + // before non-optional ones. This matches the TS behavior where + // hoistableFromOptionals entries are inserted into the JS Set before + // instruction-based entries, and the first insertion determines the + // root access type. + let mut sorted_deps: Vec<&ReactiveScopeDependency> = hoistable_objects.collect(); + sorted_deps.sort_by(|a, b| { + let a_optional = !a.path.is_empty() && a.path[0].optional; + let b_optional = !b.path.is_empty() && b.path[0].optional; + b_optional.cmp(&a_optional) + }); + + for dep in sorted_deps { + let root = hoistable_roots.entry(dep.identifier).or_insert_with(|| { + let access_type = if !dep.path.is_empty() && dep.path[0].optional { + HoistableAccessType::Optional + } else { + HoistableAccessType::NonNull + }; + (HoistableNode { properties: FxHashMap::default(), access_type }, dep.reactive) + }); + + let mut curr = &mut root.0; + for i in 0..dep.path.len() { + let access_type = if i + 1 < dep.path.len() && dep.path[i + 1].optional { + HoistableAccessType::Optional + } else { + HoistableAccessType::NonNull + }; + let entry = + curr.properties.entry(dep.path[i].property.clone()).or_insert_with(|| { + Box::new(HoistableNodeEntry { + node: HoistableNode { properties: FxHashMap::default(), access_type }, + }) + }); + curr = &mut entry.node; + } + } + + Self { hoistable_roots, dep_roots: FxIndexMap::default() } + } + + fn add_dependency(&mut self, dep: ReactiveScopeDependency, _env: &Environment) { + let root = self.dep_roots.entry(dep.identifier).or_insert_with(|| { + ( + DependencyNode { + properties: FxIndexMap::default(), + access_type: PropertyAccessType::UnconditionalAccess, + loc: dep.loc, + }, + dep.reactive, + ) + }); + + let mut dep_cursor = &mut root.0; + let hoistable_cursor_root = self.hoistable_roots.get(&dep.identifier); + let mut hoistable_ptr: Option<&HoistableNode> = hoistable_cursor_root.map(|(n, _)| n); + + for entry in &dep.path { + let next_hoistable: Option<&HoistableNode>; + let access_type: PropertyAccessType; + + if entry.optional { + next_hoistable = + hoistable_ptr.and_then(|h| h.properties.get(&entry.property).map(|e| &e.node)); + + if hoistable_ptr.is_some() + && hoistable_ptr.unwrap().access_type == HoistableAccessType::NonNull + { + access_type = PropertyAccessType::UnconditionalAccess; + } else { + access_type = PropertyAccessType::OptionalAccess; + } + } else if hoistable_ptr.is_some() + && hoistable_ptr.unwrap().access_type == HoistableAccessType::NonNull + { + next_hoistable = + hoistable_ptr.and_then(|h| h.properties.get(&entry.property).map(|e| &e.node)); + access_type = PropertyAccessType::UnconditionalAccess; + } else { + // Break: truncate dependency + break; + } + + // make_or_merge_property + let child = dep_cursor.properties.entry(entry.property.clone()).or_insert_with(|| { + Box::new(DependencyNodeEntry { + node: DependencyNode { + properties: FxIndexMap::default(), + access_type, + loc: entry.loc, + }, + }) + }); + child.node.access_type = merge_access(child.node.access_type, access_type); + + dep_cursor = &mut child.node; + hoistable_ptr = next_hoistable; + } + + // Mark final node as dependency + dep_cursor.access_type = + merge_access(dep_cursor.access_type, PropertyAccessType::OptionalDependency); + } + + fn derive_minimal_dependencies(&self, _env: &Environment) -> Vec { + let mut results = Vec::new(); + for (&root_id, (root_node, reactive)) in &self.dep_roots { + collect_minimal_deps_in_subtree(root_node, *reactive, root_id, &[], &mut results); + } + results + } +} + +fn collect_minimal_deps_in_subtree( + node: &DependencyNode, + reactive: bool, + root_id: IdentifierId, + path: &[DependencyPathEntry], + results: &mut Vec, +) { + if is_dependency_access(node.access_type) { + results.push(ReactiveScopeDependency { + identifier: root_id, + reactive, + path: path.to_vec(), + loc: node.loc, + }); + } else { + for (child_name, child_entry) in &node.properties { + let mut new_path = path.to_vec(); + new_path.push(DependencyPathEntry { + property: child_name.clone(), + optional: is_optional_access(child_entry.node.access_type), + loc: child_entry.node.loc, + }); + collect_minimal_deps_in_subtree( + &child_entry.node, + reactive, + root_id, + &new_path, + results, + ); + } + } +} + +// ============================================================================= +// collectDependencies +// ============================================================================= + +/// A declaration record: instruction id + scope stack at declaration time. +#[derive(Clone)] +struct Decl { + id: EvaluationOrder, + scope_stack: Vec, // copy of the scope stack at time of declaration +} + +/// Context for dependency collection. +struct DependencyCollectionContext<'a> { + declarations: FxHashMap, + reassignments: FxHashMap, + scope_stack: Vec, + dep_stack: Vec>, + deps: FxIndexMap>, + temporaries: &'a FxHashMap, + #[allow(dead_code)] + temporaries_used_outside_scope: &'a FxHashSet, + processed_instrs_in_optional: &'a FxHashSet, + inner_fn_context: Option, +} + +impl<'a> DependencyCollectionContext<'a> { + fn new( + temporaries_used_outside_scope: &'a FxHashSet, + temporaries: &'a FxHashMap, + processed_instrs_in_optional: &'a FxHashSet, + ) -> Self { + Self { + declarations: FxHashMap::default(), + reassignments: FxHashMap::default(), + scope_stack: Vec::new(), + dep_stack: Vec::new(), + deps: FxIndexMap::default(), + temporaries, + temporaries_used_outside_scope, + processed_instrs_in_optional, + inner_fn_context: None, + } + } + + fn enter_scope(&mut self, scope_id: ScopeId) { + self.dep_stack.push(Vec::new()); + self.scope_stack.push(scope_id); + } + + fn exit_scope(&mut self, scope_id: ScopeId, pruned: bool, env: &mut Environment) { + let scoped_deps = + self.dep_stack.pop().expect("[PropagateScopeDeps]: Unexpected scope mismatch"); + self.scope_stack.pop(); + + // Propagate dependencies upward + for dep in &scoped_deps { + if self.check_valid_dependency(dep, env) { + if let Some(top) = self.dep_stack.last_mut() { + top.push(dep.clone()); + } + } + } + + if !pruned { + self.deps.insert(scope_id, scoped_deps); + } + } + + fn current_scope(&self) -> Option { + self.scope_stack.last().copied() + } + + fn declare(&mut self, identifier_id: IdentifierId, decl: Decl, env: &Environment) { + if self.inner_fn_context.is_some() { + return; + } + let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id; + if !self.declarations.contains_key(&decl_id) { + self.declarations.insert(decl_id, decl.clone()); + } + self.reassignments.insert(identifier_id, decl); + } + + fn has_declared(&self, identifier_id: IdentifierId, env: &Environment) -> bool { + let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id; + self.declarations.contains_key(&decl_id) + } + + fn check_valid_dependency(&self, dep: &ReactiveScopeDependency, env: &Environment) -> bool { + // Ref value is not a valid dep + let ty = &env.types[env.identifiers[dep.identifier.0 as usize].type_.0 as usize]; + if crate::react_compiler_hir::is_ref_value_type(ty) { + return false; + } + // Object methods are not deps + if matches!(ty, Type::ObjectMethod) { + return false; + } + + let ident = &env.identifiers[dep.identifier.0 as usize]; + let current_declaration = self + .reassignments + .get(&dep.identifier) + .or_else(|| self.declarations.get(&ident.declaration_id)); + + if let Some(current_scope) = self.current_scope() { + if let Some(decl) = current_declaration { + let scope_range_start = env.scopes[current_scope.0 as usize].range.start; + return decl.id < scope_range_start; + } + } + false + } + + fn visit_operand(&mut self, place: &Place, env: &mut Environment) { + let dep = self.temporaries.get(&place.identifier).cloned().unwrap_or_else(|| { + ReactiveScopeDependency { + identifier: place.identifier, + reactive: place.reactive, + path: vec![], + loc: place.loc, + } + }); + self.visit_dependency(dep, env); + } + + fn visit_property( + &mut self, + object: &Place, + property: &PropertyLiteral, + optional: bool, + loc: Option, + env: &mut Environment, + ) { + let dep = get_property(object, property, optional, loc, self.temporaries, env); + self.visit_dependency(dep, env); + } + + fn visit_dependency(&mut self, dep: ReactiveScopeDependency, env: &mut Environment) { + let ident = &env.identifiers[dep.identifier.0 as usize]; + let decl_id = ident.declaration_id; + + // Record scope declarations for values used outside their declaring scope + if let Some(original_decl) = self.declarations.get(&decl_id) { + if !original_decl.scope_stack.is_empty() { + let orig_scope_stack = original_decl.scope_stack.clone(); + for &scope_id in &orig_scope_stack { + if !self.scope_stack.contains(&scope_id) { + // Check if already declared in this scope + let scope = &env.scopes[scope_id.0 as usize]; + let already_declared = scope.declarations.iter().any(|(_, d)| { + env.identifiers[d.identifier.0 as usize].declaration_id == decl_id + }); + if !already_declared { + let orig_scope_id = *orig_scope_stack.last().unwrap(); + let new_decl = crate::react_compiler_hir::ReactiveScopeDeclaration { + identifier: dep.identifier, + scope: orig_scope_id, + }; + env.scopes[scope_id.0 as usize] + .declarations + .push((dep.identifier, new_decl)); + } + } + } + } + } + + // Handle ref.current access + let dep = if crate::react_compiler_hir::is_use_ref_type( + &env.types[env.identifiers[dep.identifier.0 as usize].type_.0 as usize], + ) && dep + .path + .first() + .map(|p| p.property == PropertyLiteral::String("current".to_string())) + .unwrap_or(false) + { + ReactiveScopeDependency { + identifier: dep.identifier, + reactive: dep.reactive, + path: vec![], + loc: dep.loc, + } + } else { + dep + }; + + if self.check_valid_dependency(&dep, env) { + if let Some(top) = self.dep_stack.last_mut() { + top.push(dep); + } + } + } + + fn visit_reassignment(&mut self, place: &Place, env: &mut Environment) { + if let Some(current_scope) = self.current_scope() { + let scope = &env.scopes[current_scope.0 as usize]; + let already = scope.reassignments.iter().any(|id| { + env.identifiers[id.0 as usize].declaration_id + == env.identifiers[place.identifier.0 as usize].declaration_id + }); + if !already + && self.check_valid_dependency( + &ReactiveScopeDependency { + identifier: place.identifier, + reactive: place.reactive, + path: vec![], + loc: place.loc, + }, + env, + ) + { + env.scopes[current_scope.0 as usize].reassignments.push(place.identifier); + } + } + } + + fn is_deferred_dependency_instr(&self, instr: &Instruction) -> bool { + self.processed_instrs_in_optional + .contains(&ProcessedInstr::Instruction(instr.lvalue.identifier)) + || self.temporaries.contains_key(&instr.lvalue.identifier) + } + + fn is_deferred_dependency_terminal(&self, block_id: BlockId) -> bool { + self.processed_instrs_in_optional.contains(&ProcessedInstr::Terminal(block_id)) + } +} + +/// Recursively visit an inner function's blocks, processing all instructions +/// including nested FunctionExpressions. This mirrors the TS pattern of +/// `context.enterInnerFn(instr, () => handleFunction(innerFn))`. +fn visit_inner_function_blocks( + func_id: FunctionId, + ctx: &mut DependencyCollectionContext, + env: &mut Environment, +) { + // Clone inner function's instructions and block structure to avoid + // borrow conflicts when mutating env through handle_instruction. + let inner_instrs: Vec = env.functions[func_id.0 as usize].instructions.clone(); + let inner_blocks: Vec<(BlockId, Vec, Vec<(BlockId, IdentifierId)>, Terminal)> = + env.functions[func_id.0 as usize] + .body + .blocks + .iter() + .map(|(bid, blk)| { + let phi_ops: Vec<(BlockId, IdentifierId)> = blk + .phis + .iter() + .flat_map(|phi| { + phi.operands.iter().map(|(pred, place)| (*pred, place.identifier)) + }) + .collect(); + (*bid, blk.instructions.clone(), phi_ops, blk.terminal.clone()) + }) + .collect(); + + for (inner_bid, inner_instr_ids, inner_phis, inner_terminal) in &inner_blocks { + for &(_pred_id, op_id) in inner_phis { + if let Some(maybe_optional) = ctx.temporaries.get(&op_id) { + ctx.visit_dependency(maybe_optional.clone(), env); + } + } + + for &iid in inner_instr_ids { + let inner_instr = &inner_instrs[iid.0 as usize]; + match &inner_instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + // Recursively visit nested function expressions + let scope_stack_copy = ctx.scope_stack.clone(); + ctx.declare( + inner_instr.lvalue.identifier, + Decl { id: inner_instr.id, scope_stack: scope_stack_copy }, + env, + ); + visit_inner_function_blocks(lowered_func.func, ctx, env); + } + _ => { + handle_instruction(inner_instr, ctx, env); + } + } + } + + if !ctx.is_deferred_dependency_terminal(*inner_bid) { + let terminal_ops = visitors::each_terminal_operand(inner_terminal); + for op in &terminal_ops { + ctx.visit_operand(op, env); + } + } + } +} + +fn handle_instruction( + instr: &Instruction, + ctx: &mut DependencyCollectionContext, + env: &mut Environment, +) { + let id = instr.id; + let scope_stack_copy = ctx.scope_stack.clone(); + ctx.declare(instr.lvalue.identifier, Decl { id, scope_stack: scope_stack_copy }, env); + + if ctx.is_deferred_dependency_instr(instr) { + return; + } + + match &instr.value { + InstructionValue::PropertyLoad { object, property, loc, .. } => { + ctx.visit_property(object, property, false, *loc, env); + } + InstructionValue::StoreLocal { value: val, lvalue, .. } => { + ctx.visit_operand(val, env); + if lvalue.kind == InstructionKind::Reassign { + ctx.visit_reassignment(&lvalue.place, env); + } + let scope_stack_copy = ctx.scope_stack.clone(); + ctx.declare(lvalue.place.identifier, Decl { id, scope_stack: scope_stack_copy }, env); + } + InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::DeclareContext { lvalue, .. } => { + if convert_hoisted_lvalue_kind(lvalue.kind).is_none() { + let scope_stack_copy = ctx.scope_stack.clone(); + ctx.declare( + lvalue.place.identifier, + Decl { id, scope_stack: scope_stack_copy }, + env, + ); + } + } + InstructionValue::Destructure { value: val, lvalue, .. } => { + ctx.visit_operand(val, env); + let pattern_places = visitors::each_pattern_operand(&lvalue.pattern); + for place in &pattern_places { + if lvalue.kind == InstructionKind::Reassign { + ctx.visit_reassignment(place, env); + } + let scope_stack_copy = ctx.scope_stack.clone(); + ctx.declare(place.identifier, Decl { id, scope_stack: scope_stack_copy }, env); + } + } + InstructionValue::StoreContext { lvalue, value: val, .. } => { + if !ctx.has_declared(lvalue.place.identifier, env) + || lvalue.kind != InstructionKind::Reassign + { + let scope_stack_copy = ctx.scope_stack.clone(); + ctx.declare( + lvalue.place.identifier, + Decl { id, scope_stack: scope_stack_copy }, + env, + ); + } + // Visit all operands (lvalue.place AND value) + ctx.visit_operand(&lvalue.place, env); + ctx.visit_operand(val, env); + } + _ => { + // Visit all value operands + let operands = visitors::each_instruction_value_operand(&instr.value, env); + for operand in &operands { + ctx.visit_operand(operand, env); + } + } + } +} + +fn collect_dependencies( + func: &HirFunction, + env: &mut Environment, + used_outside_declaring_scope: &FxHashSet, + temporaries: &FxHashMap, + processed_instrs_in_optional: &FxHashSet, +) -> FxIndexMap> { + let mut ctx = DependencyCollectionContext::new( + used_outside_declaring_scope, + temporaries, + processed_instrs_in_optional, + ); + + // Declare params + for param in &func.params { + match param { + ParamPattern::Place(place) => { + ctx.declare( + place.identifier, + Decl { id: EvaluationOrder(0), scope_stack: vec![] }, + env, + ); + } + ParamPattern::Spread(spread) => { + ctx.declare( + spread.place.identifier, + Decl { id: EvaluationOrder(0), scope_stack: vec![] }, + env, + ); + } + } + } + + let mut traversal = ScopeBlockTraversal::new(); + + handle_function_deps(func, env, &mut ctx, &mut traversal); + + ctx.deps +} + +fn handle_function_deps( + func: &HirFunction, + env: &mut Environment, + ctx: &mut DependencyCollectionContext, + traversal: &mut ScopeBlockTraversal, +) { + for (block_id, block) in &func.body.blocks { + // Record scopes + traversal.record_scopes(block); + + let scope_block_info = traversal.block_infos.get(block_id).cloned(); + match &scope_block_info { + Some(ScopeBlockInfo::Begin { scope, .. }) => { + ctx.enter_scope(*scope); + } + Some(ScopeBlockInfo::End { scope, pruned, .. }) => { + ctx.exit_scope(*scope, *pruned, env); + } + None => {} + } + + // Record phi operands + for phi in &block.phis { + for (_pred_id, operand) in &phi.operands { + if let Some(maybe_optional_chain) = ctx.temporaries.get(&operand.identifier) { + ctx.visit_dependency(maybe_optional_chain.clone(), env); + } + } + } + + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let scope_stack_copy = ctx.scope_stack.clone(); + ctx.declare( + instr.lvalue.identifier, + Decl { id: instr.id, scope_stack: scope_stack_copy }, + env, + ); + + // Recursively visit inner function + let inner_func_id = lowered_func.func; + let prev_inner = ctx.inner_fn_context; + if ctx.inner_fn_context.is_none() { + ctx.inner_fn_context = Some(instr.id); + } + + visit_inner_function_blocks(inner_func_id, ctx, env); + + ctx.inner_fn_context = prev_inner; + } + _ => { + handle_instruction(instr, ctx, env); + } + } + } + + // Terminal operands + if !ctx.is_deferred_dependency_terminal(*block_id) { + let terminal_ops = visitors::each_terminal_operand(&block.terminal); + for op in &terminal_ops { + ctx.visit_operand(op, env); + } + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_lowering/build_hir.rs b/crates/oxc_react_compiler/src/react_compiler_lowering/build_hir.rs new file mode 100644 index 0000000000000..406e00619f2d2 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_lowering/build_hir.rs @@ -0,0 +1,7632 @@ +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::CompilerDiagnostic; +use crate::react_compiler_diagnostics::CompilerDiagnosticDetail; +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_diagnostics::CompilerErrorDetail; +use crate::react_compiler_diagnostics::ErrorCategory; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::*; +use crate::react_compiler_utils::FxIndexMap; +use crate::react_compiler_utils::FxIndexSet; +use crate::scope::BindingId; +use crate::scope::BindingKind as AstBindingKind; +use crate::scope::ScopeId; +use crate::scope::ScopeInfo; +use crate::scope::ScopeKind; + +use oxc_ast::ast as oxc; +use oxc_span::GetSpan; + +use crate::react_compiler_lowering::FunctionNode; +use crate::react_compiler_lowering::find_context_identifiers::find_context_identifiers; +use crate::react_compiler_lowering::hir_builder::HirBuilder; +use crate::react_compiler_lowering::hir_builder::is_always_reserved_word; +use crate::react_compiler_lowering::hir_builder::reserved_identifier_diagnostic; +use crate::react_compiler_lowering::identifier_loc_index::IdentifierLocIndex; +use crate::react_compiler_lowering::identifier_loc_index::build_identifier_loc_index; +use crate::react_compiler_lowering::source_loc::LineOffsets; + +fn validate_ts_this_parameter( + scope_info: &ScopeInfo, + function_scope: ScopeId, +) -> Result<(), CompilerError> { + let Some(scope) = scope_info.scopes.get(function_scope.0 as usize) else { + return Ok(()); + }; + let Some(binding_id) = scope.bindings.get("this") else { + return Ok(()); + }; + let Some(binding) = scope_info.bindings.get(binding_id.0 as usize) else { + return Ok(()); + }; + if matches!(binding.kind, AstBindingKind::Param) { + return Err(CompilerError::from(reserved_identifier_diagnostic("this"))); + } + Ok(()) +} + +fn is_class_scope_descendant(scope_info: &ScopeInfo, mut scope_id: ScopeId) -> bool { + while let Some(scope) = scope_info.scopes.get(scope_id.0 as usize) { + let Some(parent) = scope.parent else { + return false; + }; + let Some(parent_scope) = scope_info.scopes.get(parent.0 as usize) else { + return false; + }; + if matches!(parent_scope.kind, ScopeKind::Class) { + return true; + } + scope_id = parent; + } + false +} + +fn validate_ts_this_parameters_in_function_range( + scope_info: &ScopeInfo, + start: u32, + end: u32, +) -> Result<(), CompilerError> { + if start >= end { + return Ok(()); + } + // Only scopes that declare a `this` binding can fail this validation, so + // iterate that precomputed (usually empty) set instead of every scope in the + // program — the previous full scan was O(functions × all-scopes). + for &(node_start, scope_id) in &scope_info.this_binding_scopes { + if node_start < start || node_start >= end { + continue; + } + let Some(scope) = scope_info.scopes.get(scope_id.0 as usize) else { + continue; + }; + if !matches!(scope.kind, ScopeKind::Function) + || is_class_scope_descendant(scope_info, scope_id) + { + continue; + } + validate_ts_this_parameter(scope_info, scope_id)?; + } + Ok(()) +} + +/// Get the Babel-style type name of an Expression node (e.g. "Identifier", "NumericLiteral"). +fn build_temporary_place(builder: &mut HirBuilder<'_, '_>, loc: Option) -> Place { + let id = builder.make_temporary(loc.clone()); + Place { identifier: id, reactive: false, effect: Effect::Unknown, loc } +} + +/// Promote a temporary identifier to a named identifier (for destructuring). +/// Corresponds to TS `promoteTemporary(identifier)`. +fn promote_temporary(builder: &mut HirBuilder<'_, '_>, identifier_id: IdentifierId) { + let env = builder.environment_mut(); + let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id; + env.identifiers[identifier_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#t{}", decl_id.0))); +} + +fn lower_value_to_temporary<'a>( + builder: &mut HirBuilder<'a, '_>, + value: InstructionValue<'a>, +) -> Result { + // Optimization: if loading an unnamed temporary, skip creating a new instruction + if let InstructionValue::LoadLocal { ref place, .. } = value { + let ident = &builder.environment().identifiers[place.identifier.0 as usize]; + if ident.name.is_none() { + return Ok(place.clone()); + } + } + let loc = value.loc().cloned(); + let place = build_temporary_place(builder, loc.clone()); + builder.push(Instruction { + id: EvaluationOrder(0), + lvalue: place.clone(), + value, + loc, + effects: None, + }); + Ok(place) +} + +fn lower_expression_to_temporary<'a>( + builder: &mut HirBuilder<'a, '_>, + expr: &'a oxc::Expression<'a>, +) -> Result { + let value = lower_expression(builder, expr)?; + lower_value_to_temporary(builder, value) +} + +// ============================================================================= +// Operator conversion +// ============================================================================= + +fn is_binding_in_block_direct_statements( + binding: &crate::scope::BindingData, + stmts: &[oxc::Statement], +) -> bool { + let decl_start = match binding.declaration_start { + Some(pos) => pos, + None => return false, + }; + for stmt in stmts { + if matches!( + stmt, + oxc::Statement::VariableDeclaration(_) + | oxc::Statement::FunctionDeclaration(_) + | oxc::Statement::ClassDeclaration(_) + ) { + let span = stmt.span(); + if decl_start >= span.start && decl_start < span.end { + return true; + } + } + } + false +} + +// ============================================================================= +// Statement position helpers +// ============================================================================= + +fn statement_start(stmt: &oxc::Statement) -> Option { + Some(stmt.span().start) +} + +fn statement_end(stmt: &oxc::Statement) -> Option { + Some(stmt.span().end) +} + +/// Collect binding names from a pattern that are declared in the given scope. +fn collect_binding_names_from_pattern( + pattern: &oxc::BindingPattern, + scope_id: crate::scope::ScopeId, + scope_info: &ScopeInfo, + out: &mut FxHashSet, +) { + match pattern { + oxc::BindingPattern::BindingIdentifier(id) => { + if let Some(&binding_id) = + scope_info.scopes[scope_id.0 as usize].bindings.get(id.name.as_str()) + { + out.insert(binding_id); + } + } + oxc::BindingPattern::ObjectPattern(obj) => { + for prop in &obj.properties { + collect_binding_names_from_pattern(&prop.value, scope_id, scope_info, out); + } + if let Some(rest) = &obj.rest { + collect_binding_names_from_pattern(&rest.argument, scope_id, scope_info, out); + } + } + oxc::BindingPattern::ArrayPattern(arr) => { + for elem in arr.elements.iter().flatten() { + collect_binding_names_from_pattern(elem, scope_id, scope_info, out); + } + if let Some(rest) = &arr.rest { + collect_binding_names_from_pattern(&rest.argument, scope_id, scope_info, out); + } + } + oxc::BindingPattern::AssignmentPattern(assign) => { + collect_binding_names_from_pattern(&assign.left, scope_id, scope_info, out); + } + } +} + +// ============================================================================= +// lower_block_statement (with hoisting) +// ============================================================================= + +/// Lower a BlockStatement with hoisting support. +/// +/// Implements the TS BlockStatement hoisting pass: identifies forward references to +/// block-scoped bindings and emits DeclareContext instructions to hoist them. +fn lower_block_statement<'a>( + builder: &mut HirBuilder<'a, '_>, + statements: &'a [oxc::Statement<'a>], + block_node_id: u32, + parent_scope: Option, +) -> Result<(), CompilerError> { + let _ = lower_block_statement_inner(builder, statements, block_node_id, None, parent_scope); + Ok(()) +} + +fn lower_block_statement_with_scope<'a>( + builder: &mut HirBuilder<'a, '_>, + statements: &'a [oxc::Statement<'a>], + block_node_id: u32, + scope_override: crate::scope::ScopeId, +) -> Result<(), CompilerError> { + let _ = + lower_block_statement_inner(builder, statements, block_node_id, Some(scope_override), None); + Ok(()) +} + +fn lower_block_statement_inner<'a>( + builder: &mut HirBuilder<'a, '_>, + statements: &'a [oxc::Statement<'a>], + block_node_id: u32, + scope_override: Option, + parent_scope: Option, +) -> Result<(), CompilerDiagnostic> { + use crate::scope::BindingKind as AstBindingKind; + + // Look up the block's scope to identify hoistable bindings. + // Use the scope override if provided (for function body blocks that share the function's scope). + let block_scope_id = scope_override.or_else(|| { + let found = builder.scope_info().resolve_scope_for_node(Some(block_node_id)); + if found.is_some() { + return found; + } + // Fallback for synthetic blocks (start=0 from Hermes match desugar): + // find a descendant scope of the parent that contains the block's declarations. + let mut decl_names = Vec::new(); + for stmt in statements { + if let oxc::Statement::VariableDeclaration(vd) = stmt { + for d in &vd.declarations { + if let oxc::BindingPattern::BindingIdentifier(id) = &d.id { + decl_names.push(id.name.as_str()); + } + } + } + } + if decl_names.is_empty() { + return None; + } + let search_parent = parent_scope.unwrap_or_else(|| builder.function_scope()); + let found = + builder.scope_info().find_block_scope_by_bindings(&decl_names, search_parent, |sid| { + builder.is_synthetic_scope_claimed(sid) + }); + if let Some(sid) = found { + builder.claim_synthetic_scope(sid); + } + found + }); + + let scope_id = match block_scope_id { + Some(id) => id, + None => { + for body_stmt in statements { + lower_statement(builder, body_stmt, None, parent_scope)?; + } + return Ok(()); + } + }; + + // Collect hoistable bindings from this scope AND direct child block scopes. + // In Babel, a function body BlockStatement shares the function's scope, so + // all bindings (var, const, let) are in one scope. But our scope extraction + // may split them: function scope has params/var, child block scope has const/let. + // Including child block scope bindings matches TS behavior where + // stmt.scope.bindings includes all bindings accessible in the block. + // + // IMPORTANT: Only include bindings whose declaration falls within THIS block's + // statement range. Bindings declared in nested blocks (e.g., inside an `if` + // branch) should NOT be hoisted at the parent level — they'll be handled when + // that nested block is recursively lowered. This prevents DeclareContext from + // being emitted before an `if` terminal for variables declared within the branch. + let hoistable: Vec<(BindingId, String, AstBindingKind, String, Option, Option)> = + builder + .scope_info() + .scope_bindings_with_children(scope_id) + .filter(|b| { + !matches!(b.kind, AstBindingKind::Param | AstBindingKind::Module) + && b.declaration_type != "FunctionExpression" + && b.declaration_type != "TypeAlias" + && b.declaration_type != "OpaqueType" + && b.declaration_type != "InterfaceDeclaration" + && b.declaration_type != "TSTypeAliasDeclaration" + && b.declaration_type != "TSInterfaceDeclaration" + && b.declaration_type != "TSEnumDeclaration" + }) + .map(|b| { + ( + b.id, + b.name.clone(), + b.kind.clone(), + b.declaration_type.clone(), + b.declaration_start, + b.declaration_node_id, + ) + }) + .collect(); + + if hoistable.is_empty() { + // No hoistable bindings, just lower statements normally + for body_stmt in statements { + lower_statement(builder, body_stmt, None, Some(scope_id))?; + } + return Ok(()); + } + + // Track which bindings have been "declared" (their declaration statement has been seen) + let mut declared: FxHashSet = FxHashSet::default(); + + for body_stmt in statements { + let stmt_start = statement_start(body_stmt).unwrap_or(0); + let stmt_end = statement_end(body_stmt).unwrap_or(u32::MAX); + let is_function_decl = matches!(body_stmt, oxc::Statement::FunctionDeclaration(_)); + + // Collect ranges of nested function scopes within this statement. + // Used to check per-reference whether a reference is inside a nested function, + // rather than checking once per-statement. + let nested_function_ranges: Vec<(u32, u32)> = if is_function_decl { + // For function declarations, fnDepth starts at 1 (all refs are inside) + vec![(stmt_start, stmt_end)] + } else { + let scope_info = builder.scope_info(); + scope_info + .node_to_scope + .iter() + .filter(|&(&pos, &sid)| { + pos > stmt_start + && pos < stmt_end + && matches!(scope_info.scopes[sid.0 as usize].kind, ScopeKind::Function) + }) + .filter_map(|(&pos, _)| { + scope_info.node_to_scope_end.get(&pos).map(|&end| (pos, end)) + }) + .collect() + }; + + // Find references to not-yet-declared hoistable bindings within this statement + struct HoistInfo { + binding_id: BindingId, + name: String, + kind: AstBindingKind, + declaration_type: String, + first_ref_pos: u32, + first_ref_nid: u32, + } + let mut will_hoist: Vec = Vec::new(); + + for (binding_id, name, kind, decl_type, _decl_start, decl_node_id) in &hoistable { + if declared.contains(binding_id) { + continue; + } + + // Find the first reference (not declaration) to this binding in the statement's range. + // Exclude JSX identifier references: while Babel's scope system links JSX + // tag names to local bindings (and the context capture pass includes them), + // the TS hoisting analysis does NOT traverse JSX elements. This mismatch + // is intentional — it matches the TS behavior where adds + // "colgroup" to the context but does NOT trigger hoisting, causing + // EnterSSA to error with "Expected identifier to be defined before use". + // + // The decl_start filter excludes the binding's own declaration position from + // counting as a reference. For hoisted bindings (function declarations), this + // filter is only applied when the current statement IS a FunctionDeclaration, + // since that's the only statement type where decl_start is a declaration, not + // a reference. + let apply_decl_filter = !matches!(kind, AstBindingKind::Hoisted) || is_function_decl; + let refs_in_stmt: Vec<(u32, u32)> = builder + .scope_info() + .ref_node_id_to_binding + .iter() + .filter_map(|(&ref_nid, &ref_bid)| { + if ref_bid != *binding_id { + return None; + } + let entry = builder.identifier_locs().get(&ref_nid)?; + let ref_start = entry.start; + if ref_start < stmt_start || ref_start >= stmt_end { + return None; + } + if apply_decl_filter && *decl_node_id == Some(ref_nid) { + return None; + } + if entry.is_jsx { + return None; + } + Some((ref_start, ref_nid)) + }) + .collect(); + + if refs_in_stmt.is_empty() { + continue; + } + + let (first_ref_pos, first_ref_nid) = + *refs_in_stmt.iter().min_by_key(|(pos, _)| *pos).unwrap(); + + // Hoist if: (1) binding is "hoisted" kind (function declaration), or + // (2) any reference to this binding is inside a nested function scope. + // Check per-reference rather than per-statement to correctly handle + // statements that contain both nested functions and top-level code. + let is_hoisted_kind = matches!(kind, AstBindingKind::Hoisted); + let refs_in_nested_fn: Vec<(u32, u32)> = refs_in_stmt + .iter() + .copied() + .filter(|&(ref_pos, _)| { + nested_function_ranges + .iter() + .any(|&(fn_start, fn_end)| ref_pos >= fn_start && ref_pos < fn_end) + }) + .collect(); + let should_hoist = is_hoisted_kind || !refs_in_nested_fn.is_empty(); + if should_hoist { + // Bindings pulled in from CHILD block scopes (the + // scope_bindings_with_children descent compensates for scope + // splitting) only hoist when declared as a direct statement of + // THIS block; ones declared inside nested control-flow blocks + // are handled when those blocks are recursively lowered. TS + // never sees child-block bindings here (Babel's + // stmt.scope.bindings holds only the block's own scope), so the + // guard must NOT apply to own-scope bindings: catch params and + // for-in/for-of head vars belong to the block's scope without + // being declared by any direct statement, and TS hoists them. + let binding_data = &builder.scope_info().bindings[binding_id.0 as usize]; + if binding_data.scope != scope_id + && !is_binding_in_block_direct_statements(binding_data, statements) + { + continue; + } + // For hoisted bindings (function declarations), use the first reference + // overall. For non-hoisted bindings, use the first reference inside a + // nested function. + let (hoist_ref_pos, hoist_ref_nid) = if is_hoisted_kind { + (first_ref_pos, first_ref_nid) + } else { + *refs_in_nested_fn.iter().min_by_key(|(pos, _)| *pos).unwrap() + }; + will_hoist.push(HoistInfo { + binding_id: *binding_id, + name: name.clone(), + kind: kind.clone(), + declaration_type: decl_type.clone(), + first_ref_pos: hoist_ref_pos, + first_ref_nid: hoist_ref_nid, + }); + } + } + + // Sort by first reference position to match TS traversal order + will_hoist.sort_by_key(|h| h.first_ref_pos); + + // Emit DeclareContext for hoisted bindings + for info in &will_hoist { + if builder.environment().is_hoisted_identifier(info.binding_id.0) { + continue; + } + + let hoist_kind = match info.kind { + AstBindingKind::Const | AstBindingKind::Var => InstructionKind::HoistedConst, + AstBindingKind::Let => InstructionKind::HoistedLet, + AstBindingKind::Hoisted => InstructionKind::HoistedFunction, + _ => { + if info.declaration_type == "FunctionDeclaration" { + InstructionKind::HoistedFunction + } else if info.declaration_type == "VariableDeclarator" { + // Unsupported hoisting for this declaration kind + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "Handle non-const declarations for hoisting".to_string(), + description: Some(format!( + "variable \"{}\" declared with {:?}", + info.name, info.kind + )), + loc: None, + suggestions: None, + })?; + continue; + } else { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "Unsupported declaration type for hoisting".to_string(), + description: Some(format!( + "variable \"{}\" declared with {}", + info.name, info.declaration_type + )), + loc: None, + suggestions: None, + })?; + continue; + } + } + }; + + // Look up the reference location for the DeclareContext instruction. + let ref_loc = builder.identifier_locs().get(&info.first_ref_nid).map(|e| e.loc.clone()); + let identifier = builder.resolve_binding(&info.name, info.binding_id)?; + let place = Place { + effect: Effect::Unknown, + identifier, + reactive: false, + loc: ref_loc.clone(), + }; + lower_value_to_temporary( + builder, + InstructionValue::DeclareContext { + lvalue: LValue { kind: hoist_kind, place }, + loc: ref_loc, + }, + )?; + builder.environment_mut().add_hoisted_identifier(info.binding_id.0); + // Hoisted identifiers also become context identifiers (matching TS addHoistedIdentifier) + builder.add_context_identifier(info.binding_id); + } + + // After processing the statement, mark any bindings it declares as "seen". + // This must cover all statement types that can introduce bindings. + match body_stmt { + oxc::Statement::FunctionDeclaration(func) => { + if let Some(id) = &func.id { + if let Some(&binding_id) = builder.scope_info().scopes[scope_id.0 as usize] + .bindings + .get(id.name.as_str()) + { + declared.insert(binding_id); + } + } + } + oxc::Statement::VariableDeclaration(var_decl) => { + for decl in &var_decl.declarations { + collect_binding_names_from_pattern( + &decl.id, + scope_id, + builder.scope_info(), + &mut declared, + ); + } + } + oxc::Statement::ClassDeclaration(cls) => { + if let Some(id) = &cls.id { + if let Some(&binding_id) = builder.scope_info().scopes[scope_id.0 as usize] + .bindings + .get(id.name.as_str()) + { + declared.insert(binding_id); + } + } + } + _ => { + // For other statement types (e.g. ForStatement with VariableDeclaration in init), + // we rely on the reference_to_binding check for forward references. + // Any bindings declared by child scopes won't be in this block's scope anyway. + } + } + + lower_statement(builder, body_stmt, None, Some(scope_id))?; + } + Ok(()) +} + +// ============================================================================= +// lower_statement +// ============================================================================= + +enum FunctionBody<'a> { + Block(&'a oxc::FunctionBody<'a>), + Expression(&'a oxc::Expression<'a>), +} + +/// Main entry point: lower a function AST node into HIR. +/// +/// Receives a `FunctionNode` (discovered by the entrypoint) and lowers it to HIR. +/// The `id` parameter provides the function name (which may come from the variable +/// declarator rather than the function node itself, e.g. `const Foo = () => {}`). +pub fn lower<'a>( + func: &'a FunctionNode<'a>, + _id: Option<&str>, + scope_info: &ScopeInfo, + env: &mut Environment<'a>, + line_offsets: &LineOffsets, +) -> Result, CompilerError> { + // Extract params, body, generator, is_async, loc, scope_id, and the AST function's own id + // Note: `id` param may include inferred names (e.g., from `const Foo = () => {}`), + // but the HIR function's `id` field should only include the function's own AST id + // (FunctionDeclaration.id or FunctionExpression.id, NOT arrow functions). + let (params, body, generator, is_async, loc, start, end, ast_id) = match func { + FunctionNode::Function(f) => { + let body_ref = f.body.as_deref().expect("component function has a body"); + ( + f.params.as_ref(), + FunctionBody::Block(body_ref), + f.generator, + f.r#async, + Some(line_offsets.source_location(f.span)), + f.span.start, + f.span.end, + f.id.as_ref().map(|id| id.name.as_str()), + ) + } + FunctionNode::Arrow(arrow) => { + let body = if arrow.expression { + match arrow.body.statements.first() { + Some(oxc::Statement::ExpressionStatement(es)) => { + FunctionBody::Expression(&es.expression) + } + _ => FunctionBody::Block(arrow.body.as_ref()), + } + } else { + FunctionBody::Block(arrow.body.as_ref()) + }; + ( + arrow.params.as_ref(), + body, + false, + arrow.r#async, + Some(line_offsets.source_location(arrow.span)), + arrow.span.start, + arrow.span.end, + None, + ) + } + }; + + let scope_id = + scope_info.resolve_scope_for_node(func.node_id()).unwrap_or(scope_info.program_scope); + + validate_ts_this_parameters_in_function_range(scope_info, start, end)?; + + // Build identifier location index from the AST (replaces serialized referenceLocs/jsxReferencePositions) + let identifier_locs = build_identifier_loc_index(func, scope_info, line_offsets); + + // Pre-compute context identifiers: variables captured across function boundaries + let context_identifiers = + find_context_identifiers(func, scope_info, env, &identifier_locs, line_offsets)?; + + // For top-level functions, context is empty (no captured refs) + let context_map: FxIndexMap> = + FxIndexMap::default(); + + let (hir_func, _used_names, _child_bindings) = lower_inner( + params, + body, + ast_id, + generator, + is_async, + loc, + scope_info, + env, + None, // no pre-existing bindings for top-level + None, // no pre-existing used_names for top-level + context_map, + scope_id, + scope_id, // component_scope = function_scope for top-level + &context_identifiers, + true, // is_top_level + &identifier_locs, + line_offsets, + )?; + + Ok(hir_func) +} + +// ============================================================================= +// Stubs for future milestones +// ============================================================================= + +/// Result of resolving an identifier for assignment. +fn lower_inner<'a>( + params: &'a oxc::FormalParameters<'a>, + body: FunctionBody<'a>, + id: Option<&str>, + generator: bool, + is_async: bool, + loc: Option, + scope_info: &ScopeInfo, + env: &mut Environment<'a>, + parent_bindings: Option>, + parent_used_names: Option>, + context_map: FxIndexMap>, + function_scope: crate::scope::ScopeId, + component_scope: crate::scope::ScopeId, + context_identifiers: &FxHashSet, + is_top_level: bool, + identifier_locs: &IdentifierLocIndex, + line_offsets: &LineOffsets, +) -> Result< + ( + HirFunction<'a>, + FxIndexMap, + FxIndexMap, + ), + CompilerError, +> { + validate_ts_this_parameter(scope_info, function_scope)?; + + let mut builder = HirBuilder::new( + env, + scope_info, + function_scope, + component_scope, + context_identifiers.clone(), + parent_bindings, + Some(context_map.clone()), + None, + parent_used_names, + identifier_locs, + line_offsets, + ); + + // Build context places from the captured refs + let mut context: Vec = Vec::new(); + for (&binding_id, ctx_loc) in &context_map { + let binding = &scope_info.bindings[binding_id.0 as usize]; + let identifier = builder.resolve_binding(&binding.name, binding_id)?; + context.push(Place { + identifier, + effect: Effect::Unknown, + reactive: false, + loc: ctx_loc.clone(), + }); + } + + // Process parameters. + let mut hir_params: Vec = Vec::new(); + for param in ¶ms.items { + if param.initializer.is_none() + && let oxc::BindingPattern::BindingIdentifier(ident) = ¶m.pattern + { + if is_always_reserved_word(ident.name.as_str()) { + return Err(CompilerError::from(reserved_identifier_diagnostic( + ident.name.as_str(), + ))); + } + let start = ident.span.start; + let param_loc = builder.source_location(ident.span); + let mut binding = builder.resolve_identifier( + ident.name.as_str(), + start, + param_loc.clone(), + Some(ident.span.start), + )?; + if !matches!(binding, VariableBinding::Identifier { .. }) { + if let Some((binding_id, binding_data)) = builder + .scope_info() + .find_binding_id_in_descendants(ident.name.as_str(), builder.function_scope()) + { + let binding_kind = + crate::react_compiler_lowering::convert_binding_kind(&binding_data.kind); + let identifier = builder.resolve_binding_with_loc( + ident.name.as_str(), + binding_id, + param_loc.clone(), + )?; + binding = VariableBinding::Identifier { identifier, binding_kind }; + } + } + match binding { + VariableBinding::Identifier { identifier, .. } => { + builder.set_identifier_declaration_loc(identifier, ¶m_loc); + let place = Place { + identifier, + effect: Effect::Unknown, + reactive: false, + loc: param_loc, + }; + hir_params.push(ParamPattern::Place(place)); + } + _ => { + builder.record_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Could not find binding", + Some(format!( + "[BuildHIR] Could not find binding for param `{}`", + ident.name.as_str() + )), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: builder.source_location(ident.span), + message: Some("Could not find binding".to_string()), + identifier_name: None, + }), + ); + } + } + continue; + } + // Destructuring (`{a}`, `[a]`), defaulted (`x = 1`, `{a} = obj`), and any + // other non-plain-identifier param. Babel modeled a default param as an + // `AssignmentPattern` param node; in oxc the default lives in + // `FormalParameter.initializer` alongside the underlying `pattern`. + // Create a temporary place for the param value, promote it, push it as the + // param, then delegate the binding via `lower_binding_assignment` (running + // the default ternary first when an initializer is present). + let param_loc = builder.source_location(param.span); + let place = build_temporary_place(&mut builder, param_loc.clone()); + promote_temporary(&mut builder, place.identifier); + hir_params.push(ParamPattern::Place(place.clone())); + let value = if let Some(initializer) = ¶m.initializer { + lower_default_to_temp(&mut builder, param_loc.clone(), initializer, place)? + } else { + place + }; + lower_binding_assignment( + &mut builder, + param_loc, + InstructionKind::Let, + ¶m.pattern, + value, + AssignmentStyle::Assignment, + )?; + } + + // Rest parameter (`...rest`). Babel modeled this as a `RestElement` param; in + // oxc it is the separate `params.rest` field. Push a spread param place and + // delegate the binding of the rest argument. + if let Some(rest) = ¶ms.rest { + let rest_loc = builder.source_location(rest.span); + let place = build_temporary_place(&mut builder, rest_loc.clone()); + hir_params.push(ParamPattern::Spread(SpreadPattern { place: place.clone() })); + lower_binding_assignment( + &mut builder, + rest_loc, + InstructionKind::Let, + &rest.rest.argument, + place, + AssignmentStyle::Assignment, + )?; + } + + // Lower the body + let mut directives: Vec = Vec::new(); + match body { + FunctionBody::Expression(expr) => { + let fallthrough = builder.reserve(BlockKind::Block); + let value = lower_expression_to_temporary(&mut builder, expr)?; + builder.terminate_with_continuation( + Terminal::Return { + value, + return_variant: ReturnVariant::Implicit, + id: EvaluationOrder(0), + loc: None, + effects: None, + }, + fallthrough, + ); + } + FunctionBody::Block(block) => { + directives = block.directives.iter().map(|d| d.expression.value.to_string()).collect(); + // A function body shares the function's scope (node_to_scope maps the + // function node, not the block), so pass it as the scope override. + lower_block_statement_with_scope( + &mut builder, + &block.statements, + block.span.start, + function_scope, + )?; + } + } + + // Emit final Return(Void, undefined) + let undefined_value = + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc: None }; + let return_value = lower_value_to_temporary(&mut builder, undefined_value)?; + builder.terminate( + Terminal::Return { + value: return_value, + return_variant: ReturnVariant::Void, + id: EvaluationOrder(0), + loc: None, + effects: None, + }, + None, + ); + + // Build the HIR + let (hir_body, instructions, used_names, child_bindings) = builder.build()?; + + // Create the returns place + let returns = + crate::react_compiler_lowering::hir_builder::create_temporary_place(env, loc.clone()); + + Ok(( + HirFunction { + loc, + id: id.map(|s| s.to_string()), + name_hint: None, + fn_type: if is_top_level { env.fn_type } else { ReactFunctionType::Other }, + params: hir_params, + return_type_annotation: None, + returns, + context, + body: hir_body, + instructions, + generator, + is_async, + directives, + aliasing_effects: None, + }, + used_names, + child_bindings, + )) +} + +// ============================================================================= +// lower_expression / lower_statement — Stage 1a skeleton catch-alls. +// +// Arms are ported incrementally from `git show HEAD:.../build_hir.rs` + the +// convert-ast reference. Until an arm lands, the catch-all bails to an undefined +// primitive / no-op so the crate compiles and the differential green-set grows. +// ============================================================================= + +// ============================================================================= +// lower_identifier +// ============================================================================= + +/// Resolve an identifier to a Place. Local/context identifiers return a Place +/// referencing the binding; globals/imports emit a LoadGlobal. AST-agnostic. +fn lower_identifier( + builder: &mut HirBuilder<'_, '_>, + name: &str, + start: u32, + loc: Option, + node_id: Option, +) -> Result { + let binding = builder.resolve_identifier(name, start, loc.clone(), node_id)?; + match binding { + VariableBinding::Identifier { identifier, .. } => { + Ok(Place { identifier, effect: Effect::Unknown, reactive: false, loc }) + } + _ => { + if let VariableBinding::Global { ref name } = binding { + if name == "eval" { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::UnsupportedSyntax, + reason: "The 'eval' function is not supported".to_string(), + description: Some( + "Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler".to_string(), + ), + loc: loc.clone(), + suggestions: None, + })?; + } + } + let non_local_binding = match binding { + VariableBinding::Global { name } => NonLocalBinding::Global { name }, + VariableBinding::ImportDefault { name, module } => { + NonLocalBinding::ImportDefault { name, module } + } + VariableBinding::ImportSpecifier { name, module, imported } => { + NonLocalBinding::ImportSpecifier { name, module, imported } + } + VariableBinding::ImportNamespace { name, module } => { + NonLocalBinding::ImportNamespace { name, module } + } + VariableBinding::ModuleLocal { name } => NonLocalBinding::ModuleLocal { name }, + VariableBinding::Identifier { .. } => unreachable!(), + }; + let instr_value = + InstructionValue::LoadGlobal { binding: non_local_binding, loc: loc.clone() }; + lower_value_to_temporary(builder, instr_value) + } + } +} + +fn convert_binary_operator(op: oxc::BinaryOperator) -> BinaryOperator { + use oxc::BinaryOperator as O; + match op { + O::Addition => BinaryOperator::Add, + O::Subtraction => BinaryOperator::Subtract, + O::Multiplication => BinaryOperator::Multiply, + O::Division => BinaryOperator::Divide, + O::Remainder => BinaryOperator::Modulo, + O::Exponential => BinaryOperator::Exponent, + O::Equality => BinaryOperator::Equal, + O::StrictEquality => BinaryOperator::StrictEqual, + O::Inequality => BinaryOperator::NotEqual, + O::StrictInequality => BinaryOperator::StrictNotEqual, + O::LessThan => BinaryOperator::LessThan, + O::LessEqualThan => BinaryOperator::LessEqual, + O::GreaterThan => BinaryOperator::GreaterThan, + O::GreaterEqualThan => BinaryOperator::GreaterEqual, + O::ShiftLeft => BinaryOperator::ShiftLeft, + O::ShiftRight => BinaryOperator::ShiftRight, + O::ShiftRightZeroFill => BinaryOperator::UnsignedShiftRight, + O::BitwiseOR => BinaryOperator::BitwiseOr, + O::BitwiseXOR => BinaryOperator::BitwiseXor, + O::BitwiseAnd => BinaryOperator::BitwiseAnd, + O::In => BinaryOperator::In, + O::Instanceof => BinaryOperator::InstanceOf, + } +} + +fn convert_unary_operator(op: oxc::UnaryOperator) -> UnaryOperator { + use oxc::UnaryOperator as O; + match op { + O::UnaryNegation => UnaryOperator::Minus, + O::UnaryPlus => UnaryOperator::Plus, + O::LogicalNot => UnaryOperator::Not, + O::BitwiseNot => UnaryOperator::BitwiseNot, + O::Typeof => UnaryOperator::TypeOf, + O::Void => UnaryOperator::Void, + O::Delete => unreachable!("delete is handled in the UnaryExpression arm"), + } +} + +enum MemberProperty { + Literal(PropertyLiteral), + Computed(Place), +} + +struct LoweredMemberExpression<'a> { + object: Place, + property: MemberProperty, + value: InstructionValue<'a>, +} + +/// Lower a member access (oxc's Static / Computed / PrivateField variants) into a +/// receiver place + property + load value. +fn lower_member_expression<'a>( + builder: &mut HirBuilder<'a, '_>, + member: &'a oxc::MemberExpression<'a>, +) -> Result, CompilerError> { + lower_member_expression_impl(builder, member, None) +} + +fn lower_member_expression_impl<'a>( + builder: &mut HirBuilder<'a, '_>, + member: &'a oxc::MemberExpression<'a>, + lowered_object: Option, +) -> Result, CompilerError> { + match member { + oxc::MemberExpression::StaticMemberExpression(m) => { + let loc = builder.source_location(m.span); + let object = match lowered_object { + Some(obj) => obj, + None => lower_expression_to_temporary(builder, &m.object)?, + }; + let prop_literal = PropertyLiteral::String(m.property.name.to_string()); + let value = InstructionValue::PropertyLoad { + object: object.clone(), + property: prop_literal.clone(), + loc, + }; + Ok(LoweredMemberExpression { + object, + property: MemberProperty::Literal(prop_literal), + value, + }) + } + oxc::MemberExpression::ComputedMemberExpression(m) => { + let loc = builder.source_location(m.span); + let object = match lowered_object { + Some(obj) => obj, + None => lower_expression_to_temporary(builder, &m.object)?, + }; + // A numeric computed index is treated as a PropertyLoad (matches TS). + if let oxc::Expression::NumericLiteral(lit) = &m.expression { + let prop_literal = PropertyLiteral::Number(FloatValue::new(lit.value)); + let value = InstructionValue::PropertyLoad { + object: object.clone(), + property: prop_literal.clone(), + loc, + }; + return Ok(LoweredMemberExpression { + object, + property: MemberProperty::Literal(prop_literal), + value, + }); + } + let property = lower_expression_to_temporary(builder, &m.expression)?; + let value = InstructionValue::ComputedLoad { + object: object.clone(), + property: property.clone(), + loc, + }; + Ok(LoweredMemberExpression { + object, + property: MemberProperty::Computed(property), + value, + }) + } + oxc::MemberExpression::PrivateFieldExpression(m) => { + let loc = builder.source_location(m.span); + let object = match lowered_object { + Some(obj) => obj, + None => lower_expression_to_temporary(builder, &m.object)?, + }; + // TODO(stage1a-arms): private field access needs a private-name property + // load + OriginalNode bail; defer to a later batch. + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerMemberExpression) Handle private field property" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + Ok(LoweredMemberExpression { + object, + property: MemberProperty::Literal(PropertyLiteral::String(String::new())), + value: InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }, + }) + } + } +} + +/// Build a HIR `TemplateQuasi` from an oxc `TemplateElement`. +fn template_quasi_from_oxc(q: &oxc::TemplateElement) -> TemplateQuasi { + TemplateQuasi { raw: q.value.raw.to_string(), cooked: q.value.cooked.map(|c| c.to_string()) } +} + +/// Lower the `import` keyword callee of an `ImportExpression`. The original Babel +/// path treats this as the `Import` node, which bails (records an error) and +/// returns an undefined primitive that is then loaded to a temporary. +fn lower_import_keyword_to_temporary( + builder: &mut HirBuilder<'_, '_>, + loc: &Option, +) -> Result { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle Import expressions".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc: loc.clone() }, + ) +} + +/// Lower a `PrivateName` operand (e.g. the left side of `#f in obj`). The original +/// Babel path bails (records an error) and returns an undefined primitive that is +/// then loaded to a temporary. +fn lower_private_name_to_temporary( + builder: &mut HirBuilder<'_, '_>, + span: oxc_span::Span, +) -> Result { + let loc = builder.source_location(span); + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle PrivateName expressions".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }, + ) +} + +/// Babel/ESTree node-type tag for an oxc TS type, used as a +/// `TypeCastExpression`'s `type_annotation_name` (mirrors `get_type_annotation_name`, +/// which reads the unwrapped type's tag). +fn ts_type_node_type(ty: &oxc::TSType) -> &'static str { + match ty { + oxc::TSType::TSAnyKeyword(_) => "TSAnyKeyword", + oxc::TSType::TSBigIntKeyword(_) => "TSBigIntKeyword", + oxc::TSType::TSBooleanKeyword(_) => "TSBooleanKeyword", + oxc::TSType::TSIntrinsicKeyword(_) => "TSIntrinsicKeyword", + oxc::TSType::TSNeverKeyword(_) => "TSNeverKeyword", + oxc::TSType::TSNullKeyword(_) => "TSNullKeyword", + oxc::TSType::TSNumberKeyword(_) => "TSNumberKeyword", + oxc::TSType::TSObjectKeyword(_) => "TSObjectKeyword", + oxc::TSType::TSStringKeyword(_) => "TSStringKeyword", + oxc::TSType::TSSymbolKeyword(_) => "TSSymbolKeyword", + oxc::TSType::TSThisType(_) => "TSThisType", + oxc::TSType::TSUndefinedKeyword(_) => "TSUndefinedKeyword", + oxc::TSType::TSUnknownKeyword(_) => "TSUnknownKeyword", + oxc::TSType::TSVoidKeyword(_) => "TSVoidKeyword", + oxc::TSType::TSArrayType(_) => "TSArrayType", + oxc::TSType::TSUnionType(_) => "TSUnionType", + oxc::TSType::TSParenthesizedType(_) => "TSParenthesizedType", + oxc::TSType::TSLiteralType(_) => "TSLiteralType", + oxc::TSType::TSTypeReference(_) => "TSTypeReference", + oxc::TSType::TSTypeOperatorType(_) => "TSTypeOperator", + oxc::TSType::TSTupleType(_) => "TSTupleType", + oxc::TSType::TSIntersectionType(_) => "TSIntersectionType", + oxc::TSType::TSTypeLiteral(_) => "TSTypeLiteral", + oxc::TSType::TSTypeQuery(_) => "TSTypeQuery", + oxc::TSType::TSFunctionType(_) => "TSFunctionType", + oxc::TSType::TSConstructorType(_) => "TSConstructorType", + oxc::TSType::TSConditionalType(_) => "TSConditionalType", + oxc::TSType::TSIndexedAccessType(_) => "TSIndexedAccessType", + oxc::TSType::TSInferType(_) => "TSInferType", + oxc::TSType::TSImportType(_) => "TSImportType", + oxc::TSType::TSMappedType(_) => "TSMappedType", + oxc::TSType::TSNamedTupleMember(_) => "TSNamedTupleMember", + oxc::TSType::TSTemplateLiteralType(_) => "TSTemplateLiteralType", + oxc::TSType::TSTypePredicate(_) => "TSTypePredicate", + oxc::TSType::JSDocNullableType(_) => "JSDocNullableType", + oxc::TSType::JSDocNonNullableType(_) => "JSDocNonNullableType", + oxc::TSType::JSDocUnknownType(_) => "JSDocUnknownType", + } +} + +/// Coarse classification of an oxc TS type, mirroring `lower_type_annotation` +/// (array / primitive / everything else). +fn classify_ts_type(ty: &oxc::TSType) -> crate::react_compiler_hir::RawTypeCategory { + use crate::react_compiler_hir::RawTypeCategory; + match ty { + oxc::TSType::TSArrayType(_) => RawTypeCategory::Array, + oxc::TSType::TSTypeReference(r) => match &r.type_name { + oxc::TSTypeName::IdentifierReference(id) if id.name == "Array" => { + RawTypeCategory::Array + } + _ => RawTypeCategory::Other, + }, + oxc::TSType::TSBooleanKeyword(_) + | oxc::TSType::TSNullKeyword(_) + | oxc::TSType::TSNumberKeyword(_) + | oxc::TSType::TSStringKeyword(_) + | oxc::TSType::TSSymbolKeyword(_) + | oxc::TSType::TSUndefinedKeyword(_) + | oxc::TSType::TSVoidKeyword(_) => RawTypeCategory::Primitive, + _ => RawTypeCategory::Other, + } +} + +/// Lower the HIR `Type` for a TS type annotation from its coarse classification, +/// mirroring `lower_type_annotation`. +fn lower_ts_type(builder: &mut HirBuilder<'_, '_>, ty: &oxc::TSType) -> Type { + use crate::react_compiler_hir::RawTypeCategory; + match classify_ts_type(ty) { + RawTypeCategory::Array => Type::Object { shape_id: Some("BuiltInArray".to_string()) }, + RawTypeCategory::Primitive => Type::Primitive, + RawTypeCategory::Other => builder.make_type(), + } +} + +/// Lower `x as T` / `x satisfies T` / `x` to a `TypeCastExpression`: the inner +/// expression is lowered to a temporary and the type metadata is attached. Mirrors +/// the original Babel `TSAsExpression`/`TSSatisfiesExpression`/`TSTypeAssertion` +/// arms. The original `TSType` AST node is stored directly so codegen can re-emit +/// it by cloning (applying any identifier renames) instead of re-parsing source. +fn lower_type_cast_expression<'a>( + builder: &mut HirBuilder<'a, '_>, + span: oxc_span::Span, + expression: &'a oxc::Expression<'a>, + type_annotation: &'a oxc::TSType<'a>, + type_annotation_kind: &str, +) -> Result, CompilerError> { + let loc = builder.source_location(span); + let value = lower_expression_to_temporary(builder, expression)?; + let type_ = lower_ts_type(builder, type_annotation); + let type_annotation_name = Some(ts_type_node_type(type_annotation).to_string()); + Ok(InstructionValue::TypeCastExpression { + value, + type_, + type_annotation_name, + type_annotation_kind: Some(type_annotation_kind.to_string()), + type_annotation: Some(type_annotation), + loc, + }) +} + +/// Lower a member-expression update target (oxc's member variants of +/// `SimpleAssignmentTarget`) into a receiver place + property + load value, +/// mirroring `lower_member_expression_impl`. +fn lower_member_expression_from_simple_target<'a>( + builder: &mut HirBuilder<'a, '_>, + target: &'a oxc::SimpleAssignmentTarget<'a>, +) -> Result, CompilerError> { + match target { + oxc::SimpleAssignmentTarget::StaticMemberExpression(m) => { + let loc = builder.source_location(m.span); + let object = lower_expression_to_temporary(builder, &m.object)?; + let prop_literal = PropertyLiteral::String(m.property.name.to_string()); + let value = InstructionValue::PropertyLoad { + object: object.clone(), + property: prop_literal.clone(), + loc, + }; + Ok(LoweredMemberExpression { + object, + property: MemberProperty::Literal(prop_literal), + value, + }) + } + oxc::SimpleAssignmentTarget::ComputedMemberExpression(m) => { + let loc = builder.source_location(m.span); + let object = lower_expression_to_temporary(builder, &m.object)?; + if let oxc::Expression::NumericLiteral(lit) = &m.expression { + let prop_literal = PropertyLiteral::Number(FloatValue::new(lit.value)); + let value = InstructionValue::PropertyLoad { + object: object.clone(), + property: prop_literal.clone(), + loc, + }; + return Ok(LoweredMemberExpression { + object, + property: MemberProperty::Literal(prop_literal), + value, + }); + } + let property = lower_expression_to_temporary(builder, &m.expression)?; + let value = InstructionValue::ComputedLoad { + object: object.clone(), + property: property.clone(), + loc, + }; + Ok(LoweredMemberExpression { + object, + property: MemberProperty::Computed(property), + value, + }) + } + oxc::SimpleAssignmentTarget::PrivateFieldExpression(m) => { + let loc = builder.source_location(m.span); + let object = lower_expression_to_temporary(builder, &m.object)?; + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerMemberExpression) Handle private field property" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + Ok(LoweredMemberExpression { + object, + property: MemberProperty::Literal(PropertyLiteral::String(String::new())), + value: InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }, + }) + } + // TS casts are transparent for an assignment/update target: `(obj.x as T)` + // behaves like `obj.x`. The original stripped these before lowering. + oxc::SimpleAssignmentTarget::TSAsExpression(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + oxc::SimpleAssignmentTarget::TSSatisfiesExpression(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + oxc::SimpleAssignmentTarget::TSNonNullExpression(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + oxc::SimpleAssignmentTarget::TSTypeAssertion(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + _ => { + unreachable!("lower_member_expression_from_simple_target called on a non-member target") + } + } +} + +/// Lower a member expression that appears (behind transparent TS casts) as the +/// argument of an `UpdateExpression`, e.g. `(obj.x as T)++`. Mirrors +/// `lower_member_expression_from_simple_target` for the `Expression` form the casts +/// expose. Only member variants are reachable (callers guard with +/// `simple_target_is_member_like`). +fn lower_member_expression_from_expr<'a>( + builder: &mut HirBuilder<'a, '_>, + expr: &'a oxc::Expression<'a>, +) -> Result, CompilerError> { + match expr { + oxc::Expression::StaticMemberExpression(m) => { + let loc = builder.source_location(m.span); + let object = lower_expression_to_temporary(builder, &m.object)?; + let prop_literal = PropertyLiteral::String(m.property.name.to_string()); + let value = InstructionValue::PropertyLoad { + object: object.clone(), + property: prop_literal.clone(), + loc, + }; + Ok(LoweredMemberExpression { + object, + property: MemberProperty::Literal(prop_literal), + value, + }) + } + oxc::Expression::ComputedMemberExpression(m) => { + let loc = builder.source_location(m.span); + let object = lower_expression_to_temporary(builder, &m.object)?; + if let oxc::Expression::NumericLiteral(lit) = &m.expression { + let prop_literal = PropertyLiteral::Number(FloatValue::new(lit.value)); + let value = InstructionValue::PropertyLoad { + object: object.clone(), + property: prop_literal.clone(), + loc, + }; + return Ok(LoweredMemberExpression { + object, + property: MemberProperty::Literal(prop_literal), + value, + }); + } + let property = lower_expression_to_temporary(builder, &m.expression)?; + let value = InstructionValue::ComputedLoad { + object: object.clone(), + property: property.clone(), + loc, + }; + Ok(LoweredMemberExpression { + object, + property: MemberProperty::Computed(property), + value, + }) + } + oxc::Expression::TSAsExpression(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + oxc::Expression::TSSatisfiesExpression(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + oxc::Expression::TSNonNullExpression(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + oxc::Expression::TSTypeAssertion(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + oxc::Expression::ParenthesizedExpression(e) => { + lower_member_expression_from_expr(builder, &e.expression) + } + _ => unreachable!("lower_member_expression_from_expr called on a non-member expression"), + } +} + +/// True if `target` is a member expression, possibly behind transparent TS casts +/// (`(obj.x as T)`), so an `UpdateExpression` can treat it as a plain member update. +fn simple_target_is_member_like(target: &oxc::SimpleAssignmentTarget) -> bool { + match target { + oxc::SimpleAssignmentTarget::StaticMemberExpression(_) + | oxc::SimpleAssignmentTarget::ComputedMemberExpression(_) + | oxc::SimpleAssignmentTarget::PrivateFieldExpression(_) => true, + oxc::SimpleAssignmentTarget::TSAsExpression(e) => expr_is_member_like(&e.expression), + oxc::SimpleAssignmentTarget::TSSatisfiesExpression(e) => expr_is_member_like(&e.expression), + oxc::SimpleAssignmentTarget::TSNonNullExpression(e) => expr_is_member_like(&e.expression), + oxc::SimpleAssignmentTarget::TSTypeAssertion(e) => expr_is_member_like(&e.expression), + _ => false, + } +} + +fn expr_is_member_like(expr: &oxc::Expression) -> bool { + match expr { + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) => true, + oxc::Expression::TSAsExpression(e) => expr_is_member_like(&e.expression), + oxc::Expression::TSSatisfiesExpression(e) => expr_is_member_like(&e.expression), + oxc::Expression::TSNonNullExpression(e) => expr_is_member_like(&e.expression), + oxc::Expression::TSTypeAssertion(e) => expr_is_member_like(&e.expression), + oxc::Expression::ParenthesizedExpression(e) => expr_is_member_like(&e.expression), + _ => false, + } +} + +fn lower_arguments<'a>( + builder: &mut HirBuilder<'a, '_>, + args: &'a [oxc::Argument<'a>], +) -> Result, CompilerError> { + let mut result = Vec::new(); + for arg in args { + match arg { + oxc::Argument::SpreadElement(spread) => { + let place = lower_expression_to_temporary(builder, &spread.argument)?; + result.push(PlaceOrSpread::Spread(SpreadPattern { place })); + } + _ => { + let expr = arg.as_expression().expect("non-spread argument is an expression"); + let place = lower_expression_to_temporary(builder, expr)?; + result.push(PlaceOrSpread::Place(place)); + } + } + } + Ok(result) +} + +/// Result of resolving an identifier for assignment. +enum IdentifierForAssignment { + Place(Place), + Global { name: String }, +} + +/// Resolve an identifier as an assignment target. AST-agnostic. Returns None if +/// the binding could not be found (error recorded). +fn lower_identifier_for_assignment( + builder: &mut HirBuilder<'_, '_>, + loc: Option, + ident_loc: Option, + kind: InstructionKind, + name: &str, + start: u32, + node_id: Option, +) -> Result, CompilerError> { + let mut binding = builder.resolve_identifier(name, start, ident_loc.clone(), node_id)?; + if !matches!(binding, VariableBinding::Identifier { .. }) && kind != InstructionKind::Reassign { + if let Some((binding_id, binding_data)) = + builder.scope_info().find_binding_id_in_descendants(name, builder.function_scope()) + { + let bk = crate::react_compiler_lowering::convert_binding_kind(&binding_data.kind); + let identifier = + builder.resolve_binding_with_loc(name, binding_id, ident_loc.clone())?; + binding = VariableBinding::Identifier { identifier, binding_kind: bk }; + } + } + match binding { + VariableBinding::Identifier { identifier, binding_kind, .. } => { + if kind != InstructionKind::Reassign { + builder.set_identifier_declaration_loc(identifier, &ident_loc); + } + if binding_kind == BindingKind::Const && kind == InstructionKind::Reassign { + builder.record_error(CompilerErrorDetail { + reason: "Cannot reassign a `const` variable".to_string(), + category: ErrorCategory::Syntax, + loc: loc.clone(), + description: Some(format!("`{}` is declared as const", name)), + suggestions: None, + })?; + return Ok(None); + } + Ok(Some(IdentifierForAssignment::Place(Place { + identifier, + effect: Effect::Unknown, + reactive: false, + loc, + }))) + } + VariableBinding::Global { name: gname } => { + if kind == InstructionKind::Reassign { + Ok(Some(IdentifierForAssignment::Global { name: gname })) + } else { + builder.record_error(CompilerErrorDetail { + reason: "Could not find binding for declaration".to_string(), + category: ErrorCategory::Invariant, + loc, + description: None, + suggestions: None, + })?; + Ok(None) + } + } + _ => { + if kind == InstructionKind::Reassign { + Ok(Some(IdentifierForAssignment::Global { name: name.to_string() })) + } else { + builder.record_error(CompilerErrorDetail { + reason: "Could not find binding for declaration".to_string(), + category: ErrorCategory::Invariant, + loc, + description: None, + suggestions: None, + })?; + Ok(None) + } + } + } +} + +/// The style of assignment (used internally by the lower-assignment helpers). +/// Mirrors the original `AssignmentStyle`. +#[derive(Clone, Copy, PartialEq, Eq)] +enum AssignmentStyle { + /// Assignment via `=` + Assignment, + /// Destructuring assignment + Destructure, +} + +/// Assign `value` to a binding pattern (variable declaration / destructuring param). +/// Faithful translation of the original `lower_assignment` for the BindingPattern +/// targets (Identifier / Object / Array / Assignment). The original unified binding +/// patterns and assignment targets under `PatternLike`; oxc splits them, so this +/// handles only the binding side. `kind` is never `Reassign` on this path, so +/// `force_temporaries` is always false. +fn lower_binding_assignment<'a>( + builder: &mut HirBuilder<'a, '_>, + loc: Option, + kind: InstructionKind, + target: &'a oxc::BindingPattern<'a>, + value: Place, + assignment_style: AssignmentStyle, +) -> Result, CompilerError> { + match target { + oxc::BindingPattern::BindingIdentifier(id) => { + let start = id.span.start; + let id_loc = builder.source_location(id.span); + let result = lower_identifier_for_assignment( + builder, + loc.clone(), + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )?; + match result { + None => Ok(None), + Some(IdentifierForAssignment::Global { name }) => { + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreGlobal { name, value, loc }, + )?; + Ok(Some(temp)) + } + Some(IdentifierForAssignment::Place(place)) => { + if builder.is_context_identifier(id.name.as_str(), start, Some(start)) { + let is_hoisted = builder + .scope_info() + .resolve_reference_for_node(Some(start)) + .map(|b| builder.environment().is_hoisted_identifier(b.id.0)) + .unwrap_or(false); + if kind == InstructionKind::Const && !is_hoisted { + builder.record_error(CompilerErrorDetail { + reason: "Expected `const` declaration not to be reassigned" + .to_string(), + category: ErrorCategory::Syntax, + loc: loc.clone(), + suggestions: None, + description: None, + })?; + } + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreContext { + lvalue: LValue { place, kind }, + value, + loc, + }, + )?; + Ok(Some(temp)) + } else { + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { place, kind }, + value, + type_annotation: None, + loc, + }, + )?; + Ok(Some(temp)) + } + } + } + } + oxc::BindingPattern::ArrayPattern(pattern) => { + let mut items: Vec = Vec::new(); + let mut followups: Vec<(Place, &oxc::BindingPattern)> = Vec::new(); + + for element in &pattern.elements { + match element { + None => { + items.push(ArrayPatternElement::Hole); + } + Some(oxc::BindingPattern::BindingIdentifier(id)) => { + let start = id.span.start; + let is_context = + builder.is_context_identifier(id.name.as_str(), start, Some(start)); + // force_temporaries is always false on the binding path. + let can_use_direct = + matches!(assignment_style, AssignmentStyle::Assignment) || !is_context; + if can_use_direct { + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + id_loc.clone(), + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + items.push(ArrayPatternElement::Place(place)); + } + Some(IdentifierForAssignment::Global { .. }) => { + let temp = build_temporary_place( + builder, + builder.source_location(id.span), + ); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Place(temp.clone())); + followups.push((temp, element.as_ref().unwrap())); + } + None => { + items.push(ArrayPatternElement::Hole); + } + } + } else { + let temp = + build_temporary_place(builder, builder.source_location(id.span)); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Place(temp.clone())); + followups.push((temp, element.as_ref().unwrap())); + } + } + Some(other) => { + let elem_loc = builder.source_location(other.span()); + let temp = build_temporary_place(builder, elem_loc); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Place(temp.clone())); + followups.push((temp, other)); + } + } + } + + if let Some(rest) = &pattern.rest { + match &rest.argument { + oxc::BindingPattern::BindingIdentifier(id) => { + let start = id.span.start; + let is_context = + builder.is_context_identifier(id.name.as_str(), start, Some(start)); + let can_use_direct = + matches!(assignment_style, AssignmentStyle::Assignment) || !is_context; + if can_use_direct { + let rest_loc = builder.source_location(rest.span); + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + rest_loc, + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + items + .push(ArrayPatternElement::Spread(SpreadPattern { place })); + } + Some(IdentifierForAssignment::Global { .. }) => { + let temp = build_temporary_place( + builder, + builder.source_location(rest.span), + ); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Spread(SpreadPattern { + place: temp.clone(), + })); + followups.push((temp, &rest.argument)); + } + None => {} + } + } else { + let temp = + build_temporary_place(builder, builder.source_location(rest.span)); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Spread(SpreadPattern { + place: temp.clone(), + })); + followups.push((temp, &rest.argument)); + } + } + _ => { + let temp = + build_temporary_place(builder, builder.source_location(rest.span)); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Spread(SpreadPattern { + place: temp.clone(), + })); + followups.push((temp, &rest.argument)); + } + } + } + + let pattern_loc = builder.source_location(pattern.span); + let temporary = lower_value_to_temporary( + builder, + InstructionValue::Destructure { + lvalue: LValuePattern { + pattern: Pattern::Array(ArrayPattern { items, loc: pattern_loc }), + kind, + }, + value: value.clone(), + loc: loc.clone(), + }, + )?; + + for (place, path) in followups { + let followup_loc = builder.source_location(path.span()).or(loc.clone()); + lower_binding_assignment( + builder, + followup_loc, + kind, + path, + place, + assignment_style, + )?; + } + Ok(Some(temporary)) + } + oxc::BindingPattern::ObjectPattern(pattern) => { + let mut properties: Vec = Vec::new(); + let mut followups: Vec<(Place, &oxc::BindingPattern)> = Vec::new(); + + for prop in &pattern.properties { + if prop.computed { + builder.record_error(CompilerErrorDetail { + reason: "(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern".to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(prop.span), + description: None, + suggestions: None, + })?; + continue; + } + + let key = match lower_object_property_key(builder, &prop.key, false)? { + Some(k) => k, + None => continue, + }; + + match &prop.value { + oxc::BindingPattern::BindingIdentifier(id) => { + let start = id.span.start; + let is_context = + builder.is_context_identifier(id.name.as_str(), start, Some(start)); + let can_use_direct = + matches!(assignment_style, AssignmentStyle::Assignment) || !is_context; + if can_use_direct { + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + id_loc.clone(), + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + properties.push(ObjectPropertyOrSpread::Property( + ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place, + }, + )); + } + Some(IdentifierForAssignment::Global { .. }) => { + builder.record_error(CompilerErrorDetail { + reason: "Expected reassignment of globals to enable forceTemporaries".to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(id.span), + description: None, + suggestions: None, + })?; + } + None => { + continue; + } + } + } else { + let temp = + build_temporary_place(builder, builder.source_location(id.span)); + promote_temporary(builder, temp.identifier); + properties.push(ObjectPropertyOrSpread::Property(ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place: temp.clone(), + })); + followups.push((temp, &prop.value)); + } + } + other => { + let elem_loc = builder.source_location(other.span()); + let temp = build_temporary_place(builder, elem_loc); + promote_temporary(builder, temp.identifier); + properties.push(ObjectPropertyOrSpread::Property(ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place: temp.clone(), + })); + followups.push((temp, other)); + } + } + } + + if let Some(rest) = &pattern.rest { + match &rest.argument { + oxc::BindingPattern::BindingIdentifier(id) => { + let start = id.span.start; + let is_context = + builder.is_context_identifier(id.name.as_str(), start, Some(start)); + let can_use_direct = + matches!(assignment_style, AssignmentStyle::Assignment) || !is_context; + if can_use_direct { + let rest_loc = builder.source_location(rest.span); + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + rest_loc, + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + properties.push(ObjectPropertyOrSpread::Spread( + SpreadPattern { place }, + )); + } + Some(IdentifierForAssignment::Global { .. }) => { + builder.record_error(CompilerErrorDetail { + reason: "Expected reassignment of globals to enable forceTemporaries".to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(rest.span), + description: None, + suggestions: None, + })?; + } + None => {} + } + } else { + let temp = + build_temporary_place(builder, builder.source_location(rest.span)); + promote_temporary(builder, temp.identifier); + properties.push(ObjectPropertyOrSpread::Spread(SpreadPattern { + place: temp.clone(), + })); + followups.push((temp, &rest.argument)); + } + } + other => { + builder.record_error(CompilerErrorDetail { + reason: format!( + "(BuildHIR::lowerAssignment) Handle {} rest element in ObjectPattern", + match other { + oxc::BindingPattern::ObjectPattern(_) => "ObjectPattern", + oxc::BindingPattern::ArrayPattern(_) => "ArrayPattern", + oxc::BindingPattern::AssignmentPattern(_) => "AssignmentPattern", + _ => "unknown", + } + ), + category: ErrorCategory::Todo, + loc: builder.source_location(rest.span), + description: None, + suggestions: None, + })?; + } + } + } + + let pattern_loc = builder.source_location(pattern.span); + let temporary = lower_value_to_temporary( + builder, + InstructionValue::Destructure { + lvalue: LValuePattern { + pattern: Pattern::Object(ObjectPattern { properties, loc: pattern_loc }), + kind, + }, + value: value.clone(), + loc: loc.clone(), + }, + )?; + + for (place, path) in followups { + let followup_loc = builder.source_location(path.span()).or(loc.clone()); + lower_binding_assignment( + builder, + followup_loc, + kind, + path, + place, + assignment_style, + )?; + } + Ok(Some(temporary)) + } + oxc::BindingPattern::AssignmentPattern(pattern) => { + // Default value: if value === undefined, use default, else use value. + let pat_loc = builder.source_location(pattern.span); + let temp = lower_default_to_temp(builder, pat_loc.clone(), &pattern.right, value)?; + // Recursively assign the resolved value to the left pattern. + lower_binding_assignment(builder, pat_loc, kind, &pattern.left, temp, assignment_style) + } + } +} + +/// Lower the default-value ternary `value === undefined ? default : value` into a +/// fresh temporary and return it. Shared by the `AssignmentPattern` arm and by the +/// default-parameter (`FormalParameter.initializer`) lowering, which in Babel was a +/// single `AssignmentPattern` param node. +fn lower_default_to_temp<'a>( + builder: &mut HirBuilder<'a, '_>, + pat_loc: Option, + default: &'a oxc::Expression<'a>, + value: Place, +) -> Result { + let temp = build_temporary_place(builder, pat_loc.clone()); + + let test_block = builder.reserve(BlockKind::Value); + let continuation_block = builder.reserve(builder.current_block_kind()); + let continuation_id = continuation_block.id; + + let temp_consequent = temp.clone(); + let pat_loc_consequent = pat_loc.clone(); + let consequent = builder.try_enter(BlockKind::Value, |builder, _| { + let default_value = lower_reorderable_expression(builder, default)?; + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { place: temp_consequent.clone(), kind: InstructionKind::Const }, + value: default_value, + type_annotation: None, + loc: pat_loc_consequent.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: pat_loc_consequent.clone(), + }) + }); + + let temp_alternate = temp.clone(); + let pat_loc_alternate = pat_loc.clone(); + let value_alternate = value.clone(); + let alternate = builder.try_enter(BlockKind::Value, |builder, _| { + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { place: temp_alternate.clone(), kind: InstructionKind::Const }, + value: value_alternate.clone(), + type_annotation: None, + loc: pat_loc_alternate.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: pat_loc_alternate.clone(), + }) + }); + + builder.terminate_with_continuation( + Terminal::Ternary { + test: test_block.id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: pat_loc.clone(), + }, + test_block, + ); + + let undef = lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc: pat_loc.clone() }, + )?; + let test = lower_value_to_temporary( + builder, + InstructionValue::BinaryExpression { + left: value, + operator: BinaryOperator::StrictEqual, + right: undef, + loc: pat_loc.clone(), + }, + )?; + builder.terminate_with_continuation( + Terminal::Branch { + test, + consequent: consequent?, + alternate: alternate?, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: pat_loc, + }, + continuation_block, + ); + + Ok(temp) +} + +/// Resolve a member-expression assignment target (oxc's member variants of +/// `SimpleAssignmentTarget`) and store `value` into it, returning the store +/// temporary. Mirrors the `PatternLike::MemberExpression` arm of the original +/// `lower_assignment`. +fn lower_member_assignment_target<'a>( + builder: &mut HirBuilder<'a, '_>, + loc: Option, + kind: InstructionKind, + target: &'a oxc::SimpleAssignmentTarget<'a>, + value: Place, +) -> Result, CompilerError> { + // MemberExpression may only appear in an assignment expression (Reassign). + if kind != InstructionKind::Reassign { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Invariant, + reason: "MemberExpression may only appear in an assignment expression".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + return Ok(None); + } + match target { + oxc::SimpleAssignmentTarget::StaticMemberExpression(member) => { + let object = lower_expression_to_temporary(builder, &member.object)?; + let temp = lower_value_to_temporary( + builder, + InstructionValue::PropertyStore { + object, + property: PropertyLiteral::String(member.property.name.to_string()), + value, + loc, + }, + )?; + Ok(Some(temp)) + } + oxc::SimpleAssignmentTarget::ComputedMemberExpression(member) => { + let object = lower_expression_to_temporary(builder, &member.object)?; + // A numeric computed index is treated as a PropertyStore (matches the + // original `member.computed && NumericLiteral` branch). + if let oxc::Expression::NumericLiteral(num) = &member.expression { + let temp = lower_value_to_temporary( + builder, + InstructionValue::PropertyStore { + object, + property: PropertyLiteral::Number(FloatValue::new(num.value)), + value, + loc, + }, + )?; + return Ok(Some(temp)); + } + let property_place = lower_expression_to_temporary(builder, &member.expression)?; + let temp = lower_value_to_temporary( + builder, + InstructionValue::ComputedStore { object, property: property_place, value, loc }, + )?; + Ok(Some(temp)) + } + oxc::SimpleAssignmentTarget::PrivateFieldExpression(member) => { + // Babel modeled `a.#b = v` as a non-computed MemberExpression with a + // PrivateName property; the original `lower_assignment` member arm hit + // the generic property `_` branch and bailed with this Todo. + let object = lower_expression_to_temporary(builder, &member.object)?; + let _ = object; + builder.record_error(CompilerErrorDetail { + reason: + "(BuildHIR::lowerAssignment) Handle PrivateName properties in MemberExpression" + .to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(member.field.span), + description: None, + suggestions: None, + })?; + let temp = lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }, + )?; + Ok(Some(temp)) + } + _ => unreachable!("lower_member_assignment_target called on a non-member target"), + } +} + +/// True if `maybe` is a bare identifier assignment target that resolves to a local +/// binding (used to compute `force_temporaries`). +fn assignment_target_is_local_identifier( + builder: &mut HirBuilder<'_, '_>, + maybe: &oxc::AssignmentTargetMaybeDefault, +) -> Result { + match maybe { + oxc::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(id) => { + let start = id.span.start; + if builder.is_context_identifier(id.name.as_str(), start, Some(start)) { + return Ok(false); + } + let ident_loc = builder.source_location(id.span); + match builder.resolve_identifier(id.name.as_str(), start, ident_loc, Some(start))? { + VariableBinding::Identifier { .. } => Ok(true), + _ => Ok(false), + } + } + _ => Ok(false), + } +} + +/// Assign `value` to an assignment-expression target (`x`, `a.b`, `[a, b]`, +/// `{a, b}`). Faithful translation of the original `lower_assignment` for the +/// `PatternLike` cases that came from `AssignmentExpression.left` / destructuring +/// targets; oxc models these as `AssignmentTarget`. +fn lower_assignment_target<'a>( + builder: &mut HirBuilder<'a, '_>, + loc: Option, + kind: InstructionKind, + target: &'a oxc::AssignmentTarget<'a>, + value: Place, + assignment_style: AssignmentStyle, +) -> Result, CompilerError> { + match target { + oxc::AssignmentTarget::AssignmentTargetIdentifier(id) => { + let start = id.span.start; + let id_loc = builder.source_location(id.span); + let result = lower_identifier_for_assignment( + builder, + loc.clone(), + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )?; + match result { + None => Ok(None), + Some(IdentifierForAssignment::Global { name }) => { + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreGlobal { name, value, loc }, + )?; + Ok(Some(temp)) + } + Some(IdentifierForAssignment::Place(place)) => { + if builder.is_context_identifier(id.name.as_str(), start, Some(start)) { + let is_hoisted = builder + .scope_info() + .resolve_reference_for_node(Some(start)) + .map(|b| builder.environment().is_hoisted_identifier(b.id.0)) + .unwrap_or(false); + if kind == InstructionKind::Const && !is_hoisted { + builder.record_error(CompilerErrorDetail { + reason: "Expected `const` declaration not to be reassigned" + .to_string(), + category: ErrorCategory::Syntax, + loc: loc.clone(), + suggestions: None, + description: None, + })?; + } + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreContext { + lvalue: LValue { place, kind }, + value, + loc, + }, + )?; + Ok(Some(temp)) + } else { + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { place, kind }, + value, + type_annotation: None, + loc, + }, + )?; + Ok(Some(temp)) + } + } + } + } + oxc::AssignmentTarget::StaticMemberExpression(_) + | oxc::AssignmentTarget::ComputedMemberExpression(_) + | oxc::AssignmentTarget::PrivateFieldExpression(_) => { + let simple = target.as_simple_assignment_target().unwrap(); + lower_member_assignment_target(builder, loc, kind, simple, value) + } + oxc::AssignmentTarget::ArrayAssignmentTarget(pattern) => { + let mut items: Vec = Vec::new(); + let mut followups: Vec<(Place, FollowupTarget)> = Vec::new(); + + // force_temporaries: when kind is Reassign and any element is + // non-identifier, a context variable, or a non-local binding. + let force_temporaries = if kind == InstructionKind::Reassign { + let mut found = false; + if pattern.rest.is_some() { + found = true; + } + if !found { + for elem in &pattern.elements { + match elem { + Some(maybe) => { + if !assignment_target_is_local_identifier(builder, maybe)? { + found = true; + break; + } + } + None => { + found = true; + break; + } + } + } + } + found + } else { + false + }; + + for element in &pattern.elements { + match element { + None => { + items.push(ArrayPatternElement::Hole); + } + Some(oxc::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(id)) => { + let start = id.span.start; + let is_context = + builder.is_context_identifier(id.name.as_str(), start, Some(start)); + let can_use_direct = !force_temporaries + && (matches!(assignment_style, AssignmentStyle::Assignment) + || !is_context); + if can_use_direct { + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + id_loc.clone(), + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + items.push(ArrayPatternElement::Place(place)); + } + Some(IdentifierForAssignment::Global { .. }) => { + let temp = build_temporary_place( + builder, + builder.source_location(id.span), + ); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Place(temp.clone())); + followups.push(( + temp, + FollowupTarget::MaybeDefault(element.as_ref().unwrap()), + )); + } + None => { + items.push(ArrayPatternElement::Hole); + } + } + } else { + let temp = + build_temporary_place(builder, builder.source_location(id.span)); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Place(temp.clone())); + followups.push(( + temp, + FollowupTarget::MaybeDefault(element.as_ref().unwrap()), + )); + } + } + Some(other) => { + let elem_loc = builder.source_location(other.span()); + let temp = build_temporary_place(builder, elem_loc); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Place(temp.clone())); + followups.push((temp, FollowupTarget::MaybeDefault(other))); + } + } + } + + if let Some(rest) = &pattern.rest { + match &rest.target { + oxc::AssignmentTarget::AssignmentTargetIdentifier(id) => { + let start = id.span.start; + let is_context = + builder.is_context_identifier(id.name.as_str(), start, Some(start)); + let can_use_direct = !force_temporaries + && (matches!(assignment_style, AssignmentStyle::Assignment) + || !is_context); + if can_use_direct { + let rest_loc = builder.source_location(rest.span); + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + rest_loc, + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + items + .push(ArrayPatternElement::Spread(SpreadPattern { place })); + } + Some(IdentifierForAssignment::Global { .. }) => { + let temp = build_temporary_place( + builder, + builder.source_location(rest.span), + ); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Spread(SpreadPattern { + place: temp.clone(), + })); + followups.push((temp, FollowupTarget::Target(&rest.target))); + } + None => {} + } + } else { + let temp = + build_temporary_place(builder, builder.source_location(rest.span)); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Spread(SpreadPattern { + place: temp.clone(), + })); + followups.push((temp, FollowupTarget::Target(&rest.target))); + } + } + _ => { + let temp = + build_temporary_place(builder, builder.source_location(rest.span)); + promote_temporary(builder, temp.identifier); + items.push(ArrayPatternElement::Spread(SpreadPattern { + place: temp.clone(), + })); + followups.push((temp, FollowupTarget::Target(&rest.target))); + } + } + } + + let pattern_loc = builder.source_location(pattern.span); + let temporary = lower_value_to_temporary( + builder, + InstructionValue::Destructure { + lvalue: LValuePattern { + pattern: Pattern::Array(ArrayPattern { items, loc: pattern_loc }), + kind, + }, + value: value.clone(), + loc: loc.clone(), + }, + )?; + + for (place, path) in followups { + lower_followup_target(builder, loc.clone(), kind, path, place, assignment_style)?; + } + Ok(Some(temporary)) + } + oxc::AssignmentTarget::ObjectAssignmentTarget(pattern) => { + let mut properties: Vec = Vec::new(); + let mut followups: Vec<(Place, FollowupTarget)> = Vec::new(); + + let force_temporaries = if kind == InstructionKind::Reassign { + let mut found = false; + if pattern.rest.is_some() { + found = true; + } + if !found { + for prop in &pattern.properties { + match prop { + oxc::AssignmentTargetProperty::AssignmentTargetPropertyIdentifier( + p, + ) => { + // `{foo}` (init None) is a bare identifier; `{foo = d}` + // (init Some) is an AssignmentPattern in Babel terms. + if p.init.is_some() { + found = true; + break; + } + let start = p.binding.span.start; + let ident_loc = builder.source_location(p.binding.span); + match builder.resolve_identifier( + p.binding.name.as_str(), + start, + ident_loc, + Some(start), + )? { + VariableBinding::Identifier { .. } => {} + _ => { + found = true; + break; + } + } + } + oxc::AssignmentTargetProperty::AssignmentTargetPropertyProperty(p) => { + if !assignment_target_is_local_identifier(builder, &p.binding)? { + found = true; + break; + } + } + } + } + } + found + } else { + false + }; + + for prop in &pattern.properties { + match prop { + oxc::AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(p) => { + let key = + ObjectPropertyKey::Identifier { name: p.binding.name.to_string() }; + let id = &p.binding; + let start = id.span.start; + if let Some(default) = &p.init { + // `{foo = d}` — Babel shorthand AssignmentPattern. Lower + // via a promoted temporary + default followup. + let elem_loc = builder.source_location(p.span); + let temp = build_temporary_place(builder, elem_loc); + promote_temporary(builder, temp.identifier); + properties.push(ObjectPropertyOrSpread::Property(ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place: temp.clone(), + })); + followups.push(( + temp, + FollowupTarget::Default { + span: p.span, + default, + binding: FollowupBinding::Identifier(id), + }, + )); + continue; + } + let is_context = + builder.is_context_identifier(id.name.as_str(), start, Some(start)); + let can_use_direct = !force_temporaries + && (matches!(assignment_style, AssignmentStyle::Assignment) + || !is_context); + if can_use_direct { + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + id_loc.clone(), + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + properties.push(ObjectPropertyOrSpread::Property( + ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place, + }, + )); + } + Some(IdentifierForAssignment::Global { .. }) => { + builder.record_error(CompilerErrorDetail { + reason: "Expected reassignment of globals to enable forceTemporaries".to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(id.span), + description: None, + suggestions: None, + })?; + } + None => { + continue; + } + } + } else { + let temp = + build_temporary_place(builder, builder.source_location(id.span)); + promote_temporary(builder, temp.identifier); + properties.push(ObjectPropertyOrSpread::Property(ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place: temp.clone(), + })); + followups.push((temp, FollowupTarget::Identifier(id))); + } + } + oxc::AssignmentTargetProperty::AssignmentTargetPropertyProperty(p) => { + if p.computed { + builder.record_error(CompilerErrorDetail { + reason: "(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern".to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(p.span), + description: None, + suggestions: None, + })?; + continue; + } + let key = match lower_object_property_key(builder, &p.name, false)? { + Some(k) => k, + None => continue, + }; + match &p.binding { + oxc::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(id) => { + let start = id.span.start; + let is_context = builder.is_context_identifier( + id.name.as_str(), + start, + Some(start), + ); + let can_use_direct = !force_temporaries + && (matches!(assignment_style, AssignmentStyle::Assignment) + || !is_context); + if can_use_direct { + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + id_loc.clone(), + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + properties.push(ObjectPropertyOrSpread::Property( + ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place, + }, + )); + } + Some(IdentifierForAssignment::Global { .. }) => { + builder.record_error(CompilerErrorDetail { + reason: "Expected reassignment of globals to enable forceTemporaries".to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(id.span), + description: None, + suggestions: None, + })?; + } + None => { + continue; + } + } + } else { + let temp = build_temporary_place( + builder, + builder.source_location(id.span), + ); + promote_temporary(builder, temp.identifier); + properties.push(ObjectPropertyOrSpread::Property( + ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place: temp.clone(), + }, + )); + followups + .push((temp, FollowupTarget::MaybeDefault(&p.binding))); + } + } + other => { + let elem_loc = builder.source_location(other.span()); + let temp = build_temporary_place(builder, elem_loc); + promote_temporary(builder, temp.identifier); + properties.push(ObjectPropertyOrSpread::Property(ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place: temp.clone(), + })); + followups.push((temp, FollowupTarget::MaybeDefault(other))); + } + } + } + } + } + + if let Some(rest) = &pattern.rest { + match &rest.target { + oxc::AssignmentTarget::AssignmentTargetIdentifier(id) => { + let start = id.span.start; + let is_context = + builder.is_context_identifier(id.name.as_str(), start, Some(start)); + let can_use_direct = !force_temporaries + && (matches!(assignment_style, AssignmentStyle::Assignment) + || !is_context); + if can_use_direct { + let rest_loc = builder.source_location(rest.span); + let id_loc = builder.source_location(id.span); + match lower_identifier_for_assignment( + builder, + rest_loc, + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )? { + Some(IdentifierForAssignment::Place(place)) => { + properties.push(ObjectPropertyOrSpread::Spread( + SpreadPattern { place }, + )); + } + Some(IdentifierForAssignment::Global { .. }) => { + builder.record_error(CompilerErrorDetail { + reason: "Expected reassignment of globals to enable forceTemporaries".to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(rest.span), + description: None, + suggestions: None, + })?; + } + None => {} + } + } else { + let temp = + build_temporary_place(builder, builder.source_location(rest.span)); + promote_temporary(builder, temp.identifier); + properties.push(ObjectPropertyOrSpread::Spread(SpreadPattern { + place: temp.clone(), + })); + followups.push((temp, FollowupTarget::Target(&rest.target))); + } + } + other => { + builder.record_error(CompilerErrorDetail { + reason: format!( + "(BuildHIR::lowerAssignment) Handle {} rest element in ObjectPattern", + match other { + oxc::AssignmentTarget::ObjectAssignmentTarget(_) => { + "ObjectPattern" + } + oxc::AssignmentTarget::ArrayAssignmentTarget(_) => "ArrayPattern", + oxc::AssignmentTarget::StaticMemberExpression(_) + | oxc::AssignmentTarget::ComputedMemberExpression(_) + | oxc::AssignmentTarget::PrivateFieldExpression(_) => { + "MemberExpression" + } + _ => "unknown", + } + ), + category: ErrorCategory::Todo, + loc: builder.source_location(rest.span), + description: None, + suggestions: None, + })?; + } + } + } + + let pattern_loc = builder.source_location(pattern.span); + let temporary = lower_value_to_temporary( + builder, + InstructionValue::Destructure { + lvalue: LValuePattern { + pattern: Pattern::Object(ObjectPattern { properties, loc: pattern_loc }), + kind, + }, + value: value.clone(), + loc: loc.clone(), + }, + )?; + + for (place, path) in followups { + lower_followup_target(builder, loc.clone(), kind, path, place, assignment_style)?; + } + Ok(Some(temporary)) + } + // TS assignment-target wrappers (e.g. `(x as T) = ...`). The original + // recorded the TS-faithful Todo once in find_context_identifiers and + // returned None here. + oxc::AssignmentTarget::TSAsExpression(_) + | oxc::AssignmentTarget::TSSatisfiesExpression(_) + | oxc::AssignmentTarget::TSNonNullExpression(_) + | oxc::AssignmentTarget::TSTypeAssertion(_) => Ok(None), + } +} + +/// A destructuring followup target: either a nested assignment target, a +/// with-default wrapper element, or (for `{foo = d}` object shorthand) an +/// identifier with a default expression. +enum FollowupTarget<'a> { + Target(&'a oxc::AssignmentTarget<'a>), + MaybeDefault(&'a oxc::AssignmentTargetMaybeDefault<'a>), + /// A bare `{foo}` shorthand object property binding that needs a promoted + /// temporary followup (the Babel `obj_prop.value == Identifier` case). + Identifier(&'a oxc::IdentifierReference<'a>), + Default { + span: oxc_span::Span, + default: &'a oxc::Expression<'a>, + binding: FollowupBinding<'a>, + }, +} + +enum FollowupBinding<'a> { + Identifier(&'a oxc::IdentifierReference<'a>), + Target(&'a oxc::AssignmentTarget<'a>), +} + +/// Store `value` into the identifier-target `id` (a bare destructuring binding). +/// Mirrors the `PatternLike::Identifier` followup path of the original +/// `lower_assignment` (re-resolving the binding for the store). +fn lower_identifier_followup_store( + builder: &mut HirBuilder<'_, '_>, + loc: Option, + kind: InstructionKind, + id: &oxc::IdentifierReference, + value: Place, +) -> Result, CompilerError> { + let start = id.span.start; + let id_loc = builder.source_location(id.span); + let result = lower_identifier_for_assignment( + builder, + loc.clone(), + id_loc, + kind, + id.name.as_str(), + start, + Some(start), + )?; + match result { + None => Ok(None), + Some(IdentifierForAssignment::Global { name }) => { + let t = lower_value_to_temporary( + builder, + InstructionValue::StoreGlobal { name, value, loc }, + )?; + Ok(Some(t)) + } + Some(IdentifierForAssignment::Place(place)) => { + if builder.is_context_identifier(id.name.as_str(), start, Some(start)) { + let t = lower_value_to_temporary( + builder, + InstructionValue::StoreContext { lvalue: LValue { place, kind }, value, loc }, + )?; + Ok(Some(t)) + } else { + let t = lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { place, kind }, + value, + type_annotation: None, + loc, + }, + )?; + Ok(Some(t)) + } + } + } +} + +/// Lower a single destructuring followup (the recursion step shared by +/// `lower_assignment_target`). +fn lower_followup_target<'a>( + builder: &mut HirBuilder<'a, '_>, + loc: Option, + kind: InstructionKind, + target: FollowupTarget<'a>, + value: Place, + assignment_style: AssignmentStyle, +) -> Result, CompilerError> { + match target { + FollowupTarget::Target(t) => { + let followup_loc = builder.source_location(t.span()).or(loc); + lower_assignment_target(builder, followup_loc, kind, t, value, assignment_style) + } + FollowupTarget::MaybeDefault(m) => { + lower_assignment_target_maybe_default(builder, loc, kind, m, value, assignment_style) + } + FollowupTarget::Identifier(id) => { + let followup_loc = builder.source_location(id.span).or(loc); + lower_identifier_followup_store(builder, followup_loc, kind, id, value) + } + FollowupTarget::Default { span, default, binding } => lower_assignment_target_default( + builder, + span, + kind, + default, + binding, + value, + assignment_style, + ), + } +} + +/// Lower an `AssignmentTargetMaybeDefault` (array element / object property +/// binding). The with-default wrapper (`[a = 1]`, `{a: b = 1}`) is the Babel +/// `AssignmentPattern` case. +fn lower_assignment_target_maybe_default<'a>( + builder: &mut HirBuilder<'a, '_>, + loc: Option, + kind: InstructionKind, + maybe: &'a oxc::AssignmentTargetMaybeDefault<'a>, + value: Place, + assignment_style: AssignmentStyle, +) -> Result, CompilerError> { + match maybe { + oxc::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(with_default) => { + lower_assignment_target_default( + builder, + with_default.span, + kind, + &with_default.init, + FollowupBinding::Target(&with_default.binding), + value, + assignment_style, + ) + } + _ => { + let target = maybe.as_assignment_target().unwrap(); + let followup_loc = builder.source_location(target.span()).or(loc); + lower_assignment_target(builder, followup_loc, kind, target, value, assignment_style) + } + } +} + +/// Lower a default-value assignment target (`AssignmentPattern`): if `value === +/// undefined`, use the default, else use `value`, then assign the result into the +/// inner binding. Faithful translation of the `PatternLike::AssignmentPattern` arm. +fn lower_assignment_target_default<'a>( + builder: &mut HirBuilder<'a, '_>, + span: oxc_span::Span, + kind: InstructionKind, + default: &'a oxc::Expression<'a>, + binding: FollowupBinding<'a>, + value: Place, + assignment_style: AssignmentStyle, +) -> Result, CompilerError> { + let pat_loc = builder.source_location(span); + + let temp = build_temporary_place(builder, pat_loc.clone()); + + let test_block = builder.reserve(BlockKind::Value); + let continuation_block = builder.reserve(builder.current_block_kind()); + let continuation_id = continuation_block.id; + + let temp_consequent = temp.clone(); + let pat_loc_consequent = pat_loc.clone(); + let consequent = builder.try_enter(BlockKind::Value, |builder, _| { + let default_value = lower_reorderable_expression(builder, default)?; + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { place: temp_consequent.clone(), kind: InstructionKind::Const }, + value: default_value, + type_annotation: None, + loc: pat_loc_consequent.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: pat_loc_consequent.clone(), + }) + }); + + let temp_alternate = temp.clone(); + let pat_loc_alternate = pat_loc.clone(); + let value_alternate = value.clone(); + let alternate = builder.try_enter(BlockKind::Value, |builder, _| { + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { place: temp_alternate.clone(), kind: InstructionKind::Const }, + value: value_alternate.clone(), + type_annotation: None, + loc: pat_loc_alternate.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: pat_loc_alternate.clone(), + }) + }); + + builder.terminate_with_continuation( + Terminal::Ternary { + test: test_block.id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: pat_loc.clone(), + }, + test_block, + ); + + let undef = lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc: pat_loc.clone() }, + )?; + let test = lower_value_to_temporary( + builder, + InstructionValue::BinaryExpression { + left: value, + operator: BinaryOperator::StrictEqual, + right: undef, + loc: pat_loc.clone(), + }, + )?; + builder.terminate_with_continuation( + Terminal::Branch { + test, + consequent: consequent?, + alternate: alternate?, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: pat_loc.clone(), + }, + continuation_block, + ); + + // Recursively assign the resolved value to the inner binding. + match binding { + FollowupBinding::Target(t) => { + lower_assignment_target(builder, pat_loc, kind, t, temp, assignment_style) + } + FollowupBinding::Identifier(id) => { + // `{foo = d}` shorthand: the binding is the identifier `foo` itself. + lower_identifier_followup_store(builder, pat_loc, kind, id, temp) + } + } +} + +/// True if any node in the receiver spine carries `optional == true`. +/// +/// Mirrors `convert_ast::expr_contains_optional`: in oxc a chain like `a?.b.c` is +/// a single `ChainExpression` where each member/call carries its own `.optional` +/// flag, so a node "is in optional context" iff itself or anything deeper in its +/// receiver spine is optional. This is the predicate Babel used to decide whether +/// a node became `Optional{Member,Call}Expression` (vs a plain member/call). +fn expr_contains_optional(expr: &oxc::Expression) -> bool { + match expr { + oxc::Expression::CallExpression(c) => c.optional || expr_contains_optional(&c.callee), + oxc::Expression::StaticMemberExpression(m) => { + m.optional || expr_contains_optional(&m.object) + } + oxc::Expression::ComputedMemberExpression(m) => { + m.optional || expr_contains_optional(&m.object) + } + oxc::Expression::PrivateFieldExpression(p) => { + p.optional || expr_contains_optional(&p.object) + } + _ => false, + } +} + +/// Lower an oxc `ChainExpression` (`a?.b?.c()` etc.). oxc represents the whole +/// optional chain as one node wrapping nested member/call nodes carrying per-node +/// `.optional` flags; Babel instead split each link into +/// `Optional{Member,Call}Expression`. This fuses `convert_chain_expression` with +/// the original `lower_optional_member_expression` / `lower_optional_call_expression` +/// dispatch, reproducing the same `Optional` terminal / `OptionalCall`-`OptionalLoad` +/// HIR structure. +fn lower_chain_expression<'a>( + builder: &mut HirBuilder<'a, '_>, + chain: &'a oxc::ChainExpression<'a>, +) -> Result, CompilerError> { + match &chain.expression { + oxc::ChainElement::CallExpression(call) => { + lower_optional_call_expression_impl(builder, call, None) + } + oxc::ChainElement::TSNonNullExpression(ts) => { + // `foo?.bar!` — the non-null assertion wraps a chain-context expression. + // The original lowered `TSNonNullExpression` by recursing into its inner + // expression (loc-transparent); preserve that, keeping chain awareness. + lower_chain_subexpr(builder, &ts.expression) + } + // The `@inherit MemberExpression` variants of `ChainElement`. + oxc::ChainElement::StaticMemberExpression(_) + | oxc::ChainElement::ComputedMemberExpression(_) + | oxc::ChainElement::PrivateFieldExpression(_) => { + let member = chain.expression.as_member_expression().unwrap(); + let place = lower_optional_member_expression_impl(builder, member, None)?.1; + Ok(InstructionValue::LoadLocal { loc: place.loc.clone(), place }) + } + } +} + +/// Lower an expression that appears as a callee/object inside a chain (or wrapped +/// by a chain-context `TSNonNullExpression`), as an `InstructionValue`. +/// +/// Faithful to the original: Babel's regular `lower_expression` routed +/// `OptionalMemberExpression`/`OptionalCallExpression` into the optional impls and +/// `TSNonNullExpression` by recursing into its inner expression, while everything +/// else went through normal lowering. The oxc equivalent uses `expr_contains_optional` +/// to detect optional-context member/call nodes. +fn lower_chain_subexpr<'a>( + builder: &mut HirBuilder<'a, '_>, + expr: &'a oxc::Expression<'a>, +) -> Result, CompilerError> { + match expr { + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) + if expr_contains_optional(expr) => + { + let member = expr.as_member_expression().unwrap(); + let place = lower_optional_member_expression_impl(builder, member, None)?.1; + Ok(InstructionValue::LoadLocal { loc: place.loc.clone(), place }) + } + oxc::Expression::CallExpression(call) if expr_contains_optional(expr) => { + lower_optional_call_expression_impl(builder, call, None) + } + oxc::Expression::TSNonNullExpression(ts) => lower_chain_subexpr(builder, &ts.expression), + _ => lower_expression(builder, expr), + } +} + +/// Returns `(object, value_place)`. The `value_place` holds the result temporary; +/// the top-level caller wraps it in `LoadLocal`. `member` is one of the three oxc +/// member variants. `parent_alternate` threads the shared null/undefined block so a +/// chain only creates one alternate at the first `?.`. +fn lower_optional_member_expression_impl<'a>( + builder: &mut HirBuilder<'a, '_>, + member: &'a oxc::MemberExpression<'a>, + parent_alternate: Option, +) -> Result<(Place, Place), CompilerError> { + let optional = member.optional(); + let loc = builder.source_location(member.span()); + let place = build_temporary_place(builder, loc.clone()); + let continuation_block = builder.reserve(builder.current_block_kind()); + let continuation_id = continuation_block.id; + let consequent = builder.reserve(BlockKind::Value); + + // Block to evaluate if the receiver is null/undefined — sets result to undefined. + // Only create an alternate when first entering an optional subtree. + let alternate = if let Some(parent_alt) = parent_alternate { + Ok(parent_alt) + } else { + builder.try_enter(BlockKind::Value, |builder, _block_id| { + let temp = lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc: loc.clone() }, + )?; + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: temp, + type_annotation: None, + loc: loc.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: loc.clone(), + }) + }) + }?; + + let object_expr = member.object(); + let mut object: Option = None; + let test_block = builder.try_enter(BlockKind::Value, |builder, _block_id| { + match object_expr { + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) + if expr_contains_optional(object_expr) => + { + let object_member = object_expr.as_member_expression().unwrap(); + let (_obj, value) = + lower_optional_member_expression_impl(builder, object_member, Some(alternate))?; + object = Some(value); + } + oxc::Expression::CallExpression(opt_call) if expr_contains_optional(object_expr) => { + let value = + lower_optional_call_expression_impl(builder, opt_call, Some(alternate))?; + let value_place = lower_value_to_temporary(builder, value)?; + object = Some(value_place); + } + other => { + object = Some(lower_expression_to_temporary(builder, other)?); + } + } + let test_place = object.as_ref().unwrap().clone(); + Ok(Terminal::Branch { + test: test_place, + consequent: consequent.id, + alternate, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }) + }); + + let obj = object.unwrap(); + + // Block to evaluate if the receiver is non-null/undefined. + builder.try_enter_reserved(consequent, |builder| { + let lowered = lower_member_expression_impl(builder, member, Some(obj.clone()))?; + let temp = lower_value_to_temporary(builder, lowered.value)?; + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: temp, + type_annotation: None, + loc: loc.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: loc.clone(), + }) + })?; + + builder.terminate_with_continuation( + Terminal::Optional { + optional, + test: test_block?, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + continuation_block, + ); + + Ok((obj, place)) +} + +/// Lower an oxc optional `CallExpression` (a call link inside a `ChainExpression`). +/// `parent_alternate` threads the shared null/undefined block. +fn lower_optional_call_expression_impl<'a>( + builder: &mut HirBuilder<'a, '_>, + call: &'a oxc::CallExpression<'a>, + parent_alternate: Option, +) -> Result, CompilerError> { + let optional = call.optional; + let loc = builder.source_location(call.span); + let place = build_temporary_place(builder, loc.clone()); + let continuation_block = builder.reserve(builder.current_block_kind()); + let continuation_id = continuation_block.id; + let consequent = builder.reserve(BlockKind::Value); + + let alternate = if let Some(parent_alt) = parent_alternate { + Ok(parent_alt) + } else { + builder.try_enter(BlockKind::Value, |builder, _block_id| { + let temp = lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc: loc.clone() }, + )?; + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: temp, + type_annotation: None, + loc: loc.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: loc.clone(), + }) + }) + }?; + + // Track callee info for building the call in the consequent block. + enum CalleeInfo { + CallExpression { callee: Place }, + MethodCall { receiver: Place, property: Place }, + } + + let mut callee_info: Option = None; + + let test_block = builder.try_enter(BlockKind::Value, |builder, _block_id| { + match &call.callee { + oxc::Expression::CallExpression(opt_call) if expr_contains_optional(&call.callee) => { + let value = + lower_optional_call_expression_impl(builder, opt_call, Some(alternate))?; + let value_place = lower_value_to_temporary(builder, value)?; + callee_info = Some(CalleeInfo::CallExpression { callee: value_place }); + } + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) + if expr_contains_optional(&call.callee) => + { + let callee_member = call.callee.as_member_expression().unwrap(); + let (obj, value) = + lower_optional_member_expression_impl(builder, callee_member, Some(alternate))?; + callee_info = Some(CalleeInfo::MethodCall { receiver: obj, property: value }); + } + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) => { + let callee_member = call.callee.as_member_expression().unwrap(); + let lowered = lower_member_expression(builder, callee_member)?; + let property_place = lower_value_to_temporary(builder, lowered.value)?; + callee_info = Some(CalleeInfo::MethodCall { + receiver: lowered.object, + property: property_place, + }); + } + other => { + let callee_place = lower_expression_to_temporary(builder, other)?; + callee_info = Some(CalleeInfo::CallExpression { callee: callee_place }); + } + } + + let test_place = match callee_info.as_ref().unwrap() { + CalleeInfo::CallExpression { callee } => callee.clone(), + CalleeInfo::MethodCall { property, .. } => property.clone(), + }; + + Ok(Terminal::Branch { + test: test_place, + consequent: consequent.id, + alternate, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }) + }); + + // Block to evaluate if the callee is non-null/undefined. + builder.try_enter_reserved(consequent, |builder| { + let args = lower_arguments(builder, &call.arguments)?; + let temp = build_temporary_place(builder, loc.clone()); + + match callee_info.as_ref().unwrap() { + CalleeInfo::CallExpression { callee } => { + builder.push(Instruction { + id: EvaluationOrder(0), + lvalue: temp.clone(), + value: InstructionValue::CallExpression { + callee: callee.clone(), + args, + loc: loc.clone(), + }, + loc: loc.clone(), + effects: None, + }); + } + CalleeInfo::MethodCall { receiver, property } => { + builder.push(Instruction { + id: EvaluationOrder(0), + lvalue: temp.clone(), + value: InstructionValue::MethodCall { + receiver: receiver.clone(), + property: property.clone(), + args, + loc: loc.clone(), + }, + loc: loc.clone(), + effects: None, + }); + } + } + + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: temp, + type_annotation: None, + loc: loc.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: loc.clone(), + }) + })?; + + builder.terminate_with_continuation( + Terminal::Optional { + optional, + test: test_block?, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + continuation_block, + ); + + Ok(InstructionValue::LoadLocal { place: place.clone(), loc: place.loc }) +} + +// ============================================================================= +// Function / arrow lowering +// ============================================================================= + +/// Lower a function/arrow expression to a `FunctionExpression` instruction value. +/// Mirrors the original `lower_function_to_value`. +fn lower_function_to_value<'a>( + builder: &mut HirBuilder<'a, '_>, + func: FunctionNode<'a>, + expr_type: FunctionExpressionType, +) -> Result, CompilerError> { + let loc = match func { + FunctionNode::Arrow(arrow) => builder.source_location(arrow.span), + FunctionNode::Function(f) => builder.source_location(f.span), + }; + let name = match func { + FunctionNode::Function(f) => f.id.as_ref().map(|id| id.name.to_string()), + FunctionNode::Arrow(_) => None, + }; + let lowered_func = lower_function(builder, func)?; + Ok(InstructionValue::FunctionExpression { name, name_hint: None, lowered_func, expr_type, loc }) +} + +/// Lower a nested function/arrow node into a `LoweredFunction`. Mirrors the +/// original `lower_function`. +fn lower_function<'a>( + builder: &mut HirBuilder<'a, '_>, + func: FunctionNode<'a>, +) -> Result { + // Extract function parts from the AST node + let (params, body, id, generator, is_async, func_start, func_end, func_loc, func_node_id) = + match func { + FunctionNode::Arrow(arrow) => { + let body = if arrow.expression { + match arrow.body.statements.first() { + Some(oxc::Statement::ExpressionStatement(es)) => { + FunctionBody::Expression(&es.expression) + } + _ => FunctionBody::Block(arrow.body.as_ref()), + } + } else { + FunctionBody::Block(arrow.body.as_ref()) + }; + ( + arrow.params.as_ref(), + body, + None::<&str>, + false, + arrow.r#async, + arrow.span.start, + arrow.span.end, + builder.source_location(arrow.span), + Some(arrow.span.start), + ) + } + FunctionNode::Function(f) => { + let body_ref = f.body.as_deref().expect("function expression has a body"); + ( + f.params.as_ref(), + FunctionBody::Block(body_ref), + f.id.as_ref().map(|id| id.name.as_str()), + f.generator, + f.r#async, + f.span.start, + f.span.end, + builder.source_location(f.span), + Some(f.span.start), + ) + } + }; + + // Find the function's scope. For synthetic zero-width functions (e.g., desugared + // match IIFEs from Hermes with start=end=0), node_to_scope won't have an entry. + let function_scope = + if let Some(scope) = builder.scope_info().resolve_scope_for_node(func_node_id) { + scope + } else if func_start < func_end { + builder.scope_info().program_scope + } else { + let parent = builder.function_scope(); + let scope_info = builder.scope_info(); + let mapped: rustc_hash::FxHashSet = + scope_info.node_to_scope.values().copied().collect(); + let param_names: Vec = params + .items + .iter() + .filter_map(|p| { + if let oxc::BindingPattern::BindingIdentifier(id) = &p.pattern { + Some(id.name.to_string()) + } else { + None + } + }) + .collect(); + let mut descendants = rustc_hash::FxHashSet::default(); + descendants.insert(parent); + let mut changed = true; + while changed { + changed = false; + for (i, scope) in scope_info.scopes.iter().enumerate() { + let sid = crate::scope::ScopeId(i as u32); + if let Some(p) = scope.parent { + if descendants.contains(&p) && !descendants.contains(&sid) { + descendants.insert(sid); + changed = true; + } + } + } + } + let mut found = scope_info.program_scope; + for (i, scope) in scope_info.scopes.iter().enumerate() { + let sid = crate::scope::ScopeId(i as u32); + if let Some(p) = scope.parent { + if descendants.contains(&p) + && matches!(scope.kind, ScopeKind::Function) + && !mapped.contains(&sid) + && !builder.is_synthetic_scope_claimed(sid) + { + if !param_names.is_empty() { + let all_match = + param_names.iter().all(|name| scope.bindings.contains_key(name)); + if !all_match { + continue; + } + } + found = sid; + break; + } + } + } + builder.claim_synthetic_scope(found); + found + }; + + let component_scope = builder.component_scope(); + let scope_info = builder.scope_info(); + + let parent_bindings = builder.bindings().clone(); + let parent_used_names = builder.used_names().clone(); + let context_ids = builder.context_identifiers().clone(); + let ident_locs = builder.identifier_locs(); + let line_offsets = builder.line_offsets(); + + // For synthetic functions with zero-width position ranges, position-based + // reference filtering fails. Walk the body AST to collect actual positions. + let ref_override = if func_start >= func_end { + Some(collect_identifier_node_ids_from_body(&body)) + } else { + None + }; + + // Gather captured context + let captured_context = gather_captured_context( + scope_info, + function_scope, + component_scope, + func_start, + func_end, + ident_locs, + ref_override.as_ref(), + ); + let merged_context: FxIndexMap> = { + let parent_context = builder.context().clone(); + let mut merged = parent_context; + for (k, v) in captured_context { + merged.insert(k, v); + } + merged + }; + + // Use scope_info_and_env_mut to avoid conflicting borrows + let (scope_info, env) = builder.scope_info_and_env_mut(); + let (hir_func, child_used_names, child_bindings) = lower_inner( + params, + body, + id, + generator, + is_async, + func_loc, + scope_info, + env, + Some(parent_bindings), + Some(parent_used_names), + merged_context, + function_scope, + component_scope, + &context_ids, + false, // nested function + ident_locs, + line_offsets, + )?; + + builder.merge_used_names(child_used_names); + builder.merge_bindings(child_bindings); + + let func_id = builder.environment_mut().add_function(hir_func); + Ok(LoweredFunction { func: func_id }) +} + +/// Lower a function declaration statement to a FunctionExpression + StoreLocal. +fn lower_function_declaration<'a>( + builder: &mut HirBuilder<'a, '_>, + func_decl: &'a oxc::Function<'a>, +) -> Result<(), CompilerError> { + let loc = builder.source_location(func_decl.span); + let func_start = func_decl.span.start; + let func_end = func_decl.span.end; + + let func_name = func_decl.id.as_ref().map(|id| id.name.to_string()); + + // Find the function's scope + let function_scope = builder + .scope_info() + .resolve_scope_for_node(Some(func_decl.span.start)) + .unwrap_or(builder.scope_info().program_scope); + + let component_scope = builder.component_scope(); + let scope_info = builder.scope_info(); + + let parent_bindings = builder.bindings().clone(); + let parent_used_names = builder.used_names().clone(); + let context_ids = builder.context_identifiers().clone(); + let ident_locs = builder.identifier_locs(); + let line_offsets = builder.line_offsets(); + + // Gather captured context + let captured_context = gather_captured_context( + scope_info, + function_scope, + component_scope, + func_start, + func_end, + ident_locs, + None, + ); + let merged_context: FxIndexMap> = { + let parent_context = builder.context().clone(); + let mut merged = parent_context; + for (k, v) in captured_context { + merged.insert(k, v); + } + merged + }; + + let body_ref = func_decl.body.as_deref().expect("function declaration has a body"); + let (scope_info, env) = builder.scope_info_and_env_mut(); + let (hir_func, child_used_names, child_bindings) = lower_inner( + func_decl.params.as_ref(), + FunctionBody::Block(body_ref), + func_decl.id.as_ref().map(|id| id.name.as_str()), + func_decl.generator, + func_decl.r#async, + loc.clone(), + scope_info, + env, + Some(parent_bindings), + Some(parent_used_names), + merged_context, + function_scope, + component_scope, + &context_ids, + false, // nested function + ident_locs, + line_offsets, + )?; + + builder.merge_used_names(child_used_names); + builder.merge_bindings(child_bindings); + + let func_id = builder.environment_mut().add_function(hir_func); + let lowered_func = LoweredFunction { func: func_id }; + + // Emit FunctionExpression instruction + let fn_value = InstructionValue::FunctionExpression { + name: func_name.clone(), + name_hint: None, + lowered_func, + expr_type: FunctionExpressionType::FunctionDeclaration, + loc: loc.clone(), + }; + let fn_place = lower_value_to_temporary(builder, fn_value)?; + + // Resolve the binding for the function name and store. TS resolves the id + // via Babel's `path.scope.getBinding(name)`, which starts at the function's + // OWN scope: a body-level local that shadows the function's name resolves + // to that inner binding — storing the function into the shadow while + // references elsewhere resolve to the hoisted binding in the parent scope. + // This is a known TS quirk that we reproduce for parity (see + // todo-repro-named-function-with-shadowed-local-same-name). Fall back to + // node-based resolution when the scope walk fails (degraded scope info, + // e.g. synthetic scopes, or backends that split function-body scopes). + if let Some(ref name) = func_name { + if let Some(id_node) = &func_decl.id { + let start = id_node.span.start; + let ident_loc = builder.source_location(id_node.span); + let scope_binding = builder.get_function_declaration_binding(function_scope, name); + let mut is_context = false; + let binding = match scope_binding { + Some(binding_id) => { + is_context = builder.is_context_binding(binding_id); + let binding_kind = crate::react_compiler_lowering::convert_binding_kind( + &builder.scope_info().bindings[binding_id.0 as usize].kind, + ); + let identifier = + builder.resolve_binding_with_loc(name, binding_id, ident_loc.clone())?; + VariableBinding::Identifier { identifier, binding_kind } + } + None => { + let mut binding = builder.resolve_identifier( + name, + start, + ident_loc.clone(), + Some(id_node.span.start), + )?; + if matches!(&binding, VariableBinding::Global { .. }) { + // For function redeclarations (e.g., `function x() {} function x() {}`), + // the redeclaration's identifier may not be in ref_node_id_to_binding + // (OXC/SWC don't map constant violations). Retry using the first + // declaration's node_id from the scope chain. + let fallback = { + let si = builder.scope_info(); + let scope_id = si + .resolve_scope_for_node(Some(func_decl.span.start)) + .unwrap_or(si.program_scope); + si.get_binding(scope_id, name).map(|bid| { + let b = &si.bindings[bid.0 as usize]; + (b.declaration_start.unwrap_or(0), b.declaration_node_id) + }) + }; + if let Some((ds, ds_node_id)) = fallback { + binding = builder.resolve_identifier( + name, + ds, + ident_loc.clone(), + ds_node_id, + )?; + } + } + if matches!(&binding, VariableBinding::Identifier { .. }) { + is_context = + builder.is_context_identifier(name, start, Some(id_node.span.start)); + } + binding + } + }; + match binding { + VariableBinding::Identifier { identifier, .. } => { + // Don't override the identifier's declaration loc here. + // For function redeclarations (e.g., `function x() {} function x() {}`), + // the identifier's loc should remain the first declaration's loc, + // which was already set during define_binding. + // Use the full function declaration loc for the Place, + // matching the TS behavior where lowerAssignment uses stmt.node.loc + let place = Place { + identifier, + reactive: false, + effect: Effect::Unknown, + loc: loc.clone(), + }; + if is_context { + lower_value_to_temporary( + builder, + InstructionValue::StoreContext { + lvalue: LValue { kind: InstructionKind::Function, place }, + value: fn_place, + loc, + }, + )?; + } else { + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Function, place }, + value: fn_place, + type_annotation: None, + loc, + }, + )?; + } + } + _ => { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Invariant, + reason: format!( + "Could not find binding for function declaration `{}`", + name + ), + description: None, + loc, + suggestions: None, + })?; + } + } + } + } + Ok(()) +} + +/// Lower a function expression used as an object method. +fn lower_function_for_object_method<'a>( + builder: &mut HirBuilder<'a, '_>, + method_span: oxc_span::Span, + params: &'a oxc::FormalParameters<'a>, + body: &'a oxc::FunctionBody<'a>, + generator: bool, + is_async: bool, +) -> Result { + let func_start = method_span.start; + let func_end = method_span.end; + let func_loc = builder.source_location(method_span); + + let function_scope = builder + .scope_info() + .resolve_scope_for_node(Some(method_span.start)) + .unwrap_or(builder.scope_info().program_scope); + + let component_scope = builder.component_scope(); + let scope_info = builder.scope_info(); + + let parent_bindings = builder.bindings().clone(); + let parent_used_names = builder.used_names().clone(); + let context_ids = builder.context_identifiers().clone(); + let ident_locs = builder.identifier_locs(); + let line_offsets = builder.line_offsets(); + + let captured_context = gather_captured_context( + scope_info, + function_scope, + component_scope, + func_start, + func_end, + ident_locs, + None, + ); + let merged_context: FxIndexMap> = { + let parent_context = builder.context().clone(); + let mut merged = parent_context; + for (k, v) in captured_context { + merged.insert(k, v); + } + merged + }; + + let (scope_info, env) = builder.scope_info_and_env_mut(); + let (hir_func, child_used_names, child_bindings) = lower_inner( + params, + FunctionBody::Block(body), + None, + generator, + is_async, + func_loc, + scope_info, + env, + Some(parent_bindings), + Some(parent_used_names), + merged_context, + function_scope, + component_scope, + &context_ids, + false, // nested function + ident_locs, + line_offsets, + )?; + + builder.merge_used_names(child_used_names); + builder.merge_bindings(child_bindings); + + let func_id = builder.environment_mut().add_function(hir_func); + Ok(LoweredFunction { func: func_id }) +} + +fn gather_captured_context( + scope_info: &ScopeInfo, + function_scope: crate::scope::ScopeId, + component_scope: crate::scope::ScopeId, + func_start: u32, + func_end: u32, + identifier_locs: &IdentifierLocIndex, + ref_node_ids_override: Option<&FxIndexSet>, +) -> FxIndexMap> { + let parent_scope = scope_info.scopes[function_scope.0 as usize].parent; + let pure_scopes = match parent_scope { + Some(parent) => capture_scopes(scope_info, parent, component_scope), + None => FxIndexSet::default(), + }; + + // Collect the earliest (lowest source position) reference location for each + // captured binding. Using the minimum position makes the result independent of + // ref_node_id_to_binding iteration order, matching the behavior the TS compiler + // gets from Babel's position-ordered traversal. + let mut captured: rustc_hash::FxHashMap< + crate::scope::BindingId, + (u32, Option), // (min_position, loc) + > = rustc_hash::FxHashMap::default(); + + for (&ref_nid, &binding_id) in &scope_info.ref_node_id_to_binding { + if let Some(allowed) = ref_node_ids_override { + if !allowed.contains(&ref_nid) { + continue; + } + } else { + // Range check: use the position stored in identifier_locs + let ref_start = identifier_locs.get(&ref_nid).map(|e| e.start).unwrap_or(0); + if ref_start < func_start || ref_start >= func_end { + continue; + } + } + let binding = &scope_info.bindings[binding_id.0 as usize]; + // Skip references that are actually the binding's own declaration site + if binding.declaration_node_id == Some(ref_nid) { + continue; + } + // Skip function/class declaration names that are not expression references. + // Skip type-annotation references: TS's gatherCapturedContext traverse + // skips TypeAnnotation/TSTypeAnnotation/TypeAlias/TSTypeAliasDeclaration + // subtrees, so identifiers there never become captures (they DO still + // feed FindContextIdentifiers and the hoisting analysis, which have no + // such skip in TS). + if let Some(entry) = identifier_locs.get(&ref_nid) { + if entry.is_declaration_name || entry.in_type_annotation { + continue; + } + } + // Skip type-only bindings + if binding.declaration_type == "TypeAlias" + || binding.declaration_type == "OpaqueType" + || binding.declaration_type == "InterfaceDeclaration" + || binding.declaration_type == "TSTypeAliasDeclaration" + || binding.declaration_type == "TSInterfaceDeclaration" + || binding.declaration_type == "TSEnumDeclaration" + { + continue; + } + if pure_scopes.contains(&binding.scope) { + let ref_start = identifier_locs.get(&ref_nid).map(|e| e.start).unwrap_or(0); + // Skip references whose start offset aliases the binding's own + // declaration offset. Hermes desugars (component syntax) reuse the + // original source offsets for generated nodes, so a sibling + // reference structurally OUTSIDE this function (e.g. the forwardRef + // argument naming the desugared inner function) can fall inside the + // function's position range and alias the declaration position. In + // real source a non-declaration reference can never share its + // declaration's offset, so this only filters desugared aliases. + if binding.declaration_start == Some(ref_start) { + continue; + } + let loc = identifier_locs.get(&ref_nid).map(|entry| { + if let Some(oe_loc) = &entry.opening_element_loc { + oe_loc.clone() + } else { + entry.loc.clone() + } + }); + captured + .entry(binding.id) + .and_modify(|(min_pos, existing_loc)| { + if ref_start < *min_pos { + *min_pos = ref_start; + *existing_loc = loc.clone(); + } + }) + .or_insert((ref_start, loc)); + } + } + + // Sort captured entries by source position so context declarations appear + // in source order, matching the TS compiler's position-ordered traversal. + let mut sorted: Vec<_> = captured.into_iter().collect(); + sorted.sort_by_key(|(_, (pos, _))| *pos); + + sorted.into_iter().map(|(bid, (_, loc))| (bid, loc)).collect() +} + +fn capture_scopes( + scope_info: &ScopeInfo, + from: crate::scope::ScopeId, + to: crate::scope::ScopeId, +) -> FxIndexSet { + let mut result = FxIndexSet::default(); + let mut current = Some(from); + while let Some(scope_id) = current { + result.insert(scope_id); + if scope_id == to { + break; + } + current = scope_info.scopes[scope_id.0 as usize].parent; + } + result +} + +fn collect_identifier_node_ids_from_body(body: &FunctionBody) -> FxIndexSet { + let mut positions = FxIndexSet::default(); + match body { + FunctionBody::Block(block) => { + for stmt in &block.statements { + collect_identifier_node_ids_from_stmt(stmt, &mut positions); + } + } + FunctionBody::Expression(expr) => { + collect_identifier_node_ids_from_expr(expr, &mut positions); + } + } + positions +} + +fn collect_identifier_node_ids_from_stmt(stmt: &oxc::Statement, positions: &mut FxIndexSet) { + match stmt { + oxc::Statement::ExpressionStatement(s) => { + collect_identifier_node_ids_from_expr(&s.expression, positions) + } + oxc::Statement::ReturnStatement(s) => { + if let Some(arg) = &s.argument { + collect_identifier_node_ids_from_expr(arg, positions); + } + } + oxc::Statement::ThrowStatement(s) => { + collect_identifier_node_ids_from_expr(&s.argument, positions) + } + oxc::Statement::BlockStatement(s) => { + for stmt in &s.body { + collect_identifier_node_ids_from_stmt(stmt, positions); + } + } + oxc::Statement::IfStatement(s) => { + collect_identifier_node_ids_from_expr(&s.test, positions); + collect_identifier_node_ids_from_stmt(&s.consequent, positions); + if let Some(alt) = &s.alternate { + collect_identifier_node_ids_from_stmt(alt, positions); + } + } + oxc::Statement::VariableDeclaration(s) => { + for decl in &s.declarations { + if let Some(init) = &decl.init { + collect_identifier_node_ids_from_expr(init, positions); + } + } + } + _ => {} + } +} + +fn collect_identifier_node_ids_from_expr(expr: &oxc::Expression, positions: &mut FxIndexSet) { + match expr { + oxc::Expression::Identifier(id) => { + positions.insert(id.span.start); + } + oxc::Expression::CallExpression(call) => { + collect_identifier_node_ids_from_expr(&call.callee, positions); + for arg in &call.arguments { + if let Some(e) = arg.as_expression() { + collect_identifier_node_ids_from_expr(e, positions); + } else if let oxc::Argument::SpreadElement(s) = arg { + collect_identifier_node_ids_from_expr(&s.argument, positions); + } + } + } + oxc::Expression::BinaryExpression(e) => { + collect_identifier_node_ids_from_expr(&e.left, positions); + collect_identifier_node_ids_from_expr(&e.right, positions); + } + oxc::Expression::ConditionalExpression(e) => { + collect_identifier_node_ids_from_expr(&e.test, positions); + collect_identifier_node_ids_from_expr(&e.consequent, positions); + collect_identifier_node_ids_from_expr(&e.alternate, positions); + } + oxc::Expression::LogicalExpression(e) => { + collect_identifier_node_ids_from_expr(&e.left, positions); + collect_identifier_node_ids_from_expr(&e.right, positions); + } + oxc::Expression::StaticMemberExpression(e) => { + collect_identifier_node_ids_from_expr(&e.object, positions); + } + oxc::Expression::ComputedMemberExpression(e) => { + collect_identifier_node_ids_from_expr(&e.object, positions); + } + oxc::Expression::PrivateFieldExpression(e) => { + collect_identifier_node_ids_from_expr(&e.object, positions); + } + oxc::Expression::ChainExpression(chain) => { + collect_identifier_node_ids_from_chain_element(&chain.expression, positions); + } + oxc::Expression::UpdateExpression(e) => { + collect_identifier_node_ids_from_simple_target(&e.argument, positions); + } + oxc::Expression::FunctionExpression(func) => { + if let Some(body) = func.body.as_deref() { + for stmt in &body.statements { + collect_identifier_node_ids_from_stmt(stmt, positions); + } + } + } + oxc::Expression::UnaryExpression(e) => { + collect_identifier_node_ids_from_expr(&e.argument, positions); + } + oxc::Expression::ParenthesizedExpression(e) => { + collect_identifier_node_ids_from_expr(&e.expression, positions); + } + oxc::Expression::TSAsExpression(e) => { + collect_identifier_node_ids_from_expr(&e.expression, positions); + } + oxc::Expression::TSSatisfiesExpression(e) => { + collect_identifier_node_ids_from_expr(&e.expression, positions); + } + oxc::Expression::TSTypeAssertion(e) => { + collect_identifier_node_ids_from_expr(&e.expression, positions); + } + oxc::Expression::TSNonNullExpression(e) => { + collect_identifier_node_ids_from_expr(&e.expression, positions); + } + oxc::Expression::ArrowFunctionExpression(arrow) => { + if arrow.expression { + if let Some(oxc::Statement::ExpressionStatement(es)) = arrow.body.statements.first() + { + collect_identifier_node_ids_from_expr(&es.expression, positions); + } + } else { + for stmt in &arrow.body.statements { + collect_identifier_node_ids_from_stmt(stmt, positions); + } + } + } + oxc::Expression::JSXElement(el) => { + collect_identifier_node_ids_from_jsx_element(el, positions); + } + oxc::Expression::JSXFragment(frag) => { + for child in &frag.children { + collect_identifier_node_ids_from_jsx_child(child, positions); + } + } + oxc::Expression::ArrayExpression(arr) => { + for elem in &arr.elements { + if let Some(e) = elem.as_expression() { + collect_identifier_node_ids_from_expr(e, positions); + } else if let oxc::ArrayExpressionElement::SpreadElement(s) = elem { + collect_identifier_node_ids_from_expr(&s.argument, positions); + } + } + } + oxc::Expression::ObjectExpression(obj) => { + for prop in &obj.properties { + match prop { + oxc::ObjectPropertyKind::ObjectProperty(p) => { + collect_identifier_node_ids_from_expr(&p.value, positions); + } + oxc::ObjectPropertyKind::SpreadProperty(s) => { + collect_identifier_node_ids_from_expr(&s.argument, positions); + } + } + } + } + oxc::Expression::NewExpression(e) => { + collect_identifier_node_ids_from_expr(&e.callee, positions); + for arg in &e.arguments { + if let Some(ex) = arg.as_expression() { + collect_identifier_node_ids_from_expr(ex, positions); + } else if let oxc::Argument::SpreadElement(s) = arg { + collect_identifier_node_ids_from_expr(&s.argument, positions); + } + } + } + oxc::Expression::AssignmentExpression(e) => { + collect_identifier_node_ids_from_expr(&e.right, positions); + } + oxc::Expression::TemplateLiteral(e) => { + for expr in &e.expressions { + collect_identifier_node_ids_from_expr(expr, positions); + } + } + oxc::Expression::SequenceExpression(e) => { + for expr in &e.expressions { + collect_identifier_node_ids_from_expr(expr, positions); + } + } + _ => {} + } +} + +fn collect_identifier_node_ids_from_chain_element( + element: &oxc::ChainElement, + positions: &mut FxIndexSet, +) { + match element { + oxc::ChainElement::CallExpression(call) => { + collect_identifier_node_ids_from_expr(&call.callee, positions); + for arg in &call.arguments { + if let Some(e) = arg.as_expression() { + collect_identifier_node_ids_from_expr(e, positions); + } else if let oxc::Argument::SpreadElement(s) = arg { + collect_identifier_node_ids_from_expr(&s.argument, positions); + } + } + } + oxc::ChainElement::TSNonNullExpression(e) => { + collect_identifier_node_ids_from_expr(&e.expression, positions); + } + oxc::ChainElement::StaticMemberExpression(m) => { + collect_identifier_node_ids_from_expr(&m.object, positions); + } + oxc::ChainElement::ComputedMemberExpression(m) => { + collect_identifier_node_ids_from_expr(&m.object, positions); + } + oxc::ChainElement::PrivateFieldExpression(m) => { + collect_identifier_node_ids_from_expr(&m.object, positions); + } + } +} + +fn collect_identifier_node_ids_from_simple_target( + target: &oxc::SimpleAssignmentTarget, + positions: &mut FxIndexSet, +) { + match target { + oxc::SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => { + positions.insert(id.span.start); + } + oxc::SimpleAssignmentTarget::StaticMemberExpression(m) => { + collect_identifier_node_ids_from_expr(&m.object, positions); + } + oxc::SimpleAssignmentTarget::ComputedMemberExpression(m) => { + collect_identifier_node_ids_from_expr(&m.object, positions); + } + oxc::SimpleAssignmentTarget::PrivateFieldExpression(m) => { + collect_identifier_node_ids_from_expr(&m.object, positions); + } + _ => {} + } +} + +fn collect_identifier_node_ids_from_jsx_element( + el: &oxc::JSXElement, + positions: &mut FxIndexSet, +) { + if let oxc::JSXElementName::IdentifierReference(id) = &el.opening_element.name { + positions.insert(id.span.start); + } + for attr in &el.opening_element.attributes { + match attr { + oxc::JSXAttributeItem::Attribute(a) => { + if let Some(oxc::JSXAttributeValue::ExpressionContainer(c)) = &a.value { + if let Some(e) = c.expression.as_expression() { + collect_identifier_node_ids_from_expr(e, positions); + } + } + } + oxc::JSXAttributeItem::SpreadAttribute(a) => { + collect_identifier_node_ids_from_expr(&a.argument, positions); + } + } + } + for child in &el.children { + collect_identifier_node_ids_from_jsx_child(child, positions); + } +} + +fn collect_identifier_node_ids_from_jsx_child( + child: &oxc::JSXChild, + positions: &mut FxIndexSet, +) { + match child { + oxc::JSXChild::ExpressionContainer(c) => { + if let Some(e) = c.expression.as_expression() { + collect_identifier_node_ids_from_expr(e, positions); + } + } + oxc::JSXChild::Element(child_el) => { + collect_identifier_node_ids_from_jsx_element(child_el, positions); + } + oxc::JSXChild::Fragment(frag) => { + for c in &frag.children { + collect_identifier_node_ids_from_jsx_child(c, positions); + } + } + oxc::JSXChild::Spread(s) => { + collect_identifier_node_ids_from_expr(&s.expression, positions); + } + _ => {} + } +} + +fn lower_expression<'a>( + builder: &mut HirBuilder<'a, '_>, + expr: &'a oxc::Expression<'a>, +) -> Result, CompilerError> { + match expr { + oxc::Expression::Identifier(ident) => { + let loc = builder.source_location(ident.span); + let start = ident.span.start; + let place = + lower_identifier(builder, ident.name.as_str(), start, loc.clone(), Some(start))?; + if builder.is_context_identifier(ident.name.as_str(), start, Some(start)) { + Ok(InstructionValue::LoadContext { place, loc }) + } else { + Ok(InstructionValue::LoadLocal { place, loc }) + } + } + oxc::Expression::NullLiteral(lit) => Ok(InstructionValue::Primitive { + value: PrimitiveValue::Null, + loc: builder.source_location(lit.span), + }), + oxc::Expression::BooleanLiteral(lit) => Ok(InstructionValue::Primitive { + value: PrimitiveValue::Boolean(lit.value), + loc: builder.source_location(lit.span), + }), + oxc::Expression::NumericLiteral(lit) => Ok(InstructionValue::Primitive { + value: PrimitiveValue::Number(FloatValue::new(lit.value)), + loc: builder.source_location(lit.span), + }), + oxc::Expression::StringLiteral(lit) => Ok(InstructionValue::Primitive { + value: PrimitiveValue::String(lit.value.to_string().into()), + loc: builder.source_location(lit.span), + }), + oxc::Expression::RegExpLiteral(lit) => Ok(InstructionValue::RegExpLiteral { + pattern: lit.regex.pattern.text.to_string(), + flags: lit.regex.flags.to_inline_string().as_str().to_string(), + loc: builder.source_location(lit.span), + }), + oxc::Expression::BinaryExpression(bin) => { + let loc = builder.source_location(bin.span); + let left = lower_expression_to_temporary(builder, &bin.left)?; + let right = lower_expression_to_temporary(builder, &bin.right)?; + Ok(InstructionValue::BinaryExpression { + operator: convert_binary_operator(bin.operator), + left, + right, + loc, + }) + } + oxc::Expression::UnaryExpression(unary) => { + let loc = builder.source_location(unary.span); + match unary.operator { + oxc::UnaryOperator::Delete => match &unary.argument { + oxc::Expression::StaticMemberExpression(member) => { + let object = lower_expression_to_temporary(builder, &member.object)?; + Ok(InstructionValue::PropertyDelete { + object, + property: PropertyLiteral::String(member.property.name.to_string()), + loc, + }) + } + oxc::Expression::ComputedMemberExpression(member) => { + let object = lower_expression_to_temporary(builder, &member.object)?; + let property = lower_expression_to_temporary(builder, &member.expression)?; + Ok(InstructionValue::ComputedDelete { object, property, loc }) + } + _ => { + builder.record_error(CompilerErrorDetail { + reason: "Only object properties can be deleted".to_string(), + category: ErrorCategory::Syntax, + loc: loc.clone(), + description: None, + suggestions: None, + })?; + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + }, + op => { + let value = lower_expression_to_temporary(builder, &unary.argument)?; + Ok(InstructionValue::UnaryExpression { + operator: convert_unary_operator(op), + value, + loc, + }) + } + } + } + oxc::Expression::LogicalExpression(logical) => { + let loc = builder.source_location(logical.span); + let continuation_block = builder.reserve(builder.current_block_kind()); + let continuation_id = continuation_block.id; + let test_block = builder.reserve(BlockKind::Value); + let test_block_id = test_block.id; + let place = build_temporary_place(builder, loc.clone()); + let left_loc = builder.source_location(logical.left.span()); + let left_place = build_temporary_place(builder, left_loc); + + let consequent_block = builder.try_enter(BlockKind::Value, |builder, _block_id| { + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: left_place.clone(), + type_annotation: None, + loc: left_place.loc.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: left_place.loc.clone(), + }) + }); + + let alternate_block = builder.try_enter(BlockKind::Value, |builder, _block_id| { + let right = lower_expression_to_temporary(builder, &logical.right)?; + let right_loc = right.loc.clone(); + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: right, + type_annotation: None, + loc: right_loc.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: right_loc, + }) + }); + + let hir_op = match logical.operator { + oxc::LogicalOperator::And => LogicalOperator::And, + oxc::LogicalOperator::Or => LogicalOperator::Or, + oxc::LogicalOperator::Coalesce => LogicalOperator::NullishCoalescing, + }; + + builder.terminate_with_continuation( + Terminal::Logical { + operator: hir_op, + test: test_block_id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + test_block, + ); + + let left_value = lower_expression_to_temporary(builder, &logical.left)?; + builder.push(Instruction { + id: EvaluationOrder(0), + lvalue: left_place.clone(), + value: InstructionValue::LoadLocal { place: left_value, loc: loc.clone() }, + effects: None, + loc: loc.clone(), + }); + + builder.terminate_with_continuation( + Terminal::Branch { + test: left_place, + consequent: consequent_block?, + alternate: alternate_block?, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + continuation_block, + ); + + Ok(InstructionValue::LoadLocal { place: place.clone(), loc: place.loc.clone() }) + } + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) => { + let lowered = lower_member_expression(builder, expr.as_member_expression().unwrap())?; + Ok(lowered.value) + } + oxc::Expression::CallExpression(call) => { + let loc = builder.source_location(call.span); + if let Some(member) = call.callee.as_member_expression() { + let lowered = lower_member_expression(builder, member)?; + let property = lower_value_to_temporary(builder, lowered.value)?; + let args = lower_arguments(builder, &call.arguments)?; + Ok(InstructionValue::MethodCall { receiver: lowered.object, property, args, loc }) + } else { + let callee = lower_expression_to_temporary(builder, &call.callee)?; + let args = lower_arguments(builder, &call.arguments)?; + Ok(InstructionValue::CallExpression { callee, args, loc }) + } + } + oxc::Expression::ConditionalExpression(cond) => { + let loc = builder.source_location(cond.span); + let continuation_block = builder.reserve(builder.current_block_kind()); + let continuation_id = continuation_block.id; + let test_block = builder.reserve(BlockKind::Value); + let test_block_id = test_block.id; + let place = build_temporary_place(builder, loc.clone()); + + // Block for the consequent (test is truthy) + let consequent_ast_loc = builder.source_location(cond.consequent.span()); + let consequent_block = builder.try_enter(BlockKind::Value, |builder, _block_id| { + let consequent = lower_expression_to_temporary(builder, &cond.consequent)?; + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: consequent, + type_annotation: None, + loc: loc.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: consequent_ast_loc, + }) + }); + + // Block for the alternate (test is falsy) + let alternate_ast_loc = builder.source_location(cond.alternate.span()); + let alternate_block = builder.try_enter(BlockKind::Value, |builder, _block_id| { + let alternate = lower_expression_to_temporary(builder, &cond.alternate)?; + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: alternate, + type_annotation: None, + loc: loc.clone(), + }, + )?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: alternate_ast_loc, + }) + }); + + builder.terminate_with_continuation( + Terminal::Ternary { + test: test_block_id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + test_block, + ); + + // Now in test block: lower test expression + let test_place = lower_expression_to_temporary(builder, &cond.test)?; + builder.terminate_with_continuation( + Terminal::Branch { + test: test_place, + consequent: consequent_block?, + alternate: alternate_block?, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + continuation_block, + ); + + Ok(InstructionValue::LoadLocal { place: place.clone(), loc: place.loc.clone() }) + } + oxc::Expression::SequenceExpression(seq) => { + let loc = builder.source_location(seq.span); + + if seq.expressions.is_empty() { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Syntax, + reason: "Expected sequence expression to have at least one expression" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + return Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }); + } + + let continuation_block = builder.reserve(builder.current_block_kind()); + let continuation_id = continuation_block.id; + let place = build_temporary_place(builder, loc.clone()); + + let sequence_block = builder.try_enter(BlockKind::Sequence, |builder, _block_id| { + let mut last: Option = None; + for item in &seq.expressions { + last = Some(lower_expression_to_temporary(builder, item)?); + } + if let Some(last) = last { + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Const, place: place.clone() }, + value: last, + type_annotation: None, + loc: loc.clone(), + }, + )?; + } + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: loc.clone(), + }) + }); + + builder.terminate_with_continuation( + Terminal::Sequence { + block: sequence_block?, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + continuation_block, + ); + Ok(InstructionValue::LoadLocal { place, loc }) + } + oxc::Expression::NewExpression(new_expr) => { + let loc = builder.source_location(new_expr.span); + let callee = lower_expression_to_temporary(builder, &new_expr.callee)?; + let args = lower_arguments(builder, &new_expr.arguments)?; + Ok(InstructionValue::NewExpression { callee, args, loc }) + } + oxc::Expression::TemplateLiteral(tmpl) => { + let loc = builder.source_location(tmpl.span); + let subexprs: Vec = tmpl + .expressions + .iter() + .map(|e| lower_expression_to_temporary(builder, e)) + .collect::, _>>()?; + let quasis: Vec = + tmpl.quasis.iter().map(template_quasi_from_oxc).collect(); + Ok(InstructionValue::TemplateLiteral { subexprs, quasis, loc }) + } + oxc::Expression::TaggedTemplateExpression(tagged) => { + let loc = builder.source_location(tagged.span); + // Upstream React Compiler bails on any interpolation here; the oxc port + // instead lowers the tag plus every quasi and every `${...}` + // subexpression (mirroring `TemplateLiteral`). This is a deliberate + // divergence from the TS reference. + // + // We still bail when any quasi's cooked value differs from its raw value + // (e.g. escape sequences or graphql templates), matching upstream's + // single-quasi behavior — the HIR only round-trips raw==cooked quasis. + if tagged.quasi.quasis.iter().any(|q| { + q.value.raw.as_str() != q.value.cooked.map(|c| c.to_string()).unwrap_or_default() + }) { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + return Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }); + } + // Evaluation order: the tag is evaluated first, then each interpolated + // subexpression left-to-right. + let tag = lower_expression_to_temporary(builder, &tagged.tag)?; + let subexprs: Vec = tagged + .quasi + .expressions + .iter() + .map(|e| lower_expression_to_temporary(builder, e)) + .collect::, _>>()?; + let quasis: Vec = + tagged.quasi.quasis.iter().map(template_quasi_from_oxc).collect(); + Ok(InstructionValue::TaggedTemplateExpression { tag, quasis, subexprs, loc }) + } + oxc::Expression::AwaitExpression(await_expr) => { + let loc = builder.source_location(await_expr.span); + let value = lower_expression_to_temporary(builder, &await_expr.argument)?; + Ok(InstructionValue::Await { value, loc }) + } + oxc::Expression::YieldExpression(yld) => { + let loc = builder.source_location(yld.span); + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle YieldExpression expressions" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + oxc::Expression::MetaProperty(meta) => { + let loc = builder.source_location(meta.span); + if meta.meta.name == "import" && meta.property.name == "meta" { + Ok(InstructionValue::MetaProperty { + meta: meta.meta.name.to_string(), + property: meta.property.name.to_string(), + loc, + }) + } else { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + } + oxc::Expression::ClassExpression(cls) => { + let loc = builder.source_location(cls.span); + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle ClassExpression expressions" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + oxc::Expression::Super(sup) => { + let loc = builder.source_location(sup.span); + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle Super expressions".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + oxc::Expression::ThisExpression(this) => { + let loc = builder.source_location(this.span); + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle ThisExpression expressions".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + oxc::Expression::ImportExpression(imp) => { + // oxc's `import(source, options?)` maps to Babel's + // `CallExpression { callee: Import, arguments: [source] + options? }`. + // The `Import` keyword callee bails (records an error), then the source + // and options arguments are lowered left-to-right. + let loc = builder.source_location(imp.span); + // Babel's `Import` node carried the loc of the whole `import(...)` + // expression, so the callee bail error + temporary use the full span. + let callee = lower_import_keyword_to_temporary(builder, &loc)?; + let mut args: Vec = Vec::new(); + let source = lower_expression_to_temporary(builder, &imp.source)?; + args.push(PlaceOrSpread::Place(source)); + if let Some(options) = &imp.options { + let options = lower_expression_to_temporary(builder, options)?; + args.push(PlaceOrSpread::Place(options)); + } + Ok(InstructionValue::CallExpression { callee, args, loc }) + } + oxc::Expression::PrivateInExpression(priv_in) => { + // `#f in obj` maps to Babel's `BinaryExpression { op: In, left: PrivateName, right }`. + // The PrivateName left operand bails (records an error), then the right + // operand is lowered. + let loc = builder.source_location(priv_in.span); + let left = lower_private_name_to_temporary(builder, priv_in.left.span)?; + let right = lower_expression_to_temporary(builder, &priv_in.right)?; + Ok(InstructionValue::BinaryExpression { + operator: BinaryOperator::In, + left, + right, + loc, + }) + } + oxc::Expression::UpdateExpression(update) => { + let loc = builder.source_location(update.span); + match &update.argument { + oxc::SimpleAssignmentTarget::StaticMemberExpression(_) + | oxc::SimpleAssignmentTarget::ComputedMemberExpression(_) + | oxc::SimpleAssignmentTarget::PrivateFieldExpression(_) + | oxc::SimpleAssignmentTarget::TSAsExpression(_) + | oxc::SimpleAssignmentTarget::TSSatisfiesExpression(_) + | oxc::SimpleAssignmentTarget::TSNonNullExpression(_) + | oxc::SimpleAssignmentTarget::TSTypeAssertion(_) + if simple_target_is_member_like(&update.argument) => + { + let binary_op = match update.operator { + oxc::UpdateOperator::Increment => BinaryOperator::Add, + oxc::UpdateOperator::Decrement => BinaryOperator::Subtract, + }; + // Use the member expression's loc (not the update expression's) + // to match TS behavior where the inner operations use leftExpr.node.loc + let member_loc = builder.source_location(update.argument.span()); + let lowered = + lower_member_expression_from_simple_target(builder, &update.argument)?; + let object = lowered.object; + let lowered_property = lowered.property; + let prev_value = lower_value_to_temporary(builder, lowered.value)?; + + let one = lower_value_to_temporary( + builder, + InstructionValue::Primitive { + value: PrimitiveValue::Number(FloatValue::new(1.0)), + loc: None, + }, + )?; + let updated = lower_value_to_temporary( + builder, + InstructionValue::BinaryExpression { + operator: binary_op, + left: prev_value.clone(), + right: one, + loc: member_loc.clone(), + }, + )?; + + // Store back using the property from the lowered member expression. + // For prefix, the result is the PropertyStore/ComputedStore lvalue + // (matching TS which uses newValuePlace). For postfix, it's prev_value. + let new_value_place = match lowered_property { + MemberProperty::Literal(prop_literal) => lower_value_to_temporary( + builder, + InstructionValue::PropertyStore { + object, + property: prop_literal, + value: updated.clone(), + loc: member_loc, + }, + )?, + MemberProperty::Computed(prop_place) => lower_value_to_temporary( + builder, + InstructionValue::ComputedStore { + object, + property: prop_place, + value: updated.clone(), + loc: member_loc, + }, + )?, + }; + + // Return previous for postfix, newValuePlace for prefix + let result_place = if update.prefix { new_value_place } else { prev_value }; + Ok(InstructionValue::LoadLocal { + place: result_place.clone(), + loc: result_place.loc.clone(), + }) + } + oxc::SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) => { + let start = ident.span.start; + if builder.is_context_identifier(ident.name.as_str(), start, Some(start)) { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + return Ok(InstructionValue::Primitive { + value: PrimitiveValue::Undefined, + loc, + }); + } + + let ident_loc = builder.source_location(ident.span); + let binding = builder.resolve_identifier( + ident.name.as_str(), + start, + ident_loc.clone(), + Some(start), + )?; + if matches!(binding, VariableBinding::Global { .. }) { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: + "UpdateExpression where argument is a global is not yet supported" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + return Ok(InstructionValue::Primitive { + value: PrimitiveValue::Undefined, + loc, + }); + } + let identifier = match binding { + VariableBinding::Identifier { identifier, .. } => identifier, + _ => { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + return Ok(InstructionValue::Primitive { + value: PrimitiveValue::Undefined, + loc, + }); + } + }; + let lvalue_place = Place { + identifier, + effect: Effect::Unknown, + reactive: false, + loc: ident_loc.clone(), + }; + + // Load the current value + let value = lower_identifier( + builder, + ident.name.as_str(), + start, + ident_loc, + Some(start), + )?; + + let operation = match update.operator { + oxc::UpdateOperator::Increment => UpdateOperator::Increment, + oxc::UpdateOperator::Decrement => UpdateOperator::Decrement, + }; + + if update.prefix { + Ok(InstructionValue::PrefixUpdate { + lvalue: lvalue_place, + operation, + value, + loc, + }) + } else { + Ok(InstructionValue::PostfixUpdate { + lvalue: lvalue_place, + operation, + value, + loc, + }) + } + } + _ => { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "UpdateExpression with unsupported argument type".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + } + } + // `x as T` / `x satisfies T` / `x` lower the inner expression to a + // temporary and emit a `TypeCastExpression` carrying the type metadata, + // mirroring the original Babel logic. + oxc::Expression::TSAsExpression(ts) => { + lower_type_cast_expression(builder, ts.span, &ts.expression, &ts.type_annotation, "as") + } + oxc::Expression::TSSatisfiesExpression(ts) => lower_type_cast_expression( + builder, + ts.span, + &ts.expression, + &ts.type_annotation, + "satisfies", + ), + oxc::Expression::TSTypeAssertion(ts) => { + lower_type_cast_expression(builder, ts.span, &ts.expression, &ts.type_annotation, "as") + } + // `x!` and `x` unwrap to their inner expression (the original also just + // unwraps these). + oxc::Expression::TSNonNullExpression(ts) => lower_expression(builder, &ts.expression), + oxc::Expression::TSInstantiationExpression(ts) => lower_expression(builder, &ts.expression), + // oxc parses with `preserve_parens: true`, so `(expr)` is a real + // `ParenthesizedExpression` node. The original Babel AST never carried + // paren nodes (convert_ast stripped them), so unwrap to the inner + // expression to reproduce the original HIR. + oxc::Expression::ParenthesizedExpression(paren) => { + lower_expression(builder, &paren.expression) + } + oxc::Expression::V8IntrinsicExpression(_) => { + unreachable!( + "V8IntrinsicExpression: oxc does not emit this without ParseOptions::allow_v8_intrinsics" + ) + } + oxc::Expression::ObjectExpression(obj) => { + let loc = builder.source_location(obj.span); + let mut properties: Vec = Vec::new(); + for prop in &obj.properties { + match prop { + oxc::ObjectPropertyKind::ObjectProperty(p) => { + // In oxc, getters/setters/methods are encoded as an + // `ObjectProperty` whose value is a `FunctionExpression` + // (the Babel AST instead carried a separate `ObjectMethod` + // node). Route those through `lower_object_method`. + if p.method + || matches!(p.kind, oxc::PropertyKind::Get | oxc::PropertyKind::Set) + { + if let Some(method_prop) = lower_object_method(builder, p)? { + properties.push(ObjectPropertyOrSpread::Property(method_prop)); + } + continue; + } + let key = lower_object_property_key(builder, &p.key, p.computed)?; + let key = match key { + Some(k) => k, + None => continue, + }; + let value = lower_expression_to_temporary(builder, &p.value)?; + properties.push(ObjectPropertyOrSpread::Property(ObjectProperty { + key, + property_type: ObjectPropertyType::Property, + place: value, + })); + } + oxc::ObjectPropertyKind::SpreadProperty(spread) => { + let place = lower_expression_to_temporary(builder, &spread.argument)?; + properties.push(ObjectPropertyOrSpread::Spread(SpreadPattern { place })); + } + } + } + Ok(InstructionValue::ObjectExpression { properties, loc }) + } + oxc::Expression::ArrayExpression(arr) => { + let loc = builder.source_location(arr.span); + let mut elements: Vec = Vec::new(); + for element in &arr.elements { + match element { + oxc::ArrayExpressionElement::Elision(_) => { + elements.push(ArrayElement::Hole); + } + oxc::ArrayExpressionElement::SpreadElement(spread) => { + let place = lower_expression_to_temporary(builder, &spread.argument)?; + elements.push(ArrayElement::Spread(SpreadPattern { place })); + } + _ => { + let expr = element.to_expression(); + let place = lower_expression_to_temporary(builder, expr)?; + elements.push(ArrayElement::Place(place)); + } + } + } + Ok(InstructionValue::ArrayExpression { elements, loc }) + } + oxc::Expression::JSXElement(jsx_element) => lower_jsx_element_expr(builder, jsx_element), + oxc::Expression::JSXFragment(jsx_fragment) => { + lower_jsx_fragment_expr(builder, jsx_fragment) + } + oxc::Expression::ChainExpression(chain) => lower_chain_expression(builder, chain), + oxc::Expression::ArrowFunctionExpression(arrow) => lower_function_to_value( + builder, + FunctionNode::Arrow(arrow), + FunctionExpressionType::ArrowFunctionExpression, + ), + oxc::Expression::FunctionExpression(func) => lower_function_to_value( + builder, + FunctionNode::Function(func), + FunctionExpressionType::FunctionExpression, + ), + oxc::Expression::AssignmentExpression(assign) => { + lower_assignment_expression(builder, assign) + } + _ => { + // not-yet-ported arms bail to undefined (differential green-set grows as arms land) + let loc = builder.source_location(expr.span()); + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + } +} + +/// Lower an `AssignmentExpression`. Faithful translation of the original +/// `Expression::AssignmentExpression` arm, adapted to oxc's `AssignmentTarget` +/// split. `=` handles identifier / member / destructuring targets; compound +/// operators (`+=` etc.) handle identifier / member targets and bail on patterns. +fn lower_assignment_expression<'a>( + builder: &mut HirBuilder<'a, '_>, + assign: &'a oxc::AssignmentExpression<'a>, +) -> Result, CompilerError> { + let loc = builder.source_location(assign.span); + + if matches!(assign.operator, oxc::AssignmentOperator::Assign) { + match &assign.left { + oxc::AssignmentTarget::AssignmentTargetIdentifier(ident) => { + let start = ident.span.start; + let right = lower_expression_to_temporary(builder, &assign.right)?; + let ident_loc = builder.source_location(ident.span); + let binding = builder.resolve_identifier( + ident.name.as_str(), + start, + ident_loc.clone(), + Some(start), + )?; + match binding { + VariableBinding::Identifier { identifier, binding_kind } => { + if binding_kind == BindingKind::Const { + builder.record_error(CompilerErrorDetail { + reason: "Cannot reassign a `const` variable".to_string(), + category: ErrorCategory::Syntax, + loc: ident_loc.clone(), + description: Some(format!( + "`{}` is declared as const", + ident.name.as_str() + )), + suggestions: None, + })?; + return Ok(InstructionValue::Primitive { + value: PrimitiveValue::Undefined, + loc: ident_loc, + }); + } + let place = Place { + identifier, + reactive: false, + effect: Effect::Unknown, + loc: ident_loc, + }; + if builder.is_context_identifier(ident.name.as_str(), start, Some(start)) { + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreContext { + lvalue: LValue { + kind: InstructionKind::Reassign, + place: place.clone(), + }, + value: right, + loc: place.loc.clone(), + }, + )?; + Ok(InstructionValue::LoadLocal { place: temp.clone(), loc: temp.loc }) + } else { + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { + kind: InstructionKind::Reassign, + place: place.clone(), + }, + value: right, + type_annotation: None, + loc: place.loc.clone(), + }, + )?; + Ok(InstructionValue::LoadLocal { place: temp.clone(), loc: temp.loc }) + } + } + _ => { + let name = ident.name.to_string(); + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreGlobal { name, value: right, loc: ident_loc }, + )?; + Ok(InstructionValue::LoadLocal { place: temp.clone(), loc: temp.loc }) + } + } + } + oxc::AssignmentTarget::StaticMemberExpression(_) + | oxc::AssignmentTarget::ComputedMemberExpression(_) + | oxc::AssignmentTarget::PrivateFieldExpression(_) => { + let simple = assign.left.as_simple_assignment_target().unwrap(); + let right = lower_expression_to_temporary(builder, &assign.right)?; + let left_loc = builder.source_location(simple.span()); + let temp = match simple { + oxc::SimpleAssignmentTarget::StaticMemberExpression(member) => { + let object = lower_expression_to_temporary(builder, &member.object)?; + lower_value_to_temporary( + builder, + InstructionValue::PropertyStore { + object, + property: PropertyLiteral::String(member.property.name.to_string()), + value: right, + loc: left_loc, + }, + )? + } + oxc::SimpleAssignmentTarget::ComputedMemberExpression(member) => { + let object = lower_expression_to_temporary(builder, &member.object)?; + if let oxc::Expression::NumericLiteral(num) = &member.expression { + lower_value_to_temporary( + builder, + InstructionValue::PropertyStore { + object, + property: PropertyLiteral::Number(FloatValue::new(num.value)), + value: right, + loc: left_loc, + }, + )? + } else { + let prop = lower_expression_to_temporary(builder, &member.expression)?; + lower_value_to_temporary( + builder, + InstructionValue::ComputedStore { + object, + property: prop, + value: right, + loc: left_loc, + }, + )? + } + } + oxc::SimpleAssignmentTarget::PrivateFieldExpression(member) => { + // Babel modeled `a.#b = x` as a non-computed MemberExpression + // whose property is a PrivateName; the original fell to the + // generic property arm, lowering the PrivateName (which bails + // to an undefined temp) and emitting a ComputedStore. + let object = lower_expression_to_temporary(builder, &member.object)?; + let prop = lower_private_name_to_temporary(builder, member.field.span)?; + lower_value_to_temporary( + builder, + InstructionValue::ComputedStore { + object, + property: prop, + value: right, + loc: left_loc, + }, + )? + } + _ => unreachable!(), + }; + Ok(InstructionValue::LoadLocal { place: temp.clone(), loc: temp.loc }) + } + _ => { + // Destructuring assignment + let right = lower_expression_to_temporary(builder, &assign.right)?; + let left_loc = builder.source_location(assign.left.span()); + let result = lower_assignment_target( + builder, + left_loc, + InstructionKind::Reassign, + &assign.left, + right.clone(), + AssignmentStyle::Destructure, + )?; + match result { + Some(place) => { + Ok(InstructionValue::LoadLocal { place: place.clone(), loc: place.loc }) + } + None => Ok(InstructionValue::LoadLocal { place: right, loc }), + } + } + } + } else { + // Compound assignment operators + let binary_op = match assign.operator { + oxc::AssignmentOperator::Addition => Some(BinaryOperator::Add), + oxc::AssignmentOperator::Subtraction => Some(BinaryOperator::Subtract), + oxc::AssignmentOperator::Multiplication => Some(BinaryOperator::Multiply), + oxc::AssignmentOperator::Division => Some(BinaryOperator::Divide), + oxc::AssignmentOperator::Remainder => Some(BinaryOperator::Modulo), + oxc::AssignmentOperator::Exponential => Some(BinaryOperator::Exponent), + oxc::AssignmentOperator::ShiftLeft => Some(BinaryOperator::ShiftLeft), + oxc::AssignmentOperator::ShiftRight => Some(BinaryOperator::ShiftRight), + oxc::AssignmentOperator::ShiftRightZeroFill => Some(BinaryOperator::UnsignedShiftRight), + oxc::AssignmentOperator::BitwiseOR => Some(BinaryOperator::BitwiseOr), + oxc::AssignmentOperator::BitwiseXOR => Some(BinaryOperator::BitwiseXor), + oxc::AssignmentOperator::BitwiseAnd => Some(BinaryOperator::BitwiseAnd), + oxc::AssignmentOperator::LogicalOr + | oxc::AssignmentOperator::LogicalAnd + | oxc::AssignmentOperator::LogicalNullish => { + builder.record_error(CompilerErrorDetail { + reason: "Logical assignment operators (||=, &&=, ??=) are not yet supported" + .to_string(), + category: ErrorCategory::Todo, + loc: loc.clone(), + description: None, + suggestions: None, + })?; + return Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }); + } + oxc::AssignmentOperator::Assign => unreachable!(), + }; + let binary_op = match binary_op { + Some(op) => op, + None => { + return Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }); + } + }; + + match &assign.left { + oxc::AssignmentTarget::AssignmentTargetIdentifier(ident) => { + let start = ident.span.start; + let ident_loc = builder.source_location(ident.span); + // Read the current value through a LoadLocal/LoadContext temporary + // (as the original did via `lower_expression_to_temporary`), NOT the + // bare binding place. Without the load instruction, ConstantPropagation + // can't propagate a known value into the compound op, and the missing + // temporary shifts IdentifierId numbering away from the baseline. + let read_place = lower_identifier( + builder, + ident.name.as_str(), + start, + ident_loc.clone(), + Some(start), + )?; + let read_value = + if builder.is_context_identifier(ident.name.as_str(), start, Some(start)) { + InstructionValue::LoadContext { place: read_place, loc: ident_loc.clone() } + } else { + InstructionValue::LoadLocal { place: read_place, loc: ident_loc.clone() } + }; + let left_place = lower_value_to_temporary(builder, read_value)?; + let right = lower_expression_to_temporary(builder, &assign.right)?; + let binary_place = lower_value_to_temporary( + builder, + InstructionValue::BinaryExpression { + operator: binary_op, + left: left_place, + right, + loc: loc.clone(), + }, + )?; + let binding = builder.resolve_identifier( + ident.name.as_str(), + start, + ident_loc.clone(), + Some(start), + )?; + match binding { + VariableBinding::Identifier { identifier, .. } => { + let place = Place { + identifier, + reactive: false, + effect: Effect::Unknown, + loc: ident_loc, + }; + if builder.is_context_identifier(ident.name.as_str(), start, Some(start)) { + lower_value_to_temporary( + builder, + InstructionValue::StoreContext { + lvalue: LValue { + kind: InstructionKind::Reassign, + place: place.clone(), + }, + value: binary_place, + loc: loc.clone(), + }, + )?; + Ok(InstructionValue::LoadContext { place, loc }) + } else { + lower_value_to_temporary( + builder, + InstructionValue::StoreLocal { + lvalue: LValue { + kind: InstructionKind::Reassign, + place: place.clone(), + }, + value: binary_place, + type_annotation: None, + loc: loc.clone(), + }, + )?; + Ok(InstructionValue::LoadLocal { place, loc }) + } + } + _ => { + let name = ident.name.to_string(); + let temp = lower_value_to_temporary( + builder, + InstructionValue::StoreGlobal { + name, + value: binary_place, + loc: loc.clone(), + }, + )?; + Ok(InstructionValue::LoadLocal { place: temp.clone(), loc: temp.loc }) + } + } + } + oxc::AssignmentTarget::StaticMemberExpression(_) + | oxc::AssignmentTarget::ComputedMemberExpression(_) + | oxc::AssignmentTarget::PrivateFieldExpression(_) => { + let simple = assign.left.as_simple_assignment_target().unwrap(); + let member_loc = builder.source_location(simple.span()); + let lowered = lower_member_expression_from_simple_target(builder, simple)?; + let object = lowered.object; + let lowered_property = lowered.property; + let current_value = lower_value_to_temporary(builder, lowered.value)?; + let right = lower_expression_to_temporary(builder, &assign.right)?; + let result = lower_value_to_temporary( + builder, + InstructionValue::BinaryExpression { + operator: binary_op, + left: current_value, + right, + loc: member_loc.clone(), + }, + )?; + match lowered_property { + MemberProperty::Literal(prop_literal) => Ok(InstructionValue::PropertyStore { + object, + property: prop_literal, + value: result, + loc: member_loc, + }), + MemberProperty::Computed(prop_place) => Ok(InstructionValue::ComputedStore { + object, + property: prop_place, + value: result, + loc: member_loc, + }), + } + } + _ => { + builder.record_error(CompilerErrorDetail { + reason: "Compound assignment to complex pattern is not yet supported" + .to_string(), + category: ErrorCategory::Todo, + loc: loc.clone(), + description: None, + suggestions: None, + })?; + Ok(InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }) + } + } + } +} + +/// Lower a JSX element expression. Faithful translation of the original Babel +/// `Expression::JSXElement` arm, adapted to oxc's JSX shapes. +/// +/// fbt note: the original tracked fbt/fbs sub-tags (`collect_fbt_sub_tags`) and +/// reported duplicates, and incremented `builder.fbt_depth` around the children so +/// JSX text whitespace is preserved within fbt subtrees. Both behaviors are ported +/// below. +fn lower_jsx_element_expr<'a>( + builder: &mut HirBuilder<'a, '_>, + jsx_element: &'a oxc::JSXElement<'a>, +) -> Result, CompilerError> { + let loc = builder.source_location(jsx_element.span); + let opening_loc = builder.source_location(jsx_element.opening_element.span); + let closing_loc = + jsx_element.closing_element.as_ref().and_then(|c| builder.source_location(c.span)); + + // Lower the tag name + let tag = lower_jsx_element_name(builder, &jsx_element.opening_element.name)?; + + // Lower attributes (props) + let mut props: Vec = Vec::new(); + for attr_item in &jsx_element.opening_element.attributes { + match attr_item { + oxc::JSXAttributeItem::SpreadAttribute(spread) => { + let argument = lower_expression_to_temporary(builder, &spread.argument)?; + props.push(JsxAttribute::SpreadAttribute { argument }); + } + oxc::JSXAttributeItem::Attribute(attr) => { + // Get the attribute name + let prop_name = match &attr.name { + oxc::JSXAttributeName::Identifier(id) => { + let name = id.name.as_str(); + if name.contains(':') { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: format!( + "(BuildHIR::lowerExpression) Unexpected colon in attribute name `{}`", + name + ), + description: None, + loc: builder.source_location(id.span), + suggestions: None, + })?; + } + name.to_string() + } + oxc::JSXAttributeName::NamespacedName(ns) => { + format!("{}:{}", ns.namespace.name, ns.name.name) + } + }; + + // Get the attribute value + let value = match &attr.value { + Some(oxc::JSXAttributeValue::StringLiteral(s)) => { + let str_loc = builder.source_location(s.span); + lower_value_to_temporary( + builder, + InstructionValue::Primitive { + value: PrimitiveValue::String( + decode_jsx_entities(s.value.as_str()).into(), + ), + loc: str_loc, + }, + )? + } + Some(oxc::JSXAttributeValue::ExpressionContainer(container)) => { + match &container.expression { + oxc::JSXExpression::EmptyExpression(_) => { + // Empty expression container - skip this attribute + continue; + } + other => { + let expr = other + .as_expression() + .expect("non-empty JSX expression is an expression"); + lower_expression_to_temporary(builder, expr)? + } + } + } + Some(oxc::JSXAttributeValue::Element(el)) => { + let val = lower_jsx_element_expr(builder, el)?; + lower_value_to_temporary(builder, val)? + } + Some(oxc::JSXAttributeValue::Fragment(frag)) => { + let val = lower_jsx_fragment_expr(builder, frag)?; + lower_value_to_temporary(builder, val)? + } + None => { + // No value means boolean true (e.g.,
) + let attr_loc = builder.source_location(attr.span); + lower_value_to_temporary( + builder, + InstructionValue::Primitive { + value: PrimitiveValue::Boolean(true), + loc: attr_loc, + }, + )? + } + }; + + props.push(JsxAttribute::Attribute { name: prop_name, place: value }); + } + } + } + + // Check if this is an fbt/fbs tag, which requires special whitespace handling + let is_fbt = matches!(&tag, JsxTag::Builtin(b) if b.name == "fbt" || b.name == "fbs"); + + // Check that fbt/fbs tags are module-level imports, not local bindings. + // Matches TS: CompilerError.invariant(tagIdentifier.kind !== 'Identifier', ...) + if is_fbt { + let tag_name = match &tag { + JsxTag::Builtin(b) => b.name.clone(), + _ => "fbt".to_string(), + }; + // Get the opening element's name identifier and check if it's a local binding. + let jsx_id_name = match &jsx_element.opening_element.name { + oxc::JSXElementName::Identifier(id) => Some((id.name.as_str(), id.span)), + oxc::JSXElementName::IdentifierReference(id) => Some((id.name.as_str(), id.span)), + _ => None, + }; + if let Some((name, span)) = jsx_id_name { + let id_loc = builder.source_location(span); + // Check if fbt/fbs tag name resolves to a local binding. + // JSX identifiers may not be in our position-based reference map, + // so check if ANY binding with this name exists in the function scope. + let is_local_binding = builder.has_local_binding(name); + if is_local_binding { + // Record as a Diagnostic (not ErrorDetail) to match TS behavior + // where CompilerError.invariant creates a CompilerDiagnostic. + let reason = format!("<{}> tags should be module-level imports", tag_name); + return Err(CompilerDiagnostic::new(ErrorCategory::Invariant, &reason, None) + .with_detail(CompilerDiagnosticDetail::Error { + loc: id_loc.clone(), + message: Some(reason.clone()), + identifier_name: None, + }) + .into()); + } + } + } + + // Check for duplicate fbt:enum, fbt:plural, fbt:pronoun tags. + if is_fbt { + let tag_name = match &tag { + JsxTag::Builtin(b) => b.name.as_str(), + _ => "fbt", + }; + let mut enum_locs: Vec> = Vec::new(); + let mut plural_locs: Vec> = Vec::new(); + let mut pronoun_locs: Vec> = Vec::new(); + collect_fbt_sub_tags( + builder, + &jsx_element.children, + tag_name, + &mut enum_locs, + &mut plural_locs, + &mut pronoun_locs, + ); + + for (name, locations) in + [("enum", &enum_locs), ("plural", &plural_locs), ("pronoun", &pronoun_locs)] + { + if locations.len() > 1 { + let details: Vec = locations + .iter() + .map(|loc| CompilerDiagnosticDetail::Error { + message: Some(format!("Multiple `<{}:{}>` tags found", tag_name, name)), + loc: loc.clone(), + identifier_name: None, + }) + .collect(); + let mut diag = CompilerDiagnostic::new( + ErrorCategory::Todo, + "Support duplicate fbt tags", + Some(format!( + "Support `<{}>` tags with multiple `<{}:{}>` values", + tag_name, tag_name, name + )), + ); + diag.details = details; + builder.environment_mut().record_diagnostic(diag); + } + } + } + + // Increment fbt counter before traversing into children, as whitespace + // in jsx text is handled differently for fbt subtrees. + if is_fbt { + builder.fbt_depth += 1; + } + + // Lower children + let children: Vec = jsx_element + .children + .iter() + .map(|child| lower_jsx_element(builder, child)) + .collect::, _>>()? + .into_iter() + .flatten() + .collect(); + + if is_fbt { + builder.fbt_depth -= 1; + } + + Ok(InstructionValue::JsxExpression { + tag, + props, + children: if children.is_empty() { None } else { Some(children) }, + loc, + opening_loc, + closing_loc, + }) +} + +/// Lower a JSX fragment expression. Faithful translation of the original +/// `Expression::JSXFragment` arm. +fn lower_jsx_fragment_expr<'a>( + builder: &mut HirBuilder<'a, '_>, + jsx_fragment: &'a oxc::JSXFragment<'a>, +) -> Result, CompilerError> { + let loc = builder.source_location(jsx_fragment.span); + + // Lower children + let children: Vec = jsx_fragment + .children + .iter() + .map(|child| lower_jsx_element(builder, child)) + .collect::, _>>()? + .into_iter() + .flatten() + .collect(); + + Ok(InstructionValue::JsxFragment { children, loc }) +} + +/// Lower a JSX element name into a `JsxTag`. Faithful translation of the original +/// `lower_jsx_element_name`, adapted to oxc's `JSXElementName` shape (which splits +/// out `IdentifierReference`, `MemberExpression`, and `ThisExpression`; the latter +/// maps to the identifier `"this"`). +fn lower_jsx_element_name( + builder: &mut HirBuilder<'_, '_>, + name: &oxc::JSXElementName, +) -> Result { + // Lower a simple JSX tag identifier (component-vs-builtin split on case). + fn lower_tag_identifier( + builder: &mut HirBuilder<'_, '_>, + tag: &str, + span: oxc_span::Span, + ) -> Result { + let loc = builder.source_location(span); + let start = span.start; + if tag.starts_with(|c: char| c.is_ascii_uppercase()) { + // Component tag: resolve as identifier and load + let place = lower_identifier(builder, tag, start, loc.clone(), Some(start))?; + let load_value = if builder.is_context_identifier(tag, start, Some(start)) { + InstructionValue::LoadContext { place, loc } + } else { + InstructionValue::LoadLocal { place, loc } + }; + let temp = lower_value_to_temporary(builder, load_value)?; + Ok(JsxTag::Place(temp)) + } else { + // Builtin HTML tag + Ok(JsxTag::Builtin(BuiltinTag { name: tag.to_string(), loc })) + } + } + + match name { + oxc::JSXElementName::Identifier(id) => { + lower_tag_identifier(builder, id.name.as_str(), id.span) + } + oxc::JSXElementName::IdentifierReference(id) => { + lower_tag_identifier(builder, id.name.as_str(), id.span) + } + oxc::JSXElementName::ThisExpression(this) => { + // ``-style `this` tag lowers as the identifier "this". + lower_tag_identifier(builder, "this", this.span) + } + oxc::JSXElementName::MemberExpression(member) => { + let place = lower_jsx_member_expression(builder, member)?; + Ok(JsxTag::Place(place)) + } + oxc::JSXElementName::NamespacedName(ns) => { + let namespace = ns.namespace.name.as_str(); + let name = ns.name.name.as_str(); + let tag = format!("{}:{}", namespace, name); + let loc = builder.source_location(ns.span); + if namespace.contains(':') || name.contains(':') { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Syntax, + reason: "Expected JSXNamespacedName to have no colons in the namespace or name" + .to_string(), + description: Some(format!("Got `{}` : `{}`", namespace, name)), + loc: loc.clone(), + suggestions: None, + })?; + } + let place = lower_value_to_temporary( + builder, + InstructionValue::Primitive { + value: PrimitiveValue::String(tag.into()), + loc: loc.clone(), + }, + )?; + Ok(JsxTag::Place(place)) + } + } +} + +/// Lower a JSX member expression tag (``) into a `Place`. Faithful +/// translation of the original `lower_jsx_member_expression`, adapted to oxc's +/// `JSXMemberExpressionObject` (where the leaf object may be a `ThisExpression`, +/// which lowers as the identifier `"this"`). +fn lower_jsx_member_expression( + builder: &mut HirBuilder<'_, '_>, + expr: &oxc::JSXMemberExpression, +) -> Result { + // Use the full member expression's loc for instruction locs (matching TS: exprPath.node.loc) + let expr_loc = builder.source_location(expr.span); + let object = match &expr.object { + oxc::JSXMemberExpressionObject::IdentifierReference(id) => { + lower_jsx_member_object_identifier(builder, id.name.as_str(), id.span, &expr_loc)? + } + oxc::JSXMemberExpressionObject::ThisExpression(this) => { + lower_jsx_member_object_identifier(builder, "this", this.span, &expr_loc)? + } + oxc::JSXMemberExpressionObject::MemberExpression(inner) => { + lower_jsx_member_expression(builder, inner)? + } + }; + let prop_name = expr.property.name.as_str(); + let value = InstructionValue::PropertyLoad { + object, + property: PropertyLiteral::String(prop_name.to_string()), + loc: expr_loc, + }; + lower_value_to_temporary(builder, value) +} + +/// Lower the leaf identifier of a JSX member expression object. Uses the +/// identifier's own loc for the place, but the enclosing member expression's loc +/// for the load instruction (matching TS). +fn lower_jsx_member_object_identifier( + builder: &mut HirBuilder<'_, '_>, + name: &str, + span: oxc_span::Span, + expr_loc: &Option, +) -> Result { + let id_loc = builder.source_location(span); + let start = span.start; + let place = lower_identifier(builder, name, start, id_loc, Some(start))?; + let load_value = if builder.is_context_identifier(name, start, Some(start)) { + InstructionValue::LoadContext { place, loc: expr_loc.clone() } + } else { + InstructionValue::LoadLocal { place, loc: expr_loc.clone() } + }; + lower_value_to_temporary(builder, load_value) +} + +/// Lower a single JSX child into an optional `Place`. Faithful translation of the +/// original `lower_jsx_element` (the JSXChild handler), adapted to oxc's `JSXChild`. +fn lower_jsx_element<'a>( + builder: &mut HirBuilder<'a, '_>, + child: &'a oxc::JSXChild<'a>, +) -> Result, CompilerError> { + match child { + oxc::JSXChild::Text(text) => { + // oxc keeps JSX text raw; decode entities first so the value matches + // Babel's `JSXText.value` (the Babel bridge decoded in convert_ast). + let decoded = decode_jsx_entities(text.value.as_str()); + // FBT whitespace normalization differs from standard JSX. + // Since the fbt transform runs after, preserve all whitespace + // in FBT subtrees as is. + let value = if builder.fbt_depth > 0 { Some(decoded) } else { trim_jsx_text(&decoded) }; + match value { + None => Ok(None), + Some(value) => { + let loc = builder.source_location(text.span); + let place = lower_value_to_temporary( + builder, + InstructionValue::JSXText { value, loc }, + )?; + Ok(Some(place)) + } + } + } + oxc::JSXChild::Element(element) => { + let value = lower_jsx_element_expr(builder, element)?; + Ok(Some(lower_value_to_temporary(builder, value)?)) + } + oxc::JSXChild::Fragment(fragment) => { + let value = lower_jsx_fragment_expr(builder, fragment)?; + Ok(Some(lower_value_to_temporary(builder, value)?)) + } + oxc::JSXChild::ExpressionContainer(container) => match &container.expression { + oxc::JSXExpression::EmptyExpression(_) => Ok(None), + other => { + let expr = + other.as_expression().expect("non-empty JSX expression is an expression"); + Ok(Some(lower_expression_to_temporary(builder, expr)?)) + } + }, + oxc::JSXChild::Spread(spread) => { + Ok(Some(lower_expression_to_temporary(builder, &spread.expression)?)) + } + } +} + +/// Recursively collect the locations of ``, ``, and +/// `` sub-tags within fbt/fbs children. Faithful translation of the +/// original Babel `collect_fbt_sub_tags`, adapted to oxc's JSX shapes. +fn collect_fbt_sub_tags( + builder: &HirBuilder<'_, '_>, + children: &[oxc::JSXChild], + tag_name: &str, + enum_locs: &mut Vec>, + plural_locs: &mut Vec>, + pronoun_locs: &mut Vec>, +) { + for child in children { + match child { + oxc::JSXChild::Element(el) => { + collect_fbt_sub_tags_from_element( + builder, + el, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + oxc::JSXChild::Fragment(frag) => { + collect_fbt_sub_tags( + builder, + &frag.children, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + oxc::JSXChild::ExpressionContainer(container) => { + if let Some(expr) = container.expression.as_expression() { + collect_fbt_sub_tags_from_expr( + builder, + expr, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + } + _ => {} + } + } +} + +fn collect_fbt_sub_tags_from_element( + builder: &HirBuilder<'_, '_>, + el: &oxc::JSXElement, + tag_name: &str, + enum_locs: &mut Vec>, + plural_locs: &mut Vec>, + pronoun_locs: &mut Vec>, +) { + if let oxc::JSXElementName::NamespacedName(ns) = &el.opening_element.name { + if ns.namespace.name == tag_name { + let loc = builder.source_location(ns.span); + match ns.name.name.as_str() { + "enum" => enum_locs.push(loc), + "plural" => plural_locs.push(loc), + "pronoun" => pronoun_locs.push(loc), + _ => {} + } + } + } + collect_fbt_sub_tags(builder, &el.children, tag_name, enum_locs, plural_locs, pronoun_locs); + // Also traverse JSX attributes (matching TS expr.traverse which visits all nodes) + for attr in &el.opening_element.attributes { + if let oxc::JSXAttributeItem::Attribute(a) = attr { + match &a.value { + Some(oxc::JSXAttributeValue::ExpressionContainer(container)) => { + if let Some(expr) = container.expression.as_expression() { + collect_fbt_sub_tags_from_expr( + builder, + expr, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + } + Some(oxc::JSXAttributeValue::Element(nested)) => { + collect_fbt_sub_tags_from_element( + builder, + nested, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + _ => {} + } + } + } +} + +fn collect_fbt_sub_tags_from_expr( + builder: &HirBuilder<'_, '_>, + expr: &oxc::Expression, + tag_name: &str, + enum_locs: &mut Vec>, + plural_locs: &mut Vec>, + pronoun_locs: &mut Vec>, +) { + match expr { + oxc::Expression::JSXElement(el) => { + collect_fbt_sub_tags_from_element( + builder, + el, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + oxc::Expression::JSXFragment(frag) => { + collect_fbt_sub_tags( + builder, + &frag.children, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + oxc::Expression::ConditionalExpression(cond) => { + collect_fbt_sub_tags_from_expr( + builder, + &cond.consequent, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + collect_fbt_sub_tags_from_expr( + builder, + &cond.alternate, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + oxc::Expression::LogicalExpression(log) => { + collect_fbt_sub_tags_from_expr( + builder, + &log.left, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + collect_fbt_sub_tags_from_expr( + builder, + &log.right, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + oxc::Expression::ParenthesizedExpression(paren) => { + collect_fbt_sub_tags_from_expr( + builder, + &paren.expression, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + oxc::Expression::ArrowFunctionExpression(arrow) => { + if arrow.expression { + if let Some(oxc::Statement::ExpressionStatement(es)) = arrow.body.statements.first() + { + collect_fbt_sub_tags_from_expr( + builder, + &es.expression, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + } else { + collect_fbt_sub_tags_from_stmts( + builder, + &arrow.body.statements, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + } + oxc::Expression::CallExpression(call) => { + for arg in &call.arguments { + if let Some(arg_expr) = arg.as_expression() { + collect_fbt_sub_tags_from_expr( + builder, + arg_expr, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + } + } + _ => {} + } +} + +fn collect_fbt_sub_tags_from_stmts( + builder: &HirBuilder<'_, '_>, + stmts: &[oxc::Statement], + tag_name: &str, + enum_locs: &mut Vec>, + plural_locs: &mut Vec>, + pronoun_locs: &mut Vec>, +) { + for stmt in stmts { + match stmt { + oxc::Statement::ReturnStatement(ret) => { + if let Some(arg) = &ret.argument { + collect_fbt_sub_tags_from_expr( + builder, + arg, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + } + oxc::Statement::ExpressionStatement(expr_stmt) => { + collect_fbt_sub_tags_from_expr( + builder, + &expr_stmt.expression, + tag_name, + enum_locs, + plural_locs, + pronoun_locs, + ); + } + _ => {} + } + } +} + +/// Split a string on line endings, handling \r\n, \n, and \r. +fn split_line_endings(s: &str) -> Vec<&str> { + let mut lines = Vec::new(); + let mut start = 0; + let bytes = s.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'\r' { + lines.push(&s[start..i]); + if i + 1 < bytes.len() && bytes[i + 1] == b'\n' { + i += 2; + } else { + i += 1; + } + start = i; + } else if bytes[i] == b'\n' { + lines.push(&s[start..i]); + i += 1; + start = i; + } else { + i += 1; + } + } + lines.push(&s[start..]); + lines +} + +/// Trims whitespace according to the JSX spec. +/// Implementation ported from Babel's cleanJSXElementLiteralChild. +fn trim_jsx_text(original: &str) -> Option { + // Split on \r\n, \n, or \r to handle all line ending styles (matching TS split(/\r\n|\n|\r/)) + let lines: Vec<&str> = split_line_endings(original); + + // NOTE: when builder.fbt_depth > 0, the TS skips whitespace trimming entirely. + // That check is handled by the caller (lower_jsx_element) before calling this function. + + let mut last_non_empty_line = 0; + for (i, line) in lines.iter().enumerate() { + if line.contains(|c: char| c != ' ' && c != '\t') { + last_non_empty_line = i; + } + } + + let mut str = String::new(); + + for (i, line) in lines.iter().enumerate() { + let is_first_line = i == 0; + let is_last_line = i == lines.len() - 1; + let is_last_non_empty_line = i == last_non_empty_line; + + // Replace rendered whitespace tabs with spaces + let mut trimmed_line = line.replace('\t', " "); + + // Trim whitespace touching a newline (leading whitespace on non-first lines) + if !is_first_line { + trimmed_line = trimmed_line.trim_start_matches(' ').to_string(); + } + + // Trim whitespace touching an endline (trailing whitespace on non-last lines) + if !is_last_line { + trimmed_line = trimmed_line.trim_end_matches(' ').to_string(); + } + + if !trimmed_line.is_empty() { + if !is_last_non_empty_line { + trimmed_line.push(' '); + } + str.push_str(&trimmed_line); + } + } + + if str.is_empty() { None } else { Some(str) } +} + +/// Decode XML/HTML entities in JSX text (`&` → `&`, `>` → `>`, `{` +/// → `{`, `😀` → emoji, …) so the lowered JSX text/attribute value matches +/// Babel's decoded text. oxc keeps JSX text raw in the AST. Mirrors the +/// `decode_jsx_entities` helper in `convert_ast.rs`. Unrecognized `&…;` sequences +/// are kept verbatim. +pub(crate) fn decode_jsx_entities(s: &str) -> String { + if !s.contains('&') { + return s.to_string(); + } + let mut out = String::with_capacity(s.len()); + let mut chars = s.char_indices(); + let mut prev = 0; + while let Some((i, c)) = chars.next() { + if c != '&' { + continue; + } + let mut start = i; + let mut end = None; + for (j, c) in chars.by_ref() { + if c == ';' { + end = Some(j); + break; + } else if c == '&' { + start = j; + } + } + let Some(end) = end else { break }; + out.push_str(&s[prev..start]); + prev = end + 1; + let word = &s[start + 1..end]; + let decoded = if let Some(num) = word.strip_prefix('#') { + if let Some(hex) = num.strip_prefix(['x', 'X']) { + u32::from_str_radix(hex, 16).ok().and_then(char::from_u32) + } else { + num.parse::().ok().and_then(char::from_u32) + } + } else { + oxc_syntax::xml_entities::XML_ENTITIES.get(word).copied() + }; + match decoded { + Some(c) => out.push(c), + // Not a recognized entity — keep the `&…;` literal. + None => { + out.push('&'); + out.push_str(word); + out.push(';'); + } + } + } + out.push_str(&s[prev..]); + out +} + +/// Get the Babel-style type name of an oxc `Expression` node. Mirrors the +/// original `expression_type_name` (which read Babel-shaped variants), mapping +/// oxc's split member/chain shapes back to the Babel names the original emitted +/// (e.g. `StaticMemberExpression`/`ComputedMemberExpression`/`PrivateFieldExpression` +/// → "MemberExpression"; `ChainExpression` → "OptionalMemberExpression"). +fn expression_type_name(expr: &oxc::Expression) -> &'static str { + match expr { + oxc::Expression::Identifier(_) => "Identifier", + oxc::Expression::StringLiteral(_) => "StringLiteral", + oxc::Expression::NumericLiteral(_) => "NumericLiteral", + oxc::Expression::BooleanLiteral(_) => "BooleanLiteral", + oxc::Expression::NullLiteral(_) => "NullLiteral", + oxc::Expression::BigIntLiteral(_) => "BigIntLiteral", + oxc::Expression::RegExpLiteral(_) => "RegExpLiteral", + oxc::Expression::CallExpression(_) => "CallExpression", + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) => "MemberExpression", + oxc::Expression::ChainExpression(c) => match &c.expression { + oxc::ChainElement::CallExpression(_) => "OptionalCallExpression", + _ => "OptionalMemberExpression", + }, + oxc::Expression::BinaryExpression(_) => "BinaryExpression", + oxc::Expression::PrivateInExpression(_) => "BinaryExpression", + oxc::Expression::LogicalExpression(_) => "LogicalExpression", + oxc::Expression::UnaryExpression(_) => "UnaryExpression", + oxc::Expression::UpdateExpression(_) => "UpdateExpression", + oxc::Expression::ConditionalExpression(_) => "ConditionalExpression", + oxc::Expression::AssignmentExpression(_) => "AssignmentExpression", + oxc::Expression::SequenceExpression(_) => "SequenceExpression", + oxc::Expression::ArrowFunctionExpression(_) => "ArrowFunctionExpression", + oxc::Expression::FunctionExpression(_) => "FunctionExpression", + oxc::Expression::ObjectExpression(_) => "ObjectExpression", + oxc::Expression::ArrayExpression(_) => "ArrayExpression", + oxc::Expression::NewExpression(_) => "NewExpression", + oxc::Expression::TemplateLiteral(_) => "TemplateLiteral", + oxc::Expression::TaggedTemplateExpression(_) => "TaggedTemplateExpression", + oxc::Expression::AwaitExpression(_) => "AwaitExpression", + oxc::Expression::YieldExpression(_) => "YieldExpression", + oxc::Expression::MetaProperty(_) => "MetaProperty", + oxc::Expression::ClassExpression(_) => "ClassExpression", + oxc::Expression::Super(_) => "Super", + oxc::Expression::ImportExpression(_) => "Import", + oxc::Expression::ThisExpression(_) => "ThisExpression", + oxc::Expression::ParenthesizedExpression(_) => "ParenthesizedExpression", + oxc::Expression::JSXElement(_) => "JSXElement", + oxc::Expression::JSXFragment(_) => "JSXFragment", + oxc::Expression::TSAsExpression(_) => "TSAsExpression", + oxc::Expression::TSSatisfiesExpression(_) => "TSSatisfiesExpression", + oxc::Expression::TSNonNullExpression(_) => "TSNonNullExpression", + oxc::Expression::TSTypeAssertion(_) => "TSTypeAssertion", + oxc::Expression::TSInstantiationExpression(_) => "TSInstantiationExpression", + oxc::Expression::V8IntrinsicExpression(_) => "V8IntrinsicExpression", + } +} + +/// Lower an oxc object getter/setter/method (`ObjectProperty` whose value is a +/// `FunctionExpression`). Faithful to the original `lower_object_method`: +/// `get`/`set` record a Todo error and are skipped. The `method` case lowers the +/// key and the nested function (`lower_function_for_object_method`) and emits an +/// `ObjectMethod` instruction value. +fn lower_object_method<'a>( + builder: &mut HirBuilder<'a, '_>, + method: &'a oxc::ObjectProperty<'a>, +) -> Result, CompilerError> { + // In oxc, a shorthand method is encoded as `kind: Init, method: true`; only + // getters/setters carry a non-`Init` `PropertyKind`. + let is_method = method.method && matches!(method.kind, oxc::PropertyKind::Init); + if !is_method { + let kind_str = match method.kind { + oxc::PropertyKind::Get => "get", + oxc::PropertyKind::Set => "set", + oxc::PropertyKind::Init => "method", + }; + builder.record_error(CompilerErrorDetail { + reason: format!( + "(BuildHIR::lowerExpression) Handle {} functions in ObjectExpression", + kind_str + ), + category: ErrorCategory::Todo, + loc: builder.source_location(method.span), + description: None, + suggestions: None, + })?; + return Ok(None); + } + + let key = lower_object_property_key(builder, &method.key, method.computed)? + .unwrap_or(ObjectPropertyKey::String { name: String::new() }); + + let func = match &method.value { + oxc::Expression::FunctionExpression(func) => func, + _ => unreachable!("object method value is always a FunctionExpression in oxc"), + }; + let body = func.body.as_ref().expect("object method always has a body"); + let lowered_func = lower_function_for_object_method( + builder, + method.span, + &func.params, + body, + func.generator, + func.r#async, + )?; + + let loc = builder.source_location(method.span); + let method_value = InstructionValue::ObjectMethod { loc, lowered_func }; + let method_place = lower_value_to_temporary(builder, method_value)?; + + Ok(Some(ObjectProperty { key, property_type: ObjectPropertyType::Method, place: method_place })) +} + +/// Lower an object property key. Faithful to the original `lower_object_property_key`. +fn lower_object_property_key<'a>( + builder: &mut HirBuilder<'a, '_>, + key: &'a oxc::PropertyKey<'a>, + computed: bool, +) -> Result, CompilerError> { + match key { + // Property keys stay String-typed; oxc atoms are valid UTF-8, so + // `to_string()` reproduces the marker wire form for non-pathological keys. + oxc::PropertyKey::StringLiteral(lit) => { + Ok(Some(ObjectPropertyKey::String { name: lit.value.to_string() })) + } + oxc::PropertyKey::StaticIdentifier(ident) if !computed => { + Ok(Some(ObjectPropertyKey::Identifier { name: ident.name.to_string() })) + } + oxc::PropertyKey::Identifier(ident) if !computed => { + Ok(Some(ObjectPropertyKey::Identifier { name: ident.name.to_string() })) + } + oxc::PropertyKey::NumericLiteral(lit) if !computed => { + Ok(Some(ObjectPropertyKey::Identifier { name: lit.value.to_string() })) + } + _ if computed => { + let place = lower_expression_to_temporary(builder, key.to_expression())?; + Ok(Some(ObjectPropertyKey::Computed { name: place })) + } + _ => { + let loc = match key { + oxc::PropertyKey::StaticIdentifier(i) => builder.source_location(i.span), + oxc::PropertyKey::Identifier(i) => builder.source_location(i.span), + _ => None, + }; + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "Unsupported key type in ObjectExpression".to_string(), + description: None, + loc, + suggestions: None, + })?; + Ok(None) + } + } +} + +/// Lower a reorderable expression. Faithful to the original +/// `lower_reorderable_expression`: records an error when the expression cannot be +/// safely reordered, then lowers it to a temporary regardless. +fn lower_reorderable_expression<'a>( + builder: &mut HirBuilder<'a, '_>, + expr: &'a oxc::Expression<'a>, +) -> Result { + if !is_reorderable_expression(builder, expr, true) { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: format!( + "(BuildHIR::node.lowerReorderableExpression) Expression type `{}` cannot be safely reordered", + expression_type_name(expr) + ), + description: None, + loc: builder.source_location(expr.span()), + suggestions: None, + })?; + } + lower_expression_to_temporary(builder, expr) +} + +/// Faithful to the original `is_reorderable_expression`. oxc's split member +/// shapes (Static/Computed/PrivateField) map to the original `MemberExpression` +/// arm; optional chains (`ChainExpression`) were not handled by the original +/// (`OptionalMemberExpression` fell to `_ => false`), so they return false here. +fn is_reorderable_expression( + builder: &HirBuilder<'_, '_>, + expr: &oxc::Expression, + allow_local_identifiers: bool, +) -> bool { + match expr { + oxc::Expression::Identifier(ident) => { + let binding = builder.scope_info().resolve_reference_for_node(Some(ident.span.start)); + match binding { + None => { + // global, safe to reorder + true + } + Some(b) => { + if b.scope == builder.scope_info().program_scope { + // Module-scope binding (ModuleLocal, imports), safe to reorder + true + } else { + allow_local_identifiers + } + } + } + } + oxc::Expression::RegExpLiteral(_) + | oxc::Expression::StringLiteral(_) + | oxc::Expression::NumericLiteral(_) + | oxc::Expression::NullLiteral(_) + | oxc::Expression::BooleanLiteral(_) + | oxc::Expression::BigIntLiteral(_) => true, + oxc::Expression::UnaryExpression(unary) => { + matches!( + unary.operator, + oxc::UnaryOperator::LogicalNot + | oxc::UnaryOperator::UnaryPlus + | oxc::UnaryOperator::UnaryNegation + ) && is_reorderable_expression(builder, &unary.argument, allow_local_identifiers) + } + oxc::Expression::LogicalExpression(logical) => { + is_reorderable_expression(builder, &logical.left, allow_local_identifiers) + && is_reorderable_expression(builder, &logical.right, allow_local_identifiers) + } + oxc::Expression::ConditionalExpression(cond) => { + is_reorderable_expression(builder, &cond.test, allow_local_identifiers) + && is_reorderable_expression(builder, &cond.consequent, allow_local_identifiers) + && is_reorderable_expression(builder, &cond.alternate, allow_local_identifiers) + } + oxc::Expression::ArrayExpression(arr) => arr.elements.iter().all(|element| match element { + oxc::ArrayExpressionElement::Elision(_) => false, // holes are not reorderable + // A spread is a Babel `Expression::SpreadElement`, which the original + // hit via the catch-all `_ => false` (no SpreadElement arm), so any + // array containing a spread is not reorderable. + oxc::ArrayExpressionElement::SpreadElement(_) => false, + _ => { + is_reorderable_expression(builder, element.to_expression(), allow_local_identifiers) + } + }), + oxc::Expression::ObjectExpression(obj) => obj.properties.iter().all(|prop| match prop { + oxc::ObjectPropertyKind::ObjectProperty(p) => { + !p.computed + && !p.method + && matches!(p.kind, oxc::PropertyKind::Init) + && is_reorderable_expression(builder, &p.value, allow_local_identifiers) + } + _ => false, + }), + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) => { + // Allow member expressions where the innermost object is a global or module-local + let mut inner = expr; + loop { + inner = match inner { + oxc::Expression::StaticMemberExpression(m) => &m.object, + oxc::Expression::ComputedMemberExpression(m) => &m.object, + oxc::Expression::PrivateFieldExpression(m) => &m.object, + _ => break, + }; + } + if let oxc::Expression::Identifier(ident) = inner { + match builder.scope_info().resolve_reference_for_node(Some(ident.span.start)) { + None => true, // global + Some(binding) => { + // Module-scope bindings (ModuleLocal, imports) are safe to reorder + binding.scope == builder.scope_info().program_scope + } + } + } else { + false + } + } + oxc::Expression::ArrowFunctionExpression(arrow) => { + if arrow.expression { + match arrow.body.statements.first() { + Some(oxc::Statement::ExpressionStatement(es)) => { + is_reorderable_expression(builder, &es.expression, false) + } + _ => arrow.body.statements.is_empty(), + } + } else { + arrow.body.statements.is_empty() + } + } + oxc::Expression::CallExpression(call) => { + is_reorderable_expression(builder, &call.callee, allow_local_identifiers) + && call.arguments.iter().all(|arg| match arg { + // A spread argument is a Babel `Expression::SpreadElement`, + // which the original hit via the catch-all `_ => false`. + oxc::Argument::SpreadElement(_) => false, + _ => is_reorderable_expression( + builder, + arg.to_expression(), + allow_local_identifiers, + ), + }) + } + oxc::Expression::NewExpression(new_expr) => { + is_reorderable_expression(builder, &new_expr.callee, allow_local_identifiers) + && new_expr.arguments.iter().all(|arg| match arg { + // A spread argument is a Babel `Expression::SpreadElement`, + // which the original hit via the catch-all `_ => false`. + oxc::Argument::SpreadElement(_) => false, + _ => is_reorderable_expression( + builder, + arg.to_expression(), + allow_local_identifiers, + ), + }) + } + // TypeScript type wrappers: recurse into the inner expression. + oxc::Expression::TSAsExpression(ts) => { + is_reorderable_expression(builder, &ts.expression, allow_local_identifiers) + } + oxc::Expression::TSSatisfiesExpression(ts) => { + is_reorderable_expression(builder, &ts.expression, allow_local_identifiers) + } + oxc::Expression::TSNonNullExpression(ts) => { + is_reorderable_expression(builder, &ts.expression, allow_local_identifiers) + } + oxc::Expression::TSInstantiationExpression(ts) => { + is_reorderable_expression(builder, &ts.expression, allow_local_identifiers) + } + oxc::Expression::TSTypeAssertion(ts) => { + is_reorderable_expression(builder, &ts.expression, allow_local_identifiers) + } + oxc::Expression::ParenthesizedExpression(p) => { + is_reorderable_expression(builder, &p.expression, allow_local_identifiers) + } + _ => false, + } +} + +fn lower_statement<'a>( + builder: &mut HirBuilder<'a, '_>, + stmt: &'a oxc::Statement<'a>, + _label: Option<&str>, + parent_scope: Option, +) -> Result<(), CompilerDiagnostic> { + match stmt { + oxc::Statement::EmptyStatement(_) => {} + oxc::Statement::DebuggerStatement(dbg) => { + let loc = builder.source_location(dbg.span); + lower_value_to_temporary(builder, InstructionValue::Debugger { loc })?; + } + oxc::Statement::ExpressionStatement(expr_stmt) => { + lower_expression_to_temporary(builder, &expr_stmt.expression)?; + } + oxc::Statement::ReturnStatement(ret) => { + let loc = builder.source_location(ret.span); + let value = if let Some(arg) = &ret.argument { + lower_expression_to_temporary(builder, arg)? + } else { + lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc: None }, + )? + }; + let fallthrough = builder.reserve(BlockKind::Block); + builder.terminate_with_continuation( + Terminal::Return { + value, + return_variant: ReturnVariant::Explicit, + id: EvaluationOrder(0), + loc, + effects: None, + }, + fallthrough, + ); + } + oxc::Statement::ThrowStatement(throw) => { + let loc = builder.source_location(throw.span); + let value = lower_expression_to_temporary(builder, &throw.argument)?; + if builder.resolve_throw_handler().is_some() { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + } + let fallthrough = builder.reserve(BlockKind::Block); + builder.terminate_with_continuation( + Terminal::Throw { value, id: EvaluationOrder(0), loc }, + fallthrough, + ); + } + oxc::Statement::BlockStatement(block) => { + lower_block_statement(builder, &block.body, block.span.start, parent_scope)?; + } + oxc::Statement::VariableDeclaration(var_decl) => { + lower_variable_declaration(builder, var_decl)?; + } + oxc::Statement::FunctionDeclaration(func_decl) if func_decl.body.is_some() => { + lower_function_declaration(builder, func_decl)?; + } + oxc::Statement::IfStatement(if_stmt) => { + let loc = builder.source_location(if_stmt.span); + // Block for code following the if + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + + // Block for the consequent (if the test is truthy) + let consequent_loc = builder.source_location(if_stmt.consequent.span()); + let consequent_block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + lower_statement(builder, &if_stmt.consequent, None, parent_scope)?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: consequent_loc, + }) + })?; + + // Block for the alternate (if the test is not truthy) + let alternate_block = if let Some(alternate) = &if_stmt.alternate { + let alternate_loc = builder.source_location(alternate.span()); + builder.try_enter(BlockKind::Block, |builder, _block_id| { + lower_statement(builder, alternate, None, parent_scope)?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: alternate_loc, + }) + })? + } else { + // If there is no else clause, use the continuation directly + continuation_id + }; + + let test = lower_expression_to_temporary(builder, &if_stmt.test)?; + builder.terminate_with_continuation( + Terminal::If { + test, + consequent: consequent_block, + alternate: alternate_block, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc, + }, + continuation_block, + ); + } + oxc::Statement::ForStatement(for_stmt) => { + let loc = builder.source_location(for_stmt.span); + + let test_block = builder.reserve(BlockKind::Loop); + let test_block_id = test_block.id; + // Block for code following the loop + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + + // Init block: lower init expression/declaration, then goto test + let init_block = builder.try_enter(BlockKind::Loop, |builder, _block_id| { + let init_loc = match &for_stmt.init { + None => { + // No init expression (e.g., `for (; ...)`), add a placeholder + let placeholder = InstructionValue::Primitive { + value: PrimitiveValue::Undefined, + loc: loc.clone(), + }; + lower_value_to_temporary(builder, placeholder)?; + loc.clone() + } + Some(oxc::ForStatementInit::VariableDeclaration(var_decl)) => { + let init_loc = builder.source_location(var_decl.span); + lower_variable_declaration(builder, var_decl)?; + init_loc + } + Some(init) => { + let expr = init.to_expression(); + let init_loc = builder.source_location(expr.span()); + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + lower_expression_to_temporary(builder, expr)?; + init_loc + } + }; + Ok(Terminal::Goto { + block: test_block_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: init_loc, + }) + })?; + + // Update block (optional) + let update_block_id = if let Some(update) = &for_stmt.update { + let update_loc = builder.source_location(update.span()); + Some(builder.try_enter(BlockKind::Loop, |builder, _block_id| { + lower_expression_to_temporary(builder, update)?; + Ok(Terminal::Goto { + block: test_block_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: update_loc, + }) + })?) + } else { + None + }; + + // Loop body block + let continue_target = update_block_id.unwrap_or(test_block_id); + let body_loc = builder.source_location(for_stmt.body.span()); + let body_block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + builder.loop_scope( + _label.map(|s| s.to_string()), + continue_target, + continuation_id, + |builder| { + lower_statement(builder, &for_stmt.body, None, parent_scope)?; + Ok(Terminal::Goto { + block: continue_target, + variant: GotoVariant::Continue, + id: EvaluationOrder(0), + loc: body_loc, + }) + }, + ) + })?; + + // Emit For terminal, then fill in the test block + builder.terminate_with_continuation( + Terminal::For { + init: init_block, + test: test_block_id, + update: update_block_id, + loop_block: body_block, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + test_block, + ); + + // Fill in the test block + if let Some(test_expr) = &for_stmt.test { + let test = lower_expression_to_temporary(builder, test_expr)?; + builder.terminate_with_continuation( + Terminal::Branch { + test, + consequent: body_block, + alternate: continuation_id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + continuation_block, + ); + } else { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerStatement) Handle empty test in ForStatement" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + // Treat `for(;;)` as `while(true)` to keep the builder state consistent + let true_val = InstructionValue::Primitive { + value: PrimitiveValue::Boolean(true), + loc: loc.clone(), + }; + let test = lower_value_to_temporary(builder, true_val)?; + builder.terminate_with_continuation( + Terminal::Branch { + test, + consequent: body_block, + alternate: continuation_id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc, + }, + continuation_block, + ); + } + } + oxc::Statement::WhileStatement(while_stmt) => { + let loc = builder.source_location(while_stmt.span); + // Block used to evaluate whether to (re)enter or exit the loop + let conditional_block = builder.reserve(BlockKind::Loop); + let conditional_id = conditional_block.id; + // Block for code following the loop + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + + // Loop body + let body_loc = builder.source_location(while_stmt.body.span()); + let loop_block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + builder.loop_scope( + _label.map(|s| s.to_string()), + conditional_id, + continuation_id, + |builder| { + lower_statement(builder, &while_stmt.body, None, parent_scope)?; + Ok(Terminal::Goto { + block: conditional_id, + variant: GotoVariant::Continue, + id: EvaluationOrder(0), + loc: body_loc, + }) + }, + ) + })?; + + // Emit While terminal, jumping to the conditional block + builder.terminate_with_continuation( + Terminal::While { + test: conditional_id, + loop_block, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + conditional_block, + ); + + // Fill in the conditional block: lower test, branch + let test = lower_expression_to_temporary(builder, &while_stmt.test)?; + builder.terminate_with_continuation( + Terminal::Branch { + test, + consequent: loop_block, + alternate: continuation_id, + fallthrough: conditional_id, + id: EvaluationOrder(0), + loc, + }, + continuation_block, + ); + } + oxc::Statement::DoWhileStatement(do_while_stmt) => { + let loc = builder.source_location(do_while_stmt.span); + // Block used to evaluate whether to (re)enter or exit the loop + let conditional_block = builder.reserve(BlockKind::Loop); + let conditional_id = conditional_block.id; + // Block for code following the loop + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + + // Loop body, executed at least once unconditionally prior to exit + let body_loc = builder.source_location(do_while_stmt.body.span()); + let loop_block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + builder.loop_scope( + _label.map(|s| s.to_string()), + conditional_id, + continuation_id, + |builder| { + lower_statement(builder, &do_while_stmt.body, None, parent_scope)?; + Ok(Terminal::Goto { + block: conditional_id, + variant: GotoVariant::Continue, + id: EvaluationOrder(0), + loc: body_loc, + }) + }, + ) + })?; + + // Jump to the conditional block + builder.terminate_with_continuation( + Terminal::DoWhile { + loop_block, + test: conditional_id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + conditional_block, + ); + + // Fill in the conditional block: lower test, branch + let test = lower_expression_to_temporary(builder, &do_while_stmt.test)?; + builder.terminate_with_continuation( + Terminal::Branch { + test, + consequent: loop_block, + alternate: continuation_id, + fallthrough: conditional_id, + id: EvaluationOrder(0), + loc, + }, + continuation_block, + ); + } + oxc::Statement::ForInStatement(for_in) => { + let loc = builder.source_location(for_in.span); + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + let init_block = builder.reserve(BlockKind::Loop); + let init_block_id = init_block.id; + + let body_loc = builder.source_location(for_in.body.span()); + let loop_block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + builder.loop_scope( + _label.map(|s| s.to_string()), + init_block_id, + continuation_id, + |builder| { + lower_statement(builder, &for_in.body, None, parent_scope)?; + Ok(Terminal::Goto { + block: init_block_id, + variant: GotoVariant::Continue, + id: EvaluationOrder(0), + loc: body_loc, + }) + }, + ) + })?; + + let value = lower_expression_to_temporary(builder, &for_in.right)?; + builder.terminate_with_continuation( + Terminal::ForIn { + init: init_block_id, + loop_block, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + init_block, + ); + + // Lower the init: NextPropertyOf + assignment + let left_loc = builder.source_location(for_in.left.span()); + let next_property = lower_value_to_temporary( + builder, + InstructionValue::NextPropertyOf { value, loc: left_loc.clone() }, + )?; + + let assign_result = lower_for_in_of_left( + builder, + &for_in.left, + left_loc.clone(), + next_property.clone(), + )?; + // Use the assign result (StoreLocal temp) as the test, matching TS behavior + let test_value = assign_result.unwrap_or(next_property); + let test = lower_value_to_temporary( + builder, + InstructionValue::LoadLocal { place: test_value, loc: left_loc.clone() }, + )?; + builder.terminate_with_continuation( + Terminal::Branch { + test, + consequent: loop_block, + alternate: continuation_id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + continuation_block, + ); + } + oxc::Statement::ForOfStatement(for_of) => { + let loc = builder.source_location(for_of.span); + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + let init_block = builder.reserve(BlockKind::Loop); + let init_block_id = init_block.id; + let test_block = builder.reserve(BlockKind::Loop); + let test_block_id = test_block.id; + + if for_of.r#await { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerStatement) Handle for-await loops".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + return Ok(()); + } + + let body_loc = builder.source_location(for_of.body.span()); + let loop_block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + builder.loop_scope( + _label.map(|s| s.to_string()), + init_block_id, + continuation_id, + |builder| { + lower_statement(builder, &for_of.body, None, parent_scope)?; + Ok(Terminal::Goto { + block: init_block_id, + variant: GotoVariant::Continue, + id: EvaluationOrder(0), + loc: body_loc, + }) + }, + ) + })?; + + let value = lower_expression_to_temporary(builder, &for_of.right)?; + builder.terminate_with_continuation( + Terminal::ForOf { + init: init_block_id, + test: test_block_id, + loop_block, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + init_block, + ); + + // Init block: GetIterator, goto test + let iterator = lower_value_to_temporary( + builder, + InstructionValue::GetIterator { collection: value.clone(), loc: value.loc.clone() }, + )?; + builder.terminate_with_continuation( + Terminal::Goto { + block: test_block_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + test_block, + ); + + // Test block: IteratorNext, assign, branch + let left_loc = builder.source_location(for_of.left.span()); + let advance_iterator = lower_value_to_temporary( + builder, + InstructionValue::IteratorNext { + iterator: iterator.clone(), + collection: value.clone(), + loc: left_loc.clone(), + }, + )?; + + let assign_result = lower_for_in_of_left( + builder, + &for_of.left, + left_loc.clone(), + advance_iterator.clone(), + )?; + // Use the assign result (StoreLocal temp) as the test, matching TS behavior + let test_value = assign_result.unwrap_or(advance_iterator); + let test = lower_value_to_temporary( + builder, + InstructionValue::LoadLocal { place: test_value, loc: left_loc.clone() }, + )?; + builder.terminate_with_continuation( + Terminal::Branch { + test, + consequent: loop_block, + alternate: continuation_id, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc: loc.clone(), + }, + continuation_block, + ); + } + oxc::Statement::SwitchStatement(switch_stmt) => { + let loc = builder.source_location(switch_stmt.span); + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + + // Iterate through cases in reverse order so that previous blocks can + // fallthrough to successors + let mut fallthrough = continuation_id; + let mut cases: Vec = Vec::new(); + let mut has_default = false; + + for ii in (0..switch_stmt.cases.len()).rev() { + let case = &switch_stmt.cases[ii]; + let case_loc = builder.source_location(case.span); + + if case.test.is_none() { + if has_default { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Syntax, + reason: "Expected at most one `default` branch in a switch statement" + .to_string(), + description: None, + loc: case_loc.clone(), + suggestions: None, + })?; + break; + } + has_default = true; + } + + let fallthrough_target = fallthrough; + let block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + builder.switch_scope( + _label.map(|s| s.to_string()), + continuation_id, + |builder| { + for consequent in &case.consequent { + lower_statement(builder, consequent, None, parent_scope)?; + } + Ok(Terminal::Goto { + block: fallthrough_target, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: case_loc.clone(), + }) + }, + ) + })?; + + let test = if let Some(test_expr) = &case.test { + Some(lower_reorderable_expression(builder, test_expr)?) + } else { + None + }; + + cases.push(Case { test, block }); + fallthrough = block; + } + + // Reverse back to original order + cases.reverse(); + + // If no default case, add one that jumps to continuation + if !has_default { + cases.push(Case { test: None, block: continuation_id }); + } + + let test = lower_expression_to_temporary(builder, &switch_stmt.discriminant)?; + builder.terminate_with_continuation( + Terminal::Switch { + test, + cases, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc, + }, + continuation_block, + ); + } + oxc::Statement::TryStatement(try_stmt) => { + let loc = builder.source_location(try_stmt.span); + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + + let handler_clause = match &try_stmt.handler { + Some(h) => h, + None => { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: + "(BuildHIR::lowerStatement) Handle TryStatement without a catch clause" + .to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + return Ok(()); + } + }; + + if try_stmt.finalizer.is_some() { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause".to_string(), + description: None, + loc: loc.clone(), + suggestions: None, + })?; + } + + // Set up handler binding if catch has a param + let handler_binding_info: Option<(Place, &oxc::BindingPattern)> = if let Some(param) = + &handler_clause.param + { + // Check for destructuring in catch clause params. + // Match TS behavior: Babel doesn't register destructured catch bindings + // in its scope, so resolveIdentifier fails and records an invariant error. + let is_destructuring = matches!( + ¶m.pattern, + oxc::BindingPattern::ObjectPattern(_) | oxc::BindingPattern::ArrayPattern(_) + ); + if is_destructuring { + let mut id_locs = Vec::new(); + collect_catch_pattern_identifier_locs(builder, ¶m.pattern, &mut id_locs); + for id_loc in id_locs { + builder.record_error(CompilerErrorDetail { + reason: "(BuildHIR::lowerAssignment) Could not find binding for declaration.".to_string(), + category: ErrorCategory::Invariant, + loc: id_loc, + description: None, + suggestions: None, + })?; + } + None + } else { + let param_loc = builder.source_location(param.pattern.span()); + let id = builder.make_temporary(param_loc.clone()); + promote_temporary(builder, id); + let place = Place { + identifier: id, + effect: Effect::Unknown, + reactive: false, + loc: param_loc.clone(), + }; + // Emit DeclareLocal for the catch binding + lower_value_to_temporary( + builder, + InstructionValue::DeclareLocal { + lvalue: LValue { kind: InstructionKind::Catch, place: place.clone() }, + type_annotation: None, + loc: param_loc, + }, + )?; + Some((place, ¶m.pattern)) + } + } else { + None + }; + + // Create the handler (catch) block + let handler_binding_for_block = handler_binding_info.clone(); + let handler_loc = builder.source_location(handler_clause.span); + // Use the catch param's loc for the assignment, matching TS: handlerBinding.path.node.loc + let handler_param_loc = + handler_clause.param.as_ref().map(|p| builder.source_location(p.pattern.span())); + let handler_block = builder.try_enter(BlockKind::Catch, |builder, _block_id| { + if let Some((ref place, pattern)) = handler_binding_for_block { + lower_binding_assignment( + builder, + handler_param_loc.clone().flatten().or_else(|| handler_loc.clone()), + InstructionKind::Catch, + pattern, + place.clone(), + AssignmentStyle::Assignment, + )?; + } + // Lower the catch body using lower_block_statement to get hoisting support. + // Use the catch clause's scope (which contains the catch param binding). + // Fall back to the body block's own scope if the catch clause scope is missing. + let catch_scope = builder + .scope_info() + .resolve_scope_for_node(Some(handler_clause.span.start)) + .or_else(|| { + builder + .scope_info() + .resolve_scope_for_node(Some(handler_clause.body.span.start)) + }); + if let Some(scope_id) = catch_scope { + lower_block_statement_with_scope( + builder, + &handler_clause.body.body, + handler_clause.body.span.start, + scope_id, + )?; + } else { + lower_block_statement( + builder, + &handler_clause.body.body, + handler_clause.body.span.start, + parent_scope, + )?; + } + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: handler_loc.clone(), + }) + })?; + + // Create the try block + let try_body_loc = builder.source_location(try_stmt.block.span); + let try_block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + builder.try_enter_try_catch(handler_block, |builder| { + lower_block_statement( + builder, + &try_stmt.block.body, + try_stmt.block.span.start, + parent_scope, + )?; + Ok(()) + })?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Try, + id: EvaluationOrder(0), + loc: try_body_loc.clone(), + }) + })?; + + builder.terminate_with_continuation( + Terminal::Try { + block: try_block, + handler_binding: handler_binding_info.map(|(place, _)| place), + handler: handler_block, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc, + }, + continuation_block, + ); + } + oxc::Statement::BreakStatement(brk) => { + let loc = builder.source_location(brk.span); + let label_name = brk.label.as_ref().map(|l| l.name.as_str()); + let target = builder.lookup_break(label_name)?; + let fallthrough = builder.reserve(BlockKind::Block); + builder.terminate_with_continuation( + Terminal::Goto { + block: target, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc, + }, + fallthrough, + ); + } + oxc::Statement::ContinueStatement(cont) => { + let loc = builder.source_location(cont.span); + let label_name = cont.label.as_ref().map(|l| l.name.as_str()); + let target = builder.lookup_continue(label_name)?; + let fallthrough = builder.reserve(BlockKind::Block); + builder.terminate_with_continuation( + Terminal::Goto { + block: target, + variant: GotoVariant::Continue, + id: EvaluationOrder(0), + loc, + }, + fallthrough, + ); + } + oxc::Statement::LabeledStatement(labeled_stmt) => { + let label_name = labeled_stmt.label.name.as_str(); + let loc = builder.source_location(labeled_stmt.span); + + // Check if the body is a loop statement - if so, delegate with label + match &labeled_stmt.body { + oxc::Statement::ForStatement(_) + | oxc::Statement::WhileStatement(_) + | oxc::Statement::DoWhileStatement(_) + | oxc::Statement::ForInStatement(_) + | oxc::Statement::ForOfStatement(_) => { + // Labeled loops are special because of continue, push the label down + lower_statement(builder, &labeled_stmt.body, Some(label_name), parent_scope)?; + } + _ => { + // All other statements create a continuation block to allow `break` + let continuation_block = builder.reserve(BlockKind::Block); + let continuation_id = continuation_block.id; + let body_loc = builder.source_location(labeled_stmt.body.span()); + let label_string = label_name.to_string(); + + let block = builder.try_enter(BlockKind::Block, |builder, _block_id| { + builder.label_scope(label_string, continuation_id, |builder| { + lower_statement(builder, &labeled_stmt.body, None, parent_scope)?; + Ok(()) + })?; + Ok(Terminal::Goto { + block: continuation_id, + variant: GotoVariant::Break, + id: EvaluationOrder(0), + loc: body_loc, + }) + })?; + + builder.terminate_with_continuation( + Terminal::Label { + block, + fallthrough: continuation_id, + id: EvaluationOrder(0), + loc, + }, + continuation_block, + ); + } + } + } + oxc::Statement::WithStatement(with_stmt) => { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::UnsupportedSyntax, + reason: "JavaScript 'with' syntax is not supported".to_string(), + description: Some("'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives".to_string()), + loc: builder.source_location(with_stmt.span), + suggestions: None, + })?; + } + oxc::Statement::ClassDeclaration(cls) => { + let loc = builder.source_location(cls.span); + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::UnsupportedSyntax, + reason: "Inline `class` declarations are not supported".to_string(), + description: Some( + "Move class declarations outside of components/hooks".to_string(), + ), + loc: loc.clone(), + suggestions: None, + })?; + // Original lowered an UnsupportedNode temporary; emit the same + // Primitive::Undefined placeholder to keep IdentifierId numbering aligned. + lower_value_to_temporary( + builder, + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }, + )?; + } + oxc::Statement::ImportDeclaration(_) + | oxc::Statement::ExportNamedDeclaration(_) + | oxc::Statement::ExportDefaultDeclaration(_) + | oxc::Statement::ExportAllDeclaration(_) => { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Syntax, + reason: "JavaScript `import` and `export` statements may only appear at the top level of a module".to_string(), + description: None, + loc: builder.source_location(stmt.span()), + suggestions: None, + })?; + } + oxc::Statement::TSEnumDeclaration(e) => { + // Inline TS `enum` has runtime semantics, so re-emit it verbatim at + // this position (the original captured it via `UnsupportedNode`). + // Lower as a value-less passthrough; codegen clones it through. + let loc = builder.source_location(e.span); + lower_value_to_temporary( + builder, + InstructionValue::PassthroughStatement { stmt, loc }, + )?; + } + _ => { + // Remaining statements are skipped: bodyless FunctionDeclaration + // (== Babel TSDeclareFunction), TS/Flow type-only declarations + // (TSTypeAlias/TSInterface/TSModule/TSGlobal/TSImportEquals/ + // TSExportAssignment/TSNamespaceExport). The Flow `EnumDeclaration` + // arm is moot since oxc has no Flow enum. + } + } + Ok(()) +} + +/// Lower a `VariableDeclaration`, mirroring the original `Statement::VariableDeclaration` +/// arm (extracted so the `ForStatement` init can reuse it without re-wrapping in a +/// `Statement`). +fn lower_variable_declaration<'a>( + builder: &mut HirBuilder<'a, '_>, + var_decl: &'a oxc::VariableDeclaration<'a>, +) -> Result<(), CompilerDiagnostic> { + use oxc::VariableDeclarationKind as VK; + if matches!(var_decl.kind, VK::Var) { + builder.record_error(CompilerErrorDetail { + reason: "(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration" + .to_string(), + category: ErrorCategory::Todo, + loc: builder.source_location(var_decl.span), + description: None, + suggestions: None, + })?; + // Treat `var` as `let` so references to the variable don't break + } + let kind = match var_decl.kind { + VK::Let | VK::Var => InstructionKind::Let, + VK::Const | VK::Using | VK::AwaitUsing => InstructionKind::Const, + }; + for declarator in &var_decl.declarations { + let stmt_loc = builder.source_location(var_decl.span); + if let Some(init) = &declarator.init { + let value = lower_expression_to_temporary(builder, init)?; + let assign_style = match &declarator.id { + oxc::BindingPattern::ObjectPattern(_) | oxc::BindingPattern::ArrayPattern(_) => { + AssignmentStyle::Destructure + } + _ => AssignmentStyle::Assignment, + }; + lower_binding_assignment(builder, stmt_loc, kind, &declarator.id, value, assign_style)?; + } else if let oxc::BindingPattern::BindingIdentifier(id) = &declarator.id { + // No init: emit DeclareLocal or DeclareContext + let id_loc = builder.source_location(id.span); + let mut binding = builder.resolve_identifier( + id.name.as_str(), + id.span.start, + id_loc.clone(), + Some(id.span.start), + )?; + if !matches!(binding, VariableBinding::Identifier { .. }) { + // Position-based resolution failed (synthetic $$gen vars at + // position 0). Try scope lookup including descendants. + if let Some((binding_id, binding_data)) = builder + .scope_info() + .find_binding_id_in_descendants(id.name.as_str(), builder.function_scope()) + { + let binding_kind = + crate::react_compiler_lowering::convert_binding_kind(&binding_data.kind); + let identifier = builder.resolve_binding_with_loc( + id.name.as_str(), + binding_id, + id_loc.clone(), + )?; + binding = VariableBinding::Identifier { identifier, binding_kind }; + } + } + match binding { + VariableBinding::Identifier { identifier, .. } => { + // Update the identifier's loc to the declaration site + // (it may have been first created at a reference site during hoisting) + builder.set_identifier_declaration_loc(identifier, &id_loc); + let place = Place { + identifier, + effect: Effect::Unknown, + reactive: false, + loc: id_loc.clone(), + }; + if builder.is_context_identifier( + id.name.as_str(), + id.span.start, + Some(id.span.start), + ) { + if kind == InstructionKind::Const { + builder.record_error(CompilerErrorDetail { + reason: "Expect `const` declaration not to be reassigned" + .to_string(), + category: ErrorCategory::Syntax, + loc: id_loc.clone(), + description: None, + suggestions: None, + })?; + } + lower_value_to_temporary( + builder, + InstructionValue::DeclareContext { + lvalue: LValue { kind: InstructionKind::Let, place }, + loc: id_loc, + }, + )?; + } else { + let type_annotation = declarator + .type_annotation + .as_ref() + .map(|ann| ts_type_node_type(&ann.type_annotation).to_string()); + lower_value_to_temporary( + builder, + InstructionValue::DeclareLocal { + lvalue: LValue { kind, place }, + type_annotation, + loc: id_loc, + }, + )?; + } + } + _ => { + builder.record_error(CompilerErrorDetail { + reason: "Could not find binding for declaration".to_string(), + category: ErrorCategory::Invariant, + loc: id_loc, + description: None, + suggestions: None, + })?; + } + } + } else { + builder.record_error(CompilerErrorDetail { + reason: "Expected variable declaration to be an identifier if no initializer was provided".to_string(), + category: ErrorCategory::Syntax, + loc: builder.source_location(declarator.span), + description: None, + suggestions: None, + })?; + } + } + Ok(()) +} + +/// Lower the `left` target of a for-in / for-of loop, dispatching to the binding +/// assignment (for `VariableDeclaration`) or assignment-target (for plain +/// patterns) lowering. Mirrors the original `ForInOfLeft` match. +fn lower_for_in_of_left<'a>( + builder: &mut HirBuilder<'a, '_>, + left: &'a oxc::ForStatementLeft<'a>, + left_loc: Option, + value: Place, +) -> Result, CompilerError> { + match left { + oxc::ForStatementLeft::VariableDeclaration(var_decl) => { + if var_decl.declarations.len() != 1 { + builder.record_error(CompilerErrorDetail { + category: ErrorCategory::Invariant, + reason: format!( + "Expected only one declaration in for-in/of init, got {}", + var_decl.declarations.len() + ), + description: None, + loc: left_loc.clone(), + suggestions: None, + })?; + } + if let Some(declarator) = var_decl.declarations.first() { + lower_binding_assignment( + builder, + left_loc, + InstructionKind::Let, + &declarator.id, + value, + AssignmentStyle::Assignment, + ) + } else { + Ok(None) + } + } + _ => lower_assignment_target( + builder, + left_loc, + InstructionKind::Reassign, + left.to_assignment_target(), + value, + AssignmentStyle::Assignment, + ), + } +} + +/// Collect identifier locs from a destructured catch-clause pattern, for error +/// reporting (Babel doesn't register destructured catch bindings). +fn collect_catch_pattern_identifier_locs( + builder: &HirBuilder<'_, '_>, + pat: &oxc::BindingPattern, + locs: &mut Vec>, +) { + match pat { + oxc::BindingPattern::BindingIdentifier(id) => { + locs.push(builder.source_location(id.span)); + } + oxc::BindingPattern::ObjectPattern(obj) => { + for prop in &obj.properties { + collect_catch_pattern_identifier_locs(builder, &prop.value, locs); + } + if let Some(rest) = &obj.rest { + collect_catch_pattern_identifier_locs(builder, &rest.argument, locs); + } + } + oxc::BindingPattern::ArrayPattern(arr) => { + for elem in arr.elements.iter().flatten() { + collect_catch_pattern_identifier_locs(builder, elem, locs); + } + if let Some(rest) = &arr.rest { + collect_catch_pattern_identifier_locs(builder, &rest.argument, locs); + } + } + // The original matched only Identifier/Object/Array; AssignmentPattern + // (destructuring defaults) fell through its `_ => {}` catch-all. + oxc::BindingPattern::AssignmentPattern(_) => {} + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_lowering/find_context_identifiers.rs b/crates/oxc_react_compiler/src/react_compiler_lowering/find_context_identifiers.rs new file mode 100644 index 0000000000000..bc1a880b7e583 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_lowering/find_context_identifiers.rs @@ -0,0 +1,563 @@ +//! Rust equivalent of the TypeScript `FindContextIdentifiers` pass. +//! +//! Determines which bindings need StoreContext/LoadContext semantics by +//! walking the function's oxc AST with scope tracking to find variables that +//! cross function boundaries. +//! +//! This is a translation of the original immutable `ContextIdentifierVisitor`, +//! which was driven by a Babel-shaped AST walker. The original tracked two +//! stacks: +//! +//! * a generic `scope_stack` (the active scope, used to resolve the lexical +//! binding of a reassignment target by name), and +//! * a `function_stack` of the inner function scopes currently being walked +//! (empty at the top level of the function being compiled). +//! +//! The `Visit` impl below reproduces both stacks exactly: the generic stack is +//! pushed for every scope-creating node the original `AstWalker` pushed for +//! (functions, arrows, blocks, for-loops, switch, catch, class static blocks), +//! while the function stack is pushed only for nested function nodes — mirroring +//! the original `enter_function_*` / `enter_object_method` hooks. +//! +//! Identifiers inside TS type subtrees are deliberately NOT visited here: the +//! original walker walked type positions as opaque `RawNode`s, which never fired +//! `enter_identifier`. The post-walk supplement loop (driven by +//! `ref_node_id_to_binding`, which DOES include type references) recovers any +//! captured references hiding inside type annotations, matching the TS pass. + +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use oxc_ast::ast as oxc; +use oxc_ast::ast::AssignmentTargetMaybeDefault; +use oxc_ast::match_assignment_target; +use oxc_ast_visit::Visit; +use oxc_span::Span; +use oxc_syntax::scope::ScopeFlags; + +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_diagnostics::CompilerErrorDetail; +use crate::react_compiler_diagnostics::ErrorCategory; +use crate::react_compiler_diagnostics::SourceLocation; +use crate::react_compiler_hir::environment::Environment; +use crate::scope::BindingId; +use crate::scope::ScopeId; +use crate::scope::ScopeInfo; +use crate::scope::ScopeKind; + +use crate::react_compiler_lowering::FunctionNode; +use crate::react_compiler_lowering::source_loc::LineOffsets; + +#[derive(Default)] +struct BindingInfo { + reassigned: bool, + reassigned_by_inner_fn: bool, + referenced_by_inner_fn: bool, +} + +struct ContextIdentifierVisitor<'a, 'b> { + scope_info: &'a ScopeInfo, + line_offsets: &'a LineOffsets, + env: &'a mut Environment<'b>, + /// The active scope stack. Initialized with the function-being-compiled's + /// scope and pushed/popped for every scope-creating node, mirroring the + /// original `AstWalker`. + scope_stack: Vec, + /// Stack of inner function scopes encountered during traversal. + /// Empty when at the top level of the function being compiled. + function_stack: Vec, + binding_info: FxHashMap, + error: Option, +} + +impl<'a, 'b> ContextIdentifierVisitor<'a, 'b> { + fn current_scope(&self) -> ScopeId { + self.scope_stack.last().copied().unwrap_or(self.scope_info.program_scope) + } + + /// Push the generic scope for a node, if it creates one. Returns whether a + /// scope was pushed, so the caller knows whether to pop after walking. + fn enter_scope(&mut self, span: Span) -> bool { + if let Some(scope) = self.scope_info.resolve_scope_for_node(Some(span.start)) { + self.scope_stack.push(scope); + true + } else { + false + } + } + + fn exit_scope(&mut self, pushed: bool) { + if pushed { + self.scope_stack.pop(); + } + } + + fn push_function_scope(&mut self, span: Span) -> bool { + if let Some(scope) = self.scope_info.resolve_scope_for_node(Some(span.start)) { + self.function_stack.push(scope); + true + } else { + false + } + } + + fn pop_function_scope(&mut self, pushed: bool) { + if pushed { + self.function_stack.pop(); + } + } + + fn check_captured_reference(&mut self, span: Span) { + let binding_id = match self.scope_info.resolve_reference_id_for_node(Some(span.start)) { + Some(id) => id, + None => return, + }; + let &fn_scope = match self.function_stack.last() { + Some(s) => s, + None => return, + }; + let binding = &self.scope_info.bindings[binding_id.0 as usize]; + if is_captured_by_function(self.scope_info, binding.scope, fn_scope) { + let info = self.binding_info.entry(binding_id).or_default(); + info.referenced_by_inner_fn = true; + } + } + + fn handle_reassignment_identifier(&mut self, name: &str, current_scope: ScopeId) { + if let Some(binding_id) = self.scope_info.get_binding(current_scope, name) { + let info = self.binding_info.entry(binding_id).or_default(); + info.reassigned = true; + if let Some(&fn_scope) = self.function_stack.last() { + let binding = &self.scope_info.bindings[binding_id.0 as usize]; + if is_captured_by_function(self.scope_info, binding.scope, fn_scope) { + info.reassigned_by_inner_fn = true; + } + } + } + } + + /// Record the TS-faithful Todo for an unsupported assignment-target wrapper + /// node, recording the error once (the first time it is hit). + fn record_unsupported_lval(&mut self, type_name: &str, span: Span) { + if self.error.is_some() { + return; + } + let loc = self.line_offsets.source_location(span); + self.error = Some(make_unsupported_lval_error(self.env, type_name, Some(loc))); + } +} + +impl<'a, 'b> Visit<'a> for ContextIdentifierVisitor<'a, 'b> { + // ---- function scopes (push BOTH the generic scope and the function stack) ---- + + fn visit_function(&mut self, it: &oxc::Function<'a>, _flags: ScopeFlags) { + let scope_pushed = self.enter_scope(it.span); + let fn_pushed = self.push_function_scope(it.span); + // The original Babel walker never visited the function NAME identifier + // (`it.id`); it only walked the type-bearing parts (as opaque RawNodes), + // then params, then body. oxc's `walk_function` DOES visit `it.id` via + // `visit_binding_identifier`, which — with the inner function already on + // `function_stack` — would spuriously mark a hoisted nested-function name + // as referenced_by_inner_fn. Walk the parts manually, skipping `it.id`. + // (Type parameters / return type are no-ops via the `visit_ts_*` + // overrides, mirroring the original RawNode walk.) + if let Some(this_param) = &it.this_param { + self.visit_ts_this_parameter(this_param); + } + self.visit_formal_parameters(&it.params); + if let Some(body) = &it.body { + self.visit_function_body(body); + } + self.pop_function_scope(fn_pushed); + self.exit_scope(scope_pushed); + } + + fn visit_arrow_function_expression(&mut self, it: &oxc::ArrowFunctionExpression<'a>) { + let scope_pushed = self.enter_scope(it.span); + let fn_pushed = self.push_function_scope(it.span); + oxc_ast_visit::walk::walk_arrow_function_expression(self, it); + self.pop_function_scope(fn_pushed); + self.exit_scope(scope_pushed); + } + + // ---- non-function scope-creating nodes (push only the generic scope) ---- + + fn visit_block_statement(&mut self, it: &oxc::BlockStatement<'a>) { + let pushed = self.enter_scope(it.span); + oxc_ast_visit::walk::walk_block_statement(self, it); + self.exit_scope(pushed); + } + + fn visit_for_statement(&mut self, it: &oxc::ForStatement<'a>) { + let pushed = self.enter_scope(it.span); + oxc_ast_visit::walk::walk_for_statement(self, it); + self.exit_scope(pushed); + } + + fn visit_for_in_statement(&mut self, it: &oxc::ForInStatement<'a>) { + let pushed = self.enter_scope(it.span); + oxc_ast_visit::walk::walk_for_in_statement(self, it); + self.exit_scope(pushed); + } + + fn visit_for_of_statement(&mut self, it: &oxc::ForOfStatement<'a>) { + let pushed = self.enter_scope(it.span); + oxc_ast_visit::walk::walk_for_of_statement(self, it); + self.exit_scope(pushed); + } + + fn visit_switch_statement(&mut self, it: &oxc::SwitchStatement<'a>) { + let pushed = self.enter_scope(it.span); + oxc_ast_visit::walk::walk_switch_statement(self, it); + self.exit_scope(pushed); + } + + fn visit_catch_clause(&mut self, it: &oxc::CatchClause<'a>) { + let pushed = self.enter_scope(it.span); + oxc_ast_visit::walk::walk_catch_clause(self, it); + self.exit_scope(pushed); + } + + fn visit_static_block(&mut self, it: &oxc::StaticBlock<'a>) { + let pushed = self.enter_scope(it.span); + oxc_ast_visit::walk::walk_static_block(self, it); + self.exit_scope(pushed); + } + + // ---- identifier references (the captured-reference check) ---- + + fn visit_identifier_reference(&mut self, it: &oxc::IdentifierReference<'a>) { + self.check_captured_reference(it.span); + } + + fn visit_binding_identifier(&mut self, it: &oxc::BindingIdentifier<'a>) { + // Mirrors the original `enter_identifier`, which fired on pattern + // binding identifiers too. `check_captured_reference` is a no-op unless + // the node resolves to a captured reference. + self.check_captured_reference(it.span); + } + + fn visit_jsx_identifier(&mut self, it: &oxc::JSXIdentifier<'a>) { + self.check_captured_reference(it.span); + } + + fn visit_jsx_attribute(&mut self, it: &oxc::JSXAttribute<'a>) { + // The original `AstWalker.walk_jsx_element` walked only attribute VALUES; + // the attribute NAME was never visited. oxc's `walk_jsx_attribute` would + // otherwise fire `visit_jsx_identifier` on the name. Visit only the value. + if let Some(value) = &it.value { + self.visit_jsx_attribute_value(value); + } + } + + fn visit_jsx_namespaced_name(&mut self, _it: &oxc::JSXNamespacedName<'a>) { + // The original explicitly skipped JSXNamespacedName (both as an element + // name and as an attribute name), never visiting its namespace/name + // identifiers. oxc's `walk_jsx_namespaced_name` would visit both. + } + + // ---- reassignment tracking ---- + + fn visit_assignment_expression(&mut self, it: &oxc::AssignmentExpression<'a>) { + let current_scope = self.current_scope(); + if self.error.is_none() { + self.walk_assignment_target_for_reassignment(&it.left, current_scope); + } + oxc_ast_visit::walk::walk_assignment_expression(self, it); + } + + fn visit_update_expression(&mut self, it: &oxc::UpdateExpression<'a>) { + if let oxc::SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) = &it.argument { + let current_scope = self.current_scope(); + self.handle_reassignment_identifier(&ident.name, current_scope); + } + oxc_ast_visit::walk::walk_update_expression(self, it); + } + + // ---- positions deliberately NOT visited, matching the original walker ---- + + fn visit_static_member_expression(&mut self, it: &oxc::StaticMemberExpression<'a>) { + // Non-computed member property names (`a.b` → `b`) were never visited. + self.visit_expression(&it.object); + } + + fn visit_object_property(&mut self, it: &oxc::ObjectProperty<'a>) { + // Non-computed object keys were never visited. + if it.computed { + self.visit_property_key(&it.key); + } + self.visit_expression(&it.value); + } + + fn visit_class(&mut self, it: &oxc::Class<'a>) { + // The original walker did not recurse into a class's `super_class` + // (extends) clause nor its body members; only the type-bearing parts + // were walked, and those carried no `enter_identifier` calls. So the + // class contributes nothing to the walker-based capture analysis. + let _ = it; + } + + // ---- skip TS type subtrees (the original walked them as opaque RawNodes) ---- + + fn visit_ts_type(&mut self, _it: &oxc::TSType<'a>) {} + + fn visit_ts_type_annotation(&mut self, _it: &oxc::TSTypeAnnotation<'a>) {} + + fn visit_ts_type_parameter_instantiation( + &mut self, + _it: &oxc::TSTypeParameterInstantiation<'a>, + ) { + } + + fn visit_ts_type_parameter_declaration(&mut self, _it: &oxc::TSTypeParameterDeclaration<'a>) {} + + fn visit_ts_type_alias_declaration(&mut self, _it: &oxc::TSTypeAliasDeclaration<'a>) {} + + fn visit_ts_interface_declaration(&mut self, _it: &oxc::TSInterfaceDeclaration<'a>) {} + + fn visit_ts_enum_declaration(&mut self, _it: &oxc::TSEnumDeclaration<'a>) {} + + fn visit_ts_module_declaration(&mut self, _it: &oxc::TSModuleDeclaration<'a>) {} +} + +impl<'a, 'b> ContextIdentifierVisitor<'a, 'b> { + /// Recursively walk an assignment target to find all reassignment target + /// identifiers, mirroring the original `walk_lval_for_reassignment`. + fn walk_assignment_target_for_reassignment( + &mut self, + target: &oxc::AssignmentTarget<'a>, + current_scope: ScopeId, + ) { + match target { + oxc::AssignmentTarget::AssignmentTargetIdentifier(ident) => { + self.handle_reassignment_identifier(&ident.name, current_scope); + } + oxc::AssignmentTarget::ArrayAssignmentTarget(pat) => { + for element in pat.elements.iter().flatten() { + self.walk_maybe_default_for_reassignment(element, current_scope); + } + if let Some(rest) = &pat.rest { + self.walk_assignment_target_for_reassignment(&rest.target, current_scope); + } + } + oxc::AssignmentTarget::ObjectAssignmentTarget(pat) => { + for prop in &pat.properties { + match prop { + oxc::AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(p) => { + self.handle_reassignment_identifier(&p.binding.name, current_scope); + } + oxc::AssignmentTargetProperty::AssignmentTargetPropertyProperty(p) => { + self.walk_maybe_default_for_reassignment(&p.binding, current_scope); + } + } + } + if let Some(rest) = &pat.rest { + self.walk_assignment_target_for_reassignment(&rest.target, current_scope); + } + } + // Member expressions are interior mutability, not a variable + // reassignment — no-op. + oxc::AssignmentTarget::StaticMemberExpression(_) + | oxc::AssignmentTarget::ComputedMemberExpression(_) + | oxc::AssignmentTarget::PrivateFieldExpression(_) => {} + // Unsupported TS assignment-target wrappers throw a TS-faithful Todo. + oxc::AssignmentTarget::TSAsExpression(node) => { + self.record_unsupported_lval("TSAsExpression", node.span); + } + oxc::AssignmentTarget::TSSatisfiesExpression(node) => { + self.record_unsupported_lval("TSSatisfiesExpression", node.span); + } + oxc::AssignmentTarget::TSNonNullExpression(node) => { + self.record_unsupported_lval("TSNonNullExpression", node.span); + } + oxc::AssignmentTarget::TSTypeAssertion(node) => { + self.record_unsupported_lval("TSTypeAssertion", node.span); + } + } + } + + fn walk_maybe_default_for_reassignment( + &mut self, + target: &oxc::AssignmentTargetMaybeDefault<'a>, + current_scope: ScopeId, + ) { + match target { + oxc::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(node) => { + self.walk_assignment_target_for_reassignment(&node.binding, current_scope); + } + inner @ match_assignment_target!(AssignmentTargetMaybeDefault) => { + self.walk_assignment_target_for_reassignment( + inner.to_assignment_target(), + current_scope, + ); + } + } + } +} + +/// Build the TS-faithful Todo error for an unsupported assignment-target wrapper +/// node, mirroring the TypeScript `FindContextIdentifiers` pass. TS throws +/// immediately (CompilerError.throwTodo in handleAssignment's default case), +/// aborting before BuildHIR ever runs or logs, so this must return Err rather +/// than record-and-continue: otherwise Rust emits HIR debug entries for a +/// function TS never lowered. +fn make_unsupported_lval_error( + env: &mut Environment, + type_name: &str, + loc: Option, +) -> CompilerError { + let _ = env; + let mut err = CompilerError::new(); + err.push_error_detail(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: format!( + "[FindContextIdentifiers] Cannot handle Object destructuring assignment target {type_name}" + ), + description: None, + loc, + suggestions: None, + }); + err +} + +/// Check if a binding declared at `binding_scope` is captured by a function at `function_scope`. +/// Returns true if the binding is declared above the function (in the parent scope or higher). +fn is_captured_by_function( + scope_info: &ScopeInfo, + binding_scope: ScopeId, + function_scope: ScopeId, +) -> bool { + let fn_parent = match scope_info.scopes[function_scope.0 as usize].parent { + Some(p) => p, + None => return false, + }; + if binding_scope == fn_parent { + return true; + } + // Walk up from fn_parent to see if binding_scope is an ancestor + let mut current = scope_info.scopes[fn_parent.0 as usize].parent; + while let Some(scope_id) = current { + if scope_id == binding_scope { + return true; + } + current = scope_info.scopes[scope_id.0 as usize].parent; + } + false +} + +/// Find context identifiers for a function: variables that are captured across +/// function boundaries and need StoreContext/LoadContext semantics. +/// +/// A binding is a context identifier if: +/// - It is reassigned from inside a nested function (`reassignedByInnerFn`), OR +/// - It is reassigned AND referenced from inside a nested function +/// (`reassigned && referencedByInnerFn`) +/// +/// This is the Rust equivalent of the TypeScript `FindContextIdentifiers` pass. +pub fn find_context_identifiers( + func: &FunctionNode<'_>, + scope_info: &ScopeInfo, + env: &mut Environment, + identifier_locs: &crate::react_compiler_lowering::identifier_loc_index::IdentifierLocIndex, + line_offsets: &LineOffsets, +) -> Result, CompilerError> { + let func_scope = + scope_info.resolve_scope_for_node(func.node_id()).unwrap_or(scope_info.program_scope); + + let mut visitor = ContextIdentifierVisitor { + scope_info, + line_offsets, + env, + scope_stack: vec![func_scope], + function_stack: Vec::new(), + binding_info: FxHashMap::default(), + error: None, + }; + + // Walk params and body (like Babel's func.traverse()): the function node + // itself is not re-entered, so it is never pushed onto `function_stack`. + match func { + FunctionNode::Function(f) => { + if let Some(this_param) = &f.this_param { + visitor.visit_ts_this_parameter(this_param); + } + visitor.visit_formal_parameters(&f.params); + if let Some(body) = &f.body { + visitor.visit_function_body(body); + } + } + FunctionNode::Arrow(arrow) => { + visitor.visit_formal_parameters(&arrow.params); + if arrow.expression { + if let Some(oxc::Statement::ExpressionStatement(es)) = arrow.body.statements.first() + { + visitor.visit_expression(&es.expression); + } else { + visitor.visit_function_body(&arrow.body); + } + } else { + visitor.visit_function_body(&arrow.body); + } + } + } + + if let Some(error) = visitor.error { + return Err(error); + } + + // Supplement the walker-based analysis with referenceToBinding data. + // The AST walker doesn't visit identifiers inside type annotations, + // but Babel's traverse (used by TS findContextIdentifiers) does. + // After scope extraction includes type annotation references, + // we check if any reassigned binding has references in nested function scopes + // via referenceToBinding — matching the TS behavior. + // + // We must skip declaration sites (e.g., the `x` in `function x() {}`), + // which are included in reference_to_binding but are not true references. + // Prefer node-ID comparison (immune to position-0 collisions from synthetic + // nodes), falling back to position when node-IDs are unavailable. + let declaration_node_ids = &scope_info.declaration_node_ids; + for (&ref_nid, &binding_id) in &scope_info.ref_node_id_to_binding { + match visitor.binding_info.get(&binding_id) { + Some(info) if info.reassigned && !info.referenced_by_inner_fn => {} + _ => continue, + } + if declaration_node_ids.contains(&(binding_id, ref_nid)) { + continue; + } + // Get the reference's position from identifier_locs for range checks + let ref_pos = match identifier_locs.get(&ref_nid) { + Some(entry) => entry.start, + None => continue, + }; + let binding = &scope_info.bindings[binding_id.0 as usize]; + // Check if ref_pos is inside a nested function scope + for (&scope_start, &scope_id) in &scope_info.node_to_scope { + if scope_start <= ref_pos { + if let Some(&scope_end) = scope_info.node_to_scope_end.get(&scope_start) { + if ref_pos < scope_end + && matches!( + scope_info.scopes[scope_id.0 as usize].kind, + ScopeKind::Function + ) + && is_captured_by_function(scope_info, binding.scope, scope_id) + { + visitor.binding_info.get_mut(&binding_id).unwrap().referenced_by_inner_fn = + true; + break; + } + } + } + } + } + + // Collect results + Ok(visitor + .binding_info + .into_iter() + .filter(|(_, info)| { + info.reassigned_by_inner_fn || (info.reassigned && info.referenced_by_inner_fn) + }) + .map(|(id, _)| id) + .collect()) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_lowering/hir_builder.rs b/crates/oxc_react_compiler/src/react_compiler_lowering/hir_builder.rs new file mode 100644 index 0000000000000..7bc37c73c33d1 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_lowering/hir_builder.rs @@ -0,0 +1,1343 @@ +use crate::react_compiler_diagnostics::CompilerDiagnostic; +use crate::react_compiler_diagnostics::CompilerDiagnosticDetail; +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_diagnostics::CompilerErrorDetail; +use crate::react_compiler_diagnostics::ErrorCategory; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::each_terminal_successor; +use crate::react_compiler_hir::visitors::terminal_fallthrough; +use crate::react_compiler_hir::*; +use crate::react_compiler_utils::FxIndexMap; +use crate::react_compiler_utils::FxIndexSet; +use crate::scope::BindingId; +use crate::scope::ImportBindingKind; +use crate::scope::ScopeId; +use crate::scope::ScopeInfo; + +use oxc_span::Span; + +use crate::react_compiler_lowering::identifier_loc_index::IdentifierLocIndex; +use crate::react_compiler_lowering::source_loc::LineOffsets; + +// --------------------------------------------------------------------------- +// Reserved word check (matches TS isReservedWord) +// --------------------------------------------------------------------------- + +pub(crate) fn is_always_reserved_word(s: &str) -> bool { + matches!( + s, + "break" + | "case" + | "catch" + | "continue" + | "debugger" + | "default" + | "do" + | "else" + | "finally" + | "for" + | "function" + | "if" + | "in" + | "instanceof" + | "new" + | "return" + | "switch" + | "this" + | "throw" + | "try" + | "typeof" + | "var" + | "void" + | "while" + | "with" + | "class" + | "const" + | "enum" + | "export" + | "extends" + | "import" + | "super" + | "null" + | "true" + | "false" + | "delete" + ) +} + +pub(crate) fn reserved_identifier_diagnostic(name: &str) -> CompilerDiagnostic { + CompilerDiagnostic::new( + ErrorCategory::Syntax, + "Expected a non-reserved identifier name", + Some(format!( + "`{}` is a reserved word in JavaScript and cannot be used as an identifier name", + name + )), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: None, // GeneratedSource in TS + message: Some("reserved word".to_string()), + identifier_name: None, + }) +} + +// --------------------------------------------------------------------------- +// Scope types for tracking break/continue targets +// --------------------------------------------------------------------------- + +enum Scope { + Loop { label: Option, continue_block: BlockId, break_block: BlockId }, + Label { label: String, break_block: BlockId }, + Switch { label: Option, break_block: BlockId }, +} + +impl Scope { + fn label(&self) -> Option<&str> { + match self { + Scope::Loop { label, .. } => label.as_deref(), + Scope::Label { label, .. } => Some(label.as_str()), + Scope::Switch { label, .. } => label.as_deref(), + } + } + + fn break_block(&self) -> BlockId { + match self { + Scope::Loop { break_block, .. } => *break_block, + Scope::Label { break_block, .. } => *break_block, + Scope::Switch { break_block, .. } => *break_block, + } + } +} + +// --------------------------------------------------------------------------- +// WipBlock: a block under construction that does not yet have a terminal +// --------------------------------------------------------------------------- + +pub struct WipBlock { + pub id: BlockId, + pub instructions: Vec, + pub kind: BlockKind, +} + +fn new_block(id: BlockId, kind: BlockKind) -> WipBlock { + WipBlock { id, kind, instructions: Vec::new() } +} + +// --------------------------------------------------------------------------- +// HirBuilder: helper struct for constructing a CFG +// --------------------------------------------------------------------------- + +pub struct HirBuilder<'a, 'b> { + completed: FxIndexMap, + current: WipBlock, + entry: BlockId, + scopes: Vec, + /// Context identifiers: variables captured from an outer scope. + /// Maps the outer scope's BindingId to the source location where it was referenced. + context: FxIndexMap>, + /// Resolved bindings: maps a BindingId to the HIR IdentifierId created for it. + bindings: FxIndexMap, + /// Names already used by bindings, for collision avoidance. + /// Maps name string -> how many times it has been used (for appending _0, _1, ...). + used_names: FxIndexMap, + env: &'b mut Environment<'a>, + scope_info: &'b ScopeInfo, + exception_handler_stack: Vec, + /// Flat instruction table being built up. + instruction_table: Vec>, + /// Traversal context: counts the number of `fbt` tag parents + /// of the current babel node. + pub fbt_depth: u32, + /// The scope of the function being compiled (for context identifier checks). + function_scope: ScopeId, + /// The scope of the outermost component/hook function (for gather_captured_context). + component_scope: ScopeId, + /// Set of BindingIds for variables declared in scopes between component_scope + /// and any inner function scope, that are referenced from an inner function scope. + /// These need StoreContext/LoadContext instead of StoreLocal/LoadLocal. + context_identifiers: rustc_hash::FxHashSet, + /// Set of ScopeIds that have been matched to synthetic blocks/functions. + /// Prevents the same scope from being reused for different synthetic nodes. + claimed_synthetic_scopes: rustc_hash::FxHashSet, + /// Index mapping identifier byte offsets to source locations and JSX status. + identifier_locs: &'b IdentifierLocIndex, + /// Line-offset table for computing `SourceLocation`s from oxc byte spans. + /// Built once per file and shared; replaces the Babel `base.loc` the + /// front-end used to read off the now-removed Babel-shaped AST. + line_offsets: &'b LineOffsets, +} + +impl<'a, 'b> HirBuilder<'a, 'b> { + // ----------------------------------------------------------------------- + // M2: Core methods + // ----------------------------------------------------------------------- + + /// Create a new HirBuilder. + /// + /// - `env`: the shared environment (counters, arenas, error accumulator) + /// - `scope_info`: the scope information from the AST + /// - `function_scope`: the ScopeId of the function being compiled + /// - `bindings`: optional pre-existing bindings (e.g., from a parent function) + /// - `context`: optional pre-existing captured context map + /// - `entry_block_kind`: the kind of the entry block (defaults to `Block`) + pub fn new( + env: &'b mut Environment<'a>, + scope_info: &'b ScopeInfo, + function_scope: ScopeId, + component_scope: ScopeId, + context_identifiers: rustc_hash::FxHashSet, + bindings: Option>, + context: Option>>, + entry_block_kind: Option, + used_names: Option>, + identifier_locs: &'b IdentifierLocIndex, + line_offsets: &'b LineOffsets, + ) -> Self { + let entry = env.next_block_id(); + let kind = entry_block_kind.unwrap_or(BlockKind::Block); + HirBuilder { + completed: FxIndexMap::default(), + current: new_block(entry, kind), + entry, + scopes: Vec::new(), + context: context.unwrap_or_default(), + bindings: bindings.unwrap_or_default(), + used_names: used_names.unwrap_or_default(), + env, + scope_info, + exception_handler_stack: Vec::new(), + instruction_table: Vec::new(), + fbt_depth: 0, + function_scope, + component_scope, + context_identifiers, + claimed_synthetic_scopes: rustc_hash::FxHashSet::default(), + identifier_locs, + line_offsets, + } + } + + /// Compute the HIR `SourceLocation` for an oxc byte span. Always `Some` + /// (oxc nodes always have a span), matching what `convert_ast` produced. + pub fn source_location(&self, span: Span) -> Option { + Some(self.line_offsets.source_location(span)) + } + + /// Check if a scope is the component scope or a descendant of it. + /// Used to determine whether a binding is local to the compiled function + /// or belongs to an ancestor function scope (e.g., a factory function + /// wrapping a nested component declaration). + /// Uses component_scope (the outermost compiled function's scope) rather + /// than function_scope because inner function expressions within the + /// compiled function have their own function_scope but still consider + /// the outer component's variables as local. + fn is_scope_within_compiled_function(&self, scope_id: ScopeId) -> bool { + let mut current = Some(scope_id); + while let Some(id) = current { + if id == self.component_scope { + return true; + } + current = self.scope_info.scopes[id.0 as usize].parent; + } + false + } + + /// Access the environment. + pub fn environment(&self) -> &Environment<'a> { + self.env + } + + /// Access the environment mutably. + pub fn environment_mut(&mut self) -> &mut Environment<'a> { + self.env + } + + /// Create a new unique TypeVar type, allocated from the environment's type arena + /// so that TypeIds are consistent with identifier type slots. + pub fn make_type(&mut self) -> Type { + let type_id = self.env.make_type(); + Type::TypeVar { id: type_id } + } + + /// Access the scope info. + pub fn scope_info(&self) -> &ScopeInfo { + self.scope_info + } + + /// Look up the source location of an identifier by its node_id. + pub fn get_identifier_loc(&self, node_id: u32) -> Option { + self.identifier_locs.get(&node_id).map(|entry| entry.loc.clone()) + } + + /// Check whether a reference at the given byte offset corresponds to a + /// JSXIdentifier. Scans the node_id-keyed index for an entry whose stored + /// `start` matches the offset. + pub fn is_jsx_identifier_at_pos(&self, offset: u32) -> bool { + self.identifier_locs.values().any(|entry| entry.start == offset && entry.is_jsx) + } + + /// Access the function scope (the scope of the function being compiled). + pub fn function_scope(&self) -> ScopeId { + self.function_scope + } + + /// Access the component scope. + pub fn component_scope(&self) -> ScopeId { + self.component_scope + } + + /// Access the context map. + pub fn context(&self) -> &FxIndexMap> { + &self.context + } + + /// Access the pre-computed context identifiers set. + pub fn context_identifiers(&self) -> &rustc_hash::FxHashSet { + &self.context_identifiers + } + + /// Add a binding to the context identifiers set (used by hoisting). + pub fn add_context_identifier(&mut self, binding_id: BindingId) { + self.context_identifiers.insert(binding_id); + } + + pub fn claim_synthetic_scope(&mut self, scope_id: ScopeId) { + self.claimed_synthetic_scopes.insert(scope_id); + } + + pub fn is_synthetic_scope_claimed(&self, scope_id: ScopeId) -> bool { + self.claimed_synthetic_scopes.contains(&scope_id) + } + + /// Access scope_info and environment mutably at the same time. + /// This is safe because they are disjoint fields, but Rust's borrow checker + /// can't prove this through method calls alone. + pub fn scope_info_and_env_mut(&mut self) -> (&ScopeInfo, &mut Environment<'a>) { + (self.scope_info, self.env) + } + + /// Access the identifier location index. + /// Returns the 'a reference to avoid conflicts with mutable borrows on self. + pub fn identifier_locs(&self) -> &'b IdentifierLocIndex { + self.identifier_locs + } + + /// Access the line-offset table. + /// Returns the 'a reference to avoid conflicts with mutable borrows on self. + pub fn line_offsets(&self) -> &'b LineOffsets { + self.line_offsets + } + + /// Access the bindings map. + pub fn bindings(&self) -> &FxIndexMap { + &self.bindings + } + + /// Access the used names map. + pub fn used_names(&self) -> &FxIndexMap { + &self.used_names + } + + /// Merge used names from a child builder back into this builder. + /// This ensures name deduplication works across function scopes. + pub fn merge_used_names(&mut self, child_used_names: FxIndexMap) { + for (name, binding_id) in child_used_names { + self.used_names.entry(name).or_insert(binding_id); + } + } + + /// Merge bindings (binding_id -> IdentifierId) from a child builder back into this builder. + /// This matches TS behavior where parent and child share the same #bindings map by reference, + /// so bindings resolved by the child are automatically visible to the parent. + pub fn merge_bindings(&mut self, child_bindings: FxIndexMap) { + for (binding_id, identifier_id) in child_bindings { + self.bindings.entry(binding_id).or_insert(identifier_id); + } + } + + /// Push an instruction onto the current block. + /// + /// Adds the instruction to the flat instruction table and records + /// its InstructionId in the current block's instruction list. + /// + /// If an exception handler is active, also emits a MaybeThrow terminal + /// after the instruction to model potential control flow to the handler, + /// then continues in a new block. + pub fn push(&mut self, instruction: Instruction<'a>) { + let loc = instruction.loc.clone(); + let instr_id = InstructionId(self.instruction_table.len() as u32); + self.instruction_table.push(instruction); + self.current.instructions.push(instr_id); + + if let Some(&handler) = self.exception_handler_stack.last() { + let continuation = self.reserve(self.current_block_kind()); + self.terminate_with_continuation( + Terminal::MaybeThrow { + continuation: continuation.id, + handler: Some(handler), + id: EvaluationOrder(0), + loc, + effects: None, + }, + continuation, + ); + } + } + + /// Terminate the current block with the given terminal and start a new block. + /// + /// If `next_block_kind` is `Some`, a new current block is created with that kind. + /// Returns the BlockId of the completed block. + pub fn terminate(&mut self, terminal: Terminal, next_block_kind: Option) -> BlockId { + // The placeholder block created here (BlockId(u32::MAX)) is only used when + // next_block_kind is None, meaning this is the final terminate() call. + // It will never be read or completed because build() consumes self + // immediately after, and no further operations should occur on the builder. + let wip = + std::mem::replace(&mut self.current, new_block(BlockId(u32::MAX), BlockKind::Block)); + let block_id = wip.id; + + self.completed.insert( + block_id, + BasicBlock { + kind: wip.kind, + id: block_id, + instructions: wip.instructions, + terminal, + preds: FxIndexSet::default(), + phis: Vec::new(), + }, + ); + + if let Some(kind) = next_block_kind { + let next_id = self.env.next_block_id(); + self.current = new_block(next_id, kind); + } + block_id + } + + /// Terminate the current block with the given terminal, and set + /// a previously reserved block as the new current block. + pub fn terminate_with_continuation(&mut self, terminal: Terminal, continuation: WipBlock) { + let wip = std::mem::replace(&mut self.current, continuation); + let block_id = wip.id; + self.completed.insert( + block_id, + BasicBlock { + kind: wip.kind, + id: block_id, + instructions: wip.instructions, + terminal, + preds: FxIndexSet::default(), + phis: Vec::new(), + }, + ); + } + + /// Reserve a new block so it can be referenced before construction. + /// Use `terminate_with_continuation()` to make it current, or `complete()` to + /// save it directly. + pub fn reserve(&mut self, kind: BlockKind) -> WipBlock { + let id = self.env.next_block_id(); + new_block(id, kind) + } + + /// Save a previously reserved block as completed with the given terminal. + pub fn complete(&mut self, block: WipBlock, terminal: Terminal) { + let block_id = block.id; + self.completed.insert( + block_id, + BasicBlock { + kind: block.kind, + id: block_id, + instructions: block.instructions, + terminal, + preds: FxIndexSet::default(), + phis: Vec::new(), + }, + ); + } + + /// Sets the given wip block as current, executes the closure to populate + /// it and obtain its terminal, then completes the block and restores the + /// previous current block. + pub fn enter_reserved(&mut self, wip: WipBlock, f: impl FnOnce(&mut Self) -> Terminal) { + let prev = std::mem::replace(&mut self.current, wip); + let terminal = f(self); + let completed_wip = std::mem::replace(&mut self.current, prev); + self.completed.insert( + completed_wip.id, + BasicBlock { + kind: completed_wip.kind, + id: completed_wip.id, + instructions: completed_wip.instructions, + terminal, + preds: FxIndexSet::default(), + phis: Vec::new(), + }, + ); + } + + /// Like `enter_reserved`, but the closure returns a `Result`. + pub fn try_enter_reserved( + &mut self, + wip: WipBlock, + f: impl FnOnce(&mut Self) -> Result, + ) -> Result<(), CompilerDiagnostic> { + let prev = std::mem::replace(&mut self.current, wip); + let terminal = f(self)?; + let completed_wip = std::mem::replace(&mut self.current, prev); + self.completed.insert( + completed_wip.id, + BasicBlock { + kind: completed_wip.kind, + id: completed_wip.id, + instructions: completed_wip.instructions, + terminal, + preds: FxIndexSet::default(), + phis: Vec::new(), + }, + ); + Ok(()) + } + + /// Create a new block, set it as current, run the closure to populate it + /// and obtain its terminal, complete the block, and restore the previous + /// current block. Returns the new block's BlockId. + pub fn enter( + &mut self, + kind: BlockKind, + f: impl FnOnce(&mut Self, BlockId) -> Terminal, + ) -> BlockId { + let wip = self.reserve(kind); + let wip_id = wip.id; + self.enter_reserved(wip, |this| f(this, wip_id)); + wip_id + } + + /// Like `enter`, but the closure returns a `Result`. + pub fn try_enter( + &mut self, + kind: BlockKind, + f: impl FnOnce(&mut Self, BlockId) -> Result, + ) -> Result { + let wip = self.reserve(kind); + let wip_id = wip.id; + self.try_enter_reserved(wip, |this| f(this, wip_id))?; + Ok(wip_id) + } + + /// Push an exception handler, run the closure, then pop the handler. + pub fn enter_try_catch(&mut self, handler: BlockId, f: impl FnOnce(&mut Self)) { + self.exception_handler_stack.push(handler); + f(self); + self.exception_handler_stack.pop(); + } + + /// Like `enter_try_catch`, but the closure returns a `Result`. + pub fn try_enter_try_catch( + &mut self, + handler: BlockId, + f: impl FnOnce(&mut Self) -> Result<(), CompilerDiagnostic>, + ) -> Result<(), CompilerDiagnostic> { + self.exception_handler_stack.push(handler); + let result = f(self); + self.exception_handler_stack.pop(); + result + } + + /// Return the top of the exception handler stack, or None. + pub fn resolve_throw_handler(&self) -> Option { + self.exception_handler_stack.last().copied() + } + + /// Push a Loop scope, run the closure, pop and verify. + pub fn loop_scope( + &mut self, + label: Option, + continue_block: BlockId, + break_block: BlockId, + f: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.scopes.push(Scope::Loop { label: label.clone(), continue_block, break_block }); + let value = f(self)?; + let last = self.scopes.pop().expect("Mismatched loop scope: stack empty"); + match &last { + Scope::Loop { label: l, continue_block: c, break_block: b } => { + assert!( + *l == label && *c == continue_block && *b == break_block, + "Mismatched loop scope" + ); + } + _ => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Mismatched loop scope: expected Loop, got other", + None, + )); + } + } + Ok(value) + } + + /// Push a Label scope, run the closure, pop and verify. + pub fn label_scope( + &mut self, + label: String, + break_block: BlockId, + f: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.scopes.push(Scope::Label { label: label.clone(), break_block }); + let value = f(self)?; + let last = self.scopes.pop().expect("Mismatched label scope: stack empty"); + match &last { + Scope::Label { label: l, break_block: b } => { + assert!(*l == label && *b == break_block, "Mismatched label scope"); + } + _ => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Mismatched label scope: expected Label, got other", + None, + )); + } + } + Ok(value) + } + + /// Push a Switch scope, run the closure, pop and verify. + pub fn switch_scope( + &mut self, + label: Option, + break_block: BlockId, + f: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.scopes.push(Scope::Switch { label: label.clone(), break_block }); + let value = f(self)?; + let last = self.scopes.pop().expect("Mismatched switch scope: stack empty"); + match &last { + Scope::Switch { label: l, break_block: b } => { + assert!(*l == label && *b == break_block, "Mismatched switch scope"); + } + _ => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Mismatched switch scope: expected Switch, got other", + None, + )); + } + } + Ok(value) + } + + /// Look up the break target for the given label (or the innermost + /// loop/switch if label is None). + pub fn lookup_break(&self, label: Option<&str>) -> Result { + for scope in self.scopes.iter().rev() { + match scope { + Scope::Loop { .. } | Scope::Switch { .. } if label.is_none() => { + return Ok(scope.break_block()); + } + _ if label.is_some() && scope.label() == label => { + return Ok(scope.break_block()); + } + _ => continue, + } + } + Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Expected a loop or switch to be in scope for break", + None, + )) + } + + /// Look up the continue target for the given label (or the innermost + /// loop if label is None). Only loops support continue. + pub fn lookup_continue(&self, label: Option<&str>) -> Result { + for scope in self.scopes.iter().rev() { + match scope { + Scope::Loop { label: scope_label, continue_block, .. } => { + if label.is_none() || label == scope_label.as_deref() { + return Ok(*continue_block); + } + } + _ => { + if label.is_some() && scope.label() == label { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Continue may only refer to a labeled loop", + None, + )); + } + } + } + } + Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Expected a loop to be in scope for continue", + None, + )) + } + + /// Create a temporary identifier with a fresh id, returning its IdentifierId. + pub fn make_temporary(&mut self, loc: Option) -> IdentifierId { + let id = self.env.next_identifier_id(); + // Update the loc on the allocated identifier + self.env.identifiers[id.0 as usize].loc = loc; + id + } + + /// Set the source location for an identifier. + pub fn set_identifier_loc(&mut self, id: IdentifierId, loc: Option) { + self.env.identifiers[id.0 as usize].loc = loc; + } + + /// Record an error on the environment. + /// Returns `Err` for Invariant errors (matching TS throw behavior). + pub fn record_error(&mut self, error: CompilerErrorDetail) -> Result<(), CompilerError> { + self.env.record_error(error) + } + + /// Record a diagnostic on the environment. + pub fn record_diagnostic(&mut self, diagnostic: CompilerDiagnostic) { + self.env.record_diagnostic(diagnostic); + } + + /// Check if a name has a local binding (non-module-level). + /// This is used for checking if fbt/fbs JSX tags are local bindings + /// (which is not supported). + pub fn has_local_binding(&self, name: &str) -> bool { + if let Some(binding) = + self.scope_info.find_binding_in_descendants(name, self.component_scope) + { + return binding.scope != self.scope_info.program_scope; + } + false + } + + /// Return the kind of the current block. + pub fn current_block_kind(&self) -> BlockKind { + self.current.kind + } + + /// Construct the final HIR and instruction table from the completed blocks. + /// + /// Performs these post-build passes: + /// 1. Reverse-postorder sort + unreachable block removal + /// 2. Check for unreachable blocks containing FunctionExpression instructions + /// 3. Remove unreachable for-loop updates + /// 4. Remove dead do-while statements + /// 5. Remove unnecessary try-catch + /// 6. Number all instructions and terminals + /// 7. Mark predecessor blocks + pub fn build( + mut self, + ) -> Result< + ( + HIR, + Vec>, + FxIndexMap, + FxIndexMap, + ), + CompilerError, + > { + let mut hir = HIR { blocks: std::mem::take(&mut self.completed), entry: self.entry }; + + let mut instructions = std::mem::take(&mut self.instruction_table); + + let rpo_blocks = get_reverse_postordered_blocks(&hir, &instructions); + + // Check for unreachable blocks that contain FunctionExpression instructions. + // These could contain hoisted declarations that we can't safely remove. + for (id, block) in &hir.blocks { + if !rpo_blocks.contains_key(id) { + let has_function_expr = block.instructions.iter().any(|&instr_id| { + matches!( + instructions[instr_id.0 as usize].value, + InstructionValue::FunctionExpression { .. } + ) + }); + if has_function_expr { + let loc = block + .instructions + .first() + .and_then(|&i| instructions[i.0 as usize].loc.clone()) + .or_else(|| block.terminal.loc().copied()); + self.env.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "Support functions with unreachable code that may contain hoisted declarations".to_string(), + description: None, + loc, + suggestions: None, + })?; + } + } + } + + hir.blocks = rpo_blocks; + + remove_unreachable_for_updates(&mut hir); + remove_dead_do_while_statements(&mut hir); + remove_unnecessary_try_catch(&mut hir); + mark_instruction_ids(&mut hir, &mut instructions); + mark_predecessors(&mut hir); + + let used_names = self.used_names; + let bindings = self.bindings; + Ok((hir, instructions, used_names, bindings)) + } + + // ----------------------------------------------------------------------- + // M3: Binding resolution methods + // ----------------------------------------------------------------------- + + /// Map a BindingId to an HIR IdentifierId. + /// + /// On first encounter, creates a new Identifier with the given name and a fresh id. + /// On subsequent encounters, returns the cached IdentifierId. + /// Handles name collisions by appending `_0`, `_1`, etc. + /// + /// Records errors for variables named 'fbt' or 'this'. + pub fn resolve_binding( + &mut self, + name: &str, + binding_id: BindingId, + ) -> Result { + self.resolve_binding_with_loc(name, binding_id, None) + } + + /// Map a BindingId to an HIR IdentifierId, with an optional source location. + pub fn resolve_binding_with_loc( + &mut self, + name: &str, + binding_id: BindingId, + loc: Option, + ) -> Result { + // Check for unsupported names BEFORE the cache check. + // In TS, resolveBinding records fbt errors when node.name === 'fbt'. After a name collision + // causes a rename (e.g., "fbt" -> "fbt_0"), TS's scope.rename changes the AST node's name, + // preventing subsequent fbt error recording. We simulate this by checking whether the + // resolved name for this binding is still "fbt" (not renamed to "fbt_0" etc.). + if name == "fbt" { + // Check if this binding was previously resolved to a renamed version + let should_record_fbt_error = + if let Some(&identifier_id) = self.bindings.get(&binding_id) { + // Already resolved - check if the resolved name is still "fbt" + match &self.env.identifiers[identifier_id.0 as usize].name { + Some(IdentifierName::Named(resolved_name)) => resolved_name == "fbt", + _ => false, + } + } else { + // First resolution - always record + true + }; + if should_record_fbt_error { + let error_loc = self.scope_info.bindings[binding_id.0 as usize] + .declaration_node_id + .and_then(|nid| self.get_identifier_loc(nid)) + .or_else(|| loc.clone()); + self.env.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "Support local variables named `fbt`".to_string(), + description: Some( + "Local variables named `fbt` may conflict with the fbt plugin and are not yet supported".to_string(), + ), + loc: error_loc, + suggestions: None, + })?; + } + } + + // If we've already resolved this binding, return the cached IdentifierId + if let Some(&identifier_id) = self.bindings.get(&binding_id) { + return Ok(identifier_id); + } + + if is_always_reserved_word(name) { + // Match TS behavior: makeIdentifierName throws for reserved words. + return Err(CompilerError::from(reserved_identifier_diagnostic(name))); + } + + // Find a unique name: start with the original name, then try name_0, name_1, ... + let mut candidate = name.to_string(); + let mut index = 0u32; + loop { + if let Some(&existing_binding_id) = self.used_names.get(&candidate) { + if existing_binding_id == binding_id { + // Same binding, use this name + break; + } + // Name collision with a different binding, try the next suffix + candidate = format!("{}_{}", name, index); + index += 1; + } else { + // Name is available + break; + } + } + + // Record rename if the candidate differs from the original name + if candidate != name { + let binding = &self.scope_info.bindings[binding_id.0 as usize]; + if let Some(decl_start) = binding.declaration_start { + self.env.renames.push(crate::react_compiler_hir::environment::BindingRename { + original: name.to_string(), + renamed: candidate.clone(), + declaration_start: decl_start, + }); + } + } + + // Allocate identifier in the arena + let id = self.env.next_identifier_id(); + // Update the name and loc on the allocated identifier + self.env.identifiers[id.0 as usize].name = Some(IdentifierName::Named(candidate.clone())); + // Prefer the binding's declaration loc over the reference loc. + // This matches TS behavior where Babel's resolveBinding returns the + // binding identifier's original loc (the declaration site). + let binding = &self.scope_info.bindings[binding_id.0 as usize]; + let decl_loc = binding.declaration_node_id.and_then(|nid| self.get_identifier_loc(nid)); + if let Some(ref dl) = decl_loc { + self.env.identifiers[id.0 as usize].loc = Some(dl.clone()); + } else if let Some(ref loc) = loc { + self.env.identifiers[id.0 as usize].loc = Some(loc.clone()); + } + + self.used_names.insert(candidate, binding_id); + self.bindings.insert(binding_id, id); + Ok(id) + } + + /// Set the loc on an identifier to the declaration-site loc. + /// This overrides any previously-set loc (which may have come from a reference site). + pub fn set_identifier_declaration_loc( + &mut self, + id: IdentifierId, + loc: &Option, + ) { + if let Some(loc_val) = loc { + self.env.identifiers[id.0 as usize].loc = Some(loc_val.clone()); + } + } + + /// Resolve an identifier reference to a VariableBinding. + /// + /// Uses ScopeInfo to determine whether the reference is: + /// - Global (no binding found) + /// - ImportDefault, ImportSpecifier, ImportNamespace (program-scope import binding) + /// - ModuleLocal (program-scope non-import binding) + /// - Identifier (local binding, resolved via resolve_binding) + pub fn resolve_identifier( + &mut self, + name: &str, + _start_offset: u32, + loc: Option, + node_id: Option, + ) -> Result { + let binding_data = self.scope_info.resolve_reference_for_node(node_id); + + match binding_data { + None => { + // No binding found: this is a global + Ok(VariableBinding::Global { name: name.to_string() }) + } + Some(binding) => { + // Treat type-only declarations as globals so the compiler + // doesn't try to create/initialize HIR bindings for them. + // TSEnumDeclaration is included because enums inside function + // bodies are lowered as UnsupportedNode and their binding + // is never initialized in HIR. + if matches!( + binding.declaration_type.as_str(), + "TSTypeAliasDeclaration" + | "TSInterfaceDeclaration" + | "TSEnumDeclaration" + | "TSModuleDeclaration" + ) { + return Ok(VariableBinding::Global { name: name.to_string() }); + } + if binding.scope == self.scope_info.program_scope { + // Module-level binding: check import info + Ok(match &binding.import { + Some(import_info) => match import_info.kind { + ImportBindingKind::Default => VariableBinding::ImportDefault { + name: name.to_string(), + module: import_info.source.clone(), + }, + ImportBindingKind::Named => VariableBinding::ImportSpecifier { + name: name.to_string(), + module: import_info.source.clone(), + imported: import_info + .imported + .clone() + .unwrap_or_else(|| name.to_string()), + }, + ImportBindingKind::Namespace => VariableBinding::ImportNamespace { + name: name.to_string(), + module: import_info.source.clone(), + }, + }, + None => VariableBinding::ModuleLocal { name: name.to_string() }, + }) + } else if !self.is_scope_within_compiled_function(binding.scope) { + Ok(VariableBinding::ModuleLocal { name: name.to_string() }) + } else { + let binding_id = binding.id; + let binding_kind = + crate::react_compiler_lowering::convert_binding_kind(&binding.kind); + let identifier_id = self.resolve_binding_with_loc(name, binding_id, loc)?; + Ok(VariableBinding::Identifier { identifier: identifier_id, binding_kind }) + } + } + } + } + + /// Check if an identifier reference resolves to a context identifier. + /// + /// A context identifier is a variable declared in an ancestor scope of the + /// current function's scope, but NOT in the program scope itself and NOT + /// in the function's own scope. These are "captured" variables from an + /// enclosing function. + pub fn is_context_identifier( + &self, + _name: &str, + _start_offset: u32, + node_id: Option, + ) -> bool { + let binding = self.scope_info.resolve_reference_for_node(node_id); + + match binding { + None => false, + Some(binding_data) => { + if binding_data.scope == self.scope_info.program_scope { + return false; + } + self.context_identifiers.contains(&binding_data.id) + } + } + } + + /// Like `is_context_identifier`, for callers that already resolved a + /// BindingId instead of going through a reference node. + pub fn is_context_binding(&self, binding_id: BindingId) -> bool { + let binding = &self.scope_info.bindings[binding_id.0 as usize]; + if binding.scope == self.scope_info.program_scope { + return false; + } + self.context_identifiers.contains(&binding_id) + } + + /// Resolve the binding for a function declaration's id the way TS does: + /// Babel's `path.scope.getBinding(name)` starts at the function's OWN + /// scope, so a body-level local (or parameter) that shadows the function's + /// name resolves to that inner binding rather than to the function's + /// hoisted binding in the parent scope. + /// + /// Babel's `scope.rename` re-keys a scope's bindings when the TS builder + /// renames a shadowed binding (e.g. `init` -> `init_0`), so a binding only + /// matches if its *current* name — the resolved HIR identifier name once + /// resolved — still equals `name`. A binding renamed *to* `name` overwrites + /// the original key in Babel and takes precedence over an unresolved + /// binding with that original name. + /// + /// Returns None when the walk resolves outside the compiled function + /// (degraded scope info); callers should fall back to node-based + /// resolution in that case. + pub fn get_function_declaration_binding( + &self, + function_scope: ScopeId, + name: &str, + ) -> Option { + // None = unresolved binding; Some(matches) = resolved, current name comparison + let resolved_name_matches = |bid: BindingId| -> Option { + let &identifier_id = self.bindings.get(&bid)?; + match &self.env.identifiers[identifier_id.0 as usize].name { + Some(IdentifierName::Named(n)) => Some(n == name), + _ => Some(false), + } + }; + let mut current = Some(function_scope); + while let Some(id) = current { + let scope = &self.scope_info.scopes[id.0 as usize]; + let mut found = scope + .bindings + .values() + .copied() + .find(|&bid| resolved_name_matches(bid) == Some(true)); + if found.is_none() { + if let Some(&bid) = scope.bindings.get(name) { + // Skip bindings that were renamed away from `name`. + if resolved_name_matches(bid) != Some(false) { + found = Some(bid); + } + } + } + if let Some(bid) = found { + let binding_scope = self.scope_info.bindings[bid.0 as usize].scope; + if !self.is_scope_within_compiled_function(binding_scope) { + return None; + } + return Some(bid); + } + current = scope.parent; + } + None + } +} + +// --------------------------------------------------------------------------- +// Post-build helper functions +// --------------------------------------------------------------------------- + +/// Compute a reverse-postorder of blocks reachable from the entry. +/// +/// Visits successors in reverse order so that when the postorder list is +/// reversed, sibling edges appear in program order. +/// +/// Blocks not reachable through successors are removed. Blocks that are +/// only reachable as fallthroughs (not through real successor edges) are +/// replaced with empty blocks that have an Unreachable terminal. +pub fn get_reverse_postordered_blocks( + hir: &HIR, + _instructions: &[Instruction], +) -> FxIndexMap { + let mut visited: FxIndexSet = FxIndexSet::default(); + let mut used: FxIndexSet = FxIndexSet::default(); + let mut used_fallthroughs: FxIndexSet = FxIndexSet::default(); + let mut postorder: Vec = Vec::new(); + + fn visit( + hir: &HIR, + block_id: BlockId, + is_used: bool, + visited: &mut FxIndexSet, + used: &mut FxIndexSet, + used_fallthroughs: &mut FxIndexSet, + postorder: &mut Vec, + ) { + let was_used = used.contains(&block_id); + let was_visited = visited.contains(&block_id); + visited.insert(block_id); + if is_used { + used.insert(block_id); + } + if was_visited && (was_used || !is_used) { + return; + } + + let block = hir + .blocks + .get(&block_id) + .unwrap_or_else(|| panic!("[HIRBuilder] expected block {:?} to exist", block_id)); + + // Visit successors in reverse order so that when we reverse the + // postorder list, sibling edges come out in program order. + let mut successors = each_terminal_successor(&block.terminal); + successors.reverse(); + + let fallthrough = terminal_fallthrough(&block.terminal); + + // Visit fallthrough first (marking as not-yet-used) to ensure its + // block ID is emitted in the correct position. + if let Some(ft) = fallthrough { + if is_used { + used_fallthroughs.insert(ft); + } + visit(hir, ft, false, visited, used, used_fallthroughs, postorder); + } + for successor in successors { + visit(hir, successor, is_used, visited, used, used_fallthroughs, postorder); + } + + if !was_visited { + postorder.push(block_id); + } + } + + visit(hir, hir.entry, true, &mut visited, &mut used, &mut used_fallthroughs, &mut postorder); + + let mut blocks = FxIndexMap::default(); + for block_id in postorder.into_iter().rev() { + let block = hir.blocks.get(&block_id).unwrap(); + if used.contains(&block_id) { + blocks.insert(block_id, block.clone()); + } else if used_fallthroughs.contains(&block_id) { + blocks.insert( + block_id, + BasicBlock { + kind: block.kind, + id: block_id, + instructions: Vec::new(), + terminal: Terminal::Unreachable { + id: block.terminal.evaluation_order(), + loc: block.terminal.loc().copied(), + }, + preds: block.preds.clone(), + phis: Vec::new(), + }, + ); + } + // otherwise this block is unreachable and is dropped + } + + blocks +} + +/// For each block with a `For` terminal whose update block is not in the +/// blocks map, set update to None. +pub fn remove_unreachable_for_updates(hir: &mut HIR) { + let block_ids: FxIndexSet = hir.blocks.keys().copied().collect(); + for block in hir.blocks.values_mut() { + if let Terminal::For { update, .. } = &mut block.terminal { + if let Some(update_id) = *update { + if !block_ids.contains(&update_id) { + *update = None; + } + } + } + } +} + +/// For each block with a `DoWhile` terminal whose test block is not in +/// the blocks map, replace the terminal with a Goto to the loop block. +pub fn remove_dead_do_while_statements(hir: &mut HIR) { + let block_ids: FxIndexSet = hir.blocks.keys().copied().collect(); + for block in hir.blocks.values_mut() { + let should_replace = if let Terminal::DoWhile { test, .. } = &block.terminal { + !block_ids.contains(test) + } else { + false + }; + if should_replace { + if let Terminal::DoWhile { loop_block, id, loc, .. } = std::mem::replace( + &mut block.terminal, + Terminal::Unreachable { id: EvaluationOrder(0), loc: None }, + ) { + block.terminal = + Terminal::Goto { block: loop_block, variant: GotoVariant::Break, id, loc }; + } + } + } +} + +/// For each block with a `Try` terminal whose handler block is not in +/// the blocks map, replace the terminal with a Goto to the try block. +/// +/// Also cleans up the fallthrough block's predecessors if the handler +/// was the only path to it. +pub fn remove_unnecessary_try_catch(hir: &mut HIR) { + let block_ids: FxIndexSet = hir.blocks.keys().copied().collect(); + + // Collect the blocks that need replacement and their associated data + let replacements: Vec<(BlockId, BlockId, BlockId, BlockId, Option)> = hir + .blocks + .iter() + .filter_map(|(&block_id, block)| { + if let Terminal::Try { block: try_block, handler, fallthrough, loc, .. } = + &block.terminal + { + if !block_ids.contains(handler) { + return Some((block_id, *try_block, *handler, *fallthrough, loc.clone())); + } + } + None + }) + .collect(); + + for (block_id, try_block, handler_id, fallthrough_id, loc) in replacements { + // Replace the terminal + if let Some(block) = hir.blocks.get_mut(&block_id) { + block.terminal = Terminal::Goto { + block: try_block, + id: EvaluationOrder(0), + loc, + variant: GotoVariant::Break, + }; + } + + // Clean up fallthrough predecessor info + if let Some(fallthrough) = hir.blocks.get_mut(&fallthrough_id) { + if fallthrough.preds.len() == 1 && fallthrough.preds.contains(&handler_id) { + // The handler was the only predecessor: remove the fallthrough block + hir.blocks.shift_remove(&fallthrough_id); + } else { + fallthrough.preds.shift_remove(&handler_id); + } + } + } +} + +/// Sequentially number all instructions and terminals starting from 1. +pub fn mark_instruction_ids(hir: &mut HIR, instructions: &mut [Instruction]) { + let mut order: u32 = 0; + for block in hir.blocks.values_mut() { + for &instr_id in &block.instructions { + order += 1; + instructions[instr_id.0 as usize].id = EvaluationOrder(order); + } + order += 1; + block.terminal.set_evaluation_order(EvaluationOrder(order)); + } +} + +/// DFS from entry, for each successor add the predecessor's id to +/// the successor's preds set. +/// +/// Note: This only visits direct successors (via `each_terminal_successor`), +/// not fallthrough blocks. Fallthrough blocks are reached indirectly via +/// Goto terminals from within branching blocks, matching the TypeScript +/// `markPredecessors` behavior. +pub fn mark_predecessors(hir: &mut HIR) { + // Clear all preds first + for block in hir.blocks.values_mut() { + block.preds.clear(); + } + + let mut visited: FxIndexSet = FxIndexSet::default(); + + fn visit( + hir: &mut HIR, + block_id: BlockId, + prev_block_id: Option, + visited: &mut FxIndexSet, + ) { + // Add predecessor + if let Some(prev_id) = prev_block_id { + if let Some(block) = hir.blocks.get_mut(&block_id) { + block.preds.insert(prev_id); + } else { + return; + } + } + + if visited.contains(&block_id) { + return; + } + visited.insert(block_id); + + // Get successors before mutating + let successors = if let Some(block) = hir.blocks.get(&block_id) { + each_terminal_successor(&block.terminal) + } else { + return; + }; + + for successor in successors { + visit(hir, successor, Some(block_id), visited); + } + } + + visit(hir, hir.entry, None, &mut visited); +} + +// --------------------------------------------------------------------------- +// Public helper functions +// --------------------------------------------------------------------------- + +/// Create a temporary Place with a fresh identifier allocated in the arena. +pub fn create_temporary_place(env: &mut Environment, loc: Option) -> Place { + let id = env.next_identifier_id(); + // Update the loc on the allocated identifier + env.identifiers[id.0 as usize].loc = loc; + Place { identifier: id, reactive: false, effect: Effect::Unknown, loc: None } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_lowering/identifier_loc_index.rs b/crates/oxc_react_compiler/src/react_compiler_lowering/identifier_loc_index.rs new file mode 100644 index 0000000000000..72a5a1f10a835 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_lowering/identifier_loc_index.rs @@ -0,0 +1,332 @@ +//! Builds an index mapping identifier node-IDs to source locations. +//! +//! Walks the function's oxc AST to collect an [`IdentifierLocEntry`] for every +//! Identifier / JSXIdentifier node (and for identifiers inside TS type +//! annotations). Keyed by node_id (== `span.start`) for identity lookups; each +//! entry also stores `start` (byte offset) for range-containment checks in +//! `gather_captured_context`. +//! +//! This is a translation of the original immutable `IdentifierLocVisitor`, which +//! was driven by a Babel-shaped AST walker. That walker deliberately visited only +//! a NARROW set of identifier positions, and TS type identifiers came from +//! `collect_type_idents`, which collected only `IdentifierReference` / +//! `IdentifierName` (never `BindingIdentifier`). The overrides below restrict the +//! oxc walk to match those positions instead of relying on oxc's default +//! full-AST traversal. The traversal records: +//! +//! * every reference / binding identifier → `is_jsx = false` +//! * function / class declaration & expression names → `is_declaration_name = true` +//! * JSX element-name identifiers → `is_jsx = true` plus the enclosing +//! `JSXOpeningElement`'s loc as `opening_element_loc` +//! * identifiers inside TS type subtrees → `in_type_annotation = true` +//! +//! Positions deliberately NOT recorded, matching the original walker: +//! +//! * non-computed member property names (`a.b` → `b`) +//! * non-computed object / class member keys (`{ a: 1 }` → `a`) +//! * JSX attribute names and JSX closing-element names +//! * label identifiers (`LabeledStatement` / `break` / `continue` targets) +//! * class `super_class` (`extends Foo`) and class member bodies +//! * TS declaration statements (type alias / interface / enum / module) +//! * `BindingIdentifier`s inside TS type subtrees (e.g. type-parameter names) + +use rustc_hash::FxHashMap; + +use oxc_ast::ast as oxc; +use oxc_ast_visit::Visit; + +use crate::react_compiler_hir::SourceLocation; +use crate::scope::ScopeInfo; + +use crate::react_compiler_lowering::FunctionNode; +use crate::react_compiler_lowering::source_loc::LineOffsets; + +/// Source location and whether the identifier is a JSXIdentifier. +pub struct IdentifierLocEntry { + /// The byte offset of the identifier (base.start). Stored here so that + /// callers iterating by node_id can still do position-range containment + /// checks without a separate bridge map. + pub start: u32, + pub loc: SourceLocation, + pub is_jsx: bool, + /// For JSX identifiers that are the root name of a JSXOpeningElement, + /// stores the JSXOpeningElement's loc (which spans the full tag). + pub opening_element_loc: Option, + /// True if this identifier is the name of a function/class declaration + /// (not an expression reference). Used by `gather_captured_context` to + /// skip non-expression positions, matching the TS behavior where the + /// Expression visitor doesn't visit declaration names. + pub is_declaration_name: bool, + /// True if this identifier sits inside a type annotation subtree + /// (TypeAnnotation/TSTypeAnnotation/TypeAlias/TSTypeAliasDeclaration). + /// `gather_captured_context` skips these to match the TS + /// gatherCapturedContext traverse, which skips those subtrees; the + /// hoisting analysis and FindContextIdentifiers do NOT skip them in TS. + pub in_type_annotation: bool, +} + +/// Index mapping node_id → IdentifierLocEntry for all Identifier +/// and JSXIdentifier nodes in a function's AST. +pub type IdentifierLocIndex = FxHashMap; + +struct IdentifierLocVisitor<'a> { + line_offsets: &'a LineOffsets, + index: IdentifierLocIndex, + /// Tracks the current JSXOpeningElement's loc while walking its name. + current_opening_element_loc: Option, + /// Depth of TS type subtrees currently being walked. Identifiers recorded + /// while this is non-zero get `in_type_annotation = true`. + type_depth: u32, +} + +impl<'a> IdentifierLocVisitor<'a> { + fn record(&mut self, span: oxc_span::Span, is_jsx: bool, is_declaration_name: bool) { + let opening_element_loc = + if is_jsx { self.current_opening_element_loc.clone() } else { None }; + // `or_insert` keeps the richer entry already recorded for a node_id. + // Function/class names are recorded as declaration names *before* the + // generic binding-identifier walk re-visits them, so the declaration + // entry wins, matching the original visitor. + self.index.entry(span.start).or_insert(IdentifierLocEntry { + start: span.start, + loc: self.line_offsets.source_location(span), + is_jsx, + opening_element_loc, + is_declaration_name, + in_type_annotation: self.type_depth > 0, + }); + } + + /// Record the JSX element name identifiers (and only those) while the + /// `current_opening_element_loc` is set, mirroring the original + /// `walk_jsx_element_name` / `walk_jsx_member_expression`. + fn record_jsx_element_name(&mut self, name: &oxc::JSXElementName<'a>) { + match name { + oxc::JSXElementName::Identifier(id) => self.record(id.span, true, false), + oxc::JSXElementName::IdentifierReference(id) => self.record(id.span, true, false), + oxc::JSXElementName::ThisExpression(t) => self.record(t.span, true, false), + oxc::JSXElementName::MemberExpression(m) => self.record_jsx_member_expression(m), + // JSXNamespacedName identifiers are not visited by the original walker. + oxc::JSXElementName::NamespacedName(_) => {} + } + } + + fn record_jsx_member_expression(&mut self, expr: &oxc::JSXMemberExpression<'a>) { + match &expr.object { + oxc::JSXMemberExpressionObject::IdentifierReference(id) => { + self.record(id.span, true, false); + } + oxc::JSXMemberExpressionObject::ThisExpression(t) => self.record(t.span, true, false), + oxc::JSXMemberExpressionObject::MemberExpression(inner) => { + self.record_jsx_member_expression(inner); + } + } + self.record(expr.property.span, true, false); + } +} + +impl<'a> Visit<'a> for IdentifierLocVisitor<'a> { + fn visit_identifier_reference(&mut self, it: &oxc::IdentifierReference<'a>) { + self.record(it.span, false, false); + } + + fn visit_identifier_name(&mut self, it: &oxc::IdentifierName<'a>) { + self.record(it.span, false, false); + } + + fn visit_binding_identifier(&mut self, it: &oxc::BindingIdentifier<'a>) { + // `collect_type_idents` only collected IdentifierReference / IdentifierName, + // never BindingIdentifier, so type-parameter declaration names (``) and + // other binding positions inside type subtrees must not be recorded. + if self.type_depth > 0 { + return; + } + self.record(it.span, false, false); + } + + fn visit_jsx_identifier(&mut self, it: &oxc::JSXIdentifier<'a>) { + self.record(it.span, true, false); + } + + fn visit_function(&mut self, it: &oxc::Function<'a>, flags: oxc_syntax::scope::ScopeFlags) { + // The function's own name is a declaration name, not an expression + // reference. Record it first so the generic binding-identifier walk + // (via the default traversal below) does not overwrite the flag. + if let Some(id) = &it.id { + self.record(id.span, false, true); + } + oxc_ast_visit::walk::walk_function(self, it, flags); + } + + fn visit_class(&mut self, it: &oxc::Class<'a>) { + // The original immutable walker recorded only the class name and then the + // class's type-bearing parts (decorators / implements / type params) as + // RawNodes (type idents only). It did NOT walk `super_class` (the extends + // clause) nor the class body's method/property members. + if let Some(id) = &it.id { + self.record(id.span, false, true); + } + if let Some(type_parameters) = &it.type_parameters { + self.visit_ts_type_parameter_declaration(type_parameters); + } + if let Some(super_type_arguments) = &it.super_type_arguments { + self.visit_ts_type_parameter_instantiation(super_type_arguments); + } + self.type_depth += 1; + self.visit_ts_class_implements_list(&it.implements); + self.type_depth -= 1; + } + + fn visit_static_member_expression(&mut self, it: &oxc::StaticMemberExpression<'a>) { + // Original walked the property only when computed; a static member is + // non-computed, so the property name is never recorded. + self.visit_expression(&it.object); + } + + fn visit_object_property(&mut self, it: &oxc::ObjectProperty<'a>) { + // Original walked the key only when computed. + if it.computed { + self.visit_property_key(&it.key); + } + self.visit_expression(&it.value); + } + + fn visit_jsx_element(&mut self, it: &oxc::JSXElement<'a>) { + // Mirror the original walker: the opening element's loc is active only + // while walking the element name (and its type arguments); it is cleared + // before attributes and children. + self.current_opening_element_loc = + Some(self.line_offsets.source_location(it.opening_element.span)); + self.record_jsx_element_name(&it.opening_element.name); + if let Some(type_args) = &it.opening_element.type_arguments { + self.visit_ts_type_parameter_instantiation(type_args); + } + self.current_opening_element_loc = None; + + // The original walker visited only attribute VALUES and spread arguments, + // never attribute names, and had no closing-element handling. + for attr in &it.opening_element.attributes { + match attr { + oxc::JSXAttributeItem::Attribute(a) => { + if let Some(value) = &a.value { + match value { + oxc::JSXAttributeValue::ExpressionContainer(c) => { + self.visit_jsx_expression_container(c); + } + oxc::JSXAttributeValue::Element(el) => self.visit_jsx_element(el), + oxc::JSXAttributeValue::Fragment(f) => self.visit_jsx_fragment(f), + oxc::JSXAttributeValue::StringLiteral(_) => {} + } + } + } + oxc::JSXAttributeItem::SpreadAttribute(a) => { + self.visit_expression(&a.argument); + } + } + } + for child in &it.children { + self.visit_jsx_child(child); + } + } + + fn visit_ts_type(&mut self, it: &oxc::TSType<'a>) { + self.type_depth += 1; + oxc_ast_visit::walk::walk_ts_type(self, it); + self.type_depth -= 1; + } + + fn visit_ts_type_annotation(&mut self, it: &oxc::TSTypeAnnotation<'a>) { + self.type_depth += 1; + oxc_ast_visit::walk::walk_ts_type_annotation(self, it); + self.type_depth -= 1; + } + + fn visit_ts_type_parameter_instantiation( + &mut self, + it: &oxc::TSTypeParameterInstantiation<'a>, + ) { + self.type_depth += 1; + oxc_ast_visit::walk::walk_ts_type_parameter_instantiation(self, it); + self.type_depth -= 1; + } + + fn visit_ts_type_parameter_declaration(&mut self, it: &oxc::TSTypeParameterDeclaration<'a>) { + self.type_depth += 1; + oxc_ast_visit::walk::walk_ts_type_parameter_declaration(self, it); + self.type_depth -= 1; + } + + // The original immutable walker treated these TS declaration statements as + // no-ops (nothing inside them was recorded). Override to skip entirely. + fn visit_ts_type_alias_declaration(&mut self, _it: &oxc::TSTypeAliasDeclaration<'a>) {} + + fn visit_ts_interface_declaration(&mut self, _it: &oxc::TSInterfaceDeclaration<'a>) {} + + fn visit_ts_enum_declaration(&mut self, _it: &oxc::TSEnumDeclaration<'a>) {} + + fn visit_ts_module_declaration(&mut self, _it: &oxc::TSModuleDeclaration<'a>) {} +} + +/// Build an index of all Identifier and JSXIdentifier positions in a function's AST. +/// +/// Walks the function's params (`FormalParameters`) and body, mirroring the +/// original Babel `IdentifierLocVisitor`: the function node itself is not +/// re-entered (its own name, if any, is recorded explicitly). +pub fn build_identifier_loc_index( + func: &FunctionNode<'_>, + scope_info: &ScopeInfo, + line_offsets: &LineOffsets, +) -> IdentifierLocIndex { + // The loc index is purely position-driven; scope tracking is not required. + let _ = scope_info; + + let mut visitor = IdentifierLocVisitor { + line_offsets, + index: FxHashMap::default(), + current_opening_element_loc: None, + type_depth: 0, + }; + + match func { + FunctionNode::Function(f) => { + // The function's own name is a declaration name. + if let Some(id) = &f.id { + visitor.record(id.span, false, true); + } + if let Some(type_parameters) = &f.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } + if let Some(this_param) = &f.this_param { + visitor.visit_ts_this_parameter(this_param); + } + visitor.visit_formal_parameters(&f.params); + if let Some(return_type) = &f.return_type { + visitor.visit_ts_type_annotation(return_type); + } + if let Some(body) = &f.body { + visitor.visit_function_body(body); + } + } + FunctionNode::Arrow(arrow) => { + if let Some(type_parameters) = &arrow.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } + visitor.visit_formal_parameters(&arrow.params); + if let Some(return_type) = &arrow.return_type { + visitor.visit_ts_type_annotation(return_type); + } + if arrow.expression { + if let Some(oxc::Statement::ExpressionStatement(es)) = arrow.body.statements.first() + { + visitor.visit_expression(&es.expression); + } else { + visitor.visit_function_body(&arrow.body); + } + } else { + visitor.visit_function_body(&arrow.body); + } + } + } + + visitor.index +} diff --git a/crates/oxc_react_compiler/src/react_compiler_lowering/mod.rs b/crates/oxc_react_compiler/src/react_compiler_lowering/mod.rs new file mode 100644 index 0000000000000..f1dc8ada6bc8d --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_lowering/mod.rs @@ -0,0 +1,55 @@ +pub mod build_hir; +pub mod find_context_identifiers; +pub mod hir_builder; +pub mod identifier_loc_index; +pub mod source_loc; + +use oxc_ast::ast as oxc; + +use crate::react_compiler_hir::BindingKind; + +/// Convert AST binding kind to HIR binding kind. +pub fn convert_binding_kind(kind: &crate::scope::BindingKind) -> BindingKind { + match kind { + crate::scope::BindingKind::Var => BindingKind::Var, + crate::scope::BindingKind::Let => BindingKind::Let, + crate::scope::BindingKind::Const => BindingKind::Const, + crate::scope::BindingKind::Param => BindingKind::Param, + crate::scope::BindingKind::Module => BindingKind::Module, + crate::scope::BindingKind::Hoisted => BindingKind::Hoisted, + crate::scope::BindingKind::Local => BindingKind::Local, + crate::scope::BindingKind::Unknown => BindingKind::Unknown, + } +} + +/// Represents a reference to a function AST node for lowering. +/// Analogous to TS's `NodePath` / `BabelFn`. +/// +/// oxc collapses Babel's `FunctionDeclaration`/`FunctionExpression` into one +/// [`oxc::Function`] (discriminated by `r#type`); arrows are separate. +#[derive(Clone, Copy)] +pub enum FunctionNode<'a> { + Function(&'a oxc::Function<'a>), + Arrow(&'a oxc::ArrowFunctionExpression<'a>), +} + +impl<'a> FunctionNode<'a> { + /// The node_id of the function node, equal to its `span.start`. + pub fn node_id(&self) -> Option { + Some(match self { + FunctionNode::Function(f) => f.span.start, + FunctionNode::Arrow(a) => a.span.start, + }) + } +} + +// The main lower() function - delegates to build_hir +pub use build_hir::lower; +// Re-export post-build helper functions used by optimization passes +pub use crate::react_compiler_hir::visitors::each_terminal_successor; +pub use crate::react_compiler_hir::visitors::terminal_fallthrough; +pub use hir_builder::{ + create_temporary_place, get_reverse_postordered_blocks, mark_instruction_ids, + mark_predecessors, remove_dead_do_while_statements, remove_unnecessary_try_catch, + remove_unreachable_for_updates, +}; diff --git a/crates/oxc_react_compiler/src/react_compiler_lowering/source_loc.rs b/crates/oxc_react_compiler/src/react_compiler_lowering/source_loc.rs new file mode 100644 index 0000000000000..31cf7fe38ccaf --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_lowering/source_loc.rs @@ -0,0 +1,49 @@ +//! Source-location index for the oxc-AST front-end. +//! +//! Replaces the `loc` / `node_id` synthesis that `convert_ast` performed while +//! building the Babel-shaped AST. oxc nodes carry byte-offset [`Span`]s; the HIR +//! wants [`SourceLocation`] with `line` / `column` / `index`. This computes +//! line/column from a one-time line-offset table, byte-for-byte identical to the +//! table `convert_ast::ConvertCtx` built, so HIR locations are unchanged. +//! +//! Invariant carried over from the Babel bridge: `node_id == span.start`, and +//! `column` is a **byte** offset from the line start (not UTF-16), matching what +//! the previous pipeline produced. + +use oxc_span::Span; + +use crate::react_compiler_hir::Position; +use crate::react_compiler_hir::SourceLocation; + +/// One-time index of line-start byte offsets, for `Span` → [`SourceLocation`]. +pub struct LineOffsets { + /// Byte offset of the start of each line. `line_offsets[0] == 0`. + line_offsets: Vec, +} + +impl LineOffsets { + pub fn new(source_text: &str) -> Self { + let mut line_offsets = vec![0]; + for (i, ch) in source_text.char_indices() { + if ch == '\n' { + line_offsets.push((i + 1) as u32); + } + } + Self { line_offsets } + } + + /// Byte offset → 1-based line, byte-based column, byte index. + pub fn position(&self, offset: u32) -> Position { + let line_idx = match self.line_offsets.binary_search(&offset) { + Ok(idx) => idx, + Err(idx) => idx.saturating_sub(1), + }; + let line_start = self.line_offsets[line_idx]; + Position { line: (line_idx + 1) as u32, column: offset - line_start, index: Some(offset) } + } + + /// A [`Span`]'s byte range → an HIR [`SourceLocation`]. + pub fn source_location(&self, span: Span) -> SourceLocation { + SourceLocation { start: self.position(span.start), end: self.position(span.end) } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/constant_propagation.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/constant_propagation.rs new file mode 100644 index 0000000000000..d864a7d2288ea --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/constant_propagation.rs @@ -0,0 +1,1008 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Constant propagation/folding pass. +//! +//! Applies Sparse Conditional Constant Propagation to the given function. +//! We use abstract interpretation to record known constant values for identifiers, +//! with lack of a value indicating that the identifier does not have a known +//! constant value. +//! +//! Instructions which can be compile-time evaluated *and* whose operands are known +//! constants are replaced with the resulting constant value. +//! +//! This pass also exploits SSA form, tracking constant values of local variables. +//! For example, in `let x = 4; let y = x + 1` we know that `x = 4` in the binary +//! expression and can replace it with `Constant 5`. +//! +//! This pass also visits conditionals (currently only IfTerminal) and can prune +//! unreachable branches when the condition is a known truthy/falsey constant. +//! The pass uses fixpoint iteration, looping until no additional updates can be +//! performed. +//! +//! Analogous to TS `Optimization/ConstantPropagation.ts`. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::JsString; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + BinaryOperator, BlockKind, FloatValue, FunctionId, GotoVariant, HirFunction, IdentifierId, + InstructionValue, NonLocalBinding, Phi, Place, PrimitiveValue, PropertyLiteral, SourceLocation, + Terminal, UnaryOperator, UpdateOperator, format_js_number, +}; +use crate::react_compiler_lowering::{ + get_reverse_postordered_blocks, mark_instruction_ids, mark_predecessors, + remove_dead_do_while_statements, remove_unnecessary_try_catch, remove_unreachable_for_updates, +}; +use crate::react_compiler_ssa::enter_ssa::placeholder_function; + +use crate::react_compiler_optimization::merge_consecutive_blocks::merge_consecutive_blocks; + +// ============================================================================= +// Constant type — mirrors TS `type Constant = Primitive | LoadGlobal` +// The loc is preserved so that when we replace an instruction value with the +// constant, we use the loc from the original definition site (matching TS). +// ============================================================================= + +#[derive(Debug, Clone)] +enum Constant { + Primitive { value: PrimitiveValue, loc: Option }, + LoadGlobal { binding: NonLocalBinding, loc: Option }, +} + +impl Constant { + fn into_instruction_value<'a>(self) -> InstructionValue<'a> { + match self { + Constant::Primitive { value, loc } => InstructionValue::Primitive { value, loc }, + Constant::LoadGlobal { binding, loc } => InstructionValue::LoadGlobal { binding, loc }, + } + } +} + +/// Map of known constant values. Uses FxHashMap (not FxIndexMap) since iteration +/// order does not affect correctness — this map is only used for lookups. +type Constants = FxHashMap; + +// ============================================================================= +// Public entry point +// ============================================================================= + +pub fn constant_propagation<'a>(func: &mut HirFunction<'a>, env: &mut Environment<'a>) { + let mut constants: Constants = FxHashMap::default(); + constant_propagation_impl(func, env, &mut constants); +} + +fn constant_propagation_impl<'a>( + func: &mut HirFunction<'a>, + env: &mut Environment<'a>, + constants: &mut Constants, +) { + loop { + let have_terminals_changed = apply_constant_propagation(func, env, constants); + if !have_terminals_changed { + break; + } + /* + * If terminals have changed then blocks may have become newly unreachable. + * Re-run minification of the graph (incl reordering instruction ids) + */ + func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions); + remove_unreachable_for_updates(&mut func.body); + remove_dead_do_while_statements(&mut func.body); + remove_unnecessary_try_catch(&mut func.body); + mark_instruction_ids(&mut func.body, &mut func.instructions); + mark_predecessors(&mut func.body); + + // Now that predecessors are updated, prune phi operands that can never be reached + for (_block_id, block) in func.body.blocks.iter_mut() { + for phi in &mut block.phis { + phi.operands.retain(|pred, _operand| block.preds.contains(pred)); + } + } + + /* + * By removing some phi operands, there may be phis that were not previously + * redundant but now are + */ + crate::react_compiler_ssa::eliminate_redundant_phi(func, env); + + /* + * Finally, merge together any blocks that are now guaranteed to execute + * consecutively + */ + merge_consecutive_blocks(func, &mut env.functions); + + // TODO: port assertConsistentIdentifiers(fn) and assertTerminalSuccessorsExist(fn) + // from TS HIR validation. These are debug assertions that verify structural + // invariants after the CFG cleanup helpers run. + } +} + +fn apply_constant_propagation<'a>( + func: &mut HirFunction<'a>, + env: &mut Environment<'a>, + constants: &mut Constants, +) -> bool { + let mut has_changes = false; + + let block_ids: Vec<_> = func.body.blocks.keys().copied().collect(); + for block_id in block_ids { + let block = &func.body.blocks[&block_id]; + + // Initialize phi values if all operands have the same known constant value + let phi_updates: Vec<(IdentifierId, Constant)> = block + .phis + .iter() + .filter_map(|phi| { + let value = evaluate_phi(phi, constants)?; + Some((phi.place.identifier, value)) + }) + .collect(); + for (id, value) in phi_updates { + constants.insert(id, value); + } + + let block = &func.body.blocks[&block_id]; + let instr_ids = block.instructions.clone(); + let block_kind = block.kind; + let instr_count = instr_ids.len(); + + for (i, instr_id) in instr_ids.iter().enumerate() { + if block_kind == BlockKind::Sequence && i == instr_count - 1 { + /* + * evaluating the last value of a value block can break order of evaluation, + * skip these instructions + */ + continue; + } + let result = evaluate_instruction(constants, func, env, *instr_id); + if let Some(value) = result { + let lvalue_id = func.instructions[instr_id.0 as usize].lvalue.identifier; + constants.insert(lvalue_id, value); + } + } + + let block = &func.body.blocks[&block_id]; + match &block.terminal { + Terminal::If { test, consequent, alternate, id, loc, .. } => { + let test_value = read(constants, test); + if let Some(Constant::Primitive { value: ref prim, .. }) = test_value { + has_changes = true; + let target_block_id = if is_truthy(prim) { *consequent } else { *alternate }; + let terminal = Terminal::Goto { + variant: GotoVariant::Break, + block: target_block_id, + id: *id, + loc: *loc, + }; + func.body.blocks.get_mut(&block_id).unwrap().terminal = terminal; + } + } + Terminal::Unsupported { .. } + | Terminal::Unreachable { .. } + | Terminal::Throw { .. } + | Terminal::Return { .. } + | Terminal::Goto { .. } + | Terminal::Branch { .. } + | Terminal::Switch { .. } + | Terminal::DoWhile { .. } + | Terminal::While { .. } + | Terminal::For { .. } + | Terminal::ForOf { .. } + | Terminal::ForIn { .. } + | Terminal::Logical { .. } + | Terminal::Ternary { .. } + | Terminal::Optional { .. } + | Terminal::Label { .. } + | Terminal::Sequence { .. } + | Terminal::MaybeThrow { .. } + | Terminal::Try { .. } + | Terminal::Scope { .. } + | Terminal::PrunedScope { .. } => { + // no-op + } + } + } + + has_changes +} + +// ============================================================================= +// Phi evaluation +// ============================================================================= + +fn evaluate_phi(phi: &Phi, constants: &Constants) -> Option { + let mut value: Option = None; + for (_pred, operand) in &phi.operands { + let operand_value = constants.get(&operand.identifier)?; + + match &value { + None => { + // first iteration of the loop + value = Some(operand_value.clone()); + continue; + } + Some(current) => match (current, operand_value) { + (Constant::Primitive { value: a, .. }, Constant::Primitive { value: b, .. }) => { + // Use JS strict equality semantics: NaN !== NaN + if !js_strict_equal(a, b) { + return None; + } + } + ( + Constant::LoadGlobal { binding: a, .. }, + Constant::LoadGlobal { binding: b, .. }, + ) => { + // different global values, can't constant propagate + if a.name() != b.name() { + return None; + } + } + // found different kinds of constants, can't constant propagate + (Constant::Primitive { .. }, Constant::LoadGlobal { .. }) + | (Constant::LoadGlobal { .. }, Constant::Primitive { .. }) => { + return None; + } + }, + } + } + value +} + +// ============================================================================= +// Instruction evaluation +// ============================================================================= + +fn evaluate_instruction<'a>( + constants: &mut Constants, + func: &mut HirFunction<'a>, + env: &mut Environment<'a>, + instr_id: crate::react_compiler_hir::InstructionId, +) -> Option { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::Primitive { value, loc } => { + Some(Constant::Primitive { value: value.clone(), loc: *loc }) + } + InstructionValue::LoadGlobal { binding, loc } => { + Some(Constant::LoadGlobal { binding: binding.clone(), loc: *loc }) + } + InstructionValue::ComputedLoad { object, property, loc } => { + let prop_value = read(constants, property); + if let Some(Constant::Primitive { value: ref prim, .. }) = prop_value { + match prim { + PrimitiveValue::String(s) if s.as_str().is_some_and(is_valid_identifier) => { + let object = object.clone(); + let loc = *loc; + let new_property = + PropertyLiteral::String(s.as_str().expect("guarded utf8").to_string()); + func.instructions[instr_id.0 as usize].value = + InstructionValue::PropertyLoad { object, property: new_property, loc }; + } + PrimitiveValue::Number(n) => { + let object = object.clone(); + let loc = *loc; + let new_property = PropertyLiteral::Number(*n); + func.instructions[instr_id.0 as usize].value = + InstructionValue::PropertyLoad { object, property: new_property, loc }; + } + PrimitiveValue::Null + | PrimitiveValue::Undefined + | PrimitiveValue::Boolean(_) + | PrimitiveValue::String(_) => {} + } + } + None + } + InstructionValue::ComputedStore { object, property, value, loc } => { + let prop_value = read(constants, property); + if let Some(Constant::Primitive { value: ref prim, .. }) = prop_value { + match prim { + PrimitiveValue::String(s) if s.as_str().is_some_and(is_valid_identifier) => { + let object = object.clone(); + let store_value = value.clone(); + let loc = *loc; + let new_property = + PropertyLiteral::String(s.as_str().expect("guarded utf8").to_string()); + func.instructions[instr_id.0 as usize].value = + InstructionValue::PropertyStore { + object, + property: new_property, + value: store_value, + loc, + }; + } + PrimitiveValue::Number(n) => { + let object = object.clone(); + let store_value = value.clone(); + let loc = *loc; + let new_property = PropertyLiteral::Number(*n); + func.instructions[instr_id.0 as usize].value = + InstructionValue::PropertyStore { + object, + property: new_property, + value: store_value, + loc, + }; + } + PrimitiveValue::Null + | PrimitiveValue::Undefined + | PrimitiveValue::Boolean(_) + | PrimitiveValue::String(_) => {} + } + } + None + } + InstructionValue::PostfixUpdate { lvalue, operation, value, loc } => { + let previous = read(constants, value); + if let Some(Constant::Primitive { value: PrimitiveValue::Number(n), loc: prev_loc }) = + previous + { + let prev_val = n.value(); + let next_val = match operation { + UpdateOperator::Increment => prev_val + 1.0, + UpdateOperator::Decrement => prev_val - 1.0, + }; + // Store the updated value for the lvalue + let lvalue_id = lvalue.identifier; + constants.insert( + lvalue_id, + Constant::Primitive { + value: PrimitiveValue::Number(FloatValue::new(next_val)), + loc: *loc, + }, + ); + // But return the value prior to the update (preserving its original loc) + return Some(Constant::Primitive { + value: PrimitiveValue::Number(n), + loc: prev_loc, + }); + } + None + } + InstructionValue::PrefixUpdate { lvalue, operation, value, loc } => { + let previous = read(constants, value); + if let Some(Constant::Primitive { value: PrimitiveValue::Number(n), .. }) = previous { + let prev_val = n.value(); + let next_val = match operation { + UpdateOperator::Increment => prev_val + 1.0, + UpdateOperator::Decrement => prev_val - 1.0, + }; + let result = Constant::Primitive { + value: PrimitiveValue::Number(FloatValue::new(next_val)), + loc: *loc, + }; + // Store and return the updated value + let lvalue_id = lvalue.identifier; + constants.insert(lvalue_id, result.clone()); + return Some(result); + } + None + } + InstructionValue::UnaryExpression { operator, value, loc } => match operator { + UnaryOperator::Not => { + let operand = read(constants, value); + if let Some(Constant::Primitive { value: ref prim, .. }) = operand { + let negated = !is_truthy(prim); + let loc = *loc; + let result = + Constant::Primitive { value: PrimitiveValue::Boolean(negated), loc }; + func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive { + value: PrimitiveValue::Boolean(negated), + loc, + }; + return Some(result); + } + None + } + UnaryOperator::Minus => { + let operand = read(constants, value); + if let Some(Constant::Primitive { value: PrimitiveValue::Number(n), .. }) = operand + { + let negated = n.value() * -1.0; + let loc = *loc; + let result = Constant::Primitive { + value: PrimitiveValue::Number(FloatValue::new(negated)), + loc, + }; + func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive { + value: PrimitiveValue::Number(FloatValue::new(negated)), + loc, + }; + return Some(result); + } + None + } + UnaryOperator::Plus + | UnaryOperator::BitwiseNot + | UnaryOperator::TypeOf + | UnaryOperator::Void => None, + }, + InstructionValue::BinaryExpression { operator, left, right, loc } => { + let lhs_value = read(constants, left); + let rhs_value = read(constants, right); + if let ( + Some(Constant::Primitive { value: lhs, .. }), + Some(Constant::Primitive { value: rhs, .. }), + ) = (&lhs_value, &rhs_value) + { + let result = evaluate_binary_op(*operator, lhs, rhs); + if let Some(ref prim) = result { + let loc = *loc; + func.instructions[instr_id.0 as usize].value = + InstructionValue::Primitive { value: prim.clone(), loc }; + return Some(Constant::Primitive { value: prim.clone(), loc }); + } + } + None + } + InstructionValue::PropertyLoad { object, property, loc } => { + let object_value = read(constants, object); + if let Some(Constant::Primitive { value: PrimitiveValue::String(ref s), .. }) = + object_value + { + if let PropertyLiteral::String(prop_name) = property { + if prop_name == "length" { + // Use UTF-16 code unit count to match JS .length semantics + let len = s.len_utf16() as f64; + let loc = *loc; + let result = Constant::Primitive { + value: PrimitiveValue::Number(FloatValue::new(len)), + loc, + }; + func.instructions[instr_id.0 as usize].value = + InstructionValue::Primitive { + value: PrimitiveValue::Number(FloatValue::new(len)), + loc, + }; + return Some(result); + } + } + } + None + } + InstructionValue::TemplateLiteral { subexprs, quasis, loc } => { + if subexprs.is_empty() { + // No subexpressions: join all cooked quasis + let mut result_string = String::new(); + for q in quasis { + match &q.cooked { + Some(cooked) => result_string.push_str(cooked), + None => return None, + } + } + let loc = *loc; + let result = Constant::Primitive { + value: PrimitiveValue::String(JsString::from_marker_string(&result_string)), + loc, + }; + func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive { + value: PrimitiveValue::String(JsString::from_marker_string(&result_string)), + loc, + }; + return Some(result); + } + + if subexprs.len() != quasis.len() - 1 { + return None; + } + + if quasis.iter().any(|q| q.cooked.is_none()) { + return None; + } + + let mut quasi_index = 0usize; + let mut result_string = quasis[quasi_index].cooked.as_ref().unwrap().clone(); + quasi_index += 1; + + for sub_expr in subexprs { + let sub_expr_value = read(constants, sub_expr); + let sub_prim = match sub_expr_value { + Some(Constant::Primitive { ref value, .. }) => value, + _ => return None, + }; + + let expression_str = match sub_prim { + PrimitiveValue::Null => "null".to_string(), + PrimitiveValue::Boolean(b) => b.to_string(), + PrimitiveValue::Number(n) => format_js_number(n.value()), + PrimitiveValue::String(s) => s.to_marker_string(), + // TS rejects undefined subexpression values + PrimitiveValue::Undefined => return None, + }; + + let suffix = match &quasis[quasi_index].cooked { + Some(s) => s.clone(), + None => return None, + }; + quasi_index += 1; + + result_string.push_str(&expression_str); + result_string.push_str(&suffix); + } + + let loc = *loc; + let result = Constant::Primitive { + value: PrimitiveValue::String(JsString::from_marker_string(&result_string)), + loc, + }; + func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive { + value: PrimitiveValue::String(JsString::from_marker_string(&result_string)), + loc, + }; + Some(result) + } + InstructionValue::LoadLocal { place, .. } => { + let place_value = read(constants, place); + if let Some(ref constant) = place_value { + // Replace the LoadLocal with the constant value (including the constant's original loc) + func.instructions[instr_id.0 as usize].value = + constant.clone().into_instruction_value(); + } + place_value + } + InstructionValue::StoreLocal { lvalue, value, .. } => { + let place_value = read(constants, value); + if let Some(ref constant) = place_value { + let lvalue_id = lvalue.place.identifier; + constants.insert(lvalue_id, constant.clone()); + } + place_value + } + InstructionValue::FunctionExpression { lowered_func, .. } => { + let func_id = lowered_func.func; + process_inner_function(func_id, env, constants); + None + } + InstructionValue::ObjectMethod { lowered_func, .. } => { + let func_id = lowered_func.func; + process_inner_function(func_id, env, constants); + None + } + InstructionValue::StartMemoize { deps, .. } => { + if let Some(deps) = deps { + // Two-phase: collect which deps are constant, then mutate + let const_dep_indices: Vec = deps + .iter() + .enumerate() + .filter_map(|(i, dep)| { + if let crate::react_compiler_hir::ManualMemoDependencyRoot::NamedLocal { + value, + .. + } = &dep.root + { + let pv = read(constants, value); + if matches!(pv, Some(Constant::Primitive { .. })) { + return Some(i); + } + } + None + }) + .collect(); + for idx in const_dep_indices { + if let InstructionValue::StartMemoize { deps: Some(ref mut deps), .. } = + func.instructions[instr_id.0 as usize].value + { + if let crate::react_compiler_hir::ManualMemoDependencyRoot::NamedLocal { + constant, + .. + } = &mut deps[idx].root + { + *constant = true; + } + } + } + } + None + } + // All other instruction kinds: no constant folding + InstructionValue::LoadContext { .. } + | InstructionValue::DeclareLocal { .. } + | InstructionValue::DeclareContext { .. } + | InstructionValue::StoreContext { .. } + | InstructionValue::Destructure { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::NewExpression { .. } + | InstructionValue::CallExpression { .. } + | InstructionValue::MethodCall { .. } + | InstructionValue::TypeCastExpression { .. } + | InstructionValue::JsxExpression { .. } + | InstructionValue::ObjectExpression { .. } + | InstructionValue::ArrayExpression { .. } + | InstructionValue::JsxFragment { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::PropertyStore { .. } + | InstructionValue::PropertyDelete { .. } + | InstructionValue::ComputedDelete { .. } + | InstructionValue::StoreGlobal { .. } + | InstructionValue::TaggedTemplateExpression { .. } + | InstructionValue::Await { .. } + | InstructionValue::GetIterator { .. } + | InstructionValue::IteratorNext { .. } + | InstructionValue::NextPropertyOf { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::FinishMemoize { .. } => None, + } +} + +// ============================================================================= +// Inner function processing +// ============================================================================= + +fn process_inner_function(func_id: FunctionId, env: &mut Environment, constants: &mut Constants) { + let mut inner = + std::mem::replace(&mut env.functions[func_id.0 as usize], placeholder_function()); + constant_propagation_impl(&mut inner, env, constants); + env.functions[func_id.0 as usize] = inner; +} + +// ============================================================================= +// Helper: read constant for a place +// ============================================================================= + +fn read(constants: &Constants, place: &Place) -> Option { + constants.get(&place.identifier).cloned() +} + +// ============================================================================= +// Helper: is_valid_identifier +// ============================================================================= + +/// Check if a string is a valid JavaScript identifier. +/// Supports Unicode identifier characters per ECMAScript spec (ID_Start / ID_Continue). +/// Rejects JS reserved words (matching Babel's `isValidIdentifier` default behavior). +fn is_valid_identifier(s: &str) -> bool { + if s.is_empty() { + return false; + } + let mut chars = s.chars(); + match chars.next() { + Some(c) if is_id_start(c) => {} + _ => return false, + } + if !chars.all(is_id_continue) { + return false; + } + !is_reserved_word(s) +} + +/// JS reserved words that cannot be used as identifiers. +/// Includes keywords, future reserved words, and strict mode reserved words. +fn is_reserved_word(s: &str) -> bool { + matches!( + s, + "break" + | "case" + | "catch" + | "continue" + | "debugger" + | "default" + | "do" + | "else" + | "finally" + | "for" + | "function" + | "if" + | "in" + | "instanceof" + | "new" + | "return" + | "switch" + | "this" + | "throw" + | "try" + | "typeof" + | "var" + | "void" + | "while" + | "with" + | "class" + | "const" + | "enum" + | "export" + | "extends" + | "import" + | "super" + | "implements" + | "interface" + | "let" + | "package" + | "private" + | "protected" + | "public" + | "static" + | "yield" + | "await" + | "delete" + | "null" + | "true" + | "false" + ) +} + +/// Check if a character is valid as the start of a JS identifier (ID_Start + _ + $). +fn is_id_start(c: char) -> bool { + c == '_' || c == '$' || c.is_alphabetic() +} + +/// Check if a character is valid as a continuation of a JS identifier (ID_Continue + $ + \u200C + \u200D). +fn is_id_continue(c: char) -> bool { + c == '$' + || c == '_' + || c.is_alphanumeric() + || c == '\u{200C}' // ZWNJ + || c == '\u{200D}' // ZWJ +} + +// ============================================================================= +// Helper: is_truthy for PrimitiveValue +// ============================================================================= + +fn is_truthy(value: &PrimitiveValue) -> bool { + match value { + PrimitiveValue::Null => false, + PrimitiveValue::Undefined => false, + PrimitiveValue::Boolean(b) => *b, + PrimitiveValue::Number(n) => { + let v = n.value(); + v != 0.0 && !v.is_nan() + } + PrimitiveValue::String(s) => s.len_utf16() != 0, + } +} + +// ============================================================================= +// Binary operation evaluation +// ============================================================================= + +fn evaluate_binary_op( + operator: BinaryOperator, + lhs: &PrimitiveValue, + rhs: &PrimitiveValue, +) -> Option { + match operator { + BinaryOperator::Add => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Number(FloatValue::new(l.value() + r.value()))) + } + (PrimitiveValue::String(l), PrimitiveValue::String(r)) => { + // Concatenate as code units: JS `+` can pair up surrogate + // halves split across the operands. + let mut units = l.code_units(); + units.extend(r.code_units()); + Some(PrimitiveValue::String( + crate::react_compiler_diagnostics::JsString::from_code_units(units), + )) + } + _ => None, + }, + BinaryOperator::Subtract => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Number(FloatValue::new(l.value() - r.value()))) + } + _ => None, + }, + BinaryOperator::Multiply => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Number(FloatValue::new(l.value() * r.value()))) + } + _ => None, + }, + BinaryOperator::Divide => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Number(FloatValue::new(l.value() / r.value()))) + } + _ => None, + }, + BinaryOperator::Modulo => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Number(FloatValue::new(l.value() % r.value()))) + } + _ => None, + }, + BinaryOperator::Exponent => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Number(FloatValue::new(l.value().powf(r.value())))) + } + _ => None, + }, + BinaryOperator::BitwiseOr => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + let result = js_to_int32(l.value()) | js_to_int32(r.value()); + Some(PrimitiveValue::Number(FloatValue::new(result as f64))) + } + _ => None, + }, + BinaryOperator::BitwiseAnd => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + let result = js_to_int32(l.value()) & js_to_int32(r.value()); + Some(PrimitiveValue::Number(FloatValue::new(result as f64))) + } + _ => None, + }, + BinaryOperator::BitwiseXor => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + let result = js_to_int32(l.value()) ^ js_to_int32(r.value()); + Some(PrimitiveValue::Number(FloatValue::new(result as f64))) + } + _ => None, + }, + BinaryOperator::ShiftLeft => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + let result = js_to_int32(l.value()) << (js_to_uint32(r.value()) & 0x1f); + Some(PrimitiveValue::Number(FloatValue::new(result as f64))) + } + _ => None, + }, + BinaryOperator::ShiftRight => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + let result = js_to_int32(l.value()) >> (js_to_uint32(r.value()) & 0x1f); + Some(PrimitiveValue::Number(FloatValue::new(result as f64))) + } + _ => None, + }, + BinaryOperator::UnsignedShiftRight => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + let result = js_to_uint32(l.value()) >> (js_to_uint32(r.value()) & 0x1f); + Some(PrimitiveValue::Number(FloatValue::new(result as f64))) + } + _ => None, + }, + BinaryOperator::LessThan => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Boolean(l.value() < r.value())) + } + _ => None, + }, + BinaryOperator::LessEqual => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Boolean(l.value() <= r.value())) + } + _ => None, + }, + BinaryOperator::GreaterThan => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Boolean(l.value() > r.value())) + } + _ => None, + }, + BinaryOperator::GreaterEqual => match (lhs, rhs) { + (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => { + Some(PrimitiveValue::Boolean(l.value() >= r.value())) + } + _ => None, + }, + BinaryOperator::StrictEqual => Some(PrimitiveValue::Boolean(js_strict_equal(lhs, rhs))), + BinaryOperator::StrictNotEqual => Some(PrimitiveValue::Boolean(!js_strict_equal(lhs, rhs))), + BinaryOperator::Equal => Some(PrimitiveValue::Boolean(js_abstract_equal(lhs, rhs))), + BinaryOperator::NotEqual => Some(PrimitiveValue::Boolean(!js_abstract_equal(lhs, rhs))), + BinaryOperator::In | BinaryOperator::InstanceOf => None, + } +} + +// ============================================================================= +// JavaScript equality semantics +// ============================================================================= + +fn js_strict_equal(lhs: &PrimitiveValue, rhs: &PrimitiveValue) -> bool { + match (lhs, rhs) { + (PrimitiveValue::Null, PrimitiveValue::Null) => true, + (PrimitiveValue::Undefined, PrimitiveValue::Undefined) => true, + (PrimitiveValue::Boolean(a), PrimitiveValue::Boolean(b)) => a == b, + (PrimitiveValue::Number(a), PrimitiveValue::Number(b)) => { + let av = a.value(); + let bv = b.value(); + // NaN !== NaN in JS + if av.is_nan() || bv.is_nan() { + return false; + } + av == bv + } + (PrimitiveValue::String(a), PrimitiveValue::String(b)) => a == b, + // Different types => false + _ => false, + } +} + +/// Convert a string to a number using JS `ToNumber` semantics. +/// In JS: `""` → 0, `" "` → 0, `" 42 "` → 42, `"0x1A"` → 26, `"Infinity"` → Infinity. +fn js_to_number(s: &str) -> f64 { + let trimmed = s.trim(); + if trimmed.is_empty() { + return 0.0; + } + if trimmed == "Infinity" || trimmed == "+Infinity" { + return f64::INFINITY; + } + if trimmed == "-Infinity" { + return f64::NEG_INFINITY; + } + // Handle hex literals (0x/0X) + if trimmed.starts_with("0x") || trimmed.starts_with("0X") { + return match u64::from_str_radix(&trimmed[2..], 16) { + Ok(v) => v as f64, + Err(_) => f64::NAN, + }; + } + // Handle octal literals (0o/0O) + if trimmed.starts_with("0o") || trimmed.starts_with("0O") { + return match u64::from_str_radix(&trimmed[2..], 8) { + Ok(v) => v as f64, + Err(_) => f64::NAN, + }; + } + // Handle binary literals (0b/0B) + if trimmed.starts_with("0b") || trimmed.starts_with("0B") { + return match u64::from_str_radix(&trimmed[2..], 2) { + Ok(v) => v as f64, + Err(_) => f64::NAN, + }; + } + trimmed.parse::().unwrap_or(f64::NAN) +} + +fn js_abstract_equal(lhs: &PrimitiveValue, rhs: &PrimitiveValue) -> bool { + match (lhs, rhs) { + (PrimitiveValue::Null, PrimitiveValue::Null) => true, + (PrimitiveValue::Undefined, PrimitiveValue::Undefined) => true, + (PrimitiveValue::Null, PrimitiveValue::Undefined) + | (PrimitiveValue::Undefined, PrimitiveValue::Null) => true, + (PrimitiveValue::Boolean(a), PrimitiveValue::Boolean(b)) => a == b, + (PrimitiveValue::Number(a), PrimitiveValue::Number(b)) => { + let av = a.value(); + let bv = b.value(); + if av.is_nan() || bv.is_nan() { + return false; + } + av == bv + } + (PrimitiveValue::String(a), PrimitiveValue::String(b)) => a == b, + // Cross-type coercions for primitives + (PrimitiveValue::Number(n), PrimitiveValue::String(s)) + | (PrimitiveValue::String(s), PrimitiveValue::Number(n)) => { + // String is coerced to number using JS ToNumber semantics. + // Ill-formed strings coerce to NaN, like any non-numeric text. + let sv = match s.as_str() { + Some(utf8) => js_to_number(utf8), + None => f64::NAN, + }; + let nv = n.value(); + if nv.is_nan() || sv.is_nan() { false } else { nv == sv } + } + (PrimitiveValue::Boolean(b), other) => { + let num = if *b { 1.0 } else { 0.0 }; + js_abstract_equal(&PrimitiveValue::Number(FloatValue::new(num)), other) + } + (other, PrimitiveValue::Boolean(b)) => { + let num = if *b { 1.0 } else { 0.0 }; + js_abstract_equal(other, &PrimitiveValue::Number(FloatValue::new(num))) + } + // null/undefined vs number/string => false + _ => false, + } +} + +// ============================================================================= +// JavaScript Number.toString() approximation +// ============================================================================= + +/// ECMAScript ToInt32: convert f64 to i32 with modular (wrapping) semantics. +fn js_to_int32(n: f64) -> i32 { + if n.is_nan() || n.is_infinite() || n == 0.0 { + return 0; + } + // Truncate, then wrap to 32 bits + let int64 = (n.trunc() as i64) & 0xFFFFFFFF; + // Reinterpret as signed i32 + if int64 >= 0x80000000 { (int64 as u32) as i32 } else { int64 as i32 } +} + +/// ECMAScript ToUint32: convert f64 to u32 with modular (wrapping) semantics. +fn js_to_uint32(n: f64) -> u32 { + js_to_int32(n) as u32 +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/dead_code_elimination.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/dead_code_elimination.rs new file mode 100644 index 0000000000000..1a739a69dc43b --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/dead_code_elimination.rs @@ -0,0 +1,409 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Dead code elimination pass. +//! +//! Eliminates instructions whose values are unused, reducing generated code size. +//! Performs mark-and-sweep analysis to identify and remove dead code while +//! preserving side effects and program semantics. +//! +//! Ported from TypeScript `src/Optimization/DeadCodeElimination.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::environment::{Environment, OutputMode}; +use crate::react_compiler_hir::object_shape::HookKind; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::{ + ArrayPatternElement, BlockId, BlockKind, HirFunction, IdentifierId, InstructionKind, + InstructionValue, ObjectPropertyOrSpread, Pattern, +}; + +/// Implements dead-code elimination, eliminating instructions whose values are unused. +/// +/// Note that unreachable blocks are already pruned during HIR construction. +/// +/// Corresponds to TS `deadCodeElimination(fn: HIRFunction): void`. +pub fn dead_code_elimination(func: &mut HirFunction, env: &Environment) { + // Phase 1: Find/mark all referenced identifiers + let state = find_referenced_identifiers(func, env); + + // Phase 2: Prune / sweep unreferenced identifiers and instructions + // Collect instructions to rewrite (two-phase: collect then apply to avoid borrow conflicts) + let mut instructions_to_rewrite: Vec = Vec::new(); + + for (_block_id, block) in &mut func.body.blocks { + // Remove unused phi nodes + block.phis.retain(|phi| is_id_or_name_used(&state, &env.identifiers, phi.place.identifier)); + + // Remove instructions with unused lvalues + block.instructions.retain(|instr_id| { + let instr = &func.instructions[instr_id.0 as usize]; + is_id_or_name_used(&state, &env.identifiers, instr.lvalue.identifier) + }); + + // Collect instructions that need rewriting (not the block value) + let retained_count = block.instructions.len(); + for i in 0..retained_count { + let is_block_value = block.kind != BlockKind::Block && i == retained_count - 1; + if !is_block_value { + instructions_to_rewrite.push(block.instructions[i]); + } + } + } + + // Apply rewrites + for instr_id in instructions_to_rewrite { + rewrite_instruction(func, instr_id, &state, env); + } + + // Remove unused context variables + func.context.retain(|ctx_var| is_id_or_name_used(&state, &env.identifiers, ctx_var.identifier)); +} + +/// State for tracking referenced identifiers during mark phase. +struct State { + /// SSA-specific usages (by IdentifierId) + identifiers: FxHashSet, + /// Named variable usages (any version) + named: FxHashSet, +} + +impl State { + fn new() -> Self { + State { identifiers: FxHashSet::default(), named: FxHashSet::default() } + } + + fn count(&self) -> usize { + self.identifiers.len() + } +} + +/// Mark an identifier as being referenced (not dead code). +fn reference( + state: &mut State, + identifiers: &[crate::react_compiler_hir::Identifier], + identifier_id: IdentifierId, +) { + state.identifiers.insert(identifier_id); + let ident = &identifiers[identifier_id.0 as usize]; + if let Some(ref name) = ident.name { + state.named.insert(name.value().to_string()); + } +} + +/// Check if any version of the given identifier is used somewhere. +/// Checks both the specific SSA id and (for named identifiers) any usage of that name. +fn is_id_or_name_used( + state: &State, + identifiers: &[crate::react_compiler_hir::Identifier], + identifier_id: IdentifierId, +) -> bool { + if state.identifiers.contains(&identifier_id) { + return true; + } + let ident = &identifiers[identifier_id.0 as usize]; + if let Some(ref name) = ident.name { state.named.contains(name.value()) } else { false } +} + +/// Check if this specific SSA id is used. +fn is_id_used(state: &State, identifier_id: IdentifierId) -> bool { + state.identifiers.contains(&identifier_id) +} + +/// Phase 1: Find all referenced identifiers via fixed-point iteration. +fn find_referenced_identifiers(func: &HirFunction, env: &Environment) -> State { + let has_loop = has_back_edge(func); + // Collect block ids in reverse order (postorder - successors before predecessors) + let reversed_block_ids: Vec = func.body.blocks.keys().rev().copied().collect(); + + let mut state = State::new(); + let mut size; + + loop { + size = state.count(); + + for &block_id in &reversed_block_ids { + let block = &func.body.blocks[&block_id]; + + // Mark terminal operands + for place in visitors::each_terminal_operand(&block.terminal) { + reference(&mut state, &env.identifiers, place.identifier); + } + + // Process instructions in reverse order + let instr_count = block.instructions.len(); + for i in (0..instr_count).rev() { + let instr_id = block.instructions[i]; + let instr = &func.instructions[instr_id.0 as usize]; + + let is_block_value = block.kind != BlockKind::Block && i == instr_count - 1; + + if is_block_value { + // Last instr of a value block is never eligible for pruning + reference(&mut state, &env.identifiers, instr.lvalue.identifier); + for place in visitors::each_instruction_value_operand(&instr.value, env) { + reference(&mut state, &env.identifiers, place.identifier); + } + } else if is_id_or_name_used(&state, &env.identifiers, instr.lvalue.identifier) + || !pruneable_value(&instr.value, &state, env) + { + reference(&mut state, &env.identifiers, instr.lvalue.identifier); + + if let InstructionValue::StoreLocal { lvalue, value, .. } = &instr.value { + // If this is a Let/Const declaration, mark the initializer as referenced + // only if the SSA'd lval is also referenced + if lvalue.kind == InstructionKind::Reassign + || is_id_used(&state, lvalue.place.identifier) + { + reference(&mut state, &env.identifiers, value.identifier); + } + } else { + for place in visitors::each_instruction_value_operand(&instr.value, env) { + reference(&mut state, &env.identifiers, place.identifier); + } + } + } + } + + // Mark phi operands if phi result is used + for phi in &block.phis { + if is_id_or_name_used(&state, &env.identifiers, phi.place.identifier) { + for (_pred, operand) in &phi.operands { + reference(&mut state, &env.identifiers, operand.identifier); + } + } + } + } + + if !(state.count() > size && has_loop) { + break; + } + } + + state +} + +/// Rewrite a retained instruction (destructuring cleanup, StoreLocal -> DeclareLocal). +fn rewrite_instruction( + func: &mut HirFunction, + instr_id: crate::react_compiler_hir::InstructionId, + state: &State, + env: &Environment, +) { + let instr = &mut func.instructions[instr_id.0 as usize]; + + match &mut instr.value { + InstructionValue::Destructure { lvalue, .. } => { + match &mut lvalue.pattern { + Pattern::Array(arr) => { + // For arrays, replace unused items with holes, truncate trailing holes + let mut last_entry_index = 0; + for i in 0..arr.items.len() { + match &arr.items[i] { + ArrayPatternElement::Place(p) => { + if !is_id_or_name_used(state, &env.identifiers, p.identifier) { + arr.items[i] = ArrayPatternElement::Hole; + } else { + last_entry_index = i; + } + } + ArrayPatternElement::Spread(s) => { + if !is_id_or_name_used(state, &env.identifiers, s.place.identifier) + { + arr.items[i] = ArrayPatternElement::Hole; + } else { + last_entry_index = i; + } + } + ArrayPatternElement::Hole => {} + } + } + arr.items.truncate(last_entry_index + 1); + } + Pattern::Object(obj) => { + // For objects, prune unused properties if rest element is unused or absent + let mut next_properties: Option> = None; + for prop in &obj.properties { + match prop { + ObjectPropertyOrSpread::Property(p) => { + if is_id_or_name_used(state, &env.identifiers, p.place.identifier) { + next_properties.get_or_insert_with(Vec::new).push(prop.clone()); + } + } + ObjectPropertyOrSpread::Spread(s) => { + if is_id_or_name_used(state, &env.identifiers, s.place.identifier) { + // Rest element is used, can't prune anything + next_properties = None; + break; + } + } + } + } + if let Some(props) = next_properties { + obj.properties = props; + } + } + } + } + InstructionValue::StoreLocal { lvalue, type_annotation, loc, .. } => { + if lvalue.kind != InstructionKind::Reassign + && !is_id_used(state, lvalue.place.identifier) + { + // This is a const/let declaration where the variable is accessed later, + // but where the value is always overwritten before being read. + // Rewrite to DeclareLocal so the initializer value can be DCE'd. + let new_lvalue = lvalue.clone(); + let new_type_annotation = type_annotation.clone(); + let new_loc = *loc; + instr.value = InstructionValue::DeclareLocal { + lvalue: new_lvalue, + type_annotation: new_type_annotation, + loc: new_loc, + }; + } + } + _ => {} + } +} + +/// Returns true if it is safe to prune an instruction with the given value. +fn pruneable_value(value: &InstructionValue, state: &State, env: &Environment) -> bool { + match value { + InstructionValue::DeclareLocal { lvalue, .. } => { + // Declarations are pruneable only if the named variable is never read later + !is_id_or_name_used(state, &env.identifiers, lvalue.place.identifier) + } + InstructionValue::StoreLocal { lvalue, .. } => { + if lvalue.kind == InstructionKind::Reassign { + // Reassignments can be pruned if the specific instance being assigned is never read + !is_id_used(state, lvalue.place.identifier) + } else { + // Declarations are pruneable only if the named variable is never read later + !is_id_or_name_used(state, &env.identifiers, lvalue.place.identifier) + } + } + InstructionValue::Destructure { lvalue, .. } => { + let mut is_id_or_name_used_flag = false; + let mut is_id_used_flag = false; + for place in visitors::each_pattern_operand(&lvalue.pattern) { + if is_id_used(state, place.identifier) { + is_id_or_name_used_flag = true; + is_id_used_flag = true; + } else if is_id_or_name_used(state, &env.identifiers, place.identifier) { + is_id_or_name_used_flag = true; + } + } + if lvalue.kind == InstructionKind::Reassign { + !is_id_used_flag + } else { + !is_id_or_name_used_flag + } + } + InstructionValue::PostfixUpdate { lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue, .. } => { + // Updates are pruneable if the specific instance being assigned is never read + !is_id_used(state, lvalue.identifier) + } + InstructionValue::Debugger { .. } => { + // explicitly retain debugger statements + false + } + InstructionValue::PassthroughStatement { .. } => { + // preserved source statements (e.g. inline TS `enum`) have runtime + // semantics — never prune them + false + } + InstructionValue::CallExpression { callee, .. } => { + if env.output_mode == OutputMode::Ssr { + let callee_ty = + &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if let Some(hook_kind) = env.get_hook_kind_for_type(callee_ty).ok().flatten() { + match hook_kind { + HookKind::UseState | HookKind::UseReducer | HookKind::UseRef => { + return true; + } + _ => {} + } + } + } + false + } + InstructionValue::MethodCall { property, .. } => { + if env.output_mode == OutputMode::Ssr { + let callee_ty = + &env.types[env.identifiers[property.identifier.0 as usize].type_.0 as usize]; + if let Some(hook_kind) = env.get_hook_kind_for_type(callee_ty).ok().flatten() { + match hook_kind { + HookKind::UseState | HookKind::UseReducer | HookKind::UseRef => { + return true; + } + _ => {} + } + } + } + false + } + InstructionValue::Await { .. } + | InstructionValue::ComputedDelete { .. } + | InstructionValue::ComputedStore { .. } + | InstructionValue::PropertyDelete { .. } + | InstructionValue::PropertyStore { .. } + | InstructionValue::StoreGlobal { .. } => { + // Mutating instructions are not safe to prune + false + } + InstructionValue::NewExpression { .. } + | InstructionValue::TaggedTemplateExpression { .. } => { + // Potentially safe to prune, but we conservatively keep them + false + } + InstructionValue::GetIterator { .. } + | InstructionValue::NextPropertyOf { .. } + | InstructionValue::IteratorNext { .. } => { + // Iterator operations are always used downstream + false + } + InstructionValue::LoadContext { .. } + | InstructionValue::DeclareContext { .. } + | InstructionValue::StoreContext { .. } => false, + InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => false, + InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::ArrayExpression { .. } + | InstructionValue::BinaryExpression { .. } + | InstructionValue::ComputedLoad { .. } + | InstructionValue::ObjectMethod { .. } + | InstructionValue::FunctionExpression { .. } + | InstructionValue::LoadLocal { .. } + | InstructionValue::JsxExpression { .. } + | InstructionValue::JsxFragment { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::ObjectExpression { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::PropertyLoad { .. } + | InstructionValue::TemplateLiteral { .. } + | InstructionValue::TypeCastExpression { .. } + | InstructionValue::UnaryExpression { .. } => { + // Definitely safe to prune since they are read-only + true + } + } +} + +/// Check if the CFG has any back edges (indicating loops). +fn has_back_edge(func: &HirFunction) -> bool { + let mut visited: FxHashSet = FxHashSet::default(); + for (block_id, block) in &func.body.blocks { + for pred_id in &block.preds { + if !visited.contains(pred_id) { + return true; + } + } + visited.insert(*block_id); + } + false +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/drop_manual_memoization.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/drop_manual_memoization.rs new file mode 100644 index 0000000000000..dfd9887071660 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/drop_manual_memoization.rs @@ -0,0 +1,703 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Removes manual memoization using `useMemo` and `useCallback` APIs. +//! +//! For useMemo: replaces `Call useMemo(fn, deps)` with `Call fn()` +//! For useCallback: replaces `Call useCallback(fn, deps)` with `LoadLocal fn` +//! +//! When validation flags are set, inserts `StartMemoize`/`FinishMemoize` markers. +//! +//! Analogous to TS `Inference/DropManualMemoization.ts`. + +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::CompilerDiagnostic; +use crate::react_compiler_diagnostics::CompilerDiagnosticDetail; +use crate::react_compiler_diagnostics::ErrorCategory; +use crate::react_compiler_hir::ArrayElement; +use crate::react_compiler_hir::DependencyPathEntry; +use crate::react_compiler_hir::Effect; +use crate::react_compiler_hir::EvaluationOrder; +use crate::react_compiler_hir::HirFunction; +use crate::react_compiler_hir::IdentifierId; +use crate::react_compiler_hir::IdentifierName; +use crate::react_compiler_hir::Instruction; +use crate::react_compiler_hir::InstructionId; +use crate::react_compiler_hir::InstructionValue; +use crate::react_compiler_hir::ManualMemoDependency; +use crate::react_compiler_hir::ManualMemoDependencyRoot; +use crate::react_compiler_hir::NonLocalBinding; +use crate::react_compiler_hir::Place; +use crate::react_compiler_hir::PlaceOrSpread; +use crate::react_compiler_hir::PropertyLiteral; +use crate::react_compiler_hir::SourceLocation; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_lowering::create_temporary_place; +use crate::react_compiler_lowering::mark_instruction_ids; + +// ============================================================================= +// Types +// ============================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ManualMemoKind { + UseMemo, + UseCallback, +} + +#[derive(Debug, Clone)] +struct ManualMemoCallee { + kind: ManualMemoKind, + /// InstructionId of the LoadGlobal or PropertyLoad that loaded the callee. + load_instr_id: InstructionId, +} + +struct IdentifierSidemap { + /// Maps identifier id -> InstructionId of FunctionExpression instructions + functions: FxHashSet, + /// Maps identifier id -> ManualMemoCallee for useMemo/useCallback callees + manual_memos: FxHashMap, + /// Set of identifier ids that loaded 'React' global + react: FxHashSet, + /// Maps identifier id -> deps list info for array expressions + maybe_deps_lists: FxHashMap, + /// Maps identifier id -> ManualMemoDependency for dependency tracking + maybe_deps: FxHashMap, + /// Set of identifier ids that are results of optional chains + optionals: FxHashSet, +} + +#[derive(Debug, Clone)] +struct MaybeDepsListInfo { + loc: Option, + deps: Vec, +} + +struct ExtractedMemoArgs { + fn_place: Place, + deps_list: Option>, + deps_loc: Option, +} + +// ============================================================================= +// Main pass +// ============================================================================= + +/// Drop manual memoization (useMemo/useCallback calls), replacing them +/// with direct invocations/references. +pub fn drop_manual_memoization<'a>( + func: &mut HirFunction<'a>, + env: &mut Environment<'a>, +) -> Result<(), CompilerDiagnostic> { + let is_validation_enabled = env.validate_preserve_existing_memoization_guarantees + || env.validate_no_set_state_in_render + || env.enable_preserve_existing_memoization_guarantees; + + let optionals = find_optional_places(func)?; + let mut sidemap = IdentifierSidemap { + functions: FxHashSet::default(), + manual_memos: FxHashMap::default(), + react: FxHashSet::default(), + maybe_deps: FxHashMap::default(), + maybe_deps_lists: FxHashMap::default(), + optionals, + }; + let mut next_manual_memo_id: u32 = 0; + + // Phase 1: + // - Overwrite manual memoization CallExpression/MethodCall + // - (if validation is enabled) collect manual memoization markers + // + // queued_inserts maps InstructionId -> new Instruction to insert after that instruction + let mut queued_inserts: FxHashMap> = FxHashMap::default(); + + // Collect all block instruction lists up front to avoid borrowing func immutably + // while needing to mutate it + let all_block_instructions: Vec> = + func.body.blocks.values().map(|block| block.instructions.clone()).collect(); + + for block_instructions in &all_block_instructions { + for &instr_id in block_instructions { + let instr = &func.instructions[instr_id.0 as usize]; + + // Extract the identifier we need to look up, and whether it's a call/method + let lookup_id = match &instr.value { + InstructionValue::CallExpression { callee, .. } => Some(callee.identifier), + InstructionValue::MethodCall { property, .. } => Some(property.identifier), + _ => None, + }; + + let manual_memo = lookup_id.and_then(|id| sidemap.manual_memos.get(&id).cloned()); + + if let Some(manual_memo) = manual_memo { + process_manual_memo_call( + func, + env, + instr_id, + &manual_memo, + &mut sidemap, + is_validation_enabled, + &mut next_manual_memo_id, + &mut queued_inserts, + ); + } else { + collect_temporaries(func, env, instr_id, &mut sidemap); + } + } + } + + // Phase 2: Insert manual memoization markers as needed + if !queued_inserts.is_empty() { + let mut has_changes = false; + for block in func.body.blocks.values_mut() { + let mut next_instructions: Option> = None; + for i in 0..block.instructions.len() { + let instr_id = block.instructions[i]; + if let Some(insert_instr) = queued_inserts.remove(&instr_id) { + if next_instructions.is_none() { + next_instructions = Some(block.instructions[..i].to_vec()); + } + let ni = next_instructions.as_mut().unwrap(); + ni.push(instr_id); + // Add the new instruction to the flat table and get its InstructionId + let new_instr_id = InstructionId(func.instructions.len() as u32); + func.instructions.push(insert_instr); + ni.push(new_instr_id); + } else if let Some(ni) = next_instructions.as_mut() { + ni.push(instr_id); + } + } + if let Some(ni) = next_instructions { + block.instructions = ni; + has_changes = true; + } + } + + if has_changes { + mark_instruction_ids(&mut func.body, &mut func.instructions); + } + } + + Ok(()) +} + +// ============================================================================= +// Phase 1 helpers +// ============================================================================= + +#[allow(clippy::too_many_arguments)] +fn process_manual_memo_call<'a>( + func: &mut HirFunction<'a>, + env: &mut Environment<'a>, + instr_id: InstructionId, + manual_memo: &ManualMemoCallee, + sidemap: &mut IdentifierSidemap, + is_validation_enabled: bool, + next_manual_memo_id: &mut u32, + queued_inserts: &mut FxHashMap>, +) { + let instr = &func.instructions[instr_id.0 as usize]; + + let memo_details = extract_manual_memoization_args(instr, manual_memo.kind, sidemap, env); + + let Some(memo_details) = memo_details else { + return; + }; + + let ExtractedMemoArgs { fn_place, deps_list, deps_loc } = memo_details; + + let loc = func.instructions[instr_id.0 as usize].value.loc().cloned(); + + // Replace the instruction value with the memoization replacement + let replacement = get_manual_memoization_replacement(&fn_place, loc.clone(), manual_memo.kind); + func.instructions[instr_id.0 as usize].value = replacement; + + if is_validation_enabled { + // Bail out when we encounter manual memoization without inline function expressions + if !sidemap.functions.contains(&fn_place.identifier) { + let mut diag = CompilerDiagnostic::new( + ErrorCategory::UseMemo, + "Expected the first argument to be an inline function expression", + Some("Expected the first argument to be an inline function expression".to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: fn_place.loc.clone(), + message: Some( + "Expected the first argument to be an inline function expression".to_string(), + ), + identifier_name: None, + }); + // Match TS behavior: suggestions is [] (empty array), not null + diag.suggestions = Some(vec![]); + env.record_diagnostic(diag); + return; + } + + let memo_decl: Place = if manual_memo.kind == ManualMemoKind::UseMemo { + func.instructions[instr_id.0 as usize].lvalue.clone() + } else { + Place { + identifier: fn_place.identifier, + effect: Effect::Unknown, + reactive: false, + loc: fn_place.loc.clone(), + } + }; + + let manual_memo_id = *next_manual_memo_id; + *next_manual_memo_id += 1; + + let (start_marker, finish_marker) = make_manual_memoization_markers( + &fn_place, + env, + deps_list, + deps_loc, + &memo_decl, + manual_memo_id, + ); + + queued_inserts.insert(manual_memo.load_instr_id, start_marker); + queued_inserts.insert(instr_id, finish_marker); + } +} + +fn collect_temporaries( + func: &HirFunction<'_>, + env: &Environment<'_>, + instr_id: InstructionId, + sidemap: &mut IdentifierSidemap, +) { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + + match &instr.value { + InstructionValue::FunctionExpression { .. } => { + sidemap.functions.insert(lvalue_id); + } + InstructionValue::LoadGlobal { binding, .. } => { + let hook_name = get_hook_detection_name(binding); + let mut detected = false; + if let Some(name) = hook_name { + if name == "useMemo" { + sidemap.manual_memos.insert( + lvalue_id, + ManualMemoCallee { kind: ManualMemoKind::UseMemo, load_instr_id: instr_id }, + ); + detected = true; + } else if name == "useCallback" { + sidemap.manual_memos.insert( + lvalue_id, + ManualMemoCallee { + kind: ManualMemoKind::UseCallback, + load_instr_id: instr_id, + }, + ); + detected = true; + } + } + if !detected && binding.name() == "React" { + sidemap.react.insert(lvalue_id); + } + } + InstructionValue::PropertyLoad { object, property, .. } => { + if sidemap.react.contains(&object.identifier) { + if let PropertyLiteral::String(prop_name) = property { + if prop_name == "useMemo" { + sidemap.manual_memos.insert( + lvalue_id, + ManualMemoCallee { + kind: ManualMemoKind::UseMemo, + load_instr_id: instr_id, + }, + ); + } else if prop_name == "useCallback" { + sidemap.manual_memos.insert( + lvalue_id, + ManualMemoCallee { + kind: ManualMemoKind::UseCallback, + load_instr_id: instr_id, + }, + ); + } + } + } + } + InstructionValue::ArrayExpression { elements, .. } => { + // Check if all elements are Identifier (Place) - no spreads or holes + let all_places: Option> = elements + .iter() + .map(|e| match e { + ArrayElement::Place(p) => Some(p.clone()), + _ => None, + }) + .collect(); + + if let Some(deps) = all_places { + sidemap + .maybe_deps_lists + .insert(lvalue_id, MaybeDepsListInfo { loc: instr.value.loc().cloned(), deps }); + } + } + _ => {} + } + + let is_optional = sidemap.optionals.contains(&lvalue_id); + let maybe_dep = + collect_maybe_memo_dependencies(&instr.value, &sidemap.maybe_deps, is_optional, env); + if let Some(dep) = maybe_dep { + // For StoreLocal, also insert under the StoreLocal's lvalue place identifier, + // matching the TS behavior where collectMaybeMemoDependencies inserts into + // maybeDeps directly for StoreLocal's target variable. + if let InstructionValue::StoreLocal { lvalue, .. } = &instr.value { + sidemap.maybe_deps.insert(lvalue.place.identifier, dep.clone()); + } + sidemap.maybe_deps.insert(lvalue_id, dep); + } +} + +// ============================================================================= +// collectMaybeMemoDependencies +// ============================================================================= + +/// Collect loads from named variables and property reads into `maybe_deps`. +/// Returns the variable + property reads represented by the instruction value. +pub fn collect_maybe_memo_dependencies( + value: &InstructionValue<'_>, + maybe_deps: &FxHashMap, + optional: bool, + env: &Environment<'_>, +) -> Option { + match value { + InstructionValue::LoadGlobal { binding, loc, .. } => Some(ManualMemoDependency { + root: ManualMemoDependencyRoot::Global { identifier_name: binding.name().to_string() }, + path: vec![], + loc: loc.clone(), + }), + InstructionValue::PropertyLoad { object, property, loc, .. } => { + if let Some(object_dep) = maybe_deps.get(&object.identifier) { + Some(ManualMemoDependency { + root: object_dep.root.clone(), + path: { + let mut path = object_dep.path.clone(); + path.push(DependencyPathEntry { + property: property.clone(), + optional, + loc: loc.clone(), + }); + path + }, + loc: loc.clone(), + }) + } else { + None + } + } + InstructionValue::LoadLocal { place, .. } | InstructionValue::LoadContext { place, .. } => { + if let Some(source) = maybe_deps.get(&place.identifier) { + Some(source.clone()) + } else if matches!( + &env.identifiers[place.identifier.0 as usize].name, + Some(IdentifierName::Named(_)) + ) { + Some(ManualMemoDependency { + root: ManualMemoDependencyRoot::NamedLocal { + value: place.clone(), + constant: false, + }, + path: vec![], + loc: place.loc.clone(), + }) + } else { + None + } + } + InstructionValue::StoreLocal { lvalue, value: val, .. } => { + // Value blocks rely on StoreLocal to populate their return value. + // We need to track these as optional property chains are valid in + // source depslists + let lvalue_id = lvalue.place.identifier; + let rvalue_id = val.identifier; + if let Some(aliased) = maybe_deps.get(&rvalue_id) { + let lvalue_name = &env.identifiers[lvalue_id.0 as usize].name; + if !matches!(lvalue_name, Some(IdentifierName::Named(_))) { + // Note: we can't insert into maybe_deps here since we only have + // a shared reference. The caller handles insertion. + return Some(aliased.clone()); + } + } + None + } + _ => None, + } +} + +// ============================================================================= +// Replacement helpers +// ============================================================================= + +fn get_manual_memoization_replacement<'a>( + fn_place: &Place, + loc: Option, + kind: ManualMemoKind, +) -> InstructionValue<'a> { + if kind == ManualMemoKind::UseMemo { + // Replace with Call fn() - invoke the memo function directly + InstructionValue::CallExpression { callee: fn_place.clone(), args: vec![], loc } + } else { + // Replace with LoadLocal fn - just reference the function + InstructionValue::LoadLocal { + place: Place { + identifier: fn_place.identifier, + effect: Effect::Unknown, + reactive: false, + loc: loc.clone(), + }, + loc, + } + } +} + +fn make_manual_memoization_markers<'a>( + fn_expr: &Place, + env: &mut Environment<'a>, + deps_list: Option>, + deps_loc: Option, + memo_decl: &Place, + manual_memo_id: u32, +) -> (Instruction<'a>, Instruction<'a>) { + let start = Instruction { + id: EvaluationOrder(0), + lvalue: create_temporary_place(env, fn_expr.loc.clone()), + value: InstructionValue::StartMemoize { + manual_memo_id, + deps: deps_list, + deps_loc: Some(deps_loc), + has_invalid_deps: false, + loc: fn_expr.loc.clone(), + }, + loc: fn_expr.loc.clone(), + effects: None, + }; + let finish = Instruction { + id: EvaluationOrder(0), + lvalue: create_temporary_place(env, fn_expr.loc.clone()), + value: InstructionValue::FinishMemoize { + manual_memo_id, + decl: memo_decl.clone(), + pruned: false, + loc: fn_expr.loc.clone(), + }, + loc: fn_expr.loc.clone(), + effects: None, + }; + (start, finish) +} + +fn extract_manual_memoization_args( + instr: &Instruction, + kind: ManualMemoKind, + sidemap: &IdentifierSidemap, + env: &mut Environment, +) -> Option { + let args: &[PlaceOrSpread] = match &instr.value { + InstructionValue::CallExpression { args, .. } => args, + InstructionValue::MethodCall { args, .. } => args, + _ => return None, + }; + + let kind_name = match kind { + ManualMemoKind::UseMemo => "useMemo", + ManualMemoKind::UseCallback => "useCallback", + }; + + // Get the first arg (fn) + let fn_place = match args.first() { + Some(PlaceOrSpread::Place(p)) => p.clone(), + _ => { + let loc = instr.value.loc().cloned(); + env.record_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::UseMemo, + format!("Expected a callback function to be passed to {kind_name}"), + Some(if kind == ManualMemoKind::UseCallback { + "The first argument to useCallback() must be a function to cache".to_string() + } else { + "The first argument to useMemo() must be a function that calculates a result to cache".to_string() + }), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc, + message: Some(if kind == ManualMemoKind::UseCallback { + "Expected a callback function".to_string() + } else { + "Expected a memoization function".to_string() + }), + identifier_name: None, + }), + ); + return None; + } + }; + + // Get the second arg (deps list), if present + let deps_list_place = args.get(1); + if deps_list_place.is_none() { + return Some(ExtractedMemoArgs { fn_place, deps_list: None, deps_loc: None }); + } + + let deps_list_id = match deps_list_place { + Some(PlaceOrSpread::Place(p)) => Some(p.identifier), + _ => None, + }; + + let maybe_deps_list = deps_list_id.and_then(|id| sidemap.maybe_deps_lists.get(&id)); + + if maybe_deps_list.is_none() { + let loc = match deps_list_place { + Some(PlaceOrSpread::Place(p)) => p.loc.clone(), + _ => instr.loc.clone(), + }; + env.record_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::UseMemo, + format!("Expected the dependency list for {kind_name} to be an array literal"), + Some(format!( + "Expected the dependency list for {kind_name} to be an array literal" + )), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc, + message: Some(format!( + "Expected the dependency list for {kind_name} to be an array literal" + )), + identifier_name: None, + }), + ); + return None; + } + + let deps_info = maybe_deps_list.unwrap(); + let mut deps_list: Vec = Vec::new(); + for dep in &deps_info.deps { + let maybe_dep = sidemap.maybe_deps.get(&dep.identifier); + if let Some(d) = maybe_dep { + deps_list.push(d.clone()); + } else { + env.record_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::UseMemo, + "Expected the dependency list to be an array of simple expressions (e.g. `x`, `x.y.z`, `x?.y?.z`)", + Some("Expected the dependency list to be an array of simple expressions (e.g. `x`, `x.y.z`, `x?.y?.z`)".to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: dep.loc.clone(), + message: Some("Expected the dependency list to be an array of simple expressions (e.g. `x`, `x.y.z`, `x?.y?.z`)".to_string()), + identifier_name: None, + }), + ); + } + } + + Some(ExtractedMemoArgs { + fn_place, + deps_list: Some(deps_list), + deps_loc: deps_info.loc.clone(), + }) +} + +// ============================================================================= +// findOptionalPlaces +// ============================================================================= + +fn find_optional_places(func: &HirFunction) -> Result, CompilerDiagnostic> { + use crate::react_compiler_hir::Terminal; + + let mut optionals = FxHashSet::default(); + for block in func.body.blocks.values() { + if let Terminal::Optional { optional: true, test, fallthrough, .. } = &block.terminal { + let optional_fallthrough = *fallthrough; + let mut test_block_id = *test; + loop { + let test_block = &func.body.blocks[&test_block_id]; + match &test_block.terminal { + Terminal::Branch { consequent, fallthrough, .. } => { + if *fallthrough == optional_fallthrough { + // Found it + let consequent_block = &func.body.blocks[consequent]; + if let Some(&last_instr_id) = consequent_block.instructions.last() { + let last_instr = &func.instructions[last_instr_id.0 as usize]; + if let InstructionValue::StoreLocal { value, .. } = + &last_instr.value + { + optionals.insert(value.identifier); + } + } + break; + } else { + test_block_id = *fallthrough; + } + } + Terminal::Optional { fallthrough, .. } + | Terminal::Logical { fallthrough, .. } + | Terminal::Sequence { fallthrough, .. } + | Terminal::Ternary { fallthrough, .. } => { + test_block_id = *fallthrough; + } + Terminal::MaybeThrow { continuation, .. } => { + test_block_id = *continuation; + } + other => { + // Invariant: unexpected terminal in optional + // In TS this throws CompilerError.invariant + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!( + "Unexpected terminal kind in optional: {:?}", + std::mem::discriminant(other) + ), + None, + )); + } + } + } + } + } + Ok(optionals) +} + +fn is_known_react_module(module: &str) -> bool { + let lower = module.to_lowercase(); + lower == "react" || lower == "react-dom" +} + +/// Returns the name to use for useMemo/useCallback detection, matching the TS +/// behavior of `getGlobalDeclaration` + `getHookKindForType`. +/// +/// - `Global`: use the binding name (matches globals.get(name) in TS) +/// - `ImportSpecifier` from known React module: use the `imported` name +/// - `ImportSpecifier` from unknown module: return None (TS returns a generic +/// custom hook type with hookKind 'Custom', not 'useMemo'/'useCallback') +/// - `ModuleLocal`: return None (same reason as above) +/// - `ImportDefault`/`ImportNamespace` from known React module: use the local name +/// - `ImportDefault`/`ImportNamespace` from unknown module: return None +fn get_hook_detection_name(binding: &NonLocalBinding) -> Option<&str> { + match binding { + NonLocalBinding::Global { name } => Some(name.as_str()), + NonLocalBinding::ImportSpecifier { imported, module, .. } => { + if is_known_react_module(module) { Some(imported.as_str()) } else { None } + } + NonLocalBinding::ImportDefault { name, module } + | NonLocalBinding::ImportNamespace { name, module } => { + if is_known_react_module(module) { + Some(name.as_str()) + } else { + None + } + } + NonLocalBinding::ModuleLocal { .. } => None, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/inline_iifes.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/inline_iifes.rs new file mode 100644 index 0000000000000..140aca75446bb --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/inline_iifes.rs @@ -0,0 +1,392 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Inlines immediately invoked function expressions (IIFEs) to allow more +//! fine-grained memoization of the values they produce. +//! +//! Example: +//! ```text +//! const x = (() => { +//! const x = []; +//! x.push(foo()); +//! return x; +//! })(); +//! +//! => +//! +//! bb0: +//! // placeholder for the result, all return statements will assign here +//! let t0; +//! // Label allows using a goto (break) to exit out of the body +//! Label block=bb1 fallthrough=bb2 +//! bb1: +//! // code within the function expression +//! const x0 = []; +//! x0.push(foo()); +//! // return is replaced by assignment to the result variable... +//! t0 = x0; +//! // ...and a goto to the code after the function expression invocation +//! Goto bb2 +//! bb2: +//! // code after the IIFE call +//! const x = t0; +//! ``` +//! +//! If the inlined function has only one return, we avoid the labeled block +//! and fully inline the code. The original return is replaced with an assignment +//! to the IIFE's call expression lvalue. +//! +//! Analogous to TS `Inference/InlineImmediatelyInvokedFunctionExpressions.ts`. + +use crate::react_compiler_utils::FxIndexSet; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::{ + BasicBlock, BlockId, BlockKind, EvaluationOrder, FunctionId, GENERATED_SOURCE, GotoVariant, + HirFunction, IdentifierId, IdentifierName, Instruction, InstructionId, InstructionKind, + InstructionValue, LValue, Place, Terminal, +}; +use crate::react_compiler_lowering::{ + create_temporary_place, get_reverse_postordered_blocks, mark_instruction_ids, mark_predecessors, +}; + +use crate::react_compiler_optimization::merge_consecutive_blocks::merge_consecutive_blocks; + +/// Inline immediately invoked function expressions into the enclosing function's +/// control flow graph. +pub fn inline_immediately_invoked_function_expressions<'a>( + func: &mut HirFunction<'a>, + env: &mut Environment<'a>, +) { + // Track all function expressions that are assigned to a temporary + let mut functions: FxHashMap = FxHashMap::default(); + // Functions that are inlined (by identifier id of the callee) + let mut inlined_functions: FxHashSet = FxHashSet::default(); + + // Iterate the *existing* blocks from the outer component to find IIFEs + // and inline them. During iteration we will modify `func` (by inlining the CFG + // of IIFEs) so we explicitly copy references to just the original + // function's block IDs first. As blocks are split to make room for IIFE calls, + // the split portions of the blocks will be added to this queue. + let mut queue: Vec = func.body.blocks.keys().copied().collect(); + let mut queue_idx = 0; + + 'queue: while queue_idx < queue.len() { + let block_id = queue[queue_idx]; + queue_idx += 1; + + let block = match func.body.blocks.get(&block_id) { + Some(b) => b, + None => continue, + }; + + // We can't handle labels inside expressions yet, so we don't inline IIFEs + // if they are in an expression block. + if !is_statement_block_kind(block.kind) { + continue; + } + + let num_instructions = block.instructions.len(); + for ii in 0..num_instructions { + let instr_id = func.body.blocks[&block_id].instructions[ii]; + let instr = &func.instructions[instr_id.0 as usize]; + + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } => { + let identifier_id = instr.lvalue.identifier; + if env.identifiers[identifier_id.0 as usize].name.is_none() { + functions.insert(identifier_id, lowered_func.func); + } + continue; + } + InstructionValue::CallExpression { callee, args, .. } => { + if !args.is_empty() { + // We don't support inlining when there are arguments + continue; + } + + let callee_id = callee.identifier; + let inner_func_id = match functions.get(&callee_id) { + Some(id) => *id, + None => continue, // Not invoking a local function expression + }; + + let inner_func = &env.functions[inner_func_id.0 as usize]; + if !inner_func.params.is_empty() || inner_func.is_async || inner_func.generator + { + // Can't inline functions with params, or async/generator functions + continue; + } + + // We know this function is used for an IIFE and can prune it later + inlined_functions.insert(callee_id); + + // Capture the lvalue from the call instruction + let call_lvalue = func.instructions[instr_id.0 as usize].lvalue.clone(); + let block_terminal_id = func.body.blocks[&block_id].terminal.evaluation_order(); + let block_terminal_loc = func.body.blocks[&block_id].terminal.loc().cloned(); + let block_kind = func.body.blocks[&block_id].kind; + + // Create a new block which will contain code following the IIFE call + let continuation_block_id = env.next_block_id(); + let continuation_instructions: Vec = + func.body.blocks[&block_id].instructions[ii + 1..].to_vec(); + let continuation_terminal = func.body.blocks[&block_id].terminal.clone(); + let continuation_block = BasicBlock { + id: continuation_block_id, + instructions: continuation_instructions, + kind: block_kind, + phis: Vec::new(), + preds: FxIndexSet::default(), + terminal: continuation_terminal, + }; + func.body.blocks.insert(continuation_block_id, continuation_block); + + // Trim the original block to contain instructions up to (but not including) + // the IIFE + func.body.blocks.get_mut(&block_id).unwrap().instructions.truncate(ii); + + let has_single_return = + has_single_exit_return_terminal(&env.functions[inner_func_id.0 as usize]); + let inner_entry = env.functions[inner_func_id.0 as usize].body.entry; + + if has_single_return { + // Single-return path: simple goto replacement + func.body.blocks.get_mut(&block_id).unwrap().terminal = Terminal::Goto { + block: inner_entry, + id: block_terminal_id, + loc: block_terminal_loc, + variant: GotoVariant::Break, + }; + + // Take blocks and instructions from inner function + let inner_func = &mut env.functions[inner_func_id.0 as usize]; + let inner_blocks: Vec<(BlockId, BasicBlock)> = + inner_func.body.blocks.drain(..).collect(); + let inner_instructions: Vec> = + inner_func.instructions.drain(..).collect(); + + // Append inner instructions first, then remap block instruction IDs + let instr_offset = func.instructions.len() as u32; + func.instructions.extend(inner_instructions); + + for (_, mut inner_block) in inner_blocks { + // Remap instruction IDs in the block + for iid in &mut inner_block.instructions { + *iid = InstructionId(iid.0 + instr_offset); + } + inner_block.preds.clear(); + + if let Terminal::Return { value, id: ret_id, loc: ret_loc, .. } = + &inner_block.terminal + { + // Replace return with LoadLocal + goto + let load_instr = Instruction { + id: EvaluationOrder(0), + loc: ret_loc.clone(), + lvalue: call_lvalue.clone(), + value: InstructionValue::LoadLocal { + place: value.clone(), + loc: ret_loc.clone(), + }, + effects: None, + }; + let load_instr_id = InstructionId(func.instructions.len() as u32); + func.instructions.push(load_instr); + inner_block.instructions.push(load_instr_id); + + let ret_id = *ret_id; + let ret_loc = ret_loc.clone(); + inner_block.terminal = Terminal::Goto { + block: continuation_block_id, + id: ret_id, + loc: ret_loc, + variant: GotoVariant::Break, + }; + } + + func.body.blocks.insert(inner_block.id, inner_block); + } + } else { + // Multi-return path: uses LabelTerminal + let result = call_lvalue.clone(); + + // Set block terminal to Label + func.body.blocks.get_mut(&block_id).unwrap().terminal = Terminal::Label { + block: inner_entry, + id: EvaluationOrder(0), + fallthrough: continuation_block_id, + loc: block_terminal_loc, + }; + + // Declare the IIFE temporary + declare_temporary(env, func, block_id, &result); + + // Promote the temporary with a name as we require this to persist + let identifier_id = result.identifier; + if env.identifiers[identifier_id.0 as usize].name.is_none() { + promote_temporary(env, identifier_id); + } + + // Take blocks and instructions from inner function + let inner_func = &mut env.functions[inner_func_id.0 as usize]; + let inner_blocks: Vec<(BlockId, BasicBlock)> = + inner_func.body.blocks.drain(..).collect(); + let inner_instructions: Vec> = + inner_func.instructions.drain(..).collect(); + + // Append inner instructions first, then remap block instruction IDs + let instr_offset = func.instructions.len() as u32; + func.instructions.extend(inner_instructions); + + for (_, mut inner_block) in inner_blocks { + for iid in &mut inner_block.instructions { + *iid = InstructionId(iid.0 + instr_offset); + } + inner_block.preds.clear(); + + // Rewrite return terminals to StoreLocal + goto + if matches!(inner_block.terminal, Terminal::Return { .. }) { + rewrite_block( + env, + &mut func.instructions, + &mut inner_block, + continuation_block_id, + &result, + ); + } + + func.body.blocks.insert(inner_block.id, inner_block); + } + } + + // Ensure we visit the continuation block, since there may have been + // sequential IIFEs that need to be visited. + queue.push(continuation_block_id); + continue 'queue; + } + _ => { + // Any other use of a function expression means it isn't an IIFE + for id in visitors::each_instruction_value_operand_ids(&instr.value, env) { + functions.remove(&id); + } + } + } + } + } + + if !inlined_functions.is_empty() { + // Remove instructions that define lambdas which we inlined + for block in func.body.blocks.values_mut() { + block.instructions.retain(|instr_id| { + let instr = &func.instructions[instr_id.0 as usize]; + !inlined_functions.contains(&instr.lvalue.identifier) + }); + } + + // If terminals have changed then blocks may have become newly unreachable. + // Re-run minification of the graph (incl reordering instruction ids). + func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions); + mark_instruction_ids(&mut func.body, &mut func.instructions); + mark_predecessors(&mut func.body); + merge_consecutive_blocks(func, &mut env.functions); + } +} + +/// Returns true for "block" and "catch" block kinds which correspond to statements +/// in the source. +fn is_statement_block_kind(kind: BlockKind) -> bool { + matches!(kind, BlockKind::Block | BlockKind::Catch) +} + +/// Returns true if the function has a single exit terminal (throw/return) which is a return. +fn has_single_exit_return_terminal(func: &HirFunction<'_>) -> bool { + let mut has_return = false; + let mut exit_count = 0; + for block in func.body.blocks.values() { + match &block.terminal { + Terminal::Return { .. } => { + has_return = true; + exit_count += 1; + } + Terminal::Throw { .. } => { + exit_count += 1; + } + _ => {} + } + } + exit_count == 1 && has_return +} + +/// Rewrites the block so that all `return` terminals are replaced: +/// * Add a StoreLocal = +/// * Replace the terminal with a Goto to +fn rewrite_block<'a>( + env: &mut Environment<'a>, + instructions: &mut Vec>, + block: &mut BasicBlock, + return_target: BlockId, + return_value: &Place, +) { + if let Terminal::Return { value, loc: ret_loc, .. } = &block.terminal { + let store_lvalue = create_temporary_place(env, ret_loc.clone()); + let store_instr = Instruction { + id: EvaluationOrder(0), + loc: ret_loc.clone(), + lvalue: store_lvalue, + value: InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Reassign, place: return_value.clone() }, + value: value.clone(), + type_annotation: None, + loc: ret_loc.clone(), + }, + effects: None, + }; + let store_instr_id = InstructionId(instructions.len() as u32); + instructions.push(store_instr); + block.instructions.push(store_instr_id); + + let ret_loc = ret_loc.clone(); + block.terminal = Terminal::Goto { + block: return_target, + id: EvaluationOrder(0), + variant: GotoVariant::Break, + loc: ret_loc, + }; + } +} + +/// Emits a DeclareLocal instruction for the result temporary. +fn declare_temporary<'a>( + env: &mut Environment<'a>, + func: &mut HirFunction<'a>, + block_id: BlockId, + result: &Place, +) { + let declare_lvalue = create_temporary_place(env, result.loc.clone()); + let declare_instr = Instruction { + id: EvaluationOrder(0), + loc: GENERATED_SOURCE, + lvalue: declare_lvalue, + value: InstructionValue::DeclareLocal { + lvalue: LValue { place: result.clone(), kind: InstructionKind::Let }, + type_annotation: None, + loc: result.loc.clone(), + }, + effects: None, + }; + let instr_id = InstructionId(func.instructions.len() as u32); + func.instructions.push(declare_instr); + func.body.blocks.get_mut(&block_id).unwrap().instructions.push(instr_id); +} + +/// Promote a temporary identifier to a named identifier. +fn promote_temporary(env: &mut Environment<'_>, identifier_id: IdentifierId) { + let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id; + env.identifiers[identifier_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#t{}", decl_id.0))); +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/merge_consecutive_blocks.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/merge_consecutive_blocks.rs new file mode 100644 index 0000000000000..f84ca80898c74 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/merge_consecutive_blocks.rs @@ -0,0 +1,209 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Merges sequences of blocks that will always execute consecutively — +//! i.e., where the predecessor always transfers control to the successor +//! (ends in a goto) and where the predecessor is the only predecessor +//! for that successor (no other way to reach the successor). +//! +//! Value/loop blocks are left alone because they cannot be merged without +//! breaking the structure of the high-level terminals that reference them. +//! +//! Analogous to TS `HIR/MergeConsecutiveBlocks.ts`. + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::{ + AliasingEffect, BlockId, BlockKind, Effect, GENERATED_SOURCE, HirFunction, Instruction, + InstructionId, InstructionValue, Place, Terminal, +}; +use crate::react_compiler_lowering::mark_predecessors; +use crate::react_compiler_ssa::enter_ssa::placeholder_function; + +/// Merge consecutive blocks in the function's CFG, including inner functions. +pub fn merge_consecutive_blocks(func: &mut HirFunction, functions: &mut [HirFunction]) { + // Collect inner function IDs for recursive processing + let inner_func_ids: Vec = func + .body + .blocks + .values() + .flat_map(|block| block.instructions.iter()) + .filter_map(|instr_id| { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + Some(lowered_func.func.0 as usize) + } + _ => None, + } + }) + .collect(); + + // Recursively merge consecutive blocks in inner functions + for func_id in inner_func_ids { + // Use std::mem::replace to temporarily take the inner function out, + // process it, then put it back (standard borrow checker workaround) + let mut inner_func = std::mem::replace(&mut functions[func_id], placeholder_function()); + merge_consecutive_blocks(&mut inner_func, functions); + functions[func_id] = inner_func; + } + + // Build fallthrough set + let mut fallthrough_blocks: FxHashSet = FxHashSet::default(); + for block in func.body.blocks.values() { + if let Some(ft) = visitors::terminal_fallthrough(&block.terminal) { + fallthrough_blocks.insert(ft); + } + } + + let mut merged = MergedBlocks::new(); + + // Collect block IDs for iteration (since we modify during iteration) + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + + for block_id in &block_ids { + let block = match func.body.blocks.get(block_id) { + Some(b) => b, + None => continue, // already removed + }; + + if block.preds.len() != 1 + || block.kind != BlockKind::Block + || fallthrough_blocks.contains(block_id) + { + continue; + } + + let original_pred_id = *block.preds.iter().next().unwrap(); + let pred_id = merged.get(original_pred_id); + + // Check predecessor exists and ends in goto with block kind + let pred_is_mergeable = func + .body + .blocks + .get(&pred_id) + .map(|p| matches!(p.terminal, Terminal::Goto { .. }) && p.kind == BlockKind::Block) + .unwrap_or(false); + + if !pred_is_mergeable { + continue; + } + + // Get evaluation order from predecessor's terminal (for phi instructions) + let eval_order = func.body.blocks[&pred_id].terminal.evaluation_order(); + + // Collect phi data from the block being merged + let phis: Vec<_> = block + .phis + .iter() + .map(|phi| { + assert_eq!( + phi.operands.len(), + 1, + "Found a block with a single predecessor but where a phi has multiple ({}) operands", + phi.operands.len() + ); + let operand = phi.operands.values().next().unwrap().clone(); + (phi.place.identifier, operand) + }) + .collect(); + let block_instr_ids = block.instructions.clone(); + let block_terminal = block.terminal.clone(); + + // Create phi instructions and add to instruction table + let mut new_instr_ids = Vec::new(); + for (identifier, operand) in phis { + let lvalue = Place { + identifier, + effect: Effect::ConditionallyMutate, + reactive: false, + loc: GENERATED_SOURCE, + }; + let instr = Instruction { + id: eval_order, + lvalue: lvalue.clone(), + value: InstructionValue::LoadLocal { + place: operand.clone(), + loc: GENERATED_SOURCE, + }, + loc: GENERATED_SOURCE, + effects: Some(vec![AliasingEffect::Alias { from: operand, into: lvalue }]), + }; + let instr_id = InstructionId(func.instructions.len() as u32); + func.instructions.push(instr); + new_instr_ids.push(instr_id); + } + + // Apply merge to predecessor + let pred = func.body.blocks.get_mut(&pred_id).unwrap(); + pred.instructions.extend(new_instr_ids); + pred.instructions.extend(block_instr_ids); + pred.terminal = block_terminal; + + // Record merge and remove block + merged.merge(*block_id, pred_id); + func.body.blocks.shift_remove(block_id); + } + + // Update phi operands for merged blocks + for block in func.body.blocks.values_mut() { + for phi in &mut block.phis { + let updates: Vec<_> = phi + .operands + .iter() + .filter_map(|(pred_id, operand)| { + let mapped = merged.get(*pred_id); + if mapped != *pred_id { + Some((*pred_id, mapped, operand.clone())) + } else { + None + } + }) + .collect(); + for (old_id, new_id, operand) in updates { + phi.operands.shift_remove(&old_id); + phi.operands.insert(new_id, operand); + } + } + } + + mark_predecessors(&mut func.body); + + // Update terminal successors (including fallthroughs) for merged blocks + for block in func.body.blocks.values_mut() { + visitors::map_terminal_successors(&mut block.terminal, &mut |block_id| { + merged.get(block_id) + }); + } +} + +/// Tracks which blocks have been merged and into which target. +struct MergedBlocks { + map: FxHashMap, +} + +impl MergedBlocks { + fn new() -> Self { + Self { map: FxHashMap::default() } + } + + /// Record that `block` was merged into `into`. + fn merge(&mut self, block: BlockId, into: BlockId) { + let target = self.get(into); + self.map.insert(block, target); + } + + /// Get the id of the block that `block` has been merged into. + /// Transitive: if A merged into B which merged into C, get(A) returns C. + fn get(&self, block: BlockId) -> BlockId { + let mut current = block; + while let Some(&target) = self.map.get(¤t) { + current = target; + } + current + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/mod.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/mod.rs new file mode 100644 index 0000000000000..bdb7c276a28f6 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/mod.rs @@ -0,0 +1,24 @@ +pub mod constant_propagation; +pub mod dead_code_elimination; +pub mod drop_manual_memoization; +pub mod inline_iifes; +pub mod merge_consecutive_blocks; +pub mod name_anonymous_functions; +pub mod optimize_for_ssr; +pub mod optimize_props_method_calls; +pub mod outline_functions; +pub mod outline_jsx; +pub mod prune_maybe_throws; +pub mod prune_unused_labels_hir; + +pub use constant_propagation::constant_propagation; +pub use dead_code_elimination::dead_code_elimination; +pub use drop_manual_memoization::drop_manual_memoization; +pub use inline_iifes::inline_immediately_invoked_function_expressions; +pub use name_anonymous_functions::name_anonymous_functions; +pub use optimize_for_ssr::optimize_for_ssr; +pub use optimize_props_method_calls::optimize_props_method_calls; +pub use outline_functions::outline_functions; +pub use outline_jsx::outline_jsx; +pub use prune_maybe_throws::prune_maybe_throws; +pub use prune_unused_labels_hir::prune_unused_labels_hir; diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/name_anonymous_functions.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/name_anonymous_functions.rs new file mode 100644 index 0000000000000..481e1c7815c0a --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/name_anonymous_functions.rs @@ -0,0 +1,289 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Port of NameAnonymousFunctions from TypeScript. +//! +//! Generates descriptive names for anonymous function expressions based on +//! how they are used (assigned to variables, passed as arguments to hooks/functions, +//! used as JSX props, etc.). These names appear in React DevTools and error stacks. +//! +//! Conditional on `env.config.enable_name_anonymous_functions`. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::object_shape::HookKind; +use crate::react_compiler_hir::{ + FunctionId, HirFunction, IdentifierId, IdentifierName, Instruction, InstructionValue, + JsxAttribute, JsxTag, PlaceOrSpread, +}; + +/// Assign generated names to anonymous function expressions. +/// +/// Ported from TS `nameAnonymousFunctions` in `Transform/NameAnonymousFunctions.ts`. +pub fn name_anonymous_functions(func: &mut HirFunction, env: &mut Environment) { + let fn_id = match &func.id { + Some(id) => id.clone(), + None => return, + }; + + let nodes = name_anonymous_functions_impl(func, env); + + fn visit(node: &Node, prefix: &str, updates: &mut Vec<(FunctionId, String)>) { + if node.generated_name.is_some() && node.existing_name_hint.is_none() { + // Only add the prefix to anonymous functions regardless of nesting depth + let name = format!("{}{}]", prefix, node.generated_name.as_ref().unwrap()); + updates.push((node.function_id, name)); + } + // Whether or not we generated a name for the function at this node, + // traverse into its nested functions to assign them names + let fallback; + let label = if let Some(ref gen_name) = node.generated_name { + gen_name.as_str() + } else if let Some(ref existing) = node.fn_name { + existing.as_str() + } else { + fallback = ""; + fallback + }; + let next_prefix = format!("{}{} > ", prefix, label); + for inner in &node.inner { + visit(inner, &next_prefix, updates); + } + } + + let mut updates: Vec<(FunctionId, String)> = Vec::new(); + let prefix = format!("{}[", fn_id); + for node in &nodes { + visit(node, &prefix, &mut updates); + } + + if updates.is_empty() { + return; + } + let update_map: FxHashMap = + updates.iter().map(|(fid, name)| (*fid, name)).collect(); + + // Apply name updates to the inner HirFunction in the arena + for (function_id, name) in &updates { + env.functions[function_id.0 as usize].name_hint = Some(name.clone()); + } + + // Update name_hint on FunctionExpression instruction values in the outer function + apply_name_hints_to_instructions(&mut func.instructions, &update_map); + + // Update name_hint on FunctionExpression instruction values in all arena functions + for i in 0..env.functions.len() { + // We need to temporarily take the instructions to avoid borrow issues + let mut instructions = std::mem::take(&mut env.functions[i].instructions); + apply_name_hints_to_instructions(&mut instructions, &update_map); + env.functions[i].instructions = instructions; + } +} + +/// Apply name hints to FunctionExpression instruction values. +fn apply_name_hints_to_instructions( + instructions: &mut [Instruction], + update_map: &FxHashMap, +) { + for instr in instructions.iter_mut() { + if let InstructionValue::FunctionExpression { lowered_func, name_hint, .. } = + &mut instr.value + { + if let Some(new_name) = update_map.get(&lowered_func.func) { + *name_hint = Some((*new_name).clone()); + } + } + } +} + +struct Node { + /// The FunctionId for the inner function (via lowered_func.func) + function_id: FunctionId, + /// The generated name for this anonymous function (set based on usage context) + generated_name: Option, + /// The existing `name` on the FunctionExpression (non-anonymous functions have this) + fn_name: Option, + /// Whether the inner HirFunction already has a name_hint + existing_name_hint: Option, + /// Nested function nodes + inner: Vec, +} + +fn name_anonymous_functions_impl(func: &HirFunction, env: &Environment) -> Vec { + // Functions that we track to generate names for + let mut functions: FxHashMap = FxHashMap::default(); + // Tracks temporaries that read from variables/globals/properties + let mut names: FxHashMap = FxHashMap::default(); + // Tracks all function nodes + let mut nodes: Vec = Vec::new(); + + for block in func.body.blocks.values() { + for instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + match &instr.value { + InstructionValue::LoadGlobal { binding, .. } => { + names.insert(lvalue_id, binding.name().to_string()); + } + InstructionValue::LoadContext { place, .. } + | InstructionValue::LoadLocal { place, .. } => { + let ident = &env.identifiers[place.identifier.0 as usize]; + if let Some(IdentifierName::Named(ref name)) = ident.name { + names.insert(lvalue_id, name.clone()); + } + // If the loaded place was tracked as a function, propagate + if let Some(&node_idx) = functions.get(&place.identifier) { + functions.insert(lvalue_id, node_idx); + } + } + InstructionValue::PropertyLoad { object, property, .. } => { + if let Some(object_name) = names.get(&object.identifier) { + names.insert(lvalue_id, format!("{}.{}", object_name, property)); + } + } + InstructionValue::FunctionExpression { name, lowered_func, .. } => { + let inner_func = &env.functions[lowered_func.func.0 as usize]; + let inner = name_anonymous_functions_impl(inner_func, env); + let node = Node { + function_id: lowered_func.func, + generated_name: None, + fn_name: name.clone(), + existing_name_hint: inner_func.name_hint.clone(), + inner, + }; + let idx = nodes.len(); + nodes.push(node); + if name.is_none() { + // Only generate names for anonymous functions + functions.insert(lvalue_id, idx); + } + } + InstructionValue::StoreContext { lvalue: store_lvalue, value, .. } + | InstructionValue::StoreLocal { lvalue: store_lvalue, value, .. } => { + if let Some(&node_idx) = functions.get(&value.identifier) { + let node = &mut nodes[node_idx]; + let var_ident = &env.identifiers[store_lvalue.place.identifier.0 as usize]; + if node.generated_name.is_none() { + if let Some(IdentifierName::Named(ref var_name)) = var_ident.name { + node.generated_name = Some(var_name.clone()); + functions.remove(&value.identifier); + } + } + } + } + InstructionValue::CallExpression { callee, args, .. } => { + handle_call( + env, + func, + callee.identifier, + args, + &mut functions, + &names, + &mut nodes, + ); + } + InstructionValue::MethodCall { property, args, .. } => { + handle_call( + env, + func, + property.identifier, + args, + &mut functions, + &names, + &mut nodes, + ); + } + InstructionValue::JsxExpression { tag, props, .. } => { + for attr in props { + match attr { + JsxAttribute::SpreadAttribute { .. } => continue, + JsxAttribute::Attribute { name: attr_name, place } => { + if let Some(&node_idx) = functions.get(&place.identifier) { + let node = &mut nodes[node_idx]; + if node.generated_name.is_none() { + let element_name = match tag { + JsxTag::Builtin(builtin) => Some(builtin.name.clone()), + JsxTag::Place(tag_place) => { + names.get(&tag_place.identifier).cloned() + } + }; + let prop_name = match element_name { + None => attr_name.clone(), + Some(ref el_name) => { + format!("<{}>.{}", el_name, attr_name) + } + }; + node.generated_name = Some(prop_name); + functions.remove(&place.identifier); + } + } + } + } + } + } + _ => {} + } + } + } + + nodes +} + +/// Handle CallExpression / MethodCall to generate names for function arguments. +fn handle_call( + env: &Environment, + _func: &HirFunction, + callee_id: IdentifierId, + args: &[PlaceOrSpread], + functions: &mut FxHashMap, + names: &FxHashMap, + nodes: &mut Vec, +) { + let callee_ident = &env.identifiers[callee_id.0 as usize]; + let callee_ty = &env.types[callee_ident.type_.0 as usize]; + let hook_kind = env.get_hook_kind_for_type(callee_ty).ok().flatten(); + + let callee_name: String = if let Some(hk) = hook_kind { + if *hk != HookKind::Custom { + hk.to_string() + } else { + names.get(&callee_id).cloned().unwrap_or_else(|| "(anonymous)".to_string()) + } + } else { + names.get(&callee_id).cloned().unwrap_or_else(|| "(anonymous)".to_string()) + }; + + // Count how many args are tracked functions + let fn_arg_count = args + .iter() + .filter(|arg| { + if let PlaceOrSpread::Place(p) = arg { + functions.contains_key(&p.identifier) + } else { + false + } + }) + .count(); + + for (i, arg) in args.iter().enumerate() { + let place = match arg { + PlaceOrSpread::Spread(_) => continue, + PlaceOrSpread::Place(p) => p, + }; + if let Some(&node_idx) = functions.get(&place.identifier) { + let node = &mut nodes[node_idx]; + if node.generated_name.is_none() { + let generated_name = if fn_arg_count > 1 { + format!("{}(arg{})", callee_name, i) + } else { + format!("{}()", callee_name) + }; + node.generated_name = Some(generated_name); + functions.remove(&place.identifier); + } + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/optimize_for_ssr.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/optimize_for_ssr.rs new file mode 100644 index 0000000000000..2dcc289095ca9 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/optimize_for_ssr.rs @@ -0,0 +1,329 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Optimizes the code for running in an SSR environment. +//! +//! Assumes that setState will not be called during render during initial mount, +//! which allows inlining useState/useReducer. +//! +//! Optimizations: +//! - Inline useState/useReducer +//! - Remove effects (useEffect, useLayoutEffect, useInsertionEffect) +//! - Remove event handlers (functions that call setState or startTransition) +//! - Remove known event handler props and ref props from builtin JSX tags +//! - Inline useEffectEvent to its argument +//! +//! Ported from TypeScript `src/Optimization/OptimizeForSSR.ts`. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::object_shape::HookKind; +use crate::react_compiler_hir::visitors::{each_instruction_value_operand, each_terminal_operand}; +use crate::react_compiler_hir::{ + ArrayPatternElement, HirFunction, IdentifierId, InstructionValue, PlaceOrSpread, + PrimitiveValue, is_set_state_type, is_start_transition_type, +}; + +/// Optimizes a function for SSR by inlining state hooks, removing effects, +/// removing event handlers, and stripping known event handler / ref JSX props. +/// +/// Corresponds to TS `optimizeForSSR(fn: HIRFunction): void`. +pub fn optimize_for_ssr(func: &mut HirFunction, env: &Environment) { + // Phase 1: Identify useState/useReducer calls that can be safely inlined. + // + // For useState(initialValue) where initialValue is primitive/object/array, + // store a LoadLocal of the initial value. + // + // For useReducer(reducer, initialArg) store a LoadLocal of initialArg. + // For useReducer(reducer, initialArg, init) store a CallExpression of init(initialArg). + // + // Any use of the hook return other than the expected destructuring pattern + // prevents inlining (we delete from inlined_state if we see the identifier used + // as an operand elsewhere). + let mut inlined_state: FxHashMap = FxHashMap::default(); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::Destructure { value, lvalue, .. } => { + if inlined_state.contains_key(&env.identifiers[value.identifier.0 as usize].id) + { + if let crate::react_compiler_hir::Pattern::Array(arr) = &lvalue.pattern { + if !arr.items.is_empty() { + if let ArrayPatternElement::Place(_) = &arr.items[0] { + // Allow destructuring of inlined states + continue; + } + } + } + } + } + InstructionValue::MethodCall { property, args, .. } + | InstructionValue::CallExpression { callee: property, args, .. } => { + // Determine callee based on instruction kind + let callee_id = property.identifier; + let hook_kind = get_hook_kind(env, callee_id); + match hook_kind { + Some(HookKind::UseReducer) => { + if args.len() == 2 { + if let (PlaceOrSpread::Place(_), PlaceOrSpread::Place(arg)) = + (&args[0], &args[1]) + { + let lvalue_id = + env.identifiers[instr.lvalue.identifier.0 as usize].id; + inlined_state.insert( + lvalue_id, + InlinedStateReplacement::LoadLocal { + place: arg.clone(), + loc: arg.loc, + }, + ); + } + } else if args.len() == 3 { + if let ( + PlaceOrSpread::Place(_), + PlaceOrSpread::Place(arg), + PlaceOrSpread::Place(initializer), + ) = (&args[0], &args[1], &args[2]) + { + let lvalue_id = + env.identifiers[instr.lvalue.identifier.0 as usize].id; + let call_loc = instr.value.loc().copied(); + inlined_state.insert( + lvalue_id, + InlinedStateReplacement::CallExpression { + callee: initializer.clone(), + arg: arg.clone(), + loc: call_loc, + }, + ); + } + } + } + Some(HookKind::UseState) => { + if args.len() == 1 { + if let PlaceOrSpread::Place(arg) = &args[0] { + let arg_type = &env.types[env.identifiers + [arg.identifier.0 as usize] + .type_ + .0 + as usize]; + if crate::react_compiler_hir::is_primitive_type(arg_type) + || crate::react_compiler_hir::is_plain_object_type(arg_type) + || crate::react_compiler_hir::is_array_type(arg_type) + { + let lvalue_id = + env.identifiers[instr.lvalue.identifier.0 as usize].id; + inlined_state.insert( + lvalue_id, + InlinedStateReplacement::LoadLocal { + place: arg.clone(), + loc: arg.loc, + }, + ); + } + } + } + } + _ => {} + } + } + _ => {} + } + + // Any use of useState/useReducer return besides destructuring prevents inlining + if !inlined_state.is_empty() { + let operands = each_instruction_value_operand(&instr.value, env); + for operand in &operands { + let id = env.identifiers[operand.identifier.0 as usize].id; + inlined_state.remove(&id); + } + } + } + if !inlined_state.is_empty() { + let operands = each_terminal_operand(&block.terminal); + for operand in &operands { + let id = env.identifiers[operand.identifier.0 as usize].id; + inlined_state.remove(&id); + } + } + } + + // Phase 2: Apply transformations + // + // - Replace FunctionExpression with Primitive(undefined) if it calls setState/startTransition + // - Remove known event handler props and ref props from builtin JSX tags + // - Replace Destructure of inlined state with StoreLocal + // - Replace useEffectEvent(fn) with LoadLocal(fn) + // - Replace useEffect/useLayoutEffect/useInsertionEffect with Primitive(undefined) + // - Replace useState/useReducer with their inlined replacement + for (_block_id, block) in &mut func.body.blocks { + for &instr_id in &block.instructions { + let instr = &mut func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, loc, .. } => { + let inner_func = &env.functions[lowered_func.func.0 as usize]; + if has_known_non_render_call(inner_func, env) { + let loc = *loc; + instr.value = + InstructionValue::Primitive { value: PrimitiveValue::Undefined, loc }; + } + } + InstructionValue::JsxExpression { tag, .. } => { + if let crate::react_compiler_hir::JsxTag::Builtin(builtin) = tag { + // Only optimize non-custom-element builtin tags + if !builtin.name.contains('-') { + let tag_name = builtin.name.clone(); + // Retain only props that are not known event handlers and not "ref" + if let InstructionValue::JsxExpression { props, .. } = &mut instr.value + { + props.retain(|prop| match prop { + crate::react_compiler_hir::JsxAttribute::SpreadAttribute { + .. + } => true, + crate::react_compiler_hir::JsxAttribute::Attribute { + name, + .. + } => !is_known_event_handler(&tag_name, name) && name != "ref", + }); + } + } + } + } + InstructionValue::Destructure { value, lvalue, loc } => { + let value_id = env.identifiers[value.identifier.0 as usize].id; + if inlined_state.contains_key(&value_id) { + // Invariant: destructuring pattern must be ArrayPattern with at least one Identifier item + if let crate::react_compiler_hir::Pattern::Array(arr) = &lvalue.pattern { + if !arr.items.is_empty() { + if let ArrayPatternElement::Place(first_place) = &arr.items[0] { + let loc = *loc; + let kind = lvalue.kind; + let store = InstructionValue::StoreLocal { + lvalue: crate::react_compiler_hir::LValue { + place: first_place.clone(), + kind, + }, + value: value.clone(), + type_annotation: None, + loc, + }; + instr.value = store; + } + } + } + } + } + InstructionValue::MethodCall { property, args, loc, .. } + | InstructionValue::CallExpression { callee: property, args, loc, .. } => { + let callee_id = property.identifier; + let hook_kind = get_hook_kind(env, callee_id); + match hook_kind { + Some(HookKind::UseEffectEvent) => { + if args.len() == 1 { + if let PlaceOrSpread::Place(arg) = &args[0] { + let loc = *loc; + instr.value = + InstructionValue::LoadLocal { place: arg.clone(), loc }; + } + } + } + Some( + HookKind::UseEffect + | HookKind::UseLayoutEffect + | HookKind::UseInsertionEffect, + ) => { + let loc = *loc; + instr.value = InstructionValue::Primitive { + value: PrimitiveValue::Undefined, + loc, + }; + } + Some(HookKind::UseReducer | HookKind::UseState) => { + let lvalue_id = env.identifiers[instr.lvalue.identifier.0 as usize].id; + if let Some(replacement) = inlined_state.get(&lvalue_id) { + instr.value = match replacement { + InlinedStateReplacement::LoadLocal { place, loc } => { + InstructionValue::LoadLocal { + place: place.clone(), + loc: *loc, + } + } + InlinedStateReplacement::CallExpression { + callee, + arg, + loc, + } => InstructionValue::CallExpression { + callee: callee.clone(), + args: vec![PlaceOrSpread::Place(arg.clone())], + loc: *loc, + }, + }; + } + } + _ => {} + } + } + _ => {} + } + } + } +} + +/// Replacement values for inlined useState/useReducer calls. +#[derive(Debug, Clone)] +enum InlinedStateReplacement { + /// Replace with `LoadLocal { place }` — used for useState and useReducer(reducer, initialArg) + LoadLocal { + place: crate::react_compiler_hir::Place, + loc: Option, + }, + /// Replace with `CallExpression { callee, args: [arg] }` — used for useReducer(reducer, initialArg, init) + CallExpression { + callee: crate::react_compiler_hir::Place, + arg: crate::react_compiler_hir::Place, + loc: Option, + }, +} + +/// Returns true if the function body contains a call to setState or startTransition. +/// This identifies functions that are event handlers and can be replaced with undefined +/// during SSR. +/// +/// Corresponds to TS `hasKnownNonRenderCall(fn: HIRFunction): boolean`. +fn has_known_non_render_call(func: &HirFunction, env: &Environment) -> bool { + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + if let InstructionValue::CallExpression { callee, .. } = &instr.value { + let callee_type = + &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if is_set_state_type(callee_type) || is_start_transition_type(callee_type) { + return true; + } + } + } + } + false +} + +/// Returns true if the prop name matches the known event handler pattern `on[A-Z]`. +fn is_known_event_handler(_tag: &str, prop: &str) -> bool { + if prop.len() < 3 { + return false; + } + if !prop.starts_with("on") { + return false; + } + let third_char = prop.as_bytes()[2]; + third_char.is_ascii_uppercase() +} + +/// Get the hook kind for an identifier, if its type represents a hook. +fn get_hook_kind(env: &Environment, identifier_id: IdentifierId) -> Option { + env.get_hook_kind_for_id(identifier_id).ok().flatten().cloned() +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/optimize_props_method_calls.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/optimize_props_method_calls.rs new file mode 100644 index 0000000000000..b81894010c5ae --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/optimize_props_method_calls.rs @@ -0,0 +1,48 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Converts `MethodCall` instructions on props objects into `CallExpression` +//! instructions. +//! +//! When the receiver of a method call is typed as the component's props object, +//! we can safely convert the method call `props.foo(args)` into a direct call +//! `foo(args)` using the property as the callee. This simplifies downstream +//! analysis by removing the receiver dependency. +//! +//! Analogous to TS `Optimization/OptimizePropsMethodCalls.ts`. + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{HirFunction, InstructionValue, is_props_type}; + +pub fn optimize_props_method_calls(func: &mut HirFunction, env: &Environment) { + for (_block_id, block) in &func.body.blocks { + let instruction_ids: Vec<_> = block.instructions.clone(); + for instr_id in instruction_ids { + let instr = &mut func.instructions[instr_id.0 as usize]; + let should_replace = matches!( + &instr.value, + InstructionValue::MethodCall { receiver, .. } + if { + let identifier = &env.identifiers[receiver.identifier.0 as usize]; + let ty = &env.types[identifier.type_.0 as usize]; + is_props_type(ty) + } + ); + if should_replace { + // Take the old value out, replacing with a temporary. + // The if-let is guaranteed to match since we checked above. + let old = + std::mem::replace(&mut instr.value, InstructionValue::Debugger { loc: None }); + match old { + InstructionValue::MethodCall { property, args, loc, .. } => { + instr.value = + InstructionValue::CallExpression { callee: property, args, loc }; + } + _ => unreachable!(), + } + } + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/outline_functions.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/outline_functions.rs new file mode 100644 index 0000000000000..596d8b3e19f55 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/outline_functions.rs @@ -0,0 +1,120 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Port of OutlineFunctions from TypeScript (`Optimization/OutlineFunctions.ts`). +//! +//! Extracts anonymous function expressions that do not close over any local +//! variables into top-level outlined functions. The original instruction is +//! replaced with a `LoadGlobal` referencing the outlined function's generated name. +//! +//! Conditional on `env.config.enable_function_outlining`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + FunctionId, HirFunction, IdentifierId, InstructionValue, NonLocalBinding, +}; +use crate::react_compiler_ssa::enter_ssa::placeholder_function; + +/// Outline anonymous function expressions that have no captured context variables. +/// +/// Ported from TS `outlineFunctions` in `Optimization/OutlineFunctions.ts`. +pub fn outline_functions( + func: &mut HirFunction, + env: &mut Environment, + fbt_operands: &FxHashSet, +) { + // Collect per-instruction actions to maintain depth-first name allocation order. + // Each entry: (instr index, function_id to recurse into, should_outline) + enum Action { + /// Recurse into an inner function (FunctionExpression or ObjectMethod) + Recurse(FunctionId), + /// Recurse then outline a FunctionExpression + RecurseAndOutline { instr_idx: usize, function_id: FunctionId }, + } + + let mut actions: Vec = Vec::new(); + + for block in func.body.blocks.values() { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } => { + let inner_func = &env.functions[lowered_func.func.0 as usize]; + + // Check outlining conditions (TS only checks func.id === null, not name): + // 1. No captured context variables + // 2. Anonymous (no explicit id on the inner function) + // 3. Not an fbt operand + if inner_func.context.is_empty() + && inner_func.id.is_none() + && !fbt_operands.contains(&lvalue_id) + { + actions.push(Action::RecurseAndOutline { + instr_idx: instr_id.0 as usize, + function_id: lowered_func.func, + }); + } else { + actions.push(Action::Recurse(lowered_func.func)); + } + } + InstructionValue::ObjectMethod { lowered_func, .. } => { + // Recurse into object methods (but don't outline them) + actions.push(Action::Recurse(lowered_func.func)); + } + _ => {} + } + } + } + + // Process actions sequentially: for each instruction, recurse first (depth-first), + // then generate name and outline. This matches TS ordering where inner functions + // get names allocated before outer ones. + for action in actions { + match action { + Action::Recurse(function_id) => { + let mut inner_func = std::mem::replace( + &mut env.functions[function_id.0 as usize], + placeholder_function(), + ); + outline_functions(&mut inner_func, env, fbt_operands); + env.functions[function_id.0 as usize] = inner_func; + } + Action::RecurseAndOutline { instr_idx, function_id } => { + // First recurse into the inner function (depth-first) + let mut inner_func = std::mem::replace( + &mut env.functions[function_id.0 as usize], + placeholder_function(), + ); + outline_functions(&mut inner_func, env, fbt_operands); + env.functions[function_id.0 as usize] = inner_func; + + // Then generate the name and outline (after recursion, matching TS order) + let hint: Option = env.functions[function_id.0 as usize] + .id + .clone() + .or_else(|| env.functions[function_id.0 as usize].name_hint.clone()); + let generated_name = env.generate_globally_unique_identifier_name(hint.as_deref()); + + // Set the id on the inner function + env.functions[function_id.0 as usize].id = Some(generated_name.clone()); + + // Outline the function + let outlined_func = env.functions[function_id.0 as usize].clone(); + env.outline_function(outlined_func, None); + + // Replace the instruction value with LoadGlobal + let loc = func.instructions[instr_idx].value.loc().cloned(); + func.instructions[instr_idx].value = InstructionValue::LoadGlobal { + binding: NonLocalBinding::Global { name: generated_name }, + loc, + }; + } + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/outline_jsx.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/outline_jsx.rs new file mode 100644 index 0000000000000..7cab55f0de911 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/outline_jsx.rs @@ -0,0 +1,640 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Port of OutlineJsx from TypeScript. +//! +//! Outlines JSX expressions in callbacks into separate component functions. +//! This pass is conditional on `env.config.enable_jsx_outlining` (defaults to false). + +use crate::react_compiler_utils::FxIndexSet; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + BasicBlock, BlockId, BlockKind, EvaluationOrder, FunctionId, HIR, HirFunction, IdentifierId, + IdentifierName, Instruction, InstructionId, InstructionKind, InstructionValue, JsxAttribute, + JsxTag, LValuePattern, NonLocalBinding, ObjectPattern, ObjectProperty, ObjectPropertyKey, + ObjectPropertyOrSpread, ObjectPropertyType, ParamPattern, Pattern, Place, ReactFunctionType, + ReturnVariant, Terminal, +}; +use crate::react_compiler_utils::FxIndexMap; + +/// Outline JSX expressions in inner functions into separate outlined components. +/// +/// Ported from TS `outlineJSX` in `Optimization/OutlineJsx.ts`. +pub fn outline_jsx<'a>(func: &mut HirFunction<'a>, env: &mut Environment<'a>) { + let mut outlined_fns: Vec> = Vec::new(); + outline_jsx_impl(func, env, &mut outlined_fns); + + for outlined_fn in outlined_fns { + env.outline_function(outlined_fn, Some(ReactFunctionType::Component)); + } +} + +/// Data about a JSX instruction for outlining +struct JsxInstrInfo { + instr_idx: usize, // index into func.instructions + #[allow(dead_code)] + instr_id: InstructionId, // the InstructionId + lvalue_id: IdentifierId, + eval_order: EvaluationOrder, +} + +struct OutlinedJsxAttribute { + original_name: String, + new_name: String, + place: Place, +} + +struct OutlinedResult<'a> { + instrs: Vec>, + func: HirFunction<'a>, +} + +fn outline_jsx_impl<'a>( + func: &mut HirFunction<'a>, + env: &mut Environment<'a>, + outlined_fns: &mut Vec>, +) { + // Collect LoadGlobal instructions (tag -> instr) + let mut globals: FxHashMap = FxHashMap::default(); // id -> instr_idx + + // Process each block + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + for block_id in &block_ids { + let block = &func.body.blocks[block_id]; + let instr_ids = block.instructions.clone(); + + let mut rewrite_instr: FxHashMap>> = + FxHashMap::default(); + let mut jsx_group: Vec = Vec::new(); + let mut children_ids: FxHashSet = FxHashSet::default(); + + // First pass: collect all instruction info without borrowing func mutably + enum InstrAction { + LoadGlobal { + lvalue_id: IdentifierId, + instr_idx: usize, + }, + FunctionExpr { + func_id: FunctionId, + }, + JsxExpr { + lvalue_id: IdentifierId, + instr_idx: usize, + eval_order: EvaluationOrder, + child_ids: Vec, + }, + Other, + } + + let mut actions: Vec = Vec::new(); + for i in (0..instr_ids.len()).rev() { + let iid = instr_ids[i]; + let instr = &func.instructions[iid.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + + match &instr.value { + InstructionValue::LoadGlobal { .. } => { + actions.push(InstrAction::LoadGlobal { lvalue_id, instr_idx: iid.0 as usize }); + } + InstructionValue::FunctionExpression { lowered_func, .. } => { + actions.push(InstrAction::FunctionExpr { func_id: lowered_func.func }); + } + InstructionValue::JsxExpression { children, .. } => { + let child_ids = children + .as_ref() + .map(|kids| kids.iter().map(|c| c.identifier).collect()) + .unwrap_or_default(); + actions.push(InstrAction::JsxExpr { + lvalue_id, + instr_idx: iid.0 as usize, + eval_order: instr.id, + child_ids, + }); + } + _ => { + actions.push(InstrAction::Other); + } + } + } + + // Second pass: process actions + for action in actions { + match action { + InstrAction::LoadGlobal { lvalue_id, instr_idx } => { + globals.insert(lvalue_id, instr_idx); + } + InstrAction::FunctionExpr { func_id } => { + let mut inner_func = std::mem::replace( + &mut env.functions[func_id.0 as usize], + crate::react_compiler_ssa::enter_ssa::placeholder_function(), + ); + outline_jsx_impl(&mut inner_func, env, outlined_fns); + env.functions[func_id.0 as usize] = inner_func; + } + InstrAction::JsxExpr { lvalue_id, instr_idx, eval_order, child_ids } => { + if !children_ids.contains(&lvalue_id) { + process_and_outline_jsx( + func, + env, + &mut jsx_group, + &globals, + &mut rewrite_instr, + outlined_fns, + ); + jsx_group.clear(); + children_ids.clear(); + } + jsx_group.push(JsxInstrInfo { + instr_idx, + instr_id: InstructionId(instr_idx as u32), + lvalue_id, + eval_order, + }); + for child_id in child_ids { + children_ids.insert(child_id); + } + } + InstrAction::Other => {} + } + } + // Process remaining JSX group after the loop + process_and_outline_jsx( + func, + env, + &mut jsx_group, + &globals, + &mut rewrite_instr, + outlined_fns, + ); + if !rewrite_instr.is_empty() { + let block = func.body.blocks.get_mut(block_id).unwrap(); + let old_instr_ids = block.instructions.clone(); + let mut new_instr_ids = Vec::new(); + for &iid in &old_instr_ids { + let eval_order = func.instructions[iid.0 as usize].id; + if let Some(replacement_instrs) = rewrite_instr.get(&eval_order) { + // Add replacement instructions to the instruction table and reference them + for new_instr in replacement_instrs { + let new_idx = func.instructions.len(); + func.instructions.push(new_instr.clone()); + new_instr_ids.push(InstructionId(new_idx as u32)); + } + } else { + new_instr_ids.push(iid); + } + } + let block = func.body.blocks.get_mut(block_id).unwrap(); + block.instructions = new_instr_ids; + + // Run dead code elimination after rewriting + super::dead_code_elimination(func, env); + } + } +} + +fn process_and_outline_jsx<'a>( + func: &mut HirFunction<'a>, + env: &mut Environment<'a>, + jsx_group: &mut Vec, + globals: &FxHashMap, + rewrite_instr: &mut FxHashMap>>, + outlined_fns: &mut Vec>, +) { + if jsx_group.len() <= 1 { + return; + } + // Sort by eval order ascending (TS: sort by a.id - b.id) + jsx_group.sort_by_key(|j| j.eval_order); + + let result = process_jsx_group(func, env, jsx_group, globals); + if let Some(result) = result { + outlined_fns.push(result.func); + // Map from the LAST JSX instruction's eval order to the replacement instructions + // In the TS code, `state.jsx.at(0)` is the first element pushed during reverse iteration, + // which is the last JSX in forward block order (highest eval order). + // After sorting by eval_order ascending, that's jsx_group.last(). + let last_eval_order = jsx_group.last().unwrap().eval_order; + rewrite_instr.insert(last_eval_order, result.instrs); + } +} + +fn process_jsx_group<'a>( + func: &HirFunction<'a>, + env: &mut Environment<'a>, + jsx_group: &[JsxInstrInfo], + globals: &FxHashMap, +) -> Option> { + // Only outline in callbacks, not top-level components + if func.fn_type == ReactFunctionType::Component { + return None; + } + + let props = collect_props(func, env, jsx_group)?; + + let outlined_tag = env.generate_globally_unique_identifier_name(None); + let new_instrs = emit_outlined_jsx(func, env, jsx_group, &props, &outlined_tag)?; + let outlined_fn = emit_outlined_fn(func, env, jsx_group, &props, globals)?; + + // Set the outlined function's id + let mut outlined_fn = outlined_fn; + outlined_fn.id = Some(outlined_tag); + + Some(OutlinedResult { instrs: new_instrs, func: outlined_fn }) +} + +fn collect_props<'a>( + func: &HirFunction<'a>, + env: &mut Environment<'a>, + jsx_group: &[JsxInstrInfo], +) -> Option> { + let mut id_counter = 1u32; + let mut seen: FxHashSet = FxHashSet::default(); + let mut attributes = Vec::new(); + let jsx_ids: FxHashSet = jsx_group.iter().map(|j| j.lvalue_id).collect(); + + let mut generate_name = |old_name: &str, _env: &mut Environment| -> String { + let mut new_name = old_name.to_string(); + while seen.contains(&new_name) { + new_name = format!("{}{}", old_name, id_counter); + id_counter += 1; + } + seen.insert(new_name.clone()); + // TS: env.programContext.addNewReference(newName) + // We don't have programContext in Rust, but this is needed for unique name tracking + new_name + }; + + for info in jsx_group { + let instr = &func.instructions[info.instr_idx]; + if let InstructionValue::JsxExpression { props, children, .. } = &instr.value { + for attr in props { + match attr { + JsxAttribute::SpreadAttribute { .. } => return None, + JsxAttribute::Attribute { name, place } => { + let new_name = generate_name(name, env); + attributes.push(OutlinedJsxAttribute { + original_name: name.clone(), + new_name, + place: place.clone(), + }); + } + } + } + + if let Some(kids) = children { + for child in kids { + if jsx_ids.contains(&child.identifier) { + continue; + } + // Promote the child's identifier to a named temporary + let child_id = child.identifier; + let decl_id = env.identifiers[child_id.0 as usize].declaration_id; + if env.identifiers[child_id.0 as usize].name.is_none() { + env.identifiers[child_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#t{}", decl_id.0))); + } + + let child_name = match &env.identifiers[child_id.0 as usize].name { + Some(IdentifierName::Named(n)) => n.clone(), + Some(IdentifierName::Promoted(n)) => n.clone(), + None => format!("#t{}", decl_id.0), + }; + let new_name = generate_name("t", env); + attributes.push(OutlinedJsxAttribute { + original_name: child_name, + new_name, + place: child.clone(), + }); + } + } + } + } + + Some(attributes) +} + +fn emit_outlined_jsx<'a>( + func: &HirFunction<'a>, + env: &mut Environment<'a>, + jsx_group: &[JsxInstrInfo], + outlined_props: &[OutlinedJsxAttribute], + outlined_tag: &str, +) -> Option>> { + let props: Vec = outlined_props + .iter() + .map(|p| JsxAttribute::Attribute { name: p.new_name.clone(), place: p.place.clone() }) + .collect(); + + // Create LoadGlobal for the outlined component + let load_id = env.next_identifier_id(); + // Promote it as a JSX tag temporary + let decl_id = env.identifiers[load_id.0 as usize].declaration_id; + env.identifiers[load_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#T{}", decl_id.0))); + + let load_place = Place { + identifier: load_id, + effect: crate::react_compiler_hir::Effect::Unknown, + reactive: false, + loc: None, + }; + + let load_jsx = Instruction { + id: EvaluationOrder(0), + lvalue: load_place.clone(), + value: InstructionValue::LoadGlobal { + binding: NonLocalBinding::ModuleLocal { name: outlined_tag.to_string() }, + loc: None, + }, + loc: None, + effects: None, + }; + + // Create the replacement JsxExpression using the last JSX instruction's lvalue + let last_info = jsx_group.last().unwrap(); + let last_instr = &func.instructions[last_info.instr_idx]; + let jsx_expr = Instruction { + id: EvaluationOrder(0), + lvalue: last_instr.lvalue.clone(), + value: InstructionValue::JsxExpression { + tag: JsxTag::Place(load_place), + props, + children: None, + loc: None, + opening_loc: None, + closing_loc: None, + }, + loc: None, + effects: None, + }; + + Some(vec![load_jsx, jsx_expr]) +} + +fn emit_outlined_fn<'a>( + func: &HirFunction<'a>, + env: &mut Environment<'a>, + jsx_group: &[JsxInstrInfo], + old_props: &[OutlinedJsxAttribute], + globals: &FxHashMap, +) -> Option> { + let old_to_new_props = create_old_to_new_props_mapping(env, old_props); + + // Create props parameter + let props_obj_id = env.next_identifier_id(); + let decl_id = env.identifiers[props_obj_id.0 as usize].declaration_id; + env.identifiers[props_obj_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#t{}", decl_id.0))); + let props_obj = Place { + identifier: props_obj_id, + effect: crate::react_compiler_hir::Effect::Unknown, + reactive: false, + loc: None, + }; + + // Create destructure instruction + let destructure_instr = emit_destructure_props(env, &props_obj, &old_to_new_props); + + // Emit load globals for JSX tags + let load_global_instrs = emit_load_globals(func, jsx_group, globals)?; + + // Emit updated JSX instructions + let updated_jsx_instrs = emit_updated_jsx(func, jsx_group, &old_to_new_props); + + // Build instructions list + let mut instructions = Vec::new(); + instructions.push(destructure_instr); + instructions.extend(load_global_instrs); + instructions.extend(updated_jsx_instrs); + + // Build instruction table and instruction IDs + let mut instr_table = Vec::new(); + let mut instr_ids = Vec::new(); + for instr in instructions { + let idx = instr_table.len(); + instr_table.push(instr); + instr_ids.push(InstructionId(idx as u32)); + } + + // Return terminal uses the last instruction's lvalue + let last_lvalue = instr_table.last().unwrap().lvalue.clone(); + + // Create return place + let returns_id = env.next_identifier_id(); + let returns_place = Place { + identifier: returns_id, + effect: crate::react_compiler_hir::Effect::Unknown, + reactive: false, + loc: None, + }; + + let block = BasicBlock { + kind: BlockKind::Block, + id: BlockId(0), + instructions: instr_ids, + preds: FxIndexSet::default(), + terminal: Terminal::Return { + value: last_lvalue, + return_variant: ReturnVariant::Explicit, + id: EvaluationOrder(0), + loc: None, + effects: None, + }, + phis: Vec::new(), + }; + + let mut blocks = FxIndexMap::default(); + blocks.insert(BlockId(0), block); + + let outlined_fn = HirFunction { + id: None, + name_hint: None, + fn_type: ReactFunctionType::Other, + params: vec![ParamPattern::Place(props_obj)], + return_type_annotation: None, + returns: returns_place, + context: Vec::new(), + body: HIR { entry: BlockId(0), blocks }, + instructions: instr_table, + generator: false, + is_async: false, + directives: Vec::new(), + aliasing_effects: Some(vec![]), + loc: None, + }; + + Some(outlined_fn) +} + +fn emit_load_globals<'a>( + func: &HirFunction<'a>, + jsx_group: &[JsxInstrInfo], + globals: &FxHashMap, +) -> Option>> { + let mut instructions = Vec::new(); + for info in jsx_group { + let instr = &func.instructions[info.instr_idx]; + if let InstructionValue::JsxExpression { tag, .. } = &instr.value { + if let JsxTag::Place(tag_place) = tag { + let global_instr_idx = globals.get(&tag_place.identifier)?; + instructions.push(func.instructions[*global_instr_idx].clone()); + } + } + } + Some(instructions) +} + +fn emit_updated_jsx<'a>( + func: &HirFunction<'a>, + jsx_group: &[JsxInstrInfo], + old_to_new_props: &FxIndexMap, +) -> Vec> { + let jsx_ids: FxHashSet = jsx_group.iter().map(|j| j.lvalue_id).collect(); + let mut new_instrs = Vec::new(); + + for info in jsx_group { + let instr = &func.instructions[info.instr_idx]; + if let InstructionValue::JsxExpression { + tag, + props, + children, + loc, + opening_loc, + closing_loc, + } = &instr.value + { + let mut new_props = Vec::new(); + for prop in props { + // TS: invariant(prop.kind === 'JsxAttribute', ...) + // Spread attributes would have caused collectProps to return null earlier + let (name, place) = match prop { + JsxAttribute::Attribute { name, place } => (name, place), + JsxAttribute::SpreadAttribute { .. } => { + unreachable!("Expected only JsxAttribute, not spread") + } + }; + if name == "key" { + continue; + } + // TS: invariant(newProp !== undefined, ...) + let new_prop = old_to_new_props + .get(&place.identifier) + .expect("Expected a new property for identifier"); + new_props.push(JsxAttribute::Attribute { + name: new_prop.original_name.clone(), + place: new_prop.place.clone(), + }); + } + + let new_children = children.as_ref().map(|kids| { + kids.iter() + .map(|child| { + if jsx_ids.contains(&child.identifier) { + child.clone() + } else { + // TS: invariant(newChild !== undefined, ...) + let new_prop = old_to_new_props + .get(&child.identifier) + .expect("Expected a new prop for child identifier"); + new_prop.place.clone() + } + }) + .collect() + }); + + new_instrs.push(Instruction { + id: instr.id, + lvalue: instr.lvalue.clone(), + value: InstructionValue::JsxExpression { + tag: tag.clone(), + props: new_props, + children: new_children, + loc: *loc, + opening_loc: *opening_loc, + closing_loc: *closing_loc, + }, + loc: instr.loc, + effects: instr.effects.clone(), + }); + } + } + + new_instrs +} + +fn create_old_to_new_props_mapping( + env: &mut Environment<'_>, + old_props: &[OutlinedJsxAttribute], +) -> FxIndexMap { + let mut old_to_new = FxIndexMap::default(); + + for old_prop in old_props { + if old_prop.original_name == "key" { + continue; + } + + let new_id = env.next_identifier_id(); + env.identifiers[new_id.0 as usize].name = + Some(IdentifierName::Named(old_prop.new_name.clone())); + + let new_place = Place { + identifier: new_id, + effect: crate::react_compiler_hir::Effect::Unknown, + reactive: false, + loc: None, + }; + + old_to_new.insert( + old_prop.place.identifier, + OutlinedJsxAttribute { + original_name: old_prop.original_name.clone(), + new_name: old_prop.new_name.clone(), + place: new_place, + }, + ); + } + + old_to_new +} + +fn emit_destructure_props<'a>( + env: &mut Environment<'a>, + props_obj: &Place, + old_to_new_props: &FxIndexMap, +) -> Instruction<'a> { + let mut properties = Vec::new(); + for prop in old_to_new_props.values() { + properties.push(ObjectPropertyOrSpread::Property(ObjectProperty { + key: ObjectPropertyKey::String { name: prop.new_name.clone() }, + property_type: ObjectPropertyType::Property, + place: prop.place.clone(), + })); + } + + let lvalue_id = env.next_identifier_id(); + let lvalue = Place { + identifier: lvalue_id, + effect: crate::react_compiler_hir::Effect::Unknown, + reactive: false, + loc: None, + }; + + Instruction { + id: EvaluationOrder(0), + lvalue, + value: InstructionValue::Destructure { + lvalue: LValuePattern { + pattern: Pattern::Object(ObjectPattern { properties, loc: None }), + kind: InstructionKind::Let, + }, + value: props_obj.clone(), + loc: None, + }, + loc: None, + effects: None, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/prune_maybe_throws.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/prune_maybe_throws.rs new file mode 100644 index 0000000000000..9c2adfac7e284 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/prune_maybe_throws.rs @@ -0,0 +1,124 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Prunes `MaybeThrow` terminals for blocks that can provably never throw. +//! +//! Currently very conservative: only affects blocks with primitives or +//! array/object literals. Even a variable reference could throw due to TDZ. +//! +//! Analogous to TS `Optimization/PruneMaybeThrows.ts`. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, GENERATED_SOURCE, +}; +use crate::react_compiler_hir::{BlockId, HirFunction, Instruction, InstructionValue, Terminal}; +use crate::react_compiler_lowering::{ + get_reverse_postordered_blocks, mark_instruction_ids, remove_dead_do_while_statements, + remove_unnecessary_try_catch, remove_unreachable_for_updates, +}; + +use crate::react_compiler_optimization::merge_consecutive_blocks::merge_consecutive_blocks; + +/// Prune `MaybeThrow` terminals for blocks that cannot throw, then clean up the CFG. +pub fn prune_maybe_throws( + func: &mut HirFunction, + functions: &mut [HirFunction], +) -> Result<(), CompilerDiagnostic> { + let terminal_mapping = prune_maybe_throws_impl(func); + if let Some(terminal_mapping) = terminal_mapping { + // If terminals have changed then blocks may have become newly unreachable. + // Re-run minification of the graph (incl reordering instruction ids). + func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions); + remove_unreachable_for_updates(&mut func.body); + remove_dead_do_while_statements(&mut func.body); + remove_unnecessary_try_catch(&mut func.body); + mark_instruction_ids(&mut func.body, &mut func.instructions); + merge_consecutive_blocks(func, functions); + + // Rewrite phi operands to reference the updated predecessor blocks + for block in func.body.blocks.values_mut() { + let preds = &block.preds; + let mut phi_updates: Vec<(usize, Vec<(BlockId, BlockId)>)> = Vec::new(); + + for (phi_idx, phi) in block.phis.iter().enumerate() { + let mut updates = Vec::new(); + for (predecessor, _) in &phi.operands { + if !preds.contains(predecessor) { + let mapped_terminal = + terminal_mapping.get(predecessor).copied().ok_or_else(|| { + CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Expected non-existing phi operand's predecessor to have been mapped to a new terminal", + Some(format!( + "Could not find mapping for predecessor bb{} in block bb{}", + predecessor.0, block.id.0, + )), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: GENERATED_SOURCE, + message: None, + identifier_name: None, + }) + })?; + updates.push((*predecessor, mapped_terminal)); + } + } + if !updates.is_empty() { + phi_updates.push((phi_idx, updates)); + } + } + + for (phi_idx, updates) in phi_updates { + for (old_pred, new_pred) in updates { + let operand = block.phis[phi_idx].operands.shift_remove(&old_pred).unwrap(); + block.phis[phi_idx].operands.insert(new_pred, operand); + } + } + } + } + Ok(()) +} + +fn prune_maybe_throws_impl(func: &mut HirFunction) -> Option> { + let mut terminal_mapping: FxHashMap = FxHashMap::default(); + let instructions = &func.instructions; + + for block in func.body.blocks.values_mut() { + let continuation = match &block.terminal { + Terminal::MaybeThrow { continuation, .. } => *continuation, + _ => continue, + }; + + let can_throw = block + .instructions + .iter() + .any(|instr_id| instruction_may_throw(&instructions[instr_id.0 as usize])); + + if !can_throw { + let source = terminal_mapping.get(&block.id).copied().unwrap_or(block.id); + terminal_mapping.insert(continuation, source); + // Null out the handler rather than replacing with Goto. + // Preserving the MaybeThrow makes the continuations clear for + // BuildReactiveFunction, while nulling out the handler tells us + // that control cannot flow to the handler. + if let Terminal::MaybeThrow { handler, .. } = &mut block.terminal { + *handler = None; + } + } + } + + if terminal_mapping.is_empty() { None } else { Some(terminal_mapping) } +} + +fn instruction_may_throw(instr: &Instruction) -> bool { + match &instr.value { + InstructionValue::Primitive { .. } + | InstructionValue::ArrayExpression { .. } + | InstructionValue::ObjectExpression { .. } => false, + _ => true, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_optimization/prune_unused_labels_hir.rs b/crates/oxc_react_compiler/src/react_compiler_optimization/prune_unused_labels_hir.rs new file mode 100644 index 0000000000000..afa314b1da320 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_optimization/prune_unused_labels_hir.rs @@ -0,0 +1,94 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Removes unused labels from the HIR. +//! +//! A label terminal whose body block immediately breaks to the label's +//! fallthrough (with no other predecessors) is effectively a no-op label. +//! This pass merges such label/body/fallthrough triples into a single block. +//! +//! Analogous to TS `PruneUnusedLabelsHIR.ts`. + +use crate::react_compiler_hir::{BlockId, BlockKind, GotoVariant, HirFunction, Terminal}; +use rustc_hash::FxHashMap; + +pub fn prune_unused_labels_hir(func: &mut HirFunction) { + // Phase 1: Identify label terminals whose body block immediately breaks + // to the fallthrough, and both body and fallthrough are normal blocks. + let mut merged: Vec<(BlockId, BlockId, BlockId)> = Vec::new(); // (label, next, fallthrough) + + for (&block_id, block) in &func.body.blocks { + if let Terminal::Label { block: next_id, fallthrough: fallthrough_id, .. } = &block.terminal + { + let next = &func.body.blocks[next_id]; + let fallthrough = &func.body.blocks[fallthrough_id]; + if let Terminal::Goto { block: goto_target, variant: GotoVariant::Break, .. } = + &next.terminal + { + if goto_target == fallthrough_id + && next.kind == BlockKind::Block + && fallthrough.kind == BlockKind::Block + { + merged.push((block_id, *next_id, *fallthrough_id)); + } + } + } + } + + // Phase 2: Apply merges + let mut rewrites: FxHashMap = FxHashMap::default(); + + for (original_label_id, next_id, fallthrough_id) in &merged { + let label_id = rewrites.get(original_label_id).copied().unwrap_or(*original_label_id); + + // Validate: no phis in next or fallthrough + let next_phis_empty = func.body.blocks[next_id].phis.is_empty(); + let fallthrough_phis_empty = func.body.blocks[fallthrough_id].phis.is_empty(); + assert!( + next_phis_empty && fallthrough_phis_empty, + "Unexpected phis when merging label blocks" + ); + + // Validate: single predecessors + let next_preds_ok = func.body.blocks[next_id].preds.len() == 1 + && func.body.blocks[next_id].preds.contains(original_label_id); + let fallthrough_preds_ok = func.body.blocks[fallthrough_id].preds.len() == 1 + && func.body.blocks[fallthrough_id].preds.contains(next_id); + assert!( + next_preds_ok && fallthrough_preds_ok, + "Unexpected block predecessors when merging label blocks" + ); + + // Collect instructions from next and fallthrough + let next_instructions = func.body.blocks[next_id].instructions.clone(); + let fallthrough_instructions = func.body.blocks[fallthrough_id].instructions.clone(); + let fallthrough_terminal = func.body.blocks[fallthrough_id].terminal.clone(); + + // Merge into the label block + let label_block = func.body.blocks.get_mut(&label_id).unwrap(); + label_block.instructions.extend(next_instructions); + label_block.instructions.extend(fallthrough_instructions); + label_block.terminal = fallthrough_terminal; + + // Remove merged blocks + func.body.blocks.shift_remove(next_id); + func.body.blocks.shift_remove(fallthrough_id); + + rewrites.insert(*fallthrough_id, label_id); + } + + // Phase 3: Rewrite predecessor sets + for block in func.body.blocks.values_mut() { + let preds_to_rewrite: Vec<(BlockId, BlockId)> = block + .preds + .iter() + .filter_map(|pred| rewrites.get(pred).map(|rewritten| (*pred, *rewritten))) + .collect(); + for (old, new) in preds_to_rewrite { + block.preds.shift_remove(&old); + block.preds.insert(new); + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/assert_scope_instructions_within_scopes.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/assert_scope_instructions_within_scopes.rs new file mode 100644 index 0000000000000..1394f88f7b13a --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/assert_scope_instructions_within_scopes.rs @@ -0,0 +1,119 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Assert that all instructions involved in creating values for a given scope +//! are within the corresponding ReactiveScopeBlock. +//! +//! Corresponds to `src/ReactiveScopes/AssertScopeInstructionsWithinScope.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + EvaluationOrder, Place, ReactiveFunction, ReactiveScopeBlock, ScopeId, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionVisitor, visit_reactive_function, +}; + +/// Assert that scope instructions are within their scopes. +/// Two-pass visitor: +/// 1. Collect all scope IDs +/// 2. Check that places referencing those scopes are within active scope blocks +pub fn assert_scope_instructions_within_scopes<'a>( + func: &ReactiveFunction<'a>, + env: &Environment<'a>, +) -> Result<(), CompilerDiagnostic> { + // Pass 1: Collect all scope IDs + let mut existing_scopes: FxHashSet = FxHashSet::default(); + let find_visitor = FindAllScopesVisitor { env }; + visit_reactive_function(func, &find_visitor, &mut existing_scopes); + + // Pass 2: Check instructions against scopes + let check_visitor = CheckInstructionsAgainstScopesVisitor { env }; + let mut check_state = + CheckState { existing_scopes, active_scopes: FxHashSet::default(), error: None }; + visit_reactive_function(func, &check_visitor, &mut check_state); + if let Some(err) = check_state.error { + return Err(err); + } + Ok(()) +} + +// ============================================================================= +// Pass 1: Find all scopes +// ============================================================================= + +struct FindAllScopesVisitor<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for FindAllScopesVisitor<'a, 'e> { + type State = FxHashSet; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_scope(&self, scope: &ReactiveScopeBlock<'a>, state: &mut FxHashSet) { + self.traverse_scope(scope, state); + state.insert(scope.scope); + } +} + +// ============================================================================= +// Pass 2: Check instructions against scopes +// ============================================================================= + +struct CheckState { + existing_scopes: FxHashSet, + active_scopes: FxHashSet, + error: Option, +} + +struct CheckInstructionsAgainstScopesVisitor<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for CheckInstructionsAgainstScopesVisitor<'a, 'e> { + type State = CheckState; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_place(&self, id: EvaluationOrder, place: &Place, state: &mut CheckState) { + // getPlaceScope: check if the place's identifier has a scope that is active at this id + let identifier = &self.env.identifiers[place.identifier.0 as usize]; + if let Some(scope_id) = identifier.scope { + let scope = &self.env.scopes[scope_id.0 as usize]; + // isScopeActive: id >= scope.range.start && id < scope.range.end + let is_active_at_id = id >= scope.range.start && id < scope.range.end; + if is_active_at_id + && state.existing_scopes.contains(&scope_id) + && !state.active_scopes.contains(&scope_id) + { + state.error = Some(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Encountered an instruction that should be part of a scope, \ + but where that scope has already completed", + Some(format!( + "Instruction [{:?}] is part of scope @{:?}, \ + but that scope has already completed", + id, scope_id + )), + )); + } + } + } + + fn visit_scope(&self, scope: &ReactiveScopeBlock<'a>, state: &mut CheckState) { + state.active_scopes.insert(scope.scope); + self.traverse_scope(scope, state); + state.active_scopes.remove(&scope.scope); + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/assert_well_formed_break_targets.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/assert_well_formed_break_targets.rs new file mode 100644 index 0000000000000..5a56c30c6f2cc --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/assert_well_formed_break_targets.rs @@ -0,0 +1,63 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Assert that all break/continue targets reference existent labels. +//! +//! Corresponds to `src/ReactiveScopes/AssertWellFormedBreakTargets.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::{ + BlockId, ReactiveFunction, ReactiveTerminal, ReactiveTerminalStatement, + environment::Environment, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionVisitor, visit_reactive_function, +}; + +/// Assert that all break/continue targets reference existent labels. +pub fn assert_well_formed_break_targets<'a>(func: &ReactiveFunction<'a>, env: &Environment<'a>) { + let visitor = Visitor { env }; + let mut state: FxHashSet = FxHashSet::default(); + visit_reactive_function(func, &visitor, &mut state); +} + +struct Visitor<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for Visitor<'a, 'e> { + type State = FxHashSet; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_terminal( + &self, + stmt: &ReactiveTerminalStatement<'a>, + seen_labels: &mut FxHashSet, + ) { + if let Some(label) = &stmt.label { + seen_labels.insert(label.id); + } + let terminal = &stmt.terminal; + match terminal { + ReactiveTerminal::Break { target, .. } | ReactiveTerminal::Continue { target, .. } => { + assert!( + seen_labels.contains(target), + "Unexpected break/continue to invalid label: {:?}", + target + ); + } + _ => {} + } + // Note: intentionally NOT calling self.traverse_terminal() here, + // matching TS behavior where visitTerminal override does not call traverseTerminal. + // Recursion into child blocks happens via traverseBlock→visitTerminal for nested blocks. + // The TS visitor only checks break/continue at the block level, not terminal child blocks. + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/build_reactive_function.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/build_reactive_function.rs new file mode 100644 index 0000000000000..f23fec572676e --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/build_reactive_function.rs @@ -0,0 +1,1472 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Converts the HIR CFG into a tree-structured ReactiveFunction. +//! +//! Corresponds to `src/ReactiveScopes/BuildReactiveFunction.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + BasicBlock, BlockId, EvaluationOrder, GotoVariant, HirFunction, InstructionValue, Place, + PrunedReactiveScopeBlock, ReactiveBlock, ReactiveFunction, ReactiveInstruction, ReactiveLabel, + ReactiveScopeBlock, ReactiveStatement, ReactiveSwitchCase, ReactiveTerminal, + ReactiveTerminalStatement, ReactiveTerminalTargetKind, ReactiveValue, Terminal, +}; + +/// Convert the HIR CFG into a tree-structured ReactiveFunction. +pub fn build_reactive_function<'a>( + hir: &HirFunction<'a>, + env: &Environment<'a>, +) -> Result, CompilerDiagnostic> { + let mut ctx = Context::new(hir); + let mut driver = Driver { cx: &mut ctx, hir, env }; + + let entry_block_id = hir.body.entry; + let mut body = Vec::new(); + driver.visit_block(entry_block_id, &mut body)?; + + Ok(ReactiveFunction { + loc: hir.loc, + id: hir.id.clone(), + name_hint: hir.name_hint.clone(), + params: hir.params.clone(), + generator: hir.generator, + is_async: hir.is_async, + body, + directives: hir.directives.clone(), + }) +} + +// ============================================================================= +// ControlFlowTarget +// ============================================================================= + +#[derive(Debug)] +enum ControlFlowTarget { + If { + block: BlockId, + id: u32, + }, + Switch { + block: BlockId, + id: u32, + }, + Case { + block: BlockId, + id: u32, + }, + Loop { + block: BlockId, + #[allow(dead_code)] + owns_block: bool, + continue_block: BlockId, + loop_block: Option, + owns_loop: bool, + id: u32, + }, +} + +impl ControlFlowTarget { + fn block(&self) -> BlockId { + match self { + ControlFlowTarget::If { block, .. } + | ControlFlowTarget::Switch { block, .. } + | ControlFlowTarget::Case { block, .. } + | ControlFlowTarget::Loop { block, .. } => *block, + } + } + + fn id(&self) -> u32 { + match self { + ControlFlowTarget::If { id, .. } + | ControlFlowTarget::Switch { id, .. } + | ControlFlowTarget::Case { id, .. } + | ControlFlowTarget::Loop { id, .. } => *id, + } + } + + fn is_loop(&self) -> bool { + matches!(self, ControlFlowTarget::Loop { .. }) + } +} + +// ============================================================================= +// Context +// ============================================================================= + +struct Context<'a, 'h> { + ir: &'h HirFunction<'a>, + next_schedule_id: u32, + emitted: FxHashSet, + scope_fallthroughs: FxHashSet, + scheduled: FxHashSet, + catch_handlers: FxHashSet, + control_flow_stack: Vec, +} + +impl<'a, 'h> Context<'a, 'h> { + fn new(ir: &'h HirFunction<'a>) -> Self { + Self { + ir, + next_schedule_id: 0, + emitted: FxHashSet::default(), + scope_fallthroughs: FxHashSet::default(), + scheduled: FxHashSet::default(), + catch_handlers: FxHashSet::default(), + control_flow_stack: Vec::new(), + } + } + + fn block(&self, id: BlockId) -> &BasicBlock { + &self.ir.body.blocks[&id] + } + + fn schedule_catch_handler(&mut self, block: BlockId) { + self.catch_handlers.insert(block); + } + + fn reachable(&self, id: BlockId) -> bool { + let block = self.block(id); + !matches!(block.terminal, Terminal::Unreachable { .. }) + } + + fn schedule(&mut self, block: BlockId, target_type: &str) -> Result { + let id = self.next_schedule_id; + self.next_schedule_id += 1; + if self.scheduled.contains(&block) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("Break block is already scheduled: bb{}", block.0), + None, + )); + } + self.scheduled.insert(block); + let target = match target_type { + "if" => ControlFlowTarget::If { block, id }, + "switch" => ControlFlowTarget::Switch { block, id }, + "case" => ControlFlowTarget::Case { block, id }, + _ => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("Unknown target type: {}", target_type), + None, + )); + } + }; + self.control_flow_stack.push(target); + Ok(id) + } + + fn schedule_loop( + &mut self, + fallthrough_block: BlockId, + continue_block: BlockId, + loop_block: Option, + ) -> Result { + let id = self.next_schedule_id; + self.next_schedule_id += 1; + let owns_block = !self.scheduled.contains(&fallthrough_block); + self.scheduled.insert(fallthrough_block); + if self.scheduled.contains(&continue_block) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("Continue block is already scheduled: bb{}", continue_block.0), + None, + )); + } + self.scheduled.insert(continue_block); + let mut owns_loop = false; + if let Some(lb) = loop_block { + owns_loop = !self.scheduled.contains(&lb); + self.scheduled.insert(lb); + } + + self.control_flow_stack.push(ControlFlowTarget::Loop { + block: fallthrough_block, + owns_block, + continue_block, + loop_block, + owns_loop, + id, + }); + Ok(id) + } + + fn unschedule(&mut self, schedule_id: u32) -> Result<(), CompilerDiagnostic> { + let last = self.control_flow_stack.pop().expect("Can only unschedule the last target"); + if last.id() != schedule_id { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Can only unschedule the last target".to_string(), + None, + )); + } + match &last { + ControlFlowTarget::Loop { block, continue_block, loop_block, owns_loop, .. } => { + // TS: always removes block from scheduled for loops + // (ownsBlock is boolean, so `!== null` is always true) + self.scheduled.remove(block); + self.scheduled.remove(continue_block); + if *owns_loop { + if let Some(lb) = loop_block { + self.scheduled.remove(lb); + } + } + } + _ => { + self.scheduled.remove(&last.block()); + } + } + Ok(()) + } + + fn unschedule_all(&mut self, schedule_ids: &[u32]) -> Result<(), CompilerDiagnostic> { + for &id in schedule_ids.iter().rev() { + self.unschedule(id)?; + } + Ok(()) + } + + fn is_scheduled(&self, block: BlockId) -> bool { + self.scheduled.contains(&block) || self.catch_handlers.contains(&block) + } + + fn get_break_target( + &self, + block: BlockId, + ) -> Result<(BlockId, ReactiveTerminalTargetKind), CompilerDiagnostic> { + let mut has_preceding_loop = false; + for i in (0..self.control_flow_stack.len()).rev() { + let target = &self.control_flow_stack[i]; + if target.block() == block { + let kind = if target.is_loop() { + if has_preceding_loop { + ReactiveTerminalTargetKind::Labeled + } else { + ReactiveTerminalTargetKind::Unlabeled + } + } else if i == self.control_flow_stack.len() - 1 { + ReactiveTerminalTargetKind::Implicit + } else { + ReactiveTerminalTargetKind::Labeled + }; + return Ok((target.block(), kind)); + } + has_preceding_loop = has_preceding_loop || target.is_loop(); + } + Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("Expected a break target for bb{}", block.0), + None, + )) + } + + fn get_continue_target(&self, block: BlockId) -> Option<(BlockId, ReactiveTerminalTargetKind)> { + let mut has_preceding_loop = false; + for i in (0..self.control_flow_stack.len()).rev() { + let target = &self.control_flow_stack[i]; + if let ControlFlowTarget::Loop { block: fallthrough_block, continue_block, .. } = target + { + if *continue_block == block { + let kind = if has_preceding_loop { + ReactiveTerminalTargetKind::Labeled + } else if i == self.control_flow_stack.len() - 1 { + ReactiveTerminalTargetKind::Implicit + } else { + ReactiveTerminalTargetKind::Unlabeled + }; + return Some((*fallthrough_block, kind)); + } + } + has_preceding_loop = has_preceding_loop || target.is_loop(); + } + None + } +} + +// ============================================================================= +// Driver +// ============================================================================= + +struct Driver<'a, 'b, 'h> { + cx: &'b mut Context<'a, 'h>, + hir: &'h HirFunction<'a>, + #[allow(dead_code)] + env: &'h Environment<'a>, +} + +impl<'a, 'b, 'h> Driver<'a, 'b, 'h> { + fn traverse_block( + &mut self, + block_id: BlockId, + ) -> Result, CompilerDiagnostic> { + let mut block_value = Vec::new(); + self.visit_block(block_id, &mut block_value)?; + Ok(block_value) + } + + fn visit_block( + &mut self, + mut block_id: BlockId, + block_value: &mut ReactiveBlock<'a>, + ) -> Result<(), CompilerDiagnostic> { + // Use a loop to avoid deep recursion for fallthrough chains. + // Each terminal that would tail-call visit_block(fallthrough, block_value) + // instead sets next_block and continues the loop. + loop { + // Extract data from block before any mutable operations + let block = &self.hir.body.blocks[&block_id]; + let block_id_val = block.id; + let instructions: Vec<_> = block.instructions.clone(); + let terminal = block.terminal.clone(); + + if !self.cx.emitted.insert(block_id_val) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("Block bb{} was already emitted", block_id_val.0), + None, + )); + } + + // Emit instructions + for instr_id in &instructions { + let instr = &self.hir.instructions[instr_id.0 as usize]; + block_value.push(ReactiveStatement::Instruction(ReactiveInstruction { + id: instr.id, + lvalue: Some(instr.lvalue.clone()), + value: ReactiveValue::Instruction(instr.value.clone()), + effects: instr.effects.clone(), + loc: instr.loc, + })); + } + + // Process terminal + let mut schedule_ids: Vec = Vec::new(); + let mut next_block: Option = None; + + match &terminal { + Terminal::If { test, consequent, alternate, fallthrough, id, loc } => { + // TS: reachable(fallthrough) && !isScheduled(fallthrough) + let fallthrough_id = + if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) { + Some(*fallthrough) + } else { + None + }; + // TS: alternate !== fallthrough ? alternate : null + let alternate_id = + if *alternate != *fallthrough { Some(*alternate) } else { None }; + + if let Some(ft) = fallthrough_id { + schedule_ids.push(self.cx.schedule(ft, "if")?); + } + + let consequent_block = if self.cx.is_scheduled(*consequent) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!( + "Unexpected 'if' where consequent is already scheduled (bb{})", + consequent.0 + ), + None, + )); + } else { + self.traverse_block(*consequent)? + }; + + let alternate_block = if let Some(alt) = alternate_id { + if self.cx.is_scheduled(alt) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!( + "Unexpected 'if' where the alternate is already scheduled (bb{})", + alt.0 + ), + None, + )); + } else { + Some(self.traverse_block(alt)?) + } + } else { + None + }; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::If { + test: test.clone(), + consequent: consequent_block, + alternate: alternate_block, + id: *id, + loc: *loc, + }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::Switch { test, cases, fallthrough, id, loc } => { + // TS: reachable(fallthrough) && !isScheduled(fallthrough) + let fallthrough_id = + if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) { + Some(*fallthrough) + } else { + None + }; + if let Some(ft) = fallthrough_id { + schedule_ids.push(self.cx.schedule(ft, "switch")?); + } + + // TS processes cases in reverse order, then reverses the result. + // This ensures that later cases are scheduled when earlier cases + // are traversed, matching fallthrough semantics. + let mut reactive_cases = Vec::new(); + for case in cases.iter().rev() { + let case_block_id = case.block; + + if self.cx.is_scheduled(case_block_id) { + // TS: asserts case.block === fallthrough, then skips (return) + if case_block_id != *fallthrough { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'switch' where a case is already scheduled and block is not the fallthrough".to_string(), + None, + )); + } + continue; + } + + let consequent = self.traverse_block(case_block_id)?; + let case_schedule_id = self.cx.schedule(case_block_id, "case")?; + schedule_ids.push(case_schedule_id); + + reactive_cases.push(ReactiveSwitchCase { + test: case.test.clone(), + block: Some(consequent), + }); + } + reactive_cases.reverse(); + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::Switch { + test: test.clone(), + cases: reactive_cases, + id: *id, + loc: *loc, + }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::DoWhile { loop_block, test, fallthrough, id, loc } => { + let fallthrough_id = + if !self.cx.is_scheduled(*fallthrough) { Some(*fallthrough) } else { None }; + let loop_id = + if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough { + Some(*loop_block) + } else { + None + }; + + schedule_ids.push(self.cx.schedule_loop( + *fallthrough, + *test, + Some(*loop_block), + )?); + + let loop_body = if let Some(lid) = loop_id { + self.traverse_block(lid)? + } else { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'do-while' where the loop is already scheduled", + None, + )); + }; + let test_result = self.visit_value_block(*test, *loc, None)?; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::DoWhile { + loop_block: loop_body, + test: test_result.value, + id: *id, + loc: *loc, + }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::While { test, loop_block, fallthrough, id, loc } => { + // TS: reachable(fallthrough) && !isScheduled(fallthrough) + let fallthrough_id = + if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) { + Some(*fallthrough) + } else { + None + }; + let loop_id = + if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough { + Some(*loop_block) + } else { + None + }; + + schedule_ids.push(self.cx.schedule_loop( + *fallthrough, + *test, + Some(*loop_block), + )?); + + let test_result = self.visit_value_block(*test, *loc, None)?; + + let loop_body = if let Some(lid) = loop_id { + self.traverse_block(lid)? + } else { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'while' where the loop is already scheduled", + None, + )); + }; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::While { + test: test_result.value, + loop_block: loop_body, + id: *id, + loc: *loc, + }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::For { init, test, update, loop_block, fallthrough, id, loc } => { + let loop_id = + if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough { + Some(*loop_block) + } else { + None + }; + + let fallthrough_id = + if !self.cx.is_scheduled(*fallthrough) { Some(*fallthrough) } else { None }; + + // Continue block is update (if present) or test + let continue_block = update.unwrap_or(*test); + schedule_ids.push(self.cx.schedule_loop( + *fallthrough, + continue_block, + Some(*loop_block), + )?); + + let init_result = self.visit_value_block(*init, *loc, None)?; + let init_value = self.value_block_result_to_sequence(init_result, *loc); + + let test_result = self.visit_value_block(*test, *loc, None)?; + + let update_result = match update { + Some(u) => Some(self.visit_value_block(*u, *loc, None)?), + None => None, + }; + + let loop_body = if let Some(lid) = loop_id { + self.traverse_block(lid)? + } else { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'for' where the loop is already scheduled", + None, + )); + }; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::For { + init: init_value, + test: test_result.value, + update: update_result.map(|r| r.value), + loop_block: loop_body, + id: *id, + loc: *loc, + }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::ForOf { init, test, loop_block, fallthrough, id, loc } => { + let loop_id = + if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough { + Some(*loop_block) + } else { + None + }; + + let fallthrough_id = + if !self.cx.is_scheduled(*fallthrough) { Some(*fallthrough) } else { None }; + + // TS: scheduleLoop(fallthrough, init, loop) + schedule_ids.push(self.cx.schedule_loop( + *fallthrough, + *init, + Some(*loop_block), + )?); + + let init_result = self.visit_value_block(*init, *loc, None)?; + let init_value = self.value_block_result_to_sequence(init_result, *loc); + + let test_result = self.visit_value_block(*test, *loc, None)?; + let test_value = self.value_block_result_to_sequence(test_result, *loc); + + let loop_body = if let Some(lid) = loop_id { + self.traverse_block(lid)? + } else { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'for-of' where the loop is already scheduled", + None, + )); + }; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::ForOf { + init: init_value, + test: test_value, + loop_block: loop_body, + id: *id, + loc: *loc, + }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::ForIn { init, loop_block, fallthrough, id, loc } => { + let loop_id = + if !self.cx.is_scheduled(*loop_block) && *loop_block != *fallthrough { + Some(*loop_block) + } else { + None + }; + + let fallthrough_id = + if !self.cx.is_scheduled(*fallthrough) { Some(*fallthrough) } else { None }; + + schedule_ids.push(self.cx.schedule_loop( + *fallthrough, + *init, + Some(*loop_block), + )?); + + let init_result = self.visit_value_block(*init, *loc, None)?; + let init_value = self.value_block_result_to_sequence(init_result, *loc); + + let loop_body = if let Some(lid) = loop_id { + self.traverse_block(lid)? + } else { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'for-in' where the loop is already scheduled", + None, + )); + }; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::ForIn { + init: init_value, + loop_block: loop_body, + id: *id, + loc: *loc, + }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::Label { block: label_block, fallthrough, id, loc } => { + // TS: reachable(fallthrough) && !isScheduled(fallthrough) + let fallthrough_id = + if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) { + Some(*fallthrough) + } else { + None + }; + if let Some(ft) = fallthrough_id { + schedule_ids.push(self.cx.schedule(ft, "if")?); + } + + if self.cx.is_scheduled(*label_block) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'label' where the block is already scheduled".to_string(), + None, + )); + } + let label_body = self.traverse_block(*label_block)?; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::Label { block: label_body, id: *id, loc: *loc }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::Sequence { .. } + | Terminal::Optional { .. } + | Terminal::Ternary { .. } + | Terminal::Logical { .. } => { + let fallthrough = match &terminal { + Terminal::Sequence { fallthrough, .. } + | Terminal::Optional { fallthrough, .. } + | Terminal::Ternary { fallthrough, .. } + | Terminal::Logical { fallthrough, .. } => *fallthrough, + _ => unreachable!(), + }; + let fallthrough_id = + if !self.cx.is_scheduled(fallthrough) { Some(fallthrough) } else { None }; + if let Some(ft) = fallthrough_id { + schedule_ids.push(self.cx.schedule(ft, "if")?); + } + + let result = self.visit_value_block_terminal(&terminal)?; + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Instruction(ReactiveInstruction { + id: result.id, + lvalue: Some(result.place), + value: result.value, + effects: None, + loc: *terminal_loc(&terminal), + })); + + next_block = fallthrough_id; + } + + Terminal::Goto { block: goto_block, variant, id, loc } => { + match variant { + GotoVariant::Break => { + if let Some(stmt) = self.visit_break(*goto_block, *id, *loc)? { + block_value.push(stmt); + } + } + GotoVariant::Continue => { + let stmt = self.visit_continue(*goto_block, *id, *loc)?; + block_value.push(stmt); + } + GotoVariant::Try => { + // noop + } + } + } + + Terminal::MaybeThrow { continuation, .. } => { + if !self.cx.is_scheduled(*continuation) { + next_block = Some(*continuation); + } + } + + Terminal::Try { + block: try_block, + handler_binding, + handler, + fallthrough, + id, + loc, + } => { + let fallthrough_id = + if self.cx.reachable(*fallthrough) && !self.cx.is_scheduled(*fallthrough) { + Some(*fallthrough) + } else { + None + }; + if let Some(ft) = fallthrough_id { + schedule_ids.push(self.cx.schedule(ft, "if")?); + } + self.cx.schedule_catch_handler(*handler); + + let try_body = self.traverse_block(*try_block)?; + let handler_body = self.traverse_block(*handler)?; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::Try { + block: try_body, + handler_binding: handler_binding.clone(), + handler: handler_body, + id: *id, + loc: *loc, + }, + label: fallthrough_id.map(|ft| ReactiveLabel { id: ft, implicit: false }), + })); + + next_block = fallthrough_id; + } + + Terminal::Scope { fallthrough, block: scope_block, scope, .. } => { + let fallthrough_id = + if !self.cx.is_scheduled(*fallthrough) { Some(*fallthrough) } else { None }; + if let Some(ft) = fallthrough_id { + schedule_ids.push(self.cx.schedule(ft, "if")?); + self.cx.scope_fallthroughs.insert(ft); + } + + if self.cx.is_scheduled(*scope_block) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'scope' where the block is already scheduled".to_string(), + None, + )); + } + let scope_body = self.traverse_block(*scope_block)?; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::Scope(ReactiveScopeBlock { + scope: *scope, + instructions: scope_body, + })); + + next_block = fallthrough_id; + } + + Terminal::PrunedScope { fallthrough, block: scope_block, scope, .. } => { + let fallthrough_id = + if !self.cx.is_scheduled(*fallthrough) { Some(*fallthrough) } else { None }; + if let Some(ft) = fallthrough_id { + schedule_ids.push(self.cx.schedule(ft, "if")?); + self.cx.scope_fallthroughs.insert(ft); + } + + if self.cx.is_scheduled(*scope_block) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'scope' where the block is already scheduled".to_string(), + None, + )); + } + let scope_body = self.traverse_block(*scope_block)?; + + self.cx.unschedule_all(&schedule_ids)?; + block_value.push(ReactiveStatement::PrunedScope(PrunedReactiveScopeBlock { + scope: *scope, + instructions: scope_body, + })); + + next_block = fallthrough_id; + } + + Terminal::Return { value, id, loc, .. } => { + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::Return { + value: value.clone(), + id: *id, + loc: *loc, + }, + label: None, + })); + } + + Terminal::Throw { value, id, loc } => { + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::Throw { + value: value.clone(), + id: *id, + loc: *loc, + }, + label: None, + })); + } + + Terminal::Unreachable { .. } => { + // noop + } + + Terminal::Unsupported { .. } => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected unsupported terminal", + None, + )); + } + + Terminal::Branch { test, consequent, alternate, id, loc, .. } => { + let consequent_block = if self.cx.is_scheduled(*consequent) { + if let Some(stmt) = self.visit_break(*consequent, *id, *loc)? { + vec![stmt] + } else { + Vec::new() + } + } else { + self.traverse_block(*consequent)? + }; + + if self.cx.is_scheduled(*alternate) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected 'branch' where the alternate is already scheduled" + .to_string(), + None, + )); + } + let alternate_block = self.traverse_block(*alternate)?; + + block_value.push(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::If { + test: test.clone(), + consequent: consequent_block, + alternate: Some(alternate_block), + id: *id, + loc: *loc, + }, + label: None, + })); + } + } + match next_block { + Some(nb) => block_id = nb, + None => return Ok(()), + } + } // end loop + } + + // ========================================================================= + // Value block processing + // ========================================================================= + + fn visit_value_block( + &mut self, + block_id: BlockId, + loc: Option, + fallthrough: Option, + ) -> Result, CompilerDiagnostic> { + let block = &self.hir.body.blocks[&block_id]; + let block_id_val = block.id; + let terminal = block.terminal.clone(); + let instructions: Vec<_> = block.instructions.clone(); + + // If we've reached the fallthrough, stop + if let Some(ft) = fallthrough { + if block_id == ft { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!( + "Did not expect to reach the fallthrough of a value block (bb{})", + block_id.0 + ), + None, + )); + } + } + + match &terminal { + Terminal::Branch { test, id: term_id, .. } => { + if instructions.is_empty() { + Ok(ValueBlockResult { + block: block_id_val, + place: test.clone(), + value: ReactiveValue::Instruction(InstructionValue::LoadLocal { + place: test.clone(), + loc: test.loc, + }), + id: *term_id, + }) + } else { + Ok(self.extract_value_block_result(&instructions, block_id_val, loc)) + } + } + Terminal::Goto { .. } => { + if instructions.is_empty() { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected empty block with `goto` terminal", + Some(format!("Block bb{} is empty", block_id.0)), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc, + message: Some("Unexpected empty block with `goto` terminal".to_string()), + identifier_name: None, + })); + } + Ok(self.extract_value_block_result(&instructions, block_id_val, loc)) + } + Terminal::MaybeThrow { continuation, .. } => { + let continuation_id = *continuation; + let continuation_block = self.cx.block(continuation_id); + let cont_instructions_empty = continuation_block.instructions.is_empty(); + let cont_is_goto = matches!(continuation_block.terminal, Terminal::Goto { .. }); + let cont_block_id = continuation_block.id; + + if cont_instructions_empty && cont_is_goto { + Ok(self.extract_value_block_result(&instructions, cont_block_id, loc)) + } else { + let continuation = self.visit_value_block(continuation_id, loc, fallthrough)?; + Ok(self.wrap_with_sequence(&instructions, continuation, loc)) + } + } + _ => { + // Value block ended in a value terminal, recurse to get the value + // of that terminal and stitch them together in a sequence. + // TS: visitValueBlock(init.fallthrough, loc) — does NOT propagate fallthrough + let init = self.visit_value_block_terminal(&terminal)?; + let init_fallthrough = init.fallthrough; + let init_instr = ReactiveInstruction { + id: init.id, + lvalue: Some(init.place), + value: init.value, + effects: None, + loc, + }; + let final_result = self.visit_value_block(init_fallthrough, loc, None)?; + + // Combine block instructions + init instruction, then wrap + let mut all_instrs: Vec> = instructions + .iter() + .map(|iid| { + let instr = &self.hir.instructions[iid.0 as usize]; + ReactiveInstruction { + id: instr.id, + lvalue: Some(instr.lvalue.clone()), + value: ReactiveValue::Instruction(instr.value.clone()), + effects: instr.effects.clone(), + loc: instr.loc, + } + }) + .collect(); + all_instrs.push(init_instr); + + if all_instrs.is_empty() { + Ok(final_result) + } else { + Ok(ValueBlockResult { + block: final_result.block, + place: final_result.place.clone(), + value: ReactiveValue::SequenceExpression { + instructions: all_instrs, + id: final_result.id, + value: Box::new(final_result.value), + loc, + }, + id: final_result.id, + }) + } + } + } + } + + fn visit_test_block( + &mut self, + test_block_id: BlockId, + loc: Option, + terminal_kind: &str, + ) -> Result, CompilerDiagnostic> { + let test = self.visit_value_block(test_block_id, loc, None)?; + let test_block = &self.hir.body.blocks[&test.block]; + match &test_block.terminal { + Terminal::Branch { consequent, alternate, loc: branch_loc, .. } => { + Ok(TestBlockResult { + test, + consequent: *consequent, + alternate: *alternate, + branch_loc: *branch_loc, + }) + } + other => Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!( + "Expected a branch terminal for {} test block, got {:?}", + terminal_kind, + std::mem::discriminant(other) + ), + None, + )), + } + } + + fn visit_value_block_terminal( + &mut self, + terminal: &Terminal, + ) -> Result, CompilerDiagnostic> { + match terminal { + Terminal::Sequence { block, fallthrough, id, loc } => { + let block_result = self.visit_value_block(*block, *loc, Some(*fallthrough))?; + Ok(ValueTerminalResult { + value: block_result.value, + place: block_result.place, + fallthrough: *fallthrough, + id: *id, + }) + } + Terminal::Optional { optional, test, fallthrough, id, loc } => { + let test_result = self.visit_test_block(*test, *loc, "optional")?; + let consequent = + self.visit_value_block(test_result.consequent, *loc, Some(*fallthrough))?; + let call = ReactiveValue::SequenceExpression { + instructions: vec![ReactiveInstruction { + id: test_result.test.id, + lvalue: Some(test_result.test.place.clone()), + value: test_result.test.value, + effects: None, + loc: test_result.branch_loc, + }], + id: consequent.id, + value: Box::new(consequent.value), + loc: *loc, + }; + Ok(ValueTerminalResult { + place: consequent.place, + value: ReactiveValue::OptionalExpression { + optional: *optional, + value: Box::new(call), + id: *id, + loc: *loc, + }, + fallthrough: *fallthrough, + id: *id, + }) + } + Terminal::Logical { operator, test, fallthrough, id, loc } => { + let test_result = self.visit_test_block(*test, *loc, "logical")?; + let left_final = + self.visit_value_block(test_result.consequent, *loc, Some(*fallthrough))?; + let left = ReactiveValue::SequenceExpression { + instructions: vec![ReactiveInstruction { + id: test_result.test.id, + lvalue: Some(test_result.test.place.clone()), + value: test_result.test.value, + effects: None, + loc: *loc, + }], + id: left_final.id, + value: Box::new(left_final.value), + loc: *loc, + }; + let right = + self.visit_value_block(test_result.alternate, *loc, Some(*fallthrough))?; + Ok(ValueTerminalResult { + place: left_final.place, + value: ReactiveValue::LogicalExpression { + operator: *operator, + left: Box::new(left), + right: Box::new(right.value), + loc: *loc, + }, + fallthrough: *fallthrough, + id: *id, + }) + } + Terminal::Ternary { test, fallthrough, id, loc } => { + let test_result = self.visit_test_block(*test, *loc, "ternary")?; + let consequent = + self.visit_value_block(test_result.consequent, *loc, Some(*fallthrough))?; + let alternate = + self.visit_value_block(test_result.alternate, *loc, Some(*fallthrough))?; + Ok(ValueTerminalResult { + place: consequent.place, + value: ReactiveValue::ConditionalExpression { + test: Box::new(test_result.test.value), + consequent: Box::new(consequent.value), + alternate: Box::new(alternate.value), + loc: *loc, + }, + fallthrough: *fallthrough, + id: *id, + }) + } + Terminal::MaybeThrow { .. } => Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected maybe-throw in visit_value_block_terminal", + None, + )), + Terminal::Label { .. } => Err(CompilerDiagnostic::new( + ErrorCategory::Todo, + "Support labeled statements combined with value blocks is not yet implemented", + None, + )), + _ => Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unsupported terminal kind in value block", + None, + )), + } + } + + fn extract_value_block_result( + &self, + instructions: &[crate::react_compiler_hir::InstructionId], + block_id: BlockId, + loc: Option, + ) -> ValueBlockResult<'a> { + let last_id = instructions.last().expect("Expected non-empty instructions"); + let last_instr = &self.hir.instructions[last_id.0 as usize]; + + let remaining: Vec> = instructions[..instructions.len() - 1] + .iter() + .map(|iid| { + let instr = &self.hir.instructions[iid.0 as usize]; + ReactiveInstruction { + id: instr.id, + lvalue: Some(instr.lvalue.clone()), + value: ReactiveValue::Instruction(instr.value.clone()), + effects: instr.effects.clone(), + loc: instr.loc, + } + }) + .collect(); + + // If the last instruction is a StoreLocal to a temporary (unnamed identifier), + // convert it to a LoadLocal of the value being stored, matching the TS behavior. + let (value, place) = match &last_instr.value { + InstructionValue::StoreLocal { lvalue, value: store_value, .. } => { + let ident = &self.env.identifiers[lvalue.place.identifier.0 as usize]; + if ident.name.is_none() { + ( + ReactiveValue::Instruction(InstructionValue::LoadLocal { + place: store_value.clone(), + loc: store_value.loc, + }), + lvalue.place.clone(), + ) + } else { + ( + ReactiveValue::Instruction(last_instr.value.clone()), + last_instr.lvalue.clone(), + ) + } + } + _ => (ReactiveValue::Instruction(last_instr.value.clone()), last_instr.lvalue.clone()), + }; + let id = last_instr.id; + + if remaining.is_empty() { + ValueBlockResult { block: block_id, place, value, id } + } else { + ValueBlockResult { + block: block_id, + place: place.clone(), + value: ReactiveValue::SequenceExpression { + instructions: remaining, + id, + value: Box::new(value), + loc, + }, + id, + } + } + } + + fn wrap_with_sequence( + &self, + instructions: &[crate::react_compiler_hir::InstructionId], + continuation: ValueBlockResult<'a>, + loc: Option, + ) -> ValueBlockResult<'a> { + if instructions.is_empty() { + return continuation; + } + + let reactive_instrs: Vec> = instructions + .iter() + .map(|iid| { + let instr = &self.hir.instructions[iid.0 as usize]; + ReactiveInstruction { + id: instr.id, + lvalue: Some(instr.lvalue.clone()), + value: ReactiveValue::Instruction(instr.value.clone()), + effects: instr.effects.clone(), + loc: instr.loc, + } + }) + .collect(); + + ValueBlockResult { + block: continuation.block, + place: continuation.place.clone(), + value: ReactiveValue::SequenceExpression { + instructions: reactive_instrs, + id: continuation.id, + value: Box::new(continuation.value), + loc, + }, + id: continuation.id, + } + } + + /// Converts the result of visit_value_block into a SequenceExpression that includes + /// the instruction with its lvalue. This is needed for for/for-of/for-in init/test + /// blocks where the instruction's lvalue assignment must be preserved. + /// + /// This also flattens nested SequenceExpressions that can occur from MaybeThrow + /// handling in try-catch blocks. + /// + /// TS: valueBlockResultToSequence() + fn value_block_result_to_sequence( + &self, + result: ValueBlockResult<'a>, + loc: Option, + ) -> ReactiveValue<'a> { + // Collect all instructions from potentially nested SequenceExpressions + let mut instructions: Vec> = Vec::new(); + let mut inner_value = result.value; + + // Flatten nested SequenceExpressions + loop { + match inner_value { + ReactiveValue::SequenceExpression { instructions: seq_instrs, value, .. } => { + instructions.extend(seq_instrs); + inner_value = *value; + } + _ => break, + } + } + + // Only add the final instruction if the innermost value is not just a LoadLocal + // of the same place we're storing to (which would be a no-op). + let is_load_of_same_place = match &inner_value { + ReactiveValue::Instruction(InstructionValue::LoadLocal { place, .. }) => { + place.identifier == result.place.identifier + } + _ => false, + }; + + if !is_load_of_same_place { + instructions.push(ReactiveInstruction { + id: result.id, + lvalue: Some(result.place), + value: inner_value, + effects: None, + loc, + }); + } + + ReactiveValue::SequenceExpression { + instructions, + id: result.id, + value: Box::new(ReactiveValue::Instruction(InstructionValue::Primitive { + value: crate::react_compiler_hir::PrimitiveValue::Undefined, + loc, + })), + loc, + } + } + + fn visit_break( + &self, + block: BlockId, + id: EvaluationOrder, + loc: Option, + ) -> Result>, CompilerDiagnostic> { + let (target_block, target_kind) = self.cx.get_break_target(block)?; + if self.cx.scope_fallthroughs.contains(&target_block) { + if target_kind != ReactiveTerminalTargetKind::Implicit { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Expected reactive scope to implicitly break to fallthrough".to_string(), + None, + )); + } + return Ok(None); + } + Ok(Some(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::Break { target: target_block, id, target_kind, loc }, + label: None, + }))) + } + + fn visit_continue( + &self, + block: BlockId, + id: EvaluationOrder, + loc: Option, + ) -> Result, CompilerDiagnostic> { + let (target_block, target_kind) = match self.cx.get_continue_target(block) { + Some(result) => result, + None => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("Expected continue target to be scheduled for bb{}", block.0), + None, + )); + } + }; + + Ok(ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::Continue { target: target_block, id, target_kind, loc }, + label: None, + })) + } +} + +// ============================================================================= +// Helper types +// ============================================================================= + +struct ValueBlockResult<'a> { + block: BlockId, + place: Place, + value: ReactiveValue<'a>, + id: EvaluationOrder, +} + +struct TestBlockResult<'a> { + test: ValueBlockResult<'a>, + consequent: BlockId, + alternate: BlockId, + branch_loc: Option, +} + +struct ValueTerminalResult<'a> { + value: ReactiveValue<'a>, + place: Place, + fallthrough: BlockId, + id: EvaluationOrder, +} + +/// Helper to get loc from a terminal +fn terminal_loc(terminal: &Terminal) -> &Option { + match terminal { + Terminal::If { loc, .. } + | Terminal::Branch { loc, .. } + | Terminal::Logical { loc, .. } + | Terminal::Ternary { loc, .. } + | Terminal::Optional { loc, .. } + | Terminal::Throw { loc, .. } + | Terminal::Return { loc, .. } + | Terminal::Goto { loc, .. } + | Terminal::Switch { loc, .. } + | Terminal::DoWhile { loc, .. } + | Terminal::While { loc, .. } + | Terminal::For { loc, .. } + | Terminal::ForOf { loc, .. } + | Terminal::ForIn { loc, .. } + | Terminal::Label { loc, .. } + | Terminal::Sequence { loc, .. } + | Terminal::Unreachable { loc, .. } + | Terminal::Unsupported { loc, .. } + | Terminal::MaybeThrow { loc, .. } + | Terminal::Scope { loc, .. } + | Terminal::PrunedScope { loc, .. } + | Terminal::Try { loc, .. } => loc, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/codegen_reactive_function.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/codegen_reactive_function.rs new file mode 100644 index 0000000000000..a0e95e8ae5ccb --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/codegen_reactive_function.rs @@ -0,0 +1,3203 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Code generation pass: converts a `ReactiveFunction` tree back into a Babel-compatible +//! AST with memoization (useMemoCache) wired in. +//! +//! This is the final pass in the compilation pipeline. +//! +//! Corresponds to `src/ReactiveScopes/CodegenReactiveFunction.ts` in the TS compiler. + +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::CompilerDiagnostic; +use crate::react_compiler_diagnostics::CompilerDiagnosticDetail; +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_diagnostics::CompilerErrorDetail; +use crate::react_compiler_diagnostics::ErrorCategory; +use crate::react_compiler_diagnostics::SourceLocation as DiagSourceLocation; +use crate::react_compiler_hir::ArrayElement; +use crate::react_compiler_hir::ArrayPattern; +use crate::react_compiler_hir::BlockId; +use crate::react_compiler_hir::DeclarationId; +use crate::react_compiler_hir::FunctionExpressionType; +use crate::react_compiler_hir::IdentifierId; +use crate::react_compiler_hir::InstructionKind; +use crate::react_compiler_hir::InstructionValue; +use crate::react_compiler_hir::JsxAttribute; +use crate::react_compiler_hir::JsxTag; +use crate::react_compiler_hir::LogicalOperator; +use crate::react_compiler_hir::ObjectPattern; +use crate::react_compiler_hir::ObjectPropertyKey; +use crate::react_compiler_hir::ObjectPropertyOrSpread; +use crate::react_compiler_hir::ObjectPropertyType; +use crate::react_compiler_hir::ParamPattern; +use crate::react_compiler_hir::Pattern; +use crate::react_compiler_hir::Place; +use crate::react_compiler_hir::PlaceOrSpread; +use crate::react_compiler_hir::PrimitiveValue; +use crate::react_compiler_hir::PropertyLiteral; +use crate::react_compiler_hir::ScopeId; +use crate::react_compiler_hir::SpreadPattern; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::reactive::PrunedReactiveScopeBlock; +use crate::react_compiler_hir::reactive::ReactiveBlock; +use crate::react_compiler_hir::reactive::ReactiveFunction; +use crate::react_compiler_hir::reactive::ReactiveInstruction; +use crate::react_compiler_hir::reactive::ReactiveScopeBlock; +use crate::react_compiler_hir::reactive::ReactiveStatement; +use crate::react_compiler_hir::reactive::ReactiveTerminal; +use crate::react_compiler_hir::reactive::ReactiveTerminalTargetKind; +use crate::react_compiler_hir::reactive::ReactiveValue; + +use crate::react_compiler_reactive_scopes::build_reactive_function::build_reactive_function; +use crate::react_compiler_reactive_scopes::prune_hoisted_contexts::prune_hoisted_contexts; +use crate::react_compiler_reactive_scopes::prune_unused_labels::prune_unused_labels; +use crate::react_compiler_reactive_scopes::prune_unused_lvalues::prune_unused_lvalues; +use crate::react_compiler_reactive_scopes::rename_variables::rename_variables; +use crate::react_compiler_reactive_scopes::visitors::ReactiveFunctionVisitor; +use crate::react_compiler_reactive_scopes::visitors::visit_reactive_function; + +// ============================================================================= +// Public API +// ============================================================================= + +pub const MEMO_CACHE_SENTINEL: &str = "react.memo_cache_sentinel"; +pub const EARLY_RETURN_SENTINEL: &str = "react.early_return_sentinel"; + +/// FBT tags whose children get special codegen treatment. +const SINGLE_CHILD_FBT_TAGS: &[&str] = &["fbt:param", "fbs:param"]; + +/// Top-level entry point: produces an oxc-shaped +/// [`crate::react_compiler::entrypoint::compile_result::CodegenFunction`] from a +/// reactive function, building oxc AST directly via [`oxc_ast::AstBuilder`]. +pub fn codegen_function<'a, 'h>( + ast: &oxc_ast::AstBuilder<'a>, + func: &ReactiveFunction<'h>, + env: &mut Environment<'h>, + unique_identifiers: FxHashSet, + fbt_operands: FxHashSet, +) -> Result, CompilerError> { + use crate::react_compiler::entrypoint::compile_result::CodegenFunction as OxcCodegenFunction; + use oxc_span::SPAN; + + let fn_name = func.id.as_deref().unwrap_or("[[ anonymous ]]"); + // Outlined functions reuse the same `fbtOperands` set as the main function + // (see TS `codegenFunction`), so keep a copy before it is moved into the context. + let fbt_operands_for_outlined = fbt_operands.clone(); + let mut cx = OxcContext::new(*ast, env, fn_name.to_string(), unique_identifiers, fbt_operands); + + // The value-emission port covers most instruction kinds, but a few sub-emitters + // (function/object/JSX expressions, hook-guard wrapping, TS-type reparse) are + // deferred to later batches and currently raise an invariant error. Until they + // land — and until the emitted body is actually spliced into the program — fall + // back to an empty body on any emission error, preserving the pre-batch behavior + // (the original program is returned un-memoized by `compile_program`) without + // surfacing spurious diagnostics. This shim is removed once emission is complete. + let mut compiled = match ox_codegen_reactive_function(&mut cx, func) { + Ok(compiled) => compiled, + Err(_) => OxcCompiledFunction { + params: ast.alloc_formal_parameters( + SPAN, + oxc_ast::ast::FormalParameterKind::FormalParameter, + ast.vec(), + None::>, + ), + body: ast.alloc_function_body(SPAN, ast.vec(), ast.vec()), + generator: false, + is_async: false, + memo_slots_used: 0, + memo_blocks: 0, + memo_values: 0, + pruned_memo_blocks: 0, + pruned_memo_values: 0, + }, + }; + + let cache_count = compiled.memo_slots_used; + if cache_count != 0 { + let cache_name = cx.synthesize_name("$"); + // const $ = useMemoCache(N) + let use_memo_cache = ast.expression_call( + SPAN, + ast.expression_identifier(SPAN, "useMemoCache"), + None::>, + ast.vec1(oxc_ast::ast::Argument::from(ox_number(ast, cache_count as f64))), + false, + ); + let declarator = ast.variable_declarator( + SPAN, + oxc_ast::ast::VariableDeclarationKind::Const, + ast.binding_pattern_binding_identifier(SPAN, ox_str(ast, &cache_name)), + None::>, + Some(use_memo_cache), + false, + ); + let preface = oxc_ast::ast::Statement::VariableDeclaration(ast.alloc_variable_declaration( + SPAN, + oxc_ast::ast::VariableDeclarationKind::Const, + ast.vec1(declarator), + false, + )); + let body_stmts = std::mem::replace(&mut compiled.body.statements, ast.vec()); + let mut new_body = ast.vec1(preface); + new_body.extend(body_stmts); + compiled.body.statements = new_body; + } + + let id = func.id.as_deref().map(|name| ast.binding_identifier(SPAN, ox_str(ast, name))); + + // Release the borrow of `env` held by `cx` so the outlined functions can be + // compiled with fresh contexts (mirrors TS `codegenFunction`). + drop(cx); + + let outlined = ox_codegen_outlined(ast, env, fbt_operands_for_outlined)?; + + Ok(OxcCodegenFunction { + loc: func.loc, + id, + name_hint: func.name_hint.clone(), + params: compiled.params, + body: compiled.body, + generator: func.generator, + is_async: func.is_async, + memo_slots_used: compiled.memo_slots_used, + memo_blocks: compiled.memo_blocks, + memo_values: compiled.memo_values, + pruned_memo_blocks: compiled.pruned_memo_blocks, + pruned_memo_values: compiled.pruned_memo_values, + outlined, + }) +} + +/// Compile the functions accumulated during the outlining passes (stored on the +/// `Environment`) into `CodegenFunction`s. Mirrors the `outlined` loop in TS +/// `codegenFunction`: for each entry build its reactive function, run the same +/// prune passes + variable renaming, then codegen it with a fresh context. +fn ox_codegen_outlined<'a>( + ast: &oxc_ast::AstBuilder<'a>, + env: &mut Environment, + fbt_operands: FxHashSet, +) -> Result< + Vec>, + CompilerError, +> { + use crate::react_compiler::entrypoint::compile_result::OutlinedFunction as OxcOutlinedFunction; + + let entries = env.take_outlined_functions(); + let mut outlined = Vec::with_capacity(entries.len()); + for entry in entries { + let mut reactive_function = build_reactive_function(&entry.func, env).map_err(|diag| { + let loc = diag.primary_location().cloned(); + let mut err = CompilerError::new(); + err.push_error_detail(crate::react_compiler_diagnostics::CompilerErrorDetail { + category: diag.category, + reason: diag.reason, + description: diag.description, + loc, + suggestions: diag.suggestions, + }); + err + })?; + prune_unused_labels(&mut reactive_function, env)?; + prune_unused_lvalues(&mut reactive_function, env); + prune_hoisted_contexts(&mut reactive_function, env)?; + + let identifiers = rename_variables(&mut reactive_function, env); + + let func = + codegen_function(ast, &reactive_function, env, identifiers, fbt_operands.clone())?; + outlined.push(OxcOutlinedFunction { func, fn_type: entry.fn_type }); + } + Ok(outlined) +} + +// ============================================================================= +// oxc codegen orchestration +// +// Walks the reactive function tree and builds oxc nodes via `AstBuilder`. The +// HIR-driven control flow mirrors the TS compiler's `CodegenReactiveFunction`. +// ============================================================================= + +use oxc_ast::ast as oxc; +use oxc_span::GetSpan; +use oxc_span::SPAN; + +// Temp value tracking. Maps a temporary's declaration to its emitted oxc value +// (`None` for params/catch bindings that are declared but have no inlinable value). +// oxc nodes are not `Clone`; the snapshot/restore in block codegen and the per-place +// read both clone into the arena via [`CloneIn`] (see `ox_clone_temporaries` / +// `ox_codegen_place`). +type OxcTemporaries<'a> = FxHashMap>>; + +use oxc_allocator::CloneIn; + +/// oxc analog of the Babel `ExpressionOrJsxText`: an instruction value is usually an +/// expression, but JSX children codegen can produce raw `JSXText`, which is not an +/// `Expression` in oxc. +enum OxValue<'a> { + Expression(oxc::Expression<'a>), + JsxText(oxc_allocator::Box<'a, oxc::JSXText<'a>>), +} + +impl<'a> OxValue<'a> { + fn clone_in(&self, allocator: &'a oxc_allocator::Allocator) -> OxValue<'a> { + match self { + OxValue::Expression(e) => OxValue::Expression(e.clone_in(allocator)), + OxValue::JsxText(t) => OxValue::JsxText(t.clone_in(allocator)), + } + } +} + +/// Clone the temporaries map, cloning each oxc value into the arena. +fn ox_clone_temporaries<'a>( + ast: &oxc_ast::AstBuilder<'a>, + temp: &OxcTemporaries<'a>, +) -> OxcTemporaries<'a> { + temp.iter().map(|(id, v)| (*id, v.as_ref().map(|v| v.clone_in(ast.allocator)))).collect() +} + +struct OxcContext<'a, 'env, 'h> { + ast: oxc_ast::AstBuilder<'a>, + env: &'env mut Environment<'h>, + #[allow(dead_code)] + fn_name: String, + next_cache_index: u32, + declarations: FxHashSet, + temp: OxcTemporaries<'a>, + object_methods: FxHashMap< + IdentifierId, + (InstructionValue<'h>, Option), + >, + unique_identifiers: FxHashSet, + #[allow(dead_code)] + fbt_operands: FxHashSet, + synthesized_names: FxHashMap, +} + +impl<'a, 'env, 'h> OxcContext<'a, 'env, 'h> { + fn new( + ast: oxc_ast::AstBuilder<'a>, + env: &'env mut Environment<'h>, + fn_name: String, + unique_identifiers: FxHashSet, + fbt_operands: FxHashSet, + ) -> Self { + OxcContext { + ast, + env, + fn_name, + next_cache_index: 0, + declarations: FxHashSet::default(), + temp: FxHashMap::default(), + object_methods: FxHashMap::default(), + unique_identifiers, + fbt_operands, + synthesized_names: FxHashMap::default(), + } + } + + fn alloc_cache_index(&mut self) -> u32 { + let idx = self.next_cache_index; + self.next_cache_index += 1; + idx + } + + fn declare(&mut self, identifier_id: IdentifierId) { + let ident = &self.env.identifiers[identifier_id.0 as usize]; + self.declarations.insert(ident.declaration_id); + } + + fn has_declared(&self, identifier_id: IdentifierId) -> bool { + let ident = &self.env.identifiers[identifier_id.0 as usize]; + self.declarations.contains(&ident.declaration_id) + } + + fn synthesize_name(&mut self, name: &str) -> String { + if let Some(prev) = self.synthesized_names.get(name) { + return prev.clone(); + } + let mut validated = name.to_string(); + let mut index = 0u32; + while self.unique_identifiers.contains(&validated) { + validated = format!("{name}{index}"); + index += 1; + } + self.unique_identifiers.insert(validated.clone()); + self.synthesized_names.insert(name.to_string(), validated.clone()); + validated + } + + #[allow(dead_code)] + fn record_error(&mut self, detail: CompilerErrorDetail) -> Result<(), CompilerError> { + self.env.record_error(detail) + } +} + +/// Intermediate oxc-shaped function: like the Babel `CodegenFunction`, but holding +/// the arena-allocated oxc params/body so `codegen_function` can splice the memo cache. +struct OxcCompiledFunction<'a> { + params: oxc_allocator::Box<'a, oxc::FormalParameters<'a>>, + body: oxc_allocator::Box<'a, oxc::FunctionBody<'a>>, + generator: bool, + is_async: bool, + memo_slots_used: u32, + memo_blocks: u32, + memo_values: u32, + pruned_memo_blocks: u32, + pruned_memo_values: u32, +} + +/// JSX text children containing any of these characters must be wrapped in an +/// expression container (`{"..."}`) rather than emitted as raw JSX text. +const JSX_TEXT_CHILD_REQUIRES_EXPR_CONTAINER_PATTERN: &[char] = &['<', '>', '&', '{', '}']; +/// JSX string attribute values containing these characters must be wrapped in an +/// expression container. +const STRING_REQUIRES_EXPR_CONTAINER_CHARS: &str = "\"\\"; + +/// Reference to an lvalue target during pattern codegen. +enum LvalueRef<'a> { + Place(&'a Place), + Pattern(&'a Pattern), + // Constructed once nested spread/rest lvalue emission is ported; the match arm + // in `ox_codegen_lvalue` already handles it. + #[allow(dead_code)] + Spread(&'a SpreadPattern), +} + +fn ox_number<'a>(ast: &oxc_ast::AstBuilder<'a>, value: f64) -> oxc::Expression<'a> { + ast.expression_numeric_literal(SPAN, value, None, oxc::NumberBase::Decimal) +} + +/// Allocate a `&'a str` in the arena (satisfies the builders' `IntoIn` slots for +/// both `Atom` and `Str`). +fn ox_str<'a>(ast: &oxc_ast::AstBuilder<'a>, s: &str) -> &'a str { + oxc_allocator::StringBuilder::from_str_in(s, ast.allocator).into_str() +} + +/// Re-emit a TS type annotation stored on a `TypeCastExpression` into the output +/// allocator. The lowering stores the original `&TSType` AST node directly, so the +/// common case (no identifier renames apply) is a cheap `clone_in`, no parser. +/// +/// When some identifier reference inside the type has a binding rename (e.g. a +/// `typeof field` whose value binding was renamed to `field_3`), we cannot just +/// clone: we re-emit the original source slice with the renames applied as text +/// edits and re-parse it (correctness over speed for this rare case). Returns +/// `None` only when the rename path is needed but the source / span is +/// unavailable or unparsable. +fn ox_reparse_ts_type<'a>( + cx: &OxcContext<'a, '_, '_>, + ty: &oxc::TSType<'_>, +) -> Option> { + // Compute the rename edits that apply to identifier references inside the + // type, as text edits keyed by absolute source offset. An ident is renamed + // only if it is an actual reference (its node-id is in `reference_node_ids`, + // excluding type labels / property keys) and a binding rename applies for the + // nearest enclosing declaration. Without this, a re-emitted `typeof field` + // keeps the pre-rename name while the value binding was renamed to `field_3`. + let edits: Vec<(u32, usize, String)> = if cx.env.renames.is_empty() { + Vec::new() + } else { + struct Collector { + out: Vec<(u32, String)>, + } + impl<'v> oxc_ast_visit::Visit<'v> for Collector { + fn visit_identifier_reference(&mut self, it: &oxc::IdentifierReference<'v>) { + self.out.push((it.span.start, it.name.to_string())); + } + fn visit_identifier_name(&mut self, it: &oxc::IdentifierName<'v>) { + self.out.push((it.span.start, it.name.to_string())); + } + } + let mut collector = Collector { out: Vec::new() }; + oxc_ast_visit::Visit::visit_ts_type(&mut collector, ty); + + let mut edits: Vec<(u32, usize, String)> = Vec::new(); + for (start, name) in &collector.out { + if *start == 0 || !cx.env.reference_node_ids.contains(start) { + continue; + } + if let Some(rename) = cx + .env + .renames + .iter() + .filter(|r| &r.original == name && r.declaration_start <= *start) + .max_by_key(|r| r.declaration_start) + { + edits.push((*start, name.len(), rename.renamed.clone())); + } + } + edits + }; + + // Common case: no renames apply — clone the stored type directly into the + // output allocator, no parser. This is the perf win over re-parsing. + if edits.is_empty() { + return Some(ty.clone_in(cx.ast.allocator)); + } + + // Rename case: re-emit the original source slice with renames applied as text + // edits (right-to-left so earlier offsets stay valid) and re-parse it. + let span = ty.span(); + let source = cx.env.code.as_deref()?; + let start = span.start as usize; + let end = span.end as usize; + if start >= source.len() || end > source.len() || start >= end { + return None; + } + let slice = &source[start..end]; + let mut edits = edits; + edits.sort_by_key(|edit| std::cmp::Reverse(edit.0)); + let mut text = slice.to_string(); + for (abs_start, old_len, renamed) in edits { + if let Some(rel) = (abs_start as usize).checked_sub(start) { + if rel + old_len <= text.len() { + text.replace_range(rel..rel + old_len, &renamed); + } + } + } + let slice: &str = &text; + // Wrap the type in a cast so the parser yields a `TSAsExpression` whose + // `type_annotation` is exactly the parsed type. + let wrapped = oxc_allocator::StringBuilder::from_strs_array_in( + ["let __oxc_t = null as ", slice, ";"], + cx.ast.allocator, + ) + .into_str(); + let parsed = + oxc_parser::Parser::new(cx.ast.allocator, wrapped, oxc_span::SourceType::tsx()).parse(); + if parsed.panicked { + return None; + } + let stmt = parsed.program.body.into_iter().next()?; + let oxc::Statement::VariableDeclaration(decl) = stmt else { return None }; + let init = decl.unbox().declarations.into_iter().next()?.init?; + let oxc::Expression::TSAsExpression(ts_as) = init else { return None }; + Some(ts_as.unbox().type_annotation) +} + +/// Build `Symbol.for("")`. +fn ox_symbol_for<'a>(ast: &oxc_ast::AstBuilder<'a>, name: &str) -> oxc::Expression<'a> { + let callee = oxc::Expression::from(ast.member_expression_static( + SPAN, + ast.expression_identifier(SPAN, "Symbol"), + ast.identifier_name(SPAN, "for"), + false, + )); + ast.expression_call( + SPAN, + callee, + None::>, + ast.vec1(oxc::Argument::from(ast.expression_string_literal(SPAN, ox_str(ast, name), None))), + false, + ) +} + +/// `$[index]` computed member expression. +fn ox_cache_index<'a>( + ast: &oxc_ast::AstBuilder<'a>, + cache_name: &str, + index: u32, +) -> oxc::Expression<'a> { + oxc::Expression::from(ast.member_expression_computed( + SPAN, + ast.expression_identifier(SPAN, ox_str(ast, cache_name)), + ox_number(ast, index as f64), + false, + )) +} + +fn ox_codegen_reactive_function<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + func: &ReactiveFunction<'h>, +) -> Result, CompilerError> { + // Register parameters + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(sp) => &sp.place, + }; + let ident = &cx.env.identifiers[place.identifier.0 as usize]; + cx.temp.insert(ident.declaration_id, None); + cx.declare(place.identifier); + } + + let params = ox_convert_parameters(cx, &func.params)?; + let mut statements = ox_codegen_block(cx, &func.body)?; + + // Directives + let directives = cx.ast.vec_from_iter(func.directives.iter().map(|d| { + cx.ast.directive( + SPAN, + cx.ast.string_literal(SPAN, ox_str(&cx.ast, d), None), + ox_str(&cx.ast, d), + ) + })); + + // Remove trailing `return undefined` + if let Some(oxc::Statement::ReturnStatement(ret)) = statements.last() { + if ret.argument.is_none() { + statements.pop(); + } + } + + let (memo_blocks, memo_values, pruned_memo_blocks, pruned_memo_values) = + count_memo_blocks(func, cx.env); + + let body = cx.ast.alloc_function_body(SPAN, directives, statements); + + Ok(OxcCompiledFunction { + params, + body, + generator: func.generator, + is_async: func.is_async, + memo_slots_used: cx.next_cache_index, + memo_blocks, + memo_values, + pruned_memo_blocks, + pruned_memo_values, + }) +} + +fn ox_convert_parameters<'a>( + cx: &mut OxcContext<'a, '_, '_>, + params: &[ParamPattern], +) -> Result>, CompilerError> { + let mut items: Vec> = Vec::new(); + let mut rest: Option> = None; + for param in params { + match param { + ParamPattern::Place(place) => { + let binding = ox_binding_for_identifier(cx, place.identifier)?; + items.push(cx.ast.formal_parameter( + SPAN, + cx.ast.vec(), + binding, + None::>, + None::>, + false, + None, + false, + false, + )); + } + ParamPattern::Spread(spread) => { + let binding = ox_binding_for_identifier(cx, spread.place.identifier)?; + let rest_elem = cx.ast.binding_rest_element(SPAN, binding); + rest = Some(cx.ast.formal_parameter_rest( + SPAN, + cx.ast.vec(), + rest_elem, + None::>, + )); + } + } + } + let items_vec = cx.ast.vec_from_iter(items); + Ok(cx.ast.alloc_formal_parameters( + SPAN, + oxc::FormalParameterKind::FormalParameter, + items_vec, + rest, + )) +} + +fn ox_binding_for_identifier<'a>( + cx: &OxcContext<'a, '_, '_>, + identifier_id: IdentifierId, +) -> Result, CompilerError> { + let name = ox_identifier_name(cx.env, identifier_id)?; + Ok(cx.ast.binding_pattern_binding_identifier(SPAN, ox_str(&cx.ast, &name))) +} + +fn ox_identifier_name( + env: &Environment, + identifier_id: IdentifierId, +) -> Result { + let ident = &env.identifiers[identifier_id.0 as usize]; + match &ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => Ok(n.clone()), + Some(crate::react_compiler_hir::IdentifierName::Promoted(n)) => Ok(n.clone()), + None => Err(invariant_err( + "Expected temporaries to be promoted to named identifiers in an earlier pass", + None, + )), + } +} + +// ============================================================================= +// Block codegen (oxc) +// ============================================================================= + +fn ox_codegen_block<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + block: &ReactiveBlock<'h>, +) -> Result>, CompilerError> { + let temp_snapshot = ox_clone_temporaries(&cx.ast, &cx.temp); + let result = ox_codegen_block_no_reset(cx, block)?; + cx.temp = temp_snapshot; + Ok(result) +} + +fn ox_codegen_block_no_reset<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + block: &ReactiveBlock<'h>, +) -> Result>, CompilerError> { + let mut statements: oxc_allocator::Vec<'a, oxc::Statement<'a>> = cx.ast.vec(); + for item in block { + match item { + ReactiveStatement::Instruction(instr) => { + if let Some(stmt) = ox_codegen_instruction_nullable(cx, instr)? { + statements.push(stmt); + } + } + ReactiveStatement::PrunedScope(PrunedReactiveScopeBlock { instructions, .. }) => { + let scope_block = ox_codegen_block_no_reset(cx, instructions)?; + statements.extend(scope_block); + } + ReactiveStatement::Scope(ReactiveScopeBlock { scope, instructions }) => { + let temp_snapshot = ox_clone_temporaries(&cx.ast, &cx.temp); + ox_codegen_reactive_scope(cx, &mut statements, *scope, instructions)?; + cx.temp = temp_snapshot; + } + ReactiveStatement::Terminal(term_stmt) => { + let stmt = ox_codegen_terminal(cx, &term_stmt.terminal)?; + let Some(stmt) = stmt else { + continue; + }; + if let Some(ref label) = term_stmt.label { + if !label.implicit { + let inner = match stmt { + oxc::Statement::BlockStatement(mut bs) if bs.body.len() == 1 => { + bs.body.pop().unwrap() + } + other => other, + }; + let label_ident = cx + .ast + .label_identifier(SPAN, ox_str(&cx.ast, &codegen_label(label.id))); + statements.push(cx.ast.statement_labeled(SPAN, label_ident, inner)); + } else if let oxc::Statement::BlockStatement(bs) = stmt { + let bs = bs.unbox(); + statements.extend(bs.body); + } else { + statements.push(stmt); + } + } else if let oxc::Statement::BlockStatement(bs) = stmt { + let bs = bs.unbox(); + statements.extend(bs.body); + } else { + statements.push(stmt); + } + } + } + } + Ok(statements) +} + +fn ox_codegen_block_statement<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + block: &ReactiveBlock<'h>, +) -> Result, CompilerError> { + let body = ox_codegen_block(cx, block)?; + Ok(cx.ast.block_statement(SPAN, body)) +} + +// ============================================================================= +// Reactive scope codegen (memoization) (oxc) +// ============================================================================= + +fn ox_codegen_reactive_scope<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + statements: &mut oxc_allocator::Vec<'a, oxc::Statement<'a>>, + scope_id: ScopeId, + block: &ReactiveBlock<'h>, +) -> Result<(), CompilerError> { + let scope_deps = cx.env.scopes[scope_id.0 as usize].dependencies.clone(); + let scope_decls = cx.env.scopes[scope_id.0 as usize].declarations.clone(); + let scope_reassignments = cx.env.scopes[scope_id.0 as usize].reassignments.clone(); + + let mut cache_store_stmts: oxc_allocator::Vec<'a, oxc::Statement<'a>> = cx.ast.vec(); + let mut cache_load_stmts: oxc_allocator::Vec<'a, oxc::Statement<'a>> = cx.ast.vec(); + let mut cache_loads: Vec<(String, u32)> = Vec::new(); + let mut change_exprs: Vec> = Vec::new(); + + let mut deps = scope_deps; + deps.sort_by(|a, b| compare_scope_dependency(a, b, cx.env)); + + for dep in &deps { + let index = cx.alloc_cache_index(); + let cache_name = cx.synthesize_name("$"); + let dep_expr = ox_codegen_dependency(cx, dep)?; + let comparison = cx.ast.expression_binary( + SPAN, + ox_cache_index(&cx.ast, &cache_name, index), + oxc::BinaryOperator::StrictInequality, + dep_expr, + ); + change_exprs.push(comparison); + + let dep_value = ox_codegen_dependency(cx, dep)?; + let store = cx.ast.expression_assignment( + SPAN, + oxc::AssignmentOperator::Assign, + oxc::AssignmentTarget::from(oxc::SimpleAssignmentTarget::from(ast_member_target( + &cx.ast, + &cache_name, + index, + ))), + dep_value, + ); + cache_store_stmts.push(cx.ast.statement_expression(SPAN, store)); + } + + let mut first_output_index: Option = None; + + let mut decls = scope_decls; + decls.sort_by(|(_id_a, a), (_id_b, b)| compare_scope_declaration(a, b, cx.env)); + + for (_ident_id, decl) in &decls { + let index = cx.alloc_cache_index(); + if first_output_index.is_none() { + first_output_index = Some(index); + } + let name = ox_identifier_name(cx.env, decl.identifier)?; + if !cx.has_declared(decl.identifier) { + let declarator = cx.ast.variable_declarator( + SPAN, + oxc::VariableDeclarationKind::Let, + cx.ast.binding_pattern_binding_identifier(SPAN, ox_str(&cx.ast, &name)), + None::>, + None, + false, + ); + statements.push(oxc::Statement::VariableDeclaration( + cx.ast.alloc_variable_declaration( + SPAN, + oxc::VariableDeclarationKind::Let, + cx.ast.vec1(declarator), + false, + ), + )); + } + cache_loads.push((name, index)); + cx.declare(decl.identifier); + } + + for reassignment_id in scope_reassignments { + let index = cx.alloc_cache_index(); + if first_output_index.is_none() { + first_output_index = Some(index); + } + let name = ox_identifier_name(cx.env, reassignment_id)?; + cache_loads.push((name, index)); + } + + let test_condition = if change_exprs.is_empty() { + let first_idx = first_output_index.ok_or_else(|| { + invariant_err("Expected scope to have at least one declaration", None) + })?; + let cache_name = cx.synthesize_name("$"); + cx.ast.expression_binary( + SPAN, + ox_cache_index(&cx.ast, &cache_name, first_idx), + oxc::BinaryOperator::StrictEquality, + ox_symbol_for(&cx.ast, MEMO_CACHE_SENTINEL), + ) + } else { + change_exprs + .into_iter() + .reduce(|acc, expr| { + cx.ast.expression_logical(SPAN, acc, oxc::LogicalOperator::Or, expr) + }) + .unwrap() + }; + + let mut computation_body = ox_codegen_block(cx, block)?; + + for (name, index) in &cache_loads { + let cache_name = cx.synthesize_name("$"); + // $[index] = name + let store = cx.ast.expression_assignment( + SPAN, + oxc::AssignmentOperator::Assign, + oxc::AssignmentTarget::from(oxc::SimpleAssignmentTarget::from(ast_member_target( + &cx.ast, + &cache_name, + *index, + ))), + cx.ast.expression_identifier(SPAN, ox_str(&cx.ast, name)), + ); + cache_store_stmts.push(cx.ast.statement_expression(SPAN, store)); + // name = $[index] + let load = cx.ast.expression_assignment( + SPAN, + oxc::AssignmentOperator::Assign, + oxc::AssignmentTarget::AssignmentTargetIdentifier( + cx.ast.alloc_identifier_reference(SPAN, ox_str(&cx.ast, name)), + ), + ox_cache_index(&cx.ast, &cache_name, *index), + ); + cache_load_stmts.push(cx.ast.statement_expression(SPAN, load)); + } + + computation_body.extend(cache_store_stmts); + + let memo_stmt = cx.ast.statement_if( + SPAN, + test_condition, + cx.ast.statement_block(SPAN, computation_body), + Some(cx.ast.statement_block(SPAN, cache_load_stmts)), + ); + statements.push(memo_stmt); + + // Early return + let early_return_value = cx.env.scopes[scope_id.0 as usize].early_return_value.clone(); + if let Some(ref early_return) = early_return_value { + let early_ident = &cx.env.identifiers[early_return.value.0 as usize]; + let name = match &early_ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => n.clone(), + Some(crate::react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(), + None => { + return Err(invariant_err( + "Expected early return value to be promoted to a named variable", + early_return.loc, + )); + } + }; + let test = cx.ast.expression_binary( + SPAN, + cx.ast.expression_identifier(SPAN, ox_str(&cx.ast, &name)), + oxc::BinaryOperator::StrictInequality, + ox_symbol_for(&cx.ast, EARLY_RETURN_SENTINEL), + ); + let return_stmt = cx.ast.statement_return( + SPAN, + Some(cx.ast.expression_identifier(SPAN, ox_str(&cx.ast, &name))), + ); + let consequent = cx.ast.statement_block(SPAN, cx.ast.vec1(return_stmt)); + statements.push(cx.ast.statement_if(SPAN, test, consequent, None)); + } + + Ok(()) +} + +/// Build `$[index]` as a `MemberExpression` for use as an assignment target. +fn ast_member_target<'a>( + ast: &oxc_ast::AstBuilder<'a>, + cache_name: &str, + index: u32, +) -> oxc::MemberExpression<'a> { + ast.member_expression_computed( + SPAN, + ast.expression_identifier(SPAN, ox_str(ast, cache_name)), + ox_number(ast, index as f64), + false, + ) +} + +// ============================================================================= +// Terminal codegen (oxc) +// ============================================================================= + +fn ox_codegen_terminal<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + terminal: &ReactiveTerminal<'h>, +) -> Result>, CompilerError> { + match terminal { + ReactiveTerminal::Break { target, target_kind, .. } => { + if *target_kind == ReactiveTerminalTargetKind::Implicit { + return Ok(None); + } + let label = if *target_kind == ReactiveTerminalTargetKind::Labeled { + Some(cx.ast.label_identifier(SPAN, ox_str(&cx.ast, &codegen_label(*target)))) + } else { + None + }; + Ok(Some(cx.ast.statement_break(SPAN, label))) + } + ReactiveTerminal::Continue { target, target_kind, .. } => { + if *target_kind == ReactiveTerminalTargetKind::Implicit { + return Ok(None); + } + let label = if *target_kind == ReactiveTerminalTargetKind::Labeled { + Some(cx.ast.label_identifier(SPAN, ox_str(&cx.ast, &codegen_label(*target)))) + } else { + None + }; + Ok(Some(cx.ast.statement_continue(SPAN, label))) + } + ReactiveTerminal::Return { value, .. } => { + let expr = ox_codegen_place_to_expression(cx, value)?; + if let oxc::Expression::Identifier(ref ident) = expr { + if ident.name == "undefined" { + return Ok(Some(cx.ast.statement_return(SPAN, None))); + } + } + Ok(Some(cx.ast.statement_return(SPAN, Some(expr)))) + } + ReactiveTerminal::Throw { value, .. } => { + let expr = ox_codegen_place_to_expression(cx, value)?; + Ok(Some(cx.ast.statement_throw(SPAN, expr))) + } + ReactiveTerminal::If { test, consequent, alternate, .. } => { + let test_expr = ox_codegen_place_to_expression(cx, test)?; + let consequent_block = ox_codegen_block_statement(cx, consequent)?; + let consequent = oxc::Statement::BlockStatement(cx.ast.alloc(consequent_block)); + let alternate = if let Some(alt) = alternate { + let block = ox_codegen_block_statement(cx, alt)?; + if block.body.is_empty() { + None + } else { + Some(oxc::Statement::BlockStatement(cx.ast.alloc(block))) + } + } else { + None + }; + Ok(Some(cx.ast.statement_if(SPAN, test_expr, consequent, alternate))) + } + ReactiveTerminal::Switch { test, cases, .. } => { + let test_expr = ox_codegen_place_to_expression(cx, test)?; + let mut switch_cases: oxc_allocator::Vec<'a, oxc::SwitchCase<'a>> = cx.ast.vec(); + for case in cases { + let case_test = case + .test + .as_ref() + .map(|t| ox_codegen_place_to_expression(cx, t)) + .transpose()?; + let block = + case.block.as_ref().map(|b| ox_codegen_block_statement(cx, b)).transpose()?; + let consequent: oxc_allocator::Vec<'a, oxc::Statement<'a>> = match block { + Some(b) if b.body.is_empty() => cx.ast.vec(), + Some(b) => cx.ast.vec1(oxc::Statement::BlockStatement(cx.ast.alloc(b))), + None => cx.ast.vec(), + }; + switch_cases.push(cx.ast.switch_case(SPAN, case_test, consequent)); + } + Ok(Some(cx.ast.statement_switch(SPAN, test_expr, switch_cases))) + } + ReactiveTerminal::DoWhile { loop_block, test, .. } => { + let test_expr = ox_codegen_instruction_value_to_expression(cx, test)?; + let body = ox_codegen_block_statement(cx, loop_block)?; + let body = oxc::Statement::BlockStatement(cx.ast.alloc(body)); + Ok(Some(cx.ast.statement_do_while(SPAN, body, test_expr))) + } + ReactiveTerminal::While { test, loop_block, .. } => { + let test_expr = ox_codegen_instruction_value_to_expression(cx, test)?; + let body = ox_codegen_block_statement(cx, loop_block)?; + let body = oxc::Statement::BlockStatement(cx.ast.alloc(body)); + Ok(Some(cx.ast.statement_while(SPAN, test_expr, body))) + } + ReactiveTerminal::For { init, test, update, loop_block, .. } => { + let init_val = ox_codegen_for_init(cx, init)?; + let test_expr = ox_codegen_instruction_value_to_expression(cx, test)?; + let update_expr = update + .as_ref() + .map(|u| ox_codegen_instruction_value_to_expression(cx, u)) + .transpose()?; + let body = ox_codegen_block_statement(cx, loop_block)?; + let body = oxc::Statement::BlockStatement(cx.ast.alloc(body)); + Ok(Some(cx.ast.statement_for(SPAN, init_val, Some(test_expr), update_expr, body))) + } + ReactiveTerminal::ForIn { init, loop_block, loc, .. } => { + ox_codegen_for_in(cx, init, loop_block, *loc) + } + ReactiveTerminal::ForOf { init, test, loop_block, loc, .. } => { + ox_codegen_for_of(cx, init, test, loop_block, *loc) + } + ReactiveTerminal::Label { block, .. } => { + let body = ox_codegen_block_statement(cx, block)?; + Ok(Some(oxc::Statement::BlockStatement(cx.ast.alloc(body)))) + } + ReactiveTerminal::Try { block, handler_binding, handler, .. } => { + let catch_param = match handler_binding.as_ref() { + Some(binding) => { + let ident = &cx.env.identifiers[binding.identifier.0 as usize]; + cx.temp.insert(ident.declaration_id, None); + let pattern = ox_binding_for_identifier(cx, binding.identifier)?; + Some(cx.ast.catch_parameter( + SPAN, + pattern, + None::>, + )) + } + None => None, + }; + let try_block = ox_codegen_block_statement(cx, block)?; + let handler_block = ox_codegen_block_statement(cx, handler)?; + let handler = cx.ast.catch_clause(SPAN, catch_param, handler_block); + Ok(Some(cx.ast.statement_try( + SPAN, + try_block, + Some(handler), + None::>, + ))) + } + } +} + +fn ox_codegen_for_in<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + init: &ReactiveValue<'h>, + loop_block: &ReactiveBlock<'h>, + loc: Option, +) -> Result>, CompilerError> { + let ReactiveValue::SequenceExpression { instructions, .. } = init else { + return Err(invariant_err("Expected a sequence expression init for for..in", None)); + }; + if instructions.len() != 2 { + cx.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "Support non-trivial for..in inits".to_string(), + description: None, + loc, + suggestions: None, + })?; + return Ok(Some(cx.ast.statement_empty(SPAN))); + } + let iterable_collection = &instructions[0]; + let iterable_item = &instructions[1]; + let instr_value = get_instruction_value(&iterable_item.value)?; + let (lval, var_decl_kind) = ox_extract_for_in_of_lval(cx, instr_value, "for..in", loc)?; + let right = ox_codegen_instruction_value_to_expression(cx, &iterable_collection.value)?; + let body = ox_codegen_block_statement(cx, loop_block)?; + let body = oxc::Statement::BlockStatement(cx.ast.alloc(body)); + let declarator = cx.ast.variable_declarator( + SPAN, + var_decl_kind, + lval, + None::>, + None, + false, + ); + let decl = + cx.ast.alloc_variable_declaration(SPAN, var_decl_kind, cx.ast.vec1(declarator), false); + let left = oxc::ForStatementLeft::VariableDeclaration(decl); + Ok(Some(cx.ast.statement_for_in(SPAN, left, right, body))) +} + +fn ox_codegen_for_of<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + init: &ReactiveValue<'h>, + test: &ReactiveValue<'h>, + loop_block: &ReactiveBlock<'h>, + loc: Option, +) -> Result>, CompilerError> { + let ReactiveValue::SequenceExpression { instructions: init_instrs, .. } = init else { + return Err(invariant_err("Expected a sequence expression init for for..of", None)); + }; + if init_instrs.len() != 1 { + return Err(invariant_err( + "Expected a single-expression sequence expression init for for..of", + None, + )); + } + let get_iter_value = get_instruction_value(&init_instrs[0].value)?; + let InstructionValue::GetIterator { collection, .. } = get_iter_value else { + return Err(invariant_err("Expected GetIterator in for..of init", None)); + }; + + let ReactiveValue::SequenceExpression { instructions: test_instrs, .. } = test else { + return Err(invariant_err("Expected a sequence expression test for for..of", None)); + }; + if test_instrs.len() != 2 { + cx.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "Support non-trivial for..of inits".to_string(), + description: None, + loc, + suggestions: None, + })?; + return Ok(Some(cx.ast.statement_empty(SPAN))); + } + let iterable_item = &test_instrs[1]; + let instr_value = get_instruction_value(&iterable_item.value)?; + let (lval, var_decl_kind) = ox_extract_for_in_of_lval(cx, instr_value, "for..of", loc)?; + + let right = ox_codegen_place_to_expression(cx, collection)?; + let body = ox_codegen_block_statement(cx, loop_block)?; + let body = oxc::Statement::BlockStatement(cx.ast.alloc(body)); + let declarator = cx.ast.variable_declarator( + SPAN, + var_decl_kind, + lval, + None::>, + None, + false, + ); + let decl = + cx.ast.alloc_variable_declaration(SPAN, var_decl_kind, cx.ast.vec1(declarator), false); + let left = oxc::ForStatementLeft::VariableDeclaration(decl); + Ok(Some(cx.ast.statement_for_of(SPAN, false, left, right, body))) +} + +fn ox_extract_for_in_of_lval<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + instr_value: &InstructionValue<'h>, + context_name: &str, + loc: Option, +) -> Result<(oxc::BindingPattern<'a>, oxc::VariableDeclarationKind), CompilerError> { + let (lval, kind) = match instr_value { + InstructionValue::StoreLocal { lvalue, .. } => { + (ox_codegen_lvalue(cx, &LvalueRef::Place(&lvalue.place))?, lvalue.kind) + } + InstructionValue::Destructure { lvalue, .. } => { + (ox_codegen_lvalue(cx, &LvalueRef::Pattern(&lvalue.pattern))?, lvalue.kind) + } + InstructionValue::StoreContext { .. } => { + cx.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: format!("Support non-trivial {} inits", context_name), + description: None, + loc, + suggestions: None, + })?; + return Ok(( + cx.ast.binding_pattern_binding_identifier(SPAN, "_"), + oxc::VariableDeclarationKind::Let, + )); + } + _ => { + return Err(invariant_err( + &format!( + "Expected a StoreLocal or Destructure in {} collection, found {:?}", + context_name, + std::mem::discriminant(instr_value) + ), + None, + )); + } + }; + let var_decl_kind = match kind { + InstructionKind::Const => oxc::VariableDeclarationKind::Const, + InstructionKind::Let => oxc::VariableDeclarationKind::Let, + _ => { + return Err(invariant_err( + &format!("Unexpected {:?} variable in {} collection", kind, context_name), + None, + )); + } + }; + Ok((lval, var_decl_kind)) +} + +fn ox_codegen_for_init<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + init: &ReactiveValue<'h>, +) -> Result>, CompilerError> { + if let ReactiveValue::SequenceExpression { instructions, .. } = init { + let block_items: Vec = + instructions.iter().map(|i| ReactiveStatement::Instruction(i.clone())).collect(); + let body = ox_codegen_block(cx, &block_items)?; + let mut declarators: oxc_allocator::Vec<'a, oxc::VariableDeclarator<'a>> = cx.ast.vec(); + let mut kind = oxc::VariableDeclarationKind::Const; + for stmt in body { + // Fold `name = init` assignment into the last declarator when possible. + if let oxc::Statement::ExpressionStatement(ref expr_stmt) = stmt { + if let oxc::Expression::AssignmentExpression(ref assign) = expr_stmt.expression { + if matches!(assign.operator, oxc::AssignmentOperator::Assign) { + if let oxc::AssignmentTarget::AssignmentTargetIdentifier(ref left_ident) = + assign.left + { + if let Some(top) = declarators.last_mut() { + if let oxc::BindingPattern::BindingIdentifier(ref top_ident) = + top.id + { + if top_ident.name == left_ident.name && top.init.is_none() { + // Move the assignment's right-hand side into the declarator. + if let oxc::Statement::ExpressionStatement(expr_stmt) = stmt + { + if let oxc::Expression::AssignmentExpression(assign) = + expr_stmt.unbox().expression + { + top.init = Some(assign.unbox().right); + } + } + continue; + } + } + } + } + } + } + } + + if let oxc::Statement::VariableDeclaration(var_decl) = stmt { + let var_decl = var_decl.unbox(); + match var_decl.kind { + oxc::VariableDeclarationKind::Let | oxc::VariableDeclarationKind::Const => {} + _ => { + return Err(invariant_err( + "Expected a let or const variable declaration", + None, + )); + } + } + if matches!(var_decl.kind, oxc::VariableDeclarationKind::Let) { + kind = oxc::VariableDeclarationKind::Let; + } + declarators.extend(var_decl.declarations); + } else { + return Err(invariant_err("Expected a variable declaration", None)); + } + } + if declarators.is_empty() { + return Err(invariant_err("Expected a variable declaration in for-init", None)); + } + let decl = cx.ast.alloc_variable_declaration(SPAN, kind, declarators, false); + Ok(Some(oxc::ForStatementInit::VariableDeclaration(decl))) + } else { + let expr = ox_codegen_instruction_value_to_expression(cx, init)?; + Ok(Some(oxc::ForStatementInit::from(expr))) + } +} + +// ============================================================================= +// Per-instruction value emission (oxc). +// +// Ports the Babel reference value tree-walk (`codegen_instruction*`, +// `codegen_store_or_declare`, `emit_store`, `codegen_instruction_value`, +// `codegen_base_instruction_value`, `codegen_place`, `codegen_lvalue`, +// `codegen_argument`, `codegen_dependency`) to build oxc nodes via `AstBuilder`. +// The HIR-driven control flow is identical; only node construction differs. Since +// oxc tracks positions by `Span` (not Babel-style locs), the per-node loc +// propagation (`apply_loc_to_value` / place-loc overrides) collapses to `SPAN`. +// +// `FunctionExpression` / `ObjectExpression` / JSX / non-trivial `TypeCastExpression` +// emission are deferred to later batches and currently raise an invariant error +// (which fails compilation of that function and falls back to the original program, +// matching the current differential floor). +// ============================================================================= + +fn ox_convert_value_to_expression<'a>( + ast: &oxc_ast::AstBuilder<'a>, + value: OxValue<'a>, +) -> oxc::Expression<'a> { + match value { + OxValue::Expression(e) => e, + OxValue::JsxText(text) => ast.expression_string_literal(SPAN, text.value.as_str(), None), + } +} + +fn ox_codegen_instruction_nullable<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + instr: &ReactiveInstruction<'h>, +) -> Result>, CompilerError> { + if let ReactiveValue::Instruction(ref value) = instr.value { + match value { + InstructionValue::StoreLocal { .. } + | InstructionValue::StoreContext { .. } + | InstructionValue::Destructure { .. } + | InstructionValue::DeclareLocal { .. } + | InstructionValue::DeclareContext { .. } => { + return ox_codegen_store_or_declare(cx, instr, value); + } + InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => { + return Ok(None); + } + InstructionValue::Debugger { .. } => { + return Ok(Some(cx.ast.statement_debugger(SPAN))); + } + InstructionValue::PassthroughStatement { stmt, .. } => { + // Re-emit the preserved source statement (e.g. an inline TS + // `enum`) verbatim by cloning it into the output allocator. + return Ok(Some(stmt.clone_in(cx.ast.allocator))); + } + InstructionValue::ObjectMethod { loc, .. } => { + invariant( + instr.lvalue.is_some(), + "Expected object methods to have a temp lvalue", + None, + )?; + let lvalue = instr.lvalue.as_ref().unwrap(); + cx.object_methods.insert(lvalue.identifier, (value.clone(), *loc)); + return Ok(None); + } + _ => {} + } + } + let expr_value = ox_codegen_instruction_value(cx, &instr.value)?; + let stmt = ox_codegen_instruction(cx, instr, expr_value)?; + if matches!(stmt, oxc::Statement::EmptyStatement(_)) { Ok(None) } else { Ok(Some(stmt)) } +} + +fn ox_codegen_store_or_declare<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + instr: &ReactiveInstruction<'h>, + value: &InstructionValue<'h>, +) -> Result>, CompilerError> { + match value { + InstructionValue::StoreLocal { lvalue, value: val, .. } => { + let mut kind = lvalue.kind; + if cx.has_declared(lvalue.place.identifier) { + kind = InstructionKind::Reassign; + } + let rhs = ox_codegen_place_to_expression(cx, val)?; + ox_emit_store(cx, instr, kind, &LvalueRef::Place(&lvalue.place), Some(rhs)) + } + InstructionValue::StoreContext { lvalue, value: val, .. } => { + let rhs = ox_codegen_place_to_expression(cx, val)?; + ox_emit_store(cx, instr, lvalue.kind, &LvalueRef::Place(&lvalue.place), Some(rhs)) + } + InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::DeclareContext { lvalue, .. } => { + if cx.has_declared(lvalue.place.identifier) { + return Ok(None); + } + ox_emit_store(cx, instr, lvalue.kind, &LvalueRef::Place(&lvalue.place), None) + } + InstructionValue::Destructure { lvalue, value: val, .. } => { + let kind = lvalue.kind; + for place in crate::react_compiler_hir::visitors::each_pattern_operand(&lvalue.pattern) + { + let ident = &cx.env.identifiers[place.identifier.0 as usize]; + if kind != InstructionKind::Reassign && ident.name.is_none() { + cx.temp.insert(ident.declaration_id, None); + } + } + let rhs = ox_codegen_place_to_expression(cx, val)?; + ox_emit_store(cx, instr, kind, &LvalueRef::Pattern(&lvalue.pattern), Some(rhs)) + } + _ => unreachable!(), + } +} + +fn ox_emit_store<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + instr: &ReactiveInstruction<'h>, + kind: InstructionKind, + lvalue: &LvalueRef, + value: Option>, +) -> Result>, CompilerError> { + match kind { + InstructionKind::Const => { + if instr.lvalue.is_some() { + return Err(invariant_err_with_detail_message( + "Const declaration cannot be referenced as an expression", + "this is Const", + instr.loc, + )); + } + let lval = ox_codegen_lvalue(cx, lvalue)?; + Ok(Some(ox_make_var_decl(cx, oxc::VariableDeclarationKind::Const, lval, value))) + } + InstructionKind::Function => { + let lval = ox_codegen_lvalue(cx, lvalue)?; + let oxc::BindingPattern::BindingIdentifier(fn_id) = lval else { + return Err(invariant_err( + "Expected an identifier as function declaration lvalue", + None, + )); + }; + let Some(rhs) = value else { + return Err(invariant_err( + "Expected a function value for function declaration", + None, + )); + }; + match rhs { + oxc::Expression::FunctionExpression(func_expr) => { + let func_expr = func_expr.unbox(); + let decl = cx.ast.alloc_function( + SPAN, + oxc::FunctionType::FunctionDeclaration, + Some(fn_id.unbox()), + func_expr.generator, + func_expr.r#async, + false, + func_expr.type_parameters, + func_expr.this_param, + func_expr.params, + func_expr.return_type, + func_expr.body, + ); + Ok(Some(oxc::Statement::FunctionDeclaration(decl))) + } + _ => Err(invariant_err( + "Expected a function expression for function declaration", + None, + )), + } + } + InstructionKind::Let => { + if instr.lvalue.is_some() { + return Err(invariant_err_with_detail_message( + "Const declaration cannot be referenced as an expression", + "this is Let", + instr.loc, + )); + } + let lval = ox_codegen_lvalue(cx, lvalue)?; + Ok(Some(ox_make_var_decl(cx, oxc::VariableDeclarationKind::Let, lval, value))) + } + InstructionKind::Reassign => { + let Some(rhs) = value else { + return Err(invariant_err("Expected a value for reassignment", None)); + }; + let lval = ox_codegen_lvalue(cx, lvalue)?; + let target = ox_binding_pattern_to_assignment_target(cx, lval)?; + let expr = + cx.ast.expression_assignment(SPAN, oxc::AssignmentOperator::Assign, target, rhs); + if let Some(ref lvalue_place) = instr.lvalue { + let is_store_context = matches!( + &instr.value, + ReactiveValue::Instruction(InstructionValue::StoreContext { .. }) + ); + if !is_store_context { + let ident = &cx.env.identifiers[lvalue_place.identifier.0 as usize]; + cx.temp.insert(ident.declaration_id, Some(OxValue::Expression(expr))); + return Ok(None); + } + let stmt = ox_codegen_instruction(cx, instr, OxValue::Expression(expr))?; + if matches!(stmt, oxc::Statement::EmptyStatement(_)) { + return Ok(None); + } + return Ok(Some(stmt)); + } + Ok(Some(cx.ast.statement_expression(SPAN, expr))) + } + InstructionKind::Catch => Ok(Some(cx.ast.statement_empty(SPAN))), + InstructionKind::HoistedLet + | InstructionKind::HoistedConst + | InstructionKind::HoistedFunction => Err(invariant_err( + &format!("Expected {:?} to have been pruned in PruneHoistedContexts", kind), + None, + )), + } +} + +/// Build `kind id = init;` (or `kind id;` when `init` is `None`). +fn ox_make_var_decl<'a>( + cx: &OxcContext<'a, '_, '_>, + kind: oxc::VariableDeclarationKind, + id: oxc::BindingPattern<'a>, + init: Option>, +) -> oxc::Statement<'a> { + let declarator = cx.ast.variable_declarator( + SPAN, + kind, + id, + None::>, + init, + false, + ); + oxc::Statement::VariableDeclaration(cx.ast.alloc_variable_declaration( + SPAN, + kind, + cx.ast.vec1(declarator), + false, + )) +} + +fn ox_codegen_instruction<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + instr: &ReactiveInstruction<'h>, + value: OxValue<'a>, +) -> Result, CompilerError> { + let Some(ref lvalue) = instr.lvalue else { + let expr = ox_convert_value_to_expression(&cx.ast, value); + return Ok(cx.ast.statement_expression(SPAN, expr)); + }; + let ident = &cx.env.identifiers[lvalue.identifier.0 as usize]; + if ident.name.is_none() { + cx.temp.insert(ident.declaration_id, Some(value)); + return Ok(cx.ast.statement_empty(SPAN)); + } + let expr_value = ox_convert_value_to_expression(&cx.ast, value); + let name = ox_identifier_name(cx.env, lvalue.identifier)?; + if cx.has_declared(lvalue.identifier) { + let target = oxc::AssignmentTarget::AssignmentTargetIdentifier( + cx.ast.alloc_identifier_reference(SPAN, ox_str(&cx.ast, &name)), + ); + let expr = + cx.ast.expression_assignment(SPAN, oxc::AssignmentOperator::Assign, target, expr_value); + Ok(cx.ast.statement_expression(SPAN, expr)) + } else { + let id = cx.ast.binding_pattern_binding_identifier(SPAN, ox_str(&cx.ast, &name)); + Ok(ox_make_var_decl(cx, oxc::VariableDeclarationKind::Const, id, Some(expr_value))) + } +} + +// ============================================================================= +// Instruction value codegen (oxc) +// ============================================================================= + +fn ox_codegen_instruction_value_to_expression<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + instr_value: &ReactiveValue<'h>, +) -> Result, CompilerError> { + let value = ox_codegen_instruction_value(cx, instr_value)?; + Ok(ox_convert_value_to_expression(&cx.ast, value)) +} + +fn ox_codegen_instruction_value<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + instr_value: &ReactiveValue<'h>, +) -> Result, CompilerError> { + match instr_value { + ReactiveValue::Instruction(iv) => ox_codegen_base_instruction_value(cx, iv), + ReactiveValue::LogicalExpression { operator, left, right, .. } => { + let left_expr = ox_codegen_instruction_value_to_expression(cx, left)?; + let right_expr = ox_codegen_instruction_value_to_expression(cx, right)?; + Ok(OxValue::Expression(cx.ast.expression_logical( + SPAN, + left_expr, + ox_convert_logical_operator(operator), + right_expr, + ))) + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + let test_expr = ox_codegen_instruction_value_to_expression(cx, test)?; + let cons_expr = ox_codegen_instruction_value_to_expression(cx, consequent)?; + let alt_expr = ox_codegen_instruction_value_to_expression(cx, alternate)?; + Ok(OxValue::Expression( + cx.ast.expression_conditional(SPAN, test_expr, cons_expr, alt_expr), + )) + } + ReactiveValue::SequenceExpression { instructions, value, .. } => { + let block_items: Vec = + instructions.iter().map(|i| ReactiveStatement::Instruction(i.clone())).collect(); + let body = ox_codegen_block_no_reset(cx, &block_items)?; + let mut expressions: oxc_allocator::Vec<'a, oxc::Expression<'a>> = cx.ast.vec(); + for stmt in body { + match stmt { + oxc::Statement::ExpressionStatement(es) => { + expressions.push(es.unbox().expression); + } + oxc::Statement::VariableDeclaration(_) => { + cx.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block".to_string(), + description: None, + loc: None, + suggestions: None, + })?; + expressions.push(cx.ast.expression_string_literal( + SPAN, + "TODO handle declaration", + None, + )); + } + _ => { + cx.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: "(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of statement to expression".to_string(), + description: None, + loc: None, + suggestions: None, + })?; + expressions.push(cx.ast.expression_string_literal( + SPAN, + "TODO handle statement", + None, + )); + } + } + } + let final_expr = ox_codegen_instruction_value_to_expression(cx, value)?; + if expressions.is_empty() { + Ok(OxValue::Expression(final_expr)) + } else { + expressions.push(final_expr); + Ok(OxValue::Expression(cx.ast.expression_sequence(SPAN, expressions))) + } + } + ReactiveValue::OptionalExpression { value, optional, .. } => { + let opt_value = ox_codegen_instruction_value_to_expression(cx, value)?; + ox_make_optional(cx, opt_value, *optional) + } + } +} + +/// Strip a `ChainExpression` wrapper from a sub-expression so its inner element can +/// be folded into the enclosing optional chain. In oxc, `a?.b.c(d)` is a single +/// `ChainExpression` wrapping the outermost member/call; inner optional members are +/// plain members with `optional: true`. When the callee/object of an outer chain +/// element is itself a `ChainExpression`, it must be unwrapped to avoid emitting +/// spurious parens (e.g. `(a?.b)(d)` instead of `a?.b(d)`). +fn ox_unwrap_chain(expr: oxc::Expression<'_>) -> oxc::Expression<'_> { + match expr { + oxc::Expression::ChainExpression(chain) => { + let chain = chain.unbox(); + match chain.expression { + oxc::ChainElement::CallExpression(call) => oxc::Expression::CallExpression(call), + oxc::ChainElement::ComputedMemberExpression(m) => { + oxc::Expression::ComputedMemberExpression(m) + } + oxc::ChainElement::StaticMemberExpression(m) => { + oxc::Expression::StaticMemberExpression(m) + } + oxc::ChainElement::PrivateFieldExpression(m) => { + oxc::Expression::PrivateFieldExpression(m) + } + oxc::ChainElement::TSNonNullExpression(e) => { + oxc::Expression::TSNonNullExpression(e) + } + } + } + other => other, + } +} + +/// Re-wrap a call/member expression as an optional-chaining element, mirroring the +/// Babel reference's `OptionalExpression` arm. +fn ox_make_optional<'a>( + cx: &mut OxcContext<'a, '_, '_>, + expr: oxc::Expression<'a>, + optional: bool, +) -> Result, CompilerError> { + let chain_element: oxc::ChainElement<'a> = match expr { + oxc::Expression::ChainExpression(chain) => { + // Already a chain; update the optional flag on the head element. + let chain = chain.unbox(); + match chain.expression { + oxc::ChainElement::CallExpression(call) => { + let mut call = call.unbox(); + call.optional = optional; + oxc::ChainElement::CallExpression(cx.ast.alloc(call)) + } + oxc::ChainElement::ComputedMemberExpression(m) => { + let mut m = m.unbox(); + m.optional = optional; + oxc::ChainElement::ComputedMemberExpression(cx.ast.alloc(m)) + } + oxc::ChainElement::StaticMemberExpression(m) => { + let mut m = m.unbox(); + m.optional = optional; + oxc::ChainElement::StaticMemberExpression(cx.ast.alloc(m)) + } + other => other, + } + } + oxc::Expression::CallExpression(call) => { + let mut call = call.unbox(); + call.callee = ox_unwrap_chain(call.callee); + oxc::ChainElement::CallExpression(cx.ast.alloc_call_expression( + SPAN, + call.callee, + call.type_arguments, + call.arguments, + optional, + )) + } + oxc::Expression::ComputedMemberExpression(m) => { + let m = m.unbox(); + oxc::ChainElement::ComputedMemberExpression(cx.ast.alloc_computed_member_expression( + SPAN, + ox_unwrap_chain(m.object), + m.expression, + optional, + )) + } + oxc::Expression::StaticMemberExpression(m) => { + let m = m.unbox(); + oxc::ChainElement::StaticMemberExpression(cx.ast.alloc_static_member_expression( + SPAN, + ox_unwrap_chain(m.object), + m.property, + optional, + )) + } + _ => { + return Err(invariant_err( + "Expected optional value to resolve to call or member expression", + None, + )); + } + }; + Ok(OxValue::Expression(cx.ast.expression_chain(SPAN, chain_element))) +} + +fn ox_codegen_base_instruction_value<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + iv: &InstructionValue<'h>, +) -> Result, CompilerError> { + match iv { + InstructionValue::Primitive { value, .. } => { + Ok(OxValue::Expression(ox_codegen_primitive_value(&cx.ast, value))) + } + InstructionValue::BinaryExpression { operator, left, right, .. } => { + let left_expr = ox_codegen_place_to_expression(cx, left)?; + let right_expr = ox_codegen_place_to_expression(cx, right)?; + Ok(OxValue::Expression(cx.ast.expression_binary( + SPAN, + left_expr, + ox_convert_binary_operator(operator), + right_expr, + ))) + } + InstructionValue::UnaryExpression { operator, value, .. } => { + let arg = ox_codegen_place_to_expression(cx, value)?; + Ok(OxValue::Expression(cx.ast.expression_unary( + SPAN, + ox_convert_unary_operator(operator), + arg, + ))) + } + InstructionValue::LoadLocal { place, .. } | InstructionValue::LoadContext { place, .. } => { + let expr = ox_codegen_place_to_expression(cx, place)?; + Ok(OxValue::Expression(expr)) + } + InstructionValue::LoadGlobal { binding, .. } => Ok(OxValue::Expression( + cx.ast.expression_identifier(SPAN, ox_str(&cx.ast, binding.name())), + )), + InstructionValue::CallExpression { callee, args, .. } => { + let callee_expr = ox_codegen_place_to_expression(cx, callee)?; + let arguments = ox_codegen_arguments(cx, args)?; + let call_expr = cx.ast.expression_call( + SPAN, + callee_expr, + None::>, + arguments, + false, + ); + let result = ox_maybe_wrap_hook_call(cx, call_expr, callee.identifier)?; + Ok(OxValue::Expression(result)) + } + InstructionValue::MethodCall { property, args, .. } => { + let member_expr = ox_codegen_place_to_expression(cx, property)?; + if !ox_is_member_like(&member_expr) { + let msg = format!("Got: '{}'", ox_expression_type_name(&member_expr)); + let mut err = CompilerError::new(); + err.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::Invariant, + "[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression", + None, + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: property.loc, + message: Some(msg), + identifier_name: None, + }), + ); + return Err(err); + } + let arguments = ox_codegen_arguments(cx, args)?; + let call_expr = cx.ast.expression_call( + SPAN, + member_expr, + None::>, + arguments, + false, + ); + let result = ox_maybe_wrap_hook_call(cx, call_expr, property.identifier)?; + Ok(OxValue::Expression(result)) + } + InstructionValue::NewExpression { callee, args, .. } => { + let callee_expr = ox_codegen_place_to_expression(cx, callee)?; + let arguments = ox_codegen_arguments(cx, args)?; + Ok(OxValue::Expression(cx.ast.expression_new( + SPAN, + callee_expr, + None::>, + arguments, + ))) + } + InstructionValue::ArrayExpression { elements, .. } => { + let mut elems: oxc_allocator::Vec<'a, oxc::ArrayExpressionElement<'a>> = cx.ast.vec(); + for el in elements { + match el { + ArrayElement::Place(place) => { + let expr = ox_codegen_place_to_expression(cx, place)?; + elems.push(oxc::ArrayExpressionElement::from(expr)); + } + ArrayElement::Spread(spread) => { + let arg = ox_codegen_place_to_expression(cx, &spread.place)?; + elems.push(oxc::ArrayExpressionElement::SpreadElement( + cx.ast.alloc_spread_element(SPAN, arg), + )); + } + ArrayElement::Hole => { + elems.push(cx.ast.array_expression_element_elision(SPAN)); + } + } + } + Ok(OxValue::Expression(cx.ast.expression_array(SPAN, elems))) + } + InstructionValue::ObjectExpression { properties, .. } => { + ox_codegen_object_expression(cx, properties) + } + InstructionValue::PropertyLoad { object, property, .. } => { + let obj = ox_codegen_place_to_expression(cx, object)?; + let member = ox_property_member(cx, obj, property); + Ok(OxValue::Expression(oxc::Expression::from(member))) + } + InstructionValue::PropertyStore { object, property, value, .. } => { + let obj = ox_codegen_place_to_expression(cx, object)?; + let member = ox_property_member(cx, obj, property); + let val = ox_codegen_place_to_expression(cx, value)?; + let target = oxc::AssignmentTarget::from(oxc::SimpleAssignmentTarget::from(member)); + Ok(OxValue::Expression(cx.ast.expression_assignment( + SPAN, + oxc::AssignmentOperator::Assign, + target, + val, + ))) + } + InstructionValue::PropertyDelete { object, property, .. } => { + let obj = ox_codegen_place_to_expression(cx, object)?; + let member = ox_property_member(cx, obj, property); + Ok(OxValue::Expression(cx.ast.expression_unary( + SPAN, + oxc::UnaryOperator::Delete, + oxc::Expression::from(member), + ))) + } + InstructionValue::ComputedLoad { object, property, .. } => { + let obj = ox_codegen_place_to_expression(cx, object)?; + let prop = ox_codegen_place_to_expression(cx, property)?; + let member = cx.ast.member_expression_computed(SPAN, obj, prop, false); + Ok(OxValue::Expression(oxc::Expression::from(member))) + } + InstructionValue::ComputedStore { object, property, value, .. } => { + let obj = ox_codegen_place_to_expression(cx, object)?; + let prop = ox_codegen_place_to_expression(cx, property)?; + let member = cx.ast.member_expression_computed(SPAN, obj, prop, false); + let val = ox_codegen_place_to_expression(cx, value)?; + let target = oxc::AssignmentTarget::from(oxc::SimpleAssignmentTarget::from(member)); + Ok(OxValue::Expression(cx.ast.expression_assignment( + SPAN, + oxc::AssignmentOperator::Assign, + target, + val, + ))) + } + InstructionValue::ComputedDelete { object, property, .. } => { + let obj = ox_codegen_place_to_expression(cx, object)?; + let prop = ox_codegen_place_to_expression(cx, property)?; + let member = cx.ast.member_expression_computed(SPAN, obj, prop, false); + Ok(OxValue::Expression(cx.ast.expression_unary( + SPAN, + oxc::UnaryOperator::Delete, + oxc::Expression::from(member), + ))) + } + InstructionValue::RegExpLiteral { pattern, flags, .. } => { + let regex_flags = ox_parse_regexp_flags(flags); + let regex = oxc::RegExp { + pattern: oxc::RegExpPattern { + text: ox_str(&cx.ast, pattern).into(), + pattern: None, + }, + flags: regex_flags, + }; + Ok(OxValue::Expression(cx.ast.expression_reg_exp_literal(SPAN, regex, None))) + } + InstructionValue::MetaProperty { meta, property, .. } => { + let meta_ident = cx.ast.identifier_name(SPAN, ox_str(&cx.ast, meta)); + let prop_ident = cx.ast.identifier_name(SPAN, ox_str(&cx.ast, property)); + Ok(OxValue::Expression(cx.ast.expression_meta_property(SPAN, meta_ident, prop_ident))) + } + InstructionValue::Await { value, .. } => { + let arg = ox_codegen_place_to_expression(cx, value)?; + Ok(OxValue::Expression(cx.ast.expression_await(SPAN, arg))) + } + InstructionValue::GetIterator { collection, .. } => { + let expr = ox_codegen_place_to_expression(cx, collection)?; + Ok(OxValue::Expression(expr)) + } + InstructionValue::IteratorNext { iterator, .. } => { + let expr = ox_codegen_place_to_expression(cx, iterator)?; + Ok(OxValue::Expression(expr)) + } + InstructionValue::NextPropertyOf { value, .. } => { + let expr = ox_codegen_place_to_expression(cx, value)?; + Ok(OxValue::Expression(expr)) + } + InstructionValue::PostfixUpdate { operation, lvalue, .. } => { + let arg = ox_codegen_place_to_expression(cx, lvalue)?; + let target = ox_expression_to_simple_assignment_target(cx, arg)?; + Ok(OxValue::Expression(cx.ast.expression_update( + SPAN, + ox_convert_update_operator(operation), + false, + target, + ))) + } + InstructionValue::PrefixUpdate { operation, lvalue, .. } => { + let arg = ox_codegen_place_to_expression(cx, lvalue)?; + let target = ox_expression_to_simple_assignment_target(cx, arg)?; + Ok(OxValue::Expression(cx.ast.expression_update( + SPAN, + ox_convert_update_operator(operation), + true, + target, + ))) + } + InstructionValue::StoreLocal { lvalue, value, .. } => { + invariant( + lvalue.kind == InstructionKind::Reassign, + "Unexpected StoreLocal in codegenInstructionValue", + None, + )?; + let lval = ox_codegen_lvalue(cx, &LvalueRef::Place(&lvalue.place))?; + let target = ox_binding_pattern_to_assignment_target(cx, lval)?; + let rhs = ox_codegen_place_to_expression(cx, value)?; + Ok(OxValue::Expression(cx.ast.expression_assignment( + SPAN, + oxc::AssignmentOperator::Assign, + target, + rhs, + ))) + } + InstructionValue::StoreGlobal { name, value, .. } => { + let rhs = ox_codegen_place_to_expression(cx, value)?; + let target = oxc::AssignmentTarget::AssignmentTargetIdentifier( + cx.ast.alloc_identifier_reference(SPAN, ox_str(&cx.ast, name)), + ); + Ok(OxValue::Expression(cx.ast.expression_assignment( + SPAN, + oxc::AssignmentOperator::Assign, + target, + rhs, + ))) + } + InstructionValue::FunctionExpression { + name, name_hint, lowered_func, expr_type, .. + } => ox_codegen_function_expression(cx, name, name_hint, lowered_func, expr_type), + InstructionValue::TaggedTemplateExpression { tag, quasis, subexprs, .. } => { + let tag_expr = ox_codegen_place_to_expression(cx, tag)?; + let mut exprs: oxc_allocator::Vec<'a, oxc::Expression<'a>> = cx.ast.vec(); + for p in subexprs { + exprs.push(ox_codegen_place_to_expression(cx, p)?); + } + let quasi = ox_template_literal(cx, quasis, exprs); + Ok(OxValue::Expression(cx.ast.expression_tagged_template( + SPAN, + tag_expr, + None::>, + quasi, + ))) + } + InstructionValue::TemplateLiteral { subexprs, quasis, .. } => { + let mut exprs: oxc_allocator::Vec<'a, oxc::Expression<'a>> = cx.ast.vec(); + for p in subexprs { + exprs.push(ox_codegen_place_to_expression(cx, p)?); + } + let template = ox_template_literal(cx, quasis, exprs); + Ok(OxValue::Expression(oxc::Expression::TemplateLiteral(cx.ast.alloc(template)))) + } + InstructionValue::TypeCastExpression { + value, + type_annotation_kind, + type_annotation, + .. + } => { + let expr = ox_codegen_place_to_expression(cx, value)?; + // Recover the TS type from its original source span and re-wrap the + // inner expression, matching the baseline output. If the type can't be + // recovered, fall back to the unwrapped expression. + let wrapped = match (type_annotation_kind.as_deref(), type_annotation) { + (Some("satisfies"), Some(ta)) => match ox_reparse_ts_type(cx, ta) { + Some(ty) => cx.ast.expression_ts_satisfies(SPAN, expr, ty), + None => expr, + }, + (Some("as"), Some(ta)) => match ox_reparse_ts_type(cx, ta) { + Some(ty) => cx.ast.expression_ts_as(SPAN, expr, ty), + None => expr, + }, + _ => expr, + }; + Ok(OxValue::Expression(wrapped)) + } + InstructionValue::JSXText { value, .. } => { + Ok(OxValue::JsxText(cx.ast.alloc_jsx_text(SPAN, ox_str(&cx.ast, value), None))) + } + InstructionValue::JsxExpression { tag, props, children, .. } => { + ox_codegen_jsx_expression(cx, tag, props, children) + } + InstructionValue::JsxFragment { children, .. } => { + let mut child_nodes: oxc_allocator::Vec<'a, oxc::JSXChild<'a>> = cx.ast.vec(); + for child in children { + child_nodes.push(ox_codegen_jsx_element(cx, child)?); + } + let opening = cx.ast.jsx_opening_fragment(SPAN); + let closing = cx.ast.jsx_closing_fragment(SPAN); + let fragment = cx.ast.jsx_fragment(SPAN, opening, child_nodes, closing); + Ok(OxValue::Expression(oxc::Expression::JSXFragment(cx.ast.alloc(fragment)))) + } + InstructionValue::StartMemoize { .. } + | InstructionValue::FinishMemoize { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::DeclareLocal { .. } + | InstructionValue::DeclareContext { .. } + | InstructionValue::Destructure { .. } + | InstructionValue::ObjectMethod { .. } + | InstructionValue::StoreContext { .. } => Err(invariant_err( + &format!("Unexpected {:?} in codegenInstructionValue", std::mem::discriminant(iv)), + None, + )), + } +} + +/// Build `obj.prop` / `obj[prop]` member expression from a `PropertyLiteral`. +fn ox_property_member<'a>( + cx: &OxcContext<'a, '_, '_>, + object: oxc::Expression<'a>, + property: &PropertyLiteral, +) -> oxc::MemberExpression<'a> { + match property { + PropertyLiteral::String(s) => cx.ast.member_expression_static( + SPAN, + object, + cx.ast.identifier_name(SPAN, ox_str(&cx.ast, s)), + false, + ), + PropertyLiteral::Number(n) => { + cx.ast.member_expression_computed(SPAN, object, ox_number(&cx.ast, n.value()), false) + } + } +} + +fn ox_template_literal<'a>( + cx: &OxcContext<'a, '_, '_>, + quasis: &[crate::react_compiler_hir::TemplateQuasi], + expressions: oxc_allocator::Vec<'a, oxc::Expression<'a>>, +) -> oxc::TemplateLiteral<'a> { + let mut quasi_vec: oxc_allocator::Vec<'a, oxc::TemplateElement<'a>> = cx.ast.vec(); + let len = quasis.len(); + for (i, q) in quasis.iter().enumerate() { + let value = oxc::TemplateElementValue { + raw: ox_str(&cx.ast, &q.raw).into(), + cooked: q.cooked.as_deref().map(|c| ox_str(&cx.ast, c).into()), + }; + quasi_vec.push(cx.ast.template_element(SPAN, value, i == len - 1)); + } + cx.ast.template_literal(SPAN, quasi_vec, expressions) +} + +fn ox_codegen_arguments<'a>( + cx: &mut OxcContext<'a, '_, '_>, + args: &[PlaceOrSpread], +) -> Result>, CompilerError> { + let mut out: oxc_allocator::Vec<'a, oxc::Argument<'a>> = cx.ast.vec(); + for arg in args { + out.push(ox_codegen_argument(cx, arg)?); + } + Ok(out) +} + +fn ox_codegen_argument<'a>( + cx: &mut OxcContext<'a, '_, '_>, + arg: &PlaceOrSpread, +) -> Result, CompilerError> { + match arg { + PlaceOrSpread::Place(place) => { + Ok(oxc::Argument::from(ox_codegen_place_to_expression(cx, place)?)) + } + PlaceOrSpread::Spread(spread) => { + let expr = ox_codegen_place_to_expression(cx, &spread.place)?; + Ok(oxc::Argument::SpreadElement(cx.ast.alloc_spread_element(SPAN, expr))) + } + } +} + +fn ox_is_member_like(expr: &oxc::Expression) -> bool { + matches!( + expr, + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) + | oxc::Expression::PrivateFieldExpression(_) + | oxc::Expression::ChainExpression(_) + ) +} + +fn ox_expression_type_name(expr: &oxc::Expression) -> &'static str { + match expr { + oxc::Expression::Identifier(_) => "Identifier", + _ => "unknown", + } +} + +// ============================================================================= +// Place / lvalue / dependency codegen (oxc) +// ============================================================================= + +fn ox_codegen_place_to_expression<'a>( + cx: &mut OxcContext<'a, '_, '_>, + place: &Place, +) -> Result, CompilerError> { + let value = ox_codegen_place(cx, place)?; + Ok(ox_convert_value_to_expression(&cx.ast, value)) +} + +fn ox_codegen_place<'a>( + cx: &mut OxcContext<'a, '_, '_>, + place: &Place, +) -> Result, CompilerError> { + let ident = &cx.env.identifiers[place.identifier.0 as usize]; + let declaration_id = ident.declaration_id; + if let Some(tmp) = cx.temp.get(&declaration_id) { + if let Some(val) = tmp { + return Ok(val.clone_in(cx.ast.allocator)); + } + } else if ident.name.is_none() { + return Err(invariant_err( + &format!( + "[Codegen] No value found for temporary, identifier id={}", + place.identifier.0 + ), + place.loc, + )); + } + let name = ox_identifier_name(cx.env, place.identifier)?; + Ok(OxValue::Expression(cx.ast.expression_identifier(SPAN, ox_str(&cx.ast, &name)))) +} + +fn ox_codegen_lvalue<'a>( + cx: &mut OxcContext<'a, '_, '_>, + pattern: &LvalueRef, +) -> Result, CompilerError> { + match pattern { + LvalueRef::Place(place) => ox_binding_for_identifier(cx, place.identifier), + LvalueRef::Pattern(pat) => match pat { + Pattern::Array(arr) => ox_codegen_array_pattern(cx, arr), + Pattern::Object(obj) => ox_codegen_object_pattern(cx, obj), + }, + LvalueRef::Spread(spread) => ox_binding_for_identifier(cx, spread.place.identifier), + } +} + +fn ox_codegen_array_pattern<'a>( + cx: &mut OxcContext<'a, '_, '_>, + pattern: &ArrayPattern, +) -> Result, CompilerError> { + let mut elements: oxc_allocator::Vec<'a, Option>> = cx.ast.vec(); + let mut rest: Option> = None; + for item in &pattern.items { + match item { + crate::react_compiler_hir::ArrayPatternElement::Place(place) => { + elements.push(Some(ox_binding_for_identifier(cx, place.identifier)?)); + } + crate::react_compiler_hir::ArrayPatternElement::Spread(spread) => { + let inner = ox_binding_for_identifier(cx, spread.place.identifier)?; + rest = Some(cx.ast.binding_rest_element(SPAN, inner)); + } + crate::react_compiler_hir::ArrayPatternElement::Hole => { + elements.push(None); + } + } + } + Ok(cx.ast.binding_pattern_array_pattern(SPAN, elements, rest)) +} + +fn ox_codegen_object_pattern<'a>( + cx: &mut OxcContext<'a, '_, '_>, + pattern: &ObjectPattern, +) -> Result, CompilerError> { + let mut properties: oxc_allocator::Vec<'a, oxc::BindingProperty<'a>> = cx.ast.vec(); + let mut rest: Option> = None; + for prop in &pattern.properties { + match prop { + ObjectPropertyOrSpread::Property(obj_prop) => { + let (key, computed) = ox_codegen_object_property_key(cx, &obj_prop.key)?; + let value = ox_binding_for_identifier(cx, obj_prop.place.identifier)?; + let shorthand = !computed + && matches!( + (&key, &value), + ( + oxc::PropertyKey::StaticIdentifier(k), + oxc::BindingPattern::BindingIdentifier(v), + ) if k.name == v.name + ); + properties.push(cx.ast.binding_property(SPAN, key, value, shorthand, computed)); + } + ObjectPropertyOrSpread::Spread(spread) => { + let inner = ox_binding_for_identifier(cx, spread.place.identifier)?; + rest = Some(cx.ast.binding_rest_element(SPAN, inner)); + } + } + } + Ok(cx.ast.binding_pattern_object_pattern(SPAN, properties, rest)) +} + +/// Build an object pattern key, returning `(key, computed)`. +fn ox_codegen_object_property_key<'a>( + cx: &mut OxcContext<'a, '_, '_>, + key: &ObjectPropertyKey, +) -> Result<(oxc::PropertyKey<'a>, bool), CompilerError> { + match key { + ObjectPropertyKey::String { name } => Ok(( + oxc::PropertyKey::from(cx.ast.expression_string_literal( + SPAN, + ox_str(&cx.ast, name), + None, + )), + false, + )), + ObjectPropertyKey::Identifier { name } => { + Ok((cx.ast.property_key_static_identifier(SPAN, ox_str(&cx.ast, name)), false)) + } + ObjectPropertyKey::Computed { name } => { + let expr = ox_codegen_place_to_expression(cx, name)?; + Ok((oxc::PropertyKey::from(expr), true)) + } + ObjectPropertyKey::Number { name } => { + Ok((oxc::PropertyKey::from(ox_number(&cx.ast, name.value())), false)) + } + } +} + +fn ox_codegen_dependency<'a>( + cx: &mut OxcContext<'a, '_, '_>, + dep: &crate::react_compiler_hir::ReactiveScopeDependency, +) -> Result, CompilerError> { + let name = ox_identifier_name(cx.env, dep.identifier)?; + let mut object = cx.ast.expression_identifier(SPAN, ox_str(&cx.ast, &name)); + if !dep.path.is_empty() { + let has_optional = dep.path.iter().any(|p| p.optional); + // Build every member as a plain member expression carrying its own optional + // flag. In oxc, an optional chain like `a.b?.c.d?.e` is a single + // `ChainExpression` wrapping the outermost member, with inner members being + // plain members whose `optional` flags are preserved. Wrapping each step in + // its own `ChainExpression` would force spurious parens such as `(((a.b)?.c).d)?.e`. + for path_entry in &dep.path { + let member = ox_property_member(cx, object, &path_entry.property); + object = match member { + oxc::MemberExpression::StaticMemberExpression(m) => { + let m = m.unbox(); + oxc::Expression::StaticMemberExpression(cx.ast.alloc_static_member_expression( + SPAN, + m.object, + m.property, + path_entry.optional, + )) + } + oxc::MemberExpression::ComputedMemberExpression(m) => { + let m = m.unbox(); + oxc::Expression::ComputedMemberExpression( + cx.ast.alloc_computed_member_expression( + SPAN, + m.object, + m.expression, + path_entry.optional, + ), + ) + } + oxc::MemberExpression::PrivateFieldExpression(m) => { + oxc::Expression::PrivateFieldExpression(m) + } + }; + } + // Wrap the whole access path in a single chain only when it actually contains + // an optional access. + if has_optional { + let chain = match object { + oxc::Expression::StaticMemberExpression(m) => { + oxc::ChainElement::StaticMemberExpression(m) + } + oxc::Expression::ComputedMemberExpression(m) => { + oxc::ChainElement::ComputedMemberExpression(m) + } + oxc::Expression::PrivateFieldExpression(m) => { + oxc::ChainElement::PrivateFieldExpression(m) + } + other => return Ok(other), + }; + object = cx.ast.expression_chain(SPAN, chain); + } + } + Ok(object) +} + +/// Convert a `BindingPattern` (from `ox_codegen_lvalue`) into an `AssignmentTarget` +/// for reassignment / `StoreLocal` emission. +fn ox_binding_pattern_to_assignment_target<'a>( + cx: &OxcContext<'a, '_, '_>, + pattern: oxc::BindingPattern<'a>, +) -> Result, CompilerError> { + match pattern { + oxc::BindingPattern::BindingIdentifier(id) => { + let id = id.unbox(); + Ok(oxc::AssignmentTarget::AssignmentTargetIdentifier( + cx.ast.alloc_identifier_reference(SPAN, id.name), + )) + } + oxc::BindingPattern::ObjectPattern(obj) => { + let obj = obj.unbox(); + let mut properties = cx.ast.vec(); + for prop in obj.properties { + properties.push(ox_binding_property_to_assignment_target_property(cx, prop)?); + } + let rest = ox_binding_rest_to_assignment_target_rest(cx, obj.rest)?; + let target = cx.ast.object_assignment_target(SPAN, properties, rest); + Ok(oxc::AssignmentTarget::ObjectAssignmentTarget(cx.ast.alloc(target))) + } + oxc::BindingPattern::ArrayPattern(arr) => { + let arr = arr.unbox(); + let mut elements = cx.ast.vec(); + for elem in arr.elements { + match elem { + Some(p) => elements.push(Some(ox_binding_pattern_to_maybe_default(cx, p)?)), + None => elements.push(None), + } + } + let rest = ox_binding_rest_to_assignment_target_rest(cx, arr.rest)?; + let target = cx.ast.array_assignment_target(SPAN, elements, rest); + Ok(oxc::AssignmentTarget::ArrayAssignmentTarget(cx.ast.alloc(target))) + } + oxc::BindingPattern::AssignmentPattern(_) => { + Err(invariant_err("Unexpected top-level default in a reassignment target", None)) + } + } +} + +/// Convert an optional `BindingRestElement` (`...rest`) into an `AssignmentTargetRest`. +fn ox_binding_rest_to_assignment_target_rest<'a>( + cx: &OxcContext<'a, '_, '_>, + rest: Option>>, +) -> Result>>, CompilerError> { + match rest { + Some(rest) => { + let rest = rest.unbox(); + let target = ox_binding_pattern_to_assignment_target(cx, rest.argument)?; + Ok(Some(cx.ast.alloc(cx.ast.assignment_target_rest(SPAN, target)))) + } + None => Ok(None), + } +} + +/// Convert a `BindingPattern` element into an `AssignmentTargetMaybeDefault`, +/// turning an `AssignmentPattern` (`x = default`) into an `AssignmentTargetWithDefault`. +fn ox_binding_pattern_to_maybe_default<'a>( + cx: &OxcContext<'a, '_, '_>, + pattern: oxc::BindingPattern<'a>, +) -> Result, CompilerError> { + match pattern { + oxc::BindingPattern::AssignmentPattern(assign) => { + let assign = assign.unbox(); + let binding = ox_binding_pattern_to_assignment_target(cx, assign.left)?; + let with_default = cx.ast.assignment_target_with_default(SPAN, binding, assign.right); + Ok(oxc::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault( + cx.ast.alloc(with_default), + )) + } + other => { + let target = ox_binding_pattern_to_assignment_target(cx, other)?; + Ok(oxc::AssignmentTargetMaybeDefault::from(target)) + } + } +} + +/// Convert a destructuring `BindingProperty` into an `AssignmentTargetProperty`. +/// Shorthand properties (`{a}` / `{a = d}`) become identifier targets; the rest +/// become `name: target` properties. +fn ox_binding_property_to_assignment_target_property<'a>( + cx: &OxcContext<'a, '_, '_>, + prop: oxc::BindingProperty<'a>, +) -> Result, CompilerError> { + if prop.shorthand { + let (id, init) = match prop.value { + oxc::BindingPattern::BindingIdentifier(id) => (id.unbox(), None), + oxc::BindingPattern::AssignmentPattern(assign) => { + let assign = assign.unbox(); + match assign.left { + oxc::BindingPattern::BindingIdentifier(id) => (id.unbox(), Some(assign.right)), + _ => return Err(invariant_err("Unexpected shorthand default target", None)), + } + } + _ => return Err(invariant_err("Unexpected shorthand property value", None)), + }; + Ok(cx.ast.assignment_target_property_assignment_target_property_identifier( + SPAN, + cx.ast.identifier_reference(SPAN, id.name), + init, + )) + } else { + let binding = ox_binding_pattern_to_maybe_default(cx, prop.value)?; + let property = + cx.ast.assignment_target_property_property(SPAN, prop.key, binding, prop.computed); + Ok(oxc::AssignmentTargetProperty::AssignmentTargetPropertyProperty(cx.ast.alloc(property))) + } +} + +/// Convert an expression to a `SimpleAssignmentTarget` for update expressions. +fn ox_expression_to_simple_assignment_target<'a>( + cx: &OxcContext<'a, '_, '_>, + expr: oxc::Expression<'a>, +) -> Result, CompilerError> { + match expr { + oxc::Expression::Identifier(id) => { + let id = id.unbox(); + Ok(oxc::SimpleAssignmentTarget::AssignmentTargetIdentifier( + cx.ast.alloc_identifier_reference(SPAN, id.name), + )) + } + oxc::Expression::StaticMemberExpression(m) => { + Ok(oxc::SimpleAssignmentTarget::from(oxc::MemberExpression::StaticMemberExpression(m))) + } + oxc::Expression::ComputedMemberExpression(m) => Ok(oxc::SimpleAssignmentTarget::from( + oxc::MemberExpression::ComputedMemberExpression(m), + )), + _ => Err(invariant_err("Expected a simple assignment target for update expression", None)), + } +} + +// ============================================================================= +// Deferred sub-emitters (later batches): function/object/jsx expression codegen. +// ============================================================================= + +fn ox_codegen_function_expression<'a>( + cx: &mut OxcContext<'a, '_, '_>, + name: &Option, + name_hint: &Option, + lowered_func: &crate::react_compiler_hir::LoweredFunction, + expr_type: &FunctionExpressionType, +) -> Result, CompilerError> { + let func = cx.env.functions[lowered_func.func.0 as usize].clone(); + let mut reactive_fn = build_reactive_function(&func, cx.env)?; + prune_unused_labels(&mut reactive_fn, cx.env)?; + prune_unused_lvalues(&mut reactive_fn, cx.env); + prune_hoisted_contexts(&mut reactive_fn, cx.env)?; + + let fn_result = ox_codegen_inner_function(cx, &reactive_fn)?; + + let value = match expr_type { + FunctionExpressionType::ArrowFunctionExpression => { + let mut fn_result = fn_result; + // Optimize single-return arrow functions into expression bodies. + let single_return_arg = if fn_result.body.statements.len() == 1 + && reactive_fn.directives.is_empty() + && matches!( + fn_result.body.statements.last(), + Some(oxc::Statement::ReturnStatement(ret)) if ret.argument.is_some() + ) { + let stmt = fn_result.body.statements.pop().unwrap(); + let oxc::Statement::ReturnStatement(ret) = stmt else { unreachable!() }; + ret.unbox().argument + } else { + None + }; + match single_return_arg { + Some(arg) => { + let stmts = cx.ast.vec1(cx.ast.statement_expression(SPAN, arg)); + let body = cx.ast.alloc_function_body(SPAN, cx.ast.vec(), stmts); + ox_build_arrow(cx, fn_result.params, body, fn_result.is_async, true) + } + None => { + ox_build_arrow(cx, fn_result.params, fn_result.body, fn_result.is_async, false) + } + } + } + _ => { + let id = name.as_ref().map(|n| cx.ast.binding_identifier(SPAN, ox_str(&cx.ast, n))); + let func = cx.ast.function( + SPAN, + oxc::FunctionType::FunctionExpression, + id, + fn_result.generator, + fn_result.is_async, + false, + None::>, + None::>, + fn_result.params, + None::>, + Some(fn_result.body), + ); + oxc::Expression::FunctionExpression(cx.ast.alloc(func)) + } + }; + + // enableNameAnonymousFunctions: `({ "": })[""]` + if cx.env.config.enable_name_anonymous_functions && name.is_none() && name_hint.is_some() { + let hint = name_hint.as_ref().unwrap().clone(); + let key = oxc::PropertyKey::from(cx.ast.expression_string_literal( + SPAN, + ox_str(&cx.ast, &hint), + None, + )); + let prop = + cx.ast.object_property(SPAN, oxc::PropertyKind::Init, key, value, false, false, false); + let props = cx.ast.vec1(oxc::ObjectPropertyKind::ObjectProperty(cx.ast.alloc(prop))); + let object = cx.ast.expression_object(SPAN, props); + let member = cx.ast.member_expression_computed( + SPAN, + object, + cx.ast.expression_string_literal(SPAN, ox_str(&cx.ast, &hint), None), + false, + ); + return Ok(OxValue::Expression(oxc::Expression::from(member))); + } + + Ok(OxValue::Expression(value)) +} + +fn ox_build_arrow<'a>( + cx: &OxcContext<'a, '_, '_>, + params: oxc_allocator::Box<'a, oxc::FormalParameters<'a>>, + body: oxc_allocator::Box<'a, oxc::FunctionBody<'a>>, + is_async: bool, + expression: bool, +) -> oxc::Expression<'a> { + cx.ast.expression_arrow_function( + SPAN, + expression, + is_async, + None::>, + params, + None::>, + body, + ) +} + +/// Run the inner-function codegen with a fresh context (mirrors the Babel reference's +/// `Context::new` + `codegen_reactive_function` for function/object-method expressions). +fn ox_codegen_inner_function<'a, 'h>( + cx: &mut OxcContext<'a, '_, 'h>, + reactive_fn: &ReactiveFunction<'h>, +) -> Result, CompilerError> { + let fn_name = reactive_fn.id.as_deref().unwrap_or("[[ anonymous ]]").to_string(); + let mut inner_cx = OxcContext::new( + cx.ast, + cx.env, + fn_name, + cx.unique_identifiers.clone(), + cx.fbt_operands.clone(), + ); + inner_cx.temp = ox_clone_temporaries(&cx.ast, &cx.temp); + ox_codegen_reactive_function(&mut inner_cx, reactive_fn) +} + +fn ox_codegen_object_expression<'a>( + cx: &mut OxcContext<'a, '_, '_>, + properties: &[ObjectPropertyOrSpread], +) -> Result, CompilerError> { + let mut props: oxc_allocator::Vec<'a, oxc::ObjectPropertyKind<'a>> = cx.ast.vec(); + for prop in properties { + match prop { + ObjectPropertyOrSpread::Property(obj_prop) => { + let (key, key_computed) = ox_codegen_object_property_key(cx, &obj_prop.key)?; + match obj_prop.property_type { + ObjectPropertyType::Property => { + let value = ox_codegen_place_to_expression(cx, &obj_prop.place)?; + let shorthand = !key_computed + && matches!( + (&key, &value), + ( + oxc::PropertyKey::StaticIdentifier(k), + oxc::Expression::Identifier(v), + ) if k.name == v.name + ); + let p = cx.ast.object_property( + SPAN, + oxc::PropertyKind::Init, + key, + value, + false, + shorthand, + key_computed, + ); + props.push(oxc::ObjectPropertyKind::ObjectProperty(cx.ast.alloc(p))); + } + ObjectPropertyType::Method => { + let method_data = + cx.object_methods.get(&obj_prop.place.identifier).cloned(); + let Some((InstructionValue::ObjectMethod { lowered_func, .. }, _)) = + method_data + else { + return Err(invariant_err("Expected ObjectMethod instruction", None)); + }; + + let func = cx.env.functions[lowered_func.func.0 as usize].clone(); + let mut reactive_fn = build_reactive_function(&func, cx.env)?; + prune_unused_labels(&mut reactive_fn, cx.env)?; + prune_unused_lvalues(&mut reactive_fn, cx.env); + + let fn_result = ox_codegen_inner_function(cx, &reactive_fn)?; + let method = cx.ast.function( + SPAN, + oxc::FunctionType::FunctionExpression, + None, + fn_result.generator, + fn_result.is_async, + false, + None::>, + None::>, + fn_result.params, + None::>, + Some(fn_result.body), + ); + let func_expr = oxc::Expression::FunctionExpression(cx.ast.alloc(method)); + let p = cx.ast.object_property( + SPAN, + oxc::PropertyKind::Init, + key, + func_expr, + true, + false, + key_computed, + ); + props.push(oxc::ObjectPropertyKind::ObjectProperty(cx.ast.alloc(p))); + } + } + } + ObjectPropertyOrSpread::Spread(spread) => { + let arg = ox_codegen_place_to_expression(cx, &spread.place)?; + let spread_el = cx.ast.spread_element(SPAN, arg); + props.push(oxc::ObjectPropertyKind::SpreadProperty(cx.ast.alloc(spread_el))); + } + } + } + Ok(OxValue::Expression(cx.ast.expression_object(SPAN, props))) +} + +// ============================================================================= +// JSX codegen (oxc) +// ============================================================================= + +fn ox_codegen_jsx_expression<'a>( + cx: &mut OxcContext<'a, '_, '_>, + tag: &JsxTag, + props: &[JsxAttribute], + children: &Option>, +) -> Result, CompilerError> { + let mut attributes: oxc_allocator::Vec<'a, oxc::JSXAttributeItem<'a>> = cx.ast.vec(); + for attr in props { + attributes.push(ox_codegen_jsx_attribute(cx, attr)?); + } + + let (tag_value, is_fbt_tag) = match tag { + JsxTag::Place(place) => (ox_codegen_place_to_expression(cx, place)?, false), + JsxTag::Builtin(builtin) => { + let is_fbt = SINGLE_CHILD_FBT_TAGS.contains(&builtin.name.as_str()); + (cx.ast.expression_string_literal(SPAN, ox_str(&cx.ast, &builtin.name), None), is_fbt) + } + }; + + let opening_name = ox_expression_to_jsx_tag(cx, &tag_value)?; + + let mut child_nodes: oxc_allocator::Vec<'a, oxc::JSXChild<'a>> = cx.ast.vec(); + if let Some(c) = children { + for child in c { + if is_fbt_tag { + child_nodes.push(ox_codegen_jsx_fbt_child_element(cx, child)?); + } else { + child_nodes.push(ox_codegen_jsx_element(cx, child)?); + } + } + } + + let is_self_closing = children.is_none(); + let opening = cx.ast.jsx_opening_element( + SPAN, + opening_name, + None::>, + attributes, + ); + let closing = if is_self_closing { + None + } else { + let closing_name = ox_expression_to_jsx_tag(cx, &tag_value)?; + Some(cx.ast.jsx_closing_element(SPAN, closing_name)) + }; + let element = cx.ast.jsx_element(SPAN, opening, child_nodes, closing); + Ok(OxValue::Expression(oxc::Expression::JSXElement(cx.ast.alloc(element)))) +} + +fn ox_string_requires_expr_container(s: &str) -> bool { + for c in s.chars() { + if STRING_REQUIRES_EXPR_CONTAINER_CHARS.contains(c) { + return true; + } + let code = c as u32; + if code <= 0x1F || code == 0x7F || (0x80..=0x9F).contains(&code) || code >= 0xA0 { + return true; + } + } + false +} + +pub(crate) fn ox_encode_jsx_text(raw: &str) -> String { + let mut escaped = String::with_capacity(raw.len()); + for ch in raw.chars() { + match ch { + '&' => escaped.push_str("&"), + '<' => escaped.push_str("<"), + '>' => escaped.push_str(">"), + '{' => escaped.push_str("{"), + '}' => escaped.push_str("}"), + _ => escaped.push(ch), + } + } + escaped +} + +fn ox_codegen_jsx_attribute<'a>( + cx: &mut OxcContext<'a, '_, '_>, + attr: &JsxAttribute, +) -> Result, CompilerError> { + match attr { + JsxAttribute::Attribute { name, place } => { + let prop_name = if name.contains(':') { + let parts: Vec<&str> = name.splitn(2, ':').collect(); + let namespace = cx.ast.jsx_identifier(SPAN, ox_str(&cx.ast, parts[0])); + let local = cx.ast.jsx_identifier(SPAN, ox_str(&cx.ast, parts[1])); + cx.ast.jsx_attribute_name_namespaced_name(SPAN, namespace, local) + } else { + cx.ast.jsx_attribute_name_identifier(SPAN, ox_str(&cx.ast, name)) + }; + + let is_fbt_operand = cx.fbt_operands.contains(&place.identifier); + let inner_value = ox_codegen_place_to_expression(cx, place)?; + let attr_value = match inner_value { + oxc::Expression::StringLiteral(ref s) + if !ox_string_requires_expr_container(s.value.as_str()) || is_fbt_operand => + { + let value = s.value; + Some(cx.ast.jsx_attribute_value_string_literal(SPAN, value, None)) + } + _ => { + let expr = oxc::JSXExpression::from(inner_value); + Some(cx.ast.jsx_attribute_value_expression_container(SPAN, expr)) + } + }; + Ok(cx.ast.jsx_attribute_item_attribute(SPAN, prop_name, attr_value)) + } + JsxAttribute::SpreadAttribute { argument } => { + let expr = ox_codegen_place_to_expression(cx, argument)?; + Ok(cx.ast.jsx_attribute_item_spread_attribute(SPAN, expr)) + } + } +} + +fn ox_codegen_jsx_element<'a>( + cx: &mut OxcContext<'a, '_, '_>, + place: &Place, +) -> Result, CompilerError> { + let value = ox_codegen_place(cx, place)?; + match value { + OxValue::JsxText(text) => { + let raw = text.value.as_str(); + if raw.contains(JSX_TEXT_CHILD_REQUIRES_EXPR_CONTAINER_PATTERN) { + let lit = cx.ast.expression_string_literal(SPAN, ox_str(&cx.ast, raw), None); + Ok(cx.ast.jsx_child_expression_container(SPAN, oxc::JSXExpression::from(lit))) + } else { + let encoded = ox_encode_jsx_text(raw); + Ok(cx.ast.jsx_child_text(SPAN, ox_str(&cx.ast, &encoded), None)) + } + } + OxValue::Expression(oxc::Expression::JSXElement(elem)) => { + let elem = elem.unbox(); + Ok(cx.ast.jsx_child_element( + SPAN, + elem.opening_element, + elem.children, + elem.closing_element, + )) + } + OxValue::Expression(oxc::Expression::JSXFragment(frag)) => { + let frag = frag.unbox(); + Ok(cx.ast.jsx_child_fragment( + SPAN, + frag.opening_fragment, + frag.children, + frag.closing_fragment, + )) + } + OxValue::Expression(expr) => { + Ok(cx.ast.jsx_child_expression_container(SPAN, oxc::JSXExpression::from(expr))) + } + } +} + +fn ox_codegen_jsx_fbt_child_element<'a>( + cx: &mut OxcContext<'a, '_, '_>, + place: &Place, +) -> Result, CompilerError> { + let value = ox_codegen_place(cx, place)?; + match value { + OxValue::JsxText(text) => { + let encoded = ox_encode_jsx_text(text.value.as_str()); + Ok(cx.ast.jsx_child_text(SPAN, ox_str(&cx.ast, &encoded), None)) + } + OxValue::Expression(oxc::Expression::JSXElement(elem)) => { + let elem = elem.unbox(); + Ok(cx.ast.jsx_child_element( + SPAN, + elem.opening_element, + elem.children, + elem.closing_element, + )) + } + OxValue::Expression(expr) => { + Ok(cx.ast.jsx_child_expression_container(SPAN, oxc::JSXExpression::from(expr))) + } + } +} + +/// Build a `JSXElementName` from a tag expression following the TS compiler's +/// identifier-reference rule (uppercase / contains-`.` names become references). +fn ox_expression_to_jsx_tag<'a>( + cx: &OxcContext<'a, '_, '_>, + expr: &oxc::Expression<'a>, +) -> Result, CompilerError> { + match expr { + oxc::Expression::Identifier(ident) => Ok(ox_jsx_element_name_from_ident(cx, &ident.name)), + oxc::Expression::StaticMemberExpression(_) + | oxc::Expression::ComputedMemberExpression(_) => { + let member = ox_convert_member_expression_to_jsx(cx, expr)?; + Ok(cx.ast.jsx_element_name_member_expression(SPAN, member.0, member.1)) + } + oxc::Expression::StringLiteral(s) => { + let tag_text = s.value.as_str(); + if tag_text.contains(':') { + let parts: Vec<&str> = tag_text.splitn(2, ':').collect(); + let namespace = cx.ast.jsx_identifier(SPAN, ox_str(&cx.ast, parts[0])); + let name = cx.ast.jsx_identifier(SPAN, ox_str(&cx.ast, parts[1])); + Ok(cx.ast.jsx_element_name_namespaced_name(SPAN, namespace, name)) + } else { + Ok(ox_jsx_element_name_from_ident(cx, tag_text)) + } + } + _ => Err(invariant_err("Expected JSX tag to be an identifier or string", None)), + } +} + +fn ox_jsx_element_name_from_ident<'a>( + cx: &OxcContext<'a, '_, '_>, + name: &str, +) -> oxc::JSXElementName<'a> { + let first_char = name.chars().next().unwrap_or('a'); + if first_char.is_uppercase() || name.contains('.') { + cx.ast.jsx_element_name_identifier_reference(SPAN, ox_str(&cx.ast, name)) + } else { + cx.ast.jsx_element_name_identifier(SPAN, ox_str(&cx.ast, name)) + } +} + +/// Convert an oxc member expression into a JSX member expression's +/// `(object, property)` pair. +fn ox_convert_member_expression_to_jsx<'a>( + cx: &OxcContext<'a, '_, '_>, + expr: &oxc::Expression<'a>, +) -> Result<(oxc::JSXMemberExpressionObject<'a>, oxc::JSXIdentifier<'a>), CompilerError> { + let oxc::Expression::StaticMemberExpression(me) = expr else { + return Err(invariant_err("Expected JSX member expression property to be a string", None)); + }; + let property = cx.ast.jsx_identifier(SPAN, ox_str(&cx.ast, me.property.name.as_str())); + let object = match &me.object { + oxc::Expression::Identifier(ident) => cx + .ast + .jsx_member_expression_object_identifier_reference(SPAN, ox_str(&cx.ast, &ident.name)), + oxc::Expression::StaticMemberExpression(_) => { + let inner = ox_convert_member_expression_to_jsx(cx, &me.object)?; + cx.ast.jsx_member_expression_object_member_expression(SPAN, inner.0, inner.1) + } + _ => { + return Err(invariant_err( + "Expected JSX member expression to be an identifier or nested member expression", + None, + )); + } + }; + Ok((object, property)) +} + +fn ox_maybe_wrap_hook_call<'a>( + cx: &OxcContext<'a, '_, '_>, + call_expr: oxc::Expression<'a>, + _callee_id: IdentifierId, +) -> Result, CompilerError> { + // enableEmitHookGuards wrapping is deferred to a later batch; the guard is + // off by default, so unwrapped calls match the differential floor. + if cx.env.hook_guard_name.is_some() + && cx.env.output_mode == crate::react_compiler_hir::environment::OutputMode::Client + { + return Err(invariant_err( + "Hook guard wrapping in oxc codegen is not yet ported (deferred to a later batch)", + None, + )); + } + Ok(call_expr) +} + +// ============================================================================= +// Operator conversions (HIR -> oxc) +// ============================================================================= + +fn ox_convert_binary_operator( + op: &crate::react_compiler_hir::BinaryOperator, +) -> oxc::BinaryOperator { + use crate::react_compiler_hir::BinaryOperator as Hir; + use oxc::BinaryOperator as Ox; + match op { + Hir::Equal => Ox::Equality, + Hir::NotEqual => Ox::Inequality, + Hir::StrictEqual => Ox::StrictEquality, + Hir::StrictNotEqual => Ox::StrictInequality, + Hir::LessThan => Ox::LessThan, + Hir::LessEqual => Ox::LessEqualThan, + Hir::GreaterThan => Ox::GreaterThan, + Hir::GreaterEqual => Ox::GreaterEqualThan, + Hir::ShiftLeft => Ox::ShiftLeft, + Hir::ShiftRight => Ox::ShiftRight, + Hir::UnsignedShiftRight => Ox::ShiftRightZeroFill, + Hir::Add => Ox::Addition, + Hir::Subtract => Ox::Subtraction, + Hir::Multiply => Ox::Multiplication, + Hir::Divide => Ox::Division, + Hir::Modulo => Ox::Remainder, + Hir::Exponent => Ox::Exponential, + Hir::BitwiseOr => Ox::BitwiseOR, + Hir::BitwiseXor => Ox::BitwiseXOR, + Hir::BitwiseAnd => Ox::BitwiseAnd, + Hir::In => Ox::In, + Hir::InstanceOf => Ox::Instanceof, + } +} + +fn ox_convert_unary_operator(op: &crate::react_compiler_hir::UnaryOperator) -> oxc::UnaryOperator { + use crate::react_compiler_hir::UnaryOperator as Hir; + use oxc::UnaryOperator as Ox; + match op { + Hir::Minus => Ox::UnaryNegation, + Hir::Plus => Ox::UnaryPlus, + Hir::Not => Ox::LogicalNot, + Hir::BitwiseNot => Ox::BitwiseNot, + Hir::TypeOf => Ox::Typeof, + Hir::Void => Ox::Void, + } +} + +fn ox_convert_logical_operator(op: &LogicalOperator) -> oxc::LogicalOperator { + match op { + LogicalOperator::And => oxc::LogicalOperator::And, + LogicalOperator::Or => oxc::LogicalOperator::Or, + LogicalOperator::NullishCoalescing => oxc::LogicalOperator::Coalesce, + } +} + +fn ox_convert_update_operator( + op: &crate::react_compiler_hir::UpdateOperator, +) -> oxc::UpdateOperator { + match op { + crate::react_compiler_hir::UpdateOperator::Increment => oxc::UpdateOperator::Increment, + crate::react_compiler_hir::UpdateOperator::Decrement => oxc::UpdateOperator::Decrement, + } +} + +// ============================================================================= +// Primitive / literal helpers (oxc) +// ============================================================================= + +fn ox_codegen_primitive_value<'a>( + ast: &oxc_ast::AstBuilder<'a>, + value: &PrimitiveValue, +) -> oxc::Expression<'a> { + match value { + PrimitiveValue::Number(n) => { + let f = n.value(); + if f.is_nan() { + ast.expression_identifier(SPAN, "NaN") + } else if f.is_infinite() { + if f > 0.0 { + ast.expression_identifier(SPAN, "Infinity") + } else { + ast.expression_unary( + SPAN, + oxc::UnaryOperator::UnaryNegation, + ast.expression_identifier(SPAN, "Infinity"), + ) + } + } else if f < 0.0 { + ast.expression_unary(SPAN, oxc::UnaryOperator::UnaryNegation, ox_number(ast, -f)) + } else { + ox_number(ast, f) + } + } + PrimitiveValue::Boolean(b) => ast.expression_boolean_literal(SPAN, *b), + PrimitiveValue::String(s) => { + ast.expression_string_literal(SPAN, ox_str(ast, &s.to_string_lossy()), None) + } + PrimitiveValue::Null => ast.expression_null_literal(SPAN), + PrimitiveValue::Undefined => ast.expression_identifier(SPAN, "undefined"), + } +} + +fn ox_parse_regexp_flags(flags_str: &str) -> oxc::RegExpFlags { + let mut flags = oxc::RegExpFlags::empty(); + for c in flags_str.chars() { + match c { + 'g' => flags |= oxc::RegExpFlags::G, + 'i' => flags |= oxc::RegExpFlags::I, + 'm' => flags |= oxc::RegExpFlags::M, + 's' => flags |= oxc::RegExpFlags::S, + 'u' => flags |= oxc::RegExpFlags::U, + 'y' => flags |= oxc::RegExpFlags::Y, + 'd' => flags |= oxc::RegExpFlags::D, + 'v' => flags |= oxc::RegExpFlags::V, + _ => {} + } + } + flags +} + +// ============================================================================= +// CountMemoBlockVisitor — uses ReactiveFunctionVisitor trait +// ============================================================================= + +/// Counts memo blocks and pruned memo blocks in a reactive function. +/// TS: `class CountMemoBlockVisitor extends ReactiveFunctionVisitor` +struct CountMemoBlockVisitor<'a, 'e> { + env: &'e Environment<'a>, +} + +struct CountMemoBlockState { + memo_blocks: u32, + memo_values: u32, + pruned_memo_blocks: u32, + pruned_memo_values: u32, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for CountMemoBlockVisitor<'a, 'e> { + type State = CountMemoBlockState; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_scope(&self, scope_block: &ReactiveScopeBlock<'a>, state: &mut CountMemoBlockState) { + state.memo_blocks += 1; + let scope = &self.env.scopes[scope_block.scope.0 as usize]; + state.memo_values += scope.declarations.len() as u32; + self.traverse_scope(scope_block, state); + } + + fn visit_pruned_scope( + &self, + scope_block: &PrunedReactiveScopeBlock<'a>, + state: &mut CountMemoBlockState, + ) { + state.pruned_memo_blocks += 1; + let scope = &self.env.scopes[scope_block.scope.0 as usize]; + state.pruned_memo_values += scope.declarations.len() as u32; + self.traverse_pruned_scope(scope_block, state); + } +} + +fn count_memo_blocks<'a>( + func: &ReactiveFunction<'a>, + env: &Environment<'a>, +) -> (u32, u32, u32, u32) { + let visitor = CountMemoBlockVisitor { env }; + let mut state = CountMemoBlockState { + memo_blocks: 0, + memo_values: 0, + pruned_memo_blocks: 0, + pruned_memo_values: 0, + }; + visit_reactive_function(func, &visitor, &mut state); + (state.memo_blocks, state.memo_values, state.pruned_memo_blocks, state.pruned_memo_values) +} + +fn codegen_label(id: BlockId) -> String { + format!("bb{}", id.0) +} + +fn get_instruction_value<'a, 'r>( + reactive_value: &'r ReactiveValue<'a>, +) -> Result<&'r InstructionValue<'a>, CompilerError> { + match reactive_value { + ReactiveValue::Instruction(iv) => Ok(iv), + _ => Err(invariant_err("Expected base instruction value", None)), + } +} + +fn invariant( + condition: bool, + reason: &str, + loc: Option, +) -> Result<(), CompilerError> { + if !condition { Err(invariant_err(reason, loc)) } else { Ok(()) } +} + +fn invariant_err(reason: &str, loc: Option) -> CompilerError { + // Use CompilerDiagnostic (with details array) to match TS CompilerError.invariant() + let mut err = CompilerError::new(); + err.push_diagnostic( + CompilerDiagnostic::new(ErrorCategory::Invariant, reason, None::).with_detail( + CompilerDiagnosticDetail::Error { + loc, + message: Some(reason.to_string()), + identifier_name: None, + }, + ), + ); + err +} + +fn invariant_err_with_detail_message( + reason: &str, + message: &str, + loc: Option, +) -> CompilerError { + let mut err = CompilerError::new(); + let diagnostic = crate::react_compiler_diagnostics::CompilerDiagnostic::new( + ErrorCategory::Invariant, + reason, + None::, + ) + .with_detail(crate::react_compiler_diagnostics::CompilerDiagnosticDetail::Error { + loc, + message: Some(message.to_string()), + identifier_name: None, + }); + err.push_diagnostic(diagnostic); + err +} + +fn compare_scope_dependency( + a: &crate::react_compiler_hir::ReactiveScopeDependency, + b: &crate::react_compiler_hir::ReactiveScopeDependency, + env: &Environment, +) -> std::cmp::Ordering { + let a_name = dep_to_sort_key(a, env); + let b_name = dep_to_sort_key(b, env); + a_name.cmp(&b_name) +} + +fn dep_to_sort_key( + dep: &crate::react_compiler_hir::ReactiveScopeDependency, + env: &Environment, +) -> String { + let ident = &env.identifiers[dep.identifier.0 as usize]; + let base = match &ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => n.clone(), + Some(crate::react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(), + None => format!("_t{}", dep.identifier.0), + }; + let mut parts = vec![base]; + for entry in &dep.path { + let prefix = if entry.optional { "?" } else { "" }; + let prop = match &entry.property { + PropertyLiteral::String(s) => s.clone(), + PropertyLiteral::Number(n) => format!("{}", n), + }; + parts.push(format!("{prefix}{prop}")); + } + parts.join(".") +} + +fn compare_scope_declaration( + a: &crate::react_compiler_hir::ReactiveScopeDeclaration, + b: &crate::react_compiler_hir::ReactiveScopeDeclaration, + env: &Environment, +) -> std::cmp::Ordering { + let a_name = ident_sort_key(a.identifier, env); + let b_name = ident_sort_key(b.identifier, env); + a_name.cmp(&b_name) +} + +fn ident_sort_key(id: IdentifierId, env: &Environment) -> String { + let ident = &env.identifiers[id.0 as usize]; + match &ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => n.clone(), + Some(crate::react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(), + None => format!("_t{}", id.0), + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/extract_scope_declarations_from_destructuring.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/extract_scope_declarations_from_destructuring.rs new file mode 100644 index 0000000000000..e9452ad46ac33 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/extract_scope_declarations_from_destructuring.rs @@ -0,0 +1,222 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! ExtractScopeDeclarationsFromDestructuring — handles destructuring patterns +//! where some bindings are scope declarations and others aren't. +//! +//! Corresponds to `src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::{ + DeclarationId, IdentifierId, IdentifierName, InstructionKind, InstructionValue, LValue, + ParamPattern, Place, ReactiveFunction, ReactiveInstruction, ReactiveScopeBlock, + ReactiveStatement, ReactiveValue, environment::Environment, visitors, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionTransform, Transformed, transform_reactive_function, +}; + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Extracts scope declarations from destructuring patterns where some bindings +/// are scope declarations and others aren't. +/// TS: `extractScopeDeclarationsFromDestructuring` +pub fn extract_scope_declarations_from_destructuring<'a>( + func: &mut ReactiveFunction<'a>, + env: &mut Environment<'a>, +) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + let mut declared: FxHashSet = FxHashSet::default(); + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + let identifier = &env.identifiers[place.identifier.0 as usize]; + declared.insert(identifier.declaration_id); + } + let mut transform = Transform { env }; + let mut state = ExtractState { declared }; + transform_reactive_function(func, &mut transform, &mut state) +} + +struct ExtractState { + declared: FxHashSet, +} + +struct Transform<'a, 'e> { + env: &'e mut Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for Transform<'a, 'e> { + type State = ExtractState; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut ExtractState, + ) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + let scope_data = &self.env.scopes[scope.scope.0 as usize]; + let decl_ids: Vec = scope_data + .declarations + .iter() + .map(|(_, d)| { + let identifier = &self.env.identifiers[d.identifier.0 as usize]; + identifier.declaration_id + }) + .collect(); + for decl_id in decl_ids { + state.declared.insert(decl_id); + } + self.traverse_scope(scope, state) + } + + fn transform_instruction( + &mut self, + instruction: &mut ReactiveInstruction<'a>, + state: &mut ExtractState, + ) -> Result>, crate::react_compiler_diagnostics::CompilerError> + { + self.visit_instruction(instruction, state)?; + + let mut extra_instructions: Option>> = None; + + if let ReactiveValue::Instruction(InstructionValue::Destructure { + lvalue, + value: _destr_value, + loc, + }) = &mut instruction.value + { + // Check if this is a mixed destructuring (some declared, some not) + let mut reassigned: FxHashSet = FxHashSet::default(); + let mut has_declaration = false; + + for place in visitors::each_pattern_operand(&lvalue.pattern) { + let identifier = &self.env.identifiers[place.identifier.0 as usize]; + if state.declared.contains(&identifier.declaration_id) { + reassigned.insert(place.identifier); + } else { + has_declaration = true; + } + } + + if !has_declaration { + // All reassignments + lvalue.kind = InstructionKind::Reassign; + } else if !reassigned.is_empty() { + // Mixed: replace reassigned items with temporaries and emit separate assignments + let mut renamed: Vec<(Place, Place)> = Vec::new(); + let instr_loc = instruction.loc.clone(); + let destr_loc = loc.clone(); + + let env = &mut *self.env; // reborrow + visitors::map_pattern_operands(&mut lvalue.pattern, &mut |place: Place| { + if !reassigned.contains(&place.identifier) { + return place; + } + // Create a temporary place (matches TS clonePlaceToTemporary) + let temp_id = env.next_identifier_id(); + let decl_id = env.identifiers[temp_id.0 as usize].declaration_id; + // Copy type from original identifier to temporary + let original_type = env.identifiers[place.identifier.0 as usize].type_; + env.identifiers[temp_id.0 as usize].type_ = original_type; + // Set identifier loc to the place's source location + // (matches TS makeTemporaryIdentifier which receives place.loc) + env.identifiers[temp_id.0 as usize].loc = place.loc.clone(); + // Promote the temporary + env.identifiers[temp_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#t{}", decl_id.0))); + let temporary = Place { + identifier: temp_id, + effect: place.effect, + reactive: place.reactive, + loc: None, // GeneratedSource — matches TS createTemporaryPlace + }; + let original = place; + renamed.push((original.clone(), temporary.clone())); + temporary + }); + + // Build extra StoreLocal instructions for each renamed place + let mut extra = Vec::new(); + for (original, temporary) in renamed { + extra.push(ReactiveInstruction { + id: instruction.id, + lvalue: None, + value: ReactiveValue::Instruction(InstructionValue::StoreLocal { + lvalue: LValue { kind: InstructionKind::Reassign, place: original }, + value: temporary, + type_annotation: None, + loc: destr_loc.clone(), + }), + effects: None, + loc: instr_loc.clone(), + }); + } + extra_instructions = Some(extra); + } + } + + // Update state.declared with declarations from the instruction(s) + if let Some(ref extras) = extra_instructions { + // Process the original instruction + update_declared_from_instruction(instruction, &self.env, state); + // Process extra instructions + for extra_instr in extras { + update_declared_from_instruction(extra_instr, &self.env, state); + } + } else { + update_declared_from_instruction(instruction, &self.env, state); + } + + if let Some(extras) = extra_instructions { + // Clone the original instruction and build the replacement list + let mut all_instructions = Vec::new(); + all_instructions.push(ReactiveStatement::Instruction(instruction.clone())); + for extra in extras { + all_instructions.push(ReactiveStatement::Instruction(extra)); + } + Ok(Transformed::ReplaceMany(all_instructions)) + } else { + Ok(Transformed::Keep) + } + } +} + +fn update_declared_from_instruction<'a>( + instr: &ReactiveInstruction<'a>, + env: &Environment<'a>, + state: &mut ExtractState, +) { + if let ReactiveValue::Instruction(iv) = &instr.value { + match iv { + InstructionValue::DeclareContext { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } + | InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } => { + if lvalue.kind != InstructionKind::Reassign { + let identifier = &env.identifiers[lvalue.place.identifier.0 as usize]; + state.declared.insert(identifier.declaration_id); + } + } + InstructionValue::Destructure { lvalue, .. } => { + if lvalue.kind != InstructionKind::Reassign { + for place in visitors::each_pattern_operand(&lvalue.pattern) { + let identifier = &env.identifiers[place.identifier.0 as usize]; + state.declared.insert(identifier.declaration_id); + } + } + } + _ => {} + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/merge_reactive_scopes_that_invalidate_together.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/merge_reactive_scopes_that_invalidate_together.rs new file mode 100644 index 0000000000000..4538180892858 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/merge_reactive_scopes_that_invalidate_together.rs @@ -0,0 +1,553 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! MergeReactiveScopesThatInvalidateTogether — merges adjacent or nested scopes +//! that share dependencies (and thus invalidate together) to reduce memoization overhead. +//! +//! Corresponds to `src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts`. + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_hir::{ + DeclarationId, DependencyPathEntry, EvaluationOrder, InstructionKind, InstructionValue, Place, + ReactiveBlock, ReactiveFunction, ReactiveScopeBlock, ReactiveScopeDependency, + ReactiveStatement, ReactiveValue, ScopeId, Type, + environment::Environment, + object_shape::{BUILT_IN_ARRAY_ID, BUILT_IN_FUNCTION_ID, BUILT_IN_JSX_ID, BUILT_IN_OBJECT_ID}, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionTransform, ReactiveFunctionVisitor, Transformed, transform_reactive_function, + visit_reactive_function, +}; + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Merges adjacent reactive scopes that share dependencies (invalidate together). +/// TS: `mergeReactiveScopesThatInvalidateTogether` +pub fn merge_reactive_scopes_that_invalidate_together<'a>( + func: &mut ReactiveFunction<'a>, + env: &mut Environment<'a>, +) -> Result<(), CompilerError> { + // Pass 1: find last usage of each declaration + let visitor = FindLastUsageVisitor { env: &*env }; + let mut last_usage: FxHashMap = FxHashMap::default(); + visit_reactive_function(func, &visitor, &mut last_usage); + + // Pass 2+3: merge scopes + let mut transform = MergeTransform { env, last_usage, temporaries: FxHashMap::default() }; + let mut state: Option> = None; + transform_reactive_function(func, &mut transform, &mut state) +} + +// ============================================================================= +// Pass 1: FindLastUsageVisitor +// ============================================================================= + +/// TS: `class FindLastUsageVisitor extends ReactiveFunctionVisitor` +struct FindLastUsageVisitor<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for FindLastUsageVisitor<'a, 'e> { + type State = FxHashMap; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_place(&self, id: EvaluationOrder, place: &Place, state: &mut Self::State) { + let decl_id = self.env.identifiers[place.identifier.0 as usize].declaration_id; + let entry = state.entry(decl_id).or_insert(id); + if id > *entry { + *entry = id; + } + } +} + +// ============================================================================= +// Pass 2+3: MergeTransform +// ============================================================================= + +/// TS: `class Transform extends ReactiveFunctionTransform` +struct MergeTransform<'a, 'e> { + env: &'e mut Environment<'a>, + last_usage: FxHashMap, + temporaries: FxHashMap, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for MergeTransform<'a, 'e> { + type State = Option>; + + fn env(&self) -> &Environment<'a> { + self.env + } + + /// TS: `override transformScope(scopeBlock, state)` + fn transform_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result>, CompilerError> { + let scope_deps = self.env.scopes[scope.scope.0 as usize].dependencies.clone(); + // Save parent state and recurse with this scope's deps as state + let parent_state = state.take(); + *state = Some(scope_deps.clone()); + self.visit_scope(scope, state)?; + // Restore parent state + *state = parent_state; + + // If parent has deps and they match, flatten the inner scope + if let Some(parent_deps) = state.as_ref() { + if are_equal_dependencies(parent_deps, &scope_deps, self.env) { + let instructions = std::mem::take(&mut scope.instructions); + return Ok(Transformed::ReplaceMany(instructions)); + } + } + Ok(Transformed::Keep) + } + + /// TS: `override visitBlock(block, state)` + fn visit_block( + &mut self, + block: &mut ReactiveBlock<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + // Pass 1: traverse nested (scope flattening handled by transform_scope) + self.traverse_block(block, state)?; + // Pass 2+3: merge consecutive scopes in this block + self.merge_scopes_in_block(block)?; + Ok(()) + } +} + +impl<'a, 'e> MergeTransform<'a, 'e> { + /// Identify and merge consecutive scopes that invalidate together. + fn merge_scopes_in_block( + &mut self, + block: &mut ReactiveBlock<'a>, + ) -> Result<(), CompilerError> { + // Pass 2: identify scopes for merging + struct MergedScope { + scope_id: ScopeId, + from: usize, + to: usize, + lvalues: FxHashSet, + } + + let mut current: Option = None; + let mut merged: Vec = Vec::new(); + + let block_len = block.len(); + for i in 0..block_len { + match &block[i] { + ReactiveStatement::Terminal(_) => { + // Don't merge across terminals + if let Some(c) = current.take() { + if c.to > c.from + 1 { + merged.push(c); + } + } + } + ReactiveStatement::PrunedScope(_) => { + // Don't merge across pruned scopes + if let Some(c) = current.take() { + if c.to > c.from + 1 { + merged.push(c); + } + } + } + ReactiveStatement::Instruction(instr) => { + match &instr.value { + ReactiveValue::Instruction(iv) => { + match iv { + InstructionValue::BinaryExpression { .. } + | InstructionValue::ComputedLoad { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::LoadLocal { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::PropertyLoad { .. } + | InstructionValue::TemplateLiteral { .. } + | InstructionValue::UnaryExpression { .. } => { + if let Some(ref mut c) = current { + if let Some(lvalue) = &instr.lvalue { + let decl_id = self.env.identifiers + [lvalue.identifier.0 as usize] + .declaration_id; + c.lvalues.insert(decl_id); + if let InstructionValue::LoadLocal { place, .. } = iv { + let src_decl = self.env.identifiers + [place.identifier.0 as usize] + .declaration_id; + self.temporaries.insert(decl_id, src_decl); + } + } + } + } + InstructionValue::StoreLocal { lvalue, value, .. } => { + if let Some(ref mut c) = current { + if lvalue.kind == InstructionKind::Const { + // Add the instruction lvalue (if any) + if let Some(instr_lvalue) = &instr.lvalue { + let decl_id = self.env.identifiers + [instr_lvalue.identifier.0 as usize] + .declaration_id; + c.lvalues.insert(decl_id); + } + // Add the StoreLocal's lvalue place + let store_decl = self.env.identifiers + [lvalue.place.identifier.0 as usize] + .declaration_id; + c.lvalues.insert(store_decl); + // Track temporary mapping + let value_decl = self.env.identifiers + [value.identifier.0 as usize] + .declaration_id; + let mapped = self + .temporaries + .get(&value_decl) + .copied() + .unwrap_or(value_decl); + self.temporaries.insert(store_decl, mapped); + } else { + // Non-const StoreLocal — reset + let c = current.take().unwrap(); + if c.to > c.from + 1 { + merged.push(c); + } + } + } + } + _ => { + // Other instructions prevent merging + if let Some(c) = current.take() { + if c.to > c.from + 1 { + merged.push(c); + } + } + } + } + } + _ => { + // Non-Instruction reactive values prevent merging + if let Some(c) = current.take() { + if c.to > c.from + 1 { + merged.push(c); + } + } + } + } + } + ReactiveStatement::Scope(scope_block) => { + let next_scope_id = scope_block.scope; + if let Some(ref mut c) = current { + let current_scope_id = c.scope_id; + if can_merge_scopes( + current_scope_id, + next_scope_id, + self.env, + &self.temporaries, + ) && are_lvalues_last_used_by_scope( + next_scope_id, + &c.lvalues, + &self.last_usage, + self.env, + ) { + // Merge: extend the current scope's range + let next_range_end = + self.env.scopes[next_scope_id.0 as usize].range.end; + let current_range_end = + self.env.scopes[current_scope_id.0 as usize].range.end; + self.env.scopes[current_scope_id.0 as usize].range.end = + EvaluationOrder(current_range_end.0.max(next_range_end.0)); + + // Merge declarations from next into current + let next_decls = + self.env.scopes[next_scope_id.0 as usize].declarations.clone(); + for (key, value) in next_decls { + let current_decls = + &mut self.env.scopes[current_scope_id.0 as usize].declarations; + if let Some(existing) = + current_decls.iter_mut().find(|(k, _)| *k == key) + { + existing.1 = value; + } else { + current_decls.push((key, value)); + } + } + + // Prune declarations that are no longer used after the merged scope + update_scope_declarations(current_scope_id, &self.last_usage, self.env); + + c.to = i + 1; + c.lvalues.clear(); + + if !scope_is_eligible_for_merging(next_scope_id, self.env) { + let c = current.take().unwrap(); + if c.to > c.from + 1 { + merged.push(c); + } + } + } else { + // Cannot merge — reset + let c = current.take().unwrap(); + if c.to > c.from + 1 { + merged.push(c); + } + // Start new candidate if eligible + if scope_is_eligible_for_merging(next_scope_id, self.env) { + current = Some(MergedScope { + scope_id: next_scope_id, + from: i, + to: i + 1, + lvalues: FxHashSet::default(), + }); + } + } + } else { + // No current — start new candidate if eligible + if scope_is_eligible_for_merging(next_scope_id, self.env) { + current = Some(MergedScope { + scope_id: next_scope_id, + from: i, + to: i + 1, + lvalues: FxHashSet::default(), + }); + } + } + } + } + } + // Flush remaining + if let Some(c) = current.take() { + if c.to > c.from + 1 { + merged.push(c); + } + } + + // Pass 3: apply merges + if merged.is_empty() { + return Ok(()); + } + + let mut next_instructions: Vec> = Vec::new(); + let mut index = 0; + let all_stmts: Vec> = std::mem::take(block); + + for entry in &merged { + // Push everything before the merge range + while index < entry.from { + next_instructions.push(all_stmts[index].clone()); + index += 1; + } + // The first item in the merge range must be a scope + let mut merged_scope = match &all_stmts[entry.from] { + ReactiveStatement::Scope(s) => s.clone(), + _ => { + return Err(crate::react_compiler_diagnostics::CompilerDiagnostic::new( + crate::react_compiler_diagnostics::ErrorCategory::Invariant, + "MergeConsecutiveScopes: Expected scope at starting index", + None, + ) + .into()); + } + }; + index += 1; + while index < entry.to { + let stmt = &all_stmts[index]; + index += 1; + match stmt { + ReactiveStatement::Scope(inner_scope) => { + merged_scope.instructions.extend(inner_scope.instructions.clone()); + self.env.scopes[merged_scope.scope.0 as usize] + .merged + .push(inner_scope.scope); + } + _ => { + merged_scope.instructions.push(stmt.clone()); + } + } + } + next_instructions.push(ReactiveStatement::Scope(merged_scope)); + } + // Push remaining + while index < all_stmts.len() { + next_instructions.push(all_stmts[index].clone()); + index += 1; + } + + *block = next_instructions; + Ok(()) + } +} + +// ============================================================================= +// Helper functions +// ============================================================================= + +/// Updates scope declarations to remove any that are not used after the scope. +fn update_scope_declarations<'a>( + scope_id: ScopeId, + last_usage: &FxHashMap, + env: &mut Environment<'a>, +) { + let range_end = env.scopes[scope_id.0 as usize].range.end; + env.scopes[scope_id.0 as usize].declarations.retain(|(_id, decl)| { + let decl_declaration_id = env.identifiers[decl.identifier.0 as usize].declaration_id; + match last_usage.get(&decl_declaration_id) { + Some(last_used_at) => *last_used_at >= range_end, + // If not tracked, keep the declaration (conservative) + None => true, + } + }); +} + +/// Returns whether all lvalues are last used at or before the given scope. +fn are_lvalues_last_used_by_scope<'a>( + scope_id: ScopeId, + lvalues: &FxHashSet, + last_usage: &FxHashMap, + env: &Environment<'a>, +) -> bool { + let range_end = env.scopes[scope_id.0 as usize].range.end; + for lvalue in lvalues { + if let Some(&last_used_at) = last_usage.get(lvalue) { + if last_used_at >= range_end { + return false; + } + } + } + true +} + +/// Check if two scopes can be merged. +fn can_merge_scopes<'a>( + current_id: ScopeId, + next_id: ScopeId, + env: &Environment<'a>, + temporaries: &FxHashMap, +) -> bool { + let current = &env.scopes[current_id.0 as usize]; + let next = &env.scopes[next_id.0 as usize]; + + // Don't merge scopes with reassignments + if !current.reassignments.is_empty() || !next.reassignments.is_empty() { + return false; + } + + // Merge scopes whose dependencies are identical + if are_equal_dependencies(¤t.dependencies, &next.dependencies, env) { + return true; + } + + // Merge scopes where outputs of current are inputs of next + // Build synthetic dependencies from current's declarations + let current_decl_deps: Vec = current + .declarations + .iter() + .map(|(_key, decl)| ReactiveScopeDependency { + identifier: decl.identifier, + reactive: true, + path: Vec::new(), + loc: None, + }) + .collect(); + + if are_equal_dependencies(¤t_decl_deps, &next.dependencies, env) { + return true; + } + + // Check if all next deps have empty paths, always-invalidating types, + // and correspond to current declarations (possibly through temporaries) + if !next.dependencies.is_empty() + && next.dependencies.iter().all(|dep| { + if !dep.path.is_empty() { + return false; + } + let dep_type = &env.types[env.identifiers[dep.identifier.0 as usize].type_.0 as usize]; + if !is_always_invalidating_type(dep_type) { + return false; + } + let dep_decl = env.identifiers[dep.identifier.0 as usize].declaration_id; + current.declarations.iter().any(|(_key, decl)| { + let decl_decl_id = env.identifiers[decl.identifier.0 as usize].declaration_id; + decl_decl_id == dep_decl + || temporaries.get(&dep_decl).copied() == Some(decl_decl_id) + }) + }) + { + return true; + } + + false +} + +/// Check if a type is always invalidating (guaranteed to change when inputs change). +pub fn is_always_invalidating_type(ty: &Type) -> bool { + match ty { + Type::Object { shape_id } => { + if let Some(id) = shape_id { + matches!( + id.as_str(), + s if s == BUILT_IN_ARRAY_ID + || s == BUILT_IN_OBJECT_ID + || s == BUILT_IN_FUNCTION_ID + || s == BUILT_IN_JSX_ID + ) + } else { + false + } + } + Type::Function { .. } => true, + _ => false, + } +} + +/// Check if two dependency lists are equal. +fn are_equal_dependencies<'a>( + a: &[ReactiveScopeDependency], + b: &[ReactiveScopeDependency], + env: &Environment<'a>, +) -> bool { + if a.len() != b.len() { + return false; + } + for a_val in a { + let a_decl = env.identifiers[a_val.identifier.0 as usize].declaration_id; + let found = b.iter().any(|b_val| { + let b_decl = env.identifiers[b_val.identifier.0 as usize].declaration_id; + a_decl == b_decl && are_equal_paths(&a_val.path, &b_val.path) + }); + if !found { + return false; + } + } + true +} + +/// Check if two dependency paths are equal. +fn are_equal_paths(a: &[DependencyPathEntry], b: &[DependencyPathEntry]) -> bool { + a.len() == b.len() + && a.iter() + .zip(b.iter()) + .all(|(ai, bi)| ai.property == bi.property && ai.optional == bi.optional) +} + +/// Check if a scope is eligible for merging with subsequent scopes. +fn scope_is_eligible_for_merging<'a>(scope_id: ScopeId, env: &Environment<'a>) -> bool { + let scope = &env.scopes[scope_id.0 as usize]; + if scope.dependencies.is_empty() { + // No dependencies means output never changes — eligible + return true; + } + scope.declarations.iter().any(|(_key, decl)| { + let ty = &env.types[env.identifiers[decl.identifier.0 as usize].type_.0 as usize]; + is_always_invalidating_type(ty) + }) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/mod.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/mod.rs new file mode 100644 index 0000000000000..ed452bdde3e1d --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/mod.rs @@ -0,0 +1,77 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Reactive scope passes for the React Compiler. +//! +//! Converts the HIR CFG into a tree-structured `ReactiveFunction` and runs +//! scope-related transformation passes (pruning, merging, renaming, etc.). +//! +//! Corresponds to `src/ReactiveScopes/` in the TypeScript compiler. + +mod assert_scope_instructions_within_scopes; +mod assert_well_formed_break_targets; +mod build_reactive_function; +pub mod codegen_reactive_function; +mod extract_scope_declarations_from_destructuring; +mod merge_reactive_scopes_that_invalidate_together; +#[cfg(feature = "debug")] +pub mod print_reactive_function; +/// Stub when the `debug` feature is off: the pipeline calls these in its +/// `if debug_enabled` blocks; keep the signatures, drop the IR printing. +#[cfg(not(feature = "debug"))] +pub mod print_reactive_function { + use crate::react_compiler_hir::HirFunction; + use crate::react_compiler_hir::ReactiveFunction; + use crate::react_compiler_hir::environment::Environment; + use crate::react_compiler_hir::print::PrintFormatter; + + pub type HirFunctionFormatter<'h> = dyn Fn(&mut PrintFormatter<'_, 'h>, &HirFunction<'h>); + + pub fn debug_reactive_function<'h>( + _func: &ReactiveFunction<'h>, + _env: &Environment<'h>, + ) -> String { + String::new() + } + + pub fn debug_reactive_function_with_formatter<'h>( + _func: &ReactiveFunction<'h>, + _env: &Environment<'h>, + _hir_formatter: Option<&HirFunctionFormatter<'h>>, + ) -> String { + String::new() + } +} +mod promote_used_temporaries; +mod propagate_early_returns; +mod prune_always_invalidating_scopes; +mod prune_hoisted_contexts; +mod prune_non_escaping_scopes; +mod prune_non_reactive_dependencies; +mod prune_unused_labels; +mod prune_unused_lvalues; +mod prune_unused_scopes; +mod rename_variables; +mod stabilize_block_ids; +pub mod visitors; + +pub use assert_scope_instructions_within_scopes::assert_scope_instructions_within_scopes; +pub use assert_well_formed_break_targets::assert_well_formed_break_targets; +pub use build_reactive_function::build_reactive_function; +pub use codegen_reactive_function::codegen_function; +pub use extract_scope_declarations_from_destructuring::extract_scope_declarations_from_destructuring; +pub use merge_reactive_scopes_that_invalidate_together::merge_reactive_scopes_that_invalidate_together; +pub use print_reactive_function::debug_reactive_function; +pub use promote_used_temporaries::promote_used_temporaries; +pub use propagate_early_returns::propagate_early_returns; +pub use prune_always_invalidating_scopes::prune_always_invalidating_scopes; +pub use prune_hoisted_contexts::prune_hoisted_contexts; +pub use prune_non_escaping_scopes::prune_non_escaping_scopes; +pub use prune_non_reactive_dependencies::prune_non_reactive_dependencies; +pub use prune_unused_labels::prune_unused_labels; +pub use prune_unused_lvalues::prune_unused_lvalues; +pub use prune_unused_scopes::prune_unused_scopes; +pub use rename_variables::rename_variables; +pub use stabilize_block_ids::stabilize_block_ids; diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/print_reactive_function.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/print_reactive_function.rs new file mode 100644 index 0000000000000..3386b7f0e2cc8 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/print_reactive_function.rs @@ -0,0 +1,551 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Verbose debug printer for ReactiveFunction. +//! +//! Produces output identical to the TS `printDebugReactiveFunction`. +//! Delegates shared formatting (Places, Identifiers, Scopes, Types, +//! InstructionValues, Effects, Errors) to `crate::react_compiler_hir::print::PrintFormatter`. + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::print::{self, PrintFormatter}; +use crate::react_compiler_hir::{ + HirFunction, ParamPattern, ReactiveBlock, ReactiveFunction, ReactiveInstruction, + ReactiveStatement, ReactiveTerminal, ReactiveTerminalStatement, ReactiveValue, +}; + +// ============================================================================= +// DebugPrinter — thin wrapper around PrintFormatter for reactive-specific logic +// ============================================================================= + +pub struct DebugPrinter<'a, 'h> { + pub fmt: PrintFormatter<'a, 'h>, + /// Optional formatter for HIR functions (used for inner functions in FunctionExpression/ObjectMethod) + pub hir_formatter: Option<&'a HirFunctionFormatter<'h>>, +} + +impl<'a, 'h> DebugPrinter<'a, 'h> { + pub fn new(env: &'a Environment<'h>) -> Self { + Self { fmt: PrintFormatter::new(env), hir_formatter: None } + } + + // ========================================================================= + // ReactiveFunction + // ========================================================================= + + pub fn format_reactive_function(&mut self, func: &ReactiveFunction<'h>) { + self.fmt.indent(); + self.fmt.line(&format!( + "id: {}", + match &func.id { + Some(id) => format!("\"{}\"", id), + None => "null".to_string(), + } + )); + self.fmt.line(&format!( + "name_hint: {}", + match &func.name_hint { + Some(h) => format!("\"{}\"", h), + None => "null".to_string(), + } + )); + self.fmt.line(&format!("generator: {}", func.generator)); + self.fmt.line(&format!("is_async: {}", func.is_async)); + self.fmt.line(&format!("loc: {}", print::format_loc(&func.loc))); + + // params + self.fmt.line("params:"); + self.fmt.indent(); + for (i, param) in func.params.iter().enumerate() { + match param { + ParamPattern::Place(place) => { + self.fmt.format_place_field(&format!("[{}]", i), place); + } + ParamPattern::Spread(spread) => { + self.fmt.line(&format!("[{}] Spread:", i)); + self.fmt.indent(); + self.fmt.format_place_field("place", &spread.place); + self.fmt.dedent(); + } + } + } + self.fmt.dedent(); + + // directives + self.fmt.line("directives:"); + self.fmt.indent(); + for (i, d) in func.directives.iter().enumerate() { + self.fmt.line(&format!("[{}] \"{}\"", i, d)); + } + self.fmt.dedent(); + + self.fmt.line(""); + self.fmt.line("Body:"); + self.fmt.indent(); + self.format_reactive_block(&func.body); + self.fmt.dedent(); + self.fmt.dedent(); + } + + // ========================================================================= + // ReactiveBlock + // ========================================================================= + + fn format_reactive_block(&mut self, block: &ReactiveBlock<'h>) { + for stmt in block.iter() { + self.format_reactive_statement(stmt); + } + } + + fn format_reactive_statement(&mut self, stmt: &ReactiveStatement<'h>) { + match stmt { + ReactiveStatement::Instruction(instr) => { + self.format_reactive_instruction_block(instr); + } + ReactiveStatement::Terminal(term) => { + self.fmt.line("ReactiveTerminalStatement {"); + self.fmt.indent(); + self.format_terminal_statement(term); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveStatement::Scope(scope) => { + self.fmt.line("ReactiveScopeBlock {"); + self.fmt.indent(); + self.fmt.format_scope_field("scope", scope.scope); + self.fmt.line("instructions:"); + self.fmt.indent(); + self.format_reactive_block(&scope.instructions); + self.fmt.dedent(); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveStatement::PrunedScope(scope) => { + self.fmt.line("PrunedReactiveScopeBlock {"); + self.fmt.indent(); + self.fmt.format_scope_field("scope", scope.scope); + self.fmt.line("instructions:"); + self.fmt.indent(); + self.format_reactive_block(&scope.instructions); + self.fmt.dedent(); + self.fmt.dedent(); + self.fmt.line("}"); + } + } + } + + // ========================================================================= + // ReactiveInstruction + // ========================================================================= + + fn format_reactive_instruction_block(&mut self, instr: &ReactiveInstruction<'h>) { + self.fmt.line("ReactiveInstruction {"); + self.fmt.indent(); + self.format_reactive_instruction(instr); + self.fmt.dedent(); + self.fmt.line("}"); + } + + fn format_reactive_instruction(&mut self, instr: &ReactiveInstruction<'h>) { + self.fmt.line(&format!("id: {}", instr.id.0)); + match &instr.lvalue { + Some(place) => self.fmt.format_place_field("lvalue", place), + None => self.fmt.line("lvalue: null"), + } + self.fmt.line("value:"); + self.fmt.indent(); + self.format_reactive_value(&instr.value); + self.fmt.dedent(); + match &instr.effects { + Some(effects) => { + self.fmt.line("effects:"); + self.fmt.indent(); + for (i, eff) in effects.iter().enumerate() { + self.fmt.line(&format!("[{}] {}", i, self.fmt.format_effect(eff))); + } + self.fmt.dedent(); + } + None => self.fmt.line("effects: null"), + } + self.fmt.line(&format!("loc: {}", print::format_loc(&instr.loc))); + } + + // ========================================================================= + // ReactiveValue + // ========================================================================= + + fn format_reactive_value(&mut self, value: &ReactiveValue<'h>) { + match value { + ReactiveValue::Instruction(iv) => { + // Build the inner function formatter callback if we have an hir_formatter + let hir_formatter = self.hir_formatter; + let inner_func_cb: Option< + Box, &HirFunction<'h>) + '_>, + > = hir_formatter.map(|hf| { + Box::new(move |fmt: &mut PrintFormatter<'_, 'h>, func: &HirFunction<'h>| { + hf(fmt, func); + }) + as Box, &HirFunction<'h>) + '_> + }); + self.fmt.format_instruction_value( + iv, + inner_func_cb.as_ref().map(|cb| { + cb.as_ref() as &dyn Fn(&mut PrintFormatter<'_, 'h>, &HirFunction<'h>) + }), + ); + } + ReactiveValue::LogicalExpression { operator, left, right, loc } => { + self.fmt.line("LogicalExpression {"); + self.fmt.indent(); + self.fmt.line(&format!("operator: \"{}\"", operator)); + self.fmt.line("left:"); + self.fmt.indent(); + self.format_reactive_value(left); + self.fmt.dedent(); + self.fmt.line("right:"); + self.fmt.indent(); + self.format_reactive_value(right); + self.fmt.dedent(); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, loc } => { + self.fmt.line("ConditionalExpression {"); + self.fmt.indent(); + self.fmt.line("test:"); + self.fmt.indent(); + self.format_reactive_value(test); + self.fmt.dedent(); + self.fmt.line("consequent:"); + self.fmt.indent(); + self.format_reactive_value(consequent); + self.fmt.dedent(); + self.fmt.line("alternate:"); + self.fmt.indent(); + self.format_reactive_value(alternate); + self.fmt.dedent(); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveValue::SequenceExpression { instructions, id, value, loc } => { + self.fmt.line("SequenceExpression {"); + self.fmt.indent(); + self.fmt.line("instructions:"); + self.fmt.indent(); + for (i, instr) in instructions.iter().enumerate() { + self.fmt.line(&format!("[{}]:", i)); + self.fmt.indent(); + self.format_reactive_instruction_block(instr); + self.fmt.dedent(); + } + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line("value:"); + self.fmt.indent(); + self.format_reactive_value(value); + self.fmt.dedent(); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveValue::OptionalExpression { id, value, optional, loc } => { + self.fmt.line("OptionalExpression {"); + self.fmt.indent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line("value:"); + self.fmt.indent(); + self.format_reactive_value(value); + self.fmt.dedent(); + self.fmt.line(&format!("optional: {}", optional)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + } + } + + // ========================================================================= + // ReactiveTerminal + // ========================================================================= + + fn format_terminal_statement(&mut self, stmt: &ReactiveTerminalStatement<'h>) { + match &stmt.label { + Some(label) => { + self.fmt.line(&format!( + "label: {{ id: bb{}, implicit: {} }}", + label.id.0, label.implicit + )); + } + None => self.fmt.line("label: null"), + } + self.fmt.line("terminal:"); + self.fmt.indent(); + self.format_reactive_terminal(&stmt.terminal); + self.fmt.dedent(); + } + + fn format_reactive_terminal(&mut self, terminal: &ReactiveTerminal<'h>) { + match terminal { + ReactiveTerminal::Break { target, id, target_kind, loc } => { + self.fmt.line("Break {"); + self.fmt.indent(); + self.fmt.line(&format!("target: bb{}", target.0)); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("targetKind: \"{}\"", target_kind)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::Continue { target, id, target_kind, loc } => { + self.fmt.line("Continue {"); + self.fmt.indent(); + self.fmt.line(&format!("target: bb{}", target.0)); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("targetKind: \"{}\"", target_kind)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::Return { value, id, loc } => { + self.fmt.line("Return {"); + self.fmt.indent(); + self.fmt.format_place_field("value", value); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::Throw { value, id, loc } => { + self.fmt.line("Throw {"); + self.fmt.indent(); + self.fmt.format_place_field("value", value); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::Switch { test, cases, id, loc } => { + self.fmt.line("Switch {"); + self.fmt.indent(); + self.fmt.format_place_field("test", test); + self.fmt.line("cases:"); + self.fmt.indent(); + for (i, case) in cases.iter().enumerate() { + self.fmt.line(&format!("[{}] {{", i)); + self.fmt.indent(); + match &case.test { + Some(p) => { + self.fmt.format_place_field("test", p); + } + None => { + self.fmt.line("test: null"); + } + } + match &case.block { + Some(block) => { + self.fmt.line("block:"); + self.fmt.indent(); + self.format_reactive_block(block); + self.fmt.dedent(); + } + None => self.fmt.line("block: undefined"), + } + self.fmt.dedent(); + self.fmt.line("}"); + } + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::DoWhile { loop_block, test, id, loc } => { + self.fmt.line("DoWhile {"); + self.fmt.indent(); + self.fmt.line("loop:"); + self.fmt.indent(); + self.format_reactive_block(loop_block); + self.fmt.dedent(); + self.fmt.line("test:"); + self.fmt.indent(); + self.format_reactive_value(test); + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::While { test, loop_block, id, loc } => { + self.fmt.line("While {"); + self.fmt.indent(); + self.fmt.line("test:"); + self.fmt.indent(); + self.format_reactive_value(test); + self.fmt.dedent(); + self.fmt.line("loop:"); + self.fmt.indent(); + self.format_reactive_block(loop_block); + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::For { init, test, update, loop_block, id, loc } => { + self.fmt.line("For {"); + self.fmt.indent(); + self.fmt.line("init:"); + self.fmt.indent(); + self.format_reactive_value(init); + self.fmt.dedent(); + self.fmt.line("test:"); + self.fmt.indent(); + self.format_reactive_value(test); + self.fmt.dedent(); + match update { + Some(u) => { + self.fmt.line("update:"); + self.fmt.indent(); + self.format_reactive_value(u); + self.fmt.dedent(); + } + None => self.fmt.line("update: null"), + } + self.fmt.line("loop:"); + self.fmt.indent(); + self.format_reactive_block(loop_block); + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::ForOf { init, test, loop_block, id, loc } => { + self.fmt.line("ForOf {"); + self.fmt.indent(); + self.fmt.line("init:"); + self.fmt.indent(); + self.format_reactive_value(init); + self.fmt.dedent(); + self.fmt.line("test:"); + self.fmt.indent(); + self.format_reactive_value(test); + self.fmt.dedent(); + self.fmt.line("loop:"); + self.fmt.indent(); + self.format_reactive_block(loop_block); + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::ForIn { init, loop_block, id, loc } => { + self.fmt.line("ForIn {"); + self.fmt.indent(); + self.fmt.line("init:"); + self.fmt.indent(); + self.format_reactive_value(init); + self.fmt.dedent(); + self.fmt.line("loop:"); + self.fmt.indent(); + self.format_reactive_block(loop_block); + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::If { test, consequent, alternate, id, loc } => { + self.fmt.line("If {"); + self.fmt.indent(); + self.fmt.format_place_field("test", test); + self.fmt.line("consequent:"); + self.fmt.indent(); + self.format_reactive_block(consequent); + self.fmt.dedent(); + match alternate { + Some(alt) => { + self.fmt.line("alternate:"); + self.fmt.indent(); + self.format_reactive_block(alt); + self.fmt.dedent(); + } + None => self.fmt.line("alternate: null"), + } + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::Label { block, id, loc } => { + self.fmt.line("Label {"); + self.fmt.indent(); + self.fmt.line("block:"); + self.fmt.indent(); + self.format_reactive_block(block); + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + ReactiveTerminal::Try { block, handler_binding, handler, id, loc } => { + self.fmt.line("Try {"); + self.fmt.indent(); + self.fmt.line("block:"); + self.fmt.indent(); + self.format_reactive_block(block); + self.fmt.dedent(); + match handler_binding { + Some(p) => self.fmt.format_place_field("handlerBinding", p), + None => self.fmt.line("handlerBinding: null"), + } + self.fmt.line("handler:"); + self.fmt.indent(); + self.format_reactive_block(handler); + self.fmt.dedent(); + self.fmt.line(&format!("id: {}", id.0)); + self.fmt.line(&format!("loc: {}", print::format_loc(loc))); + self.fmt.dedent(); + self.fmt.line("}"); + } + } + } +} + +// ============================================================================= +// Entry point +// ============================================================================= + +/// Type alias for a function formatter callback that can print HIR functions. +/// Used to format inner functions in FunctionExpression/ObjectMethod values. +pub type HirFunctionFormatter<'h> = dyn Fn(&mut PrintFormatter<'_, 'h>, &HirFunction<'h>); + +pub fn debug_reactive_function<'h>(func: &ReactiveFunction<'h>, env: &Environment<'h>) -> String { + debug_reactive_function_with_formatter(func, env, None) +} + +pub fn debug_reactive_function_with_formatter<'h>( + func: &ReactiveFunction<'h>, + env: &Environment<'h>, + hir_formatter: Option<&HirFunctionFormatter<'h>>, +) -> String { + let mut printer = DebugPrinter::new(env); + printer.hir_formatter = hir_formatter; + printer.format_reactive_function(func); + + // TODO: Print outlined functions when they've been converted to reactive form + + printer.fmt.line(""); + printer.fmt.line("Environment:"); + printer.fmt.indent(); + printer.fmt.format_errors(&env.errors); + printer.fmt.dedent(); + + printer.fmt.to_string_output() +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/promote_used_temporaries.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/promote_used_temporaries.rs new file mode 100644 index 0000000000000..31327047b22c9 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/promote_used_temporaries.rs @@ -0,0 +1,1057 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! PromoteUsedTemporaries — promotes temporary variables to named variables +//! if they're used by scopes. +//! +//! Corresponds to `src/ReactiveScopes/PromoteUsedTemporaries.ts`. + +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::DeclarationId; +use crate::react_compiler_hir::FunctionId; +use crate::react_compiler_hir::IdentifierId; +use crate::react_compiler_hir::IdentifierName; +use crate::react_compiler_hir::InstructionKind; +use crate::react_compiler_hir::InstructionValue; +use crate::react_compiler_hir::JsxTag; +use crate::react_compiler_hir::ParamPattern; +use crate::react_compiler_hir::Place; +use crate::react_compiler_hir::ReactiveBlock; +use crate::react_compiler_hir::ReactiveFunction; +use crate::react_compiler_hir::ReactiveInstruction; +use crate::react_compiler_hir::ReactiveStatement; +use crate::react_compiler_hir::ReactiveTerminal; +use crate::react_compiler_hir::ReactiveTerminalStatement; +use crate::react_compiler_hir::ReactiveValue; +use crate::react_compiler_hir::ScopeId; +use crate::react_compiler_hir::environment::Environment; + +// ============================================================================= +// State +// ============================================================================= + +struct State { + tags: FxHashSet, + promoted: FxHashSet, + pruned: FxHashMap, +} + +struct PrunedInfo { + active_scopes: Vec, + used_outside_scope: bool, +} + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Promotes temporary (unnamed) identifiers used in scopes to named identifiers. +/// TS: `promoteUsedTemporaries` +pub fn promote_used_temporaries(func: &mut ReactiveFunction, env: &mut Environment) { + let mut state = State { + tags: FxHashSet::default(), + promoted: FxHashSet::default(), + pruned: FxHashMap::default(), + }; + + // Phase 1: collect promotable temporaries (jsx tags, pruned scope usage) + let mut active_scopes: Vec = Vec::new(); + collect_promotable_block(&func.body, &mut state, &mut active_scopes, env); + + // Promote params + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + let identifier = &env.identifiers[place.identifier.0 as usize]; + if identifier.name.is_none() { + promote_identifier(place.identifier, &mut state, env); + } + } + + // Phase 2: promote identifiers used in scopes + promote_temporaries_block(&func.body, &mut state, env); + + // Phase 3: promote interposed temporaries + let mut consts: FxHashSet = FxHashSet::default(); + let mut globals: FxHashSet = FxHashSet::default(); + for param in &func.params { + match param { + ParamPattern::Place(p) => { + consts.insert(p.identifier); + } + ParamPattern::Spread(s) => { + consts.insert(s.place.identifier); + } + } + } + let mut inter_state: FxHashMap = FxHashMap::default(); + promote_interposed_block( + &func.body, + &mut state, + &mut inter_state, + &mut consts, + &mut globals, + env, + ); + + // Phase 4: promote all instances of promoted declaration IDs + promote_all_instances_params(func, &mut state, env); + promote_all_instances_block(&func.body, &mut state, env); +} + +// ============================================================================= +// Phase 1: CollectPromotableTemporaries +// ============================================================================= + +fn collect_promotable_block( + block: &ReactiveBlock, + state: &mut State, + active_scopes: &mut Vec, + env: &Environment, +) { + for stmt in block { + match stmt { + ReactiveStatement::Instruction(instr) => { + collect_promotable_instruction(instr, state, active_scopes, env); + } + ReactiveStatement::Scope(scope) => { + let scope_id = scope.scope; + active_scopes.push(scope_id); + collect_promotable_block(&scope.instructions, state, active_scopes, env); + active_scopes.pop(); + } + ReactiveStatement::PrunedScope(scope) => { + let scope_data = &env.scopes[scope.scope.0 as usize]; + for (_id, decl) in &scope_data.declarations { + let identifier = &env.identifiers[decl.identifier.0 as usize]; + state.pruned.insert( + identifier.declaration_id, + PrunedInfo { + active_scopes: active_scopes.clone(), + used_outside_scope: false, + }, + ); + } + collect_promotable_block(&scope.instructions, state, active_scopes, env); + } + ReactiveStatement::Terminal(terminal) => { + collect_promotable_terminal(terminal, state, active_scopes, env); + } + } + } +} + +fn collect_promotable_place( + place: &Place, + state: &mut State, + active_scopes: &[ScopeId], + env: &Environment, +) { + if !active_scopes.is_empty() { + let identifier = &env.identifiers[place.identifier.0 as usize]; + if let Some(pruned) = state.pruned.get_mut(&identifier.declaration_id) { + if let Some(last) = active_scopes.last() { + if !pruned.active_scopes.contains(last) { + pruned.used_outside_scope = true; + } + } + } + } +} + +fn collect_promotable_instruction( + instr: &ReactiveInstruction, + state: &mut State, + active_scopes: &mut Vec, + env: &Environment, +) { + collect_promotable_value(&instr.value, state, active_scopes, env); +} + +fn collect_promotable_value( + value: &ReactiveValue, + state: &mut State, + active_scopes: &mut Vec, + env: &Environment, +) { + match value { + ReactiveValue::Instruction(instr_value) => { + // Visit operands + for place in crate::react_compiler_hir::visitors::each_instruction_value_operand( + instr_value, + env, + ) { + collect_promotable_place(&place, state, active_scopes, env); + } + // Check for JSX tag + if let InstructionValue::JsxExpression { tag: JsxTag::Place(place), .. } = instr_value { + let identifier = &env.identifiers[place.identifier.0 as usize]; + state.tags.insert(identifier.declaration_id); + } + } + ReactiveValue::SequenceExpression { instructions, value: inner, .. } => { + for instr in instructions { + collect_promotable_instruction(instr, state, active_scopes, env); + } + collect_promotable_value(inner, state, active_scopes, env); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + collect_promotable_value(test, state, active_scopes, env); + collect_promotable_value(consequent, state, active_scopes, env); + collect_promotable_value(alternate, state, active_scopes, env); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + collect_promotable_value(left, state, active_scopes, env); + collect_promotable_value(right, state, active_scopes, env); + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + collect_promotable_value(inner, state, active_scopes, env); + } + } +} + +fn collect_promotable_terminal( + stmt: &ReactiveTerminalStatement, + state: &mut State, + active_scopes: &mut Vec, + env: &Environment, +) { + match &stmt.terminal { + ReactiveTerminal::Break { .. } | ReactiveTerminal::Continue { .. } => {} + ReactiveTerminal::Return { value, .. } | ReactiveTerminal::Throw { value, .. } => { + collect_promotable_place(value, state, active_scopes, env); + } + ReactiveTerminal::For { init, test, update, loop_block, .. } => { + collect_promotable_value(init, state, active_scopes, env); + collect_promotable_value(test, state, active_scopes, env); + collect_promotable_block(loop_block, state, active_scopes, env); + if let Some(update) = update { + collect_promotable_value(update, state, active_scopes, env); + } + } + ReactiveTerminal::ForOf { init, test, loop_block, .. } => { + collect_promotable_value(init, state, active_scopes, env); + collect_promotable_value(test, state, active_scopes, env); + collect_promotable_block(loop_block, state, active_scopes, env); + } + ReactiveTerminal::ForIn { init, loop_block, .. } => { + collect_promotable_value(init, state, active_scopes, env); + collect_promotable_block(loop_block, state, active_scopes, env); + } + ReactiveTerminal::DoWhile { loop_block, test, .. } => { + collect_promotable_block(loop_block, state, active_scopes, env); + collect_promotable_value(test, state, active_scopes, env); + } + ReactiveTerminal::While { test, loop_block, .. } => { + collect_promotable_value(test, state, active_scopes, env); + collect_promotable_block(loop_block, state, active_scopes, env); + } + ReactiveTerminal::If { test, consequent, alternate, .. } => { + collect_promotable_place(test, state, active_scopes, env); + collect_promotable_block(consequent, state, active_scopes, env); + if let Some(alt) = alternate { + collect_promotable_block(alt, state, active_scopes, env); + } + } + ReactiveTerminal::Switch { test, cases, .. } => { + collect_promotable_place(test, state, active_scopes, env); + for case in cases { + if let Some(t) = &case.test { + collect_promotable_place(t, state, active_scopes, env); + } + if let Some(block) = &case.block { + collect_promotable_block(block, state, active_scopes, env); + } + } + } + ReactiveTerminal::Label { block, .. } => { + collect_promotable_block(block, state, active_scopes, env); + } + ReactiveTerminal::Try { block, handler_binding, handler, .. } => { + collect_promotable_block(block, state, active_scopes, env); + if let Some(binding) = handler_binding { + collect_promotable_place(binding, state, active_scopes, env); + } + collect_promotable_block(handler, state, active_scopes, env); + } + } +} + +// ============================================================================= +// Phase 2: PromoteTemporaries +// ============================================================================= + +fn promote_temporaries_block(block: &ReactiveBlock, state: &mut State, env: &mut Environment) { + for stmt in block { + match stmt { + ReactiveStatement::Instruction(instr) => { + promote_temporaries_value(&instr.value, state, env); + } + ReactiveStatement::Scope(scope) => { + let scope_id = scope.scope; + let scope_data = &env.scopes[scope_id.0 as usize]; + // Collect all IDs to promote first + let mut ids_to_check: Vec = Vec::new(); + ids_to_check.extend(scope_data.dependencies.iter().map(|d| d.identifier)); + ids_to_check.extend(scope_data.declarations.iter().map(|(_, d)| d.identifier)); + for id in ids_to_check { + let identifier = &env.identifiers[id.0 as usize]; + if identifier.name.is_none() { + promote_identifier(id, state, env); + } + } + promote_temporaries_block(&scope.instructions, state, env); + } + ReactiveStatement::PrunedScope(scope) => { + let scope_id = scope.scope; + let scope_data = &env.scopes[scope_id.0 as usize]; + let decls: Vec<(IdentifierId, DeclarationId)> = scope_data + .declarations + .iter() + .map(|(_, d)| { + let identifier = &env.identifiers[d.identifier.0 as usize]; + (d.identifier, identifier.declaration_id) + }) + .collect(); + for (id, decl_id) in decls { + let identifier = &env.identifiers[id.0 as usize]; + if identifier.name.is_none() { + if let Some(pruned) = state.pruned.get(&decl_id) { + if pruned.used_outside_scope { + promote_identifier(id, state, env); + } + } + } + } + promote_temporaries_block(&scope.instructions, state, env); + } + ReactiveStatement::Terminal(terminal) => { + promote_temporaries_terminal(terminal, state, env); + } + } + } +} + +fn promote_temporaries_value(value: &ReactiveValue, state: &mut State, env: &mut Environment) { + match value { + ReactiveValue::Instruction(instr_value) => { + // Visit inner functions: promote params and recurse into nested functions + // TS: visitHirFunction(value.loweredFunc.func, state) + match instr_value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + visit_hir_function_for_promotion(lowered_func.func, state, env); + } + _ => {} + } + } + ReactiveValue::SequenceExpression { instructions, value: inner, .. } => { + for instr in instructions { + promote_temporaries_value(&instr.value, state, env); + } + promote_temporaries_value(inner, state, env); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + promote_temporaries_value(test, state, env); + promote_temporaries_value(consequent, state, env); + promote_temporaries_value(alternate, state, env); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + promote_temporaries_value(left, state, env); + promote_temporaries_value(right, state, env); + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + promote_temporaries_value(inner, state, env); + } + } +} + +fn promote_temporaries_terminal( + stmt: &ReactiveTerminalStatement, + state: &mut State, + env: &mut Environment, +) { + match &stmt.terminal { + ReactiveTerminal::Break { .. } | ReactiveTerminal::Continue { .. } => {} + ReactiveTerminal::Return { .. } | ReactiveTerminal::Throw { .. } => {} + ReactiveTerminal::For { init, test, update, loop_block, .. } => { + promote_temporaries_value(init, state, env); + promote_temporaries_value(test, state, env); + promote_temporaries_block(loop_block, state, env); + if let Some(update) = update { + promote_temporaries_value(update, state, env); + } + } + ReactiveTerminal::ForOf { init, test, loop_block, .. } => { + promote_temporaries_value(init, state, env); + promote_temporaries_value(test, state, env); + promote_temporaries_block(loop_block, state, env); + } + ReactiveTerminal::ForIn { init, loop_block, .. } => { + promote_temporaries_value(init, state, env); + promote_temporaries_block(loop_block, state, env); + } + ReactiveTerminal::DoWhile { loop_block, test, .. } => { + promote_temporaries_block(loop_block, state, env); + promote_temporaries_value(test, state, env); + } + ReactiveTerminal::While { test, loop_block, .. } => { + promote_temporaries_value(test, state, env); + promote_temporaries_block(loop_block, state, env); + } + ReactiveTerminal::If { consequent, alternate, .. } => { + promote_temporaries_block(consequent, state, env); + if let Some(alt) = alternate { + promote_temporaries_block(alt, state, env); + } + } + ReactiveTerminal::Switch { cases, .. } => { + for case in cases { + if let Some(block) = &case.block { + promote_temporaries_block(block, state, env); + } + } + } + ReactiveTerminal::Label { block, .. } => { + promote_temporaries_block(block, state, env); + } + ReactiveTerminal::Try { block, handler, .. } => { + promote_temporaries_block(block, state, env); + promote_temporaries_block(handler, state, env); + } + } +} + +// ============================================================================= +// Helper: visit inner HIR function for promotion (mirrors TS visitHirFunction) +// ============================================================================= + +/// Promotes params and recursively visits nested functions for param promotion. +/// Specialized version of the TS `visitHirFunction` pattern for the PromoteTemporaries +/// phase — only promotes unnamed params and recurses into nested functions. +/// Other `visitHirFunction` behaviors (visitPlace on terminal operands, visitInstruction +/// on all instructions) are no-ops for this phase and are intentionally omitted. +fn visit_hir_function_for_promotion(func_id: FunctionId, state: &mut State, env: &mut Environment) { + // Promote params of this function + let param_ids: Vec = { + let func = &env.functions[func_id.0 as usize]; + func.params + .iter() + .map(|param| match param { + ParamPattern::Place(p) => p.identifier, + ParamPattern::Spread(s) => s.place.identifier, + }) + .collect() + }; + for id in param_ids { + let identifier = &env.identifiers[id.0 as usize]; + if identifier.name.is_none() { + promote_identifier(id, state, env); + } + } + + // Find nested FunctionExpression/ObjectMethod in body instructions + let nested_func_ids: Vec = { + let func = &env.functions[func_id.0 as usize]; + let mut nested = Vec::new(); + for (_, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + nested.push(lowered_func.func); + } + _ => {} + } + } + } + nested + }; + for nested_id in nested_func_ids { + visit_hir_function_for_promotion(nested_id, state, env); + } +} + +// ============================================================================= +// Phase 3: PromoteInterposedTemporaries +// ============================================================================= + +fn promote_interposed_block( + block: &ReactiveBlock, + state: &mut State, + inter_state: &mut FxHashMap, + consts: &mut FxHashSet, + globals: &mut FxHashSet, + env: &mut Environment, +) { + for stmt in block { + match stmt { + ReactiveStatement::Instruction(instr) => { + promote_interposed_instruction(instr, state, inter_state, consts, globals, env); + } + ReactiveStatement::Scope(scope) => { + promote_interposed_block( + &scope.instructions, + state, + inter_state, + consts, + globals, + env, + ); + } + ReactiveStatement::PrunedScope(scope) => { + promote_interposed_block( + &scope.instructions, + state, + inter_state, + consts, + globals, + env, + ); + } + ReactiveStatement::Terminal(terminal) => { + promote_interposed_terminal(terminal, state, inter_state, consts, globals, env); + } + } + } +} + +fn promote_interposed_place( + place: &Place, + state: &mut State, + inter_state: &mut FxHashMap, + consts: &FxHashSet, + env: &mut Environment, +) { + if let Some(&(id, needs_promotion)) = inter_state.get(&place.identifier) { + let identifier = &env.identifiers[id.0 as usize]; + if needs_promotion && identifier.name.is_none() && !consts.contains(&id) { + promote_identifier(id, state, env); + } + } +} + +fn promote_interposed_instruction( + instr: &ReactiveInstruction, + state: &mut State, + inter_state: &mut FxHashMap, + consts: &mut FxHashSet, + globals: &mut FxHashSet, + env: &mut Environment, +) { + // Check instruction value lvalues (assignment targets) + match &instr.value { + ReactiveValue::Instruction(iv) => { + // Check eachInstructionValueLValue: these should all be named + // (the TS pass asserts this but we just skip in Rust) + + match iv { + InstructionValue::CallExpression { .. } + | InstructionValue::MethodCall { .. } + | InstructionValue::Await { .. } + | InstructionValue::PropertyStore { .. } + | InstructionValue::PropertyDelete { .. } + | InstructionValue::ComputedStore { .. } + | InstructionValue::ComputedDelete { .. } + | InstructionValue::PostfixUpdate { .. } + | InstructionValue::PrefixUpdate { .. } + | InstructionValue::StoreLocal { .. } + | InstructionValue::StoreContext { .. } + | InstructionValue::StoreGlobal { .. } + | InstructionValue::Destructure { .. } => { + let mut const_store = false; + + match iv { + InstructionValue::StoreContext { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } => { + if lvalue.kind == InstructionKind::Const + || lvalue.kind == InstructionKind::HoistedConst + { + consts.insert(lvalue.place.identifier); + const_store = true; + } + } + _ => {} + } + if let InstructionValue::Destructure { lvalue, .. } = iv { + if lvalue.kind == InstructionKind::Const + || lvalue.kind == InstructionKind::HoistedConst + { + for operand in crate::react_compiler_hir::visitors::each_pattern_operand( + &lvalue.pattern, + ) { + consts.insert(operand.identifier); + } + const_store = true; + } + } + if let InstructionValue::MethodCall { property, .. } = iv { + consts.insert(property.identifier); + } + + // Visit operands + for place in + crate::react_compiler_hir::visitors::each_instruction_value_operand(iv, env) + { + promote_interposed_place(&place, state, inter_state, consts, env); + } + + if !const_store + && (instr.lvalue.is_none() + || env.identifiers + [instr.lvalue.as_ref().unwrap().identifier.0 as usize] + .name + .is_some()) + { + // Mark all tracked temporaries as needing promotion + let keys: Vec = inter_state.keys().cloned().collect(); + for key in keys { + if let Some(entry) = inter_state.get_mut(&key) { + entry.1 = true; + } + } + } + if let Some(lvalue) = &instr.lvalue { + let identifier = &env.identifiers[lvalue.identifier.0 as usize]; + if identifier.name.is_none() { + inter_state.insert(lvalue.identifier, (lvalue.identifier, false)); + } + } + } + InstructionValue::DeclareContext { lvalue, .. } + | InstructionValue::DeclareLocal { lvalue, .. } => { + if lvalue.kind == InstructionKind::Const + || lvalue.kind == InstructionKind::HoistedConst + { + consts.insert(lvalue.place.identifier); + } + // Visit operands + for place in + crate::react_compiler_hir::visitors::each_instruction_value_operand(iv, env) + { + promote_interposed_place(&place, state, inter_state, consts, env); + } + } + InstructionValue::LoadContext { place: load_place, .. } + | InstructionValue::LoadLocal { place: load_place, .. } => { + if let Some(lvalue) = &instr.lvalue { + let identifier = &env.identifiers[lvalue.identifier.0 as usize]; + if identifier.name.is_none() { + if consts.contains(&load_place.identifier) { + consts.insert(lvalue.identifier); + } + inter_state.insert(lvalue.identifier, (lvalue.identifier, false)); + } + } + // Visit operands + for place in + crate::react_compiler_hir::visitors::each_instruction_value_operand(iv, env) + { + promote_interposed_place(&place, state, inter_state, consts, env); + } + } + InstructionValue::PropertyLoad { object, .. } + | InstructionValue::ComputedLoad { object, .. } => { + if let Some(lvalue) = &instr.lvalue { + if globals.contains(&object.identifier) { + globals.insert(lvalue.identifier); + consts.insert(lvalue.identifier); + } + let identifier = &env.identifiers[lvalue.identifier.0 as usize]; + if identifier.name.is_none() { + inter_state.insert(lvalue.identifier, (lvalue.identifier, false)); + } + } + // Visit operands + for place in + crate::react_compiler_hir::visitors::each_instruction_value_operand(iv, env) + { + promote_interposed_place(&place, state, inter_state, consts, env); + } + } + InstructionValue::LoadGlobal { .. } => { + if let Some(lvalue) = &instr.lvalue { + globals.insert(lvalue.identifier); + } + // Visit operands + for place in + crate::react_compiler_hir::visitors::each_instruction_value_operand(iv, env) + { + promote_interposed_place(&place, state, inter_state, consts, env); + } + } + _ => { + // Default: visit operands + for place in + crate::react_compiler_hir::visitors::each_instruction_value_operand(iv, env) + { + promote_interposed_place(&place, state, inter_state, consts, env); + } + } + } + } + ReactiveValue::SequenceExpression { instructions, value: inner, .. } => { + for sub_instr in instructions { + promote_interposed_instruction(sub_instr, state, inter_state, consts, globals, env); + } + promote_interposed_value(inner, state, inter_state, consts, globals, env); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + promote_interposed_value(test, state, inter_state, consts, globals, env); + promote_interposed_value(consequent, state, inter_state, consts, globals, env); + promote_interposed_value(alternate, state, inter_state, consts, globals, env); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + promote_interposed_value(left, state, inter_state, consts, globals, env); + promote_interposed_value(right, state, inter_state, consts, globals, env); + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + promote_interposed_value(inner, state, inter_state, consts, globals, env); + } + } +} + +fn promote_interposed_value( + value: &ReactiveValue, + state: &mut State, + inter_state: &mut FxHashMap, + consts: &mut FxHashSet, + globals: &mut FxHashSet, + env: &mut Environment, +) { + match value { + ReactiveValue::Instruction(iv) => { + for place in + crate::react_compiler_hir::visitors::each_instruction_value_operand(iv, env) + { + promote_interposed_place(&place, state, inter_state, consts, env); + } + } + ReactiveValue::SequenceExpression { instructions, value: inner, .. } => { + for instr in instructions { + promote_interposed_instruction(instr, state, inter_state, consts, globals, env); + } + promote_interposed_value(inner, state, inter_state, consts, globals, env); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + promote_interposed_value(test, state, inter_state, consts, globals, env); + promote_interposed_value(consequent, state, inter_state, consts, globals, env); + promote_interposed_value(alternate, state, inter_state, consts, globals, env); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + promote_interposed_value(left, state, inter_state, consts, globals, env); + promote_interposed_value(right, state, inter_state, consts, globals, env); + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + promote_interposed_value(inner, state, inter_state, consts, globals, env); + } + } +} + +fn promote_interposed_terminal( + stmt: &ReactiveTerminalStatement, + state: &mut State, + inter_state: &mut FxHashMap, + consts: &mut FxHashSet, + globals: &mut FxHashSet, + env: &mut Environment, +) { + match &stmt.terminal { + ReactiveTerminal::Break { .. } | ReactiveTerminal::Continue { .. } => {} + ReactiveTerminal::Return { value, .. } | ReactiveTerminal::Throw { value, .. } => { + promote_interposed_place(value, state, inter_state, consts, env); + } + ReactiveTerminal::For { init, test, update, loop_block, .. } => { + promote_interposed_value(init, state, inter_state, consts, globals, env); + promote_interposed_value(test, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + if let Some(update) = update { + promote_interposed_value(update, state, inter_state, consts, globals, env); + } + } + ReactiveTerminal::ForOf { init, test, loop_block, .. } => { + promote_interposed_value(init, state, inter_state, consts, globals, env); + promote_interposed_value(test, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + } + ReactiveTerminal::ForIn { init, loop_block, .. } => { + promote_interposed_value(init, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + } + ReactiveTerminal::DoWhile { loop_block, test, .. } => { + promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + promote_interposed_value(test, state, inter_state, consts, globals, env); + } + ReactiveTerminal::While { test, loop_block, .. } => { + promote_interposed_value(test, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + } + ReactiveTerminal::If { test, consequent, alternate, .. } => { + promote_interposed_place(test, state, inter_state, consts, env); + promote_interposed_block(consequent, state, inter_state, consts, globals, env); + if let Some(alt) = alternate { + promote_interposed_block(alt, state, inter_state, consts, globals, env); + } + } + ReactiveTerminal::Switch { test, cases, .. } => { + promote_interposed_place(test, state, inter_state, consts, env); + for case in cases { + if let Some(t) = &case.test { + promote_interposed_place(t, state, inter_state, consts, env); + } + if let Some(block) = &case.block { + promote_interposed_block(block, state, inter_state, consts, globals, env); + } + } + } + ReactiveTerminal::Label { block, .. } => { + promote_interposed_block(block, state, inter_state, consts, globals, env); + } + ReactiveTerminal::Try { block, handler_binding, handler, .. } => { + promote_interposed_block(block, state, inter_state, consts, globals, env); + if let Some(binding) = handler_binding { + promote_interposed_place(binding, state, inter_state, consts, env); + } + promote_interposed_block(handler, state, inter_state, consts, globals, env); + } + } +} + +// ============================================================================= +// Phase 4: PromoteAllInstancesOfPromotedTemporaries +// ============================================================================= + +fn promote_all_instances_params(func: &ReactiveFunction, state: &mut State, env: &mut Environment) { + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + let identifier = &env.identifiers[place.identifier.0 as usize]; + if identifier.name.is_none() && state.promoted.contains(&identifier.declaration_id) { + promote_identifier(place.identifier, state, env); + } + } +} + +fn promote_all_instances_block(block: &ReactiveBlock, state: &mut State, env: &mut Environment) { + for stmt in block { + match stmt { + ReactiveStatement::Instruction(instr) => { + promote_all_instances_instruction(instr, state, env); + } + ReactiveStatement::Scope(scope) => { + promote_all_instances_block(&scope.instructions, state, env); + promote_all_instances_scope_identifiers(scope.scope, state, env); + } + ReactiveStatement::PrunedScope(scope) => { + promote_all_instances_block(&scope.instructions, state, env); + promote_all_instances_scope_identifiers(scope.scope, state, env); + } + ReactiveStatement::Terminal(terminal) => { + promote_all_instances_terminal(terminal, state, env); + } + } + } +} + +fn promote_all_instances_scope_identifiers( + scope_id: ScopeId, + state: &mut State, + env: &mut Environment, +) { + let scope_data = &env.scopes[scope_id.0 as usize]; + + // Collect identifiers to promote + let decl_ids: Vec = + scope_data.declarations.iter().map(|(_, d)| d.identifier).collect(); + let dep_ids: Vec = scope_data.dependencies.iter().map(|d| d.identifier).collect(); + let reassign_ids: Vec = scope_data.reassignments.clone(); + + for id in decl_ids { + let identifier = &env.identifiers[id.0 as usize]; + if identifier.name.is_none() && state.promoted.contains(&identifier.declaration_id) { + promote_identifier(id, state, env); + } + } + for id in dep_ids { + let identifier = &env.identifiers[id.0 as usize]; + if identifier.name.is_none() && state.promoted.contains(&identifier.declaration_id) { + promote_identifier(id, state, env); + } + } + for id in reassign_ids { + let identifier = &env.identifiers[id.0 as usize]; + if identifier.name.is_none() && state.promoted.contains(&identifier.declaration_id) { + promote_identifier(id, state, env); + } + } +} + +fn promote_all_instances_place(place: &Place, state: &mut State, env: &mut Environment) { + let identifier = &env.identifiers[place.identifier.0 as usize]; + if identifier.name.is_none() && state.promoted.contains(&identifier.declaration_id) { + promote_identifier(place.identifier, state, env); + } +} + +fn promote_all_instances_instruction( + instr: &ReactiveInstruction, + state: &mut State, + env: &mut Environment, +) { + if let Some(lvalue) = &instr.lvalue { + promote_all_instances_place(lvalue, state, env); + } + promote_all_instances_value(&instr.value, state, env); +} + +fn promote_all_instances_value(value: &ReactiveValue, state: &mut State, env: &mut Environment) { + match value { + ReactiveValue::Instruction(iv) => { + for place in + crate::react_compiler_hir::visitors::each_instruction_value_operand(iv, env) + { + promote_all_instances_place(&place, state, env); + } + // Visit inner functions + match iv { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let func_id = lowered_func.func; + let inner_func = &env.functions[func_id.0 as usize]; + let param_ids: Vec = inner_func + .params + .iter() + .map(|p| match p { + ParamPattern::Place(p) => p.identifier, + ParamPattern::Spread(s) => s.place.identifier, + }) + .collect(); + for id in param_ids { + let identifier = &env.identifiers[id.0 as usize]; + if identifier.name.is_none() + && state.promoted.contains(&identifier.declaration_id) + { + promote_identifier(id, state, env); + } + } + } + _ => {} + } + } + ReactiveValue::SequenceExpression { instructions, value: inner, .. } => { + for instr in instructions { + promote_all_instances_instruction(instr, state, env); + } + promote_all_instances_value(inner, state, env); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + promote_all_instances_value(test, state, env); + promote_all_instances_value(consequent, state, env); + promote_all_instances_value(alternate, state, env); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + promote_all_instances_value(left, state, env); + promote_all_instances_value(right, state, env); + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + promote_all_instances_value(inner, state, env); + } + } +} + +fn promote_all_instances_terminal( + stmt: &ReactiveTerminalStatement, + state: &mut State, + env: &mut Environment, +) { + match &stmt.terminal { + ReactiveTerminal::Break { .. } | ReactiveTerminal::Continue { .. } => {} + ReactiveTerminal::Return { value, .. } | ReactiveTerminal::Throw { value, .. } => { + promote_all_instances_place(value, state, env); + } + ReactiveTerminal::For { init, test, update, loop_block, .. } => { + promote_all_instances_value(init, state, env); + promote_all_instances_value(test, state, env); + promote_all_instances_block(loop_block, state, env); + if let Some(update) = update { + promote_all_instances_value(update, state, env); + } + } + ReactiveTerminal::ForOf { init, test, loop_block, .. } => { + promote_all_instances_value(init, state, env); + promote_all_instances_value(test, state, env); + promote_all_instances_block(loop_block, state, env); + } + ReactiveTerminal::ForIn { init, loop_block, .. } => { + promote_all_instances_value(init, state, env); + promote_all_instances_block(loop_block, state, env); + } + ReactiveTerminal::DoWhile { loop_block, test, .. } => { + promote_all_instances_block(loop_block, state, env); + promote_all_instances_value(test, state, env); + } + ReactiveTerminal::While { test, loop_block, .. } => { + promote_all_instances_value(test, state, env); + promote_all_instances_block(loop_block, state, env); + } + ReactiveTerminal::If { test, consequent, alternate, .. } => { + promote_all_instances_place(test, state, env); + promote_all_instances_block(consequent, state, env); + if let Some(alt) = alternate { + promote_all_instances_block(alt, state, env); + } + } + ReactiveTerminal::Switch { test, cases, .. } => { + promote_all_instances_place(test, state, env); + for case in cases { + if let Some(t) = &case.test { + promote_all_instances_place(t, state, env); + } + if let Some(block) = &case.block { + promote_all_instances_block(block, state, env); + } + } + } + ReactiveTerminal::Label { block, .. } => { + promote_all_instances_block(block, state, env); + } + ReactiveTerminal::Try { block, handler_binding, handler, .. } => { + promote_all_instances_block(block, state, env); + if let Some(binding) = handler_binding { + promote_all_instances_place(binding, state, env); + } + promote_all_instances_block(handler, state, env); + } + } +} + +// ============================================================================= +// Helpers +// ============================================================================= + +fn promote_identifier(identifier_id: IdentifierId, state: &mut State, env: &mut Environment) { + let identifier = &env.identifiers[identifier_id.0 as usize]; + assert!( + identifier.name.is_none(), + "promoteTemporary: Expected to be called only for temporary variables" + ); + let decl_id = identifier.declaration_id; + if state.tags.contains(&decl_id) { + // JSX tag temporary: use capitalized name + env.identifiers[identifier_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#T{}", decl_id.0))); + } else { + env.identifiers[identifier_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#t{}", decl_id.0))); + } + state.promoted.insert(decl_id); +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/propagate_early_returns.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/propagate_early_returns.rs new file mode 100644 index 0000000000000..4019952d03778 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/propagate_early_returns.rs @@ -0,0 +1,349 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! PropagateEarlyReturns — ensures reactive blocks honor early return semantics. +//! +//! When a scope contains an early return, creates a sentinel-based check so that +//! cached scopes can properly replay the early return behavior. +//! +//! Corresponds to `src/ReactiveScopes/PropagateEarlyReturns.ts`. + +use crate::react_compiler_hir::{ + BlockId, Effect, EvaluationOrder, IdentifierId, IdentifierName, InstructionKind, + InstructionValue, LValue, NonLocalBinding, Place, PlaceOrSpread, PrimitiveValue, + PropertyLiteral, ReactiveFunction, ReactiveInstruction, ReactiveLabel, ReactiveScopeBlock, + ReactiveScopeDeclaration, ReactiveScopeEarlyReturn, ReactiveStatement, ReactiveTerminal, + ReactiveTerminalStatement, ReactiveTerminalTargetKind, ReactiveValue, environment::Environment, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionTransform, Transformed, transform_reactive_function, +}; + +/// The sentinel string used to detect early returns. +/// TS: `EARLY_RETURN_SENTINEL` from CodegenReactiveFunction. +const EARLY_RETURN_SENTINEL: &str = "react.early_return_sentinel"; + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Propagate early return semantics through reactive scopes. +/// TS: `propagateEarlyReturns` +pub fn propagate_early_returns<'a>(func: &mut ReactiveFunction<'a>, env: &mut Environment<'a>) { + let mut transform = Transform { env }; + let mut state = State { within_reactive_scope: false, early_return_value: None }; + // The TS version doesn't produce errors from this pass, so we ignore the Result. + let _ = transform_reactive_function(func, &mut transform, &mut state); +} + +// ============================================================================= +// State +// ============================================================================= + +#[derive(Debug, Clone)] +struct EarlyReturnInfo { + value: IdentifierId, + loc: Option, + label: BlockId, +} + +struct State { + within_reactive_scope: bool, + early_return_value: Option, +} + +// ============================================================================= +// Transform implementation (ReactiveFunctionTransform) +// ============================================================================= + +/// TS: `class Transform extends ReactiveFunctionTransform` +struct Transform<'a, 'e> { + env: &'e mut Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for Transform<'a, 'e> { + type State = State; + + fn env(&self) -> &Environment<'a> { + self.env + } + + /// TS: `override visitScope` + fn visit_scope( + &mut self, + scope_block: &mut ReactiveScopeBlock<'a>, + parent_state: &mut State, + ) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + let scope_id = scope_block.scope; + + // Exit early if an earlier pass has already created an early return + if self.env.scopes[scope_id.0 as usize].early_return_value.is_some() { + return Ok(()); + } + + let mut inner_state = State { + within_reactive_scope: true, + early_return_value: parent_state.early_return_value.clone(), + }; + self.traverse_scope(scope_block, &mut inner_state)?; + + if let Some(early_return_value) = inner_state.early_return_value { + if !parent_state.within_reactive_scope { + // This is the outermost scope wrapping an early return + apply_early_return_to_scope(scope_block, self.env, &early_return_value); + } else { + // Not outermost — bubble up + parent_state.early_return_value = Some(early_return_value); + } + } + + Ok(()) + } + + /// TS: `override transformTerminal` + fn transform_terminal( + &mut self, + stmt: &mut ReactiveTerminalStatement<'a>, + state: &mut State, + ) -> Result>, crate::react_compiler_diagnostics::CompilerError> + { + if state.within_reactive_scope { + if let ReactiveTerminal::Return { value, .. } = &stmt.terminal { + let loc = value.loc; + + let early_return_value = if let Some(ref existing) = state.early_return_value { + existing.clone() + } else { + // Create a new early return identifier + let identifier_id = create_temporary_place_id(self.env, loc); + promote_temporary(self.env, identifier_id); + let label = self.env.next_block_id(); + EarlyReturnInfo { value: identifier_id, loc, label } + }; + + state.early_return_value = Some(early_return_value.clone()); + + let return_value = value.clone(); + + return Ok(Transformed::ReplaceMany(vec![ + // StoreLocal: reassign the early return value + ReactiveStatement::Instruction(ReactiveInstruction { + id: EvaluationOrder(0), + lvalue: None, + value: ReactiveValue::Instruction(InstructionValue::StoreLocal { + lvalue: LValue { + kind: InstructionKind::Reassign, + place: Place { + identifier: early_return_value.value, + effect: Effect::Capture, + reactive: true, + loc, + }, + }, + value: return_value, + type_annotation: None, + loc, + }), + effects: None, + loc, + }), + // Break to the label + ReactiveStatement::Terminal(ReactiveTerminalStatement { + terminal: ReactiveTerminal::Break { + target: early_return_value.label, + id: EvaluationOrder(0), + target_kind: ReactiveTerminalTargetKind::Labeled, + loc, + }, + label: None, + }), + ])); + } + } + + // Default: traverse into the terminal's sub-blocks + self.visit_terminal(stmt, state)?; + Ok(Transformed::Keep) + } +} + +// ============================================================================= +// Apply early return transformation to the outermost scope +// ============================================================================= + +fn apply_early_return_to_scope<'a>( + scope_block: &mut ReactiveScopeBlock<'a>, + env: &mut Environment<'a>, + early_return: &EarlyReturnInfo, +) { + let scope_id = scope_block.scope; + let loc = early_return.loc; + + // Set early return value on the scope + env.scopes[scope_id.0 as usize].early_return_value = Some(ReactiveScopeEarlyReturn { + value: early_return.value, + loc: early_return.loc, + label: early_return.label, + }); + + // Add the early return identifier as a scope declaration + env.scopes[scope_id.0 as usize].declarations.push(( + early_return.value, + ReactiveScopeDeclaration { identifier: early_return.value, scope: scope_id }, + )); + + // Create temporary places for the sentinel initialization + let sentinel_temp = create_temporary_place_id(env, loc); + let symbol_temp = create_temporary_place_id(env, loc); + let for_temp = create_temporary_place_id(env, loc); + let arg_temp = create_temporary_place_id(env, loc); + + let original_instructions = std::mem::take(&mut scope_block.instructions); + + scope_block.instructions = vec![ + // LoadGlobal Symbol + ReactiveStatement::Instruction(ReactiveInstruction { + id: EvaluationOrder(0), + lvalue: Some(Place { + identifier: symbol_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + }), + value: ReactiveValue::Instruction(InstructionValue::LoadGlobal { + binding: NonLocalBinding::Global { name: "Symbol".to_string() }, + loc, + }), + effects: None, + loc, + }), + // PropertyLoad Symbol.for + ReactiveStatement::Instruction(ReactiveInstruction { + id: EvaluationOrder(0), + lvalue: Some(Place { + identifier: for_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + }), + value: ReactiveValue::Instruction(InstructionValue::PropertyLoad { + object: Place { + identifier: symbol_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + }, + property: PropertyLiteral::String("for".to_string()), + loc, + }), + effects: None, + loc, + }), + // Primitive: the sentinel string + ReactiveStatement::Instruction(ReactiveInstruction { + id: EvaluationOrder(0), + lvalue: Some(Place { + identifier: arg_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + }), + value: ReactiveValue::Instruction(InstructionValue::Primitive { + value: PrimitiveValue::String(EARLY_RETURN_SENTINEL.into()), + loc, + }), + effects: None, + loc, + }), + // MethodCall: Symbol.for("react.early_return_sentinel") + ReactiveStatement::Instruction(ReactiveInstruction { + id: EvaluationOrder(0), + lvalue: Some(Place { + identifier: sentinel_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + }), + value: ReactiveValue::Instruction(InstructionValue::MethodCall { + receiver: Place { + identifier: symbol_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + }, + property: Place { + identifier: for_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + }, + args: vec![PlaceOrSpread::Place(Place { + identifier: arg_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + })], + loc, + }), + effects: None, + loc, + }), + // StoreLocal: let earlyReturnValue = sentinel + ReactiveStatement::Instruction(ReactiveInstruction { + id: EvaluationOrder(0), + lvalue: None, + value: ReactiveValue::Instruction(InstructionValue::StoreLocal { + lvalue: LValue { + kind: InstructionKind::Let, + place: Place { + identifier: early_return.value, + effect: Effect::ConditionallyMutate, + reactive: true, + loc, + }, + }, + value: Place { + identifier: sentinel_temp, + effect: Effect::Unknown, + reactive: false, + loc: None, // GeneratedSource + }, + type_annotation: None, + loc, + }), + effects: None, + loc, + }), + // Label terminal wrapping the original instructions + ReactiveStatement::Terminal(ReactiveTerminalStatement { + label: Some(ReactiveLabel { id: early_return.label, implicit: false }), + terminal: ReactiveTerminal::Label { + block: original_instructions, + id: EvaluationOrder(0), + loc: None, // GeneratedSource + }, + }), + ]; +} + +// ============================================================================= +// Helper: create a temporary place identifier +// ============================================================================= + +fn create_temporary_place_id<'a>( + env: &mut Environment<'a>, + loc: Option, +) -> IdentifierId { + let id = env.next_identifier_id(); + env.identifiers[id.0 as usize].loc = loc; + id +} + +fn promote_temporary<'a>(env: &mut Environment<'a>, identifier_id: IdentifierId) { + let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id; + env.identifiers[identifier_id.0 as usize].name = + Some(IdentifierName::Promoted(format!("#t{}", decl_id.0))); +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_always_invalidating_scopes.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_always_invalidating_scopes.rs new file mode 100644 index 0000000000000..58f3155aa544c --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_always_invalidating_scopes.rs @@ -0,0 +1,147 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! PruneAlwaysInvalidatingScopes +//! +//! Some instructions will *always* produce a new value, and unless memoized will *always* +//! invalidate downstream reactive scopes. This pass finds such values and prunes downstream +//! memoization. +//! +//! Corresponds to `src/ReactiveScopes/PruneAlwaysInvalidatingScopes.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::{ + IdentifierId, InstructionValue, PrunedReactiveScopeBlock, ReactiveFunction, + ReactiveInstruction, ReactiveScopeBlock, ReactiveStatement, ReactiveValue, + environment::Environment, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionTransform, Transformed, transform_reactive_function, +}; + +/// Prunes scopes that always invalidate because they depend on unmemoized +/// always-invalidating values. +/// TS: `pruneAlwaysInvalidatingScopes` +pub fn prune_always_invalidating_scopes<'a>( + func: &mut ReactiveFunction<'a>, + env: &Environment<'a>, +) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + let mut transform = Transform { + env, + always_invalidating_values: FxHashSet::default(), + unmemoized_values: FxHashSet::default(), + }; + let mut state = false; // withinScope + transform_reactive_function(func, &mut transform, &mut state) +} + +struct Transform<'a, 'e> { + env: &'e Environment<'a>, + always_invalidating_values: FxHashSet, + unmemoized_values: FxHashSet, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for Transform<'a, 'e> { + type State = bool; // withinScope + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn transform_instruction( + &mut self, + instruction: &mut ReactiveInstruction<'a>, + within_scope: &mut bool, + ) -> Result>, crate::react_compiler_diagnostics::CompilerError> + { + self.visit_instruction(instruction, within_scope)?; + + let lvalue = &instruction.lvalue; + match &instruction.value { + ReactiveValue::Instruction( + InstructionValue::ArrayExpression { .. } + | InstructionValue::ObjectExpression { .. } + | InstructionValue::JsxExpression { .. } + | InstructionValue::JsxFragment { .. } + | InstructionValue::NewExpression { .. }, + ) => { + if let Some(lv) = lvalue { + self.always_invalidating_values.insert(lv.identifier); + if !*within_scope { + self.unmemoized_values.insert(lv.identifier); + } + } + } + ReactiveValue::Instruction(InstructionValue::StoreLocal { + value: store_value, + lvalue: store_lvalue, + .. + }) => { + if self.always_invalidating_values.contains(&store_value.identifier) { + self.always_invalidating_values.insert(store_lvalue.place.identifier); + } + if self.unmemoized_values.contains(&store_value.identifier) { + self.unmemoized_values.insert(store_lvalue.place.identifier); + } + } + ReactiveValue::Instruction(InstructionValue::LoadLocal { place, .. }) => { + if let Some(lv) = lvalue { + if self.always_invalidating_values.contains(&place.identifier) { + self.always_invalidating_values.insert(lv.identifier); + } + if self.unmemoized_values.contains(&place.identifier) { + self.unmemoized_values.insert(lv.identifier); + } + } + } + _ => {} + } + Ok(Transformed::Keep) + } + + fn transform_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + _within_scope: &mut bool, + ) -> Result>, crate::react_compiler_diagnostics::CompilerError> + { + let mut within_scope = true; + self.visit_scope(scope, &mut within_scope)?; + + let scope_id = scope.scope; + let scope_data = &self.env.scopes[scope_id.0 as usize]; + + for dep in &scope_data.dependencies { + if self.unmemoized_values.contains(&dep.identifier) { + // This scope depends on an always-invalidating value, prune it + // Propagate always-invalidating and unmemoized to declarations/reassignments + let decl_ids: Vec = + scope_data.declarations.iter().map(|(_, decl)| decl.identifier).collect(); + let reassign_ids: Vec = scope_data.reassignments.clone(); + + for id in &decl_ids { + if self.always_invalidating_values.contains(id) { + self.unmemoized_values.insert(*id); + } + } + for id in &reassign_ids { + if self.always_invalidating_values.contains(id) { + self.unmemoized_values.insert(*id); + } + } + + return Ok(Transformed::Replace(ReactiveStatement::PrunedScope( + PrunedReactiveScopeBlock { + scope: scope.scope, + instructions: std::mem::take(&mut scope.instructions), + }, + ))); + } + } + Ok(Transformed::Keep) + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_hoisted_contexts.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_hoisted_contexts.rs new file mode 100644 index 0000000000000..229fee01df429 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_hoisted_contexts.rs @@ -0,0 +1,207 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! PruneHoistedContexts — removes hoisted context variable declarations +//! and transforms references to their original instruction kinds. +//! +//! Corresponds to `src/ReactiveScopes/PruneHoistedContexts.ts`. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::{CompilerError, CompilerErrorDetail, ErrorCategory}; +use crate::react_compiler_hir::{ + EvaluationOrder, IdentifierId, InstructionKind, InstructionValue, Place, ReactiveFunction, + ReactiveInstruction, ReactiveScopeBlock, ReactiveStatement, ReactiveValue, + environment::Environment, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionTransform, Transformed, transform_reactive_function, +}; + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Prunes DeclareContexts lowered for HoistedConsts and transforms any +/// references back to their original instruction kind. +/// TS: `pruneHoistedContexts` +pub fn prune_hoisted_contexts<'a>( + func: &mut ReactiveFunction<'a>, + env: &Environment<'a>, +) -> Result<(), CompilerError> { + let mut transform = Transform { env }; + let mut state = VisitorState { active_scopes: Vec::new(), uninitialized: FxHashMap::default() }; + transform_reactive_function(func, &mut transform, &mut state) +} + +// ============================================================================= +// State +// ============================================================================= + +#[derive(Debug, Clone)] +enum UninitializedKind { + UnknownKind, + Func { definition: Option }, +} + +struct VisitorState { + active_scopes: Vec>, + uninitialized: FxHashMap, +} + +impl VisitorState { + fn find_in_active_scopes(&self, id: IdentifierId) -> bool { + for scope in &self.active_scopes { + if scope.contains(&id) { + return true; + } + } + false + } +} + +struct Transform<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for Transform<'a, 'e> { + type State = VisitorState; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut VisitorState, + ) -> Result<(), CompilerError> { + let scope_data = &self.env.scopes[scope.scope.0 as usize]; + let decl_ids: rustc_hash::FxHashSet = + scope_data.declarations.iter().map(|(id, _)| *id).collect(); + + // Add declared but not initialized variables + for (_, decl) in &scope_data.declarations { + state.uninitialized.insert(decl.identifier, UninitializedKind::UnknownKind); + } + + state.active_scopes.push(decl_ids); + self.traverse_scope(scope, state)?; + state.active_scopes.pop(); + + // Clean up uninitialized after scope + let scope_data = &self.env.scopes[scope.scope.0 as usize]; + for (_, decl) in &scope_data.declarations { + state.uninitialized.remove(&decl.identifier); + } + Ok(()) + } + + fn visit_place( + &mut self, + _id: EvaluationOrder, + place: &Place, + state: &mut VisitorState, + ) -> Result<(), CompilerError> { + if let Some(kind) = state.uninitialized.get(&place.identifier) { + if let UninitializedKind::Func { definition } = kind { + if *definition != Some(place.identifier) { + let mut err = CompilerError::new(); + err.push_error_detail( + CompilerErrorDetail::new( + ErrorCategory::Todo, + "[PruneHoistedContexts] Rewrite hoisted function references" + .to_string(), + ) + .with_loc(place.loc), + ); + return Err(err); + } + } + } + Ok(()) + } + + fn transform_instruction( + &mut self, + instruction: &mut ReactiveInstruction<'a>, + state: &mut VisitorState, + ) -> Result>, CompilerError> { + // Remove hoisted declarations to preserve TDZ + if let ReactiveValue::Instruction(InstructionValue::DeclareContext { lvalue, .. }) = + &instruction.value + { + let maybe_non_hoisted = convert_hoisted_lvalue_kind(lvalue.kind); + if let Some(non_hoisted) = maybe_non_hoisted { + if non_hoisted == InstructionKind::Function + && state.uninitialized.contains_key(&lvalue.place.identifier) + { + state.uninitialized.insert( + lvalue.place.identifier, + UninitializedKind::Func { definition: None }, + ); + } + return Ok(Transformed::Remove); + } + } + + if let ReactiveValue::Instruction(InstructionValue::StoreContext { lvalue, .. }) = + &mut instruction.value + { + if lvalue.kind != InstructionKind::Reassign { + let lvalue_id = lvalue.place.identifier; + let is_declared_by_scope = state.find_in_active_scopes(lvalue_id); + if is_declared_by_scope { + if lvalue.kind == InstructionKind::Let || lvalue.kind == InstructionKind::Const + { + lvalue.kind = InstructionKind::Reassign; + } else if lvalue.kind == InstructionKind::Function { + if let Some(kind) = state.uninitialized.get(&lvalue_id) { + if !matches!(kind, UninitializedKind::Func { .. }) { + let mut err = CompilerError::new(); + err.push_error_detail( + CompilerErrorDetail::new( + ErrorCategory::Invariant, + "[PruneHoistedContexts] Unexpected hoisted function" + .to_string(), + ) + .with_loc(instruction.loc), + ); + return Err(err); + } + // References to hoisted functions are now "safe" as + // variable assignments have finished. + state.uninitialized.remove(&lvalue_id); + } + } else { + let mut err = CompilerError::new(); + err.push_error_detail( + CompilerErrorDetail::new( + ErrorCategory::Todo, + "[PruneHoistedContexts] Unexpected kind".to_string(), + ) + .with_loc(instruction.loc), + ); + return Err(err); + } + } + } + } + + self.visit_instruction(instruction, state)?; + Ok(Transformed::Keep) + } +} + +/// Corresponds to TS `convertHoistedLValueKind` — returns None for non-hoisted kinds. +fn convert_hoisted_lvalue_kind(kind: InstructionKind) -> Option { + match kind { + InstructionKind::HoistedLet => Some(InstructionKind::Let), + InstructionKind::HoistedConst => Some(InstructionKind::Const), + InstructionKind::HoistedFunction => Some(InstructionKind::Function), + _ => None, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_non_escaping_scopes.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_non_escaping_scopes.rs new file mode 100644 index 0000000000000..4fbcd10a2e951 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_non_escaping_scopes.rs @@ -0,0 +1,1177 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! PruneNonEscapingScopes — prunes reactive scopes that are not necessary +//! to bound downstream computation. +//! +//! Corresponds to `src/ReactiveScopes/PruneNonEscapingScopes.ts`. + +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::ArrayPatternElement; +use crate::react_compiler_hir::DeclarationId; +use crate::react_compiler_hir::Effect; +use crate::react_compiler_hir::EvaluationOrder; +use crate::react_compiler_hir::IdentifierId; +use crate::react_compiler_hir::InstructionKind; +use crate::react_compiler_hir::InstructionValue; +use crate::react_compiler_hir::JsxAttribute; +use crate::react_compiler_hir::JsxTag; +use crate::react_compiler_hir::ObjectPropertyOrSpread; +use crate::react_compiler_hir::Pattern; +use crate::react_compiler_hir::Place; +use crate::react_compiler_hir::PlaceOrSpread; +use crate::react_compiler_hir::ReactiveFunction; +use crate::react_compiler_hir::ReactiveInstruction; +use crate::react_compiler_hir::ReactiveScopeBlock; +use crate::react_compiler_hir::ReactiveStatement; +use crate::react_compiler_hir::ReactiveTerminal; +use crate::react_compiler_hir::ReactiveTerminalStatement; +use crate::react_compiler_hir::ReactiveValue; +use crate::react_compiler_hir::ScopeId; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::each_instruction_value_operand; +use crate::react_compiler_utils::FxIndexSet; + +use crate::react_compiler_reactive_scopes::visitors::ReactiveFunctionTransform; +use crate::react_compiler_reactive_scopes::visitors::ReactiveFunctionVisitor; +use crate::react_compiler_reactive_scopes::visitors::Transformed; +use crate::react_compiler_reactive_scopes::visitors::transform_reactive_function; +use crate::react_compiler_reactive_scopes::visitors::visit_reactive_function; + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Prunes reactive scopes whose outputs don't escape. +/// TS: `pruneNonEscapingScopes` +pub fn prune_non_escaping_scopes<'a>( + func: &mut ReactiveFunction<'a>, + env: &mut Environment<'a>, +) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + // First build up a map of which instructions are involved in creating which values, + // and which values are returned. + let mut state = CollectState::new(); + for param in &func.params { + let place = match param { + crate::react_compiler_hir::ParamPattern::Place(p) => p, + crate::react_compiler_hir::ParamPattern::Spread(s) => &s.place, + }; + let identifier = &env.identifiers[place.identifier.0 as usize]; + state.declare(identifier.declaration_id); + } + let visitor = CollectDependenciesVisitor::new(env); + let mut visitor_state = (state, Vec::::new()); + visit_reactive_function(func, &visitor, &mut visitor_state); + let (state, _) = visitor_state; + + // Then walk outward from the returned values and find all captured operands. + let memoized = compute_memoized_identifiers(&state); + + // Prune scopes that do not declare/reassign any escaping values + let mut transform = PruneScopesTransform { + env, + pruned_scopes: FxHashSet::default(), + reassignments: FxHashMap::default(), + }; + let mut memoized_state = memoized; + transform_reactive_function(func, &mut transform, &mut memoized_state) +} + +// ============================================================================= +// MemoizationLevel +// ============================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MemoizationLevel { + /// The value should be memoized if it escapes + Memoized, + /// Values that are memoized if their dependencies are memoized + Conditional, + /// Values that cannot be compared with Object.is, but which by default don't need to be memoized + Unmemoized, + /// The value will never be memoized: used for values that can be cheaply compared w Object.is + Never, +} + +/// Given an identifier that appears as an lvalue multiple times with different memoization levels, +/// determines the final memoization level. +fn join_aliases(kind1: MemoizationLevel, kind2: MemoizationLevel) -> MemoizationLevel { + if kind1 == MemoizationLevel::Memoized || kind2 == MemoizationLevel::Memoized { + MemoizationLevel::Memoized + } else if kind1 == MemoizationLevel::Conditional || kind2 == MemoizationLevel::Conditional { + MemoizationLevel::Conditional + } else if kind1 == MemoizationLevel::Unmemoized || kind2 == MemoizationLevel::Unmemoized { + MemoizationLevel::Unmemoized + } else { + MemoizationLevel::Never + } +} + +// ============================================================================= +// Graph nodes +// ============================================================================= + +/// A node in the graph describing the memoization level of a given identifier +/// as well as its dependencies and scopes. +struct IdentifierNode { + level: MemoizationLevel, + memoized: bool, + dependencies: FxIndexSet, + scopes: FxIndexSet, + seen: bool, +} + +/// A scope node describing its dependencies. +struct ScopeNode { + dependencies: Vec, + seen: bool, +} + +// ============================================================================= +// CollectState (TS: State class) +// ============================================================================= + +struct CollectState { + /// Maps lvalues for LoadLocal to the identifier being loaded, to resolve indirections. + definitions: FxHashMap, + identifiers: FxHashMap, + scopes: FxHashMap, + escaping_values: FxIndexSet, +} + +impl CollectState { + fn new() -> Self { + CollectState { + definitions: FxHashMap::default(), + identifiers: FxHashMap::default(), + scopes: FxHashMap::default(), + escaping_values: FxIndexSet::default(), + } + } + + /// Declare a new identifier, used for function id and params. + fn declare(&mut self, id: DeclarationId) { + self.identifiers.insert( + id, + IdentifierNode { + level: MemoizationLevel::Never, + memoized: false, + dependencies: FxIndexSet::default(), + scopes: FxIndexSet::default(), + seen: false, + }, + ); + } + + /// Associates the identifier with its scope, if there is one and it is active for + /// the given instruction id. + fn visit_operand<'a>( + &mut self, + env: &Environment<'a>, + id: EvaluationOrder, + place: &Place, + identifier: DeclarationId, + ) { + if let Some(scope_id) = get_place_scope(env, id, place.identifier) { + let node = self.scopes.entry(scope_id).or_insert_with(|| { + let scope_data = &env.scopes[scope_id.0 as usize]; + let dependencies = scope_data + .dependencies + .iter() + .map(|dep| env.identifiers[dep.identifier.0 as usize].declaration_id) + .collect(); + ScopeNode { dependencies, seen: false } + }); + // Avoid unused variable warning — we needed the entry to exist + let _ = node; + let identifier_node = self + .identifiers + .get_mut(&identifier) + .expect("Expected identifier to be initialized"); + identifier_node.scopes.insert(scope_id); + } + } + + /// Resolve an identifier through definitions (LoadLocal indirections). + fn resolve(&self, id: DeclarationId) -> DeclarationId { + self.definitions.get(&id).copied().unwrap_or(id) + } +} + +// ============================================================================= +// MemoizationOptions +// ============================================================================= + +struct MemoizationOptions { + memoize_jsx_elements: bool, + force_memoize_primitives: bool, +} + +// ============================================================================= +// LValueMemoization +// ============================================================================= + +struct LValueMemoization { + place_identifier: IdentifierId, + level: MemoizationLevel, +} + +// ============================================================================= +// Helper: get_place_scope +// ============================================================================= + +fn get_place_scope<'a>( + env: &Environment<'a>, + id: EvaluationOrder, + identifier_id: IdentifierId, +) -> Option { + let scope_id = env.identifiers[identifier_id.0 as usize].scope?; + if env.scopes[scope_id.0 as usize].range.contains(id) { Some(scope_id) } else { None } +} + +// ============================================================================= +// Helper: get_function_call_signature (for noAlias check) +// ============================================================================= + +// ============================================================================= +// Helper: compute pattern lvalues +// ============================================================================= + +fn compute_pattern_lvalues(pattern: &Pattern) -> Vec { + let mut lvalues = Vec::new(); + match pattern { + Pattern::Array(array_pattern) => { + for item in &array_pattern.items { + match item { + ArrayPatternElement::Place(place) => { + lvalues.push(LValueMemoization { + place_identifier: place.identifier, + level: MemoizationLevel::Conditional, + }); + } + ArrayPatternElement::Spread(spread) => { + lvalues.push(LValueMemoization { + place_identifier: spread.place.identifier, + level: MemoizationLevel::Memoized, + }); + } + ArrayPatternElement::Hole => {} + } + } + } + Pattern::Object(object_pattern) => { + for property in &object_pattern.properties { + match property { + ObjectPropertyOrSpread::Property(prop) => { + lvalues.push(LValueMemoization { + place_identifier: prop.place.identifier, + level: MemoizationLevel::Conditional, + }); + } + ObjectPropertyOrSpread::Spread(spread) => { + lvalues.push(LValueMemoization { + place_identifier: spread.place.identifier, + level: MemoizationLevel::Memoized, + }); + } + } + } + } + } + lvalues +} + +// ============================================================================= +// CollectDependenciesVisitor +// ============================================================================= + +struct CollectDependenciesVisitor<'a, 'e> { + env: &'e Environment<'a>, + options: MemoizationOptions, +} + +impl<'a, 'e> CollectDependenciesVisitor<'a, 'e> { + fn new(env: &'e Environment<'a>) -> Self { + CollectDependenciesVisitor { + env, + options: MemoizationOptions { + memoize_jsx_elements: !env.config.enable_forest, + force_memoize_primitives: env.config.enable_forest + || env.enable_preserve_existing_memoization_guarantees, + }, + } + } + + /// Given a value, returns a description of how it should be memoized. + fn compute_memoization_inputs( + &self, + id: EvaluationOrder, + value: &ReactiveValue<'a>, + lvalue: Option, + state: &mut CollectState, + ) -> (Vec, Vec<(IdentifierId, EvaluationOrder)>) { + match value { + ReactiveValue::ConditionalExpression { consequent, alternate, .. } => { + let (_, cons_rvalues) = + self.compute_memoization_inputs(id, consequent, None, state); + let (_, alt_rvalues) = self.compute_memoization_inputs(id, alternate, None, state); + let mut rvalues = cons_rvalues; + rvalues.extend(alt_rvalues); + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, rvalues) + } + ReactiveValue::LogicalExpression { left, right, .. } => { + let (_, left_rvalues) = self.compute_memoization_inputs(id, left, None, state); + let (_, right_rvalues) = self.compute_memoization_inputs(id, right, None, state); + let mut rvalues = left_rvalues; + rvalues.extend(right_rvalues); + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, rvalues) + } + ReactiveValue::SequenceExpression { instructions, value: inner, .. } => { + for instr in instructions { + self.visit_value_for_memoization( + instr.id, + &instr.value, + instr.lvalue.as_ref().map(|lv| lv.identifier), + state, + ); + } + let (_, rvalues) = self.compute_memoization_inputs(id, inner, None, state); + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, rvalues) + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + let (_, rvalues) = self.compute_memoization_inputs(id, inner, None, state); + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, rvalues) + } + ReactiveValue::Instruction(instr_value) => { + self.compute_instruction_memoization_inputs(id, instr_value, lvalue) + } + } + } + + /// Compute memoization inputs for an InstructionValue. + fn compute_instruction_memoization_inputs( + &self, + id: EvaluationOrder, + value: &InstructionValue<'a>, + lvalue: Option, + ) -> (Vec, Vec<(IdentifierId, EvaluationOrder)>) { + let env = self.env; + let options = &self.options; + + match value { + InstructionValue::JsxExpression { tag, props, children, .. } => { + let mut rvalues: Vec<(IdentifierId, EvaluationOrder)> = Vec::new(); + if let JsxTag::Place(place) = tag { + rvalues.push((place.identifier, id)); + } + for prop in props { + match prop { + JsxAttribute::Attribute { place, .. } => { + rvalues.push((place.identifier, id)); + } + JsxAttribute::SpreadAttribute { argument, .. } => { + rvalues.push((argument.identifier, id)); + } + } + } + if let Some(children) = children { + for child in children { + rvalues.push((child.identifier, id)); + } + } + let level = if options.memoize_jsx_elements { + MemoizationLevel::Memoized + } else { + MemoizationLevel::Unmemoized + }; + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { place_identifier: lv, level }] + } else { + vec![] + }; + (lvalues, rvalues) + } + InstructionValue::JsxFragment { children, .. } => { + let level = if options.memoize_jsx_elements { + MemoizationLevel::Memoized + } else { + MemoizationLevel::Unmemoized + }; + let rvalues: Vec<(IdentifierId, EvaluationOrder)> = + children.iter().map(|c| (c.identifier, id)).collect(); + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { place_identifier: lv, level }] + } else { + vec![] + }; + (lvalues, rvalues) + } + InstructionValue::NextPropertyOf { .. } + | InstructionValue::StartMemoize { .. } + | InstructionValue::FinishMemoize { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::ComputedDelete { .. } + | InstructionValue::PropertyDelete { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::TemplateLiteral { .. } + | InstructionValue::Primitive { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::BinaryExpression { .. } + | InstructionValue::UnaryExpression { .. } => { + if options.force_memoize_primitives { + let level = MemoizationLevel::Conditional; + let operands = each_instruction_value_operand(value, env); + let rvalues: Vec<(IdentifierId, EvaluationOrder)> = + operands.iter().map(|p| (p.identifier, id)).collect(); + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { place_identifier: lv, level }] + } else { + vec![] + }; + (lvalues, rvalues) + } else { + let level = MemoizationLevel::Never; + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { place_identifier: lv, level }] + } else { + vec![] + }; + (lvalues, vec![]) + } + } + InstructionValue::Await { value: inner, .. } + | InstructionValue::TypeCastExpression { value: inner, .. } => { + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, vec![(inner.identifier, id)]) + } + InstructionValue::IteratorNext { iterator, collection, .. } => { + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, vec![(iterator.identifier, id), (collection.identifier, id)]) + } + InstructionValue::GetIterator { collection, .. } => { + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, vec![(collection.identifier, id)]) + } + InstructionValue::LoadLocal { place, .. } => { + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, vec![(place.identifier, id)]) + } + InstructionValue::LoadContext { place, .. } => { + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }] + } else { + vec![] + }; + (lvalues, vec![(place.identifier, id)]) + } + InstructionValue::DeclareContext { lvalue: decl_lvalue, .. } => { + let mut lvalues = vec![LValueMemoization { + place_identifier: decl_lvalue.place.identifier, + level: MemoizationLevel::Memoized, + }]; + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Unmemoized, + }); + } + (lvalues, vec![]) + } + InstructionValue::DeclareLocal { lvalue: decl_lvalue, .. } => { + let mut lvalues = vec![LValueMemoization { + place_identifier: decl_lvalue.place.identifier, + level: MemoizationLevel::Unmemoized, + }]; + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Unmemoized, + }); + } + (lvalues, vec![]) + } + InstructionValue::PrefixUpdate { lvalue: upd_lvalue, value: upd_value, .. } + | InstructionValue::PostfixUpdate { lvalue: upd_lvalue, value: upd_value, .. } => { + let mut lvalues = vec![LValueMemoization { + place_identifier: upd_lvalue.identifier, + level: MemoizationLevel::Conditional, + }]; + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }); + } + (lvalues, vec![(upd_value.identifier, id)]) + } + InstructionValue::StoreLocal { lvalue: store_lvalue, value: store_value, .. } => { + let mut lvalues = vec![LValueMemoization { + place_identifier: store_lvalue.place.identifier, + level: MemoizationLevel::Conditional, + }]; + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }); + } + (lvalues, vec![(store_value.identifier, id)]) + } + InstructionValue::StoreContext { lvalue: store_lvalue, value: store_value, .. } => { + let mut lvalues = vec![LValueMemoization { + place_identifier: store_lvalue.place.identifier, + level: MemoizationLevel::Memoized, + }]; + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }); + } + (lvalues, vec![(store_value.identifier, id)]) + } + InstructionValue::StoreGlobal { value: store_value, .. } => { + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Unmemoized, + }] + } else { + vec![] + }; + (lvalues, vec![(store_value.identifier, id)]) + } + InstructionValue::Destructure { lvalue: dest_lvalue, value: dest_value, .. } => { + let mut lvalues = Vec::new(); + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }); + } + lvalues.extend(compute_pattern_lvalues(&dest_lvalue.pattern)); + (lvalues, vec![(dest_value.identifier, id)]) + } + InstructionValue::ComputedLoad { object, .. } + | InstructionValue::PropertyLoad { object, .. } => { + let level = MemoizationLevel::Conditional; + let lvalues = if let Some(lv) = lvalue { + vec![LValueMemoization { place_identifier: lv, level }] + } else { + vec![] + }; + (lvalues, vec![(object.identifier, id)]) + } + InstructionValue::ComputedStore { object, value: store_value, .. } => { + let mut lvalues = vec![LValueMemoization { + place_identifier: object.identifier, + level: MemoizationLevel::Conditional, + }]; + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Conditional, + }); + } + (lvalues, vec![(store_value.identifier, id)]) + } + InstructionValue::TaggedTemplateExpression { tag, .. } => { + let no_alias = env.has_no_alias_signature(tag.identifier); + let mut lvalues = Vec::new(); + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Memoized, + }); + } + if no_alias { + return (lvalues, vec![]); + } + let operands = each_instruction_value_operand(value, env); + for op in &operands { + if op.effect.is_mutable() { + lvalues.push(LValueMemoization { + place_identifier: op.identifier, + level: MemoizationLevel::Memoized, + }); + } + } + let rvalues: Vec<(IdentifierId, EvaluationOrder)> = + operands.iter().map(|p| (p.identifier, id)).collect(); + (lvalues, rvalues) + } + InstructionValue::CallExpression { callee, .. } => { + let no_alias = env.has_no_alias_signature(callee.identifier); + let mut lvalues = Vec::new(); + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Memoized, + }); + } + if no_alias { + return (lvalues, vec![]); + } + let operands = each_instruction_value_operand(value, env); + for op in &operands { + if op.effect.is_mutable() { + lvalues.push(LValueMemoization { + place_identifier: op.identifier, + level: MemoizationLevel::Memoized, + }); + } + } + let rvalues: Vec<(IdentifierId, EvaluationOrder)> = + operands.iter().map(|p| (p.identifier, id)).collect(); + (lvalues, rvalues) + } + InstructionValue::MethodCall { property, .. } => { + let no_alias = env.has_no_alias_signature(property.identifier); + let mut lvalues = Vec::new(); + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Memoized, + }); + } + if no_alias { + return (lvalues, vec![]); + } + let operands = each_instruction_value_operand(value, env); + for op in &operands { + if op.effect.is_mutable() { + lvalues.push(LValueMemoization { + place_identifier: op.identifier, + level: MemoizationLevel::Memoized, + }); + } + } + let rvalues: Vec<(IdentifierId, EvaluationOrder)> = + operands.iter().map(|p| (p.identifier, id)).collect(); + (lvalues, rvalues) + } + InstructionValue::RegExpLiteral { .. } + | InstructionValue::ArrayExpression { .. } + | InstructionValue::NewExpression { .. } + | InstructionValue::ObjectExpression { .. } + | InstructionValue::PropertyStore { .. } => { + let operands = each_instruction_value_operand(value, env); + let mut lvalues: Vec = operands + .iter() + .filter(|op| op.effect.is_mutable()) + .map(|op| LValueMemoization { + place_identifier: op.identifier, + level: MemoizationLevel::Memoized, + }) + .collect(); + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Memoized, + }); + } + let rvalues: Vec<(IdentifierId, EvaluationOrder)> = + operands.iter().map(|p| (p.identifier, id)).collect(); + (lvalues, rvalues) + } + InstructionValue::ObjectMethod { .. } | InstructionValue::FunctionExpression { .. } => { + // The canonical each_instruction_value_operand already includes context + // (captured variables) for FunctionExpression/ObjectMethod. + let operands = each_instruction_value_operand(value, env); + let mut lvalues: Vec = operands + .iter() + .filter(|op| op.effect.is_mutable()) + .map(|op| LValueMemoization { + place_identifier: op.identifier, + level: MemoizationLevel::Memoized, + }) + .collect(); + if let Some(lv) = lvalue { + lvalues.push(LValueMemoization { + place_identifier: lv, + level: MemoizationLevel::Memoized, + }); + } + let rvalues: Vec<(IdentifierId, EvaluationOrder)> = + operands.iter().map(|p| (p.identifier, id)).collect(); + (lvalues, rvalues) + } + } + } + + fn visit_value_for_memoization( + &self, + id: EvaluationOrder, + value: &ReactiveValue<'a>, + lvalue: Option, + state: &mut CollectState, + ) { + let env = self.env; + // Determine the level of memoization for this value and the lvalues/rvalues + let (aliasing_lvalues, aliasing_rvalues) = + self.compute_memoization_inputs(id, value, lvalue, state); + + // Associate all the rvalues with the instruction's scope if it has one + // We need to collect rvalue data first to avoid borrow issues + let rvalue_data: Vec<(IdentifierId, DeclarationId)> = aliasing_rvalues + .iter() + .map(|(identifier_id, _)| { + let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id; + let operand_id = state.resolve(decl_id); + (*identifier_id, operand_id) + }) + .collect(); + + for (identifier_id, operand_id) in &rvalue_data { + // Build the Place data needed for get_place_scope + state.visit_operand( + env, + id, + &Place { + identifier: *identifier_id, + effect: Effect::Read, + reactive: false, + loc: None, + }, + *operand_id, + ); + } + + // Add the operands as dependencies of all lvalues + for lv in &aliasing_lvalues { + let lvalue_decl_id = env.identifiers[lv.place_identifier.0 as usize].declaration_id; + let lvalue_id = state.resolve(lvalue_decl_id); + let node = state.identifiers.entry(lvalue_id).or_insert_with(|| IdentifierNode { + level: MemoizationLevel::Never, + memoized: false, + dependencies: FxIndexSet::default(), + scopes: FxIndexSet::default(), + seen: false, + }); + node.level = join_aliases(node.level, lv.level); + for (_, operand_id) in &rvalue_data { + if *operand_id == lvalue_id { + continue; + } + node.dependencies.insert(*operand_id); + } + + state.visit_operand( + env, + id, + &Place { + identifier: lv.place_identifier, + effect: Effect::Read, + reactive: false, + loc: None, + }, + lvalue_id, + ); + } + + // Handle LoadLocal definitions and hook calls + if let ReactiveValue::Instruction(instr_value) = value { + if let InstructionValue::LoadLocal { place, .. } = instr_value { + if let Some(lv_id) = lvalue { + let lv_decl = env.identifiers[lv_id.0 as usize].declaration_id; + let place_decl = env.identifiers[place.identifier.0 as usize].declaration_id; + state.definitions.insert(lv_decl, place_decl); + } + } else if let InstructionValue::CallExpression { callee, args, .. } = instr_value { + if env.get_hook_kind_for_id(callee.identifier).ok().flatten().is_some() { + let no_alias = env.has_no_alias_signature(callee.identifier); + if !no_alias { + for arg in args { + let place = match arg { + PlaceOrSpread::Spread(spread) => &spread.place, + PlaceOrSpread::Place(place) => place, + }; + let decl = env.identifiers[place.identifier.0 as usize].declaration_id; + state.escaping_values.insert(decl); + } + } + } + } else if let InstructionValue::MethodCall { property, args, .. } = instr_value { + if env.get_hook_kind_for_id(property.identifier).ok().flatten().is_some() { + let no_alias = env.has_no_alias_signature(property.identifier); + if !no_alias { + for arg in args { + let place = match arg { + PlaceOrSpread::Spread(spread) => &spread.place, + PlaceOrSpread::Place(place) => place, + }; + let decl = env.identifiers[place.identifier.0 as usize].declaration_id; + state.escaping_values.insert(decl); + } + } + } + } + } + } +} + +// ============================================================================= +// ReactiveFunctionVisitor impl for CollectDependenciesVisitor +// ============================================================================= + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for CollectDependenciesVisitor<'a, 'e> { + type State = (CollectState, Vec); + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_instruction(&self, instruction: &ReactiveInstruction<'a>, state: &mut Self::State) { + self.visit_value_for_memoization( + instruction.id, + &instruction.value, + instruction.lvalue.as_ref().map(|lv| lv.identifier), + &mut state.0, + ); + } + + fn visit_terminal(&self, stmt: &ReactiveTerminalStatement<'a>, state: &mut Self::State) { + // Traverse terminal blocks first (TS: this.traverseTerminal(stmt, scopes)) + self.traverse_terminal(stmt, state); + + // Handle return terminals + if let ReactiveTerminal::Return { value, .. } = &stmt.terminal { + let env = self.env; + let decl = env.identifiers[value.identifier.0 as usize].declaration_id; + state.0.escaping_values.insert(decl); + + // If the return is within a scope, associate those scopes with the returned value + let identifier_node = + state.0.identifiers.get_mut(&decl).expect("Expected identifier to be initialized"); + for scope_id in &state.1 { + identifier_node.scopes.insert(*scope_id); + } + } + } + + fn visit_scope(&self, scope: &ReactiveScopeBlock<'a>, state: &mut Self::State) { + let env = self.env; + let scope_id = scope.scope; + let scope_data = &env.scopes[scope_id.0 as usize]; + + // If a scope reassigns any variables, set the chain of active scopes as a dependency + // of those variables. + for reassignment_id in &scope_data.reassignments { + let decl = env.identifiers[reassignment_id.0 as usize].declaration_id; + let identifier_node = + state.0.identifiers.get_mut(&decl).expect("Expected identifier to be initialized"); + for s in &state.1 { + identifier_node.scopes.insert(*s); + } + identifier_node.scopes.insert(scope_id); + } + + // TS: this.traverseScope(scope, [...scopes, scope.scope]) + state.1.push(scope_id); + self.traverse_scope(scope, state); + state.1.pop(); + } +} + +// ============================================================================= +// computeMemoizedIdentifiers +// ============================================================================= + +fn compute_memoized_identifiers(state: &CollectState) -> FxHashSet { + let mut memoized = FxHashSet::default(); + + // We need mutable access to the nodes, so we clone the state into mutable structures + let mut identifier_nodes: FxHashMap< + DeclarationId, + (MemoizationLevel, bool, FxIndexSet, FxIndexSet, bool), + > = state + .identifiers + .iter() + .map(|(id, node)| { + ( + *id, + ( + node.level, + node.memoized, + node.dependencies.clone(), + node.scopes.clone(), + node.seen, + ), + ) + }) + .collect(); + + let mut scope_nodes: FxHashMap, bool)> = state + .scopes + .iter() + .map(|(id, node)| (*id, (node.dependencies.clone(), node.seen))) + .collect(); + + fn visit( + id: DeclarationId, + force_memoize: bool, + identifier_nodes: &mut FxHashMap< + DeclarationId, + (MemoizationLevel, bool, FxIndexSet, FxIndexSet, bool), + >, + scope_nodes: &mut FxHashMap, bool)>, + memoized: &mut FxHashSet, + ) -> bool { + let Some(&(level, _, _, _, seen)) = identifier_nodes.get(&id) else { + return false; + }; + if seen { + return identifier_nodes.get(&id).unwrap().1; + } + + // Mark as seen, temporarily mark as non-memoized + identifier_nodes.get_mut(&id).unwrap().4 = true; // seen = true + identifier_nodes.get_mut(&id).unwrap().1 = false; // memoized = false + + // Visit dependencies + let deps: Vec = + identifier_nodes.get(&id).unwrap().2.iter().copied().collect(); + let mut has_memoized_dependency = false; + for dep in deps { + let is_dep_memoized = visit(dep, false, identifier_nodes, scope_nodes, memoized); + has_memoized_dependency |= is_dep_memoized; + } + + if level == MemoizationLevel::Memoized + || (level == MemoizationLevel::Conditional + && (has_memoized_dependency || force_memoize)) + || (level == MemoizationLevel::Unmemoized && force_memoize) + { + identifier_nodes.get_mut(&id).unwrap().1 = true; // memoized = true + memoized.insert(id); + let scopes: Vec = + identifier_nodes.get(&id).unwrap().3.iter().copied().collect(); + for scope_id in scopes { + force_memoize_scope_dependencies(scope_id, identifier_nodes, scope_nodes, memoized); + } + } + identifier_nodes.get(&id).unwrap().1 + } + + fn force_memoize_scope_dependencies( + id: ScopeId, + identifier_nodes: &mut FxHashMap< + DeclarationId, + (MemoizationLevel, bool, FxIndexSet, FxIndexSet, bool), + >, + scope_nodes: &mut FxHashMap, bool)>, + memoized: &mut FxHashSet, + ) { + let seen = scope_nodes.get(&id).expect("Expected a node for all scopes").1; + if seen { + return; + } + scope_nodes.get_mut(&id).unwrap().1 = true; // seen = true + + let deps: Vec = scope_nodes.get(&id).unwrap().0.clone(); + for dep in deps { + visit(dep, true, identifier_nodes, scope_nodes, memoized); + } + } + + // Walk from the "roots" aka returned/escaping identifiers + let escaping: Vec = state.escaping_values.iter().copied().collect(); + for value in escaping { + visit(value, false, &mut identifier_nodes, &mut scope_nodes, &mut memoized); + } + + memoized +} + +// ============================================================================= +// PruneScopesTransform +// ============================================================================= + +struct PruneScopesTransform<'a, 'e> { + env: &'e Environment<'a>, + pruned_scopes: FxHashSet, + reassignments: FxHashMap>, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for PruneScopesTransform<'a, 'e> { + type State = FxHashSet; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn transform_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut FxHashSet, + ) -> Result>, crate::react_compiler_diagnostics::CompilerError> + { + self.visit_scope(scope, state)?; + + let scope_id = scope.scope; + let scope_data = &self.env.scopes[scope_id.0 as usize]; + + // Keep scopes that appear empty (value being memoized may be early-returned) + // or have early return values + if (scope_data.declarations.is_empty() && scope_data.reassignments.is_empty()) + || scope_data.early_return_value.is_some() + { + return Ok(Transformed::Keep); + } + + let has_memoized_output = scope_data.declarations.iter().any(|(_, decl)| { + let decl_id = self.env.identifiers[decl.identifier.0 as usize].declaration_id; + state.contains(&decl_id) + }) || scope_data.reassignments.iter().any(|reassign_id| { + let decl_id = self.env.identifiers[reassign_id.0 as usize].declaration_id; + state.contains(&decl_id) + }); + + if has_memoized_output { + Ok(Transformed::Keep) + } else { + self.pruned_scopes.insert(scope_id); + Ok(Transformed::ReplaceMany(std::mem::take(&mut scope.instructions))) + } + } + + fn transform_instruction( + &mut self, + instruction: &mut ReactiveInstruction<'a>, + state: &mut FxHashSet, + ) -> Result>, crate::react_compiler_diagnostics::CompilerError> + { + self.traverse_instruction(instruction, state)?; + + match &mut instruction.value { + ReactiveValue::Instruction(InstructionValue::StoreLocal { + value: store_value, + lvalue: store_lvalue, + .. + }) if store_lvalue.kind == InstructionKind::Reassign => { + let decl_id = + self.env.identifiers[store_lvalue.place.identifier.0 as usize].declaration_id; + let ids = self.reassignments.entry(decl_id).or_insert_with(FxHashSet::default); + ids.insert(store_value.identifier); + } + ReactiveValue::Instruction(InstructionValue::LoadLocal { place, .. }) => { + let has_scope = self.env.identifiers[place.identifier.0 as usize].scope.is_some(); + let lvalue_no_scope = instruction + .lvalue + .as_ref() + .map(|lv| self.env.identifiers[lv.identifier.0 as usize].scope.is_none()) + .unwrap_or(false); + if has_scope && lvalue_no_scope { + if let Some(lv) = &instruction.lvalue { + let decl_id = self.env.identifiers[lv.identifier.0 as usize].declaration_id; + let ids = + self.reassignments.entry(decl_id).or_insert_with(FxHashSet::default); + ids.insert(place.identifier); + } + } + } + ReactiveValue::Instruction(InstructionValue::FinishMemoize { + decl, pruned, .. + }) => { + let decl_has_scope = + self.env.identifiers[decl.identifier.0 as usize].scope.is_some(); + if !decl_has_scope { + // If the manual memo was a useMemo that got inlined, iterate through + // all reassignments to the iife temporary to ensure they're memoized. + let decl_id = self.env.identifiers[decl.identifier.0 as usize].declaration_id; + let decls: Vec = self + .reassignments + .get(&decl_id) + .map(|ids| ids.iter().copied().collect()) + .unwrap_or_else(|| vec![decl.identifier]); + + if decls.iter().all(|d| { + let scope = self.env.identifiers[d.0 as usize].scope; + scope.is_none() || self.pruned_scopes.contains(&scope.unwrap()) + }) { + *pruned = true; + } + } else { + let scope = self.env.identifiers[decl.identifier.0 as usize].scope; + if let Some(scope_id) = scope { + if self.pruned_scopes.contains(&scope_id) { + *pruned = true; + } + } + } + } + _ => {} + } + + Ok(Transformed::Keep) + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_non_reactive_dependencies.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_non_reactive_dependencies.rs new file mode 100644 index 0000000000000..6b042a61a21d7 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_non_reactive_dependencies.rs @@ -0,0 +1,246 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! PruneNonReactiveDependencies + CollectReactiveIdentifiers +//! +//! Corresponds to `src/ReactiveScopes/PruneNonReactiveDependencies.ts` +//! and `src/ReactiveScopes/CollectReactiveIdentifiers.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::{ + EvaluationOrder, IdentifierId, InstructionValue, Place, PrunedReactiveScopeBlock, + ReactiveFunction, ReactiveInstruction, ReactiveScopeBlock, ReactiveValue, + environment::Environment, is_primitive_type, is_use_ref_type, object_shape, + visitors as hir_visitors, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + self, ReactiveFunctionTransform, ReactiveFunctionVisitor, +}; + +// ============================================================================= +// CollectReactiveIdentifiers +// ============================================================================= + +/// Collects identifiers that are reactive. +/// TS: `collectReactiveIdentifiers` +pub fn collect_reactive_identifiers<'a>( + func: &ReactiveFunction<'a>, + env: &Environment<'a>, +) -> FxHashSet { + let visitor = CollectVisitor { env }; + let mut state = FxHashSet::default(); + crate::react_compiler_reactive_scopes::visitors::visit_reactive_function( + func, &visitor, &mut state, + ); + state +} + +struct CollectVisitor<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for CollectVisitor<'a, 'e> { + type State = FxHashSet; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_lvalue(&self, id: EvaluationOrder, lvalue: &Place, state: &mut Self::State) { + // Visitors don't visit lvalues as places by default, but we want to visit all places + self.visit_place(id, lvalue, state); + } + + fn visit_place(&self, _id: EvaluationOrder, place: &Place, state: &mut Self::State) { + if place.reactive { + state.insert(place.identifier); + } + } + + fn visit_pruned_scope(&self, scope: &PrunedReactiveScopeBlock<'a>, state: &mut Self::State) { + self.traverse_pruned_scope(scope, state); + + let scope_data = &self.env.scopes[scope.scope.0 as usize]; + for (_id, decl) in &scope_data.declarations { + let identifier = &self.env.identifiers[decl.identifier.0 as usize]; + let ty = &self.env.types[identifier.type_.0 as usize]; + if !is_primitive_type(ty) && !is_stable_ref_type(ty, state, identifier.id) { + state.insert(*_id); + } + } + } +} + +/// TS: `isStableRefType` +fn is_stable_ref_type( + ty: &crate::react_compiler_hir::Type, + reactive_identifiers: &FxHashSet, + id: IdentifierId, +) -> bool { + is_use_ref_type(ty) && !reactive_identifiers.contains(&id) +} + +// ============================================================================= +// isStableType (ported from HIR.ts) +// ============================================================================= + +/// TS: `isStableType` +fn is_stable_type(ty: &crate::react_compiler_hir::Type) -> bool { + is_set_state_type(ty) + || is_set_action_state_type(ty) + || is_dispatcher_type(ty) + || is_use_ref_type(ty) + || is_start_transition_type(ty) + || is_set_optimistic_type(ty) +} + +fn is_set_state_type(ty: &crate::react_compiler_hir::Type) -> bool { + matches!(ty, crate::react_compiler_hir::Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_SET_STATE_ID) +} + +fn is_set_action_state_type(ty: &crate::react_compiler_hir::Type) -> bool { + matches!(ty, crate::react_compiler_hir::Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_SET_ACTION_STATE_ID) +} + +fn is_dispatcher_type(ty: &crate::react_compiler_hir::Type) -> bool { + matches!(ty, crate::react_compiler_hir::Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_DISPATCH_ID) +} + +fn is_start_transition_type(ty: &crate::react_compiler_hir::Type) -> bool { + matches!(ty, crate::react_compiler_hir::Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_START_TRANSITION_ID) +} + +fn is_set_optimistic_type(ty: &crate::react_compiler_hir::Type) -> bool { + matches!(ty, crate::react_compiler_hir::Type::Function { shape_id: Some(id), .. } if id == object_shape::BUILT_IN_SET_OPTIMISTIC_ID) +} + +// ============================================================================= +// PruneNonReactiveDependencies +// ============================================================================= + +/// Prunes dependencies that are guaranteed to be non-reactive. +/// TS: `pruneNonReactiveDependencies` +pub fn prune_non_reactive_dependencies<'a>( + func: &mut ReactiveFunction<'a>, + env: &mut Environment<'a>, +) { + let reactive_ids = collect_reactive_identifiers(func, env); + let mut visitor = PruneVisitor { env }; + let mut state = reactive_ids; + visitors::transform_reactive_function(func, &mut visitor, &mut state) + .expect("PruneNonReactiveDependencies should not fail"); +} + +struct PruneVisitor<'a, 'e> { + env: &'e mut Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for PruneVisitor<'a, 'e> { + type State = FxHashSet; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_instruction( + &mut self, + instruction: &mut ReactiveInstruction<'a>, + state: &mut Self::State, + ) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + self.traverse_instruction(instruction, state)?; + + let lvalue = &instruction.lvalue; + match &instruction.value { + ReactiveValue::Instruction(InstructionValue::LoadLocal { place, .. }) => { + if let Some(lv) = lvalue { + if state.contains(&place.identifier) { + state.insert(lv.identifier); + } + } + } + ReactiveValue::Instruction(InstructionValue::StoreLocal { + value: store_value, + lvalue: store_lvalue, + .. + }) => { + if state.contains(&store_value.identifier) { + state.insert(store_lvalue.place.identifier); + if let Some(lv) = lvalue { + state.insert(lv.identifier); + } + } + } + ReactiveValue::Instruction(InstructionValue::Destructure { + value: destr_value, + lvalue: destr_lvalue, + .. + }) => { + if state.contains(&destr_value.identifier) { + for operand in hir_visitors::each_pattern_operand(&destr_lvalue.pattern) { + let ident = &self.env.identifiers[operand.identifier.0 as usize]; + let ty = &self.env.types[ident.type_.0 as usize]; + if is_stable_type(ty) { + continue; + } + state.insert(operand.identifier); + } + if let Some(lv) = lvalue { + state.insert(lv.identifier); + } + } + } + ReactiveValue::Instruction(InstructionValue::PropertyLoad { object, .. }) => { + if let Some(lv) = lvalue { + let ident = &self.env.identifiers[lv.identifier.0 as usize]; + let ty = &self.env.types[ident.type_.0 as usize]; + if state.contains(&object.identifier) && !is_stable_type(ty) { + state.insert(lv.identifier); + } + } + } + ReactiveValue::Instruction(InstructionValue::ComputedLoad { + object, property, .. + }) => { + if let Some(lv) = lvalue { + if state.contains(&object.identifier) || state.contains(&property.identifier) { + state.insert(lv.identifier); + } + } + } + _ => {} + } + Ok(()) + } + + fn visit_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + self.traverse_scope(scope, state)?; + + let scope_id = scope.scope; + let scope_data = &mut self.env.scopes[scope_id.0 as usize]; + + // Remove non-reactive dependencies + scope_data.dependencies.retain(|dep| state.contains(&dep.identifier)); + + // If any deps remain, mark all declarations and reassignments as reactive + if !scope_data.dependencies.is_empty() { + let decl_ids: Vec = + scope_data.declarations.iter().map(|(_, decl)| decl.identifier).collect(); + for id in decl_ids { + state.insert(id); + } + let reassign_ids: Vec = scope_data.reassignments.clone(); + for id in reassign_ids { + state.insert(id); + } + } + Ok(()) + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_labels.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_labels.rs new file mode 100644 index 0000000000000..54b4ea1aade64 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_labels.rs @@ -0,0 +1,92 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Flattens labeled terminals where the label is not reachable, and +//! nulls out labels for other terminals where the label is unused. +//! +//! Corresponds to `src/ReactiveScopes/PruneUnusedLabels.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::{ + BlockId, ReactiveFunction, ReactiveStatement, ReactiveTerminal, ReactiveTerminalStatement, + ReactiveTerminalTargetKind, environment::Environment, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionTransform, Transformed, transform_reactive_function, +}; + +/// Prune unused labels from a reactive function. +pub fn prune_unused_labels<'a>( + func: &mut ReactiveFunction<'a>, + env: &Environment<'a>, +) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + let mut transform = Transform { env }; + let mut labels: FxHashSet = FxHashSet::default(); + transform_reactive_function(func, &mut transform, &mut labels) +} + +struct Transform<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for Transform<'a, 'e> { + type State = FxHashSet; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn transform_terminal( + &mut self, + stmt: &mut ReactiveTerminalStatement<'a>, + state: &mut FxHashSet, + ) -> Result>, crate::react_compiler_diagnostics::CompilerError> + { + // Traverse children first + self.traverse_terminal(stmt, state)?; + + // Collect labeled break/continue targets + match &stmt.terminal { + ReactiveTerminal::Break { + target, + target_kind: ReactiveTerminalTargetKind::Labeled, + .. + } + | ReactiveTerminal::Continue { + target, + target_kind: ReactiveTerminalTargetKind::Labeled, + .. + } => { + state.insert(*target); + } + _ => {} + } + + // Is this terminal reachable via a break/continue to its label? + let is_reachable_label = + stmt.label.as_ref().map_or(false, |label| state.contains(&label.id)); + + if let ReactiveTerminal::Label { block, .. } = &mut stmt.terminal { + if !is_reachable_label { + // Flatten labeled terminals where the label isn't necessary. + // Note: In TS, there's a check for `last.terminal.target === null` + // to pop a trailing break, but since target is always a BlockId (number), + // that check is always false, so the trailing break is never removed. + let flattened = std::mem::take(block); + return Ok(Transformed::ReplaceMany(flattened)); + } + } + + if !is_reachable_label { + if let Some(label) = &mut stmt.label { + label.implicit = true; + } + } + + Ok(Transformed::Keep) + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_lvalues.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_lvalues.rs new file mode 100644 index 0000000000000..bb82919ec42b0 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_lvalues.rs @@ -0,0 +1,207 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! PruneUnusedLValues (PruneTemporaryLValues) +//! +//! Nulls out lvalues for temporary variables that are never accessed later. +//! +//! Corresponds to `src/ReactiveScopes/PruneTemporaryLValues.ts`. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::{ + DeclarationId, EvaluationOrder, Place, ReactiveFunction, ReactiveInstruction, + ReactiveStatement, ReactiveValue, environment::Environment, +}; + +use crate::react_compiler_reactive_scopes::visitors::{self, ReactiveFunctionVisitor}; + +/// Nulls out lvalues for unnamed temporaries that are never used. +/// TS: `pruneUnusedLValues` +/// +/// Uses ReactiveFunctionVisitor to collect unnamed lvalue DeclarationIds, +/// removing them when referenced as operands. After the visitor pass, +/// a second pass nulls out the remaining unused lvalues. +/// +/// This uses a two-phase approach because Rust's ReactiveFunctionVisitor +/// takes immutable references, so we cannot modify lvalues during the visit. +/// The TS version stores mutable instruction references and modifies them +/// after the visitor completes. +pub fn prune_unused_lvalues<'a>(func: &mut ReactiveFunction<'a>, env: &Environment<'a>) { + // Phase 1: Use ReactiveFunctionVisitor to identify unused unnamed lvalues. + // When we see an unnamed lvalue on an instruction, we add its DeclarationId. + // When we see a place reference (operand), we remove its DeclarationId. + let visitor = Visitor { env }; + let mut lvalues: FxHashSet = FxHashSet::default(); + visitors::visit_reactive_function(func, &visitor, &mut lvalues); + + // Phase 2: Null out lvalues whose DeclarationId remains in the map. + // In the TS, this is done by iterating the stored instruction references. + // In Rust, we walk the tree to find instructions with matching DeclarationIds. + if !lvalues.is_empty() { + null_unused_lvalues(&mut func.body, env, &lvalues); + } +} + +/// TS: `type LValues = Map` +/// In Rust, we only need the set of DeclarationIds (not the instruction refs) +/// because we apply changes in a separate pass. +type LValues = FxHashSet; + +/// TS: `class Visitor extends ReactiveFunctionVisitor` +struct Visitor<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for Visitor<'a, 'e> { + type State = LValues; + + fn env(&self) -> &Environment<'a> { + self.env + } + + /// TS: `visitPlace(_id, place, state) { state.delete(place.identifier.declarationId) }` + fn visit_place(&self, _id: EvaluationOrder, place: &Place, state: &mut LValues) { + let ident = &self.env.identifiers[place.identifier.0 as usize]; + state.remove(&ident.declaration_id); + } + + /// TS: `visitInstruction(instruction, state)` + /// Calls traverseInstruction first (visits operands via visitPlace), + /// then checks if the lvalue is unnamed and adds to map. + fn visit_instruction(&self, instruction: &ReactiveInstruction<'a>, state: &mut LValues) { + self.traverse_instruction(instruction, state); + if let Some(lv) = &instruction.lvalue { + let ident = &self.env.identifiers[lv.identifier.0 as usize]; + if ident.name.is_none() { + state.insert(ident.declaration_id); + } + } + } +} + +/// Phase 2: Walk the tree and null out lvalues whose DeclarationId is unused. +/// This is necessary because Rust's visitor takes immutable references. +fn null_unused_lvalues<'a>( + block: &mut Vec>, + env: &Environment<'a>, + unused: &FxHashSet, +) { + for stmt in block.iter_mut() { + match stmt { + ReactiveStatement::Instruction(instr) => { + null_unused_in_instruction(instr, env, unused); + } + ReactiveStatement::Scope(scope) => { + null_unused_lvalues(&mut scope.instructions, env, unused); + } + ReactiveStatement::PrunedScope(scope) => { + null_unused_lvalues(&mut scope.instructions, env, unused); + } + ReactiveStatement::Terminal(stmt) => { + null_unused_in_terminal(&mut stmt.terminal, env, unused); + } + } + } +} + +fn null_unused_in_instruction<'a>( + instr: &mut ReactiveInstruction<'a>, + env: &Environment<'a>, + unused: &FxHashSet, +) { + if let Some(lv) = &instr.lvalue { + let ident = &env.identifiers[lv.identifier.0 as usize]; + if unused.contains(&ident.declaration_id) { + instr.lvalue = None; + } + } + null_unused_in_value(&mut instr.value, env, unused); +} + +fn null_unused_in_value<'a>( + value: &mut ReactiveValue<'a>, + env: &Environment<'a>, + unused: &FxHashSet, +) { + match value { + ReactiveValue::SequenceExpression { instructions, value: inner, .. } => { + for instr in instructions.iter_mut() { + null_unused_in_instruction(instr, env, unused); + } + null_unused_in_value(inner, env, unused); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + null_unused_in_value(left, env, unused); + null_unused_in_value(right, env, unused); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + null_unused_in_value(test, env, unused); + null_unused_in_value(consequent, env, unused); + null_unused_in_value(alternate, env, unused); + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + null_unused_in_value(inner, env, unused); + } + ReactiveValue::Instruction(_) => {} + } +} + +fn null_unused_in_terminal<'a>( + terminal: &mut crate::react_compiler_hir::ReactiveTerminal<'a>, + env: &Environment<'a>, + unused: &FxHashSet, +) { + use crate::react_compiler_hir::ReactiveTerminal; + match terminal { + ReactiveTerminal::Break { .. } | ReactiveTerminal::Continue { .. } => {} + ReactiveTerminal::Return { .. } | ReactiveTerminal::Throw { .. } => {} + ReactiveTerminal::For { init, test, update, loop_block, .. } => { + null_unused_in_value(init, env, unused); + null_unused_in_value(test, env, unused); + null_unused_lvalues(loop_block, env, unused); + if let Some(update) = update { + null_unused_in_value(update, env, unused); + } + } + ReactiveTerminal::ForOf { init, test, loop_block, .. } => { + null_unused_in_value(init, env, unused); + null_unused_in_value(test, env, unused); + null_unused_lvalues(loop_block, env, unused); + } + ReactiveTerminal::ForIn { init, loop_block, .. } => { + null_unused_in_value(init, env, unused); + null_unused_lvalues(loop_block, env, unused); + } + ReactiveTerminal::DoWhile { loop_block, test, .. } => { + null_unused_lvalues(loop_block, env, unused); + null_unused_in_value(test, env, unused); + } + ReactiveTerminal::While { test, loop_block, .. } => { + null_unused_in_value(test, env, unused); + null_unused_lvalues(loop_block, env, unused); + } + ReactiveTerminal::If { consequent, alternate, .. } => { + null_unused_lvalues(consequent, env, unused); + if let Some(alt) = alternate { + null_unused_lvalues(alt, env, unused); + } + } + ReactiveTerminal::Switch { cases, .. } => { + for case in cases.iter_mut() { + if let Some(block) = &mut case.block { + null_unused_lvalues(block, env, unused); + } + } + } + ReactiveTerminal::Label { block, .. } => { + null_unused_lvalues(block, env, unused); + } + ReactiveTerminal::Try { block, handler, .. } => { + null_unused_lvalues(block, env, unused); + null_unused_lvalues(handler, env, unused); + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_scopes.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_scopes.rs new file mode 100644 index 0000000000000..a31192d95dde2 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/prune_unused_scopes.rs @@ -0,0 +1,97 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! PruneUnusedScopes — converts scopes without outputs into regular blocks. +//! +//! Corresponds to `src/ReactiveScopes/PruneUnusedScopes.ts`. + +use crate::react_compiler_hir::{ + PrunedReactiveScopeBlock, ReactiveFunction, ReactiveScopeBlock, ReactiveStatement, + ReactiveTerminal, ReactiveTerminalStatement, environment::Environment, +}; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionTransform, Transformed, transform_reactive_function, +}; + +struct State { + has_return_statement: bool, +} + +/// Converts scopes without outputs into pruned-scopes (regular blocks). +/// TS: `pruneUnusedScopes` +pub fn prune_unused_scopes<'a>( + func: &mut ReactiveFunction<'a>, + env: &Environment<'a>, +) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + let mut transform = Transform { env }; + let mut state = State { has_return_statement: false }; + transform_reactive_function(func, &mut transform, &mut state) +} + +struct Transform<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for Transform<'a, 'e> { + type State = State; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_terminal( + &mut self, + stmt: &mut ReactiveTerminalStatement<'a>, + state: &mut State, + ) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + self.traverse_terminal(stmt, state)?; + if matches!(stmt.terminal, ReactiveTerminal::Return { .. }) { + state.has_return_statement = true; + } + Ok(()) + } + + fn transform_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + _state: &mut State, + ) -> Result>, crate::react_compiler_diagnostics::CompilerError> + { + let mut scope_state = State { has_return_statement: false }; + self.visit_scope(scope, &mut scope_state)?; + + let scope_id = scope.scope; + let scope_data = &self.env.scopes[scope_id.0 as usize]; + + if !scope_state.has_return_statement + && scope_data.reassignments.is_empty() + && (scope_data.declarations.is_empty() || !has_own_declaration(scope_data, scope_id)) + { + // Replace with pruned scope + Ok(Transformed::Replace(ReactiveStatement::PrunedScope(PrunedReactiveScopeBlock { + scope: scope.scope, + instructions: std::mem::take(&mut scope.instructions), + }))) + } else { + Ok(Transformed::Keep) + } + } +} + +/// Does the scope block declare any values of its own? +/// Returns false if all declarations are propagated from nested scopes. +/// TS: `hasOwnDeclaration` +fn has_own_declaration( + scope_data: &crate::react_compiler_hir::ReactiveScope, + scope_id: crate::react_compiler_hir::ScopeId, +) -> bool { + for (_, decl) in &scope_data.declarations { + if decl.scope == scope_id { + return true; + } + } + false +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/rename_variables.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/rename_variables.rs new file mode 100644 index 0000000000000..f2089a4e122a3 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/rename_variables.rs @@ -0,0 +1,425 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! RenameVariables — renames variables for output, assigns unique names, +//! handles SSA renames. +//! +//! Corresponds to `src/ReactiveScopes/RenameVariables.ts`. + +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; + +use crate::react_compiler_hir::DeclarationId; +use crate::react_compiler_hir::EvaluationOrder; +use crate::react_compiler_hir::FunctionId; +use crate::react_compiler_hir::IdentifierName; +use crate::react_compiler_hir::InstructionValue; +use crate::react_compiler_hir::ParamPattern; +use crate::react_compiler_hir::Place; +use crate::react_compiler_hir::PrunedReactiveScopeBlock; +use crate::react_compiler_hir::ReactiveBlock; +use crate::react_compiler_hir::ReactiveFunction; +use crate::react_compiler_hir::ReactiveScopeBlock; +use crate::react_compiler_hir::ReactiveValue; +use crate::react_compiler_hir::environment::Environment; + +use crate::react_compiler_reactive_scopes::visitors::ReactiveFunctionVisitor; +use crate::react_compiler_reactive_scopes::visitors::{self}; + +// ============================================================================= +// Scopes +// ============================================================================= + +struct Scopes { + seen: FxHashMap, + stack: Vec>, + globals: FxHashSet, + names: FxHashSet, +} + +impl Scopes { + fn new(globals: FxHashSet) -> Self { + Self { + seen: FxHashMap::default(), + stack: vec![FxHashMap::default()], + globals, + names: FxHashSet::default(), + } + } + + fn visit_identifier<'a>( + &mut self, + identifier_id: crate::react_compiler_hir::IdentifierId, + env: &Environment<'a>, + ) { + let identifier = &env.identifiers[identifier_id.0 as usize]; + let original_name = match &identifier.name { + Some(name) => name.clone(), + None => return, + }; + let declaration_id = identifier.declaration_id; + + if self.seen.contains_key(&declaration_id) { + return; + } + + let original_value = original_name.value().to_string(); + let is_promoted = matches!(original_name, IdentifierName::Promoted(_)); + let is_promoted_temp = is_promoted && original_value.starts_with("#t"); + let is_promoted_jsx = is_promoted && original_value.starts_with("#T"); + + let mut name: String; + let mut id: u32 = 0; + if is_promoted_temp { + name = format!("t{}", id); + id += 1; + } else if is_promoted_jsx { + name = format!("T{}", id); + id += 1; + } else { + name = original_value.clone(); + } + + while self.lookup(&name).is_some() || self.globals.contains(&name) { + if is_promoted_temp { + name = format!("t{}", id); + id += 1; + } else if is_promoted_jsx { + name = format!("T{}", id); + id += 1; + } else { + name = format!("{}${}", original_value, id); + id += 1; + } + } + + let identifier_name = IdentifierName::Named(name.clone()); + self.seen.insert(declaration_id, identifier_name); + self.stack.last_mut().unwrap().insert(name.clone(), declaration_id); + self.names.insert(name); + } + + fn lookup(&self, name: &str) -> Option { + for scope in self.stack.iter().rev() { + if let Some(id) = scope.get(name) { + return Some(*id); + } + } + None + } + + fn enter(&mut self) { + self.stack.push(FxHashMap::default()); + } + + fn leave(&mut self) { + self.stack.pop(); + } +} + +// ============================================================================= +// Visitor — TS: `class Visitor extends ReactiveFunctionVisitor` +// ============================================================================= + +struct Visitor<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for Visitor<'a, 'e> { + type State = Scopes; + + fn env(&self) -> &Environment<'a> { + self.env + } + + /// TS: `visitParam(place, state) { state.visit(place.identifier) }` + fn visit_param(&self, place: &Place, state: &mut Scopes) { + state.visit_identifier(place.identifier, self.env); + } + + /// TS: `visitLValue(_id, lvalue, state) { state.visit(lvalue.identifier) }` + fn visit_lvalue(&self, _id: EvaluationOrder, lvalue: &Place, state: &mut Scopes) { + state.visit_identifier(lvalue.identifier, self.env); + } + + /// TS: `visitPlace(_id, place, state) { state.visit(place.identifier) }` + fn visit_place(&self, _id: EvaluationOrder, place: &Place, state: &mut Scopes) { + state.visit_identifier(place.identifier, self.env); + } + + /// TS: `visitBlock(block, state) { state.enter(() => { this.traverseBlock(block, state) }) }` + fn visit_block(&self, block: &ReactiveBlock<'a>, state: &mut Scopes) { + state.enter(); + self.traverse_block(block, state); + state.leave(); + } + + /// TS: `visitPrunedScope(scopeBlock, state) { this.traverseBlock(scopeBlock.instructions, state) }` + /// No enter/leave — names assigned inside pruned scopes remain visible in + /// the enclosing scope, preventing name reuse. + fn visit_pruned_scope(&self, scope: &PrunedReactiveScopeBlock<'a>, state: &mut Scopes) { + self.traverse_block(&scope.instructions, state); + } + + /// TS: `visitScope(scope, state) { for (const [_, decl] of scope.scope.declarations) state.visit(decl.identifier); this.traverseScope(scope, state) }` + fn visit_scope(&self, scope: &ReactiveScopeBlock<'a>, state: &mut Scopes) { + let scope_data = &self.env.scopes[scope.scope.0 as usize]; + let decl_ids: Vec = + scope_data.declarations.iter().map(|(_, d)| d.identifier).collect(); + for id in decl_ids { + state.visit_identifier(id, self.env); + } + self.traverse_scope(scope, state); + } + + /// TS: `visitValue(id, value, state) { this.traverseValue(id, value, state); if (value.kind === 'FunctionExpression' || value.kind === 'ObjectMethod') this.visitHirFunction(value.loweredFunc.func, state) }` + fn visit_value(&self, id: EvaluationOrder, value: &ReactiveValue<'a>, state: &mut Scopes) { + self.traverse_value(id, value, state); + if let ReactiveValue::Instruction(iv) = value { + match iv { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + self.visit_hir_function(lowered_func.func, state); + } + _ => {} + } + } + } +} + +// ============================================================================= +// Public entry point +// ============================================================================= + +/// Renames variables for output — assigns unique names, handles SSA renames. +/// Returns a Set of all unique variable names used. +/// TS: `renameVariables` +pub fn rename_variables<'a>( + func: &mut ReactiveFunction<'a>, + env: &mut Environment<'a>, +) -> FxHashSet { + rename_variables_with_parent(func, env, None) +} + +fn rename_variables_with_parent<'a>( + func: &mut ReactiveFunction<'a>, + env: &mut Environment<'a>, + parent_names: Option<&FxHashSet>, +) -> FxHashSet { + let globals = collect_referenced_globals(&func.body, env); + + // Phase 1: Use ReactiveFunctionVisitor to compute the rename mapping. + // This collects DeclarationId -> IdentifierName without mutating env. + let mut scopes = Scopes::new(globals.clone()); + // If parent names are provided (for outlined functions), pre-populate + // the scope stack so that parameter names don't collide with parent + // variables. In the TS compiler, outlined functions are placed in the + // parent function body and processed within the parent's scope context. + if let Some(parent) = parent_names { + scopes.enter(); + for name in parent { + scopes.stack.last_mut().unwrap().insert(name.clone(), DeclarationId(u32::MAX)); + scopes.names.insert(name.clone()); + } + } + rename_variables_impl(func, &Visitor { env }, &mut scopes); + + // Phase 2: Apply the computed renames to all identifiers in env. + for identifier in env.identifiers.iter_mut() { + if let Some(mapped_name) = scopes.seen.get(&identifier.declaration_id) { + if identifier.name.is_some() { + identifier.name = Some(mapped_name.clone()); + } + } + } + + let mut result: FxHashSet = scopes.names; + result.extend(globals); + result +} + +/// TS: `renameVariablesImpl` +fn rename_variables_impl<'a>( + func: &ReactiveFunction<'a>, + visitor: &Visitor<'a, '_>, + scopes: &mut Scopes, +) { + scopes.enter(); + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + visitor.visit_param(place, scopes); + } + visitors::visit_reactive_function(func, visitor, scopes); + scopes.leave(); +} + +// ============================================================================= +// CollectReferencedGlobals +// ============================================================================= + +/// Collects all globally referenced names from the reactive function. +/// TS: `collectReferencedGlobals` +fn collect_referenced_globals<'a>( + block: &ReactiveBlock<'a>, + env: &Environment<'a>, +) -> FxHashSet { + let mut globals = FxHashSet::default(); + collect_globals_block(block, &mut globals, env); + globals +} + +fn collect_globals_block<'a>( + block: &ReactiveBlock<'a>, + globals: &mut FxHashSet, + env: &Environment<'a>, +) { + for stmt in block { + match stmt { + crate::react_compiler_hir::ReactiveStatement::Instruction(instr) => { + collect_globals_value(&instr.value, globals, env); + } + crate::react_compiler_hir::ReactiveStatement::Scope(scope) => { + collect_globals_block(&scope.instructions, globals, env); + } + crate::react_compiler_hir::ReactiveStatement::PrunedScope(scope) => { + collect_globals_block(&scope.instructions, globals, env); + } + crate::react_compiler_hir::ReactiveStatement::Terminal(terminal) => { + collect_globals_terminal(terminal, globals, env); + } + } + } +} + +fn collect_globals_value<'a>( + value: &ReactiveValue<'a>, + globals: &mut FxHashSet, + env: &Environment<'a>, +) { + match value { + ReactiveValue::Instruction(iv) => { + if let InstructionValue::LoadGlobal { binding, .. } = iv { + globals.insert(binding.name().to_string()); + } + // Visit inner functions + match iv { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + collect_globals_hir_function(lowered_func.func, globals, env); + } + _ => {} + } + } + ReactiveValue::SequenceExpression { instructions, value: inner, .. } => { + for instr in instructions { + collect_globals_value(&instr.value, globals, env); + } + collect_globals_value(inner, globals, env); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + collect_globals_value(test, globals, env); + collect_globals_value(consequent, globals, env); + collect_globals_value(alternate, globals, env); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + collect_globals_value(left, globals, env); + collect_globals_value(right, globals, env); + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + collect_globals_value(inner, globals, env); + } + } +} + +/// Recursively collects LoadGlobal names from an inner HIR function. +fn collect_globals_hir_function<'a>( + func_id: FunctionId, + globals: &mut FxHashSet, + env: &Environment<'a>, +) { + let inner_func = &env.functions[func_id.0 as usize]; + let block_ids: Vec<_> = inner_func.body.blocks.keys().copied().collect(); + for block_id in block_ids { + let inner_func = &env.functions[func_id.0 as usize]; + let block = &inner_func.body.blocks[&block_id]; + for instr_id in &block.instructions { + let instr = &inner_func.instructions[instr_id.0 as usize]; + if let InstructionValue::LoadGlobal { binding, .. } = &instr.value { + globals.insert(binding.name().to_string()); + } + // Recurse into nested function expressions + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + collect_globals_hir_function(lowered_func.func, globals, env); + } + _ => {} + } + } + } +} + +fn collect_globals_terminal<'a>( + stmt: &crate::react_compiler_hir::ReactiveTerminalStatement<'a>, + globals: &mut FxHashSet, + env: &Environment<'a>, +) { + match &stmt.terminal { + crate::react_compiler_hir::ReactiveTerminal::Break { .. } + | crate::react_compiler_hir::ReactiveTerminal::Continue { .. } => {} + crate::react_compiler_hir::ReactiveTerminal::Return { .. } + | crate::react_compiler_hir::ReactiveTerminal::Throw { .. } => {} + crate::react_compiler_hir::ReactiveTerminal::For { + init, test, update, loop_block, .. + } => { + collect_globals_value(init, globals, env); + collect_globals_value(test, globals, env); + collect_globals_block(loop_block, globals, env); + if let Some(update) = update { + collect_globals_value(update, globals, env); + } + } + crate::react_compiler_hir::ReactiveTerminal::ForOf { init, test, loop_block, .. } => { + collect_globals_value(init, globals, env); + collect_globals_value(test, globals, env); + collect_globals_block(loop_block, globals, env); + } + crate::react_compiler_hir::ReactiveTerminal::ForIn { init, loop_block, .. } => { + collect_globals_value(init, globals, env); + collect_globals_block(loop_block, globals, env); + } + crate::react_compiler_hir::ReactiveTerminal::DoWhile { loop_block, test, .. } => { + collect_globals_block(loop_block, globals, env); + collect_globals_value(test, globals, env); + } + crate::react_compiler_hir::ReactiveTerminal::While { test, loop_block, .. } => { + collect_globals_value(test, globals, env); + collect_globals_block(loop_block, globals, env); + } + crate::react_compiler_hir::ReactiveTerminal::If { consequent, alternate, .. } => { + collect_globals_block(consequent, globals, env); + if let Some(alt) = alternate { + collect_globals_block(alt, globals, env); + } + } + crate::react_compiler_hir::ReactiveTerminal::Switch { cases, .. } => { + for case in cases { + if let Some(block) = &case.block { + collect_globals_block(block, globals, env); + } + } + } + crate::react_compiler_hir::ReactiveTerminal::Label { block, .. } => { + collect_globals_block(block, globals, env); + } + crate::react_compiler_hir::ReactiveTerminal::Try { block, handler, .. } => { + collect_globals_block(block, globals, env); + collect_globals_block(handler, globals, env); + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/stabilize_block_ids.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/stabilize_block_ids.rs new file mode 100644 index 0000000000000..13c102b7f6876 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/stabilize_block_ids.rs @@ -0,0 +1,130 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! StabilizeBlockIds +//! +//! Rewrites block IDs to sequential values so that the output is deterministic +//! regardless of the order in which blocks were created. +//! +//! Corresponds to `src/ReactiveScopes/StabilizeBlockIds.ts`. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_hir::{ + BlockId, ReactiveFunction, ReactiveScopeBlock, ReactiveTerminal, ReactiveTerminalStatement, + environment::Environment, +}; +use crate::react_compiler_utils::FxIndexSet; + +use crate::react_compiler_reactive_scopes::visitors::{ + ReactiveFunctionTransform, ReactiveFunctionVisitor, transform_reactive_function, + visit_reactive_function, +}; + +/// Rewrites block IDs to sequential values. +/// TS: `stabilizeBlockIds` +pub fn stabilize_block_ids<'a>(func: &mut ReactiveFunction<'a>, env: &mut Environment<'a>) { + // Pass 1: Collect referenced labels (preserving insertion order to match TS Set behavior) + let mut referenced: FxIndexSet = FxIndexSet::default(); + let collector = CollectReferencedLabels { env: &*env }; + visit_reactive_function(func, &collector, &mut referenced); + + // Build mappings: referenced block IDs -> sequential IDs (insertion-order deterministic) + let mut mappings: FxHashMap = FxHashMap::default(); + for block_id in &referenced { + let len = mappings.len() as u32; + mappings.entry(*block_id).or_insert(BlockId(len)); + } + + // Pass 2: Rewrite block IDs using ReactiveFunctionTransform + let mut rewriter = RewriteBlockIds { env }; + let _ = transform_reactive_function(func, &mut rewriter, &mut mappings); +} + +// ============================================================================= +// Pass 1: CollectReferencedLabels +// ============================================================================= + +struct CollectReferencedLabels<'a, 'e> { + env: &'e Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionVisitor<'a> for CollectReferencedLabels<'a, 'e> { + type State = FxIndexSet; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_scope(&self, scope: &ReactiveScopeBlock<'a>, state: &mut Self::State) { + let scope_data = &self.env.scopes[scope.scope.0 as usize]; + if let Some(ref early_return) = scope_data.early_return_value { + state.insert(early_return.label); + } + self.traverse_scope(scope, state); + } + + fn visit_terminal(&self, stmt: &ReactiveTerminalStatement<'a>, state: &mut Self::State) { + if let Some(ref label) = stmt.label { + if !label.implicit { + state.insert(label.id); + } + } + self.traverse_terminal(stmt, state); + } +} + +// ============================================================================= +// Pass 2: RewriteBlockIds +// ============================================================================= + +fn get_or_insert_mapping(mappings: &mut FxHashMap, id: BlockId) -> BlockId { + let len = mappings.len() as u32; + *mappings.entry(id).or_insert(BlockId(len)) +} + +/// TS: `class RewriteBlockIds extends ReactiveFunctionVisitor>` +struct RewriteBlockIds<'a, 'e> { + env: &'e mut Environment<'a>, +} + +impl<'a, 'e> ReactiveFunctionTransform<'a> for RewriteBlockIds<'a, 'e> { + type State = FxHashMap; + + fn env(&self) -> &Environment<'a> { + self.env + } + + fn visit_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + let scope_data = &mut self.env.scopes[scope.scope.0 as usize]; + if let Some(ref mut early_return) = scope_data.early_return_value { + early_return.label = get_or_insert_mapping(state, early_return.label); + } + self.traverse_scope(scope, state) + } + + fn visit_terminal( + &mut self, + stmt: &mut ReactiveTerminalStatement<'a>, + state: &mut Self::State, + ) -> Result<(), crate::react_compiler_diagnostics::CompilerError> { + if let Some(ref mut label) = stmt.label { + label.id = get_or_insert_mapping(state, label.id); + } + + match &mut stmt.terminal { + ReactiveTerminal::Break { target, .. } | ReactiveTerminal::Continue { target, .. } => { + *target = get_or_insert_mapping(state, *target); + } + _ => {} + } + + self.traverse_terminal(stmt, state) + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/visitors.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/visitors.rs new file mode 100644 index 0000000000000..e52c8f6554dff --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/visitors.rs @@ -0,0 +1,742 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Visitor and transform traits for ReactiveFunction. +//! +//! Corresponds to `src/ReactiveScopes/visitors.ts` in the TypeScript compiler. + +use crate::react_compiler_diagnostics::CompilerError; +use crate::react_compiler_hir::{ + EvaluationOrder, FunctionId, InstructionValue, ParamPattern, Place, PrunedReactiveScopeBlock, + ReactiveBlock, ReactiveFunction, ReactiveInstruction, ReactiveScopeBlock, ReactiveStatement, + ReactiveTerminal, ReactiveTerminalStatement, ReactiveValue, environment::Environment, +}; + +// ============================================================================= +// ReactiveFunctionVisitor trait +// ============================================================================= + +/// Visitor trait for walking a ReactiveFunction tree. +/// +/// Override individual `visit_*` methods to customize behavior; call the +/// corresponding `traverse_*` to continue the default recursion. +/// +/// TS: `class ReactiveFunctionVisitor` +pub trait ReactiveFunctionVisitor<'a> { + type State; + + /// Provide Environment access. The default traversal uses this to include + /// FunctionExpression/ObjectMethod context places as operands (matching the + /// TS `eachInstructionValueOperand` behavior). + fn env(&self) -> &Environment<'a>; + + fn visit_id(&self, _id: EvaluationOrder, _state: &mut Self::State) {} + + fn visit_place(&self, _id: EvaluationOrder, _place: &Place, _state: &mut Self::State) {} + + fn visit_lvalue(&self, _id: EvaluationOrder, _lvalue: &Place, _state: &mut Self::State) {} + + fn visit_param(&self, _place: &Place, _state: &mut Self::State) {} + + /// Walk an inner HIR function, visiting params, instructions (with lvalues, + /// value-lvalues, operands, and nested functions), and terminal operands. + /// TS: `visitHirFunction` + fn visit_hir_function(&self, func_id: FunctionId, state: &mut Self::State) { + let inner_func = &self.env().functions[func_id.0 as usize]; + for param in &inner_func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + self.visit_param(place, state); + } + let block_ids: Vec<_> = inner_func.body.blocks.keys().copied().collect(); + for block_id in block_ids { + let inner_func = &self.env().functions[func_id.0 as usize]; + let block = &inner_func.body.blocks[&block_id]; + let instr_ids: Vec<_> = block.instructions.clone(); + let terminal_operands: Vec = + crate::react_compiler_hir::visitors::each_terminal_operand(&block.terminal); + let terminal_id = block.terminal.evaluation_order(); + + for instr_id in &instr_ids { + let inner_func = &self.env().functions[func_id.0 as usize]; + let instr = &inner_func.instructions[instr_id.0 as usize]; + // Build a temporary ReactiveInstruction for the visitor + let reactive_instr = ReactiveInstruction { + id: instr.id, + lvalue: Some(instr.lvalue.clone()), + value: ReactiveValue::Instruction(instr.value.clone()), + effects: None, + loc: instr.loc, + }; + self.visit_instruction(&reactive_instr, state); + // Recurse into nested functions + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + self.visit_hir_function(lowered_func.func, state); + } + _ => {} + } + } + for operand in &terminal_operands { + self.visit_place(terminal_id, operand, state); + } + } + } + + fn visit_value(&self, id: EvaluationOrder, value: &ReactiveValue<'a>, state: &mut Self::State) { + self.traverse_value(id, value, state); + } + + fn traverse_value( + &self, + id: EvaluationOrder, + value: &ReactiveValue<'a>, + state: &mut Self::State, + ) { + match value { + ReactiveValue::OptionalExpression { value: inner, .. } => { + self.visit_value(id, inner, state); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + self.visit_value(id, left, state); + self.visit_value(id, right, state); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + self.visit_value(id, test, state); + self.visit_value(id, consequent, state); + self.visit_value(id, alternate, state); + } + ReactiveValue::SequenceExpression { + instructions, id: seq_id, value: inner, .. + } => { + for instr in instructions { + self.visit_instruction(instr, state); + } + self.visit_value(*seq_id, inner, state); + } + ReactiveValue::Instruction(instr_value) => { + let operands = crate::react_compiler_hir::visitors::each_instruction_value_operand( + instr_value, + self.env(), + ); + for place in &operands { + self.visit_place(id, place, state); + } + } + } + } + + fn visit_instruction(&self, instruction: &ReactiveInstruction<'a>, state: &mut Self::State) { + self.traverse_instruction(instruction, state); + } + + fn traverse_instruction(&self, instruction: &ReactiveInstruction<'a>, state: &mut Self::State) { + self.visit_id(instruction.id, state); + // Visit instruction-level lvalue + if let Some(lvalue) = &instruction.lvalue { + self.visit_lvalue(instruction.id, lvalue, state); + } + // Visit value-level lvalues (TS: eachInstructionValueLValue) + if let ReactiveValue::Instruction(iv) = &instruction.value { + for place in crate::react_compiler_hir::visitors::each_instruction_value_lvalue(iv) { + self.visit_lvalue(instruction.id, &place, state); + } + } + self.visit_value(instruction.id, &instruction.value, state); + } + + fn visit_terminal(&self, stmt: &ReactiveTerminalStatement<'a>, state: &mut Self::State) { + self.traverse_terminal(stmt, state); + } + + fn traverse_terminal(&self, stmt: &ReactiveTerminalStatement<'a>, state: &mut Self::State) { + let terminal = &stmt.terminal; + let id = terminal_id(terminal); + self.visit_id(id, state); + match terminal { + ReactiveTerminal::Break { .. } | ReactiveTerminal::Continue { .. } => {} + ReactiveTerminal::Return { value, id, .. } => { + self.visit_place(*id, value, state); + } + ReactiveTerminal::Throw { value, id, .. } => { + self.visit_place(*id, value, state); + } + ReactiveTerminal::For { init, test, update, loop_block, id, .. } => { + self.visit_value(*id, init, state); + self.visit_value(*id, test, state); + self.visit_block(loop_block, state); + if let Some(update) = update { + self.visit_value(*id, update, state); + } + } + ReactiveTerminal::ForOf { init, test, loop_block, id, .. } => { + self.visit_value(*id, init, state); + self.visit_value(*id, test, state); + self.visit_block(loop_block, state); + } + ReactiveTerminal::ForIn { init, loop_block, id, .. } => { + self.visit_value(*id, init, state); + self.visit_block(loop_block, state); + } + ReactiveTerminal::DoWhile { loop_block, test, id, .. } => { + self.visit_block(loop_block, state); + self.visit_value(*id, test, state); + } + ReactiveTerminal::While { test, loop_block, id, .. } => { + self.visit_value(*id, test, state); + self.visit_block(loop_block, state); + } + ReactiveTerminal::If { test, consequent, alternate, id, .. } => { + self.visit_place(*id, test, state); + self.visit_block(consequent, state); + if let Some(alt) = alternate { + self.visit_block(alt, state); + } + } + ReactiveTerminal::Switch { test, cases, id, .. } => { + self.visit_place(*id, test, state); + for case in cases { + if let Some(t) = &case.test { + self.visit_place(*id, t, state); + } + if let Some(block) = &case.block { + self.visit_block(block, state); + } + } + } + ReactiveTerminal::Label { block, .. } => { + self.visit_block(block, state); + } + ReactiveTerminal::Try { block, handler_binding, handler, id, .. } => { + self.visit_block(block, state); + if let Some(binding) = handler_binding { + self.visit_place(*id, binding, state); + } + self.visit_block(handler, state); + } + } + } + + fn visit_scope(&self, scope: &ReactiveScopeBlock<'a>, state: &mut Self::State) { + self.traverse_scope(scope, state); + } + + fn traverse_scope(&self, scope: &ReactiveScopeBlock<'a>, state: &mut Self::State) { + self.visit_block(&scope.instructions, state); + } + + fn visit_pruned_scope(&self, scope: &PrunedReactiveScopeBlock<'a>, state: &mut Self::State) { + self.traverse_pruned_scope(scope, state); + } + + fn traverse_pruned_scope(&self, scope: &PrunedReactiveScopeBlock<'a>, state: &mut Self::State) { + self.visit_block(&scope.instructions, state); + } + + fn visit_block(&self, block: &ReactiveBlock<'a>, state: &mut Self::State) { + self.traverse_block(block, state); + } + + fn traverse_block(&self, block: &ReactiveBlock<'a>, state: &mut Self::State) { + for stmt in block { + match stmt { + ReactiveStatement::Instruction(instr) => { + self.visit_instruction(instr, state); + } + ReactiveStatement::Scope(scope) => { + self.visit_scope(scope, state); + } + ReactiveStatement::PrunedScope(scope) => { + self.visit_pruned_scope(scope, state); + } + ReactiveStatement::Terminal(terminal) => { + self.visit_terminal(terminal, state); + } + } + } + } +} + +/// Entry point for visiting a reactive function. +/// TS: `visitReactiveFunction` +pub fn visit_reactive_function<'a, V: ReactiveFunctionVisitor<'a>>( + func: &ReactiveFunction<'a>, + visitor: &V, + state: &mut V::State, +) { + visitor.visit_block(&func.body, state); +} + +// ============================================================================= +// Transformed / TransformedValue enums +// ============================================================================= + +/// Result of transforming a ReactiveStatement. +/// TS: `Transformed` +pub enum Transformed { + Keep, + Remove, + Replace(T), + ReplaceMany(Vec), +} + +/// Result of transforming a ReactiveValue. +/// TS: `TransformedValue` +#[allow(dead_code)] +pub enum TransformedValue<'a> { + Keep, + Replace(ReactiveValue<'a>), +} + +// ============================================================================= +// ReactiveFunctionTransform trait +// ============================================================================= + +/// Transform trait for modifying a ReactiveFunction tree in-place. +/// +/// Extends the visitor pattern with `transform_*` methods that can modify +/// or remove statements. The `traverse_block` implementation handles applying +/// transform results to the block. +/// +/// TS: `class ReactiveFunctionTransform` +pub trait ReactiveFunctionTransform<'a> { + type State; + + /// Provide Environment access. The default traversal uses this to include + /// FunctionExpression/ObjectMethod context places as operands (matching the + /// TS `eachInstructionValueOperand` behavior). + fn env(&self) -> &Environment<'a>; + + fn visit_id( + &mut self, + _id: EvaluationOrder, + _state: &mut Self::State, + ) -> Result<(), CompilerError> { + Ok(()) + } + + fn visit_place( + &mut self, + _id: EvaluationOrder, + _place: &Place, + _state: &mut Self::State, + ) -> Result<(), CompilerError> { + Ok(()) + } + + fn visit_lvalue( + &mut self, + _id: EvaluationOrder, + _lvalue: &Place, + _state: &mut Self::State, + ) -> Result<(), CompilerError> { + Ok(()) + } + + fn visit_value( + &mut self, + id: EvaluationOrder, + value: &mut ReactiveValue<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.traverse_value(id, value, state) + } + + fn traverse_value( + &mut self, + id: EvaluationOrder, + value: &mut ReactiveValue<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + match value { + ReactiveValue::OptionalExpression { value: inner, .. } => { + let next = self.transform_value(id, inner, state)?; + if let TransformedValue::Replace(new_value) = next { + **inner = new_value; + } + } + ReactiveValue::LogicalExpression { left, right, .. } => { + let next_left = self.transform_value(id, left, state)?; + if let TransformedValue::Replace(new_value) = next_left { + **left = new_value; + } + let next_right = self.transform_value(id, right, state)?; + if let TransformedValue::Replace(new_value) = next_right { + **right = new_value; + } + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + let next_test = self.transform_value(id, test, state)?; + if let TransformedValue::Replace(new_value) = next_test { + **test = new_value; + } + let next_cons = self.transform_value(id, consequent, state)?; + if let TransformedValue::Replace(new_value) = next_cons { + **consequent = new_value; + } + let next_alt = self.transform_value(id, alternate, state)?; + if let TransformedValue::Replace(new_value) = next_alt { + **alternate = new_value; + } + } + ReactiveValue::SequenceExpression { + instructions, id: seq_id, value: inner, .. + } => { + let seq_id = *seq_id; + for instr in instructions.iter_mut() { + self.visit_instruction(instr, state)?; + } + let next = self.transform_value(seq_id, inner, state)?; + if let TransformedValue::Replace(new_value) = next { + **inner = new_value; + } + } + ReactiveValue::Instruction(instr_value) => { + // Collect operands before visiting to avoid borrow conflict + // (self.env() borrows self immutably, self.visit_place() needs &mut self). + let operands = crate::react_compiler_hir::visitors::each_instruction_value_operand( + instr_value, + self.env(), + ); + for place in &operands { + self.visit_place(id, place, state)?; + } + } + } + Ok(()) + } + + fn visit_instruction( + &mut self, + instruction: &mut ReactiveInstruction<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.traverse_instruction(instruction, state) + } + + fn transform_value( + &mut self, + id: EvaluationOrder, + value: &mut ReactiveValue<'a>, + state: &mut Self::State, + ) -> Result, CompilerError> { + self.visit_value(id, value, state)?; + Ok(TransformedValue::Keep) + } + + fn traverse_instruction( + &mut self, + instruction: &mut ReactiveInstruction<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.visit_id(instruction.id, state)?; + // Visit instruction-level lvalue + if let Some(lvalue) = &instruction.lvalue { + self.visit_lvalue(instruction.id, lvalue, state)?; + } + // Visit value-level lvalues (TS: eachInstructionValueLValue) + if let ReactiveValue::Instruction(iv) = &instruction.value { + for place in crate::react_compiler_hir::visitors::each_instruction_value_lvalue(iv) { + self.visit_lvalue(instruction.id, &place, state)?; + } + } + let next_value = self.transform_value(instruction.id, &mut instruction.value, state)?; + if let TransformedValue::Replace(new_value) = next_value { + instruction.value = new_value; + } + Ok(()) + } + + fn visit_terminal( + &mut self, + stmt: &mut ReactiveTerminalStatement<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.traverse_terminal(stmt, state) + } + + fn traverse_terminal( + &mut self, + stmt: &mut ReactiveTerminalStatement<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + let terminal = &mut stmt.terminal; + let id = terminal_id(terminal); + self.visit_id(id, state)?; + match terminal { + ReactiveTerminal::Break { .. } | ReactiveTerminal::Continue { .. } => {} + ReactiveTerminal::Return { value, id, .. } => { + self.visit_place(*id, value, state)?; + } + ReactiveTerminal::Throw { value, id, .. } => { + self.visit_place(*id, value, state)?; + } + ReactiveTerminal::For { init, test, update, loop_block, id, .. } => { + let id = *id; + let next_init = self.transform_value(id, init, state)?; + if let TransformedValue::Replace(new_value) = next_init { + *init = new_value; + } + let next_test = self.transform_value(id, test, state)?; + if let TransformedValue::Replace(new_value) = next_test { + *test = new_value; + } + if let Some(update) = update { + let next_update = self.transform_value(id, update, state)?; + if let TransformedValue::Replace(new_value) = next_update { + *update = new_value; + } + } + self.visit_block(loop_block, state)?; + } + ReactiveTerminal::ForOf { init, test, loop_block, id, .. } => { + let id = *id; + let next_init = self.transform_value(id, init, state)?; + if let TransformedValue::Replace(new_value) = next_init { + *init = new_value; + } + let next_test = self.transform_value(id, test, state)?; + if let TransformedValue::Replace(new_value) = next_test { + *test = new_value; + } + self.visit_block(loop_block, state)?; + } + ReactiveTerminal::ForIn { init, loop_block, id, .. } => { + let id = *id; + let next_init = self.transform_value(id, init, state)?; + if let TransformedValue::Replace(new_value) = next_init { + *init = new_value; + } + self.visit_block(loop_block, state)?; + } + ReactiveTerminal::DoWhile { loop_block, test, id, .. } => { + let id = *id; + self.visit_block(loop_block, state)?; + let next_test = self.transform_value(id, test, state)?; + if let TransformedValue::Replace(new_value) = next_test { + *test = new_value; + } + } + ReactiveTerminal::While { test, loop_block, id, .. } => { + let id = *id; + let next_test = self.transform_value(id, test, state)?; + if let TransformedValue::Replace(new_value) = next_test { + *test = new_value; + } + self.visit_block(loop_block, state)?; + } + ReactiveTerminal::If { test, consequent, alternate, id, .. } => { + self.visit_place(*id, test, state)?; + self.visit_block(consequent, state)?; + if let Some(alt) = alternate { + self.visit_block(alt, state)?; + } + } + ReactiveTerminal::Switch { test, cases, id, .. } => { + let id = *id; + self.visit_place(id, test, state)?; + for case in cases.iter_mut() { + if let Some(t) = &case.test { + self.visit_place(id, t, state)?; + } + if let Some(block) = &mut case.block { + self.visit_block(block, state)?; + } + } + } + ReactiveTerminal::Label { block, .. } => { + self.visit_block(block, state)?; + } + ReactiveTerminal::Try { block, handler_binding, handler, id, .. } => { + let id = *id; + self.visit_block(block, state)?; + if let Some(binding) = handler_binding { + self.visit_place(id, binding, state)?; + } + self.visit_block(handler, state)?; + } + } + Ok(()) + } + + fn visit_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.traverse_scope(scope, state) + } + + fn traverse_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.visit_block(&mut scope.instructions, state) + } + + fn visit_pruned_scope( + &mut self, + scope: &mut PrunedReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.traverse_pruned_scope(scope, state) + } + + fn traverse_pruned_scope( + &mut self, + scope: &mut PrunedReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.visit_block(&mut scope.instructions, state) + } + + fn visit_block( + &mut self, + block: &mut ReactiveBlock<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + self.traverse_block(block, state) + } + + fn transform_instruction( + &mut self, + instruction: &mut ReactiveInstruction<'a>, + state: &mut Self::State, + ) -> Result>, CompilerError> { + self.visit_instruction(instruction, state)?; + Ok(Transformed::Keep) + } + + fn transform_terminal( + &mut self, + stmt: &mut ReactiveTerminalStatement<'a>, + state: &mut Self::State, + ) -> Result>, CompilerError> { + self.visit_terminal(stmt, state)?; + Ok(Transformed::Keep) + } + + fn transform_scope( + &mut self, + scope: &mut ReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result>, CompilerError> { + self.visit_scope(scope, state)?; + Ok(Transformed::Keep) + } + + fn transform_pruned_scope( + &mut self, + scope: &mut PrunedReactiveScopeBlock<'a>, + state: &mut Self::State, + ) -> Result>, CompilerError> { + self.visit_pruned_scope(scope, state)?; + Ok(Transformed::Keep) + } + + fn traverse_block( + &mut self, + block: &mut ReactiveBlock<'a>, + state: &mut Self::State, + ) -> Result<(), CompilerError> { + let mut next_block: Option>> = None; + let len = block.len(); + for i in 0..len { + // Take the statement out temporarily + let mut stmt = std::mem::replace( + &mut block[i], + // Placeholder — will be overwritten or discarded + ReactiveStatement::Instruction(ReactiveInstruction { + id: EvaluationOrder(0), + lvalue: None, + value: ReactiveValue::Instruction( + crate::react_compiler_hir::InstructionValue::Debugger { loc: None }, + ), + effects: None, + loc: None, + }), + ); + let transformed = match &mut stmt { + ReactiveStatement::Instruction(instr) => { + self.transform_instruction(instr, state)? + } + ReactiveStatement::Scope(scope) => self.transform_scope(scope, state)?, + ReactiveStatement::PrunedScope(scope) => { + self.transform_pruned_scope(scope, state)? + } + ReactiveStatement::Terminal(terminal) => { + self.transform_terminal(terminal, state)? + } + }; + match transformed { + Transformed::Keep => { + if let Some(ref mut nb) = next_block { + nb.push(stmt); + } else { + // Put it back + block[i] = stmt; + } + } + Transformed::Remove => { + if next_block.is_none() { + next_block = Some(block[..i].to_vec()); + } + } + Transformed::Replace(replacement) => { + if next_block.is_none() { + next_block = Some(block[..i].to_vec()); + } + next_block.as_mut().unwrap().push(replacement); + } + Transformed::ReplaceMany(replacements) => { + if next_block.is_none() { + next_block = Some(block[..i].to_vec()); + } + next_block.as_mut().unwrap().extend(replacements); + } + } + } + if let Some(nb) = next_block { + *block = nb; + } + Ok(()) + } +} + +/// Entry point for transforming a reactive function. +/// TS: `visitReactiveFunction` (used with transforms too) +pub fn transform_reactive_function<'a, T: ReactiveFunctionTransform<'a>>( + func: &mut ReactiveFunction<'a>, + transform: &mut T, + state: &mut T::State, +) -> Result<(), CompilerError> { + transform.visit_block(&mut func.body, state) +} + +// ============================================================================= +// Helper: extract terminal ID +// ============================================================================= + +fn terminal_id(terminal: &ReactiveTerminal) -> EvaluationOrder { + match terminal { + ReactiveTerminal::Break { id, .. } + | ReactiveTerminal::Continue { id, .. } + | ReactiveTerminal::Return { id, .. } + | ReactiveTerminal::Throw { id, .. } + | ReactiveTerminal::Switch { id, .. } + | ReactiveTerminal::DoWhile { id, .. } + | ReactiveTerminal::While { id, .. } + | ReactiveTerminal::For { id, .. } + | ReactiveTerminal::ForOf { id, .. } + | ReactiveTerminal::ForIn { id, .. } + | ReactiveTerminal::If { id, .. } + | ReactiveTerminal::Label { id, .. } + | ReactiveTerminal::Try { id, .. } => *id, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_ssa/eliminate_redundant_phi.rs b/crates/oxc_react_compiler/src/react_compiler_ssa/eliminate_redundant_phi.rs new file mode 100644 index 0000000000000..7b0e15e293f2e --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_ssa/eliminate_redundant_phi.rs @@ -0,0 +1,156 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::*; + +use crate::react_compiler_ssa::enter_ssa::placeholder_function; + +// ============================================================================= +// Helper: rewrite_place +// ============================================================================= + +fn rewrite_place(place: &mut Place, rewrites: &FxHashMap) { + if let Some(&rewrite) = rewrites.get(&place.identifier) { + place.identifier = rewrite; + } +} + +// ============================================================================= +// Public entry point +// ============================================================================= + +pub fn eliminate_redundant_phi(func: &mut HirFunction, env: &mut Environment) { + let mut rewrites: FxHashMap = FxHashMap::default(); + eliminate_redundant_phi_impl(func, env, &mut rewrites); +} + +// ============================================================================= +// Inner implementation +// ============================================================================= + +fn eliminate_redundant_phi_impl( + func: &mut HirFunction, + env: &mut Environment, + rewrites: &mut FxHashMap, +) { + let ir = &mut func.body; + + let mut has_back_edge = false; + let mut visited: FxHashSet = FxHashSet::default(); + + let mut size; + loop { + size = rewrites.len(); + + let block_ids: Vec = ir.blocks.keys().copied().collect(); + for block_id in &block_ids { + let block_id = *block_id; + + if !has_back_edge { + let block = ir.blocks.get(&block_id).unwrap(); + for pred_id in &block.preds { + if !visited.contains(pred_id) { + has_back_edge = true; + } + } + } + visited.insert(block_id); + + // Find any redundant phis: rewrite operands, identify redundant phis, remove them. + // Matches TS behavior: each phi's operands are rewritten before checking redundancy, + // so that rewrites from earlier phis in the same block are visible to later phis. + let block = ir.blocks.get_mut(&block_id).unwrap(); + block.phis.retain_mut(|phi| { + // Remap phis in case operands are from eliminated phis + for (_, operand) in phi.operands.iter_mut() { + rewrite_place(operand, rewrites); + } + + // Find if the phi can be eliminated + let mut same: Option = None; + let mut is_redundant = true; + for (_, operand) in &phi.operands { + if (same.is_some() && operand.identifier == same.unwrap()) + || operand.identifier == phi.place.identifier + { + continue; + } else if same.is_some() { + is_redundant = false; + break; + } else { + same = Some(operand.identifier); + } + } + if is_redundant { + let same = same.expect("Expected phis to be non-empty"); + rewrites.insert(phi.place.identifier, same); + false // remove this phi + } else { + true // keep this phi + } + }); + + // Rewrite instructions + let instruction_ids: Vec = + ir.blocks.get(&block_id).unwrap().instructions.clone(); + + for instr_id in &instruction_ids { + let instr_idx = instr_id.0 as usize; + let instr = &mut func.instructions[instr_idx]; + + // Rewrite all lvalues (matches TS eachInstructionLValue) + rewrite_place(&mut instr.lvalue, rewrites); + visitors::for_each_instruction_value_lvalue_mut(&mut instr.value, &mut |place| { + rewrite_place(place, rewrites); + }); + + // Rewrite operands using canonical visitor + visitors::for_each_instruction_value_operand_mut( + &mut func.instructions[instr_idx].value, + &mut |place| { + rewrite_place(place, rewrites); + }, + ); + + // Handle FunctionExpression/ObjectMethod context and recursion + let instr = &func.instructions[instr_idx]; + let func_expr_id = match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + Some(lowered_func.func) + } + _ => None, + }; + + if let Some(fid) = func_expr_id { + // Rewrite context places + let context = &mut env.functions[fid.0 as usize].context; + for place in context.iter_mut() { + rewrite_place(place, rewrites); + } + + // Take inner function out, process it, put it back + let mut inner_func = std::mem::replace( + &mut env.functions[fid.0 as usize], + placeholder_function(), + ); + + eliminate_redundant_phi_impl(&mut inner_func, env, rewrites); + + env.functions[fid.0 as usize] = inner_func; + } + } + + // Rewrite terminal operands using canonical visitor + let terminal = &mut ir.blocks.get_mut(&block_id).unwrap().terminal; + visitors::for_each_terminal_operand_mut(terminal, &mut |place| { + rewrite_place(place, rewrites); + }); + } + + if !(rewrites.len() > size && has_back_edge) { + break; + } + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_ssa/enter_ssa.rs b/crates/oxc_react_compiler/src/react_compiler_ssa/enter_ssa.rs new file mode 100644 index 0000000000000..6b293644dadf4 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_ssa/enter_ssa.rs @@ -0,0 +1,491 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors; +use crate::react_compiler_hir::*; +use crate::react_compiler_utils::FxIndexMap; + +// ============================================================================= +// SSABuilder +// ============================================================================= + +struct IncompletePhi { + old_place: Place, + new_place: Place, +} + +struct State { + defs: FxHashMap, + incomplete_phis: Vec, +} + +struct SSABuilder { + states: FxHashMap, + current: Option, + unsealed_preds: FxHashMap, + block_preds: FxHashMap>, + unknown: FxHashSet, + context: FxHashSet, + pending_phis: FxHashMap>, + processed_functions: Vec, +} + +impl SSABuilder { + fn new(blocks: &FxIndexMap) -> Self { + let mut block_preds = FxHashMap::default(); + for (id, block) in blocks { + block_preds.insert(*id, block.preds.iter().copied().collect()); + } + SSABuilder { + states: FxHashMap::default(), + current: None, + unsealed_preds: FxHashMap::default(), + block_preds, + unknown: FxHashSet::default(), + context: FxHashSet::default(), + pending_phis: FxHashMap::default(), + processed_functions: Vec::new(), + } + } + + fn define_function(&mut self, func: &HirFunction) { + for (id, block) in &func.body.blocks { + self.block_preds.insert(*id, block.preds.iter().copied().collect()); + } + } + + fn state_mut(&mut self) -> &mut State { + let current = self.current.expect("we need to be in a block to access state!"); + self.states.get_mut(¤t).expect("state not found for current block") + } + + fn make_id(&mut self, old_id: IdentifierId, env: &mut Environment) -> IdentifierId { + let new_id = env.next_identifier_id(); + let old = &env.identifiers[old_id.0 as usize]; + let declaration_id = old.declaration_id; + let name = old.name.clone(); + let loc = old.loc; + let new_ident = &mut env.identifiers[new_id.0 as usize]; + new_ident.declaration_id = declaration_id; + new_ident.name = name; + new_ident.loc = loc; + new_id + } + + fn define_place( + &mut self, + old_place: &Place, + env: &mut Environment, + ) -> Result { + let old_id = old_place.identifier; + + if self.unknown.contains(&old_id) { + let ident = &env.identifiers[old_id.0 as usize]; + let name = match &ident.name { + Some(name) => format!("{}${}", name.value(), old_id.0), + None => format!("${}", old_id.0), + }; + return Err(CompilerDiagnostic::new( + ErrorCategory::Todo, + "[hoisting] EnterSSA: Expected identifier to be defined before being used", + Some(format!("Identifier {} is undefined", name)), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: old_place.loc, + message: None, + identifier_name: None, + })); + } + + // Do not redefine context references. + if self.context.contains(&old_id) { + return Ok(self.get_place(old_place, env)); + } + + let new_id = self.make_id(old_id, env); + self.state_mut().defs.insert(old_id, new_id); + Ok(Place { + identifier: new_id, + effect: old_place.effect, + reactive: old_place.reactive, + loc: old_place.loc, + }) + } + + #[allow(dead_code)] + fn define_context( + &mut self, + old_place: &Place, + env: &mut Environment, + ) -> Result { + let old_id = old_place.identifier; + let new_place = self.define_place(old_place, env)?; + self.context.insert(old_id); + Ok(new_place) + } + + /// A function's context places capture a *binding*, not a value: the + /// variable is only read when the function is later called, so a context + /// place may reference a binding that is declared after the function + /// expression itself (eg `const colgroup = useMemo(() => ...)`, + /// where the JSX tag name resolves to the variable being assigned). Unmark + /// such identifiers so the later declaration doesn't error; if the function + /// body actually *reads* the variable before it is defined, visiting the + /// body re-marks it and the hoisting bailout in define_place still applies. + fn unmark_unknown(&mut self, id: IdentifierId) { + self.unknown.remove(&id); + } + + fn get_place(&mut self, old_place: &Place, env: &mut Environment) -> Place { + let current_id = self.current.expect("must be in a block"); + let new_id = self.get_id_at(old_place, current_id, env); + Place { + identifier: new_id, + effect: old_place.effect, + reactive: old_place.reactive, + loc: old_place.loc, + } + } + + fn get_id_at( + &mut self, + old_place: &Place, + block_id: BlockId, + env: &mut Environment, + ) -> IdentifierId { + if let Some(state) = self.states.get(&block_id) { + if let Some(&new_id) = state.defs.get(&old_place.identifier) { + return new_id; + } + } + + let preds = self.block_preds.get(&block_id).cloned().unwrap_or_default(); + + if preds.is_empty() { + self.unknown.insert(old_place.identifier); + return old_place.identifier; + } + + let unsealed = self.unsealed_preds.get(&block_id).copied().unwrap_or(0); + if unsealed > 0 { + let new_id = self.make_id(old_place.identifier, env); + let new_place = Place { + identifier: new_id, + effect: old_place.effect, + reactive: old_place.reactive, + loc: old_place.loc, + }; + let state = self.states.get_mut(&block_id).unwrap(); + state.incomplete_phis.push(IncompletePhi { old_place: old_place.clone(), new_place }); + state.defs.insert(old_place.identifier, new_id); + return new_id; + } + + if preds.len() == 1 { + let pred = preds[0]; + let new_id = self.get_id_at(old_place, pred, env); + self.states.get_mut(&block_id).unwrap().defs.insert(old_place.identifier, new_id); + return new_id; + } + + let new_id = self.make_id(old_place.identifier, env); + self.states.get_mut(&block_id).unwrap().defs.insert(old_place.identifier, new_id); + let new_place = Place { + identifier: new_id, + effect: old_place.effect, + reactive: old_place.reactive, + loc: old_place.loc, + }; + self.add_phi(block_id, old_place, &new_place, env); + new_id + } + + fn add_phi( + &mut self, + block_id: BlockId, + old_place: &Place, + new_place: &Place, + env: &mut Environment, + ) { + let preds = self.block_preds.get(&block_id).cloned().unwrap_or_default(); + + let mut pred_defs: FxIndexMap = FxIndexMap::default(); + for pred_block_id in &preds { + let pred_id = self.get_id_at(old_place, *pred_block_id, env); + pred_defs.insert( + *pred_block_id, + Place { + identifier: pred_id, + effect: old_place.effect, + reactive: old_place.reactive, + loc: old_place.loc, + }, + ); + } + + let phi = Phi { place: new_place.clone(), operands: pred_defs }; + + self.pending_phis.entry(block_id).or_default().push(phi); + } + + fn fix_incomplete_phis(&mut self, block_id: BlockId, env: &mut Environment) { + let incomplete_phis: Vec = + self.states.get_mut(&block_id).unwrap().incomplete_phis.drain(..).collect(); + for phi in &incomplete_phis { + self.add_phi(block_id, &phi.old_place, &phi.new_place, env); + } + } + + fn start_block(&mut self, block_id: BlockId) { + self.current = Some(block_id); + self.states + .insert(block_id, State { defs: FxHashMap::default(), incomplete_phis: Vec::new() }); + } +} + +// ============================================================================= +// Public entry point +// ============================================================================= + +pub fn enter_ssa(func: &mut HirFunction, env: &mut Environment) -> Result<(), CompilerDiagnostic> { + let mut builder = SSABuilder::new(&func.body.blocks); + let root_entry = func.body.entry; + enter_ssa_impl(func, &mut builder, env, root_entry)?; + + // Apply all pending phis to the actual blocks + apply_pending_phis(func, env, &mut builder); + + Ok(()) +} + +fn apply_pending_phis(func: &mut HirFunction, env: &mut Environment, builder: &mut SSABuilder) { + for (block_id, block) in func.body.blocks.iter_mut() { + if let Some(phis) = builder.pending_phis.remove(block_id) { + block.phis.extend(phis); + } + } + for fid in &builder.processed_functions.clone() { + let inner_func = &mut env.functions[fid.0 as usize]; + for (block_id, block) in inner_func.body.blocks.iter_mut() { + if let Some(phis) = builder.pending_phis.remove(block_id) { + block.phis.extend(phis); + } + } + } +} + +fn enter_ssa_impl( + func: &mut HirFunction, + builder: &mut SSABuilder, + env: &mut Environment, + root_entry: BlockId, +) -> Result<(), CompilerDiagnostic> { + let mut visited_blocks: FxHashSet = FxHashSet::default(); + let block_ids: Vec = func.body.blocks.keys().copied().collect(); + + for block_id in &block_ids { + let block_id = *block_id; + + if visited_blocks.contains(&block_id) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("found a cycle! visiting bb{} again", block_id.0), + None, + )); + } + + visited_blocks.insert(block_id); + builder.start_block(block_id); + + // Handle params at the root entry + if block_id == root_entry { + if !func.context.is_empty() { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Expected function context to be empty for outer function declarations", + None, + )); + } + let params = std::mem::take(&mut func.params); + let mut new_params = Vec::with_capacity(params.len()); + for param in params { + new_params.push(match param { + ParamPattern::Place(p) => ParamPattern::Place(builder.define_place(&p, env)?), + ParamPattern::Spread(s) => ParamPattern::Spread(SpreadPattern { + place: builder.define_place(&s.place, env)?, + }), + }); + } + func.params = new_params; + } + + // Process instructions + let instruction_ids: Vec = + func.body.blocks.get(&block_id).unwrap().instructions.clone(); + + for instr_id in &instruction_ids { + let instr_idx = instr_id.0 as usize; + let instr = &mut func.instructions[instr_idx]; + + // For FunctionExpression/ObjectMethod, we need to handle context + // mapping specially because env.functions is borrowed by the closure. + // First, check if this is a FunctionExpression/ObjectMethod and handle + // context mapping separately. + let func_expr_id = match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => Some(lowered_func.func), + _ => None, + }; + + // Map context places for function expressions before other operands + if let Some(fid) = func_expr_id { + let context = std::mem::take(&mut env.functions[fid.0 as usize].context); + env.functions[fid.0 as usize].context = + context.into_iter().map(|place| builder.get_place(&place, env)).collect(); + } + + // Map non-context operands + visitors::for_each_instruction_value_operand_mut(&mut instr.value, &mut |place| { + *place = builder.get_place(place, env); + }); + + // Map lvalues (skip DeclareContext/StoreContext — context variables + // don't participate in SSA renaming) + let instr = &mut func.instructions[instr_idx]; + let mut lvalue_err: Option = None; + visitors::for_each_instruction_lvalue_mut(instr, &mut |place| { + if lvalue_err.is_none() { + match builder.define_place(place, env) { + Ok(new_place) => *place = new_place, + Err(e) => lvalue_err = Some(e), + } + } + }); + if let Some(e) = lvalue_err { + return Err(e); + } + + // Handle inner function SSA + if let Some(fid) = func_expr_id { + let context_ids: Vec = env.functions[fid.0 as usize] + .context + .iter() + .map(|place| place.identifier) + .collect(); + for id in context_ids { + builder.unmark_unknown(id); + } + builder.processed_functions.push(fid); + let inner_func = &mut env.functions[fid.0 as usize]; + let inner_entry = inner_func.body.entry; + let entry_block = inner_func.body.blocks.get_mut(&inner_entry).unwrap(); + + if !entry_block.preds.is_empty() { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Expected function expression entry block to have zero predecessors", + None, + )); + } + entry_block.preds.insert(block_id); + + builder.define_function(inner_func); + + let saved_current = builder.current; + + // Map inner function params + let inner_params = std::mem::take(&mut env.functions[fid.0 as usize].params); + let mut new_inner_params = Vec::with_capacity(inner_params.len()); + for param in inner_params { + new_inner_params.push(match param { + ParamPattern::Place(p) => { + ParamPattern::Place(builder.define_place(&p, env)?) + } + ParamPattern::Spread(s) => ParamPattern::Spread(SpreadPattern { + place: builder.define_place(&s.place, env)?, + }), + }); + } + env.functions[fid.0 as usize].params = new_inner_params; + + // Take the inner function out of the arena to process it + let mut inner_func = + std::mem::replace(&mut env.functions[fid.0 as usize], placeholder_function()); + + enter_ssa_impl(&mut inner_func, builder, env, root_entry)?; + + // Put it back + env.functions[fid.0 as usize] = inner_func; + + builder.current = saved_current; + + // Clear entry preds + env.functions[fid.0 as usize] + .body + .blocks + .get_mut(&inner_entry) + .unwrap() + .preds + .clear(); + builder.block_preds.insert(inner_entry, Vec::new()); + } + } + + // Map terminal operands + let terminal = &mut func.body.blocks.get_mut(&block_id).unwrap().terminal; + visitors::for_each_terminal_operand_mut(terminal, &mut |place| { + *place = builder.get_place(place, env); + }); + + // Handle successors + let terminal_ref = &func.body.blocks.get(&block_id).unwrap().terminal; + let successors = visitors::each_terminal_successor(terminal_ref); + for output_id in successors { + let output_preds_len = + builder.block_preds.get(&output_id).map(|p| p.len() as u32).unwrap_or(0); + + let count = if builder.unsealed_preds.contains_key(&output_id) { + builder.unsealed_preds[&output_id] - 1 + } else { + output_preds_len - 1 + }; + builder.unsealed_preds.insert(output_id, count); + + if count == 0 && visited_blocks.contains(&output_id) { + builder.fix_incomplete_phis(output_id, env); + } + } + } + + Ok(()) +} + +/// Create a placeholder HirFunction for temporarily swapping an inner function +/// out of `env.functions` via `std::mem::replace`. The placeholder is never +/// read — the real function is swapped back immediately after processing. +pub fn placeholder_function<'a>() -> HirFunction<'a> { + HirFunction { + loc: None, + id: None, + name_hint: None, + fn_type: ReactFunctionType::Other, + params: Vec::new(), + return_type_annotation: None, + returns: Place { + identifier: IdentifierId(0), + effect: Effect::Unknown, + reactive: false, + loc: None, + }, + context: Vec::new(), + body: HIR { entry: BlockId(0), blocks: FxIndexMap::default() }, + instructions: Vec::new(), + generator: false, + is_async: false, + directives: Vec::new(), + aliasing_effects: None, + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_ssa/mod.rs b/crates/oxc_react_compiler/src/react_compiler_ssa/mod.rs new file mode 100644 index 0000000000000..592e5c59034bb --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_ssa/mod.rs @@ -0,0 +1,7 @@ +mod eliminate_redundant_phi; +pub mod enter_ssa; +mod rewrite_instruction_kinds_based_on_reassignment; + +pub use eliminate_redundant_phi::eliminate_redundant_phi; +pub use enter_ssa::enter_ssa; +pub use rewrite_instruction_kinds_based_on_reassignment::rewrite_instruction_kinds_based_on_reassignment; diff --git a/crates/oxc_react_compiler/src/react_compiler_ssa/rewrite_instruction_kinds_based_on_reassignment.rs b/crates/oxc_react_compiler/src/react_compiler_ssa/rewrite_instruction_kinds_based_on_reassignment.rs new file mode 100644 index 0000000000000..5d0742be8cd26 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_ssa/rewrite_instruction_kinds_based_on_reassignment.rs @@ -0,0 +1,377 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Rewrites InstructionKind of instructions which declare/assign variables, +//! converting the first declaration to Const/Let depending on whether it is +//! subsequently reassigned, and ensuring that subsequent reassignments are +//! marked as Reassign. +//! +//! Ported from TypeScript `src/SSA/RewriteInstructionKindsBasedOnReassignment.ts`. +//! +//! Note that declarations which were const in the original program cannot become +//! `let`, but the inverse is not true: a `let` which was reassigned in the source +//! may be converted to a `const` if the reassignment is not used and was removed +//! by dead code elimination. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::visitors::each_pattern_operand; +use crate::react_compiler_hir::{ + BlockKind, DeclarationId, HirFunction, InstructionKind, InstructionValue, ParamPattern, Place, +}; + +use crate::react_compiler_hir::environment::Environment; + +/// Create an invariant CompilerError (matches TS CompilerError.invariant). +/// When a loc is provided, creates a CompilerDiagnostic with an error detail item +/// (matching TS CompilerError.invariant which uses .withDetails()). +fn invariant_error(reason: &str, description: Option) -> CompilerError { + invariant_error_with_loc(reason, description, None) +} + +fn invariant_error_with_loc( + reason: &str, + description: Option, + loc: Option, +) -> CompilerError { + let mut err = CompilerError::new(); + let diagnostic = CompilerDiagnostic::new(ErrorCategory::Invariant, reason, description) + .with_detail(CompilerDiagnosticDetail::Error { + loc, + message: Some(reason.to_string()), + identifier_name: None, + }); + err.push_diagnostic(diagnostic); + err +} + +/// Format an InstructionKind variant name (matches TS `${kind}` interpolation). +fn format_kind(kind: Option) -> String { + match kind { + Some(InstructionKind::Const) => "Const".to_string(), + Some(InstructionKind::Let) => "Let".to_string(), + Some(InstructionKind::Reassign) => "Reassign".to_string(), + Some(InstructionKind::Catch) => "Catch".to_string(), + Some(InstructionKind::HoistedConst) => "HoistedConst".to_string(), + Some(InstructionKind::HoistedLet) => "HoistedLet".to_string(), + Some(InstructionKind::HoistedFunction) => "HoistedFunction".to_string(), + Some(InstructionKind::Function) => "Function".to_string(), + None => "null".to_string(), + } +} + +/// Format a Place like TS `printPlace()`: ` $[]{reactive}` +fn format_place(place: &Place, env: &Environment) -> String { + let ident = &env.identifiers[place.identifier.0 as usize]; + let name = match &ident.name { + Some(n) => n.value().to_string(), + None => String::new(), + }; + let scope = match ident.scope { + Some(scope_id) => format!("_@{}", scope_id.0), + None => String::new(), + }; + let mutable_range = if ident.mutable_range.end.0 > ident.mutable_range.start.0 + 1 { + format!("[{}:{}]", ident.mutable_range.start.0, ident.mutable_range.end.0) + } else { + String::new() + }; + let reactive = if place.reactive { "{reactive}" } else { "" }; + format!( + "{} {}${}{}{}{}", + place.effect, name, place.identifier.0, scope, mutable_range, reactive + ) +} + +/// Index into a collected list of declaration mutations to apply. +/// +/// We use a two-phase approach: first collect which declarations exist, +/// then apply mutations. This is because in the TS code, `declarations` +/// map stores references to LValue/LValuePattern and mutates `kind` through them. +/// In Rust, we track instruction indices and apply changes in a second pass. +enum DeclarationLoc { + /// An LValue from DeclareLocal or StoreLocal — identified by (block_index, instr_index_in_block) + Instruction { block_index: usize, instr_local_index: usize }, + /// A parameter or context variable (seeded as Let, may be upgraded to Let on reassignment — already Let) + ParamOrContext, +} + +pub fn rewrite_instruction_kinds_based_on_reassignment( + func: &mut HirFunction, + env: &Environment, +) -> Result<(), CompilerError> { + // Phase 1: Collect all information about which declarations need updates. + // + // Track: for each DeclarationId, the location of its first declaration, + // and whether it needs to be changed to Let (because of reassignment). + let mut declarations: FxHashMap = FxHashMap::default(); + // Track which (block_index, instr_local_index) should have their lvalue.kind set to Reassign + let mut reassign_locs: Vec<(usize, usize)> = Vec::new(); + // Track which declaration locations need to be set to Let + let mut let_locs: Vec<(usize, usize)> = Vec::new(); + // Track which (block_index, instr_local_index) should have their lvalue.kind set to Const + let mut const_locs: Vec<(usize, usize)> = Vec::new(); + // Track which (block_index, instr_local_index) Destructure instructions get a specific kind + let mut destructure_kind_locs: Vec<(usize, usize, InstructionKind)> = Vec::new(); + + // Seed with parameters + for param in &func.params { + let place: &Place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + let ident = &env.identifiers[place.identifier.0 as usize]; + if ident.name.is_some() { + declarations.insert(ident.declaration_id, DeclarationLoc::ParamOrContext); + } + } + + // Seed with context variables + for place in &func.context { + let ident = &env.identifiers[place.identifier.0 as usize]; + if ident.name.is_some() { + declarations.insert(ident.declaration_id, DeclarationLoc::ParamOrContext); + } + } + + // Process all blocks + let block_keys: Vec<_> = func.body.blocks.keys().cloned().collect(); + for (block_index, block_id) in block_keys.iter().enumerate() { + let block = &func.body.blocks[block_id]; + let block_kind = block.kind; + for (local_idx, instr_id) in block.instructions.iter().enumerate() { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::DeclareLocal { lvalue, .. } => { + let decl_id = + env.identifiers[lvalue.place.identifier.0 as usize].declaration_id; + if declarations.contains_key(&decl_id) { + return Err(invariant_error_with_loc( + "Expected variable not to be defined prior to declaration", + Some(format!( + "{} was already defined", + format_place(&lvalue.place, env), + )), + lvalue.place.loc, + )); + } + declarations.insert( + decl_id, + DeclarationLoc::Instruction { block_index, instr_local_index: local_idx }, + ); + } + InstructionValue::StoreLocal { lvalue, .. } => { + let ident = &env.identifiers[lvalue.place.identifier.0 as usize]; + if ident.name.is_some() { + let decl_id = ident.declaration_id; + if let Some(existing) = declarations.get(&decl_id) { + // Reassignment: mark existing declaration as Let, current as Reassign + match existing { + DeclarationLoc::Instruction { + block_index: bi, + instr_local_index: ili, + } => { + let_locs.push((*bi, *ili)); + } + DeclarationLoc::ParamOrContext => { + // Already Let, no-op + } + } + reassign_locs.push((block_index, local_idx)); + } else { + // First store — mark as Const + // Mirrors TS: CompilerError.invariant(!declarations.has(...)) + if declarations.contains_key(&decl_id) { + return Err(invariant_error_with_loc( + "Expected variable not to be defined prior to declaration", + Some(format!( + "{} was already defined", + format_place(&lvalue.place, env), + )), + lvalue.place.loc, + )); + } + declarations.insert( + decl_id, + DeclarationLoc::Instruction { + block_index, + instr_local_index: local_idx, + }, + ); + const_locs.push((block_index, local_idx)); + } + } + } + InstructionValue::Destructure { lvalue, .. } => { + let mut kind: Option = None; + for place in each_pattern_operand(&lvalue.pattern) { + let ident = &env.identifiers[place.identifier.0 as usize]; + if ident.name.is_none() { + if !(kind.is_none() || kind == Some(InstructionKind::Const)) { + return Err(invariant_error_with_loc( + "Expected consistent kind for destructuring", + Some(format!( + "other places were `{}` but '{}' is const", + format_kind(kind), + format_place(&place, env), + )), + place.loc, + )); + } + kind = Some(InstructionKind::Const); + } else { + let decl_id = ident.declaration_id; + if let Some(existing) = declarations.get(&decl_id) { + // Reassignment + if !(kind.is_none() || kind == Some(InstructionKind::Reassign)) { + return Err(invariant_error_with_loc( + "Expected consistent kind for destructuring", + Some(format!( + "Other places were `{}` but '{}' is reassigned", + format_kind(kind), + format_place(&place, env), + )), + place.loc, + )); + } + kind = Some(InstructionKind::Reassign); + match existing { + DeclarationLoc::Instruction { + block_index: bi, + instr_local_index: ili, + } => { + let_locs.push((*bi, *ili)); + } + DeclarationLoc::ParamOrContext => { + // Already Let + } + } + } else { + // New declaration + if block_kind == BlockKind::Value { + return Err(invariant_error_with_loc( + "TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)", + None, + place.loc, + )); + } + declarations.insert( + decl_id, + DeclarationLoc::Instruction { + block_index, + instr_local_index: local_idx, + }, + ); + if !(kind.is_none() || kind == Some(InstructionKind::Const)) { + return Err(invariant_error_with_loc( + "Expected consistent kind for destructuring", + Some(format!( + "Other places were `{}` but '{}' is const", + format_kind(kind), + format_place(&place, env), + )), + place.loc, + )); + } + kind = Some(InstructionKind::Const); + } + } + } + let kind = + kind.ok_or_else(|| invariant_error("Expected at least one operand", None))?; + destructure_kind_locs.push((block_index, local_idx, kind)); + } + InstructionValue::PostfixUpdate { lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue, .. } => { + let ident = &env.identifiers[lvalue.identifier.0 as usize]; + let decl_id = ident.declaration_id; + let Some(existing) = declarations.get(&decl_id) else { + return Err(invariant_error_with_loc( + "Expected variable to have been defined", + Some(format!("No declaration for {}", format_place(lvalue, env),)), + lvalue.loc, + )); + }; + match existing { + DeclarationLoc::Instruction { block_index: bi, instr_local_index: ili } => { + let_locs.push((*bi, *ili)); + } + DeclarationLoc::ParamOrContext => { + // Already Let + } + } + } + _ => {} + } + } + } + + // Phase 2: Apply all collected mutations. + + // Helper: given (block_index, instr_local_index), get the InstructionId + // and mutate the instruction's lvalue kind. + for (bi, ili) in const_locs { + let block_id = &block_keys[bi]; + let instr_id = func.body.blocks[block_id].instructions[ili]; + let instr = &mut func.instructions[instr_id.0 as usize]; + match &mut instr.value { + InstructionValue::StoreLocal { lvalue, .. } => { + lvalue.kind = InstructionKind::Const; + } + _ => {} + } + } + + for (bi, ili) in reassign_locs { + let block_id = &block_keys[bi]; + let instr_id = func.body.blocks[block_id].instructions[ili]; + let instr = &mut func.instructions[instr_id.0 as usize]; + match &mut instr.value { + InstructionValue::StoreLocal { lvalue, .. } => { + lvalue.kind = InstructionKind::Reassign; + } + _ => {} + } + } + + // Apply destructure_kind_locs BEFORE let_locs: a Destructure that first + // declares a variable gets kind=Const here, but if a later instruction + // reassigns that variable the Destructure must become Let. Applying + // let_locs afterwards allows it to override the Const set here, matching + // the TS behaviour where `declaration.kind = Let` mutates the original + // lvalue reference after the Destructure's own `lvalue.kind = kind`. + for (bi, ili, kind) in destructure_kind_locs { + let block_id = &block_keys[bi]; + let instr_id = func.body.blocks[block_id].instructions[ili]; + let instr = &mut func.instructions[instr_id.0 as usize]; + match &mut instr.value { + InstructionValue::Destructure { lvalue, .. } => { + lvalue.kind = kind; + } + _ => {} + } + } + + for (bi, ili) in let_locs { + let block_id = &block_keys[bi]; + let instr_id = func.body.blocks[block_id].instructions[ili]; + let instr = &mut func.instructions[instr_id.0 as usize]; + match &mut instr.value { + InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::StoreLocal { lvalue, .. } => { + lvalue.kind = InstructionKind::Let; + } + InstructionValue::Destructure { lvalue, .. } => { + lvalue.kind = InstructionKind::Let; + } + _ => {} + } + } + + Ok(()) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_typeinference/infer_types.rs b/crates/oxc_react_compiler/src/react_compiler_typeinference/infer_types.rs new file mode 100644 index 0000000000000..8f310b632f7cd --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_typeinference/infer_types.rs @@ -0,0 +1,1492 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Type inference pass. +//! +//! Generates type equations from the HIR, unifies them, and applies the +//! resolved types back to identifiers. Analogous to TS `InferTypes.ts`. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; +use crate::react_compiler_hir::environment::{Environment, is_hook_name}; +use crate::react_compiler_hir::object_shape::{ + BUILT_IN_ARRAY_ID, BUILT_IN_FUNCTION_ID, BUILT_IN_JSX_ID, BUILT_IN_MIXED_READONLY_ID, + BUILT_IN_OBJECT_ID, BUILT_IN_PROPS_ID, BUILT_IN_REF_VALUE_ID, BUILT_IN_SET_STATE_ID, + BUILT_IN_USE_REF_ID, ShapeRegistry, +}; +use crate::react_compiler_hir::{ + ArrayPatternElement, BinaryOperator, FunctionId, HirFunction, Identifier, IdentifierId, + IdentifierName, InstructionId, InstructionKind, InstructionValue, JsxAttribute, + LoweredFunction, ManualMemoDependencyRoot, NonLocalBinding, ObjectPropertyKey, + ObjectPropertyOrSpread, ParamPattern, Pattern, PropertyLiteral, PropertyNameKind, + ReactFunctionType, SourceLocation, Terminal, Type, TypeId, +}; +use crate::react_compiler_ssa::enter_ssa::placeholder_function; + +// ============================================================================= +// Public API +// ============================================================================= + +pub fn infer_types( + func: &mut HirFunction, + env: &mut Environment, +) -> Result<(), CompilerDiagnostic> { + let enable_treat_ref_like_identifiers_as_refs = + env.config.enable_treat_ref_like_identifiers_as_refs; + let enable_treat_set_identifiers_as_state_setters = + env.config.enable_treat_set_identifiers_as_state_setters; + // Pre-compute custom hook type for property resolution fallback + let custom_hook_type = env.get_custom_hook_type_opt(); + let mut unifier = Unifier::new( + enable_treat_ref_like_identifiers_as_refs, + custom_hook_type, + enable_treat_set_identifiers_as_state_setters, + ); + generate(func, env, &mut unifier)?; + + apply_function(func, &env.functions, &mut env.identifiers, &mut env.types, &mut unifier); + Ok(()) +} + +// ============================================================================= +// Helpers +// ============================================================================= + +/// Get the type for an identifier as a TypeVar referencing its type slot. +fn get_type(id: IdentifierId, identifiers: &[Identifier]) -> Type { + let type_id = identifiers[id.0 as usize].type_; + Type::TypeVar { id: type_id } +} + +/// Allocate a new TypeVar in the types arena (standalone, no &mut Environment needed). +fn make_type(types: &mut Vec) -> Type { + let id = TypeId(types.len() as u32); + types.push(Type::TypeVar { id }); + Type::TypeVar { id } +} + +/// Pre-resolve LoadGlobal types for a single function's instructions. +fn pre_resolve_globals( + func: &HirFunction, + function_key: u32, + env: &mut Environment, + global_types: &mut FxHashMap<(u32, InstructionId), Type>, +) { + for &instr_id in func.body.blocks.values().flat_map(|b| &b.instructions) { + let instr = &func.instructions[instr_id.0 as usize]; + if let InstructionValue::LoadGlobal { binding, loc, .. } = &instr.value { + if let Some(global_type) = env.get_global_declaration(binding, *loc).ok().flatten() { + global_types.insert((function_key, instr_id), global_type); + } + } + } +} + +/// Recursively pre-resolve LoadGlobal types for an inner function and its children. +fn pre_resolve_globals_recursive( + func_id: FunctionId, + env: &mut Environment, + global_types: &mut FxHashMap<(u32, InstructionId), Type>, +) { + // Collect LoadGlobal bindings and child function IDs in one pass to avoid + // borrow conflicts (we need &env.functions to read, then &mut env for + // get_global_declaration). + let inner = &env.functions[func_id.0 as usize]; + let mut load_globals: Vec<(InstructionId, NonLocalBinding, Option)> = + Vec::new(); + let mut child_func_ids: Vec = Vec::new(); + + for block in inner.body.blocks.values() { + for &instr_id in &block.instructions { + let instr = &inner.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::LoadGlobal { binding, loc, .. } => { + load_globals.push((instr_id, binding.clone(), *loc)); + } + InstructionValue::FunctionExpression { + lowered_func: LoweredFunction { func: fid }, + .. + } + | InstructionValue::ObjectMethod { + lowered_func: LoweredFunction { func: fid }, + .. + } => { + child_func_ids.push(*fid); + } + _ => {} + } + } + } + + // Now resolve globals (no longer borrowing env.functions) + for (instr_id, binding, loc) in load_globals { + if let Some(global_type) = env.get_global_declaration(&binding, loc).ok().flatten() { + global_types.insert((func_id.0, instr_id), global_type); + } + } + + // Recurse into child functions + for child_id in child_func_ids { + pre_resolve_globals_recursive(child_id, env, global_types); + } +} + +fn is_primitive_binary_op(op: &BinaryOperator) -> bool { + matches!( + op, + BinaryOperator::Add + | BinaryOperator::Subtract + | BinaryOperator::Divide + | BinaryOperator::Modulo + | BinaryOperator::Multiply + | BinaryOperator::Exponent + | BinaryOperator::BitwiseAnd + | BinaryOperator::BitwiseOr + | BinaryOperator::ShiftRight + | BinaryOperator::ShiftLeft + | BinaryOperator::BitwiseXor + | BinaryOperator::GreaterThan + | BinaryOperator::LessThan + | BinaryOperator::GreaterEqual + | BinaryOperator::LessEqual + ) +} + +/// Resolve a property type from the shapes registry. +/// If `custom_hook_type` is provided and the property name looks like a hook, +/// it will be used as a fallback when no matching property is found (matching +/// TS `getPropertyType` behavior). +fn resolve_property_type( + shapes: &ShapeRegistry, + resolved_object: &Type, + property_name: &PropertyNameKind, + custom_hook_type: Option<&Type>, +) -> Option { + let shape_id = match resolved_object { + Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), + _ => { + // No shape, but if property name is hook-like, return hook type + if let Some(hook_type) = custom_hook_type { + if let PropertyNameKind::Literal { value: PropertyLiteral::String(s) } = + property_name + { + if is_hook_name(s) { + return Some(hook_type.clone()); + } + } + } + return None; + } + }; + let shape_id = match shape_id { + Some(id) => id, + None => { + // Object/Function with no shapeId: TS getPropertyType falls through + // to hook-name check, TS getFallthroughPropertyType returns null + if let PropertyNameKind::Literal { value: PropertyLiteral::String(s) } = property_name { + if is_hook_name(s) { + return custom_hook_type.cloned(); + } + } + return None; + } + }; + let shape = shapes.get(shape_id)?; + + match property_name { + PropertyNameKind::Literal { value } => match value { + PropertyLiteral::String(s) => shape + .properties + .get(s.as_str()) + .or_else(|| shape.properties.get("*")) + .cloned() + // Hook-name fallback: if property is not found in shape but looks + // like a hook name, return the custom hook type + .or_else(|| if is_hook_name(s) { custom_hook_type.cloned() } else { None }), + PropertyLiteral::Number(_) => shape.properties.get("*").cloned(), + }, + PropertyNameKind::Computed { .. } => shape.properties.get("*").cloned(), + } +} + +/// Check if a property access looks like a ref pattern (e.g. `ref.current`, `fooRef.current`). +/// Matches TS `isRefLikeName` in InferTypes.ts. +fn is_ref_like_name(object_name: &str, property_name: &PropertyNameKind) -> bool { + let is_current = match property_name { + PropertyNameKind::Literal { value: PropertyLiteral::String(s) } => s == "current", + _ => false, + }; + if !is_current { + return false; + } + // Match TS regex: /^(?:[a-zA-Z$_][a-zA-Z$_0-9]*)Ref$|^ref$/ + // "Ref" alone does NOT match — requires at least one character before "Ref" + // (e.g., "fooRef", "aRef" match, but bare "Ref" does not). + object_name == "ref" + || (object_name.len() > 3 + && object_name.ends_with("Ref") + && object_name[..1] + .chars() + .next() + .is_some_and(|c| c.is_ascii_alphabetic() || c == '$' || c == '_')) +} + +/// Type equality matching TS `typeEquals`. +/// +/// Note: Function equality only compares return types (matching TS `funcTypeEquals` +/// which ignores `shapeId` and `isConstructor`). Phi equality always returns false +/// because the TS `phiTypeEquals` has a bug where `return false` is outside the +/// `if` block, so it unconditionally returns false. +fn type_equals(a: &Type, b: &Type) -> bool { + match (a, b) { + (Type::TypeVar { id: id_a }, Type::TypeVar { id: id_b }) => id_a == id_b, + (Type::Primitive, Type::Primitive) => true, + (Type::Poly, Type::Poly) => true, + (Type::ObjectMethod, Type::ObjectMethod) => true, + (Type::Object { shape_id: sa }, Type::Object { shape_id: sb }) => sa == sb, + (Type::Function { return_type: ra, .. }, Type::Function { return_type: rb, .. }) => { + type_equals(ra, rb) + } + _ => false, + } +} + +fn set_name(names: &mut FxHashMap, id: IdentifierId, source: &Identifier) { + if let Some(IdentifierName::Named(ref name)) = source.name { + names.insert(id, name.clone()); + } +} + +fn get_name(names: &FxHashMap, id: IdentifierId) -> String { + names.get(&id).cloned().unwrap_or_default() +} + +// ============================================================================= +// Generate equations +// ============================================================================= + +/// Generate type equations from a top-level function. +/// +/// Takes `&mut Environment` for convenience. Inner functions use +/// `generate_for_function_id` with split borrows instead, because the +/// take/replace pattern on `env.functions` requires separate `&mut` access +/// to different fields. +fn generate( + func: &HirFunction, + env: &mut Environment, + unifier: &mut Unifier, +) -> Result<(), CompilerDiagnostic> { + // Component params + if func.fn_type == ReactFunctionType::Component { + if let Some(first) = func.params.first() { + if let ParamPattern::Place(place) = first { + let ty = get_type(place.identifier, &env.identifiers); + unifier.unify( + ty, + Type::Object { shape_id: Some(BUILT_IN_PROPS_ID.to_string()) }, + &env.shapes, + )?; + } + } + if let Some(second) = func.params.get(1) { + if let ParamPattern::Place(place) = second { + let ty = get_type(place.identifier, &env.identifiers); + unifier.unify( + ty, + Type::Object { shape_id: Some(BUILT_IN_USE_REF_ID.to_string()) }, + &env.shapes, + )?; + } + } + } + + // Pre-resolve LoadGlobal types for all functions (outer + inner). We do + // this before the instruction loop because get_global_declaration needs + // &mut env, but generate_instruction_types takes split borrows on env fields. + // The key is (function_key, InstructionId) where function_key is u32::MAX + // for the outer function and FunctionId.0 for inner functions. + let mut global_types: FxHashMap<(u32, InstructionId), Type> = FxHashMap::default(); + pre_resolve_globals(func, u32::MAX, env, &mut global_types); + // Also pre-resolve inner functions recursively + for &instr_id in func.body.blocks.values().flat_map(|b| &b.instructions) { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::FunctionExpression { + lowered_func: LoweredFunction { func: func_id }, + .. + } + | InstructionValue::ObjectMethod { + lowered_func: LoweredFunction { func: func_id }, + .. + } => { + pre_resolve_globals_recursive(*func_id, env, &mut global_types); + } + _ => {} + } + } + + let mut names: FxHashMap = FxHashMap::default(); + let mut return_types: Vec = Vec::new(); + + for (_block_id, block) in &func.body.blocks { + // Phis + for phi in &block.phis { + let left = get_type(phi.place.identifier, &env.identifiers); + let operands: Vec = + phi.operands.values().map(|p| get_type(p.identifier, &env.identifiers)).collect(); + unifier.unify(left, Type::Phi { operands }, &env.shapes)?; + } + + // Instructions — use split borrows: &env.identifiers, &env.shapes + // are immutable, while &mut env.types and &mut env.functions are mutable. + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + generate_instruction_types( + instr, + instr_id, + u32::MAX, + &env.identifiers, + &mut env.types, + &mut env.functions, + &mut names, + &global_types, + &env.shapes, + unifier, + )?; + } + + // Return terminals + if let Terminal::Return { ref value, .. } = block.terminal { + return_types.push(get_type(value.identifier, &env.identifiers)); + } + } + + // Unify return types + let returns_type = get_type(func.returns.identifier, &env.identifiers); + if return_types.len() > 1 { + unifier.unify(returns_type, Type::Phi { operands: return_types }, &env.shapes)?; + } else if return_types.len() == 1 { + unifier.unify(returns_type, return_types.into_iter().next().unwrap(), &env.shapes)?; + } + Ok(()) +} + +/// Recursively generate equations for an inner function (accessed via FunctionId). +fn generate_for_function_id( + func_id: FunctionId, + identifiers: &[Identifier], + types: &mut Vec, + functions: &mut Vec, + global_types: &FxHashMap<(u32, InstructionId), Type>, + shapes: &ShapeRegistry, + unifier: &mut Unifier, +) -> Result<(), CompilerDiagnostic> { + // Take the function out temporarily to avoid borrow conflicts + let inner = std::mem::replace(&mut functions[func_id.0 as usize], placeholder_function()); + + // Process params for component inner functions + if inner.fn_type == ReactFunctionType::Component { + if let Some(first) = inner.params.first() { + if let ParamPattern::Place(place) = first { + let ty = get_type(place.identifier, identifiers); + unifier.unify( + ty, + Type::Object { shape_id: Some(BUILT_IN_PROPS_ID.to_string()) }, + shapes, + )?; + } + } + if let Some(second) = inner.params.get(1) { + if let ParamPattern::Place(place) = second { + let ty = get_type(place.identifier, identifiers); + unifier.unify( + ty, + Type::Object { shape_id: Some(BUILT_IN_USE_REF_ID.to_string()) }, + shapes, + )?; + } + } + } + + // TS creates a fresh `names` Map per recursive `generate` call, so inner + // functions don't inherit or pollute the outer function's name mappings. + let mut inner_names: FxHashMap = FxHashMap::default(); + let mut inner_return_types: Vec = Vec::new(); + + for (_block_id, block) in &inner.body.blocks { + for phi in &block.phis { + let left = get_type(phi.place.identifier, identifiers); + let operands: Vec = + phi.operands.values().map(|p| get_type(p.identifier, identifiers)).collect(); + unifier.unify(left, Type::Phi { operands }, shapes)?; + } + + for &instr_id in &block.instructions { + let instr = &inner.instructions[instr_id.0 as usize]; + generate_instruction_types( + instr, + instr_id, + func_id.0, + identifiers, + types, + functions, + &mut inner_names, + global_types, + shapes, + unifier, + )?; + } + + if let Terminal::Return { ref value, .. } = block.terminal { + inner_return_types.push(get_type(value.identifier, identifiers)); + } + } + + let returns_type = get_type(inner.returns.identifier, identifiers); + if inner_return_types.len() > 1 { + unifier.unify(returns_type, Type::Phi { operands: inner_return_types }, shapes)?; + } else if inner_return_types.len() == 1 { + unifier.unify(returns_type, inner_return_types.into_iter().next().unwrap(), shapes)?; + } + + // Put the function back + functions[func_id.0 as usize] = inner; + Ok(()) +} + +fn generate_instruction_types( + instr: &crate::react_compiler_hir::Instruction, + instr_id: InstructionId, + function_key: u32, + identifiers: &[Identifier], + types: &mut Vec, + functions: &mut Vec, + names: &mut FxHashMap, + global_types: &FxHashMap<(u32, InstructionId), Type>, + shapes: &ShapeRegistry, + unifier: &mut Unifier, +) -> Result<(), CompilerDiagnostic> { + let left = get_type(instr.lvalue.identifier, identifiers); + + match &instr.value { + InstructionValue::TemplateLiteral { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::Primitive { .. } => { + unifier.unify(left, Type::Primitive, shapes)?; + } + + InstructionValue::UnaryExpression { .. } => { + unifier.unify(left, Type::Primitive, shapes)?; + } + + InstructionValue::LoadLocal { place, .. } => { + set_name(names, instr.lvalue.identifier, &identifiers[place.identifier.0 as usize]); + let place_type = get_type(place.identifier, identifiers); + unifier.unify(left, place_type, shapes)?; + } + + InstructionValue::DeclareContext { .. } | InstructionValue::LoadContext { .. } => { + // Intentionally skip type inference for most context variables + } + + InstructionValue::StoreContext { lvalue, value, .. } => { + if lvalue.kind == InstructionKind::Const { + let lvalue_type = get_type(lvalue.place.identifier, identifiers); + let value_type = get_type(value.identifier, identifiers); + unifier.unify(lvalue_type, value_type, shapes)?; + } + } + + InstructionValue::StoreLocal { lvalue, value, .. } => { + let value_type = get_type(value.identifier, identifiers); + unifier.unify(left, value_type.clone(), shapes)?; + let lvalue_type = get_type(lvalue.place.identifier, identifiers); + unifier.unify(lvalue_type, value_type, shapes)?; + } + + InstructionValue::StoreGlobal { value, .. } => { + let value_type = get_type(value.identifier, identifiers); + unifier.unify(left, value_type, shapes)?; + } + + InstructionValue::BinaryExpression { + operator, left: bin_left, right: bin_right, .. + } => { + if is_primitive_binary_op(operator) { + let left_operand_type = get_type(bin_left.identifier, identifiers); + unifier.unify(left_operand_type, Type::Primitive, shapes)?; + let right_operand_type = get_type(bin_right.identifier, identifiers); + unifier.unify(right_operand_type, Type::Primitive, shapes)?; + } + unifier.unify(left, Type::Primitive, shapes)?; + } + + InstructionValue::PostfixUpdate { value, lvalue, .. } + | InstructionValue::PrefixUpdate { value, lvalue, .. } => { + let value_type = get_type(value.identifier, identifiers); + unifier.unify(value_type, Type::Primitive, shapes)?; + let lvalue_type = get_type(lvalue.identifier, identifiers); + unifier.unify(lvalue_type, Type::Primitive, shapes)?; + unifier.unify(left, Type::Primitive, shapes)?; + } + + InstructionValue::LoadGlobal { .. } => { + // Type was pre-resolved in generate() via env.get_global_declaration() + if let Some(global_type) = global_types.get(&(function_key, instr_id)) { + unifier.unify(left, global_type.clone(), shapes)?; + } + } + + InstructionValue::CallExpression { callee, .. } => { + let return_type = make_type(types); + let mut shape_id = None; + if unifier.enable_treat_set_identifiers_as_state_setters { + let name = get_name(names, callee.identifier); + if name.starts_with("set") { + shape_id = Some(BUILT_IN_SET_STATE_ID.to_string()); + } + } + let callee_type = get_type(callee.identifier, identifiers); + unifier.unify( + callee_type, + Type::Function { + shape_id, + return_type: Box::new(return_type.clone()), + is_constructor: false, + }, + shapes, + )?; + unifier.unify(left, return_type, shapes)?; + } + + InstructionValue::TaggedTemplateExpression { tag, .. } => { + let return_type = make_type(types); + let tag_type = get_type(tag.identifier, identifiers); + unifier.unify( + tag_type, + Type::Function { + shape_id: None, + return_type: Box::new(return_type.clone()), + is_constructor: false, + }, + shapes, + )?; + unifier.unify(left, return_type, shapes)?; + } + + InstructionValue::ObjectExpression { properties, .. } => { + for prop in properties { + if let ObjectPropertyOrSpread::Property(obj_prop) = prop { + if let ObjectPropertyKey::Computed { name } = &obj_prop.key { + let name_type = get_type(name.identifier, identifiers); + unifier.unify(name_type, Type::Primitive, shapes)?; + } + } + } + unifier.unify( + left, + Type::Object { shape_id: Some(BUILT_IN_OBJECT_ID.to_string()) }, + shapes, + )?; + } + + InstructionValue::ArrayExpression { .. } => { + unifier.unify( + left, + Type::Object { shape_id: Some(BUILT_IN_ARRAY_ID.to_string()) }, + shapes, + )?; + } + + InstructionValue::PropertyLoad { object, property, .. } => { + let object_type = get_type(object.identifier, identifiers); + let object_name = get_name(names, object.identifier); + unifier.unify( + left, + Type::Property { + object_type: Box::new(object_type), + object_name, + property_name: PropertyNameKind::Literal { value: property.clone() }, + }, + shapes, + )?; + } + + InstructionValue::ComputedLoad { object, property, .. } => { + let object_type = get_type(object.identifier, identifiers); + let object_name = get_name(names, object.identifier); + let prop_type = get_type(property.identifier, identifiers); + unifier.unify( + left, + Type::Property { + object_type: Box::new(object_type), + object_name, + property_name: PropertyNameKind::Computed { value: Box::new(prop_type) }, + }, + shapes, + )?; + } + + InstructionValue::MethodCall { property, .. } => { + let return_type = make_type(types); + let prop_type = get_type(property.identifier, identifiers); + unifier.unify( + prop_type, + Type::Function { + return_type: Box::new(return_type.clone()), + shape_id: None, + is_constructor: false, + }, + shapes, + )?; + unifier.unify(left, return_type, shapes)?; + } + + InstructionValue::Destructure { lvalue, value, .. } => match &lvalue.pattern { + Pattern::Array(array_pattern) => { + for (i, item) in array_pattern.items.iter().enumerate() { + match item { + ArrayPatternElement::Place(place) => { + let item_type = get_type(place.identifier, identifiers); + let value_type = get_type(value.identifier, identifiers); + let object_name = get_name(names, value.identifier); + unifier.unify( + item_type, + Type::Property { + object_type: Box::new(value_type), + object_name, + property_name: PropertyNameKind::Literal { + value: PropertyLiteral::String(i.to_string()), + }, + }, + shapes, + )?; + } + ArrayPatternElement::Spread(spread) => { + let spread_type = get_type(spread.place.identifier, identifiers); + unifier.unify( + spread_type, + Type::Object { shape_id: Some(BUILT_IN_ARRAY_ID.to_string()) }, + shapes, + )?; + } + ArrayPatternElement::Hole => { + continue; + } + } + } + } + Pattern::Object(object_pattern) => { + for prop in &object_pattern.properties { + if let ObjectPropertyOrSpread::Property(obj_prop) = prop { + match &obj_prop.key { + ObjectPropertyKey::Identifier { name } + | ObjectPropertyKey::String { name } => { + let prop_place_type = + get_type(obj_prop.place.identifier, identifiers); + let value_type = get_type(value.identifier, identifiers); + let object_name = get_name(names, value.identifier); + unifier.unify( + prop_place_type, + Type::Property { + object_type: Box::new(value_type), + object_name, + property_name: PropertyNameKind::Literal { + value: PropertyLiteral::String(name.clone()), + }, + }, + shapes, + )?; + } + _ => {} + } + } + } + } + }, + + InstructionValue::TypeCastExpression { value, .. } => { + let value_type = get_type(value.identifier, identifiers); + unifier.unify(left, value_type, shapes)?; + } + + InstructionValue::PropertyDelete { .. } | InstructionValue::ComputedDelete { .. } => { + unifier.unify(left, Type::Primitive, shapes)?; + } + + InstructionValue::FunctionExpression { + lowered_func: LoweredFunction { func: func_id }, + .. + } => { + // Recurse into inner function first + generate_for_function_id( + *func_id, + identifiers, + types, + functions, + global_types, + shapes, + unifier, + )?; + // Get the inner function's return type + let inner_func = &functions[func_id.0 as usize]; + let inner_return_type = get_type(inner_func.returns.identifier, identifiers); + unifier.unify( + left, + Type::Function { + shape_id: Some(BUILT_IN_FUNCTION_ID.to_string()), + return_type: Box::new(inner_return_type), + is_constructor: false, + }, + shapes, + )?; + } + + InstructionValue::NextPropertyOf { .. } => { + unifier.unify(left, Type::Primitive, shapes)?; + } + + InstructionValue::ObjectMethod { + lowered_func: LoweredFunction { func: func_id }, .. + } => { + generate_for_function_id( + *func_id, + identifiers, + types, + functions, + global_types, + shapes, + unifier, + )?; + unifier.unify(left, Type::ObjectMethod, shapes)?; + } + + InstructionValue::JsxExpression { props, .. } => { + if unifier.enable_treat_ref_like_identifiers_as_refs { + for prop in props { + if let JsxAttribute::Attribute { name, place } = prop { + if name == "ref" { + let ref_type = get_type(place.identifier, identifiers); + unifier.unify( + ref_type, + Type::Object { shape_id: Some(BUILT_IN_USE_REF_ID.to_string()) }, + shapes, + )?; + } + } + } + } + unifier.unify( + left, + Type::Object { shape_id: Some(BUILT_IN_JSX_ID.to_string()) }, + shapes, + )?; + } + + InstructionValue::JsxFragment { .. } => { + unifier.unify( + left, + Type::Object { shape_id: Some(BUILT_IN_JSX_ID.to_string()) }, + shapes, + )?; + } + + InstructionValue::NewExpression { callee, .. } => { + let return_type = make_type(types); + let callee_type = get_type(callee.identifier, identifiers); + unifier.unify( + callee_type, + Type::Function { + return_type: Box::new(return_type.clone()), + shape_id: None, + is_constructor: true, + }, + shapes, + )?; + unifier.unify(left, return_type, shapes)?; + } + + InstructionValue::PropertyStore { object, property, .. } => { + let dummy = make_type(types); + let object_type = get_type(object.identifier, identifiers); + let object_name = get_name(names, object.identifier); + unifier.unify( + dummy, + Type::Property { + object_type: Box::new(object_type), + object_name, + property_name: PropertyNameKind::Literal { value: property.clone() }, + }, + shapes, + )?; + } + + InstructionValue::DeclareLocal { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::ComputedStore { .. } + | InstructionValue::Await { .. } + | InstructionValue::GetIterator { .. } + | InstructionValue::IteratorNext { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } + | InstructionValue::FinishMemoize { .. } => { + // No type equations for these + } + + InstructionValue::StartMemoize { .. } => { + // No type equations for StartMemoize itself + } + } + Ok(()) +} + +// ============================================================================= +// Apply resolved types +// ============================================================================= + +fn apply_function( + func: &HirFunction, + functions: &[HirFunction], + identifiers: &mut [Identifier], + types: &mut Vec, + unifier: &Unifier, +) { + for (_block_id, block) in &func.body.blocks { + // Phi places + for phi in &block.phis { + resolve_identifier(phi.place.identifier, identifiers, types, unifier); + } + + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + + // Instruction lvalue + resolve_identifier(instr.lvalue.identifier, identifiers, types, unifier); + + // LValues from instruction values (StoreLocal, StoreContext, DeclareLocal, DeclareContext, Destructure) + apply_instruction_lvalues(&instr.value, identifiers, types, unifier); + + // Operands + apply_instruction_operands(&instr.value, identifiers, types, unifier); + + // Recurse into inner functions + match &instr.value { + InstructionValue::FunctionExpression { + lowered_func: LoweredFunction { func: func_id }, + .. + } + | InstructionValue::ObjectMethod { + lowered_func: LoweredFunction { func: func_id }, + .. + } => { + let inner_func = &functions[func_id.0 as usize]; + // Resolve types for captured context variable places (matching TS + // where eachInstructionValueOperand yields func.context places) + for ctx in &inner_func.context { + resolve_identifier(ctx.identifier, identifiers, types, unifier); + } + apply_function(inner_func, functions, identifiers, types, unifier); + } + _ => {} + } + } + } + + // Resolve return type + resolve_identifier(func.returns.identifier, identifiers, types, unifier); +} + +fn resolve_identifier( + id: IdentifierId, + identifiers: &mut [Identifier], + types: &mut Vec, + unifier: &Unifier, +) { + let type_id = identifiers[id.0 as usize].type_; + let current_type = types[type_id.0 as usize].clone(); + let resolved = unifier.get(¤t_type); + types[type_id.0 as usize] = resolved; +} + +/// Resolve types for instruction lvalues (mirrors TS eachInstructionLValue). +fn apply_instruction_lvalues( + value: &InstructionValue, + identifiers: &mut [Identifier], + types: &mut Vec, + unifier: &Unifier, +) { + match value { + InstructionValue::StoreLocal { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } => { + resolve_identifier(lvalue.place.identifier, identifiers, types, unifier); + } + InstructionValue::DeclareLocal { lvalue, .. } + | InstructionValue::DeclareContext { lvalue, .. } => { + resolve_identifier(lvalue.place.identifier, identifiers, types, unifier); + } + InstructionValue::Destructure { lvalue, .. } => match &lvalue.pattern { + Pattern::Array(array_pattern) => { + for item in &array_pattern.items { + match item { + ArrayPatternElement::Place(place) => { + resolve_identifier(place.identifier, identifiers, types, unifier); + } + ArrayPatternElement::Spread(spread) => { + resolve_identifier( + spread.place.identifier, + identifiers, + types, + unifier, + ); + } + ArrayPatternElement::Hole => {} + } + } + } + Pattern::Object(object_pattern) => { + for prop in &object_pattern.properties { + match prop { + ObjectPropertyOrSpread::Property(obj_prop) => { + resolve_identifier( + obj_prop.place.identifier, + identifiers, + types, + unifier, + ); + } + ObjectPropertyOrSpread::Spread(spread) => { + resolve_identifier( + spread.place.identifier, + identifiers, + types, + unifier, + ); + } + } + } + } + }, + _ => {} + } +} + +/// Resolve types for instruction operands (mirrors TS eachInstructionOperand). +fn apply_instruction_operands( + value: &InstructionValue, + identifiers: &mut [Identifier], + types: &mut Vec, + unifier: &Unifier, +) { + match value { + InstructionValue::LoadLocal { place, .. } | InstructionValue::LoadContext { place, .. } => { + resolve_identifier(place.identifier, identifiers, types, unifier); + } + InstructionValue::StoreLocal { value: val, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::StoreContext { value: val, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::StoreGlobal { value: val, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::Destructure { value: val, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::BinaryExpression { left, right, .. } => { + resolve_identifier(left.identifier, identifiers, types, unifier); + resolve_identifier(right.identifier, identifiers, types, unifier); + } + InstructionValue::UnaryExpression { value: val, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::TypeCastExpression { value: val, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::CallExpression { callee, args, .. } => { + resolve_identifier(callee.identifier, identifiers, types, unifier); + for arg in args { + match arg { + crate::react_compiler_hir::PlaceOrSpread::Place(p) => { + resolve_identifier(p.identifier, identifiers, types, unifier); + } + crate::react_compiler_hir::PlaceOrSpread::Spread(s) => { + resolve_identifier(s.place.identifier, identifiers, types, unifier); + } + } + } + } + InstructionValue::MethodCall { receiver, property, args, .. } => { + resolve_identifier(receiver.identifier, identifiers, types, unifier); + resolve_identifier(property.identifier, identifiers, types, unifier); + for arg in args { + match arg { + crate::react_compiler_hir::PlaceOrSpread::Place(p) => { + resolve_identifier(p.identifier, identifiers, types, unifier); + } + crate::react_compiler_hir::PlaceOrSpread::Spread(s) => { + resolve_identifier(s.place.identifier, identifiers, types, unifier); + } + } + } + } + InstructionValue::NewExpression { callee, args, .. } => { + resolve_identifier(callee.identifier, identifiers, types, unifier); + for arg in args { + match arg { + crate::react_compiler_hir::PlaceOrSpread::Place(p) => { + resolve_identifier(p.identifier, identifiers, types, unifier); + } + crate::react_compiler_hir::PlaceOrSpread::Spread(s) => { + resolve_identifier(s.place.identifier, identifiers, types, unifier); + } + } + } + } + InstructionValue::TaggedTemplateExpression { tag, subexprs, .. } => { + resolve_identifier(tag.identifier, identifiers, types, unifier); + for sub in subexprs { + resolve_identifier(sub.identifier, identifiers, types, unifier); + } + } + InstructionValue::PropertyLoad { object, .. } => { + resolve_identifier(object.identifier, identifiers, types, unifier); + } + InstructionValue::PropertyStore { object, value: val, .. } => { + resolve_identifier(object.identifier, identifiers, types, unifier); + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::PropertyDelete { object, .. } => { + resolve_identifier(object.identifier, identifiers, types, unifier); + } + InstructionValue::ComputedLoad { object, property, .. } => { + resolve_identifier(object.identifier, identifiers, types, unifier); + resolve_identifier(property.identifier, identifiers, types, unifier); + } + InstructionValue::ComputedStore { object, property, value: val, .. } => { + resolve_identifier(object.identifier, identifiers, types, unifier); + resolve_identifier(property.identifier, identifiers, types, unifier); + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::ComputedDelete { object, property, .. } => { + resolve_identifier(object.identifier, identifiers, types, unifier); + resolve_identifier(property.identifier, identifiers, types, unifier); + } + InstructionValue::ObjectExpression { properties, .. } => { + for prop in properties { + match prop { + ObjectPropertyOrSpread::Property(obj_prop) => { + resolve_identifier(obj_prop.place.identifier, identifiers, types, unifier); + if let ObjectPropertyKey::Computed { name } = &obj_prop.key { + resolve_identifier(name.identifier, identifiers, types, unifier); + } + } + ObjectPropertyOrSpread::Spread(spread) => { + resolve_identifier(spread.place.identifier, identifiers, types, unifier); + } + } + } + } + InstructionValue::ArrayExpression { elements, .. } => { + for elem in elements { + match elem { + crate::react_compiler_hir::ArrayElement::Place(p) => { + resolve_identifier(p.identifier, identifiers, types, unifier); + } + crate::react_compiler_hir::ArrayElement::Spread(s) => { + resolve_identifier(s.place.identifier, identifiers, types, unifier); + } + crate::react_compiler_hir::ArrayElement::Hole => {} + } + } + } + InstructionValue::JsxExpression { tag, props, children, .. } => { + if let crate::react_compiler_hir::JsxTag::Place(p) = tag { + resolve_identifier(p.identifier, identifiers, types, unifier); + } + for attr in props { + match attr { + JsxAttribute::Attribute { place, .. } => { + resolve_identifier(place.identifier, identifiers, types, unifier); + } + JsxAttribute::SpreadAttribute { argument } => { + resolve_identifier(argument.identifier, identifiers, types, unifier); + } + } + } + if let Some(children) = children { + for child in children { + resolve_identifier(child.identifier, identifiers, types, unifier); + } + } + } + InstructionValue::JsxFragment { children, .. } => { + for child in children { + resolve_identifier(child.identifier, identifiers, types, unifier); + } + } + InstructionValue::FunctionExpression { .. } | InstructionValue::ObjectMethod { .. } => { + // Inner functions are handled separately via recursion in apply_function + } + InstructionValue::TemplateLiteral { subexprs, .. } => { + for sub in subexprs { + resolve_identifier(sub.identifier, identifiers, types, unifier); + } + } + InstructionValue::PrefixUpdate { value: val, lvalue, .. } + | InstructionValue::PostfixUpdate { value: val, lvalue, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + resolve_identifier(lvalue.identifier, identifiers, types, unifier); + } + InstructionValue::Await { value: val, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::GetIterator { collection, .. } => { + resolve_identifier(collection.identifier, identifiers, types, unifier); + } + InstructionValue::IteratorNext { iterator, collection, .. } => { + resolve_identifier(iterator.identifier, identifiers, types, unifier); + resolve_identifier(collection.identifier, identifiers, types, unifier); + } + InstructionValue::NextPropertyOf { value: val, .. } => { + resolve_identifier(val.identifier, identifiers, types, unifier); + } + InstructionValue::FinishMemoize { decl, .. } => { + resolve_identifier(decl.identifier, identifiers, types, unifier); + } + InstructionValue::StartMemoize { deps, .. } => { + // Resolve types for deps with NamedLocal kind (matching TS + // eachInstructionOperand which yields dep.root.value for NamedLocal deps) + if let Some(deps) = deps { + for dep in deps { + if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &dep.root { + resolve_identifier(value.identifier, identifiers, types, unifier); + } + } + } + } + InstructionValue::Primitive { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::LoadGlobal { .. } + | InstructionValue::DeclareLocal { .. } + | InstructionValue::DeclareContext { .. } + | InstructionValue::RegExpLiteral { .. } + | InstructionValue::MetaProperty { .. } + | InstructionValue::Debugger { .. } + | InstructionValue::PassthroughStatement { .. } => { + // No operand places + } + } +} + +// ============================================================================= +// Unifier +// ============================================================================= + +struct Unifier { + substitutions: FxHashMap, + enable_treat_ref_like_identifiers_as_refs: bool, + enable_treat_set_identifiers_as_state_setters: bool, + custom_hook_type: Option, +} + +impl Unifier { + fn new( + enable_treat_ref_like_identifiers_as_refs: bool, + custom_hook_type: Option, + enable_treat_set_identifiers_as_state_setters: bool, + ) -> Self { + Unifier { + substitutions: FxHashMap::default(), + enable_treat_ref_like_identifiers_as_refs, + enable_treat_set_identifiers_as_state_setters, + custom_hook_type, + } + } + + fn unify( + &mut self, + t_a: Type, + t_b: Type, + shapes: &ShapeRegistry, + ) -> Result<(), CompilerDiagnostic> { + self.unify_impl(t_a, t_b, shapes) + } + + fn unify_impl( + &mut self, + t_a: Type, + t_b: Type, + shapes: &ShapeRegistry, + ) -> Result<(), CompilerDiagnostic> { + // Handle Property in the RHS position + if let Type::Property { ref object_type, ref object_name, ref property_name } = t_b { + // Check enableTreatRefLikeIdentifiersAsRefs + if self.enable_treat_ref_like_identifiers_as_refs + && is_ref_like_name(object_name, property_name) + { + self.unify_impl( + *object_type.clone(), + Type::Object { shape_id: Some(BUILT_IN_USE_REF_ID.to_string()) }, + shapes, + )?; + self.unify_impl( + t_a, + Type::Object { shape_id: Some(BUILT_IN_REF_VALUE_ID.to_string()) }, + shapes, + )?; + return Ok(()); + } + + // Resolve property type via the shapes registry + let resolved_object = self.get(object_type); + let property_type = resolve_property_type( + shapes, + &resolved_object, + property_name, + self.custom_hook_type.as_ref(), + ); + if let Some(property_type) = property_type { + self.unify_impl(t_a, property_type, shapes)?; + } + return Ok(()); + } + + if type_equals(&t_a, &t_b) { + return Ok(()); + } + + if let Type::TypeVar { .. } = &t_a { + self.bind_variable_to(t_a, t_b, shapes)?; + return Ok(()); + } + + if let Type::TypeVar { .. } = &t_b { + self.bind_variable_to(t_b, t_a, shapes)?; + return Ok(()); + } + + if let ( + Type::Function { return_type: ret_a, is_constructor: con_a, .. }, + Type::Function { return_type: ret_b, is_constructor: con_b, .. }, + ) = (&t_a, &t_b) + { + if con_a == con_b { + self.unify_impl(*ret_a.clone(), *ret_b.clone(), shapes)?; + } + } + Ok(()) + } + + fn bind_variable_to( + &mut self, + v: Type, + ty: Type, + shapes: &ShapeRegistry, + ) -> Result<(), CompilerDiagnostic> { + let v_id = match &v { + Type::TypeVar { id } => *id, + _ => return Ok(()), + }; + + if let Type::Poly = &ty { + // Ignore PolyType + return Ok(()); + } + + if let Some(existing) = self.substitutions.get(&v_id).cloned() { + self.unify_impl(existing, ty, shapes)?; + return Ok(()); + } + + if let Type::TypeVar { id: ty_id } = &ty { + if let Some(existing) = self.substitutions.get(ty_id).cloned() { + self.unify_impl(v, existing, shapes)?; + return Ok(()); + } + } + + if let Type::Phi { ref operands } = ty { + if operands.is_empty() { + return Err(CompilerDiagnostic { + category: ErrorCategory::Invariant, + reason: "there should be at least one operand".to_string(), + description: None, + details: vec![], + suggestions: None, + }); + } + + let mut candidate_type: Option = None; + for operand in operands { + let resolved = self.get(operand); + match &candidate_type { + None => { + candidate_type = Some(resolved); + } + Some(candidate) => { + if !type_equals(&resolved, candidate) { + let union_type = try_union_types(&resolved, candidate); + if let Some(union) = union_type { + candidate_type = Some(union); + } else { + candidate_type = None; + break; + } + } + // else same type, continue + } + } + } + + if let Some(candidate) = candidate_type { + self.unify_impl(v, candidate, shapes)?; + return Ok(()); + } + } + + if self.occurs_check(&v, &ty) { + let resolved_type = self.try_resolve_type(&v, &ty); + if let Some(resolved) = resolved_type { + self.substitutions.insert(v_id, resolved); + return Ok(()); + } + return Err(CompilerDiagnostic { + category: ErrorCategory::Invariant, + reason: "cycle detected".to_string(), + description: None, + details: vec![], + suggestions: None, + }); + } + + self.substitutions.insert(v_id, ty); + Ok(()) + } + + fn try_resolve_type(&mut self, v: &Type, ty: &Type) -> Option { + match ty { + Type::Phi { operands } => { + let mut new_operands = Vec::new(); + for operand in operands { + if let Type::TypeVar { id } = operand { + if let Type::TypeVar { id: v_id } = v { + if id == v_id { + continue; // skip self-reference + } + } + } + let resolved = self.try_resolve_type(v, operand)?; + new_operands.push(resolved); + } + Some(Type::Phi { operands: new_operands }) + } + Type::TypeVar { id } => { + let substitution = self.get(ty); + if !type_equals(&substitution, ty) { + let resolved = self.try_resolve_type(v, &substitution)?; + self.substitutions.insert(*id, resolved.clone()); + Some(resolved) + } else { + Some(ty.clone()) + } + } + Type::Property { object_type, object_name, property_name } => { + let resolved_obj = self.get(object_type); + let object_type = self.try_resolve_type(v, &resolved_obj)?; + Some(Type::Property { + object_type: Box::new(object_type), + object_name: object_name.clone(), + property_name: property_name.clone(), + }) + } + Type::Function { shape_id, return_type, is_constructor } => { + let resolved_ret = self.get(return_type); + let return_type = self.try_resolve_type(v, &resolved_ret)?; + Some(Type::Function { + shape_id: shape_id.clone(), + return_type: Box::new(return_type), + is_constructor: *is_constructor, + }) + } + Type::ObjectMethod | Type::Object { .. } | Type::Primitive | Type::Poly => { + Some(ty.clone()) + } + } + } + + fn occurs_check(&self, v: &Type, ty: &Type) -> bool { + if type_equals(v, ty) { + return true; + } + + if let Type::TypeVar { id } = ty { + if let Some(sub) = self.substitutions.get(id) { + return self.occurs_check(v, sub); + } + } + + if let Type::Phi { operands } = ty { + return operands.iter().any(|o| self.occurs_check(v, o)); + } + + if let Type::Function { return_type, .. } = ty { + return self.occurs_check(v, return_type); + } + + false + } + + fn get(&self, ty: &Type) -> Type { + if let Type::TypeVar { id } = ty { + if let Some(sub) = self.substitutions.get(id) { + return self.get(sub); + } + } + + if let Type::Phi { operands } = ty { + return Type::Phi { operands: operands.iter().map(|o| self.get(o)).collect() }; + } + + if let Type::Function { is_constructor, shape_id, return_type } = ty { + return Type::Function { + is_constructor: *is_constructor, + shape_id: shape_id.clone(), + return_type: Box::new(self.get(return_type)), + }; + } + + ty.clone() + } +} + +// ============================================================================= +// Union types helper +// ============================================================================= + +fn try_union_types(ty1: &Type, ty2: &Type) -> Option { + let (readonly_type, other_type) = if matches!(ty1, Type::Object { shape_id } if shape_id.as_deref() == Some(BUILT_IN_MIXED_READONLY_ID)) + { + (ty1, ty2) + } else if matches!(ty2, Type::Object { shape_id } if shape_id.as_deref() == Some(BUILT_IN_MIXED_READONLY_ID)) + { + (ty2, ty1) + } else { + return None; + }; + + if matches!(other_type, Type::Primitive) { + // Union(Primitive | MixedReadonly) = MixedReadonly + return Some(readonly_type.clone()); + } else if matches!(other_type, Type::Object { shape_id } if shape_id.as_deref() == Some(BUILT_IN_ARRAY_ID)) + { + // Union(Array | MixedReadonly) = Array + return Some(other_type.clone()); + } + + None +} diff --git a/crates/oxc_react_compiler/src/react_compiler_typeinference/mod.rs b/crates/oxc_react_compiler/src/react_compiler_typeinference/mod.rs new file mode 100644 index 0000000000000..d4fe866033745 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_typeinference/mod.rs @@ -0,0 +1,3 @@ +pub mod infer_types; + +pub use infer_types::infer_types; diff --git a/crates/oxc_react_compiler/src/react_compiler_utils/disjoint_set.rs b/crates/oxc_react_compiler/src/react_compiler_utils/disjoint_set.rs new file mode 100644 index 0000000000000..c5f9f3d262e7c --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_utils/disjoint_set.rs @@ -0,0 +1,146 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! A generic disjoint-set (union-find) data structure. +//! +//! Ported from TypeScript `src/Utils/DisjointSet.ts`. + +use rustc_hash::FxHashSet; +use std::hash::Hash; + +use crate::react_compiler_utils::FxIndexMap; + +/// A Union-Find data structure for grouping items into disjoint sets. +/// +/// Corresponds to TS `DisjointSet` in `src/Utils/DisjointSet.ts`. +/// Uses `FxIndexMap` to preserve insertion order (matching TS `Map` behavior). +pub struct DisjointSet { + entries: FxIndexMap, +} + +impl DisjointSet { + pub fn new() -> Self { + DisjointSet { entries: FxIndexMap::default() } + } + + /// Updates the graph to reflect that the given items form a set, + /// linking any previous sets that the items were part of into a single set. + /// + /// Corresponds to TS `union(items: Array): void`. + pub fn union(&mut self, items: &[K]) { + if items.is_empty() { + return; + } + let root = self.find(items[0]); + for &item in &items[1..] { + let item_root = self.find(item); + if item_root != root { + self.entries.insert(item_root, root); + } + } + } + + /// Find the root of the set containing `item`, with path compression. + /// If `item` is not in the set, it is inserted as its own root. + /// + /// Note: callers that need null/None semantics for missing items should + /// use `find_opt()` instead. + pub fn find(&mut self, item: K) -> K { + let parent = match self.entries.get(&item) { + Some(&p) => p, + None => { + self.entries.insert(item, item); + return item; + } + }; + if parent == item { + return item; + } + let root = self.find(parent); + self.entries.insert(item, root); + root + } + + /// Find the root of the set containing `item`, returning `None` if the item + /// was never added to the set. + /// + /// Corresponds to TS `find(item: T): T | null`. + pub fn find_opt(&mut self, item: K) -> Option { + if !self.entries.contains_key(&item) { + return None; + } + Some(self.find(item)) + } + + /// Returns true if the item is present in the set. + /// + /// Corresponds to TS `has(item: T): boolean`. + pub fn has(&self, item: K) -> bool { + self.entries.contains_key(&item) + } + + /// Forces the set into canonical form (all items pointing directly to their + /// root) and returns a map of items to their roots. + /// + /// Corresponds to TS `canonicalize(): Map`. + pub fn canonicalize(&mut self) -> FxIndexMap { + let mut result = FxIndexMap::default(); + let keys: Vec = self.entries.keys().copied().collect(); + for item in keys { + let root = self.find(item); + result.insert(item, root); + } + result + } + + /// Calls the provided callback once for each item in the disjoint set, + /// passing the item and the group root to which it belongs. + /// + /// Corresponds to TS `forEach(fn: (item: T, group: T) => void): void`. + pub fn for_each(&mut self, mut f: F) + where + F: FnMut(K, K), + { + let keys: Vec = self.entries.keys().copied().collect(); + for item in keys { + let group = self.find(item); + f(item, group); + } + } + + /// Groups all items by their root and returns the groups as a list of sets. + /// + /// Corresponds to TS `buildSets(): Array>`. + pub fn build_sets(&mut self) -> Vec> { + let mut group_to_index: FxIndexMap = FxIndexMap::default(); + let mut sets: Vec> = Vec::new(); + let keys: Vec = self.entries.keys().copied().collect(); + for item in keys { + let group = self.find(item); + let idx = match group_to_index.get(&group) { + Some(&idx) => idx, + None => { + let idx = sets.len(); + group_to_index.insert(group, idx); + sets.push(FxHashSet::default()); + idx + } + }; + sets[idx].insert(item); + } + sets + } + + /// Returns the number of items in the set. + /// + /// Corresponds to TS `get size(): number`. + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_utils/mod.rs b/crates/oxc_react_compiler/src/react_compiler_utils/mod.rs new file mode 100644 index 0000000000000..1dc325f63eac9 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_utils/mod.rs @@ -0,0 +1,8 @@ +pub mod disjoint_set; + +pub use disjoint_set::DisjointSet; + +/// `IndexMap` keyed with the fast `FxHasher` (rustc-hash) instead of the default SipHash. +pub type FxIndexMap = indexmap::IndexMap; +/// `IndexSet` keyed with the fast `FxHasher` (rustc-hash) instead of the default SipHash. +pub type FxIndexSet = indexmap::IndexSet; diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/mod.rs b/crates/oxc_react_compiler/src/react_compiler_validation/mod.rs new file mode 100644 index 0000000000000..f6afec6b69d19 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/mod.rs @@ -0,0 +1,32 @@ +pub mod validate_context_variable_lvalues; +pub mod validate_exhaustive_dependencies; +pub mod validate_hooks_usage; +pub mod validate_locals_not_reassigned_after_render; +pub mod validate_no_capitalized_calls; +pub mod validate_no_derived_computations_in_effects; +pub mod validate_no_freezing_known_mutable_functions; +pub mod validate_no_jsx_in_try_statement; +pub mod validate_no_ref_access_in_render; +pub mod validate_no_set_state_in_effects; +pub mod validate_no_set_state_in_render; +pub mod validate_preserved_manual_memoization; +pub mod validate_static_components; +pub mod validate_use_memo; + +pub use validate_context_variable_lvalues::{ + validate_context_variable_lvalues, validate_context_variable_lvalues_with_errors, +}; +pub use validate_exhaustive_dependencies::validate_exhaustive_dependencies; +pub use validate_hooks_usage::validate_hooks_usage; +pub use validate_locals_not_reassigned_after_render::validate_locals_not_reassigned_after_render; +pub use validate_no_capitalized_calls::validate_no_capitalized_calls; +pub use validate_no_derived_computations_in_effects::validate_no_derived_computations_in_effects; +pub use validate_no_derived_computations_in_effects::validate_no_derived_computations_in_effects_exp; +pub use validate_no_freezing_known_mutable_functions::validate_no_freezing_known_mutable_functions; +pub use validate_no_jsx_in_try_statement::validate_no_jsx_in_try_statement; +pub use validate_no_ref_access_in_render::validate_no_ref_access_in_render; +pub use validate_no_set_state_in_effects::validate_no_set_state_in_effects; +pub use validate_no_set_state_in_render::validate_no_set_state_in_render; +pub use validate_preserved_manual_memoization::validate_preserved_manual_memoization; +pub use validate_static_components::validate_static_components; +pub use validate_use_memo::validate_use_memo; diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_context_variable_lvalues.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_context_variable_lvalues.rs new file mode 100644 index 0000000000000..752056f98067b --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_context_variable_lvalues.rs @@ -0,0 +1,213 @@ +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::{each_instruction_value_lvalue, each_pattern_operand}; +use crate::react_compiler_hir::{ + FunctionId, HirFunction, Identifier, IdentifierId, InstructionValue, Place, +}; + +/// Variable reference kind: local, context, or destructure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum VarRefKind { + Local, + Context, + Destructure, +} + +impl std::fmt::Display for VarRefKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VarRefKind::Local => write!(f, "local"), + VarRefKind::Context => write!(f, "context"), + VarRefKind::Destructure => write!(f, "destructure"), + } + } +} + +type IdentifierKinds = FxHashMap; + +/// Validates that context variable lvalues are used consistently. +/// +/// Port of ValidateContextVariableLValues.ts +pub fn validate_context_variable_lvalues( + func: &HirFunction, + env: &mut Environment, +) -> Result<(), CompilerDiagnostic> { + validate_context_variable_lvalues_with_errors( + func, + &env.functions, + &env.identifiers, + &mut env.errors, + ) +} + +/// Like [`validate_context_variable_lvalues`], but writes diagnostics into the +/// provided `errors` instead of `env.errors`. Useful when the caller wants to +/// discard the diagnostics (e.g. when lowering is incomplete). +pub fn validate_context_variable_lvalues_with_errors( + func: &HirFunction, + functions: &[HirFunction], + identifiers: &[Identifier], + errors: &mut CompilerError, +) -> Result<(), CompilerDiagnostic> { + let mut identifier_kinds: IdentifierKinds = FxHashMap::default(); + validate_context_variable_lvalues_impl( + func, + &mut identifier_kinds, + functions, + identifiers, + errors, + ) +} + +fn validate_context_variable_lvalues_impl( + func: &HirFunction, + identifier_kinds: &mut IdentifierKinds, + functions: &[HirFunction], + identifiers: &[Identifier], + errors: &mut CompilerError, +) -> Result<(), CompilerDiagnostic> { + let mut inner_function_ids: Vec = Vec::new(); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let value = &instr.value; + + match value { + InstructionValue::DeclareContext { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } => { + visit( + identifier_kinds, + &lvalue.place, + VarRefKind::Context, + identifiers, + errors, + )?; + } + InstructionValue::LoadContext { place, .. } => { + visit(identifier_kinds, place, VarRefKind::Context, identifiers, errors)?; + } + InstructionValue::StoreLocal { lvalue, .. } + | InstructionValue::DeclareLocal { lvalue, .. } => { + visit(identifier_kinds, &lvalue.place, VarRefKind::Local, identifiers, errors)?; + } + InstructionValue::LoadLocal { place, .. } => { + visit(identifier_kinds, place, VarRefKind::Local, identifiers, errors)?; + } + InstructionValue::PostfixUpdate { lvalue, .. } + | InstructionValue::PrefixUpdate { lvalue, .. } => { + visit(identifier_kinds, lvalue, VarRefKind::Local, identifiers, errors)?; + } + InstructionValue::Destructure { lvalue, .. } => { + for place in each_pattern_operand(&lvalue.pattern) { + visit( + identifier_kinds, + &place, + VarRefKind::Destructure, + identifiers, + errors, + )?; + } + } + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + inner_function_ids.push(lowered_func.func); + } + _ => { + for _ in each_instruction_value_lvalue(value) { + errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::Todo, + "ValidateContextVariableLValues: unhandled instruction variant", + None, + ) + .with_detail( + CompilerDiagnosticDetail::Error { + loc: value.loc().copied(), + message: None, + identifier_name: None, + }, + ), + ); + } + } + } + } + } + + // Process inner functions after the block loop to avoid borrow conflicts + for func_id in inner_function_ids { + let inner_func = &functions[func_id.0 as usize]; + validate_context_variable_lvalues_impl( + inner_func, + identifier_kinds, + functions, + identifiers, + errors, + )?; + } + + Ok(()) +} + +/// Format a place like TS `printPlace()`: ` $` +fn format_place(place: &Place, identifiers: &[Identifier]) -> String { + let id = place.identifier; + let ident = &identifiers[id.0 as usize]; + let name = match &ident.name { + Some(n) => n.value().to_string(), + None => String::new(), + }; + format!("{} {}${}", place.effect, name, id.0) +} + +fn visit( + identifiers: &mut IdentifierKinds, + place: &Place, + kind: VarRefKind, + env_identifiers: &[Identifier], + errors: &mut CompilerError, +) -> Result<(), CompilerDiagnostic> { + if let Some((prev_place, prev_kind)) = identifiers.get(&place.identifier) { + let was_context = *prev_kind == VarRefKind::Context; + let is_context = kind == VarRefKind::Context; + if was_context != is_context { + if *prev_kind == VarRefKind::Destructure || kind == VarRefKind::Destructure { + let loc = if kind == VarRefKind::Destructure { place.loc } else { prev_place.loc }; + errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::Todo, + "Support destructuring of context variables", + None, + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc, + message: None, + identifier_name: None, + }), + ); + return Ok(()); + } + let place_str = format_place(place, env_identifiers); + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Expected all references to a variable to be consistently local or context references", + Some(format!( + "Identifier {} is referenced as a {} variable, but was previously referenced as a {} variable", + place_str, kind, prev_kind + )), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: place.loc, + message: Some(format!("this is {}", prev_kind)), + identifier_name: None, + })); + } + } + identifiers.insert(place.identifier, (place.clone(), kind)); + Ok(()) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_exhaustive_dependencies.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_exhaustive_dependencies.rs new file mode 100644 index 0000000000000..a5c1a30bd7b88 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_exhaustive_dependencies.rs @@ -0,0 +1,1613 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerSuggestion, CompilerSuggestionOperation, + ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::environment_config::ExhaustiveEffectDepsMode; +use crate::react_compiler_hir::visitors::{ + each_instruction_value_lvalue, each_instruction_value_operand_with_functions, + each_terminal_operand, +}; +use crate::react_compiler_hir::{ + ArrayElement, BlockId, DependencyPathEntry, HirFunction, Identifier, IdentifierId, + InstructionKind, InstructionValue, ManualMemoDependency, ManualMemoDependencyRoot, + NonLocalBinding, ParamPattern, Place, PlaceOrSpread, PropertyLiteral, Terminal, Type, +}; + +/// Port of ValidateExhaustiveDependencies.ts +/// +/// Validates that existing manual memoization is exhaustive and does not +/// have extraneous dependencies. The goal is to ensure auto-memoization +/// will not substantially change program behavior. +/// +/// Note: takes `&mut HirFunction` (deviating from the read-only validation convention) +/// because it sets `has_invalid_deps` on StartMemoize instructions when validation +/// errors are found, so that ValidatePreservedManualMemoization can skip those blocks. +pub fn validate_exhaustive_dependencies( + func: &mut HirFunction, + env: &mut Environment, +) -> Result<(), CompilerDiagnostic> { + let reactive = collect_reactive_identifiers(func, &env.functions); + let validate_memo = env.config.validate_exhaustive_memoization_dependencies; + let validate_effect = env.config.validate_exhaustive_effect_dependencies.clone(); + + let mut temporaries: FxHashMap = FxHashMap::default(); + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + temporaries.insert( + place.identifier, + Temporary::Local { + identifier: place.identifier, + path: Vec::new(), + context: false, + loc: place.loc, + }, + ); + } + + let mut start_memo: Option = None; + let mut memo_locals: FxHashSet = FxHashSet::default(); + + // Callbacks struct holding the mutable state + let mut callbacks = Callbacks { + start_memo: &mut start_memo, + memo_locals: &mut memo_locals, + validate_memo, + validate_effect: validate_effect.clone(), + reactive: &reactive, + diagnostics: Vec::new(), + invalid_memo_ids: FxHashSet::default(), + }; + + collect_dependencies( + func, + &env.identifiers, + &env.types, + &env.functions, + &mut temporaries, + &mut Some(&mut callbacks), + false, + )?; + + // Set has_invalid_deps on StartMemoize instructions that had validation errors + if !callbacks.invalid_memo_ids.is_empty() { + for instr in func.instructions.iter_mut() { + if let InstructionValue::StartMemoize { manual_memo_id, has_invalid_deps, .. } = + &mut instr.value + { + if callbacks.invalid_memo_ids.contains(manual_memo_id) { + *has_invalid_deps = true; + } + } + } + } + + // Record all diagnostics on the environment + for diagnostic in callbacks.diagnostics { + env.record_diagnostic(diagnostic); + } + Ok(()) +} + +// ============================================================================= +// Internal types +// ============================================================================= + +/// Info extracted from a StartMemoize instruction +struct StartMemoInfo { + manual_memo_id: u32, + deps: Option>, + deps_loc: Option>, + #[allow(dead_code)] + loc: Option, +} + +/// A temporary value tracked during dependency collection +#[derive(Debug, Clone)] +enum Temporary { + Local { + identifier: IdentifierId, + path: Vec, + context: bool, + loc: Option, + }, + Global { + binding: NonLocalBinding, + }, + Aggregate { + dependencies: Vec, + loc: Option, + }, +} + +/// An inferred dependency (Local or Global) +#[derive(Debug, Clone)] +enum InferredDependency { + Local { + identifier: IdentifierId, + path: Vec, + #[allow(dead_code)] + context: bool, + loc: Option, + }, + Global { + binding: NonLocalBinding, + }, +} + +/// Hashable key for deduplicating inferred dependencies in a Set +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum InferredDependencyKey { + Local { identifier: IdentifierId, path_key: String }, + Global { name: String }, +} + +fn dep_to_key(dep: &InferredDependency) -> InferredDependencyKey { + match dep { + InferredDependency::Local { identifier, path, .. } => { + InferredDependencyKey::Local { identifier: *identifier, path_key: path_to_string(path) } + } + InferredDependency::Global { binding } => { + InferredDependencyKey::Global { name: binding.name().to_string() } + } + } +} + +fn path_to_string(path: &[DependencyPathEntry]) -> String { + path.iter() + .map(|p| format!("{}{}", if p.optional { "?." } else { "." }, p.property)) + .collect::>() + .join("") +} + +/// Callbacks for StartMemoize/FinishMemoize/Effect events +struct Callbacks<'a> { + start_memo: &'a mut Option, + #[allow(dead_code)] + memo_locals: &'a mut FxHashSet, + validate_memo: bool, + validate_effect: ExhaustiveEffectDepsMode, + reactive: &'a FxHashSet, + diagnostics: Vec, + /// manual_memo_ids that had validation errors (to set has_invalid_deps) + invalid_memo_ids: FxHashSet, +} + +// ============================================================================= +// Helper: type checking functions +// ============================================================================= + +fn is_effect_event_function_type(ty: &Type) -> bool { + matches!(ty, Type::Function { shape_id: Some(id), .. } if id == "BuiltInEffectEventFunction") +} + +fn is_stable_type(ty: &Type) -> bool { + match ty { + Type::Function { shape_id: Some(id), .. } => matches!( + id.as_str(), + "BuiltInSetState" + | "BuiltInSetActionState" + | "BuiltInDispatch" + | "BuiltInStartTransition" + | "BuiltInSetOptimistic" + ), + Type::Object { shape_id: Some(id) } => matches!(id.as_str(), "BuiltInUseRefId"), + _ => false, + } +} + +fn is_effect_hook(ty: &Type) -> bool { + matches!(ty, Type::Function { shape_id: Some(id), .. } + if id == "BuiltInUseEffectHook" + || id == "BuiltInUseLayoutEffectHook" + || id == "BuiltInUseInsertionEffectHook" + ) +} + +fn is_primitive_type(ty: &Type) -> bool { + matches!(ty, Type::Primitive) +} + +fn is_use_ref_type(ty: &Type) -> bool { + matches!(ty, Type::Object { shape_id: Some(id) } if id == "BuiltInUseRefId") +} + +fn get_identifier_type<'a>( + id: IdentifierId, + identifiers: &'a [Identifier], + types: &'a [Type], +) -> &'a Type { + let ident = &identifiers[id.0 as usize]; + &types[ident.type_.0 as usize] +} + +fn get_identifier_name(id: IdentifierId, identifiers: &[Identifier]) -> Option { + identifiers[id.0 as usize].name.as_ref().map(|n| n.value().to_string()) +} + +// ============================================================================= +// Path helpers (matching TS areEqualPaths, isSubPath, isSubPathIgnoringOptionals) +// ============================================================================= + +fn are_equal_paths(a: &[DependencyPathEntry], b: &[DependencyPathEntry]) -> bool { + a.len() == b.len() + && a.iter() + .zip(b.iter()) + .all(|(ai, bi)| ai.property == bi.property && ai.optional == bi.optional) +} + +fn is_sub_path(subpath: &[DependencyPathEntry], path: &[DependencyPathEntry]) -> bool { + subpath.len() <= path.len() + && subpath + .iter() + .zip(path.iter()) + .all(|(a, b)| a.property == b.property && a.optional == b.optional) +} + +fn is_sub_path_ignoring_optionals( + subpath: &[DependencyPathEntry], + path: &[DependencyPathEntry], +) -> bool { + subpath.len() <= path.len() + && subpath.iter().zip(path.iter()).all(|(a, b)| a.property == b.property) +} + +// ============================================================================= +// Collect reactive identifiers +// ============================================================================= + +fn collect_reactive_identifiers( + func: &HirFunction, + functions: &[HirFunction], +) -> FxHashSet { + let mut reactive = FxHashSet::default(); + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + // Check instruction lvalue + if instr.lvalue.reactive { + reactive.insert(instr.lvalue.identifier); + } + // Check inner lvalues (Destructure patterns, StoreLocal, DeclareLocal, etc.) + // Matches TS eachInstructionLValue which yields both instr.lvalue and + // eachInstructionValueLValue(instr.value) + for lvalue in each_instruction_value_lvalue(&instr.value) { + if lvalue.reactive { + reactive.insert(lvalue.identifier); + } + } + for operand in each_instruction_value_operand_with_functions(&instr.value, functions) { + if operand.reactive { + reactive.insert(operand.identifier); + } + } + } + for operand in each_terminal_operand(&block.terminal) { + if operand.reactive { + reactive.insert(operand.identifier); + } + } + } + reactive +} + +// ============================================================================= +// findOptionalPlaces +// ============================================================================= + +fn find_optional_places(func: &HirFunction) -> FxHashMap { + let mut optionals: FxHashMap = FxHashMap::default(); + let mut visited: FxHashSet = FxHashSet::default(); + + for (_block_id, block) in &func.body.blocks { + if visited.contains(&block.id) { + continue; + } + if let Terminal::Optional { test, fallthrough: optional_fallthrough, optional, .. } = + &block.terminal + { + visited.insert(block.id); + let mut test_block_id = *test; + let mut queue: Vec> = vec![Some(*optional)]; + + 'outer: loop { + let test_block = &func.body.blocks[&test_block_id]; + visited.insert(test_block.id); + match &test_block.terminal { + Terminal::Branch { test: test_place, consequent, fallthrough, .. } => { + let is_optional = queue + .pop() + .expect("Expected an optional value for each optional test condition"); + if let Some(opt) = is_optional { + optionals.insert(test_place.identifier, opt); + } + if fallthrough == optional_fallthrough { + // Found the end of the optional chain + let consequent_block = &func.body.blocks[consequent]; + if let Some(last_id) = consequent_block.instructions.last() { + let last_instr = &func.instructions[last_id.0 as usize]; + if let InstructionValue::StoreLocal { value, .. } = + &last_instr.value + { + if let Some(opt) = is_optional { + optionals.insert(value.identifier, opt); + } + } + } + break 'outer; + } else { + test_block_id = *fallthrough; + } + } + Terminal::Optional { optional: opt, test: inner_test, .. } => { + queue.push(Some(*opt)); + test_block_id = *inner_test; + } + Terminal::Logical { test: inner_test, .. } + | Terminal::Ternary { test: inner_test, .. } => { + queue.push(None); + test_block_id = *inner_test; + } + Terminal::Sequence { block: seq_block, .. } => { + test_block_id = *seq_block; + } + Terminal::MaybeThrow { continuation, .. } => { + test_block_id = *continuation; + } + _ => { + // Unexpected terminal in optional — skip rather than panic + break 'outer; + } + } + } + // TS asserts queue.length === 0 here, but we skip the assertion + // to avoid panicking on edge cases. + } + } + + optionals +} + +// ============================================================================= +// Dependency collection +// ============================================================================= + +fn add_dependency( + dep: &Temporary, + dependencies: &mut Vec, + dep_keys: &mut FxHashSet, + locals: &FxHashSet, +) { + match dep { + Temporary::Aggregate { dependencies: agg_deps, .. } => { + for d in agg_deps { + add_dependency_inferred(d, dependencies, dep_keys, locals); + } + } + Temporary::Global { binding } => { + let inferred = InferredDependency::Global { binding: binding.clone() }; + let key = dep_to_key(&inferred); + if dep_keys.insert(key) { + dependencies.push(inferred); + } + } + Temporary::Local { identifier, path, context, loc } => { + if !locals.contains(identifier) { + let inferred = InferredDependency::Local { + identifier: *identifier, + path: path.clone(), + context: *context, + loc: *loc, + }; + let key = dep_to_key(&inferred); + if dep_keys.insert(key) { + dependencies.push(inferred); + } + } + } + } +} + +fn add_dependency_inferred( + dep: &InferredDependency, + dependencies: &mut Vec, + dep_keys: &mut FxHashSet, + locals: &FxHashSet, +) { + match dep { + InferredDependency::Global { .. } => { + let key = dep_to_key(dep); + if dep_keys.insert(key) { + dependencies.push(dep.clone()); + } + } + InferredDependency::Local { identifier, .. } => { + if !locals.contains(identifier) { + let key = dep_to_key(dep); + if dep_keys.insert(key) { + dependencies.push(dep.clone()); + } + } + } + } +} + +fn visit_candidate_dependency( + place: &Place, + temporaries: &FxHashMap, + dependencies: &mut Vec, + dep_keys: &mut FxHashSet, + locals: &FxHashSet, +) { + if let Some(dep) = temporaries.get(&place.identifier) { + add_dependency(dep, dependencies, dep_keys, locals); + } +} + +fn collect_dependencies( + func: &HirFunction, + identifiers: &[Identifier], + types: &[Type], + functions: &[HirFunction], + temporaries: &mut FxHashMap, + callbacks: &mut Option<&mut Callbacks<'_>>, + is_function_expression: bool, +) -> Result { + let optionals = find_optional_places(func); + let mut locals: FxHashSet = FxHashSet::default(); + + if is_function_expression { + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + locals.insert(place.identifier); + } + } + + let mut dependencies: Vec = Vec::new(); + let mut dep_keys: FxHashSet = FxHashSet::default(); + + // Saved state for when we're inside a memo block (StartMemoize..FinishMemoize). + // In TS, `dependencies` and `locals` are shared by reference between the main + // collection loop and the callbacks — StartMemoize clears them, FinishMemoize + // reads and clears them. We simulate this by saving/restoring. + let mut saved_dependencies: Option> = None; + let mut saved_dep_keys: Option> = None; + let mut saved_locals: Option> = None; + + for (_block_id, block) in &func.body.blocks { + // Process phis + for phi in &block.phis { + let mut deps: Vec = Vec::new(); + for (_pred_id, operand) in &phi.operands { + if let Some(dep) = temporaries.get(&operand.identifier) { + match dep { + Temporary::Aggregate { dependencies: agg, .. } => { + deps.extend(agg.iter().cloned()); + } + Temporary::Local { identifier, path, context, loc } => { + deps.push(InferredDependency::Local { + identifier: *identifier, + path: path.clone(), + context: *context, + loc: *loc, + }); + } + Temporary::Global { binding } => { + deps.push(InferredDependency::Global { binding: binding.clone() }); + } + } + } + } + if deps.is_empty() { + continue; + } else if deps.len() == 1 { + let dep = &deps[0]; + match dep { + InferredDependency::Local { identifier, path, context, loc } => { + temporaries.insert( + phi.place.identifier, + Temporary::Local { + identifier: *identifier, + path: path.clone(), + context: *context, + loc: *loc, + }, + ); + } + InferredDependency::Global { binding } => { + temporaries.insert( + phi.place.identifier, + Temporary::Global { binding: binding.clone() }, + ); + } + } + } else { + temporaries.insert( + phi.place.identifier, + Temporary::Aggregate { dependencies: deps, loc: None }, + ); + } + } + + // Process instructions + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + + match &instr.value { + InstructionValue::LoadGlobal { binding, .. } => { + temporaries.insert(lvalue_id, Temporary::Global { binding: binding.clone() }); + } + InstructionValue::LoadContext { place, .. } + | InstructionValue::LoadLocal { place, .. } => { + if let Some(temp) = temporaries.get(&place.identifier).cloned() { + match &temp { + Temporary::Local { .. } => { + // Update loc to the load site + let mut updated = temp.clone(); + if let Temporary::Local { loc, .. } = &mut updated { + *loc = place.loc; + } + temporaries.insert(lvalue_id, updated); + } + _ => { + temporaries.insert(lvalue_id, temp); + } + } + if locals.contains(&place.identifier) { + locals.insert(lvalue_id); + } + } + } + InstructionValue::DeclareLocal { lvalue: decl_lv, .. } => { + temporaries.insert( + decl_lv.place.identifier, + Temporary::Local { + identifier: decl_lv.place.identifier, + path: Vec::new(), + context: false, + loc: decl_lv.place.loc, + }, + ); + locals.insert(decl_lv.place.identifier); + } + InstructionValue::StoreLocal { lvalue: store_lv, value: store_val, .. } => { + let has_name = identifiers[store_lv.place.identifier.0 as usize].name.is_some(); + if !has_name { + // Unnamed: propagate temporary + if let Some(temp) = temporaries.get(&store_val.identifier).cloned() { + temporaries.insert(store_lv.place.identifier, temp); + } + } else { + // Named: visit the value and create a new local + visit_candidate_dependency( + store_val, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + if store_lv.kind != InstructionKind::Reassign { + temporaries.insert( + store_lv.place.identifier, + Temporary::Local { + identifier: store_lv.place.identifier, + path: Vec::new(), + context: false, + loc: store_lv.place.loc, + }, + ); + locals.insert(store_lv.place.identifier); + } + } + } + InstructionValue::DeclareContext { lvalue: decl_lv, .. } => { + temporaries.insert( + decl_lv.place.identifier, + Temporary::Local { + identifier: decl_lv.place.identifier, + path: Vec::new(), + context: true, + loc: decl_lv.place.loc, + }, + ); + } + InstructionValue::StoreContext { lvalue: store_lv, value: store_val, .. } => { + visit_candidate_dependency( + store_val, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + if store_lv.kind != InstructionKind::Reassign { + temporaries.insert( + store_lv.place.identifier, + Temporary::Local { + identifier: store_lv.place.identifier, + path: Vec::new(), + context: true, + loc: store_lv.place.loc, + }, + ); + locals.insert(store_lv.place.identifier); + } + } + InstructionValue::Destructure { value: destr_val, lvalue: destr_lv, .. } => { + visit_candidate_dependency( + destr_val, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + if destr_lv.kind != InstructionKind::Reassign { + for lv_place in each_instruction_value_lvalue(&instr.value) { + temporaries.insert( + lv_place.identifier, + Temporary::Local { + identifier: lv_place.identifier, + path: Vec::new(), + context: false, + loc: lv_place.loc, + }, + ); + locals.insert(lv_place.identifier); + } + } + } + InstructionValue::PropertyLoad { object, property, .. } => { + // Number properties or ref.current: visit the object directly + let is_numeric = matches!(property, PropertyLiteral::Number(_)); + let is_ref_current = + is_use_ref_type(get_identifier_type(object.identifier, identifiers, types)) + && *property == PropertyLiteral::String("current".to_string()); + + if is_numeric || is_ref_current { + visit_candidate_dependency( + object, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + } else { + // Extend path + let obj_temp = temporaries.get(&object.identifier).cloned(); + if let Some(Temporary::Local { identifier, path, context, .. }) = obj_temp { + let optional = + optionals.get(&object.identifier).copied().unwrap_or(false); + let mut new_path = path.clone(); + new_path.push(DependencyPathEntry { + optional, + property: property.clone(), + loc: instr.value.loc().copied(), + }); + temporaries.insert( + lvalue_id, + Temporary::Local { + identifier, + path: new_path, + context, + loc: instr.value.loc().copied(), + }, + ); + } + } + } + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let inner_func = &functions[lowered_func.func.0 as usize]; + let function_deps = collect_dependencies( + inner_func, + identifiers, + types, + functions, + temporaries, + &mut None, + true, + )?; + temporaries.insert(lvalue_id, function_deps.clone()); + add_dependency(&function_deps, &mut dependencies, &mut dep_keys, &locals); + } + InstructionValue::StartMemoize { manual_memo_id, deps, deps_loc, loc, .. } => { + if let Some(cb) = callbacks.as_mut() { + // onStartMemoize — mirrors TS behavior of clearing dependencies and locals + *cb.start_memo = Some(StartMemoInfo { + manual_memo_id: *manual_memo_id, + deps: deps.clone(), + deps_loc: *deps_loc, + loc: *loc, + }); + // Save current state and clear, matching TS which clears the shared + // dependencies/locals sets on StartMemoize + saved_dependencies = Some(std::mem::take(&mut dependencies)); + saved_dep_keys = Some(std::mem::take(&mut dep_keys)); + saved_locals = Some(std::mem::take(&mut locals)); + } + } + InstructionValue::FinishMemoize { manual_memo_id, decl, .. } => { + if let Some(cb) = callbacks.as_mut() { + // onFinishMemoize — mirrors TS behavior + let sm = cb.start_memo.take(); + if let Some(sm) = sm { + assert_eq!( + sm.manual_memo_id, *manual_memo_id, + "Found FinishMemoize without corresponding StartMemoize" + ); + + if cb.validate_memo { + // Visit the decl to add it as a dependency candidate + // (matches TS: visitCandidateDependency(value.decl, ...)) + visit_candidate_dependency( + decl, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + + // Use ALL dependencies collected since StartMemoize cleared the set. + // This matches TS: `const inferred = Array.from(dependencies)` + let inferred: Vec = dependencies.clone(); + + let diagnostic = validate_dependencies( + inferred, + &sm.deps.unwrap_or_default(), + cb.reactive, + sm.deps_loc.unwrap_or(None), + ErrorCategory::MemoDependencies, + "all", + identifiers, + types, + )?; + if let Some(diag) = diagnostic { + cb.diagnostics.push(diag); + cb.invalid_memo_ids.insert(sm.manual_memo_id); + } + } + + // Restore saved state (matching TS: dependencies.clear(), locals.clear()) + // We restore instead of just clearing because we need the outer deps back + if let Some(saved) = saved_dependencies.take() { + // Merge current memo-block deps into the restored outer deps + let memo_deps = std::mem::replace(&mut dependencies, saved); + let _memo_keys = std::mem::replace( + &mut dep_keys, + saved_dep_keys.take().unwrap_or_default(), + ); + locals = saved_locals.take().unwrap_or_default(); + // Add memo deps to outer deps (they're still valid outer deps) + for d in memo_deps { + let key = dep_to_key(&d); + if dep_keys.insert(key) { + dependencies.push(d); + } + } + } + } + } + } + InstructionValue::ArrayExpression { elements, loc, .. } => { + let mut array_deps: Vec = Vec::new(); + let mut array_keys: FxHashSet = FxHashSet::default(); + let empty_locals = FxHashSet::default(); + for elem in elements { + let place = match elem { + ArrayElement::Place(p) => Some(p), + ArrayElement::Spread(s) => Some(&s.place), + ArrayElement::Hole => None, + }; + if let Some(place) = place { + // Visit with empty locals for manual deps + visit_candidate_dependency( + place, + temporaries, + &mut array_deps, + &mut array_keys, + &empty_locals, + ); + // Visit normally + visit_candidate_dependency( + place, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + } + } + temporaries.insert( + lvalue_id, + Temporary::Aggregate { dependencies: array_deps, loc: *loc }, + ); + } + InstructionValue::CallExpression { callee, args, .. } => { + // Check if this is an effect hook call + if let Some(cb) = callbacks.as_mut() { + let callee_ty = get_identifier_type(callee.identifier, identifiers, types); + if is_effect_hook(callee_ty) + && !matches!(cb.validate_effect, ExhaustiveEffectDepsMode::Off) + { + if args.len() >= 2 { + let fn_arg = match &args[0] { + PlaceOrSpread::Place(p) => Some(p), + _ => None, + }; + let deps_arg = match &args[1] { + PlaceOrSpread::Place(p) => Some(p), + _ => None, + }; + if let (Some(fn_place), Some(deps_place)) = (fn_arg, deps_arg) { + let fn_deps = temporaries.get(&fn_place.identifier).cloned(); + let manual_deps = + temporaries.get(&deps_place.identifier).cloned(); + if let ( + Some(Temporary::Aggregate { + dependencies: fn_dep_list, + .. + }), + Some(Temporary::Aggregate { + dependencies: manual_dep_list, + loc: manual_loc, + }), + ) = (fn_deps, manual_deps) + { + let effect_report_mode = match &cb.validate_effect { + ExhaustiveEffectDepsMode::All => "all", + ExhaustiveEffectDepsMode::MissingOnly => "missing-only", + ExhaustiveEffectDepsMode::ExtraOnly => "extra-only", + ExhaustiveEffectDepsMode::Off => unreachable!(), + }; + // Convert manual deps to ManualMemoDependency format + let manual_memo_deps: Vec = + manual_dep_list + .iter() + .map(|dep| match dep { + InferredDependency::Local { + identifier, + path, + loc, + .. + } => ManualMemoDependency { + root: ManualMemoDependencyRoot::NamedLocal { + value: Place { + identifier: *identifier, + effect: + crate::react_compiler_hir::Effect::Read, + reactive: cb + .reactive + .contains(identifier), + loc: *loc, + }, + constant: false, + }, + path: path.clone(), + loc: *loc, + }, + InferredDependency::Global { binding } => { + ManualMemoDependency { + root: + ManualMemoDependencyRoot::Global { + identifier_name: binding + .name() + .to_string(), + }, + path: Vec::new(), + loc: None, + } + } + }) + .collect(); + + let diagnostic = validate_dependencies( + fn_dep_list, + &manual_memo_deps, + cb.reactive, + manual_loc, + ErrorCategory::EffectExhaustiveDependencies, + effect_report_mode, + identifiers, + types, + )?; + if let Some(diag) = diagnostic { + cb.diagnostics.push(diag); + } + } + } + } + } + } + + // Visit all operands except for MethodCall's property + for operand in + each_instruction_value_operand_with_functions(&instr.value, functions) + { + visit_candidate_dependency( + &operand, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + } + } + InstructionValue::MethodCall { receiver, property, args, .. } => { + // Check if this is an effect hook call + if let Some(cb) = callbacks.as_mut() { + let prop_ty = get_identifier_type(property.identifier, identifiers, types); + if is_effect_hook(prop_ty) + && !matches!(cb.validate_effect, ExhaustiveEffectDepsMode::Off) + { + if args.len() >= 2 { + let fn_arg = match &args[0] { + PlaceOrSpread::Place(p) => Some(p), + _ => None, + }; + let deps_arg = match &args[1] { + PlaceOrSpread::Place(p) => Some(p), + _ => None, + }; + if let (Some(fn_place), Some(deps_place)) = (fn_arg, deps_arg) { + let fn_deps = temporaries.get(&fn_place.identifier).cloned(); + let manual_deps = + temporaries.get(&deps_place.identifier).cloned(); + if let ( + Some(Temporary::Aggregate { + dependencies: fn_dep_list, + .. + }), + Some(Temporary::Aggregate { + dependencies: manual_dep_list, + loc: manual_loc, + }), + ) = (fn_deps, manual_deps) + { + let effect_report_mode = match &cb.validate_effect { + ExhaustiveEffectDepsMode::All => "all", + ExhaustiveEffectDepsMode::MissingOnly => "missing-only", + ExhaustiveEffectDepsMode::ExtraOnly => "extra-only", + ExhaustiveEffectDepsMode::Off => unreachable!(), + }; + let manual_memo_deps: Vec = + manual_dep_list + .iter() + .map(|dep| match dep { + InferredDependency::Local { + identifier, + path, + loc, + .. + } => ManualMemoDependency { + root: ManualMemoDependencyRoot::NamedLocal { + value: Place { + identifier: *identifier, + effect: + crate::react_compiler_hir::Effect::Read, + reactive: cb + .reactive + .contains(identifier), + loc: *loc, + }, + constant: false, + }, + path: path.clone(), + loc: *loc, + }, + InferredDependency::Global { binding } => { + ManualMemoDependency { + root: + ManualMemoDependencyRoot::Global { + identifier_name: binding + .name() + .to_string(), + }, + path: Vec::new(), + loc: None, + } + } + }) + .collect(); + + let diagnostic = validate_dependencies( + fn_dep_list, + &manual_memo_deps, + cb.reactive, + manual_loc, + ErrorCategory::EffectExhaustiveDependencies, + effect_report_mode, + identifiers, + types, + )?; + if let Some(diag) = diagnostic { + cb.diagnostics.push(diag); + } + } + } + } + } + } + + // Visit operands, skipping the method property itself + visit_candidate_dependency( + receiver, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + // Skip property — matches TS behavior + for arg in args { + let place = match arg { + PlaceOrSpread::Place(p) => p, + PlaceOrSpread::Spread(s) => &s.place, + }; + visit_candidate_dependency( + place, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + } + } + _ => { + // Default: visit all operands + for operand in + each_instruction_value_operand_with_functions(&instr.value, functions) + { + visit_candidate_dependency( + &operand, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + } + // Track lvalues as locals + for lv in each_instruction_lvalue_ids(&instr.value, lvalue_id) { + locals.insert(lv); + } + } + } + } + + // Terminal operands + for operand in &each_terminal_operand(&block.terminal) { + if optionals.contains_key(&operand.identifier) { + continue; + } + visit_candidate_dependency( + operand, + temporaries, + &mut dependencies, + &mut dep_keys, + &locals, + ); + } + } + + Ok(Temporary::Aggregate { dependencies, loc: None }) +} + +// ============================================================================= +// validateDependencies +// ============================================================================= + +fn validate_dependencies( + mut inferred: Vec, + manual_dependencies: &[ManualMemoDependency], + reactive: &FxHashSet, + manual_memo_loc: Option, + category: ErrorCategory, + exhaustive_deps_report_mode: &str, + identifiers: &[Identifier], + types: &[Type], +) -> Result, CompilerDiagnostic> { + // Sort dependencies by name and path + inferred.sort_by(|a, b| { + match (a, b) { + ( + InferredDependency::Global { binding: ab }, + InferredDependency::Global { binding: bb }, + ) => ab.name().cmp(bb.name()), + ( + InferredDependency::Local { identifier: a_id, path: a_path, .. }, + InferredDependency::Local { identifier: b_id, path: b_path, .. }, + ) => { + let a_name = get_identifier_name(*a_id, identifiers); + let b_name = get_identifier_name(*b_id, identifiers); + match (a_name.as_deref(), b_name.as_deref()) { + (Some(an), Some(bn)) => { + if *a_id != *b_id { + an.cmp(bn) + } else if a_path.len() != b_path.len() { + a_path.len().cmp(&b_path.len()) + } else { + // Compare path entries + for (ap, bp) in a_path.iter().zip(b_path.iter()) { + let a_opt = if ap.optional { 0i32 } else { 1 }; + let b_opt = if bp.optional { 0i32 } else { 1 }; + if a_opt != b_opt { + return a_opt.cmp(&b_opt); + } + let prop_cmp = + ap.property.to_string().cmp(&bp.property.to_string()); + if prop_cmp != std::cmp::Ordering::Equal { + return prop_cmp; + } + } + std::cmp::Ordering::Equal + } + } + _ => std::cmp::Ordering::Equal, + } + } + ( + InferredDependency::Global { binding: ab }, + InferredDependency::Local { identifier: b_id, .. }, + ) => { + let a_name = ab.name(); + let b_name = get_identifier_name(*b_id, identifiers); + match b_name.as_deref() { + Some(bn) => a_name.cmp(bn), + None => std::cmp::Ordering::Equal, + } + } + ( + InferredDependency::Local { identifier: a_id, .. }, + InferredDependency::Global { binding: bb }, + ) => { + let a_name = get_identifier_name(*a_id, identifiers); + let b_name = bb.name(); + match a_name.as_deref() { + Some(an) => an.cmp(b_name), + None => std::cmp::Ordering::Equal, + } + } + } + }); + + // Remove redundant inferred dependencies + // retainWhere logic: keep dep[ix] only if no earlier entry is equal or a subpath prefix + // Mirrors TS: retainWhere(inferred, (dep, ix) => { + // const match = inferred.findIndex(prevDep => isEqualTemporary(prevDep, dep) || ...); + // return match === -1 || match >= ix; + // }) + { + let snapshot = inferred.clone(); + let mut write_index = 0; + for ix in 0..snapshot.len() { + let dep = &snapshot[ix]; + let first_match = snapshot.iter().position(|prev_dep| { + is_equal_temporary(prev_dep, dep) + || (matches!( + (prev_dep, dep), + (InferredDependency::Local { .. }, InferredDependency::Local { .. }) + ) && { + if let ( + InferredDependency::Local { + identifier: prev_id, path: prev_path, .. + }, + InferredDependency::Local { + identifier: dep_id, path: dep_path, .. + }, + ) = (prev_dep, dep) + { + prev_id == dep_id && is_sub_path(prev_path, dep_path) + } else { + false + } + }) + }); + + let keep = match first_match { + None => true, + Some(m) => m >= ix, + }; + if keep { + inferred[write_index] = snapshot[ix].clone(); + write_index += 1; + } + } + inferred.truncate(write_index); + } + + // Validate manual deps + let mut matched: FxHashSet = FxHashSet::default(); // indices into manual_dependencies + let mut missing: Vec<&InferredDependency> = Vec::new(); + let mut extra: Vec<&ManualMemoDependency> = Vec::new(); + + for inferred_dep in &inferred { + match inferred_dep { + InferredDependency::Global { binding } => { + for (i, manual_dep) in manual_dependencies.iter().enumerate() { + if let ManualMemoDependencyRoot::Global { identifier_name } = &manual_dep.root { + if identifier_name == binding.name() { + matched.insert(i); + extra.push(manual_dep); + } + } + } + continue; + } + InferredDependency::Local { identifier, path, loc: _, .. } => { + // Skip effect event functions + let ty = get_identifier_type(*identifier, identifiers, types); + if is_effect_event_function_type(ty) { + continue; + } + + let mut has_matching = false; + for (i, manual_dep) in manual_dependencies.iter().enumerate() { + if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &manual_dep.root { + if value.identifier == *identifier + && (are_equal_paths(&manual_dep.path, path) + || is_sub_path_ignoring_optionals(&manual_dep.path, path)) + { + has_matching = true; + matched.insert(i); + } + } + } + + if has_matching || is_optional_dependency(*identifier, reactive, identifiers, types) + { + continue; + } + + missing.push(inferred_dep); + } + } + } + + // Check for extra dependencies + for (i, dep) in manual_dependencies.iter().enumerate() { + if matched.contains(&i) { + continue; + } + if let ManualMemoDependencyRoot::NamedLocal { constant, value, .. } = &dep.root { + if *constant { + let dep_ty = get_identifier_type(value.identifier, identifiers, types); + // Constant-folded primitives: skip + if !value.reactive && is_primitive_type(dep_ty) { + continue; + } + } + } + extra.push(dep); + } + + // Filter based on report mode + let filtered_missing: Vec<&InferredDependency> = + if exhaustive_deps_report_mode == "extra-only" { Vec::new() } else { missing }; + let filtered_extra: Vec<&ManualMemoDependency> = + if exhaustive_deps_report_mode == "missing-only" { Vec::new() } else { extra }; + + if filtered_missing.is_empty() && filtered_extra.is_empty() { + return Ok(None); + } + + // Build suggestion when we have valid index info (matches TS behavior) + let suggestion = manual_memo_loc.and_then(|loc| { + let start_index = loc.start.index?; + let end_index = loc.end.index?; + let text = format!( + "[{}]", + inferred + .iter() + .filter(|dep| { + match dep { + InferredDependency::Local { identifier, .. } => { + let ty = get_identifier_type(*identifier, identifiers, types); + !is_optional_dependency(*identifier, reactive, identifiers, types) + && !is_effect_event_function_type(ty) + } + InferredDependency::Global { .. } => false, + } + }) + .map(|dep| print_inferred_dependency(dep, identifiers)) + .collect::>() + .join(", ") + ); + Some(CompilerSuggestion { + op: CompilerSuggestionOperation::Replace, + range: (start_index as usize, end_index as usize), + description: "Update dependencies".to_string(), + text: Some(text), + }) + }); + + let mut diagnostic = + create_diagnostic(category, &filtered_missing, &filtered_extra, suggestion, identifiers)?; + + // Add detail items for missing deps + for dep in &filtered_missing { + if let InferredDependency::Local { identifier, path: _, loc, .. } = dep { + let mut hint = String::new(); + let ty = get_identifier_type(*identifier, identifiers, types); + if is_stable_type(ty) { + hint = ". Refs, setState functions, and other \"stable\" values generally do not need to be added as dependencies, but this variable may change over time to point to different values".to_string(); + } + let dep_str = print_inferred_dependency(dep, identifiers); + diagnostic.details.push(CompilerDiagnosticDetail::Error { + loc: *loc, + message: Some(format!("Missing dependency `{dep_str}`{hint}")), + identifier_name: None, + }); + } + } + + // Add detail items for extra deps + for dep in &filtered_extra { + match &dep.root { + ManualMemoDependencyRoot::Global { .. } => { + let dep_str = print_manual_memo_dependency(dep, identifiers); + diagnostic.details.push(CompilerDiagnosticDetail::Error { + loc: dep.loc.or(manual_memo_loc), + message: Some(format!( + "Unnecessary dependency `{dep_str}`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change" + )), + identifier_name: None, + }); + } + ManualMemoDependencyRoot::NamedLocal { value, .. } => { + // Check if there's a matching inferred dep + let matching_inferred = inferred.iter().find(|inf_dep| { + if let InferredDependency::Local { + identifier: inf_id, path: inf_path, .. + } = inf_dep + { + *inf_id == value.identifier + && is_sub_path_ignoring_optionals(inf_path, &dep.path) + } else { + false + } + }); + + if let Some(matching) = matching_inferred { + if let InferredDependency::Local { identifier, .. } = matching { + let matching_ty = get_identifier_type(*identifier, identifiers, types); + if is_effect_event_function_type(matching_ty) { + let dep_str = print_manual_memo_dependency(dep, identifiers); + diagnostic.details.push(CompilerDiagnosticDetail::Error { + loc: dep.loc.or(manual_memo_loc), + message: Some(format!( + "Functions returned from `useEffectEvent` must not be included in the dependency array. Remove `{dep_str}` from the dependencies." + )), + identifier_name: None, + }); + } else if !is_optional_dependency_inferred( + matching, + reactive, + identifiers, + types, + ) { + let dep_str = print_manual_memo_dependency(dep, identifiers); + let inferred_str = print_inferred_dependency(matching, identifiers); + diagnostic.details.push(CompilerDiagnosticDetail::Error { + loc: dep.loc.or(manual_memo_loc), + message: Some(format!( + "Overly precise dependency `{dep_str}`, use `{inferred_str}` instead" + )), + identifier_name: None, + }); + } else { + let dep_str = print_manual_memo_dependency(dep, identifiers); + diagnostic.details.push(CompilerDiagnosticDetail::Error { + loc: dep.loc.or(manual_memo_loc), + message: Some(format!("Unnecessary dependency `{dep_str}`")), + identifier_name: None, + }); + } + } + } else { + let dep_str = print_manual_memo_dependency(dep, identifiers); + diagnostic.details.push(CompilerDiagnosticDetail::Error { + loc: dep.loc.or(manual_memo_loc), + message: Some(format!("Unnecessary dependency `{dep_str}`")), + identifier_name: None, + }); + } + } + } + } + + // Add hint showing inferred dependencies when a suggestion was generated + // (matches TS: only adds hint when suggestion != null, using suggestion.text) + if let Some(ref suggestions) = diagnostic.suggestions { + if let Some(suggestion) = suggestions.first() { + if let Some(ref text) = suggestion.text { + diagnostic.details.push(CompilerDiagnosticDetail::Hint { + message: format!("Inferred dependencies: `{text}`"), + }); + } + } + } + + Ok(Some(diagnostic)) +} + +// ============================================================================= +// Printing helpers +// ============================================================================= + +fn print_inferred_dependency(dep: &InferredDependency, identifiers: &[Identifier]) -> String { + match dep { + InferredDependency::Global { binding } => binding.name().to_string(), + InferredDependency::Local { identifier, path, .. } => { + let name = get_identifier_name(*identifier, identifiers) + .unwrap_or_else(|| "".to_string()); + let path_str: String = path + .iter() + .map(|p| format!("{}.{}", if p.optional { "?" } else { "" }, p.property)) + .collect(); + format!("{name}{path_str}") + } + } +} + +fn print_manual_memo_dependency(dep: &ManualMemoDependency, identifiers: &[Identifier]) -> String { + let name = match &dep.root { + ManualMemoDependencyRoot::Global { identifier_name } => identifier_name.clone(), + ManualMemoDependencyRoot::NamedLocal { value, .. } => { + get_identifier_name(value.identifier, identifiers) + .unwrap_or_else(|| "".to_string()) + } + }; + let path_str: String = dep + .path + .iter() + .map(|p| format!("{}.{}", if p.optional { "?" } else { "" }, p.property)) + .collect(); + format!("{name}{path_str}") +} + +// ============================================================================= +// Optional dependency check +// ============================================================================= + +fn is_optional_dependency( + identifier: IdentifierId, + reactive: &FxHashSet, + identifiers: &[Identifier], + types: &[Type], +) -> bool { + if reactive.contains(&identifier) { + return false; + } + let ty = get_identifier_type(identifier, identifiers, types); + is_stable_type(ty) || is_primitive_type(ty) +} + +fn is_optional_dependency_inferred( + dep: &InferredDependency, + reactive: &FxHashSet, + identifiers: &[Identifier], + types: &[Type], +) -> bool { + match dep { + InferredDependency::Local { identifier, .. } => { + is_optional_dependency(*identifier, reactive, identifiers, types) + } + InferredDependency::Global { .. } => false, + } +} + +// ============================================================================= +// Equality check for temporaries +// ============================================================================= + +fn is_equal_temporary(a: &InferredDependency, b: &InferredDependency) -> bool { + match (a, b) { + ( + InferredDependency::Global { binding: ab }, + InferredDependency::Global { binding: bb }, + ) => ab.name() == bb.name(), + ( + InferredDependency::Local { identifier: a_id, path: a_path, .. }, + InferredDependency::Local { identifier: b_id, path: b_path, .. }, + ) => a_id == b_id && are_equal_paths(a_path, b_path), + _ => false, + } +} + +// ============================================================================= +// createDiagnostic +// ============================================================================= + +fn create_diagnostic( + category: ErrorCategory, + missing: &[&InferredDependency], + extra: &[&ManualMemoDependency], + suggestion: Option, + _identifiers: &[Identifier], +) -> Result { + let missing_str = if !missing.is_empty() { Some("missing") } else { None }; + let extra_str = if !extra.is_empty() { Some("extra") } else { None }; + + let (reason, description) = match category { + ErrorCategory::MemoDependencies => { + let reason_parts: Vec<&str> = + [missing_str, extra_str].iter().filter_map(|x| *x).collect(); + let reason = format!("Found {} memoization dependencies", reason_parts.join("/")); + + let desc_parts: Vec<&str> = [ + if !missing.is_empty() { + Some("Missing dependencies can cause a value to update less often than it should, resulting in stale UI") + } else { + None + }, + if !extra.is_empty() { + Some("Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often") + } else { + None + }, + ] + .iter() + .filter_map(|x| *x) + .collect(); + let description = desc_parts.join(". "); + (reason, description) + } + ErrorCategory::EffectExhaustiveDependencies => { + let reason_parts: Vec<&str> = + [missing_str, extra_str].iter().filter_map(|x| *x).collect(); + let reason = format!("Found {} effect dependencies", reason_parts.join("/")); + + let desc_parts: Vec<&str> = [ + if !missing.is_empty() { + Some("Missing dependencies can cause an effect to fire less often than it should") + } else { + None + }, + if !extra.is_empty() { + Some("Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects") + } else { + None + }, + ] + .iter() + .filter_map(|x| *x) + .collect(); + let description = desc_parts.join(". "); + (reason, description) + } + _ => { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + format!("Unexpected error category: {:?}", category), + None, + )); + } + }; + + Ok(CompilerDiagnostic { + category, + reason, + description: Some(description), + details: Vec::new(), + suggestions: suggestion.map(|s| vec![s]), + }) +} + +/// Collect lvalue identifier ids from instruction value (for the default branch). +/// Thin wrapper around canonical `each_instruction_value_lvalue` that maps to ids. +fn each_instruction_lvalue_ids( + value: &InstructionValue, + lvalue_id: IdentifierId, +) -> Vec { + let mut ids = vec![lvalue_id]; + for place in each_instruction_value_lvalue(value) { + ids.push(place.identifier); + } + ids +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_hooks_usage.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_hooks_usage.rs new file mode 100644 index 0000000000000..6b603934315de --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_hooks_usage.rs @@ -0,0 +1,513 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Validates hooks usage rules. +//! +//! Port of ValidateHooksUsage.ts. +//! Ensures hooks are called unconditionally, not passed as values, +//! and not called dynamically. Also validates that hooks are not +//! called inside function expressions. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerError, CompilerErrorDetail, ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::dominator::compute_unconditional_blocks; +use crate::react_compiler_hir::environment::{Environment, is_hook_name}; +use crate::react_compiler_hir::object_shape::HookKind; +use crate::react_compiler_hir::visitors::{each_pattern_operand, each_terminal_operand}; +use crate::react_compiler_hir::{ + FunctionId, HirFunction, Identifier, IdentifierId, InstructionValue, ParamPattern, Place, + PropertyLiteral, Type, visitors, +}; +use crate::react_compiler_utils::FxIndexMap; + +/// Value classification for hook validation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Kind { + Error, + KnownHook, + PotentialHook, + Global, + Local, +} + +fn join_kinds(a: Kind, b: Kind) -> Kind { + if a == Kind::Error || b == Kind::Error { + Kind::Error + } else if a == Kind::KnownHook || b == Kind::KnownHook { + Kind::KnownHook + } else if a == Kind::PotentialHook || b == Kind::PotentialHook { + Kind::PotentialHook + } else if a == Kind::Global || b == Kind::Global { + Kind::Global + } else { + Kind::Local + } +} + +fn get_kind_for_place( + place: &Place, + value_kinds: &FxHashMap, + identifiers: &[Identifier], +) -> Kind { + let known_kind = value_kinds.get(&place.identifier).copied(); + let ident = &identifiers[place.identifier.0 as usize]; + if let Some(ref name) = ident.name { + if is_hook_name(name.value()) { + return join_kinds(known_kind.unwrap_or(Kind::Local), Kind::PotentialHook); + } + } + known_kind.unwrap_or(Kind::Local) +} + +fn ident_is_hook_name(identifier_id: IdentifierId, identifiers: &[Identifier]) -> bool { + let ident = &identifiers[identifier_id.0 as usize]; + if let Some(ref name) = ident.name { is_hook_name(name.value()) } else { false } +} + +fn get_hook_kind_for_id<'a>( + identifier_id: IdentifierId, + identifiers: &[Identifier], + types: &[Type], + env: &'a Environment, +) -> Result, CompilerDiagnostic> { + let identifier = &identifiers[identifier_id.0 as usize]; + let ty = &types[identifier.type_.0 as usize]; + env.get_hook_kind_for_type(ty) +} + +fn visit_place( + place: &Place, + value_kinds: &FxHashMap, + errors_by_loc: &mut FxIndexMap, + env: &mut Environment, +) -> Result<(), CompilerError> { + let kind = value_kinds.get(&place.identifier).copied(); + if kind == Some(Kind::KnownHook) { + record_invalid_hook_usage_error(place, errors_by_loc, env)?; + } + Ok(()) +} + +fn record_conditional_hook_error( + place: &Place, + value_kinds: &mut FxHashMap, + errors_by_loc: &mut FxIndexMap, + env: &mut Environment, +) -> Result<(), CompilerError> { + value_kinds.insert(place.identifier, Kind::Error); + let reason = "Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)".to_string(); + if let Some(loc) = place.loc { + let previous = errors_by_loc.get(&loc); + if previous.is_none() || previous.unwrap().reason != reason { + errors_by_loc.insert( + loc, + CompilerErrorDetail { + category: ErrorCategory::Hooks, + reason, + description: None, + loc: Some(loc), + suggestions: None, + }, + ); + } + } else { + env.record_error(CompilerErrorDetail { + category: ErrorCategory::Hooks, + reason, + description: None, + loc: None, + suggestions: None, + })?; + } + Ok(()) +} + +fn record_invalid_hook_usage_error( + place: &Place, + errors_by_loc: &mut FxIndexMap, + env: &mut Environment, +) -> Result<(), CompilerError> { + let reason = "Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values".to_string(); + if let Some(loc) = place.loc { + if !errors_by_loc.contains_key(&loc) { + errors_by_loc.insert( + loc, + CompilerErrorDetail { + category: ErrorCategory::Hooks, + reason, + description: None, + loc: Some(loc), + suggestions: None, + }, + ); + } + } else { + env.record_error(CompilerErrorDetail { + category: ErrorCategory::Hooks, + reason, + description: None, + loc: None, + suggestions: None, + })?; + } + Ok(()) +} + +fn record_dynamic_hook_usage_error( + place: &Place, + errors_by_loc: &mut FxIndexMap, + env: &mut Environment, +) -> Result<(), CompilerError> { + let reason = "Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks".to_string(); + if let Some(loc) = place.loc { + if !errors_by_loc.contains_key(&loc) { + errors_by_loc.insert( + loc, + CompilerErrorDetail { + category: ErrorCategory::Hooks, + reason, + description: None, + loc: Some(loc), + suggestions: None, + }, + ); + } + } else { + env.record_error(CompilerErrorDetail { + category: ErrorCategory::Hooks, + reason, + description: None, + loc: None, + suggestions: None, + })?; + } + Ok(()) +} + +/// Validates hooks usage rules for a function. +pub fn validate_hooks_usage( + func: &HirFunction, + env: &mut Environment, +) -> Result<(), crate::react_compiler_diagnostics::CompilerDiagnostic> { + let unconditional_blocks = compute_unconditional_blocks(func, env.next_block_id().0)?; + let mut errors_by_loc: FxIndexMap = FxIndexMap::default(); + let mut value_kinds: FxHashMap = FxHashMap::default(); + + // Process params + for param in &func.params { + let place = match param { + ParamPattern::Place(p) => p, + ParamPattern::Spread(s) => &s.place, + }; + let kind = get_kind_for_place(place, &value_kinds, &env.identifiers); + value_kinds.insert(place.identifier, kind); + } + + // Process blocks + for (_block_id, block) in &func.body.blocks { + // Process phis + for phi in &block.phis { + let mut kind = if ident_is_hook_name(phi.place.identifier, &env.identifiers) { + Kind::PotentialHook + } else { + Kind::Local + }; + for (_, operand) in &phi.operands { + if let Some(&operand_kind) = value_kinds.get(&operand.identifier) { + kind = join_kinds(kind, operand_kind); + } + } + value_kinds.insert(phi.place.identifier, kind); + } + + // Process instructions + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + + match &instr.value { + InstructionValue::LoadGlobal { .. } => { + if get_hook_kind_for_id(lvalue_id, &env.identifiers, &env.types, env)?.is_some() + { + value_kinds.insert(lvalue_id, Kind::KnownHook); + } else { + value_kinds.insert(lvalue_id, Kind::Global); + } + } + InstructionValue::LoadContext { place, .. } + | InstructionValue::LoadLocal { place, .. } => { + visit_place(place, &value_kinds, &mut errors_by_loc, env)?; + let kind = get_kind_for_place(place, &value_kinds, &env.identifiers); + value_kinds.insert(lvalue_id, kind); + } + InstructionValue::StoreLocal { lvalue, value, .. } + | InstructionValue::StoreContext { lvalue, value, .. } => { + visit_place(value, &value_kinds, &mut errors_by_loc, env)?; + let kind = join_kinds( + get_kind_for_place(value, &value_kinds, &env.identifiers), + get_kind_for_place(&lvalue.place, &value_kinds, &env.identifiers), + ); + value_kinds.insert(lvalue.place.identifier, kind); + value_kinds.insert(lvalue_id, kind); + } + InstructionValue::ComputedLoad { object, .. } => { + visit_place(object, &value_kinds, &mut errors_by_loc, env)?; + let kind = get_kind_for_place(object, &value_kinds, &env.identifiers); + let lvalue_kind = + get_kind_for_place(&instr.lvalue, &value_kinds, &env.identifiers); + value_kinds.insert(lvalue_id, join_kinds(lvalue_kind, kind)); + } + InstructionValue::PropertyLoad { object, property, .. } => { + let object_kind = get_kind_for_place(object, &value_kinds, &env.identifiers); + let is_hook_property = match property { + PropertyLiteral::String(s) => is_hook_name(s), + PropertyLiteral::Number(_) => false, + }; + let kind = match object_kind { + Kind::Error => Kind::Error, + Kind::KnownHook => { + if is_hook_property { + Kind::KnownHook + } else { + Kind::Local + } + } + Kind::PotentialHook => Kind::PotentialHook, + Kind::Global => { + if is_hook_property { + Kind::KnownHook + } else { + Kind::Global + } + } + Kind::Local => { + if is_hook_property { + Kind::PotentialHook + } else { + Kind::Local + } + } + }; + value_kinds.insert(lvalue_id, kind); + } + InstructionValue::CallExpression { callee, args, .. } => { + let callee_kind = get_kind_for_place(callee, &value_kinds, &env.identifiers); + let is_hook_callee = + callee_kind == Kind::KnownHook || callee_kind == Kind::PotentialHook; + if is_hook_callee && !unconditional_blocks.contains(&block.id) { + record_conditional_hook_error( + callee, + &mut value_kinds, + &mut errors_by_loc, + env, + )?; + } else if callee_kind == Kind::PotentialHook { + record_dynamic_hook_usage_error(callee, &mut errors_by_loc, env)?; + } + // Visit all operands except callee + for arg in args { + let place = match arg { + crate::react_compiler_hir::PlaceOrSpread::Place(p) => p, + crate::react_compiler_hir::PlaceOrSpread::Spread(s) => &s.place, + }; + visit_place(place, &value_kinds, &mut errors_by_loc, env)?; + } + } + InstructionValue::MethodCall { receiver, property, args, .. } => { + let callee_kind = get_kind_for_place(property, &value_kinds, &env.identifiers); + let is_hook_callee = + callee_kind == Kind::KnownHook || callee_kind == Kind::PotentialHook; + if is_hook_callee && !unconditional_blocks.contains(&block.id) { + record_conditional_hook_error( + property, + &mut value_kinds, + &mut errors_by_loc, + env, + )?; + } else if callee_kind == Kind::PotentialHook { + record_dynamic_hook_usage_error(property, &mut errors_by_loc, env)?; + } + // Visit receiver and args (not property) + visit_place(receiver, &value_kinds, &mut errors_by_loc, env)?; + for arg in args { + let place = match arg { + crate::react_compiler_hir::PlaceOrSpread::Place(p) => p, + crate::react_compiler_hir::PlaceOrSpread::Spread(s) => &s.place, + }; + visit_place(place, &value_kinds, &mut errors_by_loc, env)?; + } + } + InstructionValue::Destructure { lvalue, value, .. } => { + visit_place(value, &value_kinds, &mut errors_by_loc, env)?; + let object_kind = get_kind_for_place(value, &value_kinds, &env.identifiers); + // Process instr.lvalue and all pattern operands (matching TS eachInstructionLValue) + let pattern_places = each_pattern_operand(&lvalue.pattern); + let all_lvalues = + std::iter::once(instr.lvalue.clone()).chain(pattern_places.into_iter()); + for place in all_lvalues { + let is_hook_property = + ident_is_hook_name(place.identifier, &env.identifiers); + let kind = match object_kind { + Kind::Error => Kind::Error, + Kind::KnownHook => Kind::KnownHook, + Kind::PotentialHook => Kind::PotentialHook, + Kind::Global => { + if is_hook_property { + Kind::KnownHook + } else { + Kind::Global + } + } + Kind::Local => { + if is_hook_property { + Kind::PotentialHook + } else { + Kind::Local + } + } + }; + value_kinds.insert(place.identifier, kind); + } + } + InstructionValue::ObjectMethod { lowered_func, .. } + | InstructionValue::FunctionExpression { lowered_func, .. } => { + visit_function_expression(env, lowered_func.func)?; + } + _ => { + // For all other instructions: visit operands, set lvalue kinds + // Matches TS which uses eachInstructionOperand + eachInstructionLValue + visit_all_operands(&instr.value, &value_kinds, &mut errors_by_loc, env)?; + // Set kind for instr.lvalue + let kind = get_kind_for_place(&instr.lvalue, &value_kinds, &env.identifiers); + value_kinds.insert(lvalue_id, kind); + // Also set kind for value-level lvalues (e.g. DeclareLocal, PrefixUpdate, PostfixUpdate) + for lv in visitors::each_instruction_value_lvalue(&instr.value) { + let lv_kind = get_kind_for_place(&lv, &value_kinds, &env.identifiers); + value_kinds.insert(lv.identifier, lv_kind); + } + } + } + } + + // Visit terminal operands + for place in each_terminal_operand(&block.terminal) { + visit_place(&place, &value_kinds, &mut errors_by_loc, env)?; + } + } + + // Record all accumulated errors (in insertion order, matching TS Map iteration) + for (_, error_detail) in errors_by_loc { + env.record_error(error_detail)?; + } + Ok(()) +} + +/// Visit a function expression to check for hook calls inside it. +/// Processes instructions in order, visiting nested functions immediately +/// (before processing subsequent calls) to match TS error ordering. +fn visit_function_expression( + env: &mut Environment, + func_id: FunctionId, +) -> Result<(), CompilerError> { + // Collect items in instruction order to process them sequentially. + // Each item is either a call to check or a nested function to visit. + enum Item { + Call(IdentifierId, Option), + NestedFunc(FunctionId), + } + + let func = &env.functions[func_id.0 as usize]; + let mut items: Vec = Vec::new(); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::ObjectMethod { lowered_func, .. } + | InstructionValue::FunctionExpression { lowered_func, .. } => { + items.push(Item::NestedFunc(lowered_func.func)); + } + InstructionValue::CallExpression { callee, .. } => { + items.push(Item::Call(callee.identifier, callee.loc)); + } + InstructionValue::MethodCall { property, .. } => { + items.push(Item::Call(property.identifier, property.loc)); + } + _ => {} + } + } + } + + // Process items in instruction order (matching TS which visits nested + // functions immediately before processing subsequent calls) + for item in items { + match item { + Item::Call(identifier_id, loc) => { + let identifier = &env.identifiers[identifier_id.0 as usize]; + let ty = &env.types[identifier.type_.0 as usize]; + let hook_kind = env.get_hook_kind_for_type(ty).ok().flatten().cloned(); + if let Some(hook_kind) = hook_kind { + let description = format!( + "Cannot call {} within a function expression", + if hook_kind == HookKind::Custom { + "hook" + } else { + hook_kind_display(&hook_kind) + } + ); + env.record_error(CompilerErrorDetail { + category: ErrorCategory::Hooks, + reason: "Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)".to_string(), + description: Some(description), + loc, + suggestions: None, + })?; + } + } + Item::NestedFunc(nested_func_id) => { + visit_function_expression(env, nested_func_id)?; + } + } + } + Ok(()) +} + +fn hook_kind_display(kind: &HookKind) -> &'static str { + match kind { + HookKind::UseContext => "useContext", + HookKind::UseState => "useState", + HookKind::UseActionState => "useActionState", + HookKind::UseReducer => "useReducer", + HookKind::UseRef => "useRef", + HookKind::UseEffect => "useEffect", + HookKind::UseLayoutEffect => "useLayoutEffect", + HookKind::UseInsertionEffect => "useInsertionEffect", + HookKind::UseMemo => "useMemo", + HookKind::UseCallback => "useCallback", + HookKind::UseTransition => "useTransition", + HookKind::UseImperativeHandle => "useImperativeHandle", + HookKind::UseEffectEvent => "useEffectEvent", + HookKind::UseOptimistic => "useOptimistic", + HookKind::Custom => "hook", + } +} + +/// Visit all operands of an instruction value (generic fallback). +/// Uses the canonical `each_instruction_value_operand` from visitors. +fn visit_all_operands( + value: &InstructionValue, + value_kinds: &FxHashMap, + errors_by_loc: &mut FxIndexMap, + env: &mut Environment, +) -> Result<(), CompilerError> { + let operands = visitors::each_instruction_value_operand(value, &*env); + for place in &operands { + visit_place(place, value_kinds, errors_by_loc, env)?; + } + Ok(()) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_locals_not_reassigned_after_render.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_locals_not_reassigned_after_render.rs new file mode 100644 index 0000000000000..0076eaeeca229 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_locals_not_reassigned_after_render.rs @@ -0,0 +1,278 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::{ + each_instruction_lvalue_ids, each_instruction_value_operand, each_terminal_operand, +}; +use crate::react_compiler_hir::{ + Effect, HirFunction, Identifier, IdentifierId, IdentifierName, InstructionValue, Place, Type, +}; + +/// Validates that local variables cannot be reassigned after render. +/// This prevents a category of bugs in which a closure captures a +/// binding from one render but does not update. +pub fn validate_locals_not_reassigned_after_render(func: &HirFunction, env: &mut Environment) { + let mut context_variables: FxHashSet = FxHashSet::default(); + let mut diagnostics: Vec = Vec::new(); + + let reassignment = get_context_reassignment( + func, + &env.identifiers, + &env.types, + &env.functions, + env, + &mut context_variables, + false, + false, + &mut diagnostics, + ); + + // Record accumulated errors (from async function checks in inner functions) first + for diagnostic in diagnostics { + env.record_diagnostic(diagnostic); + } + + // Then record the top-level reassignment error if any + if let Some(reassignment_place) = reassignment { + let variable_name = format_variable_name(&reassignment_place, &env.identifiers); + env.record_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::Immutability, + "Cannot reassign variable after render completes", + Some(format!( + "Reassigning {} after render has completed can cause inconsistent \ + behavior on subsequent renders. Consider using state instead", + variable_name + )), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: reassignment_place.loc, + message: Some(format!("Cannot reassign {} after render completes", variable_name)), + identifier_name: None, + }), + ); + } +} + +/// Format a variable name for error messages. Uses the named identifier if +/// available, otherwise falls back to "variable". +fn format_variable_name(place: &Place, identifiers: &[Identifier]) -> String { + let identifier = &identifiers[place.identifier.0 as usize]; + match &identifier.name { + Some(IdentifierName::Named(name)) => format!("`{}`", name), + _ => "variable".to_string(), + } +} + +/// Recursively checks whether a function (or its dependencies) reassigns a +/// context variable. Returns the reassigned place if found, or None. +/// +/// Side effects: accumulates async-function reassignment diagnostics into `diagnostics`. +fn get_context_reassignment( + func: &HirFunction, + identifiers: &[Identifier], + types: &[Type], + functions: &[HirFunction], + env: &Environment, + context_variables: &mut FxHashSet, + is_function_expression: bool, + is_async: bool, + diagnostics: &mut Vec, +) -> Option { + // Maps identifiers to the place that they reassign + let mut reassigning_functions: FxHashMap = FxHashMap::default(); + + for (_block_id, block) in &func.body.blocks { + for &instruction_id in &block.instructions { + let instr = &func.instructions[instruction_id.0 as usize]; + + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } + | InstructionValue::ObjectMethod { lowered_func, .. } => { + let inner_function = &functions[lowered_func.func.0 as usize]; + let inner_is_async = is_async || inner_function.is_async; + + // Recursively check the inner function + let mut reassignment = get_context_reassignment( + inner_function, + identifiers, + types, + functions, + env, + context_variables, + true, + inner_is_async, + diagnostics, + ); + + // If the function itself doesn't reassign, check if one of its + // dependencies (operands) is a reassigning function + if reassignment.is_none() { + for context_place in &inner_function.context { + if let Some(reassignment_place) = + reassigning_functions.get(&context_place.identifier) + { + reassignment = Some(reassignment_place.clone()); + break; + } + } + } + + // If the function or its dependencies reassign, handle it + if let Some(ref reassignment_place) = reassignment { + if inner_is_async { + // Async functions that reassign get an immediate error + let variable_name = + format_variable_name(reassignment_place, identifiers); + diagnostics.push( + CompilerDiagnostic::new( + ErrorCategory::Immutability, + "Cannot reassign variable in async function", + Some( + "Reassigning a variable in an async function can cause \ + inconsistent behavior on subsequent renders. \ + Consider using state instead" + .to_string(), + ), + ) + .with_detail( + CompilerDiagnosticDetail::Error { + loc: reassignment_place.loc, + message: Some(format!("Cannot reassign {}", variable_name)), + identifier_name: None, + }, + ), + ); + // Return null (don't propagate further) — matches TS behavior + return None; + } else { + // Propagate reassignment info on the lvalue + reassigning_functions + .insert(instr.lvalue.identifier, reassignment_place.clone()); + } + } + } + + InstructionValue::StoreLocal { lvalue, value, .. } => { + if let Some(reassignment_place) = reassigning_functions.get(&value.identifier) { + let reassignment_place = reassignment_place.clone(); + reassigning_functions + .insert(lvalue.place.identifier, reassignment_place.clone()); + reassigning_functions.insert(instr.lvalue.identifier, reassignment_place); + } + } + + InstructionValue::LoadLocal { place, .. } => { + if let Some(reassignment_place) = reassigning_functions.get(&place.identifier) { + reassigning_functions + .insert(instr.lvalue.identifier, reassignment_place.clone()); + } + } + + InstructionValue::DeclareContext { lvalue, .. } => { + if !is_function_expression { + context_variables.insert(lvalue.place.identifier); + } + } + + InstructionValue::StoreContext { lvalue, value, .. } => { + // If we're inside a function expression and the target is a + // context variable from the outer scope, this is a reassignment + if is_function_expression + && context_variables.contains(&lvalue.place.identifier) + { + return Some(lvalue.place.clone()); + } + + // In the outer function, track context variables + if !is_function_expression { + context_variables.insert(lvalue.place.identifier); + } + + // Propagate reassigning function info through StoreContext + if let Some(reassignment_place) = reassigning_functions.get(&value.identifier) { + let reassignment_place = reassignment_place.clone(); + reassigning_functions + .insert(lvalue.place.identifier, reassignment_place.clone()); + reassigning_functions.insert(instr.lvalue.identifier, reassignment_place); + } + } + + _ => { + // For calls with noAlias signatures, only check the callee/receiver + // (not args) to avoid false positives from callbacks that reassign + // context variables. + let operands: Vec = match &instr.value { + InstructionValue::CallExpression { callee, .. } => { + if env.has_no_alias_signature(callee.identifier) { + vec![callee.clone()] + } else { + each_instruction_value_operand(&instr.value, env) + } + } + InstructionValue::MethodCall { receiver, property, .. } => { + if env.has_no_alias_signature(property.identifier) { + vec![receiver.clone(), property.clone()] + } else { + each_instruction_value_operand(&instr.value, env) + } + } + InstructionValue::TaggedTemplateExpression { tag, .. } => { + if env.has_no_alias_signature(tag.identifier) { + vec![tag.clone()] + } else { + each_instruction_value_operand(&instr.value, env) + } + } + _ => each_instruction_value_operand(&instr.value, env), + }; + + for operand in &operands { + // Invariant: effects must be inferred before this pass runs + assert!( + operand.effect != Effect::Unknown, + "Expected effects to be inferred prior to \ + ValidateLocalsNotReassignedAfterRender" + ); + + if let Some(reassignment_place) = + reassigning_functions.get(&operand.identifier).cloned() + { + if operand.effect == Effect::Freeze { + // Functions that reassign local variables are inherently + // mutable and unsafe to pass where a frozen value is expected. + return Some(reassignment_place); + } else { + // If the operand is not frozen but does reassign, then the + // lvalues of the instruction could also be reassigning + for lvalue_id in each_instruction_lvalue_ids(instr) { + reassigning_functions + .insert(lvalue_id, reassignment_place.clone()); + } + } + } + } + } + } + } + + // Check terminal operands for reassigning functions + for operand in each_terminal_operand(&block.terminal) { + if let Some(reassignment_place) = reassigning_functions.get(&operand.identifier) { + return Some(reassignment_place.clone()); + } + } + } + + None +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_capitalized_calls.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_capitalized_calls.rs new file mode 100644 index 0000000000000..0024801354e90 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_capitalized_calls.rs @@ -0,0 +1,82 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{CompilerError, CompilerErrorDetail, ErrorCategory}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{HirFunction, IdentifierId, InstructionValue, PropertyLiteral}; + +/// Validates that capitalized functions are not called directly (they should be rendered as JSX). +/// +/// Port of ValidateNoCapitalizedCalls.ts. +pub fn validate_no_capitalized_calls( + func: &HirFunction, + env: &mut Environment, +) -> Result<(), CompilerError> { + // Build the allow list from global registry keys + config entries + let mut allow_list: FxHashSet = env.globals().keys().cloned().collect(); + if let Some(config_entries) = &env.config.validate_no_capitalized_calls { + for entry in config_entries { + allow_list.insert(entry.clone()); + } + } + + let mut capital_load_globals: FxHashMap = FxHashMap::default(); + let mut capitalized_properties: FxHashMap = FxHashMap::default(); + + let reason = "Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config"; + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + let value = &instr.value; + + match value { + InstructionValue::LoadGlobal { binding, .. } => { + let name = binding.name(); + if !name.is_empty() + && name.starts_with(|c: char| c.is_ascii_uppercase()) + // We don't want to flag CONSTANTS() + && name != name.to_uppercase() + && !allow_list.contains(name) + { + capital_load_globals.insert(lvalue_id, name.to_string()); + } + } + InstructionValue::CallExpression { callee, loc, .. } => { + let callee_id = callee.identifier; + if let Some(callee_name) = capital_load_globals.get(&callee_id) { + env.record_error(CompilerErrorDetail { + category: ErrorCategory::CapitalizedCalls, + reason: reason.to_string(), + description: Some(format!("{callee_name} may be a component")), + loc: *loc, + suggestions: None, + })?; + continue; + } + } + InstructionValue::PropertyLoad { property, .. } => { + if let PropertyLiteral::String(prop_name) = property { + if prop_name.starts_with(|c: char| c.is_ascii_uppercase()) { + capitalized_properties.insert(lvalue_id, prop_name.clone()); + } + } + } + InstructionValue::MethodCall { property, loc, .. } => { + let property_id = property.identifier; + if let Some(prop_name) = capitalized_properties.get(&property_id) { + env.record_error(CompilerErrorDetail { + category: ErrorCategory::CapitalizedCalls, + reason: reason.to_string(), + description: Some(format!("{prop_name} may be a component")), + loc: *loc, + suggestions: None, + })?; + } + } + _ => {} + } + } + } + Ok(()) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_derived_computations_in_effects.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_derived_computations_in_effects.rs new file mode 100644 index 0000000000000..8979b85d342ae --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_derived_computations_in_effects.rs @@ -0,0 +1,1396 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Validates that useEffect is not used for derived computations which could/should +//! be performed in render. +//! +//! See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state +//! +//! Port of ValidateNoDerivedComputationsInEffects_exp.ts. + +use crate::react_compiler_utils::{FxIndexMap, FxIndexSet}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, CompilerErrorDetail, ErrorCategory, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::{ + each_instruction_lvalue_ids, each_instruction_operand as canonical_each_instruction_operand, +}; +use crate::react_compiler_hir::{ + ArrayElement, BlockId, Effect, EvaluationOrder, FunctionId, HirFunction, Identifier, + IdentifierId, IdentifierName, InstructionValue, ParamPattern, PlaceOrSpread, ReactFunctionType, + ReturnVariant, SourceLocation, Type, is_set_state_type, is_use_effect_hook_type, + is_use_ref_type, is_use_state_type, +}; + +/// Get the user-visible name for an identifier, matching Babel's +/// loc.identifierName behavior. First checks the identifier's own name, +/// then falls back to extracting the name from the source code at the +/// given source location. This handles SSA identifiers whose names were +/// lost during compiler passes. +fn get_identifier_name_with_loc( + id: IdentifierId, + identifiers: &[Identifier], + loc: &Option, + source_code: Option<&str>, +) -> Option { + let ident = &identifiers[id.0 as usize]; + match &ident.name { + Some(IdentifierName::Named(name)) | Some(IdentifierName::Promoted(name)) => { + return Some(name.clone()); + } + _ => {} + } + // Fall back: find another identifier with the same declaration_id that has a name. + let decl_id = ident.declaration_id; + for other in identifiers { + if other.declaration_id == decl_id { + match &other.name { + Some(IdentifierName::Named(name)) | Some(IdentifierName::Promoted(name)) => { + return Some(name.clone()); + } + _ => {} + } + } + } + // Fall back to extracting from source code using UTF-16 code unit indices. + // Babel/JS positions use UTF-16 code unit offsets, but Rust strings are UTF-8, + // so we need to convert between the two. + if let (Some(loc), Some(code)) = (loc, source_code) { + let start_utf16 = loc.start.index? as usize; + let end_utf16 = loc.end.index? as usize; + if start_utf16 < end_utf16 { + // Convert UTF-16 code unit offsets to UTF-8 byte offsets + let mut utf16_pos = 0usize; + let mut byte_start = None; + let mut byte_end = None; + for (byte_idx, ch) in code.char_indices() { + if utf16_pos == start_utf16 { + byte_start = Some(byte_idx); + } + if utf16_pos == end_utf16 { + byte_end = Some(byte_idx); + break; + } + utf16_pos += ch.len_utf16(); + } + // Handle end at the very end of string + if utf16_pos == end_utf16 && byte_end.is_none() { + byte_end = Some(code.len()); + } + if let (Some(start), Some(end)) = (byte_start, byte_end) { + let slice = &code[start..end]; + if !slice.is_empty() + && slice.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '$') + { + return Some(slice.to_string()); + } + } + } + } + None +} + +const MAX_FIXPOINT_ITERATIONS: usize = 100; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TypeOfValue { + Ignored, + FromProps, + FromState, + FromPropsAndState, +} + +#[derive(Debug, Clone)] +struct DerivationMetadata { + type_of_value: TypeOfValue, + place_identifier: IdentifierId, + place_name: Option, + source_ids: FxIndexSet, + is_state_source: bool, +} + +/// Metadata about a useEffect call site. +struct EffectMetadata { + effect_func_id: FunctionId, + dep_elements: Vec, +} + +#[derive(Debug, Clone)] +struct DepElement { + identifier: IdentifierId, + loc: Option, +} + +struct ValidationContext { + /// Map from lvalue identifier to the FunctionId of function expressions + functions: FxHashMap, + /// Map from lvalue identifier to ArrayExpression elements (candidate deps) + candidate_dependencies: FxHashMap>, + derivation_cache: DerivationCache, + effects_cache: FxHashMap, + set_state_loads: FxHashMap>, + set_state_usages: FxHashMap>, +} + +/// A hashable key for SourceLocation to use in FxHashSet +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct LocKey { + start_line: u32, + start_col: u32, + end_line: u32, + end_col: u32, +} + +impl LocKey { + fn from_loc(loc: &Option) -> Self { + match loc { + Some(loc) => LocKey { + start_line: loc.start.line, + start_col: loc.start.column, + end_line: loc.end.line, + end_col: loc.end.column, + }, + None => LocKey { start_line: 0, start_col: 0, end_line: 0, end_col: 0 }, + } + } +} + +#[derive(Debug, Clone)] +struct DerivationCache { + has_changes: bool, + cache: FxHashMap, + previous_cache: Option>, +} + +impl DerivationCache { + fn new() -> Self { + DerivationCache { has_changes: false, cache: FxHashMap::default(), previous_cache: None } + } + + fn take_snapshot(&mut self) { + let mut prev = FxHashMap::default(); + for (key, value) in &self.cache { + prev.insert( + *key, + DerivationMetadata { + place_identifier: value.place_identifier, + place_name: value.place_name.clone(), + source_ids: value.source_ids.clone(), + type_of_value: value.type_of_value, + is_state_source: value.is_state_source, + }, + ); + } + self.previous_cache = Some(prev); + } + + fn check_for_changes(&mut self) { + let prev = match &self.previous_cache { + Some(p) => p, + None => { + self.has_changes = true; + return; + } + }; + + for (key, value) in &self.cache { + match prev.get(key) { + None => { + self.has_changes = true; + return; + } + Some(prev_value) => { + if !is_derivation_equal(prev_value, value) { + self.has_changes = true; + return; + } + } + } + } + + if self.cache.len() != prev.len() { + self.has_changes = true; + return; + } + + self.has_changes = false; + } + + fn snapshot(&mut self) -> bool { + let has_changes = self.has_changes; + self.has_changes = false; + has_changes + } + + fn add_derivation_entry( + &mut self, + derived_id: IdentifierId, + derived_name: Option, + source_ids: FxIndexSet, + type_of_value: TypeOfValue, + is_state_source: bool, + ) { + let mut final_is_source = is_state_source; + if !final_is_source { + for source_id in &source_ids { + if let Some(source_metadata) = self.cache.get(source_id) { + if source_metadata.is_state_source + && !matches!(&source_metadata.place_name, Some(IdentifierName::Named(_))) + { + final_is_source = true; + break; + } + } + } + } + + self.cache.insert( + derived_id, + DerivationMetadata { + place_identifier: derived_id, + place_name: derived_name, + source_ids, + type_of_value, + is_state_source: final_is_source, + }, + ); + } +} + +fn is_derivation_equal(a: &DerivationMetadata, b: &DerivationMetadata) -> bool { + if a.type_of_value != b.type_of_value { + return false; + } + if a.source_ids.len() != b.source_ids.len() { + return false; + } + for id in &a.source_ids { + if !b.source_ids.contains(id) { + return false; + } + } + true +} + +fn join_value(lvalue_type: TypeOfValue, value_type: TypeOfValue) -> TypeOfValue { + if lvalue_type == TypeOfValue::Ignored { + return value_type; + } + if value_type == TypeOfValue::Ignored { + return lvalue_type; + } + if lvalue_type == value_type { + return lvalue_type; + } + TypeOfValue::FromPropsAndState +} + +fn get_root_set_state( + key: IdentifierId, + loads: &FxHashMap>, + visited: &mut FxHashSet, +) -> Option { + if visited.contains(&key) { + return None; + } + visited.insert(key); + + match loads.get(&key) { + None => None, + Some(None) => Some(key), + Some(Some(parent_id)) => get_root_set_state(*parent_id, loads, visited), + } +} + +fn maybe_record_set_state_for_instr( + instr: &crate::react_compiler_hir::Instruction, + env: &Environment, + set_state_loads: &mut FxHashMap>, + set_state_usages: &mut FxHashMap>, +) { + let identifiers = &env.identifiers; + let types = &env.types; + + let all_lvalues = each_instruction_lvalue_ids(instr); + for &lvalue_id in &all_lvalues { + // Check if this is a LoadLocal from a known setState + if let InstructionValue::LoadLocal { place, .. } = &instr.value { + if set_state_loads.contains_key(&place.identifier) { + set_state_loads.insert(lvalue_id, Some(place.identifier)); + } else { + // Only check root setState if not a LoadLocal from a known chain + let lvalue_ident = &identifiers[lvalue_id.0 as usize]; + let lvalue_ty = &types[lvalue_ident.type_.0 as usize]; + if is_set_state_type(lvalue_ty) { + set_state_loads.insert(lvalue_id, None); + } + } + } else { + // Check if lvalue is a setState type (root setState) + let lvalue_ident = &identifiers[lvalue_id.0 as usize]; + let lvalue_ty = &types[lvalue_ident.type_.0 as usize]; + if is_set_state_type(lvalue_ty) { + set_state_loads.insert(lvalue_id, None); + } + } + + let root = get_root_set_state(lvalue_id, set_state_loads, &mut FxHashSet::default()); + if let Some(root_id) = root { + set_state_usages.entry(root_id).or_insert_with(|| { + let mut set = FxHashSet::default(); + set.insert(LocKey::from_loc(&instr.lvalue.loc)); + set + }); + } + } +} + +fn is_mutable_at( + env: &Environment, + eval_order: EvaluationOrder, + identifier_id: IdentifierId, +) -> bool { + env.identifiers[identifier_id.0 as usize].mutable_range.contains(eval_order) +} + +pub fn validate_no_derived_computations_in_effects_exp( + func: &HirFunction, + env: &Environment, +) -> Result { + let identifiers = &env.identifiers; + + let mut context = ValidationContext { + functions: FxHashMap::default(), + candidate_dependencies: FxHashMap::default(), + derivation_cache: DerivationCache::new(), + effects_cache: FxHashMap::default(), + set_state_loads: FxHashMap::default(), + set_state_usages: FxHashMap::default(), + }; + + // Initialize derivation cache based on function type + if func.fn_type == ReactFunctionType::Hook { + for param in &func.params { + if let ParamPattern::Place(place) = param { + let name = identifiers[place.identifier.0 as usize].name.clone(); + context.derivation_cache.cache.insert( + place.identifier, + DerivationMetadata { + place_identifier: place.identifier, + place_name: name, + source_ids: FxIndexSet::default(), + type_of_value: TypeOfValue::FromProps, + is_state_source: true, + }, + ); + } + } + } else if func.fn_type == ReactFunctionType::Component { + if let Some(param) = func.params.first() { + if let ParamPattern::Place(place) = param { + let name = identifiers[place.identifier.0 as usize].name.clone(); + context.derivation_cache.cache.insert( + place.identifier, + DerivationMetadata { + place_identifier: place.identifier, + place_name: name, + source_ids: FxIndexSet::default(), + type_of_value: TypeOfValue::FromProps, + is_state_source: true, + }, + ); + } + } + } + + // Fixpoint iteration + let mut is_first_pass = true; + let mut iteration_count = 0; + loop { + context.derivation_cache.take_snapshot(); + + for (_block_id, block) in &func.body.blocks { + record_phi_derivations(block, &mut context, env); + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + record_instruction_derivations(instr, &mut context, is_first_pass, func, env)?; + } + } + + context.derivation_cache.check_for_changes(); + is_first_pass = false; + iteration_count += 1; + assert!( + iteration_count < MAX_FIXPOINT_ITERATIONS, + "[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge." + ); + + if !context.derivation_cache.snapshot() { + break; + } + } + + // Validate all effect sites + let mut errors = CompilerError::new(); + let effects_cache: Vec<(IdentifierId, FunctionId, Vec)> = context + .effects_cache + .iter() + .map(|(k, v)| (*k, v.effect_func_id, v.dep_elements.clone())) + .collect(); + + for (_key, effect_func_id, dep_elements) in &effects_cache { + validate_effect(*effect_func_id, dep_elements, &mut context, func, env, &mut errors); + } + + Ok(errors) +} + +fn record_phi_derivations( + block: &crate::react_compiler_hir::BasicBlock, + context: &mut ValidationContext, + env: &Environment, +) { + let identifiers = &env.identifiers; + for phi in &block.phis { + let mut type_of_value = TypeOfValue::Ignored; + let mut source_ids: FxIndexSet = FxIndexSet::default(); + + for (_block_id, operand) in &phi.operands { + if let Some(operand_metadata) = context.derivation_cache.cache.get(&operand.identifier) + { + type_of_value = join_value(type_of_value, operand_metadata.type_of_value); + source_ids.insert(operand.identifier); + } + } + + if type_of_value != TypeOfValue::Ignored { + let name = identifiers[phi.place.identifier.0 as usize].name.clone(); + context.derivation_cache.add_derivation_entry( + phi.place.identifier, + name, + source_ids, + type_of_value, + false, + ); + } + } +} + +fn record_instruction_derivations( + instr: &crate::react_compiler_hir::Instruction, + context: &mut ValidationContext, + is_first_pass: bool, + _outer_func: &HirFunction, + env: &Environment, +) -> Result<(), CompilerDiagnostic> { + let identifiers = &env.identifiers; + let types = &env.types; + let functions = &env.functions; + let lvalue_id = instr.lvalue.identifier; + + // maybeRecordSetState + maybe_record_set_state_for_instr( + instr, + env, + &mut context.set_state_loads, + &mut context.set_state_usages, + ); + + let mut type_of_value = TypeOfValue::Ignored; + let is_source = false; + let mut sources: FxIndexSet = FxIndexSet::default(); + + match &instr.value { + InstructionValue::FunctionExpression { lowered_func, .. } => { + context.functions.insert(lvalue_id, lowered_func.func); + // Recurse into the inner function + let inner_func = &functions[lowered_func.func.0 as usize]; + for (_block_id, block) in &inner_func.body.blocks { + record_phi_derivations(block, context, env); + for &inner_instr_id in &block.instructions { + let inner_instr = &inner_func.instructions[inner_instr_id.0 as usize]; + record_instruction_derivations( + inner_instr, + context, + is_first_pass, + inner_func, + env, + )?; + } + } + } + InstructionValue::CallExpression { callee, args, .. } => { + let callee_type = &types[identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if is_use_effect_hook_type(callee_type) && args.len() == 2 { + if let ( + crate::react_compiler_hir::PlaceOrSpread::Place(arg0), + crate::react_compiler_hir::PlaceOrSpread::Place(arg1), + ) = (&args[0], &args[1]) + { + let effect_function = context.functions.get(&arg0.identifier).copied(); + let deps = context.candidate_dependencies.get(&arg1.identifier).cloned(); + if let (Some(effect_func_id), Some(dep_elements)) = (effect_function, deps) { + context.effects_cache.insert( + arg0.identifier, + EffectMetadata { effect_func_id, dep_elements }, + ); + } + } + } + + // Check if lvalue is useState type + let lvalue_type = &types[identifiers[lvalue_id.0 as usize].type_.0 as usize]; + if is_use_state_type(lvalue_type) { + let name = identifiers[lvalue_id.0 as usize].name.clone(); + context.derivation_cache.add_derivation_entry( + lvalue_id, + name, + FxIndexSet::default(), + TypeOfValue::FromState, + true, + ); + return Ok(()); + } + } + InstructionValue::MethodCall { property, args, .. } => { + let prop_type = &types[identifiers[property.identifier.0 as usize].type_.0 as usize]; + if is_use_effect_hook_type(prop_type) && args.len() == 2 { + if let ( + crate::react_compiler_hir::PlaceOrSpread::Place(arg0), + crate::react_compiler_hir::PlaceOrSpread::Place(arg1), + ) = (&args[0], &args[1]) + { + let effect_function = context.functions.get(&arg0.identifier).copied(); + let deps = context.candidate_dependencies.get(&arg1.identifier).cloned(); + if let (Some(effect_func_id), Some(dep_elements)) = (effect_function, deps) { + context.effects_cache.insert( + arg0.identifier, + EffectMetadata { effect_func_id, dep_elements }, + ); + } + } + } + + // Check if lvalue is useState type + let lvalue_type = &types[identifiers[lvalue_id.0 as usize].type_.0 as usize]; + if is_use_state_type(lvalue_type) { + let name = identifiers[lvalue_id.0 as usize].name.clone(); + context.derivation_cache.add_derivation_entry( + lvalue_id, + name, + FxIndexSet::default(), + TypeOfValue::FromState, + true, + ); + return Ok(()); + } + } + InstructionValue::ArrayExpression { elements, .. } => { + let dep_elements: Vec = elements + .iter() + .filter_map(|el| match el { + ArrayElement::Place(p) => { + Some(DepElement { identifier: p.identifier, loc: p.loc }) + } + _ => None, + }) + .collect(); + context.candidate_dependencies.insert(lvalue_id, dep_elements); + } + _ => {} + } + + // Collect operand derivations + for (operand_id, operand_loc) in each_instruction_operand(instr, env) { + // Track setState usages + if context.set_state_loads.contains_key(&operand_id) { + let root = + get_root_set_state(operand_id, &context.set_state_loads, &mut FxHashSet::default()); + if let Some(root_id) = root { + if let Some(usages) = context.set_state_usages.get_mut(&root_id) { + usages.insert(LocKey::from_loc(&operand_loc)); + } + } + } + + if let Some(operand_metadata) = context.derivation_cache.cache.get(&operand_id) { + type_of_value = join_value(type_of_value, operand_metadata.type_of_value); + sources.insert(operand_id); + } + } + + if type_of_value == TypeOfValue::Ignored { + return Ok(()); + } + + // Record derivation for ALL lvalue places (including destructured variables) + for &lv_id in &each_instruction_lvalue_ids(instr) { + let name = identifiers[lv_id.0 as usize].name.clone(); + context.derivation_cache.add_derivation_entry( + lv_id, + name, + sources.clone(), + type_of_value, + is_source, + ); + } + + if matches!(&instr.value, InstructionValue::FunctionExpression { .. }) { + // Don't record mutation effects for FunctionExpressions + return Ok(()); + } + + // Handle mutable operands + for operand in each_instruction_operand_with_effect(instr, env) { + if operand.effect.is_mutable() { + if is_mutable_at(env, instr.id, operand.id) { + if let Some(existing) = context.derivation_cache.cache.get_mut(&operand.id) { + existing.type_of_value = join_value(type_of_value, existing.type_of_value); + } else { + let name = identifiers[operand.id.0 as usize].name.clone(); + context.derivation_cache.add_derivation_entry( + operand.id, + name, + sources.clone(), + type_of_value, + false, + ); + } + } + } else if matches!(operand.effect, Effect::Unknown) { + return Err(CompilerDiagnostic::new( + ErrorCategory::Invariant, + "Unexpected unknown effect", + None, + )); + } + // Freeze | Read => no-op + } + Ok(()) +} + +struct OperandWithEffect { + id: IdentifierId, + effect: Effect, +} + +/// Collects operand (IdentifierId, loc) pairs from an instruction. +/// Thin wrapper around canonical `each_instruction_operand` that maps Places to (id, loc) pairs. +fn each_instruction_operand( + instr: &crate::react_compiler_hir::Instruction, + env: &Environment, +) -> Vec<(IdentifierId, Option)> { + canonical_each_instruction_operand(instr, env) + .into_iter() + .map(|place| (place.identifier, place.loc)) + .collect() +} + +/// Collects operands with their effects. +/// Thin wrapper around canonical `each_instruction_operand` that maps Places to OperandWithEffect. +fn each_instruction_operand_with_effect( + instr: &crate::react_compiler_hir::Instruction, + env: &Environment, +) -> Vec { + canonical_each_instruction_operand(instr, env) + .into_iter() + .map(|place| OperandWithEffect { id: place.identifier, effect: place.effect }) + .collect() +} + +// ============================================================================= +// Tree building and rendering (for error messages) +// ============================================================================= + +struct TreeNode { + name: String, + type_of_value: TypeOfValue, + is_source: bool, + children: Vec, +} + +fn build_tree_node( + source_id: IdentifierId, + context: &ValidationContext, + visited: &FxHashSet, +) -> Vec { + let source_metadata = match context.derivation_cache.cache.get(&source_id) { + Some(m) => m, + None => return Vec::new(), + }; + + if source_metadata.is_state_source { + if let Some(IdentifierName::Named(name)) = &source_metadata.place_name { + return vec![TreeNode { + name: name.clone(), + type_of_value: source_metadata.type_of_value, + is_source: true, + children: Vec::new(), + }]; + } + } + + let mut children: Vec = Vec::new(); + let mut named_siblings: FxIndexSet = FxIndexSet::default(); + + for child_id in &source_metadata.source_ids { + assert_ne!( + *child_id, source_id, + "Unexpected self-reference: a value should not have itself as a source" + ); + + let mut new_visited = visited.clone(); + if let Some(IdentifierName::Named(name)) = &source_metadata.place_name { + new_visited.insert(name.clone()); + } + + let child_nodes = build_tree_node(*child_id, context, &new_visited); + for child_node in child_nodes { + if !named_siblings.contains(&child_node.name) { + named_siblings.insert(child_node.name.clone()); + children.push(child_node); + } + } + } + + if let Some(IdentifierName::Named(name)) = &source_metadata.place_name { + if !visited.contains(name) { + return vec![TreeNode { + name: name.clone(), + type_of_value: source_metadata.type_of_value, + is_source: source_metadata.is_state_source, + children, + }]; + } + } + + children +} + +fn render_tree( + node: &TreeNode, + indent: &str, + is_last: bool, + props_set: &mut FxIndexSet, + state_set: &mut FxIndexSet, +) -> String { + let prefix = format!( + "{}{}", + indent, + if is_last { "\u{2514}\u{2500}\u{2500} " } else { "\u{251c}\u{2500}\u{2500} " } + ); + let child_indent = format!("{}{}", indent, if is_last { " " } else { "\u{2502} " }); + + let mut result = format!("{}{}", prefix, node.name); + + if node.is_source { + let type_label = match node.type_of_value { + TypeOfValue::FromProps => { + props_set.insert(node.name.clone()); + "Prop" + } + TypeOfValue::FromState => { + state_set.insert(node.name.clone()); + "State" + } + _ => { + props_set.insert(node.name.clone()); + state_set.insert(node.name.clone()); + "Prop and State" + } + }; + result += &format!(" ({})", type_label); + } + + if !node.children.is_empty() { + result += "\n"; + for (index, child) in node.children.iter().enumerate() { + let is_last_child = index == node.children.len() - 1; + result += &render_tree(child, &child_indent, is_last_child, props_set, state_set); + if index < node.children.len() - 1 { + result += "\n"; + } + } + } + + result +} + +fn get_fn_local_deps( + func_id: Option, + env: &Environment, +) -> Option> { + let func_id = func_id?; + let inner = &env.functions[func_id.0 as usize]; + let mut deps: FxHashSet = FxHashSet::default(); + + for (_block_id, block) in &inner.body.blocks { + for &instr_id in &block.instructions { + let instr = &inner.instructions[instr_id.0 as usize]; + if let InstructionValue::LoadLocal { place, .. } = &instr.value { + deps.insert(place.identifier); + } + } + } + + Some(deps) +} + +fn validate_effect( + effect_func_id: FunctionId, + dependencies: &[DepElement], + context: &mut ValidationContext, + _outer_func: &HirFunction, + env: &Environment, + errors: &mut CompilerError, +) { + let identifiers = &env.identifiers; + let types = &env.types; + let functions = &env.functions; + let effect_function = &functions[effect_func_id.0 as usize]; + let mut seen_blocks: FxHashSet = FxHashSet::default(); + + struct DerivedSetStateCall { + callee_loc: Option, + callee_id: IdentifierId, + callee_identifier_name: Option, + source_ids: FxIndexSet, + } + + let mut effect_derived_set_state_calls: Vec = Vec::new(); + let mut effect_set_state_usages: FxHashMap> = + FxHashMap::default(); + + // Consider setStates in the effect's dependency array as being part of effectSetStateUsages + for dep in dependencies { + let root = + get_root_set_state(dep.identifier, &context.set_state_loads, &mut FxHashSet::default()); + if let Some(root_id) = root { + let mut set = FxHashSet::default(); + set.insert(LocKey::from_loc(&dep.loc)); + effect_set_state_usages.insert(root_id, set); + } + } + + let mut cleanup_function_deps: Option> = None; + let mut globals: FxHashSet = FxHashSet::default(); + + for (_block_id, block) in &effect_function.body.blocks { + // Check for return -> cleanup function + if let crate::react_compiler_hir::Terminal::Return { + value, + return_variant: ReturnVariant::Explicit, + .. + } = &block.terminal + { + let func_id = context.functions.get(&value.identifier).copied(); + cleanup_function_deps = get_fn_local_deps(func_id, env); + } + + // Skip if block has a back edge (pred not yet seen) + let has_back_edge = block.preds.iter().any(|pred| !seen_blocks.contains(pred)); + if has_back_edge { + return; + } + + for &instr_id in &block.instructions { + let instr = &effect_function.instructions[instr_id.0 as usize]; + + // Early return if any instruction derives from a ref + let lvalue_type = + &types[identifiers[instr.lvalue.identifier.0 as usize].type_.0 as usize]; + if is_use_ref_type(lvalue_type) { + return; + } + + // maybeRecordSetState for effect instructions + maybe_record_set_state_for_instr( + instr, + env, + &mut context.set_state_loads, + &mut effect_set_state_usages, + ); + + // Track setState usages for operands + for (operand_id, operand_loc) in each_instruction_operand(instr, env) { + if context.set_state_loads.contains_key(&operand_id) { + let root = get_root_set_state( + operand_id, + &context.set_state_loads, + &mut FxHashSet::default(), + ); + if let Some(root_id) = root { + if let Some(usages) = effect_set_state_usages.get_mut(&root_id) { + usages.insert(LocKey::from_loc(&operand_loc)); + } + } + } + } + + match &instr.value { + InstructionValue::CallExpression { callee, args, .. } => { + let callee_type = + &types[identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if is_set_state_type(callee_type) && args.len() == 1 { + if let crate::react_compiler_hir::PlaceOrSpread::Place(arg0) = &args[0] { + let callee_metadata = + context.derivation_cache.cache.get(&callee.identifier); + + // If the setState comes from a source other than local state, skip + if let Some(cm) = callee_metadata { + if cm.type_of_value != TypeOfValue::FromState { + continue; + } + } else { + continue; + } + + let arg_metadata = context.derivation_cache.cache.get(&arg0.identifier); + if let Some(am) = arg_metadata { + // Get the user-visible identifier name, matching Babel's + // loc.identifierName. Falls back to extracting from source code. + let callee_ident_name = get_identifier_name_with_loc( + callee.identifier, + identifiers, + &callee.loc, + env.code.as_deref(), + ); + effect_derived_set_state_calls.push(DerivedSetStateCall { + callee_loc: callee.loc, + callee_id: callee.identifier, + callee_identifier_name: callee_ident_name, + source_ids: am.source_ids.clone(), + }); + } + } + } else { + // Check if callee is from props/propsAndState -> bail + let callee_metadata = + context.derivation_cache.cache.get(&callee.identifier); + if let Some(cm) = callee_metadata { + if cm.type_of_value == TypeOfValue::FromProps + || cm.type_of_value == TypeOfValue::FromPropsAndState + { + return; + } + } + + if globals.contains(&callee.identifier) { + return; + } + } + } + InstructionValue::LoadGlobal { .. } => { + globals.insert(instr.lvalue.identifier); + for (operand_id, _) in each_instruction_operand(instr, env) { + globals.insert(operand_id); + } + } + _ => {} + } + } + seen_blocks.insert(block.id); + } + + // Emit errors for derived setState calls + for derived in &effect_derived_set_state_calls { + let root_set_state_call = get_root_set_state( + derived.callee_id, + &context.set_state_loads, + &mut FxHashSet::default(), + ); + if let Some(root_id) = root_set_state_call { + let effect_usage_count = + effect_set_state_usages.get(&root_id).map(|s| s.len()).unwrap_or(0); + let total_usage_count = + context.set_state_usages.get(&root_id).map(|s| s.len()).unwrap_or(0); + if effect_set_state_usages.contains_key(&root_id) + && context.set_state_usages.contains_key(&root_id) + && effect_usage_count == total_usage_count - 1 + { + let mut props_set: FxIndexSet = FxIndexSet::default(); + let mut state_set: FxIndexSet = FxIndexSet::default(); + + let mut root_nodes_map: FxIndexMap = FxIndexMap::default(); + for id in &derived.source_ids { + let nodes = build_tree_node(*id, context, &FxHashSet::default()); + for node in nodes { + if !root_nodes_map.contains_key(&node.name) { + root_nodes_map.insert(node.name.clone(), node); + } + } + } + let root_nodes: Vec<&TreeNode> = root_nodes_map.values().collect(); + + let trees: Vec = root_nodes + .iter() + .enumerate() + .map(|(index, node)| { + render_tree( + node, + "", + index == root_nodes.len() - 1, + &mut props_set, + &mut state_set, + ) + }) + .collect(); + + // Check cleanup function dependencies + let should_skip = if let Some(ref cleanup_deps) = cleanup_function_deps { + derived.source_ids.iter().any(|dep| cleanup_deps.contains(dep)) + } else { + false + }; + if should_skip { + return; + } + + let mut root_sources = String::new(); + if !props_set.is_empty() { + let props_list: Vec<&str> = props_set.iter().map(|s| s.as_str()).collect(); + root_sources += &format!("Props: [{}]", props_list.join(", ")); + } + if !state_set.is_empty() { + if !root_sources.is_empty() { + root_sources += "\n"; + } + let state_list: Vec<&str> = state_set.iter().map(|s| s.as_str()).collect(); + root_sources += &format!("State: [{}]", state_list.join(", ")); + } + + let description = format!( + "Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\n\ + This setState call is setting a derived value that depends on the following reactive sources:\n\n\ + {}\n\n\ + Data Flow Tree:\n\ + {}\n\n\ + See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state", + root_sources, + trees.join("\n"), + ); + + errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::EffectDerivationsOfState, + "You might not need an effect. Derive values in render, not effects.", + Some(description), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: derived.callee_loc, + message: Some( + "This should be computed during render, not in an effect".to_string(), + ), + identifier_name: derived.callee_identifier_name.clone(), + }), + ); + } + } + } +} + +// ============================================================================= +// Non-exp version: ValidateNoDerivedComputationsInEffects +// Port of ValidateNoDerivedComputationsInEffects.ts +// ============================================================================= + +/// Non-experimental version of the derived-computations-in-effects validation. +/// Records errors directly on the Environment (matching TS `env.recordError()` behavior). +pub fn validate_no_derived_computations_in_effects( + func: &HirFunction, + env: &mut Environment, +) -> Result<(), CompilerError> { + // Phase 1: Collect effect call sites (func_id + resolved deps). + // Done with only immutable borrows of env fields. + let effects_to_validate: Vec<(FunctionId, Vec)> = { + let ids = &env.identifiers; + let tys = &env.types; + let mut candidate_deps: FxHashMap> = FxHashMap::default(); + let mut functions_map: FxHashMap = FxHashMap::default(); + let mut locals_map: FxHashMap = FxHashMap::default(); + let mut result = Vec::new(); + + for (_, block) in &func.body.blocks { + for &iid in &block.instructions { + let instr = &func.instructions[iid.0 as usize]; + match &instr.value { + InstructionValue::LoadLocal { place, .. } => { + locals_map.insert(instr.lvalue.identifier, place.identifier); + } + InstructionValue::ArrayExpression { elements, .. } => { + let elem_ids: Vec = elements + .iter() + .filter_map(|e| match e { + ArrayElement::Place(p) => Some(p.identifier), + _ => None, + }) + .collect(); + if elem_ids.len() == elements.len() { + candidate_deps.insert(instr.lvalue.identifier, elem_ids); + } + } + InstructionValue::FunctionExpression { lowered_func, .. } => { + functions_map.insert(instr.lvalue.identifier, lowered_func.func); + } + InstructionValue::CallExpression { callee, args, .. } => { + let callee_ty = &tys[ids[callee.identifier.0 as usize].type_.0 as usize]; + if is_use_effect_hook_type(callee_ty) && args.len() == 2 { + if let (PlaceOrSpread::Place(arg0), PlaceOrSpread::Place(arg1)) = + (&args[0], &args[1]) + { + if let (Some(&func_id), Some(dep_elements)) = ( + functions_map.get(&arg0.identifier), + candidate_deps.get(&arg1.identifier), + ) { + if !dep_elements.is_empty() { + let resolved: Vec = dep_elements + .iter() + .map(|d| locals_map.get(d).copied().unwrap_or(*d)) + .collect(); + result.push((func_id, resolved)); + } + } + } + } + } + InstructionValue::MethodCall { property, args, .. } => { + let callee_ty = &tys[ids[property.identifier.0 as usize].type_.0 as usize]; + if is_use_effect_hook_type(callee_ty) && args.len() == 2 { + if let (PlaceOrSpread::Place(arg0), PlaceOrSpread::Place(arg1)) = + (&args[0], &args[1]) + { + if let (Some(&func_id), Some(dep_elements)) = ( + functions_map.get(&arg0.identifier), + candidate_deps.get(&arg1.identifier), + ) { + if !dep_elements.is_empty() { + let resolved: Vec = dep_elements + .iter() + .map(|d| locals_map.get(d).copied().unwrap_or(*d)) + .collect(); + result.push((func_id, resolved)); + } + } + } + } + } + _ => {} + } + } + } + result + }; + + // Phase 2: Validate each collected effect and record error details. + // Uses ErrorDetail (flat loc format) to match TS behavior where + // env.recordError(new CompilerErrorDetail({...})) is used. + for (func_id, resolved_deps) in effects_to_validate { + let details = validate_effect_non_exp( + &env.functions[func_id.0 as usize], + &resolved_deps, + &env.identifiers, + &env.types, + ); + for detail in details { + env.record_error(detail)?; + } + } + Ok(()) +} + +fn validate_effect_non_exp( + effect_func: &HirFunction, + effect_deps: &[IdentifierId], + ids: &[Identifier], + tys: &[Type], +) -> Vec { + // Check that the effect function only captures effect deps and setState + for ctx in &effect_func.context { + let ctx_ty = &tys[ids[ctx.identifier.0 as usize].type_.0 as usize]; + if is_set_state_type(ctx_ty) { + continue; + } else if effect_deps.iter().any(|d| *d == ctx.identifier) { + continue; + } else { + return Vec::new(); + } + } + + // Check that all effect deps are actually used in the function + for dep in effect_deps { + if !effect_func.context.iter().any(|c| c.identifier == *dep) { + return Vec::new(); + } + } + + let mut seen_blocks: FxHashSet = FxHashSet::default(); + let mut dep_values: FxHashMap> = FxHashMap::default(); + for dep in effect_deps { + dep_values.insert(*dep, vec![*dep]); + } + + let mut set_state_locs: Vec = Vec::new(); + + for (_, block) in &effect_func.body.blocks { + for &pred in &block.preds { + if !seen_blocks.contains(&pred) { + return Vec::new(); + } + } + + for phi in &block.phis { + let mut aggregate: FxHashSet = FxHashSet::default(); + for operand in phi.operands.values() { + if let Some(deps) = dep_values.get(&operand.identifier) { + for d in deps { + aggregate.insert(*d); + } + } + } + if !aggregate.is_empty() { + dep_values.insert(phi.place.identifier, aggregate.into_iter().collect()); + } + } + + for &iid in &block.instructions { + let instr = &effect_func.instructions[iid.0 as usize]; + match &instr.value { + InstructionValue::Primitive { .. } + | InstructionValue::JSXText { .. } + | InstructionValue::LoadGlobal { .. } => {} + InstructionValue::LoadLocal { place, .. } => { + if let Some(deps) = dep_values.get(&place.identifier) { + dep_values.insert(instr.lvalue.identifier, deps.clone()); + } + } + InstructionValue::ComputedLoad { .. } + | InstructionValue::PropertyLoad { .. } + | InstructionValue::BinaryExpression { .. } + | InstructionValue::TemplateLiteral { .. } + | InstructionValue::CallExpression { .. } + | InstructionValue::MethodCall { .. } => { + let mut aggregate: FxHashSet = FxHashSet::default(); + for operand in non_exp_value_operands(&instr.value) { + if let Some(deps) = dep_values.get(&operand) { + for d in deps { + aggregate.insert(*d); + } + } + } + if !aggregate.is_empty() { + dep_values.insert(instr.lvalue.identifier, aggregate.into_iter().collect()); + } + + if let InstructionValue::CallExpression { callee, args, .. } = &instr.value { + let callee_ty = &tys[ids[callee.identifier.0 as usize].type_.0 as usize]; + if is_set_state_type(callee_ty) && args.len() == 1 { + if let PlaceOrSpread::Place(arg) = &args[0] { + if let Some(deps) = dep_values.get(&arg.identifier) { + let dep_set: FxHashSet<_> = deps.iter().collect(); + if dep_set.len() == effect_deps.len() { + if let Some(loc) = callee.loc { + set_state_locs.push(loc); + } + } else { + return Vec::new(); + } + } else { + return Vec::new(); + } + } + } + } + } + _ => { + return Vec::new(); + } + } + } + + match &block.terminal { + crate::react_compiler_hir::Terminal::Return { value, .. } + | crate::react_compiler_hir::Terminal::Throw { value, .. } => { + if dep_values.contains_key(&value.identifier) { + return Vec::new(); + } + } + crate::react_compiler_hir::Terminal::If { test, .. } + | crate::react_compiler_hir::Terminal::Branch { test, .. } => { + if dep_values.contains_key(&test.identifier) { + return Vec::new(); + } + } + crate::react_compiler_hir::Terminal::Switch { test, .. } => { + if dep_values.contains_key(&test.identifier) { + return Vec::new(); + } + } + _ => {} + } + + seen_blocks.insert(block.id); + } + + set_state_locs + .into_iter() + .map(|loc| { + CompilerErrorDetail { + category: ErrorCategory::EffectDerivationsOfState, + reason: "Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)".to_string(), + description: None, + loc: Some(loc), + suggestions: None, + } + }) + .collect() +} + +/// Collects operand IdentifierIds for a subset of instruction variants used +/// by `validate_effect_non_exp`. +/// +/// NOTE: This intentionally does NOT use the canonical `each_instruction_value_operand` +/// because: (1) `validate_effect_non_exp` only matches specific variants +/// (ComputedLoad, PropertyLoad, BinaryExpression, TemplateLiteral, CallExpression, +/// MethodCall), so FunctionExpression/ObjectMethod context handling is unnecessary; +/// and (2) the caller does not have access to `env` which the canonical function requires +/// for resolving function expression context captures. +fn non_exp_value_operands(value: &InstructionValue) -> Vec { + match value { + InstructionValue::ComputedLoad { object, property, .. } => { + vec![object.identifier, property.identifier] + } + InstructionValue::PropertyLoad { object, .. } => vec![object.identifier], + InstructionValue::BinaryExpression { left, right, .. } => { + vec![left.identifier, right.identifier] + } + InstructionValue::TemplateLiteral { subexprs, .. } => { + subexprs.iter().map(|s| s.identifier).collect() + } + InstructionValue::CallExpression { callee, args, .. } => { + let mut op_ids = vec![callee.identifier]; + for a in args { + match a { + PlaceOrSpread::Place(p) => op_ids.push(p.identifier), + PlaceOrSpread::Spread(s) => op_ids.push(s.place.identifier), + } + } + op_ids + } + InstructionValue::MethodCall { receiver, property, args, .. } => { + let mut op_ids = vec![receiver.identifier, property.identifier]; + for a in args { + match a { + PlaceOrSpread::Place(p) => op_ids.push(p.identifier), + PlaceOrSpread::Spread(s) => op_ids.push(s.place.identifier), + } + } + op_ids + } + _ => Vec::new(), + } +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_freezing_known_mutable_functions.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_freezing_known_mutable_functions.rs new file mode 100644 index 0000000000000..4e17479fb074b --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_freezing_known_mutable_functions.rs @@ -0,0 +1,219 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::{each_instruction_value_operand, each_terminal_operand}; +use crate::react_compiler_hir::{ + AliasingEffect, Effect, HirFunction, Identifier, IdentifierId, IdentifierName, + InstructionValue, Place, Type, +}; + +/// Information about a known mutation effect: which identifier is mutated, and +/// the source location of the mutation. +#[derive(Debug, Clone)] +struct MutationInfo { + value_identifier: IdentifierId, + value_loc: Option, +} + +/// Validates that functions with known mutations (ie due to types) cannot be passed +/// where a frozen value is expected. +/// +/// Because a function that mutates a captured variable is equivalent to a mutable value, +/// and the receiver has no way to avoid calling the function, this pass detects functions +/// with *known* mutations (Mutate or MutateTransitive, not conditional) that are passed +/// where a frozen value is expected and reports an error. +pub fn validate_no_freezing_known_mutable_functions(func: &HirFunction, env: &mut Environment) { + let diagnostics = check_no_freezing_known_mutable_functions( + func, + &env.identifiers, + &env.types, + &env.functions, + env, + ); + for diagnostic in diagnostics { + env.record_diagnostic(diagnostic); + } +} + +fn check_no_freezing_known_mutable_functions( + func: &HirFunction, + identifiers: &[Identifier], + types: &[Type], + functions: &[HirFunction], + env: &Environment, +) -> Vec { + // Maps an identifier to the mutation effect that makes it "known mutable" + let mut context_mutation_effects: FxHashMap = FxHashMap::default(); + let mut diagnostics: Vec = Vec::new(); + + for (_block_id, block) in &func.body.blocks { + for &instruction_id in &block.instructions { + let instr = &func.instructions[instruction_id.0 as usize]; + + match &instr.value { + InstructionValue::LoadLocal { place, .. } => { + // Propagate known mutation from the loaded place to the lvalue + if let Some(mutation_info) = context_mutation_effects.get(&place.identifier) { + context_mutation_effects + .insert(instr.lvalue.identifier, mutation_info.clone()); + } + } + + InstructionValue::StoreLocal { lvalue, value, .. } => { + // Propagate known mutation from the stored value to both the + // instruction lvalue and the StoreLocal's target lvalue + if let Some(mutation_info) = context_mutation_effects.get(&value.identifier) { + let mutation_info = mutation_info.clone(); + context_mutation_effects + .insert(instr.lvalue.identifier, mutation_info.clone()); + context_mutation_effects.insert(lvalue.place.identifier, mutation_info); + } + } + + InstructionValue::FunctionExpression { lowered_func, .. } => { + let inner_function = &functions[lowered_func.func.0 as usize]; + if let Some(ref aliasing_effects) = inner_function.aliasing_effects { + let context_ids: FxHashSet = + inner_function.context.iter().map(|place| place.identifier).collect(); + + 'effects: for effect in aliasing_effects { + match effect { + AliasingEffect::Mutate { value, .. } + | AliasingEffect::MutateTransitive { value, .. } => { + // If the mutated value is already known-mutable, propagate + if let Some(known_mutation) = + context_mutation_effects.get(&value.identifier) + { + context_mutation_effects.insert( + instr.lvalue.identifier, + known_mutation.clone(), + ); + } else if context_ids.contains(&value.identifier) + && !is_ref_or_ref_like_mutable_type( + value.identifier, + identifiers, + types, + ) + { + // New known mutation of a context variable + context_mutation_effects.insert( + instr.lvalue.identifier, + MutationInfo { + value_identifier: value.identifier, + value_loc: value.loc, + }, + ); + break 'effects; + } + } + + AliasingEffect::MutateConditionally { value, .. } + | AliasingEffect::MutateTransitiveConditionally { value, .. } => { + // Only propagate existing known mutations for conditional effects + if let Some(known_mutation) = + context_mutation_effects.get(&value.identifier) + { + context_mutation_effects.insert( + instr.lvalue.identifier, + known_mutation.clone(), + ); + } + } + + _ => {} + } + } + } + } + + _ => { + // For all other instruction kinds, check operands for freeze violations + for operand in each_instruction_value_operand(&instr.value, env) { + check_operand_for_freeze_violation( + &operand, + &context_mutation_effects, + identifiers, + &mut diagnostics, + ); + } + } + } + } + + // Also check terminal operands + for operand in each_terminal_operand(&block.terminal) { + check_operand_for_freeze_violation( + &operand, + &context_mutation_effects, + identifiers, + &mut diagnostics, + ); + } + } + + diagnostics +} + +/// If an operand with Effect::Freeze is a known-mutable function, emit a diagnostic. +fn check_operand_for_freeze_violation( + operand: &Place, + context_mutation_effects: &FxHashMap, + identifiers: &[Identifier], + diagnostics: &mut Vec, +) { + if operand.effect == Effect::Freeze { + if let Some(mutation_info) = context_mutation_effects.get(&operand.identifier) { + let identifier = &identifiers[mutation_info.value_identifier.0 as usize]; + let variable_name = match &identifier.name { + Some(IdentifierName::Named(name)) => format!("`{}`", name), + _ => "a local variable".to_string(), + }; + + diagnostics.push( + CompilerDiagnostic::new( + ErrorCategory::Immutability, + "Cannot modify local variables after render completes", + Some(format!( + "This argument is a function which may reassign or mutate {} after render, \ + which can cause inconsistent behavior on subsequent renders. \ + Consider using state instead", + variable_name + )), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: operand.loc, + message: Some(format!( + "This function may (indirectly) reassign or modify {} after render", + variable_name + )), + identifier_name: None, + }) + .with_detail(CompilerDiagnosticDetail::Error { + loc: mutation_info.value_loc, + message: Some(format!("This modifies {}", variable_name)), + identifier_name: None, + }), + ); + } + } +} + +/// Check if an identifier's type is a ref or ref-like mutable type. +fn is_ref_or_ref_like_mutable_type( + identifier_id: IdentifierId, + identifiers: &[Identifier], + types: &[Type], +) -> bool { + let identifier = &identifiers[identifier_id.0 as usize]; + crate::react_compiler_hir::is_ref_or_ref_like_mutable_type(&types[identifier.type_.0 as usize]) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_jsx_in_try_statement.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_jsx_in_try_statement.rs new file mode 100644 index 0000000000000..78a5096ddca30 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_jsx_in_try_statement.rs @@ -0,0 +1,65 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Validates against constructing JSX within try/catch blocks. +//! +//! Developers may not be aware of error boundaries and lazy evaluation of JSX, leading them +//! to use patterns such as `let el; try { el = } catch { ... }` to attempt to +//! catch rendering errors. Such code will fail to catch errors in rendering, but developers +//! may not realize this right away. +//! +//! This validation pass errors for JSX created within a try block. JSX is allowed within a +//! catch statement, unless that catch is itself nested inside an outer try. +//! +//! Port of ValidateNoJSXInTryStatement.ts. + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, +}; +use crate::react_compiler_hir::{BlockId, HirFunction, InstructionValue, Terminal}; + +pub fn validate_no_jsx_in_try_statement(func: &HirFunction) -> CompilerError { + let mut active_try_blocks: Vec = Vec::new(); + let mut error = CompilerError::new(); + + for (_block_id, block) in &func.body.blocks { + // Remove completed try blocks (retainWhere equivalent) + active_try_blocks.retain(|id| *id != block.id); + + if !active_try_blocks.is_empty() { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::JsxExpression { loc, .. } + | InstructionValue::JsxFragment { loc, .. } => { + error.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::ErrorBoundaries, + "Avoid constructing JSX within try/catch", + Some( + "React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)".to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: *loc, + message: Some( + "Avoid constructing JSX within try/catch".to_string(), + ), + identifier_name: None, + }), + ); + } + _ => {} + } + } + } + + if let Terminal::Try { handler, .. } = &block.terminal { + active_try_blocks.push(*handler); + } + } + + error +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_ref_access_in_render.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_ref_access_in_render.rs new file mode 100644 index 0000000000000..5242d8d6c60a3 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_ref_access_in_render.rs @@ -0,0 +1,1139 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::object_shape::HookKind; +use crate::react_compiler_hir::visitors::{ + each_instruction_value_operand as canonical_each_instruction_value_operand, + each_pattern_operand, each_terminal_operand, +}; +use crate::react_compiler_hir::{ + AliasingEffect, BlockId, HirFunction, Identifier, IdentifierId, InstructionValue, Place, + PrimitiveValue, PropertyLiteral, Terminal, Type, UnaryOperator, +}; + +const ERROR_DESCRIPTION: &str = "React refs are values that are not needed for rendering. \ + Refs should only be accessed outside of render, such as in event handlers or effects. \ + Accessing a ref value (the `current` property) during render can cause your component \ + not to update as expected (https://react.dev/reference/react/useRef)"; + +// --- RefId --- + +type RefId = u32; + +static REF_ID_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + +fn next_ref_id() -> RefId { + REF_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed) +} + +// --- RefAccessType / RefAccessRefType / RefFnType --- + +/// Corresponds to TS `RefAccessType`. +/// +/// PartialEq matches the TS `tyEqual` semantics: Ref ignores ref_id, +/// RefValue compares loc but ignores ref_id. This is critical for fixpoint +/// convergence — join creates fresh ref_ids, and comparing them would +/// prevent the environment from stabilizing. +#[derive(Debug, Clone)] +enum RefAccessType { + None, + Nullable, + Guard { ref_id: RefId }, + Ref { ref_id: RefId }, + RefValue { loc: Option, ref_id: Option }, + Structure { value: Option>, fn_type: Option }, +} + +impl PartialEq for RefAccessType { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (RefAccessType::None, RefAccessType::None) => true, + (RefAccessType::Nullable, RefAccessType::Nullable) => true, + (RefAccessType::Guard { ref_id: a }, RefAccessType::Guard { ref_id: b }) => a == b, + (RefAccessType::Ref { .. }, RefAccessType::Ref { .. }) => true, + (RefAccessType::RefValue { loc: a, .. }, RefAccessType::RefValue { loc: b, .. }) => { + a == b + } + ( + RefAccessType::Structure { value: a_val, fn_type: a_fn }, + RefAccessType::Structure { value: b_val, fn_type: b_fn }, + ) => a_val == b_val && a_fn == b_fn, + _ => false, + } + } +} + +/// Corresponds to TS `RefAccessRefType` — the subset of `RefAccessType` that can appear +/// inside `Structure.value` and be joined via `join_ref_access_ref_types`. +/// +/// PartialEq mirrors RefAccessType: Ref ignores ref_id, RefValue compares +/// loc only. +#[derive(Debug, Clone)] +enum RefAccessRefType { + Ref { ref_id: RefId }, + RefValue { loc: Option, ref_id: Option }, + Structure { value: Option>, fn_type: Option }, +} + +impl PartialEq for RefAccessRefType { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (RefAccessRefType::Ref { .. }, RefAccessRefType::Ref { .. }) => true, + ( + RefAccessRefType::RefValue { loc: a, .. }, + RefAccessRefType::RefValue { loc: b, .. }, + ) => a == b, + ( + RefAccessRefType::Structure { value: a_val, fn_type: a_fn }, + RefAccessRefType::Structure { value: b_val, fn_type: b_fn }, + ) => a_val == b_val && a_fn == b_fn, + _ => false, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +struct RefFnType { + read_ref_effect: bool, + return_type: Box, +} + +impl RefAccessType { + /// Try to convert a `RefAccessType` to a `RefAccessRefType` (the Ref/RefValue/Structure subset). + fn to_ref_type(&self) -> Option { + match self { + RefAccessType::Ref { ref_id } => Some(RefAccessRefType::Ref { ref_id: *ref_id }), + RefAccessType::RefValue { loc, ref_id } => { + Some(RefAccessRefType::RefValue { loc: *loc, ref_id: *ref_id }) + } + RefAccessType::Structure { value, fn_type } => { + Some(RefAccessRefType::Structure { value: value.clone(), fn_type: fn_type.clone() }) + } + _ => None, + } + } + + /// Convert a `RefAccessRefType` back to a `RefAccessType`. + fn from_ref_type(ref_type: &RefAccessRefType) -> Self { + match ref_type { + RefAccessRefType::Ref { ref_id } => RefAccessType::Ref { ref_id: *ref_id }, + RefAccessRefType::RefValue { loc, ref_id } => { + RefAccessType::RefValue { loc: *loc, ref_id: *ref_id } + } + RefAccessRefType::Structure { value, fn_type } => { + RefAccessType::Structure { value: value.clone(), fn_type: fn_type.clone() } + } + } + } +} + +// --- Join operations --- + +fn join_ref_access_ref_types(a: &RefAccessRefType, b: &RefAccessRefType) -> RefAccessRefType { + match (a, b) { + ( + RefAccessRefType::RefValue { ref_id: a_id, .. }, + RefAccessRefType::RefValue { ref_id: b_id, .. }, + ) => { + if a_id == b_id { + a.clone() + } else { + RefAccessRefType::RefValue { loc: None, ref_id: None } + } + } + (RefAccessRefType::RefValue { .. }, _) => { + RefAccessRefType::RefValue { loc: None, ref_id: None } + } + (_, RefAccessRefType::RefValue { .. }) => { + RefAccessRefType::RefValue { loc: None, ref_id: None } + } + (RefAccessRefType::Ref { ref_id: a_id }, RefAccessRefType::Ref { ref_id: b_id }) => { + if a_id == b_id { a.clone() } else { RefAccessRefType::Ref { ref_id: next_ref_id() } } + } + (RefAccessRefType::Ref { .. }, _) | (_, RefAccessRefType::Ref { .. }) => { + RefAccessRefType::Ref { ref_id: next_ref_id() } + } + ( + RefAccessRefType::Structure { value: a_value, fn_type: a_fn }, + RefAccessRefType::Structure { value: b_value, fn_type: b_fn }, + ) => { + let fn_type = match (a_fn, b_fn) { + (None, other) | (other, None) => other.clone(), + (Some(a_fn), Some(b_fn)) => Some(RefFnType { + read_ref_effect: a_fn.read_ref_effect || b_fn.read_ref_effect, + return_type: Box::new(join_ref_access_types( + &a_fn.return_type, + &b_fn.return_type, + )), + }), + }; + let value = match (a_value, b_value) { + (None, other) | (other, None) => other.clone(), + (Some(a_val), Some(b_val)) => { + Some(Box::new(join_ref_access_ref_types(a_val, b_val))) + } + }; + RefAccessRefType::Structure { value, fn_type } + } + } +} + +fn join_ref_access_types(a: &RefAccessType, b: &RefAccessType) -> RefAccessType { + match (a, b) { + (RefAccessType::None, other) | (other, RefAccessType::None) => other.clone(), + (RefAccessType::Guard { ref_id: a_id }, RefAccessType::Guard { ref_id: b_id }) => { + if a_id == b_id { a.clone() } else { RefAccessType::None } + } + (RefAccessType::Guard { .. }, RefAccessType::Nullable) + | (RefAccessType::Nullable, RefAccessType::Guard { .. }) => RefAccessType::None, + (RefAccessType::Guard { .. }, other) | (other, RefAccessType::Guard { .. }) => { + other.clone() + } + (RefAccessType::Nullable, other) | (other, RefAccessType::Nullable) => other.clone(), + _ => match (a.to_ref_type(), b.to_ref_type()) { + (Some(a_ref), Some(b_ref)) => { + RefAccessType::from_ref_type(&join_ref_access_ref_types(&a_ref, &b_ref)) + } + (Some(r), None) | (None, Some(r)) => RefAccessType::from_ref_type(&r), + _ => RefAccessType::None, + }, + } +} + +fn join_ref_access_types_many(types: &[RefAccessType]) -> RefAccessType { + types.iter().fold(RefAccessType::None, |acc, t| join_ref_access_types(&acc, t)) +} + +// --- Env --- + +struct Env { + changed: bool, + data: FxHashMap, + temporaries: FxHashMap, +} + +impl Env { + fn new() -> Self { + Self { changed: false, data: FxHashMap::default(), temporaries: FxHashMap::default() } + } + + fn define(&mut self, key: IdentifierId, value: Place) { + self.temporaries.insert(key, value); + } + + fn reset_changed(&mut self) { + self.changed = false; + } + + fn has_changed(&self) -> bool { + self.changed + } + + fn get(&self, key: IdentifierId) -> Option<&RefAccessType> { + let operand_id = self.temporaries.get(&key).map(|p| p.identifier).unwrap_or(key); + self.data.get(&operand_id) + } + + fn set(&mut self, key: IdentifierId, value: RefAccessType) { + let operand_id = self.temporaries.get(&key).map(|p| p.identifier).unwrap_or(key); + let current = self.data.get(&operand_id); + let widened_value = join_ref_access_types(&value, current.unwrap_or(&RefAccessType::None)); + if current.is_none() && widened_value == RefAccessType::None { + // No change needed + } else if current.map_or(true, |c| c != &widened_value) { + self.changed = true; + } + self.data.insert(operand_id, widened_value); + } +} + +// --- Helper functions --- + +fn ref_type_of_type(id: IdentifierId, identifiers: &[Identifier], types: &[Type]) -> RefAccessType { + let identifier = &identifiers[id.0 as usize]; + let ty = &types[identifier.type_.0 as usize]; + if crate::react_compiler_hir::is_ref_value_type(ty) { + RefAccessType::RefValue { loc: None, ref_id: None } + } else if crate::react_compiler_hir::is_use_ref_type(ty) { + RefAccessType::Ref { ref_id: next_ref_id() } + } else { + RefAccessType::None + } +} + +fn is_ref_type(id: IdentifierId, identifiers: &[Identifier], types: &[Type]) -> bool { + let identifier = &identifiers[id.0 as usize]; + crate::react_compiler_hir::is_use_ref_type(&types[identifier.type_.0 as usize]) +} + +fn is_ref_value_type(id: IdentifierId, identifiers: &[Identifier], types: &[Type]) -> bool { + let identifier = &identifiers[id.0 as usize]; + crate::react_compiler_hir::is_ref_value_type(&types[identifier.type_.0 as usize]) +} + +fn destructure(ty: &RefAccessType) -> RefAccessType { + match ty { + RefAccessType::Structure { value: Some(inner), .. } => { + destructure(&RefAccessType::from_ref_type(inner)) + } + other => other.clone(), + } +} + +// --- Validation helpers --- + +fn validate_no_direct_ref_value_access( + errors: &mut Vec, + operand: &Place, + env: &Env, +) { + if let Some(ty) = env.get(operand.identifier) { + let ty = destructure(ty); + if let RefAccessType::RefValue { loc, .. } = &ty { + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: loc.or(operand.loc), + message: Some("Cannot access ref value during render".to_string()), + identifier_name: None, + }), + ); + } + } +} + +fn validate_no_ref_value_access(errors: &mut Vec, env: &Env, operand: &Place) { + if let Some(ty) = env.get(operand.identifier) { + let ty = destructure(ty); + match &ty { + RefAccessType::RefValue { loc, .. } => { + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: loc.or(operand.loc), + message: Some("Cannot access ref value during render".to_string()), + identifier_name: None, + }), + ); + } + RefAccessType::Structure { fn_type: Some(fn_type), .. } if fn_type.read_ref_effect => { + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: operand.loc, + message: Some("Cannot access ref value during render".to_string()), + identifier_name: None, + }), + ); + } + _ => {} + } + } +} + +fn validate_no_ref_passed_to_function( + errors: &mut Vec, + env: &Env, + operand: &Place, + loc: Option, +) { + if let Some(ty) = env.get(operand.identifier) { + let ty = destructure(ty); + match &ty { + RefAccessType::Ref { .. } | RefAccessType::RefValue { .. } => { + let error_loc = if let RefAccessType::RefValue { loc: ref_loc, .. } = &ty { + ref_loc.or(loc) + } else { + loc + }; + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: error_loc, + message: Some( + "Passing a ref to a function may read its value during render" + .to_string(), + ), + identifier_name: None, + }), + ); + } + RefAccessType::Structure { fn_type: Some(fn_type), .. } if fn_type.read_ref_effect => { + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc, + message: Some( + "Passing a ref to a function may read its value during render" + .to_string(), + ), + identifier_name: None, + }), + ); + } + _ => {} + } + } +} + +fn validate_no_ref_update( + errors: &mut Vec, + env: &Env, + operand: &Place, + loc: Option, +) { + if let Some(ty) = env.get(operand.identifier) { + let ty = destructure(ty); + match &ty { + RefAccessType::Ref { .. } | RefAccessType::RefValue { .. } => { + let error_loc = if let RefAccessType::RefValue { loc: ref_loc, .. } = &ty { + ref_loc.or(loc) + } else { + loc + }; + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: error_loc, + message: Some("Cannot update ref during render".to_string()), + identifier_name: None, + }), + ); + } + _ => {} + } + } +} + +fn guard_check(errors: &mut Vec, operand: &Place, env: &Env) { + if matches!(env.get(operand.identifier), Some(RefAccessType::Guard { .. })) { + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: operand.loc, + message: Some("Cannot access ref value during render".to_string()), + identifier_name: None, + }), + ); + } +} + +// --- Main entry point --- + +pub fn validate_no_ref_access_in_render(func: &HirFunction, env: &mut Environment) { + let mut ref_env = Env::new(); + collect_temporaries_sidemap(func, &mut ref_env, &env.identifiers, &env.types); + let mut errors: Vec = Vec::new(); + validate_no_ref_access_in_render_impl( + func, + &env.identifiers, + &env.types, + &env.functions, + &*env, + &mut ref_env, + &mut errors, + ); + for diagnostic in errors { + env.record_diagnostic(diagnostic); + } +} + +fn collect_temporaries_sidemap( + func: &HirFunction, + env: &mut Env, + identifiers: &[Identifier], + types: &[Type], +) { + for (_, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::LoadLocal { place, .. } => { + let temp = env + .temporaries + .get(&place.identifier) + .cloned() + .unwrap_or_else(|| place.clone()); + env.define(instr.lvalue.identifier, temp); + } + InstructionValue::StoreLocal { lvalue, value, .. } => { + let temp = env + .temporaries + .get(&value.identifier) + .cloned() + .unwrap_or_else(|| value.clone()); + env.define(instr.lvalue.identifier, temp.clone()); + env.define(lvalue.place.identifier, temp); + } + InstructionValue::PropertyLoad { object, property, .. } => { + if is_ref_type(object.identifier, identifiers, types) + && *property == PropertyLiteral::String("current".to_string()) + { + continue; + } + let temp = env + .temporaries + .get(&object.identifier) + .cloned() + .unwrap_or_else(|| object.clone()); + env.define(instr.lvalue.identifier, temp); + } + _ => {} + } + } + } +} + +fn validate_no_ref_access_in_render_impl( + func: &HirFunction, + identifiers: &[Identifier], + types: &[Type], + functions: &[HirFunction], + env: &Environment, + ref_env: &mut Env, + errors: &mut Vec, +) -> RefAccessType { + let mut return_values: Vec = Vec::new(); + + // Process params + for param in &func.params { + let place = match param { + crate::react_compiler_hir::ParamPattern::Place(p) => p, + crate::react_compiler_hir::ParamPattern::Spread(s) => &s.place, + }; + ref_env.set(place.identifier, ref_type_of_type(place.identifier, identifiers, types)); + } + + // Collect identifiers that are interpolated as JSX children + let mut interpolated_as_jsx: FxHashSet = FxHashSet::default(); + for (_, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::JsxExpression { children: Some(children), .. } => { + for child in children { + interpolated_as_jsx.insert(child.identifier); + } + } + InstructionValue::JsxFragment { children, .. } => { + for child in children { + interpolated_as_jsx.insert(child.identifier); + } + } + _ => {} + } + } + } + + // Fixed-point iteration (up to 10 iterations) + for iteration in 0..10 { + if iteration > 0 && !ref_env.has_changed() { + break; + } + ref_env.reset_changed(); + return_values.clear(); + let mut safe_blocks: Vec<(BlockId, RefId)> = Vec::new(); + + for (_, block) in &func.body.blocks { + safe_blocks.retain(|(block_id, _)| *block_id != block.id); + + // Process phis + for phi in &block.phis { + let phi_types: Vec = phi + .operands + .values() + .map(|operand| { + ref_env.get(operand.identifier).cloned().unwrap_or(RefAccessType::None) + }) + .collect(); + ref_env.set(phi.place.identifier, join_ref_access_types_many(&phi_types)); + } + + // Process instructions + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::JsxExpression { .. } + | InstructionValue::JsxFragment { .. } => { + for operand in &canonical_each_instruction_value_operand(&instr.value, env) + { + validate_no_direct_ref_value_access(errors, operand, ref_env); + } + } + InstructionValue::ComputedLoad { object, property, .. } => { + validate_no_direct_ref_value_access(errors, property, ref_env); + let obj_type = ref_env.get(object.identifier).cloned(); + let lookup_type = match &obj_type { + Some(RefAccessType::Structure { value: Some(value), .. }) => { + Some(RefAccessType::from_ref_type(value)) + } + Some(RefAccessType::Ref { ref_id }) => Some(RefAccessType::RefValue { + loc: instr.loc, + ref_id: Some(*ref_id), + }), + _ => None, + }; + ref_env.set( + instr.lvalue.identifier, + lookup_type.unwrap_or_else(|| { + ref_type_of_type(instr.lvalue.identifier, identifiers, types) + }), + ); + } + InstructionValue::PropertyLoad { object, .. } => { + let obj_type = ref_env.get(object.identifier).cloned(); + let lookup_type = match &obj_type { + Some(RefAccessType::Structure { value: Some(value), .. }) => { + Some(RefAccessType::from_ref_type(value)) + } + Some(RefAccessType::Ref { ref_id }) => Some(RefAccessType::RefValue { + loc: instr.loc, + ref_id: Some(*ref_id), + }), + _ => None, + }; + ref_env.set( + instr.lvalue.identifier, + lookup_type.unwrap_or_else(|| { + ref_type_of_type(instr.lvalue.identifier, identifiers, types) + }), + ); + } + InstructionValue::TypeCastExpression { value, .. } => { + ref_env.set( + instr.lvalue.identifier, + ref_env.get(value.identifier).cloned().unwrap_or_else(|| { + ref_type_of_type(instr.lvalue.identifier, identifiers, types) + }), + ); + } + InstructionValue::LoadContext { place, .. } + | InstructionValue::LoadLocal { place, .. } => { + ref_env.set( + instr.lvalue.identifier, + ref_env.get(place.identifier).cloned().unwrap_or_else(|| { + ref_type_of_type(instr.lvalue.identifier, identifiers, types) + }), + ); + } + InstructionValue::StoreContext { lvalue, value, .. } + | InstructionValue::StoreLocal { lvalue, value, .. } => { + ref_env.set( + lvalue.place.identifier, + ref_env.get(value.identifier).cloned().unwrap_or_else(|| { + ref_type_of_type(lvalue.place.identifier, identifiers, types) + }), + ); + ref_env.set( + instr.lvalue.identifier, + ref_env.get(value.identifier).cloned().unwrap_or_else(|| { + ref_type_of_type(instr.lvalue.identifier, identifiers, types) + }), + ); + } + InstructionValue::Destructure { value, lvalue, .. } => { + let obj_type = ref_env.get(value.identifier).cloned(); + let lookup_type = match &obj_type { + Some(RefAccessType::Structure { value: Some(value), .. }) => { + Some(RefAccessType::from_ref_type(value)) + } + _ => None, + }; + ref_env.set( + instr.lvalue.identifier, + lookup_type.clone().unwrap_or_else(|| { + ref_type_of_type(instr.lvalue.identifier, identifiers, types) + }), + ); + for pattern_place in each_pattern_operand(&lvalue.pattern) { + ref_env.set( + pattern_place.identifier, + lookup_type.clone().unwrap_or_else(|| { + ref_type_of_type(pattern_place.identifier, identifiers, types) + }), + ); + } + } + InstructionValue::ObjectMethod { lowered_func, .. } + | InstructionValue::FunctionExpression { lowered_func, .. } => { + let inner = &functions[lowered_func.func.0 as usize]; + let mut inner_errors: Vec = Vec::new(); + let result = validate_no_ref_access_in_render_impl( + inner, + identifiers, + types, + functions, + env, + ref_env, + &mut inner_errors, + ); + let (return_type, read_ref_effect) = if inner_errors.is_empty() { + (result, false) + } else { + (RefAccessType::None, true) + }; + ref_env.set( + instr.lvalue.identifier, + RefAccessType::Structure { + value: None, + fn_type: Some(RefFnType { + read_ref_effect, + return_type: Box::new(return_type), + }), + }, + ); + } + InstructionValue::MethodCall { property, .. } + | InstructionValue::CallExpression { callee: property, .. } => { + let callee = property; + let mut return_type = RefAccessType::None; + let fn_type = ref_env.get(callee.identifier).cloned(); + let mut did_error = false; + + if let Some(RefAccessType::Structure { fn_type: Some(fn_ty), .. }) = + &fn_type + { + return_type = *fn_ty.return_type.clone(); + if fn_ty.read_ref_effect { + did_error = true; + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail( + CompilerDiagnosticDetail::Error { + loc: callee.loc, + message: Some( + "This function accesses a ref value".to_string(), + ), + identifier_name: None, + }, + ), + ); + } + } + + /* + * If we already reported an error on this instruction, don't report + * duplicate errors + */ + if !did_error { + let is_ref_lvalue = + is_ref_type(instr.lvalue.identifier, identifiers, types); + let callee_identifier = &identifiers[callee.identifier.0 as usize]; + let callee_type = &types[callee_identifier.type_.0 as usize]; + let hook_kind = env.get_hook_kind_for_type(callee_type).ok().flatten(); + + if is_ref_lvalue + || (hook_kind.is_some() + && !matches!(hook_kind, Some(&HookKind::UseState)) + && !matches!(hook_kind, Some(&HookKind::UseReducer))) + { + for operand in + &canonical_each_instruction_value_operand(&instr.value, env) + { + /* + * Allow passing refs or ref-accessing functions when: + * 1. lvalue is a ref (mergeRefs pattern) + * 2. calling hooks (independently validated) + */ + validate_no_direct_ref_value_access(errors, operand, ref_env); + } + } else if interpolated_as_jsx.contains(&instr.lvalue.identifier) { + for operand in + &canonical_each_instruction_value_operand(&instr.value, env) + { + /* + * Special case: the lvalue is passed as a jsx child + */ + validate_no_ref_value_access(errors, ref_env, operand); + } + } else if hook_kind.is_none() { + if let Some(ref effects) = instr.effects { + /* + * For non-hook functions with known aliasing effects, + * use the effects to determine what validation to apply. + * Track visited id:kind pairs to avoid duplicate errors. + */ + let mut visited_effects: FxHashSet = + FxHashSet::default(); + for effect in effects { + let (place, validation) = match effect { + AliasingEffect::Freeze { value, .. } => { + (Some(value), "direct-ref") + } + AliasingEffect::Mutate { value, .. } + | AliasingEffect::MutateTransitive { value, .. } + | AliasingEffect::MutateConditionally { + value, .. + } + | AliasingEffect::MutateTransitiveConditionally { + value, + .. + } => (Some(value), "ref-passed"), + AliasingEffect::Render { place, .. } => { + (Some(place), "ref-passed") + } + AliasingEffect::Capture { from, .. } + | AliasingEffect::Alias { from, .. } + | AliasingEffect::MaybeAlias { from, .. } + | AliasingEffect::Assign { from, .. } + | AliasingEffect::CreateFrom { from, .. } => { + (Some(from), "ref-passed") + } + AliasingEffect::ImmutableCapture { from, .. } => { + /* + * ImmutableCapture: check whether the same + * operand also has a Freeze effect to + * distinguish known signatures from + * downgraded defaults. + */ + let is_frozen = effects.iter().any(|e| { + matches!( + e, + AliasingEffect::Freeze { value, .. } + if value.identifier == from.identifier + ) + }); + ( + Some(from), + if is_frozen { + "direct-ref" + } else { + "ref-passed" + }, + ) + } + _ => (None, "none"), + }; + if let Some(place) = place { + if validation != "none" { + let key = format!( + "{}:{}", + place.identifier.0, validation + ); + if visited_effects.insert(key) { + if validation == "direct-ref" { + validate_no_direct_ref_value_access( + errors, place, ref_env, + ); + } else { + validate_no_ref_passed_to_function( + errors, ref_env, place, place.loc, + ); + } + } + } + } + } + } else { + for operand in + &canonical_each_instruction_value_operand(&instr.value, env) + { + validate_no_ref_passed_to_function( + errors, + ref_env, + operand, + operand.loc, + ); + } + } + } else { + for operand in + &canonical_each_instruction_value_operand(&instr.value, env) + { + validate_no_ref_passed_to_function( + errors, + ref_env, + operand, + operand.loc, + ); + } + } + } + ref_env.set(instr.lvalue.identifier, return_type); + } + InstructionValue::ObjectExpression { .. } + | InstructionValue::ArrayExpression { .. } => { + let operands = canonical_each_instruction_value_operand(&instr.value, env); + let mut types_vec: Vec = Vec::new(); + for operand in &operands { + validate_no_direct_ref_value_access(errors, operand, ref_env); + types_vec.push( + ref_env + .get(operand.identifier) + .cloned() + .unwrap_or(RefAccessType::None), + ); + } + let value = join_ref_access_types_many(&types_vec); + match &value { + RefAccessType::None + | RefAccessType::Guard { .. } + | RefAccessType::Nullable => { + ref_env.set(instr.lvalue.identifier, RefAccessType::None); + } + _ => { + ref_env.set( + instr.lvalue.identifier, + RefAccessType::Structure { + value: value.to_ref_type().map(Box::new), + fn_type: None, + }, + ); + } + } + } + InstructionValue::PropertyDelete { object, .. } + | InstructionValue::PropertyStore { object, .. } + | InstructionValue::ComputedDelete { object, .. } + | InstructionValue::ComputedStore { object, .. } => { + let target = ref_env.get(object.identifier).cloned(); + let mut found_safe = false; + if matches!(&instr.value, InstructionValue::PropertyStore { .. }) { + if let Some(RefAccessType::Ref { ref_id }) = &target { + if let Some(pos) = safe_blocks.iter().position(|(_, r)| r == ref_id) + { + safe_blocks.remove(pos); + found_safe = true; + } + } + } + if !found_safe { + validate_no_ref_update(errors, ref_env, object, instr.loc); + } + match &instr.value { + InstructionValue::ComputedDelete { property, .. } + | InstructionValue::ComputedStore { property, .. } => { + validate_no_ref_value_access(errors, ref_env, property); + } + _ => {} + } + match &instr.value { + InstructionValue::ComputedStore { value, .. } + | InstructionValue::PropertyStore { value, .. } => { + validate_no_direct_ref_value_access(errors, value, ref_env); + let value_type = ref_env.get(value.identifier).cloned(); + if let Some(RefAccessType::Structure { .. }) = &value_type { + let mut object_type = value_type.unwrap(); + if let Some(t) = &target { + object_type = join_ref_access_types(&object_type, t); + } + ref_env.set(object.identifier, object_type); + } + } + _ => {} + } + } + InstructionValue::StartMemoize { .. } + | InstructionValue::FinishMemoize { .. } => {} + InstructionValue::LoadGlobal { binding, .. } => { + if binding.name() == "undefined" { + ref_env.set(instr.lvalue.identifier, RefAccessType::Nullable); + } + } + InstructionValue::Primitive { value, .. } => { + if matches!(value, PrimitiveValue::Null | PrimitiveValue::Undefined) { + ref_env.set(instr.lvalue.identifier, RefAccessType::Nullable); + } + } + InstructionValue::UnaryExpression { operator, value, .. } => { + if *operator == UnaryOperator::Not { + if let Some(RefAccessType::RefValue { ref_id: Some(ref_id), .. }) = + ref_env.get(value.identifier).cloned().as_ref() + { + /* + * Record an error suggesting the `if (ref.current == null)` pattern, + * but also record the lvalue as a guard so that we don't emit a + * second error for the write to the ref + */ + ref_env.set( + instr.lvalue.identifier, + RefAccessType::Guard { ref_id: *ref_id }, + ); + errors.push( + CompilerDiagnostic::new( + ErrorCategory::Refs, + "Cannot access refs during render", + Some(ERROR_DESCRIPTION.to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: value.loc, + message: Some( + "Cannot access ref value during render" + .to_string(), + ), + identifier_name: None, + }) + .with_detail(CompilerDiagnosticDetail::Hint { + message: "To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`".to_string(), + }), + ); + } else { + validate_no_ref_value_access(errors, ref_env, value); + } + } else { + validate_no_ref_value_access(errors, ref_env, value); + } + } + InstructionValue::BinaryExpression { left, right, .. } => { + let left_type = ref_env.get(left.identifier).cloned(); + let right_type = ref_env.get(right.identifier).cloned(); + let mut nullish = false; + let mut found_ref_id: Option = None; + + if let Some(RefAccessType::RefValue { ref_id: Some(id), .. }) = &left_type { + found_ref_id = Some(*id); + } else if let Some(RefAccessType::RefValue { ref_id: Some(id), .. }) = + &right_type + { + found_ref_id = Some(*id); + } + + if matches!(&left_type, Some(RefAccessType::Nullable)) { + nullish = true; + } else if matches!(&right_type, Some(RefAccessType::Nullable)) { + nullish = true; + } + + if let Some(ref_id) = found_ref_id { + if nullish { + ref_env + .set(instr.lvalue.identifier, RefAccessType::Guard { ref_id }); + } else { + validate_no_ref_value_access(errors, ref_env, left); + validate_no_ref_value_access(errors, ref_env, right); + } + } else { + validate_no_ref_value_access(errors, ref_env, left); + validate_no_ref_value_access(errors, ref_env, right); + } + } + _ => { + for operand in &canonical_each_instruction_value_operand(&instr.value, env) + { + validate_no_ref_value_access(errors, ref_env, operand); + } + } + } + + // Guard values are derived from ref.current, so they can only be used + // in if statement targets + for operand in &canonical_each_instruction_value_operand(&instr.value, env) { + guard_check(errors, operand, ref_env); + } + + if is_ref_type(instr.lvalue.identifier, identifiers, types) + && !matches!( + ref_env.get(instr.lvalue.identifier), + Some(RefAccessType::Ref { .. }) + ) + { + let existing = ref_env + .get(instr.lvalue.identifier) + .cloned() + .unwrap_or(RefAccessType::None); + ref_env.set( + instr.lvalue.identifier, + join_ref_access_types( + &existing, + &RefAccessType::Ref { ref_id: next_ref_id() }, + ), + ); + } + if is_ref_value_type(instr.lvalue.identifier, identifiers, types) + && !matches!( + ref_env.get(instr.lvalue.identifier), + Some(RefAccessType::RefValue { .. }) + ) + { + let existing = ref_env + .get(instr.lvalue.identifier) + .cloned() + .unwrap_or(RefAccessType::None); + ref_env.set( + instr.lvalue.identifier, + join_ref_access_types( + &existing, + &RefAccessType::RefValue { loc: instr.loc, ref_id: None }, + ), + ); + } + } + + // Check if terminal is an `if` — push safe block for guard + if let Terminal::If { test, fallthrough, .. } = &block.terminal { + if let Some(RefAccessType::Guard { ref_id }) = ref_env.get(test.identifier) { + if !safe_blocks.iter().any(|(_, r)| r == ref_id) { + safe_blocks.push((*fallthrough, *ref_id)); + } + } + } + + // Process terminal operands + for operand in &each_terminal_operand(&block.terminal) { + if !matches!(&block.terminal, Terminal::Return { .. }) { + validate_no_ref_value_access(errors, ref_env, operand); + if !matches!(&block.terminal, Terminal::If { .. }) { + guard_check(errors, operand, ref_env); + } + } else { + // Allow functions containing refs to be returned, but not direct ref values + validate_no_direct_ref_value_access(errors, operand, ref_env); + guard_check(errors, operand, ref_env); + if let Some(ty) = ref_env.get(operand.identifier) { + return_values.push(ty.clone()); + } + } + } + } + + if !errors.is_empty() { + return RefAccessType::None; + } + } + + if ref_env.has_changed() { + errors.push(CompilerDiagnostic::new( + crate::react_compiler_diagnostics::ErrorCategory::Invariant, + "Ref type environment did not converge", + None, + )); + return RefAccessType::None; + } + + join_ref_access_types_many(&return_values) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_set_state_in_effects.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_set_state_in_effects.rs new file mode 100644 index 0000000000000..bdf2a5a277fa5 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_set_state_in_effects.rs @@ -0,0 +1,566 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Validates against calling setState in the body of an effect (useEffect and friends), +//! while allowing calling setState in callbacks scheduled by the effect. +//! +//! Calling setState during execution of a useEffect triggers a re-render, which is +//! often bad for performance and frequently has more efficient and straightforward +//! alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples. +//! +//! Port of ValidateNoSetStateInEffects.ts. + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, +}; +use crate::react_compiler_hir::dominator::{compute_post_dominator_tree, post_dominator_frontier}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + BlockId, HirFunction, Identifier, IdentifierId, IdentifierName, InstructionValue, + PlaceOrSpread, PropertyLiteral, SourceLocation, Terminal, Type, is_ref_value_type, + is_set_state_type, is_use_effect_event_type, is_use_effect_hook_type, + is_use_insertion_effect_hook_type, is_use_layout_effect_hook_type, is_use_ref_type, visitors, +}; + +pub fn validate_no_set_state_in_effects( + func: &HirFunction, + env: &Environment, +) -> Result { + let identifiers = &env.identifiers; + let types = &env.types; + let functions = &env.functions; + let enable_verbose = env.config.enable_verbose_no_set_state_in_effect; + let enable_allow_set_state_from_refs = env.config.enable_allow_set_state_from_refs_in_effects; + + // Map from IdentifierId to the Place where the setState originated + let mut set_state_functions: FxHashMap = FxHashMap::default(); + let mut errors = CompilerError::new(); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::LoadLocal { place, .. } => { + if set_state_functions.contains_key(&place.identifier) { + let info = set_state_functions[&place.identifier].clone(); + set_state_functions.insert(instr.lvalue.identifier, info); + } + } + InstructionValue::StoreLocal { lvalue, value, .. } => { + if set_state_functions.contains_key(&value.identifier) { + let info = set_state_functions[&value.identifier].clone(); + set_state_functions.insert(lvalue.place.identifier, info.clone()); + set_state_functions.insert(instr.lvalue.identifier, info); + } + } + InstructionValue::FunctionExpression { lowered_func, .. } => { + // Check if any context capture references a setState + let inner_func = &functions[lowered_func.func.0 as usize]; + let has_set_state_operand = inner_func.context.iter().any(|ctx_place| { + is_set_state_type_by_id(ctx_place.identifier, identifiers, types) + || set_state_functions.contains_key(&ctx_place.identifier) + }); + + if has_set_state_operand { + let callee = get_set_state_call( + inner_func, + &mut set_state_functions, + identifiers, + types, + functions, + enable_allow_set_state_from_refs, + env.next_block_id_counter, + env.code.as_deref(), + )?; + if let Some(info) = callee { + set_state_functions.insert(instr.lvalue.identifier, info); + } + } + } + InstructionValue::MethodCall { property, args, .. } => { + let prop_type = + &types[identifiers[property.identifier.0 as usize].type_.0 as usize]; + if is_use_effect_event_type(prop_type) { + if let Some(first_arg) = args.first() { + if let PlaceOrSpread::Place(arg_place) = first_arg { + if let Some(info) = set_state_functions.get(&arg_place.identifier) { + set_state_functions + .insert(instr.lvalue.identifier, info.clone()); + } + } + } + } else if is_use_effect_hook_type(prop_type) + || is_use_layout_effect_hook_type(prop_type) + || is_use_insertion_effect_hook_type(prop_type) + { + if let Some(first_arg) = args.first() { + if let PlaceOrSpread::Place(arg_place) = first_arg { + if let Some(info) = set_state_functions.get(&arg_place.identifier) { + push_error(&mut errors, info, enable_verbose); + } + } + } + } + } + InstructionValue::CallExpression { callee, args, .. } => { + let callee_type = + &types[identifiers[callee.identifier.0 as usize].type_.0 as usize]; + if is_use_effect_event_type(callee_type) { + if let Some(first_arg) = args.first() { + if let PlaceOrSpread::Place(arg_place) = first_arg { + if let Some(info) = set_state_functions.get(&arg_place.identifier) { + set_state_functions + .insert(instr.lvalue.identifier, info.clone()); + } + } + } + } else if is_use_effect_hook_type(callee_type) + || is_use_layout_effect_hook_type(callee_type) + || is_use_insertion_effect_hook_type(callee_type) + { + if let Some(first_arg) = args.first() { + if let PlaceOrSpread::Place(arg_place) = first_arg { + if let Some(info) = set_state_functions.get(&arg_place.identifier) { + push_error(&mut errors, info, enable_verbose); + } + } + } + } + } + _ => {} + } + } + } + + Ok(errors) +} + +#[derive(Debug, Clone)] +struct SetStateInfo { + loc: Option, + identifier_name: Option, +} + +/// Get the user-visible name for an identifier, matching Babel's +/// loc.identifierName behavior. First checks the identifier's own name, +/// then falls back to extracting the name from the source code at the +/// given source location (the callee's loc). This handles SSA identifiers +/// whose names were lost during compiler passes. +fn get_identifier_name_with_loc( + id: IdentifierId, + identifiers: &[Identifier], + loc: &Option, + source_code: Option<&str>, +) -> Option { + let ident = &identifiers[id.0 as usize]; + if let Some(IdentifierName::Named(name)) = &ident.name { + return Some(name.clone()); + } + // Fall back to extracting from source code + if let (Some(loc), Some(code)) = (loc, source_code) { + let start_idx = loc.start.index? as usize; + let end_idx = loc.end.index? as usize; + if start_idx < code.len() && end_idx <= code.len() && start_idx < end_idx { + let slice = &code[start_idx..end_idx]; + if !slice.is_empty() + && slice.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '$') + { + return Some(slice.to_string()); + } + } + } + None +} + +fn is_set_state_type_by_id( + identifier_id: IdentifierId, + identifiers: &[Identifier], + types: &[Type], +) -> bool { + let ident = &identifiers[identifier_id.0 as usize]; + let ty = &types[ident.type_.0 as usize]; + is_set_state_type(ty) +} + +fn push_error(errors: &mut CompilerError, info: &SetStateInfo, enable_verbose: bool) { + if enable_verbose { + errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::EffectSetState, + "Calling setState synchronously within an effect can trigger cascading renders", + Some( + "Effects are intended to synchronize state between React and external systems. \ + Calling setState synchronously causes cascading renders that hurt performance.\n\n\ + This pattern may indicate one of several issues:\n\n\ + **1. Non-local derived data**: If the value being set could be computed from props/state \ + but requires data from a parent component, consider restructuring state ownership so the \ + derivation can happen during render in the component that owns the relevant state.\n\n\ + **2. Derived event pattern**: If you're detecting when a prop changes (e.g., `isPlaying` \ + transitioning from false to true), this often indicates the parent should provide an event \ + callback (like `onPlay`) instead of just the current state. Request access to the original event.\n\n\ + **3. Force update / external sync**: If you're forcing a re-render to sync with an external \ + data source (mutable values outside React), use `useSyncExternalStore` to properly subscribe \ + to external state changes.\n\n\ + See: https://react.dev/learn/you-might-not-need-an-effect".to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: info.loc, + message: Some( + "Avoid calling setState() directly within an effect".to_string(), + ), + identifier_name: info.identifier_name.clone(), + }), + ); + } else { + errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::EffectSetState, + "Calling setState synchronously within an effect can trigger cascading renders", + Some( + "Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. \ + In general, the body of an effect should do one or both of the following:\n\ + * Update external systems with the latest state from React.\n\ + * Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n\ + Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. \ + (https://react.dev/learn/you-might-not-need-an-effect)".to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: info.loc, + message: Some( + "Avoid calling setState() directly within an effect".to_string(), + ), + identifier_name: info.identifier_name.clone(), + }), + ); + } +} + +/// Recursively collect all Place identifiers from a destructure pattern. +fn collect_destructure_places( + pattern: &crate::react_compiler_hir::Pattern, + ref_derived_values: &mut FxHashSet, +) { + match pattern { + crate::react_compiler_hir::Pattern::Array(arr) => { + for item in &arr.items { + match item { + crate::react_compiler_hir::ArrayPatternElement::Place(p) => { + ref_derived_values.insert(p.identifier); + } + crate::react_compiler_hir::ArrayPatternElement::Spread(s) => { + ref_derived_values.insert(s.place.identifier); + } + crate::react_compiler_hir::ArrayPatternElement::Hole => {} + } + } + } + crate::react_compiler_hir::Pattern::Object(obj) => { + for prop in &obj.properties { + match prop { + crate::react_compiler_hir::ObjectPropertyOrSpread::Property(p) => { + ref_derived_values.insert(p.place.identifier); + } + crate::react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => { + ref_derived_values.insert(s.place.identifier); + } + } + } + } + } +} + +fn is_derived_from_ref( + id: IdentifierId, + ref_derived_values: &FxHashSet, + identifiers: &[Identifier], + types: &[Type], +) -> bool { + if ref_derived_values.contains(&id) { + return true; + } + let ident = &identifiers[id.0 as usize]; + let ty = &types[ident.type_.0 as usize]; + is_use_ref_type(ty) || is_ref_value_type(ty) +} + +/// Collects all operand IdentifierIds from an instruction value. +/// Uses the canonical `each_instruction_value_operand_with_functions` from visitors. +fn collect_operands(value: &InstructionValue, functions: &[HirFunction]) -> Vec { + visitors::each_instruction_value_operand_with_functions(value, functions) + .into_iter() + .map(|p| p.identifier) + .collect() +} + +/// Creates a function that checks whether a block is "control-dominated" by +/// a ref-derived condition. A block is ref-controlled if its post-dominator +/// frontier contains a block whose terminal tests a ref-derived value. +fn create_ref_controlled_block_checker( + func: &HirFunction, + next_block_id_counter: u32, + ref_derived_values: &FxHashSet, + identifiers: &[Identifier], + types: &[Type], +) -> Result, CompilerDiagnostic> { + let post_dominators = compute_post_dominator_tree(func, next_block_id_counter, false)?; + let mut cache: FxHashMap = FxHashMap::default(); + + for (block_id, _block) in &func.body.blocks { + let frontier = post_dominator_frontier(func, &post_dominators, *block_id); + let mut is_controlled = false; + + for frontier_block_id in &frontier { + let control_block = &func.body.blocks[frontier_block_id]; + match &control_block.terminal { + Terminal::If { test, .. } | Terminal::Branch { test, .. } => { + if is_derived_from_ref(test.identifier, ref_derived_values, identifiers, types) + { + is_controlled = true; + break; + } + } + Terminal::Switch { test, cases, .. } => { + if is_derived_from_ref(test.identifier, ref_derived_values, identifiers, types) + { + is_controlled = true; + break; + } + for case in cases { + if let Some(case_test) = &case.test { + if is_derived_from_ref( + case_test.identifier, + ref_derived_values, + identifiers, + types, + ) { + is_controlled = true; + break; + } + } + } + if is_controlled { + break; + } + } + _ => {} + } + } + + cache.insert(*block_id, is_controlled); + } + + Ok(cache) +} + +/// Checks inner function body for direct setState calls. Returns the callee Place info +/// if a setState call is found in the function body. +/// Tracks ref-derived values to allow setState when the value being set comes from a ref. +fn get_set_state_call( + func: &HirFunction, + set_state_functions: &mut FxHashMap, + identifiers: &[Identifier], + types: &[Type], + functions: &[HirFunction], + enable_allow_set_state_from_refs: bool, + next_block_id_counter: u32, + source_code: Option<&str>, +) -> Result, CompilerDiagnostic> { + let mut ref_derived_values: FxHashSet = FxHashSet::default(); + + // First pass: collect ref-derived values (needed before building control dominator checker) + // We do a pre-pass to seed ref_derived_values so the control dominator checker has them. + if enable_allow_set_state_from_refs { + for (_block_id, block) in &func.body.blocks { + for phi in &block.phis { + let is_phi_derived = phi.operands.values().any(|operand| { + is_derived_from_ref(operand.identifier, &ref_derived_values, identifiers, types) + }); + if is_phi_derived { + ref_derived_values.insert(phi.place.identifier); + } + } + + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + + let operands = collect_operands(&instr.value, functions); + let has_ref_operand = operands.iter().any(|op_id| { + is_derived_from_ref(*op_id, &ref_derived_values, identifiers, types) + }); + + if has_ref_operand { + ref_derived_values.insert(instr.lvalue.identifier); + if let InstructionValue::Destructure { lvalue, .. } = &instr.value { + collect_destructure_places(&lvalue.pattern, &mut ref_derived_values); + } + if let InstructionValue::StoreLocal { lvalue, .. } = &instr.value { + ref_derived_values.insert(lvalue.place.identifier); + } + } + + if let InstructionValue::PropertyLoad { object, property, .. } = &instr.value { + if *property == PropertyLiteral::String("current".to_string()) { + let obj_ident = &identifiers[object.identifier.0 as usize]; + let obj_ty = &types[obj_ident.type_.0 as usize]; + if is_use_ref_type(obj_ty) || is_ref_value_type(obj_ty) { + ref_derived_values.insert(instr.lvalue.identifier); + } + } + } + } + } + } + + // Build control dominator checker after collecting ref-derived values + let ref_controlled_blocks = if enable_allow_set_state_from_refs { + create_ref_controlled_block_checker( + func, + next_block_id_counter, + &ref_derived_values, + identifiers, + types, + )? + } else { + FxHashMap::default() + }; + + let is_ref_controlled_block = |block_id: BlockId| -> bool { + ref_controlled_blocks.get(&block_id).copied().unwrap_or(false) + }; + + // Reset and redo: second pass with control dominator info available + ref_derived_values.clear(); + + for (_block_id, block) in &func.body.blocks { + // Track ref-derived values through phis + if enable_allow_set_state_from_refs { + for phi in &block.phis { + if is_derived_from_ref( + phi.place.identifier, + &ref_derived_values, + identifiers, + types, + ) { + continue; + } + let is_phi_derived = phi.operands.values().any(|operand| { + is_derived_from_ref(operand.identifier, &ref_derived_values, identifiers, types) + }); + if is_phi_derived { + ref_derived_values.insert(phi.place.identifier); + } else { + // Fallback: check if any predecessor block is ref-controlled + let mut found = false; + for pred in phi.operands.keys() { + if is_ref_controlled_block(*pred) { + ref_derived_values.insert(phi.place.identifier); + found = true; + break; + } + } + if found { + continue; + } + } + } + } + + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + + // Track ref-derived values through instructions + if enable_allow_set_state_from_refs { + let operands = collect_operands(&instr.value, functions); + let has_ref_operand = operands.iter().any(|op_id| { + is_derived_from_ref(*op_id, &ref_derived_values, identifiers, types) + }); + + if has_ref_operand { + ref_derived_values.insert(instr.lvalue.identifier); + // For Destructure, also mark all pattern places as ref-derived + if let InstructionValue::Destructure { lvalue, .. } = &instr.value { + collect_destructure_places(&lvalue.pattern, &mut ref_derived_values); + } + // For StoreLocal, propagate to the local variable + if let InstructionValue::StoreLocal { lvalue, .. } = &instr.value { + ref_derived_values.insert(lvalue.place.identifier); + } + } + + // Special case: PropertyLoad of .current on ref/refValue + if let InstructionValue::PropertyLoad { object, property, .. } = &instr.value { + if *property == PropertyLiteral::String("current".to_string()) { + let obj_ident = &identifiers[object.identifier.0 as usize]; + let obj_ty = &types[obj_ident.type_.0 as usize]; + if is_use_ref_type(obj_ty) || is_ref_value_type(obj_ty) { + ref_derived_values.insert(instr.lvalue.identifier); + } + } + } + } + + match &instr.value { + InstructionValue::LoadLocal { place, .. } => { + if set_state_functions.contains_key(&place.identifier) { + let info = set_state_functions[&place.identifier].clone(); + set_state_functions.insert(instr.lvalue.identifier, info); + } + } + InstructionValue::StoreLocal { lvalue, value, .. } => { + if set_state_functions.contains_key(&value.identifier) { + let info = set_state_functions[&value.identifier].clone(); + set_state_functions.insert(lvalue.place.identifier, info.clone()); + set_state_functions.insert(instr.lvalue.identifier, info); + } + } + InstructionValue::CallExpression { callee, args, .. } => { + if is_set_state_type_by_id(callee.identifier, identifiers, types) + || set_state_functions.contains_key(&callee.identifier) + { + if enable_allow_set_state_from_refs { + // Check if the first argument is ref-derived + if let Some(first_arg) = args.first() { + if let PlaceOrSpread::Place(arg_place) = first_arg { + if is_derived_from_ref( + arg_place.identifier, + &ref_derived_values, + identifiers, + types, + ) { + // Allow setState when value is derived from ref + return Ok(None); + } + } + } + // Check if the current block is controlled by a ref-derived condition + if is_ref_controlled_block(block.id) { + continue; + } + } + // Get the user-visible identifier name, matching Babel's + // loc.identifierName behavior. Uses declaration_id to find + // the original named identifier when SSA creates unnamed copies. + let callee_name = get_identifier_name_with_loc( + callee.identifier, + identifiers, + &callee.loc, + source_code, + ); + return Ok(Some(SetStateInfo { + loc: callee.loc, + identifier_name: callee_name, + })); + } + } + _ => {} + } + } + } + Ok(None) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_set_state_in_render.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_set_state_in_render.rs new file mode 100644 index 0000000000000..331199117d1f2 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_no_set_state_in_render.rs @@ -0,0 +1,185 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Validates that the function does not unconditionally call setState during render. +//! +//! Port of ValidateNoSetStateInRender.ts. + +use rustc_hash::FxHashSet; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, +}; +use crate::react_compiler_hir::dominator::compute_unconditional_blocks; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + BlockId, HirFunction, Identifier, IdentifierId, InstructionValue, Type, +}; + +pub fn validate_no_set_state_in_render( + func: &HirFunction, + env: &mut Environment, +) -> Result<(), CompilerDiagnostic> { + let mut unconditional_set_state_functions: FxHashSet = FxHashSet::default(); + let next_block_id = env.next_block_id().0; + let diagnostics = validate_impl( + func, + &env.identifiers, + &env.types, + &env.functions, + next_block_id, + env.config.enable_use_keyed_state, + &mut unconditional_set_state_functions, + )?; + for diag in diagnostics { + env.record_diagnostic(diag); + } + Ok(()) +} + +fn is_set_state_id( + identifier_id: IdentifierId, + identifiers: &[Identifier], + types: &[Type], +) -> bool { + let ident = &identifiers[identifier_id.0 as usize]; + let ty = &types[ident.type_.0 as usize]; + crate::react_compiler_hir::is_set_state_type(ty) +} + +fn validate_impl( + func: &HirFunction, + identifiers: &[Identifier], + types: &[Type], + functions: &[HirFunction], + next_block_id_counter: u32, + enable_use_keyed_state: bool, + unconditional_set_state_functions: &mut FxHashSet, +) -> Result, CompilerDiagnostic> { + let unconditional_blocks: FxHashSet = + compute_unconditional_blocks(func, next_block_id_counter)?; + let mut active_manual_memo_id: Option = None; + let mut errors: Vec = Vec::new(); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + match &instr.value { + InstructionValue::LoadLocal { place, .. } => { + if unconditional_set_state_functions.contains(&place.identifier) { + unconditional_set_state_functions.insert(instr.lvalue.identifier); + } + } + InstructionValue::StoreLocal { lvalue, value, .. } => { + if unconditional_set_state_functions.contains(&value.identifier) { + unconditional_set_state_functions.insert(lvalue.place.identifier); + unconditional_set_state_functions.insert(instr.lvalue.identifier); + } + } + InstructionValue::ObjectMethod { lowered_func, .. } + | InstructionValue::FunctionExpression { lowered_func, .. } => { + let inner_func = &functions[lowered_func.func.0 as usize]; + + // Check if any operand references a setState. + // For FunctionExpression/ObjectMethod, operands are the context captures. + let has_set_state_operand = inner_func.context.iter().any(|ctx_place| { + is_set_state_id(ctx_place.identifier, identifiers, types) + || unconditional_set_state_functions.contains(&ctx_place.identifier) + }); + + if has_set_state_operand { + let inner_errors = validate_impl( + inner_func, + identifiers, + types, + functions, + next_block_id_counter, + enable_use_keyed_state, + unconditional_set_state_functions, + )?; + if !inner_errors.is_empty() { + unconditional_set_state_functions.insert(instr.lvalue.identifier); + } + } + } + InstructionValue::StartMemoize { manual_memo_id, .. } => { + assert!( + active_manual_memo_id.is_none(), + "Unexpected nested StartMemoize instructions" + ); + active_manual_memo_id = Some(*manual_memo_id); + } + InstructionValue::FinishMemoize { manual_memo_id, .. } => { + assert!( + active_manual_memo_id == Some(*manual_memo_id), + "Expected FinishMemoize to align with previous StartMemoize instruction" + ); + active_manual_memo_id = None; + } + InstructionValue::CallExpression { callee, .. } => { + if is_set_state_id(callee.identifier, identifiers, types) + || unconditional_set_state_functions.contains(&callee.identifier) + { + if active_manual_memo_id.is_some() { + errors.push( + CompilerDiagnostic::new( + ErrorCategory::RenderSetState, + "Calling setState from useMemo may trigger an infinite loop", + Some( + "Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)".to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: callee.loc, + message: Some("Found setState() within useMemo()".to_string()), + identifier_name: None, + }), + ); + } else if unconditional_blocks.contains(&block.id) { + if enable_use_keyed_state { + errors.push( + CompilerDiagnostic::new( + ErrorCategory::RenderSetState, + "Cannot call setState during render", + Some( + "Calling setState during render may trigger an infinite loop.\n\ + * To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n\ + * To derive data from other state/props, compute the derived data during render without using state".to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: callee.loc, + message: Some("Found setState() in render".to_string()), + identifier_name: None, + }), + ); + } else { + errors.push( + CompilerDiagnostic::new( + ErrorCategory::RenderSetState, + "Cannot call setState during render", + Some( + "Calling setState during render may trigger an infinite loop.\n\ + * To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n\ + * To derive data from other state/props, compute the derived data during render without using state".to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: callee.loc, + message: Some("Found setState() in render".to_string()), + identifier_name: None, + }), + ); + } + } + } + } + _ => {} + } + } + } + + Ok(errors) +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_preserved_manual_memoization.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_preserved_manual_memoization.rs new file mode 100644 index 0000000000000..6adbd330c65c0 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_preserved_manual_memoization.rs @@ -0,0 +1,747 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Port of ValidatePreservedManualMemoization.ts +//! +//! Validates that all explicit manual memoization (useMemo/useCallback) was +//! accurately preserved, and that no originally memoized values became +//! unmemoized in the output. + +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::{ + DeclarationId, DependencyPathEntry, Identifier, IdentifierId, IdentifierName, InstructionKind, + InstructionValue, ManualMemoDependency, ManualMemoDependencyRoot, Place, ReactiveBlock, + ReactiveFunction, ReactiveInstruction, ReactiveScopeBlock, ReactiveStatement, ReactiveValue, + ScopeId, +}; + +/// State tracked during manual memo validation within a StartMemoize..FinishMemoize range. +struct ManualMemoBlockState { + /// Reassigned temporaries (declaration_id -> set of identifier ids that were reassigned to it). + reassignments: FxHashMap>, + /// Source location of the StartMemoize instruction. + loc: Option, + /// Declarations produced within this manual memo block. + decls: FxHashSet, + /// Normalized deps from source (useMemo/useCallback dep array). + deps_from_source: Option>, + /// Manual memo id from StartMemoize. + manual_memo_id: u32, +} + +/// Top-level visitor state. +struct VisitorState<'a, 'h> { + env: &'a mut Environment<'h>, + manual_memo_state: Option, + /// Completed (non-pruned) scope IDs. + scopes: FxHashSet, + /// Completed pruned scope IDs. + pruned_scopes: FxHashSet, + /// Map from identifier ID to its normalized manual memo dependency. + temporaries: FxHashMap, +} + +/// Validate that manual memoization (useMemo/useCallback) is preserved. +/// +/// Walks the reactive function looking for StartMemoize/FinishMemoize instructions +/// and checks that: +/// 1. Dependencies' scopes have completed before the memo block starts +/// 2. Memoized values are actually within scopes (not unmemoized) +/// 3. Inferred scope dependencies match the source dependencies +pub fn validate_preserved_manual_memoization( + func: &ReactiveFunction<'_>, + env: &mut Environment<'_>, +) { + let mut state = VisitorState { + env, + manual_memo_state: None, + scopes: FxHashSet::default(), + pruned_scopes: FxHashSet::default(), + temporaries: FxHashMap::default(), + }; + visit_block(&func.body, &mut state); +} + +fn is_named(ident: &Identifier) -> bool { + matches!(ident.name, Some(IdentifierName::Named(_))) +} + +fn visit_block(block: &ReactiveBlock, state: &mut VisitorState<'_, '_>) { + for stmt in block { + visit_statement(stmt, state); + } +} + +fn visit_statement(stmt: &ReactiveStatement, state: &mut VisitorState<'_, '_>) { + match stmt { + ReactiveStatement::Instruction(instr) => { + visit_instruction(instr, state); + } + ReactiveStatement::Terminal(terminal) => { + visit_terminal(terminal, state); + } + ReactiveStatement::Scope(scope_block) => { + visit_scope(scope_block, state); + } + ReactiveStatement::PrunedScope(pruned) => { + visit_pruned_scope(pruned, state); + } + } +} + +fn visit_terminal( + terminal: &crate::react_compiler_hir::ReactiveTerminalStatement, + state: &mut VisitorState<'_, '_>, +) { + use crate::react_compiler_hir::ReactiveTerminal; + match &terminal.terminal { + ReactiveTerminal::If { consequent, alternate, .. } => { + visit_block(consequent, state); + if let Some(alt) = alternate { + visit_block(alt, state); + } + } + ReactiveTerminal::Switch { cases, .. } => { + for case in cases { + if let Some(ref block) = case.block { + visit_block(block, state); + } + } + } + ReactiveTerminal::For { loop_block, .. } + | ReactiveTerminal::ForOf { loop_block, .. } + | ReactiveTerminal::ForIn { loop_block, .. } + | ReactiveTerminal::While { loop_block, .. } + | ReactiveTerminal::DoWhile { loop_block, .. } => { + visit_block(loop_block, state); + } + ReactiveTerminal::Label { block, .. } => { + visit_block(block, state); + } + ReactiveTerminal::Try { block, handler, .. } => { + visit_block(block, state); + visit_block(handler, state); + } + _ => {} + } +} + +fn visit_scope(scope_block: &ReactiveScopeBlock, state: &mut VisitorState<'_, '_>) { + // Traverse the scope's instructions first + visit_block(&scope_block.instructions, state); + + // After traversing, validate scope dependencies against manual memo deps + if let Some(ref memo_state) = state.manual_memo_state { + if let Some(ref deps_from_source) = memo_state.deps_from_source { + let scope = &state.env.scopes[scope_block.scope.0 as usize]; + let deps = scope.dependencies.clone(); + let memo_loc = memo_state.loc; + let decls = memo_state.decls.clone(); + let deps_from_source = deps_from_source.clone(); + let temporaries = state.temporaries.clone(); + for dep in &deps { + validate_inferred_dep( + dep.identifier, + &dep.path, + &temporaries, + &decls, + &deps_from_source, + state.env, + memo_loc, + ); + } + } + } + + // Mark scope and merged scopes as completed + let scope = &state.env.scopes[scope_block.scope.0 as usize]; + let merged = scope.merged.clone(); + state.scopes.insert(scope_block.scope); + for merged_id in merged { + state.scopes.insert(merged_id); + } +} + +fn visit_pruned_scope( + pruned: &crate::react_compiler_hir::PrunedReactiveScopeBlock, + state: &mut VisitorState<'_, '_>, +) { + visit_block(&pruned.instructions, state); + state.pruned_scopes.insert(pruned.scope); +} + +fn visit_instruction(instr: &ReactiveInstruction, state: &mut VisitorState<'_, '_>) { + // Record temporaries and deps in the instruction's value + record_temporaries(instr, state); + + match &instr.value { + ReactiveValue::Instruction(InstructionValue::StartMemoize { + manual_memo_id, + deps, + has_invalid_deps, + .. + }) => { + // TS: CompilerError.invariant(state.manualMemoState == null, ...) + if state.manual_memo_state.is_some() { + return; + } + + // TS: if (value.hasInvalidDeps === true) { return; } + if *has_invalid_deps { + return; + } + + let deps_from_source = deps.clone(); + + state.manual_memo_state = Some(ManualMemoBlockState { + loc: instr.loc, + decls: FxHashSet::default(), + deps_from_source, + manual_memo_id: *manual_memo_id, + reassignments: FxHashMap::default(), + }); + + // Check that each dependency's scope has completed before the memo + // TS: for (const {identifier, loc} of eachInstructionValueOperand(value)) + let operand_places = start_memoize_operands(deps); + for place in &operand_places { + let ident = &state.env.identifiers[place.identifier.0 as usize]; + if let Some(scope_id) = ident.scope { + if !state.scopes.contains(&scope_id) && !state.pruned_scopes.contains(&scope_id) + { + let diag = CompilerDiagnostic::new( + ErrorCategory::PreserveManualMemo, + "Existing memoization could not be preserved", + Some( + "React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. \ + This dependency may be mutated later, which could cause the value to change unexpectedly".to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: place.loc, + message: Some( + "This dependency may be modified later".to_string(), + ), + identifier_name: None, + }); + state.env.record_diagnostic(diag); + } + } + } + } + ReactiveValue::Instruction(InstructionValue::FinishMemoize { + decl, + pruned, + manual_memo_id, + .. + }) => { + if state.manual_memo_state.is_none() { + // StartMemoize had invalid deps, skip validation + return; + } + + // TS: CompilerError.invariant(state.manualMemoState.manualMemoId === value.manualMemoId, ...) + if state + .manual_memo_state + .as_ref() + .map_or(true, |s| s.manual_memo_id != *manual_memo_id) + { + state.manual_memo_state = None; + return; + } + + let memo_state = state.manual_memo_state.take().unwrap(); + + if !pruned { + // Check if the declared value is unmemoized + let decl_ident = &state.env.identifiers[decl.identifier.0 as usize]; + + if decl_ident.scope.is_none() { + // If the manual memo was inlined (useMemo -> IIFE), check reassignments + let decls_to_check = memo_state + .reassignments + .get(&decl_ident.declaration_id) + .map(|ids| ids.iter().copied().collect::>()) + .unwrap_or_else(|| vec![decl.identifier]); + + for id in decls_to_check { + if is_unmemoized(id, &state.scopes, &state.env.identifiers) { + record_unmemoized_error(decl.loc, state.env); + } + } + } else { + // Single identifier with scope + if is_unmemoized(decl.identifier, &state.scopes, &state.env.identifiers) { + record_unmemoized_error(decl.loc, state.env); + } + } + } + } + ReactiveValue::Instruction(InstructionValue::StoreLocal { lvalue, value, .. }) => { + // Track reassignments from inlining of manual memo + if state.manual_memo_state.is_some() && lvalue.kind == InstructionKind::Reassign { + let decl_id = + state.env.identifiers[lvalue.place.identifier.0 as usize].declaration_id; + state + .manual_memo_state + .as_mut() + .unwrap() + .reassignments + .entry(decl_id) + .or_default() + .insert(value.identifier); + } + } + ReactiveValue::Instruction(InstructionValue::LoadLocal { place, .. }) => { + if state.manual_memo_state.is_some() { + let place_ident = &state.env.identifiers[place.identifier.0 as usize]; + if let Some(ref lvalue) = instr.lvalue { + let lvalue_ident = &state.env.identifiers[lvalue.identifier.0 as usize]; + if place_ident.scope.is_some() && lvalue_ident.scope.is_none() { + state + .manual_memo_state + .as_mut() + .unwrap() + .reassignments + .entry(lvalue_ident.declaration_id) + .or_default() + .insert(place.identifier); + } + } + } + } + _ => {} + } +} + +fn record_unmemoized_error(loc: Option, env: &mut Environment) { + let diag = CompilerDiagnostic::new( + ErrorCategory::PreserveManualMemo, + "Existing memoization could not be preserved", + Some( + "React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output".to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc, + message: Some("Could not preserve existing memoization".to_string()), + identifier_name: None, + }); + env.record_diagnostic(diag); +} + +/// Record temporaries from an instruction. +/// TS: `recordTemporaries` +fn record_temporaries(instr: &ReactiveInstruction, state: &mut VisitorState<'_, '_>) { + let lvalue = &instr.lvalue; + let lv_id = lvalue.as_ref().map(|lv| lv.identifier); + if let Some(id) = lv_id { + if state.temporaries.contains_key(&id) { + return; + } + } + + if let Some(ref lvalue) = instr.lvalue { + let lv_ident = &state.env.identifiers[lvalue.identifier.0 as usize]; + if is_named(lv_ident) && state.manual_memo_state.is_some() { + state.manual_memo_state.as_mut().unwrap().decls.insert(lv_ident.declaration_id); + } + } + + // Record deps from the instruction value first (before setting lvalue temporary) + record_deps_in_value(&instr.value, state); + + // Then set the lvalue temporary (TS always sets this, even for unnamed lvalues) + if let Some(ref lvalue) = instr.lvalue { + state.temporaries.insert( + lvalue.identifier, + ManualMemoDependency { + root: ManualMemoDependencyRoot::NamedLocal { + value: lvalue.clone(), + constant: false, + }, + path: Vec::new(), + loc: lvalue.loc, + }, + ); + } +} + +/// Record dependencies from a reactive value. +/// TS: `recordDepsInValue` +fn record_deps_in_value(value: &ReactiveValue, state: &mut VisitorState<'_, '_>) { + match value { + ReactiveValue::SequenceExpression { instructions, value, .. } => { + for instr in instructions { + visit_instruction(instr, state); + } + record_deps_in_value(value, state); + } + ReactiveValue::OptionalExpression { value: inner, .. } => { + record_deps_in_value(inner, state); + } + ReactiveValue::ConditionalExpression { test, consequent, alternate, .. } => { + record_deps_in_value(test, state); + record_deps_in_value(consequent, state); + record_deps_in_value(alternate, state); + } + ReactiveValue::LogicalExpression { left, right, .. } => { + record_deps_in_value(left, state); + record_deps_in_value(right, state); + } + ReactiveValue::Instruction(iv) => { + // TS: collectMaybeMemoDependencies(value, this.temporaries, false) + // Called for side-effect of building up the dependency chain through + // LoadGlobal -> PropertyLoad -> ... The return value is discarded here + // (only used in DropManualMemoization's caller), but we need to store + // the result in temporaries for the lvalue of the enclosing instruction. + // That storage is handled by record_temporaries after this function returns. + + // Track store targets within manual memo blocks + // TS: if (value.kind === 'StoreLocal' || value.kind === 'StoreContext' || value.kind === 'Destructure') + match iv { + InstructionValue::StoreLocal { lvalue, .. } + | InstructionValue::StoreContext { lvalue, .. } => { + if let Some(ref mut memo_state) = state.manual_memo_state { + let ident = &state.env.identifiers[lvalue.place.identifier.0 as usize]; + memo_state.decls.insert(ident.declaration_id); + if is_named(ident) { + state.temporaries.insert( + lvalue.place.identifier, + ManualMemoDependency { + root: ManualMemoDependencyRoot::NamedLocal { + value: lvalue.place.clone(), + constant: false, + }, + path: Vec::new(), + loc: lvalue.place.loc, + }, + ); + } + } + } + InstructionValue::Destructure { lvalue, .. } => { + if let Some(ref mut memo_state) = state.manual_memo_state { + for place in destructure_lvalue_places(&lvalue.pattern) { + let ident = &state.env.identifiers[place.identifier.0 as usize]; + memo_state.decls.insert(ident.declaration_id); + if is_named(ident) { + state.temporaries.insert( + place.identifier, + ManualMemoDependency { + root: ManualMemoDependencyRoot::NamedLocal { + value: place.clone(), + constant: false, + }, + path: Vec::new(), + loc: place.loc, + }, + ); + } + } + } + } + _ => {} + } + } + } +} + +/// Get operand places from a StartMemoize instruction's deps. +fn start_memoize_operands(deps: &Option>) -> Vec { + let mut result = Vec::new(); + if let Some(deps) = deps { + for dep in deps { + if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &dep.root { + result.push(value.clone()); + } + } + } + result +} + +/// Get lvalue places from a Destructure pattern. +fn destructure_lvalue_places(pattern: &crate::react_compiler_hir::Pattern) -> Vec<&Place> { + let mut result = Vec::new(); + match pattern { + crate::react_compiler_hir::Pattern::Array(arr) => { + for item in &arr.items { + match item { + crate::react_compiler_hir::ArrayPatternElement::Place(place) => { + result.push(place); + } + crate::react_compiler_hir::ArrayPatternElement::Spread(spread) => { + result.push(&spread.place); + } + crate::react_compiler_hir::ArrayPatternElement::Hole => {} + } + } + } + crate::react_compiler_hir::Pattern::Object(obj) => { + for entry in &obj.properties { + match entry { + crate::react_compiler_hir::ObjectPropertyOrSpread::Property(prop) => { + result.push(&prop.place); + } + crate::react_compiler_hir::ObjectPropertyOrSpread::Spread(spread) => { + result.push(&spread.place); + } + } + } + } + } + result +} + +/// Check if an identifier is unmemoized (has a scope that hasn't completed). +fn is_unmemoized( + id: IdentifierId, + completed_scopes: &FxHashSet, + identifiers: &[Identifier], +) -> bool { + let ident = &identifiers[id.0 as usize]; + if let Some(scope_id) = ident.scope { !completed_scopes.contains(&scope_id) } else { false } +} + +// ============================================================================= +// Dependency comparison (port of compareDeps / validateInferredDep) +// ============================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum CompareDependencyResult { + Ok = 0, + RootDifference = 1, + PathDifference = 2, + Subpath = 3, + RefAccessDifference = 4, +} + +fn compare_deps( + inferred: &ManualMemoDependency, + source: &ManualMemoDependency, +) -> CompareDependencyResult { + let roots_equal = match (&inferred.root, &source.root) { + ( + ManualMemoDependencyRoot::Global { identifier_name: a }, + ManualMemoDependencyRoot::Global { identifier_name: b }, + ) => a == b, + ( + ManualMemoDependencyRoot::NamedLocal { value: a, .. }, + ManualMemoDependencyRoot::NamedLocal { value: b, .. }, + ) => a.identifier == b.identifier, + _ => false, + }; + if !roots_equal { + return CompareDependencyResult::RootDifference; + } + + let min_len = inferred.path.len().min(source.path.len()); + let mut is_subpath = true; + for i in 0..min_len { + if inferred.path[i].property != source.path[i].property { + is_subpath = false; + break; + } else if inferred.path[i].optional != source.path[i].optional { + return CompareDependencyResult::PathDifference; + } + } + + if is_subpath + && (source.path.len() == inferred.path.len() + || (inferred.path.len() >= source.path.len() + && !inferred.path.iter().any(|t| { + t.property + == crate::react_compiler_hir::PropertyLiteral::String("current".to_string()) + }))) + { + CompareDependencyResult::Ok + } else if is_subpath { + if source.path.iter().any(|t| { + t.property == crate::react_compiler_hir::PropertyLiteral::String("current".to_string()) + }) || inferred.path.iter().any(|t| { + t.property == crate::react_compiler_hir::PropertyLiteral::String("current".to_string()) + }) { + CompareDependencyResult::RefAccessDifference + } else { + CompareDependencyResult::Subpath + } + } else { + CompareDependencyResult::PathDifference + } +} + +/// Pretty-print a reactive scope dependency (e.g., `x.a.b?.c`) +fn pretty_print_scope_dependency( + dep_id: IdentifierId, + dep_path: &[DependencyPathEntry], + identifiers: &[crate::react_compiler_hir::Identifier], +) -> String { + let ident = &identifiers[dep_id.0 as usize]; + let root_str = match &ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => n.clone(), + Some(crate::react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(), + None => "[unnamed]".to_string(), + }; + let path_str: String = dep_path + .iter() + .map(|entry| { + let prop = match &entry.property { + crate::react_compiler_hir::PropertyLiteral::String(s) => s.clone(), + crate::react_compiler_hir::PropertyLiteral::Number(n) => format!("{}", n), + }; + if entry.optional { format!("?.{}", prop) } else { format!(".{}", prop) } + }) + .collect(); + format!("{}{}", root_str, path_str) +} + +/// Pretty-print a manual memo dependency for error messages. +fn print_manual_memo_dependency( + dep: &ManualMemoDependency, + identifiers: &[crate::react_compiler_hir::Identifier], + with_optional: bool, +) -> String { + let root_str = match &dep.root { + ManualMemoDependencyRoot::NamedLocal { value, .. } => { + let ident = &identifiers[value.identifier.0 as usize]; + match &ident.name { + Some(crate::react_compiler_hir::IdentifierName::Named(n)) => n.clone(), + Some(crate::react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(), + None => "[unnamed]".to_string(), + } + } + ManualMemoDependencyRoot::Global { identifier_name } => identifier_name.clone(), + }; + let path_str: String = dep + .path + .iter() + .map(|entry| { + let prop = match &entry.property { + crate::react_compiler_hir::PropertyLiteral::String(s) => s.clone(), + crate::react_compiler_hir::PropertyLiteral::Number(n) => format!("{}", n), + }; + if with_optional && entry.optional { + format!("?.{}", prop) + } else { + format!(".{}", prop) + } + }) + .collect(); + format!("{}{}", root_str, path_str) +} + +fn get_compare_dependency_result_description(result: CompareDependencyResult) -> &'static str { + match result { + CompareDependencyResult::Ok => "Dependencies equal", + CompareDependencyResult::RootDifference | CompareDependencyResult::PathDifference => { + "Inferred different dependency than source" + } + CompareDependencyResult::RefAccessDifference => "Differences in ref.current access", + CompareDependencyResult::Subpath => "Inferred less specific property than source", + } +} + +/// Validate that an inferred dependency matches a source dependency or was produced +/// within the manual memo block. +fn validate_inferred_dep( + dep_id: IdentifierId, + dep_path: &[DependencyPathEntry], + temporaries: &FxHashMap, + decls_within_memo_block: &FxHashSet, + valid_deps_in_memo_block: &[ManualMemoDependency], + env: &mut Environment, + memo_location: Option, +) { + // Normalize the dependency through temporaries + let normalized_dep = if let Some(temp) = temporaries.get(&dep_id) { + let mut path = temp.path.clone(); + path.extend_from_slice(dep_path); + ManualMemoDependency { root: temp.root.clone(), path, loc: temp.loc } + } else { + let ident = &env.identifiers[dep_id.0 as usize]; + // TS: CompilerError.invariant(dep.identifier.name?.kind === 'named', ...) + if !is_named(ident) { + return; + } + ManualMemoDependency { + root: ManualMemoDependencyRoot::NamedLocal { + value: Place { + identifier: dep_id, + effect: crate::react_compiler_hir::Effect::Read, + reactive: false, + loc: ident.loc, + }, + constant: false, + }, + path: dep_path.to_vec(), + loc: ident.loc, + } + }; + + // Check if the dep was declared within the memo block + if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &normalized_dep.root { + let ident = &env.identifiers[value.identifier.0 as usize]; + if decls_within_memo_block.contains(&ident.declaration_id) { + return; + } + } + + // Compare against each valid source dependency + let mut error_diagnostic: Option = None; + for source_dep in valid_deps_in_memo_block { + let result = compare_deps(&normalized_dep, source_dep); + if result == CompareDependencyResult::Ok { + return; + } + error_diagnostic = Some(match error_diagnostic { + Some(prev) => prev.max(result), + None => result, + }); + } + + let ident = &env.identifiers[dep_id.0 as usize]; + + let extra = if is_named(ident) { + // Use the original dep_id/dep_path (matching TS prettyPrintScopeDependency(dep)) + let dep_str = pretty_print_scope_dependency(dep_id, dep_path, &env.identifiers); + let source_deps_str: String = valid_deps_in_memo_block + .iter() + .map(|d| print_manual_memo_dependency(d, &env.identifiers, true)) + .collect::>() + .join(", "); + let result_desc = error_diagnostic + .map(|d| get_compare_dependency_result_description(d).to_string()) + .unwrap_or_else(|| "Inferred dependency not present in source".to_string()); + format!( + "The inferred dependency was `{}`, but the source dependencies were [{}]. {}", + dep_str, source_deps_str, result_desc + ) + } else { + String::new() + }; + + let description = format!( + "React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. \ + The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. {}", + extra + ); + + let diag = CompilerDiagnostic::new( + ErrorCategory::PreserveManualMemo, + "Existing memoization could not be preserved", + Some(description.trim().to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: memo_location, + message: Some("Could not preserve existing manual memoization".to_string()), + identifier_name: None, + }); + env.record_diagnostic(diag); +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_static_components.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_static_components.rs new file mode 100644 index 0000000000000..8c8aa9ef7f143 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_static_components.rs @@ -0,0 +1,98 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Validates against components that are created dynamically and whose identity +//! is not guaranteed to be stable (which would cause the component to reset on +//! each re-render). +//! +//! Port of ValidateStaticComponents.ts. + +use rustc_hash::FxHashMap; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::{HirFunction, IdentifierId, InstructionValue, JsxTag}; + +/// Validates that components used in JSX are not dynamically created during render. +/// +/// Returns a CompilerError containing all diagnostics found (may be empty). +/// Called via `env.logErrors()` pattern in Pipeline.ts. +pub fn validate_static_components(func: &HirFunction) -> CompilerError { + let mut error = CompilerError::new(); + let mut known_dynamic_components: FxHashMap> = + FxHashMap::default(); + + for (_block_id, block) in &func.body.blocks { + // Process phis: propagate dynamic component knowledge through phi nodes + 'phis: for phi in &block.phis { + for (_pred, operand) in &phi.operands { + if let Some(loc) = known_dynamic_components.get(&operand.identifier) { + known_dynamic_components.insert(phi.place.identifier, *loc); + continue 'phis; + } + } + } + + // Process instructions + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue_id = instr.lvalue.identifier; + let value = &instr.value; + + match value { + InstructionValue::FunctionExpression { loc, .. } + | InstructionValue::NewExpression { loc, .. } + | InstructionValue::MethodCall { loc, .. } + | InstructionValue::CallExpression { loc, .. } => { + known_dynamic_components.insert(lvalue_id, *loc); + } + InstructionValue::LoadLocal { place, .. } => { + if let Some(loc) = known_dynamic_components.get(&place.identifier) { + known_dynamic_components.insert(lvalue_id, *loc); + } + } + InstructionValue::StoreLocal { lvalue, value: val, .. } => { + if let Some(loc) = known_dynamic_components.get(&val.identifier) { + let loc = *loc; + known_dynamic_components.insert(lvalue_id, loc); + known_dynamic_components.insert(lvalue.place.identifier, loc); + } + } + InstructionValue::JsxExpression { tag, .. } => { + if let JsxTag::Place(tag_place) = tag { + if let Some(location) = known_dynamic_components.get(&tag_place.identifier) + { + let location = *location; + let diagnostic = CompilerDiagnostic::new( + ErrorCategory::StaticComponents, + "Cannot create components during render", + Some("Components created during render will reset their state each time they are created. Declare components outside of render".to_string()), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: tag_place.loc, + message: Some( + "This component is created during render".to_string(), + ), + identifier_name: None, + }) + .with_detail(CompilerDiagnosticDetail::Error { + loc: location, + message: Some( + "The component is created during render here".to_string(), + ), + identifier_name: None, + }); + error.push_diagnostic(diagnostic); + } + } + } + _ => {} + } + } + } + + error +} diff --git a/crates/oxc_react_compiler/src/react_compiler_validation/validate_use_memo.rs b/crates/oxc_react_compiler/src/react_compiler_validation/validate_use_memo.rs new file mode 100644 index 0000000000000..2b42e2336b6e6 --- /dev/null +++ b/crates/oxc_react_compiler/src/react_compiler_validation/validate_use_memo.rs @@ -0,0 +1,308 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_diagnostics::{ + CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, SourceLocation, +}; +use crate::react_compiler_hir::environment::Environment; +use crate::react_compiler_hir::visitors::{ + each_instruction_value_operand_with_functions, each_terminal_operand, +}; +use crate::react_compiler_hir::{ + FunctionId, HirFunction, IdentifierId, InstructionValue, ParamPattern, Place, PlaceOrSpread, + ReturnVariant, Terminal, +}; + +/// Validates useMemo() usage patterns. +/// +/// Port of ValidateUseMemo.ts. +/// Returns VoidUseMemo errors separately (for logging via logErrors, not as compile errors). +pub fn validate_use_memo(func: &HirFunction, env: &mut Environment) -> CompilerError { + validate_use_memo_impl( + func, + &env.functions, + &mut env.errors, + env.config.validate_no_void_use_memo, + ) +} + +/// Information about a FunctionExpression needed for validation. +struct FuncExprInfo { + func_id: FunctionId, + loc: Option, +} + +fn validate_use_memo_impl( + func: &HirFunction, + functions: &[HirFunction], + errors: &mut CompilerError, + validate_no_void_use_memo: bool, +) -> CompilerError { + let mut void_memo_errors = CompilerError::new(); + let mut use_memos: FxHashSet = FxHashSet::default(); + let mut react: FxHashSet = FxHashSet::default(); + let mut func_exprs: FxHashMap = FxHashMap::default(); + let mut unused_use_memos: FxHashMap)> = + FxHashMap::default(); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + let lvalue = &instr.lvalue; + let value = &instr.value; + + // Remove used operands from unused_use_memos + if !unused_use_memos.is_empty() { + for operand_id in each_instruction_value_operand_ids(value, functions) { + unused_use_memos.remove(&operand_id); + } + } + + match value { + InstructionValue::LoadGlobal { binding, .. } => { + let name = binding.name(); + if name == "useMemo" { + use_memos.insert(lvalue.identifier); + } else if name == "React" { + react.insert(lvalue.identifier); + } + } + InstructionValue::PropertyLoad { object, property, .. } => { + if react.contains(&object.identifier) { + if let crate::react_compiler_hir::PropertyLiteral::String(prop_name) = + property + { + if prop_name == "useMemo" { + use_memos.insert(lvalue.identifier); + } + } + } + } + InstructionValue::FunctionExpression { lowered_func, loc, .. } => { + func_exprs.insert( + lvalue.identifier, + FuncExprInfo { func_id: lowered_func.func, loc: *loc }, + ); + } + InstructionValue::CallExpression { callee, args, .. } => { + handle_possible_use_memo_call( + functions, + errors, + &mut void_memo_errors, + &use_memos, + &func_exprs, + &mut unused_use_memos, + callee, + args, + lvalue, + validate_no_void_use_memo, + ); + } + InstructionValue::MethodCall { property, args, .. } => { + handle_possible_use_memo_call( + functions, + errors, + &mut void_memo_errors, + &use_memos, + &func_exprs, + &mut unused_use_memos, + property, + args, + lvalue, + validate_no_void_use_memo, + ); + } + _ => {} + } + } + + // Check terminal operands for unused_use_memos + if !unused_use_memos.is_empty() { + for operand_id in each_terminal_operand_ids(&block.terminal) { + unused_use_memos.remove(&operand_id); + } + } + } + + // Report unused useMemo results + if !unused_use_memos.is_empty() { + for (loc, ident_name) in unused_use_memos.values() { + void_memo_errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::VoidUseMemo, + "useMemo() result is unused", + Some( + "This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects" + .to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: Some(*loc), + message: Some("useMemo() result is unused".to_string()), + identifier_name: ident_name.clone(), + }), + ); + } + } + + void_memo_errors +} + +#[allow(clippy::too_many_arguments)] +fn handle_possible_use_memo_call( + functions: &[HirFunction], + errors: &mut CompilerError, + void_memo_errors: &mut CompilerError, + use_memos: &FxHashSet, + func_exprs: &FxHashMap, + unused_use_memos: &mut FxHashMap)>, + callee: &Place, + args: &[PlaceOrSpread], + lvalue: &Place, + validate_no_void_use_memo: bool, +) { + let is_use_memo = use_memos.contains(&callee.identifier); + if !is_use_memo || args.is_empty() { + return; + } + + let first_arg = match &args[0] { + PlaceOrSpread::Place(place) => place, + PlaceOrSpread::Spread(_) => return, + }; + + let body_info = match func_exprs.get(&first_arg.identifier) { + Some(info) => info, + None => return, + }; + + let body_func = &functions[body_info.func_id.0 as usize]; + + // Validate no parameters + if !body_func.params.is_empty() { + let first_param = &body_func.params[0]; + let loc = match first_param { + ParamPattern::Place(place) => place.loc, + ParamPattern::Spread(spread) => spread.place.loc, + }; + errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::UseMemo, + "useMemo() callbacks may not accept parameters", + Some( + "useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation" + .to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc, + message: Some("Callbacks with parameters are not supported".to_string()), + identifier_name: None, + }), + ); + } + + // Validate not async or generator + if body_func.is_async || body_func.generator { + errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::UseMemo, + "useMemo() callbacks may not be async or generator functions", + Some( + "useMemo() callbacks are called once and must synchronously return a value" + .to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: body_info.loc, + message: Some("Async and generator functions are not supported".to_string()), + identifier_name: None, + }), + ); + } + + // Validate no context variable assignment + validate_no_context_variable_assignment(body_func, errors); + + if validate_no_void_use_memo && !has_non_void_return(body_func) { + void_memo_errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::VoidUseMemo, + "useMemo() callbacks must return a value", + Some( + "This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects" + .to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: body_info.loc, + message: Some("useMemo() callbacks must return a value".to_string()), + identifier_name: None, + }), + ); + } else if validate_no_void_use_memo { + if let Some(callee_loc) = callee.loc { + // The callee is always useMemo/React.useMemo since we checked is_use_memo above. + // The identifierName in Babel's AST SourceLocation is "useMemo". + unused_use_memos.insert(lvalue.identifier, (callee_loc, Some("useMemo".to_string()))); + } + } +} + +fn validate_no_context_variable_assignment(func: &HirFunction, errors: &mut CompilerError) { + let context: FxHashSet = + func.context.iter().map(|place| place.identifier).collect(); + + for (_block_id, block) in &func.body.blocks { + for &instr_id in &block.instructions { + let instr = &func.instructions[instr_id.0 as usize]; + if let InstructionValue::StoreContext { lvalue, .. } = &instr.value { + if context.contains(&lvalue.place.identifier) { + errors.push_diagnostic( + CompilerDiagnostic::new( + ErrorCategory::UseMemo, + "useMemo() callbacks may not reassign variables declared outside of the callback", + Some( + "useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function" + .to_string(), + ), + ) + .with_detail(CompilerDiagnosticDetail::Error { + loc: lvalue.place.loc, + message: Some("Cannot reassign variable".to_string()), + identifier_name: None, + }), + ); + } + } + } + } +} + +fn has_non_void_return(func: &HirFunction) -> bool { + for (_block_id, block) in &func.body.blocks { + if let Terminal::Return { return_variant, .. } = &block.terminal { + if matches!(return_variant, ReturnVariant::Explicit | ReturnVariant::Implicit) { + return true; + } + } + } + false +} + +/// Collect all operand IdentifierIds from an InstructionValue. +/// Thin wrapper around canonical `each_instruction_value_operand_with_functions` that maps to ids. +fn each_instruction_value_operand_ids( + value: &InstructionValue, + functions: &[HirFunction], +) -> Vec { + each_instruction_value_operand_with_functions(value, functions) + .into_iter() + .map(|p| p.identifier) + .collect() +} + +/// Collect all operand IdentifierIds from a Terminal. +/// Thin wrapper around canonical `each_terminal_operand` that maps to ids. +fn each_terminal_operand_ids(terminal: &Terminal) -> Vec { + each_terminal_operand(terminal).into_iter().map(|p| p.identifier).collect() +} diff --git a/crates/oxc_react_compiler/src/scope.rs b/crates/oxc_react_compiler/src/scope.rs new file mode 100644 index 0000000000000..f693a12dec202 --- /dev/null +++ b/crates/oxc_react_compiler/src/scope.rs @@ -0,0 +1,326 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::react_compiler_utils::FxIndexMap; + +/// Identifies a scope in the scope table. Copy-able, used as an index. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ScopeId(pub u32); + +/// Identifies a binding (variable declaration) in the binding table. Copy-able, used as an index. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BindingId(pub u32); + +#[derive(Debug, Clone)] +pub struct ScopeData { + pub id: ScopeId, + pub parent: Option, + pub kind: ScopeKind, + /// Bindings declared directly in this scope, keyed by name. + /// Maps to BindingId for lookup in the binding table. + pub bindings: FxHashMap, +} + +#[derive(Debug, Clone)] +pub enum ScopeKind { + Program, + Function, + Block, + For, + Class, + Switch, + Catch, +} + +#[derive(Debug, Clone)] +pub struct BindingData { + pub id: BindingId, + pub name: String, + pub kind: BindingKind, + /// The scope this binding is declared in. + pub scope: ScopeId, + /// The type of the declaration AST node (e.g., "FunctionDeclaration", + /// "VariableDeclarator"). Used by the compiler to distinguish function + /// declarations from variable declarations during hoisting. + pub declaration_type: String, + /// The start offset of the binding's declaration identifier. + /// Used to distinguish declaration sites from references in `reference_to_binding`. + pub declaration_start: Option, + /// The node-ID of the binding's declaration identifier. + /// Preferred over `declaration_start` for distinguishing declarations from + /// references, as positions can collide for synthetic nodes at position 0. + pub declaration_node_id: Option, + /// For import bindings: the source module and import details. + pub import: Option, +} + +#[derive(Debug, Clone)] +pub enum BindingKind { + Var, + Let, + Const, + Param, + /// Import bindings (import declarations). + Module, + /// Function declarations (hoisted). + Hoisted, + /// Other local bindings (class declarations, etc.). + Local, + /// Binding kind not recognized by the serializer. + Unknown, +} + +#[derive(Debug, Clone)] +pub struct ImportBindingData { + /// The module specifier string (e.g., "react" in `import {useState} from 'react'`). + pub source: String, + pub kind: ImportBindingKind, + /// For named imports: the imported name (e.g., "bar" in `import {bar as baz} from 'foo'`). + /// None for default and namespace imports. + pub imported: Option, +} + +#[derive(Debug, Clone)] +pub enum ImportBindingKind { + Default, + Named, + Namespace, +} + +/// Complete scope information for a program. Stored separately from the AST +/// and linked via position-based lookup maps. +#[derive(Debug, Clone)] +pub struct ScopeInfo { + /// All scopes, indexed by ScopeId. scopes[id.0] gives the ScopeData for that scope. + pub scopes: Vec, + /// All bindings, indexed by BindingId. bindings[id.0] gives the BindingData. + pub bindings: Vec, + + /// Maps an AST node's start offset to the scope it creates. + /// + /// **NOT for identity lookups** — use `node_id_to_scope` (via `resolve_scope_for_node`) + /// instead. Retained only for position-range containment queries + /// (e.g., "is reference R inside function scope S?"). + pub node_to_scope: FxHashMap, + + /// Maps an AST node's start offset to the node's end offset. + /// Parallel to `node_to_scope` — used for position-range containment checks. + pub node_to_scope_end: FxHashMap, + + /// **DEPRECATED** — retained only for Babel bridge JSON deserialization. + /// All backends pass empty maps; only the Babel bridge populates this. + /// Use `ref_node_id_to_binding` for all lookups and iteration. + pub reference_to_binding: FxIndexMap, + + /// Maps an identifier reference's node-ID to the binding it resolves to. + /// Only present for identifiers that resolve to a binding (not globals). + /// Uses FxIndexMap to preserve insertion order. + pub ref_node_id_to_binding: FxIndexMap, + + /// Maps a scope-creating AST node's node-ID to the scope it creates. + pub node_id_to_scope: FxHashMap, + + /// The program-level (module) scope. Always scopes[0]. + pub program_scope: ScopeId, + + /// Direct child scopes per scope (`children[id.0]` = its child scope ids), + /// built once from the parent links. Lets descendant queries + /// (`find_block_scope_by_bindings`) run in O(descendants) instead of a + /// per-call O(scopes²) fixpoint over every scope. + pub children: Vec>, + + /// `(node_start, scope_id)` for the scopes that declare a `this` binding, in + /// `node_to_scope` iteration order. The TS `this`-parameter validation only + /// concerns these (usually none), so it iterates this set instead of every + /// scope per function — avoiding an O(functions × all-scopes) scan. + pub this_binding_scopes: Vec<(u32, ScopeId)>, + + /// `(binding_id, node_id)` pairs where a reference node id IS that binding's + /// own declaration site (so it isn't a real reference). Program-wide and + /// built once; `find_context_identifiers` consults it per function instead of + /// rebuilding it from every reference each time. + pub declaration_node_ids: FxHashSet<(BindingId, u32)>, +} + +impl ScopeInfo { + /// Look up a binding by name starting from the given scope, + /// walking up the parent chain. Returns None for globals. + pub fn get_binding(&self, scope_id: ScopeId, name: &str) -> Option { + let mut current = Some(scope_id); + while let Some(id) = current { + let scope = &self.scopes[id.0 as usize]; + if let Some(&binding_id) = scope.bindings.get(name) { + return Some(binding_id); + } + current = scope.parent; + } + None + } + + /// Look up the scope for an AST node by its unique node ID. + pub fn resolve_scope_by_node_id(&self, node_id: u32) -> Option { + self.node_id_to_scope.get(&node_id).copied() + } + + /// Resolve the scope for an AST node by node_id. + /// Returns None if node_id is None (the node has no scope entry) or if the + /// node_id doesn't map to any scope. This is expected for AST nodes that + /// don't create their own scope — e.g., a function body BlockStatement in + /// Babel shares the function's scope and never gets a _nodeId assigned by + /// scope extraction. + pub fn resolve_scope_for_node(&self, node_id: Option) -> Option { + let nid = node_id?; + self.node_id_to_scope.get(&nid).copied() + } + + /// Look up the binding for an identifier reference by its unique node ID. + /// Returns None for globals/unresolved references. + pub fn resolve_reference_by_node_id(&self, node_id: u32) -> Option { + self.ref_node_id_to_binding.get(&node_id).copied() + } + + /// Resolve the binding for an identifier by node_id. + /// Returns None if node_id is None or if the identifier doesn't resolve to + /// a binding (i.e., it's a global/unresolved reference). + pub fn resolve_reference_id_for_node(&self, node_id: Option) -> Option { + let nid = node_id?; + self.ref_node_id_to_binding.get(&nid).copied() + } + + /// Resolve the binding for an identifier by node_id. + /// Returns None if node_id is None or if the identifier doesn't resolve to + /// a binding (i.e., it's a global/unresolved reference). + pub fn resolve_reference_for_node(&self, node_id: Option) -> Option<&BindingData> { + self.resolve_reference_id_for_node(node_id).map(|id| &self.bindings[id.0 as usize]) + } + + /// Find a binding by name within the descendants of a given scope. + pub fn find_binding_in_descendants( + &self, + name: &str, + ancestor: ScopeId, + ) -> Option<&BindingData> { + // Collect `ancestor` plus all transitive descendants via the precomputed + // children adjacency — O(descendants). This previously ran a fixpoint that + // rescanned every scope on each pass (O(scopes²) per call); since these + // descendant queries run per block statement, they dominated lowering on + // large components. The resulting set is identical, so selection is unchanged. + let mut descendants = rustc_hash::FxHashSet::default(); + descendants.insert(ancestor); + let mut stack = vec![ancestor]; + while let Some(sid) = stack.pop() { + for &child in &self.children[sid.0 as usize] { + if descendants.insert(child) { + stack.push(child); + } + } + } + for sid in &descendants { + let scope = &self.scopes[sid.0 as usize]; + if let Some(id) = scope.bindings.get(name) { + return Some(&self.bindings[id.0 as usize]); + } + } + None + } + + /// Like find_binding_in_descendants, but returns the BindingData with its id + /// for use in resolve_binding. + pub fn find_binding_id_in_descendants( + &self, + name: &str, + ancestor: ScopeId, + ) -> Option<(BindingId, &BindingData)> { + // Collect `ancestor` plus all transitive descendants via the precomputed + // children adjacency — O(descendants). This previously ran a fixpoint that + // rescanned every scope on each pass (O(scopes²) per call); since these + // descendant queries run per block statement, they dominated lowering on + // large components. The resulting set is identical, so selection is unchanged. + let mut descendants = rustc_hash::FxHashSet::default(); + descendants.insert(ancestor); + let mut stack = vec![ancestor]; + while let Some(sid) = stack.pop() { + for &child in &self.children[sid.0 as usize] { + if descendants.insert(child) { + stack.push(child); + } + } + } + for sid in &descendants { + let scope = &self.scopes[sid.0 as usize]; + if let Some(&id) = scope.bindings.get(name) { + return Some((id, &self.bindings[id.0 as usize])); + } + } + None + } + + /// Get all bindings declared in a scope (for hoisting iteration). + pub fn scope_bindings(&self, scope_id: ScopeId) -> impl Iterator { + self.scopes[scope_id.0 as usize].bindings.values().map(|id| &self.bindings[id.0 as usize]) + } + + /// Get bindings from a scope AND its direct child block scopes. + /// In Babel, a function body's BlockStatement shares the function's scope, + /// so all bindings (var, const, let) appear in one scope. But our scope + /// extraction may split them: function scope has params/var, a child block + /// scope has const/let. This method merges them to match TS behavior. + pub fn scope_bindings_with_children( + &self, + scope_id: ScopeId, + ) -> impl Iterator { + let mut binding_ids: Vec = Vec::new(); + // Add bindings from the scope itself + for &id in self.scopes[scope_id.0 as usize].bindings.values() { + binding_ids.push(id); + } + // Add bindings from direct child block scopes + for scope in self.scopes.iter() { + if scope.parent == Some(scope_id) && matches!(scope.kind, ScopeKind::Block) { + for &id in scope.bindings.values() { + binding_ids.push(id); + } + } + } + binding_ids.into_iter().map(|id| &self.bindings[id.0 as usize]) + } + + /// Find a block scope by matching variable names declared within it. + /// Used for synthetic blocks (position 0) where position-based lookup fails. + /// The `is_claimed` predicate allows skipping scopes already matched to other blocks. + pub fn find_block_scope_by_bindings( + &self, + names: &[&str], + ancestor: ScopeId, + is_claimed: impl Fn(ScopeId) -> bool, + ) -> Option { + // Collect `ancestor` plus all transitive descendants via the precomputed + // children adjacency — O(descendants). This previously ran a fixpoint that + // rescanned every scope on each pass (O(scopes²) per call); since these + // descendant queries run per block statement, they dominated lowering on + // large components. The resulting set is identical, so selection is unchanged. + let mut descendants = rustc_hash::FxHashSet::default(); + descendants.insert(ancestor); + let mut stack = vec![ancestor]; + while let Some(sid) = stack.pop() { + for &child in &self.children[sid.0 as usize] { + if descendants.insert(child) { + stack.push(child); + } + } + } + for sid in &descendants { + let scope = &self.scopes[sid.0 as usize]; + if matches!(scope.kind, ScopeKind::Function) { + continue; + } + if is_claimed(*sid) { + continue; + } + let all_match = names.iter().all(|name| scope.bindings.contains_key(*name)); + if all_match { + return Some(*sid); + } + } + None + } +} diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 52091083765e8..bfdc72a0ee462 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -229,10 +229,12 @@ impl<'a> Transformer<'a> { return (scoping, Diagnostics::new()); }; let result = react_compiler_transform(program, self.allocator, options); - let Some(compiled) = result.program else { + if !result.changed { + // The program was left untouched; keep the existing scoping. return (scoping, result.diagnostics); - }; - *program = compiled; + } + // The compiler spliced the memoized functions into `program` in place; + // rebuild scoping for the mutated AST. let scoping = SemanticBuilder::new().with_enum_eval(true).build(program).semantic.into_scoping(); (scoping, result.diagnostics) diff --git a/napi/transform/Cargo.toml b/napi/transform/Cargo.toml index 780d274aa16f8..c975ce8af7a76 100644 --- a/napi/transform/Cargo.toml +++ b/napi/transform/Cargo.toml @@ -24,6 +24,7 @@ doctest = false [dependencies] oxc = { workspace = true, features = ["full"] } oxc_napi = { workspace = true } +oxc_react_compiler = { workspace = true } oxc_sourcemap = { workspace = true, features = ["napi"] } rustc-hash = { workspace = true } diff --git a/napi/transform/index.d.ts b/napi/transform/index.d.ts index de149d5c5986e..66b14e970366c 100644 --- a/napi/transform/index.d.ts +++ b/napi/transform/index.d.ts @@ -329,6 +329,47 @@ export interface PluginsOptions { taggedTemplateEscape?: boolean } +export interface ReactCompilerOptions { + sourcemap?: boolean + /** + * How the compiler decides which functions to compile. + * One of `"infer"` (default), `"syntax"`, `"annotation"`, `"all"`. + */ + compilationMode?: string + /** + * When the compiler should throw on an error rather than skip the function. + * One of `"none"` (default), `"critical_errors"`, `"all_errors"`. + */ + panicThreshold?: string +} + +export interface ReactCompilerResult { + /** + * The compiled code. + * + * When the compiler makes no changes (the file has no React component or + * hook, or compilation bails out), this is the **original source unchanged** + * — mirroring `babel-plugin-react-compiler`, which leaves untouched + * functions as-is. + */ + code: string + map?: SourceMap + /** + * `true` if the compiler memoized at least one function (i.e. `code` is the + * recompiled program rather than the original source). + */ + changed: boolean + errors: Array +} + +/** + * Run the React Compiler (oxc's native Rust port) on a single file. + * + * Parses `source_text`, applies the compiler, and returns the recompiled code. + * Intended to mirror `babel-plugin-react-compiler` for a single-file transform. + */ +export declare function reactCompilerSync(filename: string, sourceText: string, options?: ReactCompilerOptions | undefined | null): ReactCompilerResult + export interface ReactRefreshOptions { /** * Specify the identifier of the refresh registration variable. diff --git a/napi/transform/index.js b/napi/transform/index.js index 703aabf9140e9..6045ea85b031e 100644 --- a/napi/transform/index.js +++ b/napi/transform/index.js @@ -598,12 +598,13 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Severity, HelperMode, isolatedDeclaration, isolatedDeclarationSync, moduleRunnerTransform, moduleRunnerTransformSync, transform, transformSync } = nativeBinding +const { Severity, HelperMode, isolatedDeclaration, isolatedDeclarationSync, moduleRunnerTransform, moduleRunnerTransformSync, reactCompilerSync, transform, transformSync } = nativeBinding export { Severity } export { HelperMode } export { isolatedDeclaration } export { isolatedDeclarationSync } export { moduleRunnerTransform } export { moduleRunnerTransformSync } +export { reactCompilerSync } export { transform } export { transformSync } diff --git a/napi/transform/src/lib.rs b/napi/transform/src/lib.rs index 950724e42a558..e5b91e9ab16db 100644 --- a/napi/transform/src/lib.rs +++ b/napi/transform/src/lib.rs @@ -14,5 +14,8 @@ static ALLOC: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc; mod isolated_declaration; pub use isolated_declaration::*; +mod react_compiler; +pub use react_compiler::*; + mod transformer; pub use transformer::*; diff --git a/napi/transform/src/react_compiler.rs b/napi/transform/src/react_compiler.rs new file mode 100644 index 0000000000000..cdff6afe6b091 --- /dev/null +++ b/napi/transform/src/react_compiler.rs @@ -0,0 +1,101 @@ +use std::path::Path; + +use napi_derive::napi; + +use oxc::{ + allocator::Allocator, + codegen::{Codegen, CodegenOptions}, + parser::Parser, + span::SourceType, +}; +use oxc_napi::OxcError; +use oxc_sourcemap::napi::SourceMap; + +#[napi(object)] +pub struct ReactCompilerResult { + /// The compiled code. + /// + /// When the compiler makes no changes (the file has no React component or + /// hook, or compilation bails out), this is the **original source unchanged** + /// — mirroring `babel-plugin-react-compiler`, which leaves untouched + /// functions as-is. + pub code: String, + pub map: Option, + /// `true` if the compiler memoized at least one function (i.e. `code` is the + /// recompiled program rather than the original source). + pub changed: bool, + pub errors: Vec, +} + +#[napi(object)] +#[derive(Default)] +pub struct ReactCompilerOptions { + pub sourcemap: Option, + /// How the compiler decides which functions to compile. + /// One of `"infer"` (default), `"syntax"`, `"annotation"`, `"all"`. + pub compilation_mode: Option, + /// When the compiler should throw on an error rather than skip the function. + /// One of `"none"` (default), `"critical_errors"`, `"all_errors"`. + pub panic_threshold: Option, +} + +fn react_compiler_impl( + filename: &str, + source_text: &str, + options: Option, +) -> ReactCompilerResult { + let source_path = Path::new(filename); + let source_type = SourceType::from_path(source_path).unwrap_or_else(|_| SourceType::tsx()); + let allocator = Allocator::default(); + let options = options.unwrap_or_default(); + + let mut parsed = Parser::new(&allocator, source_text, source_type).parse(); + + let mut plugin_options = oxc_react_compiler::default_plugin_options(); + plugin_options.filename = Some(filename.to_string()); + if let Some(mode) = options.compilation_mode { + plugin_options.compilation_mode = mode; + } + if let Some(threshold) = options.panic_threshold { + plugin_options.panic_threshold = threshold; + } + + let result = oxc_react_compiler::transform(&mut parsed.program, &allocator, plugin_options); + + let parser_diagnostics = std::mem::take(&mut parsed.diagnostics); + let diagnostics = parser_diagnostics.into_iter().chain(result.diagnostics).collect::>(); + let errors = OxcError::from_diagnostics(filename, source_text, diagnostics); + + if result.changed { + // The compiler spliced the memoized functions into `parsed.program` in place. + let source_map_path = match options.sourcemap { + Some(true) => Some(source_path.to_path_buf()), + _ => None, + }; + let codegen_ret = Codegen::new() + .with_options(CodegenOptions { source_map_path, ..CodegenOptions::default() }) + .build(&parsed.program); + ReactCompilerResult { + code: codegen_ret.code, + map: codegen_ret.map.map(SourceMap::from), + changed: true, + errors, + } + } else { + ReactCompilerResult { code: source_text.to_string(), map: None, changed: false, errors } + } +} + +/// Run the React Compiler (oxc's native Rust port) on a single file. +/// +/// Parses `source_text`, applies the compiler, and returns the recompiled code. +/// Intended to mirror `babel-plugin-react-compiler` for a single-file transform. +#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)] +#[napi] +pub fn react_compiler_sync( + filename: String, + source_text: String, + options: Option, +) -> ReactCompilerResult { + react_compiler_impl(&filename, &source_text, options) +} diff --git a/napi/transform/transform.wasi-browser.js b/napi/transform/transform.wasi-browser.js index dfa60b786b1e3..38f3e91bd0940 100644 --- a/napi/transform/transform.wasi-browser.js +++ b/napi/transform/transform.wasi-browser.js @@ -63,5 +63,6 @@ export const isolatedDeclaration = __napiModule.exports.isolatedDeclaration export const isolatedDeclarationSync = __napiModule.exports.isolatedDeclarationSync export const moduleRunnerTransform = __napiModule.exports.moduleRunnerTransform export const moduleRunnerTransformSync = __napiModule.exports.moduleRunnerTransformSync +export const reactCompilerSync = __napiModule.exports.reactCompilerSync export const transform = __napiModule.exports.transform export const transformSync = __napiModule.exports.transformSync diff --git a/napi/transform/transform.wasi.cjs b/napi/transform/transform.wasi.cjs index 6cfc77d231712..0e3beaf4033f6 100644 --- a/napi/transform/transform.wasi.cjs +++ b/napi/transform/transform.wasi.cjs @@ -114,5 +114,6 @@ module.exports.isolatedDeclaration = __napiModule.exports.isolatedDeclaration module.exports.isolatedDeclarationSync = __napiModule.exports.isolatedDeclarationSync module.exports.moduleRunnerTransform = __napiModule.exports.moduleRunnerTransform module.exports.moduleRunnerTransformSync = __napiModule.exports.moduleRunnerTransformSync +module.exports.reactCompilerSync = __napiModule.exports.reactCompilerSync module.exports.transform = __napiModule.exports.transform module.exports.transformSync = __napiModule.exports.transformSync diff --git a/oxlintrc.json b/oxlintrc.json index 4b7944a32258f..f54900bf31be6 100644 --- a/oxlintrc.json +++ b/oxlintrc.json @@ -89,6 +89,7 @@ "**/generated/**", "npm/runtime/**", "tasks/coverage/**", + "tasks/react_compiler_compare/**", "crates/oxc_semantic/tests/**", "napi/minify/**", // TODO "apps/oxlint/src-js/package/config.generated.ts", diff --git a/tasks/benchmark/benches/react_compiler.rs b/tasks/benchmark/benches/react_compiler.rs index 086a5f328bbe2..a5b203bd4c2fb 100644 --- a/tasks/benchmark/benches/react_compiler.rs +++ b/tasks/benchmark/benches/react_compiler.rs @@ -25,10 +25,10 @@ fn bench_react_compiler(criterion: &mut Criterion) { // Create a fresh AST for each iteration. The compiler builds // semantic data and allocates the compiled program in the same arena. - let ParserReturn { program, .. } = + let ParserReturn { mut program, .. } = Parser::new(&allocator, source_text, source_type).parse(); - runner.run(|| transform(&program, &allocator, options.clone())); + runner.run(|| transform(&mut program, &allocator, options.clone())); }); }); } diff --git a/tasks/react_compiler_compare/.gitignore b/tasks/react_compiler_compare/.gitignore new file mode 100644 index 0000000000000..9461b72963a03 --- /dev/null +++ b/tasks/react_compiler_compare/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +report*.md +package-lock.json diff --git a/tasks/react_compiler_compare/README.md b/tasks/react_compiler_compare/README.md new file mode 100644 index 0000000000000..d2a8938dd34da --- /dev/null +++ b/tasks/react_compiler_compare/README.md @@ -0,0 +1,96 @@ +# react_compiler_compare + +Compares [`babel-plugin-react-compiler`](https://www.npmjs.com/package/babel-plugin-react-compiler) +against oxc's native React Compiler (exposed via `oxc-transform`'s `reactCompilerSync`) +over real-world source files. + +## Setup + +```bash +# 1. Build the napi binding that exposes the compiler to JS +cd napi/transform && pnpm build && cd - + +# 2. Install the babel comparison deps (isolated; not part of the pnpm workspace) +cd tasks/react_compiler_compare && npm install +``` + +> [!IMPORTANT] +> `oxc_react_compiler` is a Rust port of the **oxc-project fork** of react-compiler +> (`~/github/oxc-project/oxc-react-compiler/react-compiler`), **not** of any published +> npm release. The fork has diverged from npm by ~a year (e.g. it alphabetically +> sorts reactive-scope dependencies via `compareScopeDependency`; older npm builds +> don't). So comparing against **npm** mostly measures _version drift_, while comparing +> against the **fork** measures true _port fidelity_. Use `BPRC` to pick the reference. + +### Comparing against built source (recommended — true port fidelity) + +Build either the oxc fork **or** upstream `react/react` main and point `BPRC` at the +built plugin. As of 2026-06-18 the oxc fork is a same-day mirror of upstream main, so +both give the same result (~86%): + +```bash +# upstream react/react main (sparse-clone just the compiler) +git clone --depth 1 --filter=blob:none --sparse https://github.com/react/react.git /tmp/react-upstream +cd /tmp/react-upstream && git sparse-checkout set compiler +cd compiler && yarn install && yarn workspace babel-plugin-react-compiler build && cd - +BPRC=/tmp/react-upstream/compiler/packages/babel-plugin-react-compiler/dist/index.js \ + REPOS=/path/to/oxc-ecosystem-ci/repos node compare.mjs + +# ...or the oxc fork it directly ports +# cd ~/github/oxc-project/oxc-react-compiler/react-compiler +# yarn install && yarn workspace babel-plugin-react-compiler build +# BPRC=.../packages/babel-plugin-react-compiler/dist/index.js node compare.mjs +``` + +Without `BPRC`, the pinned npm `babel-plugin-react-compiler` in `package.json` is used. + +## Run + +```bash +# default: stride-sample 5000 .jsx/.tsx files across all repos +REPOS=/path/to/oxc-ecosystem-ci/repos node compare.mjs + +# options +node compare.mjs --cap=2000 # sample size (default 5000) +node compare.mjs --all # every candidate file (slow) +node compare.mjs --repos=kibana,next # only these repos +node compare.mjs --report=out.md # report path +``` + +Env: `REPOS` (scan dir, default `../../../oxc-ecosystem-ci/repos`), +`OXC_TRANSFORM` (path to the built `oxc-transform` entry, default +`../../napi/transform/index.js`). + +## Methodology + +Both compilers run on each file; **both outputs are then normalized** through +`@babel/parser` + `@babel/generator` (formatting metadata stripped) and +string-compared, so only _semantic_ differences in the memoization count — not +quote style / spacing / codegen formatting. A file "counts" toward the rate only +when react-compiler actually memoized something (otherwise it's a trivial no-op +match). Mismatches are bucketed by dominant cause. + +## Findings + +**0 oxc crashes** across thousands of real-world files (robustness). + +| reference | identical-output on memoized files | +| -------------------------------------------------------- | ------------------------------------- | +| npm `babel-plugin-react-compiler@334f00b` (a year stale) | ~31% — dominated by **version drift** | +| upstream `react/react` main (built, via `BPRC`) | **85.6%** (1747/2040 over 3000 files) | +| the oxc fork it ports (same-day mirror of upstream) | 85.6% — identical to upstream main | + +Against the fork, the remaining ~14% are genuine port gaps: + +- **mutation / immutability inference false-positives** (most serious): oxc rejects + some legal mutations the fork accepts — e.g. `ref.current = x` on a `useRef` value + raises `[ReactCompiler] Immutability: This value cannot be modified`, which errors + the whole component and emits an **empty body**. Fixing the inference to treat ref + `.current` (and other allowed mutations) as mutable closes these. +- **memoization decisions**: oxc declines to compile a few components the fork does. +- **cache-slot counts**: `_c(33)` vs `_c(31)` — different memoization granularity. +- **outlining**: minor differences in which inline callbacks get outlined. + +The `~31%` vs npm is almost entirely the fork's alphabetical dependency sort +(`compareScopeDependency`) that the stale npm build lacks — i.e. oxc is _correct_ +relative to what it ports; the npm number is not a fair fidelity measure. diff --git a/tasks/react_compiler_compare/compare.mjs b/tasks/react_compiler_compare/compare.mjs new file mode 100644 index 0000000000000..66107d791f860 --- /dev/null +++ b/tasks/react_compiler_compare/compare.mjs @@ -0,0 +1,310 @@ +// Compare `babel-plugin-react-compiler` (npm) against oxc's native React +// Compiler exposed via `oxc-transform`'s `reactCompilerSync`, over real-world +// files (e.g. the clones in oxc-ecosystem-ci/repos). +// +// Methodology: run both compilers on each file, then normalize BOTH outputs +// through @babel/parser + @babel/generator (stripping formatting metadata) and +// string-compare, so only *semantic* differences in the memoization count. +// +// Setup: +// cd tasks/react_compiler_compare && npm install +// (build the napi binding first: cd napi/transform && pnpm build) +// +// Usage: +// REPOS=/path/to/oxc-ecosystem-ci/repos node compare.mjs [--cap=N] [--all] \ +// [--repos=kibana,next] [--report=path] +// +// Env: +// REPOS dir to scan for *.jsx/*.tsx (default: ../../../oxc-ecosystem-ci/repos) +// OXC_TRANSFORM path to the built oxc-transform entry (default: ../../napi/transform/index.js) + +import { readFileSync, writeFileSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import * as babel from "@babel/core"; +import { parse } from "@babel/parser"; +import _generate from "@babel/generator"; + +const generate = _generate.default || _generate; +const ReactCompilerMod = await import(process.env.BPRC || "babel-plugin-react-compiler"); +const ReactCompiler = ReactCompilerMod.default || ReactCompilerMod; +const bprcVersion = (() => { + try { + return JSON.parse( + readFileSync( + new URL("./node_modules/babel-plugin-react-compiler/package.json", import.meta.url), + ), + ).version; + } catch { + return "unknown"; + } +})(); + +const here = fileURLToPath(new URL(".", import.meta.url)); +const oxcTransformPath = + process.env.OXC_TRANSFORM || new URL("../../napi/transform/index.js", import.meta.url).href; +const { reactCompilerSync } = await import(oxcTransformPath); + +const REPOS_DIR = + process.env.REPOS || fileURLToPath(new URL("../../../oxc-ecosystem-ci/repos", import.meta.url)); + +const args = Object.fromEntries( + process.argv.slice(2).map((a) => { + const m = a.match(/^--([^=]+)(?:=(.*))?$/); + return m ? [m[1], m[2] ?? true] : [a, true]; + }), +); +const CAP = args.all ? Infinity : Number(args.cap ?? 5000); +const REPORT = args.report ?? `${here}report.md`; +const reposFilter = args.repos ? new Set(String(args.repos).split(",")) : null; + +function parserPlugins(file) { + const isTs = file.endsWith(".tsx") || file.endsWith(".ts"); + const isJsx = file.endsWith(".tsx") || file.endsWith(".jsx") || file.endsWith(".js"); + const isFlow = file.endsWith(".flow.js") || file.endsWith(".flow"); + const p = []; + if (isTs) p.push("typescript"); + if (isFlow) p.push("flow"); + if (isJsx) p.push("jsx"); + return p; +} + +function babelCompile(src, filename, plugins) { + const res = babel.transformSync(src, { + filename, + babelrc: false, + configFile: false, + browserslistConfigFile: false, + sourceType: "module", + parserOpts: { plugins, sourceType: "module", allowReturnOutsideFunction: true }, + plugins: [[ReactCompiler, { target: "19" }]], + cloneInputAst: false, + }); + return res.code; +} + +// Strip per-node formatting metadata so @babel/generator emits a canonical form +// (default quotes, no original raw tokens, no comments/locations). Without this, +// generator preserves each parse's `extra.raw`, so 'x' vs "x" would falsely diff. +function stripFormatting(node) { + if (!node || typeof node !== "object") return; + if (Array.isArray(node)) { + for (const c of node) stripFormatting(c); + return; + } + delete node.extra; + delete node.start; + delete node.end; + delete node.loc; + delete node.range; + delete node.leadingComments; + delete node.trailingComments; + delete node.innerComments; + for (const k in node) { + const v = node[k]; + if (v && typeof v === "object") stripFormatting(v); + } +} + +function canon(code, plugins) { + const ast = parse(code, { sourceType: "module", plugins, errorRecovery: false }); + stripFormatting(ast); + return generate(ast, { compact: false, concise: false, retainLines: false, comments: false }) + .code; +} + +const memoized = (code) => code.includes("react/compiler-runtime") || /\b_c\(\d/.test(code); +const cacheCount = (code) => { + const m = code.match(/_c\((\d+)\)/); + return m ? +m[1] : null; +}; + +// Bucket a mismatch by its dominant cause (heuristic, on canonicalized outputs). +function classify(cb, co, bMemo, oMemo) { + if (bMemo !== oMemo) return "memo-decision"; + const bc = cacheCount(cb), + oc = cacheCount(co); + if (bc !== null && oc !== null && bc !== oc) return "cache-count"; + const baseTemps = (cb.match(/_temp\d*/g) || []).length, + oxcTemps = (co.match(/_temp\d*/g) || []).length; + if (baseTemps !== oxcTemps) return "outlining"; + const norm = (s) => + s + .split("\n") + .map((l) => l.trim()) + .filter(Boolean) + .sort() + .join("\n"); + if (norm(cb) === norm(co)) return "line-order"; + return "other"; +} + +function shortDiff(cb, co) { + const baseLines = cb.split("\n"), + oxcLines = co.split("\n"); + const out = []; + const max = Math.max(baseLines.length, oxcLines.length); + for (let i = 0; i < max && out.length < 16; i++) { + if (baseLines[i] !== oxcLines[i]) { + if (baseLines[i] !== undefined) out.push(`- ${baseLines[i].trim().slice(0, 120)}`); + if (oxcLines[i] !== undefined) out.push(`+ ${oxcLines[i].trim().slice(0, 120)}`); + } + } + return out.join("\n"); +} + +// ---- collect candidate files ---- +const findCmd = + `find ${REPOS_DIR} -type f \\( -name '*.jsx' -o -name '*.tsx' \\) ` + + `-not -path '*/node_modules/*' -not -path '*/dist/*' -not -path '*/build/*' ` + + `-not -path '*/.next/*' -not -name '*.min.js'`; +let files = execSync(findCmd, { maxBuffer: 1 << 30 }) + .toString() + .trim() + .split("\n") + .filter(Boolean); +if (reposFilter) + files = files.filter((f) => reposFilter.has(f.slice(REPOS_DIR.length + 1).split("/")[0])); +files.sort(); +const total = files.length; +if (files.length > CAP) { + const stride = files.length / CAP; + const sampled = []; + for (let i = 0; i < CAP; i++) sampled.push(files[Math.floor(i * stride)]); + files = sampled; +} +console.log( + `candidates=${total} sampled=${files.length} cap=${CAP === Infinity ? "all" : CAP} bprc=${bprcVersion}`, +); + +const stats = { + processed: 0, + babelError: 0, + oxcError: 0, + bothNoop: 0, + memoMatch: 0, + memoMismatch: 0, + noopMismatch: 0, + canonError: 0, +}; +const buckets = {}; +const bucketSamples = {}; +const byRepo = {}; + +let n = 0; +for (const file of files) { + n++; + if (n % 500 === 0) + console.log( + ` ${n}/${files.length} memoMatch=${stats.memoMatch} memoMismatch=${stats.memoMismatch} babelErr=${stats.babelError}`, + ); + const repo = file.slice(REPOS_DIR.length + 1).split("/")[0]; + let src; + try { + src = readFileSync(file, "utf8"); + } catch { + continue; + } + if (src.length > 400_000) continue; + const plugins = parserPlugins(file); + + let babelCode; + try { + babelCode = babelCompile(src, file, plugins); + } catch { + stats.babelError++; + continue; + } + + let oxc; + try { + oxc = reactCompilerSync(file, src); + } catch { + stats.oxcError++; + continue; + } + + stats.processed++; + const bMemo = memoized(babelCode); + const oMemo = oxc.changed || memoized(oxc.code); + + let cb, co; + try { + cb = canon(babelCode, plugins); + co = canon(oxc.code, plugins); + } catch { + stats.canonError++; + continue; + } + + const equal = cb === co; + byRepo[repo] ??= { match: 0, mismatch: 0 }; + if (equal) { + byRepo[repo].match++; + if (bMemo) stats.memoMatch++; + else stats.bothNoop++; + } else { + byRepo[repo].mismatch++; + if (bMemo || oMemo) { + stats.memoMismatch++; + const cause = classify(cb, co, bMemo, oMemo); + buckets[cause] = (buckets[cause] ?? 0) + 1; + bucketSamples[cause] ??= []; + if (bucketSamples[cause].length < 4) + bucketSamples[cause].push({ + file: file.slice(REPOS_DIR.length + 1), + diff: shortDiff(cb, co), + }); + } else { + stats.noopMismatch++; + } + } +} + +const interesting = stats.memoMatch + stats.memoMismatch; +const pct = interesting ? ((stats.memoMatch / interesting) * 100).toFixed(2) : "n/a"; +const lines = []; +lines.push( + `# react-compiler: oxc \`reactCompilerSync\` vs babel-plugin-react-compiler@${bprcVersion}`, +); +lines.push(""); +lines.push(`candidates(.jsx/.tsx)=${total} sampled=${files.length}`); +lines.push(""); +lines.push("## Results"); +lines.push(`- processed (both compiled): **${stats.processed}**`); +lines.push(`- babel parse/transform errors (out of scope): ${stats.babelError}`); +lines.push(`- oxc threw: ${stats.oxcError}`); +lines.push(`- canonicalization errors: ${stats.canonError}`); +lines.push(""); +lines.push("### On files where react-compiler memoized something (the real comparison)"); +lines.push(`- **match: ${stats.memoMatch}**`); +lines.push(`- **mismatch: ${stats.memoMismatch}**`); +lines.push(`- => identical-output rate on memoized files: **${pct}%**`); +lines.push(""); +lines.push("### Non-memoized files"); +lines.push(`- both no-op, canon-equal: ${stats.bothNoop}`); +lines.push(`- canon differs without memo: ${stats.noopMismatch}`); +lines.push(""); +lines.push("## Mismatch causes (on memoized files; - babel / + oxc)"); +for (const [cause, count] of Object.entries(buckets).sort((a, b) => b[1] - a[1])) + lines.push(`- **${cause}**: ${count}`); +lines.push(""); +lines.push("## Per-repo match/mismatch"); +for (const [repo, r] of Object.entries(byRepo).sort((a, b) => b[1].mismatch - a[1].mismatch)) { + if (r.mismatch) lines.push(`- ${repo}: ${r.match} match / ${r.mismatch} mismatch`); +} +lines.push(""); +lines.push("## Samples by cause (- babel / + oxc, canonicalized)"); +for (const [cause, samples] of Object.entries(bucketSamples)) { + lines.push(`\n### cause: ${cause}`); + for (const s of samples) { + lines.push(`\n**${s.file}**`); + lines.push("```diff"); + lines.push(s.diff || "(diff beyond first 16 changed lines)"); + lines.push("```"); + } +} +const report = lines.join("\n"); +writeFileSync(REPORT, report); +console.log("\n" + report.split("## Samples by cause")[0]); +console.log(`\nfull report -> ${REPORT}`); diff --git a/tasks/react_compiler_compare/package.json b/tasks/react_compiler_compare/package.json new file mode 100644 index 0000000000000..33be8d2e38dd9 --- /dev/null +++ b/tasks/react_compiler_compare/package.json @@ -0,0 +1,16 @@ +{ + "name": "react-compiler-compare", + "version": "0.0.0", + "private": true, + "description": "Compare babel-plugin-react-compiler vs oxc reactCompilerSync over real-world files", + "type": "module", + "scripts": { + "compare": "node compare.mjs" + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/parser": "^7.26.0", + "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725" + } +}