perf(linter/plugins): create AST nodes with Object.create instead of __proto__ literals (#27816)

## Summary

oxlint's raw transfer deserializer creates every AST node as an object
literal with `__proto__: NodeProto` (for the lazy `loc` getter). V8
creates such object literals on a slow path: it builds a plain object,
then changes its prototype with a runtime call.

This PR changes the raw transfer codegen (`LocFieldAdder` in
`tasks/ast_tools/src/generators/raw_transfer.rs`) to create nodes with
`Object.create(NodeProto)` followed by property assignments:

```js
// `main`
node = (parent = { __proto__: NodeProto, type: "CallExpression", callee: null, /* ... */ });

// This PR
node = (parent = (node$0 = Object.create(NodeProto), node$0.type = "CallExpression", node$0.callee = null, /* ... */ node$0));
```

Nodes keep the same prototype, own properties, property order and
attributes. `napi/parser`'s deserializers are unaffected.

## Benchmark

Deserialization time when linting
[vscode](https://github.com/microsoft/vscode) @ `3db52ff` (15,122 files,
53.4M AST nodes) with a single JS rule which visits `DebuggerStatement`
(to make sure deserialization isn't eliminated as dead code). Apple M4
Pro, Node 26.5.0, release build.

Measured by timing each [`deserializeProgramOnly` call in
`initAst`](https://github.com/oxc-project/oxc/blob/6fe4c1b9117a35561fb40b843da92a3bdbe2bff3/apps/oxlint/src-js/plugins/source_code.ts#L86)
with `performance.now()` (local instrumentation, not part of this PR),
summed over all 15,086 deserialized files. Medians of 5 interleaved
runs, after a warmup run.

| | Deserialize |
|---|--:|
| `main` | 4.49 s |
| This PR | 0.91 s (**4.9×** faster) |


---

AI disclosure: this PR was written with AI assistance (Claude Code).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Charles Wang authored and GitHub committed 2026-10-29 01:14:18 +00:00
1 parent 2683225190
commit 481b638b70
2 files changed
+2870 -2408

No files matched your search

File diff suppressed because it is too large. Load diff
+282 -23
View File
@@ -1,6 +1,6 @@
//! Generator for raw transfer deserializer. //! Generator for raw transfer deserializer.
use std::{borrow::Cow, fmt::Debug, str}; use std::{borrow::Cow, fmt::Debug, mem, str};
use cow_utils::CowUtils; use cow_utils::CowUtils;
use lazy_regex::{Captures, Lazy, Regex, lazy_regex, regex::Replacer}; use lazy_regex::{Captures, Lazy, Regex, lazy_regex, regex::Replacer};
@@ -8,16 +8,19 @@ use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use oxc_allocator::Allocator; use oxc_allocator::{Allocator, ReplaceWith, Vec as ArenaVec};
use oxc_ast::{ use oxc_ast::{
ast::{ ast::{
Expression, LogicalOperator, ObjectExpression, ObjectPropertyKind, Program, PropertyKey, Argument, AssignmentOperator, AssignmentTarget, BindingIdentifier, BindingPattern,
PropertyKind, Expression, Function, IdentifierName, LogicalExpression, LogicalOperator, ObjectExpression,
ObjectProperty, ObjectPropertyKind, Program, PropertyKey, PropertyKind, Statement,
VariableDeclarationKind, VariableDeclarator,
}, },
builder::AstBuilder, builder::AstBuilder,
}; };
use oxc_ast_visit::{VisitMut, walk_mut}; use oxc_ast_visit::{VisitMut, walk_mut};
use oxc_span::SPAN; use oxc_span::SPAN;
use oxc_syntax::scope::ScopeFlags;
use crate::{ use crate::{
ALLOCATOR_CRATE_PATH, Generator, NAPI_PARSER_PACKAGE_PATH, OXLINT_APP_PATH, ALLOCATOR_CRATE_PATH, Generator, NAPI_PARSER_PACKAGE_PATH, OXLINT_APP_PATH,
@@ -1817,23 +1820,268 @@ fn get_constants(schema: &Schema) -> Constants {
} }
} }
/// Visitor to add `__proto__` field to all objects with `range` field in all deserialize functions, /// Visitor to make all objects with `range` field in all deserialize functions have `NodeProto` as prototype,
/// to give access to `loc`. /// to give access to `loc`.
/// ///
/// Each object literal is replaced with a sequence expression which creates the object with
/// `Object.create(NodeProto)` and assigns its properties, using a temp var:
///
/// ```js
/// // Object literal
/// { type: 'X', foo, ...(RANGE && { range: [start, end] }) }
/// // Replaced with
/// (node$0 = Object.create(NodeProto), node$0.type = 'X', node$0.foo = foo,
/// RANGE && (node$0.range = [start, end]), node$0)
/// ```
///
/// V8 creates objects this way much faster than with object literals containing `__proto__`.
/// The sequence expression creates the object and then evaluates property values in the same order
/// as the object literal does, and produces an identical object: same prototype, same own properties
/// in same order with same attributes.
///
/// Getters are defined with `Object.defineProperty`, with the same attributes as a getter in an object literal
/// (enumerable, configurable).
///
/// Temp vars are declared at the start of each function. Each level of nested object literals uses
/// a separate temp var (`node$0`, `node$1`, ...).
///
/// Works on AST pre-minification. /// Works on AST pre-minification.
struct LocFieldAdder<'a> { struct LocFieldAdder<'a> {
ast: AstBuilder<'a>, ast: AstBuilder<'a>,
allocator: &'a Allocator,
/// Nesting depth of object literals being converted
depth: usize,
/// Number of temp vars required in current function. `None` if not in a function.
temp_count: Option<usize>,
} }
impl<'a> LocFieldAdder<'a> { impl<'a> LocFieldAdder<'a> {
fn new(allocator: &'a Allocator) -> Self { fn new(allocator: &'a Allocator) -> Self {
Self { ast: AstBuilder::new(allocator) } Self { ast: AstBuilder::new(allocator), allocator, depth: 0, temp_count: None }
}
/// Create sequence expression which creates object with `NodeProto` as prototype,
/// and assigns properties of `obj_expr` to it, using temp var `temp`.
fn create_node(&self, obj_expr: ObjectExpression<'a>, temp: &'a str) -> Expression<'a> {
let mut exprs = ArenaVec::new_in(&self.ast);
// `node$0 = Object.create(NodeProto)`
let callee = Expression::new_static_member_expression(
SPAN,
Expression::new_identifier(SPAN, "Object", &self.ast),
IdentifierName::new(SPAN, "create", &self.ast),
false,
&self.ast,
);
let proto = Argument::from(Expression::new_identifier(SPAN, "NodeProto", &self.ast));
let object_create = Expression::new_call_expression(
SPAN,
callee,
None,
ArenaVec::from_iter_in([proto], &self.ast),
false,
&self.ast,
);
let target = AssignmentTarget::new_assignment_target_identifier(SPAN, temp, &self.ast);
exprs.push(Expression::new_assignment_expression(
SPAN,
AssignmentOperator::Assign,
target,
object_create,
&self.ast,
));
for prop in obj_expr.properties {
match prop {
ObjectPropertyKind::ObjectProperty(prop) => exprs.push(self.define(prop, temp)),
ObjectPropertyKind::SpreadProperty(spread) => {
// `...(cond && { a, b })` -> `cond && (node$0.a = a, node$0.b = b)`
let Expression::LogicalExpression(logical_expr) =
spread.unbox().argument.into_inner_expression()
else {
panic!("Unsupported spread in object literal");
};
let LogicalExpression { left, operator, right, .. } = logical_expr.unbox();
let Expression::ObjectExpression(obj_expr) = right.into_inner_expression()
else {
panic!("Unsupported spread in object literal");
};
assert!(
operator == LogicalOperator::And,
"Unsupported spread in object literal"
);
let mut assignments = ArenaVec::new_in(&self.ast);
for prop in obj_expr.unbox().properties {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
panic!("Unsupported spread in object literal");
};
assignments.push(self.define(prop, temp));
}
let right = if assignments.len() == 1 {
assignments.pop().unwrap()
} else {
Expression::new_sequence_expression(SPAN, assignments, &self.ast)
};
exprs.push(Expression::new_logical_expression(
SPAN,
left,
LogicalOperator::And,
right,
&self.ast,
));
}
}
}
exprs.push(Expression::new_identifier(SPAN, temp, &self.ast));
Expression::new_sequence_expression(SPAN, exprs, &self.ast)
}
/// Create expression which defines property `prop` on temp var `temp`.
///
/// * `foo: value` -> `node$0.foo = value`
/// * `get foo() {}` ->
/// `Object.defineProperty(node$0, 'foo', { get() {}, enumerable: true, configurable: true })`
fn define(
&self,
prop: oxc_allocator::Box<'a, ObjectProperty<'a>>,
temp: &'a str,
) -> Expression<'a> {
assert!(!prop.computed && !prop.method, "Unsupported property in object literal");
let PropertyKey::StaticIdentifier(key) = &prop.key else {
panic!("Unsupported property in object literal");
};
let key_name = key.name.as_str();
// Assigning these would hit a getter / setter on prototype chain, instead of defining a property
assert!(!matches!(key_name, "loc" | "__proto__"), "Unsupported property `{key_name}`");
match prop.kind {
PropertyKind::Init => {
let ObjectProperty { key, value, .. } = prop.unbox();
let PropertyKey::StaticIdentifier(key) = key else { unreachable!() };
let target = AssignmentTarget::new_static_member_expression(
SPAN,
Expression::new_identifier(SPAN, temp, &self.ast),
key.unbox(),
false,
&self.ast,
);
Expression::new_assignment_expression(
SPAN,
AssignmentOperator::Assign,
target,
value,
&self.ast,
)
}
PropertyKind::Get => {
// Same attributes as a getter in an object literal
let descriptor_prop = |name, value, method| {
ObjectPropertyKind::new_object_property(
SPAN,
PropertyKind::Init,
PropertyKey::new_static_identifier(SPAN, name, &self.ast),
value,
method,
false,
false,
&self.ast,
)
};
let descriptor = Expression::new_object_expression(
SPAN,
ArenaVec::from_iter_in(
[
descriptor_prop("get", prop.unbox().value, true),
descriptor_prop(
"enumerable",
Expression::new_boolean_literal(SPAN, true, &self.ast),
false,
),
descriptor_prop(
"configurable",
Expression::new_boolean_literal(SPAN, true, &self.ast),
false,
),
],
&self.ast,
),
&self.ast,
);
let define_property = Expression::new_static_member_expression(
SPAN,
Expression::new_identifier(SPAN, "Object", &self.ast),
IdentifierName::new(SPAN, "defineProperty", &self.ast),
false,
&self.ast,
);
let args = [
Argument::from(Expression::new_identifier(SPAN, temp, &self.ast)),
Argument::from(Expression::new_string_literal(SPAN, key_name, None, &self.ast)),
Argument::from(descriptor),
];
Expression::new_call_expression(
SPAN,
define_property,
None,
ArenaVec::from_iter_in(args, &self.ast),
false,
&self.ast,
)
}
PropertyKind::Set => panic!("Unsupported setter in object literal"),
}
} }
} }
impl<'a> VisitMut<'a> for LocFieldAdder<'a> { impl<'a> VisitMut<'a> for LocFieldAdder<'a> {
fn visit_object_expression(&mut self, obj_expr: &mut ObjectExpression<'a>) { fn visit_function(&mut self, func: &mut Function<'a>, flags: ScopeFlags) {
// Check if object has `range` field (`...(RANGE && { range: [start, end] })`) let depth = mem::take(&mut self.depth);
let temp_count = self.temp_count.replace(0);
walk_mut::walk_function(self, func, flags);
// Declare temp vars at start of function (`let node$0, node$1;`)
let count = self.temp_count.unwrap();
if count > 0 {
let declarations = ArenaVec::from_iter_in(
(0..count).map(|index| {
VariableDeclarator::new(
SPAN,
BindingPattern::new_binding_identifier(
SPAN,
temp_name(index, self.allocator),
&self.ast,
),
None,
None,
false,
&self.ast,
)
}),
&self.ast,
);
let decl = Statement::new_variable_declaration(
SPAN,
VariableDeclarationKind::Let,
declarations,
false,
&self.ast,
);
func.body.as_mut().unwrap().statements.insert(0, decl);
}
self.depth = depth;
self.temp_count = temp_count;
}
fn visit_expression(&mut self, expr: &mut Expression<'a>) {
// Check if expression is object with `range` field (`...(RANGE && { range: [start, end] })`)
let Expression::ObjectExpression(obj_expr) = expr else {
walk_mut::walk_expression(self, expr);
return;
};
let has_range_field = obj_expr.properties.iter().any(|prop| { let has_range_field = obj_expr.properties.iter().any(|prop| {
if let ObjectPropertyKind::SpreadProperty(spread) = prop if let ObjectPropertyKind::SpreadProperty(spread) = prop
&& let Expression::ParenthesizedExpression(paren_expr) = &spread.argument && let Expression::ParenthesizedExpression(paren_expr) = &spread.argument
@@ -1847,22 +2095,33 @@ impl<'a> VisitMut<'a> for LocFieldAdder<'a> {
false false
} }
}); });
if !has_range_field {
if has_range_field { walk_mut::walk_expression(self, expr);
// Insert `__proto__: NodeProto` as first field return;
let prop = ObjectPropertyKind::new_object_property(
SPAN,
PropertyKind::Init,
PropertyKey::new_static_identifier(SPAN, "__proto__", &self.ast),
Expression::new_identifier(SPAN, "NodeProto", &self.ast),
false,
false,
false,
&self.ast,
);
obj_expr.properties.insert(0, prop);
} }
walk_mut::walk_object_expression(self, obj_expr); // Convert any nested object literals first, using the next temp var
let temp_count =
self.temp_count.as_mut().expect("Object literal with `range` field outside function");
let index = self.depth;
*temp_count = (*temp_count).max(index + 1);
self.depth += 1;
walk_mut::walk_expression(self, expr);
self.depth -= 1;
let temp = temp_name(index, self.allocator);
expr.replace_with(|expr| {
let Expression::ObjectExpression(obj_expr) = expr else { unreachable!() };
self.create_node(obj_expr.unbox(), temp)
});
}
fn visit_binding_identifier(&mut self, ident: &mut BindingIdentifier<'a>) {
assert!(!ident.name.starts_with("node$"), "`{}` clashes with temp vars", ident.name);
} }
} }
/// Get name of temp var for object literals at nesting depth `index`.
fn temp_name(index: usize, allocator: &Allocator) -> &str {
allocator.alloc_str(&format!("node${index}"))
}