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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
// 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 STEP `/* ... */` comment-skip rule -- and, on top of it, the trivia
//! rule that says a comment is legal wherever whitespace is -- shared by every
//! scanner that has no reason to answer an unterminated comment differently
//! from the others.
//!
//! [`skip_step_comment`] answers an unterminated `/*` by refusing it: it
//! returns `None`, the same as "not a comment here" from the caller's point
//! of view. That is correct for a scanner walking already-located,
//! well-formed record bytes looking for structure — an unterminated comment
//! there means the input is corrupt, and silently consuming the rest of it
//! is worse than stopping. [`super::scanner::EntityScanner`] and
//! `ifc-lite-geometry`'s `IfcTriangulatedFaceSet` CoordIndex walk both used
//! to hand-roll this and disagreed with each other on exactly this case
//! (issue #3303); both now call this one function.
//!
//! `ifc-lite-export`'s STEP HEADER prescan (`source_header::Lex`) answers the
//! same question differently, on purpose, and that is not the divergence
//! #3303 is about: a header prescan that swallows every later record has
//! lost the schema, so it treats an unterminated `/*` as ordinary text
//! instead of refusing. See the doc comment on `Lex::skip_comment_at` for
//! why that call site needs its own answer. It still calls this function to
//! find where a *closed* comment ends — only the unterminated case is
//! handled differently, and only at that one call site.
/// If a STEP `/* ... */` comment starts at `bytes[i]`, the index just past
/// its closing `*/`.
///
/// Returns `None` when `bytes[i]` doesn't begin a comment, and ALSO when it
/// does but no closing `*/` exists anywhere in `bytes` after it — an
/// unterminated comment is refused rather than silently run to end of input.
/// A caller that needs to tell those two `None` cases apart already knows
/// which one it's in, from having checked `bytes[i..i + 2] == b"/*"` itself
/// before calling (as every call site in this repo does), so the single
/// `Option` return doesn't lose information.
/// Index of the first byte at or after `i` that is neither ASCII whitespace
/// nor part of a `/* ... */` comment.
///
/// ISO 10303-21 allows a comment ANYWHERE whitespace is allowed, which
/// includes inside a record: between an instance name and its `=`, between the
/// `=` and the type name, and between the type name and its `(`. A scanner
/// that skips only whitespace at those points reads
/// `#1 /* was #7 */ = IFCWALL(…);` as no record at all.
///
/// `None` when a comment opens here and never closes — everything from there
/// to the end of `bytes` is inside it, so there is nothing left to find. That
/// is [`skip_step_comment`]'s answer, deliberately, and not a separate rule.
///
/// This is the matched pair of `skipTrivia` in
/// `packages/parser/src/step-lexing.ts`; the two halves are changed together.
/// ASCII whitespace per ISO 10303-21: space, tab, LF, CR, form feed, vertical
/// tab.
///
/// Spelled out rather than `u8::is_ascii_whitespace`, which follows the
/// WhatWG Infra Standard's definition and EXCLUDES vertical tab (0x0B) — its
/// docs say as much. `ifc-lite-export`'s `source_header::is_step_space`
/// already carries this exact set for this exact reason (see its doc
/// comment): reaching for a stdlib predicate here once mismatched the
/// TypeScript half's `isSpaceByte`/`isAsciiSpace` on `\x0B` and, before that
/// mismatch was found, TypeScript's `isSpaceByte` itself omitted both `\x0C`
/// and `\x0B` — issue #3733, a form feed silently dropping an entity that
/// this scanner parsed. Keep this set and `isSpaceByte` in
/// `packages/parser/src/step-lexing.ts` identical.
/// Where a refused record ends, and whether anything after it can be read.
///
/// The two failures are separate because they call for opposite answers. A
/// record whose parens never balance (`#2=IFCB(2;`) still leaves the rest of
/// the file readable, so a scan drops that ONE record and hunts on; one whose
/// literal or comment never closes puts everything to end of input inside it,
/// so there is nothing to resume from and the scan stops (#3695). Collapsing
/// them cost the whole tail of the file for a missing `)`, which is the same
/// amplification #4179 is about.
/// Whether the last significant byte of `plain` is `')'`, or `carried` when
/// `plain` holds nothing but STEP whitespace (so the answer stands from before
/// it). The cheap half of the record-boundary rule argued on
/// [`close_step_record`]; walks backwards, stopping on the first byte for
/// every real record.
/// Offset just past the `)` that balances the first `(` in `bytes`, or `None`
/// when the input runs out, a comment or literal never closes, or a `)`
/// arrives before any `(`.
///
/// # Where a STEP record ends, and whether a `;` is its own (#4179)
///
/// This doc is the one argument for a rule four scans implement:
/// [`EntityScanner::find_entity_end`](super::scanner) and this function in
/// Rust, `StepTokenizer.scanEntitiesFast` (`packages/parser/src/tokenizer.ts`)
/// with `packages/parser/src/step-record-boundary.ts` in TypeScript, and
/// `WORKER_CODE` (`packages/parser/src/scan-worker-source.ts`), which
/// hand-duplicates it because a Blob worker cannot import at runtime. Rust is
/// the source of truth per AGENTS.md; the halves are changed together.
///
/// ## The two rules
///
/// A "skip to the next unquoted `;`" scan is bounded only by end-of-buffer, so
/// a record missing its own `;` takes the first `;` it can reach — the NEXT
/// record's, or the footer's — reporting success while swallowing whatever lay
/// between. Two rules read off the ISO 10303-21 grammar bound it to the
/// record's own body:
///
/// * `simple_record` is `keyword '(' [parameter_list] ')'`, so the last
/// significant byte before the terminator is always `')'`. A `;` preceded
/// by anything else, as in `#2=IFCWALL('b')\nENDSEC;`, is the footer's.
/// * `'='` appears in the exchange structure only in
/// `entity_instance_name '=' record`, never inside a parameter list, so
/// one before the terminator, as in `#2=IFCB(2)\n#3=IFCC(3);`, means the
/// scan walked into the NEXT declaration.
///
/// Both are judged only on bytes outside strings and comments: an `=` in
/// either is text, and a comment between the `)` and the `;` is trivia.
///
/// The two are a cheap approximation of one general property — *the `;` is the
/// first non-trivia byte after the `)` balancing the record's own `(`* — which
/// is what THIS function computes exactly. The approximation is deliberate:
/// the exact rule costs a full paren balance per record on the hottest
/// structural path, so it runs only where a byte test already refused. It is
/// strictly weaker, and admits shapes the exact rule rejects, `#2=IFCB(2) (3);`
/// among them.
///
/// ## Why a refusal RECOVERS instead of stopping
///
/// 10303-21 closes every record's parameter list, so the `)` this function
/// finds is a real grammar boundary even when the `;` after it is missing.
/// That lets the scan drop the ONE broken record and carry on. The rest of the
/// file is not the malformed record's to take: a shard whose scanner stops
/// hands back no handoff, and both stitches — `stitchShards`
/// (`packages/geometry/src/shard-stitch.ts`) and the native
/// `parallel_scan::stitch` — read that as "no more real entities" and discard
/// every LATER shard, including bytes those shards already scanned cleanly.
/// Measured on a 40-record file whose #20 lost its `;`, at four shards: 40
/// records in, 19 out. An unterminated string or comment has no such `)`,
/// nothing to resume from, so that case still stops as #3695 set it.
///
/// A string literal is jumped whole and a `/* … */` comment after it, in that
/// order, so a `(` or `)` inside either is text; the same rule and the same
/// order the scanner's own body walk uses. The matched TypeScript half is
/// `findEntityLength` in `packages/parser/src/step-lexing.ts`.