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}