1use std::fmt::Debug;
12use std::num::ParseIntError;
13use std::str::FromStr;
14
15use time::OffsetDateTime;
16use time::format_description::well_known::Rfc3339;
17
18use crate::errors::{NameError, SnapshotError};
19use crate::model::names::{
20 ApplicationName, Arity, CommitSha, DependencyName, DependencyVersion, FieldName, FunctionName,
21 MacroName, ModuleName, ProjectName, RecordName, TagName, TypeName,
22};
23use crate::model::snapshot::{
24 ArityMatch, CallbackSig, Deprecation, DeprecationReplacement, FORMAT_VERSION, FunArity,
25 HrlFile, IfdefGuardKind, IfdefMacro, Module, Provenance, RecordDecl, RecordField,
26 SUPPORTED_FORMAT_VERSIONS, Snapshot, SnapshotHeader, SpecSig, TestExportVariant,
27 TestOnlyExport, TypeArity, TypeDecl, VariantCBlock, VendoredDep, VendoredDepSource,
28 VersionedMachineVersion, Visibility, WireConstantBinding, WireValue, state,
29};
30use crate::snapshot::SNAPSHOT_SIZE_LIMIT;
31
32pub fn parse(input: &str) -> Result<Snapshot<state::Canonical>, SnapshotError> {
33 if input.len() > SNAPSHOT_SIZE_LIMIT {
34 return Err(SnapshotError::SizeLimit {
35 size: input.len(),
36 limit: SNAPSHOT_SIZE_LIMIT,
37 });
38 }
39 let mut parser = Parser::new(input);
40 parser.parse()
41}
42
43struct Parser<'a> {
44 lines: Vec<(usize, &'a str)>,
45 cursor: usize,
46}
47
48impl<'a> Parser<'a> {
49 fn new(input: &'a str) -> Self {
50 let lines = input.lines().enumerate().map(|(i, l)| (i + 1, l)).collect();
51 Self { lines, cursor: 0 }
52 }
53
54 fn parse(&mut self) -> Result<Snapshot<state::Canonical>, SnapshotError> {
55 let header = self.parse_header()?;
56 let mut modules: Vec<Module> = Vec::new();
57 let mut headers: Vec<HrlFile> = Vec::new();
58 let mut headers_started = false;
59 loop {
60 self.skip_blank();
61 let Some((lineno, line)) = self.peek() else {
62 break;
63 };
64 if let Some(rest) = line.strip_prefix("module ") {
65 if headers_started {
66 return Err(SnapshotError::NotCanonical {
67 line: lineno,
68 detail: "module after header block".into(),
69 });
70 }
71 self.advance();
72 let name = ModuleName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
73 if let Some(prev) = modules.last()
74 && name <= prev.name
75 {
76 return Err(SnapshotError::NotCanonical {
77 line: lineno,
78 detail: format!("modules out of order at {name}"),
79 });
80 }
81 let module = self.parse_module_body(name)?;
82 modules.push(module);
83 } else if let Some(rest) = line.strip_prefix("header ") {
84 headers_started = true;
85 self.advance();
86 let path = rest.trim().to_owned();
87 if let Some(prev) = headers.last()
88 && path.as_str() <= prev.path.as_str()
89 {
90 return Err(SnapshotError::NotCanonical {
91 line: lineno,
92 detail: format!("headers out of order at {path}"),
93 });
94 }
95 let hrl = self.parse_header_body(path)?;
96 headers.push(hrl);
97 } else {
98 return Err(SnapshotError::UnexpectedToken {
99 line: lineno,
100 detail: format!("expected 'module' or 'header', got {line:?}"),
101 });
102 }
103 }
104 Ok(Snapshot::from_canonical_parts(header, modules, headers))
105 }
106
107 fn parse_header(&mut self) -> Result<SnapshotHeader, SnapshotError> {
108 let mut project: Option<ProjectName> = None;
109 let mut tag: Option<TagName> = None;
110 let mut branch: Option<String> = None;
111 let mut commit: Option<CommitSha> = None;
112 let mut scanned_paths: Vec<String> = Vec::new();
113 let mut apps_scanned: Vec<ApplicationName> = Vec::new();
114 let mut generated_by: Option<String> = None;
115 let mut generated_at: Option<OffsetDateTime> = None;
116 let mut extractor_version: String = String::new();
117 let mut dep_pins: Vec<VendoredDep> = Vec::new();
118 let mut format_version_seen = false;
119 loop {
120 let Some((lineno, line)) = self.peek() else {
121 break;
122 };
123 let Some(content) = line.strip_prefix("# ") else {
124 break;
125 };
126 self.advance();
127 if content == "backhopper snapshot" {
128 continue;
129 }
130 let (key, value) =
131 content
132 .split_once(": ")
133 .ok_or_else(|| SnapshotError::MalformedHeader {
134 line: lineno,
135 detail: format!("missing ': ' in header line {line:?}"),
136 })?;
137 match key {
138 "format-version" => {
139 let parsed: u32 =
140 value.parse().map_err(|_| SnapshotError::MalformedHeader {
141 line: lineno,
142 detail: format!("non-integer format-version {value:?}"),
143 })?;
144 if !SUPPORTED_FORMAT_VERSIONS.contains(&parsed) {
145 return Err(SnapshotError::UnknownFormatVersion {
146 found: value.to_owned(),
147 expected: FORMAT_VERSION,
148 });
149 }
150 format_version_seen = true;
151 }
152 "project" => {
153 project = Some(ProjectName::from_str(value).map_err(SnapshotError::Name)?);
154 }
155 "tag" => {
156 tag = Some(TagName::from_str(value).map_err(SnapshotError::Name)?);
157 }
158 "branch" => {
159 branch = Some(value.to_owned());
160 }
161 "commit" => {
162 commit = Some(CommitSha::from_str(value).map_err(SnapshotError::Name)?);
163 }
164 "scanned-paths" => {
165 scanned_paths = value
166 .split(',')
167 .map(|p| p.trim().to_owned())
168 .filter(|p| !p.is_empty())
169 .collect();
170 }
171 "apps-scanned" => {
172 apps_scanned = value
173 .split(',')
174 .map(str::trim)
175 .filter(|s| !s.is_empty())
176 .map(ApplicationName::from_str)
177 .collect::<Result<_, _>>()
178 .map_err(SnapshotError::Name)?;
179 }
180 "generated-by" => generated_by = Some(value.to_owned()),
181 "extractor-version" => value.clone_into(&mut extractor_version),
182 "dep-pin" => {
183 dep_pins.push(parse_dep_pin_line(lineno, value)?);
184 }
185 "generated-at" => {
186 generated_at = Some(OffsetDateTime::parse(value, &Rfc3339).map_err(|_| {
187 SnapshotError::MalformedHeader {
188 line: lineno,
189 detail: format!("invalid timestamp {value:?}"),
190 }
191 })?);
192 }
193 other => {
194 return Err(SnapshotError::UnknownHeaderKey {
195 line: lineno,
196 key: other.to_owned(),
197 });
198 }
199 }
200 }
201 if !format_version_seen {
202 return Err(SnapshotError::MissingHeaderKey {
203 key: "format-version",
204 });
205 }
206 Ok(SnapshotHeader {
207 project: project.ok_or(SnapshotError::MissingHeaderKey { key: "project" })?,
208 tag: tag.ok_or(SnapshotError::MissingHeaderKey { key: "tag" })?,
209 branch,
210 commit: commit.ok_or(SnapshotError::MissingHeaderKey { key: "commit" })?,
211 scanned_paths,
212 apps_scanned,
213 generated_by: generated_by.ok_or(SnapshotError::MissingHeaderKey {
214 key: "generated-by",
215 })?,
216 generated_at: generated_at.ok_or(SnapshotError::MissingHeaderKey {
217 key: "generated-at",
218 })?,
219 extractor_version,
220 dep_pins,
221 })
222 }
223
224 fn parse_module_body(&mut self, name: ModuleName) -> Result<Module, SnapshotError> {
225 let mut module = Module::new(name);
226 let mut state = ClassOrder::new("entry-class order");
227 loop {
228 let Some((lineno, line)) = self.peek() else {
229 break;
230 };
231 if !line.starts_with(" ") {
232 break;
233 }
234 let trimmed = &line[2..];
235 self.advance();
236 if let Some(rest) = trimmed.strip_prefix("visibility ") {
237 state.advance(EntryClass::Visibility, lineno)?;
238 let kw = rest.trim();
239 module.visibility =
240 Visibility::from_keyword(kw).ok_or_else(|| SnapshotError::UnexpectedToken {
241 line: lineno,
242 detail: format!("unknown visibility {kw:?}"),
243 })?;
244 } else if let Some(rest) = trimmed.strip_prefix("path ") {
245 state.advance(EntryClass::Path, lineno)?;
246 module.path = Some(rest.trim().to_owned());
247 } else if let Some(rest) = trimmed.strip_prefix("app ") {
248 state.advance(EntryClass::App, lineno)?;
249 let app = ApplicationName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
250 module.app = Some(app);
251 } else if let Some(rest) = trimmed.strip_prefix("behaviour ") {
252 state.advance(EntryClass::Behaviour, lineno)?;
253 let m = ModuleName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
254 if let Some(prev) = module.behaviours.last()
255 && &m <= prev
256 {
257 return Err(SnapshotError::NotCanonical {
258 line: lineno,
259 detail: format!("behaviour {m} out of order"),
260 });
261 }
262 module.behaviours.push(m);
263 } else if let Some(rest) = trimmed.strip_prefix("export ") {
264 state.advance(EntryClass::Export, lineno)?;
265 let fa = parse_fun_arity(rest.trim()).map_err(SnapshotError::Name)?;
266 check_order(&module.exports, &fa, lineno, "export")?;
267 module.exports.push(fa);
268 } else if let Some(rest) = trimmed.strip_prefix("export_type ") {
269 state.advance(EntryClass::ExportType, lineno)?;
270 let ta = parse_type_arity(rest.trim()).map_err(SnapshotError::Name)?;
271 check_order(&module.export_types, &ta, lineno, "export_type")?;
272 module.export_types.push(ta);
273 } else if let Some(rest) = trimmed.strip_prefix("callback ") {
274 state.advance(EntryClass::Callback, lineno)?;
275 let (fa, sig) = parse_fun_arity_and_signature(rest).map_err(|e| {
276 SnapshotError::UnexpectedToken {
277 line: lineno,
278 detail: e,
279 }
280 })?;
281 let signature = self.collect_continuation(sig);
282 module.callbacks.push(CallbackSig {
283 name: fa.name,
284 arity: fa.arity,
285 signature,
286 });
287 } else if let Some(rest) = trimmed.strip_prefix("optional_callback ") {
288 state.advance(EntryClass::OptionalCallback, lineno)?;
289 let fa = parse_fun_arity(rest.trim()).map_err(SnapshotError::Name)?;
290 check_order(&module.optional_callbacks, &fa, lineno, "optional_callback")?;
291 module.optional_callbacks.push(fa);
292 } else if let Some(rest) = trimmed.strip_prefix("spec ") {
293 state.advance(EntryClass::Spec, lineno)?;
294 let (fa, sig) = parse_fun_arity_and_signature(rest).map_err(|e| {
295 SnapshotError::UnexpectedToken {
296 line: lineno,
297 detail: e,
298 }
299 })?;
300 let signature = self.collect_continuation(sig);
301 module.specs.push(SpecSig {
302 name: fa.name,
303 arity: fa.arity,
304 signature,
305 });
306 } else if let Some(rest) = trimmed.strip_prefix("type ") {
307 state.advance(EntryClass::Type, lineno)?;
308 let (ta, rhs) =
309 parse_type_decl_line(rest).map_err(|e| SnapshotError::UnexpectedToken {
310 line: lineno,
311 detail: e,
312 })?;
313 let rhs_full = self.collect_continuation(rhs);
314 module.types.push(TypeDecl {
315 name: ta.name,
316 arity: ta.arity,
317 rhs: rhs_full,
318 });
319 } else if let Some(rest) = trimmed.strip_prefix("opaque ") {
320 state.advance(EntryClass::Opaque, lineno)?;
321 let ta = parse_type_arity(rest.trim()).map_err(SnapshotError::Name)?;
322 check_order(&module.opaques, &ta, lineno, "opaque")?;
323 module.opaques.push(ta);
324 } else if let Some(rest) = trimmed.strip_prefix("deprecated ") {
325 state.advance(EntryClass::Deprecated, lineno)?;
326 let dep = parse_deprecation(rest).map_err(|e| SnapshotError::UnexpectedToken {
327 line: lineno,
328 detail: e,
329 })?;
330 module.deprecations.push(dep);
331 } else if let Some(rest) = trimmed.strip_prefix("record ") {
332 state.advance(EntryClass::Record, lineno)?;
333 let name = RecordName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
334 let fields = self.parse_record_fields()?;
335 module.records.push(RecordDecl { name, fields });
336 } else if let Some(rest) = trimmed.strip_prefix("test_only_export ") {
337 state.advance(EntryClass::TestOnlyExport, lineno)?;
338 let entry = parse_test_only_export(rest).map_err(|detail| {
339 SnapshotError::UnexpectedToken {
340 line: lineno,
341 detail,
342 }
343 })?;
344 module.test_only_exports.push(entry);
345 } else if let Some(rest) = trimmed.strip_prefix("ifdef_macro ") {
346 state.advance(EntryClass::IfdefMacro, lineno)?;
347 let entry =
348 parse_ifdef_macro(rest).map_err(|detail| SnapshotError::UnexpectedToken {
349 line: lineno,
350 detail,
351 })?;
352 module.ifdef_macros.push(entry);
353 } else if let Some(rest) = trimmed.strip_prefix("variant_c_block ") {
354 state.advance(EntryClass::VariantCBlock, lineno)?;
355 let entry = parse_variant_c_block(rest).map_err(|detail| {
356 SnapshotError::UnexpectedToken {
357 line: lineno,
358 detail,
359 }
360 })?;
361 module.variant_c_blocks.push(entry);
362 } else if let Some(rest) = trimmed.strip_prefix("versioned_machine ") {
363 state.advance(EntryClass::VersionedMachine, lineno)?;
364 if module.versioned_machine_version.is_some() {
365 return Err(SnapshotError::UnexpectedToken {
366 line: lineno,
367 detail: "duplicate versioned_machine entry".to_owned(),
368 });
369 }
370 let entry = parse_versioned_machine(rest).map_err(|detail| {
371 SnapshotError::UnexpectedToken {
372 line: lineno,
373 detail,
374 }
375 })?;
376 module.versioned_machine_version = Some(entry);
377 } else if let Some(rest) = trimmed.strip_prefix("wire_constant ") {
378 state.advance(EntryClass::WireConstant, lineno)?;
379 let entry =
380 parse_wire_constant(rest).map_err(|detail| SnapshotError::UnexpectedToken {
381 line: lineno,
382 detail,
383 })?;
384 if let Some(prev) = module.wire_constants.last()
385 && entry.macro_name <= prev.macro_name
386 {
387 return Err(SnapshotError::NotCanonical {
388 line: lineno,
389 detail: format!("wire_constant {} out of order", entry.macro_name),
390 });
391 }
392 module.wire_constants.push(entry);
393 } else {
394 return Err(SnapshotError::UnexpectedToken {
395 line: lineno,
396 detail: format!("unknown module entry {trimmed:?}"),
397 });
398 }
399 }
400 Ok(module)
401 }
402
403 fn parse_header_body(&mut self, path: String) -> Result<HrlFile, SnapshotError> {
404 let mut hrl = HrlFile::new(path);
405 let mut state = ClassOrder::new("hrl-entry-class order");
406 loop {
407 let Some((lineno, line)) = self.peek() else {
408 break;
409 };
410 if !line.starts_with(" ") {
411 break;
412 }
413 let trimmed = &line[2..];
414 self.advance();
415 if let Some(rest) = trimmed.strip_prefix("type ") {
416 state.advance(HrlEntryClass::Type, lineno)?;
417 let (ta, rhs) =
418 parse_type_decl_line(rest).map_err(|e| SnapshotError::UnexpectedToken {
419 line: lineno,
420 detail: e,
421 })?;
422 let rhs_full = self.collect_continuation(rhs);
423 hrl.types.push(TypeDecl {
424 name: ta.name,
425 arity: ta.arity,
426 rhs: rhs_full,
427 });
428 } else if let Some(rest) = trimmed.strip_prefix("opaque ") {
429 state.advance(HrlEntryClass::Opaque, lineno)?;
430 let ta = parse_type_arity(rest.trim()).map_err(SnapshotError::Name)?;
431 hrl.opaques.push(ta);
432 } else if let Some(rest) = trimmed.strip_prefix("record ") {
433 state.advance(HrlEntryClass::Record, lineno)?;
434 let name = RecordName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
435 let fields = self.parse_record_fields()?;
436 hrl.records.push(RecordDecl { name, fields });
437 } else {
438 return Err(SnapshotError::UnexpectedToken {
439 line: lineno,
440 detail: format!("unknown header entry {trimmed:?}"),
441 });
442 }
443 }
444 Ok(hrl)
445 }
446
447 fn parse_record_fields(&mut self) -> Result<Vec<RecordField>, SnapshotError> {
448 let mut fields: Vec<RecordField> = Vec::new();
449 while let Some((flineno, fline)) = self.peek() {
450 if !fline.starts_with(" ") {
451 break;
452 }
453 let ftrimmed = &fline[4..];
454 if let Some(fbody) = ftrimmed.strip_prefix("field ") {
455 self.advance();
456 let (fname_str, ftype_repr) = match fbody.split_once(" :: ") {
457 Some((n, t)) => (n.trim(), Some(t.trim().to_owned())),
458 None => (fbody.trim(), None),
459 };
460 fields.push(RecordField {
461 name: FieldName::from_str(fname_str).map_err(SnapshotError::Name)?,
462 type_repr: ftype_repr,
463 });
464 } else if fline.starts_with(" ") {
465 let Some(last) = fields.last_mut() else {
466 return Err(SnapshotError::UnexpectedToken {
467 line: flineno,
468 detail: format!("expected 'field', got {ftrimmed:?}"),
469 });
470 };
471 let Some(repr) = last.type_repr.as_mut() else {
472 return Err(SnapshotError::UnexpectedToken {
473 line: flineno,
474 detail: format!(
475 "type continuation but the previous field has no '::' type: {fline:?}",
476 ),
477 });
478 };
479 self.advance();
480 repr.push('\n');
481 repr.push_str(fline);
482 } else {
483 return Err(SnapshotError::UnexpectedToken {
484 line: flineno,
485 detail: format!("expected 'field', got {ftrimmed:?}"),
486 });
487 }
488 }
489 Ok(fields)
490 }
491
492 fn collect_continuation(&mut self, first: String) -> String {
493 let mut s = first;
494 while let Some((_, line)) = self.peek()
495 && line.starts_with(" ")
496 {
497 self.advance();
498 s.push('\n');
499 s.push_str(line);
500 }
501 s
502 }
503
504 fn peek(&self) -> Option<(usize, &'a str)> {
505 self.lines.get(self.cursor).copied()
506 }
507
508 fn advance(&mut self) {
509 self.cursor += 1;
510 }
511
512 fn skip_blank(&mut self) {
513 while let Some((_, line)) = self.peek() {
514 if line.is_empty() {
515 self.advance();
516 } else {
517 break;
518 }
519 }
520 }
521}
522
523fn parse_fun_arity(s: &str) -> Result<FunArity, NameError> {
524 let (n, a) = s
525 .split_once('/')
526 .ok_or_else(|| NameError::PatternMismatch {
527 kind: "function/arity",
528 value: s.to_owned(),
529 pattern: "name/arity",
530 })?;
531 Ok(FunArity {
532 name: FunctionName::from_str(n)?,
533 arity: Arity::from_str(a)?,
534 })
535}
536
537fn parse_type_arity(s: &str) -> Result<TypeArity, NameError> {
538 let (n, a) = s
539 .split_once('/')
540 .ok_or_else(|| NameError::PatternMismatch {
541 kind: "type/arity",
542 value: s.to_owned(),
543 pattern: "name/arity",
544 })?;
545 Ok(TypeArity {
546 name: TypeName::from_str(n)?,
547 arity: Arity::from_str(a)?,
548 })
549}
550
551fn parse_fun_arity_and_signature(s: &str) -> Result<(FunArity, String), String> {
552 let (head, sig) = s
553 .split_once(' ')
554 .ok_or_else(|| format!("expected 'name/arity body' in {s:?}"))?;
555 let fa = parse_fun_arity(head).map_err(|e| e.to_string())?;
556 Ok((fa, sig.to_owned()))
557}
558
559fn parse_test_only_export(s: &str) -> Result<TestOnlyExport, String> {
560 let mut parts = s.split_whitespace();
561 let variant_token = parts
562 .next()
563 .ok_or_else(|| format!("missing variant in test_only_export {s:?}"))?;
564 let variant = TestExportVariant::from_label(variant_token)
565 .ok_or_else(|| format!("unknown test_only_export variant {variant_token:?}"))?;
566 let fa_token = parts
567 .next()
568 .ok_or_else(|| format!("missing function/arity in test_only_export {s:?}"))?;
569 let fa = parse_fun_arity(fa_token).map_err(|e| e.to_string())?;
570 let mut export_line: Option<usize> = None;
571 let mut body_line: Option<usize> = None;
572 for token in parts {
573 let (key, value) = token
574 .split_once('=')
575 .ok_or_else(|| format!("expected key=value in test_only_export, got {token:?}"))?;
576 match key {
577 "export_line" => {
578 export_line = Some(
579 value
580 .parse()
581 .map_err(|e: ParseIntError| format!("invalid export_line: {e}"))?,
582 );
583 }
584 "body_line" => {
585 body_line = Some(
586 value
587 .parse()
588 .map_err(|e: ParseIntError| format!("invalid body_line: {e}"))?,
589 );
590 }
591 other => return Err(format!("unknown test_only_export key {other:?}")),
592 }
593 }
594 let export_line = export_line.ok_or_else(|| "missing export_line".to_owned())?;
595 if matches!(variant, TestExportVariant::B) && body_line.is_none() {
596 return Err("variant=b requires body_line".into());
597 }
598 Ok(TestOnlyExport {
599 function: fa.name,
600 arity: fa.arity,
601 export_line,
602 body_line,
603 variant,
604 })
605}
606
607fn parse_ifdef_macro(s: &str) -> Result<IfdefMacro, String> {
608 let mut parts = s.split_whitespace();
609 let name_token = parts
610 .next()
611 .ok_or_else(|| format!("missing macro name in ifdef_macro {s:?}"))?;
612 let name = name_token.to_owned();
613 let mut guard_kind: Option<IfdefGuardKind> = None;
614 let mut line: Option<usize> = None;
615 for token in parts {
616 let (key, value) = token
617 .split_once('=')
618 .ok_or_else(|| format!("expected key=value in ifdef_macro, got {token:?}"))?;
619 match key {
620 "guard" => {
621 guard_kind = Some(
622 IfdefGuardKind::from_label(value)
623 .ok_or_else(|| format!("unknown ifdef_macro guard {value:?}"))?,
624 );
625 }
626 "line" => {
627 line = Some(
628 value
629 .parse()
630 .map_err(|e: ParseIntError| format!("invalid line: {e}"))?,
631 );
632 }
633 other => return Err(format!("unknown ifdef_macro key {other:?}")),
634 }
635 }
636 let guard_kind = guard_kind.ok_or_else(|| "missing guard".to_owned())?;
637 let line = line.ok_or_else(|| "missing line".to_owned())?;
638 Ok(IfdefMacro {
639 name,
640 line,
641 guard_kind,
642 })
643}
644
645fn parse_variant_c_block(s: &str) -> Result<VariantCBlock, String> {
646 let mut guard: Option<String> = None;
647 let mut start_line: Option<usize> = None;
648 let mut else_line: Option<usize> = None;
649 let mut end_line: Option<usize> = None;
650 for token in s.split_whitespace() {
651 let (key, value) = token
652 .split_once('=')
653 .ok_or_else(|| format!("expected key=value in variant_c_block, got {token:?}"))?;
654 match key {
655 "guard" => guard = Some(value.to_owned()),
656 "start_line" => {
657 start_line = Some(
658 value
659 .parse()
660 .map_err(|e: ParseIntError| format!("invalid start_line: {e}"))?,
661 );
662 }
663 "else_line" => {
664 else_line = Some(
665 value
666 .parse()
667 .map_err(|e: ParseIntError| format!("invalid else_line: {e}"))?,
668 );
669 }
670 "end_line" => {
671 end_line = Some(
672 value
673 .parse()
674 .map_err(|e: ParseIntError| format!("invalid end_line: {e}"))?,
675 );
676 }
677 other => return Err(format!("unknown variant_c_block key {other:?}")),
678 }
679 }
680 let guard = guard.ok_or_else(|| "missing guard".to_owned())?;
681 let start_line = start_line.ok_or_else(|| "missing start_line".to_owned())?;
682 let end_line = end_line.ok_or_else(|| "missing end_line".to_owned())?;
683 Ok(VariantCBlock {
684 guard,
685 start_line,
686 end_line,
687 else_line,
688 })
689}
690
691fn parse_type_decl_line(s: &str) -> Result<(TypeArity, String), String> {
692 let (head, rhs) = s
693 .split_once(" :: ")
694 .ok_or_else(|| format!("expected 'name/arity :: rhs' in {s:?}"))?;
695 let ta = parse_type_arity(head).map_err(|e| e.to_string())?;
696 Ok((ta, rhs.to_owned()))
697}
698
699fn parse_deprecation(rest: &str) -> Result<Deprecation, String> {
700 if rest.trim_start().starts_with("module") {
701 return Ok(Deprecation {
702 function: None,
703 arity_match: ArityMatch::Any,
704 since: None,
705 replacement: None,
706 reason: None,
707 module_wide: true,
708 });
709 }
710 let mut tokens = rest.split_whitespace().peekable();
711 let head = tokens.next().ok_or("empty deprecation")?;
712 let (function, arity_match) = if head == "*" {
713 (None, ArityMatch::Any)
714 } else {
715 let (n, a) = head
716 .split_once('/')
717 .ok_or_else(|| format!("expected name/arity got {head:?}"))?;
718 let arity_match = if a == "*" {
719 ArityMatch::Any
720 } else {
721 ArityMatch::Exact {
722 arity: Arity::from_str(a).map_err(|e| e.to_string())?,
723 }
724 };
725 let function = FunctionName::from_str(n).map_err(|e| e.to_string())?;
726 (Some(function), arity_match)
727 };
728 let mut since: Option<TagName> = None;
729 let mut replacement: Option<DeprecationReplacement> = None;
730 let mut reason: Option<String> = None;
731 while let Some(tok) = tokens.next() {
732 match tok {
733 "since" => {
734 let value = tokens.next().ok_or("missing 'since' value")?;
735 since = Some(TagName::from_str(value).map_err(|e| e.to_string())?);
736 }
737 "use" => {
738 let target = tokens.next().ok_or("missing 'use' target")?;
739 let (n, a) = target
740 .split_once('/')
741 .ok_or_else(|| format!("expected use name/arity got {target:?}"))?;
742 replacement = Some(DeprecationReplacement {
743 function: FunctionName::from_str(n).map_err(|e| e.to_string())?,
744 arity: Arity::from_str(a).map_err(|e| e.to_string())?,
745 });
746 }
747 "reason" => {
748 let open = rest.find('"').ok_or("reason must be a quoted string")?;
750 let close = rest.rfind('"').ok_or("reason must be a quoted string")?;
751 if close <= open {
752 return Err("reason must be a quoted string".into());
753 }
754 reason = Some(unescape_reason(&rest[open + 1..close]));
755 break;
756 }
757 other => return Err(format!("unexpected deprecation token {other:?}")),
758 }
759 }
760 Ok(Deprecation {
761 function,
762 arity_match,
763 since,
764 replacement,
765 reason,
766 module_wide: false,
767 })
768}
769
770fn unescape_reason(s: &str) -> String {
772 let mut out = String::with_capacity(s.len());
773 let mut chars = s.chars();
774 while let Some(c) = chars.next() {
775 if c != '\\' {
776 out.push(c);
777 continue;
778 }
779 match chars.next() {
780 Some('n') => out.push('\n'),
781 Some('t') => out.push('\t'),
782 Some('r') => out.push('\r'),
783 Some('"') => out.push('"'),
784 Some('\\') => out.push('\\'),
785 Some(other) => {
786 out.push('\\');
787 out.push(other);
788 }
789 None => out.push('\\'),
790 }
791 }
792 out
793}
794
795trait ArityOrdered {
798 type Key: Ord;
799 fn order_key(&self) -> Self::Key;
800 fn describe(&self) -> String;
801}
802
803impl ArityOrdered for FunArity {
804 type Key = (FunctionName, Arity);
805 fn order_key(&self) -> Self::Key {
806 (self.name.clone(), self.arity)
807 }
808 fn describe(&self) -> String {
809 format!("{}/{}", self.name, self.arity)
810 }
811}
812
813impl ArityOrdered for TypeArity {
814 type Key = (TypeName, Arity);
815 fn order_key(&self) -> Self::Key {
816 (self.name.clone(), self.arity)
817 }
818 fn describe(&self) -> String {
819 format!("{}/{}", self.name, self.arity)
820 }
821}
822
823fn check_order<T: ArityOrdered>(
824 existing: &[T],
825 item: &T,
826 lineno: usize,
827 label: &str,
828) -> Result<(), SnapshotError> {
829 if let Some(prev) = existing.last() {
830 if item.order_key() <= prev.order_key() {
831 return Err(SnapshotError::NotCanonical {
832 line: lineno,
833 detail: format!("{label} {} out of order", item.describe()),
834 });
835 }
836 }
837 Ok(())
838}
839
840fn parse_dep_pin_line(line: usize, value: &str) -> Result<VendoredDep, SnapshotError> {
841 let (name_part, after_eq) =
842 value
843 .split_once('=')
844 .ok_or_else(|| SnapshotError::MalformedHeader {
845 line,
846 detail: format!("dep-pin missing '=': {value:?}"),
847 })?;
848 let name_str = name_part.trim();
849 let source_and_version = after_eq.trim();
850 let (source_label, version_str) =
851 source_and_version
852 .split_once(' ')
853 .ok_or_else(|| SnapshotError::MalformedHeader {
854 line,
855 detail: format!("dep-pin missing version after source: {value:?}"),
856 })?;
857 let source_label = source_label.trim();
858 let source = VendoredDepSource::from_label(source_label).ok_or_else(|| {
859 SnapshotError::MalformedHeader {
860 line,
861 detail: format!("unknown dep-pin source {source_label:?}"),
862 }
863 })?;
864 let name = DependencyName::from_str(name_str).map_err(SnapshotError::Name)?;
865 let version = DependencyVersion::from_str(version_str.trim()).map_err(SnapshotError::Name)?;
866 Ok(VendoredDep {
867 name,
868 version,
869 source,
870 })
871}
872
873fn parse_versioned_machine(rest: &str) -> Result<VersionedMachineVersion, String> {
874 let (fa_part, after_fa) = rest
875 .split_once('=')
876 .ok_or_else(|| format!("versioned_machine missing '=': {rest:?}"))?;
877 let fa = parse_fun_arity(fa_part.trim()).map_err(|e| e.to_string())?;
878 let after_fa = after_fa.trim();
879 let (value_part, provenance_part) = match after_fa.split_once(' ') {
880 Some((v, p)) => (v.trim(), p.trim()),
881 None => (after_fa, ""),
882 };
883 let value = match value_part {
884 "-" => None,
885 s => Some(
886 s.parse::<u64>()
887 .map_err(|e| format!("versioned_machine value: {e}"))?,
888 ),
889 };
890 let provenance = parse_provenance(provenance_part)?;
891 Ok(VersionedMachineVersion {
892 function: fa.name,
893 arity: fa.arity,
894 value,
895 provenance,
896 })
897}
898
899fn parse_provenance(rest: &str) -> Result<Provenance, String> {
900 if rest.is_empty() || rest == "literal" {
901 return Ok(Provenance::Literal);
902 }
903 if let Some(after) = rest.strip_prefix("macro_body ") {
904 let (name_str, at_tail) = match after.split_once(" at ") {
905 Some((n, p)) => (n.trim(), Some(p.trim().to_owned())),
906 None => (after.trim(), None),
907 };
908 let macro_name = MacroName::from_str(name_str).map_err(|e| e.to_string())?;
909 return Ok(Provenance::MacroBody {
910 macro_name,
911 defined_in: at_tail,
912 });
913 }
914 Err(format!("unknown provenance {rest:?}"))
915}
916
917fn parse_wire_constant(rest: &str) -> Result<WireConstantBinding, String> {
918 let (name_part, after_eq) = rest
919 .split_once('=')
920 .ok_or_else(|| format!("wire_constant missing '=': {rest:?}"))?;
921 let macro_name = MacroName::from_str(name_part.trim()).map_err(|e| e.to_string())?;
922 let body = after_eq.trim();
923 let (body_no_at, defined_in) = match body.rsplit_once(" at ") {
924 Some((b, p)) => (b.trim(), Some(p.trim().to_owned())),
925 None => (body, None),
926 };
927 let (kind, value_str) = body_no_at
928 .split_once(' ')
929 .ok_or_else(|| format!("wire_constant missing type/value: {rest:?}"))?;
930 let value = match kind.trim() {
931 "u64" => WireValue::U64(
932 value_str
933 .trim()
934 .parse::<u64>()
935 .map_err(|e| format!("wire_constant u64 value: {e}"))?,
936 ),
937 "bytes" => WireValue::Bytes(value_str.trim().as_bytes().to_vec()),
938 "opaque" => WireValue::Opaque(value_str.trim().to_owned()),
939 other => return Err(format!("unknown wire_constant kind {other:?}")),
940 };
941 Ok(WireConstantBinding {
942 macro_name,
943 value,
944 defined_in,
945 })
946}
947
948#[derive(Debug)]
949struct ClassOrder<T> {
950 label: &'static str,
951 last: Option<T>,
952}
953
954impl<T: Copy + Ord + Debug> ClassOrder<T> {
955 fn new(label: &'static str) -> Self {
956 Self { label, last: None }
957 }
958
959 fn advance(&mut self, c: T, line: usize) -> Result<(), SnapshotError> {
960 if let Some(prev) = self.last
961 && c < prev
962 {
963 return Err(SnapshotError::NotCanonical {
964 line,
965 detail: format!("{}: {c:?} after {prev:?}", self.label),
966 });
967 }
968 self.last = Some(c);
969 Ok(())
970 }
971}
972
973#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
974enum EntryClass {
975 Visibility,
976 Path,
977 App,
978 Behaviour,
979 Export,
980 ExportType,
981 Callback,
982 OptionalCallback,
983 Spec,
984 Type,
985 Opaque,
986 Deprecated,
987 Record,
988 TestOnlyExport,
989 IfdefMacro,
990 VariantCBlock,
991 VersionedMachine,
992 WireConstant,
993}
994
995#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
996enum HrlEntryClass {
997 Type,
998 Opaque,
999 Record,
1000}