mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(parser): reuse comment annotations after rewind (#28408)
Reuse stored comment classifications after parser rewinds. Local arm64 Astro parse time drops ~3%, with unchanged allocations. Validated independently against main: parser and Node suites, unchanged conformance, and 22,248 differential comparisons. AI assistance: Codex.
This commit is contained in:
1 file changed
+78
-12
@@ -282,10 +282,12 @@ impl<'a> TriviaBuilder<'a> {
|
||||
}
|
||||
|
||||
/// Update annotation state for the comment at `index`.
|
||||
fn set_annotation_flags(&mut self, comment: &Comment, index: usize) {
|
||||
let range = match comment.content {
|
||||
CommentContent::PureNotApplied => &mut self.pure_comments,
|
||||
CommentContent::NoSideEffectsNotApplied => &mut self.no_side_effects_comments,
|
||||
fn set_annotation_flags(&mut self, content: CommentContent, index: usize) {
|
||||
let range = match content {
|
||||
CommentContent::Pure | CommentContent::PureNotApplied => &mut self.pure_comments,
|
||||
CommentContent::NoSideEffects | CommentContent::NoSideEffectsNotApplied => {
|
||||
&mut self.no_side_effects_comments
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
@@ -302,24 +304,30 @@ impl<'a> TriviaBuilder<'a> {
|
||||
}
|
||||
|
||||
fn add_comment(&mut self, mut comment: Comment, source_text: &str) {
|
||||
Self::parse_annotation(&mut comment, source_text);
|
||||
// The comments array is an ordered vec, only add the comment if its not added before,
|
||||
// to avoid situations where the parser needs to rewind and tries to reinsert the comment.
|
||||
if let Some(last_comment) = self.comments.last()
|
||||
&& comment.span.start <= last_comment.span.start
|
||||
{
|
||||
// Duplicate from parser lookahead/rewind — update annotation flags
|
||||
// to point to the existing comment.
|
||||
if let Ok(index) = self
|
||||
.comments
|
||||
.binary_search_by_key(&comment.span.start, |existing| existing.span.start)
|
||||
// Reuse the classification from before lookahead/rewind, including annotations
|
||||
// that have already been applied. Usually this is the most recent comment.
|
||||
let index = if comment.span.start == last_comment.span.start {
|
||||
Some(self.comments.len() - 1)
|
||||
} else {
|
||||
self.comments
|
||||
.binary_search_by_key(&comment.span.start, |existing| existing.span.start)
|
||||
.ok()
|
||||
};
|
||||
if let Some(index) = index
|
||||
&& self.comments[index].span == comment.span
|
||||
{
|
||||
self.set_annotation_flags(&comment, index);
|
||||
self.set_annotation_flags(self.comments[index].content, index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Self::parse_annotation(&mut comment, source_text);
|
||||
|
||||
// This newly added comment may be preceded by a newline.
|
||||
// Use `saw_newline_for_comment` which tracks newlines since the last comment or token,
|
||||
// not just since the last token.
|
||||
@@ -341,7 +349,7 @@ impl<'a> TriviaBuilder<'a> {
|
||||
|
||||
// Set annotation flags here (not in `parse_annotation`) so the index is correct
|
||||
// even when the dedup check above skips a duplicate from parser lookahead/rewind.
|
||||
self.set_annotation_flags(&comment, self.comments.len());
|
||||
self.set_annotation_flags(comment.content, self.comments.len());
|
||||
self.comments.push(comment);
|
||||
if becomes_trailing {
|
||||
self.attach_pending_comments(
|
||||
@@ -562,6 +570,64 @@ mod test {
|
||||
|
||||
use crate::Parser;
|
||||
|
||||
use super::TriviaBuilder;
|
||||
|
||||
#[test]
|
||||
fn annotation_flags_restored_when_replaying_comments() {
|
||||
for annotation in ["/* @__PURE__ */", "/* @__NO_SIDE_EFFECTS__ */"] {
|
||||
for applied in [false, true] {
|
||||
for suffix in ["", "/* later comment */"] {
|
||||
let source = format!("{annotation}{suffix}");
|
||||
let annotation_end = u32::try_from(annotation.len()).unwrap();
|
||||
let source_end = u32::try_from(source.len()).unwrap();
|
||||
let allocator = Allocator::default();
|
||||
let mut builder = TriviaBuilder::new_in(&allocator);
|
||||
builder.add_block_comment(
|
||||
0,
|
||||
annotation_end,
|
||||
CommentKind::SingleLineBlock,
|
||||
&source,
|
||||
);
|
||||
if !suffix.is_empty() {
|
||||
builder.add_block_comment(
|
||||
annotation_end,
|
||||
source_end,
|
||||
CommentKind::SingleLineBlock,
|
||||
&source,
|
||||
);
|
||||
}
|
||||
|
||||
let pure = builder.previous_token_pure_comments();
|
||||
let no_side_effects = builder.previous_token_no_side_effects_comments();
|
||||
if applied {
|
||||
if let Some(range) = pure {
|
||||
builder.mark_pure_comments_applied(range);
|
||||
}
|
||||
if let Some(range) = no_side_effects {
|
||||
builder.mark_no_side_effects_comments_applied(range);
|
||||
}
|
||||
}
|
||||
let content = builder.comments[0].content;
|
||||
builder.clear_pure_comments();
|
||||
builder.clear_no_side_effects_comments();
|
||||
|
||||
// Replaying the annotation must restore its range without changing
|
||||
// whether it was already applied, even when a later comment exists.
|
||||
builder.add_block_comment(
|
||||
0,
|
||||
annotation_end,
|
||||
CommentKind::SingleLineBlock,
|
||||
&source,
|
||||
);
|
||||
assert_eq!(builder.previous_token_pure_comments(), pure);
|
||||
assert_eq!(builder.previous_token_no_side_effects_comments(), no_side_effects);
|
||||
assert_eq!(builder.comments[0].content, content);
|
||||
assert_eq!(builder.comments.len(), 1 + usize::from(!suffix.is_empty()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_comments(source_text: &str) -> Vec<Comment> {
|
||||
let allocator = Allocator::default();
|
||||
let source_type = SourceType::default();
|
||||
|
||||
Reference in new issue
Block a user