Skip to content

Commit a1321c3

Browse files
committed
perf: refactor rope to iter
1 parent 23ac553 commit a1321c3

File tree

8 files changed

+217
-118
lines changed

8 files changed

+217
-118
lines changed

src/cached_source.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,15 @@ impl Source for CachedSource {
9090
// so transmuting Vec<&str> to Vec<&'static str> is safe in this context.
9191
// This allows us to store string slices in the cache without additional allocations.
9292
unsafe {
93-
std::mem::transmute::<Vec<&str>, Vec<&'static str>>(self.rope())
93+
std::mem::transmute::<Vec<&str>, Vec<&'static str>>(
94+
self.rope().collect(),
95+
)
9496
}
9597
});
9698
SourceValue::String(Cow::Owned(rope.join("")))
9799
}
98100

99-
fn rope(&self) -> Vec<&str> {
101+
fn rope(&self) -> Box<dyn Iterator<Item = &str> + '_> {
100102
self.inner.rope()
101103
}
102104

src/concat_source.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,12 @@ impl Source for ConcatSource {
175175
}
176176
}
177177

178-
fn rope(&self) -> Vec<&str> {
178+
fn rope(&self) -> Box<dyn Iterator<Item = &str> + '_> {
179179
let children = self.optimized_children();
180180
if children.len() == 1 {
181181
children[0].rope()
182182
} else {
183-
children
184-
.iter()
185-
.flat_map(|child| child.rope())
186-
.collect::<Vec<_>>()
183+
Box::new(children.iter().flat_map(|child| child.rope()))
187184
}
188185
}
189186

src/original_source.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ impl Source for OriginalSource {
5656
SourceValue::String(Cow::Borrowed(&self.value))
5757
}
5858

59-
fn rope(&self) -> Vec<&str> {
60-
vec![self.value.as_ref()]
59+
fn rope(&self) -> Box<dyn Iterator<Item = &str> + '_> {
60+
Box::new(std::iter::once(self.value.as_ref()))
6161
}
6262

