Skip to main content

sim_lib_standard_core/
lang_profile.rs

1//! Shared language-profile helpers.
2//!
3//! Every per-language crate (`sim-lib-lang-*`) builds a [`LanguageProfile`],
4//! attaches [`FidelityBadge`]s to it, and installs it with the same fixed
5//! sequence: resolve backing libraries, register the profile, publish the live
6//! profile claims, then publish any extra metadata claims owned by the profile
7//! receipt. These helpers capture that shared shape so the per-crate code only
8//! carries the language-specific definitions.
9
10use std::sync::Arc;
11
12use sim_kernel::{
13    AbiVersion, Cx, Export, Lib, LibId, LibManifest, LibTarget, Linker, Ref, Result, Symbol,
14    Version,
15};
16
17use crate::{
18    FidelityBadge, LanguageProfile, LanguageProfileValue, ProfileRegistry,
19    publish_profile_claims_for_lib,
20};
21
22/// Organ claim publisher used by recorded profile installs.
23pub type ProfileOrganPublisher = fn(&mut Cx, LibId) -> Result<()>;
24
25type ProfileBackingInstaller = fn(&mut Cx) -> Result<()>;
26
27/// One backing library requirement for a profile organ.
28#[derive(Clone)]
29pub struct ProfileBackingLib {
30    organ: Symbol,
31    manifest: Symbol,
32    install: Option<ProfileBackingInstaller>,
33    publish_claims: Option<ProfileOrganPublisher>,
34}
35
36impl ProfileBackingLib {
37    /// Declare a loadable backing library for `organ`.
38    pub fn loadable(
39        organ: Symbol,
40        manifest: Symbol,
41        install: ProfileBackingInstaller,
42        publish_claims: Option<ProfileOrganPublisher>,
43    ) -> Self {
44        Self {
45            organ,
46            manifest,
47            install: Some(install),
48            publish_claims,
49        }
50    }
51
52    /// Declare a backing library requirement for `organ` without an installer
53    /// in the current runtime.
54    pub fn unresolved(organ: Symbol, manifest: Symbol) -> Self {
55        Self {
56            organ,
57            manifest,
58            install: None,
59            publish_claims: None,
60        }
61    }
62}
63
64/// Build a [`FidelityBadge`] for `profile` from a badge symbol, level, and the
65/// conformance `test` that evidences it.
66///
67/// This is the shared body for the per-language `profile_badge` helper.
68pub fn fidelity_badge(profile: &Symbol, badge: Symbol, level: u8, test: &Symbol) -> FidelityBadge {
69    FidelityBadge::new(
70        Ref::Symbol(profile.clone()),
71        badge,
72        level,
73        Ref::Symbol(test.clone()),
74    )
75}
76
77/// Data form of a fidelity badge: a badge symbol, its level, and the
78/// conformance test that evidences it. Resolve it against a profile symbol with
79/// [`FidelityBadgeSpec::into_badge`].
80#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct FidelityBadgeSpec {
82    /// The badge name.
83    pub badge: Symbol,
84    /// Fidelity level, higher meaning more faithful.
85    pub level: u8,
86    /// Conformance test that evidences the badge.
87    pub test: Symbol,
88}
89
90impl FidelityBadgeSpec {
91    /// Resolve this spec into a [`FidelityBadge`] for `profile`.
92    pub fn into_badge(self, profile: &Symbol) -> FidelityBadge {
93        fidelity_badge(profile, self.badge, self.level, &self.test)
94    }
95}
96
97/// Install a fully-built [`LanguageProfile`]: resolve its backing libraries,
98/// register the resulting live profile, publish its profile claims, then run
99/// any extra metadata publishers against the profile receipt.
100///
101/// Organs without installed backing libraries are removed from the live claim
102/// set and recorded under
103/// [`LanguageProfile::backing_requirements`](crate::LanguageProfile::backing_requirements)
104/// instead.
105pub fn install_language_profile(
106    cx: &mut Cx,
107    registry: &mut ProfileRegistry,
108    profile: LanguageProfile,
109    backing_libs: &[ProfileBackingLib],
110    extra_publishers: &[ProfileOrganPublisher],
111) -> Result<LanguageProfile> {
112    let profile = resolve_profile_backings(cx, profile, backing_libs)?;
113    let lib_id = ensure_profile_lib(cx, &profile)?;
114    registry.register_profile(profile.clone())?;
115    publish_profile_claims_for_lib(cx, lib_id, &profile)?;
116    for publish in extra_publishers {
117        publish(cx, lib_id)?;
118    }
119    Ok(profile)
120}
121
122/// Manifest id used for the load receipt that owns a language profile.
123pub fn language_profile_lib_symbol(profile: &Symbol) -> Symbol {
124    Symbol::qualified("standard/profile", profile.to_string())
125}
126
127#[derive(Clone)]
128struct LanguageProfileLib {
129    profile: LanguageProfile,
130}
131
132impl LanguageProfileLib {
133    fn new(profile: LanguageProfile) -> Self {
134        Self { profile }
135    }
136}
137
138impl Lib for LanguageProfileLib {
139    fn manifest(&self) -> LibManifest {
140        LibManifest {
141            id: language_profile_lib_symbol(&self.profile.symbol),
142            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
143            abi: AbiVersion { major: 0, minor: 1 },
144            target: LibTarget::HostRegistered,
145            requires: Vec::new(),
146            capabilities: Vec::new(),
147            exports: vec![Export::Value {
148                symbol: self.profile.symbol.clone(),
149            }],
150        }
151    }
152
153    fn load(&self, cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
154        linker.value(
155            self.profile.symbol.clone(),
156            cx.factory()
157                .opaque(Arc::new(LanguageProfileValue::new(self.profile.clone())))?,
158        )
159    }
160}
161
162fn ensure_profile_lib(cx: &mut Cx, profile: &LanguageProfile) -> Result<LibId> {
163    let lib = LanguageProfileLib::new(profile.clone());
164    let manifest = lib.manifest();
165    if let Some(loaded) = cx.registry().lib(&manifest.id) {
166        return Ok(loaded.id);
167    }
168    cx.load_lib(&lib)
169}
170
171fn resolve_profile_backings(
172    cx: &mut Cx,
173    profile: LanguageProfile,
174    backing_libs: &[ProfileBackingLib],
175) -> Result<LanguageProfile> {
176    let mut declared = std::collections::BTreeMap::new();
177    for backing in backing_libs {
178        let replaced = declared.insert(backing.organ.clone(), backing.clone());
179        if replaced.is_some() {
180            return Err(sim_kernel::Error::Eval(format!(
181                "duplicate backing library spec for organ {}",
182                backing.organ
183            )));
184        }
185    }
186
187    let requested_organs = profile.organs.clone();
188    let mut resolved = profile;
189    resolved.organs.clear();
190    resolved.backing_requirements.clear();
191
192    for organ in requested_organs {
193        let backing = declared.remove(&organ.organ).unwrap_or_else(|| {
194            ProfileBackingLib::unresolved(
195                organ.organ.clone(),
196                default_backing_manifest_for_organ(&organ.organ),
197            )
198        });
199        if let Some(loaded) = resolve_loaded_backing(cx, &backing)? {
200            resolved.organs.push(organ);
201            if let Some(publish_claims) = backing.publish_claims {
202                publish_claims(cx, loaded.id)?;
203            }
204        } else {
205            resolved = resolved.with_backing_requirement(backing.manifest);
206        }
207    }
208
209    if let Some(unused) = declared.into_values().next() {
210        return Err(sim_kernel::Error::Eval(format!(
211            "backing library spec for {} does not match any declared organ",
212            unused.organ
213        )));
214    }
215
216    Ok(resolved)
217}
218
219fn resolve_loaded_backing(
220    cx: &mut Cx,
221    backing: &ProfileBackingLib,
222) -> Result<Option<sim_kernel::LoadedLib>> {
223    let Some(install) = backing.install else {
224        return Ok(None);
225    };
226    install(cx)?;
227    let loaded = cx
228        .registry()
229        .lib(&backing.manifest)
230        .cloned()
231        .ok_or_else(|| {
232            sim_kernel::Error::Eval(format!(
233                "backing library {} for organ {} did not load",
234                backing.manifest, backing.organ
235            ))
236        })?;
237    if loaded.exports.is_empty() {
238        return Err(sim_kernel::Error::Eval(format!(
239            "backing library {} for organ {} published no live exports",
240            backing.manifest, backing.organ
241        )));
242    }
243    Ok(Some(loaded))
244}
245
246fn default_backing_manifest_for_organ(organ: &Symbol) -> Symbol {
247    Symbol::qualified("sim", organ.name.clone())
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use std::cell::RefCell;
254
255    fn profile_symbol() -> Symbol {
256        Symbol::qualified("test", "profile")
257    }
258
259    fn test_symbol() -> Symbol {
260        Symbol::qualified("test", "conformance")
261    }
262
263    fn badge_symbol() -> Symbol {
264        Symbol::qualified("test", "badge")
265    }
266
267    #[test]
268    fn fidelity_badge_matches_manual_construction() {
269        let profile = profile_symbol();
270        let test = test_symbol();
271        let helper = fidelity_badge(&profile, badge_symbol(), 1, &test);
272        let manual = FidelityBadge::new(
273            Ref::Symbol(profile.clone()),
274            badge_symbol(),
275            1,
276            Ref::Symbol(test.clone()),
277        );
278        assert_eq!(helper, manual);
279    }
280
281    #[test]
282    fn spec_into_badge_matches_fidelity_badge() {
283        let profile = profile_symbol();
284        let spec = FidelityBadgeSpec {
285            badge: badge_symbol(),
286            level: 2,
287            test: test_symbol(),
288        };
289        let from_spec = spec.into_badge(&profile);
290        let direct = fidelity_badge(&profile, badge_symbol(), 2, &test_symbol());
291        assert_eq!(from_spec, direct);
292    }
293
294    #[test]
295    fn install_language_profile_registers_and_runs_publishers_in_order() {
296        use std::sync::Arc;
297
298        use sim_kernel::{
299            AbiVersion, DefaultFactory, Export, Lib, LibManifest, LibTarget, LoadCx,
300            NoopEvalPolicy, Version,
301        };
302
303        thread_local! {
304            static CALLS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
305        }
306
307        struct FixtureLib {
308            manifest: Symbol,
309            export: Symbol,
310        }
311
312        impl Lib for FixtureLib {
313            fn manifest(&self) -> LibManifest {
314                LibManifest {
315                    id: self.manifest.clone(),
316                    version: Version("0.1.0".to_owned()),
317                    abi: AbiVersion { major: 0, minor: 1 },
318                    target: LibTarget::HostRegistered,
319                    requires: Vec::new(),
320                    capabilities: Vec::new(),
321                    exports: vec![Export::Value {
322                        symbol: self.export.clone(),
323                    }],
324                }
325            }
326
327            fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
328                linker.value(self.export.clone(), cx.factory().bool(true)?)?;
329                Ok(())
330            }
331        }
332
333        fn install_backing_one(cx: &mut Cx) -> Result<()> {
334            let manifest = Symbol::qualified("sim", "organ-one");
335            if cx.registry().lib(&manifest).is_none() {
336                cx.load_lib(&FixtureLib {
337                    manifest: manifest.clone(),
338                    export: Symbol::qualified("test", "backing-one"),
339                })?;
340            }
341            Ok(())
342        }
343
344        fn install_backing_two(cx: &mut Cx) -> Result<()> {
345            let manifest = Symbol::qualified("sim", "organ-two");
346            if cx.registry().lib(&manifest).is_none() {
347                cx.load_lib(&FixtureLib {
348                    manifest: manifest.clone(),
349                    export: Symbol::qualified("test", "backing-two"),
350                })?;
351            }
352            Ok(())
353        }
354
355        fn first(_cx: &mut Cx, _lib_id: LibId) -> Result<()> {
356            CALLS.with(|calls| calls.borrow_mut().push(1));
357            Ok(())
358        }
359
360        fn second(_cx: &mut Cx, _lib_id: LibId) -> Result<()> {
361            CALLS.with(|calls| calls.borrow_mut().push(2));
362            Ok(())
363        }
364
365        CALLS.with(|calls| calls.borrow_mut().clear());
366
367        let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
368        let mut registry = ProfileRegistry::new();
369        let profile = LanguageProfile::new(profile_symbol())
370            .with_organ(crate::OrganUse::new(Symbol::qualified("organ", "one")))
371            .with_organ(crate::OrganUse::new(Symbol::qualified("organ", "two")));
372
373        let installed = install_language_profile(
374            &mut cx,
375            &mut registry,
376            profile.clone(),
377            &[
378                ProfileBackingLib::loadable(
379                    Symbol::qualified("organ", "one"),
380                    Symbol::qualified("sim", "organ-one"),
381                    install_backing_one,
382                    Some(first),
383                ),
384                ProfileBackingLib::loadable(
385                    Symbol::qualified("organ", "two"),
386                    Symbol::qualified("sim", "organ-two"),
387                    install_backing_two,
388                    Some(second),
389                ),
390            ],
391            &[],
392        )
393        .expect("install language profile");
394
395        assert_eq!(installed, profile);
396        assert!(registry.profile(&profile_symbol()).is_some());
397        CALLS.with(|calls| assert_eq!(*calls.borrow(), vec![1, 2]));
398    }
399
400    #[test]
401    fn install_language_profile_records_unresolved_backing_requirements() {
402        use std::sync::Arc;
403
404        use sim_kernel::{
405            ClaimPattern, DefaultFactory, NoopEvalPolicy, Ref, card::card_kind_predicate,
406            standard::standard_organ_predicate, standard::standard_profile_kind,
407        };
408
409        let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
410        let mut registry = ProfileRegistry::new();
411        let organ = Symbol::qualified("organ", "missing");
412        let profile =
413            LanguageProfile::new(profile_symbol()).with_organ(crate::OrganUse::new(organ.clone()));
414
415        let installed = install_language_profile(&mut cx, &mut registry, profile, &[], &[])
416            .expect("install language profile");
417
418        assert!(installed.organs.is_empty());
419        assert_eq!(
420            installed.backing_requirements,
421            vec![Symbol::qualified("sim", "missing")]
422        );
423        assert_eq!(
424            registry
425                .profile(&profile_symbol())
426                .unwrap()
427                .backing_requirements,
428            vec![Symbol::qualified("sim", "missing")]
429        );
430        assert_eq!(
431            cx.query_facts(ClaimPattern::exact(
432                Ref::Symbol(profile_symbol()),
433                card_kind_predicate(),
434                Ref::Symbol(standard_profile_kind()),
435            ))
436            .unwrap()
437            .len(),
438            1
439        );
440        assert!(
441            cx.query_facts(ClaimPattern::exact(
442                Ref::Symbol(profile_symbol()),
443                standard_organ_predicate(),
444                Ref::Symbol(organ),
445            ))
446            .unwrap()
447            .is_empty()
448        );
449    }
450}