Skip to main content

perl_parser_core/hir/
model.rs

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