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}