Skip to main content

sim_run_core/
load.rs

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