Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
9 changes: 8 additions & 1 deletion src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
/// we always fold constant expressions.
pub should_fold_typescript_constant_expressions: bool,

/// Set while visiting the arguments of a macro call so `handle_identifier`
/// substitutes from `const_values` even when `features.inlining` is off.
/// Narrower than `should_fold_typescript_constant_expressions` so dynamic
/// `import()` / `require()` specifiers are not collapsed as a side effect.
pub is_inside_macro_arguments: bool,

pub emitted_namespace_vars: RefMap,
pub is_exported_inside_namespace: RefRefMap,
pub local_type_names: StringBoolMap,
Expand Down Expand Up @@ -1813,7 +1819,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
) -> Expr {
let ref_ = ident.ref_;

if self.options.features.inlining {
if self.options.features.inlining || self.is_inside_macro_arguments {
if let Some(replacement) = self.const_values.get(&ref_) {
let replacement = *replacement;
self.ignore_usage(ref_);
Expand Down Expand Up @@ -9141,6 +9147,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
parse_pass_symbol_uses: None,
has_commonjs_export_names: false,
should_fold_typescript_constant_expressions: false,
is_inside_macro_arguments: false,
emitted_namespace_vars: RefMap::default(),
is_exported_inside_namespace: Default::default(),
local_type_names: StringBoolMap::default(),
Expand Down
54 changes: 48 additions & 6 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.visit_decl(
decl,
was_anonymous_named_expr,
was_const && !is_after,
was_const,
is_after,
if Self::ALLOW_MACROS {
prev_macro_call_count != self.macro_call_count
} else {
Expand All @@ -429,7 +430,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let replacer = _ptr.get();
if !self.replace_decl_and_possibly_remove(decl, replacer) {
let is_after = self.vis_scope().is_after_const_local_prefix;
self.visit_decl(decl, false, was_const && !is_after, false);
self.visit_decl(decl, false, was_const, is_after, false);
} else {
continue 'outer;
}
Expand Down Expand Up @@ -540,20 +541,32 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
&mut self,
decl: &mut G::Decl,
was_anonymous_named_expr: bool,
could_be_const_value: bool,
was_const: bool,
is_after_const_local_prefix: bool,
could_be_macro: bool,
) {
let could_be_const_value = was_const && !is_after_const_local_prefix;
// Optionally preserve the name
match decl.binding.data {
BData::BIdentifier(id) => {
let id_ref = id.r#ref;
if could_be_const_value || (Self::ALLOW_MACROS && could_be_macro) {
// When the file imports macros, also track `const` literals that
// appear after the leading declaration prefix so they can be
// substituted into macro arguments. Visiting is top-down, so an
// entry only becomes visible to textually-later uses. Macro-
// derived values are only recorded for `const` bindings so a
// reassignable `let`/`var` never enters the map.
let track_for_macro_args =
Self::ALLOW_MACROS && was_const && self.macro_.refs.count() > 0;
let track_macro_result = Self::ALLOW_MACROS && was_const && could_be_macro;
if could_be_const_value || track_macro_result || track_for_macro_args {
if let Some(val) = decl.value {
if val.can_be_const_value() {
self.const_values.put(id_ref, val).expect("oom");
}
Comment thread
robobun marked this conversation as resolved.
}
} else {
}
if !could_be_const_value && !track_macro_result {
self.vis_scope().is_after_const_local_prefix = true;
}
// SAFETY: original_name is arena-owned, valid for 'a.
Expand All @@ -568,7 +581,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
BData::BObject(_) | BData::BArray(_) => {
if Self::ALLOW_MACROS {
if could_be_macro && let Some(value) = decl.value {
if was_const
&& could_be_macro
&& let Some(value) = decl.value
{
self.visit_binding_and_expr_for_macro(decl.binding, value);
}
}
Expand Down Expand Up @@ -1663,6 +1679,32 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

let symbol: &Symbol = &p.symbols[id.inner_index() as usize];

// Drop a `const` whose value was already substituted at
// every use via `const_values` (use count now 0). The
// "remove inlined constants" pass above only walks the
// leading declaration prefix, so a past-prefix const that
// `track_for_macro_args` recorded would otherwise be left
// as dead output. Presence in `const_values` already
// implies the binding was `const` and the initialiser is
// side-effect free (`can_be_const_value()`), so no
// separate kind check is needed after `select_local_kind`
// may have rewritten it.
if symbol.use_count_estimate == 0 && p.const_values.contains(&id) {
match local.decls.len_u32() {
1 => {
local.decls.clear();
let new_len = output.len() - 1;
output.truncate(new_len);
continue 'inner;
}
_ => {
let n = local.decls.len() - 1;
local.decls.truncate(n);
continue 'inner;
}
}
}

// Try to substitute the identifier with the initializer. This will
// fail if something with side effects is in between the declaration
// and the usage.
Expand Down
5 changes: 5 additions & 0 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// Restored manually below.
let old_should_fold_typescript_constant_expressions =
p.should_fold_typescript_constant_expressions;
let old_is_inside_macro_arguments = p.is_inside_macro_arguments;
let old_is_control_flow_dead = p.is_control_flow_dead;

// We want to forcefully fold constants inside of
Expand All @@ -1973,6 +1974,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.options.ignore_dce_annotations = true;
p.should_fold_typescript_constant_expressions = true;
}
if is_macro_ref {
p.is_inside_macro_arguments = true;
}

// When a value is targeted by `--drop`, it will be removed.
// The HMR APIs in `import.meta.hot` are implicitly dropped when HMR is disabled.
Expand Down Expand Up @@ -2003,6 +2007,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.options.ignore_dce_annotations = old_ce;
p.should_fold_typescript_constant_expressions =
old_should_fold_typescript_constant_expressions;
p.is_inside_macro_arguments = old_is_inside_macro_arguments;

if method_call_should_be_replaced_with_undefined {
p.is_control_flow_dead = old_is_control_flow_dead;
Expand Down
123 changes: 123 additions & 0 deletions src/js_parser_jsc/expr_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use bun_collections::VecExt;
use bun_core::{StackCheck, String as BunString, strings};
use bun_jsc::{JSGlobalObject, JSValue, JsError, bun_string_jsc};

use E::TemplateContents;

/// Map a `bun_jsc::JsError` into the AST-layer `ToJSError`. Orphan rules forbid
/// `impl From<JsError> for ToJSError` here (both foreign), so callers use
/// `.map_err(js_err)?` instead of bare `?`.
Expand Down Expand Up @@ -74,6 +76,7 @@ fn data_to_js_with_check(
ExprData::EInlinedEnum(inlined) => {
data_to_js_with_check(&inlined.value.data, global, stack_check)
}
ExprData::ETemplate(tmpl) => template_to_js(tmpl, global, stack_check),

ExprData::EIdentifier(_)
| ExprData::EImportIdentifier(_)
Expand All @@ -84,6 +87,126 @@ fn data_to_js_with_check(
}
}

fn append_e_string_utf16(s: &E::String, out: &mut Vec<u16>) {
if s.is_utf8() {
let bytes = s.slice8();
match strings::wtf8_to_utf16_alloc(bytes) {
Some(wide) => out.extend_from_slice(&wide),
None => out.extend(bytes.iter().map(|&b| u16::from(b))),
}
let mut next = s.next;
while let Some(part) = next {
let part = part.get();
match strings::wtf8_to_utf16_alloc(&part.data) {
Some(wide) => out.extend_from_slice(&wide),
None => out.extend(part.data.iter().map(|&b| u16::from(b))),
}
next = part.next;
}
} else {
out.extend_from_slice(s.slice16());
}
}

fn append_template_contents_utf16(
c: &TemplateContents,
out: &mut Vec<u16>,
) -> Result<(), ToJSError> {
match c {
TemplateContents::Cooked(s) => {
append_e_string_utf16(s, out);
Ok(())
}
// A non-tagged template never carries a raw (undefined-cooked) part.
TemplateContents::Raw(_) => Err(ToJSError::CannotConvertArgumentTypeToJS),
}
}

fn append_ascii_utf16(s: &[u8], out: &mut Vec<u16>) {
out.extend(s.iter().map(|&b| u16::from(b)));
}

fn append_template_part_value_utf16(
data: &ExprData,
out: &mut Vec<u16>,
stack_check: StackCheck,
) -> Result<(), ToJSError> {
if !stack_check.is_safe_to_recurse() {
return Err(ToJSError::CannotConvertArgumentTypeToJS);
}
match data {
ExprData::EString(s) => append_e_string_utf16(s, out),
ExprData::ENumber(n) => {
let v = n.value();
if v.is_nan() {
append_ascii_utf16(b"NaN", out);
} else if v.is_infinite() {
append_ascii_utf16(
if v.is_sign_negative() {
b"-Infinity"
} else {
b"Infinity"
},
out,
);
} else {
let mut buf = [0u8; 124];
let s = bun_core::fmt::FormatDouble::dtoa(&mut buf, v);
append_ascii_utf16(s, out);
}
}
ExprData::ENull(_) => append_ascii_utf16(b"null", out),
ExprData::EUndefined(_) => append_ascii_utf16(b"undefined", out),
ExprData::EBoolean(b) | ExprData::EBranchBoolean(b) => {
append_ascii_utf16(if b.value { b"true" } else { b"false" }, out)
}
ExprData::EInlinedEnum(inlined) => {
return append_template_part_value_utf16(&inlined.value.data, out, stack_check);
}
ExprData::ETemplate(inner) => {
if inner.tag.is_some() {
return Err(ToJSError::CannotConvertArgumentTypeToJS);
}
append_template_contents_utf16(&inner.head, out)?;
for part in inner.parts() {
append_template_part_value_utf16(&part.value.data, out, stack_check)?;
append_template_contents_utf16(&part.tail, out)?;
}
}
ExprData::EIdentifier(_)
| ExprData::EImportIdentifier(_)
| ExprData::EPrivateIdentifier(_)
| ExprData::ECommonjsExportIdentifier(_) => {
return Err(ToJSError::CannotConvertIdentifierToJS);
}
_ => return Err(ToJSError::CannotConvertArgumentTypeToJS),
}
Ok(())
}

pub(crate) fn template_to_js(
tmpl: &E::Template,
global: &JSGlobalObject,
stack_check: StackCheck,
) -> Result<JSValue, ToJSError> {
if tmpl.tag.is_some() {
return Err(ToJSError::CannotConvertArgumentTypeToJS);
}
let mut out: Vec<u16> = Vec::new();
append_template_contents_utf16(&tmpl.head, &mut out)?;
for part in tmpl.parts() {
append_template_part_value_utf16(&part.value.data, &mut out, stack_check)?;
append_template_contents_utf16(&part.tail, &mut out)?;
}
if out.is_empty() {
let emp = BunString::EMPTY;
return bun_string_jsc::to_js(&emp, global).map_err(js_err);
}
let (mut s, chars) = BunString::create_uninitialized_utf16(out.len());
chars.copy_from_slice(&out);
bun_string_jsc::transfer_to_js(&mut s, global).map_err(js_err)
}

pub(crate) fn array_to_js(
this: &E::Array,
global: &JSGlobalObject,
Expand Down
Loading
Loading