ad_astra/runtime/
package.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::{
36    cmp::Ordering,
37    fmt::{Debug, Display, Formatter},
38    hash::{Hash, Hasher},
39    ops::Deref,
40    sync::{RwLock, RwLockReadGuard},
41};
42
43use ahash::{AHashMap, AHashSet, RandomState};
44use lady_deirdre::{
45    arena::Id,
46    sync::{Lazy, Table},
47};
48use semver::{Version, VersionReq};
49
50use crate::{
51    report::debug_unreachable,
52    runtime::{
53        Cell,
54        RustOrigin,
55        TypeMeta,
56        __intrinsics::{DeclarationGroup, PackageDeclaration},
57    },
58};
59
60/// A type that represents the Script Package of a crate.
61///
62/// This trait is automatically implemented on a struct type when you
63/// export it as a crate package.
64///
65/// Through the [ScriptPackage::meta] function, you gain access to the
66/// [PackageMeta] object. This can be used, for example, to instantiate new
67/// [script modules](crate::analysis::ScriptModule) that can be analyzed in
68/// accordance with the exported semantics of the crate or to run an LSP server.
69///
70/// ```
71/// use ad_astra::{export, runtime::ScriptPackage};
72///
73/// #[export(package)]
74/// #[derive(Default)]
75/// struct Package;
76///
77/// assert_eq!(Package::meta().name(), "ad-astra");
78/// ```
79pub trait ScriptPackage {
80    /// Returns a Rust source code location that points to where the package
81    /// type was declared.
82    ///
83    /// This is a shortcut for [PackageMeta::origin].
84    #[inline(always)]
85    fn origin() -> &'static RustOrigin {
86        Self::meta().origin()
87    }
88
89    /// Returns the name of the package's crate.
90    ///
91    /// This is a shortcut for [PackageMeta::name].
92    #[inline(always)]
93    fn name() -> &'static str {
94        Self::meta().name()
95    }
96
97    /// Returns the version of the package's crate.
98    ///
99    /// This is a shortcut for [PackageMeta::version].
100    #[inline(always)]
101    fn version() -> &'static str {
102        Self::meta().version()
103    }
104
105    /// Returns a reference to the full metadata object of the crate's package.
106    fn meta() -> &'static PackageMeta;
107}
108
109/// Metadata for the [ScriptPackage].
110///
111/// You cannot instantiate this object manually; it is created automatically
112/// by the Script Engine for each exported Script Package per crate. However,
113/// you can obtain a static reference to the PackageMeta in several ways.
114/// For instance, you can get it from the [ScriptPackage::meta] function of
115/// the exported package struct. You can also manually find the reference
116/// using the [PackageMeta::of] function.
117///
118/// ```
119/// use ad_astra::{
120///     export,
121///     runtime::{PackageMeta, ScriptPackage},
122/// };
123///
124/// #[export(package)]
125/// #[derive(Default)]
126/// struct Package;
127///
128/// let package_meta = Package::meta();
129///
130/// let same_package =
131///     PackageMeta::of(package_meta.name(), &format!("={}", package_meta.version())).unwrap();
132///
133/// assert_eq!(package_meta, same_package);
134/// ```
135///
136/// You can use this reference to instantiate
137/// [script modules](crate::analysis::ScriptModule) or to run the LSP server.
138///
139/// The alternative [Debug] implementation for the package lists all script
140/// modules currently associated with this Script Package. The alternative
141/// [Display] implementation prints the canonical name of the package's crate:
142/// `<package_name>@<package_version>`.
143pub struct PackageMeta {
144    origin: &'static RustOrigin,
145    declaration: PackageDeclaration,
146    modules: RwLock<AHashSet<Id>>,
147}
148
149impl PartialEq for PackageMeta {
150    #[inline]
151    fn eq(&self, other: &Self) -> bool {
152        if self.declaration.name.ne(other.declaration.name) {
153            return false;
154        }
155
156        if self.declaration.version.ne(other.declaration.version) {
157            return false;
158        }
159
160        true
161    }
162}
163
164impl Eq for PackageMeta {}
165
166impl Hash for PackageMeta {
167    fn hash<H: Hasher>(&self, state: &mut H) {
168        self.declaration.name.hash(state);
169        self.declaration.version.hash(state);
170    }
171}
172
173impl Ord for PackageMeta {
174    #[inline]
175    fn cmp(&self, other: &Self) -> Ordering {
176        match self.declaration.name.cmp(other.declaration.name) {
177            Ordering::Equal => self.declaration.version.cmp(other.declaration.version),
178            other => other,
179        }
180    }
181}
182
183impl PartialOrd for PackageMeta {
184    #[inline(always)]
185    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
186        Some(self.cmp(other))
187    }
188}
189
190impl Debug for PackageMeta {
191    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
192        let alternate = formatter.alternate();
193
194        let mut debug_struct = formatter.debug_struct("PackageMeta");
195
196        if alternate {
197            debug_struct.field("origin", self.origin());
198        }
199
200        debug_struct
201            .field("name", &self.name())
202            .field("version", &self.version());
203
204        if alternate {
205            if let Ok(modules) = self.modules.try_read() {
206                struct ListModules<'a> {
207                    modules: RwLockReadGuard<'a, AHashSet<Id>>,
208                }
209
210                impl<'a> Debug for ListModules<'a> {
211                    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
212                        let mut list = formatter.debug_list();
213
214                        for id in self.modules.iter() {
215                            let name = id.name();
216
217                            match name.is_empty() {
218                                true => list.entry(&format_args!("‹#{}›", id.into_inner())),
219                                false => list.entry(&format_args!("‹{}›", name)),
220                            };
221                        }
222
223                        list.finish()
224                    }
225                }
226
227                let print_modules = ListModules { modules };
228
229                debug_struct.field("modules", &print_modules);
230            }
231
232            let prototype = self.declaration.instance.ty().prototype();
233
234            debug_struct.field("prototype", prototype);
235        }
236
237        debug_struct.finish()
238    }
239}
240
241impl Display for PackageMeta {
242    #[inline]
243    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
244        formatter.write_str(self.name())?;
245
246        if formatter.alternate() {
247            formatter.write_fmt(format_args!("@{}", self.version()))?;
248        }
249
250        Ok(())
251    }
252}
253
254impl PackageMeta {
255    #[inline(always)]
256    fn new(origin: &'static RustOrigin, declaration: PackageDeclaration) -> Self {
257        Self {
258            origin,
259            declaration,
260            modules: RwLock::new(AHashSet::new()),
261        }
262    }
263
264    /// Looks up the `PackageMeta` by the package's crate `name` and `version`.
265    ///
266    /// The `name` should match the exact crate name specified in the crate's
267    /// `Cargo.toml` configuration.
268    ///
269    /// The `version` specifies the crate version requirement. There can be
270    /// multiple crates with the same name but different versions in the crate
271    /// dependency graph. The format of the `version` string is the same as used
272    /// in the `[dependencies]` section of `Cargo.toml`. For example, you can
273    /// specify the version as `3.0` to match the latest minor version, or use
274    /// the equality sign `=2.1.5` to match a specific version exactly.
275    ///
276    /// The function returns None if there are no crates with the specified
277    /// name and version requirements or if the crates do not have an exported
278    /// Script Package.
279    pub fn of(name: &str, version: &str) -> Option<&'static Self> {
280        let registry = PackageRegistry::get();
281
282        let version_set = registry.index.get(name)?;
283
284        let requirement = VersionReq::parse(version).ok()?;
285
286        let mut candidate: Option<(&Version, &PackageMeta)> = None;
287
288        for (version, variant) in version_set {
289            if !requirement.matches(version) {
290                continue;
291            }
292
293            match candidate {
294                Some((previous, _)) if previous > version => (),
295                _ => candidate = Some((version, variant)),
296            }
297        }
298
299        let (_, meta) = candidate?;
300
301        Some(meta)
302    }
303
304    #[inline(always)]
305    pub(crate) fn by_id(id: Id) -> Option<&'static Self> {
306        let registry = ModuleRegistry::get();
307
308        Some(*registry.index.get(&id)?)
309    }
310
311    /// Returns the Rust source code location that points to where the
312    /// package type was declared.
313    #[inline(always)]
314    pub fn origin(&self) -> &'static RustOrigin {
315        self.origin
316    }
317
318    /// Returns the name of the crate for this package, as specified in the
319    /// crate's `Cargo.toml`.
320    #[inline(always)]
321    pub fn name(&self) -> &'static str {
322        self.declaration.name
323    }
324
325    /// Returns the version of the crate for this package, as specified in the
326    /// crate's `Cargo.toml`.
327    #[inline(always)]
328    pub fn version(&self) -> &'static str {
329        self.declaration.version
330    }
331
332    /// Returns the documentation URL of the crate for this package, as
333    /// specified in the crate's `Cargo.toml`.
334    #[inline(always)]
335    pub fn doc(&self) -> Option<&'static str> {
336        self.declaration.doc
337    }
338
339    /// Returns the type metadata of the Rust struct that has been exported
340    /// as a [ScriptPackage].
341    #[inline(always)]
342    pub fn ty(&self) -> &'static TypeMeta {
343        self.declaration.instance.deref().ty()
344    }
345
346    /// Returns a smart pointer to the instance of the Rust struct that
347    /// represents the [ScriptPackage].
348    ///
349    /// The Script Engine automatically instantiates each package struct type
350    /// during initialization, using the [Default] constructor of the Rust
351    /// struct.
352    ///
353    /// Through this instance, you can access the exported fields and methods of
354    /// the struct. The crate's exported global functions and statics are also
355    /// available as [components](crate::runtime::Object::component) of this
356    /// type. Additionally, dependency crates (that have exported ScriptPackage)
357    /// become components of this instance.
358    ///
359    /// The script code can access this instance using the `crate` script
360    /// keyword or by referencing dependent crates
361    /// (`my_crate.dep_crate` or `crate.dep_crate`).
362    #[inline(always)]
363    pub fn instance(&self) -> Cell {
364        self.declaration.instance.deref().clone()
365    }
366
367    // Safety: `id` is not registered anywhere.
368    #[inline(always)]
369    pub(crate) unsafe fn attach_module(&'static self, id: Id) {
370        let mut modules = self
371            .modules
372            .write()
373            .unwrap_or_else(|poison| poison.into_inner());
374
375        if !modules.insert(id) {
376            // Safety: Upheld by the caller.
377            unsafe { debug_unreachable!("Duplicate module.") }
378        }
379
380        let registry = ModuleRegistry::get();
381
382        if registry.index.insert(id, self).is_some() {
383            // Safety: Upheld by the caller.
384            unsafe { debug_unreachable!("Duplicate module.") }
385        }
386    }
387
388    // Safety: `id` exists in this Package.
389    #[inline(always)]
390    pub(crate) unsafe fn detach_module(&'static self, id: Id) {
391        let mut modules = self
392            .modules
393            .write()
394            .unwrap_or_else(|poison| poison.into_inner());
395
396        if !modules.remove(&id) {
397            // Safety: Upheld by the caller.
398            unsafe { debug_unreachable!("Missing module.") }
399        }
400
401        let registry = ModuleRegistry::get();
402
403        if registry.index.remove(&id).is_none() {
404            // Safety: Upheld by the caller.
405            unsafe { debug_unreachable!("Missing module.") }
406        }
407    }
408}
409
410struct PackageRegistry {
411    index: AHashMap<&'static str, AHashMap<Version, PackageMeta>>,
412}
413
414impl PackageRegistry {
415    #[inline(always)]
416    fn get() -> &'static Self {
417        static REGISTRY: Lazy<PackageRegistry> = Lazy::new(|| {
418            let mut index = AHashMap::<&'static str, AHashMap<Version, PackageMeta>>::new();
419
420            for group in DeclarationGroup::enumerate() {
421                let origin = group.origin;
422
423                for declaration in &group.packages {
424                    let declaration = declaration();
425
426                    let version_set = index.entry(declaration.name).or_default();
427
428                    let version = match Version::parse(declaration.version) {
429                        Ok(version) => version,
430
431                        Err(error) => {
432                            let name = declaration.name;
433                            let version = &declaration.version;
434
435                            origin.blame(&format!(
436                                "Package {name}@{version} version parse error. {error}",
437                            ))
438                        }
439                    };
440
441                    if let Some(previous) = version_set.get(&version) {
442                        let name = declaration.name;
443                        let version = &declaration.version;
444                        let previous = previous.origin;
445
446                        origin.blame(&format!(
447                            "Package {name}@{version} already declared in {previous}.",
448                        ))
449                    }
450
451                    let meta = PackageMeta::new(origin, declaration);
452
453                    if let Some(_) = version_set.insert(version, meta) {
454                        // Safety: Uniqueness checked above.
455                        unsafe { debug_unreachable!("Duplicate package entry.") }
456                    }
457                }
458            }
459
460            PackageRegistry { index }
461        });
462
463        REGISTRY.deref()
464    }
465}
466
467struct ModuleRegistry {
468    index: Table<Id, &'static PackageMeta, RandomState>,
469}
470
471impl ModuleRegistry {
472    fn get() -> &'static Self {
473        static REGISTRY: Lazy<ModuleRegistry> = Lazy::new(|| ModuleRegistry {
474            index: Table::new(),
475        });
476
477        &REGISTRY
478    }
479}