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