Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/oxc_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ pub use crate::{
options::{CodegenOptions, CommentOptions, LegalComment},
};

// Re-export `IndentChar` from `oxc_data_structures`
pub use oxc_data_structures::code_buffer::IndentChar;

/// Output from [`Codegen::build`]
#[non_exhaustive]
pub struct CodegenReturn {
Expand Down Expand Up @@ -171,6 +174,7 @@ impl<'a> Codegen<'a> {
#[must_use]
pub fn with_options(mut self, options: CodegenOptions) -> Self {
self.quote = if options.single_quote { Quote::Single } else { Quote::Double };
self.code = CodeBuffer::with_indent(options.indent_char, options.indent_width);
self.options = options;
self
}
Expand Down
31 changes: 29 additions & 2 deletions crates/oxc_codegen/src/options.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::path::PathBuf;

use oxc_data_structures::code_buffer::{DEFAULT_INDENT_WIDTH, IndentChar};

/// Codegen Options.
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
pub struct CodegenOptions {
/// Use single quotes instead of double quotes.
///
Expand All @@ -26,6 +28,29 @@ pub struct CodegenOptions {
///
/// Default is `None` - no sourcemap is produced.
pub source_map_path: Option<PathBuf>,

/// Indentation character.
///
/// Default is [`IndentChar::Tab`].
pub indent_char: IndentChar,

/// Number of characters per indentation level.
///
/// Default is `1`.
pub indent_width: usize,
}

impl Default for CodegenOptions {
fn default() -> Self {
Self {
single_quote: false,
minify: false,
comments: CommentOptions::default(),
source_map_path: None,
indent_char: IndentChar::default(),
indent_width: DEFAULT_INDENT_WIDTH,
}
}
}

impl CodegenOptions {
Expand All @@ -36,6 +61,8 @@ impl CodegenOptions {
minify: true,
comments: CommentOptions::disabled(),
source_map_path: None,
indent_char: IndentChar::default(),
indent_width: DEFAULT_INDENT_WIDTH,
}
}

Expand Down Expand Up @@ -92,7 +119,7 @@ pub struct CommentOptions {
/// * starts with `//!` or `/*!`.
/// * contains `/* @license */` or `/* @preserve */`
///
/// Default is [LegalComment::Inline].
/// Default is [`LegalComment::Inline`].
pub legal: LegalComment,
}

Expand Down
45 changes: 44 additions & 1 deletion crates/oxc_codegen/tests/integration/js.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use oxc_codegen::CodegenOptions;
use oxc_codegen::{CodegenOptions, IndentChar};

use crate::tester::{
test, test_minify, test_minify_same, test_options, test_same, test_with_parse_options,
Expand Down Expand Up @@ -605,3 +605,46 @@ fn v8_intrinsics() {
parse_opts,
);
}

#[test]
fn indentation() {
// Test default - tabs with width 1
test_options(
"if (true) {\nif (nested) {\nconsole.log('test');\n}\n}",
"if (true) {\n\tif (nested) {\n\t\tconsole.log(\"test\");\n\t}\n}\n",
CodegenOptions::default(),
);

// Test tabs with width 2
test_options(
"if (true) {\nif (nested) {\nconsole.log('test');\n}\n}",
"if (true) {\n\t\tif (nested) {\n\t\t\t\tconsole.log(\"test\");\n\t\t}\n}\n",
CodegenOptions {
indent_char: IndentChar::Tab,
indent_width: 2,
..CodegenOptions::default()
},
);

// Test spaces with width 2
test_options(
"if (true) {\nif (nested) {\nconsole.log('test');\n}\n}",
"if (true) {\n if (nested) {\n console.log(\"test\");\n }\n}\n",
CodegenOptions {
indent_char: IndentChar::Space,
indent_width: 2,
..CodegenOptions::default()
},
);

// Test spaces with width 4
test_options(
"if (true) {\nif (nested) {\nconsole.log('test');\n}\n}",
"if (true) {\n if (nested) {\n console.log(\"test\");\n }\n}\n",
CodegenOptions {
indent_char: IndentChar::Space,
indent_width: 4,
..CodegenOptions::default()
},
);
}
146 changes: 120 additions & 26 deletions crates/oxc_data_structures/src/code_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@ use std::iter;

use crate::assert_unchecked;

/// Character to use for indentation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum IndentChar {
/// Use tab character for indentation.
#[default]
Tab = b'\t',
/// Use space character for indentation.
Space = b' ',
}

/// Default indentation width.
pub const DEFAULT_INDENT_WIDTH: usize = 1;

/// A string builder for constructing source code.
///
/// `CodeBuffer` provides safe abstractions over a byte array.
/// [`CodeBuffer`] provides safe abstractions over a byte array.
/// Essentially same as `String` but with additional methods.
///
/// Use one of the various `print_*` methods to add text into the buffer.
Expand All @@ -31,14 +45,29 @@ use crate::assert_unchecked;
/// ```
///
/// [`into_string`]: CodeBuffer::into_string
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
pub struct CodeBuffer {
/// INVARIANT: `buf` is a valid UTF-8 string.
buf: Vec<u8>,
/// Character to use for indentation.
indent_char: IndentChar,
/// Number of indent characters per indentation level.
indent_width: usize,
}

impl Default for CodeBuffer {
#[inline]
fn default() -> Self {
Self {
buf: Vec::new(),
indent_char: IndentChar::default(),
indent_width: DEFAULT_INDENT_WIDTH,
}
}
}

impl CodeBuffer {
/// Create a new empty `CodeBuffer`.
/// Create a new [`CodeBuffer`].
///
/// # Example
/// ```
Expand All @@ -54,7 +83,22 @@ impl CodeBuffer {
Self::default()
}

/// Create a new, empty `CodeBuffer` with the specified capacity.
/// Create a new [`CodeBuffer`] with specified indentation.
///
/// # Example
/// ```
/// # use oxc_data_structures::code_buffer::{CodeBuffer, IndentChar};
/// let mut code = CodeBuffer::with_indent(IndentChar::Space, 4);
///
/// // This will use 4 spaces per indentation level
/// code.print_indent(2); // prints 8 spaces
/// ```
#[inline]
pub fn with_indent(indent_char: IndentChar, indent_width: usize) -> Self {
Self { buf: Vec::new(), indent_char, indent_width }
}

/// Create a new [`CodeBuffer`] with the specified capacity.
///
/// The buffer will be able to hold at least `capacity` bytes without reallocating.
/// This method is allowed to allocate for more bytes than `capacity`.
Expand All @@ -64,10 +108,34 @@ impl CodeBuffer {
/// minimum *capacity* specified, the buffer will have a zero *length*.
///
/// # Panics
/// Panics if the new capacity exceeds `isize::MAX` bytes.
/// Panics if `capacity` exceeds `isize::MAX` bytes.
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self { buf: Vec::with_capacity(capacity) }
Self {
buf: Vec::with_capacity(capacity),
indent_char: IndentChar::default(),
indent_width: DEFAULT_INDENT_WIDTH,
}
}

/// Create a new [`CodeBuffer`] with the specified capacity and indentation.
///
/// The buffer will be able to hold at least `capacity` bytes without reallocating.
/// This method is allowed to allocate for more bytes than `capacity`.
/// If `capacity` is 0, the buffer will not allocate.
///
/// It is important to note that although the returned buffer has the
/// minimum *capacity* specified, the buffer will have a zero *length*.
///
/// # Panics
/// Panics if `capacity` exceeds `isize::MAX` bytes.
#[inline]
pub fn with_capacity_and_indent(
capacity: usize,
indent_char: IndentChar,
indent_width: usize,
) -> Self {
Self { buf: Vec::with_capacity(capacity), indent_char, indent_width }
}

/// Returns the number of bytes in the buffer.
Expand Down Expand Up @@ -411,51 +479,56 @@ impl CodeBuffer {
self.buf.extend(bytes);
}

/// Print `n` tab characters into the buffer (indentation).
/// Print `depth` levels of indentation into the buffer.
///
/// Uses the configured indentation character and width.
/// Prints `depth * indent_width` indent characters.
///
/// Optimized on assumption that more that 16 levels of indentation is rare.
/// Optimized on assumption that more than 16 characters of indentation is rare.
///
/// Fast path is to write 16 bytes of tabs in a single load + store,
/// but only advance `len` by `n` bytes. This avoids a `memset` function call.
/// Fast path is to write 16 bytes of tabs/spaces in a single load + store,
/// but only advance `len` by the actual number of bytes. This avoids a `memset` function call.
///
/// Take alternative slow path if either:
/// 1. `n > 16`.
/// 1. Total characters to print > 16.
/// 2. Less than 16 bytes spare capacity in buffer (needs to grow).
/// Both of these cases should be rare.
///
/// <https://godbolt.org/z/e1EP5cnPc>
/// <https://godbolt.org/z/zPT6Mzqsx>
#[inline]
pub fn print_indent(&mut self, n: usize) {
pub fn print_indent(&mut self, depth: usize) {
/// Size of chunks to write indent in.
/// 16 is largest register size (XMM) available on all x86_84 targets.
/// 16 is largest register size (XMM) available on all x86_64 targets.
const CHUNK_SIZE: usize = 16;

#[cold]
#[inline(never)]
fn write_slow(code_buffer: &mut CodeBuffer, n: usize) {
code_buffer.buf.extend(iter::repeat_n(b'\t', n));
fn write_slow(code_buffer: &mut CodeBuffer, bytes: usize) {
code_buffer.buf.extend(iter::repeat_n(code_buffer.indent_char as u8, bytes));
}

let bytes = depth * self.indent_width;

let len = self.len();
let spare_capacity = self.capacity() - len;
if n > CHUNK_SIZE || spare_capacity < CHUNK_SIZE {
write_slow(self, n);
if bytes > CHUNK_SIZE || spare_capacity < CHUNK_SIZE {
write_slow(self, bytes);
return;
}

// Write 16 tabs into buffer.
// On x86_86, this is 1 XMM register load + 1 XMM store (16 byte copy).
// Write 16 bytes of the indent character into buffer.
// On x86_64, this is 4 SIMD instructions (16 byte copy).
// SAFETY: We checked there are at least 16 bytes spare capacity.
unsafe {
let ptr = self.buf.as_mut_ptr().add(len).cast::<[u8; CHUNK_SIZE]>();
ptr.write([b'\t'; CHUNK_SIZE]);
ptr.write([self.indent_char as u8; CHUNK_SIZE]);
}

// Update length of buffer.
// SAFETY: We checked there's at least 16 bytes spare capacity, and `n <= 16`,
// so `len + n` cannot exceed capacity.
// `len` cannot exceed `isize::MAX`, so `len + n` cannot wrap around.
unsafe { self.buf.set_len(len + n) };
// SAFETY: We checked there's at least 16 bytes spare capacity, and `bytes <= 16`,
// so `len + bytes` cannot exceed capacity.
// `len` cannot exceed `isize::MAX`, so `len + bytes` cannot wrap around.
unsafe { self.buf.set_len(len + bytes) };
}

/// Get contents of buffer as a byte slice.
Expand Down Expand Up @@ -512,7 +585,7 @@ impl From<CodeBuffer> for String {

#[cfg(test)]
mod test {
use super::CodeBuffer;
use super::{CodeBuffer, IndentChar};

#[test]
fn empty() {
Expand Down Expand Up @@ -642,4 +715,25 @@ mod test {
code.print_str("bar");
assert_eq!(code.last_char(), Some('r'));
}

#[test]
fn test_cached_indent_tabs() {
let mut code = CodeBuffer::with_indent(IndentChar::Tab, 1);
code.print_indent(2);
assert_eq!(code.into_string(), "\t\t");
}

#[test]
fn test_cached_indent_spaces_width_2() {
let mut code = CodeBuffer::with_indent(IndentChar::Space, 2);
code.print_indent(2);
assert_eq!(code.into_string(), " "); // 2 levels * 2 spaces = 4 spaces
}

#[test]
fn test_cached_indent_spaces_width_4() {
let mut code = CodeBuffer::with_indent(IndentChar::Space, 4);
code.print_indent(2);
assert_eq!(code.into_string(), " "); // 2 levels * 4 spaces = 8 spaces
}
}
Loading