Skip to main content

lattice_compilation/
parser_factory.rs

1//! CM.6b: the plugin-parser *factory* seam.
2//!
3//! Design: [`compilation-mode.md`](../../../docs/dev/architecture/compilation-mode.md)
4//! §5 (the parser registry as an extensibility seam).
5//!
6//! ## Why a factory and not a parser
7//!
8//! A [`CompilationParser`] carries pending multi-line state behind
9//! `&mut self`, and a run has **two** readers — stdout and stderr —
10//! each on its own thread with its own [`ParserRegistry`]. Those
11//! streams are independent: a header line on stderr must not prime a
12//! diagnostic that a stdout line then completes. So a parser cannot be
13//! shared between them, and for a WASM-backed parser it *could* not be
14//! anyway (it owns a `wasmtime::Store`).
15//!
16//! What the registry therefore holds is not a parser but the means to
17//! mint one: each reader calls [`CompilationParserFactories::create_all`]
18//! once at the top of its loop and owns what it gets back for the run.
19//!
20//! ## Lifetime
21//!
22//! Registration is copy-on-write RCU behind an `ArcSwap` — the same
23//! wait-free-read / rare-write idiom the picker registry uses. Reads
24//! happen once per run (not per line), writes only on plugin load and
25//! unload. Each run snapshots the handle once, so a plugin loaded
26//! mid-build joins the *next* build rather than half of this one.
27//!
28//! [`ParserRegistry`]: crate::ParserRegistry
29
30use std::sync::Arc;
31
32use crate::parser::CompilationParser;
33
34/// Mints a fresh [`CompilationParser`] for one pipe reader.
35///
36/// Implemented by `lattice-plugin-host` over a compiled `error-parser`
37/// component; the compilation crate never learns what a plugin is.
38pub trait CompilationParserFactory: Send + Sync + std::fmt::Debug {
39    /// The host-issued plugin id that contributed this factory.
40    /// Teardown removes by it — see
41    /// [`CompilationParserFactories::unregister_plugin`].
42    fn plugin_id(&self) -> u64;
43
44    /// Mint a parser for one reader.
45    ///
46    /// `None` means instantiation failed. The implementor logs it; the
47    /// run continues with whatever parsers it did get, because one bad
48    /// plugin must cost its own entries and not the build (the same
49    /// contract `WasmErrorParser`'s trap-poisoning follows).
50    fn create(&self) -> Option<Box<dyn CompilationParser>>;
51}
52
53/// The registered set of plugin parser factories, in registration
54/// order.
55///
56/// `Clone` so the handle below can RCU it: clone → mutate → store.
57/// Cloning is cheap — the factories are `Arc`-shared.
58#[derive(Default, Clone, Debug)]
59pub struct CompilationParserFactories {
60    factories: Vec<Arc<dyn CompilationParserFactory>>,
61}
62
63impl CompilationParserFactories {
64    /// An empty set.
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Append a factory. Order is preserved and becomes the order the
70    /// minted parsers see each line in.
71    pub fn register(&mut self, factory: Arc<dyn CompilationParserFactory>) {
72        self.factories.push(factory);
73    }
74
75    /// Drop every factory contributed by `plugin_id`, returning how
76    /// many were removed. Idempotent: a second call reports zero, which
77    /// is what the teardown contract requires of a double-unload.
78    pub fn unregister_plugin(&mut self, plugin_id: u64) -> usize {
79        let before = self.factories.len();
80        self.factories.retain(|f| f.plugin_id() != plugin_id);
81        before - self.factories.len()
82    }
83
84    /// Mint one parser per factory, skipping any that failed to
85    /// instantiate.
86    pub fn create_all(&self) -> Vec<Box<dyn CompilationParser>> {
87        self.factories.iter().filter_map(|f| f.create()).collect()
88    }
89
90    /// How many factories are registered.
91    pub fn len(&self) -> usize {
92        self.factories.len()
93    }
94
95    /// Whether no factory is registered — the overwhelmingly common
96    /// case, and the one a run checks to skip the snapshot entirely.
97    pub fn is_empty(&self) -> bool {
98        self.factories.is_empty()
99    }
100
101    /// A fresh empty [`CompilationParserFactoriesHandle`].
102    ///
103    /// Exists so consumers — the plugin host's teardown tests, the
104    /// loader's harness — do not each have to name `arc_swap` just to
105    /// build the handle this crate defines.
106    pub fn new_handle() -> CompilationParserFactoriesHandle {
107        Arc::new(arc_swap::ArcSwap::from_pointee(Self::new()))
108    }
109}
110
111/// The runtime-mutable handle, registered as a boot service under this
112/// exact alias (the `ServiceRegistry` Arc/TypeId convention).
113///
114/// `lattice-plugin-loader` reaches it via
115/// `service::<CompilationParserFactoriesHandle>()` and RCU-registers
116/// each loaded `error-parser` plugin's factory.
117pub type CompilationParserFactoriesHandle = Arc<arc_swap::ArcSwap<CompilationParserFactories>>;
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use lattice_protocol::error_list::{ErrorEntry, ErrorSeverity};
123    use std::sync::atomic::{AtomicUsize, Ordering};
124
125    /// A stand-in for a plugin factory: counts how many parsers it was
126    /// asked for, and each parser it mints counts its own feeds — so a
127    /// test can prove the two readers got *different* instances.
128    #[derive(Debug)]
129    struct CountingFactory {
130        plugin_id: u64,
131        created: Arc<AtomicUsize>,
132        fail: bool,
133    }
134
135    #[derive(Debug)]
136    struct CountingParser {
137        fed: usize,
138    }
139
140    impl CompilationParser for CountingParser {
141        fn feed(&mut self, line: &str) -> Vec<ErrorEntry> {
142            self.fed += 1;
143            if line == "BOOM" {
144                return vec![ErrorEntry {
145                    path: std::path::PathBuf::from("x.rs"),
146                    // The per-instance feed count, so two instances that
147                    // saw different numbers of lines are distinguishable.
148                    line: self.fed as u32,
149                    col: 0,
150                    severity: ErrorSeverity::Error,
151                    message: "boom".to_string(),
152                }];
153            }
154            Vec::new()
155        }
156    }
157
158    impl CompilationParserFactory for CountingFactory {
159        fn plugin_id(&self) -> u64 {
160            self.plugin_id
161        }
162        fn create(&self) -> Option<Box<dyn CompilationParser>> {
163            self.created.fetch_add(1, Ordering::SeqCst);
164            if self.fail {
165                return None;
166            }
167            Some(Box::new(CountingParser { fed: 0 }))
168        }
169    }
170
171    fn factory(plugin_id: u64, created: Arc<AtomicUsize>) -> Arc<dyn CompilationParserFactory> {
172        Arc::new(CountingFactory {
173            plugin_id,
174            created,
175            fail: false,
176        })
177    }
178
179    #[test]
180    fn create_all_mints_one_parser_per_factory() {
181        let created = Arc::new(AtomicUsize::new(0));
182        let mut set = CompilationParserFactories::new();
183        set.register(factory(1, created.clone()));
184        set.register(factory(2, created.clone()));
185
186        assert_eq!(set.create_all().len(), 2);
187        assert_eq!(created.load(Ordering::SeqCst), 2);
188    }
189
190    #[test]
191    fn each_call_mints_independent_instances() {
192        // The property the whole factory shape exists for: two readers
193        // must not share pending state. Feed the two sets a different
194        // number of lines and assert their entries disagree.
195        let created = Arc::new(AtomicUsize::new(0));
196        let mut set = CompilationParserFactories::new();
197        set.register(factory(1, created.clone()));
198
199        let mut out_side = set.create_all();
200        let mut err_side = set.create_all();
201        assert_eq!(created.load(Ordering::SeqCst), 2);
202
203        out_side[0].feed("a");
204        out_side[0].feed("b");
205        let out_entry = out_side[0].feed("BOOM");
206        let err_entry = err_side[0].feed("BOOM");
207
208        assert_eq!(out_entry[0].line, 3, "stdout instance saw three lines");
209        assert_eq!(
210            err_entry[0].line, 1,
211            "stderr instance saw one — pending state is not shared"
212        );
213    }
214
215    #[test]
216    fn a_factory_that_fails_to_instantiate_is_skipped() {
217        // A broken plugin costs its own entries, never the build: the
218        // other factory still yields a parser.
219        let created = Arc::new(AtomicUsize::new(0));
220        let mut set = CompilationParserFactories::new();
221        set.register(Arc::new(CountingFactory {
222            plugin_id: 1,
223            created: created.clone(),
224            fail: true,
225        }));
226        set.register(factory(2, created.clone()));
227
228        assert_eq!(set.create_all().len(), 1);
229        assert_eq!(created.load(Ordering::SeqCst), 2, "both were asked");
230    }
231
232    #[test]
233    fn unregister_plugin_removes_only_that_plugins_factories() {
234        let created = Arc::new(AtomicUsize::new(0));
235        let mut set = CompilationParserFactories::new();
236        set.register(factory(1, created.clone()));
237        set.register(factory(1, created.clone()));
238        set.register(factory(2, created.clone()));
239
240        assert_eq!(set.unregister_plugin(1), 2);
241        assert_eq!(set.len(), 1);
242        // Idempotent — the teardown contract's double-unload case.
243        assert_eq!(set.unregister_plugin(1), 0);
244        assert_eq!(set.unregister_plugin(2), 1);
245        assert!(set.is_empty());
246    }
247}