feat(codegen): keep arrow function PIFEs (#12353)

PIFE is the abbreviation of "Possibly-Invoked Function Expressions". It is a function expression wrapped with a parenthesized expression.
PIFEs annotate functions that are likely to be invoked eagerly. When v8 encounters such expressions, it compiles them eagerly (rather than compiling it later). See [v8's blog post](https://v8.dev/blog/preparser#pife) for more details.

The blog post only mentions regular FunctionExpressions, but ArrowFunctions are also supported. The cases that will be eagerly compiled are:

- when `!` comes before function literals ([code](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3730-L3733)) (e.g. `!function(){}`)
- when a function expression is wrapped with parenthesis ([code](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2243-L2248)) (e.g. `(function () {})`, `(async function () {})`)
- when an arrow function is wrapped with parenthesis ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2173-L2174), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2232-L2259), [code3](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3297-L3298)) (e.g. `console.log((() => {}))`)
- when `()` or ``` `` ``` (tagged templates) comes after function literals (only in some cases) ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3987-L3992), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L4344-L4348)) (e.g. `~function(){}()`, ```~function(){}`` ```)
- when [explicit compile hints](https://v8.dev/blog/explicit-compile-hints) are used ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser.cc#L2717-L2720), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L5070-L5073)) (e.g. `//# allFunctionsCalledOnLoad`)

Keeping PIFEs as-is in the code generation will unlock optimizations like https://github.com/rolldown/rolldown/pull/5319.

_Note: this PR only implements arrow function PIFEs for now._
This commit is contained in:
sapphi-red committed 2025-07-20 13:13:12 +00:00
1 parent 998c67bf95
commit 0920e9827e
20 files changed
+101 -28

No files matched your search

+8
View File
@@ -1935,6 +1935,14 @@ pub struct ArrowFunctionExpression<'a> {
#[builder(default)]
#[estree(skip)]
pub pure: bool,
#[builder(default)]
#[estree(skip)]
/// `true` if the function should be marked as "Possibly-Invoked Function Expression" (PIFE).
///
/// References:
/// - v8 blog post about PIFEs: <https://v8.dev/blog/preparser#pife>
/// - introduced PR: <https://github.com/oxc-project/oxc/pull/12353>
pub pife: bool,
}
/// Generator Function Definitions
@@ -617,7 +617,7 @@ const _: () = {
assert!(offset_of!(FunctionBody, directives) == 8);
assert!(offset_of!(FunctionBody, statements) == 32);
// Padding: 1 bytes
// Padding: 0 bytes
assert!(size_of::<ArrowFunctionExpression>() == 48);
assert!(align_of::<ArrowFunctionExpression>() == 8);
assert!(offset_of!(ArrowFunctionExpression, span) == 0);
@@ -629,6 +629,7 @@ const _: () = {
assert!(offset_of!(ArrowFunctionExpression, body) == 32);
assert!(offset_of!(ArrowFunctionExpression, scope_id) == 40);
assert!(offset_of!(ArrowFunctionExpression, pure) == 46);
assert!(offset_of!(ArrowFunctionExpression, pife) == 47);
// Padding: 7 bytes
assert!(size_of::<YieldExpression>() == 32);
@@ -2207,7 +2208,7 @@ const _: () = {
assert!(offset_of!(FunctionBody, directives) == 8);
assert!(offset_of!(FunctionBody, statements) == 24);
// Padding: 1 bytes
// Padding: 0 bytes
assert!(size_of::<ArrowFunctionExpression>() == 32);
assert!(align_of::<ArrowFunctionExpression>() == 4);
assert!(offset_of!(ArrowFunctionExpression, span) == 0);
@@ -2219,6 +2220,7 @@ const _: () = {
assert!(offset_of!(ArrowFunctionExpression, body) == 20);
assert!(offset_of!(ArrowFunctionExpression, scope_id) == 24);
assert!(offset_of!(ArrowFunctionExpression, pure) == 30);
assert!(offset_of!(ArrowFunctionExpression, pife) == 31);
// Padding: 3 bytes
assert!(size_of::<YieldExpression>() == 20);
+20 -10
View File
@@ -364,7 +364,7 @@ impl<'a> AstBuilder<'a> {
))
}
/// Build an [`Expression::ArrowFunctionExpression`] with `scope_id` and `pure`.
/// Build an [`Expression::ArrowFunctionExpression`] with `scope_id` and `pure` and `pife`.
///
/// This node contains an [`ArrowFunctionExpression`] that will be stored in the memory arena.
///
@@ -378,8 +378,9 @@ impl<'a> AstBuilder<'a> {
/// * `body`: See `expression` for whether this arrow expression returns an expression.
/// * `scope_id`
/// * `pure`: `true` if the function is marked with a `/*#__NO_SIDE_EFFECTS__*/` comment
/// * `pife`: `true` if the function should be marked as "Possibly-Invoked Function Expression" (PIFE).
#[inline]
pub fn expression_arrow_function_with_scope_id_and_pure<T1, T2, T3, T4>(
pub fn expression_arrow_function_with_scope_id_and_pure_and_pife<T1, T2, T3, T4>(
self,
span: Span,
expression: bool,
@@ -390,6 +391,7 @@ impl<'a> AstBuilder<'a> {
body: T4,
scope_id: ScopeId,
pure: bool,
pife: bool,
) -> Expression<'a>
where
T1: IntoIn<'a, Option<Box<'a, TSTypeParameterDeclaration<'a>>>>,
@@ -398,7 +400,7 @@ impl<'a> AstBuilder<'a> {
T4: IntoIn<'a, Box<'a, FunctionBody<'a>>>,
{
Expression::ArrowFunctionExpression(
self.alloc_arrow_function_expression_with_scope_id_and_pure(
self.alloc_arrow_function_expression_with_scope_id_and_pure_and_pife(
span,
expression,
r#async,
@@ -408,6 +410,7 @@ impl<'a> AstBuilder<'a> {
body,
scope_id,
pure,
pife,
),
)
}
@@ -6067,6 +6070,7 @@ impl<'a> AstBuilder<'a> {
body: body.into_in(self.allocator),
scope_id: Default::default(),
pure: Default::default(),
pife: Default::default(),
}
}
@@ -6114,10 +6118,10 @@ impl<'a> AstBuilder<'a> {
)
}
/// Build an [`ArrowFunctionExpression`] with `scope_id` and `pure`.
/// Build an [`ArrowFunctionExpression`] with `scope_id` and `pure` and `pife`.
///
/// If you want the built node to be allocated in the memory arena,
/// use [`AstBuilder::alloc_arrow_function_expression_with_scope_id_and_pure`] instead.
/// use [`AstBuilder::alloc_arrow_function_expression_with_scope_id_and_pure_and_pife`] instead.
///
/// ## Parameters
/// * `span`: The [`Span`] covering this node
@@ -6129,8 +6133,9 @@ impl<'a> AstBuilder<'a> {
/// * `body`: See `expression` for whether this arrow expression returns an expression.
/// * `scope_id`
/// * `pure`: `true` if the function is marked with a `/*#__NO_SIDE_EFFECTS__*/` comment
/// * `pife`: `true` if the function should be marked as "Possibly-Invoked Function Expression" (PIFE).
#[inline]
pub fn arrow_function_expression_with_scope_id_and_pure<T1, T2, T3, T4>(
pub fn arrow_function_expression_with_scope_id_and_pure_and_pife<T1, T2, T3, T4>(
self,
span: Span,
expression: bool,
@@ -6141,6 +6146,7 @@ impl<'a> AstBuilder<'a> {
body: T4,
scope_id: ScopeId,
pure: bool,
pife: bool,
) -> ArrowFunctionExpression<'a>
where
T1: IntoIn<'a, Option<Box<'a, TSTypeParameterDeclaration<'a>>>>,
@@ -6158,13 +6164,14 @@ impl<'a> AstBuilder<'a> {
body: body.into_in(self.allocator),
scope_id: Cell::new(Some(scope_id)),
pure,
pife,
}
}
/// Build an [`ArrowFunctionExpression`] with `scope_id` and `pure`, and store it in the memory arena.
/// Build an [`ArrowFunctionExpression`] with `scope_id` and `pure` and `pife`, and store it in the memory arena.
///
/// Returns a [`Box`] containing the newly-allocated node.
/// If you want a stack-allocated node, use [`AstBuilder::arrow_function_expression_with_scope_id_and_pure`] instead.
/// If you want a stack-allocated node, use [`AstBuilder::arrow_function_expression_with_scope_id_and_pure_and_pife`] instead.
///
/// ## Parameters
/// * `span`: The [`Span`] covering this node
@@ -6176,8 +6183,9 @@ impl<'a> AstBuilder<'a> {
/// * `body`: See `expression` for whether this arrow expression returns an expression.
/// * `scope_id`
/// * `pure`: `true` if the function is marked with a `/*#__NO_SIDE_EFFECTS__*/` comment
/// * `pife`: `true` if the function should be marked as "Possibly-Invoked Function Expression" (PIFE).
#[inline]
pub fn alloc_arrow_function_expression_with_scope_id_and_pure<T1, T2, T3, T4>(
pub fn alloc_arrow_function_expression_with_scope_id_and_pure_and_pife<T1, T2, T3, T4>(
self,
span: Span,
expression: bool,
@@ -6188,6 +6196,7 @@ impl<'a> AstBuilder<'a> {
body: T4,
scope_id: ScopeId,
pure: bool,
pife: bool,
) -> Box<'a, ArrowFunctionExpression<'a>>
where
T1: IntoIn<'a, Option<Box<'a, TSTypeParameterDeclaration<'a>>>>,
@@ -6196,7 +6205,7 @@ impl<'a> AstBuilder<'a> {
T4: IntoIn<'a, Box<'a, FunctionBody<'a>>>,
{
Box::new_in(
self.arrow_function_expression_with_scope_id_and_pure(
self.arrow_function_expression_with_scope_id_and_pure_and_pife(
span,
expression,
r#async,
@@ -6206,6 +6215,7 @@ impl<'a> AstBuilder<'a> {
body,
scope_id,
pure,
pife,
),
self.allocator,
)
@@ -3677,6 +3677,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for ArrowFunctionExpression<'_> {
body: CloneIn::clone_in(&self.body, allocator),
scope_id: Default::default(),
pure: CloneIn::clone_in(&self.pure, allocator),
pife: CloneIn::clone_in(&self.pife, allocator),
}
}
@@ -3691,6 +3692,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for ArrowFunctionExpression<'_> {
body: CloneIn::clone_in_with_semantic_ids(&self.body, allocator),
scope_id: CloneIn::clone_in_with_semantic_ids(&self.scope_id, allocator),
pure: CloneIn::clone_in_with_semantic_ids(&self.pure, allocator),
pife: CloneIn::clone_in_with_semantic_ids(&self.pife, allocator),
}
}
}
@@ -1107,6 +1107,7 @@ impl ContentEq for ArrowFunctionExpression<'_> {
&& ContentEq::content_eq(&self.return_type, &other.return_type)
&& ContentEq::content_eq(&self.body, &other.body)
&& ContentEq::content_eq(&self.pure, &other.pure)
&& ContentEq::content_eq(&self.pife, &other.pife)
}
}
@@ -1140,6 +1140,7 @@ impl<'a> Dummy<'a> for ArrowFunctionExpression<'a> {
body: Dummy::dummy(allocator),
scope_id: Dummy::dummy(allocator),
pure: Dummy::dummy(allocator),
pife: Dummy::dummy(allocator),
}
}
}
@@ -278,7 +278,7 @@ pub static STRUCTS: phf::Map<&'static str, StructDetails> = ::phf::Map {
("TemplateElement", StructDetails { field_order: None }),
(
"ArrowFunctionExpression",
StructDetails { field_order: Some(&[0, 6, 7, 1, 2, 3, 4, 5, 8]) },
StructDetails { field_order: Some(&[0, 6, 7, 1, 2, 3, 4, 5, 8, 9]) },
),
("NumericLiteral", StructDetails { field_order: None }),
("TSTupleType", StructDetails { field_order: None }),
+1 -1
View File
@@ -1633,7 +1633,7 @@ impl Gen for PropertyKey<'_> {
impl GenExpr for ArrowFunctionExpression<'_> {
fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) {
p.wrap(precedence >= Precedence::Assign, |p| {
p.wrap(precedence >= Precedence::Assign || self.pife, |p| {
if self.r#async {
p.print_space_before_identifier();
p.add_source_mapping(self.span);
@@ -498,7 +498,7 @@ fn test_for() {
test("for (x(a in b);;);", "for (x(a in b);;);\n");
test("for (x[a in b];;);", "for (x[a in b];;);\n");
test("for (x?.[a in b];;);", "for (x?.[a in b];;);\n");
test("for ((x => a in b);;);", "for ((x) => (a in b);;);\n");
test("for ((x => a in b);;);", "for (((x) => (a in b));;);\n");
// Make sure for-of loops with commas are wrapped in parentheses
test("for (let a in b, c);", "for (let a in b, c);\n");
+9 -1
View File
@@ -251,7 +251,7 @@ fn r#yield() {
fn arrow() {
test_minify("x => a, b", "x=>a,b;");
test_minify("x => (a, b)", "x=>(a,b);");
test_minify("x => (a => b)", "x=>a=>b;");
test_minify("x => (a => b)", "x=>(a=>b);");
test_minify("x => y => a, b", "x=>y=>a,b;");
test_minify("x => y => (a = b)", "x=>y=>a=b;");
test_minify("x => y => z => a = b, c", "x=>y=>z=>a=b,c;");
@@ -415,6 +415,14 @@ fn pure_comment() {
test_same("false || /* @__PURE__ */ noEffect();\n");
}
#[test]
fn pife() {
test_same("foo((() => 0));\n");
test_minify_same("foo((()=>0));");
test_same("(() => 0)();\n");
test_minify_same("(()=>0)();");
}
// followup from https://github.com/oxc-project/oxc/pull/6422
#[test]
fn in_expr_in_sequence_in_for_loop_init() {
@@ -7551,6 +7551,11 @@ impl<'a> AstNode<'a, ArrowFunctionExpression<'a>> {
self.inner.pure
}
#[inline]
pub fn pife(&self) -> bool {
self.inner.pife
}
pub fn format_leading_comments(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
format_leading_comments(self.span()).fmt(f)
}
@@ -977,7 +977,7 @@ fn test_remove_dead_expr_nullish_related() {
test("x('' ?? 1)", "x('');");
test("x(/./ ?? 1)", "x(/./);");
test("x({} ?? 1)", "x({});");
test("x((() => {}) ?? 1)", "x(() => {});");
test("x((() => {}) ?? 1)", "x((() => {}));");
test("x(class {} ?? 1)", "x(class {});");
test("x(function() {} ?? 1)", "x(function() {});");
test("x(null ?? 1)", "x(1);");
+5 -1
View File
@@ -226,12 +226,16 @@ impl<'a> ParserImpl<'a> {
self.expect(Kind::RParen);
// ParenthesizedExpression is from acorn --preserveParens
let expression = if expressions.len() == 1 {
let mut expression = if expressions.len() == 1 {
expressions.remove(0)
} else {
self.ast.expression_sequence(expr_span, expressions)
};
if let Expression::ArrowFunctionExpression(arrow_expr) = &mut expression {
arrow_expr.pife = true;
}
if self.options.preserve_parens {
self.ast.expression_parenthesized(self.end_span(span), expression)
} else {
@@ -954,8 +954,8 @@ impl<'a> ArrowFunctionConverter<'a> {
);
let statements = ctx.ast.vec1(ctx.ast.statement_expression(SPAN, init));
let body = ctx.ast.function_body(SPAN, ctx.ast.vec(), statements);
let init = ctx.ast.expression_arrow_function_with_scope_id_and_pure(
SPAN, true, false, NONE, params, NONE, body, scope_id, false,
let init = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife(
SPAN, true, false, NONE, params, NONE, body, scope_id, false, false,
);
ctx.ast.variable_declarator(
SPAN,
@@ -160,7 +160,7 @@ impl<'a> NullishCoalescingOperator<'a, '_> {
ctx.ast.vec(),
ctx.ast.vec1(ctx.ast.statement_expression(SPAN, new_expr)),
);
let arrow_function = ctx.ast.expression_arrow_function_with_scope_id_and_pure(
let arrow_function = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife(
SPAN,
true,
false,
@@ -170,6 +170,7 @@ impl<'a> NullishCoalescingOperator<'a, '_> {
body,
current_scope_id,
false,
false,
);
// `(x) => x;` -> `((x) => x)();`
new_expr = ctx.ast.expression_call(SPAN, arrow_function, NONE, ctx.ast.vec(), false);
@@ -303,7 +303,7 @@ impl<'a> ClassProperties<'a, '_> {
let body = ctx.ast.vec1(ctx.ast.statement_expression(SPAN, body_exprs));
// `(..._args) => (super(..._args), <inits>, this)`
let super_func = ctx.ast.expression_arrow_function_with_scope_id_and_pure(
let super_func = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife(
SPAN,
true,
false,
@@ -321,6 +321,7 @@ impl<'a> ClassProperties<'a, '_> {
ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), body),
super_func_scope_id,
false,
false,
);
// `var _super = (..._args) => ( ... );`
@@ -69,8 +69,8 @@ pub fn wrap_statements_in_arrow_function_iife<'a>(
let kind = FormalParameterKind::ArrowFormalParameters;
let params = ctx.ast.alloc_formal_parameters(SPAN, kind, ctx.ast.vec(), NONE);
let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), stmts);
let arrow = ctx.ast.expression_arrow_function_with_scope_id_and_pure(
SPAN, false, false, NONE, params, NONE, body, scope_id, false,
let arrow = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife(
SPAN, false, false, NONE, params, NONE, body, scope_id, false, false,
);
ctx.ast.expression_call(span, arrow, NONE, ctx.ast.vec(), false)
}
@@ -7332,6 +7332,8 @@ pub(crate) const OFFSET_ARROW_FUNCTION_EXPRESSION_SCOPE_ID: usize =
offset_of!(ArrowFunctionExpression, scope_id);
pub(crate) const OFFSET_ARROW_FUNCTION_EXPRESSION_PURE: usize =
offset_of!(ArrowFunctionExpression, pure);
pub(crate) const OFFSET_ARROW_FUNCTION_EXPRESSION_PIFE: usize =
offset_of!(ArrowFunctionExpression, pife);
#[repr(transparent)]
#[derive(Clone, Copy, Debug)]
@@ -7401,6 +7403,13 @@ impl<'a, 't> ArrowFunctionExpressionWithoutTypeParameters<'a, 't> {
&*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PURE) as *const bool)
}
}
#[inline]
pub fn pife(self) -> &'t bool {
unsafe {
&*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PIFE) as *const bool)
}
}
}
impl<'a, 't> GetAddress for ArrowFunctionExpressionWithoutTypeParameters<'a, 't> {
@@ -7478,6 +7487,13 @@ impl<'a, 't> ArrowFunctionExpressionWithoutParams<'a, 't> {
&*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PURE) as *const bool)
}
}
#[inline]
pub fn pife(self) -> &'t bool {
unsafe {
&*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PIFE) as *const bool)
}
}
}
impl<'a, 't> GetAddress for ArrowFunctionExpressionWithoutParams<'a, 't> {
@@ -7555,6 +7571,13 @@ impl<'a, 't> ArrowFunctionExpressionWithoutReturnType<'a, 't> {
&*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PURE) as *const bool)
}
}
#[inline]
pub fn pife(self) -> &'t bool {
unsafe {
&*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PIFE) as *const bool)
}
}
}
impl<'a, 't> GetAddress for ArrowFunctionExpressionWithoutReturnType<'a, 't> {
@@ -7632,6 +7655,13 @@ impl<'a, 't> ArrowFunctionExpressionWithoutBody<'a, 't> {
&*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PURE) as *const bool)
}
}
#[inline]
pub fn pife(self) -> &'t bool {
unsafe {
&*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PIFE) as *const bool)
}
}
}
impl<'a, 't> GetAddress for ArrowFunctionExpressionWithoutBody<'a, 't> {
@@ -1,4 +1,4 @@
() => {
(() => {
var _a;
return (_a = a) !== null && _a !== void 0 ? _a : b;
};
});
@@ -1,7 +1,7 @@
while (item) {
var _s = $RefreshSig$();
_s((item) => {
_s(((item) => {
_s();
useFoo();
}, "useFoo{}", true)(item);
}), "useFoo{}", true)(item);
}