From af54fb2aba46af538b4564045ba40ba7b854d1d3 Mon Sep 17 00:00:00 2001 From: Skgland Date: Sun, 26 Apr 2026 13:43:04 +0200 Subject: [PATCH 1/9] optimize put_back_char Always encode the char directly into the buffer. Only shift the buffer content if there isn't enough room in the front. --- src/parser/char_reader.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index 69d592e1..1ad6971e 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -235,16 +235,17 @@ impl CharRead for CharReader { #[inline(always)] fn put_back_char(&mut self, c: char) { let c_len = c.len_utf8(); + if c_len <= self.pos { + self.pos -= c_len; + } else { + self.buf.insert_from_slice( + 0, + &[0u8; 4/* char::MAX_LEN_UTF8 once msrv reached 1.93 */][..c_len - self.pos], + ); + self.pos = 0; + } - let mut shifted_slice = SmallVec::<[u8; 32]>::new(); - shifted_slice.extend_from_slice(&self.buf[self.pos..]); - - self.buf.clear(); - self.buf.resize(c_len, 0); - c.encode_utf8(&mut self.buf[..c_len]); - - self.buf.extend_from_slice(&shifted_slice); - self.pos = 0; + c.encode_utf8(&mut self.buf[self.pos..]); } #[inline(always)] From 48a34aa2cc2ae321af47787ab99197afd3219611 Mon Sep 17 00:00:00 2001 From: Skgland Date: Thu, 30 Apr 2026 19:01:34 +0200 Subject: [PATCH 2/9] add a test with bad utf-8 --- src/parser/char_reader.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index 1ad6971e..caffc7c1 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -380,6 +380,33 @@ mod tests { assert!(read_string.read_char().is_none()); } + #[test] + fn interspersed_bad_utf8() { + let mut read_string = CharReader::new(Cursor::new(b"a string\xffmore_text\xff")); + + for c in "a string".chars() { + assert_eq!(read_string.peek_char().unwrap().ok(), Some(c)); + assert_eq!(read_string.read_char().unwrap().ok(), Some(c)); + } + + assert_eq!( + read_string.peek_char().unwrap().unwrap_err().kind(), + std::io::ErrorKind::InvalidData + ); + + for c in "more_text".chars() { + assert_eq!(read_string.peek_char().unwrap().ok(), Some(c)); + assert_eq!(read_string.read_char().unwrap().ok(), Some(c)); + } + + assert_eq!( + read_string.peek_char().unwrap().unwrap_err().kind(), + std::io::ErrorKind::InvalidData + ); + + assert!(read_string.read_char().is_none()); + } + #[test] fn greek_string() { let mut read_string = CharReader::new(Cursor::new("λέξη")); From 327a677a7c3270bab0046bcabd8d60eebbcce178 Mon Sep 17 00:00:00 2001 From: Skgland Date: Thu, 30 Apr 2026 19:02:27 +0200 Subject: [PATCH 3/9] use a function rather than a closure --- src/parser/char_reader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index caffc7c1..334a75f5 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -135,7 +135,7 @@ impl CharRead for CharReader { Err(e) => return Some(Err(e)), } - let bad_bytes_error = |buf: &[u8]| { + fn bad_bytes_error(buf: &[u8]) -> std::io::Error { // If we have 4 bytes that still don't make up // a valid code point, then we have garbage. From f8ddd78776c88d86d1d421ff7f80a6bd2430b952 Mon Sep 17 00:00:00 2001 From: Skgland Date: Thu, 30 Apr 2026 19:44:31 +0200 Subject: [PATCH 4/9] optimize CharReader --- src/parser/char_reader.rs | 170 +++++++++++++++++++++----------------- 1 file changed, 93 insertions(+), 77 deletions(-) diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index 334a75f5..62cc8156 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -102,19 +102,27 @@ impl CharReader { } impl CharReader { + pub fn read_chunck(&mut self) -> io::Result { + let mut chunk = [0u8; 8 * 1024]; + let nread = self.inner.read(&mut chunk)?; + self.buf.extend_from_slice(&chunk[..nread]); + Ok(nread) + } + pub fn refresh_buffer(&mut self) -> io::Result<&[u8]> { // If we've reached the end of our internal buffer then we need to fetch // some more data from the underlying reader. // Branch using `>=` instead of the more correct `==` // to tell the compiler that the pos..cap slice is always valid. if self.pos >= self.buf.len() { - self.buf.clear(); + // make some space in buf + if self.buf.len() > 4 { + // keep 4 bytes so that put_back_char can put back at least one char + self.buf.drain(4..); + } + self.pos = self.buf.len(); - let mut chunk = [0u8; 8 * 1024]; - let nread = self.inner.read(&mut chunk)?; - - self.buf.extend_from_slice(&chunk[..nread]); - self.pos = 0; + self.read_chunck()?; } Ok(&self.buf[self.pos..]) @@ -143,93 +151,83 @@ impl CharRead for CharReader { // leading bytes until either the buffer is // empty, or we have a valid code point. - let mut split_point = 1; - let mut badbytes = vec![]; + // note we might have a sequence of invalid bytes followed by valid bytes followed by invalid bytes - loop { - let (bad, rest) = buf.split_at(split_point); + let err = str::from_utf8(buf).expect_err("the start of buf should be invalid utf-8"); + assert_eq!(err.valid_up_to(), 0, "the error should be a prefix"); - if rest.is_empty() || str::from_utf8(rest).is_ok() { - badbytes.extend_from_slice(bad); - break; - } + let invalid_prefix = err.error_len().expect("we should have at least 4 bytes"); - split_point += 1; - } + let bad_bytes = buf[..invalid_prefix].to_vec(); // Raise the error. If we still have data in // the buffer, it will be returned on the next // loop. - io::Error::new(io::ErrorKind::InvalidData, BadUtf8Error { bytes: badbytes }) - }; + io::Error::new( + io::ErrorKind::InvalidData, + BadUtf8Error { bytes: bad_bytes }, + ) + } - loop { + // while we haven't consumed all bytes from the buffer + while self.pos < self.buf.len() { + // buf must be non-empty let buf = &self.buf[self.pos..]; - if !buf.is_empty() { - let e = match str::from_utf8(buf) { - Ok(s) => { - let mut chars = s.chars(); - let c = chars.next().unwrap(); + // we need at most 4 bytes for a char so don't decode the whole buffer + // as it can be quite large and we are going to discard the remaining chars anyway + // if there is a valid prefix + let prefix = if buf.len() > 4 { &buf[..4] } else { buf }; - return Some(Ok(c)); - } - Err(e) => e, - }; + let e = match str::from_utf8(prefix) { + Ok(s) => { + let mut chars = s.chars(); + let c = chars.next().expect( + "a non-empty buffer that is valid utf-8 contains at least one character", + ); - if buf.len() - e.valid_up_to() >= 4 { - return Some(Err(bad_bytes_error(buf))); - } else if self.pos >= self.buf.len() { - return None; - } else if self.buf.len() - self.pos >= 4 && self.pos < e.valid_up_to() { - return match str::from_utf8(&self.buf[self.pos..self.pos + e.valid_up_to()]) { - Ok(s) => { - let mut chars = s.chars(); - let c = chars.next().unwrap(); - - Some(Ok(c)) - } - Err(e) => { - let badbytes = self.buf[self.pos..self.pos + e.valid_up_to()].to_vec(); - - Some(Err(io::Error::new( - io::ErrorKind::InvalidData, - BadUtf8Error { bytes: badbytes }, - ))) - } - }; - } else { - let buf_len = self.buf.len(); - - for (c, idx) in (self.pos..buf_len).enumerate() { - self.buf[c] = self.buf[idx]; - } - - self.buf.truncate(buf_len - self.pos); - - let buf_len = self.buf.len(); - self.pos = 0; - - if buf_len >= 4 { - continue; - } - - let mut word = [0u8; 4]; - let word_slice = &mut word[buf_len..4]; - - match self.inner.read(word_slice) { - Err(e) => return Some(Err(e)), - Ok(0) => return Some(Err(bad_bytes_error(&self.buf))), - Ok(nread) => { - self.buf.extend_from_slice(&word_slice[0..nread]); - } - } + return Some(Ok(c)); + } + Err(e) => e, + }; + + if e.valid_up_to() != 0 { + // the valid prefix is non-empty so it is guaranteed that we can decode at least one char + let c = str::from_utf8(&prefix[..e.valid_up_to()]) + .expect("prefix is verified valid up to this point") + .chars() + .next() + .expect("the valid prefix was non-empty"); + return Some(Ok(c)); + } + + if e.error_len().is_some() { + return Some(Err(bad_bytes_error(buf))); + } + + // buf is too short to deterin if the remaining bytes in buf are a valid char + // i.e. the content of bufg is a prefix of a valid utf-8 encoded char + // + // we need to read more data from the underlying stream + // so that we can determin its validity + + if self.buf.len() > 4 { + // keep a prefix of 4 bytes so that we put back at least one char + self.buf.drain(4..self.pos); + self.pos = 4; + } + + match self.read_chunck() { + Err(e) => return Some(Err(e)), + Ok(0) => return Some(Err(bad_bytes_error(&self.buf))), + Ok(_) => { + // successfully filled the buffer with another chuck of data } - } else { - return None; } } + + None } #[inline(always)] @@ -394,6 +392,15 @@ mod tests { std::io::ErrorKind::InvalidData ); + let err = read_string + .read_char() + .unwrap() + .unwrap_err() + .downcast::() + .unwrap(); + + read_string.consume(err.bytes.len()); + for c in "more_text".chars() { assert_eq!(read_string.peek_char().unwrap().ok(), Some(c)); assert_eq!(read_string.read_char().unwrap().ok(), Some(c)); @@ -404,6 +411,15 @@ mod tests { std::io::ErrorKind::InvalidData ); + let err = read_string + .read_char() + .unwrap() + .unwrap_err() + .downcast::() + .unwrap(); + + read_string.consume(err.bytes.len()); + assert!(read_string.read_char().is_none()); } From 89dde3f3707a1868e23a2fbf0d43fca7dbb6de26 Mon Sep 17 00:00:00 2001 From: Skgland Date: Thu, 30 Apr 2026 19:54:39 +0200 Subject: [PATCH 5/9] un-ignore lorem_ipsum tests as they are no longer slow --- src/parser/char_reader.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index 62cc8156..e3d08087 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -448,7 +448,6 @@ mod tests { } #[test] - #[cfg_attr(miri, ignore = "slow and not very relevant")] fn greek_lorem_ipsum() { let lorem_ipsum = "Λορεμ ιπσθμ δολορ σιτ αμετ, οφφενδιτ εφφιcιενδι σιτ ει, ηαρθμ λεγερε qθαερενδθμ ιθσ νε. Ηασ νο εροσ @@ -520,7 +519,6 @@ mod tests { } #[test] - #[cfg_attr(miri, ignore = "slow and not very relevant")] fn armenian_lorem_ipsum() { let lorem_ipsum = "լոռեմ իպսում դոլոռ սիթ ամեթ, նովում գռաեծո սեա եա, աբհոռռեանթ դիսպութանդո եի քուի. իդ քուոդ ինդոծթում @@ -594,7 +592,6 @@ mod tests { } #[test] - #[cfg_attr(miri, ignore = "slow and not very relevant")] fn russian_lorem_ipsum() { let lorem_ipsum = "Лорем ипсум долор сит амет, атяуи дицам еи сит, ид сеа фацилис елаборарет. Меа еу яуас алияуид, те яуи From 6b9a291f4e07051f20a3dfe88d479829156b4b95 Mon Sep 17 00:00:00 2001 From: Skgland Date: Sat, 23 May 2026 11:28:45 +0200 Subject: [PATCH 6/9] fix spelling --- src/parser/char_reader.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index e3d08087..508a8759 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -102,7 +102,7 @@ impl CharReader { } impl CharReader { - pub fn read_chunck(&mut self) -> io::Result { + pub fn read_chunk(&mut self) -> io::Result { let mut chunk = [0u8; 8 * 1024]; let nread = self.inner.read(&mut chunk)?; self.buf.extend_from_slice(&chunk[..nread]); @@ -122,7 +122,7 @@ impl CharReader { } self.pos = self.buf.len(); - self.read_chunck()?; + self.read_chunk()?; } Ok(&self.buf[self.pos..]) @@ -218,7 +218,7 @@ impl CharRead for CharReader { self.pos = 4; } - match self.read_chunck() { + match self.read_chunk() { Err(e) => return Some(Err(e)), Ok(0) => return Some(Err(bad_bytes_error(&self.buf))), Ok(_) => { From 54b303490547c07a142be6f071373cc4f8dbd726 Mon Sep 17 00:00:00 2001 From: Skgland Date: Sun, 24 May 2026 10:25:24 +0200 Subject: [PATCH 7/9] remove out-dated comment and fix spelling/grammar --- src/parser/char_reader.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index 508a8759..ee7aa824 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -160,10 +160,6 @@ impl CharRead for CharReader { let bad_bytes = buf[..invalid_prefix].to_vec(); - // Raise the error. If we still have data in - // the buffer, it will be returned on the next - // loop. - io::Error::new( io::ErrorKind::InvalidData, BadUtf8Error { bytes: bad_bytes }, @@ -206,14 +202,14 @@ impl CharRead for CharReader { return Some(Err(bad_bytes_error(buf))); } - // buf is too short to deterin if the remaining bytes in buf are a valid char - // i.e. the content of bufg is a prefix of a valid utf-8 encoded char + // buf is too short to determin if the remaining bytes in buf are a valid char + // i.e. the content of buf is a prefix of a valid utf-8 encoded char // // we need to read more data from the underlying stream // so that we can determin its validity if self.buf.len() > 4 { - // keep a prefix of 4 bytes so that we put back at least one char + // keep a prefix of 4 bytes so that we can put back at least one char self.buf.drain(4..self.pos); self.pos = 4; } From 6e50268efb4d3660318ea135998441d0bca435c0 Mon Sep 17 00:00:00 2001 From: Skgland Date: Sun, 24 May 2026 10:30:30 +0200 Subject: [PATCH 8/9] fix spelling again --- src/parser/char_reader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index ee7aa824..af1f0b6a 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -202,7 +202,7 @@ impl CharRead for CharReader { return Some(Err(bad_bytes_error(buf))); } - // buf is too short to determin if the remaining bytes in buf are a valid char + // buf is too short to determine if the remaining bytes in buf are a valid char // i.e. the content of buf is a prefix of a valid utf-8 encoded char // // we need to read more data from the underlying stream From 6c7d5e82785731396da16053d580b759f0ac4df8 Mon Sep 17 00:00:00 2001 From: Skgland Date: Mon, 25 May 2026 16:39:37 +0200 Subject: [PATCH 9/9] more spelling fixes --- src/machine/dispatch.rs | 4 ++-- src/parser/char_reader.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/machine/dispatch.rs b/src/machine/dispatch.rs index bcc270a3..90a65cb1 100644 --- a/src/machine/dispatch.rs +++ b/src/machine/dispatch.rs @@ -1623,7 +1623,7 @@ impl Machine { } let Some(inst) = self.code.get(self.machine_st.p) else { - // a seperate function marked #[cold] to make the compiler/branch-predictor prefer the happy path + // a separate function marked #[cold] to make the compiler/branch-predictor prefer the happy path handle_code_index_oob(self.code.len(), self.machine_st.p); }; @@ -6038,7 +6038,7 @@ impl Machine { } } -#[cold] // this is a seperate function so that we can annotate it as cold +#[cold] // this is a separate function so that we can annotate it as cold #[track_caller] fn handle_code_index_oob(code_len: usize, p: usize) -> ! { panic!("code pointer p = {p} is oob for code area of size {code_len}"); diff --git a/src/parser/char_reader.rs b/src/parser/char_reader.rs index af1f0b6a..d9e41203 100644 --- a/src/parser/char_reader.rs +++ b/src/parser/char_reader.rs @@ -206,7 +206,7 @@ impl CharRead for CharReader { // i.e. the content of buf is a prefix of a valid utf-8 encoded char // // we need to read more data from the underlying stream - // so that we can determin its validity + // so that we can determine its validity if self.buf.len() > 4 { // keep a prefix of 4 bytes so that we can put back at least one char @@ -218,7 +218,7 @@ impl CharRead for CharReader { Err(e) => return Some(Err(e)), Ok(0) => return Some(Err(bad_bytes_error(&self.buf))), Ok(_) => { - // successfully filled the buffer with another chuck of data + // successfully filled the buffer with another chunk of data } } }