nibli_render/overlay.rs
1//! Per-example domain-gloss overlay: an OPTIONAL display layer that renders a
2//! curated corpus's proxy predicates and opaque constants in real domain terms
3//! (`prevents` -> "inhibits", `flukonazol` -> "fluconazole"), scoped to a loaded
4//! example.
5//!
6//! The engine dictionary (`nibli_lexicon`) is always the FALLBACK: an example
7//! with no entry for a relation, and every Custom (user-authored) KB, render with
8//! the literal glosses. The overlay is installed only for the duration of one
9//! render call via [`with_overlay`] and read back through [`active`], so it never
10//! leaks into the back-translation ("What Nibli Understood") tab — that surface
11//! calls the renderer WITHOUT an overlay and stays deliberately literal as the
12//! firewall's verification view.
13
14use std::cell::Cell;
15
16/// A domain-term overlay for one curated example. The tables are tiny (a handful
17/// of entries each), so a linear scan is fine.
18#[derive(Clone, Copy, Debug)]
19pub struct DomainGloss {
20 /// Place-frame template overrides keyed by bare gismu. Placeholders `{x1}`..
21 /// may reorder, e.g. `se cuts` -> "{x2} is metabolized by {x1}" (the IR has
22 /// already swapped the `se` args, so the template keys on the bare gismu).
23 pub templates: &'static [(&'static str, &'static str)],
24 /// Single-word noun-gloss overrides keyed by gismu (e.g. the "a <noun>" of an
25 /// existence clause).
26 pub glosses: &'static [(&'static str, &'static str)],
27 /// Display-name overrides keyed by constant (cmevla), e.g. `varfarin` ->
28 /// "warfarin", `siptucin` -> "CYP2C9".
29 pub names: &'static [(&'static str, &'static str)],
30}
31
32impl DomainGloss {
33 /// Overlay place-frame template for `relation`, if any.
34 pub fn template(&self, relation: &str) -> Option<&'static str> {
35 lookup(self.templates, relation)
36 }
37
38 /// Overlay noun gloss for `relation`, if any.
39 pub fn gloss(&self, relation: &str) -> Option<&'static str> {
40 lookup(self.glosses, relation)
41 }
42
43 /// Overlay display name for a constant, if any.
44 pub fn name(&self, constant: &str) -> Option<&'static str> {
45 lookup(self.names, constant)
46 }
47}
48
49fn lookup(table: &[(&'static str, &'static str)], key: &str) -> Option<&'static str> {
50 table.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
51}
52
53thread_local! {
54 /// The overlay active for the current render call (`None` = dictionary
55 /// fallback). A `&'static` reference, so the cell is `Copy`.
56 static OVERLAY: Cell<Option<&'static DomainGloss>> = const { Cell::new(None) };
57}
58
59/// Run `f` with `overlay` installed as the active render overlay, restoring the
60/// previous value afterward (panic-safe via the drop guard). Intended for the
61/// single-threaded UI render path; other threads see `None`.
62pub(crate) fn with_overlay<T>(overlay: Option<&'static DomainGloss>, f: impl FnOnce() -> T) -> T {
63 struct Guard(Option<&'static DomainGloss>);
64 impl Drop for Guard {
65 fn drop(&mut self) {
66 OVERLAY.with(|o| o.set(self.0));
67 }
68 }
69 let _guard = OVERLAY.with(|o| Guard(o.replace(overlay)));
70 f()
71}
72
73/// The overlay active for this render call, if any.
74pub(crate) fn active() -> Option<&'static DomainGloss> {
75 OVERLAY.with(Cell::get)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 static SAMPLE: DomainGloss = DomainGloss {
83 templates: &[("dangerous", "{x1} is at toxicity risk")],
84 glosses: &[("chemical", "drug")],
85 names: &[("varfarin", "warfarin")],
86 };
87
88 #[test]
89 fn lookups_resolve() {
90 assert_eq!(
91 SAMPLE.template("dangerous"),
92 Some("{x1} is at toxicity risk")
93 );
94 assert_eq!(SAMPLE.template("increases"), None);
95 assert_eq!(SAMPLE.gloss("chemical"), Some("drug"));
96 assert_eq!(SAMPLE.name("varfarin"), Some("warfarin"));
97 assert_eq!(SAMPLE.name("adam"), None);
98 }
99
100 #[test]
101 fn overlay_is_scoped_and_restored() {
102 assert!(active().is_none());
103 with_overlay(Some(&SAMPLE), || {
104 assert!(std::ptr::eq(active().unwrap(), &SAMPLE));
105 // Nested override + restore.
106 with_overlay(None, || assert!(active().is_none()));
107 assert!(std::ptr::eq(active().unwrap(), &SAMPLE));
108 });
109 assert!(active().is_none());
110 }
111}