mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user