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 receipts: Vec<LoadReceipt>,
32 config: RuntimeConfigState,
33}
34
35impl LoadSession {
36 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 pub fn add_loader(&mut self, loader: impl LibLoader + 'static) {
56 self.loaders.add_loader(loader);
57 }
58
59 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 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 pub fn with_loader(mut self, loader: impl LibLoader + 'static) -> Self {
75 self.add_loader(loader);
76 self
77 }
78
79 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 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 pub fn with_crates_io_resolver(mut self, resolver: CratesIoResolver) -> Self {
100 self.crates_io = resolver;
101 self
102 }
103
104 pub fn with_context(mut self, configure: impl FnOnce(&mut Cx)) -> Self {
109 configure(&mut self.cx);
110 self
111 }
112
113 pub fn with_capability(mut self, capability: CapabilityName) -> Self {
120 self.seat.grant(&mut self.cx, capability);
121 self
122 }
123
124 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 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 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 pub fn receipts(&self) -> &[LoadReceipt] {
180 &self.receipts
181 }
182
183 pub fn config_state(&self) -> &RuntimeConfigState {
185 &self.config
186 }
187
188 pub fn config_state_mut(&mut self) -> &mut RuntimeConfigState {
190 &mut self.config
191 }
192
193 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 }
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 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 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}