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
19pub struct LoadSession {
21 cx: Cx,
22 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 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 pub fn add_loader(&mut self, loader: impl LibLoader + 'static) {
58 self.loaders.add_loader(loader);
59 }
60
61 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 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 pub fn with_loader(mut self, loader: impl LibLoader + 'static) -> Self {
77 self.add_loader(loader);
78 self
79 }
80
81 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 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 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 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 pub fn with_crates_io_resolver(mut self, resolver: CratesIoResolver) -> Self {
122 self.crates_io = resolver;
123 self
124 }
125
126 pub fn with_context(mut self, configure: impl FnOnce(&mut Cx)) -> Self {
131 configure(&mut self.cx);
132 self
133 }
134
135 pub fn with_capability(mut self, capability: CapabilityName) -> Self {
142 self.seat.grant(&mut self.cx, capability);
143 self
144 }
145
146 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 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 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 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 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 pub fn receipts(&self) -> &[LoadReceipt] {
219 &self.receipts
220 }
221
222 pub fn config_state(&self) -> &RuntimeConfigState {
224 &self.config
225 }
226
227 pub fn config_state_mut(&mut self) -> &mut RuntimeConfigState {
229 &mut self.config
230 }
231
232 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 }
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 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 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}