1use crate::parser::{parse_chunk, parse_prefix, Chunk};
25use crate::references::ReferenceCheck;
26use crate::{parse_with, DataSection, Exchange, ParseOptions, ParseOutcome, StepError};
27
28pub fn parse_parallel_with(
44 input: &[u8],
45 options: ParseOptions,
46 threads: usize,
47) -> Result<ParseOutcome, StepError> {
48 if threads <= 1 || !crate::is_step_file(input) {
49 return parse_with(input, options);
50 }
51 try_parallel(input, options, threads).map_or_else(|| parse_with(input, options), Ok)
52}
53
54fn try_parallel(input: &[u8], options: ParseOptions, threads: usize) -> Option<ParseOutcome> {
56 let (header, data_start) = parse_prefix(input, options).ok()??;
57 let mut diagnostics = Vec::new();
58 let bounds = boundaries(input, data_start, threads);
59 let starts = std::iter::once(data_start).chain(bounds.iter().copied());
60 let ends = bounds
61 .iter()
62 .copied()
63 .map(Some)
64 .chain(std::iter::once(None));
65 let slices: Vec<(usize, Option<usize>)> = starts.zip(ends).collect();
66
67 let chunks: Vec<Result<Chunk, StepError>> = std::thread::scope(|scope| {
68 let handles: Vec<_> = slices
69 .iter()
70 .map(|&(start, end)| scope.spawn(move || parse_chunk(input, options, start, end)))
71 .collect();
72 handles
73 .into_iter()
74 .map(|handle| {
75 handle
76 .join()
77 .unwrap_or_else(|panic| std::panic::resume_unwind(panic))
78 })
79 .collect()
80 });
81
82 let mut parsed = Vec::with_capacity(chunks.len());
83 for chunk in chunks {
84 let chunk = chunk.ok()?;
85 if !chunk.aligned {
86 return None;
87 }
88 parsed.push(chunk);
89 }
90
91 let mut references = options.check_references.then(ReferenceCheck::default);
92 let mut records = Vec::with_capacity(parsed.iter().map(|chunk| chunk.records.len()).sum());
93 for chunk in parsed {
94 diagnostics.extend(chunk.diagnostics);
99 for (record, span) in chunk.records.into_iter().zip(chunk.spans) {
100 if let Some(check) = &mut references {
101 check.record(&record, span, &mut diagnostics);
102 }
103 records.push(record);
104 }
105 }
106 if let Some(check) = references {
107 check.finish(&mut diagnostics);
108 }
109
110 Some(ParseOutcome {
111 exchange: Exchange {
112 header,
113 data: DataSection { records },
114 },
115 diagnostics,
116 })
117}
118
119fn boundaries(input: &[u8], data_start: usize, threads: usize) -> Vec<usize> {
123 let step = (input.len() - data_start) / threads;
124 let mut out = Vec::with_capacity(threads - 1);
125 let mut from = data_start;
126 for k in 1..threads {
127 let target = data_start + step * k;
128 if target <= from {
129 continue;
130 }
131 match next_boundary(input, target) {
132 Some(boundary) => {
133 out.push(boundary);
134 from = boundary;
135 }
136 None => break,
137 }
138 }
139 out
140}
141
142fn next_boundary(input: &[u8], from: usize) -> Option<usize> {
143 let mut position = from;
144 while let Some(offset) = memchr::memchr(b';', &input[position..]) {
145 let after = position + offset + 1;
146 let rest = &input[after..];
147 let blank = rest
148 .iter()
149 .take_while(|byte| byte.is_ascii_whitespace())
150 .count();
151 if rest.get(blank) == Some(&b'#') && rest.get(blank + 1).is_some_and(u8::is_ascii_digit) {
152 return Some(after);
153 }
154 position = after;
155 }
156 None
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{boundaries, next_boundary, try_parallel};
162 use crate::parser::parse_chunk;
163 use crate::{parse_with, ParseOptions};
164 use std::fmt::Write as _;
165
166 fn plain(n: usize) -> Vec<u8> {
169 let mut out = String::from(
170 "ISO-10303-21;HEADER;FILE_DESCRIPTION(('d'),'2;1');\
171FILE_NAME('n','t',('a'),('o'),'p','s','z');FILE_SCHEMA(('IFC4'));ENDSEC;DATA;\n",
172 );
173 for id in 1..=n {
174 writeln!(out, "#{id}=IFCPOINT(({id}.,#{}));", n + 1 - id)
175 .expect("writing to a String cannot fail");
176 }
177 out.push_str("ENDSEC;END-ISO-10303-21;\n");
178 out.into_bytes()
179 }
180
181 #[test]
182 fn clean_input_takes_the_parallel_path_and_matches() {
183 let input = plain(2_000);
184 for options in [
185 ParseOptions::strict(),
186 ParseOptions::lenient().check_references(true),
187 ] {
188 for threads in [2, 4, 8] {
189 let parallel =
190 try_parallel(&input, options, threads).expect("clean input must not fall back");
191 assert_eq!(
192 Ok(parallel),
193 parse_with(&input, options),
194 "threads={threads}"
195 );
196 }
197 }
198 }
199
200 #[test]
201 fn a_slice_whose_end_is_inside_a_record_stops_there_unaligned() {
202 let input = plain(2_000);
203 let text = std::str::from_utf8(&input).expect("ascii");
204 let start = text.find("#1=").expect("first record");
205 let middle = text.find("#1000=").expect("record 1000") + 3;
206 let chunk = parse_chunk(&input, ParseOptions::strict(), start, Some(middle))
207 .expect("parses up to the overshoot");
208 assert!(!chunk.aligned);
209 assert_eq!(chunk.records.len(), 1_000);
212 }
213
214 #[test]
215 fn a_boundary_is_just_past_a_semicolon_before_an_id() {
216 let input: &[u8] = b"DATA;#1=A();\n #2=B('x;y');#3=C();";
217 let after = |needle: &[u8]| {
218 input
219 .windows(needle.len())
220 .position(|window| window == needle)
221 .expect("needle present")
222 + needle.len()
223 };
224 assert_eq!(next_boundary(input, 0), Some(after(b"DATA;")));
225 assert_eq!(next_boundary(input, 6), Some(after(b"#1=A();")));
226 assert_eq!(next_boundary(input, 16), Some(after(b"'x;y');")));
229 assert_eq!(next_boundary(input, input.len() - 3), None);
230 }
231
232 #[test]
233 fn boundaries_are_strictly_increasing_and_bounded() {
234 let mut input = b"DATA;".to_vec();
235 for id in 1..=100 {
236 input.extend_from_slice(format!("#{id}=A({id});").as_bytes());
237 }
238 for threads in [2, 3, 8, 64, 500] {
239 let cuts = boundaries(&input, 5, threads);
240 assert!(cuts.len() < threads);
241 assert!(cuts.windows(2).all(|pair| pair[0] < pair[1]));
242 assert!(cuts.iter().all(|&cut| cut > 5 && cut < input.len()));
243 assert!(cuts.iter().all(|&cut| input[cut - 1] == b';'));
244 }
245 }
246}