use std::sync::mpsc;
use std::time::{Duration, Instant};
use crate::ast::Pattern;
use crate::engine::{Span, scan_tokens_from};
use crate::lexer::lex_with_blobs;
use crate::token::{Token, TokenKind};
const CHUNK_BYTES: usize = 32 * 1024;
const MAX_CHUNKS: usize = 64;
pub struct GrainTiming {
pub producer_busy: Duration,
pub consumer_busy: Duration,
pub wall: Duration,
pub chunks: usize,
}
impl GrainTiming {
#[must_use]
pub fn overlap(&self) -> Duration {
(self.producer_busy + self.consumer_busy).saturating_sub(self.wall)
}
#[must_use]
pub fn overlapped(&self) -> bool {
self.overlap() > Duration::ZERO
}
}
#[must_use]
pub fn scan_dual_grain(pattern: &Pattern, input: &[u8]) -> (Vec<Span>, GrainTiming) {
let defers = pattern.depends_on_whole_input();
let target = (input.len() / CHUNK_BYTES).clamp(2, MAX_CHUNKS);
let bounds = crate::parallel_lex::safe_boundaries(input, target);
let ranges: Vec<(usize, usize)> = bounds.windows(2).map(|w| (w[0], w[1])).collect();
let blobs = crate::lexer::blob_runs(input);
let (tx, rx) = mpsc::channel::<ChunkMsg>();
let wall_start = Instant::now();
let (matches, producer_busy, consumer_busy) = std::thread::scope(|sc| {
let ranges_ref = &ranges;
let blobs_ref = &blobs;
let producer = sc.spawn(move || {
let mut busy = Duration::ZERO;
for &(s, e) in ranges_ref {
let t = Instant::now();
let chunk_blobs = crate::parallel_lex::chunk_blobs(blobs_ref, s, e);
let mut toks = lex_with_blobs(
&input[s..e],
&chunk_blobs,
(e - s) / crate::lexer::TOKEN_BYTES_ESTIMATE,
);
for tk in &mut toks {
tk.shift(s);
tk.set_mate(None);
}
busy += t.elapsed();
if tx.send(ChunkMsg { byte_end: e, toks }).is_err() {
break;
}
}
busy
});
let consumer = sc.spawn(move || consume(pattern, input, defers, &rx));
let producer_busy = producer.join().expect("byte grain panicked");
let (matches, consumer_busy) = consumer.join().expect("token grain panicked");
(matches, producer_busy, consumer_busy)
});
let wall = wall_start.elapsed();
(matches, GrainTiming { producer_busy, consumer_busy, wall, chunks: ranges.len() })
}
struct ChunkMsg {
byte_end: usize,
toks: Vec<Token>,
}
fn consume(
pattern: &Pattern,
input: &[u8],
defers: bool,
rx: &mpsc::Receiver<ChunkMsg>,
) -> (Vec<Span>, Duration) {
let mut busy = Duration::ZERO;
let mut acc: Vec<Token> = Vec::new();
let mut open_stack: Vec<usize> = Vec::new();
let mut committed_tok = 0usize;
let mut committed_byte = 0usize;
let mut out: Vec<Span> = Vec::new();
let mut marks: Vec<(usize, usize)> = Vec::new();
for msg in rx {
let t = Instant::now();
stitch_chunk(&mut acc, &mut open_stack, msg.toks);
marks.push((msg.byte_end, acc.len()));
if !defers {
let found = resume(pattern, input, &acc, committed_tok);
let candidates = marks.len().saturating_sub(1);
if let Some((cut_byte, cut_tok)) = marks
.iter()
.take(candidates)
.rev()
.find(|&&(b, _)| b > committed_byte && !found.iter().any(|m| m.start() < b && m.end() > b))
.copied()
{
for m in &found {
if m.end() <= cut_byte {
out.push(*m);
}
}
committed_byte = cut_byte;
committed_tok = cut_tok;
}
}
busy += t.elapsed();
}
let t = Instant::now();
out.extend(resume(pattern, input, &acc, committed_tok));
busy += t.elapsed();
(out, busy)
}
fn resume(pattern: &Pattern, input: &[u8], toks: &[Token], from: usize) -> Vec<Span> {
crate::nfa::scan_nfa_over_serial_from(pattern, input, toks, from)
.unwrap_or_else(|| scan_tokens_from(pattern, input, toks, from))
}
fn stitch_chunk(acc: &mut Vec<Token>, open_stack: &mut Vec<usize>, toks: Vec<Token>) {
for mut tk in toks {
let idx = acc.len();
match tk.kind {
TokenKind::Open(_) => open_stack.push(idx),
TokenKind::Close(bk) => {
if let Some(&open_idx) = open_stack.last()
&& acc[open_idx].kind == TokenKind::Open(bk)
{
open_stack.pop();
acc[open_idx].set_mate(Some(idx));
tk.set_mate(Some(open_idx));
}
}
_ => {}
}
acc.push(tk);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
use crate::scan;
fn assert_dual_equals_single(pattern_src: &str, input: &str) {
let pat = parse(pattern_src).expect("pattern parses");
let single = scan(&pat, input.as_bytes());
let (dual, _) = scan_dual_grain(&pat, input.as_bytes());
assert_eq!(dual, single, "dual-grain differs from single-grain on {pattern_src:?}");
}
#[test]
fn dual_equals_single_where_the_two_engines_read_an_empty_iteration_apart() {
const INPUT: &str = "bar 771 baz baz ";
for pattern in [
r"(\N? | \W)+ .",
r"(\W? | \N)+ .",
r"(\N? | \W?)+ .",
r"(\W | \N?)+ .",
r"(\N | \W)+ .",
r"(\N?)+ .",
] {
assert_dual_equals_single(pattern, INPUT);
}
}
#[test]
fn dual_equals_single_on_blob_bearing_input() {
const ALPHA: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut input = Vec::new();
let mut x = 0x1234_5678u64;
for i in 0..4000 {
input.extend_from_slice(
format!("the quick brown fox jumps over the lazy dog line {i}\n").as_bytes(),
);
for _ in 0..64 {
x = x
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
input.push(ALPHA[((x >> 58) % 64) as usize]);
}
input.push(b'\n');
}
let pat = parse("\\W \\W").expect("pattern parses");
let single = scan(&pat, &input);
let (dual, timing) = scan_dual_grain(&pat, &input);
assert!(timing.chunks > 1, "the input must actually split");
assert_eq!(dual, single, "dual-grain differs from single-grain on blob input");
}
#[test]
fn dual_equals_single_over_mixed_patterns() {
let cases: &[(&str, &str)] = &[
("<`[a-z]+`:t>.*</=t>", "<div>hi</div>\n<span>yo</span>\n<DIV>no</DIV>\n"),
("`\\d+`:n =n", "12 12 ok\n34 56 no\n77 77 yes\n"),
("`[A-Z]+` \\N", "ABC 12\nxy 9\nDEF 34\n"),
("\\W ~\"END\"", "begin END\nmiddle here\nlast END now\n"),
("\\d \\W", "12 kg\nab cd\n9 m\n"),
];
for (pat, input) in cases {
assert_dual_equals_single(pat, input);
}
}
#[test]
fn dual_equals_single_on_structural_only() {
assert_dual_equals_single("\\N \\W", "weight 12 kg\nlen 5 m\nmass 9 g\n");
assert_dual_equals_single("\\W\\B(.*)", "call f(g(x))\nrun h(k)\ntail\n");
}
#[test]
fn greedy_match_spanning_a_chunk_seam_is_not_truncated() {
assert_dual_equals_single(".*", "a b c d e f\ng h i j k l\nm n o p q r\n");
assert_dual_equals_single("<\\W:t>.*</=t>", "<x>\naaa bbb ccc\nddd eee fff\n</x>\n");
}
#[test]
fn grains_agree_on_a_larger_input() {
let mut input = String::new();
for i in 0..20_000 {
input.push_str(&format!("row {i} val {} tag t{}\n", i * 7, i % 5));
}
let pat = parse("`[a-z]+` \\N").expect("pattern parses");
let (dual, timing) = scan_dual_grain(&pat, input.as_bytes());
let single = scan(&pat, input.as_bytes());
assert_eq!(dual, single, "dual-grain must match single-grain");
assert!(timing.chunks >= 2, "input should split into several chunks");
}
}