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
3 changes: 1 addition & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ jobs:
- name: Prefetch dependencies
run: |
cargo fetch --locked
cargo +"${MSRV}" fetch --locked

- name: Check formatting
run: cargo fmt --all -- --check
Expand All @@ -86,7 +85,7 @@ jobs:
run: cargo check --offline --lib --no-default-features --features comments --target wasm32v1-none

- name: Check MSRV
run: cargo +"${MSRV}" check --offline --all-targets --locked
run: cargo +"${MSRV}" check --lib --all-features --locked

- name: Verify crate packaging
run: cargo publish --dry-run
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ target
*.swp
/perf.*
/coverage.sh
/TEAM.md
/OPTIMIZE.md
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

## 2.1.0

**Performance**

- Avoid temporary allocations when moving post-colon comments, folding quoted-scalar
newlines, resolving common tag components, and tracking block-scalar line breaks.
- Reuse parser comment buffers when collecting comments and ordering them around empty nodes.
- Batch long literal and folded scalar lines for character and fallible iterator inputs.
- Scan unescaped Unicode quoted-scalar runs in chunks for string input, preserving borrowed
values, printable-character validation, and character and byte positions.
- Add the defaulted `Input::take_quoted_scalar_chunk` hook, returning consumed text and its
Unicode character count. Existing custom inputs retain their ASCII chunk behavior without
implementing the new method.
- Extend CodSpeed coverage with scalar and comment workloads, including Unicode, iterator
inputs, common tags, multiline quotes, chomping, and comments around empty nodes.

**Support user cases**

- Add `Options::emit_anchor_events` (default `false`) to emit `Event::Anchor(id, name)`
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ exclude = [
"/tests/fuzz_targets.rs",
# We don't need the documents either.
"/documents",
# Codspeed is internal tool
# Benchmarks are internal tools
"/tools/codspeed"
]

Expand Down
20 changes: 20 additions & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,26 @@ pub trait Input {
""
}

/// Consume an ordinary printable run inside a quoted scalar and return its character count.
///
/// Like [`Self::take_quoted_scalar_ascii_chunk`], this optional hook leaves matching quotes,
/// double-quoted escapes, ASCII whitespace, and invalid control characters for the scanner.
/// It may additionally consume printable non-ASCII characters. The returned count must be
/// the number of Unicode scalar values, not bytes. A non-empty slice must exactly match the
/// consumed source prefix; an empty slice must consume nothing and have a count of zero.
/// Source byte offsets advance by the slice's byte length. No prior lookahead is required,
/// buffered characters must not be skipped, and callers refresh lookahead before peeking.
///
/// YAML printable characters are tab, LF, CR, U+0020–U+007E, U+0085, U+00A0–U+D7FF,
/// U+E000–U+FFFD, and U+10000–U+10FFFF. Tab, LF, CR, and space still end this run so the
/// scanner can fold whitespace. U+0085, U+2028, and U+2029 remain ordinary content.
/// The default preserves existing ASCII hook overrides and otherwise consumes nothing.
#[inline]
fn take_quoted_scalar_chunk(&mut self, single: bool) -> (&str, usize) {
let chunk = self.take_quoted_scalar_ascii_chunk(single);
(chunk, chunk.len())
}

/// Fetch a chunk of plain scalar characters.
///
/// This optimization method allows the input to batch process characters.
Expand Down
41 changes: 41 additions & 0 deletions src/input/buffered.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::char_traits::is_breakz;
use crate::error::ErrorKind;
use crate::input::{BorrowedInput, Input};

use alloc::string::String;
use arraydeque::ArrayDeque;

/// The size of the [`BufferedInput`] buffer.
Expand Down Expand Up @@ -159,6 +160,41 @@ impl<T: Iterator<Item = char>> Input for BufferedInput<T> {
}
}

fn fetch_block_scalar_line(&mut self, out: &mut String) -> usize {
let mut consumed = 0;

// Drain existing lookahead before reading directly from the iterator. Keep both
// real delimiters and EOF padding at the front of the logical stream.
while let Some(character) = self.buffer.front().copied() {
if is_breakz(character) {
self.fill_lookahead();
return consumed;
}
self.pop_buffered();
out.push(character);
consumed += 1;
}

if !self.source_exhausted {
for character in self.input.by_ref() {
if is_breakz(character) {
self.buffer.push_back(character).unwrap();
self.real_buffered += 1;
self.fill_lookahead();
return consumed;
}
out.push(character);
consumed += 1;
}
self.source_exhausted = true;
}

// Restore the active window once for the entire line, including at EOF. A
// fallible iterator's first error stays latched in its adapter during this refill.
self.fill_lookahead();
consumed
}

#[inline]
fn skip(&mut self) {
self.skip_one();
Expand Down Expand Up @@ -284,6 +320,11 @@ impl<T: Iterator<Item = Result<char, ErrorKind>>> Input for FallibleBufferedInpu
self.inner.raw_read_non_breakz_ch()
}

#[inline]
fn fetch_block_scalar_line(&mut self, out: &mut String) -> usize {
self.inner.fetch_block_scalar_line(out)
}

#[inline]
fn skip(&mut self) {
self.inner.skip();
Expand Down
37 changes: 37 additions & 0 deletions src/input/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,43 @@ impl Input for StrInput<'_> {
chunk
}

#[inline]
fn take_quoted_scalar_chunk(&mut self, single: bool) -> (&str, usize) {
// Keep the byte-only fast path for ASCII runs. Unicode runs can also include the
// following ASCII text, avoiding repeated scanner and marker updates at boundaries.
if self.buffer.as_bytes().first().map_or(true, u8::is_ascii) {
let chunk = self.take_quoted_scalar_ascii_chunk(single);
return (chunk, chunk.len());
}

let quote = if single { b'\'' } else { b'"' };
let bytes = self.buffer.as_bytes();
let mut end = 0;
let mut count = 0;
while end < bytes.len() {
let byte = bytes[end];
if byte.is_ascii() {
// ASCII tails use the same byte predicate as the ASCII-led fast path.
if !(b'!'..=b'~').contains(&byte) || byte == quote || (!single && byte == b'\\') {
break;
}
end += 1;
} else {
// `end` stays on a UTF-8 boundary. YAML blanks, breaks, quotes and
// escapes are ASCII, so only printability needs checking here.
let character = self.buffer[end..].chars().next().unwrap();
if !crate::char_traits::is_printable(character) {
break;
}
end += character.len_utf8();
}
count += 1;
}
let (chunk, remaining) = self.buffer.split_at(end);
self.buffer = remaining;
(chunk, count)
}

fn fetch_plain_scalar_chunk(
&mut self,
out: &mut String,
Expand Down
Loading
Loading