perf(formatter_core): shrink FormatElement to 16B (#28012)

Remaining room of improvement relate to #24558.

Sizes on 64-bit platforms (size / align, in bytes):

- `FormatElement`: 24 / 8 -> 16 / 8
- `Interned`: 16 / 8 -> 12 / 4
- `BestFittingElement`: 16 / 8 -> 12 / 4
- `FormatElement::{Token, Text}` text: `&str` 16 / 8 -> `ThinStr` 12 / 4
- `TextWidth`: 4 / 4 -> 3 / 1
- `LabelId`: 8 / 8 -> 4 / 4
- `Prefix`: 8 / 8 -> 8 / 4
This commit is contained in:
leaysgur committed 2026-10-21 05:21:14 +00:00
1 parent e97a8b1218
commit 7948f0b356
21 files changed
+272 -135

No files matched your search

@@ -78,7 +78,7 @@ impl<'a, 'b> FmtCtx<'a, 'b> {
/// However, the default value is sufficient in practice.
pub fn text_element(text: &str) -> FormatElement<'_> {
let width = TextWidth::from_text(text, IndentWidth::default());
FormatElement::Text { text, width }
FormatElement::text(text, width)
}
fn convert_doc<'a>(
@@ -540,7 +540,7 @@ pub fn postprocess<'a>(ir: &mut ArenaVec<'a, FormatElement<'a>>, allocator: &'a
let single = read - run_start == 1;
let text: &str = if single {
let FormatElement::Text { text, .. } = ir[run_start] else { unreachable!() };
text
text.as_str()
} else {
let mut sb = ArenaStringBuilder::new_in(allocator);
for element in &ir[run_start..read] {
@@ -850,7 +850,7 @@ mod tests {
fn exact(count: u32) -> FormatElement<'static> {
FormatElement::Line(LineMode::ExactLineBreaks(NonZeroU32::new(count).unwrap()))
}
const A: FormatElement<'static> = FormatElement::Token { text: "a" };
const A: FormatElement<'static> = FormatElement::token("a");
const HARD: FormatElement<'static> = FormatElement::Line(LineMode::Hard);
const EMPTY: FormatElement<'static> = FormatElement::Line(LineMode::Empty);
const SPACE: FormatElement<'static> = FormatElement::Space;
@@ -905,14 +905,14 @@ mod tests {
// Dangling comment in an otherwise-empty object:
// `{` indent[softline, "// x", hardline] softline `}`
let doc = to_doc(&[
FormatElement::Token { text: "{" },
FormatElement::token("{"),
FormatElement::Tag(Tag::StartIndent),
SOFT,
FormatElement::Token { text: "// x" },
FormatElement::token("// x"),
HARD,
FormatElement::Tag(Tag::EndIndent),
SOFT,
FormatElement::Token { text: "}" },
FormatElement::token("}"),
]);
assert_eq!(
doc["doc"],
@@ -90,7 +90,7 @@ impl<'a> SourceLine<'a> {
// Everything before the `import` keyword (e.g. leading comments) is not the head.
if !seen_import_keyword {
if matches!(element, FormatElement::Token { text: "import" }) {
if matches!(element, FormatElement::Token { text } if text.as_str() == "import") {
seen_import_keyword = true;
expect_type_keyword = true;
}
@@ -100,7 +100,7 @@ impl<'a> SourceLine<'a> {
match element {
FormatElement::Token { text } => {
if !seen_from_keyword {
match *text {
match text.as_str() {
"type" if expect_type_keyword => is_type_import = true,
"*" => has_namespace_specifier = true,
"{" => has_named_specifier = true,
@@ -115,7 +115,7 @@ impl<'a> SourceLine<'a> {
}
FormatElement::Text { text, .. } => {
if source.is_none() {
source = Some(text);
source = Some(text.as_str());
// The first `Text` after `from` is the source;
// nothing after it (import attributes, trailing comments) affects the metadata.
if seen_from_keyword {
+2 -2
View File
@@ -360,8 +360,8 @@ pub(crate) enum JsLabels {
}
impl Label for JsLabels {
fn id(&self) -> u64 {
*self as u64
fn id(&self) -> u32 {
*self as u32
}
fn debug_name(&self) -> &'static str {
@@ -29,7 +29,7 @@ fn write_text_piece<'a>(text: &'a str, indent_width: IndentWidth, f: &mut JsForm
return;
}
let width = TextWidth::from_text(text, indent_width);
f.write_element(FormatElement::Text { text, width });
f.write_element(FormatElement::text(text, width));
}
/// Print `${expr}` like a plain template expression, minus source-indentation preservation.
@@ -136,8 +136,11 @@ pub(super) fn format_css_doc<'a>(
// [literal, index, literal, ...] (invalid matches fold into the literal),
// so even parts are text and odd parts are a placeholder.
FormatElement::Text { text, .. } if text.contains(PLACEHOLDER_PREFIX) => {
let parts =
super::split_on_placeholders(text, PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX);
let parts = super::split_on_placeholders(
text.as_str(),
PLACEHOLDER_PREFIX,
PLACEHOLDER_SUFFIX,
);
for (i, part) in parts.iter().enumerate() {
if i % 2 == 0 {
write_text_piece(part, indent_width, f);
@@ -189,7 +189,7 @@ fn build_graphql_comment_ir<'a>(
parts.push(FormatElement::ExpandParent);
let arena_text = allocator.alloc_str(line);
let width = TextWidth::from_text(arena_text, indent_width);
parts.push(FormatElement::Text { text: arena_text, width });
parts.push(FormatElement::text(arena_text, width));
} else {
if seen_comment {
parts.push(FormatElement::Line(LineMode::Hard));
@@ -197,7 +197,7 @@ fn build_graphql_comment_ir<'a>(
}
let arena_text = allocator.alloc_str(line);
let width = TextWidth::from_text(arena_text, indent_width);
parts.push(FormatElement::Text { text: arena_text, width });
parts.push(FormatElement::text(arena_text, width));
}
seen_comment = true;
@@ -184,7 +184,7 @@ pub(super) fn format_html_doc<'a>(
}
// No placeholder to substitute; push the escaped text if escaping changed it
let Some(text) = escaped else { return false };
out.push(FormatElement::Text { text, width: TextWidth::from_text(text, indent_width) });
out.push(FormatElement::text(text, TextWidth::from_text(text, indent_width)));
true
});
let format_content = format_once(move |f: &mut JsFormatter<'_, 'a>| f.write_elements(ir));
@@ -313,7 +313,7 @@ mod tests {
use super::placeholders_are_sequential;
fn text(text: &'static str) -> FormatElement<'static> {
FormatElement::Text { text, width: TextWidth::from_text(text, IndentWidth::default()) }
FormatElement::text(text, TextWidth::from_text(text, IndentWidth::default()))
}
fn best_fitting<'a>(
@@ -403,7 +403,7 @@ fn escape_text_in_ir<'a>(
let indent_width = f.options().indent_width;
map_text_in_ir(ir, allocator, &mut |text, out| {
let Some(text) = escape(text, allocator) else { return false };
out.push(FormatElement::Text { text, width: TextWidth::from_text(text, indent_width) });
out.push(FormatElement::text(text, TextWidth::from_text(text, indent_width)));
true
})
}
@@ -10,8 +10,6 @@
//! ([`FormatContext::get_tailwind_class`], fed from the document's sorted classes);
//! a context returning the default `None` prints an `<UNKNOWN_TAILWIND_CLASS_INDEX<..>>` marker.
#![expect(clippy::mutable_key_type)]
use cow_utils::CowUtils;
use oxc_allocator::Allocator;
@@ -132,13 +130,10 @@ where
FormatElement::Token { .. } => element.clone(),
FormatElement::Text { text, width: _ } => {
let text = text.cow_replace('"', "\\\"");
FormatElement::Text {
text: f.allocator().alloc_str(&text),
width: TextWidth::from_text(
&text,
f.options().indent_width(),
),
}
FormatElement::text(
f.allocator().alloc_str(&text),
TextWidth::from_text(&text, f.options().indent_width()),
)
}
_ => unreachable!(),
};
@@ -1,4 +1,3 @@
#![expect(clippy::mutable_key_type)]
use std::ops::Deref;
use oxc_allocator::ArenaVec;
@@ -3,6 +3,7 @@ pub mod document;
pub(crate) mod formatted;
pub(crate) mod group_id;
pub mod tag;
mod thin;
// #[cfg(target_pointer_width = "64")]
// use biome_rowan::static_assert;
@@ -14,31 +15,20 @@ use unicode_width::UnicodeWidthStr;
use oxc_allocator::ArenaVec;
use crate::IndentWidth;
use crate::{IndentWidth, LineWidth};
use self::tag::{LabelId, Tag, TagKind};
use self::thin::ThinSlice;
pub use self::thin::ThinStr;
// `FormatElement` must have the same layout in debug and release builds.
//
// So that memory measurements taken with `debug-assertions` enabled (`cargo allocs` runs with the `coverage` profile) reflect release layouts.
// That is why `GroupId` and `LabelId` keep their debug names in side tables instead of inline fields.
const _: () = {
if cfg!(target_pointer_width = "64") {
assert!(
size_of::<FormatElement>() == 24,
"`FormatElement` size exceeds 24 bytes, expected 24 bytes in 64-bit platforms"
);
} else if cfg!(target_family = "wasm") || align_of::<u64>() == 8 {
// Some 32-bit platforms have 8-byte alignment for `u64` and `f64`, while others have 4-byte alignment.
//
// Skip these assertions on 32-bit platforms where `u64` / `f64` have 4-byte alignment,
// because some layout calculations may be incorrect. https://github.com/oxc-project/oxc/pull/13716
assert!(
size_of::<FormatElement>() == 16,
"`FormatElement` size exceeds 16 bytes, expected 16 bytes in 32-bit platforms"
);
}
};
//
// It is 16 bytes on both 64-bit and 32-bit platforms, as the largest payloads fit after the 1-byte tag:
// `Text` (a 4-aligned 12-byte `ThinStr` and a 3-byte `TextWidth`) and `Tag` (12 bytes, 4-aligned).
const _: () = assert!(size_of::<FormatElement>() == 16, "`FormatElement` size exceeds 16 bytes");
/// Language agnostic IR for formatting source code.
///
@@ -54,10 +44,10 @@ pub enum FormatElement<'a> {
ExpandParent,
/// A ASCII only Token that contains no line breaks or tab characters.
Token { text: &'static str },
Token { text: ThinStr<'static> },
/// An arbitrary text that can contain tabs, newlines, and unicode characters.
Text { text: &'a str, width: TextWidth },
Text { text: ThinStr<'a>, width: TextWidth },
/// Prevents that line suffixes move past this boundary. Forces the printer to print any pending
/// line suffixes, potentially by inserting a hard line break.
@@ -170,17 +160,17 @@ impl PrintMode {
}
#[derive(Clone)]
pub struct Interned<'a>(&'a [FormatElement<'a>]);
pub struct Interned<'a>(ThinSlice<'a, FormatElement<'a>>);
impl<'a> Interned<'a> {
pub(crate) fn new(content: ArenaVec<'a, FormatElement<'a>>) -> Self {
Self(content.into_arena_slice())
Self(ThinSlice::new(content.into_arena_slice()))
}
}
impl PartialEq for Interned<'_> {
fn eq(&self, other: &Interned<'_>) -> bool {
ptr::eq(self.0, other.0)
ptr::eq(self.0.get(), other.0.get())
}
}
@@ -202,7 +192,7 @@ impl<'a> Deref for Interned<'a> {
type Target = [FormatElement<'a>];
fn deref(&self) -> &Self::Target {
self.0
self.0.get()
}
}
@@ -253,7 +243,22 @@ pub fn normalize_newlines<const N: usize>(text: &str, terminators: [char; N]) ->
}
}
impl FormatElement<'_> {
impl<'a> FormatElement<'a> {
/// Creates a [FormatElement::Token].
///
/// `text` must be ASCII without line breaks or tabs, as the printer counts its width as its length.
/// Unlike [crate::builders::token], this is not checked, so that it can be `const`.
#[inline]
pub const fn token(text: &'static str) -> Self {
FormatElement::Token { text: ThinStr::new(text) }
}
/// Creates a [FormatElement::Text].
#[inline]
pub const fn text(text: &'a str, width: TextWidth) -> Self {
FormatElement::Text { text: ThinStr::new(text), width }
}
/// Returns `true` if self is a [FormatElement::Tag] and [Tag::is_end] is `true`.
pub const fn is_end_tag(&self) -> bool {
match self {
@@ -328,7 +333,7 @@ pub struct BestFittingElement<'a> {
/// The different variants for this element.
/// The first element is the one that takes up the most space horizontally (the most flat),
/// The last element takes up the least space horizontally (but most horizontal space).
variants: &'a [&'a [FormatElement<'a>]],
variants: ThinSlice<'a, &'a [FormatElement<'a>]>,
}
impl<'a> BestFittingElement<'a> {
@@ -347,7 +352,7 @@ impl<'a> BestFittingElement<'a> {
"Requires at least the least expanded and most expanded variants"
);
Self { variants: variants.into_arena_slice() }
Self { variants: ThinSlice::new(variants.into_arena_slice()) }
}
/// Returns the most expanded variant
@@ -369,8 +374,8 @@ impl<'a> BestFittingElement<'a> {
unsafe { self.variants.split_last().unwrap_unchecked() }
}
pub fn variants(&self) -> &[&'a [FormatElement<'a>]] {
self.variants
pub fn variants(&self) -> &'a [&'a [FormatElement<'a>]] {
self.variants.get()
}
/// Returns the least expanded variant
@@ -387,7 +392,7 @@ impl<'a> BestFittingElement<'a> {
impl std::fmt::Debug for BestFittingElement<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list().entries(self.variants).finish()
self.variants.fmt(f)
}
}
@@ -427,19 +432,30 @@ pub trait FormatElements {
///
/// ## Representation
///
/// Uses a single `u32` to efficiently store the width value and two flags:
/// Uses 3 bytes, a `u8` for two flags and a `u16` for the width value:
///
/// - Bit 31: Multiline flag (1 = multiline, 0 = single line)
/// - Bit 30: No-expand flag (1 = multiline text that does not expand enclosing groups)
/// - Bits 0-29: Width value (stored directly)
/// - Flag bit 0: Multiline flag (1 = multiline, 0 = single line)
/// - Flag bit 1: No-expand flag (1 = multiline text that does not expand enclosing groups)
///
/// This encoding allows `TextWidth` to fit in 4 bytes while supporting both a width value
/// and boolean flags. The maximum representable width is 2^30 - 1 (0x3FFFFFFF).
/// 3 bytes fit next to the tag of [`FormatElement::Text`], which keeps the element at 16 bytes.
/// Keeping the width as a `u16` lets it be read with one load (on x86-64 and arm64, which allow unaligned loads),
/// instead of assembling it from bytes.
/// The maximum representable width is `u16::MAX`, larger widths saturate to it.
///
/// The maximum value is sufficient in practice as texts that long would exceed any
/// reasonable line width configuration (typically < 500 columns).
/// Saturating does not change how the printer breaks lines, as it only compares widths with the print width,
/// which is below the maximum for any [`LineWidth`] (asserted below).
/// Code that uses a width for more than that (e.g. padding table columns) measures it with [`TextWidth::measure`].
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct TextWidth(u32);
#[repr(C, packed)]
pub struct TextWidth {
flags: u8,
width: u16,
}
const _: () = assert!(
(LineWidth::MAX as u32) < TextWidth::MAX_WIDTH,
"`LineWidth::MAX` exceeds the maximum `TextWidth`, saturated widths would change the output"
);
/// Every byte in `0x20..=0x7E` is a single-width ASCII character,
/// so display width equals byte length and the text is guaranteed single-line.
@@ -449,38 +465,42 @@ fn is_all_printable_ascii(s: &str) -> bool {
}
impl TextWidth {
/// Bit mask for the multiline flag (highest bit)
const MULTILINE_MASK: u32 = 1 << 31;
/// Bit mask for the multiline flag
const MULTILINE_MASK: u8 = 1;
/// Bit mask for the "multiline but does not expand enclosing groups" flag.
/// Equivalent to Prettier's `literallineWithoutBreakParent`: the newlines still
/// print literally, but `Document::propagate_expand` / `will_break` ignore them.
const NO_EXPAND_MASK: u32 = 1 << 30;
const NO_EXPAND_MASK: u8 = 1 << 1;
/// Bit mask for extracting the width value (all bits except the flags)
const WIDTH_MASK: u32 = Self::NO_EXPAND_MASK - 1;
/// The maximum width value, larger widths saturate to it
const MAX_WIDTH: u32 = u16::MAX as u32;
/// Encodes width and multiline flag into a single u32.
const fn encode(width: u32, multiline: bool) -> u32 {
debug_assert!(width <= Self::WIDTH_MASK, "width exceeds maximum representable value");
/// Encodes width and multiline flag, saturating the width.
#[inline]
const fn encode(width: u32, multiline: bool) -> Self {
let width = if width > Self::MAX_WIDTH { Self::MAX_WIDTH } else { width };
// Set multiline flag if needed
if multiline { width | Self::MULTILINE_MASK } else { width }
#[expect(clippy::cast_possible_truncation)]
Self { flags: if multiline { Self::MULTILINE_MASK } else { 0 }, width: width as u16 }
}
/// Creates a single-line text width.
#[inline]
pub const fn single(width: u32) -> Self {
Self(Self::encode(width, false))
Self::encode(width, false)
}
/// Creates a multi-line text width.
#[inline]
pub const fn multiline(width: u32) -> Self {
Self(Self::encode(width, true))
Self::encode(width, true)
}
/// Returns the width value.
#[inline]
pub const fn value(self) -> u32 {
self.0 & Self::WIDTH_MASK
self.width as u32
}
/// Calculates width from text, handling tabs, newlines, and Unicode.
@@ -488,17 +508,29 @@ impl TextWidth {
/// NOTE: Uses `UnicodeWidthStr::width()` for accurate emoji sequence handling.
/// Counting by `char` can lead to incorrect widths for complex Unicode sequences.
/// e.g. "🗑️" (U+1F5D1 U+FE0F) is a single emoji with width 2, but counting chars gives width 1.
#[expect(clippy::cast_possible_truncation)]
pub fn from_text(text: &str, indent_width: IndentWidth) -> TextWidth {
let (width, multiline) = Self::measure_text(text, indent_width);
Self::encode(width, multiline)
}
/// The width of `text` up to its first line break, as [`TextWidth::from_text`] but not saturated.
/// Use it where the width is used for more than comparing with the print width (e.g. table column padding).
pub fn measure(text: &str, indent_width: IndentWidth) -> u32 {
Self::measure_text(text, indent_width).0
}
/// Returns the width up to the first line break, and whether there is one.
#[expect(clippy::cast_possible_truncation)]
fn measure_text(text: &str, indent_width: IndentWidth) -> (u32, bool) {
// Fast path for empty text
if text.is_empty() {
return Self::single(0);
return (0, false);
}
// Excludes `\t`, `\n`, control bytes and multi-byte UTF-8,
// those fall through to the scan below.
if is_all_printable_ascii(text) {
return Self::single(text.len() as u32);
return (text.len() as u32, false);
}
let mut width = 0u32;
@@ -512,14 +544,14 @@ impl TextWidth {
}
'\n' => {
width += text[segment_start..i].width() as u32;
return Self::multiline(width);
return (width, true);
}
_ => {}
}
}
width += text[segment_start..].width() as u32;
Self::single(width)
(width, false)
}
/// Creates width from a string known to not contain whitespace.
@@ -533,21 +565,28 @@ impl TextWidth {
}
/// Returns true if the text contains newlines.
#[inline]
pub const fn is_multiline(self) -> bool {
(self.0 & Self::MULTILINE_MASK) != 0
(self.flags & Self::MULTILINE_MASK) != 0
}
/// Marks a multiline width so its newlines do NOT expand enclosing groups
/// (Prettier's `literallineWithoutBreakParent`). No-op for single-line widths.
#[must_use]
#[inline]
pub const fn without_expand_parent(self) -> Self {
if self.is_multiline() { Self(self.0 | Self::NO_EXPAND_MASK) } else { self }
if self.is_multiline() {
Self { flags: self.flags | Self::NO_EXPAND_MASK, width: self.width }
} else {
self
}
}
/// Returns true if the text forces enclosing groups to expand:
/// it contains newlines and is not marked [`Self::without_expand_parent`].
#[inline]
pub const fn propagates_expand(self) -> bool {
(self.0 & (Self::MULTILINE_MASK | Self::NO_EXPAND_MASK)) == Self::MULTILINE_MASK
(self.flags & (Self::MULTILINE_MASK | Self::NO_EXPAND_MASK)) == Self::MULTILINE_MASK
}
}
@@ -573,6 +612,13 @@ mod tests {
debug_assert!(width.is_multiline());
}
#[test]
fn from_text_saturates_but_measure_does_not() {
let text = "a".repeat(70_000);
debug_assert_eq!(TextWidth::from_text(&text, indent_width(2)).value(), u32::from(u16::MAX));
debug_assert_eq!(TextWidth::measure(&text, indent_width(2)), 70_000);
}
#[test]
fn from_text_uses_indent_width_for_tabs() {
let width = TextWidth::from_text("\t", indent_width(4));
@@ -724,8 +770,8 @@ mod tests {
let w = indent_width(4);
for &s in &cases {
debug_assert_eq!(
TextWidth::from_text(s, w).0,
from_text_slow(s, w).0,
TextWidth::from_text(s, w),
from_text_slow(s, w),
"from_text mismatch for {s:?}"
);
}
@@ -748,8 +794,8 @@ mod tests {
#[expect(clippy::cast_possible_truncation)]
let expected = TextWidth::single(n.width() as u32);
debug_assert_eq!(
TextWidth::from_non_whitespace_str(n).0,
expected.0,
TextWidth::from_non_whitespace_str(n),
expected,
"from_non_whitespace_str mismatch for {n:?}"
);
}
@@ -77,6 +77,13 @@ pub enum Tag {
EndMarkAsRoot,
}
// `Tag` must fit next to the tag of [crate::FormatElement] (see the size assertion in `format_element/mod.rs`),
// so its payloads keep it 4-byte aligned: no `u64` or plain pointer (use `packed(4)` like [Prefix]).
const _: () = assert!(
size_of::<Tag>() <= 12 && align_of::<Tag>() <= 4,
"`Tag` exceeds 12 bytes or 4-byte alignment, `FormatElement` would exceed 16 bytes"
);
impl Tag {
/// Returns `true` if `self` is any start tag.
pub const fn is_start(&self) -> bool {
@@ -261,8 +268,10 @@ impl Align {
/// The string a [Tag::StartPrefix] prints after the indention on every new line.
///
/// Prefixes are syntax tokens (`"> "`, `" * "`) or syntax columns ([Self::spaces]), so `'static`;
/// the double reference keeps the payload one pointer wide (see the size assertion in `format_element/mod.rs`).
/// the double reference keeps the payload one pointer wide, and `packed(4)` keeps [Tag] 4-byte aligned
/// (see the size assertion next to [Tag]).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(C, packed(4))]
pub struct Prefix(pub(crate) &'static &'static str);
/// The widest single space prefix, wider runs chain several (see [Prefix::spaces]).
@@ -315,7 +324,7 @@ impl Prefix {
/// Registering the name also asserts that no two labels share a `value`.
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct LabelId {
value: u64,
value: u32,
}
impl std::fmt::Debug for LabelId {
@@ -344,9 +353,9 @@ mod debug_names {
/// All `(value, name)` pairs ever registered.
/// Labels are few (one `Label` impl per language with a handful of variants), so a linear scan is fine.
static NAMES: Mutex<Vec<(u64, &'static str)>> = Mutex::new(Vec::new());
static NAMES: Mutex<Vec<(u32, &'static str)>> = Mutex::new(Vec::new());
pub(super) fn record(value: u64, name: &'static str) {
pub(super) fn record(value: u32, name: &'static str) {
let mut names = NAMES.lock().unwrap();
if let Some((_, existing_name)) = names.iter().find(|(existing, _)| *existing == value) {
assert_eq!(
@@ -358,7 +367,7 @@ mod debug_names {
}
}
pub(super) fn lookup(value: u64) -> Option<&'static str> {
pub(super) fn lookup(value: u32) -> Option<&'static str> {
NAMES
.lock()
.unwrap()
@@ -370,8 +379,8 @@ mod debug_names {
/// Defines the valid labels of a language. You want to have at most one implementation per formatter
/// project.
pub trait Label {
/// Returns the `u64` uniquely identifying this specific label.
fn id(&self) -> u64;
/// Returns the `u32` uniquely identifying this specific label.
fn id(&self) -> u32;
/// Returns the name of the label that is shown in debug builds.
fn debug_name(&self) -> &'static str;
@@ -0,0 +1,93 @@
use std::{fmt, marker::PhantomData, ops::Deref, ptr::NonNull};
/// A `&'a [T]` stored as a pointer and a `u32` length.
///
/// It takes 12 bytes and is aligned to 4 bytes, where a `&[T]` takes 16 bytes.
/// This leaves room for the tag of [`crate::FormatElement`], which keeps the element at 16 bytes.
#[repr(C, packed(4))]
pub struct ThinSlice<'a, T> {
ptr: NonNull<T>,
len: u32,
_marker: PhantomData<&'a [T]>,
}
impl<'a, T> ThinSlice<'a, T> {
/// # Panics
/// If `slice` is longer than `u32::MAX`.
#[inline]
pub const fn new(slice: &'a [T]) -> Self {
assert!(slice.len() <= u32::MAX as usize, "slice longer than `u32::MAX`");
#[expect(clippy::cast_possible_truncation)]
Self { ptr: NonNull::from_ref(slice).cast(), len: slice.len() as u32, _marker: PhantomData }
}
#[inline]
pub fn get(self) -> &'a [T] {
// SAFETY: `ptr` and `len` come from a `&'a [T]` in `new`
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len as usize) }
}
}
impl<T> Clone for ThinSlice<'_, T> {
#[inline]
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ThinSlice<'_, T> {}
impl<T> Deref for ThinSlice<'_, T> {
type Target = [T];
#[inline]
fn deref(&self) -> &Self::Target {
(*self).get()
}
}
impl<T: PartialEq> PartialEq for ThinSlice<'_, T> {
fn eq(&self, other: &Self) -> bool {
**self == **other
}
}
impl<T: Eq> Eq for ThinSlice<'_, T> {}
impl<T: fmt::Debug> fmt::Debug for ThinSlice<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
/// A `&'a str` stored as a pointer and a `u32` length, see `ThinSlice`.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ThinStr<'a>(ThinSlice<'a, u8>);
impl<'a> ThinStr<'a> {
#[inline]
pub(crate) const fn new(text: &'a str) -> Self {
Self(ThinSlice::new(text.as_bytes()))
}
#[inline]
pub fn as_str(self) -> &'a str {
// SAFETY: The bytes come from a `&'a str` in `new`
unsafe { std::str::from_utf8_unchecked(self.0.get()) }
}
}
impl Deref for ThinStr<'_> {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
(*self).as_str()
}
}
impl fmt::Debug for ThinStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
@@ -142,7 +142,6 @@ pub fn dispatch_ir<'a, C: FormatContext>(
/// it either pushes replacement elements into the output and returns `true`, or returns `false` to keep the element unchanged.
/// A shared `Interned` subtree is rebuilt once and the rebuilt element re-shared.
/// Nothing is rebuilt until the first change, an IR with none is returned as is.
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
pub fn map_text_in_ir<'a, F>(
ir: ArenaVec<'a, FormatElement<'a>>,
allocator: &'a Allocator,
@@ -156,7 +155,6 @@ where
}
/// Returns `None` if no element changed.
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
#[expect(clippy::option_option)]
fn map_text_in_ir_impl<'a, F>(
ir: &[FormatElement<'a>],
@@ -174,7 +172,7 @@ where
for (index, element) in ir.iter().enumerate() {
let changed = match element {
FormatElement::Text { text, .. } | FormatElement::Token { text } => {
map_text(text, &mut out)
map_text(text.as_str(), &mut out)
}
FormatElement::BestFitting(best_fitting) => {
let mut changed = false;
@@ -249,10 +247,7 @@ pub fn push_text_with_literal_lines<'a>(
out.push(FormatElement::Line(LineMode::Literal));
}
if !line.is_empty() {
out.push(FormatElement::Text {
text: line,
width: TextWidth::from_text(line, indent_width),
});
out.push(FormatElement::text(line, TextWidth::from_text(line, indent_width)));
}
}
}
@@ -1,4 +1,3 @@
#![expect(clippy::mutable_key_type)]
use std::{fmt::Debug, ops::Deref};
use rustc_hash::FxHashMap;
@@ -10,7 +10,7 @@ use oxc_allocator::ArenaVec;
use crate::{
Argument, Arguments, Format, FormatContext, FormatElement, FormatOptions, Formatter, GroupId,
format_element::{
self, LineMode, PrintMode, TextWidth,
self, LineMode, PrintMode, TextWidth, ThinStr,
tag::{self, Condition, DedentMode, GroupMode, LabelId, Tag},
},
};
@@ -244,7 +244,7 @@ impl<T> std::fmt::Debug for FormatOnce<T> {
#[inline]
pub fn token(text: &'static str) -> Token {
debug_assert_token_ascii_only_and_no_linebreaks(text);
Token { text }
Token { text: ThinStr::new(text) }
}
fn debug_assert_token_ascii_only_and_no_linebreaks(text: &str) {
@@ -256,10 +256,11 @@ fn debug_assert_token_ascii_only_and_no_linebreaks(text: &str) {
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct Token {
text: &'static str,
text: ThinStr<'static>,
}
impl<C> Format<'_, C> for Token {
#[inline]
fn fmt(&self, f: &mut Formatter<'_, '_, C>) {
f.write_element(FormatElement::Token { text: self.text });
}
@@ -267,7 +268,7 @@ impl<C> Format<'_, C> for Token {
impl std::fmt::Debug for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::write!(f, "Token({})", self.text)
std::write!(f, "Token({})", self.text.as_str())
}
}
@@ -314,12 +315,13 @@ impl<'a, C> Format<'a, C> for Text<'a>
where
C: FormatContext,
{
#[inline]
fn fmt(&self, f: &mut Formatter<'_, 'a, C>) {
let width = self
.width
.unwrap_or_else(|| TextWidth::from_text(self.text, f.options().indent_width()));
let width = if self.expand_parent { width } else { width.without_expand_parent() };
f.write_element(FormatElement::Text { text: self.text, width });
f.write_element(FormatElement::text(self.text, width));
}
}
@@ -53,9 +53,9 @@ macro_rules! format_args {
/// assert_eq!(
/// f.into_vec().as_slice(),
/// [
/// FormatElement::Token { text: "Hello" },
/// FormatElement::token("Hello"),
/// FormatElement::Space,
/// FormatElement::Token { text: "World" },
/// FormatElement::token("World"),
/// ]
/// );
/// ```
@@ -93,7 +93,6 @@ impl<'ast, C> FormatState<'ast, C> {
self.session.group_id_builder().group_id(debug_name)
}
#[expect(clippy::mutable_key_type)]
pub fn printed_interned_elements(&mut self) -> &mut FxHashMap<Interned<'ast>, usize> {
&mut self.printed_interned_elements
}
+1 -4
View File
@@ -96,10 +96,7 @@ fn format_embedded(source: &str, options: CssFormatOptions) -> String {
embedded.ir.iter().map(|element| match element {
FormatElement::EmbedPlaceholder(index) => {
let text = allocator.alloc_str(&std::format!("`PLACEHOLDER-{index}`"));
FormatElement::Text {
text,
width: TextWidth::from_text(text, options.indent_width),
}
FormatElement::text(text, TextWidth::from_text(text, options.indent_width))
}
other => other.clone(),
}),
@@ -35,7 +35,7 @@ pub fn write_table<'a>(table: &'a Table<'a>, f: &mut MarkdownFormatter<'_, 'a>)
for (i, cell) in row.children.iter().enumerate() {
let text =
collect_phrasing(&cell.children, InlineParent::default(), f).into_str(allocator);
let width = TextWidth::from_text(text, f.options().indent_width).value() as usize;
let width = TextWidth::measure(text, f.options().indent_width) as usize;
widths[i] = widths[i].max(width);
cells.push(Cell { text, width });
}
@@ -3,74 +3,74 @@ checker.ts:
sys allocs: 18979
sys reallocs: 494
sys deallocs: 18978
sys alloc bytes: 6581369 # 6.58 MB
sys alloc bytes: 6244889 # 6.24 MB
sys peak growth: 2973842 # 2.97 MB
arena allocs: 36968
arena reallocs: 927
arena size: 51409112 # 51.41 MB
arena size: 38665048 # 38.67 MB
App.tsx:
file size: 415342 # 415.34 kB
sys allocs: 5177
sys reallocs: 89
sys deallocs: 5176
sys alloc bytes: 1293782 # 1.29 MB
sys alloc bytes: 1130046 # 1.13 MB
sys peak growth: 435478 # 435.48 kB
arena allocs: 11512
arena reallocs: 364
arena size: 9480664 # 9.48 MB
arena size: 7117368 # 7.12 MB
RadixUIAdoptionSection.jsx:
file size: 2520 # 2.52 kB
sys allocs: 68
sys reallocs: 52
sys deallocs: 67
sys alloc bytes: 36388 # 36.39 kB
sys alloc bytes: 28124 # 28.12 kB
sys peak growth: 12728 # 12.73 kB
arena allocs: 57
arena reallocs: 0
arena size: 76280 # 76.28 kB
arena size: 57944 # 57.94 kB
pdf.mjs:
file size: 567296 # 567.30 kB
sys allocs: 9736
sys reallocs: 167
sys deallocs: 9735
sys alloc bytes: 2642168 # 2.64 MB
sys alloc bytes: 2384856 # 2.38 MB
sys peak growth: 1156488 # 1.16 MB
arena allocs: 20583
arena reallocs: 428
arena size: 14139600 # 14.14 MB
arena size: 11029152 # 11.03 MB
antd.js:
file size: 6686316 # 6.69 MB
sys allocs: 74785
sys reallocs: 351
sys deallocs: 74784
sys alloc bytes: 25342050 # 25.34 MB
sys alloc bytes: 23137418 # 23.14 MB
sys peak growth: 13710144 # 13.71 MB
arena allocs: 120676
arena reallocs: 1889
arena size: 156478528 # 156.48 MB
arena size: 114800840 # 114.80 MB
binder.ts:
file size: 193077 # 193.08 kB
sys allocs: 968
sys reallocs: 35
sys deallocs: 967
sys alloc bytes: 341493 # 341.49 kB
sys alloc bytes: 320893 # 320.89 kB
sys peak growth: 195197 # 195.20 kB
arena allocs: 2092
arena reallocs: 38
arena size: 3353016 # 3.35 MB
arena size: 2545448 # 2.55 MB
kitchen-sink.tsx:
file size: 732898 # 732.90 kB
sys allocs: 12610
sys reallocs: 398
sys deallocs: 12609
sys alloc bytes: 4178154 # 4.18 MB
sys alloc bytes: 3737938 # 3.74 MB
sys peak growth: 1491884 # 1.49 MB
arena allocs: 30666
arena reallocs: 1041
arena size: 18552208 # 18.55 MB
arena size: 14614680 # 14.61 MB