ad_astra/analysis/module.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::fmt::{Debug, Display, Formatter};
36
37use ahash::RandomState;
38use lady_deirdre::{
39 analysis::{Analyzer, AnalyzerConfig, MutationAccess, TaskHandle, TaskPriority, TriggerHandle},
40 arena::{Id, Identifiable},
41};
42
43use crate::{
44 analysis::{ModuleReadGuard, ModuleResult, ModuleResultEx, ModuleWriteGuard},
45 format::format_script_path,
46 report::system_panic,
47 runtime::PackageMeta,
48 syntax::ScriptNode,
49};
50
51/// An in-memory representation of the Ad Astra script module.
52///
53/// This object owns the script's source code text, its syntax and semantics,
54/// and is responsible for keeping this data in sync with source code edits,
55/// ensuring the up-to-date semantics are available for query.
56///
57/// To execute the script's source code, you need to load it into the
58/// ScriptModule object, compile it, and then run the compiled assembly.
59///
60/// ## Creation
61///
62/// To create a ScriptModule, you can load the source code text, for example,
63/// from disk, and then pass it into the ScriptModule constructor:
64/// [ScriptModule::new].
65///
66/// The constructor requires an additional parameter, which is a package
67/// metadata object. The module will be analyzed under the Rust symbols exported
68/// into this package object. For more details, see the
69/// [ScriptPackage](crate::runtime::ScriptPackage) documentation.
70///
71/// ```rust
72/// # use ad_astra::{
73/// # analysis::ScriptModule, export, lady_deirdre::analysis::TriggerHandle,
74/// # runtime::ScriptPackage,
75/// # };
76/// #
77/// #[export(package)]
78/// #[derive(Default)]
79/// struct Package;
80///
81/// let _module = ScriptModule::<TriggerHandle>::new(
82/// Package::meta(),
83/// "let foo = 10;",
84/// );
85/// ```
86///
87/// ## Access
88///
89/// The ScriptModule is specifically designed for use in multi-threaded
90/// environments. Although multi-threading is not a strict requirement, and
91/// the ScriptModule can also be used in single-threaded applications, its
92/// access API follows the read-write lock design pattern to address
93/// concurrent access operations.
94///
95/// You access the ScriptModule's content using read and write access guards,
96/// similar to [RwLock](std::sync::RwLock). The
97/// [read](ScriptModule::read) and [write](ScriptModule::write) functions
98/// provide read and write access guards, respectively. Both functions may block
99/// if the ScriptModule is currently locked for the opposite type of access,
100/// though non-blocking "try_" variants are available. Like RwLock, you can
101/// have multiple read guards simultaneously, but at most one write guard.
102///
103/// ```rust
104/// # use ad_astra::{
105/// # analysis::{ModuleRead, ScriptModule},
106/// # export,
107/// # lady_deirdre::analysis::TriggerHandle,
108/// # runtime::ScriptPackage,
109/// # };
110/// #
111/// # #[export(package)]
112/// # #[derive(Default)]
113/// # struct Package;
114/// #
115/// // Module creation
116/// let module = ScriptModule::new(Package::meta(), "let foo = 10;");
117///
118/// let handle = TriggerHandle::new();
119/// let module_read = module.read(&handle, 1).unwrap(); // Acquiring read guard.
120///
121/// println!("{}", module_read.text()); // Prints module source code.
122/// ```
123///
124/// ## Available Operations
125///
126/// The [ModuleReadGuard] object, created by the [read](ScriptModule::read)
127/// function, implements the [ModuleRead](crate::analysis::ModuleRead) trait,
128/// which provides the following operations:
129///
130/// - Reading the source code text via the
131/// [text](crate::analysis::ModuleRead::text) function.
132/// - Requesting source code diagnostics (errors and warnings) via the
133/// [diagnostics](crate::analysis::ModuleRead::diagnostics) function.
134/// - Querying for semantic metadata about specific syntax constructs within
135/// specified source code ranges via the
136/// [symbols](crate::analysis::ModuleRead::symbols) function.
137/// - Compiling the module into Ad Astra assembly for execution via the
138/// [compile](crate::analysis::ModuleRead::compile) function.
139///
140/// The [ModuleWriteGuard] object, created by the [write](ScriptModule::write)
141/// function, represents exclusive access to the ScriptModule content. This
142/// object implements both the ModuleRead and
143/// [ModuleWrite](crate::analysis::ModuleWrite) traits. Through the ModuleRead
144/// trait, you gain access to the operations listed above, and through the
145/// ModuleWrite trait, you can perform content mutation operations:
146///
147/// - Editing the source code text within a specified range via the
148/// [edit](crate::analysis::ModuleWrite::edit) function.
149/// - Probing the source code for code-completion candidates via the
150/// [completions](crate::analysis::ModuleWrite::completions) function. Even
151/// though this function does not ultimately change the source code text, it
152/// requires write access to probe the code through temporary mutation.
153///
154/// ## Multi-Threaded Design
155///
156/// A key difference from RwLock is that the ScriptModule's access
157/// guards can be gracefully interrupted.
158///
159/// Both [read](ScriptModule::read) and [write](ScriptModule::write) access
160/// functions (including their "try_" variants) require two additional
161/// parameters: an access priority number and a handle object.
162///
163/// The handle object allows you to revoke previously granted read/write access
164/// from another thread. The priority number indicates the priority of the task
165/// you intend to perform with the access guard object.
166///
167/// For example, if several working threads are currently reading the
168/// ScriptModule with one priority number, and another working thread
169/// simultaneously attempts to acquire write access with a higher priority
170/// number, the ScriptModule automatically revokes all read access grants to
171/// prioritize the write access.
172///
173/// When the ScriptModule revokes an access grant, all guard access operations
174/// will start yielding an
175/// [Interrupted](crate::analysis::ModuleError::Interrupted) error. In this
176/// case, the thread owning the guard should drop the guard object as soon as
177/// possible to allow another working thread to proceed. The former thread can
178/// later acquire a new access guard to continue its work.
179///
180/// ```rust
181/// # use ad_astra::{
182/// # analysis::{ModuleError, ModuleRead, ScriptModule},
183/// # export,
184/// # lady_deirdre::analysis::{TaskHandle, TriggerHandle},
185/// # runtime::ScriptPackage,
186/// # };
187/// #
188/// # #[export(package)]
189/// # #[derive(Default)]
190/// # struct Package;
191/// #
192/// let module = ScriptModule::new(Package::meta(), "let foo = 10;");
193///
194/// let handle = TriggerHandle::new();
195/// let module_read = module.read(&handle, 1).unwrap(); // Acquiring read access.
196///
197/// // Revoking access manually.
198/// // In a multi-threaded environment, you can clone and move this `handle`
199/// // object into another working thread and trigger it there instead.
200/// handle.trigger();
201///
202/// // Since the read access has been revoked, the diagnostics request function
203/// // returns an Interrupted error.
204/// assert!(matches!(
205/// module_read.diagnostics(2),
206/// Err(ModuleError::Interrupted(_)),
207/// ));
208/// ```
209///
210/// Although Ad Astra does not have a built-in worker manager and does not spawn
211/// any threads, the above mechanism helps you organize highly concurrent
212/// multi-threaded analysis tools with task priorities.
213///
214/// Note that analysis read operations (such as
215/// [diagnostics](crate::analysis::ModuleRead::diagnostics) or
216/// [symbols](crate::analysis::ModuleRead::symbols)) typically don't block each
217/// other when requested from independent threads. Ad Astra's semantic analyzer
218/// can infer module semantics concurrently.
219///
220/// ## Incremental Analysis
221///
222/// When you [edit](crate::analysis::ModuleWrite::edit) the source code of the
223/// ScriptModule, the underlying algorithm does not reparse the entire module's
224/// syntax. Instead, it typically reparses only a small fragment that includes
225/// the edited text and updates the existing in-memory data structures. This
226/// technique, known as incremental reparsing, allows for quick updates to
227/// script modules with every keystroke, even when the source code text is
228/// large.
229///
230/// Additionally, semantic analysis is demand-driven. The ScriptModule does not
231/// compute the script's semantics until specific semantic facts are queried.
232/// When these facts are queried, the underlying algorithm attempts to compute
233/// (or update previously computed) the smallest subset of the inner semantic
234/// representation required to fulfill the request. Thus, semantic analysis
235/// is also incremental and usually localized to the specific query.
236///
237/// ## Identification
238///
239/// Each instance of the ScriptModule has a globally unique associated
240/// identifier ([Id]). This Id object is Copy, Eq, Ord, and Hash, and is unique
241/// per ScriptModule instance within the current process.
242///
243/// Most API objects related to script modules also expose their script module
244/// ids. These identifiers can be retrieved using the [Identifiable::id]
245/// function and compared for equality.
246///
247/// Additionally, in multi-script projects, you can use the identifier as a key
248/// type in a hash map to store multiple ScriptModule instances within a single
249/// hash map.
250///
251/// ## Naming
252///
253/// The API allows you to assign a potentially non-unique string name to a
254/// ScriptModule instance using the [ScriptModule::rename] function. For
255/// example, if you load a script from disk, you might consider assigning
256/// the file name to the ScriptModule object as a module name.
257///
258/// API functions that print a module's content to the terminal will use the
259/// assigned name of the ScriptModule as a content header, which helps
260/// simplify script identification.
261pub struct ScriptModule<H: TaskHandle = TriggerHandle> {
262 id: Id,
263 package: &'static PackageMeta,
264 analyzer: Analyzer<ScriptNode, H, RandomState>,
265}
266
267impl<H: TaskHandle> Drop for ScriptModule<H> {
268 fn drop(&mut self) {
269 // Safety: Module was attached during creation.
270 unsafe { self.package.detach_module(self.id) }
271 }
272}
273
274impl<H: TaskHandle> Debug for ScriptModule<H> {
275 #[inline(always)]
276 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
277 formatter.write_fmt(format_args!(
278 "ScriptModule({})",
279 format_script_path(self.id, Some(self.package))
280 ))
281 }
282}
283
284impl<H: TaskHandle> Display for ScriptModule<H> {
285 #[inline(always)]
286 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
287 formatter.write_str(&format_script_path(self.id, Some(self.package)))
288 }
289}
290
291impl<H: TaskHandle> PartialEq for ScriptModule<H> {
292 #[inline(always)]
293 fn eq(&self, other: &Self) -> bool {
294 self.id.eq(&other.id)
295 }
296}
297
298impl<H: TaskHandle> Identifiable for ScriptModule<H> {
299 #[inline(always)]
300 fn id(&self) -> Id {
301 self.id
302 }
303}
304
305impl<H: TaskHandle> ScriptModule<H> {
306 /// Constructs a ScriptObject.
307 ///
308 /// The `package` argument specifies the package under which the source code
309 /// will be analyzed (see [ScriptPackage](crate::runtime::ScriptPackage) for
310 /// details).
311 ///
312 /// The `text` argument is the source code of the script.
313 pub fn new(package: &'static PackageMeta, text: impl AsRef<str>) -> Self {
314 let mut config = AnalyzerConfig::default();
315
316 config.single_document = true;
317
318 let analyzer = Analyzer::new(config);
319
320 let id = {
321 let handle = H::default();
322
323 let mut task = match analyzer.mutate(&handle, 0) {
324 Ok(task) => task,
325 Err(error) => system_panic!("Script creation failure. {error}",),
326 };
327
328 task.add_mutable_doc(text)
329 };
330
331 // Safety: Ids are globally unique.
332 unsafe { package.attach_module(id) };
333
334 Self {
335 id,
336 package,
337 analyzer,
338 }
339 }
340
341 /// Returns the metadata object of the script package under which this
342 /// script module is being analyzed.
343 ///
344 /// This value is equal to the one provided to the [constructor](Self::new)
345 /// function.
346 ///
347 /// See [ScriptPackage](crate::runtime::ScriptPackage) for details.
348 #[inline(always)]
349 pub fn package(&self) -> &'static PackageMeta {
350 self.package
351 }
352
353 /// Sets the user-facing string name of the script module.
354 ///
355 /// This name will be used by the crate API as a header for script snippets
356 /// when they are printed to the terminal. For example, if you print module
357 /// diagnostics using the
358 /// [ModuleDiagnostics::highlight](crate::analysis::ModuleDiagnostics::highlight)
359 /// function, the snippet printer will use the name specified by this
360 /// function.
361 ///
362 /// For instance, you can use the file name as the module name if the
363 /// script's source code was loaded from disk.
364 ///
365 /// Unlike the module's [Id], which is globally unique per ScriptModule
366 /// instance, the string name is not required to be unique (although it is
367 /// generally preferable).
368 ///
369 /// To get a copy of the name set previously, use the [Id::name] function:
370 /// `module.id().name()`.
371 ///
372 /// To unset the name, you can supply an empty string to this function.
373 /// By default, script modules do not have names (their names are empty
374 /// strings).
375 #[inline(always)]
376 pub fn rename(&self, name: impl AsRef<str>) {
377 self.id.set_name(String::from(name.as_ref()))
378 }
379
380 /// Requests access for [read operations](ScriptModule#available-operations).
381 ///
382 /// This function may block the current thread if read access cannot be
383 /// granted instantly. It may return an
384 /// [Interrupted](crate::analysis::ModuleError::Interrupted) error if the
385 /// read operation cannot be granted, for example, if another thread
386 /// simultaneously attempts to acquire write access with a higher priority.
387 ///
388 /// The `handle` argument specifies a reference to the handle object
389 /// through which granted access can be manually revoked from another
390 /// thread.
391 ///
392 /// The `priority` argument specifies the grant priority. If there are
393 /// conflicting grants with a lower priority, they will be revoked.
394 #[inline(always)]
395 pub fn read<'a>(
396 &'a self,
397 handle: &'a H,
398 priority: TaskPriority,
399 ) -> ModuleResult<ModuleReadGuard<H>> {
400 let task = self
401 .analyzer
402 .analyze(handle, priority)
403 .into_module_result(self.id)?;
404
405 Ok(ModuleReadGuard {
406 id: self.id,
407 package: self.package,
408 task,
409 })
410 }
411
412 /// A non-blocking alternative to the [read](Self::read) function. If read
413 /// access cannot be granted instantly, this function will not block the
414 /// current thread. Instead, it will immediately return an
415 /// [Interrupted](crate::analysis::ModuleError::Interrupted) error.
416 #[inline(always)]
417 pub fn try_read<'a>(
418 &'a self,
419 handle: &'a H,
420 priority: TaskPriority,
421 ) -> ModuleResult<ModuleReadGuard<H>> {
422 let task = self
423 .analyzer
424 .try_analyze(handle, priority)
425 .into_module_result(self.id)?;
426
427 Ok(ModuleReadGuard {
428 id: self.id,
429 package: self.package,
430 task,
431 })
432 }
433
434 /// Requests access for [write operations](ScriptModule#available-operations).
435 ///
436 /// This function may block the current thread if write access cannot be
437 /// granted instantly. It may return an
438 /// [Interrupted](crate::analysis::ModuleError::Interrupted) error if the
439 /// write operation cannot be granted, for example, if another thread
440 /// simultaneously attempts to acquire read or write access with a higher
441 /// priority.
442 ///
443 /// The `handle` argument specifies a reference to the handle object
444 /// through which granted access can be manually revoked from another
445 /// thread.
446 ///
447 /// The `priority` argument specifies the grant priority. If there are
448 /// conflicting grants with a lower priority, they will be revoked.
449 #[inline(always)]
450 pub fn write<'a>(
451 &'a self,
452 handle: &'a H,
453 priority: TaskPriority,
454 ) -> ModuleResult<ModuleWriteGuard<H>> {
455 let task = self
456 .analyzer
457 .exclusive(handle, priority)
458 .into_module_result(self.id)?;
459
460 Ok(ModuleWriteGuard {
461 id: self.id,
462 package: self.package,
463 task,
464 })
465 }
466
467 /// A non-blocking alternative to the [write](Self::write) function. If
468 /// write access cannot be granted instantly, this function will not block
469 /// the current thread. Instead, it will immediately return an
470 /// [Interrupted](crate::analysis::ModuleError::Interrupted) error.
471 #[inline(always)]
472 pub fn try_write<'a>(
473 &'a self,
474 handle: &'a H,
475 priority: TaskPriority,
476 ) -> ModuleResult<ModuleWriteGuard<H>> {
477 let task = self
478 .analyzer
479 .try_exclusive(handle, priority)
480 .into_module_result(self.id)?;
481
482 Ok(ModuleWriteGuard {
483 id: self.id,
484 package: self.package,
485 task,
486 })
487 }
488
489 /// Reverts the [deny_access](Self::deny_access) action back to its default
490 /// state, enabling read/write operation requests.
491 #[inline(always)]
492 pub fn allow_access(&self) {
493 self.analyzer.set_access_level(0);
494 }
495
496 /// Immediately revokes all previously granted read/write access guards and
497 /// prevents any new incoming read/write access requests.
498 ///
499 /// This function is useful when you want to gracefully shut down a
500 /// continuous compilation/analysis process.
501 ///
502 /// You can revert this action by calling the
503 /// [allow_access](Self::allow_access) function to enable access requests
504 /// again.
505 #[inline(always)]
506 pub fn deny_access(&self) {
507 self.analyzer.set_access_level(TaskPriority::MAX);
508 }
509
510 /// Returns true if the ScriptModule allows read/write access to its
511 /// content.
512 ///
513 /// By default, the ScriptModule allows read and write access, but this can
514 /// be prevented using the [deny_access](Self::deny_access) function. In
515 /// such a case, is_access_allowed would return false.
516 #[inline(always)]
517 pub fn is_access_allowed(&self) -> bool {
518 self.analyzer.get_access_level() < TaskPriority::MAX
519 }
520}