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 ®ISTRY
478 }
479}