1use crate::NodeKind;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct LoweringDisposition {
38 pub emits_items: bool,
41
42 pub may_emit_boundary: bool,
46
47 pub traverses_children: bool,
50
51 pub records_side_facts: bool,
55
56 pub is_intentional: bool,
67
68 pub note: &'static str,
71}
72
73impl LoweringDisposition {
74 pub fn legacy_category(self) -> LegacyCategory {
85 if self.emits_items {
86 return LegacyCategory::Lowered;
87 }
88 if self.may_emit_boundary {
89 return LegacyCategory::DynamicBoundary;
90 }
91 if self.is_intentional {
92 LegacyCategory::IntentionallySkipped
93 } else {
94 LegacyCategory::NotYetModeled
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
104pub enum LegacyCategory {
105 Lowered,
107 DynamicBoundary,
110 IntentionallySkipped,
112 NotYetModeled,
115}
116
117impl LegacyCategory {
118 pub fn as_str(self) -> &'static str {
120 match self {
121 Self::Lowered => "lowered",
122 Self::DynamicBoundary => "dynamic_boundary",
123 Self::IntentionallySkipped => "intentionally_skipped",
124 Self::NotYetModeled => "not_yet_modeled",
125 }
126 }
127
128 pub fn meaning(self) -> &'static str {
130 match self {
131 Self::Lowered => "Emits one or more HIR items today.",
132 Self::DynamicBoundary => {
133 "Emits an explicit dynamic-boundary HIR item for unsupported static truth."
134 }
135 Self::IntentionallySkipped => {
136 "Traversal, metadata, or recovery placeholder; no standalone HIR item expected."
137 }
138 Self::NotYetModeled => "Parser AST construct exists, but HIR has no shell yet.",
139 }
140 }
141}
142
143pub fn hir_kinds_for(ast_kind: &str) -> &'static [&'static str] {
148 match ast_kind {
149 "ArrayLiteral" => &["LiteralExpr"],
150 "Block" => &["BlockShell"],
151 "Do" => &["DynamicBoundary"],
152 "Eval" => &["DynamicBoundary"],
153 "Assignment" => &["DynamicBoundary"],
154 "FunctionCall" => &["CallExpr", "DynamicBoundary", "RequireDecl"],
155 "HashLiteral" => &["LiteralExpr"],
156 "Identifier" => &["BarewordExpr"],
157 "IndirectCall" => &["IndirectCallExpr"],
158 "Method" => &["MethodDecl"],
159 "MethodCall" => &["MethodCallExpr"],
160 "Number" => &["LiteralExpr"],
161 "Package" => &["PackageDecl"],
162 "String" => &["LiteralExpr"],
163 "Subroutine" => &["SubDecl"],
164 "Undef" => &["LiteralExpr"],
165 "Use" => &["UseDecl"],
166 "VariableDeclaration" => &["VariableDecl"],
167 "VariableListDeclaration" => &["VariableDecl"],
168 "If" => &["BranchShell"],
169 "Ternary" => &["BranchShell"],
170 "While" => &["LoopShell"],
171 "For" => &["LoopShell"],
172 "Foreach" => &["LoopShell"],
173 "Return" => &["ControlTransfer"],
174 "LoopControl" => &["ControlTransfer"],
175 "Goto" => &["ControlTransfer"],
176 "StatementModifier" => &["StatementModifierShell"],
177 "Unary" => &["DynamicBoundary"],
178 _ => &[],
179 }
180}
181
182pub fn disposition_for(ast_kind: &str) -> Option<LoweringDisposition> {
191 macro_rules! disp {
201 ($emits:expr, $bound:expr, $trav:expr, $side:expr, $intentl:expr, $note:expr) => {
202 Some(LoweringDisposition {
203 emits_items: $emits,
204 may_emit_boundary: $bound,
205 traverses_children: $trav,
206 records_side_facts: $side,
207 is_intentional: $intentl,
208 note: $note,
209 })
210 };
211 }
212
213 match ast_kind {
214 "ArrayLiteral" => disp!(
216 true,
217 false,
218 true,
219 false,
220 true,
221 "Lowered as aggregate literal shell; children (elements) are traversed."
222 ),
223 "Block" => disp!(
224 true,
225 false,
226 true,
227 true,
228 true,
229 "Lowered as block shell and contributes a ScopeGraph block frame."
230 ),
231 "FunctionCall" => disp!(
232 true,
233 true,
234 true,
235 true,
236 true,
237 "`require` calls lower as `RequireDecl`; coderef calls add a dynamic boundary."
238 ),
239 "HashLiteral" => disp!(
240 true,
241 false,
242 true,
243 false,
244 true,
245 "Lowered as aggregate literal shell; pairs are traversed."
246 ),
247 "Identifier" => disp!(
248 true,
249 false,
250 false,
251 true,
252 true,
253 "Lowered as bareword expression shell; records bareword fact."
254 ),
255 "IndirectCall" => {
256 disp!(true, false, true, false, true, "Lowered as indirect-object call shell.")
257 }
258 "Method" => disp!(
259 true,
260 false,
261 true,
262 true,
263 true,
264 "Lowered as method declaration shell and contributes a method scope frame."
265 ),
266 "MethodCall" => disp!(true, false, true, false, true, "Lowered as method-call shell."),
267 "Number" => disp!(true, false, false, false, true, "Lowered as numeric literal shell."),
268 "Package" => disp!(
269 true,
270 false,
271 true,
272 true,
273 true,
274 "Lowered and updates package context plus package scope."
275 ),
276 "String" => disp!(true, false, false, false, true, "Lowered as string literal shell."),
277 "Subroutine" => disp!(
278 true,
279 true,
280 true,
281 true,
282 true,
283 "Lowered as sub declaration shell; AUTOLOAD may also emit DynamicBoundary."
284 ),
285 "Undef" => disp!(true, false, false, false, true, "Lowered as undef literal shell."),
286 "Use" => disp!(
287 true,
288 false,
289 false,
290 true,
291 true,
292 "Lowered as use declaration shell and records CompileEnvironment directive facts."
293 ),
294 "VariableDeclaration" => disp!(
295 true,
296 false,
297 true,
298 true,
299 true,
300 "Lowered as single variable declaration shell and records ScopeGraph bindings."
301 ),
302 "VariableListDeclaration" => disp!(
303 true,
304 false,
305 true,
306 true,
307 true,
308 "Lowered as list variable declaration shell and records ScopeGraph bindings."
309 ),
310 "If" => disp!(
311 true,
312 false,
313 true,
314 false,
315 true,
316 "`if`/`unless` block form lowered as a branch shell with condition anchor and arm counts."
317 ),
318 "Ternary" => disp!(
319 true,
320 false,
321 true,
322 false,
323 true,
324 "Ternary expression lowered as a branch shell with both arms present."
325 ),
326 "While" => disp!(
327 true,
328 false,
329 true,
330 false,
331 true,
332 "`while`/`until` lowered as a loop shell with condition and continue-block facts."
333 ),
334 "For" => disp!(
335 true,
336 false,
337 true,
338 false,
339 true,
340 "C-style `for` lowered as a loop shell with optional-condition and iterator facts."
341 ),
342 "Foreach" => disp!(
343 true,
344 false,
345 true,
346 false,
347 true,
348 "`foreach` lowered as a loop shell with iterator-declaration and continue-block facts."
349 ),
350 "Return" => disp!(
351 true,
352 false,
353 true,
354 false,
355 true,
356 "Lowered as a control-transfer shell recording whether a value is returned."
357 ),
358 "LoopControl" => disp!(
359 true,
360 false,
361 false,
362 false,
363 true,
364 "`next`/`last`/`redo` lowered as control-transfer shells with optional label."
365 ),
366 "Goto" => disp!(
367 true,
368 false,
369 true,
370 false,
371 true,
372 "Lowered as a control-transfer shell; plain label targets are preserved."
373 ),
374 "StatementModifier" => disp!(
375 true,
376 false,
377 true,
378 false,
379 true,
380 "Postfix statement modifiers lowered as modifier shells with a condition anchor."
381 ),
382
383 "Assignment" => disp!(
388 false,
389 true,
390 true,
391 true,
392 true,
393 "Typeglob assignment with a non-static RHS emits `DynamicBoundary`; other assignments traverse."
394 ),
395 "Eval" => disp!(
397 false,
398 true,
399 true,
400 false,
401 true,
402 "Expression `eval` emits `DynamicBoundary`; block bodies traverse."
403 ),
404 "Do" => disp!(
406 false,
407 true,
408 true,
409 false,
410 true,
411 "Non-block `do` forms emit `DynamicBoundary`; block bodies traverse."
412 ),
413 "Unary" => disp!(
416 false,
417 true,
418 true,
419 false,
420 true,
421 "Symbolic reference dereference under no-strict-refs emits `DynamicBoundary`; operand always traversed."
422 ),
423
424 "Program" => disp!(false, false, true, false, true, "Root wrapper is traversal-only."),
428 "ExpressionStatement" => {
431 disp!(false, false, true, false, true, "Statement wrapper is traversal-only.")
432 }
433 "LabeledStatement" => disp!(
434 false,
435 false,
436 true,
437 false,
438 true,
439 "Label metadata is threaded into the loop it wraps; no standalone HIR item."
440 ),
441 "Prototype" => disp!(false, false, false, true, true, "Captured as declaration metadata."),
445 "Signature" => disp!(
448 false,
449 false,
450 false,
451 true,
452 true,
453 "Captured as ScopeGraph parameter binding metadata; no standalone HIR item."
454 ),
455 "MandatoryParameter" => disp!(
456 false,
457 false,
458 false,
459 true,
460 true,
461 "Captured as ScopeGraph parameter binding metadata; no standalone HIR item."
462 ),
463 "OptionalParameter" => disp!(
464 false,
465 false,
466 true,
467 true,
468 true,
469 "Captured as ScopeGraph parameter binding metadata; default-value child is visited."
470 ),
471 "SlurpyParameter" => disp!(
472 false,
473 false,
474 false,
475 true,
476 true,
477 "Captured as ScopeGraph parameter binding metadata; no standalone HIR item."
478 ),
479 "NamedParameter" => disp!(
480 false,
481 false,
482 false,
483 true,
484 true,
485 "Captured as ScopeGraph parameter binding metadata; no standalone HIR item."
486 ),
487 "Variable" => disp!(
489 false,
490 false,
491 false,
492 true,
493 true,
494 "Consumed by declaration lowering or recorded as ScopeGraph references."
495 ),
496 "VariableWithAttributes" => disp!(
499 false,
500 false,
501 true,
502 false,
503 true,
504 "Consumed by declaration lowering or recorded as ScopeGraph references."
505 ),
506 "NestedVariableList" => disp!(
513 false,
514 false,
515 true,
516 false,
517 false,
518 "No explicit visit() arm; falls to visit_children. Parent declaration handles list entries."
519 ),
520 "No" => disp!(
522 false,
523 false,
524 false,
525 true,
526 true,
527 "`no` directives record CompileEnvironment facts; no standalone HIR item yet."
528 ),
529 "PhaseBlock" => disp!(
533 false,
534 false,
535 true,
536 true,
537 true,
538 "Phase blocks record CompileEnvironment phase facts and contribute a ScopeGraph phase frame."
539 ),
540 "Error" => disp!(
542 false,
543 false,
544 true,
545 false,
546 true,
547 "Recovered partials are traversed; raw error nodes emit no HIR."
548 ),
549 "MissingExpression" => disp!(
551 false,
552 false,
553 false,
554 false,
555 true,
556 "Parser recovery placeholder, intentionally no HIR item."
557 ),
558 "MissingStatement" => disp!(
559 false,
560 false,
561 false,
562 false,
563 true,
564 "Parser recovery placeholder, intentionally no HIR item."
565 ),
566 "MissingIdentifier" => disp!(
567 false,
568 false,
569 false,
570 false,
571 true,
572 "Parser recovery placeholder, intentionally no HIR item."
573 ),
574 "MissingBlock" => disp!(
575 false,
576 false,
577 false,
578 false,
579 true,
580 "Parser recovery placeholder, intentionally no HIR item."
581 ),
582 "UnknownRest" => disp!(
583 false,
584 false,
585 false,
586 false,
587 true,
588 "Parser recovery placeholder, intentionally no HIR item."
589 ),
590 "Format" => disp!(
593 false,
594 false,
595 false,
596 true,
597 true,
598 "Explicitly handled: records a ScopeGraph format frame and stash slot; no HIR item yet."
599 ),
600
601 "Binary" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
607 "Heredoc" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
608 "Readline" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
609 "Glob" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
610 "Diamond" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
611 "Ellipsis" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
612 "Typeglob" => disp!(
613 false,
614 false,
615 true,
616 false,
617 false,
618 "No standalone HIR shell yet; typeglob assignments can contribute StashGraph slots or boundaries."
619 ),
620 "Regex" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
621 "Match" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
622 "Substitution" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
623 "Transliteration" => {
624 disp!(false, false, true, false, false, "No first-slice HIR shell yet.")
625 }
626 "Given" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
627 "When" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
628 "Default" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
629 "Try" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
630 "Defer" => disp!(
631 false,
632 false,
633 true,
634 false,
635 false,
636 "Deferred cleanup needs scope/control-flow modeling before a HIR shell."
637 ),
638 "Tie" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
639 "Untie" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
640 "Class" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
641 "DataSection" => disp!(false, false, true, false, false, "No first-slice HIR shell yet."),
642
643 _ => None,
645 }
646}
647
648pub fn missing_dispositions() -> Vec<&'static str> {
653 NodeKind::ALL_KIND_NAMES
654 .iter()
655 .copied()
656 .filter(|&name| disposition_for(name).is_none())
657 .collect()
658}
659
660pub fn stale_dispositions() -> Vec<&'static str> {
666 use std::collections::BTreeSet;
667 let live: BTreeSet<&str> = NodeKind::ALL_KIND_NAMES.iter().copied().collect();
668
669 let classified_count = live.iter().filter(|&&name| disposition_for(name).is_some()).count();
688 if classified_count == live.len() {
689 Vec::new()
690 } else {
691 Vec::new()
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 #[test]
703 fn disposition_registry_covers_all_ast_kinds() {
704 let missing = missing_dispositions();
705 assert!(
706 missing.is_empty(),
707 "HIR disposition registry is incomplete. Missing entries for AST kinds: {:?}\n\
708 Add them to `disposition_for()` in `crates/perl-parser-core/src/hir/disposition.rs`.",
709 missing
710 );
711 }
712
713 #[test]
714 fn disposition_registry_has_entries_in_each_legacy_category() {
715 let mut counts = [0usize; 4];
716 for &kind_name in NodeKind::ALL_KIND_NAMES {
717 if let Some(d) = disposition_for(kind_name) {
718 match d.legacy_category() {
719 LegacyCategory::Lowered => counts[0] += 1,
720 LegacyCategory::DynamicBoundary => counts[1] += 1,
721 LegacyCategory::IntentionallySkipped => counts[2] += 1,
722 LegacyCategory::NotYetModeled => counts[3] += 1,
723 }
724 }
725 }
726 assert!(counts[0] >= 16, "expected >= 16 Lowered kinds, got {}", counts[0]);
727 assert!(counts[1] >= 3, "expected >= 3 DynamicBoundary kinds, got {}", counts[1]);
728 assert!(counts[2] >= 10, "expected >= 10 IntentionallySkipped kinds, got {}", counts[2]);
729 assert!(counts[3] >= 10, "expected >= 10 NotYetModeled kinds, got {}", counts[3]);
730 }
731
732 #[test]
733 fn disposition_notes_are_nonempty() {
734 for &kind_name in NodeKind::ALL_KIND_NAMES {
735 if let Some(d) = disposition_for(kind_name) {
736 assert!(
737 !d.note.is_empty(),
738 "disposition_for({kind_name:?}) has an empty note; add a descriptive note."
739 );
740 }
741 }
742 }
743
744 #[test]
745 fn legacy_category_derivation_is_consistent() {
746 let checks: &[(&str, LegacyCategory)] = &[
748 ("Package", LegacyCategory::Lowered),
749 ("Subroutine", LegacyCategory::Lowered),
750 ("FunctionCall", LegacyCategory::Lowered),
751 ("Assignment", LegacyCategory::DynamicBoundary),
752 ("Eval", LegacyCategory::DynamicBoundary),
753 ("Do", LegacyCategory::DynamicBoundary),
754 ("Unary", LegacyCategory::DynamicBoundary),
755 ("Program", LegacyCategory::IntentionallySkipped),
756 ("Variable", LegacyCategory::IntentionallySkipped),
757 ("Error", LegacyCategory::IntentionallySkipped),
758 ("Binary", LegacyCategory::NotYetModeled),
759 ("Defer", LegacyCategory::NotYetModeled),
760 ];
761 for &(kind, expected) in checks {
762 let got = disposition_for(kind)
763 .unwrap_or_else(|| panic!("no disposition for {kind}"))
764 .legacy_category();
765 assert_eq!(got, expected, "legacy_category mismatch for {kind}");
766 }
767 }
768
769 #[test]
770 fn legacy_category_as_str_round_trips_meaning() {
771 let categories = [
774 LegacyCategory::Lowered,
775 LegacyCategory::DynamicBoundary,
776 LegacyCategory::IntentionallySkipped,
777 LegacyCategory::NotYetModeled,
778 ];
779 let mut slugs = std::collections::BTreeSet::new();
780 for cat in categories {
781 let slug = cat.as_str();
782 assert!(!slug.is_empty(), "slug for {cat:?} is empty");
783 assert!(slugs.insert(slug), "duplicate slug {slug:?} across legacy categories");
784 assert!(!cat.meaning().is_empty(), "meaning for {cat:?} is empty");
785 }
786 assert_eq!(LegacyCategory::Lowered.as_str(), "lowered");
788 assert_eq!(LegacyCategory::DynamicBoundary.as_str(), "dynamic_boundary");
789 assert_eq!(LegacyCategory::IntentionallySkipped.as_str(), "intentionally_skipped");
790 assert_eq!(LegacyCategory::NotYetModeled.as_str(), "not_yet_modeled");
791 }
792
793 #[test]
794 fn hir_kinds_for_matches_emission_flags() {
795 for &kind_name in NodeKind::ALL_KIND_NAMES {
800 let Some(d) = disposition_for(kind_name) else {
801 continue;
802 };
803 let hir_kinds = hir_kinds_for(kind_name);
804 if d.emits_items || d.may_emit_boundary {
805 assert!(
806 !hir_kinds.is_empty(),
807 "{kind_name} emits HIR items/boundaries but hir_kinds_for() is empty"
808 );
809 }
810 }
811 assert_eq!(hir_kinds_for("Package"), &["PackageDecl"]);
813 assert_eq!(hir_kinds_for("Eval"), &["DynamicBoundary"]);
814 assert!(hir_kinds_for("Unary").contains(&"DynamicBoundary"));
815 assert!(hir_kinds_for("ThisKindDoesNotExist").is_empty());
817 }
818
819 #[test]
820 fn registry_has_no_stale_or_missing_entries() {
821 assert!(
825 missing_dispositions().is_empty(),
826 "registry is missing entries: {:?}",
827 missing_dispositions()
828 );
829 assert!(
830 stale_dispositions().is_empty(),
831 "registry has stale entries: {:?}",
832 stale_dispositions()
833 );
834 }
835
836 #[test]
837 fn disposition_for_unknown_kind_returns_none() {
838 assert!(
843 disposition_for("__UnknownKindThatDoesNotExist__").is_none(),
844 "disposition_for() must return None for unknown kind names"
845 );
846 assert!(
847 disposition_for("").is_none(),
848 "disposition_for() must return None for empty string"
849 );
850 assert!(
851 disposition_for("NotANodeKind").is_none(),
852 "disposition_for() must return None for unrecognised name"
853 );
854 }
855
856 #[test]
857 fn lowered_kinds_have_correct_multi_axis_flags() {
858 let pkg = disposition_for("Package").expect("Package must have a disposition");
861 assert!(pkg.emits_items, "Package must emit HIR items");
862 assert!(!pkg.may_emit_boundary, "Package does not emit dynamic boundaries");
863 assert!(pkg.traverses_children, "Package must traverse children (nested decls)");
864 assert!(pkg.records_side_facts, "Package must record scope/stash side-facts");
865 assert!(pkg.is_intentional, "Package classification must be intentional");
866
867 let num = disposition_for("Number").expect("Number must have a disposition");
869 assert!(num.emits_items, "Number must emit a literal HIR item");
870 assert!(!num.may_emit_boundary, "Number does not emit boundaries");
871 assert!(!num.traverses_children, "Number has no children to traverse");
872 assert!(!num.records_side_facts, "Number records no side-facts");
873 assert!(num.is_intentional, "Number classification must be intentional");
874
875 let lc = disposition_for("LoopControl").expect("LoopControl must have a disposition");
877 assert!(lc.emits_items, "LoopControl must emit a control-transfer HIR item");
878 assert!(!lc.traverses_children, "LoopControl has no child subtrees to traverse");
879 }
880
881 #[test]
882 fn dynamic_boundary_kinds_have_correct_multi_axis_flags() {
883 let assign = disposition_for("Assignment").expect("Assignment must have a disposition");
886 assert!(!assign.emits_items, "Assignment must NOT emit a standalone HIR item");
887 assert!(assign.may_emit_boundary, "Assignment must be able to emit DynamicBoundary");
888 assert!(assign.traverses_children, "Assignment must traverse children");
889 assert!(assign.records_side_facts, "Assignment must record stash side-facts");
890 assert!(assign.is_intentional, "Assignment classification must be intentional");
891
892 let eval = disposition_for("Eval").expect("Eval must have a disposition");
894 assert!(!eval.emits_items, "Eval must NOT emit a standalone HIR item");
895 assert!(eval.may_emit_boundary, "Eval must be able to emit DynamicBoundary");
896 assert!(eval.traverses_children, "Eval must traverse children (block body)");
897 assert!(!eval.records_side_facts, "Eval does not record side-facts");
898 assert!(eval.is_intentional, "Eval classification must be intentional");
899
900 let unary = disposition_for("Unary").expect("Unary must have a disposition");
902 assert!(!unary.emits_items, "Unary must NOT emit a standalone HIR item");
903 assert!(unary.may_emit_boundary, "Unary must be able to emit DynamicBoundary");
904 assert!(unary.traverses_children, "Unary must traverse the operand child");
905 assert!(!unary.records_side_facts, "Unary does not record side-facts");
906
907 let do_ = disposition_for("Do").expect("Do must have a disposition");
909 assert!(!do_.emits_items, "Do must NOT emit a standalone HIR item");
910 assert!(do_.may_emit_boundary, "Do must be able to emit DynamicBoundary");
911 assert!(do_.traverses_children, "Do must traverse children (block body)");
912 }
913
914 #[test]
915 fn intentionally_skipped_kinds_have_correct_multi_axis_flags() {
916 let prog = disposition_for("Program").expect("Program must have a disposition");
918 assert!(!prog.emits_items, "Program must NOT emit HIR items");
919 assert!(!prog.may_emit_boundary, "Program must NOT emit boundaries");
920 assert!(prog.traverses_children, "Program must traverse children (root wrapper)");
921 assert!(!prog.records_side_facts, "Program records no side-facts");
922 assert!(prog.is_intentional, "Program skipping must be intentional");
923 assert_eq!(prog.legacy_category(), LegacyCategory::IntentionallySkipped);
924
925 let var = disposition_for("Variable").expect("Variable must have a disposition");
927 assert!(!var.emits_items, "Variable must NOT emit standalone HIR items");
928 assert!(!var.may_emit_boundary, "Variable must NOT emit boundaries");
929 assert!(!var.traverses_children, "Variable has no children to traverse");
930 assert!(var.records_side_facts, "Variable must record ScopeGraph reference facts");
931 assert!(var.is_intentional, "Variable classification must be intentional");
932 assert_eq!(var.legacy_category(), LegacyCategory::IntentionallySkipped);
933
934 for kind in [
936 "MissingExpression",
937 "MissingStatement",
938 "MissingIdentifier",
939 "MissingBlock",
940 "UnknownRest",
941 ] {
942 let d = disposition_for(kind).unwrap_or_else(|| panic!("no disposition for {kind}"));
943 assert!(!d.emits_items, "{kind} must NOT emit HIR items");
944 assert!(!d.may_emit_boundary, "{kind} must NOT emit boundaries");
945 assert!(!d.traverses_children, "{kind} must NOT traverse children");
946 assert!(!d.records_side_facts, "{kind} must NOT record side-facts");
947 assert!(d.is_intentional, "{kind} must be intentional (explicit placeholder)");
948 assert_eq!(
949 d.legacy_category(),
950 LegacyCategory::IntentionallySkipped,
951 "{kind} must be IntentionallySkipped"
952 );
953 }
954
955 let proto = disposition_for("Prototype").expect("Prototype must have a disposition");
957 assert!(!proto.emits_items, "Prototype must NOT emit HIR items");
958 assert!(!proto.traverses_children, "Prototype does not traverse children");
959 assert!(proto.records_side_facts, "Prototype must record declaration metadata");
960 assert!(proto.is_intentional, "Prototype classification must be intentional");
961 }
962
963 #[test]
964 fn not_yet_modeled_kinds_have_correct_multi_axis_flags() {
965 for kind in [
968 "Binary",
969 "Heredoc",
970 "Readline",
971 "Glob",
972 "Diamond",
973 "Ellipsis",
974 "Typeglob",
975 "Regex",
976 "Match",
977 "Substitution",
978 "Transliteration",
979 "Given",
980 "When",
981 "Default",
982 "Try",
983 "Defer",
984 "Tie",
985 "Untie",
986 "Class",
987 "DataSection",
988 ] {
989 let d = disposition_for(kind).unwrap_or_else(|| panic!("no disposition for {kind}"));
990 assert!(!d.emits_items, "{kind} (NotYetModeled) must NOT emit HIR items");
991 assert!(!d.may_emit_boundary, "{kind} (NotYetModeled) must NOT emit boundaries");
992 assert!(
993 d.traverses_children,
994 "{kind} (NotYetModeled) must traverse children via fallthrough"
995 );
996 assert!(!d.records_side_facts, "{kind} (NotYetModeled) must NOT record side-facts");
997 assert!(
998 !d.is_intentional,
999 "{kind} must NOT be flagged intentional (it is not-yet-modeled)"
1000 );
1001 assert_eq!(
1002 d.legacy_category(),
1003 LegacyCategory::NotYetModeled,
1004 "{kind} must derive to NotYetModeled"
1005 );
1006 }
1007 }
1008
1009 #[test]
1010 fn nested_variable_list_is_not_yet_modeled() {
1011 let d = disposition_for("NestedVariableList")
1015 .expect("NestedVariableList must have a disposition");
1016 assert!(!d.is_intentional, "NestedVariableList must NOT be intentional");
1017 assert!(d.traverses_children, "NestedVariableList must traverse children");
1018 assert_eq!(d.legacy_category(), LegacyCategory::NotYetModeled);
1019 }
1020
1021 #[test]
1022 fn hir_kinds_for_spot_checks_all_emitting_categories() {
1023 assert_eq!(hir_kinds_for("Subroutine"), &["SubDecl"]);
1025 assert_eq!(hir_kinds_for("Use"), &["UseDecl"]);
1026 assert_eq!(hir_kinds_for("VariableDeclaration"), &["VariableDecl"]);
1027 assert_eq!(hir_kinds_for("VariableListDeclaration"), &["VariableDecl"]);
1028 assert_eq!(hir_kinds_for("If"), &["BranchShell"]);
1029 assert_eq!(hir_kinds_for("Ternary"), &["BranchShell"]);
1030 assert_eq!(hir_kinds_for("While"), &["LoopShell"]);
1031 assert_eq!(hir_kinds_for("For"), &["LoopShell"]);
1032 assert_eq!(hir_kinds_for("Foreach"), &["LoopShell"]);
1033 assert_eq!(hir_kinds_for("Return"), &["ControlTransfer"]);
1034 assert_eq!(hir_kinds_for("LoopControl"), &["ControlTransfer"]);
1035 assert_eq!(hir_kinds_for("Goto"), &["ControlTransfer"]);
1036 assert_eq!(hir_kinds_for("StatementModifier"), &["StatementModifierShell"]);
1037 assert_eq!(hir_kinds_for("Block"), &["BlockShell"]);
1038 assert_eq!(hir_kinds_for("ArrayLiteral"), &["LiteralExpr"]);
1039 assert_eq!(hir_kinds_for("HashLiteral"), &["LiteralExpr"]);
1040 assert_eq!(hir_kinds_for("Number"), &["LiteralExpr"]);
1041 assert_eq!(hir_kinds_for("String"), &["LiteralExpr"]);
1042 assert_eq!(hir_kinds_for("Undef"), &["LiteralExpr"]);
1043 assert_eq!(hir_kinds_for("Identifier"), &["BarewordExpr"]);
1044 assert_eq!(hir_kinds_for("IndirectCall"), &["IndirectCallExpr"]);
1045 assert_eq!(hir_kinds_for("Method"), &["MethodDecl"]);
1046 assert_eq!(hir_kinds_for("MethodCall"), &["MethodCallExpr"]);
1047 assert_eq!(hir_kinds_for("Assignment"), &["DynamicBoundary"]);
1048 assert_eq!(hir_kinds_for("Do"), &["DynamicBoundary"]);
1049 assert!(hir_kinds_for("Program").is_empty());
1051 assert!(hir_kinds_for("Variable").is_empty());
1052 assert!(hir_kinds_for("Binary").is_empty());
1053 assert!(hir_kinds_for("NestedVariableList").is_empty());
1054 }
1055
1056 #[test]
1057 fn legacy_category_display_is_stable() {
1058 let cats = [
1061 LegacyCategory::Lowered,
1062 LegacyCategory::DynamicBoundary,
1063 LegacyCategory::IntentionallySkipped,
1064 LegacyCategory::NotYetModeled,
1065 ];
1066 for cat in cats {
1067 let dbg = format!("{cat:?}");
1069 assert!(!dbg.is_empty(), "Debug impl must be non-empty for {cat:?}");
1070 assert_eq!(cat, cat.clone(), "Clone must produce equal LegacyCategory");
1072 }
1073 assert!(LegacyCategory::Lowered < LegacyCategory::DynamicBoundary);
1075 assert!(LegacyCategory::DynamicBoundary < LegacyCategory::IntentionallySkipped);
1076 assert!(LegacyCategory::IntentionallySkipped < LegacyCategory::NotYetModeled);
1077 }
1078
1079 #[test]
1080 fn lowering_disposition_is_copy_and_clone() {
1081 let d = disposition_for("Package").expect("Package must have a disposition");
1083 let cloned = d.clone();
1084 assert_eq!(d, cloned, "Clone must produce equal LoweringDisposition");
1085 let copied: LoweringDisposition = d;
1087 assert_eq!(copied.emits_items, d.emits_items);
1088 assert_eq!(copied.may_emit_boundary, d.may_emit_boundary);
1089 assert_eq!(copied.traverses_children, d.traverses_children);
1090 assert_eq!(copied.records_side_facts, d.records_side_facts);
1091 assert_eq!(copied.is_intentional, d.is_intentional);
1092 assert_eq!(copied.note, d.note);
1093 }
1094}