diff --git a/src/js_parser/react_compiler_host.rs b/src/js_parser/react_compiler_host.rs index 425bafcd61c8..1d8c69638a6a 100644 --- a/src/js_parser/react_compiler_host.rs +++ b/src/js_parser/react_compiler_host.rs @@ -177,6 +177,18 @@ impl<'a, const TS: bool, const SCAN_ONLY: bool> bun_react_compiler::Host p.find_symbol(bun_ast::Loc::EMPTY, name).expect("oom").r#ref } + fn is_unbound_global(&mut self, name: &'static [u8]) -> bool { + let p = &mut *self.p; + let Ok(found) = p.find_symbol_with_record_usage::(bun_ast::Loc::EMPTY, name) else { + return false; + }; + // No symbol has the name yet, or only the one of a global. + !found.is_inside_with_scope + && (found.r#ref.is_empty() + || p.symbols[found.r#ref.inner_index() as usize].kind + == js_ast::symbol::Kind::Unbound) + } + fn record_usage(&mut self, ref_: js_ast::Ref) { self.p.record_usage(ref_); } diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index cafb4f0281bf..516f476d7546 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -85,7 +85,8 @@ bun_core::declare_scope!(cache, visible); /// Version 50: The body of a class with lowered class decorators names the class by the /// binding that holds what the decorators return. /// Version 51: A `bun:` specifier that names no builtin is printed as it is written. -const EXPECTED_VERSION: u32 = 51; +/// Version 52: `--react-compiler` reads the method of a member call ahead of a statement that can change it. +const EXPECTED_VERSION: u32 = 52; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/src/react_compiler/codegen.rs b/src/react_compiler/codegen.rs index 29a8a3ac03cb..79094735d683 100644 --- a/src/react_compiler/codegen.rs +++ b/src/react_compiler/codegen.rs @@ -96,10 +96,11 @@ enum WellKnown { Underscore, MemoCacheSentinel, EarlyReturnSentinel, + Function, } impl WellKnown { - const COUNT: usize = 6; + const COUNT: usize = 7; } /// Host-side state shared across nested function-expression codegen so the @@ -228,6 +229,19 @@ pub(crate) fn codegen_function( context: &mut ProgramContext, unique_identifiers: HashSet, ) -> Result { + // Not in upstream: asked first, so that a function that stays as written has no use of `Function` on record. + if !env.explicit_receiver_calls.is_empty() { + let binds_function = env.identifiers.iter().any(|identifier| { + (identifier.name.as_ref()).is_some_and(|name| name.value() == b"Function") + }); + if binds_function || !cg.host.is_unbound_global(b"Function") { + return Err(crate::diagnostics::cold_todo( + "A method call that reads its method ahead of its arguments needs the global `Function`", + None, + )); + } + } + let mut cx = Context::new(env, cg, unique_identifiers); // Fast Refresh: Bun handles HMR via its own React Refresh transform; the @@ -1940,14 +1954,19 @@ fn codegen_base_instruction_value( Ok(maybe_wrap_hook_call(cx, call_expr, callee.identifier)) } InstructionValue::MethodCall { - receiver: _, + receiver, property, args, .. } => { let member_expr = codegen_place_to_expression(cx, property)?; if !matches!(member_expr.data, ExprData::EDot(_) | ExprData::EIndex(_)) { - return Err(method_call_property_err(property.loc, member_expr)); + if !cx.env.explicit_receiver_calls.contains(property.identifier) { + return Err(method_call_property_err(property.loc, member_expr)); + } + let call_expr = + codegen_explicit_receiver_call(cx, member_expr, receiver, args, loc)?; + return Ok(maybe_wrap_hook_call(cx, call_expr, property.identifier)); } let arguments = codegen_arguments(cx, args)?; let call_expr = Expr::init( @@ -2318,6 +2337,51 @@ fn codegen_base_instruction_value( // Function expression codegen // ============================================================================= +/// Not in upstream: `Function.prototype.call.call(method, receiver, ...args)`, which reads nothing of the method. +fn codegen_explicit_receiver_call( + cx: &mut Context, + method: Expr, + receiver: &Place, + args: &[PlaceOrSpread], + loc: Loc, +) -> Result { + // The load of the method printed the receiver once, so only a name can print it again. + if cx.env.identifiers[receiver.identifier.0 as usize] + .name + .is_none() + { + return Err(method_call_property_err(receiver.loc, method)); + } + // `codegen_function` checked that the name is the global. + let function = cx.cg.well_known_global(WellKnown::Function, b"Function"); + let mut call_args: ExprNodeList = AstAlloc::vec_with_capacity(args.len() + 2); + call_args.push(method); + call_args.push(codegen_place_to_expression(cx, receiver)?); + for arg in args { + call_args.push(codegen_argument(cx, arg)?); + } + let mut target = Expr::init_identifier(function, loc); + for name in [b"prototype".as_slice(), b"call", b"call"] { + target = Expr::init( + E::Dot { + target, + name: StoreStr::new(name), + name_loc: loc, + ..Default::default() + }, + loc, + ); + } + Ok(Expr::init( + E::Call { + target, + args: call_args, + ..Default::default() + }, + loc, + )) +} + fn codegen_function_expression( cx: &mut Context, name: &Option, diff --git a/src/react_compiler/hir/environment.rs b/src/react_compiler/hir/environment.rs index 32cbfd324bb6..f17cae1a50d6 100644 --- a/src/react_compiler/hir/environment.rs +++ b/src/react_compiler/hir/environment.rs @@ -1,5 +1,6 @@ use crate::collections::FxHashMap as HashMap; use crate::collections::FxHashSet as HashSet; +use crate::collections::IdBitSet; use crate::diagnostics::CompilerDiagnostic; use crate::diagnostics::CompilerError; @@ -121,6 +122,9 @@ pub struct Environment { // identifiers on first use, then updated with each generated name. // Matches Babel's generateUid behavior of checking hasBinding/hasReference. uid_known_names: Option>, + + /// Not in upstream: the MethodCall properties that phase 3 named, not those of facebook/react#34072. + pub(crate) explicit_receiver_calls: IdBitSet, } /// An outlined function entry, stored on Environment during compilation. @@ -218,6 +222,7 @@ impl Environment { default_mutating_hook: None, outlined_functions: AstAlloc::vec(), uid_known_names: None, + explicit_receiver_calls: IdBitSet::new(), config, } } diff --git a/src/react_compiler/observer.rs b/src/react_compiler/observer.rs index bfda911b43c3..1d11c4671307 100644 --- a/src/react_compiler/observer.rs +++ b/src/react_compiler/observer.rs @@ -300,6 +300,9 @@ impl Host for RecordingHost<'_> { self.referenced.push(reference); reference } + fn is_unbound_global(&mut self, name: &'static [u8]) -> bool { + self.host.is_unbound_global(name) + } fn record_usage(&mut self, ref_: Ref) { self.referenced.push(ref_); self.host.record_usage(ref_); diff --git a/src/react_compiler/program.rs b/src/react_compiler/program.rs index 1c848652085d..50731b0a68c3 100644 --- a/src/react_compiler/program.rs +++ b/src/react_compiler/program.rs @@ -149,6 +149,11 @@ pub trait Host { self.new_generated(name) } + /// Whether no scope from the body of the function outward binds `name`. The lookup records no use. + fn is_unbound_global(&mut self, _name: &'static [u8]) -> bool { + false + } + fn record_usage(&mut self, ref_: Ref); fn add_import_record(&mut self, path: &[u8], kind: ImportKind) -> (u32, Ref); } diff --git a/src/react_compiler/reactive_scopes/promote_used_temporaries.rs b/src/react_compiler/reactive_scopes/promote_used_temporaries.rs index 23046cb01b83..433d8c4056f9 100644 --- a/src/react_compiler/reactive_scopes/promote_used_temporaries.rs +++ b/src/react_compiler/reactive_scopes/promote_used_temporaries.rs @@ -12,8 +12,10 @@ use crate::collections::FxHashSet as HashSet; use crate::collections::IdBitSet; use crate::collections::IdMap; +use crate::hir::AliasingEffect; use crate::hir::ArrayElement; use crate::hir::DeclarationId; +use crate::hir::EvaluationOrder; use crate::hir::FunctionId; use crate::hir::IdentifierId; use crate::hir::IdentifierName; @@ -21,6 +23,7 @@ use crate::hir::InstructionKind; use crate::hir::InstructionValue; use crate::hir::JsxAttribute; use crate::hir::JsxTag; +use crate::hir::MutableRangeId; use crate::hir::ObjectPropertyOrSpread; use crate::hir::ParamPattern; use crate::hir::Place; @@ -35,6 +38,7 @@ use crate::hir::ScopeId; use crate::hir::environment::Environment; use crate::hir::is_primitive_type; use crate::hir::visitors::Places; +use crate::reactive_scopes::visitors::ReactiveFunctionVisitor; // ============================================================================= // State @@ -103,6 +107,7 @@ pub(crate) fn promote_used_temporaries( let mut inter_state = InterState::new(inline_macro_operands); promote_interposed_block( &func.body, + None, &mut state, &mut inter_state, &mut consts, @@ -586,6 +591,8 @@ fn visit_hir_function_for_promotion(func_id: FunctionId, state: &mut State, env: enum Effect { None, Read, + /// A read that runs an iterator. Only `promote_late_method` tells it from `Read`. + Iterate, Write, } @@ -595,10 +602,11 @@ struct Temporary { effect: Effect, } -/// Positions of the last statement that reads and of the last statement that writes. +/// Positions of the last statement that reads, that runs an iterator, and that writes. #[derive(Clone, Copy, Default)] struct Statements { read: u32, + iterate: u32, write: u32, } @@ -647,6 +655,10 @@ impl<'a> InterState<'a> { match effect { Effect::None => {} Effect::Read => statements.read = self.position, + Effect::Iterate => { + statements.read = self.position; + statements.iterate = self.position; + } Effect::Write => statements.write = self.position, } } @@ -656,6 +668,10 @@ impl<'a> InterState<'a> { match temporary.effect { Effect::None => {} Effect::Read => statements.read = statements.read.max(temporary.position), + Effect::Iterate => { + statements.read = statements.read.max(temporary.position); + statements.iterate = statements.iterate.max(temporary.position); + } Effect::Write => statements.write = statements.write.max(temporary.position), } } @@ -666,11 +682,16 @@ impl<'a> InterState<'a> { let write = self.statements.write.max(in_value_block.write); match temporary.effect { Effect::None => false, - Effect::Read => write > temporary.position, + Effect::Read | Effect::Iterate => write > temporary.position, Effect::Write => read.max(write) > temporary.position, } } + /// A statement of the block behind the temporary runs an iterator. `promote_late_method` is for a block only. + fn iterates_behind(&self, temporary: Temporary) -> bool { + temporary.effect != Effect::None && self.statements.iterate > temporary.position + } + fn enter_value_block(&mut self) -> Option { let outer = self.value_block_statements; self.value_block_statements = Some(outer.unwrap_or_default()); @@ -682,8 +703,18 @@ impl<'a> InterState<'a> { } } +/// What prints in front of an instruction: the statements of its block, then in `outer` those around that block. +#[derive(Clone, Copy)] +struct Before<'a> { + /// The terminal behind `statements` that holds the inner block, unless it is a label. All of it counts, the call too. + terminal: Option<&'a ReactiveTerminalStatement>, + statements: &'a [ReactiveStatement], + outer: Option<&'a Before<'a>>, +} + fn promote_interposed_block( block: &ReactiveBlock, + outer: Option<&Before<'_>>, state: &mut State, inter_state: &mut InterState<'_>, consts: &mut IdBitSet, @@ -694,16 +725,30 @@ fn promote_interposed_block( return; } - for stmt in block { + for (index, stmt) in block.iter().enumerate() { + let before = Before { + terminal: None, + statements: &block[..index], + outer, + }; match stmt { ReactiveStatement::Instruction(instr) => { - promote_interposed_instruction(instr, state, inter_state, consts, globals, env); + promote_interposed_instruction( + instr, + &before, + state, + inter_state, + consts, + globals, + env, + ); } ReactiveStatement::Scope(scope) => { // The memo block compares its dependencies first, and what writes must not print in it: it may not run. inter_state.statement(Effect::Read); promote_interposed_block( &scope.instructions, + Some(&before), state, inter_state, consts, @@ -714,6 +759,7 @@ fn promote_interposed_block( ReactiveStatement::PrunedScope(scope) => { promote_interposed_block( &scope.instructions, + Some(&before), state, inter_state, consts, @@ -722,7 +768,15 @@ fn promote_interposed_block( ); } ReactiveStatement::Terminal(terminal) => { - promote_interposed_terminal(terminal, state, inter_state, consts, globals, env); + promote_interposed_terminal( + terminal, + &before, + state, + inter_state, + consts, + globals, + env, + ); } } } @@ -882,7 +936,7 @@ fn instruction_effect( .iter() .any(|element| matches!(element, ArrayElement::Spread(_))) => { - Effect::Read + Effect::Iterate } InstructionValue::ObjectExpression { properties, .. } if properties @@ -903,8 +957,10 @@ fn instruction_effect( } /// Returns the effect of the expression that the instruction prints as. +/// `before` holds the statements in front of the instruction. A value block has none. fn promote_interposed_instruction( instr: &ReactiveInstruction, + before: &Before<'_>, state: &mut State, inter_state: &mut InterState<'_>, consts: &mut IdBitSet, @@ -956,7 +1012,25 @@ fn promote_interposed_instruction( } _ => {} } - promote_interposed_value(&instr.value, state, inter_state, consts, globals, env) + let effect = + promote_interposed_value(&instr.value, state, inter_state, consts, globals, env); + // The operands are decided: an argument that got a name is a statement behind the method now. + if let InstructionValue::MethodCall { property, .. } = iv + && let Some(temporary) = inline_temporary(property, state, inter_state, env) + && (inter_state.conflicts(temporary) || inter_state.iterates_behind(temporary)) + { + promote_late_method( + iv, + before, + temporary, + state, + inter_state, + consts, + globals, + env, + ); + } + effect } _ => { let outer = inter_state.enter_value_block(); @@ -1020,6 +1094,11 @@ fn promote_interposed_value( for instr in instructions { effect = effect.max(promote_interposed_instruction( instr, + &Before { + terminal: None, + statements: &[], + outer: None, + }, state, inter_state, consts, @@ -1060,6 +1139,245 @@ fn promote_interposed_value( } } +/// Not in upstream: names a method that a statement behind its load can change, or else the key of a computed one. +#[cold] +#[allow(clippy::too_many_arguments)] +fn promote_late_method( + call: &InstructionValue, + before: &Before<'_>, + temporary: Temporary, + state: &mut State, + inter_state: &mut InterState<'_>, + consts: &IdBitSet, + globals: &IdBitSet, + env: &mut Environment, +) { + let InstructionValue::MethodCall { + receiver, property, .. + } = call + else { + return; + }; + let declaration_of = + |place: &Place| env.identifiers[place.identifier.0 as usize].declaration_id; + let mut behind = Behind { + property: declaration_of(property), + state, + inter_state, + mutations: Mutations { + env, + consts, + globals, + receiver: env.enable_memoization().then(|| { + env.identifiers[receiver.identifier.0 as usize] + .mutable_range + .id + }), + }, + in_call: crate::hir::visitors::operands(call, &env.functions) + .map(declaration_of) + .collect(), + is_mutated: false, + load: None, + }; + let mut before = Some(before); + while let Some(&Before { + terminal, + statements, + outer, + }) = before + { + if let Some(terminal) = terminal { + behind + .mutations + .visit_terminal(terminal, &mut behind.is_mutated); + } + if behind.walk(statements) { + break; + } + before = outer; + } + let Behind { + is_mutated, load, .. + } = behind; + let Some(load) = load else { + // A call in a value block prints in place, and the load of its method with it. + debug_assert!(inter_state.value_block_statements.is_some() || !env.has_stack()); + return; + }; + if is_mutated && can_method_change(receiver, property, env) { + promote_identifier(property.identifier, state, env); + inter_state.promoted(temporary); + env.explicit_receiver_calls.insert(property.identifier); + // The call prints the receiver again, so the receiver needs a name too. + if let Some(temporary) = inline_temporary(receiver, state, inter_state, env) { + promote_identifier(receiver.identifier, state, env); + inter_state.promoted(temporary); + } + } else if let ReactiveValue::Instruction(load @ InstructionValue::ComputedLoad { .. }) = + &load.value + { + // The method stays in the call, and the key with it. The key is an operand like any other. + let operands = crate::hir::visitors::each_instruction_value_operand(load, env); + promote_interposed_operands(operands, state, inter_state, consts, env); + } +} + +/// False for a receiver that nothing mutates once it is made, and for a property that has the signature of a method. +fn can_method_change(receiver: &Place, property: &Place, env: &Environment) -> bool { + let range = &env.identifiers[receiver.identifier.0 as usize].mutable_range; + let type_ = &env.types[env.identifiers[property.identifier.0 as usize].type_.0 as usize]; + range.end.0 > range.start.0 + 1 + && !(env.get_function_signature(type_).ok().flatten()) + // A hook has its signature from its name alone. + .is_some_and(|signature| signature.hook_kind.is_none()) +} + +/// What prints between the load of a method and its call, read from the call back to the load. +struct Behind<'a, 'b> { + property: DeclarationId, + state: &'b State, + inter_state: &'b InterState<'b>, + mutations: Mutations<'b>, + /// The temporaries that print inside the call. They run behind the read of the method in the source too. + in_call: HashSet, + is_mutated: bool, + load: Option<&'a ReactiveInstruction>, +} + +impl<'a> Behind<'a, '_> { + /// Returns true at the load of the method. + fn walk(&mut self, statements: &'a [ReactiveStatement]) -> bool { + let env = self.mutations.env; + if !env.has_stack() { + return true; + } + for statement in statements.iter().rev() { + match statement { + ReactiveStatement::Instruction(instruction) => { + let lvalue = instruction.lvalue.as_ref(); + let declaration_id = lvalue + .map(|lvalue| env.identifiers[lvalue.identifier.0 as usize].declaration_id); + if declaration_id == Some(self.property) { + self.load = Some(instruction); + return true; + } + if let Some(lvalue) = lvalue + && declaration_id.is_some_and(|id| self.in_call.contains(&id)) + && inline_temporary(lvalue, self.state, self.inter_state, env).is_some() + { + Operands(env).visit_value( + instruction.id, + &instruction.value, + &mut self.in_call, + ); + } else { + self.mutations + .visit_instruction(instruction, &mut self.is_mutated); + } + } + // A pruned scope prints its statements in the block around it. + ReactiveStatement::PrunedScope(scope) => { + if self.walk(&scope.instructions) { + return true; + } + } + ReactiveStatement::Scope(scope) => { + self.mutations.visit_scope(scope, &mut self.is_mutated); + } + ReactiveStatement::Terminal(terminal) => { + self.mutations + .visit_terminal(terminal, &mut self.is_mutated); + } + } + } + false + } +} + +/// The operands of a value, by declaration. +struct Operands<'a>(&'a Environment); + +impl ReactiveFunctionVisitor for Operands<'_> { + type State = HashSet; + + fn env(&self) -> &Environment { + self.0 + } + + fn visit_place(&self, _id: EvaluationOrder, place: &Place, operands: &mut Self::State) { + operands.insert(self.0.identifiers[place.identifier.0 as usize].declaration_id); + } +} + +/// Sets its state when a statement can mutate the receiver of a method call. +struct Mutations<'a> { + env: &'a Environment, + consts: &'a IdBitSet, + globals: &'a IdBitSet, + /// The mutable range that the receiver shares. `None` without memoization, where every mutation counts. + receiver: Option, +} + +impl Mutations<'_> { + fn reaches(&self, place: &Place) -> bool { + self.receiver.is_none_or(|receiver| { + self.env.identifiers[place.identifier.0 as usize] + .mutable_range + .id + == receiver + }) + } +} + +impl ReactiveFunctionVisitor for Mutations<'_> { + type State = bool; + + fn env(&self) -> &Environment { + self.env + } + + fn visit_instruction(&self, instruction: &ReactiveInstruction, is_mutated: &mut bool) { + if let Some(effects) = &instruction.effects { + *is_mutated |= effects.iter().any(|effect| match effect { + AliasingEffect::Mutate { value, .. } + | AliasingEffect::MutateConditionally { value } + | AliasingEffect::MutateTransitive { value } + | AliasingEffect::MutateTransitiveConditionally { value } => self.reaches(value), + // No wildcard: a kind of effect that a sync adds has to be put on one side here. + AliasingEffect::Freeze { .. } + | AliasingEffect::Capture { .. } + | AliasingEffect::Alias { .. } + | AliasingEffect::MaybeAlias { .. } + | AliasingEffect::Assign { .. } + | AliasingEffect::Create { .. } + | AliasingEffect::CreateFrom { .. } + | AliasingEffect::ImmutableCapture { .. } + | AliasingEffect::Apply(_) + | AliasingEffect::CreateFunction { .. } + | AliasingEffect::MutateFrozen { .. } + | AliasingEffect::MutateGlobal { .. } + | AliasingEffect::Impure { .. } + | AliasingEffect::Render { .. } => false, + }); + } + if !matches!(instruction.value, ReactiveValue::Instruction(_)) { + self.traverse_value(instruction.id, &instruction.value, is_mutated); + } + } + + /// The last instruction of a value block has no record of its effects. It can mutate what it uses. + fn visit_value(&self, id: EvaluationOrder, value: &ReactiveValue, is_mutated: &mut bool) { + let ReactiveValue::Instruction(iv) = value else { + return self.traverse_value(id, value, is_mutated); + }; + let operands = crate::hir::visitors::each_instruction_value_operand(iv, self.env); + *is_mutated |= instruction_effect(iv, &operands, self.consts, self.globals, self.env) + >= Effect::Iterate + && operands.iter().any(|operand| self.reaches(operand)); + } +} + /// The value of a terminal prints as part of the statement of that terminal. fn promote_interposed_terminal_value( value: &ReactiveValue, @@ -1088,12 +1406,20 @@ fn promote_interposed_terminal_place( fn promote_interposed_terminal( stmt: &ReactiveTerminalStatement, + before: &Before<'_>, state: &mut State, inter_state: &mut InterState<'_>, consts: &mut IdBitSet, globals: &mut IdBitSet, env: &mut Environment, ) { + // A labeled block runs once, in sequence. An early return puts the statements of a memo block in one. + let is_sequence = matches!(stmt.terminal, ReactiveTerminal::Label { .. }); + let around = Before { + terminal: (!is_sequence).then_some(stmt), + ..*before + }; + let around = Some(&around); match &stmt.terminal { ReactiveTerminal::Break { .. } | ReactiveTerminal::Continue { .. } => {} ReactiveTerminal::Return { value, .. } | ReactiveTerminal::Throw { value, .. } => { @@ -1108,7 +1434,7 @@ fn promote_interposed_terminal( } => { promote_interposed_terminal_value(init, state, inter_state, consts, globals, env); promote_interposed_terminal_value(test, state, inter_state, consts, globals, env); - promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, around, state, inter_state, consts, globals, env); if let Some(update) = update { promote_interposed_terminal_value(update, state, inter_state, consts, globals, env); } @@ -1125,25 +1451,25 @@ fn promote_interposed_terminal( let test = promote_interposed_value(test, state, inter_state, consts, globals, env); inter_state.exit_value_block(outer); inter_state.statement(init.max(test)); - promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, around, state, inter_state, consts, globals, env); } ReactiveTerminal::ForIn { init, loop_block, .. } => { promote_interposed_terminal_value(init, state, inter_state, consts, globals, env); - promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, around, state, inter_state, consts, globals, env); } ReactiveTerminal::DoWhile { loop_block, test, .. } => { - promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, around, state, inter_state, consts, globals, env); promote_interposed_terminal_value(test, state, inter_state, consts, globals, env); } ReactiveTerminal::While { test, loop_block, .. } => { promote_interposed_terminal_value(test, state, inter_state, consts, globals, env); - promote_interposed_block(loop_block, state, inter_state, consts, globals, env); + promote_interposed_block(loop_block, around, state, inter_state, consts, globals, env); } ReactiveTerminal::If { test, @@ -1152,9 +1478,9 @@ fn promote_interposed_terminal( .. } => { promote_interposed_terminal_place(test, state, inter_state, consts, env); - promote_interposed_block(consequent, state, inter_state, consts, globals, env); + promote_interposed_block(consequent, around, state, inter_state, consts, globals, env); if let Some(alt) = alternate { - promote_interposed_block(alt, state, inter_state, consts, globals, env); + promote_interposed_block(alt, around, state, inter_state, consts, globals, env); } } ReactiveTerminal::Switch { test, cases, .. } => { @@ -1169,12 +1495,20 @@ fn promote_interposed_terminal( inter_state.statement(effect); for case in cases { if let Some(block) = &case.block { - promote_interposed_block(block, state, inter_state, consts, globals, env); + promote_interposed_block( + block, + around, + state, + inter_state, + consts, + globals, + env, + ); } } } ReactiveTerminal::Label { block, .. } => { - promote_interposed_block(block, state, inter_state, consts, globals, env); + promote_interposed_block(block, around, state, inter_state, consts, globals, env); } ReactiveTerminal::Try { block, @@ -1182,11 +1516,11 @@ fn promote_interposed_terminal( handler, .. } => { - promote_interposed_block(block, state, inter_state, consts, globals, env); + promote_interposed_block(block, around, 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); + promote_interposed_block(handler, around, state, inter_state, consts, globals, env); } } } diff --git a/test/aot/parallel/a-method-that-the-react-compiler-reads-early-is-called-with-its-receiver/a-statement-of-an-argument-replaces-the-method.js b/test/aot/parallel/a-method-that-the-react-compiler-reads-early-is-called-with-its-receiver/a-statement-of-an-argument-replaces-the-method.js new file mode 100644 index 000000000000..48f99c3e18cb --- /dev/null +++ b/test/aot/parallel/a-method-that-the-react-compiler-reads-early-is-called-with-its-receiver/a-statement-of-an-argument-replaces-the-method.js @@ -0,0 +1,42 @@ +// Flags: --react-compiler +// The React Compiler moves a statement out of an argument of `a.b(..)`, and reads `a.b` ahead of it. The call is then `Function.prototype.call.call(method, receiver, ..)`. +function who(suffix = "") { + return this.name + ":old" + suffix; +} +function other() { + return this.name + ":new"; +} +const make = name => ({ name, who, inner: { name: name + ".inner", who } }); + +function statementReplacesTheMethod(text) { + "use memo"; + const named = make(text); + return named.who((() => { named.who = other; return "!"; })()); +} +function receiverIsAPropertyLoad(text) { + "use memo"; + const outer = make(text); + return outer.inner.who((() => { outer.inner.who = other; return "!"; })()); +} +function statementInASpreadArgument(text) { + "use memo"; + const named = make(text); + return named.who(...(() => { named.who = other; return ["!"]; })()); +} +function keyIsAMemberThatIsUpdated(text) { + "use memo"; + const parts = [n => text + ":first:" + n, n => text + ":second:" + n]; + const state = { i: 0 }; + return parts[state.i](state.i++) + state.i; +} +// The method of an array keeps its member call. +function pushBehindAMemberUpdate(text) { + "use memo"; + const out = []; + const counter = { n: 0 }; + for (const letter of text) out.push(letter + counter.n++); + return out.join(); +} +for (const run of [statementReplacesTheMethod, receiverIsAPropertyLoad, statementInASpreadArgument, keyIsAMemberThatIsUpdated, pushBehindAMemberUpdate]) { + console.log(run.name, run("ab"), run("ab"), run("cd")); +} diff --git a/test/aot/react-compiler/same-as-bundle/pure-components/MethodCalls.tsx b/test/aot/react-compiler/same-as-bundle/pure-components/MethodCalls.tsx new file mode 100644 index 000000000000..d884caa25931 --- /dev/null +++ b/test/aot/react-compiler/same-as-bundle/pure-components/MethodCalls.tsx @@ -0,0 +1,57 @@ +// A statement in an argument mutates the receiver of the call. The compiler reads the method ahead of the statement, and calls it with the receiver given. +type Props = { a: number; text: string }; +type Named = { name: string; who: (this: Named, suffix?: string) => string; inner?: Named }; + +function who(this: Named, suffix = "") { + return this.name + ":old" + suffix; +} +function other(this: Named) { + return this.name + ":new"; +} +const make = (name: string): Named => ({ name, who }); + +export function StatementReplacesTheMethod({ a, text }: Props) { + "use memo"; + const named = make(text + a); + return [ + named.who( + (() => { + named.who = other; + return "!"; + })(), + ), + ]; +} + +export function StatementInASpreadArgument({ a, text }: Props) { + "use memo"; + const named = make(text + a); + return [ + named.who( + ...((): [string] => { + named.who = other; + return ["!"]; + })(), + ), + ]; +} + +export function ReceiverIsAPropertyLoad({ a, text }: Props) { + "use memo"; + const outer = { inner: make(text + a) }; + return [ + outer.inner.who( + (() => { + outer.inner.who = other; + return "!"; + })(), + ), + ]; +} + +export function KeyIsAMemberThatIsUpdated({ a, text }: Props) { + "use memo"; + const parts = [(n: number) => text + ":first:" + n, (n: number) => text + ":second:" + n]; + const state = { i: a % 2 }; + return [parts[state.i](state.i++), state.i]; +} diff --git a/test/aot/react-compiler/same-as-bundle/pure-components/main.tsx b/test/aot/react-compiler/same-as-bundle/pure-components/main.tsx index b748211ebc43..acacba457d16 100644 --- a/test/aot/react-compiler/same-as-bundle/pure-components/main.tsx +++ b/test/aot/react-compiler/same-as-bundle/pure-components/main.tsx @@ -1,6 +1,7 @@ // Each component renders three times: twice with equal props, once with others. The components are pure, so the output is the same with the compiler off. import { render } from "../react-stand-in/render"; import * as JoinedText from "./JoinedText"; +import * as MethodCalls from "./MethodCalls"; import * as Shapes from "./Shapes"; type Component = (props: unknown) => unknown; @@ -11,6 +12,6 @@ const allProps = [ { a: 1, text: "x", list, nested: { value: 7 } }, { a: 2, text: "y", list: [3, 4], nested: null }, ]; -for (const [name, component] of Object.entries({ ...Shapes, ...JoinedText })) { +for (const [name, component] of Object.entries({ ...Shapes, ...JoinedText, ...MethodCalls })) { for (const props of allProps) console.log(name, JSON.stringify(render(component as Component, props))); } diff --git a/test/aot/runner-suites.known.txt b/test/aot/runner-suites.known.txt index b1b0ee19d1ff..76605a92cd62 100644 --- a/test/aot/runner-suites.known.txt +++ b/test/aot/runner-suites.known.txt @@ -3,6 +3,7 @@ # `! ` before a line: in a Bun built with assertions one of them stops that test's child run. # `x64 ` or `arm64 ` before a line: that test fails on that architecture alone. test/cli/test/bun-test.test.ts: --react-compiler > a compiled file is cached whatever its size ["--react-compiler"] +test/cli/test/bun-test.test.ts: --react-compiler > a function that binds Function keeps a method call that a statement of an argument could change test/cli/test/bun-test.test.ts: --react-compiler > a pattern test/cli/test/bun-test.test.ts: --react-compiler > exclude alone test/cli/test/bun-test.test.ts: --react-compiler > false, and the flag diff --git a/test/bundler/transpiler/react-compiler.test.ts b/test/bundler/transpiler/react-compiler.test.ts index ff34a7a9f3e4..2b179749dc7d 100644 --- a/test/bundler/transpiler/react-compiler.test.ts +++ b/test/bundler/transpiler/react-compiler.test.ts @@ -3973,6 +3973,1087 @@ describe("bundler", () => { }); } + // Codegen prints the method of `o.who(...)` inside the call, so that the + // call has its receiver. So the method was read behind every statement that + // stands between its load and the call: the body of an inlined IIFE, a + // dependency of a memo block of an argument, an earlier argument that has a + // name. A statement that mutates the receiver then decided which function + // ran, or whether the read threw. The key of `o[key](...)` was read there + // too. Now such a method is read where the source reads it, and called with + // the receiver given. Each function renders with a new cache, with the same + // cache and equal props, and with the same cache and other props. + { + type Cell = { name: string; source: string; expected: (name: string) => string }; + const cells: Cell[] = []; + + // Each callee reads a method of `target`. Each producer puts a statement + // that replaces that method between the read and the call. + const callees = { + Member: { setup: `const o = make(p.name);`, target: `o`, callee: `o.who` }, + ComputedMember: { setup: `const o = make(p.name); const key = "who";`, target: `o`, callee: `o[key]` }, + MemberOfAMember: { setup: `const o = make(p.name);`, target: `o.inner`, callee: `o.inner.who` }, + MemberOfACall: { setup: `const o = make(p.name);`, target: `o`, callee: `same(o).who` }, + MemberOfALogical: { setup: `const o = make(p.name); const b = make("B");`, target: `o`, callee: `(o ?? b).who` }, + }; + const producers = { + BehindAnInlinedFunction: (t: string) => `(() => { ${t}.who = other; return ""; })()`, + BehindAManualMemo: (t: string) => `useMemo(() => { ${t}.who = other; return ""; }, [])`, + // An inlined function with two returns is a labeled block. + BehindALabeledBlock: (t: string) => + `(() => { if (p.n > 2) { ${t}.who = other; return ""; } ${t}.who = other; return ""; })()`, + // The first argument has a name, because a statement or a memo block follows it. + BehindAnArgumentAndAStatement: (t: string) => `replace(${t}), (() => { local.n = 1; return ""; })()`, + BehindAnArgumentAndAMemberUpdate: (t: string) => `replace(${t}), local.n++`, + BehindAnArgumentAndAMemoBlock: (t: string) => `replace(${t}), {p.n}`, + // The memo block of the element depends on the sum, so the sum has a name. + BehindADependencyOfAMemoBlock: (t: string) => `{replaceAndCount(${t}) + p.n}`, + BehindASpreadAndAMemoBlock: (t: string) => `...replaceAndList(${t}), {p.n}`, + }; + // A function that is left as it is written prints the right result too. So each of these calls has to print with + // the receiver given. Without memo blocks the last three producers leave no statement. + const replaced = (producerNames: string[]) => + Object.keys(callees).flatMap(calleeName => producerNames.map(producerName => calleeName + producerName)); + // The forms by hand whose call stands in a block of its own, with the load of its method ahead of that block. + const formsWithTheReceiverGiven = [ + "MethodAheadOfAMemoBlock", + "MethodAheadOfAMemoBlockWithAnEarlyReturn", + "MethodAheadOfAScopeThatAHookPrunes", + "MethodAheadOfAScopeThatALoopPrunes", + "MethodAheadOfASwitchThatHoldsTheCall", + "MethodAheadOfASwitchWithACaseThatFallsThrough", + "MethodAheadOfASwitchWithNothingBetween", + "MethodAheadOfTwoMemoBlocks", + "MethodAheadOfTwoScopesThatALoopPrunes", + "MethodBehindALoopAheadOfAScopeThatAHookPrunes", + ]; + const receiverGiven = { + browser: [...replaced(Object.keys(producers)), ...formsWithTheReceiverGiven], + bun: [...replaced(Object.keys(producers).slice(0, -3)), ...formsWithTheReceiverGiven], + }; + // The walk finds the load of these ahead of the block of the call, and nothing between mutates the receiver. The + // member call stays, with its key named. + const keyedCallsAheadOfABlock = [ + "KeyAheadOfAMemoBlock", + "KeyAheadOfAMemoBlockWithAnEarlyReturn", + "KeyAheadOfAScopeThatAHookPrunes", + "KeyAheadOfAScopeThatALoopPrunes", + "KeyAheadOfASwitchThatHoldsTheCall", + "KeyThatMutatesTheReceiverAheadOfAMemoBlock", + "KeyThatMutatesTheReceiverAheadOfAScopeThatALoopPrunes", + "KeyWithAnEffectAheadOfAMemoBlock", + ]; + for (const [calleeName, { setup, target, callee }] of Object.entries(callees)) { + for (const [producerName, producer] of Object.entries(producers)) { + cells.push({ + name: calleeName + producerName, + source: ` + export function ${calleeName + producerName}(p) { + ${setup} + const local = { n: 0 }; + return
{${callee}(${producer(target)})}
; + }`, + expected: name => name + ":old", + }); + } + } + + // Each key is read ahead of a statement that changes what it evaluates + // to, or that shows when it ran. Nothing mutates a prop, so the method of + // a prop stays in the call and only its key gets a name. + const keys = { + KeyIsACall: { + setup: `const log = [];`, + key: `pick(log)`, + statement: `log.push("statement")`, + argument: `note(log)`, + count: `noteAndCount(log)`, + result: `result + " " + log`, + expected: (name: string) => name + ":old key,statement", + }, + KeyIsAMember: { + setup: `const selector = { key: "who" };`, + key: `selector.key`, + statement: `selector.key = "alt"`, + argument: `retarget(selector)`, + count: `retargetAndCount(selector)`, + result: `result`, + expected: (name: string) => name + ":old", + }, + }; + const receivers = { + OfALocal: { setup: `const o = make(p.name);`, receiver: `o` }, + OfAProp: { setup: ``, receiver: `p.api` }, + }; + const keyProducers = { + BehindAnInlinedFunction: (key: (typeof keys)["KeyIsACall"]) => `(() => { ${key.statement}; return ""; })()`, + BehindAnArgumentAndAMemoBlock: (key: (typeof keys)["KeyIsACall"]) => `${key.argument}, {p.n}`, + BehindADependencyOfAMemoBlock: (key: (typeof keys)["KeyIsACall"]) => `{${key.count} + p.n}`, + }; + for (const [keyName, key] of Object.entries(keys)) { + for (const [receiverName, { setup, receiver }] of Object.entries(receivers)) { + for (const [producerName, producer] of Object.entries(keyProducers)) { + cells.push({ + name: keyName + receiverName + producerName, + source: ` + export function ${keyName + receiverName + producerName}(p) { + ${setup} + ${key.setup} + const result = ${receiver}[${key.key}](${producer(key)}); + return
{${key.result}}
; + }`, + expected: key.expected, + }); + } + } + } + // A namespace lists its exports by name. + const expectedOfCells = cells + .map(({ name, expected }) => `${name}=${expected("A")} | ${expected("A")} | ${expected("C")}`) + .sort(); + + // The forms that are not a cell of the tables, with what each prints. + const expectedOfForms = [ + "DependencyThatIteratesTheReceiver=A:old | A:old | C:old", + "DependencyWithACallInATest=A:old | A:old | C:old", + "EarlierArgumentIsAChainOfMethods=A:olda | A:olda | C:oldc", + "EarlierArgumentIsAMethodOfAProp=A:oldA:old | A:oldA:old | C:oldC:old", + "EarlierArgumentOverAnotherLocal=A:old1 | A:old1 | C:old1", + "KeyAheadOfAMemoBlock=B:oldalt;1i | B:oldalt;1i | B:oldalt;1i", + "KeyAheadOfAMemoBlockWithAnEarlyReturn=B:oldalt;1i | B:oldalt;1i | B:oldalt;1i", + "KeyAheadOfAScopeThatAHookPrunes=A:old;2 | A:old;2 | C:old;2", + "KeyAheadOfAScopeThatALoopPrunes=A:old;2A:old;2 | A:old;2A:old;2 | C:old;2C:old;2", + "KeyAheadOfASwitchThatHoldsTheCall=A:old | A:old | C:old", + "KeyInAScopeThatAHookPrunes=A:old 0,key,statement | A:old 0,key,statement | C:old 0,key,statement", + "KeyIsAConversionOfAMemberThatIsUpdated=A:first | A:first | C:first", + "KeyIsAMemberThatIsUpdated=A:first | A:first | C:first", + "KeyIsReassigned=A:old | A:old | C:old", + "KeyOfAMethodOfAProp=A:old | A:old | C:old", + "KeyThatMutatesTheReceiverAheadOfAMemoBlock=B:old;1 | B:old;1 | B:old;1", + "KeyThatMutatesTheReceiverAheadOfAScopeThatALoopPrunes=A1:old;2A2:old;2 | A1:old;2A2:old;2 | C1:old;2C2:old;2", + "KeyWithAnEffectAheadOfAMemoBlock=B:old;1i1 | B:old;1i2 | B:old;1i3", + "LaterArgumentBehindADependency=A:old | A:old | C:old", + "LaterArgumentBehindAStatement=A:old | A:old | C:old", + "MemoBlocksAheadOfTheReceiver=A:old16 | A:old16 | C:old16", + "MethodAheadOfAMemoBlock=A:old;1 | A:old;1 | C:old;1", + "MethodAheadOfAMemoBlockWithAnEarlyReturn=A:old;1 | A:old;1 | C:old;1", + "MethodAheadOfAScopeThatAHookPrunes=A:old;2 | A:old;2 | C:old;2", + "MethodAheadOfAScopeThatALoopPrunes=A1:old;2A2:old;2 | A1:old;2A2:old;2 | C1:old;2C2:old;2", + "MethodAheadOfASwitchThatHoldsTheCall=A:old | A:old | C:old", + "MethodAheadOfASwitchWithACaseThatFallsThrough=A:old | A:old | C:old", + "MethodAheadOfASwitchWithNothingBetween=A:old | A:old | C:old", + "MethodAheadOfTwoMemoBlocks=A:old;12 | A:old;12 | C:old;12", + "MethodAheadOfTwoScopesThatALoopPrunes=A1:old;22A2:old;22 | A1:old;22A2:old;22 | C1:old;22C2:old;22", + "MethodBehindALoopAheadOfAScopeThatAHookPrunes=A:old;2 | A:old;2 | C:old;2", + "MethodHasItsOwnCall=A:old | A:old | C:old", + "MethodInAMemoBlockAheadOfTheCall=A:old;1 | A:old;1 | C:old;1", + "MethodIsAProxy=A:old | A:old | C:old", + "MethodIsMissing=TypeError after statement,argument | TypeError after statement,argument | TypeError after statement,argument", + "MethodOfAPropBehindAStatement=A:old | A:old | C:old", + "PushBehindAMemberUpdate=A0,x1 | A0,x1 | C0,x1", + "PushOfAnElementWithComputedProps=1:2 | 1:2 | 1:3", + "PushOfAnObjectWithACallInALogical=2:true | 2:true | 2:false", + "SpreadOfAStatementThatReplacesTheMethod=A:oldxy | A:oldxy | C:oldxy", + "StatementBehindANullReceiver=TypeError, ran: A | TypeError, ran: A | TypeError, ran: C", + "StatementBehindAReadThatThrows=threw the read of the method | threw the read of the method | threw the read of the method", + "StatementChangesWhatAGetterReads=A:old | A:old | C:old", + "StatementReassignsTheReceiver=A:old | A:old | C:old", + ]; + + // Where no statement can mutate the receiver, the call is the member call + // that the source has. The names of the temporaries are normalized. + const memberCalls = { + browser: { + // The memo block of the receiver holds nothing that the statement mutates. + EarlierArgumentIsAMethodOfAProp: "o.who(t, t)", + EarlierArgumentOverAnotherLocal: "o.who(t, t)", + KeyIsAConversionOfAMemberThatIsUpdated: "fns[t](t)", + // The argument that replaces the method prints inside the call, behind the read of the method. + LaterArgumentBehindADependency: "o.who(t, replace(o))", + LaterArgumentBehindAStatement: 'o.who("", replace(o))', + // Nothing mutates a prop. + KeyInAScopeThatAHookPrunes: 't[t]("")', + KeyOfAMethodOfAProp: "t[t](t)", + MethodOfAPropBehindAStatement: 't.who("")', + // The inference takes `push` of an array for that method. + PushBehindAMemberUpdate: "items.push(row + t)", + PushOfAnElementWithComputedProps: "items.push(t)", + PushOfAnObjectWithACallInALogical: "items.push(t)", + }, + // Without memo blocks no two values share a mutable range, and every mutation between the read and the call counts. + bun: { + KeyInAScopeThatAHookPrunes: 't[t]("")', + KeyOfAMethodOfAProp: "t[t](t)", + MethodOfAPropBehindAStatement: 't.who("")', + PushBehindAMemberUpdate: "items.push(row + t)", + }, + }; + const memberCall = + /\bitems\.push\((?:row \+ )?t\d+\)|\bfns\[t\d+\]\(t\d+\)|\bt\d+\[t\d+\]\((?:t\d+|"")\)|\b(?:t\d+|o)\.who\((?:t\d+|""|replace\(o\)|, )*\)/; + + for (const [name, options] of [ + ["plain", { reactCompiler: false, target: "browser" }], + ["browser", { reactCompiler: true, target: "browser" }], + ["bun", { reactCompiler: true, target: "bun" }], + ["minify-syntax", { reactCompiler: true, target: "browser", minifySyntax: true }], + ["bun-minify-syntax", { reactCompiler: true, target: "bun", minifySyntax: true }], + ] as const) { + itBundled(`react-compiler/MethodIsReadAheadOfAStatementThatCanChangeIt-${name}`, { + files: { + "/entry.ts": /* ts */ ` + import { render } from "react"; + import * as cells from "./cells"; + import * as forms from "./forms"; + const who = function (this: { name: string } | undefined) { + return (this === undefined ? "no this" : this.name) + ":old"; + }; + const lines: string[] = []; + for (const [name, Form] of [...Object.entries(cells), ...Object.entries(forms)]) { + const results: string[] = []; + for (const props of [{ name: "A", n: 2 }, { name: "A", n: 2 }, { name: "C", n: 3 }]) { + try { + results.push(String(render(Form, { ...props, api: { name: props.name, who } }).props.children)); + } catch (e) { + results.push("threw " + e.message); + } + } + lines.push(name + "=" + results.join(" | ")); + } + console.log(lines.join("\\n")); + `, + "/helpers.js": /* js */ ` + // A method says which object it ran on, and which of the two functions it is. + export function who(...suffix) { + return (this === undefined ? "no this" : this.name) + ":old" + suffix.filter(s => typeof s === "string").join(""); + } + export function other() { + return (this === undefined ? "no this" : this.name) + ":new"; + } + export const make = name => ({ name, who, alt: other, inner: { name, who } }); + export const same = o => o; + export const replace = o => { + o.who = other; + return ""; + }; + export const replaceAndCount = o => { + o.who = other; + return 1; + }; + export const replaceAndList = o => { + o.who = other; + return ["", ""]; + }; + export const pick = log => { + log.push("key"); + return "who"; + }; + export const note = log => { + log.push("statement"); + return ""; + }; + export const noteAndCount = log => { + log.push("statement"); + return 1; + }; + export const retarget = selector => { + selector.key = "alt"; + return ""; + }; + export const retargetAndCount = selector => { + selector.key = "alt"; + return 1; + }; + export const tick = log => { + log.push("argument"); + return 1; + }; + export const size = list => list.length; + // Counts its conversions to a property key. The hook reads the count in every render. + let conversions = 0; + export const countedKey = { + toString() { + conversions++; + return "who"; + }, + }; + export const useConversions = () => conversions; + `, + "/cells.jsx": + ` + import { useMemo } from "react"; + import { make, note, noteAndCount, other, pick, replace, replaceAndCount, replaceAndList, retarget, retargetAndCount, same } from "./helpers"; + ` + cells.map(cell => cell.source).join(""), + "/forms.jsx": /* jsx */ ` + import { useState } from "react"; + import { countedKey, make, other, pick, replace, same, size, tick, useConversions, who } from "./helpers"; + + const withGetter = o => + Object.defineProperty(o, "who", { + get() { + const name = this.name; + return () => name + ":old"; + }, + }); + const guarded = o => + Object.defineProperty(o, "who", { + get() { + if (!this.ok) throw new Error("the read of the method"); + return () => "called"; + }, + }); + const iterable = name => ({ + name, + who, + *[Symbol.iterator]() { + this.who = other; + yield 1; + }, + }); + // Answers every read, also a read of "call". + const answersEveryRead = new Proxy(who, { get: () => () => "a read of the method" }); + const withItsOwnCall = Object.assign( + function () { + return this.name + ":old"; + }, + { call: () => "its own call" }, + ); + + // The getter reads what the statement changes. + export function StatementChangesWhatAGetterReads(p) { + const o = withGetter({ name: p.name }); + return
{o.who((() => { o.name = "changed"; return ""; })())}
; + } + // The read of the method throws, and the statement would stop that. + export function StatementBehindAReadThatThrows(p) { + const o = guarded({ ok: false, name: p.name }); + return
{o.who((() => { o.ok = true; return ""; })())}
; + } + // The receiver is null, so the statement must not run. + export function StatementBehindANullReceiver(p) { + const box = { inner: null, ran: [p.name] }; + let out; + try { + out = box.inner.who((() => { box.ran.push("statement"); return ""; })()); + } catch (e) { + out = (e instanceof TypeError ? "TypeError" : e) + ", ran: " + box.ran; + } + return
{out}
; + } + // The statement assigns the variable of the receiver, and then replaces the method of the new object. + export function StatementReassignsTheReceiver(p) { + let o = make(p.name); + return
{o.who((() => { o = make("B"); o.who = other; return ""; })())}
; + } + // The call is the test of a ternary, which has no record of what it mutates. + export function DependencyWithACallInATest(p) { + const o = make(p.name); + return
{o.who({replace(o) ? p.n : -p.n})}
; + } + // The spread runs the iterator of the receiver. + export function DependencyThatIteratesTheReceiver(p) { + const o = iterable(p.name); + return
{o.who({[...o].length + p.n})}
; + } + // Memo blocks stand ahead of the receiver, and another value shares its memo block. + export function MemoBlocksAheadOfTheReceiver(p) { + const a1 = {p.n + 1}; + const a2 = {p.n + 2}; + const a3 = {p.n + 3}; + const a4 = {p.n + 4}; + const a5 = {p.n + 5}; + const a6 = {p.n + 6}; + const list = []; + const o = make(p.name); + list.push(p.n); + const result = o.who((() => { o.who = other; return ""; })()); + return
{result + list.length + [a1, a2, a3, a4, a5, a6].length}
; + } + // The call with the receiver given must not read "call" of the method. + export function MethodIsAProxy(p) { + const o = { name: p.name, who: answersEveryRead }; + return
{o.who((() => { o.touched = true; return ""; })())}
; + } + export function MethodHasItsOwnCall(p) { + const o = { name: p.name, who: withItsOwnCall }; + return
{o.who((() => { o.touched = true; return ""; })())}
; + } + // The arguments run before the call throws. + export function MethodIsMissing(p) { + const o = { name: p.name }; + const log = []; + let out; + try { + out = o.missing((() => { o.touched = true; log.push("statement"); return ""; })(), tick(log)); + } catch (e) { + out = (e instanceof TypeError ? "TypeError" : e) + " after " + log; + } + return
{out}
; + } + // A spread argument prints in the call with the receiver given. + export function SpreadOfAStatementThatReplacesTheMethod(p) { + const o = make(p.name); + return
{o.who(...(() => { o.who = other; return ["x", "y"]; })())}
; + } + export function KeyIsReassigned(p) { + const o = make(p.name); + let key = "who"; + return
{o[key]((() => { key = "alt"; return ""; })())}
; + } + // A postfix update of a member is a statement too. + export function KeyIsAMemberThatIsUpdated(p) { + const fns = [() => p.name + ":first", () => p.name + ":second"]; + const state = { i: 0 }; + return
{fns[state.i](state.i++)}
; + } + export function KeyIsAConversionOfAMemberThatIsUpdated(p) { + const fns = { 0: () => p.name + ":first", 1: () => p.name + ":second" }; + const state = { i: 0 }; + return
{fns[String(state.i)](state.i++)}
; + } + export function KeyOfAMethodOfAProp(p) { + const keys = ["who", "alt"]; + const state = { i: 0 }; + return
{p.api[keys[state.i]](state.i++)}
; + } + // A hook stands in the scope of \`log\`, so that scope is pruned. The call does not mutate \`log\`: it stands + // behind the scope, and the load of its method in it. + export function KeyInAScopeThatAHookPrunes(p) { + const log = []; + const [n] = useState(0); + log.push(n); + const result = p.api[pick(log)]((() => { log.push("statement"); return ""; })()); + return
{result + " " + log}
; + } + // The result of the call is a primitive and no argument is mutable, so the call is a member of no scope. + // It stands in the scope of \`last\`, which the loop prunes, and the load of its method ahead of that scope. + export function MethodAheadOfAScopeThatALoopPrunes(p) { + let out = ""; + for (const suffix of ["1", "2"]) { + const o = make(p.name + suffix); + let last; + const result = o.who((() => { o.who = other; return {o}; })(), (last = [suffix], "")) + ";"; + last.push(result); + out += result + last.length; + } + return
{out}
; + } + export function KeyAheadOfAScopeThatALoopPrunes(p) { + let out = ""; + for (const suffix of ["1", "2"]) { + let key = "who"; + let last; + const result = p.api[key]((() => { key = "alt"; return ""; })(), (last = [suffix], "")) + ";"; + last.push(result); + out += result + last.length; + } + return
{out}
; + } + // One hook prunes the scope that holds the load, and one the scope that holds the call. + export function MethodAheadOfAScopeThatAHookPrunes(p) { + const o = make(p.name); + const [n] = useState(0); + let last; + const result = o.who((() => { o.who = other; return {o}{n}; })(), (last = [], "")) + ";"; + const [m] = useState(0); + last.push(result, m); + return
{result + last.length}
; + } + export function KeyAheadOfAScopeThatAHookPrunes(p) { + let key = "who"; + const [n] = useState(0); + let last; + const result = p.api[key]((() => { key = "alt"; return n; })(), (last = [], "")) + ";"; + const [m] = useState(0); + last.push(result, m); + return
{result + last.length}
; + } + // The memo block of \`last\` holds the call. The scope ahead of it holds the load, and a hook prunes that scope. + export function MethodAheadOfAMemoBlock(p) { + const o = make(p.name); + const [n] = useState(0); + let last; + const result = o.who((() => { o.who = other; return {o}{n}; })(), (last = [], "")) + ";"; + last.push(result); + return
{result + last.length}
; + } + // An early return puts the statements of the memo block of \`last\` in a labeled block. + export function MethodAheadOfAMemoBlockWithAnEarlyReturn(p) { + const o = make(p.name); + const [n] = useState(0); + let last; + const result = o.who((() => { o.who = other; return {o}{n}; })(), (last = [], "")) + ";"; + if (p.n > 100) return null; + last.push(result); + return
{result + last.length}
; + } + export function KeyAheadOfAMemoBlockWithAnEarlyReturn(p) { + const o = make("B"); + const frozen = {o}; + let key = same("who"); + let last; + const result = o[key]((key = "alt"), (last = [], "")) + ";"; + if (p.n > 100) return null; + last.push(result); + return
{result + last.length + frozen.type}
; + } + // The inlined function has its one return in a case, so the call and all behind it stand in that case. + export function MethodAheadOfASwitchThatHoldsTheCall(p) { + const o = make(p.name); + return
{o.who((() => { switch (p.n) { default: o.who = other; return ""; } })())}
; + } + // The statement stands in another case, which falls through to the case of the call. + export function MethodAheadOfASwitchWithACaseThatFallsThrough(p) { + const o = make(p.name); + return
{o.who((() => { switch (p.n) { case 2: o.who = other; default: return ""; } })())}
; + } + export function KeyAheadOfASwitchThatHoldsTheCall(p) { + let key = "who"; + return
{p.api[key]((() => { switch (p.n) { default: key = "alt"; return ""; } })())}
; + } + // Nothing touches the receiver here. On the way out of a block of a terminal the whole terminal counts, the + // call too, so this call prints with the receiver given. + export function MethodAheadOfASwitchWithNothingBetween(p) { + const o = make(p.name); + const local = { n: 0 }; + return
{o.who((() => { switch (p.n) { default: local.n = 1; return ""; } })())}
; + } + // The loop prunes both scopes: that of \`outerList\`, and that of \`last\` in it, which holds the call. + export function MethodAheadOfTwoScopesThatALoopPrunes(p) { + let out = ""; + for (const suffix of ["1", "2"]) { + const o = make(p.name + suffix); + let outerList, last; + const result = + o.who((() => { o.who = other; return {o}; })(), (outerList = [suffix], ""), (last = [suffix], "")) + + ";"; + last.push(result); + outerList.push(result); + out += result + last.length + outerList.length; + } + return
{out}
; + } + // The loop keeps the body of the function a statement with --minify-syntax too. + export function MethodBehindALoopAheadOfAScopeThatAHookPrunes(p) { + const o = make(p.name); + const [n] = useState(0); + let last; + const result = + o.who((() => { for (const key of ["who"]) o[key] = other; return {o}{n}; })(), (last = [], "")) + ";"; + const [m] = useState(0); + last.push(result, m); + return
{result + last.length}
; + } + // The call stands in the memo block of \`last\`, and that block in the memo block of \`outerList\`. + export function MethodAheadOfTwoMemoBlocks(p) { + const o = make(p.name); + const [n] = useState(0); + let outerList, last; + const result = + o.who((() => { o.who = other; return {o}{n}; })(), (outerList = [], ""), (last = [], "")) + ";"; + last.push(result); + outerList.push(last, p.n); + return
{result + last.length + outerList.length}
; + } + // No statement stands between the load and the call. The key has an effect, and the memo block of \`last\` + // must not hold it: the block runs once, and the source converts the key in every render. + export function KeyWithAnEffectAheadOfAMemoBlock(p) { + const o = make("B"); + const frozen = {o}; + let last; + const result = o[String(countedKey)]((last = [], "")) + ";"; + last.push(result); + const conversions = useConversions(); + return
{result + last.length + frozen.type + conversions}
; + } + // The key mutates the receiver ahead of the load of the method, and nothing does between the load and the + // call. The walk finds the load ahead of the block of the call, and the member call stays. + export function KeyThatMutatesTheReceiverAheadOfAMemoBlock(p) { + const o = make("B"); + let last; + const result = + o[(o.touched = true, same("who"))]( + (() => { const x = make("X"); x.who = other; return {o}{x}; })(), + (last = [], ""), + ) + ";"; + last.push(result); + return
{result + last.length}
; + } + export function KeyThatMutatesTheReceiverAheadOfAScopeThatALoopPrunes(p) { + let out = ""; + for (const suffix of ["1", "2"]) { + const o = make(p.name + suffix); + const local = { n: 0 }; + let last; + const result = + o[(o.touched = true, "who")]((() => { local.n = 1; return {o}; })(), (last = [suffix], "")) + ";"; + last.push(result); + out += result + last.length; + } + return
{out}
; + } + // Without a hook the memo block of \`o\` holds the load of the method, and codegen cannot print the call behind it. + export function MethodInAMemoBlockAheadOfTheCall(p) { + const o = make(p.name); + let last; + const result = o.who((() => { o.who = other; return {o}; })(), (last = [], "")) + ";"; + last.push(result); + return
{result + last.length}
; + } + // Nothing here is reactive, so the memo block of \`last\` does not depend on the method. That block holds the + // call, and the load of the method stands ahead of it. + export function KeyAheadOfAMemoBlock(p) { + const o = make("B"); + const frozen = {o}; + let key = same("who"); + let last; + const result = o[key]((key = "alt"), (last = [], "")) + ";"; + last.push(result); + return
{result + last.length + frozen.type}
; + } + export function MethodOfAPropBehindAStatement(p) { + const local = { n: 0 }; + return
{p.api.who((() => { local.n = 1; return ""; })())}
; + } + // The earlier argument gets a name ahead of the memo block, and it cannot mutate the receiver. + export function EarlierArgumentOverAnotherLocal(p) { + const o = make(p.name); + const log = []; + return
{o.who(String(tick(log)), {p.n})}
; + } + export function EarlierArgumentIsAMethodOfAProp(p) { + const o = make(p.name); + return
{o.who(p.api.who(), {p.n})}
; + } + // The call mutates its arguments together with the receiver, so the mutation of one of them counts. + export function EarlierArgumentIsAChainOfMethods(p) { + const o = make(p.name); + return
{o.who(p.name.trim().toLowerCase(), {p.n})}
; + } + // The statement mutates another object, and the argument that replaces the method prints inside the call. + export function LaterArgumentBehindAStatement(p) { + const o = make(p.name); + const local = { n: 0 }; + return
{o.who((() => { local.n = 1; return ""; })(), replace(o))}
; + } + export function LaterArgumentBehindADependency(p) { + const o = make(p.name); + return
{o.who({size(Object.keys(p)) + p.n}, replace(o))}
; + } + // The commonest member call behind a statement. + export function PushOfAnElementWithComputedProps(p) { + const items = []; + items.push(); + return
{items.length + ":" + items[0].props.title}
; + } + export function PushBehindAMemberUpdate(p) { + const items = []; + const state = { i: 0 }; + for (const row of [p.name, "x"]) { + items.push(row + state.i++); + } + return
{items.join()}
; + } + // The test of the logical expression is a call on a value that shares the memo block of the receiver. + export function PushOfAnObjectWithACallInALogical(p) { + const others = p.name.split("").map(letter => letter + "!"); + const items = [{ value: "a", disabled: others.includes("a!") }]; + items.push({ value: "b", disabled: others.includes("A!") || p.n > 5 }); + return
{items.length + ":" + items[1].disabled}
; + } + `, + "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + "/node_modules/react/index.js": /* js */ ` + const caches = new Map(); + let current; + export const render = (Component, props) => { + current = Component; + return Component(props); + }; + export const memoCache = size => { + let cache = caches.get(current); + if (!cache) caches.set(current, (cache = new Array(size).fill(Symbol.for("react.memo_cache_sentinel")))); + return cache; + }; + export const useMemo = fn => fn(); + export const useState = value => [value, () => {}]; + `, + "/node_modules/react/compiler-runtime.js": /* js */ ` + import { memoCache } from "./index.js"; + export const c = size => memoCache(size); + `, + "/node_modules/react/jsx-runtime.js": /* js */ ` + export const jsx = (type, props) => ({ type, props }); + export const jsxs = jsx; + `, + "/node_modules/react/jsx-dev-runtime.js": /* js */ ` + export const jsxDEV = (type, props) => ({ type, props }); + `, + }, + ...options, + backend: "cli", + onAfterBundle(api) { + if (name !== "browser" && name !== "bun") return; + const bodies = Object.fromEntries( + Array.from(api.readFile("/out.js").matchAll(/^function (\w+)\(p\) \{$([\s\S]*?)^\}$/gm), match => [ + match[1], + match[2], + ]), + ); + expect( + Object.fromEntries( + Object.keys(memberCalls[name]).map(form => [ + form, + bodies[form].includes("Function.prototype.call.call(") + ? "Function.prototype.call.call" + : bodies[form].match(memberCall)?.[0].replace(/\bt\d+\b/g, "t"), + ]), + ), + ).toEqual(memberCalls[name]); + expect(receiverGiven[name].filter(cell => !bodies[cell].includes("Function.prototype.call.call("))).toEqual( + [], + ); + if (name !== "browser") return; + expect( + keyedCallsAheadOfABlock.filter( + form => + bodies[form].includes("Function.prototype.call.call(") || !/\b(?:t\d+|o)\[t\d+\]\(/.test(bodies[form]), + ), + ).toEqual([]); + // The read of the method, or of the key, stands ahead of the memo block that holds the call. + expect(bodies.MethodAheadOfAMemoBlock).toMatch( + /const (t\d+) = t\d+\.who;[\s\S]*\bif \(\$\[\d+\] !== [\s\S]*Function\.prototype\.call\.call\(\1, /, + ); + expect(bodies.KeyAheadOfAMemoBlock).toMatch(/const (t\d+) = key;[\s\S]*\bif \(\$\[\d+\] [\s\S]*\bo\[\1\]\(/); + // A compiled function starts with its memo cache. In these three the load of the method is in a memo block + // and the call is behind that block, which codegen cannot print. + expect(Object.keys(bodies).filter(form => !/^\s*let \$ = c\(\d+\);/.test(bodies[form]))).toEqual([ + "KeyIsACallOfAPropBehindAnInlinedFunction", + "KeyIsACallOfAPropBehindADependencyOfAMemoBlock", + "MethodInAMemoBlockAheadOfTheCall", + ]); + }, + run: { stdout: [...expectedOfCells, ...expectedOfForms].join("\n") }, + }); + } + } + + // The call with the receiver given is `Function.prototype.call.call(method, receiver, ...)`. + // A function that can see another `Function` is left as it is written, and + // a binding that only a type uses stays unused. + itBundled("react-compiler/MethodReadAheadOfAStatementNeedsTheGlobalFunction", { + files: { + "/entry.jsx": /* jsx */ ` + import { other, shadow, who } from "./shared"; + import { ShadowedInTheFile } from "./shadowed"; + import { InterfaceInTheFile } from "./interface"; + import { ImportOfAType } from "./import-of-a-type"; + import { TypeImport } from "./type-import"; + + function ShadowedInTheBody(p) { + const Function = shadow; + const o = { name: p.name, who }; + return
{o.who((() => { o.who = other; return ""; })()) + " " + typeof Function}
; + } + function ShadowedInALoop(p) { + const o = { name: p.name, who }; + let out = ""; + for (const Function of [shadow]) { + out = o.who((() => { o.who = other; return ""; })()) + " " + typeof Function; + } + return
{out}
; + } + function UsesTheGlobal(p) { + const o = { name: p.name, who }; + return
{o.who((() => { o.who = other; return ""; })()) + " " + typeof Function}
; + } + for (const Form of [ + ShadowedInTheFile, + ShadowedInTheBody, + ShadowedInALoop, + ImportOfAType, + UsesTheGlobal, + InterfaceInTheFile, + TypeImport, + ]) { + console.log(Form.name + "=" + Form({ name: "A" }).props.children); + } + `, + "/shared.ts": /* ts */ ` + export type Named = { name: string; who: (this: Named, suffix?: string) => string }; + export function who(this: Named) { + return this.name + ":old"; + } + export function other(this: Named) { + return this.name + ":new"; + } + export const shadow = { prototype: { call: { call: () => "shadowed" } } }; + export interface Function { + (): string; + } + `, + "/shadowed.jsx": /* jsx */ ` + import { other, shadow, who } from "./shared"; + const Function = shadow; + export function ShadowedInTheFile(p) { + const o = { name: p.name, who }; + return
{o.who((() => { o.who = other; return ""; })()) + " " + typeof Function}
; + } + `, + "/interface.tsx": /* tsx */ ` + import { type Named, other, who } from "./shared"; + interface Function { + (): string; + } + export function InterfaceInTheFile(p: { name: string; callback?: Function }) { + const o: Named = { name: p.name, who }; + return
{o.who((() => { o.who = other; return ""; })()) + " " + typeof globalThis.Function}
; + } + `, + "/type-import.tsx": /* tsx */ ` + import type { Function } from "./shared"; + import { type Named, other, who } from "./shared"; + export function TypeImport(p: { name: string; callback?: Function }) { + const o: Named = { name: p.name, who }; + return
{o.who((() => { o.who = other; return ""; })()) + " " + typeof globalThis.Function}
; + } + `, + // Only types use the import, so it names nothing when the file runs. A use on record would keep it. + "/import-of-a-type.tsx": /* tsx */ ` + import { Function, type Named, other, who } from "./shared"; + export function ImportOfAType(p: { name: string; callback?: Function }) { + const o: Named = { name: p.name, who }; + return
{o.who((() => { o.who = other; return ""; })()) + " " + typeof globalThis.Function}
; + } + `, + "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + "/node_modules/react/index.js": ``, + "/node_modules/react/jsx-dev-runtime.js": /* js */ ` + export const jsxDEV = (type, props) => ({ type, props }); + `, + "/node_modules/react/compiler-runtime.js": /* js */ ` + export function c(size) { + return new Array(size).fill(Symbol.for("react.memo_cache_sentinel")); + } + `, + }, + reactCompiler: true, + backend: "cli", + target: "browser", + onAfterBundle(api) { + // A compiled function starts with its memo cache. + const compiled = api.readFile("/out.js").matchAll(/function (\w+)\(p\) \{\s*let \$ = c\(\d+\);/g); + expect(Array.from(compiled, match => match[1]).sort()).toEqual([ + "InterfaceInTheFile", + "TypeImport", + "UsesTheGlobal", + ]); + }, + run: { + stdout: ` + ShadowedInTheFile=A:old object + ShadowedInTheBody=A:old object + ShadowedInALoop=A:old object + ImportOfAType=A:old function + UsesTheGlobal=A:old function + InterfaceInTheFile=A:old function + TypeImport=A:old function + `, + }, + }); + + // Member calls that still read the method, or an argument, behind a statement. Each line says what the function + // prints today in place of what the plain build prints. + for (const target of ["browser", "bun"] as const) { + itBundled(`react-compiler/MethodIsStillReadBehindAStatement-${target}`, { + todo: true, + files: { + "/entry.jsx": /* jsx */ ` + function who(...suffix) { + return (this === undefined ? "no this" : this.name) + ":old" + suffix.filter(s => typeof s === "string").join(""); + } + function other() { + return (this === undefined ? "no this" : this.name) + ":new"; + } + const table = { name: "table", who }; + const replace = o => { + o.who = other; + return ""; + }; + const restore = o => { + o.who = who; + return ""; + }; + const newJoin = function () { + return this[0] + ":new"; + }; + let version = 0; + const bump = () => { + version++; + return ""; + }; + const versioned = name => + Object.defineProperty({ name }, "who", { + get() { + const v = version % 2; + return () => name + ":" + v; + }, + }); + const note = log => { + log.push("argument"); + return ""; + }; + // Each of these runs code of the receiver where the inference records no mutation of it. + const withIterator = name => ({ + name, + who, + *[Symbol.iterator]() { + this.who = other; + yield "x"; + }, + }); + const withGetter = name => ({ + name, + who, + get swap() { + this.who = other; + return "g"; + }, + }); + const withToString = name => ({ + name, + who, + toString() { + this.who = other; + return "s"; + }, + }); + const withTrap = name => + new Proxy( + { name, who }, + { + ownKeys(target) { + target.who = other; + return ["name"]; + }, + }, + ); + + // The pass does not run in a function inside the component: "A:second". + function CallInACallback(p) { + const fns = [() => p.name + ":first", () => p.name + ":second"]; + const state = { i: 0 }; + const run = () => fns[state.i](state.i++); + return
{run()}
; + } + // A method of the props object is called as a function of its own: "no this:old". + function MethodOfTheProps(p) { + const local = { n: 0 }; + return
{p.who((() => { local.n = 1; return ""; })())}
; + } + // A load from a module-level object counts as a constant: "table:newA". + function MethodOfAModuleLevelObject(p) { + const local = { n: 0 }; + const result = table.who(replace(table), (() => { local.n = 1; return ""; })()); + return
{result + restore(table) + p.name}
; + } + // The inference takes \`join\` of an array for the method of arrays. Without memo blocks: "A:new". + function MethodOfAnArrayIsReplaced(p) { + const items = [p.name, "x"]; + return
{items.join((() => { items.join = newJoin; return "-"; })())}
; + } + // The getter reads what no value of the function holds. With memo blocks: "A:1". + function GetterReadsModuleState(p) { + const o = versioned(p.name); + const local = { n: 0 }; + const result = o.who(bump(), (() => { local.n = 1; return ""; })()); + bump(); + return
{result}
; + } + // A spread argument is iterated at the call: "A:oldxy". + function SpreadIsIteratedBehindAStatement(p) { + const o = { name: p.name, who }; + const list = ["x"]; + return
{o.who(...list, (() => { list.push("y"); return ""; })())}
; + } + // Nothing mutates a prop, so its method is read at the call, behind the argument: "TypeError, ran: argument". + function MethodOfAPropThatIsNull(p) { + const log = []; + const state = { i: 0 }; + let out; + try { + out = p.none.who(note(log), state.i++); + } catch (e) { + out = (e instanceof TypeError ? "TypeError" : e) + ", ran: " + (log.length ? log : "nothing"); + } + return
{out}
; + } + // An array pattern runs the iterator of the receiver: "A:new". + function ArrayPatternOverTheReceiver(p) { + const o = withIterator(p.name); + return
{o.who((() => { const [x] = o; return x; })())}
; + } + // A getter of the receiver replaces the method: "A:new". + function GetterOfTheReceiverReplacesTheMethod(p) { + const o = withGetter(p.name); + return
{o.who((() => { const g = o.swap; return g; })())}
; + } + // The conversion to a string runs \`toString\` of the receiver: "A:new". + function ConversionOfTheReceiverToAString(p) { + const o = withToString(p.name); + return
{o.who((() => { const s = String(o); return s; })())}
; + } + // The head of the loop runs the \`ownKeys\` trap of the receiver: "A:new". + function TrapOfTheReceiverReplacesTheMethod(p) { + const o = withTrap(p.name); + return
{o.who((() => { for (const key in o) {} return ""; })())}
; + } + for (const Form of [ + CallInACallback, + MethodOfTheProps, + MethodOfAModuleLevelObject, + MethodOfAnArrayIsReplaced, + GetterReadsModuleState, + SpreadIsIteratedBehindAStatement, + MethodOfAPropThatIsNull, + ArrayPatternOverTheReceiver, + GetterOfTheReceiverReplacesTheMethod, + ConversionOfTheReceiverToAString, + TrapOfTheReceiverReplacesTheMethod, + ]) { + console.log(Form.name + "=" + Form({ name: "A", who }).props.children); + } + `, + "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + "/node_modules/react/index.js": ``, + "/node_modules/react/jsx-dev-runtime.js": /* js */ ` + export const jsxDEV = (type, props) => ({ type, props }); + `, + "/node_modules/react/compiler-runtime.js": /* js */ ` + export function c(size) { + return new Array(size).fill(Symbol.for("react.memo_cache_sentinel")); + } + `, + }, + reactCompiler: true, + backend: "cli", + target, + run: { + stdout: ` + CallInACallback=A:first + MethodOfTheProps=A:old + MethodOfAModuleLevelObject=table:oldA + MethodOfAnArrayIsReplaced=A-x + GetterReadsModuleState=A:0 + SpreadIsIteratedBehindAStatement=A:oldx + MethodOfAPropThatIsNull=TypeError, ran: nothing + ArrayPatternOverTheReceiver=A:oldx + GetterOfTheReceiverReplacesTheMethod=A:oldg + ConversionOfTheReceiverToAString=A:olds + TrapOfTheReceiverReplacesTheMethod=A:old + `, + }, + }); + } + itBundled("react-compiler/SourceMapOfCatch", { files: { "/entry.jsx": /* jsx */ ` diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index 16ecb3c3281a..a0ba057e58f2 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -2107,7 +2107,7 @@ describe.concurrent("--react-compiler", () => { stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - return { compiled: stdout.match(/compiled: (.*)/)?.[1] ?? stderr, stderr, exitCode }; + return { compiled: stdout.match(/compiled: (.*)/)?.[1] ?? stderr, stdout, stderr, exitCode }; } const everything = "List useTotal Story Other"; @@ -2245,6 +2245,48 @@ describe.concurrent("--react-compiler", () => { expect(result.exitCode).toBe(0); }); + // The compiler calls a method that it read ahead of a statement as `Function.prototype.call.call(method, receiver)`. + // `bun test` prints the names of a file as they are written, so a function that binds `Function` stays as it is. + test("a function that binds Function keeps a method call that a statement of an argument could change", async () => { + const result = await compiled( + { + "src/Shadows.jsx": ` + function who() { + return this.name + ":old"; + } + function other() { + return this.name + ":new"; + } + const shadow = { prototype: { call: { call: () => "shadowed" } } }; + export function InALoop(p) { + const o = { name: p.name, who }; + let out = ""; + for (const Function of [shadow]) { + out = o.who((() => { o.who = other; return ""; })()) + " " + typeof Function; + } + return
{out}
; + } + export function TheGlobal(p) { + const o = { name: p.name, who }; + return
{o.who((() => { o.who = other; return ""; })()) + " " + typeof Function}
; + } + `, + "compiled.test.tsx": ` + import { test } from "bun:test"; + import * as all from "./src/Shadows"; + test("which are compiled", () => { + ${printCompiled} + console.log("rendered: " + Object.values(all).map(Form => Form({ name: "A" }).props.children).join(", ")); + }); + `, + }, + ["--react-compiler"], + ); + expect(result.stdout).toContain("rendered: A:old object, A:old function"); + expect(result.compiled).toBe("TheGlobal"); + expect(result.exitCode).toBe(0); + }); + test("a data: URL that is longer than a path can be is not compiled", async () => { const result = await compiled( {