Skip to content
10 changes: 10 additions & 0 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,16 @@ impl ImportIdentifier {
pub const fn was_originally_identifier(self) -> bool {
self.ref_.user_bit(0)
}

/// Set for the React Compiler on the `ns.foo` of a call `ns.foo()`, which a folded `(0, ns.foo)()` is not.
#[inline]
pub const fn is_member_call_target(self) -> bool {
self.ref_.user_bit(1)
}
#[inline]
pub fn set_is_member_call_target(&mut self, v: bool) {
self.ref_.set_user_bit(1, v);
}
}

/// This is a dot expression on exports, such as `exports.<ref>`. It is given
Expand Down
18 changes: 16 additions & 2 deletions src/js_parser/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ fn e_string_eql_bytes(s: &E::EString, other: &[u8]) -> bool {
}
}

/// The import item of `ns.member`, marked where a call has it as its target, for the React Compiler.
fn member_call_target(is_member_call: bool, mut item: Expr) -> Expr {
if is_member_call && let js_ast::ExprData::EImportIdentifier(import) = &mut item.data {
import.set_is_member_call_target(true);
}
item
}

// File-split mixin pattern: a direct `impl P` block.

impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> {
Expand Down Expand Up @@ -233,6 +241,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
.put(name, new_item)
.expect("unreachable");
p.is_import_item.insert(new_ref, ());
if p.options.features.react_compiler.is_enabled() {
p.react_compiler_namespace_of_item.insert(new_ref, id.ref_);
}

let symbol = &mut p.symbols[new_ref.inner_index() as usize];

Expand Down Expand Up @@ -283,7 +294,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.symbols[ref_.inner_index() as usize].set_called_as_method(true);
}

