Skip to main content

perl_parser_core/hir/
model.rs

1//! HIR data model.
2
3use crate::SourceLocation;
4use perl_semantic_facts::{
5    AnchorId, Confidence, ExportSet, ExportTag, FileId, ImportKind, ImportSpec, ImportSymbols,
6    Provenance, ScopeId, VisibleSymbol, VisibleSymbolContext, VisibleSymbolSource,
7};
8use std::collections::BTreeMap;
9
10/// Stable identifier for a HIR item within one lowered file.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
12#[non_exhaustive]
13pub struct HirId {
14    index: u32,
15}
16
17impl HirId {
18    /// Create an identifier from a zero-based lowering index.
19    #[inline]
20    pub const fn from_index(index: u32) -> Self {
21        Self { index }
22    }
23
24    /// Return the zero-based lowering index.
25    #[inline]
26    pub const fn index(self) -> u32 {
27        self.index
28    }
29}
30
31/// Stable identifier for a HIR scope frame within one lowered file.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33#[non_exhaustive]
34pub struct HirScopeId {
35    index: u32,
36}
37
38impl HirScopeId {
39    /// Create a scope identifier from a zero-based lowering index.
40    #[inline]
41    pub const fn from_index(index: u32) -> Self {
42        Self { index }
43    }
44
45    /// Return the zero-based lowering index.
46    #[inline]
47    pub const fn index(self) -> u32 {
48        self.index
49    }
50}
51
52/// Stable identifier for a HIR binding within one lowered file.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
54#[non_exhaustive]
55pub struct HirBindingId {
56    index: u32,
57}
58
59impl HirBindingId {
60    /// Create a binding identifier from a zero-based lowering index.
61    #[inline]
62    pub const fn from_index(index: u32) -> Self {
63        Self { index }
64    }
65
66    /// Return the zero-based lowering index.
67    #[inline]
68    pub const fn index(self) -> u32 {
69        self.index
70    }
71}
72
73/// Parser AST location that produced a HIR item.
74#[derive(Debug, Clone, PartialEq, Eq)]
75#[non_exhaustive]
76pub struct AstAnchor {
77    /// Parser AST node kind name.
78    pub node_kind: &'static str,
79    /// Full AST node source range.
80    pub range: SourceLocation,
81    /// Precise name range when the AST exposes one.
82    pub name_range: Option<SourceLocation>,
83}
84
85/// Recovery quality for a lowered HIR item.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87#[non_exhaustive]
88pub enum RecoveryConfidence {
89    /// Lowered from a normally parsed AST node.
90    Parsed,
91    /// Lowered from a parser recovery wrapper with a partial valid tree.
92    Recovered,
93    /// Lowered from a partially known or placeholder AST shape.
94    Partial,
95    /// Lowering could not classify recovery confidence yet.
96    Unknown,
97}
98
99/// HIR for one parsed file.
100#[derive(Debug, Clone, PartialEq, Eq, Default)]
101#[non_exhaustive]
102pub struct HirFile {
103    /// Items lowered in stable depth-first source order.
104    pub items: Vec<HirItem>,
105    /// Scope and binding graph lowered beside HIR items.
106    pub scope_graph: ScopeGraph,
107    /// Package stash graph lowered beside HIR items.
108    pub stash_graph: StashGraph,
109    /// Compile-environment facts lowered beside HIR items.
110    pub compile_environment: CompileEnvironment,
111    /// Source-backed subroutine prototype facts lowered beside HIR items.
112    pub prototype_table: PrototypeTable,
113    /// Source-backed bareword classification facts lowered beside HIR items.
114    pub bareword_table: BarewordTable,
115}
116
117impl HirFile {
118    /// Return true when no HIR items were lowered.
119    #[inline]
120    pub fn is_empty(&self) -> bool {
121        self.items.is_empty()
122    }
123
124    /// Project compile-time effects using the default model metadata.
125    ///
126    /// This is a compiler-substrate proof surface only. It links existing HIR
127    /// facts to the state mutations that produced them without changing LSP
128    /// provider behavior.
129    #[must_use]
130    pub fn compile_effects(&self) -> Vec<CompileEffect> {
131        self.compile_effects_with_source_hash(None)
132    }
133
134    /// Project compile-time effects and attach a caller-supplied source hash.
135    ///
136    /// Parser-core does not own a source database, so persisted workspace
137    /// callers can pass the source hash they use for freshness. Fixture-only
138    /// callers may use [`HirFile::compile_effects`].
139    #[must_use]
140    pub fn compile_effects_with_source_hash(
141        &self,
142        source_hash: Option<String>,
143    ) -> Vec<CompileEffect> {
144        compile_effects_from_file(self, source_hash)
145    }
146
147    /// Project framework-adapter facts using the default registry.
148    ///
149    /// This is a compiler-substrate proof surface only. It does not change LSP
150    /// provider behavior.
151    #[must_use]
152    pub fn framework_facts(&self) -> FrameworkFactGraph {
153        FrameworkAdapterRegistry::default().project_file(self)
154    }
155}
156
157/// Source-backed subroutine prototype facts lowered from parsed declarations.
158#[derive(Debug, Clone, PartialEq, Eq, Default)]
159#[non_exhaustive]
160pub struct PrototypeTable {
161    /// Prototype facts in stable source order.
162    pub facts: Vec<PrototypeFact>,
163}
164
165/// One source-backed prototype fact for a named subroutine declaration.
166#[derive(Debug, Clone, PartialEq, Eq)]
167#[non_exhaustive]
168pub struct PrototypeFact {
169    /// Subroutine name as written in the declaration.
170    pub sub_name: String,
171    /// Package context active at the declaration.
172    pub package_context: Option<String>,
173    /// Prototype content without the surrounding parentheses.
174    pub content: String,
175    /// Precise source range for the prototype node.
176    pub range: SourceLocation,
177    /// Full declaration source range.
178    pub declaration_range: SourceLocation,
179    /// HIR item that declared the subroutine.
180    pub declaration_item: HirId,
181    /// Scope owning the subroutine declaration.
182    pub scope_id: Option<HirScopeId>,
183    /// Source anchor for this prototype fact.
184    pub anchor_id: AnchorId,
185    /// Provenance for the lowered prototype fact.
186    pub provenance: CompileProvenance,
187    /// Confidence for the lowered prototype fact.
188    pub confidence: CompileConfidence,
189}
190
191/// Source-backed bareword facts lowered from parsed identifiers.
192#[derive(Debug, Clone, PartialEq, Eq, Default)]
193#[non_exhaustive]
194pub struct BarewordTable {
195    /// Bareword facts in stable source order.
196    pub facts: Vec<BarewordFact>,
197}
198
199/// One source-backed syntactic bareword classification.
200#[derive(Debug, Clone, PartialEq, Eq)]
201#[non_exhaustive]
202pub struct BarewordFact {
203    /// Bareword text as parsed.
204    pub name: String,
205    /// Syntactic role observed by HIR lowering.
206    pub role: BarewordRole,
207    /// Package context active at the bareword.
208    pub package_context: Option<String>,
209    /// Precise source range for the bareword node.
210    pub range: SourceLocation,
211    /// HIR item that exposed the bareword expression.
212    pub source_item: HirId,
213    /// Scope owning the bareword expression.
214    pub scope_id: Option<HirScopeId>,
215    /// Source anchor for this bareword fact.
216    pub anchor_id: AnchorId,
217    /// Provenance for the lowered bareword fact.
218    pub provenance: CompileProvenance,
219    /// Confidence for the lowered bareword fact.
220    pub confidence: CompileConfidence,
221}
222
223/// Syntactic roles for parsed barewords.
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
225#[non_exhaustive]
226pub enum BarewordRole {
227    /// Plain expression-position bareword with unresolved meaning.
228    Expression,
229    /// Qualified name such as `Foo::Bar` outside a more specific context.
230    QualifiedName,
231    /// Bareword used as a static `require` module target.
232    ModuleRequest,
233    /// Bareword used as a class/object receiver for `->`.
234    MethodReceiver,
235    /// Bareword used as the object in an indirect-object call.
236    IndirectObject,
237    /// Bareword used as an autoquoted hash key.
238    HashKey,
239}
240
241/// One lowered HIR item with common metadata required by compiler layers.
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[non_exhaustive]
244pub struct HirItem {
245    /// Stable item id for this file.
246    pub id: HirId,
247    /// Lowered language construct.
248    pub kind: HirKind,
249    /// Source range for the construct.
250    pub range: SourceLocation,
251    /// Parser AST anchor for this item.
252    pub anchor: AstAnchor,
253    /// Recovery quality inherited from parser recovery.
254    pub recovery_confidence: RecoveryConfidence,
255    /// Package context known at lowering time.
256    pub package_context: Option<String>,
257    /// Scope context known at lowering time.
258    pub scope_context: Option<HirScopeId>,
259}
260
261/// Current HIR compile-effect model version.
262pub const COMPILE_EFFECT_MODEL_VERSION: u32 = 1;
263
264/// One Rust-modeled Perl compile-time effect.
265///
266/// Effects connect source constructs to compiler state mutations and the
267/// semantic fact categories emitted from those mutations. They are proof data
268/// for compiler-substrate work and do not change provider behavior.
269#[derive(Debug, Clone, PartialEq, Eq)]
270#[non_exhaustive]
271pub struct CompileEffect {
272    /// Stable ordinal after source-order sorting.
273    pub ordinal: u32,
274    /// Effect category.
275    pub kind: CompileEffectKind,
276    /// Source construct category.
277    pub source_kind: CompileEffectSourceKind,
278    /// Semantic fact category emitted by this effect.
279    pub fact_kind: CompileEffectFactKind,
280    /// Human-readable fact name.
281    pub fact_name: Option<String>,
282    /// Source range for the effect.
283    pub range: SourceLocation,
284    /// HIR item that produced this effect, when available.
285    pub source_item: Option<HirId>,
286    /// Scope containing this effect, when known.
287    pub scope_id: Option<HirScopeId>,
288    /// Package context active at the effect, when known.
289    pub package_context: Option<String>,
290    /// Source anchor of the emitted fact, when available.
291    pub fact_anchor_id: Option<AnchorId>,
292    /// Dynamic-boundary reason, when this effect records unsupported behavior.
293    pub dynamic_reason: Option<String>,
294    /// Caller-supplied source hash used for freshness, when available.
295    pub source_hash: Option<String>,
296    /// Compile-effect model version.
297    pub model_version: u32,
298    /// How this effect was produced.
299    pub provenance: CompileProvenance,
300    /// Confidence in this effect.
301    pub confidence: CompileConfidence,
302}
303
304/// Compiler state mutation represented by an effect.
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
306#[non_exhaustive]
307pub enum CompileEffectKind {
308    /// Declare a package/stash.
309    DeclarePackage,
310    /// Declare a subroutine code slot.
311    DeclareSub,
312    /// Declare a method code slot.
313    DeclareMethod,
314    /// Declare a lexical or package binding.
315    DeclareBinding,
316    /// Set effective pragma or feature state.
317    SetPragmaState,
318    /// Add an include path.
319    AddIncludePath,
320    /// Remove an include path.
321    RemoveIncludePath,
322    /// Record a module load or resolution request.
323    RequestModule,
324    /// Record an import-symbol relationship.
325    ImportSymbols,
326    /// Assign an inheritance edge.
327    AssignInheritance,
328    /// Assign a simple typeglob alias.
329    AssignGlobAlias,
330    /// Define a constant-like code slot.
331    DefineConstant,
332    /// Register a prototype-bearing subroutine.
333    RegisterPrototype,
334    /// Emit a dynamic-boundary fact instead of guessing.
335    EmitDynamicBoundary,
336}
337
338/// Source construct that produced a compile effect.
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
340#[non_exhaustive]
341pub enum CompileEffectSourceKind {
342    /// `package` declaration.
343    PackageDecl,
344    /// `sub` declaration.
345    SubDecl,
346    /// `method` declaration.
347    MethodDecl,
348    /// Variable declaration.
349    VariableDecl,
350    /// `use` directive.
351    UseDirective,
352    /// `no` directive.
353    NoDirective,
354    /// `require` directive.
355    RequireDirective,
356    /// Compile-time phase block.
357    PhaseBlock,
358    /// Symbolic-reference dereference.
359    SymbolicReferenceDeref,
360    /// Assignment expression.
361    Assignment,
362    /// Typeglob assignment.
363    TypeglobAssignment,
364    /// Derived HIR scope graph fact.
365    ScopeGraph,
366    /// Derived HIR stash graph fact.
367    StashGraph,
368    /// Derived compile-environment fact.
369    CompileEnvironment,
370}
371
372/// Semantic fact category emitted by a compile effect.
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
374#[non_exhaustive]
375pub enum CompileEffectFactKind {
376    /// Package fact.
377    Package,
378    /// Subroutine fact.
379    Sub,
380    /// Method fact.
381    Method,
382    /// Binding fact.
383    Binding,
384    /// Pragma-state fact.
385    PragmaState,
386    /// Include-root fact.
387    IncludeRoot,
388    /// Module-request fact.
389    ModuleRequest,
390    /// Import specification fact.
391    ImportSpec,
392    /// Inheritance edge fact.
393    InheritanceEdge,
394    /// Glob slot fact.
395    GlobSlot,
396    /// Constant fact.
397    Constant,
398    /// Prototype fact.
399    Prototype,
400    /// Dynamic-boundary fact.
401    DynamicBoundary,
402}
403
404#[derive(Debug)]
405struct CompileEffectEntry {
406    source_order: u32,
407    effect: CompileEffect,
408}
409
410fn compile_effects_from_file(file: &HirFile, source_hash: Option<String>) -> Vec<CompileEffect> {
411    let mut entries = Vec::new();
412    let mut next_order = 0;
413
414    for item in &file.items {
415        push_item_effects(item, &source_hash, &mut entries, &mut next_order);
416    }
417    for fact in &file.prototype_table.facts {
418        push_compile_effect(
419            &mut entries,
420            &mut next_order,
421            CompileEffectSeed {
422                kind: CompileEffectKind::RegisterPrototype,
423                source_kind: CompileEffectSourceKind::SubDecl,
424                fact_kind: CompileEffectFactKind::Prototype,
425                fact_name: Some(fact.sub_name.clone()),
426                range: fact.range,
427                source_item: Some(fact.declaration_item),
428                scope_id: fact.scope_id,
429                package_context: fact.package_context.clone(),
430                fact_anchor_id: Some(fact.anchor_id),
431                dynamic_reason: None,
432                source_hash: source_hash.clone(),
433                provenance: fact.provenance,
434                confidence: fact.confidence,
435            },
436        );
437    }
438    for binding in &file.scope_graph.bindings {
439        push_compile_effect(
440            &mut entries,
441            &mut next_order,
442            CompileEffectSeed {
443                kind: CompileEffectKind::DeclareBinding,
444                source_kind: CompileEffectSourceKind::ScopeGraph,
445                fact_kind: CompileEffectFactKind::Binding,
446                fact_name: Some(format!("{}{}", binding.sigil, binding.name)),
447                range: binding.range,
448                source_item: binding.declaration_item,
449                scope_id: Some(binding.scope_id),
450                package_context: binding.package_context.clone(),
451                fact_anchor_id: Some(AnchorId(binding.range.start as u64)),
452                dynamic_reason: None,
453                source_hash: source_hash.clone(),
454                provenance: CompileProvenance::ExactAst,
455                confidence: CompileConfidence::High,
456            },
457        );
458    }
459    for fact in &file.compile_environment.pragma_state_facts {
460        push_compile_effect(
461            &mut entries,
462            &mut next_order,
463            CompileEffectSeed {
464                kind: CompileEffectKind::SetPragmaState,
465                source_kind: CompileEffectSourceKind::CompileEnvironment,
466                fact_kind: CompileEffectFactKind::PragmaState,
467                fact_name: Some("strict/warnings/feature".to_string()),
468                range: fact.range,
469                source_item: fact.directive_item,
470                scope_id: fact.scope_id,
471                package_context: fact.package_context.clone(),
472                fact_anchor_id: Some(fact.anchor_id),
473                dynamic_reason: None,
474                source_hash: source_hash.clone(),
475                provenance: fact.provenance,
476                confidence: fact.confidence,
477            },
478        );
479    }
480    for root in &file.compile_environment.inc_roots {
481        let kind = match root.action {
482            IncRootAction::Add => CompileEffectKind::AddIncludePath,
483            IncRootAction::Remove => CompileEffectKind::RemoveIncludePath,
484        };
485        push_compile_effect(
486            &mut entries,
487            &mut next_order,
488            CompileEffectSeed {
489                kind,
490                source_kind: match root.action {
491                    IncRootAction::Add => CompileEffectSourceKind::UseDirective,
492                    IncRootAction::Remove => CompileEffectSourceKind::NoDirective,
493                },
494                fact_kind: CompileEffectFactKind::IncludeRoot,
495                fact_name: Some(root.path.clone()),
496                range: root.range,
497                source_item: root.directive_item,
498                scope_id: root.scope_id,
499                package_context: root.package_context.clone(),
500                fact_anchor_id: Some(AnchorId(root.range.start as u64)),
501                dynamic_reason: None,
502                source_hash: source_hash.clone(),
503                provenance: root.provenance,
504                confidence: root.confidence,
505            },
506        );
507    }
508    for request in &file.compile_environment.module_requests {
509        push_compile_effect(
510            &mut entries,
511            &mut next_order,
512            CompileEffectSeed {
513                kind: CompileEffectKind::RequestModule,
514                source_kind: module_request_source_kind(request.kind),
515                fact_kind: CompileEffectFactKind::ModuleRequest,
516                fact_name: request.target.clone().or_else(|| Some("<dynamic>".to_string())),
517                range: request.range,
518                source_item: request.directive_item,
519                scope_id: request.scope_id,
520                package_context: request.package_context.clone(),
521                fact_anchor_id: Some(AnchorId(request.range.start as u64)),
522                dynamic_reason: None,
523                source_hash: source_hash.clone(),
524                provenance: request.provenance,
525                confidence: request.confidence,
526            },
527        );
528    }
529    for spec in file.compile_environment.import_specs(FileId(0)) {
530        push_compile_effect(
531            &mut entries,
532            &mut next_order,
533            CompileEffectSeed {
534                kind: CompileEffectKind::ImportSymbols,
535                source_kind: import_spec_source_kind(&spec),
536                fact_kind: CompileEffectFactKind::ImportSpec,
537                fact_name: Some(spec.module.clone()),
538                range: SourceLocation::new(
539                    spec.span_start_byte.unwrap_or_default() as usize,
540                    spec.span_start_byte.unwrap_or_default() as usize,
541                ),
542                source_item: None,
543                scope_id: spec.scope_id.map(|scope| HirScopeId::from_index(scope.0 as u32)),
544                package_context: None,
545                fact_anchor_id: spec.anchor_id,
546                dynamic_reason: None,
547                source_hash: source_hash.clone(),
548                provenance: fact_provenance_to_compile(spec.provenance),
549                confidence: fact_confidence_to_compile(spec.confidence),
550            },
551        );
552    }
553    for edge in &file.stash_graph.inheritance_edges {
554        push_compile_effect(
555            &mut entries,
556            &mut next_order,
557            CompileEffectSeed {
558                kind: CompileEffectKind::AssignInheritance,
559                source_kind: CompileEffectSourceKind::StashGraph,
560                fact_kind: CompileEffectFactKind::InheritanceEdge,
561                fact_name: Some(format!("{}->{}", edge.from_package, edge.to_package)),
562                range: edge.range,
563                source_item: edge.declaration_item,
564                scope_id: None,
565                package_context: Some(edge.from_package.clone()),
566                fact_anchor_id: Some(AnchorId(edge.range.start as u64)),
567                dynamic_reason: None,
568                source_hash: source_hash.clone(),
569                provenance: stash_provenance_to_compile(edge.provenance),
570                confidence: stash_confidence_to_compile(edge.confidence),
571            },
572        );
573    }
574    for package in &file.stash_graph.packages {
575        for slot in &package.slots {
576            push_slot_effects(package, slot, &source_hash, &mut entries, &mut next_order);
577        }
578    }
579    for boundary in &file.compile_environment.dynamic_boundaries {
580        push_compile_effect(
581            &mut entries,
582            &mut next_order,
583            CompileEffectSeed {
584                kind: CompileEffectKind::EmitDynamicBoundary,
585                source_kind: compile_boundary_source_kind(boundary.kind),
586                fact_kind: CompileEffectFactKind::DynamicBoundary,
587                fact_name: Some(format!("{:?}", boundary.kind)),
588                range: boundary.range,
589                source_item: boundary.boundary_item,
590                scope_id: boundary.scope_id,
591                package_context: boundary.package_context.clone(),
592                fact_anchor_id: Some(AnchorId(boundary.range.start as u64)),
593                dynamic_reason: Some(boundary.reason.clone()),
594                source_hash: source_hash.clone(),
595                provenance: boundary.provenance,
596                confidence: boundary.confidence,
597            },
598        );
599    }
600    for boundary in &file.stash_graph.dynamic_boundaries {
601        push_compile_effect(
602            &mut entries,
603            &mut next_order,
604            CompileEffectSeed {
605                kind: CompileEffectKind::EmitDynamicBoundary,
606                source_kind: CompileEffectSourceKind::StashGraph,
607                fact_kind: CompileEffectFactKind::DynamicBoundary,
608                fact_name: boundary.symbol.clone(),
609                range: boundary.range,
610                source_item: boundary.boundary_item,
611                scope_id: None,
612                package_context: boundary.package.clone(),
613                fact_anchor_id: Some(AnchorId(boundary.range.start as u64)),
614                dynamic_reason: Some(boundary.reason.clone()),
615                source_hash: source_hash.clone(),
616                provenance: stash_provenance_to_compile(boundary.provenance),
617                confidence: stash_confidence_to_compile(boundary.confidence),
618            },
619        );
620    }
621
622    entries.sort_by_key(|entry| (entry.effect.range.start, entry.source_order));
623    entries
624        .into_iter()
625        .enumerate()
626        .map(|(ordinal, mut entry)| {
627            entry.effect.ordinal = ordinal as u32;
628            entry.effect
629        })
630        .collect()
631}
632
633fn push_item_effects(
634    item: &HirItem,
635    source_hash: &Option<String>,
636    entries: &mut Vec<CompileEffectEntry>,
637    next_order: &mut u32,
638) {
639    match &item.kind {
640        HirKind::PackageDecl(decl) => {
641            push_compile_effect(
642                entries,
643                next_order,
644                CompileEffectSeed {
645                    kind: CompileEffectKind::DeclarePackage,
646                    source_kind: CompileEffectSourceKind::PackageDecl,
647                    fact_kind: CompileEffectFactKind::Package,
648                    fact_name: Some(decl.name.clone()),
649                    range: item.range,
650                    source_item: Some(item.id),
651                    scope_id: item.scope_context,
652                    package_context: Some(decl.name.clone()),
653                    fact_anchor_id: Some(AnchorId(item.range.start as u64)),
654                    dynamic_reason: None,
655                    source_hash: source_hash.clone(),
656                    provenance: CompileProvenance::ExactAst,
657                    confidence: CompileConfidence::High,
658                },
659            );
660        }
661        HirKind::SubDecl(decl) => {
662            let Some(name) = &decl.name else {
663                return;
664            };
665            push_compile_effect(
666                entries,
667                next_order,
668                CompileEffectSeed {
669                    kind: CompileEffectKind::DeclareSub,
670                    source_kind: CompileEffectSourceKind::SubDecl,
671                    fact_kind: CompileEffectFactKind::Sub,
672                    fact_name: Some(name.clone()),
673                    range: item.range,
674                    source_item: Some(item.id),
675                    scope_id: item.scope_context,
676                    package_context: item.package_context.clone(),
677                    fact_anchor_id: Some(AnchorId(item.range.start as u64)),
678                    dynamic_reason: None,
679                    source_hash: source_hash.clone(),
680                    provenance: CompileProvenance::ExactAst,
681                    confidence: CompileConfidence::High,
682                },
683            );
684        }
685        HirKind::MethodDecl(decl) => {
686            push_compile_effect(
687                entries,
688                next_order,
689                CompileEffectSeed {
690                    kind: CompileEffectKind::DeclareMethod,
691                    source_kind: CompileEffectSourceKind::MethodDecl,
692                    fact_kind: CompileEffectFactKind::Method,
693                    fact_name: Some(decl.name.clone()),
694                    range: item.range,
695                    source_item: Some(item.id),
696                    scope_id: item.scope_context,
697                    package_context: item.package_context.clone(),
698                    fact_anchor_id: Some(AnchorId(item.range.start as u64)),
699                    dynamic_reason: None,
700                    source_hash: source_hash.clone(),
701                    provenance: CompileProvenance::ExactAst,
702                    confidence: CompileConfidence::High,
703                },
704            );
705        }
706        _ => {}
707    }
708}
709
710fn push_slot_effects(
711    package: &PackageStash,
712    slot: &GlobSlot,
713    source_hash: &Option<String>,
714    entries: &mut Vec<CompileEffectEntry>,
715    next_order: &mut u32,
716) {
717    let (kind, fact_kind) = match slot.source {
718        GlobSlotSource::TypeglobAlias => {
719            (CompileEffectKind::AssignGlobAlias, CompileEffectFactKind::GlobSlot)
720        }
721        GlobSlotSource::ConstantDeclaration => {
722            (CompileEffectKind::DefineConstant, CompileEffectFactKind::Constant)
723        }
724        _ => return,
725    };
726
727    push_compile_effect(
728        entries,
729        next_order,
730        CompileEffectSeed {
731            kind,
732            source_kind: match slot.source {
733                GlobSlotSource::TypeglobAlias => CompileEffectSourceKind::TypeglobAssignment,
734                _ => CompileEffectSourceKind::StashGraph,
735            },
736            fact_kind,
737            fact_name: Some(format!("{}::{}", package.package, slot.name)),
738            range: slot.range,
739            source_item: slot.declaration_item,
740            scope_id: None,
741            package_context: Some(package.package.clone()),
742            fact_anchor_id: Some(AnchorId(slot.range.start as u64)),
743            dynamic_reason: None,
744            source_hash: source_hash.clone(),
745            provenance: stash_provenance_to_compile(slot.provenance),
746            confidence: stash_confidence_to_compile(slot.confidence),
747        },
748    );
749}
750
751#[derive(Debug)]
752struct CompileEffectSeed {
753    kind: CompileEffectKind,
754    source_kind: CompileEffectSourceKind,
755    fact_kind: CompileEffectFactKind,
756    fact_name: Option<String>,
757    range: SourceLocation,
758    source_item: Option<HirId>,
759    scope_id: Option<HirScopeId>,
760    package_context: Option<String>,
761    fact_anchor_id: Option<AnchorId>,
762    dynamic_reason: Option<String>,
763    source_hash: Option<String>,
764    provenance: CompileProvenance,
765    confidence: CompileConfidence,
766}
767
768fn push_compile_effect(
769    entries: &mut Vec<CompileEffectEntry>,
770    next_order: &mut u32,
771    seed: CompileEffectSeed,
772) {
773    let order = *next_order;
774    *next_order += 1;
775    entries.push(CompileEffectEntry {
776        source_order: order,
777        effect: CompileEffect {
778            ordinal: 0,
779            kind: seed.kind,
780            source_kind: seed.source_kind,
781            fact_kind: seed.fact_kind,
782            fact_name: seed.fact_name,
783            range: seed.range,
784            source_item: seed.source_item,
785            scope_id: seed.scope_id,
786            package_context: seed.package_context,
787            fact_anchor_id: seed.fact_anchor_id,
788            dynamic_reason: seed.dynamic_reason,
789            source_hash: seed.source_hash,
790            model_version: COMPILE_EFFECT_MODEL_VERSION,
791            provenance: seed.provenance,
792            confidence: seed.confidence,
793        },
794    });
795}
796
797fn module_request_source_kind(kind: ModuleRequestKind) -> CompileEffectSourceKind {
798    match kind {
799        ModuleRequestKind::Require => CompileEffectSourceKind::RequireDirective,
800        _ => CompileEffectSourceKind::UseDirective,
801    }
802}
803
804fn import_spec_source_kind(spec: &ImportSpec) -> CompileEffectSourceKind {
805    match spec.kind {
806        ImportKind::Require | ImportKind::DynamicRequire => {
807            CompileEffectSourceKind::RequireDirective
808        }
809        _ => CompileEffectSourceKind::UseDirective,
810    }
811}
812
813fn compile_boundary_source_kind(kind: CompileEnvironmentBoundaryKind) -> CompileEffectSourceKind {
814    match kind {
815        CompileEnvironmentBoundaryKind::DynamicRequire => CompileEffectSourceKind::RequireDirective,
816        CompileEnvironmentBoundaryKind::DynamicPragmaArgs
817        | CompileEnvironmentBoundaryKind::DynamicIncRoot => CompileEffectSourceKind::UseDirective,
818        CompileEnvironmentBoundaryKind::PhaseBlockExecution => CompileEffectSourceKind::PhaseBlock,
819        CompileEnvironmentBoundaryKind::SymbolicReferenceDeref => {
820            CompileEffectSourceKind::SymbolicReferenceDeref
821        }
822    }
823}
824
825fn stash_provenance_to_compile(provenance: StashProvenance) -> CompileProvenance {
826    match provenance {
827        StashProvenance::ExactAst => CompileProvenance::ExactAst,
828        StashProvenance::DesugaredAst => CompileProvenance::DesugaredAst,
829        StashProvenance::DynamicBoundary => CompileProvenance::DynamicBoundary,
830    }
831}
832
833fn stash_confidence_to_compile(confidence: StashConfidence) -> CompileConfidence {
834    match confidence {
835        StashConfidence::High => CompileConfidence::High,
836        StashConfidence::Medium => CompileConfidence::Medium,
837        StashConfidence::Low => CompileConfidence::Low,
838    }
839}
840
841fn fact_provenance_to_compile(provenance: Provenance) -> CompileProvenance {
842    match provenance {
843        // Exact AST-derived facts (fully statically known).
844        Provenance::ExactAst | Provenance::LiteralRequireImport => CompileProvenance::ExactAst,
845        Provenance::DesugaredAst
846        | Provenance::SemanticAnalyzer
847        | Provenance::FrameworkSynthesis
848        | Provenance::ImportExportInference
849        | Provenance::PragmaInference => CompileProvenance::DesugaredAst,
850        Provenance::NameHeuristic | Provenance::SearchFallback | Provenance::DynamicBoundary => {
851            CompileProvenance::DynamicBoundary
852        }
853    }
854}
855
856fn fact_confidence_to_compile(confidence: Confidence) -> CompileConfidence {
857    match confidence {
858        Confidence::High => CompileConfidence::High,
859        Confidence::Medium => CompileConfidence::Medium,
860        Confidence::Low => CompileConfidence::Low,
861    }
862}
863
864/// HIR-local scope graph for compiler-substrate proof.
865///
866/// The graph is intentionally parser-core-local. Later compiler fact export can
867/// map these ids to `perl-semantic-facts` ids without changing provider
868/// behavior in this first scope/pad slice.
869#[derive(Debug, Clone, PartialEq, Eq, Default)]
870#[non_exhaustive]
871pub struct ScopeGraph {
872    /// Scope frames in stable creation order.
873    pub scopes: Vec<ScopeFrame>,
874    /// Bindings in stable declaration order.
875    pub bindings: Vec<Binding>,
876    /// Variable references observed while lowering.
877    pub references: Vec<BindingReference>,
878}
879
880impl ScopeGraph {
881    /// Return the root file scope, when present.
882    #[inline]
883    pub fn root_scope(&self) -> Option<&ScopeFrame> {
884        self.scopes.first()
885    }
886}
887
888/// One lexical/package scope frame.
889#[derive(Debug, Clone, PartialEq, Eq)]
890#[non_exhaustive]
891pub struct ScopeFrame {
892    /// Stable scope id.
893    pub id: HirScopeId,
894    /// Parent scope id, absent for the file scope.
895    pub parent: Option<HirScopeId>,
896    /// Scope category.
897    pub kind: ScopeKind,
898    /// Source range covered by the scope.
899    pub range: SourceLocation,
900    /// Package context active for this scope, when known.
901    pub package_context: Option<String>,
902}
903
904/// Scope frame category.
905#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
906#[non_exhaustive]
907pub enum ScopeKind {
908    /// Whole-file root scope.
909    File,
910    /// Package context scope.
911    Package,
912    /// Plain block scope.
913    Block,
914    /// Subroutine pad scope.
915    Subroutine,
916    /// Method pad scope.
917    Method,
918    /// Signature parameter scope.
919    Signature,
920    /// Legacy `format` declaration scope.
921    Format,
922    /// Dynamic/string eval scope boundary.
923    EvalString,
924    /// Compile-time phase block scope, such as `BEGIN`.
925    PhaseBlock,
926}
927
928/// Compiler binding produced from a HIR declaration.
929#[derive(Debug, Clone, PartialEq, Eq)]
930#[non_exhaustive]
931pub struct Binding {
932    /// Stable binding id.
933    pub id: HirBindingId,
934    /// Scope that owns this binding.
935    pub scope_id: HirScopeId,
936    /// Variable sigil.
937    pub sigil: String,
938    /// Variable name without sigil.
939    pub name: String,
940    /// Source range of the binding declaration token.
941    pub range: SourceLocation,
942    /// Storage class represented by the declaration.
943    pub storage: StorageClass,
944    /// Package context active for this binding, when known.
945    pub package_context: Option<String>,
946    /// HIR item that declared this binding.
947    pub declaration_item: Option<HirId>,
948    /// Earlier visible binding shadowed by this declaration, when known.
949    pub shadows: Option<HirBindingId>,
950}
951
952/// Storage class represented by a binding.
953#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
954#[non_exhaustive]
955pub enum StorageClass {
956    /// Lexical `my` variable.
957    LexicalMy,
958    /// Persistent lexical `state` variable.
959    LexicalState,
960    /// `our` package variable made lexically visible.
961    PackageOur,
962    /// `local` package variable localization.
963    LocalizedPackage,
964    /// Signature parameter binding.
965    Parameter,
966    /// Method invocant binding.
967    MethodInvocant,
968    /// Implicit lexical binding such as `$_`.
969    Implicit,
970    /// Package global observed without a lexical binding.
971    PackageGlobal,
972}
973
974/// Variable reference and its lexical binding resolution.
975#[derive(Debug, Clone, PartialEq, Eq)]
976#[non_exhaustive]
977pub struct BindingReference {
978    /// Scope containing the reference.
979    pub scope_id: HirScopeId,
980    /// Variable sigil.
981    pub sigil: String,
982    /// Variable name without sigil.
983    pub name: String,
984    /// Source range for the reference token.
985    pub range: SourceLocation,
986    /// Resolved binding, if one was visible in the scope chain.
987    pub resolved_binding: Option<HirBindingId>,
988}
989
990/// HIR-local package stash graph for compiler-substrate proof.
991///
992/// This graph is intentionally parser-core-local. It records package/stash
993/// facts with provenance and confidence, but no LSP provider consumes it yet.
994#[derive(Debug, Clone, PartialEq, Eq, Default)]
995#[non_exhaustive]
996pub struct StashGraph {
997    /// Package stashes in stable first-seen order.
998    pub packages: Vec<PackageStash>,
999    /// Inheritance edges in stable source order.
1000    pub inheritance_edges: Vec<PackageInheritanceEdge>,
1001    /// Static Exporter-style declarations in stable source order.
1002    pub export_declarations: Vec<ExportDeclaration>,
1003    /// Dynamic stash boundaries in stable source order.
1004    pub dynamic_boundaries: Vec<StashDynamicBoundary>,
1005}
1006
1007impl StashGraph {
1008    /// Project static HIR/stash export declarations into canonical export facts.
1009    ///
1010    /// This is a compiler-substrate projection only. It does not execute Perl,
1011    /// inspect the filesystem, or change workspace/LSP provider behavior.
1012    #[must_use]
1013    pub fn export_sets(&self) -> Vec<ExportSet> {
1014        let mut builders = BTreeMap::<String, ExportSetBuilder>::new();
1015
1016        for declaration in &self.export_declarations {
1017            let builder = builders.entry(declaration.package.clone()).or_insert_with(|| {
1018                ExportSetBuilder::new(
1019                    declaration.package.clone(),
1020                    declaration.range,
1021                    stash_provenance_to_fact(declaration.provenance),
1022                    stash_confidence_to_fact(declaration.confidence),
1023                )
1024            });
1025            builder.absorb(declaration);
1026        }
1027
1028        builders.into_values().map(ExportSetBuilder::into_export_set).collect()
1029    }
1030
1031    /// Project static constant-like code slots into a compile-time constant table.
1032    ///
1033    /// The table is a compiler-substrate receipt only. It records facts already
1034    /// present in the stash graph and does not execute Perl, evaluate constant
1035    /// values, or change provider behavior.
1036    #[must_use]
1037    pub fn constant_table(&self) -> ConstantTable {
1038        let mut entries = Vec::new();
1039
1040        for package in &self.packages {
1041            for slot in &package.slots {
1042                if slot.kind != GlobSlotKind::Code
1043                    || slot.source != GlobSlotSource::ConstantDeclaration
1044                {
1045                    continue;
1046                }
1047
1048                entries.push(ConstantTableEntry {
1049                    package: package.package.clone(),
1050                    name: slot.name.clone(),
1051                    canonical_name: format!("{}::{}", package.package, slot.name),
1052                    range: slot.range,
1053                    declaration_item: slot.declaration_item,
1054                    source: slot.source,
1055                    provenance: slot.provenance,
1056                    confidence: slot.confidence,
1057                });
1058            }
1059        }
1060
1061        ConstantTable { entries }
1062    }
1063}
1064
1065/// Compile-time constant projection derived from the HIR stash graph.
1066#[derive(Debug, Clone, PartialEq, Eq, Default)]
1067#[non_exhaustive]
1068pub struct ConstantTable {
1069    /// Static constant-like code slots in stable source order.
1070    pub entries: Vec<ConstantTableEntry>,
1071}
1072
1073impl ConstantTable {
1074    /// Returns true when no static constants were recorded.
1075    #[must_use]
1076    pub fn is_empty(&self) -> bool {
1077        self.entries.is_empty()
1078    }
1079}
1080
1081/// One static constant-like code slot.
1082#[derive(Debug, Clone, PartialEq, Eq)]
1083#[non_exhaustive]
1084pub struct ConstantTableEntry {
1085    /// Package that owns the constant.
1086    pub package: String,
1087    /// Bare constant name.
1088    pub name: String,
1089    /// Fully qualified constant name.
1090    pub canonical_name: String,
1091    /// Source range for the declaration or desugaring that produced the slot.
1092    pub range: SourceLocation,
1093    /// HIR item that produced the slot, when available.
1094    pub declaration_item: Option<HirId>,
1095    /// Source shape that produced this constant-like code slot.
1096    pub source: GlobSlotSource,
1097    /// How this constant fact was produced.
1098    pub provenance: StashProvenance,
1099    /// Confidence in this constant fact.
1100    pub confidence: StashConfidence,
1101}
1102
1103#[derive(Debug)]
1104struct ExportSetBuilder {
1105    module_name: String,
1106    anchor_range: SourceLocation,
1107    default_exports: Vec<String>,
1108    optional_exports: Vec<String>,
1109    tags: BTreeMap<String, Vec<String>>,
1110    provenance: Provenance,
1111    confidence: Confidence,
1112}
1113
1114impl ExportSetBuilder {
1115    fn new(
1116        module_name: String,
1117        anchor_range: SourceLocation,
1118        provenance: Provenance,
1119        confidence: Confidence,
1120    ) -> Self {
1121        Self {
1122            module_name,
1123            anchor_range,
1124            default_exports: Vec::new(),
1125            optional_exports: Vec::new(),
1126            tags: BTreeMap::new(),
1127            provenance,
1128            confidence,
1129        }
1130    }
1131
1132    fn absorb(&mut self, declaration: &ExportDeclaration) {
1133        if declaration.range.start < self.anchor_range.start {
1134            self.anchor_range = declaration.range;
1135        }
1136        self.provenance =
1137            combine_provenance(self.provenance, stash_provenance_to_fact(declaration.provenance));
1138        self.confidence =
1139            combine_confidence(self.confidence, stash_confidence_to_fact(declaration.confidence));
1140
1141        match declaration.kind {
1142            ExportDeclarationKind::Default => {
1143                self.default_exports.extend(declaration.symbols.iter().cloned());
1144            }
1145            ExportDeclarationKind::Optional => {
1146                self.optional_exports.extend(declaration.symbols.iter().cloned());
1147            }
1148            ExportDeclarationKind::Tag => {
1149                if let Some(tag_name) = &declaration.tag_name {
1150                    self.tags
1151                        .entry(tag_name.clone())
1152                        .or_default()
1153                        .extend(declaration.symbols.iter().cloned());
1154                }
1155            }
1156        }
1157    }
1158
1159    fn into_export_set(mut self) -> ExportSet {
1160        sort_dedup(&mut self.default_exports);
1161        sort_dedup(&mut self.optional_exports);
1162
1163        let tags = self
1164            .tags
1165            .into_iter()
1166            .map(|(name, mut members)| {
1167                sort_dedup(&mut members);
1168                ExportTag { name, members }
1169            })
1170            .collect();
1171
1172        ExportSet {
1173            default_exports: self.default_exports,
1174            optional_exports: self.optional_exports,
1175            tags,
1176            provenance: self.provenance,
1177            confidence: self.confidence,
1178            module_name: Some(self.module_name),
1179            anchor_id: Some(AnchorId(self.anchor_range.start as u64)),
1180        }
1181    }
1182}
1183
1184fn sort_dedup(values: &mut Vec<String>) {
1185    values.sort();
1186    values.dedup();
1187}
1188
1189fn combine_provenance(current: Provenance, next: Provenance) -> Provenance {
1190    if current == Provenance::DynamicBoundary || next == Provenance::DynamicBoundary {
1191        Provenance::DynamicBoundary
1192    } else if current == Provenance::ImportExportInference
1193        || next == Provenance::ImportExportInference
1194    {
1195        Provenance::ImportExportInference
1196    } else {
1197        current
1198    }
1199}
1200
1201fn combine_confidence(current: Confidence, next: Confidence) -> Confidence {
1202    match (current, next) {
1203        (Confidence::Low, _) | (_, Confidence::Low) => Confidence::Low,
1204        (Confidence::Medium, _) | (_, Confidence::Medium) => Confidence::Medium,
1205        (Confidence::High, Confidence::High) => Confidence::High,
1206    }
1207}
1208
1209fn stash_provenance_to_fact(provenance: StashProvenance) -> Provenance {
1210    match provenance {
1211        StashProvenance::ExactAst => Provenance::ExactAst,
1212        StashProvenance::DesugaredAst => Provenance::DesugaredAst,
1213        StashProvenance::DynamicBoundary => Provenance::DynamicBoundary,
1214    }
1215}
1216
1217fn stash_confidence_to_fact(confidence: StashConfidence) -> Confidence {
1218    match confidence {
1219        StashConfidence::High => Confidence::High,
1220        StashConfidence::Medium => Confidence::Medium,
1221        StashConfidence::Low => Confidence::Low,
1222    }
1223}
1224
1225/// One Perl package stash.
1226#[derive(Debug, Clone, PartialEq, Eq)]
1227#[non_exhaustive]
1228pub struct PackageStash {
1229    /// Package name.
1230    pub package: String,
1231    /// Source range that first established this package.
1232    pub range: SourceLocation,
1233    /// HIR item that first established this package, when available.
1234    pub declaration_item: Option<HirId>,
1235    /// Symbol slots observed for this package.
1236    pub slots: Vec<GlobSlot>,
1237    /// How this stash fact was produced.
1238    pub provenance: StashProvenance,
1239    /// Confidence in this stash fact.
1240    pub confidence: StashConfidence,
1241}
1242
1243/// One slot inside a Perl typeglob.
1244#[derive(Debug, Clone, PartialEq, Eq)]
1245#[non_exhaustive]
1246pub struct GlobSlot {
1247    /// Symbol name without sigil.
1248    pub name: String,
1249    /// Slot category.
1250    pub kind: GlobSlotKind,
1251    /// Source range for the declaration or mutation that produced this slot.
1252    pub range: SourceLocation,
1253    /// HIR item that produced this slot, when available.
1254    pub declaration_item: Option<HirId>,
1255    /// Source shape that produced this slot.
1256    pub source: GlobSlotSource,
1257    /// Static alias target, when this slot is an alias.
1258    pub alias_target: Option<String>,
1259    /// How this slot fact was produced.
1260    pub provenance: StashProvenance,
1261    /// Confidence in this slot fact.
1262    pub confidence: StashConfidence,
1263}
1264
1265/// Perl typeglob slot category.
1266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1267#[non_exhaustive]
1268pub enum GlobSlotKind {
1269    /// Scalar slot: `$Package::name`.
1270    Scalar,
1271    /// Array slot: `@Package::name`.
1272    Array,
1273    /// Hash slot: `%Package::name`.
1274    Hash,
1275    /// Code slot: `Package::name()`.
1276    Code,
1277    /// IO slot / filehandle slot.
1278    Io,
1279    /// Format slot.
1280    Format,
1281}
1282
1283/// Source shape that populated a glob slot.
1284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1285#[non_exhaustive]
1286pub enum GlobSlotSource {
1287    /// `package` declaration.
1288    PackageDeclaration,
1289    /// `sub` declaration.
1290    SubDeclaration,
1291    /// `method` declaration.
1292    MethodDeclaration,
1293    /// `our` declaration.
1294    OurDeclaration,
1295    /// Legacy `format` declaration.
1296    FormatDeclaration,
1297    /// `use constant` compile-time declaration.
1298    ConstantDeclaration,
1299    /// Package variable assignment such as `@ISA = ...`.
1300    PackageAssignment,
1301    /// Static typeglob alias assignment.
1302    TypeglobAlias,
1303}
1304
1305/// Provenance for HIR-local stash facts.
1306#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1307#[non_exhaustive]
1308pub enum StashProvenance {
1309    /// Fact came directly from parser AST syntax.
1310    ExactAst,
1311    /// Fact came from a simple compile-time desugaring such as `use parent`.
1312    DesugaredAst,
1313    /// Fact came from conservative dynamic-boundary classification.
1314    DynamicBoundary,
1315}
1316
1317/// Confidence for HIR-local stash facts.
1318#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1319#[non_exhaustive]
1320pub enum StashConfidence {
1321    /// High-confidence exact or simple desugared fact.
1322    High,
1323    /// Medium-confidence static interpretation.
1324    Medium,
1325    /// Low-confidence dynamic-boundary fact.
1326    Low,
1327}
1328
1329/// Inheritance edge established by `@ISA`, `use parent`, or `use base`.
1330#[derive(Debug, Clone, PartialEq, Eq)]
1331#[non_exhaustive]
1332pub struct PackageInheritanceEdge {
1333    /// Package inheriting from the target.
1334    pub from_package: String,
1335    /// Parent package.
1336    pub to_package: String,
1337    /// Source range for the edge.
1338    pub range: SourceLocation,
1339    /// HIR item that produced this edge, when available.
1340    pub declaration_item: Option<HirId>,
1341    /// Source shape that produced this edge.
1342    pub source: InheritanceSource,
1343    /// How this edge fact was produced.
1344    pub provenance: StashProvenance,
1345    /// Confidence in this edge fact.
1346    pub confidence: StashConfidence,
1347}
1348
1349/// Source shape that established an inheritance edge.
1350#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1351#[non_exhaustive]
1352pub enum InheritanceSource {
1353    /// `our @ISA = ...`.
1354    IsaAssignment,
1355    /// `use parent ...`.
1356    UseParent,
1357    /// `use base ...`.
1358    UseBase,
1359}
1360
1361/// Static Exporter-style declaration observed in a package stash.
1362#[derive(Debug, Clone, PartialEq, Eq)]
1363#[non_exhaustive]
1364pub struct ExportDeclaration {
1365    /// Package declaring the export list.
1366    pub package: String,
1367    /// Export declaration category.
1368    pub kind: ExportDeclarationKind,
1369    /// Tag name for `%EXPORT_TAGS` entries.
1370    pub tag_name: Option<String>,
1371    /// Static exported symbols.
1372    pub symbols: Vec<String>,
1373    /// Source range for the declaration.
1374    pub range: SourceLocation,
1375    /// HIR item that produced this declaration, when available.
1376    pub declaration_item: Option<HirId>,
1377    /// How this export declaration was produced.
1378    pub provenance: StashProvenance,
1379    /// Confidence in this export declaration.
1380    pub confidence: StashConfidence,
1381}
1382
1383/// Export declaration category.
1384#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1385#[non_exhaustive]
1386pub enum ExportDeclarationKind {
1387    /// `@EXPORT`.
1388    Default,
1389    /// `@EXPORT_OK`.
1390    Optional,
1391    /// `%EXPORT_TAGS`.
1392    Tag,
1393}
1394
1395/// Dynamic stash mutation boundary.
1396#[derive(Debug, Clone, PartialEq, Eq)]
1397#[non_exhaustive]
1398pub struct StashDynamicBoundary {
1399    /// Package affected by the boundary, when known.
1400    pub package: Option<String>,
1401    /// Symbol affected by the boundary, when statically known.
1402    pub symbol: Option<String>,
1403    /// Source range for the boundary.
1404    pub range: SourceLocation,
1405    /// HIR item that also records this boundary, when available.
1406    pub boundary_item: Option<HirId>,
1407    /// Boundary category.
1408    pub kind: StashDynamicBoundaryKind,
1409    /// Short reason for status/proof output.
1410    pub reason: String,
1411    /// How this boundary fact was produced.
1412    pub provenance: StashProvenance,
1413    /// Confidence in this boundary fact.
1414    pub confidence: StashConfidence,
1415}
1416
1417/// Dynamic stash boundary category.
1418#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1419#[non_exhaustive]
1420pub enum StashDynamicBoundaryKind {
1421    /// Stash/typeglob assignment with a non-static RHS.
1422    DynamicStashMutation,
1423    /// Export declaration has a non-static member list or tag shape.
1424    DynamicExportDeclaration,
1425    /// `AUTOLOAD` makes method lookup dynamic for this package.
1426    Autoload,
1427}
1428
1429/// HIR-local compile environment for compiler-substrate proof.
1430///
1431/// This model records compile-time directives, pragma state changes, include
1432/// roots, module requests, phase blocks, and dynamic boundaries without
1433/// changing LSP provider behavior.
1434#[derive(Debug, Clone, PartialEq, Eq, Default)]
1435#[non_exhaustive]
1436pub struct CompileEnvironment {
1437    /// `use`, `no`, and `require` directives in stable source order.
1438    pub directives: Vec<CompileDirective>,
1439    /// Pragma or feature effects in stable source order.
1440    pub pragma_effects: Vec<PragmaEffect>,
1441    /// Effective strict/warnings/feature state facts in source order.
1442    pub pragma_state_facts: Vec<PragmaStateFact>,
1443    /// Include-root effects such as `use lib` and `no lib`.
1444    pub inc_roots: Vec<IncRootFact>,
1445    /// Static and dynamic module requests observed in the file.
1446    pub module_requests: Vec<ModuleRequest>,
1447    /// Compile-time phase blocks observed in source order.
1448    pub phase_blocks: Vec<CompilePhaseBlock>,
1449    /// Unsupported or dynamic compile-environment boundaries.
1450    pub dynamic_boundaries: Vec<CompileEnvironmentBoundary>,
1451}
1452
1453impl CompileEnvironment {
1454    /// Effective pragma state facts in source order.
1455    #[must_use]
1456    pub fn pragma_state_facts(&self) -> &[PragmaStateFact] {
1457        &self.pragma_state_facts
1458    }
1459
1460    /// Return the latest effective pragma state fact at or before `offset`.
1461    #[must_use]
1462    pub fn pragma_state_at(&self, offset: usize) -> Option<&PragmaStateFact> {
1463        let idx = self.pragma_state_facts.partition_point(|fact| fact.range.start <= offset);
1464        if idx > 0 { self.pragma_state_facts.get(idx - 1) } else { None }
1465    }
1466
1467    /// Project HIR compile directives into canonical import facts.
1468    ///
1469    /// This is a compiler-substrate projection only. It does not inspect the
1470    /// filesystem, execute Perl, or change workspace/LSP provider behavior.
1471    #[must_use]
1472    pub fn import_specs(&self, file_id: FileId) -> Vec<ImportSpec> {
1473        self.directives
1474            .iter()
1475            .filter_map(|directive| import_spec_from_directive(directive, file_id))
1476            .collect()
1477    }
1478
1479    /// Build module-resolution candidate facts from static module requests.
1480    ///
1481    /// The HIR layer records lexical include-root effects and module requests,
1482    /// but it does not read process environment, inspect the filesystem, or
1483    /// depend on the downstream `perl-module` resolver. Callers provide
1484    /// configured, `PERL5LIB`, and system roots explicitly; this method combines
1485    /// them with source-order lexical `use lib` roots active at each request.
1486    #[must_use]
1487    pub fn module_resolution_candidates(
1488        &self,
1489        supplied_roots: &[ModuleResolutionRoot],
1490    ) -> Vec<ModuleResolutionCandidate> {
1491        self.module_requests
1492            .iter()
1493            .enumerate()
1494            .filter_map(|(request_index, request)| {
1495                let target = request.target.as_ref()?;
1496                let normalized_target = normalize_module_target(target);
1497                let relative_path = module_target_to_relative_path(&normalized_target)?;
1498                let candidate_roots =
1499                    self.candidate_roots_for_request(request, &relative_path, supplied_roots);
1500                let status = if candidate_roots.is_empty() {
1501                    ModuleResolutionCandidateStatus::NotFound
1502                } else {
1503                    ModuleResolutionCandidateStatus::CandidateBuilt
1504                };
1505
1506                Some(ModuleResolutionCandidate {
1507                    request_index,
1508                    directive_item: request.directive_item,
1509                    request_kind: request.kind,
1510                    target: normalized_target,
1511                    relative_path,
1512                    roots: candidate_roots,
1513                    status,
1514                    resolved_path: None,
1515                    range: request.range,
1516                    package_context: request.package_context.clone(),
1517                    provenance: request.provenance,
1518                    confidence: request.confidence,
1519                })
1520            })
1521            .collect()
1522    }
1523
1524    /// Resolve static module candidate facts using a caller-supplied path predicate.
1525    ///
1526    /// This preserves the HIR layer's explicit boundary: the caller supplies
1527    /// roots and the existence check, so parser-core still does not read
1528    /// ambient process state or spawn Perl.
1529    #[must_use]
1530    pub fn resolved_module_resolution_candidates(
1531        &self,
1532        supplied_roots: &[ModuleResolutionRoot],
1533        mut path_exists: impl FnMut(&str) -> bool,
1534    ) -> Vec<ModuleResolutionCandidate> {
1535        let mut candidates = self.module_resolution_candidates(supplied_roots);
1536
1537        for candidate in &mut candidates {
1538            if candidate.status != ModuleResolutionCandidateStatus::CandidateBuilt {
1539                continue;
1540            }
1541
1542            if let Some(root) =
1543                candidate.roots.iter().find(|root| path_exists(&root.candidate_path))
1544            {
1545                candidate.status = ModuleResolutionCandidateStatus::Resolved;
1546                candidate.resolved_path = Some(root.candidate_path.clone());
1547            } else {
1548                candidate.status = ModuleResolutionCandidateStatus::NotFound;
1549            }
1550        }
1551
1552        candidates
1553    }
1554
1555    fn candidate_roots_for_request(
1556        &self,
1557        request: &ModuleRequest,
1558        relative_path: &str,
1559        supplied_roots: &[ModuleResolutionRoot],
1560    ) -> Vec<ModuleResolutionCandidateRoot> {
1561        let active_lexical_roots = self.active_lexical_roots_for_request(request);
1562        active_lexical_roots
1563            .iter()
1564            .map(|root| ModuleResolutionRoot {
1565                path: root.path.clone(),
1566                kind: root.kind,
1567                source: root.source.clone(),
1568            })
1569            .chain(supplied_roots.iter().cloned())
1570            .enumerate()
1571            .map(|(precedence, root)| ModuleResolutionCandidateRoot {
1572                path: root.path.clone(),
1573                kind: root.kind,
1574                source: root.source,
1575                candidate_path: join_candidate_path(&root.path, relative_path),
1576                precedence,
1577            })
1578            .collect()
1579    }
1580
1581    fn active_lexical_roots_for_request(&self, request: &ModuleRequest) -> Vec<ActiveLexicalRoot> {
1582        let mut active = Vec::new();
1583
1584        for (order, root) in self.inc_roots.iter().enumerate() {
1585            if root.range.start > request.range.start {
1586                continue;
1587            }
1588            if root.kind != IncRootKind::UseLib {
1589                continue;
1590            }
1591
1592            match root.action {
1593                IncRootAction::Add => {
1594                    active.push(ActiveLexicalRoot {
1595                        path: root.path.clone(),
1596                        kind: root.kind,
1597                        source: "use-lib-lexical".to_string(),
1598                        range_start: root.range.start,
1599                        order,
1600                    });
1601                }
1602                IncRootAction::Remove => {
1603                    active.retain(|active_root| active_root.path != root.path);
1604                }
1605            }
1606        }
1607
1608        active.sort_by(|left, right| {
1609            right.range_start.cmp(&left.range_start).then_with(|| left.order.cmp(&right.order))
1610        });
1611
1612        active
1613    }
1614}
1615
1616#[derive(Debug, Clone, PartialEq, Eq)]
1617struct ActiveLexicalRoot {
1618    path: String,
1619    kind: IncRootKind,
1620    source: String,
1621    range_start: usize,
1622    order: usize,
1623}
1624
1625/// One compile-time directive.
1626#[derive(Debug, Clone, PartialEq, Eq)]
1627#[non_exhaustive]
1628pub struct CompileDirective {
1629    /// Directive action.
1630    pub action: CompileDirectiveAction,
1631    /// Module or pragma name.
1632    pub module: Option<String>,
1633    /// Static arguments captured by the parser.
1634    pub args: Vec<String>,
1635    /// Source range for the directive.
1636    pub range: SourceLocation,
1637    /// HIR item attached to this directive, when one exists.
1638    pub item_id: Option<HirId>,
1639    /// Scope containing the directive.
1640    pub scope_id: Option<HirScopeId>,
1641    /// Package context active at the directive.
1642    pub package_context: Option<String>,
1643    /// Directive classification.
1644    pub kind: CompileDirectiveKind,
1645    /// How this fact was produced.
1646    pub provenance: CompileProvenance,
1647    /// Confidence in this fact.
1648    pub confidence: CompileConfidence,
1649}
1650
1651/// Compile-time directive action.
1652#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1653#[non_exhaustive]
1654pub enum CompileDirectiveAction {
1655    /// `use Module ...`.
1656    Use,
1657    /// `no Module ...`.
1658    No,
1659    /// `require Module`.
1660    Require,
1661}
1662
1663/// Compile-time directive classification.
1664#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1665#[non_exhaustive]
1666pub enum CompileDirectiveKind {
1667    /// `strict` pragma.
1668    Strict,
1669    /// `warnings` pragma.
1670    Warnings,
1671    /// `feature` pragma.
1672    Feature,
1673    /// `lib` include-path pragma.
1674    Lib,
1675    /// Inheritance helper such as `parent` or `base`.
1676    Inheritance,
1677    /// Constant declaration helper.
1678    Constant,
1679    /// Ordinary module load/import directive.
1680    Module,
1681    /// Dynamic or unsupported directive shape.
1682    Dynamic,
1683}
1684
1685/// Pragma or feature state change.
1686#[derive(Debug, Clone, PartialEq, Eq)]
1687#[non_exhaustive]
1688pub struct PragmaEffect {
1689    /// Pragma name.
1690    pub pragma: String,
1691    /// Whether the pragma is being enabled (`use`) or disabled (`no`).
1692    pub enabled: bool,
1693    /// Static, normalized categories or feature names captured by the parser.
1694    pub args: Vec<String>,
1695    /// Whether this effect applies broadly or to listed categories/features.
1696    pub argument_kind: PragmaArgumentKind,
1697    /// Source range for the effect.
1698    pub range: SourceLocation,
1699    /// Directive that produced this effect.
1700    pub directive_item: Option<HirId>,
1701    /// Scope containing the effect.
1702    pub scope_id: Option<HirScopeId>,
1703    /// Package context active at the effect.
1704    pub package_context: Option<String>,
1705    /// How this fact was produced.
1706    pub provenance: CompileProvenance,
1707    /// Confidence in this fact.
1708    pub confidence: CompileConfidence,
1709}
1710
1711/// Static pragma argument shape.
1712#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1713#[non_exhaustive]
1714pub enum PragmaArgumentKind {
1715    /// No arguments were supplied, so the pragma transition applies broadly.
1716    Broad,
1717    /// Static category, warning, or feature names were supplied.
1718    Categories,
1719}
1720
1721/// Effective strict/warnings/feature state after a compile-time transition.
1722#[derive(Debug, Clone, PartialEq, Eq)]
1723#[non_exhaustive]
1724pub struct PragmaStateFact {
1725    /// Source range for the transition that produced this state.
1726    pub range: SourceLocation,
1727    /// Stable source anchor for this transition.
1728    pub anchor_id: AnchorId,
1729    /// Directive that produced this state, when HIR has one.
1730    pub directive_item: Option<HirId>,
1731    /// Scope containing this transition.
1732    pub scope_id: Option<HirScopeId>,
1733    /// Package context active at this transition.
1734    pub package_context: Option<String>,
1735    /// Effective `strict vars` state.
1736    pub strict_vars: bool,
1737    /// Effective `strict subs` state.
1738    pub strict_subs: bool,
1739    /// Effective `strict refs` state.
1740    pub strict_refs: bool,
1741    /// Effective global warnings state.
1742    pub warnings: bool,
1743    /// Warning categories explicitly disabled in this state.
1744    pub disabled_warning_categories: Vec<String>,
1745    /// Effective feature names in this state.
1746    pub features: Vec<String>,
1747    /// How this fact was produced.
1748    pub provenance: CompileProvenance,
1749    /// Confidence in this fact.
1750    pub confidence: CompileConfidence,
1751}
1752
1753impl PragmaStateFact {
1754    /// Whether all strict categories are active in this state.
1755    #[must_use]
1756    pub fn strict_enabled(&self) -> bool {
1757        self.strict_vars && self.strict_subs && self.strict_refs
1758    }
1759
1760    /// Whether warnings are active for a category in this state.
1761    #[must_use]
1762    pub fn warning_active(&self, category: &str) -> bool {
1763        self.warnings && !self.disabled_warning_categories.iter().any(|name| name == category)
1764    }
1765
1766    /// Whether a feature is active in this state.
1767    #[must_use]
1768    pub fn has_feature(&self, feature: &str) -> bool {
1769        self.features.iter().any(|name| name == feature)
1770    }
1771}
1772
1773/// Registry for compiler-substrate framework adapters.
1774///
1775/// Adapters consume HIR/stash/import compiler facts and emit more compiler
1776/// facts. They must not directly special-case diagnostics, completion, hover,
1777/// or navigation.
1778#[derive(Debug, Clone, PartialEq, Eq)]
1779#[non_exhaustive]
1780pub struct FrameworkAdapterRegistry {
1781    adapters: Vec<FrameworkAdapterKind>,
1782}
1783
1784impl Default for FrameworkAdapterRegistry {
1785    fn default() -> Self {
1786        Self { adapters: vec![FrameworkAdapterKind::ExporterFamily] }
1787    }
1788}
1789
1790impl FrameworkAdapterRegistry {
1791    /// Create a registry with an explicit adapter set.
1792    #[must_use]
1793    pub fn new(adapters: Vec<FrameworkAdapterKind>) -> Self {
1794        Self { adapters }
1795    }
1796
1797    /// Return the adapter kinds enabled in this registry.
1798    #[must_use]
1799    pub fn adapters(&self) -> &[FrameworkAdapterKind] {
1800        &self.adapters
1801    }
1802
1803    /// Project framework compiler facts from a lowered HIR file.
1804    #[must_use]
1805    pub fn project_file(&self, file: &HirFile) -> FrameworkFactGraph {
1806        let mut graph = FrameworkFactGraph::default();
1807
1808        for adapter in &self.adapters {
1809            match adapter {
1810                FrameworkAdapterKind::ExporterFamily => {
1811                    project_exporter_family_facts(file, &mut graph);
1812                }
1813            }
1814        }
1815
1816        graph
1817    }
1818}
1819
1820/// Framework adapter kind.
1821#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1822#[non_exhaustive]
1823pub enum FrameworkAdapterKind {
1824    /// Exporter and Exporter::Tiny-style export declarations.
1825    ExporterFamily,
1826}
1827
1828/// Facts emitted by framework adapters.
1829#[derive(Debug, Clone, PartialEq, Eq, Default)]
1830#[non_exhaustive]
1831pub struct FrameworkFactGraph {
1832    /// Framework-exported symbol facts in stable source order.
1833    pub exported_symbols: Vec<FrameworkExportedSymbolFact>,
1834    /// Dynamic or unsupported framework boundaries in stable source order.
1835    pub dynamic_boundaries: Vec<FrameworkDynamicBoundaryFact>,
1836}
1837
1838/// One framework-exported symbol fact.
1839#[derive(Debug, Clone, PartialEq, Eq)]
1840#[non_exhaustive]
1841pub struct FrameworkExportedSymbolFact {
1842    /// Adapter that emitted this fact.
1843    pub adapter: FrameworkAdapterKind,
1844    /// Package declaring the export.
1845    pub package: String,
1846    /// Exported symbol name.
1847    pub name: String,
1848    /// Export relationship represented by this fact.
1849    pub kind: FrameworkExportedSymbolKind,
1850    /// Tag name when this is a `%EXPORT_TAGS` member.
1851    pub tag_name: Option<String>,
1852    /// Source range for the export declaration.
1853    pub range: SourceLocation,
1854    /// HIR item that produced the export declaration, when available.
1855    pub declaration_item: Option<HirId>,
1856    /// Backing visible-symbol fact for provider-shadow proof.
1857    pub visible_symbol: VisibleSymbol,
1858    /// Source declaration provenance.
1859    pub source_provenance: Provenance,
1860    /// Source declaration confidence.
1861    pub source_confidence: Confidence,
1862    /// Adapter fact provenance.
1863    pub provenance: Provenance,
1864    /// Adapter fact confidence.
1865    pub confidence: Confidence,
1866}
1867
1868/// Export relationship represented by a framework fact.
1869#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1870#[non_exhaustive]
1871pub enum FrameworkExportedSymbolKind {
1872    /// Symbol exported by default via `@EXPORT`.
1873    Default,
1874    /// Symbol available for explicit import via `@EXPORT_OK`.
1875    Optional,
1876    /// Symbol included in a `%EXPORT_TAGS` tag.
1877    TagMember,
1878}
1879
1880/// Dynamic or unsupported framework-adapter boundary.
1881#[derive(Debug, Clone, PartialEq, Eq)]
1882#[non_exhaustive]
1883pub struct FrameworkDynamicBoundaryFact {
1884    /// Adapter that emitted this boundary.
1885    pub adapter: FrameworkAdapterKind,
1886    /// Package affected by the boundary, when known.
1887    pub package: Option<String>,
1888    /// Symbol affected by the boundary, when statically known.
1889    pub symbol: Option<String>,
1890    /// Source range for the boundary.
1891    pub range: SourceLocation,
1892    /// HIR item that also records this boundary, when available.
1893    pub boundary_item: Option<HirId>,
1894    /// Boundary category.
1895    pub kind: StashDynamicBoundaryKind,
1896    /// Short reason for status/proof output.
1897    pub reason: String,
1898    /// Adapter fact provenance.
1899    pub provenance: Provenance,
1900    /// Adapter fact confidence.
1901    pub confidence: Confidence,
1902}
1903
1904fn project_exporter_family_facts(file: &HirFile, graph: &mut FrameworkFactGraph) {
1905    for declaration in &file.stash_graph.export_declarations {
1906        match declaration.kind {
1907            ExportDeclarationKind::Default => {
1908                project_exported_symbols_from_declaration(
1909                    declaration,
1910                    FrameworkExportedSymbolKind::Default,
1911                    None,
1912                    graph,
1913                );
1914            }
1915            ExportDeclarationKind::Optional => {
1916                project_exported_symbols_from_declaration(
1917                    declaration,
1918                    FrameworkExportedSymbolKind::Optional,
1919                    None,
1920                    graph,
1921                );
1922            }
1923            ExportDeclarationKind::Tag => {
1924                project_exported_symbols_from_declaration(
1925                    declaration,
1926                    FrameworkExportedSymbolKind::TagMember,
1927                    declaration.tag_name.as_deref(),
1928                    graph,
1929                );
1930            }
1931        }
1932    }
1933
1934    for boundary in &file.stash_graph.dynamic_boundaries {
1935        if boundary.kind != StashDynamicBoundaryKind::DynamicExportDeclaration {
1936            continue;
1937        }
1938        graph.dynamic_boundaries.push(FrameworkDynamicBoundaryFact {
1939            adapter: FrameworkAdapterKind::ExporterFamily,
1940            package: boundary.package.clone(),
1941            symbol: boundary.symbol.clone(),
1942            range: boundary.range,
1943            boundary_item: boundary.boundary_item,
1944            kind: boundary.kind,
1945            reason: boundary.reason.clone(),
1946            provenance: Provenance::DynamicBoundary,
1947            confidence: Confidence::Low,
1948        });
1949    }
1950}
1951
1952fn project_exported_symbols_from_declaration(
1953    declaration: &ExportDeclaration,
1954    kind: FrameworkExportedSymbolKind,
1955    tag_name: Option<&str>,
1956    graph: &mut FrameworkFactGraph,
1957) {
1958    for symbol in &declaration.symbols {
1959        graph.exported_symbols.push(FrameworkExportedSymbolFact {
1960            adapter: FrameworkAdapterKind::ExporterFamily,
1961            package: declaration.package.clone(),
1962            name: symbol.clone(),
1963            kind,
1964            tag_name: tag_name.map(str::to_string),
1965            range: declaration.range,
1966            declaration_item: declaration.declaration_item,
1967            visible_symbol: visible_symbol_for_export(declaration, symbol, kind),
1968            source_provenance: stash_provenance_to_fact(declaration.provenance),
1969            source_confidence: stash_confidence_to_fact(declaration.confidence),
1970            provenance: Provenance::FrameworkSynthesis,
1971            confidence: Confidence::Medium,
1972        });
1973    }
1974}
1975
1976fn visible_symbol_for_export(
1977    declaration: &ExportDeclaration,
1978    symbol: &str,
1979    kind: FrameworkExportedSymbolKind,
1980) -> VisibleSymbol {
1981    let source = match kind {
1982        FrameworkExportedSymbolKind::Default => VisibleSymbolSource::DefaultExport,
1983        FrameworkExportedSymbolKind::Optional => VisibleSymbolSource::ExplicitImport,
1984        FrameworkExportedSymbolKind::TagMember => VisibleSymbolSource::ExportTag,
1985    };
1986
1987    VisibleSymbol {
1988        name: symbol.to_string(),
1989        entity_id: None,
1990        source,
1991        confidence: Confidence::Medium,
1992        context: Some(VisibleSymbolContext::new(
1993            Some(declaration.package.clone()),
1994            None,
1995            Some(AnchorId(declaration.range.start as u64)),
1996        )),
1997    }
1998}
1999
2000/// Include-root effect.
2001#[derive(Debug, Clone, PartialEq, Eq)]
2002#[non_exhaustive]
2003pub struct IncRootFact {
2004    /// Include root path as written after static cleanup.
2005    pub path: String,
2006    /// Whether the root is added or removed.
2007    pub action: IncRootAction,
2008    /// Source of the include root.
2009    pub kind: IncRootKind,
2010    /// Source range for the effect.
2011    pub range: SourceLocation,
2012    /// Directive that produced this effect.
2013    pub directive_item: Option<HirId>,
2014    /// Scope containing the effect.
2015    pub scope_id: Option<HirScopeId>,
2016    /// Package context active at the effect.
2017    pub package_context: Option<String>,
2018    /// How this fact was produced.
2019    pub provenance: CompileProvenance,
2020    /// Confidence in this fact.
2021    pub confidence: CompileConfidence,
2022}
2023
2024/// Include-root action.
2025#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2026#[non_exhaustive]
2027pub enum IncRootAction {
2028    /// Add an include root.
2029    Add,
2030    /// Remove an include root.
2031    Remove,
2032}
2033
2034/// Include-root source.
2035#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2036#[non_exhaustive]
2037pub enum IncRootKind {
2038    /// Root came from `use lib` / `no lib`.
2039    UseLib,
2040    /// Root came from configured include paths.
2041    Configured,
2042    /// Root came from `PERL5LIB`.
2043    Perl5Lib,
2044    /// Root came from system `@INC`.
2045    SystemInc,
2046}
2047
2048/// Caller-supplied include root for module-resolution candidate facts.
2049#[derive(Debug, Clone, PartialEq, Eq)]
2050#[non_exhaustive]
2051pub struct ModuleResolutionRoot {
2052    /// Include root path as configured or observed by the caller.
2053    pub path: String,
2054    /// Root source category.
2055    pub kind: IncRootKind,
2056    /// Human-readable source label for diagnostics/status output.
2057    pub source: String,
2058}
2059
2060impl ModuleResolutionRoot {
2061    /// Create an explicit include root for module candidate projection.
2062    #[must_use]
2063    pub fn new(path: impl Into<String>, kind: IncRootKind, source: impl Into<String>) -> Self {
2064        Self { path: path.into(), kind, source: source.into() }
2065    }
2066}
2067
2068/// Module load request.
2069#[derive(Debug, Clone, PartialEq, Eq)]
2070#[non_exhaustive]
2071pub struct ModuleRequest {
2072    /// Static target, when known.
2073    pub target: Option<String>,
2074    /// Source shape that requested the module.
2075    pub kind: ModuleRequestKind,
2076    /// Source range for the request.
2077    pub range: SourceLocation,
2078    /// Directive that produced this request.
2079    pub directive_item: Option<HirId>,
2080    /// Scope containing the request.
2081    pub scope_id: Option<HirScopeId>,
2082    /// Package context active at the request.
2083    pub package_context: Option<String>,
2084    /// Static resolution status for this first slice.
2085    pub resolution: ModuleResolutionStatus,
2086    /// How this fact was produced.
2087    pub provenance: CompileProvenance,
2088    /// Confidence in this fact.
2089    pub confidence: CompileConfidence,
2090}
2091
2092/// Source shape for a module load request.
2093#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2094#[non_exhaustive]
2095pub enum ModuleRequestKind {
2096    /// `use Module`.
2097    Use,
2098    /// `require Module`.
2099    Require,
2100    /// `use parent`.
2101    Parent,
2102    /// `use base`.
2103    Base,
2104}
2105
2106/// Static module-resolution status.
2107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2108#[non_exhaustive]
2109pub enum ModuleResolutionStatus {
2110    /// Static module target was recorded, but path resolution is intentionally deferred.
2111    Deferred,
2112    /// Module target is dynamic and cannot be resolved statically.
2113    Dynamic,
2114}
2115
2116/// Derived module-resolution candidate fact keyed to a HIR module request.
2117#[derive(Debug, Clone, PartialEq, Eq)]
2118#[non_exhaustive]
2119pub struct ModuleResolutionCandidate {
2120    /// Zero-based request index in [`CompileEnvironment::module_requests`].
2121    pub request_index: usize,
2122    /// Directive HIR item that produced this request.
2123    pub directive_item: Option<HirId>,
2124    /// Source shape that requested the module.
2125    pub request_kind: ModuleRequestKind,
2126    /// Static module target.
2127    pub target: String,
2128    /// Relative module path, for example `Foo/Bar.pm`.
2129    pub relative_path: String,
2130    /// Ordered candidate roots considered for this request.
2131    pub roots: Vec<ModuleResolutionCandidateRoot>,
2132    /// Resolution status for this candidate packet.
2133    pub status: ModuleResolutionCandidateStatus,
2134    /// Candidate path selected by the resolver, when a matching file exists.
2135    pub resolved_path: Option<String>,
2136    /// Source range for the request.
2137    pub range: SourceLocation,
2138    /// Package context active at the request.
2139    pub package_context: Option<String>,
2140    /// How this fact was produced.
2141    pub provenance: CompileProvenance,
2142    /// Confidence in this fact.
2143    pub confidence: CompileConfidence,
2144}
2145
2146impl ModuleResolutionCandidate {
2147    /// Build the cache key for this module-resolution candidate.
2148    ///
2149    /// The key intentionally records request identity, root provenance/order,
2150    /// candidate paths, source anchor, and resolver epoch, but not the current
2151    /// resolution outcome. Candidate existence is tracked separately as an
2152    /// invalidation input so file appearance/removal can invalidate a cached
2153    /// result without changing the request identity.
2154    #[must_use]
2155    pub fn cache_key(&self, resolver_epoch: u64) -> ModuleResolutionCacheKey {
2156        ModuleResolutionCacheKey {
2157            resolver_epoch,
2158            request_index: self.request_index,
2159            directive_item: self.directive_item,
2160            request_kind: self.request_kind,
2161            target: self.target.clone(),
2162            relative_path: self.relative_path.clone(),
2163            roots: self
2164                .roots
2165                .iter()
2166                .map(ModuleResolutionCacheRootKey::from_candidate_root)
2167                .collect(),
2168            range: self.range,
2169            package_context: self.package_context.clone(),
2170        }
2171    }
2172
2173    /// Build cache invalidation inputs for this candidate.
2174    ///
2175    /// The caller supplies the path-existence predicate; parser-core still does
2176    /// not read ambient process state or inspect the filesystem directly.
2177    #[must_use]
2178    pub fn cache_invalidation(
2179        &self,
2180        resolver_epoch: u64,
2181        mut path_exists: impl FnMut(&str) -> bool,
2182    ) -> ModuleResolutionCacheInvalidation {
2183        let path_states = self
2184            .roots
2185            .iter()
2186            .map(|root| ModuleResolutionCandidatePathState {
2187                candidate_path: root.candidate_path.clone(),
2188                exists: path_exists(&root.candidate_path),
2189            })
2190            .collect();
2191
2192        ModuleResolutionCacheInvalidation { key: self.cache_key(resolver_epoch), path_states }
2193    }
2194}
2195
2196/// A single candidate root/path pair for a static module request.
2197#[derive(Debug, Clone, PartialEq, Eq)]
2198#[non_exhaustive]
2199pub struct ModuleResolutionCandidateRoot {
2200    /// Include root path as configured or observed by the caller.
2201    pub path: String,
2202    /// Root source category.
2203    pub kind: IncRootKind,
2204    /// Human-readable source label.
2205    pub source: String,
2206    /// Candidate module path under this root.
2207    pub candidate_path: String,
2208    /// Search precedence; lower values are searched first.
2209    pub precedence: usize,
2210}
2211
2212/// Cache key for a static module-resolution candidate.
2213#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2214#[non_exhaustive]
2215pub struct ModuleResolutionCacheKey {
2216    /// Caller-supplied resolver epoch, policy version, or filesystem snapshot id.
2217    pub resolver_epoch: u64,
2218    /// Zero-based request index in [`CompileEnvironment::module_requests`].
2219    pub request_index: usize,
2220    /// Directive HIR item that produced this request.
2221    pub directive_item: Option<HirId>,
2222    /// Source shape that requested the module.
2223    pub request_kind: ModuleRequestKind,
2224    /// Static module target.
2225    pub target: String,
2226    /// Relative module path, for example `Foo/Bar.pm`.
2227    pub relative_path: String,
2228    /// Ordered candidate roots included in the cache identity.
2229    pub roots: Vec<ModuleResolutionCacheRootKey>,
2230    /// Source range for the request.
2231    pub range: SourceLocation,
2232    /// Package context active at the request.
2233    pub package_context: Option<String>,
2234}
2235
2236/// Root identity included in module-resolution cache keys.
2237#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2238#[non_exhaustive]
2239pub struct ModuleResolutionCacheRootKey {
2240    /// Include root path as configured or observed by the caller.
2241    pub path: String,
2242    /// Root source category.
2243    pub kind: IncRootKind,
2244    /// Human-readable source label.
2245    pub source: String,
2246    /// Candidate module path under this root.
2247    pub candidate_path: String,
2248    /// Search precedence; lower values are searched first.
2249    pub precedence: usize,
2250}
2251
2252impl ModuleResolutionCacheRootKey {
2253    fn from_candidate_root(root: &ModuleResolutionCandidateRoot) -> Self {
2254        Self {
2255            path: root.path.clone(),
2256            kind: root.kind,
2257            source: root.source.clone(),
2258            candidate_path: root.candidate_path.clone(),
2259            precedence: root.precedence,
2260        }
2261    }
2262}
2263
2264/// Candidate path state used to invalidate module-resolution cache entries.
2265#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2266#[non_exhaustive]
2267pub struct ModuleResolutionCandidatePathState {
2268    /// Candidate module path under a searched root.
2269    pub candidate_path: String,
2270    /// Whether the caller observed the candidate path as existing.
2271    pub exists: bool,
2272}
2273
2274/// Cache invalidation inputs for a module-resolution candidate.
2275#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2276#[non_exhaustive]
2277pub struct ModuleResolutionCacheInvalidation {
2278    /// Stable cache key for the module-resolution request and root set.
2279    pub key: ModuleResolutionCacheKey,
2280    /// Candidate path existence states observed by the caller.
2281    pub path_states: Vec<ModuleResolutionCandidatePathState>,
2282}
2283
2284/// Static resolution state for a module candidate packet.
2285#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2286#[non_exhaustive]
2287pub enum ModuleResolutionCandidateStatus {
2288    /// Candidate paths were built but not resolved against the filesystem.
2289    CandidateBuilt,
2290    /// Dynamic module target cannot produce candidate paths.
2291    Dynamic,
2292    /// Static request has no roots to search.
2293    NotFound,
2294    /// Downstream resolver found a matching module.
2295    Resolved,
2296    /// Downstream resolver exhausted its timeout budget.
2297    TimedOut,
2298}
2299
2300/// Compile-time phase block.
2301#[derive(Debug, Clone, PartialEq, Eq)]
2302#[non_exhaustive]
2303pub struct CompilePhaseBlock {
2304    /// Phase kind.
2305    pub phase: CompilePhase,
2306    /// Source range for the block.
2307    pub range: SourceLocation,
2308    /// Scope containing the block.
2309    pub scope_id: Option<HirScopeId>,
2310    /// Package context active at the block.
2311    pub package_context: Option<String>,
2312    /// How this fact was produced.
2313    pub provenance: CompileProvenance,
2314    /// Confidence in this fact.
2315    pub confidence: CompileConfidence,
2316}
2317
2318/// Perl compile/runtime phase.
2319#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2320#[non_exhaustive]
2321pub enum CompilePhase {
2322    /// `BEGIN`.
2323    Begin,
2324    /// `UNITCHECK`.
2325    UnitCheck,
2326    /// `CHECK`.
2327    Check,
2328    /// `INIT`.
2329    Init,
2330    /// `END`.
2331    End,
2332    /// Unknown phase spelling.
2333    Unknown,
2334}
2335
2336/// Dynamic compile-environment boundary.
2337#[derive(Debug, Clone, PartialEq, Eq)]
2338#[non_exhaustive]
2339pub struct CompileEnvironmentBoundary {
2340    /// Boundary category.
2341    pub kind: CompileEnvironmentBoundaryKind,
2342    /// Source range for the boundary.
2343    pub range: SourceLocation,
2344    /// HIR item that also records this boundary, when available.
2345    pub boundary_item: Option<HirId>,
2346    /// Scope containing the boundary.
2347    pub scope_id: Option<HirScopeId>,
2348    /// Package context active at the boundary.
2349    pub package_context: Option<String>,
2350    /// Short reason for status/proof output.
2351    pub reason: String,
2352    /// How this fact was produced.
2353    pub provenance: CompileProvenance,
2354    /// Confidence in this fact.
2355    pub confidence: CompileConfidence,
2356}
2357
2358/// Dynamic compile-environment boundary category.
2359#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2360#[non_exhaustive]
2361pub enum CompileEnvironmentBoundaryKind {
2362    /// `require` target could not be determined statically.
2363    DynamicRequire,
2364    /// Pragma or feature arguments could not be determined statically.
2365    DynamicPragmaArgs,
2366    /// Include-root effect is dynamic or unsupported.
2367    DynamicIncRoot,
2368    /// Phase block contains compile-time execution that is not evaluated here.
2369    PhaseBlockExecution,
2370    /// Symbolic-reference dereference is possible while `strict refs` is disabled.
2371    SymbolicReferenceDeref,
2372}
2373
2374/// Provenance for HIR-local compile-environment facts.
2375#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2376#[non_exhaustive]
2377pub enum CompileProvenance {
2378    /// Fact came directly from parser AST syntax.
2379    ExactAst,
2380    /// Fact came from a simple compile-time desugaring.
2381    DesugaredAst,
2382    /// Fact came from conservative dynamic-boundary classification.
2383    DynamicBoundary,
2384}
2385
2386/// Confidence for HIR-local compile-environment facts.
2387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2388#[non_exhaustive]
2389pub enum CompileConfidence {
2390    /// High-confidence exact or simple desugared fact.
2391    High,
2392    /// Medium-confidence static interpretation.
2393    Medium,
2394    /// Low-confidence dynamic-boundary fact.
2395    Low,
2396}
2397
2398fn import_spec_from_directive(directive: &CompileDirective, file_id: FileId) -> Option<ImportSpec> {
2399    match directive.action {
2400        CompileDirectiveAction::Use => {
2401            let module = directive.module.as_deref()?;
2402            if is_version_pragma(module) {
2403                return None;
2404            }
2405            if module == "constant" {
2406                return Some(classify_constant_import(directive, file_id));
2407            }
2408
2409            let (kind, symbols, provenance, confidence) =
2410                classify_import_args(&directive.args, module, directive.range);
2411            Some(import_spec(
2412                module.to_string(),
2413                kind,
2414                symbols,
2415                provenance,
2416                confidence,
2417                directive,
2418                file_id,
2419            ))
2420        }
2421        CompileDirectiveAction::Require => {
2422            let (module, kind, symbols, provenance, confidence) =
2423                if let Some(module) = directive.module.as_ref() {
2424                    (
2425                        module.clone(),
2426                        ImportKind::Require,
2427                        ImportSymbols::Default,
2428                        Provenance::ExactAst,
2429                        Confidence::High,
2430                    )
2431                } else {
2432                    (
2433                        String::new(),
2434                        ImportKind::DynamicRequire,
2435                        ImportSymbols::Dynamic,
2436                        Provenance::DynamicBoundary,
2437                        Confidence::Low,
2438                    )
2439                };
2440            Some(import_spec(module, kind, symbols, provenance, confidence, directive, file_id))
2441        }
2442        CompileDirectiveAction::No => None,
2443    }
2444}
2445
2446fn import_spec(
2447    module: String,
2448    kind: ImportKind,
2449    symbols: ImportSymbols,
2450    provenance: Provenance,
2451    confidence: Confidence,
2452    directive: &CompileDirective,
2453    file_id: FileId,
2454) -> ImportSpec {
2455    ImportSpec {
2456        module,
2457        kind,
2458        symbols,
2459        provenance,
2460        confidence,
2461        file_id: Some(file_id),
2462        anchor_id: Some(AnchorId(directive.range.start as u64)),
2463        scope_id: directive.scope_id.map(|id| ScopeId(id.index() as u64)),
2464        span_start_byte: Some(directive.range.start as u32),
2465    }
2466}
2467
2468fn classify_import_args(
2469    args: &[String],
2470    module: &str,
2471    range: SourceLocation,
2472) -> (ImportKind, ImportSymbols, Provenance, Confidence) {
2473    if args.is_empty() {
2474        let bare_len = "use ".len() + module.len() + 1;
2475        let span_len = range.end.saturating_sub(range.start);
2476        if span_len > bare_len {
2477            return (
2478                ImportKind::UseEmpty,
2479                ImportSymbols::None,
2480                Provenance::ExactAst,
2481                Confidence::High,
2482            );
2483        }
2484        return (ImportKind::Use, ImportSymbols::Default, Provenance::ExactAst, Confidence::High);
2485    }
2486
2487    let mut explicit_names = Vec::new();
2488    let mut tags = Vec::new();
2489    let mut has_dynamic_arg = false;
2490
2491    for arg in args {
2492        let trimmed = arg.trim();
2493        if trimmed == "=>" || trimmed == "," || trimmed == "\\" {
2494            continue;
2495        }
2496
2497        if let Some(inner) = parse_qw_content(trimmed) {
2498            collect_qw_import_words(inner, &mut explicit_names, &mut tags);
2499            continue;
2500        }
2501
2502        let was_quoted = is_quoted(trimmed);
2503        let unquoted = unquote(trimmed);
2504        if !was_quoted && looks_like_dynamic_import_arg(unquoted) {
2505            has_dynamic_arg = true;
2506            continue;
2507        }
2508
2509        if let Some(tag) = unquoted.strip_prefix(':') {
2510            tags.push(tag.to_string());
2511            continue;
2512        }
2513
2514        if looks_like_symbol_name(unquoted) {
2515            explicit_names.push(unquoted.to_string());
2516        }
2517    }
2518
2519    if has_dynamic_arg {
2520        return (
2521            ImportKind::UseExplicitList,
2522            ImportSymbols::Dynamic,
2523            Provenance::DynamicBoundary,
2524            Confidence::Low,
2525        );
2526    }
2527
2528    if !tags.is_empty() && explicit_names.is_empty() {
2529        return (
2530            ImportKind::UseTag,
2531            ImportSymbols::Tags(tags),
2532            Provenance::ExactAst,
2533            Confidence::High,
2534        );
2535    }
2536
2537    if !tags.is_empty() && !explicit_names.is_empty() {
2538        return (
2539            ImportKind::UseExplicitList,
2540            ImportSymbols::Mixed { tags, names: explicit_names },
2541            Provenance::ExactAst,
2542            Confidence::High,
2543        );
2544    }
2545
2546    if !explicit_names.is_empty() {
2547        return (
2548            ImportKind::UseExplicitList,
2549            ImportSymbols::Explicit(explicit_names),
2550            Provenance::ExactAst,
2551            Confidence::High,
2552        );
2553    }
2554
2555    (ImportKind::UseEmpty, ImportSymbols::None, Provenance::ExactAst, Confidence::High)
2556}
2557
2558fn classify_constant_import(directive: &CompileDirective, file_id: FileId) -> ImportSpec {
2559    let mut constant_names = Vec::new();
2560    let args = &directive.args;
2561
2562    if args.first().map(String::as_str) == Some("{") {
2563        let mut index = 1;
2564        while index < args.len() {
2565            let token = args[index].trim();
2566            if token == "}" || token == "=>" || token == "," {
2567                index += 1;
2568                continue;
2569            }
2570            if index + 1 < args.len() && args[index + 1].trim() == "=>" {
2571                constant_names.push(unquote(token).to_string());
2572                index += 3;
2573            } else {
2574                index += 1;
2575            }
2576        }
2577    } else if let Some(inner) = args.first().and_then(|arg| parse_qw_content(arg.trim())) {
2578        constant_names.extend(inner.split_whitespace().map(str::to_string));
2579    } else if let Some(name) = args.first() {
2580        let trimmed = unquote(name.trim());
2581        if looks_like_constant_name(trimmed) {
2582            constant_names.push(trimmed.to_string());
2583        }
2584    }
2585
2586    let mut seen = std::collections::HashSet::new();
2587    constant_names.retain(|name| seen.insert(name.clone()));
2588
2589    let symbols = if constant_names.is_empty() {
2590        ImportSymbols::None
2591    } else {
2592        ImportSymbols::Explicit(constant_names)
2593    };
2594
2595    import_spec(
2596        "constant".to_string(),
2597        ImportKind::UseConstant,
2598        symbols,
2599        Provenance::ExactAst,
2600        Confidence::High,
2601        directive,
2602        file_id,
2603    )
2604}
2605
2606fn collect_qw_import_words(inner: &str, explicit_names: &mut Vec<String>, tags: &mut Vec<String>) {
2607    for word in inner.split_whitespace() {
2608        if let Some(tag) = word.strip_prefix(':') {
2609            tags.push(tag.to_string());
2610        } else {
2611            explicit_names.push(word.to_string());
2612        }
2613    }
2614}
2615
2616fn is_version_pragma(module: &str) -> bool {
2617    if module.chars().next().is_some_and(|character| character.is_ascii_digit()) {
2618        return true;
2619    }
2620    module.starts_with('v')
2621        && module.len() > 1
2622        && module[1..].chars().all(|character| character.is_ascii_digit() || character == '.')
2623}
2624
2625fn parse_qw_content(value: &str) -> Option<&str> {
2626    let rest = value.strip_prefix("qw")?.trim_start();
2627    let mut chars = rest.chars();
2628    let open = chars.next()?;
2629    let close = match open {
2630        '(' => ')',
2631        '[' => ']',
2632        '{' => '}',
2633        '<' => '>',
2634        other => other,
2635    };
2636    let inner_start = open.len_utf8();
2637    let inner_end = rest.len().checked_sub(close.len_utf8())?;
2638    if inner_start > inner_end || !rest.ends_with(close) {
2639        return None;
2640    }
2641    Some(&rest[inner_start..inner_end])
2642}
2643
2644fn is_quoted(value: &str) -> bool {
2645    (value.starts_with('\'') && value.ends_with('\''))
2646        || (value.starts_with('"') && value.ends_with('"'))
2647}
2648
2649fn unquote(value: &str) -> &str {
2650    if is_quoted(value) && value.len() >= 2 { &value[1..value.len() - 1] } else { value }
2651}
2652
2653fn looks_like_dynamic_import_arg(value: &str) -> bool {
2654    value.starts_with('$')
2655        || value.starts_with('@')
2656        || value.starts_with('%')
2657        || value.starts_with('&')
2658        || value.starts_with('*')
2659}
2660
2661fn looks_like_symbol_name(value: &str) -> bool {
2662    let value = unquote(value);
2663    if value.is_empty() {
2664        return false;
2665    }
2666    if value.starts_with(':') {
2667        return true;
2668    }
2669    value
2670        .chars()
2671        .next()
2672        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
2673}
2674
2675fn looks_like_constant_name(value: &str) -> bool {
2676    if value.is_empty() {
2677        return false;
2678    }
2679    value
2680        .chars()
2681        .next()
2682        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
2683}
2684
2685fn module_target_to_relative_path(target: &str) -> Option<String> {
2686    let relative_path =
2687        if target.ends_with(".pm") || target.ends_with(".pl") || target.contains(['/', '\\']) {
2688            target.replace('\\', "/")
2689        } else {
2690            let canonical = target.replace('\'', "::");
2691            format!("{}.pm", canonical.replace("::", "/"))
2692        };
2693
2694    is_safe_relative_module_path(&relative_path).then_some(relative_path)
2695}
2696
2697fn normalize_module_target(target: &str) -> String {
2698    target.trim().trim_matches('"').trim_matches('\'').to_string()
2699}
2700
2701fn is_safe_relative_module_path(path: &str) -> bool {
2702    if path.is_empty() || path.starts_with('/') || path.contains(':') {
2703        return false;
2704    }
2705
2706    path.split('/').all(|segment| !matches!(segment, "" | "." | ".."))
2707}
2708
2709fn join_candidate_path(root: &str, relative_path: &str) -> String {
2710    let normalized_root = root.replace('\\', "/");
2711    let trimmed_root = normalized_root.trim_end_matches('/');
2712    if trimmed_root.is_empty() {
2713        relative_path.to_string()
2714    } else {
2715        format!("{trimmed_root}/{relative_path}")
2716    }
2717}
2718
2719/// First-slice HIR constructs.
2720#[derive(Debug, Clone, PartialEq, Eq)]
2721#[non_exhaustive]
2722pub enum HirKind {
2723    /// `package Foo;` or block package declaration.
2724    PackageDecl(PackageDecl),
2725    /// `sub foo { ... }` declaration.
2726    SubDecl(SubDecl),
2727    /// `method foo { ... }` declaration.
2728    MethodDecl(MethodDecl),
2729    /// `use Module ...;` declaration.
2730    UseDecl(UseDecl),
2731    /// `require Module;` call recognized as a compile-time declaration shape.
2732    RequireDecl(RequireDecl),
2733    /// `my`, `our`, `state`, or `local` variable declaration.
2734    VariableDecl(VariableDecl),
2735    /// Function-like call expression shell.
2736    CallExpr(CallExpr),
2737    /// Method-call expression shell.
2738    MethodCallExpr(MethodCallExpr),
2739    /// Indirect-object method-call expression shell.
2740    IndirectCallExpr(IndirectCallExpr),
2741    /// Bareword expression shell.
2742    BarewordExpr(BarewordExpr),
2743    /// Literal expression shell.
2744    LiteralExpr(LiteralExpr),
2745    /// Block expression shell without scope construction.
2746    BlockShell(BlockShell),
2747    /// Unsupported or intentionally dynamic Perl boundary.
2748    DynamicBoundary(DynamicBoundary),
2749}
2750
2751impl HirKind {
2752    /// Canonical names for all first-slice HIR construct variants.
2753    ///
2754    /// Metrics and status generators should use this list instead of keeping a
2755    /// separate copy of the current HIR surface.
2756    pub const ALL_KIND_NAMES: &[&'static str] = &[
2757        "BarewordExpr",
2758        "BlockShell",
2759        "CallExpr",
2760        "DynamicBoundary",
2761        "IndirectCallExpr",
2762        "LiteralExpr",
2763        "MethodCallExpr",
2764        "MethodDecl",
2765        "PackageDecl",
2766        "RequireDecl",
2767        "SubDecl",
2768        "UseDecl",
2769        "VariableDecl",
2770    ];
2771}
2772
2773/// Package declaration HIR payload.
2774#[derive(Debug, Clone, PartialEq, Eq)]
2775#[non_exhaustive]
2776pub struct PackageDecl {
2777    /// Package name.
2778    pub name: String,
2779    /// Precise package-name source range.
2780    pub name_range: SourceLocation,
2781    /// Whether this declaration owns an inline block.
2782    pub has_block: bool,
2783}
2784
2785/// Subroutine declaration HIR payload.
2786#[derive(Debug, Clone, PartialEq, Eq)]
2787#[non_exhaustive]
2788pub struct SubDecl {
2789    /// Subroutine name, absent for anonymous subs.
2790    pub name: Option<String>,
2791    /// Precise subroutine-name source range when available.
2792    pub name_range: Option<SourceLocation>,
2793    /// Whether the declaration has a prototype.
2794    pub has_prototype: bool,
2795    /// Whether the declaration has a signature.
2796    pub has_signature: bool,
2797    /// Number of parsed attributes.
2798    pub attribute_count: usize,
2799}
2800
2801/// Method declaration HIR payload.
2802#[derive(Debug, Clone, PartialEq, Eq)]
2803#[non_exhaustive]
2804pub struct MethodDecl {
2805    /// Method name.
2806    pub name: String,
2807    /// Whether the declaration has a signature.
2808    pub has_signature: bool,
2809    /// Number of parsed attributes.
2810    pub attribute_count: usize,
2811}
2812
2813/// Use declaration HIR payload.
2814#[derive(Debug, Clone, PartialEq, Eq)]
2815#[non_exhaustive]
2816pub struct UseDecl {
2817    /// Module or pragma name.
2818    pub module: String,
2819    /// Parsed import arguments.
2820    pub args: Vec<String>,
2821    /// Whether the parser classified the module as a source-filter risk.
2822    pub has_filter_risk: bool,
2823}
2824
2825/// Require declaration HIR payload.
2826#[derive(Debug, Clone, PartialEq, Eq)]
2827#[non_exhaustive]
2828pub struct RequireDecl {
2829    /// Statically recognized require target when available.
2830    pub target: Option<String>,
2831    /// Number of parser arguments on the underlying function call.
2832    pub arg_count: usize,
2833}
2834
2835/// Variable declaration HIR payload.
2836#[derive(Debug, Clone, PartialEq, Eq)]
2837#[non_exhaustive]
2838pub struct VariableDecl {
2839    /// Scope/storage declarator: `my`, `our`, `state`, or `local`.
2840    pub declarator: String,
2841    /// Variables statically visible in the declaration.
2842    pub variables: Vec<VariableBinding>,
2843    /// Number of parsed attributes on the declaration.
2844    pub attribute_count: usize,
2845    /// Whether the declaration has an initializer expression.
2846    pub has_initializer: bool,
2847    /// Whether this came from a list declaration.
2848    pub is_list: bool,
2849}
2850
2851/// One variable binding named by a declaration.
2852#[derive(Debug, Clone, PartialEq, Eq)]
2853#[non_exhaustive]
2854pub struct VariableBinding {
2855    /// Variable sigil.
2856    pub sigil: String,
2857    /// Variable name without sigil.
2858    pub name: String,
2859    /// Source range for the variable token.
2860    pub range: SourceLocation,
2861}
2862
2863/// Function-like call shell payload.
2864#[derive(Debug, Clone, PartialEq, Eq)]
2865#[non_exhaustive]
2866pub struct CallExpr {
2867    /// Callee name, or parser sentinel for dynamic call forms.
2868    pub name: String,
2869    /// Number of parsed arguments.
2870    pub arg_count: usize,
2871    /// Parser-observed call shape.
2872    pub form: CallForm,
2873}
2874
2875/// Parser-observed call shape.
2876#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2877#[non_exhaustive]
2878pub enum CallForm {
2879    /// A named function call such as `foo(...)`.
2880    NamedFunction,
2881    /// A coderef/dynamic callee call such as `$callback->(...)`.
2882    Coderef,
2883}
2884
2885/// Method-call shell payload.
2886#[derive(Debug, Clone, PartialEq, Eq)]
2887#[non_exhaustive]
2888pub struct MethodCallExpr {
2889    /// Method name.
2890    pub method: String,
2891    /// Number of parsed arguments.
2892    pub arg_count: usize,
2893    /// Parser AST kind for the receiver expression.
2894    pub object_kind: &'static str,
2895}
2896
2897/// Indirect-object call shell payload.
2898#[derive(Debug, Clone, PartialEq, Eq)]
2899#[non_exhaustive]
2900pub struct IndirectCallExpr {
2901    /// Method name.
2902    pub method: String,
2903    /// Number of parsed arguments.
2904    pub arg_count: usize,
2905    /// Parser AST kind for the receiver/class expression.
2906    pub object_kind: &'static str,
2907}
2908
2909/// Bareword expression shell payload.
2910#[derive(Debug, Clone, PartialEq, Eq)]
2911#[non_exhaustive]
2912pub struct BarewordExpr {
2913    /// Bareword text as parsed.
2914    pub name: String,
2915}
2916
2917/// Literal expression shell payload.
2918#[derive(Debug, Clone, PartialEq, Eq)]
2919#[non_exhaustive]
2920pub struct LiteralExpr {
2921    /// Literal category.
2922    pub kind: LiteralKind,
2923    /// Preserved value for compact scalar literals.
2924    pub value: Option<String>,
2925    /// Whether the literal can interpolate variables.
2926    pub interpolated: Option<bool>,
2927    /// Element count for aggregate literals.
2928    pub element_count: Option<usize>,
2929    /// Pair count for hash literals.
2930    pub pair_count: Option<usize>,
2931}
2932
2933/// Literal category.
2934#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2935#[non_exhaustive]
2936pub enum LiteralKind {
2937    /// Numeric literal.
2938    Number,
2939    /// String literal.
2940    String,
2941    /// `undef`.
2942    Undef,
2943    /// Array/list literal.
2944    Array,
2945    /// Hash literal.
2946    Hash,
2947}
2948
2949/// Block shell payload.
2950#[derive(Debug, Clone, PartialEq, Eq)]
2951#[non_exhaustive]
2952pub struct BlockShell {
2953    /// Number of parsed statements directly inside the block.
2954    pub statement_count: usize,
2955}
2956
2957/// Dynamic-boundary shell payload.
2958#[derive(Debug, Clone, PartialEq, Eq)]
2959#[non_exhaustive]
2960pub struct DynamicBoundary {
2961    /// Boundary category.
2962    pub kind: DynamicBoundaryKind,
2963    /// Short human-readable reason for the boundary.
2964    pub reason: String,
2965}
2966
2967/// Dynamic-boundary category.
2968#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2969#[non_exhaustive]
2970pub enum DynamicBoundaryKind {
2971    /// Coderef/dynamic callee call through `->()`.
2972    CoderefCall,
2973    /// `eval` whose body is not a statically parsed block.
2974    EvalExpression,
2975    /// `do` whose body is not a statically parsed block.
2976    DoExpression,
2977    /// Stash/typeglob assignment whose effect cannot be modeled statically.
2978    DynamicStashMutation,
2979    /// `AUTOLOAD` declaration introduces dynamic method dispatch.
2980    Autoload,
2981    /// Symbolic-reference dereference whose target cannot be modeled statically.
2982    SymbolicReferenceDeref,
2983}