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
23pub struct LoadSession {
25 cx: Cx,
26 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 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 pub fn add_loader(&mut self, loader: impl LibLoader + 'static) {
87 self.loaders.add_loader(loader);
88 }
89
90 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 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 pub fn with_loader(mut self, loader: impl LibLoader + 'static) -> Self {
106 self.add_loader(loader);
107 self
108 }
109
110 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 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 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 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 pub fn with_crates_io_resolver(mut self, resolver: CratesIoResolver) -> Self {
151 self.crates_io = resolver;
152 self
153 }
154
155 pub fn with_context(mut self, configure: impl FnOnce(&mut Cx)) -> Self {
160 configure(&mut self.cx);
161 self
162 }
163
164 pub fn with_capability(mut self, capability: CapabilityName) -> Self {
171 expect_granted!(self.seat.grant(&mut self.cx, capability));
172 self
173 }
174
175 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 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 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 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 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 pub fn receipts(&self) -> &[LoadReceipt] {
248 &self.receipts
249 }
250
251 pub fn config_state(&self) -> &RuntimeConfigState {
253 &self.config
254 }
255
256 pub fn config_state_mut(&mut self) -> &mut RuntimeConfigState {
258 &mut self.config
259 }
260
261 pub fn native_audio_provider_active(&self) -> bool {
263 self.native_audio_provider_active
264 }
265
266 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 }
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 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 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 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}