Skip to main content

sim_run_core/
load.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    sync::Arc,
4};
5
6use sim_kernel::{
7    CapabilityName, CatalogSource, Cx, DefaultFactory, Error as KernelError, GrantSeat, Lib, LibId,
8    LibLoader, LibManifest, LibSource as KernelLibSource, LibSourceSpec as KernelLibSourceSpec,
9    LoaderRegistry, NoopEvalPolicy, Symbol,
10};
11use sim_lib_stream_host::native_audio_provider_capability;
12
13use crate::{
14    CliBoot, CliError, ConfigReportKind, CratesIoResolver, CratesIoSpec, LibSourceSpec,
15    LoadReceipt, LoadReceiptRole,
16    codec_boot::{boot_codec_name, codec_lib_symbol, explicit_codec_source_index},
17    config::{RuntimeConfigState, load_config_sources},
18    crates_io::fallback_spec_for_symbol,
19    host::{HostLibRegistry, HostSourceLoader, host_receipt},
20    source::symbol_from_text,
21};
22
23/// Kernel-backed loader session used by the command entry API.
24pub struct LoadSession {
25    cx: Cx,
26    /// Host-only grant seat minted with `cx`; the only capability-grant authority
27    /// in this session. It is never handed to a loaded callable, so a loaded lib
28    /// cannot mint its own capabilities.
29    seat: GrantSeat,
30    loaders: LoaderRegistry,
31    hosts: HostLibRegistry,
32    crates_io: CratesIoResolver,
33    catalog_sources: BTreeMap<Symbol, LibSourceSpec>,
34    default_verb_sources: BTreeMap<String, Vec<LibSourceSpec>>,
35    default_verb_config_libs: BTreeMap<String, Vec<Symbol>>,
36    receipts: Vec<LoadReceipt>,
37    config: RuntimeConfigState,
38    native_audio_provider_active: bool,
39}
40
41trait GrantOutcome {
42    fn expect_granted(self);
43}
44
45impl GrantOutcome for () {
46    fn expect_granted(self) {}
47}
48
49impl GrantOutcome for Result<(), KernelError> {
50    fn expect_granted(self) {
51        self.expect("load session grant seat grants into its own Cx");
52    }
53}
54
55macro_rules! expect_granted {
56    ($grant:expr) => {{
57        #[allow(clippy::let_unit_value)]
58        let grant_result = $grant;
59        #[allow(clippy::unit_arg)]
60        grant_result.expect_granted();
61    }};
62}
63
64impl LoadSession {
65    /// Builds a loader session with an empty static host catalog.
66    pub fn new() -> Self {
67        let mut loaders = LoaderRegistry::new();
68        loaders.add_loader(HostSourceLoader);
69        let (cx, seat) = Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
70        Self {
71            cx,
72            seat,
73            loaders,
74            hosts: HostLibRegistry::default(),
75            crates_io: CratesIoResolver::default(),
76            catalog_sources: BTreeMap::new(),
77            default_verb_sources: BTreeMap::new(),
78            default_verb_config_libs: BTreeMap::new(),
79            receipts: Vec::new(),
80            config: RuntimeConfigState::default(),
81            native_audio_provider_active: false,
82        }
83    }
84
85    /// Adds a kernel loader to the session.
86    pub fn add_loader(&mut self, loader: impl LibLoader + 'static) {
87        self.loaders.add_loader(loader);
88    }
89
90    /// Registers a catalog source for a library symbol.
91    pub fn add_catalog_source(&mut self, symbol: impl AsRef<str>, source: CatalogSource) {
92        let symbol = symbol_from_text(symbol.as_ref());
93        self.catalog_sources
94            .insert(symbol.clone(), catalog_source_spec(source.clone()));
95        self.loaders.add_source(symbol, source);
96    }
97
98    /// Registers a catalog source, builder-style.
99    pub fn with_catalog_source(mut self, symbol: impl AsRef<str>, source: CatalogSource) -> Self {
100        self.add_catalog_source(symbol, source);
101        self
102    }
103
104    /// Adds a kernel loader, builder-style.
105    pub fn with_loader(mut self, loader: impl LibLoader + 'static) -> Self {
106        self.add_loader(loader);
107        self
108    }
109
110    /// Adds a host library factory to the static host catalog.
111    pub fn add_host_factory(
112        &mut self,
113        name: impl Into<String>,
114        factory: impl Fn() -> Box<dyn Lib> + Send + Sync + 'static,
115    ) {
116        self.hosts.add(name, factory);
117    }
118
119    /// Adds a host library factory that can inspect the discovered effective
120    /// runtime config before it builds the library.
121    pub fn add_host_factory_with_config(
122        &mut self,
123        name: impl Into<String>,
124        factory: impl Fn(&RuntimeConfigState) -> Box<dyn Lib> + Send + Sync + 'static,
125    ) {
126        self.hosts.add_with_config(name, factory);
127    }
128
129    /// Adds a host library factory, builder-style.
130    pub fn with_host_factory(
131        mut self,
132        name: impl Into<String>,
133        factory: impl Fn() -> Box<dyn Lib> + Send + Sync + 'static,
134    ) -> Self {
135        self.add_host_factory(name, factory);
136        self
137    }
138
139    /// Adds a config-aware host library factory, builder-style.
140    pub fn with_host_factory_with_config(
141        mut self,
142        name: impl Into<String>,
143        factory: impl Fn(&RuntimeConfigState) -> Box<dyn Lib> + Send + Sync + 'static,
144    ) -> Self {
145        self.add_host_factory_with_config(name, factory);
146        self
147    }
148
149    /// Replaces the crates.io resolver used for `crates.io:` sources.
150    pub fn with_crates_io_resolver(mut self, resolver: CratesIoResolver) -> Self {
151        self.crates_io = resolver;
152        self
153    }
154
155    /// Applies direct access to the session context, builder-style.
156    ///
157    /// Hosts use this to install context-level runtime support while keeping
158    /// concrete command behavior in loaded libraries.
159    pub fn with_context(mut self, configure: impl FnOnce(&mut Cx)) -> Self {
160        configure(&mut self.cx);
161        self
162    }
163
164    /// Grants a capability to the session's kernel context, builder-style.
165    ///
166    /// Loaders that require a capability (for example the native dynamic-library
167    /// loader requires `native_dynamic_load_capability()`) only succeed when the
168    /// host has granted it. This lets a composed `sim` build authorize the
169    /// loaders it registers.
170    pub fn with_capability(mut self, capability: CapabilityName) -> Self {
171        expect_granted!(self.seat.grant(&mut self.cx, capability));
172        self
173    }
174
175    /// Registers sources used when `verb` is selected without explicit loads.
176    pub fn add_default_verb_sources(
177        &mut self,
178        verb: impl Into<String>,
179        sources: Vec<LibSourceSpec>,
180    ) {
181        self.default_verb_sources.insert(verb.into(), sources);
182    }
183
184    /// Registers config libraries read when `verb` is selected without explicit
185    /// loads.
186    pub fn add_default_verb_config_libs(&mut self, verb: impl Into<String>, libs: Vec<Symbol>) {
187        self.default_verb_config_libs.insert(verb.into(), libs);
188    }
189
190    /// Registers sources used when `verb` is selected without explicit loads,
191    /// builder-style.
192    pub fn with_default_verb_sources(
193        mut self,
194        verb: impl Into<String>,
195        sources: Vec<LibSourceSpec>,
196    ) -> Self {
197        self.add_default_verb_sources(verb, sources);
198        self
199    }
200
201    /// Registers config libraries read when `verb` is selected without explicit
202    /// loads, builder-style.
203    pub fn with_default_verb_config_libs(
204        mut self,
205        verb: impl Into<String>,
206        libs: Vec<Symbol>,
207    ) -> Self {
208        self.add_default_verb_config_libs(verb, libs);
209        self
210    }
211
212    /// Returns the active kernel context.
213    pub fn cx(&self) -> &Cx {
214        &self.cx
215    }
216
217    pub(crate) fn cx_mut(&mut self) -> &mut Cx {
218        &mut self.cx
219    }
220
221    pub(crate) fn crates_io(&self) -> &CratesIoResolver {
222        &self.crates_io
223    }
224
225    pub(crate) fn hosts(&self) -> &HostLibRegistry {
226        &self.hosts
227    }
228
229    pub(crate) fn catalog_sources(&self) -> &BTreeMap<Symbol, LibSourceSpec> {
230        &self.catalog_sources
231    }
232
233    pub(crate) fn resolve_data_source(&self, source: KernelLibSourceSpec) -> KernelLibSourceSpec {
234        self.loaders.resolve_source_spec(&source)
235    }
236
237    pub(crate) fn inspect_data_source_manifest(
238        &mut self,
239        source: KernelLibSourceSpec,
240    ) -> Result<LibManifest, CliError> {
241        self.loaders
242            .inspect_manifest(&mut self.cx, source.into())
243            .map_err(|err| CliError::new(format!("inspect source: {err}")))
244    }
245
246    /// Returns load receipts in boot order.
247    pub fn receipts(&self) -> &[LoadReceipt] {
248        &self.receipts
249    }
250
251    /// Returns the runtime config state discovered during boot.
252    pub fn config_state(&self) -> &RuntimeConfigState {
253        &self.config
254    }
255
256    /// Returns the runtime config state mutably for host integration.
257    pub fn config_state_mut(&mut self) -> &mut RuntimeConfigState {
258        &mut self.config
259    }
260
261    /// Returns whether the optional native audio provider loaded successfully.
262    pub fn native_audio_provider_active(&self) -> bool {
263        self.native_audio_provider_active
264    }
265
266    /// Loads every requested library in the parsed boot controls.
267    pub fn load_boot(&mut self, boot: &CliBoot) -> Result<&[LoadReceipt], CliError> {
268        self.native_audio_provider_active = false;
269        let boot = self.boot_with_default_verb_sources(boot);
270        let config_libs = config_libs_for_boot(&boot, &self.default_verb_config_libs);
271        let codec_name = boot_codec_name(&boot);
272        let codec_symbol = codec_lib_symbol(codec_name);
273        let codec_index = self.boot_codec_source_index(&boot, &codec_symbol);
274        self.config = load_config_sources(&mut self.cx, &pre_site_config(&boot), &config_libs);
275        let preloaded_config_sources =
276            self.load_config_site_sources(&boot, codec_name, &codec_symbol, codec_index)?;
277        self.config = load_config_sources(&mut self.cx, &boot.config, &config_libs);
278        self.load_native_audio_provider(&boot);
279        match codec_index {
280            Some(index) if preloaded_config_sources.contains(&index) => {}
281            Some(index) => {
282                self.load_boot_codec_source(codec_name, &codec_symbol, &boot.loads[index])?;
283            }
284            None => {
285                self.load_boot_codec_source(
286                    codec_name,
287                    &codec_symbol,
288                    &LibSourceSpec::Symbol(codec_symbol.clone()),
289                )?;
290            }
291        }
292        for (index, source) in boot.loads.iter().enumerate() {
293            if Some(index) != codec_index && !preloaded_config_sources.contains(&index) {
294                self.load_source(source)?;
295            }
296        }
297        Ok(&self.receipts)
298    }
299
300    fn load_config_site_sources(
301        &mut self,
302        boot: &CliBoot,
303        codec_name: &str,
304        codec_symbol: &str,
305        codec_index: Option<usize>,
306    ) -> Result<BTreeSet<usize>, CliError> {
307        let mut preloaded = BTreeSet::new();
308        if boot.config.site_sources.is_empty() {
309            return Ok(preloaded);
310        }
311        for (index, source) in boot.loads.iter().enumerate() {
312            let Ok(manifest) = self.inspect_source_manifest(source) else {
313                continue;
314            };
315            if !manifest_exports_requested_site(&manifest, &boot.config.site_sources) {
316                continue;
317            }
318            if Some(index) == codec_index {
319                self.load_boot_codec_source(codec_name, codec_symbol, source)?;
320            } else {
321                self.load_source(source)?;
322            }
323            preloaded.insert(index);
324        }
325        Ok(preloaded)
326    }
327
328    fn load_native_audio_provider(&mut self, boot: &CliBoot) {
329        let Some(source) = boot.native_audio_provider.as_deref() else {
330            return;
331        };
332        match self.load_source_with_role(source, LoadReceiptRole::Library) {
333            Ok(_) => {
334                expect_granted!(
335                    self.seat
336                        .grant(&mut self.cx, native_audio_provider_capability())
337                );
338                self.native_audio_provider_active = true;
339            }
340            Err(err) => {
341                self.config
342                    .push_diagnostic(format!("native audio provider skipped: {err}"));
343                // Placement resolution keeps the modeled site live when a
344                // native provider is absent or rejected.
345            }
346        }
347    }
348
349    fn boot_with_default_verb_sources(&self, boot: &CliBoot) -> CliBoot {
350        if !boot.loads.is_empty() {
351            return boot.clone();
352        }
353        let Some(verb) = boot
354            .payload
355            .args
356            .first()
357            .map(|arg| arg.to_string_lossy().into_owned())
358        else {
359            return boot.clone();
360        };
361        let Some(sources) = self.default_verb_sources.get(&verb) else {
362            return boot.clone();
363        };
364        let mut boot = boot.clone();
365        boot.loads.clone_from(sources);
366        boot
367    }
368
369    fn boot_codec_source_index(&mut self, boot: &CliBoot, codec_symbol: &str) -> Option<usize> {
370        if let Some(index) = explicit_codec_source_index(boot, codec_symbol) {
371            return Some(index);
372        }
373
374        let codec_symbol = symbol_from_text(codec_symbol);
375        for (index, source) in boot.loads.iter().enumerate() {
376            // Best-effort manifest inspection lets path/bytes/crates.io sources
377            // supply the boot codec without blocking on unrelated load errors.
378            if self
379                .inspect_source_manifest(source)
380                .ok()
381                .is_some_and(|manifest| manifest_exports_codec(&manifest, &codec_symbol))
382            {
383                return Some(index);
384            }
385        }
386        None
387    }
388
389    /// Loads one source through the kernel loader and records a receipt.
390    pub fn load_source(&mut self, source: &LibSourceSpec) -> Result<LoadReceipt, CliError> {
391        self.load_source_with_role(source, LoadReceiptRole::Library)
392    }
393
394    fn inspect_source_manifest(&mut self, source: &LibSourceSpec) -> Result<LibManifest, CliError> {
395        match source {
396            LibSourceSpec::Host(name) => self.hosts.inspect_manifest(name, &self.config),
397            LibSourceSpec::CratesIo(spec) => {
398                let resolved = self.crates_io.resolve(spec)?;
399                let data_source = sim_run_loaders::path_source_spec(resolved.artifact);
400                ensure_loadable_path(&data_source, source)?;
401                self.inspect_data_source_manifest(data_source)
402            }
403            _ => {
404                let data_source = source
405                    .to_kernel_data_source()
406                    .expect("non-host sources have data forms");
407                ensure_loadable_path(&data_source, source)?;
408                self.inspect_data_source_manifest(data_source)
409            }
410        }
411    }
412
413    fn load_boot_codec_source(
414        &mut self,
415        codec_name: &str,
416        codec_symbol: &str,
417        source: &LibSourceSpec,
418    ) -> Result<LoadReceipt, CliError> {
419        let role = LoadReceiptRole::boot_codec(codec_name, codec_symbol);
420        match self.load_source_with_role(source, role.clone()) {
421            Ok(receipt) => Ok(receipt),
422            Err(_) if self.hosts.contains(codec_symbol) => {
423                self.load_source_with_role(&LibSourceSpec::Host(codec_symbol.to_owned()), role)
424            }
425            Err(err) => Err(no_codec_error(codec_name, err)),
426        }
427    }
428
429    fn load_source_with_role(
430        &mut self,
431        source: &LibSourceSpec,
432        role: LoadReceiptRole,
433    ) -> Result<LoadReceipt, CliError> {
434        if let LibSourceSpec::Host(name) = source {
435            return self.load_host_source(source, name, role);
436        }
437        if let LibSourceSpec::CratesIo(spec) = source {
438            return self.load_crates_io_source(source, spec, role);
439        }
440
441        let data_source = source
442            .to_kernel_data_source()
443            .expect("non-host sources have data forms");
444        ensure_loadable_path(&data_source, source)?;
445        let fallback = match source {
446            LibSourceSpec::Symbol(symbol) => fallback_spec_for_symbol(symbol),
447            _ => None,
448        };
449        match self.load_data_source(source, data_source, role.clone()) {
450            Ok(receipt) => Ok(receipt),
451            Err(err) => match fallback {
452                Some(spec) => {
453                    self.load_crates_io_source(&LibSourceSpec::CratesIo(spec.clone()), &spec, role)
454                }
455                None => Err(err),
456            },
457        }
458    }
459
460    fn load_data_source(
461        &mut self,
462        source: &LibSourceSpec,
463        data_source: KernelLibSourceSpec,
464        role: LoadReceiptRole,
465    ) -> Result<LoadReceipt, CliError> {
466        let receipt = self
467            .loaders
468            .load_and_register_with_receipt(&mut self.cx, data_source)
469            .map_err(|err| load_error(source, err))?;
470        let receipt = LoadReceipt {
471            lib_id: receipt.lib_id,
472            role,
473            requested_source: LibSourceSpec::from_kernel_data_source(receipt.requested_source),
474            resolved_source: LibSourceSpec::from_kernel_data_source(receipt.resolved_source),
475            manifest: receipt.manifest,
476            dependencies: receipt.dependencies,
477            exports: receipt.exports,
478        };
479        self.receipts.push(receipt.clone());
480        Ok(receipt)
481    }
482
483    fn load_crates_io_source(
484        &mut self,
485        source: &LibSourceSpec,
486        spec: &CratesIoSpec,
487        role: LoadReceiptRole,
488    ) -> Result<LoadReceipt, CliError> {
489        let resolved = self.crates_io.resolve(spec)?;
490        let data_source = sim_run_loaders::path_source_spec(resolved.artifact);
491        ensure_loadable_path(&data_source, source)?;
492        let receipt = self
493            .loaders
494            .load_and_register_with_receipt(&mut self.cx, data_source)
495            .map_err(|err| load_error(source, err))?;
496        let receipt = LoadReceipt {
497            lib_id: receipt.lib_id,
498            role,
499            requested_source: source.clone(),
500            resolved_source: LibSourceSpec::from_kernel_data_source(receipt.resolved_source),
501            manifest: receipt.manifest,
502            dependencies: receipt.dependencies,
503            exports: receipt.exports,
504        };
505        self.receipts.push(receipt.clone());
506        Ok(receipt)
507    }
508
509    /// Unloads a receipt's library through the kernel lifecycle path.
510    pub fn unload_receipt(&mut self, receipt: &LoadReceipt) -> Result<Vec<LibId>, CliError> {
511        self.cx.unload_lib(receipt.lib_id).map_err(|err| {
512            CliError::new(format!("unload failed for {}: {err}", receipt.manifest.id))
513        })
514    }
515
516    fn load_host_source(
517        &mut self,
518        source: &LibSourceSpec,
519        name: &str,
520        role: LoadReceiptRole,
521    ) -> Result<LoadReceipt, CliError> {
522        let lib = self.hosts.instantiate(name, &self.config)?;
523        let lib_id = self
524            .loaders
525            .load_and_register(&mut self.cx, KernelLibSource::Host(lib))
526            .map_err(|err| load_error(source, err))?;
527        let loaded = self
528            .cx
529            .registry()
530            .libs()
531            .iter()
532            .find(|loaded| loaded.id == lib_id)
533            .cloned()
534            .ok_or_else(|| CliError::new(format!("loaded lib id {lib_id:?} is not registered")))?;
535        let receipt = host_receipt(source.clone(), role, loaded, self.cx.registry().libs());
536        self.receipts.push(receipt.clone());
537        Ok(receipt)
538    }
539}
540
541fn catalog_source_spec(source: CatalogSource) -> LibSourceSpec {
542    match source {
543        CatalogSource::Open { kind, payload } => {
544            LibSourceSpec::from_kernel_data_source(KernelLibSourceSpec::Open { kind, payload })
545        }
546    }
547}
548
549fn config_libs_for_boot(
550    boot: &CliBoot,
551    default_verb_config_libs: &BTreeMap<String, Vec<Symbol>>,
552) -> Vec<Symbol> {
553    let codec_name = boot_codec_name(boot);
554    let mut libs = Vec::new();
555    push_unique_symbol(&mut libs, symbol_from_text(&codec_lib_symbol(codec_name)));
556    for source in &boot.loads {
557        if let Some(symbol) = config_lib_for_source(source) {
558            push_unique_symbol(&mut libs, symbol);
559        }
560    }
561    if let Some(source) = boot.native_audio_provider.as_deref()
562        && let Some(symbol) = config_lib_for_source(source)
563    {
564        push_unique_symbol(&mut libs, symbol);
565    }
566    if let Some(verb) = boot
567        .payload
568        .args
569        .first()
570        .and_then(|arg| arg.as_os_str().to_str().map(str::to_owned))
571        && let Some(symbols) = default_verb_config_libs.get(&verb)
572    {
573        for symbol in symbols {
574            push_unique_symbol(&mut libs, symbol.clone());
575        }
576    }
577    if let Some(request) = boot.config_report.as_ref() {
578        match &request.kind {
579            ConfigReportKind::Effective { lib } => push_unique_symbol(&mut libs, lib.clone()),
580            ConfigReportKind::Status | ConfigReportKind::Sources => {
581                for lib in representative_config_report_libs() {
582                    push_unique_symbol(&mut libs, lib);
583                }
584            }
585        }
586    }
587    libs
588}
589
590fn representative_config_report_libs() -> [Symbol; 3] {
591    [
592        Symbol::qualified("sim", "cookbook"),
593        Symbol::qualified("stream", "host"),
594        Symbol::qualified("model", "defaults"),
595    ]
596}
597
598fn config_lib_for_source(source: &LibSourceSpec) -> Option<Symbol> {
599    match source {
600        LibSourceSpec::Symbol(symbol) | LibSourceSpec::Host(symbol) => {
601            Some(symbol_from_text(symbol))
602        }
603        LibSourceSpec::Path(_)
604        | LibSourceSpec::Url(_)
605        | LibSourceSpec::Bytes(_)
606        | LibSourceSpec::Open { .. }
607        | LibSourceSpec::CratesIo(_) => None,
608    }
609}
610
611fn push_unique_symbol(symbols: &mut Vec<Symbol>, symbol: Symbol) {
612    if !symbols.iter().any(|existing| existing == &symbol) {
613        symbols.push(symbol);
614    }
615}
616
617impl Default for LoadSession {
618    fn default() -> Self {
619        Self::new()
620    }
621}
622
623fn ensure_loadable_path(
624    data_source: &KernelLibSourceSpec,
625    source: &LibSourceSpec,
626) -> Result<(), CliError> {
627    if let KernelLibSourceSpec::Open { kind, payload } = data_source
628        && kind == &sim_run_loaders::path_source_kind()
629        && let Ok(path) = sim_run_loaders::path_from_payload(payload)
630        && !path.exists()
631    {
632        return Err(CliError::new(format!(
633            "path source not found for {source}: {}",
634            path.display()
635        )));
636    }
637    Ok(())
638}
639
640fn load_error(source: &LibSourceSpec, err: KernelError) -> CliError {
641    CliError::new(format!("load failed for {source}: {err}"))
642}
643
644fn no_codec_error(codec_name: &str, err: CliError) -> CliError {
645    CliError::new(format!(
646        "no codec '{codec_name}' available; provide one with --load ({err})"
647    ))
648}
649
650fn manifest_exports_codec(manifest: &LibManifest, codec_symbol: &Symbol) -> bool {
651    manifest.exports.iter().any(|export| match export {
652        sim_kernel::Export::Codec { symbol, .. } => symbol == codec_symbol,
653        _ => false,
654    })
655}
656
657fn manifest_exports_requested_site(manifest: &LibManifest, sites: &[Symbol]) -> bool {
658    manifest.exports.iter().any(|export| match export {
659        sim_kernel::Export::Site { symbol, .. } => sites.iter().any(|site| site == symbol),
660        _ => false,
661    })
662}
663
664fn pre_site_config(boot: &CliBoot) -> crate::ConfigLoadOptions {
665    let mut config = boot.config.clone();
666    config.site_sources.clear();
667    config
668}