6363
fn buffer(&self) -> Cow<[u8]> {

src/raw_source.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ impl Source for RawStringSource {
6464
SourceValue::String(Cow::Borrowed(&self.0))
6565
}
6666

67-
fn rope(&self) -> Vec<&str> {
68-
vec![self.0.as_ref()]
67+
fn rope(&self) -> Box<dyn Iterator<Item = &str> + '_> {
68+
Box::new(std::iter::once(self.0.as_ref()))
6969
}
7070

7171
fn buffer(&self) -> Cow<[u8]> {
@@ -214,8 +214,8 @@ impl Source for RawBufferSource {
214214
SourceValue::Buffer(Cow::Borrowed(&self.value))
215215
}
216216

217-
fn rope(&self) -> Vec<&str> {
218-
vec![self.get_or_init_value_as_string()]
217+
fn rope(&self) -> Box<dyn Iterator<Item = &str> + '_> {
218+
Box::new(std::iter::once(self.get_or_init_value_as_string()))
219219
}
220220

221221
fn buffer(&self) -> Cow<[u8]> {

src/replace_source.rs

Lines changed: 199 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -161,109 +161,24 @@ impl ReplaceSource {
161161

162162
impl Source for ReplaceSource {
163163
fn source(&self) -> SourceValue {
164-
let rope = self.rope();
165-
if rope.len() == 1 {
166-
SourceValue::String(Cow::Borrowed(rope[0]))
167-
} else {
168-
SourceValue::String(Cow::Owned(rope.join("")))
164+
if self.replacements.is_empty() {
165+
return self.inner.source();
169166
}
170-
}
171-
172-
fn rope(&self) -> Vec<&str> {
173-
let inner_rope = self.inner.rope();
174-
let mut rope =
175-
Vec::with_capacity(inner_rope.len() + self.replacements.len() * 2);
176-
177-
let mut pos: usize = 0;
178-
let mut replacement_idx: usize = 0;
179-
let mut replacement_end: Option<usize> = None;
180-
let mut next_replacement: Option<usize> = (replacement_idx
181-
< self.replacements.len())
182-
.then(|| self.replacements[replacement_idx].start as usize);
183-
184-
'chunk_loop: for chunk in inner_rope {
185-
let mut chunk_pos = 0;
186-
let end_pos = pos + chunk.len();
187-
188-
// Skip over when it has been replaced
189-
if let Some(replacement_end) =
190-
replacement_end.filter(|replacement_end| *replacement_end > pos)
191-
{
192-
// Skip over the whole chunk
193-
if replacement_end >= end_pos {
194-
pos = end_pos;
195-
continue;
196-
}
197-
// Partially skip over chunk
198-
chunk_pos = replacement_end - pos;
199-
pos += chunk_pos;
200-
}
201-
202-
// Is a replacement in the chunk?
203-
while let Some(next_replacement_pos) = next_replacement
204-
.filter(|next_replacement_pos| *next_replacement_pos < end_pos)
205-
{
206-
if next_replacement_pos > pos {
207-
// Emit chunk until replacement
208-
let offset = next_replacement_pos - pos;
209-
let chunk_slice = &chunk[chunk_pos..(chunk_pos + offset)];
210-
rope.push(chunk_slice);
211-
chunk_pos += offset;
212-
pos = next_replacement_pos;
213-
}
214-
// Insert replacement content split into chunks by lines
215-
let replacement = &self.replacements[replacement_idx];
216-
rope.push(&replacement.content);
217-
218-
// Remove replaced content by settings this variable
219-
replacement_end = if let Some(replacement_end) = replacement_end {
220-
Some(replacement_end.max(replacement.end as usize))
221-
} else {
222-
Some(replacement.end as usize)
223-
};
224-
225-
// Move to next replacement
226-
replacement_idx += 1;
227-
next_replacement = if replacement_idx < self.replacements.len() {
228-
Some(self.replacements[replacement_idx].start as usize)
229-
} else {
230-
None
231-
};
232-
233-
// Skip over when it has been replaced
234-
let offset = chunk.len() as i64 - end_pos as i64
235-
+ replacement_end.unwrap() as i64
236-
- chunk_pos as i64;
237-
if offset > 0 {
238-
// Skip over whole chunk
239-
if replacement_end
240-
.is_some_and(|replacement_end| replacement_end >= end_pos)
241-
{
242-
pos = end_pos;
243-
continue 'chunk_loop;
244-
}
245-
246-
// Partially skip over chunk
247-
chunk_pos += offset as usize;
248-
pos += offset as usize;
249-
}
250-
}
251-
252-
// Emit remaining chunk
253-
if chunk_pos < chunk.len() {
254-
rope.push(&chunk[chunk_pos..]);
255-
}
256-
pos = end_pos;
167+
let mut string = String::with_capacity(self.size());
168+
for chunk in self.rope() {
169+
string.push_str(chunk);
257170
}
171+
SourceValue::String(Cow::Owned(string))
172+
}
258173

259-
// Handle remaining replacements one by one
260-
while replacement_idx < self.replacements.len() {
261-
let content = &self.replacements[replacement_idx].content;
262-
rope.push(content);
263-
replacement_idx += 1;
174+
fn rope(&self) -> Box<dyn Iterator<Item = &str> + '_> {
175+
if self.replacements.is_empty() {
176+
return self.inner.rope();
264177
}
265-
266-
rope
178+
Box::new(ReplaceSourceRopeIterator::new(
179+
self.inner.rope(),
180+
&self.replacements,
181+
))
267182
}
268183

269184
fn buffer(&self) -> Cow<[u8]> {
@@ -897,6 +812,191 @@ impl PartialEq for ReplaceSource {
897812

898813
impl Eq for ReplaceSource {}
899814

815+
/// Iterator for ReplaceSource rope that applies replacements on the fly
816+
pub struct ReplaceSourceRopeIterator<'a> {
817+
inner_rope: Box<dyn Iterator<Item = &'a str> + 'a>,
818+
replacements: &'a [Replacement],
819+
820+
// Current state
821+
pos: usize,
822+
replacement_idx: usize,
823+
replacement_end: Option<usize>,
824+
825+
// Current chunk state
826+
current_chunk: Option<&'a str>,
827+
chunk_pos: usize,
828+
chunk_start_pos: usize,
829+
830+
// Buffer for remaining replacements
831+
remaining_replacements: std::collections::VecDeque<&'a str>,
832+
833+
// Flag to indicate if we're done with inner rope
834+
inner_exhausted: bool,
835+
}
836+
837+
impl<'a> ReplaceSourceRopeIterator<'a> {
838+
fn new(
839+
inner_rope: Box<dyn Iterator<Item = &'a str> + 'a>,
840+
replacements: &'a [Replacement],
841+
) -> Self {
842+
Self {
843+
inner_rope,
844+
replacements,
845+
pos: 0,
846+
replacement_idx: 0,
847+
replacement_end: None,
848+
current_chunk: None,
849+
chunk_pos: 0,
850+
chunk_start_pos: 0,
851+
remaining_replacements: std::collections::VecDeque::new(),
852+
inner_exhausted: false,
853+
}
854+
}
855+
856+
fn get_next_replacement_pos(&self) -> Option<usize> {
857+
if self.replacement_idx < self.replacements.len() {
858+
Some(self.replacements[self.replacement_idx].start as usize)
859+
} else {
860+
None
861+
}
862+
}
863+
864+
fn advance_to_next_chunk(&mut self) -> bool {
865+
if let Some(chunk) = self.inner_rope.next() {
866+
self.current_chunk = Some(chunk);
867+
self.chunk_pos = 0;
868+
self.chunk_start_pos = self.pos;
869+
true
870+
} else {
871+
self.inner_exhausted = true;
872+
false
873+
}
874+
}
875+
876+
fn process_current_chunk(&mut self) -> Option<&'a str> {
877+
let chunk = self.current_chunk?;
878+
let chunk_end_pos = self.chunk_start_pos + chunk.len();
879+
880+
// Skip over when it has been replaced
881+
if let Some(replacement_end) =
882+
self.replacement_end.filter(|&end| end > self.pos)
883+
{
884+
if replacement_end >= chunk_end_pos {
885+
// Skip entire chunk
886+
self.pos = chunk_end_pos;
887+
self.current_chunk = None;
888+
return None;
889+
}
890+
891+
// Partially skip chunk
892+
let skip_amount = replacement_end - self.pos;
893+
self.chunk_pos += skip_amount;
894+
self.pos += skip_amount;
895+
}
896+
897+
// Check for replacements in current chunk
898+
if let Some(next_replacement_pos) = self
899+
.get_next_replacement_pos()
900+
.filter(|&pos| pos < chunk_end_pos)
901+
{
902+
if next_replacement_pos > self.pos {
903+
// Emit chunk until replacement
904+
let offset = next_replacement_pos - self.pos;
905+
let chunk_slice = &chunk[self.chunk_pos..(self.chunk_pos + offset)];
906+
self.chunk_pos += offset;
907+
self.pos = next_replacement_pos;
908+
return Some(chunk_slice);
909+
}
910+
911+
// Add replacement content
912+
let replacement = &self.replacements[self.replacement_idx];
913+
self.remaining_replacements.push_back(&replacement.content);
914+
915+
// Update replacement_end
916+
self.replacement_end = Some(
917+
self
918+
.replacement_end
919+
.map(|end| end.max(replacement.end as usize))
920+
.unwrap_or(replacement.end as usize),
921+
);
922+
923+
// Move to next replacement
924+
self.replacement_idx += 1;
925+
926+
// Skip replaced content
927+
let offset = chunk.len() as i64 - chunk_end_pos as i64
928+
+ self.replacement_end.unwrap() as i64
929+
- self.chunk_pos as i64;
930+
931+
if offset > 0 {
932+
if self.replacement_end.is_some_and(|end| end >= chunk_end_pos) {
933+
// Skip entire remaining chunk
934+
self.pos = chunk_end_pos;
935+
self.current_chunk = None;
936+
return self.remaining_replacements.pop_front();
937+
}
938+
939+
// Partially skip chunk
940+
self.chunk_pos += offset as usize;
941+
self.pos += offset as usize;
942+
}
943+
944+
// Return the replacement content
945+
return self.remaining_replacements.pop_front();
946+
}
947+
948+
// Emit remaining chunk
949+
if self.chunk_pos < chunk.len() {
950+
let remaining = &chunk[self.chunk_pos..];
951+
self.pos = chunk_end_pos;
952+
self.current_chunk = None;
953+
Some(remaining)
954+
} else {
955+
self.pos = chunk_end_pos;
956+
self.current_chunk = None;
957+
None
958+
}
959+
}
960+
}
961+
962+
impl<'a> Iterator for ReplaceSourceRopeIterator<'a> {
963+
type Item = &'a str;
964+
965+
fn next(&mut self) -> Option<Self::Item> {
966+
// First, check if we have buffered replacement content
967+
if let Some(replacement) = self.remaining_replacements.pop_front() {
968+
return Some(replacement);
969+
}
970+
971+
// Process current chunk if we have one
972+
if self.current_chunk.is_some() {
973+
if let Some(result) = self.process_current_chunk() {
974+
return Some(result);
975+
}
976+
}
977+
978+
// Advance to next chunk if inner rope is not exhausted
979+
while !self.inner_exhausted {
980+
if !self.advance_to_next_chunk() {
981+
break;
982+
}
983+
984+
if let Some(result) = self.process_current_chunk() {
985+
return Some(result);
986+
}
987+
}
988+
989+
// Handle remaining replacements when inner rope is exhausted
990+
if self.replacement_idx < self.replacements.len() {
991+
let replacement = &self.replacements[self.replacement_idx];
992+
self.replacement_idx += 1;
993+
Some(&replacement.content)
994+
} else {
995+
None
996+
}
997+
}
998+
}
999+
9001000
#[cfg(test)]
9011001
mod tests {
9021002
use rustc_hash::FxHasher;

0 commit comments

Comments
 (0)