perf(formatter_core): write into one element stack through a concrete Formatter (#27559)

Makes the `Formatter` a concrete type that writes into one element stack, instead of writing through `&mut dyn Buffer` into separate buffers.

### Before

- `Formatter` held a `&mut dyn Buffer`, so every element write was a virtual call.
- There were 4 `Buffer` implementations, one per use:
  - `VecBuffer`: the root document, written directly into the arena.
  - `HeapVecBuffer`: nested staging (`intern`, best fitting variants), a watermarked view of a shared heap vector, copied into the arena one element at a time (`from_iter_in`).
  - `AccumulatorBuffer`: writes into a `ScratchBuffer`'s heap vector (JSX children, assignment left-hand sides).
  - `RemoveSoftLinesBuffer`: a wrapper buffer for `remove_soft_lines`.

### After

- `Formatter { state, start, soft_lines }` is concrete.
  - A write is a `push_fast` that inlines into the builders.
- `FormatState` owns one element stack in the arena:
  - The root formatter writes at the bottom.
  - `into_vec` returns the vector as is, so the document needs no copy (same as `VecBuffer`).
  - Nested formatters write past their parent's elements. They are taken off as one
exactly-sized bulk copy.
- `ScratchBuffer::write(f, |f| ..)` writes past the stack, then moves the elements into its heap vector.
- `remove_soft_lines` is a mode of `Formatter`, kept off the hot path (`#[cold]`).
- Removed: `VecBuffer`, `HeapVecr`, `RemoveSoftLinesBuffer`.
- Entry points change from `VecBuffer::new` / `with_capacity` to `Formatter::new` / `with_capacity` + `into_vec`.
This commit is contained in:
leaysgur committed 2026-10-15 05:02:32 +00:00
1 parent e71c88ae65
commit e5fdc9c88e
23 files changed
+373 -492

No files matched your search

+2 -2
View File
@@ -36,7 +36,7 @@ pub use self::{
use oxc_allocator::ArenaVec;
use oxc_formatter_core::{
Arguments, Buffer as _, Document, FormatElement, FormatSession, FormatState, Formatted,
VecBuffer,
Formatter,
};
/// The `format` function takes an [`Arguments`] struct and returns the resulting formatting IR.
@@ -71,7 +71,7 @@ pub fn build_ir<'ast>(
let capacity = (context.source_text().len() * 2) / 5;
let mut state = FormatState::new_with_session(context, session.clone());
let mut buffer = VecBuffer::with_capacity(capacity, &mut state);
let mut buffer = Formatter::with_capacity(capacity, &mut state);
buffer.write_fmt(arguments);
@@ -5,8 +5,8 @@ pub use super::{
};
pub use crate::source_text::SourceTextExt as _;
pub use oxc_formatter_core::{
Buffer as _, BufferExtensions, Format, Format as _, FormatOptions as _, Formatter,
MemoizeFormat, Memoized,
Buffer as _, BufferExtensions, Format, Format as _, FormatOptions as _, MemoizeFormat,
Memoized,
format_element::{
tag::{LabelId, Tag, TagKind},
*,
@@ -1,5 +1,5 @@
use oxc_ast::ast::*;
use oxc_formatter_core::{Buffer, Format, RemoveSoftLinesBuffer, SourceText};
use oxc_formatter_core::{Buffer, Format, SourceText};
use oxc_span::{GetSpan, Span};
use crate::{
@@ -750,8 +750,7 @@ fn format_signature<'a, 'b>(
write!(f, format_head);
} else {
write!(f, [space()]);
let mut buffer = RemoveSoftLinesBuffer::new(f);
write!(buffer, format_head);
f.remove_soft_lines(|f| write!(f, format_head));
}
} else {
write!(
@@ -1,6 +1,6 @@
use oxc_allocator::ArenaVec;
use oxc_ast::ast::*;
use oxc_formatter_core::{FormatElement, RemoveSoftLinesBuffer, format_element};
use oxc_formatter_core::{FormatElement, format_element};
use oxc_span::GetSpan;
use crate::{
@@ -686,7 +686,7 @@ fn write_grouped_arguments<'a>(
}
// First write the most expanded variant because it needs `arguments`.
let most_expanded = best_fitting_variant(f.state_mut(), |buffer| {
let most_expanded = best_fitting_variant(f, |buffer| {
format_all_elements_broken_out(node, elements.iter().cloned(), true, buffer);
});
@@ -738,7 +738,7 @@ fn write_grouped_arguments<'a>(
// If they break even without soft lines, we need to use the expanded layout.
// However, decorated functions are allowed to break while staying hugged.
let interned = f.intern(&format_once(|f| {
RemoveSoftLinesBuffer::new(f).write_element(cached_element);
f.remove_soft_lines(|f| f.write_element(cached_element));
}));
if let Some(interned) = interned {
@@ -767,7 +767,7 @@ fn write_grouped_arguments<'a>(
}
// Write the second variant that forces the group of the first/last argument to expand.
let middle_variant = best_fitting_variant(f.state_mut(), |buffer| {
let middle_variant = best_fitting_variant(f, |buffer| {
write!(
buffer,
[
@@ -808,7 +808,7 @@ fn write_grouped_arguments<'a>(
ArenaVec::from_array_in([middle_variant, most_expanded], f)
} else {
// Write the most flat variant with the first or last argument grouped.
let most_flat = best_fitting_variant(f.state_mut(), |buffer| {
let most_flat = best_fitting_variant(f, |buffer| {
write!(
buffer,
[
@@ -563,7 +563,7 @@ enum MultilineLayout {
struct MultilineBuilder<'a> {
layout: MultilineLayout,
/// Heap accumulator; the flat and multiline builders write alternately,
/// so each owns its own (see [`ScratchBuffer::writer`]).
/// so each owns its own (see [`ScratchBuffer::write`]).
result: ScratchBuffer<'a>,
}
@@ -607,8 +607,7 @@ impl<'a> MultilineBuilder<'a> {
separator: Option<&dyn Format<'a, JsFormatContext<'a>>>,
f: &mut JsFormatter<'_, 'a>,
) {
let mut buffer = self.result.writer(f.state_mut());
match self.layout {
self.result.write(f, |buffer| match self.layout {
MultilineLayout::Fill => {
// Make sure that the separator and content only ever write a single element
buffer.write_element(FormatElement::Tag(Tag::StartEntry));
@@ -624,7 +623,7 @@ impl<'a> MultilineBuilder<'a> {
MultilineLayout::NoFill => {
write!(buffer, [content, separator]);
}
}
});
}
/// Writes a separator into the last entry if it is an entry.
@@ -720,8 +719,7 @@ impl<'a> FlatBuilder<'a> {
return;
}
let mut buffer = self.result.writer(f.state_mut());
write!(buffer, [content]);
self.result.write(f, |buffer| write!(buffer, [content]));
}
fn disable(&mut self) {
+16 -18
View File
@@ -8,7 +8,7 @@ use std::cmp;
use oxc_allocator::{ArenaStringBuilder, ArenaVec};
use oxc_ast::ast::*;
use oxc_formatter_core::{Format, FormatElement, IndentWidth, RemoveSoftLinesBuffer, VecBuffer};
use oxc_formatter_core::{Format, FormatElement, IndentWidth};
use oxc_span::{GetSpan, Span};
use crate::{
@@ -428,9 +428,8 @@ impl<'a> Format<'a, JsFormatContext<'a>> for FormatTemplateExpression<'a, '_> {
let format_inner = format_with(|f: &mut JsFormatter<'_, 'a>| match layout {
TemplateElementLayout::SingleLine => {
// Remove soft line breaks for single-line layout
let mut buffer = RemoveSoftLinesBuffer::new(f);
if let Some(element) = &interned_expression {
buffer.write_element(element.clone());
f.remove_soft_lines(|f| f.write_element(element.clone()));
}
}
TemplateElementLayout::Fit => {
@@ -698,29 +697,28 @@ impl<'a> EachTemplateTable<'a> {
builder.entry(EachTemplateElement::LineBreak);
for expr in quasi.expressions() {
let mut vec_buffer = VecBuffer::new(f.state_mut());
// The softline buffer replaces all softline breaks with a space or removes it entirely
// to "mimic" an infinite print width
let mut buffer = RemoveSoftLinesBuffer::new(&mut vec_buffer);
let options = FormatTemplateExpressionOptions {
after_new_line: false,
indention: TemplateElementIndention::default(),
};
let mut recording = buffer.start_recording();
write!(
recording,
[FormatTemplateExpression::new(&TemplateExpression::Expression(expr), options)]
);
recording.stop();
// Removing the soft line breaks (replaced with a space or removed entirely)
// "mimics" an infinite print width
let elements = f.format_to_vec(|f| {
f.remove_soft_lines(|f| {
write!(
f,
[FormatTemplateExpression::new(
&TemplateExpression::Expression(expr),
options
)]
);
});
});
// TODO: The printed text IS the output, not just a width measurement,
// so a `TailwindClass` inside `expr` (e.g. JSX `className` under `sortTailwindcss`) prints UNSORTED.
let root =
Document::new(vec_buffer.into_vec(), f.session().unsorted_tailwind_classes());
let root = Document::new(elements, f.session().unsorted_tailwind_classes());
let print_options = f.options().as_print_options();
// TODO: if `unwrap()` panics here, it's a internal error
@@ -909,7 +909,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for AssignmentLike<'a, '_> {
// (it can incur in some transformation, like removing some escapes, etc.).
//
// 1. we create a scratch accumulator as a temporary heap buffer
// (see `AccumulatorBuffer` for why neither the arena nor the shared scratch fits)
// (see `ScratchBuffer` for why neither the arena nor the element stack fits)
// 2. we write the left hand side into the buffer and retrieve the `is_left_short` info which is computed only when we format it
// 3. we compute the layout
// 4. we write the left node inside the main buffer based on the layout
@@ -921,8 +921,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for AssignmentLike<'a, '_> {
let operator_end = self.commented_operator_position(f);
let view_limit =
operator_end.map(|end| f.context_mut().comments_mut().limit_comments_up_to(end));
let is_left_short =
self.write_left(&mut Formatter::new(&mut formatted_left.writer(f.state_mut())));
let is_left_short = formatted_left.write(f, |f| self.write_left(f));
if let Some(view_limit) = view_limit {
f.context_mut().comments_mut().restore_view_limit(view_limit);
}
+12 -10
View File
@@ -42,21 +42,23 @@ When changing printer runtime behavior, update that mirror in the same PR; oxfmt
### Choosing a staging buffer
The arena is a bump allocator and never reclaims, so a vector grown in it strands every grown-out-of allocation for the rest of the format run.
Every `Formatter` of a format run writes into one element stack owned by the `FormatState`.
The stack is in the arena, and the root formatter's elements at its bottom become the document without a copy;
what nested formatters stage on top of them leaves as exactly-sized copies.
The arena is a bump allocator and never reclaims, so a vector grown in it strands every grown-out-of allocation for the rest of the format run:
reserve the root's room up front (`Formatter::with_capacity`).
Pick by what you're building:
- Root document (feeds `Document::new` / `EmbeddedIr`): `VecBuffer` (arena)
- it moves into the `Document` for free, and heap-staging it costs an extra copy for no benefit
- Unknown-length staging that ends interned/sliced: `HeapVecBuffer`
- a watermarked view over one scratch vector owned by the format run
- the arena receives one exactly-sized copy (see its rustdoc for the full rationale)
- Root document (feeds `Document::new` / `EmbeddedIr`): `Formatter::new` / `Formatter::with_capacity`, finished with `Formatter::into_vec`
- Unknown-length staging that ends interned/sliced: `Formatter::intern` / `Formatter::format_to_vec`
- a nested formatter appends past its parent's elements and drains them before the parent writes again (LIFO)
- for `BestFitting` variants, `best_fitting_variant` already wraps this (entry tags + staging in one place)
- Accumulating across interleaved `write()` calls (multiple builders open at once), or staging that must release the state between writes and consumption: `ScratchBuffer` (one per accumulator)
- write through `ScratchBuffer::writer`, finish via `Formatter::intern_elements` (or re-emit via `ScratchBuffer::drain`, abandon via `ScratchBuffer::discard`)
- the shared scratch's LIFO rule and its exclusive state borrow rule out `HeapVecBuffer` there (see the JSX child-list builders and `AssignmentLike` in `oxc_formatter`)
- Accumulating across interleaved `write()` calls (multiple builders open at once), or staging that must release the formatter between writes and consumption: `ScratchBuffer` (one per accumulator)
- write through `ScratchBuffer::write`, finish via `Formatter::intern_elements` (or re-emit via `ScratchBuffer::drain`, abandon via `ScratchBuffer::discard`)
- the element stack's LIFO rule rules out a nested formatter there (see the JSX child-list builders and `AssignmentLike` in `oxc_formatter`)
- Known-length sequences: build exact-sized directly (e.g. `ArenaVec::from_iter_in`)
`Formatter::intern` and `BestFitting` already stage on the heap; consumer crates get this for free.
`Formatter::intern` and `BestFitting` already stage this way; consumer crates get this for free.
### Generic context design
@@ -17,15 +17,14 @@ use cow_utils::CowUtils;
use oxc_allocator::Allocator;
use crate::{
Argument, Arguments, Buffer, BufferExtensions, Document, Format, FormatContext, FormatElement,
FormatOptions, FormatState, Formatter, PrintMode, Printer, PrinterOptions, SimpleFormatContext,
VecBuffer,
Buffer, BufferExtensions, Document, Format, FormatContext, FormatElement, FormatOptions,
FormatState, Formatter, PrintMode, Printer, PrinterOptions, SimpleFormatContext,
builders::{hard_line_break, soft_line_break_or_space, space, text, token},
format_element::{
LineMode, TextWidth,
tag::{self, DedentMode, GroupMode, Tag},
},
write, write as w,
write as w,
};
impl<'a> Document<'a> {
@@ -61,10 +60,10 @@ impl std::fmt::Display for DisplayDocument<'_, '_> {
let mut state: FormatState<'_, SimpleFormatContext<'_>> =
FormatState::new(context, &allocator);
let mut buffer = VecBuffer::new(&mut state);
let mut buffer = Formatter::new(&mut state);
let elements = FormatElementSlice(self.elements);
write(&mut buffer, Arguments::new(&[Argument::new(&elements)]));
elements.fmt(&mut buffer);
let elements = buffer.into_vec();
let document = Document::new(elements, Vec::default());
+2 -5
View File
@@ -53,12 +53,9 @@ pub use session::embedded::{
};
pub use session::{FormatSession, InputKind, SessionServices, StringEmbedder, TailwindSorter};
pub use source::{SourceText, SpanCursor};
pub use write::Format;
pub use write::arguments::{Argument, Arguments};
pub use write::buffer::{
AccumulatorBuffer, Buffer, BufferExtensions, HeapVecBuffer, Recorded, Recording,
RemoveSoftLinesBuffer, ScratchBuffer, VecBuffer,
};
pub use write::buffer::{Buffer, BufferExtensions, Recorded, Recording, ScratchBuffer};
pub use write::extensions::{MemoizeFormat, Memoized};
pub use write::formatter::{Formatter, arena_cow_str};
pub use write::state::FormatState;
pub use write::{Format, write};
+6 -7
View File
@@ -1657,8 +1657,8 @@ mod tests {
use oxc_allocator::Allocator;
use crate::{
Argument, Arguments, Buffer, Document, Format, FormatState, IndentStyle, LineEnding,
Printed, Printer, PrinterOptions, SimpleFormatContext, VecBuffer, best_fitting,
Buffer, Document, Format, FormatState, IndentStyle, LineEnding, Printed, Printer,
PrinterOptions, SimpleFormatContext, best_fitting,
builders::{
align, block_indent, dedent_to_root, empty_line, exact_line_breaks, group,
hard_line_break, if_group_breaks, if_group_fits_on_line, indent, line_suffix,
@@ -1699,8 +1699,8 @@ mod tests {
{
let context = SimpleFormatContext::default();
let mut state = FormatState::new(context, allocator);
let mut buffer = VecBuffer::new(&mut state);
write(&mut buffer, Arguments::new(&[Argument::new(root)]));
let mut buffer = crate::Formatter::new(&mut state);
root.fmt(&mut buffer);
let elements = buffer.into_vec();
Document::new(elements, Vec::default()).print(0, options).expect("Document to be valid")
@@ -2504,8 +2504,7 @@ two lines`,
let allocator = Allocator::default();
let context = SimpleFormatContext::default();
let mut state = FormatState::new(context, &allocator);
let mut buffer = VecBuffer::new(&mut state);
let mut formatter = crate::Formatter::new(&mut buffer);
let mut formatter = crate::Formatter::new(&mut state);
formatter
.fill()
@@ -2531,7 +2530,7 @@ two lines`,
)
.finish();
let document = Document::new(buffer.into_vec(), Vec::default());
let document = Document::new(formatter.into_vec(), Vec::default());
let printed = Printer::new(
PrinterOptions::default()
+101 -304
View File
@@ -1,21 +1,16 @@
#![expect(clippy::mutable_key_type)]
use std::{
fmt::Debug,
marker::PhantomData,
ops::{Deref, DerefMut},
};
use std::{fmt::Debug, marker::PhantomData, ops::Deref};
use rustc_hash::FxHashMap;
use oxc_allocator::{Allocator, ArenaVec};
use crate::{
Arguments, FormatElement, FormatState,
Arguments, FormatElement, FormatState, Formatter,
format_element::{
Interned, LineMode, PrintMode,
tag::{Condition, Tag},
},
write,
};
/// A trait for writing or formatting into `FormatElement`-accepting buffers or streams.
@@ -32,9 +27,7 @@ pub trait Buffer<'ast, C> {
/// Glue for usage of the [`write!`] macro with implementors of this trait.
///
/// This method should generally not be invoked manually, but rather through the [`write!`] macro itself.
fn write_fmt(mut self: &mut Self, arguments: Arguments<'_, 'ast, C>) {
write::<C>(&mut self, arguments);
}
fn write_fmt(&mut self, arguments: Arguments<'_, 'ast, C>);
/// Returns the formatting state relevant for this formatting session.
fn state(&self) -> &FormatState<'ast, C>;
@@ -46,10 +39,6 @@ pub trait Buffer<'ast, C> {
///
/// Used by streaming IR transforms (currently `SortImportsTransform`)
/// to splice a reordered chunk back into the buffer.
/// Only the vector-backed buffers (`VecBuffer`, `HeapVecBuffer`, `AccumulatorBuffer`) support this;
/// the wrapper buffer (`RemoveSoftLinesBuffer`) is only ever active
/// inside inner-expression contexts, never on the call stack while a streaming chunk is being flushed,
/// so they implement this as `unreachable!()`.
fn replace_end(&mut self, start: usize, replacement: &[FormatElement<'ast>]);
}
@@ -80,170 +69,23 @@ impl<'ast, C, W: Buffer<'ast, C> + ?Sized> Buffer<'ast, C> for &mut W {
}
}
/// Vector backed [`Buffer`] implementation.
///
/// The buffer writes all elements into the internal elements buffer.
#[derive(Debug)]
pub struct VecBuffer<'buf, 'ast, C> {
state: &'buf mut FormatState<'ast, C>,
elements: ArenaVec<'ast, FormatElement<'ast>>,
}
impl<'buf, 'ast, C> VecBuffer<'buf, 'ast, C> {
pub fn new(state: &'buf mut FormatState<'ast, C>) -> Self {
let elements = ArenaVec::new_in(state);
Self { state, elements }
}
/// Creates a buffer with the specified capacity
pub fn with_capacity(capacity: usize, state: &'buf mut FormatState<'ast, C>) -> Self {
let elements = ArenaVec::with_capacity_in(capacity, state);
Self { state, elements }
}
/// Consumes the buffer and returns the written [`FormatElement]`s as a vector.
pub fn into_vec(self) -> ArenaVec<'ast, FormatElement<'ast>> {
self.elements
}
}
impl<'ast, C> Deref for VecBuffer<'_, 'ast, C> {
type Target = [FormatElement<'ast>];
fn deref(&self) -> &Self::Target {
&self.elements
}
}
impl<C> DerefMut for VecBuffer<'_, '_, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.elements
}
}
impl<'ast, C> Buffer<'ast, C> for VecBuffer<'_, 'ast, C> {
fn write_element(&mut self, element: FormatElement<'ast>) {
self.elements.push(element);
}
fn elements(&self) -> &[FormatElement<'ast>] {
self
}
fn state(&self) -> &FormatState<'ast, C> {
self.state
}
fn state_mut(&mut self) -> &mut FormatState<'ast, C> {
self.state
}
fn replace_end(&mut self, start: usize, replacement: &[FormatElement<'ast>]) {
// `truncate` silently ignores an out-of-bounds start; fail loudly instead
assert!(start <= self.elements.len(), "replace_end start out of bounds");
self.elements.truncate(start);
self.elements.extend_from_slice(replacement);
}
}
/// Heap-backed [`Buffer`], the staging twin of [`VecBuffer`].
///
/// Use it to build an element sequence whose final length isn't known up front
/// (interned content, best-fitting variants), then move the result into the arena
/// exactly-sized via [`HeapVecBuffer::take_into_arena_vec`] / [`HeapVecBuffer::take_into_arena_slice`].
///
/// Growing a vector directly in the arena strands every grown-out-of allocation for the rest of the format run
/// (the arena is a bump allocator and never reclaims;
/// growth can reuse the allocation only when nothing else was bumped in between, which practically never holds while formatting).
/// Heap growth is reclaimed by the system allocator, and the arena receives only the final, exactly-sized copy.
///
/// The buffer is a watermarked view over the format run's single shared staging vector ([`FormatState`]'s scratch):
/// its content is the vector's tail past the length recorded at creation,
/// drained on completion (or on drop). Multiple buffers share the vector like a stack,
/// sound because IR staging is strictly LIFO (an inner buffer always completes before its enclosing one resumes).
pub struct HeapVecBuffer<'buf, 'ast, C> {
state: &'buf mut FormatState<'ast, C>,
watermark: usize,
}
impl<'buf, 'ast, C> HeapVecBuffer<'buf, 'ast, C> {
pub fn new(state: &'buf mut FormatState<'ast, C>) -> Self {
let watermark = state.scratch().len();
Self { state, watermark }
}
/// Removes and returns the buffered elements, leaving the buffer empty.
pub(crate) fn drain(&mut self) -> std::vec::Drain<'_, FormatElement<'ast>> {
let watermark = self.watermark;
self.state.scratch_mut().drain(watermark..)
}
/// Moves the buffered elements into an exactly-sized arena vector, leaving the buffer empty.
pub fn take_into_arena_vec(&mut self) -> ArenaVec<'ast, FormatElement<'ast>> {
let allocator = self.state.allocator();
// `Drain`'s exact size hint makes `from_iter_in` allocate exactly-sized
ArenaVec::from_iter_in(self.drain(), &allocator)
}
/// Moves the buffered elements into an exactly-sized arena slice, leaving the buffer empty.
pub fn take_into_arena_slice(&mut self) -> &'ast [FormatElement<'ast>] {
self.take_into_arena_vec().into_arena_slice()
}
}
impl<C> Drop for HeapVecBuffer<'_, '_, C> {
fn drop(&mut self) {
// Forget any elements not taken, restoring the shared vector for the enclosing buffer.
let watermark = self.watermark;
self.state.scratch_mut().truncate(watermark);
}
}
impl<'ast, C> Deref for HeapVecBuffer<'_, 'ast, C> {
type Target = [FormatElement<'ast>];
fn deref(&self) -> &Self::Target {
&self.state.scratch()[self.watermark..]
}
}
impl<'ast, C> Buffer<'ast, C> for HeapVecBuffer<'_, 'ast, C> {
fn write_element(&mut self, element: FormatElement<'ast>) {
self.state.scratch_mut().push(element);
}
fn elements(&self) -> &[FormatElement<'ast>] {
self
}
fn state(&self) -> &FormatState<'ast, C> {
self.state
}
fn state_mut(&mut self) -> &mut FormatState<'ast, C> {
self.state
}
fn replace_end(&mut self, start: usize, replacement: &[FormatElement<'ast>]) {
let start = self.watermark + start;
let scratch = self.state.scratch_mut();
// `truncate` silently ignores an out-of-bounds start; fail loudly instead
assert!(start <= scratch.len(), "replace_end start out of bounds");
scratch.truncate(start);
scratch.extend_from_slice(replacement);
}
}
/// An owned heap staging vector for accumulators that outlive a single staging scope.
///
/// See [`AccumulatorBuffer`] for why interleaved or suspended accumulators can use
/// neither the arena nor the format run's shared scratch vector.
/// For element sequences that outlive a single exclusive borrow of the [`Formatter`]:
/// builders that accumulate across separate `write()` calls while other content is formatted in between
/// (multiple builders open at once, see the JSX child-list builders in `oxc_formatter`),
/// or staging that must release the formatter between being written and being consumed
/// (the assignment-like left hand side, whose layout is computed with the formatter in between).
/// Those can't stage in an arena vector of their own (every growth would strand the grown-out-of allocation,
/// where the element stack is reserved once by the root)
/// nor on the format run's element stack (suspended or interleaved use breaks its LIFO discipline);
/// a write goes past the element stack and moves here.
///
/// Content leaves the buffer only through the named consumption paths ([`crate::Formatter::intern_elements`],
/// [`ScratchBuffer::drain`], or [`ScratchBuffer::discard`]) all of which leave it empty,
/// satisfying the drop-time assertion below.
#[derive(Debug, Default)]
pub struct ScratchBuffer<'ast>(Vec<FormatElement<'ast>>);
pub struct ScratchBuffer<'ast>(pub(crate) Vec<FormatElement<'ast>>);
impl<'ast> ScratchBuffer<'ast> {
/// An empty scratch buffer.
@@ -251,15 +93,21 @@ impl<'ast> ScratchBuffer<'ast> {
Self(Vec::new())
}
/// Adapts this scratch vector into a [`Buffer`] writing into it.
/// Appends what `content` writes to this buffer, returning what `content` returns.
///
/// This is the only way to construct an [`AccumulatorBuffer`] and the only write path into the vector,
/// The only write path into the vector,
/// so accumulated content is always guarded by the drain-before-drop assertion below.
pub fn writer<'buf, C>(
&'buf mut self,
state: &'buf mut FormatState<'ast, C>,
) -> AccumulatorBuffer<'buf, 'ast, C> {
AccumulatorBuffer::new(state, &mut self.0)
pub fn write<'buf, C, R>(
&mut self,
f: &mut Formatter<'buf, 'ast, C>,
content: impl FnOnce(&mut Formatter<'_, 'ast, C>) -> R,
) -> R {
// Written past the elements on the stack, then moved here
let state = f.state_mut();
let start = state.elements().len();
let result = content(&mut Formatter::nested(&mut *state));
self.0.extend(state.elements_mut().drain(start..));
result
}
/// Removes and returns the accumulated elements, leaving the buffer empty (re-emit consumption path).
@@ -269,7 +117,7 @@ impl<'ast> ScratchBuffer<'ast> {
/// Inserts an element at `index`, shifting the rest.
/// For post-hoc adjustment of already-accumulated content
/// (e.g. slipping a separator into the last written entry); new content goes through [`ScratchBuffer::writer`].
/// (e.g. slipping a separator into the last written entry); new content goes through [`ScratchBuffer::write`].
pub fn insert(&mut self, index: usize, element: FormatElement<'ast>) {
self.0.insert(index, element);
}
@@ -300,92 +148,39 @@ impl Drop for ScratchBuffer<'_> {
}
}
/// Heap accumulator [`Buffer`] over a caller-owned element vector.
///
/// For element sequences that outlive a single exclusive borrow of the [`FormatState`]:
/// builders that accumulate across separate `write()` calls while other content is formatted in between
/// (multiple builders open at once, see the JSX child-list builders in `oxc_formatter`),
/// or staging that must release the state between being written and being consumed
/// (the assignment-like left hand side, whose layout is computed with the formatter in between).
/// Those can't stage in the arena (every growth would strand the grown-out-of allocation, see [`HeapVecBuffer`])
/// nor share the format run's scratch vector (suspended or interleaved use breaks its LIFO discipline).
///
/// Instead the caller owns a [`ScratchBuffer`] and writes through this adapter (constructed via [`ScratchBuffer::writer`]);
/// the arena receives one exactly-sized copy when the finished result is interned ([`crate::Formatter::intern_elements`]),
/// or nothing at all when the elements are re-emitted into a downstream buffer.
pub struct AccumulatorBuffer<'buf, 'ast, C> {
state: &'buf mut FormatState<'ast, C>,
elements: &'buf mut Vec<FormatElement<'ast>>,
}
impl<'buf, 'ast, C> AccumulatorBuffer<'buf, 'ast, C> {
pub(crate) fn new(
state: &'buf mut FormatState<'ast, C>,
elements: &'buf mut Vec<FormatElement<'ast>>,
) -> Self {
Self { state, elements }
}
}
impl<'ast, C> Buffer<'ast, C> for AccumulatorBuffer<'_, 'ast, C> {
fn write_element(&mut self, element: FormatElement<'ast>) {
self.elements.push(element);
}
fn elements(&self) -> &[FormatElement<'ast>] {
self.elements
}
fn state(&self) -> &FormatState<'ast, C> {
self.state
}
fn state_mut(&mut self) -> &mut FormatState<'ast, C> {
self.state
}
fn replace_end(&mut self, start: usize, replacement: &[FormatElement<'ast>]) {
// `truncate` silently ignores an out-of-bounds start; fail loudly instead
assert!(start <= self.elements.len(), "replace_end start out of bounds");
self.elements.truncate(start);
self.elements.extend_from_slice(replacement);
}
}
/// A Buffer that removes any soft line breaks.
/// State of the [`crate::Formatter::remove_soft_lines`] mode.
///
/// * Removes [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::Soft).
/// * Replaces [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::SoftOrSpace) with a [`Space`](FormatElement::Space)
pub struct RemoveSoftLinesBuffer<'buf, 'ast, C> {
inner: &'buf mut dyn Buffer<'ast, C>,
pub struct SoftLines<'ast> {
/// Caches the interned elements after the soft line breaks have been removed.
///
/// The `key` is the [Interned] element as it has been passed to [Self::write_element] or the child of another
/// The `key` is the [Interned] element as it has been passed to [Self::write] or the child of another
/// [Interned] element. The `value` is the matching document of the key where all soft line breaks have been removed.
///
/// It's fine to not snapshot the cache. The worst that can happen is that it holds on interned elements
/// that are now unused. But there's little harm in that and the cache is cleaned when dropping the buffer.
/// that are now unused. But there's little harm in that and the cache is cleaned when the mode ends.
interned_cache: FxHashMap<Interned<'ast>, Interned<'ast>>,
/// Store the conditional content stack to help determine if the current element is within expanded conditional content.
conditional_content_stack: Vec<Condition>,
/// Drops every element: the mode was entered inside an enclosing mode's expanded conditional content,
/// all of which the enclosing mode drops.
discard: bool,
}
impl<'buf, 'ast, C> RemoveSoftLinesBuffer<'buf, 'ast, C> {
/// Creates a new buffer that removes the soft line breaks before writing them into `buffer`.
pub fn new(inner: &'buf mut dyn Buffer<'ast, C>) -> Self {
Self { inner, interned_cache: FxHashMap::default(), conditional_content_stack: Vec::new() }
}
/// Removes the soft line breaks from an interned element.
fn clean_interned(&mut self, interned: Interned<'ast>) -> Interned<'ast> {
clean_interned(
interned,
&mut self.interned_cache,
&mut self.conditional_content_stack,
self.inner.state().allocator(),
)
impl<'ast> SoftLines<'ast> {
/// Starts the mode, nested in `enclosing` if one is active.
///
/// The nested mode's output passes the enclosing one unchanged (it holds no soft lines, conditional content, or best fitting),
/// so only whether the enclosing one drops everything carries over.
pub fn new(enclosing: Option<&SoftLines<'ast>>) -> Self {
Self {
interned_cache: FxHashMap::default(),
conditional_content_stack: Vec::new(),
discard: enclosing.is_some_and(|s| s.discard || s.is_in_expanded_conditional_content()),
}
}
/// Marker for whether a `StartConditionalContent(mode: Expanded)` has been
@@ -396,6 +191,58 @@ impl<'buf, 'ast, C> RemoveSoftLinesBuffer<'buf, 'ast, C> {
.last()
.is_some_and(|condition| condition.mode == PrintMode::Expanded)
}
/// Writes `element` to the element stack of `state` with the soft line breaks removed.
// Streaming twin of `push_cleaned_element`; see the note there before changing either.
// Cold: kept out of `Formatter::write_element`, which is inlined into every builder.
#[cold]
#[inline(never)]
pub fn write<C>(&mut self, element: FormatElement<'ast>, state: &mut FormatState<'ast, C>) {
let allocator = state.allocator();
self.write_cleaned(element, state.elements_mut(), allocator);
}
fn write_cleaned(
&mut self,
element: FormatElement<'ast>,
out: &mut ArenaVec<'ast, FormatElement<'ast>>,
allocator: &'ast Allocator,
) {
match element {
_ if self.discard => {}
FormatElement::Tag(Tag::StartConditionalContent(condition)) => {
self.conditional_content_stack.push(condition);
}
FormatElement::Tag(Tag::EndConditionalContent) => {
self.conditional_content_stack.pop();
}
// All content within an expanded conditional gets dropped.
// If there's a matching flat variant, that will still get kept.
_ if self.is_in_expanded_conditional_content() => {}
FormatElement::Line(LineMode::Soft) => {}
FormatElement::Line(LineMode::SoftOrSpace) => {
out.push(FormatElement::Space);
}
FormatElement::Interned(interned) => {
let cleaned = clean_interned(
interned,
&mut self.interned_cache,
&mut self.conditional_content_stack,
allocator,
);
out.push(FormatElement::Interned(cleaned));
}
// Since this mode aims to simulate infinite print width, we don't need to retain the best fitting.
// Just extract the flattest variant and then handle elements within it.
FormatElement::BestFitting(best_fitting) => {
for element in best_fitting.most_flat() {
self.write_cleaned(element.clone(), out, allocator);
}
}
element => out.push(element),
}
}
}
// Extracted to function to avoid monomorphization
@@ -461,9 +308,9 @@ fn clean_interned<'ast>(
}
}
/// NOTE: Materializing twin of [`RemoveSoftLinesBuffer::write_element`]: applies the same soft-line-removal rules,
/// but pushes into a new `Interned` body (borrowed input, clones) instead of streaming owned elements to the inner buffer.
/// Kept separate so the streaming path avoids clones and this stays non-generic (no monomorphization per context).
/// NOTE: Materializing twin of [`SoftLines::write`]: applies the same soft-line-removal rules,
/// but pushes into a new `Interned` body (borrowed input, clones) instead of streaming owned elements to the element stack.
/// Kept separate so the streaming path avoids clones.
/// The two `match`es must stay rule-for-rule in sync.
fn push_cleaned_element<'ast>(
element: &FormatElement<'ast>,
@@ -515,56 +362,6 @@ fn push_cleaned_element<'ast>(
}
}
impl<'ast, C> Buffer<'ast, C> for RemoveSoftLinesBuffer<'_, 'ast, C> {
// Streaming twin of `push_cleaned_element`; see the note there before changing either.
fn write_element(&mut self, element: FormatElement<'ast>) {
match element {
FormatElement::Tag(Tag::StartConditionalContent(condition)) => {
self.conditional_content_stack.push(condition);
}
FormatElement::Tag(Tag::EndConditionalContent) => {
self.conditional_content_stack.pop();
}
// All content within an expanded conditional gets dropped.
// If there's a matching flat variant, that will still get kept.
_ if self.is_in_expanded_conditional_content() => {}
FormatElement::Line(LineMode::Soft) => {}
FormatElement::Line(LineMode::SoftOrSpace) => {
self.inner.write_element(FormatElement::Space);
}
FormatElement::Interned(interned) => {
let cleaned = self.clean_interned(interned);
self.inner.write_element(FormatElement::Interned(cleaned));
}
// Since this buffer aims to simulate infinite print width, we don't need to retain the best fitting.
// Just extract the flattest variant and then handle elements within it.
FormatElement::BestFitting(best_fitting) => {
for element in best_fitting.most_flat() {
self.write_element(element.clone());
}
}
element => self.inner.write_element(element),
}
}
fn elements(&self) -> &[FormatElement<'ast>] {
self.inner.elements()
}
fn state(&self) -> &FormatState<'ast, C> {
self.inner.state()
}
fn state_mut(&mut self) -> &mut FormatState<'ast, C> {
self.inner.state_mut()
}
fn replace_end(&mut self, _start: usize, _replacement: &[FormatElement<'ast>]) {
unreachable!()
}
}
pub trait BufferExtensions<'ast, C>: Buffer<'ast, C> + Sized {
/// Starts a recording that gives you access to all elements that have been written between the start
/// and end of the recording.
+17 -18
View File
@@ -8,13 +8,12 @@ use std::num::NonZeroU8;
use oxc_allocator::ArenaVec;
use crate::{
Argument, Arguments, Buffer, Format, FormatContext, FormatElement, FormatOptions, FormatState,
Formatter, GroupId, HeapVecBuffer,
Argument, Arguments, Buffer, Format, FormatContext, FormatElement, FormatOptions, Formatter,
GroupId,
format_element::{
self, LineMode, PrintMode, TextWidth,
tag::{self, Condition, DedentMode, GroupMode, LabelId, Tag},
},
write,
};
use Tag::{
@@ -787,15 +786,15 @@ impl<'ast, C> Format<'ast, C> for BlockIndent<'_, 'ast, C> {
match self.mode {
IndentMode::Soft => {
let line = soft_line_break();
write(f, Arguments::new(&[Argument::new(&line)]));
line.fmt(f);
}
IndentMode::Block => {
let line = hard_line_break();
write(f, Arguments::new(&[Argument::new(&line)]));
line.fmt(f);
}
IndentMode::SoftLineOrSpace | IndentMode::SoftSpace => {
let line = soft_line_break_or_space();
write(f, Arguments::new(&[Argument::new(&line)]));
line.fmt(f);
}
}
@@ -814,15 +813,15 @@ impl<'ast, C> Format<'ast, C> for BlockIndent<'_, 'ast, C> {
match self.mode {
IndentMode::Soft => {
let line = soft_line_break();
write(f, Arguments::new(&[Argument::new(&line)]));
line.fmt(f);
}
IndentMode::Block => {
let line = hard_line_break();
write(f, Arguments::new(&[Argument::new(&line)]));
line.fmt(f);
}
IndentMode::SoftSpace => {
let line = soft_line_break_or_space();
write(f, Arguments::new(&[Argument::new(&line)]));
line.fmt(f);
}
IndentMode::SoftLineOrSpace => (),
}
@@ -1065,16 +1064,16 @@ impl<'fmt, 'ast, C> BestFitting<'fmt, 'ast, C> {
///
/// This is the single place encoding the variant shape the printer's [`format_element::BestFittingElement`] relies on:
/// an entry-tag-wrapped, exactly-sized arena slice.
/// The content is staged on the heap so only that final slice lands in the arena (see [`HeapVecBuffer`]).
pub fn best_fitting_variant<'ast, C>(
state: &mut FormatState<'ast, C>,
content: impl FnOnce(&mut HeapVecBuffer<'_, 'ast, C>),
f: &mut Formatter<'_, 'ast, C>,
content: impl FnOnce(&mut Formatter<'_, 'ast, C>),
) -> &'ast [FormatElement<'ast>] {
let mut buffer = HeapVecBuffer::new(state);
buffer.write_element(FormatElement::Tag(StartEntry));
content(&mut buffer);
buffer.write_element(FormatElement::Tag(EndEntry));
buffer.take_into_arena_slice()
f.format_to_vec(|f| {
f.write_element(FormatElement::Tag(StartEntry));
content(f);
f.write_element(FormatElement::Tag(EndEntry));
})
.into_arena_slice()
}
impl<'ast, C> Format<'ast, C> for BestFitting<'_, 'ast, C> {
@@ -1084,7 +1083,7 @@ impl<'ast, C> Format<'ast, C> for BestFitting<'_, 'ast, C> {
let mut formatted_variants = Vec::with_capacity(variants.len());
for variant in variants {
formatted_variants.push(best_fitting_variant(f.state_mut(), |buffer| {
formatted_variants.push(best_fitting_variant(f, |buffer| {
buffer.write_fmt(Arguments::from(variant));
}));
}
+124 -21
View File
@@ -5,15 +5,16 @@ use std::borrow::Cow;
use oxc_allocator::{Allocator, ArenaVec, GetAllocator};
use crate::{
Argument, Arguments, Buffer, Format, FormatContext, FormatElement, FormatSession, FormatState,
HeapVecBuffer, ScratchBuffer,
Arguments, Buffer, Format, FormatContext, FormatElement, FormatSession, FormatState,
ScratchBuffer,
builders::{FillBuilder, JoinBuilder},
format_element::Interned,
write,
write::buffer::SoftLines,
};
/// Interns an exactly-sized element sequence, the single place encoding the interning policy:
/// Interns an exactly-sized element sequence, encoding the interning policy:
/// the 0/1-element cases return without ever allocating an `ArenaVec`.
/// [`intern_tail`] is its twin for the element stack.
pub fn intern_exact<'ast>(
allocator: &'ast Allocator,
mut elements: impl ExactSizeIterator<Item = FormatElement<'ast>>,
@@ -29,6 +30,38 @@ pub fn intern_exact<'ast>(
}
}
/// Moves `elements[start..]` into an exactly-sized arena vector in one copy.
fn take_tail<'ast>(
elements: &mut ArenaVec<'ast, FormatElement<'ast>>,
start: usize,
allocator: &'ast Allocator,
) -> ArenaVec<'ast, FormatElement<'ast>> {
let tail = &elements[start..];
let len = tail.len();
let mut taken = ArenaVec::with_capacity_in(len, &allocator);
// SAFETY: `taken` has room for `len` elements, and the tail moves into it bit for bit
// while `elements` forgets it, so each element keeps exactly one owner
unsafe {
std::ptr::copy_nonoverlapping(tail.as_ptr(), taken.as_mut_ptr(), len);
taken.set_len(len);
elements.set_len(start);
}
taken
}
/// [`intern_exact`] for `elements[start..]`, which it removes.
fn intern_tail<'ast>(
elements: &mut ArenaVec<'ast, FormatElement<'ast>>,
start: usize,
allocator: &'ast Allocator,
) -> Option<FormatElement<'ast>> {
match elements.len() - start {
0 => None,
1 => elements.pop(),
_ => Some(FormatElement::Interned(Interned::new(take_tail(elements, start, allocator)))),
}
}
/// Lifts a `Cow<'ast, str>` to `&'ast str`, allocating in the arena only for the owned case.
/// Borrowed Cows already point into arena-resident source, so they pass through unchanged.
pub fn arena_cow_str<'ast, C>(cow: &Cow<'ast, str>, f: &Formatter<'_, 'ast, C>) -> &'ast str {
@@ -42,14 +75,76 @@ pub fn arena_cow_str<'ast, C>(cow: &Cow<'ast, str>, f: &Formatter<'_, 'ast, C>)
///
/// The formatter is passed to the [Format] implementation of every node in the CST so that they
/// can use it to format their children.
///
/// All formatters of a format run write into one element stack owned by the [`FormatState`].
/// A formatter's elements are the stack's tail past `start`.
/// A nested formatter (interning, best fitting variants) appends past its parent's elements
/// and drains them before the parent writes again, which the exclusive borrow of the state guarantees.
/// The root formatter starts at 0, and its elements are taken as they are, in the arena.
pub struct Formatter<'buf, 'ast, C> {
pub(crate) buffer: &'buf mut dyn Buffer<'ast, C>,
pub(crate) state: &'buf mut FormatState<'ast, C>,
pub(crate) start: usize,
/// Set inside [`Formatter::remove_soft_lines`].
pub(crate) soft_lines: Option<&'buf mut SoftLines<'ast>>,
}
impl<'buf, 'ast, C> Formatter<'buf, 'ast, C> {
/// Creates a new context that uses the given formatter context
pub fn new(buffer: &'buf mut (dyn Buffer<'ast, C> + 'buf)) -> Self {
Self { buffer }
/// Creates the root formatter of the format run of `state`, taken with [`Formatter::into_vec`].
pub fn new(state: &'buf mut FormatState<'ast, C>) -> Self {
debug_assert!(state.elements().is_empty(), "a root formatter on a state in use");
Self { state, start: 0, soft_lines: None }
}
/// Like [`Formatter::new`], reserving room for `capacity` elements.
/// The room stays reserved in the arena, and growing past it strands the outgrown vector.
pub fn with_capacity(capacity: usize, state: &'buf mut FormatState<'ast, C>) -> Self {
state.elements_mut().reserve(capacity);
Self::new(state)
}
/// A formatter writing past the elements on the stack, which its caller takes off.
pub(crate) fn nested(state: &'buf mut FormatState<'ast, C>) -> Self {
let start = state.elements().len();
Self { state, start, soft_lines: None }
}
/// Consumes the root formatter and returns the written elements as they are.
pub fn into_vec(self) -> ArenaVec<'ast, FormatElement<'ast>> {
let allocator = self.state.allocator();
std::mem::replace(self.state.elements_mut(), ArenaVec::new_in(&allocator))
}
/// Writes what `content` writes past the elements on the stack, and returns where it starts.
fn stage(&mut self, content: impl FnOnce(&mut Formatter<'_, 'ast, C>)) -> usize {
let mut staged = Formatter::nested(&mut *self.state);
content(&mut staged);
staged.start
}
/// Formats `content` into an exactly-sized arena vector without writing it to this formatter.
/// `content` writes without the [`Formatter::remove_soft_lines`] mode;
/// it applies when the result is written to this formatter.
pub fn format_to_vec(
&mut self,
content: impl FnOnce(&mut Formatter<'_, 'ast, C>),
) -> ArenaVec<'ast, FormatElement<'ast>> {
let start = self.stage(content);
let allocator = self.state.allocator();
take_tail(self.state.elements_mut(), start, allocator)
}
/// Writes what `content` writes with the soft line breaks removed, simulating an infinite print width.
///
/// * Removes [`lines`](FormatElement::Line) with the mode [`Soft`](crate::LineMode::Soft).
/// * Replaces [`lines`](FormatElement::Line) with the mode [`SoftOrSpace`](crate::LineMode::SoftOrSpace) with a [`Space`](FormatElement::Space).
/// * Keeps only the most flat variant of best fitting elements, and only the flat conditional content.
pub fn remove_soft_lines(&mut self, content: impl FnOnce(&mut Formatter<'_, 'ast, C>)) {
let mut soft_lines = SoftLines::new(self.soft_lines.as_deref());
content(&mut Formatter {
state: &mut *self.state,
start: self.start,
soft_lines: Some(&mut soft_lines),
});
}
pub fn allocator(&self) -> &'ast Allocator {
@@ -82,13 +177,13 @@ impl<'buf, 'ast, C> Formatter<'buf, 'ast, C> {
self.context().options()
}
/// Formats `content` into an interned element without writing it to the formatter's buffer.
/// The content is staged in a [`HeapVecBuffer`] and lands in the arena exactly-sized,
/// so the arena only ever holds the final interned slice, not the staging growth.
/// Formats `content` into an interned element without writing it to this formatter,
/// like [`Formatter::format_to_vec`].
/// The arena only ever holds the final, exactly-sized interned slice.
pub fn intern(&mut self, content: &dyn Format<'ast, C>) -> Option<FormatElement<'ast>> {
let mut buffer = HeapVecBuffer::new(self.state_mut());
write(&mut buffer, Arguments::new(&[Argument::new(&content)]));
intern_exact(buffer.state().allocator(), buffer.drain())
let start = self.stage(|f| content.fmt(f));
let allocator = self.state.allocator();
intern_tail(self.state.elements_mut(), start, allocator)
}
/// Interns a builder's heap accumulator, moving it into the arena exactly-sized.
@@ -97,7 +192,7 @@ impl<'buf, 'ast, C> Formatter<'buf, 'ast, C> {
&self,
elements: &mut ScratchBuffer<'ast>,
) -> Option<FormatElement<'ast>> {
intern_exact(self.allocator(), elements.drain())
intern_exact(self.allocator(), elements.0.drain(..))
}
/// Creates a [`JoinBuilder`] that joins entries together without a separator.
@@ -123,13 +218,16 @@ impl<'buf, 'ast, C> Formatter<'buf, 'ast, C> {
}
impl<'ast, C> Buffer<'ast, C> for Formatter<'_, 'ast, C> {
#[inline(always)]
#[inline]
fn write_element(&mut self, element: FormatElement<'ast>) {
self.buffer.write_element(element);
match &mut self.soft_lines {
None => self.state.elements_mut().push_fast(element),
Some(soft_lines) => soft_lines.write(element, self.state),
}
}
fn elements(&self) -> &[FormatElement<'ast>] {
self.buffer.elements()
&self.state.elements()[self.start..]
}
#[inline(always)]
@@ -140,15 +238,20 @@ impl<'ast, C> Buffer<'ast, C> for Formatter<'_, 'ast, C> {
}
fn state(&self) -> &FormatState<'ast, C> {
self.buffer.state()
self.state
}
fn state_mut(&mut self) -> &mut FormatState<'ast, C> {
self.buffer.state_mut()
self.state
}
fn replace_end(&mut self, start: usize, replacement: &[FormatElement<'ast>]) {
self.buffer.replace_end(start, replacement);
let start = self.start + start;
let elements = self.state.elements_mut();
// `truncate` silently ignores an out-of-bounds start; fail loudly instead
assert!(start <= elements.len(), "replace_end start out of bounds");
elements.truncate(start);
elements.extend_from_slice(replacement);
}
}
@@ -47,11 +47,11 @@ macro_rules! format_args {
///
/// ```text
/// use biome_formatter::prelude::*;
/// use biome_formatter::{Buffer, FormatState, SimpleFormatContext, VecBuffer, write};
/// use biome_formatter::{Buffer, FormatState, SimpleFormatContext, Formatter, write};
///
/// # fn main() {
/// let mut state = FormatState::new(SimpleFormatContext::default());
/// let mut buffer = VecBuffer::new(&mut state);
/// let mut buffer = Formatter::new(&mut state);
/// write!(&mut buffer, [token("Hello"), space()])?;
/// write!(&mut buffer, [token("World")])?;
///
+1 -9
View File
@@ -6,7 +6,7 @@ pub mod formatter;
mod macros;
pub mod state;
use crate::{Arguments, Buffer, Formatter};
use crate::Formatter;
/// Formatting trait for types that can create a formatted representation. The `biome_formatter` equivalent
/// to [std::fmt::Display].
@@ -81,11 +81,3 @@ impl<C> Format<'_, C> for () {
// Intentionally left empty
}
}
/// The `write` function takes a target buffer and an `Arguments` struct that can be precompiled with the `format_args!` macro.
///
/// The arguments will be formatted in-order into the output buffer provided.
#[inline(always)]
pub fn write<'ast, C>(output: &mut dyn Buffer<'ast, C>, args: Arguments<'_, 'ast, C>) {
Formatter::new(output).write_fmt(args);
}
+15 -16
View File
@@ -1,6 +1,6 @@
use rustc_hash::FxHashMap;
use oxc_allocator::{Allocator, GetAllocator};
use oxc_allocator::{Allocator, ArenaVec, GetAllocator};
use crate::{FormatElement, FormatSession, GroupId, InputKind, format_element::Interned};
@@ -16,9 +16,10 @@ pub struct FormatState<'ast, C> {
// For the document IR printing process
/// The interned elements that have been printed to this point
printed_interned_elements: FxHashMap<Interned<'ast>, usize>,
/// Heap staging vector shared by all [`crate::HeapVecBuffer`]s of this format run;
/// see [`crate::HeapVecBuffer`] for the watermark scheme keeping their views disjoint.
scratch: Vec<FormatElement<'ast>>,
/// The element stack every [`crate::Formatter`] of this format run writes into;
/// see [`crate::Formatter`] for how their views stay disjoint.
/// In the arena, so that the root formatter's elements become the document as they are.
elements: ArenaVec<'ast, FormatElement<'ast>>,
}
impl<C: std::fmt::Debug> std::fmt::Debug for FormatState<'_, C> {
@@ -45,12 +46,8 @@ impl<'ast, C> FormatState<'ast, C> {
/// Creates a new state on an existing session, sharing its arena and `GroupId` space.
pub fn new_with_session(context: C, session: FormatSession<'ast>) -> Self {
Self {
context,
session,
printed_interned_elements: FxHashMap::default(),
scratch: Vec::new(),
}
let elements = ArenaVec::new_in(&session.allocator());
Self { context, session, printed_interned_elements: FxHashMap::default(), elements }
}
/// The session this state formats under.
@@ -58,14 +55,16 @@ impl<'ast, C> FormatState<'ast, C> {
&self.session
}
/// The heap staging vector shared by all [`crate::HeapVecBuffer`]s of this format run.
pub(crate) fn scratch(&self) -> &[FormatElement<'ast>] {
&self.scratch
/// The element stack every [`crate::Formatter`] of this format run writes into.
#[inline]
pub(crate) fn elements(&self) -> &[FormatElement<'ast>] {
&self.elements
}
/// Mutable access to the heap staging vector; see [`FormatState::scratch`].
pub(crate) fn scratch_mut(&mut self) -> &mut Vec<FormatElement<'ast>> {
&mut self.scratch
/// Mutable access to the element stack; see [`FormatState::elements`].
#[inline]
pub(crate) fn elements_mut(&mut self) -> &mut ArenaVec<'ast, FormatElement<'ast>> {
&mut self.elements
}
/// Returns the allocator used for arena-allocating format elements.
+4 -4
View File
@@ -3,8 +3,8 @@ use oxc_css_parser::{ParserBuilder, ParserOptions, TemplatePlaceholder, ast::Sty
use oxc_allocator::{Allocator, ArenaVec};
use oxc_diagnostics::OxcDiagnostic;
use oxc_formatter_core::{
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, InputKind,
VecBuffer,
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, Formatter,
InputKind,
builders::{empty_line, hard_line_break, text},
spec::{FrontMatter, blank_front_matter, parse_front_matter, split_bom},
write,
@@ -73,7 +73,7 @@ pub fn format_with_session<'a>(
// Pre-allocate: measured on 616 real-world files (bootstrap, vscode, saleor; css/scss/less),
// 0.5x source bytes plus a 1024-element floor for tiny-file spikes avoids reallocation for 98% of the corpus.
let capacity = (source.len() / 2).max(1024);
let mut buffer = VecBuffer::with_capacity(capacity, &mut state);
let mut buffer = Formatter::with_capacity(capacity, &mut state);
write!(&mut buffer, FormatCssRoot { stylesheet: &stylesheet, has_bom, front_matter });
@@ -158,7 +158,7 @@ pub fn format_to_ir<'a>(
let context = CssFormatContext::new(options, source, comments, template_placeholders);
let mut state = FormatState::new_with_session(context, session.clone());
let mut buffer = VecBuffer::new(&mut state);
let mut buffer = Formatter::new(&mut state);
write!(&mut buffer, FormatCssEmbedded { stylesheet: &stylesheet, front_matter });
+3 -3
View File
@@ -1,7 +1,7 @@
use oxc_allocator::{Allocator, ArenaVec};
use oxc_diagnostics::OxcDiagnostic;
use oxc_formatter_core::{
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, VecBuffer,
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, Formatter,
builders::{hard_line_break, text},
write,
};
@@ -32,7 +32,7 @@ pub fn format<'a>(
// Pre-allocate: measured on 1,241 real-world files (gitlab operations, github/saleor schemas),
// 0.4x source bytes plus a 1024-element floor for small operation documents avoided reallocation for the entire corpus.
let capacity = (parsed.source.len() * 2 / 5).max(1024);
let mut buffer = VecBuffer::with_capacity(capacity, &mut state);
let mut buffer = Formatter::with_capacity(capacity, &mut state);
write!(&mut buffer, FormatGraphqlRoot { document: parsed.document, has_bom: parsed.has_bom });
@@ -92,7 +92,7 @@ pub fn format_to_ir<'a>(
let context = GraphqlFormatContext::new(options, source, comments);
let mut state = FormatState::new_with_session(context, session.clone());
let mut buffer = VecBuffer::new(&mut state);
let mut buffer = Formatter::new(&mut state);
write!(&mut buffer, FormatGraphqlEmbedded { document });
+3 -3
View File
@@ -3,7 +3,7 @@ use oxc_ast::{Comment, ast::Expression};
use oxc_diagnostics::OxcDiagnostic;
use oxc_formatter_core::{
Buffer, Document, EmbeddedIr, Format, FormatContext, FormatSession, FormatState, Formatted,
VecBuffer,
Formatter,
builders::{hard_line_break, text},
write,
};
@@ -39,7 +39,7 @@ pub fn format<'a>(
// 0.3x source bytes plus a 1024-element floor for tiny-file spikes (`{}` is 3 elements)
// avoids reallocation for 99.5% of the corpus.
let capacity = (parsed.wrapped_source.len() * 3 / 10).max(1024);
let mut buffer = VecBuffer::with_capacity(capacity, &mut state);
let mut buffer = Formatter::with_capacity(capacity, &mut state);
write!(&mut buffer, FormatJsonRoot { expression: parsed.expression, has_bom: parsed.has_bom });
@@ -115,7 +115,7 @@ pub fn format_to_ir<'a>(
parsed.source_offset,
);
let mut state = FormatState::new_with_session(context, session.clone());
let mut buffer = VecBuffer::new(&mut state);
let mut buffer = Formatter::new(&mut state);
write!(&mut buffer, FormatJsonEmbedded { expression: parsed.expression });
+4 -4
View File
@@ -1,8 +1,8 @@
use oxc_allocator::Allocator;
use oxc_diagnostics::OxcDiagnostic;
use oxc_formatter_core::{
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, InputKind,
VecBuffer,
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, Formatter,
InputKind,
builders::{empty_line, hard_line_break, text},
spec::{FrontMatter, blank_front_matter, parse_front_matter},
write, write_front_matter,
@@ -62,7 +62,7 @@ pub fn format_with_session<'a>(
// Pre-allocate: measured on 19,487 real-world files (mdn content, oxc-ecosystem-ci repos),
// 0.4x source bytes plus a 1024-element floor for small documents avoids reallocation for 97.6% of the corpus.
let capacity = (parsed.source.len() * 2 / 5).max(1024);
let mut buffer = VecBuffer::with_capacity(capacity, &mut state);
let mut buffer = Formatter::with_capacity(capacity, &mut state);
write!(&mut buffer, FormatMarkdownRoot { parsed: &parsed });
@@ -133,7 +133,7 @@ pub fn format_to_ir<'a>(
let context = MarkdownFormatContext::new(options, parsed.source, parsed.blanks, in_js_template);
let mut state = FormatState::new_with_session(context, session.clone());
let mut buffer = VecBuffer::new(&mut state);
let mut buffer = Formatter::new(&mut state);
write!(&mut buffer, FormatMarkdownEmbedded { parsed: &parsed });
+3 -3
View File
@@ -1,7 +1,7 @@
use oxc_allocator::{Allocator, ArenaVec};
use oxc_diagnostics::OxcDiagnostic;
use oxc_formatter_core::{
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, VecBuffer,
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, Formatter,
builders::{hard_line_break, text},
write,
};
@@ -36,7 +36,7 @@ pub fn format<'a>(
// Pre-allocate: measured on 6,925 real-world files (kubernetes, vscode, saleor, bootstrap),
// 0.3x source bytes plus a 1024-element floor for tiny-file spikes avoids reallocation for 99.9% of the corpus.
let capacity = (parsed.source.len() * 3 / 10).max(1024);
let mut buffer = VecBuffer::with_capacity(capacity, &mut state);
let mut buffer = Formatter::with_capacity(capacity, &mut state);
write!(&mut buffer, FormatYamlRoot { root: parsed.root, has_bom: parsed.has_bom });
@@ -95,7 +95,7 @@ pub fn format_to_ir<'a>(
let context =
YamlFormatContext::new(options, source, comments, print::last_descendant_end(root));
let mut state = FormatState::new_with_session(context, session.clone());
let mut buffer = VecBuffer::new(&mut state);
let mut buffer = Formatter::new(&mut state);
write!(&mut buffer, FormatYamlEmbedded { root });
@@ -1,76 +1,76 @@
checker.ts:
file size: 2922154 # 2.92 MB
sys allocs: 19387
sys reallocs: 1170
sys deallocs: 19386
sys alloc bytes: 6767781 # 6.77 MB
sys peak growth: 2973674 # 2.97 MB
sys allocs: 19386
sys reallocs: 495
sys deallocs: 19385
sys alloc bytes: 6677609 # 6.68 MB
sys peak growth: 2973842 # 2.97 MB
arena allocs: 1058563
arena reallocs: 927
arena size: 89926872 # 89.93 MB
App.tsx:
file size: 415342 # 415.34 kB
sys allocs: 5194
sys reallocs: 524
sys deallocs: 5193
sys alloc bytes: 1564670 # 1.56 MB
sys peak growth: 435358 # 435.36 kB
sys allocs: 5193
sys reallocs: 89
sys deallocs: 5192
sys alloc bytes: 1306838 # 1.31 MB
sys peak growth: 435478 # 435.48 kB
arena allocs: 209717
arena reallocs: 364
arena size: 16891184 # 16.89 MB
RadixUIAdoptionSection.jsx:
file size: 2520 # 2.52 kB
sys allocs: 69
sys reallocs: 69
sys deallocs: 68
sys alloc bytes: 36132 # 36.13 kB
sys peak growth: 12192 # 12.19 kB
sys allocs: 68
sys reallocs: 52
sys deallocs: 67
sys alloc bytes: 34820 # 34.82 kB
sys peak growth: 12120 # 12.12 kB
arena allocs: 1149
arena reallocs: 0
arena size: 119440 # 119.44 kB
arena size: 118600 # 118.60 kB
pdf.mjs:
file size: 567296 # 567.30 kB
sys allocs: 9777
sys reallocs: 1323
sys deallocs: 9776
sys alloc bytes: 3141552 # 3.14 MB
sys peak growth: 1156432 # 1.16 MB
sys allocs: 9776
sys reallocs: 167
sys deallocs: 9775
sys alloc bytes: 2664720 # 2.66 MB
sys peak growth: 1156488 # 1.16 MB
arena allocs: 429815
arena reallocs: 428
arena size: 29487632 # 29.49 MB
antd.js:
file size: 6686316 # 6.69 MB
sys allocs: 84592
sys reallocs: 4017
sys deallocs: 84591
sys alloc bytes: 76472316 # 76.47 MB
sys peak growth: 50737424 # 50.74 MB
sys allocs: 84591
sys reallocs: 353
sys deallocs: 84590
sys alloc bytes: 25772794 # 25.77 MB
sys peak growth: 13710144 # 13.71 MB
arena allocs: 2535528
arena reallocs: 1889
arena size: 246383800 # 246.38 MB
arena size: 246384344 # 246.38 MB
binder.ts:
file size: 193077 # 193.08 kB
sys allocs: 975
sys reallocs: 97
sys deallocs: 974
sys alloc bytes: 356005 # 356.00 kB
sys peak growth: 195149 # 195.15 kB
sys allocs: 974
sys reallocs: 35
sys deallocs: 973
sys alloc bytes: 345301 # 345.30 kB
sys peak growth: 195197 # 195.20 kB
arena allocs: 62783
arena reallocs: 38
arena size: 5661560 # 5.66 MB
arena size: 5660960 # 5.66 MB
kitchen-sink.tsx:
file size: 732898 # 732.90 kB
sys allocs: 12672
sys reallocs: 2347
sys deallocs: 12671
sys alloc bytes: 4353874 # 4.35 MB
sys peak growth: 1491796 # 1.49 MB
sys allocs: 12671
sys reallocs: 408
sys deallocs: 12670
sys alloc bytes: 4192018 # 4.19 MB
sys peak growth: 1491884 # 1.49 MB
arena allocs: 557605
arena reallocs: 1041
arena size: 38433168 # 38.43 MB