Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/js_parser/react_compiler_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<false>(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_);
}
Expand Down
4 changes: 3 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ 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 and the key of a member call ahead of a statement
/// that can change them.
Comment thread
robobun marked this conversation as resolved.
Resolved
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
Expand Down
77 changes: 74 additions & 3 deletions src/react_compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -228,6 +229,21 @@ pub(crate) fn codegen_function(
context: &mut ProgramContext,
unique_identifiers: HashSet<String>,
) -> Result<CodegenFunction, CompilerError> {
// Not in upstream: `codegen_explicit_receiver_call` prints the global `Function`. The identifiers say what the
// function binds, and the host what is around it. The question comes first, so that a function that stays as
// it is written has no use of that name on record.
Comment thread
robobun marked this conversation as resolved.
Resolved
if !env.explicit_receiver_calls.is_empty() {
let binds_function = env.identifiers.iter().any(|identifier| {
Comment thread
robobun marked this conversation as resolved.
Resolved
(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
Expand Down Expand Up @@ -1940,14 +1956,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(
Expand Down Expand Up @@ -2318,6 +2339,56 @@ fn codegen_base_instruction_value(
// Function expression codegen
// =============================================================================

/// Not in upstream: the call of a method that PromoteUsedTemporaries read ahead of a statement, as
/// `Function.prototype.call.call(method, receiver, ...args)`. `method.call(receiver, ...args)` reads `call` of the
/// method, which a member call does not: a proxy or a method with a `call` of its own answers that read.
/// `Reflect.apply(method, receiver, [...args])` makes an array for each call, and is many times slower under
/// JavaScriptCore. A method that is not callable throws from `Function.prototype.call`, and the message does not
/// name the member.
Comment thread
robobun marked this conversation as resolved.
Resolved
fn codegen_explicit_receiver_call(
cx: &mut Context,
method: Expr,
receiver: &Place,
args: &[PlaceOrSpread],
loc: Loc,
) -> Result<Expr, CompilerError> {
// 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)?);
Comment thread
robobun marked this conversation as resolved.
Resolved
for arg in args {
call_args.push(codegen_argument(cx, arg)?);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
let mut target = Expr::init_identifier(function, loc);
for name in [b"prototype".as_slice(), b"call", b"call"] {
Comment thread
robobun marked this conversation as resolved.
Resolved
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<StoreStr>,
Expand Down
8 changes: 8 additions & 0 deletions src/react_compiler/hir/environment.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -121,6 +122,12 @@ 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<HashSet<StoreStr>>,

/// Not in upstream: the properties of the MethodCalls that PromoteUsedTemporaries gave a name, to read the
/// method ahead of a statement. Codegen prints such a call with its receiver as an argument. A property that
/// has its name as the declaration of a memo block is not in the set, and stays the invariant error that it
/// is upstream (facebook/react#34072).
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) explicit_receiver_calls: IdBitSet<IdentifierId>,
}

/// An outlined function entry, stored on Environment during compilation.
Expand Down Expand Up @@ -218,6 +225,7 @@ impl Environment {
default_mutating_hook: None,
outlined_functions: AstAlloc::vec(),
uid_known_names: None,
explicit_receiver_calls: IdBitSet::new(),
config,
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/react_compiler/observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
Expand Down
6 changes: 6 additions & 0 deletions src/react_compiler/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ pub trait Host {
self.new_generated(name)
}

/// Whether `name` is the global around the function being compiled: no scope from its body outward binds it.
/// The lookup records no use, so a caller can ask ahead of a decision not to print the name.
Comment thread
robobun marked this conversation as resolved.
Resolved
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);
}
Expand Down
Loading
Loading