Skip to main content

perl_parser_core/hir/
model.rs

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