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