Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
348588e
test: constants as macro arguments in any statement position
robobun Oct 6, 2026
76ef639
js_parser: let a macro argument name any const above the call, in eve…
robobun Oct 6, 2026
740088e
test: macro argument limits, ordinary-code guards and the joined-stri…
robobun Oct 6, 2026
6296ace
docs: state the limits of a macro argument
robobun Oct 6, 2026
4715e60
js_parser: decide the macro-file record on the const path only
robobun Oct 6, 2026
35a0c86
js_parser: read the const prefix flag in visit_decl, for a const only
robobun Oct 6, 2026
60365e0
js_parser: keep the macro-file work off the let, var and ordinary-cal…
robobun Oct 6, 2026
1f6781d
Bump the transpiler cache version for the require.resolve() reader; t…
robobun Oct 6, 2026
349b724
js_parser: reduce the change to the macro argument table
robobun Oct 6, 2026
d8e9986
js_parser: shorten the comments of the macro argument table
robobun Oct 6, 2026
1c339cf
js_parser: visit macro arguments after the drop check
robobun Oct 7, 2026
2dae6ac
js_parser: write the macro argument tables from one non-generic function
robobun Oct 7, 2026
d258a5a
Merge remote-tracking branch 'origin/main' into robobun/c1c590d7/macr…
robobun Oct 7, 2026
14ae224
js_parser: record the default of a macro binding, and gate a direct e…
robobun Oct 7, 2026
ce139fb
js_parser: no direct eval gate for macro arguments; transpiler cache …
robobun Oct 7, 2026
33de1a1
Merge remote-tracking branch 'origin/main' into robobun/c1c590d7/macr…
robobun Oct 10, 2026
821c03a
js_parser: the macro runtime does not use the macro argument table
robobun Oct 10, 2026
a450032
test: feature() with a folded flag name under bun run
robobun Oct 10, 2026
b1ca02a
ci: retrigger
robobun Oct 10, 2026
7a3fd5c
test: load the macro argument cases in the test process
robobun Oct 10, 2026
e08dfa8
test: run the in-process macro argument cases one at a time
robobun Oct 10, 2026
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
4 changes: 3 additions & 1 deletion docs/bundler/macros.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export function howLong() {
}
```

This outputs:
With the minify syntax option enabled, this outputs:

```js
function howLong() {
Expand All @@ -219,6 +219,8 @@ function howLong() {
export { howLong };
```

Bun accepts a `const` as a macro argument when you declare the `const` above the call and its initializer is a number, a string, a boolean, `null`, `undefined`, or a macro call that returns one of these. Inside the argument, Bun also joins these values in a template literal or with `+`. Write the expression inside the argument and not in the initializer of a `const`. Bun does not yet join a piece that contains a non-ASCII character. To pass an object or an array that another macro returns, call that macro inside the argument.

## Examples

### Embed latest git commit hash
Expand Down
33 changes: 32 additions & 1 deletion src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ pub struct VisitArgsOpts<'a> {
#[derive(Clone, Copy)]
pub struct VisitDeclOpts {
pub(crate) was_anonymous_named_expr: bool,
pub(crate) could_be_const_value: bool,
pub(crate) was_const: bool,
pub(crate) could_be_macro: bool,
}

Expand Down Expand Up @@ -1762,12 +1762,43 @@ pub type ImportItemForNamespaceMap = StringArrayHashMap<LocRef>;

pub struct MacroState<'a> {
pub(crate) refs: MacroRefs<'a>,
/// The const table of macro arguments. `visit_macro_arguments` swaps it with `const_values`.
pub(crate) consts: bun_ast::ast_result::ConstValuesMap,
/// Inside that swap, where `features.inlining` is forced on.
pub(crate) in_args: bool,
/// `features.inlining` before the swap.
pub(crate) inlining_outside_args: bool,
}

impl<'a> MacroState<'a> {
pub(crate) fn init() -> MacroState<'a> {
MacroState {
refs: MacroRefs::default(),
consts: Default::default(),
in_args: false,
inlining_outside_args: false,
}
}

/// Inside the swap the two tables trade places. Out of line: one copy for every `P`.
#[inline(never)]
pub(crate) fn put_const(
&mut self,
const_values: &mut bun_ast::ast_result::ConstValuesMap,
r#ref: Ref,
for_inliner: Option<Expr>,
for_macro_args: Option<Expr>,
) {
let (inliner, macro_args) = if self.in_args {
(&mut self.consts, const_values)
} else {
(const_values, &mut self.consts)
};
if let Some(value) = for_inliner {
inliner.put(r#ref, value).expect("oom");
}
if let Some(value) = for_macro_args {
macro_args.put(r#ref, value).expect("oom");
}
}
}
Expand Down
172 changes: 148 additions & 24 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use bun_ast::{
Stmt, StmtData, Symbol,
};
use bun_collections::VecExt;
use bun_core::strings;
// `parser::SideEffects` is a stub enum without the assoc fns; the real
// `should_keep_stmt_in_dead_control_flow` lives on `ast::side_effects::SideEffects`.
use crate::scan::scan_side_effects::SideEffects;
Expand Down Expand Up @@ -588,12 +589,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool>
}
}

let is_after = self.vis_scope().is_after_const_local_prefix;
self.visit_decl(
decl,
VisitDeclOpts {
was_anonymous_named_expr,
could_be_const_value: was_const && !is_after,
was_const,
could_be_macro: if Self::ALLOW_MACROS {
prev_macro_call_count != self.macro_call_count
} else {
Expand All @@ -616,12 +616,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool>
// which is not mutated during the visit pass.
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,
VisitDeclOpts {
was_anonymous_named_expr: false,
could_be_const_value: was_const && !is_after,
was_const,
could_be_macro: false,
},
);
Expand Down Expand Up @@ -649,7 +648,119 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool>
j
}

pub(crate) fn visit_binding_and_expr_for_macro(&mut self, binding: Binding, expr: Expr) {
#[inline]
fn imports_macro(&self) -> bool {
Self::ALLOW_MACROS && self.macro_.refs.count() > 0
}

fn inlining_outside_macro_args(&self) -> bool {
if self.macro_.in_args {
self.macro_.inlining_outside_args
} else {
self.options.features.inlining
}
}

/// What a macro argument gets for a `const`: a primitive, or a string as one piece.
fn macro_argument_value(&mut self, value: Expr) -> Option<Expr> {
match value.data {
ExprData::ENumber(_)
| ExprData::EBoolean(_)
| ExprData::EBranchBoolean(_)
| ExprData::ENull(_)
| ExprData::EUndefined(_)
| ExprData::EInlinedEnum(_) => Some(value),
ExprData::EString(string) => match string.flattened(self.arena) {
E::Flattened::Borrowed(_) => Some(value),
E::Flattened::Owned(flat) => Some(self.new_expr(flat, value.loc)),
},
_ => None,
}
}

/// A macro's string result is UTF-16, which no fold joins. An ASCII one gets an 8-bit copy.
pub(crate) fn macro_string_for_join(&mut self, expr: Expr) -> Expr {
if let ExprData::EString(string) = expr.data {
if string.is_utf16 && strings::first_non_ascii16(string.slice16()).is_none() {
let units = string.slice16();
let ascii = self.arena.alloc_slice_fill_default::<u8>(units.len());
strings::copy_u16_into_u8(ascii, units);
return self.new_expr(E::EString::init(ascii), expr.loc);
}
}
expr
}

/// `inlinable`: the value also goes to the inliner's table, by the rule of `const_values`.
fn put_const_value_for_macros(
&mut self,
r#ref: Ref,
value: Expr,
inlinable: bool,
is_const: bool,
) {
let mirror = inlinable && self.inlining_outside_macro_args();
let for_macro_args = if mirror {
// The arguments of a macro call see what the inliner puts in ordinary code.
Some(value)
} else if is_const {
self.macro_argument_value(value)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
None
};
self.macro_.put_const(
&mut self.const_values,
r#ref,
inlinable.then_some(value),
for_macro_args,
);
}

/// `visit_decl` for an identifier binding in a file that imports a macro.
#[cold]
fn record_const_value_for_macros(
&mut self,
r#ref: Ref,
value: Option<Expr>,
was_const: bool,
could_be_macro: bool,
) {
let Some(value) = value else { return };
// The inliner takes a `const` of the leading declaration run, and any macro result.
let inlinable = (could_be_macro || !self.vis_scope().is_after_const_local_prefix)
&& value.can_be_const_value();
self.put_const_value_for_macros(r#ref, value, inlinable, was_const);
}

/// A destructured binding of a macro result. The inliner takes it only under inlining.
fn record_macro_result(
&mut self,
r#ref: Ref,
mut value: Expr,
is_const: bool,
default: Option<Expr>,
) {
// An `undefined` gives the binding its default. Only a `const` keeps that value.
if let (Some(default), ExprData::EUndefined(_)) = (default, value.data) {
if !is_const {
return;
}
let Some(default) = self.macro_argument_value(default) else {
return;
};
value = default;
}
let inlinable = self.inlining_outside_macro_args();
self.put_const_value_for_macros(r#ref, value, inlinable, is_const);
}

pub(crate) fn visit_binding_and_expr_for_macro(
&mut self,
binding: Binding,
expr: Expr,
is_const: bool,
default: Option<Expr>,
) {
match binding.data {
BData::BObject(bound_object) => {
let bound_object = bound_object.get();
Expand All @@ -669,16 +780,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool>
self.visit_binding_and_expr_for_macro(
property.value,
query.expr,
is_const,
property.default_value,
);
}
_ => {
if self.options.features.inlining {
if let BData::BIdentifier(id) = property.value.data
{
self.const_values
.put(id.r#ref, query.expr)
.expect("oom");
}
if let BData::BIdentifier(id) = property.value.data {
self.record_macro_result(
id.r#ref,
query.expr,
is_const,
property.default_value,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
}
}
}
Expand Down Expand Up @@ -710,34 +823,45 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool>
*child_expr = self.new_expr(E::Missing {}, expr.loc);
continue;
}
self.visit_binding_and_expr_for_macro(item.binding, *child_expr);
self.visit_binding_and_expr_for_macro(
item.binding,
*child_expr,
is_const,
item.default_value,
);
}
}
}
}
BData::BIdentifier(id) => {
if self.options.features.inlining {
self.const_values.put(id.r#ref, expr).expect("oom");
}
}
BData::BIdentifier(id) => self.record_macro_result(id.r#ref, expr, is_const, default),
BData::BMissing(_) => {}
}
}

pub(crate) fn visit_decl(&mut self, decl: &mut G::Decl, opts: VisitDeclOpts) {
let VisitDeclOpts {
was_anonymous_named_expr,
could_be_const_value,
was_const,
could_be_macro,
} = opts;
// 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) {
if let Some(val) = decl.value {
if val.can_be_const_value() {
self.const_values.put(id_ref, val).expect("oom");
if was_const || (Self::ALLOW_MACROS && could_be_macro) {
if self.imports_macro() {
self.record_const_value_for_macros(
id_ref,
decl.value,
was_const,
could_be_macro,
);
} else if !self.vis_scope().is_after_const_local_prefix {
// Past the leading declaration run, a use can run before the initializer.
if let Some(val) = decl.value {
if val.can_be_const_value() {
self.const_values.put(id_ref, val).expect("oom");
}
}
}
} else {
Expand All @@ -756,7 +880,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool>
BData::BObject(_) | BData::BArray(_) => {
if Self::ALLOW_MACROS {
if could_be_macro && let Some(value) = decl.value {
self.visit_binding_and_expr_for_macro(decl.binding, value);
self.visit_binding_and_expr_for_macro(decl.binding, value, was_const, None);
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/js_parser/visit/visit_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ impl BinaryExpressionVisitor {
}
Op::Code::BinAdd => {
if p.should_fold_typescript_constant_expressions {
if p.macro_.in_args {
e_.left = p.macro_string_for_join(e_.left);
e_.right = p.macro_string_for_join(e_.right);
}
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
return p.new_expr(E::Number::new(vals[0] + vals[1]), v.loc);
Expand Down
Loading
Loading