Skip to main content

perl_pragma/
map.rs

1use perl_ast::ast::Node;
2use std::ops::Range;
3
4use crate::{PragmaSnapshot, PragmaState, range_builder};
5
6/// Query object describing compile-time pragma state at a byte offset.
7#[derive(Debug, Clone, PartialEq)]
8pub struct PragmaStateQuery {
9    offset: usize,
10    snapshot: PragmaSnapshot,
11}
12
13impl PragmaStateQuery {
14    /// Byte offset this query was created for.
15    #[must_use]
16    pub fn offset(&self) -> usize {
17        self.offset
18    }
19
20    /// Immutable snapshot at this query position.
21    #[must_use]
22    pub fn snapshot(&self) -> &PragmaSnapshot {
23        &self.snapshot
24    }
25}
26
27/// Explicit compile-time pragma environment that can answer file-position
28/// queries and expose immutable snapshots.
29#[derive(Debug, Clone, Default, PartialEq)]
30pub struct CompileTimePragmaEnvironment {
31    map: PragmaMap,
32}
33
34/// One effective pragma-state transition over a source byte range.
35#[derive(Debug, Clone, PartialEq)]
36#[non_exhaustive]
37pub struct PragmaEntry {
38    /// Source byte range covered by this snapshot.
39    ///
40    /// Lexical scope restores are represented as zero-length ranges at the
41    /// scope end so callers can observe the restored state at that byte offset.
42    pub range: Range<usize>,
43    /// Immutable pragma state active for this transition.
44    pub snapshot: PragmaSnapshot,
45}
46
47/// Explicit pragma transition timeline.
48#[derive(Debug, Clone, Default, PartialEq)]
49#[non_exhaustive]
50pub struct PragmaMap {
51    entries: Box<[PragmaEntry]>,
52}
53
54impl CompileTimePragmaEnvironment {
55    /// Build a queryable environment from an AST.
56    #[must_use]
57    pub fn build(ast: &Node) -> Self {
58        let mut ranges = Vec::new();
59        let mut current_state = PragmaState::default();
60        range_builder::build_ranges(ast, &mut current_state, &mut ranges);
61        ranges.sort_by_key(|(range, _)| range.start);
62
63        let entries = ranges
64            .into_iter()
65            .map(|(range, state)| PragmaEntry { range, snapshot: PragmaSnapshot::from(state) })
66            .collect::<Vec<_>>()
67            .into_boxed_slice();
68
69        Self { map: PragmaMap { entries } }
70    }
71
72    /// Return a position query object with immutable state snapshot.
73    #[must_use]
74    pub fn query_at(&self, offset: usize) -> PragmaStateQuery {
75        PragmaStateQuery { offset, snapshot: self.snapshot_at(offset) }
76    }
77
78    /// Return the immutable snapshot active at the given byte offset.
79    #[must_use]
80    pub fn snapshot_at(&self, offset: usize) -> PragmaSnapshot {
81        self.map.snapshot_at(offset)
82    }
83
84    /// Access the underlying transition map.
85    #[must_use]
86    pub fn map(&self) -> &PragmaMap {
87        &self.map
88    }
89
90    /// Access the underlying range map for advanced consumers.
91    #[must_use]
92    pub fn as_map(&self) -> Vec<(Range<usize>, PragmaSnapshot)> {
93        self.map.to_tuples()
94    }
95}
96
97impl PragmaMap {
98    /// Return the immutable snapshot active at the given byte offset.
99    #[must_use]
100    pub fn snapshot_at(&self, offset: usize) -> PragmaSnapshot {
101        let idx = self.entries.partition_point(|entry| entry.range.start <= offset);
102        let snapshot = if idx > 0 {
103            self.entries[idx - 1].snapshot.clone()
104        } else {
105            PragmaSnapshot::default()
106        };
107
108        normalize_snapshot(snapshot)
109    }
110
111    /// Return the concrete pragma state active at the given byte offset.
112    #[must_use]
113    pub fn state_at(&self, offset: usize) -> PragmaState {
114        self.snapshot_at(offset).into()
115    }
116
117    /// Return the final top-level pragma state after all lexical restores.
118    #[must_use]
119    pub fn final_state(&self) -> PragmaState {
120        let state = self
121            .entries
122            .last()
123            .map_or_else(PragmaState::default, |entry| entry.snapshot.clone().into());
124
125        normalize_state(state)
126    }
127
128    /// Create a cursor for monotonic queries against this map.
129    #[must_use]
130    pub fn cursor(&self) -> PragmaQueryCursor {
131        PragmaQueryCursor::new()
132    }
133
134    /// Return all transition entries in source order.
135    #[must_use]
136    pub fn entries(&self) -> &[PragmaEntry] {
137        &self.entries
138    }
139
140    /// Convert this map to the legacy tuple representation.
141    #[must_use]
142    pub fn to_tuples(&self) -> Vec<(Range<usize>, PragmaSnapshot)> {
143        self.entries.iter().map(|e| (e.range.clone(), e.snapshot.clone())).collect()
144    }
145}
146
147fn normalize_snapshot(mut snapshot: PragmaSnapshot) -> PragmaSnapshot {
148    if snapshot.state.signatures_strict {
149        snapshot.state.strict_vars = true;
150        snapshot.state.strict_subs = true;
151        snapshot.state.strict_refs = true;
152    }
153
154    snapshot
155}
156
157pub(crate) fn normalize_state(mut state: PragmaState) -> PragmaState {
158    if state.signatures_strict {
159        state.strict_vars = true;
160        state.strict_subs = true;
161        state.strict_refs = true;
162    }
163
164    state
165}
166
167/// Monotonic query cursor for repeated pragma lookups.
168///
169/// Reuse a single cursor when querying offsets in non-decreasing order to
170/// avoid repeated binary searches over the pragma map.
171#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
172pub struct PragmaQueryCursor {
173    index: usize,
174}
175
176impl PragmaQueryCursor {
177    /// Create a new cursor positioned before the start of the map.
178    #[must_use]
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    /// Query state at `offset` assuming lookups are mostly non-decreasing.
184    ///
185    /// This is the primary cursor API for the explicit pragma map.
186    /// If the caller queries an older offset, this falls back to a binary
187    /// search and repositions the cursor.
188    pub fn snapshot_at(&mut self, pragma_map: &PragmaMap, offset: usize) -> PragmaSnapshot {
189        let snapshot = self
190            .entry_for_offset(pragma_map.entries(), offset)
191            .map_or_else(PragmaSnapshot::default, |entry| entry.snapshot.clone());
192
193        normalize_snapshot(snapshot)
194    }
195
196    /// Query state at `offset` against the explicit pragma map.
197    pub fn state_at(&mut self, pragma_map: &PragmaMap, offset: usize) -> PragmaState {
198        self.snapshot_at(pragma_map, offset).into()
199    }
200
201    /// Query state at `offset` assuming lookups are mostly non-decreasing.
202    ///
203    /// This legacy tuple API is retained for existing `PragmaTracker` callers.
204    /// If the caller queries an older offset, this falls back to a binary
205    /// search and repositions the cursor.
206    pub fn state_for_offset(
207        &mut self,
208        pragma_map: &[(Range<usize>, PragmaState)],
209        offset: usize,
210    ) -> PragmaState {
211        if pragma_map.is_empty() {
212            return PragmaState::default();
213        }
214
215        if self.index >= pragma_map.len() {
216            self.index = pragma_map.len() - 1;
217        }
218
219        if pragma_map[self.index].0.start > offset {
220            self.index = pragma_map.partition_point(|(range, _)| range.start <= offset);
221            if self.index > 0 {
222                self.index -= 1;
223            }
224        } else {
225            while self.index + 1 < pragma_map.len() && pragma_map[self.index + 1].0.start <= offset
226            {
227                self.index += 1;
228            }
229        }
230
231        let state = if pragma_map[self.index].0.start <= offset {
232            pragma_map[self.index].1.clone()
233        } else {
234            PragmaState::default()
235        };
236
237        normalize_state(state)
238    }
239
240    fn entry_for_offset<'a>(
241        &mut self,
242        entries: &'a [PragmaEntry],
243        offset: usize,
244    ) -> Option<&'a PragmaEntry> {
245        if entries.is_empty() {
246            return None;
247        }
248
249        if self.index >= entries.len() {
250            self.index = entries.len() - 1;
251        }
252
253        if entries[self.index].range.start > offset {
254            self.index = entries.partition_point(|entry| entry.range.start <= offset);
255            if self.index > 0 {
256                self.index -= 1;
257            }
258        } else {
259            while self.index + 1 < entries.len() && entries[self.index + 1].range.start <= offset {
260                self.index += 1;
261            }
262        }
263
264        if entries[self.index].range.start <= offset { Some(&entries[self.index]) } else { None }
265    }
266}