return Some(
let is_member_call = identifier_opts.is_call_target()
&& p.options.features.react_compiler.is_enabled();
return Some(member_call_target(
is_member_call,
p.handle_identifier(
name_loc,
E::Identifier {
Expand All @@ -302,7 +316,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// sure the value of "this" is preserved.
.with_was_originally_identifier(false),
),
);
));
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,8 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
pub(crate) react_compiler_result: Option<bun_react_compiler::CompileResult>,
/// Visiting a function that is handed to the React Compiler, which re-declares its locals as new symbols.
pub(crate) react_compiler_may_replace_body: bool,
/// The namespace of each import item that `maybe_rewrite_property_access` makes of `ns.member`, for the React Compiler.
pub(crate) react_compiler_namespace_of_item: HashMap<Ref, Ref>,

/// only applicable when `.options.features.server_components` is
/// configured to wrap exports. populated before visit pass starts.
Expand Down Expand Up @@ -9764,6 +9766,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
react_compiler_pending: None,
react_compiler_result: None,
react_compiler_may_replace_body: false,
react_compiler_namespace_of_item: Default::default(),
server_components_wrap_ref: Ref::NONE,
jest: Jest::default(),
import_records_for_current_part: BumpVec::new_in(arena),
Expand Down
23 changes: 18 additions & 5 deletions src/js_parser/react_compiler_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,24 @@ impl<'a, const TS: bool, const SCAN_ONLY: bool> bun_react_compiler::Host
fn import_records(&self) -> &[js_ast::ImportRecord<'_>] {
self.p.import_records.items()
}
fn namespace_of_import_item(&self, item: js_ast::Ref) -> Option<js_ast::Ref> {
let symbol = self.p.symbols.get(item.inner_index() as usize)?;
let name = symbol.original_name.slice();
let mut all = self.p.import_items_for_namespace.iter();
all.find_map(|(namespace, items)| (items.get(name)?.ref_ == item).then_some(*namespace))
fn namespace_member(&self, item: js_ast::Ref) -> Option<bun_react_compiler::NamespaceMember> {
let p = &*self.p;
let symbol = p.symbols.get(item.inner_index() as usize)?;
if symbol.import_item_status != js_ast::ImportItemStatus::Generated {
return None;
}
let namespace = *p.react_compiler_namespace_of_item.get(&item)?;
let import_record = match &symbol.namespace_alias {
Some(alias) => Some(alias.import_record_index),
None => (p.imports_to_convert_from_require.iter())
.find(|import| import.namespace.ref_ == namespace)
.map(|import| import.import_record_id),
};
Some(bun_react_compiler::NamespaceMember {
namespace,
name: symbol.original_name,
import_record,
})
}
fn source(&self) -> &[u8] {
&self.p.source.contents
Expand Down
25 changes: 25 additions & 0 deletions src/react_compiler/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,28 @@ overlap in arity, so lowering tells them apart by the callee
The callee is not an operand of the HIR `JsxExpression`: codegen resolves the
classic factory again (`Host::jsx_classic_factory`). Lowering rejects a factory
that is a local of the function, because the compiler would drop it as unused.

In a bundle the visit pass also replaces a member of a namespace, `ns.member`,
with an import item: a symbol of its own that the linker binds to the export,
so that no namespace object is made. That is the case for `import * as ns`, for
a module-level local that holds what `require()` or `import()` gave, and for a
`require()` of a package that the bundler unwraps. To upstream a member is not
a named import: `React.createElement` has a type, two namespaces can have a
member of one name, and a read of a member is not a constant. So lowering reads
the item as the member it stands for (`Host::namespace_member`): a `LoadGlobal`
of the namespace, a `PropertyLoad`, and a `MethodCall` where the source has
`ns.member()`. The visit pass can leave the same item of `(0, ns.member)()`,
which is a call of a value, so it marks the item of a method call
(`E::ImportIdentifier::is_member_call_target`). Codegen prints the item again
for the pair (`Environment::namespace_members`) and never the namespace, which
the source did not read as a value: where upstream compares the namespace for a
memo block, the block here depends on the member.

The namespace has the type that upstream gives it, which is that of `React` for
an import with that name. What `require()` or `import()` gave is a local to
upstream and has no type. A hook of React is React's under any name and as such
a local (`react_is_known_by_its_module`). `ns["member"]` is the same item, so
its key is a name to the compiler, as it is not to upstream. `ns?.member` is no
item to the visit pass. A member of any other import, `X.name`, is no item
either: codegen prints it anew, without the mark that the visit pass gave it
(`E::Dot::is_import_property_use`), so the linker keeps all of what `X` names.
58 changes: 53 additions & 5 deletions src/react_compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,27 @@ impl<'a, 'h> Context<'a, 'h> {
self.env.record_error(detail)
}

/// Whether `id` is the temporary that lowering loaded the `ns` of an `ns.member` in. It is not printed.
fn is_namespace_of_member(&self, id: IdentifierId) -> bool {
let members = &self.env.namespace_members;
!members.is_empty()
&& members.contains_key(&self.env.identifiers[id.0 as usize].declaration_id)
}

/// The import item that the parser's visit pass made of `ns.member`, for the temporary of that `ns`.
#[inline]
fn namespace_member(&mut self, namespace: IdentifierId) -> Option<Expr> {
if self.env.namespace_members.is_empty() {
return None;
}
let declaration = self.env.identifiers[namespace.0 as usize].declaration_id;
let item = *self.env.namespace_members.get(&declaration)?;
if let ExprData::EImportIdentifier(import) = item.data {
self.cg.host.record_usage(import.ref_);
}
Some(item)
}

fn ref_for_id(&mut self, id: IdentifierId) -> Result<Ref, CompilerError> {
if let Some(&r) = self.cg.id_to_ref.get(id) {
self.cg.host.record_usage(r);
Expand Down Expand Up @@ -1428,6 +1449,12 @@ fn codegen_instruction_nullable(
InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => {
return Ok(None);
}
InstructionValue::LoadGlobal { .. }
if (instr.lvalue.as_ref())
.is_some_and(|lvalue| cx.is_namespace_of_member(lvalue.identifier)) =>
{
return Ok(None);
}
InstructionValue::Debugger { .. } => {
return Ok(Some(Stmt {
loc: convert_loc(instr.loc),
Expand Down Expand Up @@ -1943,7 +1970,11 @@ fn codegen_base_instruction_value(
..
} => {
let member_expr = codegen_place_to_expression(cx, property)?;
if !matches!(member_expr.data, ExprData::EDot(_) | ExprData::EIndex(_)) {
// An import identifier is the item of `ns.member`: see `Context::namespace_member`.
if !matches!(
member_expr.data,
ExprData::EDot(_) | ExprData::EIndex(_) | ExprData::EImportIdentifier(_)
) {
return Err(method_call_property_err(property.loc, member_expr));
Comment thread
robobun marked this conversation as resolved.
Resolved
}
let arguments = codegen_arguments(cx, args)?;
Expand Down Expand Up @@ -1997,6 +2028,9 @@ fn codegen_base_instruction_value(
InstructionValue::PropertyLoad {
object, property, ..
} => {
if let Some(item) = cx.namespace_member(object.identifier) {
return Ok(item);
}
let obj = codegen_place_to_expression(cx, object)?;
Ok(property_access_expr(obj, property, loc, None))
}
Expand Down Expand Up @@ -3041,6 +3075,12 @@ fn codegen_place_to_expression(cx: &mut Context, place: &Place) -> Result<Expr,
place.loc,
));
}
if cx.is_namespace_of_member(place.identifier) {
return Err(invariant_err(
"[Codegen] Expected the namespace of an import item to be read for its member only",
place.loc,
));
}
let (r, _ident_loc) = convert_identifier(cx, place.identifier)?;
Ok(Expr::init_identifier(r, convert_loc(place.loc)))
}
Expand Down Expand Up @@ -3082,11 +3122,19 @@ fn codegen_dependency(
cx: &mut Context,
dep: &crate::hir::ReactiveScopeDependency,
) -> Result<Expr, CompilerError> {
let (r, loc) = convert_identifier(cx, dep.identifier)?;
let mut object = Expr::init_identifier(r, loc);
if !dep.path.is_empty() {
let mut path = &dep.path[..];
let loc = convert_loc(cx.env.identifiers[dep.identifier.0 as usize].loc);
let mut object = match cx.namespace_member(dep.identifier) {
// Not in upstream, which compares the namespace where the path is empty: here that is its member.
Some(item) => {
path = path.get(1..).unwrap_or_default();
item
}
None => Expr::init_identifier(cx.ref_for_id(dep.identifier)?, loc),
};
if !path.is_empty() {
let has_optional = dep.path.iter().any(|p| p.optional);
for path_entry in &dep.path {
for path_entry in path {
let chain = if has_optional {
Some(if path_entry.optional {
OptionalChain::Start
Expand Down
6 changes: 5 additions & 1 deletion src/react_compiler/hir/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ pub struct Environment {
// Binding name -> next `_N` to try. Valid because an error in a nested function ends all lowering.
pub(crate) binding_names: HashMap<StoreStr, u32>,

// Not in upstream. The import item of each `ns.member`, by the temporary of its `ns`: codegen prints the item.
pub(crate) namespace_members: HashMap<DeclarationId, bun_ast::Expr>,

// 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
Expand Down Expand Up @@ -203,6 +206,7 @@ impl Environment {
hook_guard_name: None,
renames: AstAlloc::vec(),
binding_names: HashMap::default(),
namespace_members: HashMap::default(),
reference_node_ids: HashSet::default(),
hoisted_identifiers: HashSet::default(),
validate_preserve_existing_memoization_guarantees: config
Expand Down Expand Up @@ -707,7 +711,7 @@ impl Environment {
module_type
}

fn is_known_react_module(&self, module_name: &[u8]) -> bool {
pub(crate) fn is_known_react_module(&self, module_name: &[u8]) -> bool {
module_name.eq_ignore_ascii_case(b"react") || module_name.eq_ignore_ascii_case(b"react-dom")
}

Expand Down
2 changes: 1 addition & 1 deletion src/react_compiler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ pub use compile_result::{CompileDiagnostic, CompileOutput};
pub use lint::{Categories, Linted, lint_function};
pub use options::ReactCompilerOptions;
pub use program::{
CompileResult, Host, JsxImportKind, PendingCompile, ReactCompilerState,
CompileResult, Host, JsxImportKind, NamespaceMember, PendingCompile, ReactCompilerState,
collect_import_bindings, finish, has_module_scope_opt_out, maybe_compile_pending,
};
21 changes: 8 additions & 13 deletions src/react_compiler/lowering/build_hir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use bun_ast::{self as ast, E, Expr, G, Loc, OpCode, Ref, StoreRef, symbol};
use super::function::{lower_function_to_value, lower_object_method};
use super::helpers::{
AssignmentStyle, MemberProperty, build_temporary_place, expression_type_name, lower_arguments,
lower_assignment, lower_expression_to_temporary, lower_identifier, lower_member_expression,
lower_object_property_key, lower_optional_call_expression, lower_optional_member_expression,
lower_value_to_temporary,
lower_assignment, lower_expression_to_temporary, lower_identifier, lower_member_callee,
lower_member_expression, lower_namespace_member, lower_object_property_key,
lower_optional_call_expression, lower_optional_member_expression, lower_value_to_temporary,
};
use super::jsx::lower_jsx_call;
use crate::lowering::FunctionNode;
Expand All @@ -29,7 +29,10 @@ pub(crate) fn lower_expression(
match &expr.data {
Data::EObjectJSON(_) | Data::EArrayJSON(_) => Ok(unsupported_node("JSONValue", loc)),
Data::EIdentifier(ident) => lower_identifier_reference(builder, ident.ref_, loc),
Data::EImportIdentifier(ident) => lower_identifier_reference(builder, ident.ref_, loc),
Data::EImportIdentifier(ident) => match builder.namespace_member(ident.ref_) {
Some(member) => Ok(lower_namespace_member(builder, expr, member, loc)?.value),
None => lower_identifier_reference(builder, ident.ref_, loc),
},
Data::ENull(_) => Ok(InstructionValue::Primitive {
value: PrimitiveValue::Null,
loc,
Expand Down Expand Up @@ -71,15 +74,7 @@ pub(crate) fn lower_expression(
return lower_optional_call_expression(builder, expr);
}
// Check if callee is a MemberExpression => MethodCall
let is_member = matches!(
&call.target.data,
Data::EDot(d) if d.optional_chain.is_none()
) || matches!(
&call.target.data,
Data::EIndex(i) if i.optional_chain.is_none()
);
if is_member {
let lowered = lower_member_expression(builder, &call.target, None)?;
if let Some(lowered) = lower_member_callee(builder, &call.target)? {
let property = lower_value_to_temporary(builder, lowered.value)?;
let args = AstAlloc::vec_from_iter(lower_arguments(builder, &call.args)?);
Ok(InstructionValue::MethodCall {
Expand Down
Loading
Loading