use std::sync::Arc;
use rayon::prelude::*;
use crate::{
application::{
chunking::chunk_spans,
config::{ProcessingError, ProcessingResult, ProcessorConfig},
},
domain::language::config::{get_language_config, LanguageConfig},
domain::state::{
adjust_for_toggles, rebase_candidate, scan_chunk, Candidate, CandidateVec, CompiledRules,
PartialState, ToggleVec,
},
domain::types::DepthVec,
};
use super::execution_mode::ExecutionMode;
pub struct DeltaStackResult {
pub boundaries: Vec<usize>,
pub chunk_count: usize,
pub thread_count: usize,
}
pub struct DeltaStackProcessor {
rules: Arc<CompiledRules>,
chunk_size: usize,
}
impl DeltaStackProcessor {
pub fn from_language_code(
config: ProcessorConfig,
code: &str,
) -> Result<Self, ProcessingError> {
let language = get_language_config(code).map_err(|e| ProcessingError::InvalidConfig {
reason: e.to_string(),
})?;
Self::from_language_config(config, language)
}
pub fn from_language_config(
config: ProcessorConfig,
language: &LanguageConfig,
) -> Result<Self, ProcessingError> {
let rules =
CompiledRules::from_config(language).map_err(|e| ProcessingError::InvalidConfig {
reason: e.to_string(),
})?;
Ok(Self {
rules: Arc::new(rules),
chunk_size: config.chunk_size,
})
}
pub fn process(&self, text: &str, mode: ExecutionMode) -> ProcessingResult<DeltaStackResult> {
if text.is_empty() {
return Ok(DeltaStackResult {
boundaries: Vec::new(),
chunk_count: 0,
thread_count: 1,
});
}
let chunks = chunk_spans(text, self.chunk_size);
let chunk_count = chunks.len();
let thread_count = mode.determine_thread_count(text.len());
let pool = if thread_count > 1 {
Some(
rayon::ThreadPoolBuilder::new()
.num_threads(thread_count)
.build()
.map_err(|e| ProcessingError::InvalidConfig {
reason: format!("Failed to create thread pool: {e}"),
})?,
)
} else {
None
};
let rules = self.rules.as_ref();
let mut states: Vec<PartialState> = if let Some(pool) = &pool {
pool.install(|| {
chunks
.par_iter()
.map(|chunk| scan_chunk(chunk, rules))
.collect()
})
} else {
chunks
.iter()
.map(|chunk| scan_chunk(chunk, rules))
.collect()
};
let mut bulk: Vec<CandidateVec> = Vec::with_capacity(chunk_count);
let mut chunk_starts: Vec<usize> = Vec::with_capacity(chunk_count);
let mut prefix: Vec<(DepthVec, u32)> = Vec::with_capacity(chunk_count);
let mut toggles_by_chunk: Vec<ToggleVec> = vec![ToggleVec::new(); chunk_count];
let mut acc = PartialState::identity();
for (i, state) in states.iter_mut().enumerate() {
bulk.push(std::mem::take(&mut state.boundaries));
chunk_starts.push(acc.chunk_len);
prefix.push((acc.deltas.clone(), acc.parity));
let toggles = acc.absorb(state, rules);
assign_toggles(&mut toggles_by_chunk, &chunk_starts, toggles, i);
}
let (mut acc, edge_toggles) = acc.resolve_edges_full(rules);
assign_toggles(
&mut toggles_by_chunk,
&chunk_starts,
edge_toggles,
chunk_count - 1,
);
let extras: Vec<Candidate> = acc.boundaries.drain(..).collect();
let reduce_chunk = |i: usize| -> Vec<usize> {
let (deltas, parity) = &prefix[i];
let toggles = &toggles_by_chunk[i];
bulk[i]
.iter()
.filter_map(|c| {
let mut c = rebase_candidate(c, chunk_starts[i], deltas, *parity);
adjust_for_toggles(
&mut c.local_depths,
&mut c.local_parity,
c.local_offset,
toggles,
);
is_boundary(&c).then_some(c.local_offset)
})
.collect()
};
let per_chunk: Vec<Vec<usize>> = if let Some(pool) = &pool {
pool.install(|| (0..chunk_count).into_par_iter().map(reduce_chunk).collect())
} else {
(0..chunk_count).map(reduce_chunk).collect()
};
let mut extra_offsets: Vec<usize> = extras
.iter()
.filter(|c| is_boundary(c))
.map(|c| c.local_offset)
.collect();
extra_offsets.sort_unstable();
let total: usize = per_chunk.iter().map(Vec::len).sum::<usize>() + extra_offsets.len();
let mut boundaries = Vec::with_capacity(total);
let mut extras_iter = extra_offsets.into_iter().peekable();
for chunk_offsets in per_chunk {
for off in chunk_offsets {
while extras_iter.peek().is_some_and(|&e| e < off) {
boundaries.push(extras_iter.next().unwrap());
}
boundaries.push(off);
}
}
boundaries.extend(extras_iter);
boundaries.dedup();
Ok(DeltaStackResult {
boundaries,
chunk_count,
thread_count,
})
}
}
fn is_boundary(c: &Candidate) -> bool {
c.local_parity == 0 && c.local_depths.iter().all(|&d| d <= 0)
}
fn assign_toggles(
by_chunk: &mut [ToggleVec],
chunk_starts: &[usize],
toggles: ToggleVec,
upto: usize,
) {
for (q, slot) in toggles {
let from = chunk_starts[..=upto].partition_point(|&s| s <= q) - 1;
for list in &mut by_chunk[from..=upto] {
list.push((q, slot));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_processor() -> DeltaStackProcessor {
DeltaStackProcessor::from_language_code(ProcessorConfig::default(), "en").unwrap()
}
#[test]
fn test_empty_text() {
let processor = create_test_processor();
let result = processor.process("", ExecutionMode::Sequential).unwrap();
assert!(result.boundaries.is_empty());
assert_eq!(result.chunk_count, 0);
assert_eq!(result.thread_count, 1);
}
#[test]
fn test_single_sentence() {
let processor = create_test_processor();
let text = "This is a sentence.";
let result = processor.process(text, ExecutionMode::Sequential).unwrap();
assert_eq!(result.boundaries.len(), 1);
assert_eq!(result.boundaries[0], 19); assert_eq!(result.chunk_count, 1);
assert_eq!(result.thread_count, 1);
}
#[test]
fn test_parallel_vs_sequential() {
let processor = create_test_processor();
let text = "First sentence. Second sentence. Third sentence.";
let seq_result = processor.process(text, ExecutionMode::Sequential).unwrap();
let par_result = processor
.process(text, ExecutionMode::Parallel { threads: Some(2) })
.unwrap();
assert_eq!(seq_result.boundaries, par_result.boundaries);
assert_eq!(seq_result.chunk_count, par_result.chunk_count);
assert_eq!(seq_result.thread_count, 1);
assert_eq!(par_result.thread_count, 2);
}
#[test]
fn test_unknown_language_code() {
let err = DeltaStackProcessor::from_language_code(ProcessorConfig::default(), "zz");
assert!(err.is_err());
}
}