Skip to main content

logicaffeine_kernel/dag/
mirror.rs

1//! The core beside a [`Context`]: a [`Kernel::logos`] per thread, kept in step with the context it
2//! last mirrored through that context's admission log.
3//!
4//! A `Context` is persistent and shared between threads; the core is a mutable, single-threaded
5//! kernel. So the core is not a field of the context but a mirror of one context lineage per
6//! thread, and [`sync`] brings it to a context:
7//!
8//! - the context's log head is the mirror's: nothing changed;
9//! - the log reaches the mirror's head: the entries since name what was registered, and those
10//!   declarations are admitted in dependency order, unless the entries change something the mirror
11//!   holds (a held name removed or registered again, a constructor added to a held family, a name a
12//!   refused declaration referenced, a change to how `Int` literals read), which is a branch. The
13//!   quotient needs no rule of its own: its certification logs all four primitives, so they arrive
14//!   together and are installed whole, or a held one is registered again;
15//! - the log reaches the standard library's head instead: the mirror restarts from a snapshot of
16//!   the standard library's core, kept per thread, and advances from there;
17//! - anything else, a branch included: the context is replayed whole ([`logos_env::replay`]).
18//!
19//! Every route ends where a replay of the context ends: the same declarations admitted, with the
20//! same verdicts.
21//!
22//! A thread's mirror lives on a core thread of its own: an admission recurses as deep as the
23//! declaration it checks, deeper than a caller's stack may reach, and the core is single-threaded
24//! state that never moves between threads. [`sync`] and [`with`] run there and answer the caller.
25
26use super::logos_env::{self, Declaration, Replayed};
27use super::tc::Kernel;
28use crate::context::{Context, LogEntry};
29use crate::prelude::StandardLibrary;
30use std::cell::{Cell, RefCell};
31use std::collections::{BTreeSet, HashSet};
32use std::sync::Arc;
33
34/// How [`sync`] brought the mirror to a context.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum Route {
37    /// The mirror already mirrored the context.
38    Current,
39    /// The context extends the mirrored one; only its new declarations were admitted.
40    Advanced,
41    /// The context extends the standard library on another branch; the mirror restarted from the
42    /// standard library's snapshot and admitted the rest.
43    Rebased,
44    /// The context was replayed whole.
45    Replayed,
46}
47
48/// What one [`sync`] did: its route, and the core's verdict on each declaration it admitted.
49#[derive(Debug)]
50pub struct Synced {
51    pub route: Route,
52    pub verdicts: Vec<Replayed>,
53    /// The refused among `verdicts` that reference a declaration the core had already refused: the
54    /// refusal follows from that one's.
55    pub downstream: BTreeSet<String>,
56}
57
58#[derive(Clone)]
59struct Mirror {
60    kernel: Kernel,
61    /// The head of the mirrored context's log.
62    head: Option<Arc<LogEntry>>,
63    /// Every name the kernel's declarations declare, refused ones included.
64    held: HashSet<String>,
65    /// The names refused declarations reference that the mirror does not hold: a replay would
66    /// decide those declarations again once these are registered.
67    awaited: HashSet<String>,
68    /// Every name of a declaration the core refused.
69    refused: HashSet<String>,
70}
71
72thread_local! {
73    /// On a core thread: its mirror.
74    static MIRROR: RefCell<Option<Mirror>> = const { RefCell::new(None) };
75    /// On a core thread: the standard library's mirror, built on first use and never advanced.
76    static BASE: RefCell<Option<Mirror>> = const { RefCell::new(None) };
77    /// On a calling thread: the core thread that holds its mirror, started on first use.
78    static CORE: RefCell<Option<CoreThread>> = const { RefCell::new(None) };
79    /// Whether this thread is a core thread.
80    static ON_CORE: Cell<bool> = const { Cell::new(false) };
81}
82
83/// The stack a core thread runs on, the one the core's own suites check on.
84const CORE_STACK: usize = 1 << 30;
85
86/// A calling thread's core thread. It ends when the calling thread does: dropping the queue ends
87/// its loop.
88struct CoreThread {
89    queue: std::sync::mpsc::Sender<Box<dyn FnOnce() + Send>>,
90}
91
92fn start_core() -> CoreThread {
93    let (queue, jobs) = std::sync::mpsc::channel::<Box<dyn FnOnce() + Send>>();
94    std::thread::Builder::new()
95        .name("logos-core-mirror".to_string())
96        .stack_size(CORE_STACK)
97        .spawn(move || {
98            ON_CORE.with(|on| on.set(true));
99            for job in jobs {
100                job();
101            }
102        })
103        .expect("a core thread starts");
104    CoreThread { queue }
105}
106
107/// Run `job` on this thread's core thread and return its answer; a panic in `job` is the caller's.
108/// On a core thread itself, `job` runs in place.
109fn on_core<R: Send + 'static>(job: impl FnOnce() -> R + Send + 'static) -> R {
110    if ON_CORE.with(Cell::get) {
111        return job();
112    }
113    let (answer, answered) = std::sync::mpsc::channel();
114    let job: Box<dyn FnOnce() + Send> = Box::new(move || {
115        let _ = answer.send(std::panic::catch_unwind(std::panic::AssertUnwindSafe(job)));
116    });
117    CORE.with(|cell| {
118        cell.borrow_mut()
119            .get_or_insert_with(start_core)
120            .queue
121            .send(job)
122            .expect("the core thread runs")
123    });
124    match answered.recv().expect("the core thread answers every job") {
125        Ok(result) => result,
126        Err(panic) => std::panic::resume_unwind(panic),
127    }
128}
129
130/// Bring this thread's mirror to `ctx`.
131pub fn sync(ctx: &Context) -> Synced {
132    let ctx = ctx.clone();
133    on_core(move || {
134        let (mirror, synced) = advance(MIRROR.with(|cell| cell.borrow_mut().take()), &ctx);
135        MIRROR.with(|cell| *cell.borrow_mut() = Some(mirror));
136        synced
137    })
138}
139
140/// Bring this thread's mirror to `ctx` and run `f` on its kernel. The mirror is out of its slot
141/// while `f` runs, so a sync inside `f` builds its own and this one is put back after.
142pub fn with<R: Send + 'static>(
143    ctx: &Context,
144    f: impl FnOnce(&mut Kernel) -> R + Send + 'static,
145) -> (Synced, R) {
146    let ctx = ctx.clone();
147    on_core(move || {
148        let (mut mirror, synced) = advance(MIRROR.with(|cell| cell.borrow_mut().take()), &ctx);
149        let result = f(&mut mirror.kernel);
150        MIRROR.with(|cell| *cell.borrow_mut() = Some(mirror));
151        (synced, result)
152    })
153}
154
155fn same(a: Option<&Arc<LogEntry>>, b: Option<&Arc<LogEntry>>) -> bool {
156    match (a, b) {
157        (Some(a), Some(b)) => Arc::ptr_eq(a, b),
158        (None, None) => true,
159        _ => false,
160    }
161}
162
163/// Walk back from `at`, pushing each entry passed onto `suffix` (newest first), until `stop` holds
164/// of the position reached: that position, or `None` when the log ended first.
165fn walk<'a>(
166    mut at: Option<&'a Arc<LogEntry>>,
167    suffix: &mut Vec<&'a LogEntry>,
168    stop: impl Fn(Option<&Arc<LogEntry>>) -> bool,
169) -> Option<Option<&'a Arc<LogEntry>>> {
170    loop {
171        if stop(at) {
172            return Some(at);
173        }
174        let entry = at?;
175        suffix.push(entry);
176        at = entry.prev.as_ref();
177    }
178}
179
180fn advance(mirror: Option<Mirror>, ctx: &Context) -> (Mirror, Synced) {
181    let base_head = StandardLibrary::registered().map(Context::admissions);
182    let at_base = |at: Option<&Arc<LogEntry>>| base_head.is_some_and(|h| same(at, h));
183    let mut suffix: Vec<&LogEntry> = Vec::new();
184    let mut at = ctx.admissions();
185    if let Some(mut mirror) = mirror {
186        let Some(stopped) = walk(at, &mut suffix, |at| {
187            same(at, mirror.head.as_ref()) || at_base(at)
188        }) else {
189            return replay(ctx);
190        };
191        if same(stopped, mirror.head.as_ref()) {
192            if suffix.is_empty() {
193                mirror.kernel.follow(ctx);
194                return (
195                    mirror,
196                    Synced {
197                        route: Route::Current,
198                        verdicts: Vec::new(),
199                        downstream: BTreeSet::new(),
200                    },
201                );
202            }
203            if let Ok((mirror, verdicts, downstream)) = extend(mirror, ctx, &suffix) {
204                return (
205                    mirror,
206                    Synced {
207                        route: Route::Advanced,
208                        verdicts,
209                        downstream,
210                    },
211                );
212            }
213        }
214        at = stopped;
215    }
216    if walk(at, &mut suffix, at_base).is_none() {
217        return replay(ctx);
218    }
219    let (base, built) = base();
220    let route = if built.is_some() {
221        Route::Replayed
222    } else {
223        Route::Rebased
224    };
225    match extend(base, ctx, &suffix) {
226        Ok((mirror, more, more_downstream)) => {
227            let (mut verdicts, mut downstream) = built.unwrap_or_default();
228            verdicts.extend(more);
229            downstream.extend(more_downstream);
230            (
231                mirror,
232                Synced {
233                    route,
234                    verdicts,
235                    downstream,
236                },
237            )
238        }
239        Err(()) => replay(ctx),
240    }
241}
242
243/// The standard library's mirror, and its verdicts when this call built it.
244fn base() -> (Mirror, Option<(Vec<Replayed>, BTreeSet<String>)>) {
245    BASE.with(|cell| {
246        let mut slot = cell.borrow_mut();
247        if let Some(base) = slot.as_ref() {
248            return (base.clone(), None);
249        }
250        let ctx = StandardLibrary::registered().expect("a context met the standard library's log");
251        let (base, synced) = replay(ctx);
252        *slot = Some(base.clone());
253        (base, Some((synced.verdicts, synced.downstream)))
254    })
255}
256
257fn replay(ctx: &Context) -> (Mirror, Synced) {
258    let (kernel, items, verdicts) = logos_env::replay_items(ctx);
259    let mut mirror = Mirror {
260        kernel,
261        head: ctx.admissions().cloned(),
262        held: HashSet::new(),
263        awaited: HashSet::new(),
264        refused: HashSet::new(),
265    };
266    let downstream = mirror.hold(&items, &verdicts);
267    (
268        mirror,
269        Synced {
270            route: Route::Replayed,
271            verdicts,
272            downstream,
273        },
274    )
275}
276
277/// Admit what `suffix` (newest first) registered on top of `mirror`; `Err` when the suffix is a
278/// branch from it.
279fn extend(
280    mut mirror: Mirror,
281    ctx: &Context,
282    suffix: &[&LogEntry],
283) -> Result<(Mirror, Vec<Replayed>, BTreeSet<String>), ()> {
284    if ctx.canonical_numerals() != mirror.kernel.numerals() {
285        return Err(());
286    }
287    let mut changed: BTreeSet<String> = BTreeSet::new();
288    for entry in suffix.iter().rev() {
289        if mirror.held.contains(&entry.name) || mirror.awaited.contains(&entry.name) {
290            return Err(());
291        }
292        if entry.removed {
293            continue;
294        }
295        let owner = ctx
296            .constructor_inductive(&entry.name)
297            .unwrap_or(&entry.name);
298        if mirror.held.contains(owner) {
299            return Err(());
300        }
301        changed.insert(owner.to_string());
302    }
303    let items = logos_env::items_named(ctx, &changed);
304    mirror.kernel.follow(ctx);
305    let verdicts = logos_env::admit_items(&mut mirror.kernel, ctx, &items);
306    let downstream = mirror.hold(&items, &verdicts);
307    mirror.head = ctx.admissions().cloned();
308    Ok((mirror, verdicts, downstream))
309}
310
311impl Mirror {
312    /// Record `items` (in admission order) as held, what the refused ones among them reference and
313    /// the mirror lacks, and which refused ones reference a declaration refused before them: those
314    /// are returned.
315    fn hold(&mut self, items: &[Declaration], verdicts: &[Replayed]) -> BTreeSet<String> {
316        for item in items {
317            self.held
318                .extend(item.names().into_iter().map(str::to_string));
319        }
320        let refused: HashSet<&str> = verdicts
321            .iter()
322            .filter(|v| v.verdict.is_err())
323            .map(|v| v.name.as_str())
324            .collect();
325        let mut downstream = BTreeSet::new();
326        for item in items {
327            let names = item.names();
328            if names.iter().any(|n| refused.contains(n)) {
329                let globals = logos_env::item_globals(item);
330                if globals
331                    .iter()
332                    .any(|g| self.refused.contains(g) && !names.contains(&g.as_str()))
333                {
334                    downstream.extend(names.iter().map(|n| n.to_string()));
335                }
336                self.refused.extend(names.iter().map(|n| n.to_string()));
337                self.awaited
338                    .extend(globals.into_iter().filter(|n| !self.held.contains(n)));
339            }
340        }
341        self.awaited.retain(|n| !self.held.contains(n));
342        downstream
343    }
344}
345
346/// Shadow mode, before a door decides: the mirror is brought to the context as the door finds it, so
347/// the sync after the door reports exactly what the door registered.
348#[cfg(feature = "shadow")]
349pub(crate) fn shadow_enter(ctx: &Context) -> Entry {
350    if compares(ctx) {
351        sync(ctx);
352    }
353    Entry(ctx.admissions().cloned())
354}
355
356/// The admission log's head as a door found it: what the door registers is what the log holds
357/// past it.
358#[cfg(feature = "shadow")]
359pub(crate) struct Entry(Option<Arc<LogEntry>>);
360
361/// The names registered in `ctx` since `entry`.
362#[cfg(feature = "shadow")]
363fn registered_since<'a>(ctx: &'a Context, entry: &Entry) -> HashSet<&'a str> {
364    let mut suffix = Vec::new();
365    walk(ctx.admissions(), &mut suffix, |at| {
366        same(at, entry.0.as_ref())
367    });
368    suffix
369        .into_iter()
370        .filter(|e| !e.removed)
371        .map(|e| e.name.as_str())
372        .collect()
373}
374
375/// Whether the shadow compares the doors' verdicts at `ctx`: not while the standard library
376/// registers, and not in a context with local bindings, where a declaration may be open in them and
377/// the core, which admits closed global declarations, has no counterpart for it.
378#[cfg(feature = "shadow")]
379fn compares(ctx: &Context) -> bool {
380    !crate::prelude::registering() && !ctx.has_local_bindings()
381}
382
383/// Where the core's verdicts on what a door registered differ from the context's: a declaration the
384/// context holds is one the core must admit, and one it holds pending (its dependencies not yet
385/// registered) one the core must refuse. A refusal `downstream` of one the core made before is that
386/// one's: it was compared where that declaration was decided, or no checked door decided it.
387#[cfg(feature = "shadow")]
388fn admission_disagreements<'a>(
389    verdicts: impl IntoIterator<Item = &'a Replayed>,
390    pending: &HashSet<&str>,
391    downstream: &BTreeSet<String>,
392) -> Vec<String> {
393    verdicts
394        .into_iter()
395        .filter_map(|v| match (&v.verdict, pending.contains(v.name.as_str())) {
396            (Ok(()), false) | (Err(_), true) => None,
397            (Err(_), false) if downstream.contains(&v.name) => None,
398            (Err(e), false) => Some(format!(
399                "`{}`: the Term kernel admits it, the core refuses it: {e}",
400                v.name
401            )),
402            (Ok(()), true) => Some(format!(
403                "`{}`: the Term kernel holds it pending, the core admits it",
404                v.name
405            )),
406        })
407        .collect()
408}
409
410/// Shadow mode, after a door decided a declaration: `entry` is the log as the door found it
411/// (`shadow_enter`), `refused` the `Term` kernel's refusal of `candidate`, or `None` when the door
412/// registered it. The core's verdict must be the same: on every
413/// name the door registered, read by a sync (a name the door left pending is a refusal), or on the
414/// refused candidate, judged in the unchanged context. Only a refusal of the theory is compared; a
415/// door's own policy (a name already taken, a redeclaration others depend on) has no counterpart in
416/// the core. Stands down where `compares` says the core has no counterpart.
417#[cfg(feature = "shadow")]
418pub(crate) fn shadow(
419    ctx: &Context,
420    entry: &Entry,
421    refused: Option<&crate::error::KernelError>,
422    candidate: impl FnOnce() -> Vec<Declaration>,
423) {
424    if !compares(ctx) {
425        return;
426    }
427    match refused {
428        None => {
429            let synced = sync(ctx);
430            let registered = registered_since(ctx, entry);
431            let pending: HashSet<&str> = ctx
432                .pending_obligations()
433                .into_iter()
434                .map(|(n, _)| n)
435                .collect();
436            let decided = synced
437                .verdicts
438                .iter()
439                .filter(|v| registered.contains(v.name.as_str()));
440            let disagreements = admission_disagreements(decided, &pending, &synced.downstream);
441            if !disagreements.is_empty() {
442                panic!(
443                    "shadow mode: the kernels disagree ({:?}):\n{}",
444                    synced.route,
445                    disagreements.join("\n")
446                );
447            }
448        }
449        Some(error) if theory_refusal(error) => {
450            let block = candidate();
451            let names: Vec<String> = block
452                .iter()
453                .flat_map(|d| d.names().into_iter().map(str::to_string))
454                .collect();
455            let judged = ctx.clone();
456            let (_, verdict) = with(ctx, move |k| logos_env::judge(k, &judged, &block));
457            if verdict.is_ok() {
458                panic!(
459                    "shadow mode: the Term kernel refuses {}, the core admits it.\nThe Term kernel: {error}",
460                    names.join(", ")
461                );
462            }
463        }
464        Some(_) => {}
465    }
466}
467
468/// Whether `error` is a refusal by a rule of the theory, which the core must make too.
469#[cfg(feature = "shadow")]
470fn theory_refusal(error: &crate::error::KernelError) -> bool {
471    use crate::error::KernelError::*;
472    matches!(
473        error,
474        UnboundVariable(_)
475            | NotAFunction(_)
476            | NotAType(_)
477            | TypeMismatch { .. }
478            | NotAnInductive(_)
479            | InvalidMotive(_)
480            | WrongNumberOfCases { .. }
481            | InvalidDeclaration(_)
482            | TerminationViolation { .. }
483            | PositivityViolation { .. }
484    )
485}
486
487#[cfg(all(test, feature = "shadow"))]
488#[path = "../../../logicaffeine_tests/unit/logicaffeine_kernel/dag/mirror/shadow_controls.rs"]
489mod shadow_controls;