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