Skip to content
1 change: 1 addition & 0 deletions src/js_parser/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
&& !identifier_opts.is_delete_target()
&& identifier_opts.assign_target() == js_ast::AssignTarget::None
&& !identifier_opts.is_call_target()
&& !identifier_opts.is_template_tag()
{
let prop: &G::Property = &obj.properties.slice()[0];
if let (Some(value), Some(key)) = (prop.value, prop.key) {
Expand Down
12 changes: 12 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
// syntactic constructs as appropriate.
pub(crate) stmt_expr_value: js_ast::ExprData,
pub(crate) call_target: js_ast::ExprData,
pub(crate) template_tag: js_ast::ExprData,
Comment thread
robobun marked this conversation as resolved.
pub(crate) delete_target: js_ast::ExprData,
pub(crate) loop_body: js_ast::StmtData,
pub(crate) module_scope: js_ast::StoreRef<js_ast::Scope>,
Expand Down Expand Up @@ -2646,6 +2647,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
js_ast::ExprData::ETemplate(mut e) => {
if let Some(tag) = e.tag.as_mut() {
// Don't substitute something into a template tag that could change "this"
match replacement.data {
js_ast::ExprData::EDot(_) | js_ast::ExprData::EIndex(_) => {
if matches!(tag.data, js_ast::ExprData::EIdentifier(id) if id.ref_.eql(r#ref))
{
break 'outer;
}
}
_ => {}
}
match self.substitute_single_use_symbol_in_expr(
*tag,
r#ref,
Expand Down Expand Up @@ -8712,6 +8723,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
allow_in: true,

call_target: null_expr_data(),
template_tag: null_expr_data(),
delete_target: null_expr_data(),
stmt_expr_value: null_expr_data(),
loop_body: null_stmt_data(),
Expand Down
16 changes: 13 additions & 3 deletions src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -993,9 +993,9 @@ impl AsyncPrefixExpression {
}

// Packed u8 — assign_target:u2, is_delete_target:b1,
// was_originally_identifier:b1, is_call_target:b1, _padding:u3 (LSB-first).
// Not all-bool (assign_target is a 2-bit enum), so per PORTING.md we use a
// transparent u8 with manual shift accessors.
// was_originally_identifier:b1, is_call_target:b1, is_template_tag:b1,
// _padding:u2 (LSB-first). Not all-bool (assign_target is a 2-bit enum),
// so per PORTING.md we use a transparent u8 with manual shift accessors.
Comment thread
robobun marked this conversation as resolved.
#[repr(transparent)]
#[derive(Clone, Copy, Default, PartialEq, Eq)]
pub struct IdentifierOpts(u8);
Expand All @@ -1005,6 +1005,7 @@ impl IdentifierOpts {
const IS_DELETE_TARGET: u8 = 1 << 2;
const WAS_ORIGINALLY_IDENTIFIER: u8 = 1 << 3;
const IS_CALL_TARGET: u8 = 1 << 4;
const IS_TEMPLATE_TAG: u8 = 1 << 5;

#[inline]
pub(crate) const fn assign_target(self) -> js_ast::AssignTarget {
Expand Down Expand Up @@ -1032,6 +1033,10 @@ impl IdentifierOpts {
pub(crate) const fn is_call_target(self) -> bool {
self.0 & Self::IS_CALL_TARGET != 0
}
#[inline]
pub(crate) const fn is_template_tag(self) -> bool {
self.0 & Self::IS_TEMPLATE_TAG != 0
}

// Builder-style helpers (this stays a packed u8 rather than a
// named-field struct).
Expand Down Expand Up @@ -1059,6 +1064,11 @@ impl IdentifierOpts {
self.0 = (self.0 & !Self::IS_CALL_TARGET) | ((v as u8) << 4);
self
}
#[inline]
pub(crate) const fn with_is_template_tag(mut self, v: bool) -> Self {
self.0 = (self.0 & !Self::IS_TEMPLATE_TAG) | ((v as u8) << 5);
self
}
}

pub(crate) fn statement_cares_about_scope(stmt: &Stmt) -> bool {
Expand Down
45 changes: 38 additions & 7 deletions src/js_parser/visit/visit_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ pub struct BinaryExpressionVisitor {

/// Input for visiting the left child
pub(crate) left_in: ExprIn,

/// Captured in `check_and_prepare` (before visiting `left`) so a nested
/// call/tagged-template inside `left` can't clobber the pointer match.
Comment thread
robobun marked this conversation as resolved.
pub(crate) is_call_target: bool,
pub(crate) is_template_tag: bool,
}

impl BinaryExpressionVisitor {
Expand All @@ -112,11 +117,10 @@ impl BinaryExpressionVisitor {
// invariant is encapsulated there. The borrow is on the `v.e` field
// only, so `v.loc` reads below split-borrow cleanly.
let e_handle: StoreRef<E::Binary> = v.e;
let e_ptr: *mut E::Binary = e_handle.as_ptr();
let is_call_target = v.is_call_target;
let is_template_tag = v.is_template_tag;
let e_ = &mut *v.e;

let is_call_target =
matches!(p.call_target, ExprData::EBinary(ptr) if core::ptr::eq(ptr.as_ptr(), e_ptr));
let was_anonymous_named_expr = e_.right.is_anonymous_named();
let prev_decorator_class_name = p.decorator_class_name;

Expand Down Expand Up @@ -225,7 +229,9 @@ impl BinaryExpressionVisitor {
} else {
// The left operand has no side effects, but we need to preserve
// the comma operator semantics when used as a call target
if is_call_target && e_.right.has_value_for_this_in_call() {
if (is_call_target || is_template_tag)
&& e_.right.has_value_for_this_in_call()
{
// Keep the comma expression to strip "this" binding
e_.left = Expr {
data: prefill::data::ZERO,
Expand Down Expand Up @@ -369,7 +375,10 @@ impl BinaryExpressionVisitor {
// "(null ?? fn)()" => "fn()"
// "(null ?? this.fn)" => "this.fn"
// "(null ?? this.fn)()" => "(0, this.fn)()"
if is_call_target && e_.right.has_value_for_this_in_call() {
// "(null ?? this.fn)`x`" => "(0, this.fn)`x`"
if (is_call_target || is_template_tag)
&& e_.right.has_value_for_this_in_call()
{
return Expr::join_with_comma(
Expr {
data: ExprData::ENumber(E::Number::new(0.0)),
Expand All @@ -392,7 +401,9 @@ impl BinaryExpressionVisitor {
// "(0 || fn)()" => "fn()"
// "(0 || this.fn)" => "this.fn"
// "(0 || this.fn)()" => "(0, this.fn)()"
if is_call_target && e_.right.has_value_for_this_in_call() {
// "(0 || this.fn)`x`" => "(0, this.fn)`x`"
if (is_call_target || is_template_tag) && e_.right.has_value_for_this_in_call()
{
return Expr::join_with_comma(
Expr {
data: prefill::data::ZERO,
Expand All @@ -414,7 +425,10 @@ impl BinaryExpressionVisitor {
// "(1 && fn)()" => "fn()"
// "(1 && this.fn)" => "this.fn"
// "(1 && this.fn)()" => "(0, this.fn)()"
if is_call_target && e_.right.has_value_for_this_in_call() {
// "(1 && this.fn)`x`" => "(0, this.fn)`x`"
if (is_call_target || is_template_tag)
&& e_.right.has_value_for_this_in_call()
{
return Expr::join_with_comma(
Expr {
data: prefill::data::ZERO,
Comment thread
robobun marked this conversation as resolved.
Expand Down Expand Up @@ -711,6 +725,17 @@ impl BinaryExpressionVisitor {
if let Some(obj) = dot.target.data.e_object() {
if obj.properties.len_u32() == 0 {
if dot.name != b"__proto__" {
if (is_call_target || is_template_tag)
&& e_.right.has_value_for_this_in_call()
{
return Expr::join_with_comma(
Expr {
data: prefill::data::ZERO,
loc: e_.left.loc,
},
e_.right,
);
}
return e_.right;
}
}
Expand Down Expand Up @@ -777,6 +802,12 @@ impl BinaryExpressionVisitor {
_ => {}
}

let e_ptr: *mut E::Binary = e_handle.as_ptr();
v.is_call_target =
matches!(p.call_target, ExprData::EBinary(ptr) if core::ptr::eq(ptr.as_ptr(), e_ptr));
v.is_template_tag =
matches!(p.template_tag, ExprData::EBinary(ptr) if core::ptr::eq(ptr.as_ptr(), e_ptr));

v.left_in = ExprIn {
assign_target: Op::Code::binary_assign_target(e_.op),
..ExprIn::default()
Expand Down
83 changes: 49 additions & 34 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,8 +692,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let expr = *e;
let _ = in_;
let mut e_ = expr.data.e_template().expect("infallible: variant checked");
if e_.tag.is_some() {
p.visit_expr(e_.tag.as_mut().unwrap());
if let Some(tag) = e_.tag.as_mut() {
p.template_tag = tag.data;
p.visit_expr(tag);
}

// Visit the interpolation values before the macro dispatch below: its
Expand Down Expand Up @@ -820,6 +821,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
e: e_,
loc: expr.loc,
left_in: ExprIn::default(),
is_call_target: false,
is_template_tag: false,
};

// Everything uses a single stack to reduce allocation overhead. This stack
Expand Down Expand Up @@ -867,6 +870,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
e: left_binary.unwrap(),
loc: left.loc,
left_in: ExprIn::default(),
is_call_target: false,
is_template_tag: false,
};
}

Expand All @@ -885,6 +890,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let expr = *e;
let mut e_ = expr.data.e_index().expect("infallible: variant checked");
let is_call_target = matches!(p.call_target, Data::EIndex(ct) if core::ptr::eq(&raw const *e_, &raw const *ct));
let is_template_tag = matches!(p.template_tag, Data::EIndex(tt) if core::ptr::eq(&raw const *e_, &raw const *tt));
let is_delete_target = matches!(p.delete_target, Data::EIndex(dt) if core::ptr::eq(&raw const *e_, &raw const *dt));

// "a['b']" => "a.b"
Expand All @@ -905,6 +911,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if is_call_target {
p.call_target = dot.data;
}
if is_template_tag {
p.template_tag = dot.data;
}
if is_delete_target {
p.delete_target = dot.data;
}
Expand Down Expand Up @@ -1020,6 +1029,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if is_call_target {
p.call_target = dot.data;
}
if is_template_tag {
p.template_tag = dot.data;
}
if is_delete_target {
p.delete_target = dot.data;
}
Expand All @@ -1041,7 +1053,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
unwrapped.loc,
IdentifierOpts::default()
.with_is_call_target(is_call_target)
// .is_template_tag = is_template_tag,
.with_is_template_tag(is_template_tag)
.with_is_delete_target(is_delete_target)
.with_assign_target(in_.assign_target),
) {
Expand All @@ -1057,7 +1069,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let target = e_.target.unwrap_inlined();
let index = e_.index.unwrap_inlined();

if p.options.features.minify_syntax {
// `[obj.m][0]` / `"s"[n]` are property references into a temporary;
// folding them to a value in tag position would rebind `this`.
Comment thread
robobun marked this conversation as resolved.
if p.options.features.minify_syntax && !is_template_tag {
Comment thread
robobun marked this conversation as resolved.
if let Some(number) = index.data.as_e_number() {
if number.value() >= 0.0
&& number.value() < (usize::MAX as f64)
Expand Down Expand Up @@ -1335,6 +1349,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mut e_ = expr.data.e_dot().expect("infallible: variant checked");
let is_delete_target = matches!(p.delete_target, Data::EDot(dt) if core::ptr::eq(&raw const *e_, &raw const *dt));
let is_call_target = matches!(p.call_target, Data::EDot(ct) if core::ptr::eq(&raw const *e_, &raw const *ct));
let is_template_tag = matches!(p.template_tag, Data::EDot(tt) if core::ptr::eq(&raw const *e_, &raw const *tt));

// `p.define: &'a Define` is `Copy`; hoist so the `dots.get` borrow is
// tied to `'a`, not `&*p`, and `&mut self` helpers below can be called
Expand Down Expand Up @@ -1427,9 +1442,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
e_.name_loc,
IdentifierOpts::default()
.with_is_call_target(is_call_target)
.with_is_template_tag(is_template_tag)
.with_assign_target(in_.assign_target)
.with_is_delete_target(is_delete_target),
// .is_template_tag = p.template_tag != null,
) {
*e = _expr;
return;
Expand Down Expand Up @@ -1460,6 +1475,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mut e_ = e.data.e_if().expect("infallible: variant checked");
let is_call_target =
matches!(p.call_target, Data::EIf(ct) if core::ptr::eq(&raw const *e_, &raw const *ct));
let is_template_tag = matches!(p.template_tag, Data::EIf(tt) if core::ptr::eq(&raw const *e_, &raw const *tt));

let prev_in_branch = p.in_branch_condition;
p.in_branch_condition = true;
Expand All @@ -1483,24 +1499,23 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.visit_expr(&mut e_.no);
p.is_control_flow_dead = old;

if side_effects.side_effects == SideEffects::CouldHaveSideEffects {
*e = SideEffects::simplify_unused_expr(p, e_.test)
.unwrap_or_else(|| p.new_expr(E::Missing {}, e_.test.loc))
.join_with_comma(e_.yes);
return;
}

// "(1 ? fn : 2)()" => "fn()"
// "(1 ? this.fn : 2)" => "this.fn"
// "(1 ? this.fn : 2)()" => "(0, this.fn)()"
if is_call_target && e_.yes.has_value_for_this_in_call() {
*e = p
.new_expr(E::Number::new(0.0), e_.test.loc)
.join_with_comma(e_.yes);
return;
// "(1 ? this.fn : 2)`x`" => "(0, this.fn)`x`"
let mut left = if side_effects.side_effects == SideEffects::CouldHaveSideEffects {
SideEffects::simplify_unused_expr(p, e_.test)
.unwrap_or_else(|| p.new_expr(E::Missing {}, e_.test.loc))
} else {
p.new_expr(E::Missing {}, e_.test.loc)
};
if left.is_missing()
&& (is_call_target || is_template_tag)
&& e_.yes.has_value_for_this_in_call()
{
left = p.new_expr(E::Number::new(0.0), e_.test.loc);
}

*e = e_.yes;
*e = left.join_with_comma(e_.yes);
return;
} else {
// "false ? dead : live"
Expand All @@ -1511,23 +1526,23 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.visit_expr(&mut e_.no);

// "(a, false) ? b : c" => "a, c"
if side_effects.side_effects == SideEffects::CouldHaveSideEffects {
*e = SideEffects::simplify_unused_expr(p, e_.test)
// "(0 ? 1 : fn)()" => "fn()"
// "(0 ? 1 : this.fn)" => "this.fn"
// "(0 ? 1 : this.fn)()" => "(0, this.fn)()"
// "(0 ? 1 : this.fn)`x`" => "(0, this.fn)`x`"
Comment thread
robobun marked this conversation as resolved.
let mut left = if side_effects.side_effects == SideEffects::CouldHaveSideEffects {
SideEffects::simplify_unused_expr(p, e_.test)
.unwrap_or_else(|| p.new_expr(E::Missing {}, e_.test.loc))
.join_with_comma(e_.no);
return;
}

// "(1 ? fn : 2)()" => "fn()"
// "(1 ? this.fn : 2)" => "this.fn"
// "(1 ? this.fn : 2)()" => "(0, this.fn)()"
if is_call_target && e_.no.has_value_for_this_in_call() {
*e = p
.new_expr(E::Number::new(0.0), e_.test.loc)
.join_with_comma(e_.no);
return;
} else {
p.new_expr(E::Missing {}, e_.test.loc)
};
if left.is_missing()
&& (is_call_target || is_template_tag)
&& e_.no.has_value_for_this_in_call()
{
left = p.new_expr(E::Number::new(0.0), e_.test.loc);
}
*e = e_.no;
*e = left.join_with_comma(e_.no);
return;
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ bun_core::declare_scope!(cache, visible);
/// bindings from the compiled bytecode after the module-loader rewrite, so the
/// record no longer carries them; blobs written in the old numbering must not
/// be read back.
const EXPECTED_VERSION: u32 = 24;
/// Version 25: `p.template_tag` is tracked during the visit pass, so wrapper
/// folds (`(0, a.b)`, `cond ? a.b : x`, `a.b ?? x`, `[a.b][0]`, `{f:a.b}.f`)
/// no longer strip the indirection when the result is a tagged-template tag.
Comment thread
robobun marked this conversation as resolved.
const EXPECTED_VERSION: u32 = 25;

/// 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
Loading
Loading