Skip to main content

uqa_analysis/
resources.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bounded ownership of immutable compiled analyzer revisions.
8
9use std::sync::{Arc, OnceLock};
10
11use parking_lot::Mutex;
12
13use crate::{
14    cache::Cache, AnalysisResult, Analyzer, AnalyzerDescriptor, AnalyzerFingerprint,
15    AnalyzerLimits, CompiledAnalyzer, TokenLengthPolicy,
16};
17
18struct Inner {
19    limits: AnalyzerLimits,
20    cache: Mutex<Cache<AnalyzerFingerprint, CompiledAnalyzer>>,
21    #[cfg(feature = "nori")]
22    nori: crate::nori::NoriResources,
23}
24
25/// Retained descriptor sizes exclude compiled heap allocations and caller-owned handles.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct AnalyzerCacheStats {
28    pub analyzers: usize,
29    pub descriptor_bytes: usize,
30}
31
32/// Cloneable compilation owner; resolved revisions remain valid after cache eviction.
33///
34/// ```
35/// use uqa_analysis::{standard_analyzer, AnalyzerLimits, AnalyzerResources};
36/// let resources = AnalyzerResources::new(AnalyzerLimits::default());
37/// let compiled = resources.compile(&standard_analyzer("english"))?;
38/// let saved = compiled.descriptor().canonical_json();
39/// let restored = resources.restore_json(saved)?;
40/// assert!(std::sync::Arc::ptr_eq(&compiled, &restored));
41/// assert_eq!(restored.analyze("The cats and")?, ["cat"]);
42/// # Ok::<(), uqa_analysis::AnalysisError>(())
43/// ```
44#[derive(Clone)]
45pub struct AnalyzerResources(Arc<Inner>);
46
47impl Default for AnalyzerResources {
48    fn default() -> Self {
49        static RESOURCES: OnceLock<AnalyzerResources> = OnceLock::new();
50        RESOURCES
51            .get_or_init(|| Self::new(AnalyzerLimits::default()))
52            .clone()
53    }
54}
55
56impl AnalyzerResources {
57    /// Create an independent owner with fixed descriptor and retention limits.
58    pub fn new(limits: AnalyzerLimits) -> Self {
59        Self(Arc::new(Inner {
60            limits,
61            cache: Mutex::new(Cache::default()),
62            #[cfg(feature = "nori")]
63            nori: crate::nori::NoriResources::default(),
64        }))
65    }
66
67    /// Use explicit immutable Korean resources without introducing a fallback resolver.
68    #[cfg(feature = "nori")]
69    pub fn with_nori_resources(limits: AnalyzerLimits, nori: crate::nori::NoriResources) -> Self {
70        Self(Arc::new(Inner {
71            limits,
72            cache: Mutex::new(Cache::default()),
73            nori,
74        }))
75    }
76
77    #[cfg(feature = "nori")]
78    pub fn nori_resources(&self) -> &crate::nori::NoriResources {
79        &self.0.nori
80    }
81
82    pub fn limits(&self) -> AnalyzerLimits {
83        self.0.limits
84    }
85
86    pub fn cache_stats(&self) -> AnalyzerCacheStats {
87        let cache = self.0.cache.lock();
88        AnalyzerCacheStats {
89            analyzers: cache.len(),
90            descriptor_bytes: cache.weight(),
91        }
92    }
93
94    pub fn compile(&self, config: &Analyzer) -> AnalysisResult<Arc<CompiledAnalyzer>> {
95        let policy = if config.uses_korean_stages() {
96            TokenLengthPolicy::DiscountOverlaps
97        } else {
98            TokenLengthPolicy::EmittedTokens
99        };
100        self.compile_with_length_policy(config, policy)
101    }
102
103    pub fn compile_with_length_policy(
104        &self,
105        config: &Analyzer,
106        policy: TokenLengthPolicy,
107    ) -> AnalysisResult<Arc<CompiledAnalyzer>> {
108        // Mutable inputs resolve outside the cache lock, even when their previous revision is cached.
109        let resolved = AnalyzerDescriptor::resolve_inputs(
110            config,
111            policy,
112            self.0.limits,
113            #[cfg(feature = "nori")]
114            &self.0.nori,
115        )?;
116        self.publish(
117            resolved.descriptor,
118            #[cfg(feature = "nori")]
119            resolved.nori,
120        )
121    }
122
123    /// Compile verified resolved inputs without consulting mutable files or named definitions.
124    pub fn restore(
125        &self,
126        descriptor: Arc<AnalyzerDescriptor>,
127    ) -> AnalysisResult<Arc<CompiledAnalyzer>> {
128        descriptor.validate_limits(self.0.limits)?;
129        if let Some(compiled) = self.0.cache.lock().get(&descriptor.fingerprint()) {
130            return Ok(compiled);
131        }
132        #[cfg(feature = "nori")]
133        let nori = {
134            let mut config = descriptor.configuration()?;
135            crate::nori::pipeline::check_resolved(&config)?;
136            crate::nori::pipeline::ResolvedNoriPipeline::resolve(&mut config, &self.0.nori)?
137        };
138        self.publish(
139            descriptor,
140            #[cfg(feature = "nori")]
141            nori,
142        )
143    }
144
145    fn publish(
146        &self,
147        descriptor: Arc<AnalyzerDescriptor>,
148        #[cfg(feature = "nori")] nori: crate::nori::pipeline::ResolvedNoriPipeline,
149    ) -> AnalysisResult<Arc<CompiledAnalyzer>> {
150        descriptor.validate_limits(self.0.limits)?;
151        let fingerprint = descriptor.fingerprint();
152        let weight = descriptor.canonical_json().len();
153        let mut cache = self.0.cache.lock();
154        if let Some(compiled) = cache.get(&fingerprint) {
155            return Ok(compiled);
156        }
157        // Executable preparation receives resolved handles and cannot call external owners.
158        let compiled = Arc::new(CompiledAnalyzer::prepare(
159            descriptor,
160            #[cfg(feature = "nori")]
161            nori,
162        )?);
163        cache.insert(
164            fingerprint,
165            compiled.clone(),
166            weight,
167            self.0.limits.max_cached_analyzers,
168            self.0.limits.max_cached_descriptor_bytes,
169        );
170        Ok(compiled)
171    }
172
173    /// Validate the persisted fingerprint and runtime profiles before publishing a compiled handle.
174    pub fn restore_json(&self, json: &str) -> AnalysisResult<Arc<CompiledAnalyzer>> {
175        self.restore(AnalyzerDescriptor::from_json(json, self.0.limits)?)
176    }
177}