1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! The native fork/join scan + handoff-stitch merge behind
//! [`super::build_entity_index_parallel`]. Split out of `parallel_scan.rs`
//! per the repo's module-size convention (see `decoder.rs` /
//! `decoder/caches.rs`) — this is the bulk of that module's logic, not test
//! code, but the same "split rather than exceed the ratchet" rule applies.
use ;
use *;
use FxHashMap;
/// Below this DATA-section size the fork/join + serial-merge overhead
/// outweighs the scan win, so we run the serial scanner unchanged.
const PARALLEL_MIN_BYTES: usize = 8 * 1024 * 1024;
/// Target minimum bytes per chunk. Chunks are byte ranges, and scan cost is
/// ~proportional to bytes, so equal byte splits balance the work; this floor
/// keeps the chunk count sane on merely-large (not huge) files.
const MIN_CHUNK_BYTES: usize = 2 * 1024 * 1024;
/// The one place the parallel path reports a refusal: once per load, on
/// the stitched count, never per shard (#3395/#3430). Same rule for the
/// #3695 malformed-record stop, on the stitched flag `stitch` attributes.
pub
/// One chunk's speculative scan: every record with `start < range_end`, plus
/// the `start` of the first record at/after `range_end` (the next chunk's
/// first real entity). `records` is strictly increasing in `start`.
pub
/// [`stitch`]'s result: the merged index, the number of refusals it
/// ATTRIBUTED (never the raw per-shard sum — see [`stitch`]'s doc), and
/// whether it attributed a real #3695 malformed-record stop.
///
/// Named rather than a positional tuple so a caller reads `stitched.refused`
/// and `stitched.malformed` rather than `.1` / `.2` — the two fields are
/// different types here (`usize` / `bool`), but [`RescanResult`] right below
/// has two `Option<usize>` fields in the same shape a tuple would let a
/// caller swap silently, and consistency between the two return types
/// (both feed the same stitch loop) keeps that discipline from looking
/// like a special case.
pub
/// Scan with an explicit chunk count. Public within the crate so the
/// byte-identity and refusal-parity tests can force many boundary
/// positions (including inside a quoted string) on a small buffer.
///
/// `n_chunks == 1` is not special-cased HERE (though [`build`] takes a
/// serial shortcut before reaching this): one chunk spans the whole file,
/// starts at 0 (so the header-aware [`EntityScanner::new`] is selected)
/// and has no boundary, which is precisely the serial scan. Routing it
/// through the same shard+stitch machinery means the `n = 1` leg of the
/// byte-identity sweep exercises this code rather than delegating past it.
pub
/// Replay the chunks in file order into one index, and count the refusals
/// that are attributable to the bytes actually retained.
///
/// ## Why a refusal needs attributing at all
///
/// A refusal is a record the scanner dropped, so it leaves no trace in
/// `records` and the stitch cannot re-derive it. A chunk `i > 0` starts at
/// an arbitrary byte and can begin inside a quoted value, where a string
/// literal shaped like `#4294967297=IFCWALL(` reads as a record and gets
/// refused. Those refusals belong to the speculative prefix this stitch
/// throws away, so summing the chunks would report refusals on a file that
/// declares none.
///
/// ## The rule, and why it is exact
///
/// Chunk `i`'s retained region begins at `target` — chunk `i-1`'s
/// validated handoff, a REAL entity start — and chunk 0's begins at the
/// header skip. Scanner events advance `position` monotonically, so every
/// event a chunk emitted before the record at `target` sits strictly below
/// `target`; and from that record on, the chunk's `position` sequence is
/// the serial scanner's (the handoff is a real start and `find_entity_end`
/// re-parses the record from its `#`). Hence:
///
/// * refusals `>= target` are exactly the post-resynchronisation ones,
/// and they are the ones a serial scan over those bytes also produces;
/// * refusals `< target` are exactly the speculative-prefix ones, and
/// they are dropped with the records they sat among.
///
/// A chunk stops at the first record at/after its `range_end`, which is
/// the next chunk's `target`, so the intervals `[target_i, target_{i+1})`
/// tile the file with no gap and no overlap: each real refusal is counted
/// once, by exactly one chunk. On the `Err` fallback the chunk is
/// discarded whole, refusals included, and the serial rescan over the same
/// bytes supplies them instead.
///
/// What this does NOT bound: a `#<digits>=` inside a quoted value that the
/// SERIAL scanner also mis-parses (only reachable on malformed input,
/// where a stray quote has already flipped `find_entity_end`'s parity)
/// still counts here, because it counts there. That is parity with the
/// serial path, which is the target — not immunity to mis-parsing, which
/// would mean giving the scanner quote context (#3395/#3430).
///
/// ## The malformed-record report is a REPORT, never a control signal
///
/// A malformed record comes in two shapes and only one of them stops the
/// scan (see `close_step_record` in ifc_lite_core's parser::lexical).
/// `malformed_start` is set for BOTH, so it cannot say which happened, and
/// this stitch used to read it as if it always meant the permanent one,
/// dropping every chunk after it. Once a missing `;` became recoverable that
/// turned one lost record into the whole tail of the file.
///
/// `handoff` is the signal that actually distinguishes them, and it always
/// did: a chunk whose scanner stopped runs out of records before
/// `range_end` and hands back `None`, which the `expected_start: None`
/// break below already drops every later chunk on. So byte-identity with
/// the serial scan is carried by the handoff alone, and `malformed_start`
/// is only ever accumulated into the returned bool, which callers report
/// once, stitched, never per shard.
///
/// "Attributed" carries the same speculative-prefix caveat as a refusal —
/// `chunk.malformed_starts` filtered by `>= target` is that, mirroring the `<
/// target` split `refusals.partition_point` already makes.
/// [`rescan_range`]'s result: the handoff for the next chunk (the first
/// entity start at/after `end`, or `None` at EOF), the refusals rescanned
/// over `[target, end)`, and the malformed-record stop over those bytes if
/// any.
///
/// Named, not a positional tuple, because `handoff` and `malformed_start`
/// are BOTH `Option<usize>` — a tuple would let a caller swap them (e.g.
/// treat the handoff as the malformed stop) with no type error to catch it.
/// Serial rescan from a known-real entity start `target` up to `end`,
/// inserting each entity.
///
/// This scan is aligned from its first byte, so every refusal — and
/// every malformed stop — it makes is one the serial builder makes too:
/// no attribution needed, unlike [`stitch`]'s `Ok` arm.