ad_astra/analysis/
read.rs

1////////////////////////////////////////////////////////////////////////////////
2// This file is part of "Ad Astra", an embeddable scripting programming       //
3// language platform.                                                         //
4//                                                                            //
5// This work is proprietary software with source-available code.              //
6//                                                                            //
7// To copy, use, distribute, or contribute to this work, you must agree to    //
8// the terms of the General License Agreement:                                //
9//                                                                            //
10// https://github.com/Eliah-Lakhin/ad-astra/blob/master/EULA.md               //
11//                                                                            //
12// The agreement grants a Basic Commercial License, allowing you to use       //
13// this work in non-commercial and limited commercial products with a total   //
14// gross revenue cap. To remove this commercial limit for one of your         //
15// products, you must acquire a Full Commercial License.                      //
16//                                                                            //
17// If you contribute to the source code, documentation, or related materials, //
18// you must grant me an exclusive license to these contributions.             //
19// Contributions are governed by the "Contributions" section of the General   //
20// License Agreement.                                                         //
21//                                                                            //
22// Copying the work in parts is strictly forbidden, except as permitted       //
23// under the General License Agreement.                                       //
24//                                                                            //
25// If you do not or cannot agree to the terms of this Agreement,              //
26// do not use this work.                                                      //
27//                                                                            //
28// This work is provided "as is", without any warranties, express or implied, //
29// except where such disclaimers are legally invalid.                         //
30//                                                                            //
31// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин).                 //
32// All rights reserved.                                                       //
33////////////////////////////////////////////////////////////////////////////////
34
35use std::ops::Deref;
36
37use ahash::RandomState;
38use lady_deirdre::{
39    analysis::{
40        AbstractTask,
41        AnalysisTask,
42        DocumentReadGuard,
43        SemanticAccess,
44        TaskHandle,
45        TriggerHandle,
46    },
47    arena::{Id, Identifiable},
48    lexis::ToSpan,
49    sync::Shared,
50    syntax::SyntaxTree,
51};
52
53use crate::{
54    analysis::{
55        symbols::{LookupOptions, ModuleSymbol, SymbolsLookup},
56        DiagnosticsDepth,
57        ModuleDiagnostics,
58        ModuleError,
59        ModuleResult,
60        ModuleResultEx,
61        ModuleText,
62    },
63    interpret::ScriptFn,
64    report::system_panic,
65    runtime::{PackageMeta, ScriptOrigin},
66    syntax::{PolyRefOrigin, ScriptNode, SpanBounds},
67};
68
69/// An object that grants non-exclusive access to the
70/// [ScriptModule](crate::analysis::ScriptModule) content.
71///
72/// Created by the [read](crate::analysis::ScriptModule::read) and
73/// [try_read](crate::analysis::ScriptModule::try_read) functions.
74///
75/// Implements the [ModuleRead] trait, which provides content read functions.
76pub struct ModuleReadGuard<'a, H: TaskHandle = TriggerHandle> {
77    pub(super) id: Id,
78    pub(super) package: &'static PackageMeta,
79    pub(super) task: AnalysisTask<'a, ScriptNode, H, RandomState>,
80}
81
82impl<'a, H: TaskHandle> Identifiable for ModuleReadGuard<'a, H> {
83    #[inline(always)]
84    fn id(&self) -> Id {
85        self.id
86    }
87}
88
89impl<'a, H: TaskHandle> ModuleRead<H> for ModuleReadGuard<'a, H> {
90    #[inline(always)]
91    fn package(&self) -> &'static PackageMeta {
92        self.package
93    }
94}
95
96impl<'a, H: TaskHandle> ModuleReadSealed<H> for ModuleReadGuard<'a, H> {
97    type Task = AnalysisTask<'a, ScriptNode, H, RandomState>;
98
99    #[inline(always)]
100    fn task(&self) -> &Self::Task {
101        &self.task
102    }
103}
104
105/// A set of read functions for the
106/// [ScriptModule](crate::analysis::ScriptModule) content.
107///
108/// This trait is implemented by both the [ModuleReadGuard] and
109/// [ModuleWriteGuard](crate::analysis::ModuleWriteGuard) objects. The trait is
110/// sealed, meaning it cannot be implemented outside of this crate.
111pub trait ModuleRead<H: TaskHandle>: Identifiable + ModuleReadSealed<H> {
112    /// Returns the metadata object of the script package under which the
113    /// underlying [ScriptModule](crate::analysis::ScriptModule) is being
114    /// analyzed.
115    ///
116    /// This value is the same as the one provided to the module's
117    /// [constructor](crate::analysis::ScriptModule::new) function.
118    ///
119    /// See [ScriptPackage](crate::runtime::ScriptPackage) for details.
120    fn package(&self) -> &'static PackageMeta;
121
122    /// Returns true if the underlying access guard has been revoked.
123    ///
124    /// If the function returns true, it indicates that the guard object needs
125    /// to be dropped as soon as possible.
126    #[inline(always)]
127    fn is_interrupted(&self) -> bool {
128        self.task().handle().is_triggered()
129    }
130
131    /// Gets access to the script module's source code text.
132    ///
133    /// See [ModuleText] for details.
134    fn text(&self) -> ModuleText {
135        ModuleText {
136            package: self.package(),
137            doc_read: self.read_doc(),
138        }
139    }
140
141    /// Computes script module diagnostics (errors and warnings).
142    ///
143    /// The returned [ModuleDiagnostics] object is a collection of inferred
144    /// issues that you can iterate through. Using this object, you can also
145    /// print the source code text annotated with diagnostic messages to the
146    /// terminal.
147    ///
148    /// The `depth` numeric argument specifies the level of diagnostic analysis
149    /// depth. Available values are 1 (syntax errors), 2 (shallow semantic
150    /// analysis), and 3 (deep semantic analysis). For details, see the
151    /// [DiagnosticsDepth] documentation.
152    fn diagnostics(&self, depth: DiagnosticsDepth) -> ModuleResult<ModuleDiagnostics> {
153        let doc_read = self.read_doc();
154
155        let ScriptNode::Root { semantics, .. } = doc_read.deref().root() else {
156            system_panic!("Incorrect root variant.");
157        };
158
159        let id = self.id();
160
161        let root_semantics = semantics.get().into_module_result(id)?;
162
163        match depth {
164            1 => {
165                let (revision, snapshot) = root_semantics
166                    .diagnostics_cross_1
167                    .snapshot(self.task())
168                    .into_module_result(id)?;
169
170                Ok(ModuleDiagnostics {
171                    id,
172                    issues: snapshot.issues.clone(),
173                    depth,
174                    revision,
175                })
176            }
177
178            2 => {
179                let (revision, snapshot) = root_semantics
180                    .diagnostics_cross_2
181                    .snapshot(self.task())
182                    .into_module_result(id)?;
183
184                Ok(ModuleDiagnostics {
185                    id,
186                    issues: snapshot.issues.clone(),
187                    depth,
188                    revision,
189                })
190            }
191
192            3 => {
193                let (revision, snapshot) = root_semantics
194                    .diagnostics_cross_3
195                    .snapshot(self.task())
196                    .into_module_result(id)?;
197
198                Ok(ModuleDiagnostics {
199                    id,
200                    issues: snapshot.issues.clone(),
201                    depth,
202                    revision,
203                })
204            }
205
206            _ => Ok(ModuleDiagnostics {
207                id,
208                issues: Shared::default(),
209                depth,
210                revision: 0,
211            }),
212        }
213    }
214
215    /// Looks up syntax constructions within the specified `span` (source code
216    /// range) based on the `options` filter.
217    ///
218    /// This function allows you to find specific source code constructions
219    /// (called "symbols") and then explore their syntax and semantics.
220    ///
221    /// For example, you can find a variable reference in the source code and
222    /// then determine where this variable was introduced.
223    ///
224    /// The `span` argument specifies the source code range for the symbol
225    /// lookup. You can use an absolute Unicode character range like `10..20`,
226    /// a [line-column](lady_deirdre::lexis::Position) range like
227    /// `Position::new(10, 3)..Position::new(12, 4)`, or a [ScriptOrigin]
228    /// instance.
229    ///
230    /// The `options` argument specifies the lookup filter. The
231    /// [LookupOptions::default] implementation searches for all kinds of
232    /// language constructions within the specified span, but you can
233    /// restrict the lookup to a particular set of symbol types.
234    ///
235    /// The function returns a [ModuleError::Cursor] error if the provided
236    /// `span` is not [valid](ToSpan::is_valid_span) for this module.
237    fn symbols(
238        &self,
239        span: impl ToSpan,
240        options: LookupOptions,
241    ) -> ModuleResult<Vec<ModuleSymbol>> {
242        let doc_read = self.read_doc();
243
244        let span = {
245            let doc_read = self.read_doc();
246
247            match span.to_site_span(doc_read.deref()) {
248                Some(span) => span,
249                None => return Err(ModuleError::Cursor(self.id())),
250            }
251        };
252
253        Ok(SymbolsLookup::lookup(doc_read.deref(), span, options))
254    }
255
256    /// Returns a range of the source code without the header and footer
257    /// comments.
258    ///
259    /// ```text
260    /// // header comment
261    ///
262    /// <content range start>let x = 10;
263    /// let y = 20;<content range end>
264    ///
265    /// // footer comment
266    /// ```
267    ///
268    /// This range is useful, for example, to determine where to place a new
269    /// global `use foo;` import statement in the source code.
270    ///
271    /// Note that the returned [ScriptOrigin] can be converted into an absolute
272    /// range of Unicode characters using the
273    /// [to_site_span](ToSpan::to_site_span) function:
274    /// `script_origin.to_site_span(&module_guard.text())`.
275    fn content_origin(&self) -> ScriptOrigin {
276        let doc_read = self.read_doc();
277
278        let ScriptNode::Root { statements, .. } = doc_read.root() else {
279            system_panic!("Incorrect root variant.");
280        };
281
282        let (Some(first), Some(last)) = (statements.first(), statements.last()) else {
283            return ScriptOrigin::eoi(self.id());
284        };
285
286        let mut start = first.script_origin(doc_read.deref(), SpanBounds::Header);
287        let end = last.script_origin(doc_read.deref(), SpanBounds::Footer);
288
289        start.union(&end);
290
291        start
292    }
293
294    /// Compiles the source code into the Ad Astra assembly, making it available
295    /// for execution. To execute the resulting ScriptFn object, use the
296    /// [ScriptFn::run] function.
297    ///
298    /// Source code compilation is typically a fast and robust process.
299    /// In general, the compiler can compile any source code text, even if it
300    /// contains [diagnostic](Self::diagnostics) errors and warnings. However,
301    /// if the code has diagnostic errors, the correctness of the resulting
302    /// ScriptFn execution flow is not guaranteed. Running such a ScriptFn
303    /// object does not result in undefined behavior, as Ad Astra's virtual
304    /// machine fully controls the assembly execution. In the worst case,
305    /// running such an assembly will result in a
306    /// [RuntimeError](crate::runtime::RuntimeError).
307    ///
308    /// The compiler attempts to compile script code with diagnostic errors in a
309    /// way that best matches the author's original intentions. However, it is
310    /// recommended to avoid running ScriptFn objects in production that have
311    /// been compiled from script modules with diagnostic errors.
312    fn compile(&self) -> ModuleResult<ScriptFn> {
313        let task = self.task();
314        let doc_read = self.read_doc();
315
316        ScriptFn::compile(task, doc_read.deref(), &doc_read.root_node_ref())
317            .into_module_result(self.id())
318    }
319}
320
321pub trait ModuleReadSealed<H: TaskHandle>: Identifiable {
322    type Task: SemanticAccess<ScriptNode, H, RandomState>;
323
324    fn task(&self) -> &Self::Task;
325
326    #[track_caller]
327    #[inline(always)]
328    fn read_doc(&self) -> DocumentReadGuard<ScriptNode, RandomState> {
329        let id = self.id();
330
331        match self.task().read_doc(id) {
332            Ok(doc_read) => doc_read,
333            Err(error) => system_panic!("Analysis internal error. {error}",),
334        }
335    }
336}