Skip to main content

bloq_compile/
cache.rs

1//! Shared template, module-summary, definition, and compiled-artifact caches.
2//!
3//! Template keys contain block signatures. Config-sharded caches keep physical
4//! targets separate; logical summaries are target-independent. Immutable
5//! templates and artifacts use `Arc`, so contexts may reuse them concurrently.
6
7use std::num::NonZeroUsize;
8use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
9
10use bloq_graph::{BlockGraph, ModuleCertificationError, ModuleCertificationLimits, ModuleSummary};
11
12use crate::block::{LoweringTemplate, LoweringTemplateId, LoweringTemplatePool};
13use crate::compile::{Signature, TemplateRef};
14use crate::config::CompileConfig;
15use crate::error::CompileError;
16
17/// A compile cache shareable by several [`CompileContext`](crate::CompileContext)s, including across
18/// threads.
19///
20/// Clones share one cache. Hand it to
21/// [`CompileContext::with_shared_cache`](crate::CompileContext::with_shared_cache)
22/// to build contexts that pool their compiled templates:
23///
24/// ```
25/// use bloq_compile::{CompileConfig, CompileContext, SharedCompileCache};
26/// use bloq_graph::GalleryItem;
27///
28/// let cache = SharedCompileCache::new();
29/// let config = CompileConfig::new(3);
30/// let first = CompileContext::with_shared_cache(config, &cache);
31/// let second = CompileContext::with_shared_cache(config, &cache);
32///
33/// first.compile(&GalleryItem::CNOT.build()).expect("CNOT compiles");
34/// // Reuses the templates the first context compiled.
35/// second.compile(&GalleryItem::CNOT.build()).expect("CNOT compiles");
36/// ```
37///
38/// Sharing never changes what a compilation produces: program-local template
39/// ids are assigned per output [`Bloq`](crate::Bloq) in lowering order, so a
40/// warm cache and a cold one emit byte-identical programs.
41#[derive(Debug, Clone, Default)]
42pub struct SharedCompileCache {
43    shards: Arc<Mutex<crate::FxMap<CompileConfig, Arc<TemplateCacheShard>>>>,
44    summaries: Arc<ModuleSummaryCache>,
45}
46
47impl SharedCompileCache {
48    /// Creates an empty cache.
49    #[must_use]
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// The shard backing `config`, created on first use.
55    pub(crate) fn shard(&self, config: CompileConfig) -> Arc<TemplateCacheShard> {
56        Arc::clone(
57            lock(&self.shards)
58                .entry(config)
59                .or_insert_with(|| Arc::new(TemplateCacheShard::default())),
60        )
61    }
62
63    pub(crate) fn summary_cache(&self) -> Arc<ModuleSummaryCache> {
64        Arc::clone(&self.summaries)
65    }
66
67    /// Clears module artifacts, definition objects, and logical summaries,
68    /// then detaches every template shard from this cache.
69    ///
70    /// The cache is append-only — nothing is ever evicted — so it grows with
71    /// every distinct config, signature, and block graph it sees.
72    ///
73    /// Existing contexts retain their immutable template pools and padding
74    /// caches, so clearing never invalidates an in-flight compilation. Their
75    /// object maps are cleared before the shards are detached.
76    pub fn clear(&self) {
77        let mut shards = lock(&self.shards);
78        for shard in shards.values() {
79            shard.clear_objects();
80        }
81        shards.clear();
82        self.summaries.clear();
83    }
84}
85
86#[derive(Debug, Default)]
87pub(crate) struct ModuleSummaryCache {
88    // ponytail: keep collision-free text keys; hash only if key memory profiles hot.
89    entries: Mutex<crate::FxMap<(String, ModuleCertificationLimits), Arc<ModuleSummary>>>,
90}
91
92impl ModuleSummaryCache {
93    pub(crate) fn get_or_summarize(
94        &self,
95        program: &BlockGraph,
96        limits: ModuleCertificationLimits,
97        jobs: Option<NonZeroUsize>,
98    ) -> Result<Arc<ModuleSummary>, ModuleCertificationError> {
99        if program.has_module_structure() {
100            program.validate_with_limits(limits).map_err(|error| {
101                ModuleCertificationError::Graph {
102                    module: program.name.clone(),
103                    source: bloq_graph::BlockGraphError::ModuleSource(Arc::new(error)),
104                }
105            })?;
106        } else {
107            program
108                .validate_resource_limits(limits)
109                .and_then(|()| program.validate_source())
110                .map_err(|source| ModuleCertificationError::Graph {
111                    module: program.name.clone(),
112                    source,
113                })?;
114        }
115        let key = (program.to_blog_text(), limits);
116        if let Some(summary) = lock(&self.entries).get(&key) {
117            return Ok(Arc::clone(summary));
118        }
119        let summary = match jobs {
120            Some(jobs) => program.summarize_root_with_jobs(limits, jobs),
121            None => program.summarize_root(limits),
122        }?;
123        Ok(Arc::clone(
124            lock(&self.entries).entry(key).or_insert(summary),
125        ))
126    }
127
128    fn clear(&self) {
129        lock(&self.entries).clear();
130    }
131}
132
133/// Templates and artifacts for one [`CompileConfig`].
134///
135/// The signature map and pool share one mutex: a published reference must resolve
136/// every template it names. Compilation happens outside that lock. Padding has
137/// its own cache because it is prepared in a separate pass.
138#[derive(Debug, Default)]
139pub(crate) struct TemplateCacheShard {
140    templates: Mutex<ShardTemplates>,
141    pub(crate) padding: crate::padding::PaddingTemplateCache,
142    module_objects: ObjectCache<(bool, String), crate::CompileArtifacts>,
143    definition_objects: ObjectCache<
144        crate::compile::DefinitionObjectCacheKey,
145        crate::compile::PreparedDefinitionObject,
146    >,
147}
148
149/// Append-only immutable objects. Racing inserts retain the first object.
150struct ObjectCache<K, V> {
151    entries: Mutex<crate::FxMap<K, Arc<V>>>,
152}
153
154// Deriving Default would needlessly require K: Default and V: Default.
155impl<K, V> Default for ObjectCache<K, V> {
156    fn default() -> Self {
157        Self {
158            entries: Mutex::default(),
159        }
160    }
161}
162
163impl<K: std::fmt::Debug, V> std::fmt::Debug for ObjectCache<K, V> {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.debug_struct("ObjectCache")
166            .field("entries", &lock(&self.entries).len())
167            .finish()
168    }
169}
170
171impl<K: Eq + std::hash::Hash, V> ObjectCache<K, V> {
172    fn get<Q>(&self, key: &Q) -> Option<Arc<V>>
173    where
174        K: std::borrow::Borrow<Q>,
175        Q: Eq + std::hash::Hash + ?Sized,
176    {
177        lock(&self.entries).get(key).cloned()
178    }
179
180    fn insert(&self, key: K, object: Arc<V>) -> Arc<V> {
181        Arc::clone(lock(&self.entries).entry(key).or_insert(object))
182    }
183
184    fn clear(&self) {
185        lock(&self.entries).clear();
186    }
187}
188
189impl TemplateCacheShard {
190    fn clear_objects(&self) {
191        self.module_objects.clear();
192        self.definition_objects.clear();
193    }
194
195    pub(crate) fn module_object(
196        &self,
197        key: &(bool, String),
198    ) -> Option<Arc<crate::CompileArtifacts>> {
199        self.module_objects.get(key)
200    }
201
202    pub(crate) fn insert_module_object(
203        &self,
204        key: (bool, String),
205        object: Arc<crate::CompileArtifacts>,
206    ) -> Arc<crate::CompileArtifacts> {
207        self.module_objects.insert(key, object)
208    }
209
210    pub(crate) fn insert_definition_object(
211        &self,
212        key: crate::compile::DefinitionObjectCacheKey,
213        object: Arc<crate::compile::PreparedDefinitionObject>,
214    ) -> Arc<crate::compile::PreparedDefinitionObject> {
215        self.definition_objects.insert(key, object)
216    }
217
218    pub(crate) fn definition_object(
219        &self,
220        key: &crate::compile::DefinitionObjectCacheKey,
221    ) -> Option<Arc<crate::compile::PreparedDefinitionObject>> {
222        self.definition_objects.get(key)
223    }
224}
225
226#[derive(Debug, Default)]
227struct ShardTemplates {
228    sig_to_template: crate::FxMap<Signature, TemplateRef>,
229    pool: LoweringTemplatePool,
230}
231
232/// Freshly compiled templates on their way into the pool: the shape of a
233/// [`TemplateRef`] before pool ids exist. Compilation produces one of these
234/// outside the lock; the shard assigns ids when it inserts.
235pub(crate) enum CompiledTemplates {
236    Fixed(Arc<LoweringTemplate>),
237    Selective {
238        when_true: Arc<LoweringTemplate>,
239        when_false: Arc<LoweringTemplate>,
240    },
241    T {
242        cultivation: Arc<LoweringTemplate>,
243        escape: Arc<LoweringTemplate>,
244    },
245}
246
247impl TemplateCacheShard {
248    /// The reference for `key`, compiling it with `compile` on a miss.
249    ///
250    /// Compile misses outside the lock. Racing inserts adopt the first reference,
251    /// preserving pool ids for existing readers.
252    pub(crate) fn get_or_compile(
253        &self,
254        key: Signature,
255        compile: impl FnOnce() -> Result<CompiledTemplates, CompileError>,
256    ) -> Result<TemplateRef, CompileError> {
257        if let Some(&template) = lock(&self.templates).sig_to_template.get(&key) {
258            return Ok(template);
259        }
260        let compiled = compile()?;
261        let mut templates = lock(&self.templates);
262        if let Some(&template) = templates.sig_to_template.get(&key) {
263            return Ok(template);
264        }
265        let pool = &mut templates.pool;
266        let template = match compiled {
267            CompiledTemplates::Fixed(template) => TemplateRef::Fixed(pool.insert(template)),
268            CompiledTemplates::Selective {
269                when_true,
270                when_false,
271            } => TemplateRef::Selective {
272                when_true: pool.insert(when_true),
273                when_false: pool.insert(when_false),
274            },
275            CompiledTemplates::T {
276                cultivation,
277                escape,
278            } => TemplateRef::T {
279                cultivation: pool.insert(cultivation),
280                escape: pool.insert(escape),
281            },
282        };
283        templates.sig_to_template.insert(key, template);
284        Ok(template)
285    }
286
287    /// [`get_or_compile`](Self::get_or_compile) for the keys only ever inserted
288    /// as a single fixed template: temporal realignments and spatial Hadamard
289    /// walls.
290    pub(crate) fn get_or_compile_fixed(
291        &self,
292        key: Signature,
293        compile: impl FnOnce() -> Result<Arc<LoweringTemplate>, CompileError>,
294    ) -> Result<LoweringTemplateId, CompileError> {
295        match self.get_or_compile(key, || compile().map(CompiledTemplates::Fixed))? {
296            TemplateRef::Fixed(id) => Ok(id),
297            TemplateRef::Selective { .. } | TemplateRef::T { .. } => {
298                unreachable!("realignment and wall keys are only ever inserted as fixed templates")
299            }
300        }
301    }
302
303    /// Snapshot the append-only pool without holding its lock during lowering.
304    ///
305    /// Take the snapshot after this compilation's last insertion so every held
306    /// id resolves. Later insertions cannot invalidate those entries.
307    pub(crate) fn pool_snapshot(&self) -> LoweringTemplatePool {
308        lock(&self.templates).pool.clone()
309    }
310
311    #[cfg(test)]
312    pub(crate) fn pool_len(&self) -> usize {
313        lock(&self.templates).pool.len()
314    }
315}
316
317/// Lock `mutex`, recovering from poisoning.
318///
319/// Keys are inserted last, so an interrupted insertion may leave an unreferenced
320/// template but never a key naming a missing template.
321pub(crate) fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
322    mutex.lock().unwrap_or_else(PoisonError::into_inner)
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::CompileContext;
329
330    #[test]
331    fn procedural_graph_summary_infers_open_port_interface() {
332        use bloq_graph::{Block, BlockKind, CubeKind, Direction, Pipe};
333        use glam::IVec3;
334        let mut graph = BlockGraph::new();
335        for (position, kind) in [
336            (-IVec3::Z, BlockKind::Port),
337            (IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)),
338            (IVec3::Z, BlockKind::Port),
339        ] {
340            graph.add_block(Block::new(position, kind));
341        }
342        graph.add_pipe(Pipe::new(-IVec3::Z, Direction::ZPLUS));
343        graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS));
344        let inferred = graph.clone().with_inferred_interface().unwrap();
345        let context = CompileContext::new(CompileConfig::default());
346        let limits = ModuleCertificationLimits::DEFAULT;
347        let implicit_summary = context.summarize(&graph, limits).unwrap();
348        assert_eq!(implicit_summary.quantum_ports().len(), 2);
349        let explicit_summary = context.summarize(&inferred, limits).unwrap();
350        assert!(Arc::ptr_eq(&implicit_summary, &explicit_summary));
351    }
352
353    #[test]
354    fn module_summaries_are_shared_across_configs_and_cleared() {
355        let program = BlockGraph::from_text(
356            r#"BLOG 1.0
357module main {
358  in q_in: data = 0
359  out q_out: data = 2
360  0: Port [0, 0, -1] <q_in>
361  1: ZXZ [0, 0, 0]
362  2: Port [0, 0, 1] <q_out>
363  [0, 0, -1] -> +Z
364  [0, 0, 0] -> +Z
365}
366"#,
367        )
368        .unwrap();
369        let cache = SharedCompileCache::new();
370        let first = CompileContext::with_shared_cache(CompileConfig::new(3), &cache);
371        let second = CompileContext::with_shared_cache(CompileConfig::new(5), &cache);
372        let limits = bloq_graph::ModuleCertificationLimits::UNLIMITED;
373        let a = first
374            .summarize_with_jobs(&program, limits, NonZeroUsize::new(2).unwrap())
375            .unwrap();
376        let b = second.summarize(&program, limits).unwrap();
377        assert!(Arc::ptr_eq(&a, &b));
378
379        cache.clear();
380        let c = second.summarize(&program, limits).unwrap();
381        assert!(!Arc::ptr_eq(&a, &c));
382    }
383}