Skip to main content

sim_lib_core/
surface.rs

1//! Surface-pack card libs and idempotent install.
2//!
3//! ~92 crates hand-write an `impl Lib` that exports a set of value cards
4//! (tables with fields like `symbol`, `layer`, `kind`, `role`, ...) and ~49
5//! guard install with `registry().lib(&id).is_some()`. This is their shared
6//! substrate: declare the cards as data ([`SurfacePackSpec`]) and install once
7//! with [`install_once`].
8//!
9//! # How to write a SIM lib
10//!
11//! A host-registered SIM lib should keep its crate entrypoint thin, put the
12//! implementation in a focused runtime module, and expose one public install
13//! function named `install_<crate>_lib` after stripping the `sim-lib-` prefix and
14//! replacing hyphens with underscores. The install function should call
15//! [`install_once`] rather than hand-checking `registry().lib(...)`, unless the
16//! crate is a documented aggregate or lower-layer exception in
17//! `scripts/check-libs.sh`.
18//!
19//! `Lib::manifest` should use `Version(env!("CARGO_PKG_VERSION").to_owned())`,
20//! `LibTarget::HostRegistered`, explicit `Export` records for every registered
21//! class/function/value/codec/number-domain surface, and only the capabilities
22//! the lib itself requires at load time. `Lib::load` owns the actual linker
23//! registration and should avoid hidden global mutation beyond the registered
24//! runtime surface.
25//!
26//! If the lib publishes value cards, prefer [`SurfacePackLib`] and
27//! [`SurfacePackSpec`] over hand-written card registration. If the lib ships
28//! recipes, keep the pure recipe data in `sim-cookbook` inputs and register the
29//! runtime projection from the higher-level lib. If it introduces object values,
30//! either derive/provide citizen read constructors or add an explicit
31//! `#[non_citizen]` exemption that names the descriptor strategy.
32
33use sim_kernel::{
34    AbiVersion, Cx, Dependency, Export, Expr, Lib, LibId, LibManifest, LibTarget, Linker, LoadCx,
35    Result, Symbol, Value, Version,
36};
37
38/// A typed card field value.
39pub enum SurfaceField {
40    /// A symbol value.
41    Symbol(Symbol),
42    /// A string value.
43    Str(String),
44    /// A list-of-symbols value (built as a list of symbol values).
45    Symbols(Vec<Symbol>),
46    /// A list-of-strings value (built as a list of string values).
47    Strs(Vec<String>),
48    /// A boolean value.
49    Bool(bool),
50    /// An unsigned-integer (i64-domain number) value.
51    U64(u64),
52    /// An arbitrary expression value.
53    Expr(Expr),
54}
55
56/// One exported value card: its symbol and its table fields.
57pub struct SurfaceValueSpec {
58    /// The card's export symbol.
59    pub symbol: Symbol,
60    /// The card's table fields, in order.
61    pub fields: Vec<(Symbol, SurfaceField)>,
62}
63
64/// A pack of value cards exported by one host-registered lib.
65pub struct SurfacePackSpec {
66    /// The lib id.
67    pub lib_id: Symbol,
68    /// The cards.
69    pub values: Vec<SurfaceValueSpec>,
70}
71
72/// A host-registered lib built from a [`SurfacePackSpec`].
73pub struct SurfacePackLib {
74    /// The pack specification.
75    pub spec: SurfacePackSpec,
76}
77
78impl Lib for SurfacePackLib {
79    fn manifest(&self) -> LibManifest {
80        LibManifest {
81            id: self.spec.lib_id.clone(),
82            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
83            abi: AbiVersion { major: 0, minor: 1 },
84            target: LibTarget::HostRegistered,
85            requires: Vec::<Dependency>::new(),
86            capabilities: Vec::new(),
87            exports: self
88                .spec
89                .values
90                .iter()
91                .map(|value| Export::Value {
92                    symbol: value.symbol.clone(),
93                })
94                .collect(),
95        }
96    }
97
98    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
99        for value in &self.spec.values {
100            let card = build_card(cx, &value.fields)?;
101            linker.value(value.symbol.clone(), card)?;
102        }
103        Ok(())
104    }
105}
106
107fn build_card(cx: &mut LoadCx, fields: &[(Symbol, SurfaceField)]) -> Result<Value> {
108    let mut entries = Vec::with_capacity(fields.len());
109    for (key, field) in fields {
110        let value = match field {
111            SurfaceField::Symbol(symbol) => cx.factory().symbol(symbol.clone())?,
112            SurfaceField::Str(text) => cx.factory().string(text.clone())?,
113            SurfaceField::Bool(flag) => cx.factory().bool(*flag)?,
114            SurfaceField::Symbols(symbols) => {
115                let items = symbols
116                    .iter()
117                    .map(|symbol| cx.factory().symbol(symbol.clone()))
118                    .collect::<Result<Vec<_>>>()?;
119                cx.factory().list(items)?
120            }
121            SurfaceField::Strs(texts) => {
122                let items = texts
123                    .iter()
124                    .map(|text| cx.factory().string(text.clone()))
125                    .collect::<Result<Vec<_>>>()?;
126                cx.factory().list(items)?
127            }
128            other => cx.factory().expr(field_to_expr(other))?,
129        };
130        entries.push((key.clone(), value));
131    }
132    cx.factory().table(entries)
133}
134
135/// Build the browse card map (an `Expr`) for one value spec -- the shared core
136/// of the many hand-written `*_card_expr` builders. This is the `Expr` (data)
137/// counterpart of the `Value` card that [`SurfacePackLib`] registers.
138pub fn card_expr(spec: &SurfaceValueSpec) -> Expr {
139    Expr::Map(
140        spec.fields
141            .iter()
142            .map(|(key, field)| (Expr::Symbol(key.clone()), field_to_expr(field)))
143            .collect(),
144    )
145}
146
147fn field_to_expr(field: &SurfaceField) -> Expr {
148    match field {
149        SurfaceField::Symbol(symbol) => Expr::Symbol(symbol.clone()),
150        SurfaceField::Str(text) => Expr::String(text.clone()),
151        SurfaceField::Bool(flag) => Expr::Bool(*flag),
152        SurfaceField::U64(number) => sim_value::build::uint(*number),
153        SurfaceField::Symbols(symbols) => Expr::List(
154            symbols
155                .iter()
156                .map(|symbol| Expr::Symbol(symbol.clone()))
157                .collect(),
158        ),
159        SurfaceField::Strs(texts) => Expr::List(
160            texts
161                .iter()
162                .map(|text| Expr::String(text.clone()))
163                .collect(),
164        ),
165        SurfaceField::Expr(expr) => expr.clone(),
166    }
167}
168
169/// Install `lib` only if its id is not already registered. Returns `true` if it
170/// was loaded, `false` if it was already present. Replaces the
171/// `registry().lib(&id).is_some()` early-return guard.
172pub fn install_once(cx: &mut Cx, lib: &impl Lib) -> Result<bool> {
173    install_once_id(cx, lib).map(|id| id.is_some())
174}
175
176/// Install `lib` only if absent, returning the newly loaded id when it loads.
177pub fn install_once_id(cx: &mut Cx, lib: &impl Lib) -> Result<Option<LibId>> {
178    let id = lib.manifest().id.clone();
179    if cx.registry().lib(&id).is_some() {
180        return Ok(None);
181    }
182    cx.load_lib(lib).map(Some)
183}
184
185/// Return the loaded id for `lib`, if it is already registered.
186pub fn installed_lib_id(cx: &Cx, lib: &impl Lib) -> Option<LibId> {
187    let id = lib.manifest().id;
188    cx.registry().lib(&id).map(|loaded| loaded.id)
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    fn pack() -> SurfacePackLib {
196        SurfacePackLib {
197            spec: SurfacePackSpec {
198                lib_id: Symbol::new("demo-pack"),
199                values: vec![
200                    SurfaceValueSpec {
201                        symbol: Symbol::qualified("demo", "Alpha"),
202                        fields: vec![
203                            (
204                                Symbol::new("symbol"),
205                                SurfaceField::Symbol(Symbol::qualified("demo", "Alpha")),
206                            ),
207                            (Symbol::new("layer"), SurfaceField::Str("demo".to_owned())),
208                            (Symbol::new("lossless"), SurfaceField::Bool(true)),
209                            (Symbol::new("rank"), SurfaceField::U64(3)),
210                            (
211                                Symbol::new("tags"),
212                                SurfaceField::Strs(vec!["a".to_owned(), "b".to_owned()]),
213                            ),
214                        ],
215                    },
216                    SurfaceValueSpec {
217                        symbol: Symbol::qualified("demo", "Beta"),
218                        fields: vec![(
219                            Symbol::new("symbol"),
220                            SurfaceField::Symbol(Symbol::qualified("demo", "Beta")),
221                        )],
222                    },
223                ],
224            },
225        }
226    }
227
228    #[test]
229    fn manifest_exports_one_value_per_card() {
230        let manifest = pack().manifest();
231        assert_eq!(manifest.id, Symbol::new("demo-pack"));
232        let symbols: Vec<String> = manifest
233            .exports
234            .iter()
235            .filter_map(|export| match export {
236                Export::Value { symbol } => Some(symbol.to_string()),
237                _ => None,
238            })
239            .collect();
240        assert_eq!(
241            symbols,
242            vec!["demo/Alpha".to_owned(), "demo/Beta".to_owned()]
243        );
244    }
245
246    #[test]
247    fn card_expr_renders_every_field_kind() {
248        let spec = &pack().spec.values[0];
249        let Expr::Map(entries) = card_expr(spec) else {
250            panic!("card_expr must build a map");
251        };
252        assert_eq!(entries.len(), 5);
253        assert_eq!(
254            entries[0],
255            (
256                Expr::Symbol(Symbol::new("symbol")),
257                Expr::Symbol(Symbol::qualified("demo", "Alpha"))
258            )
259        );
260        assert_eq!(
261            entries[1],
262            (
263                Expr::Symbol(Symbol::new("layer")),
264                Expr::String("demo".to_owned())
265            )
266        );
267        assert_eq!(
268            entries[2],
269            (Expr::Symbol(Symbol::new("lossless")), Expr::Bool(true))
270        );
271        assert_eq!(
272            entries[3],
273            (Expr::Symbol(Symbol::new("rank")), sim_value::build::uint(3))
274        );
275        assert_eq!(
276            entries[4].1,
277            Expr::List(vec![
278                Expr::String("a".to_owned()),
279                Expr::String("b".to_owned())
280            ])
281        );
282    }
283
284    #[test]
285    fn install_once_is_idempotent_and_loads_every_field_kind() {
286        let mut cx = sim_test_support::core_cx();
287        assert!(
288            install_once(&mut cx, &pack()).unwrap(),
289            "first install loads"
290        );
291        assert!(
292            !install_once(&mut cx, &pack()).unwrap(),
293            "second install is a no-op"
294        );
295    }
296}