1use std::path::PathBuf;
2
3use crate::{
4 AbiVersion, CapabilityName, ClassId, CodecId, Datum, Error, Export, ExportKind, ExportRecord,
5 ExportState, FunctionId, LibId, LibManifest, LibTarget, MacroId, NumberDomainId, Result,
6 RuntimeId, ShapeId, SiteId, Symbol, Version,
7};
8
9use super::{
10 boot::{LibBootDependency, LibBootReceipt, LibSourceSpec, RegistryBootState},
11 loaders::{CatalogSource, LibSource},
12};
13
14const BOOT_STATE_FORMAT: &str = "registry-boot-state-v1";
15
16impl RegistryBootState {
17 pub fn to_datum(&self) -> Datum {
19 node(
20 "registry-boot-state",
21 vec![
22 ("format", Datum::String(BOOT_STATE_FORMAT.to_owned())),
23 (
24 "receipts",
25 Datum::List(self.receipts.iter().map(LibBootReceipt::to_datum).collect()),
26 ),
27 ],
28 )
29 }
30
31 pub fn from_datum(datum: &Datum) -> Result<Self> {
33 let fields = expect_node(datum, "registry-boot-state")?;
34 let format = expect_string(required_field(fields, "format")?)?;
35 if format != BOOT_STATE_FORMAT {
36 return Err(Error::Lib(format!(
37 "unsupported registry boot state {format}"
38 )));
39 }
40 let receipts = expect_list(required_field(fields, "receipts")?)?
41 .iter()
42 .map(LibBootReceipt::from_datum)
43 .collect::<Result<Vec<_>>>()?;
44 Ok(Self { receipts })
45 }
46}
47
48impl LibBootReceipt {
49 pub fn to_datum(&self) -> Datum {
51 node(
52 "lib-boot-receipt",
53 vec![
54 ("lib-id", u32_datum(self.lib_id.0)),
55 ("requested-source", self.requested_source.to_datum()),
56 ("resolved-source", self.resolved_source.to_datum()),
57 ("manifest", manifest_datum(&self.manifest)),
58 (
59 "dependencies",
60 Datum::List(
61 self.dependencies
62 .iter()
63 .map(LibBootDependency::to_datum)
64 .collect(),
65 ),
66 ),
67 (
68 "exports",
69 Datum::List(self.exports.iter().map(export_record_datum).collect()),
70 ),
71 ],
72 )
73 }
74
75 pub fn from_datum(datum: &Datum) -> Result<Self> {
77 let fields = expect_node(datum, "lib-boot-receipt")?;
78 Ok(Self {
79 lib_id: LibId(expect_u32(required_field(fields, "lib-id")?)?),
80 requested_source: LibSourceSpec::from_datum(required_field(
81 fields,
82 "requested-source",
83 )?)?,
84 resolved_source: LibSourceSpec::from_datum(required_field(fields, "resolved-source")?)?,
85 manifest: manifest_from_datum(required_field(fields, "manifest")?)?,
86 dependencies: expect_list(required_field(fields, "dependencies")?)?
87 .iter()
88 .map(LibBootDependency::from_datum)
89 .collect::<Result<Vec<_>>>()?,
90 exports: expect_list(required_field(fields, "exports")?)?
91 .iter()
92 .map(export_record_from_datum)
93 .collect::<Result<Vec<_>>>()?,
94 })
95 }
96}
97
98impl LibBootDependency {
99 fn to_datum(&self) -> Datum {
100 node(
101 "lib-boot-dependency",
102 vec![
103 ("lib-id", u32_datum(self.lib_id.0)),
104 ("symbol", Datum::Symbol(self.symbol.clone())),
105 ],
106 )
107 }
108
109 fn from_datum(datum: &Datum) -> Result<Self> {
110 let fields = expect_node(datum, "lib-boot-dependency")?;
111 Ok(Self {
112 lib_id: LibId(expect_u32(required_field(fields, "lib-id")?)?),
113 symbol: expect_symbol(required_field(fields, "symbol")?)?.clone(),
114 })
115 }
116}
117
118impl LibSourceSpec {
119 pub fn to_datum(&self) -> Datum {
121 match self {
122 Self::Symbol(symbol) => source_node("symbol", Datum::Symbol(symbol.clone())),
123 Self::Path(path) => {
124 source_node("path", Datum::String(path.to_string_lossy().into_owned()))
125 }
126 Self::Url(url) => source_node("url", Datum::String(url.clone())),
127 Self::Bytes(bytes) => source_node("bytes", Datum::Bytes(bytes.clone())),
128 }
129 }
130
131 pub fn from_datum(datum: &Datum) -> Result<Self> {
133 let fields = expect_node(datum, "lib-source")?;
134 let kind = expect_symbol(required_field(fields, "kind")?)?;
135 let value = required_field(fields, "value")?;
136 match kind.name.as_ref() {
137 "symbol" if kind.namespace.is_none() => Ok(Self::Symbol(expect_symbol(value)?.clone())),
138 "path" if kind.namespace.is_none() => {
139 Ok(Self::Path(PathBuf::from(expect_string(value)?)))
140 }
141 "url" if kind.namespace.is_none() => Ok(Self::Url(expect_string(value)?.to_owned())),
142 "bytes" if kind.namespace.is_none() => Ok(Self::Bytes(expect_bytes(value)?.to_vec())),
143 _ => Err(Error::Lib(format!("unknown lib source kind {kind}"))),
144 }
145 }
146}
147
148impl From<CatalogSource> for LibSourceSpec {
149 fn from(source: CatalogSource) -> Self {
150 match source {
151 CatalogSource::Path(path) => Self::Path(path),
152 CatalogSource::Url(url) => Self::Url(url),
153 CatalogSource::Bytes(bytes) => Self::Bytes(bytes),
154 }
155 }
156}
157
158impl From<LibSourceSpec> for LibSource {
159 fn from(source: LibSourceSpec) -> Self {
160 match source {
161 LibSourceSpec::Symbol(symbol) => Self::Symbol(symbol),
162 LibSourceSpec::Path(path) => Self::Path(path),
163 LibSourceSpec::Url(url) => Self::Url(url),
164 LibSourceSpec::Bytes(bytes) => Self::Bytes(bytes),
165 }
166 }
167}
168
169impl TryFrom<LibSource> for LibSourceSpec {
170 type Error = Error;
171
172 fn try_from(source: LibSource) -> Result<Self> {
173 match source {
174 LibSource::Symbol(symbol) => Ok(Self::Symbol(symbol)),
175 LibSource::Path(path) => Ok(Self::Path(path)),
176 LibSource::Url(url) => Ok(Self::Url(url)),
177 LibSource::Bytes(bytes) => Ok(Self::Bytes(bytes)),
178 LibSource::Host(_) => Err(Error::Lib(
179 "host lib sources are live values, not boot data".to_owned(),
180 )),
181 }
182 }
183}
184
185fn manifest_datum(manifest: &LibManifest) -> Datum {
186 node(
187 "lib-manifest",
188 vec![
189 ("id", Datum::Symbol(manifest.id.clone())),
190 ("version", Datum::String(manifest.version.0.clone())),
191 ("abi-major", u16_datum(manifest.abi.major)),
192 ("abi-minor", u16_datum(manifest.abi.minor)),
193 ("target", Datum::Symbol(manifest.target.to_symbol())),
194 (
195 "requires",
196 Datum::List(manifest.requires.iter().map(dependency_datum).collect()),
197 ),
198 (
199 "capabilities",
200 Datum::List(
201 manifest
202 .capabilities
203 .iter()
204 .map(|capability| Datum::String(capability.as_str().to_owned()))
205 .collect(),
206 ),
207 ),
208 (
209 "exports",
210 Datum::List(manifest.exports.iter().map(export_datum).collect()),
211 ),
212 ],
213 )
214}
215
216fn manifest_from_datum(datum: &Datum) -> Result<LibManifest> {
217 let fields = expect_node(datum, "lib-manifest")?;
218 Ok(LibManifest {
219 id: expect_symbol(required_field(fields, "id")?)?.clone(),
220 version: Version(expect_string(required_field(fields, "version")?)?.to_owned()),
221 abi: AbiVersion {
222 major: expect_u16(required_field(fields, "abi-major")?)?,
223 minor: expect_u16(required_field(fields, "abi-minor")?)?,
224 },
225 target: LibTarget::from_symbol(expect_symbol(required_field(fields, "target")?)?),
226 requires: expect_list(required_field(fields, "requires")?)?
227 .iter()
228 .map(dependency_from_datum)
229 .collect::<Result<Vec<_>>>()?,
230 capabilities: expect_list(required_field(fields, "capabilities")?)?
231 .iter()
232 .map(|datum| Ok(CapabilityName::new(expect_string(datum)?.to_owned())))
233 .collect::<Result<Vec<_>>>()?,
234 exports: expect_list(required_field(fields, "exports")?)?
235 .iter()
236 .map(export_from_datum)
237 .collect::<Result<Vec<_>>>()?,
238 })
239}
240
241fn dependency_datum(dependency: &crate::Dependency) -> Datum {
242 node(
243 "dependency",
244 vec![
245 ("id", Datum::Symbol(dependency.id.clone())),
246 (
247 "minimum-version",
248 dependency
249 .minimum_version
250 .as_ref()
251 .map(|version| Datum::String(version.0.clone()))
252 .unwrap_or(Datum::Nil),
253 ),
254 ],
255 )
256}
257
258fn dependency_from_datum(datum: &Datum) -> Result<crate::Dependency> {
259 let fields = expect_node(datum, "dependency")?;
260 let minimum_version = match required_field(fields, "minimum-version")? {
261 Datum::Nil => None,
262 other => Some(Version(expect_string(other)?.to_owned())),
263 };
264 Ok(crate::Dependency {
265 id: expect_symbol(required_field(fields, "id")?)?.clone(),
266 minimum_version,
267 })
268}
269
270fn export_datum(export: &Export) -> Datum {
271 let (kind, symbol, stable_id) = match export {
272 Export::Class { symbol, class_id } => ("class", symbol, class_id.map(|id| id.0)),
273 Export::Function {
274 symbol,
275 function_id,
276 } => ("function", symbol, function_id.map(|id| id.0)),
277 Export::Macro { symbol, macro_id } => ("macro", symbol, macro_id.map(|id| id.0)),
278 Export::Shape { symbol, shape_id } => ("shape", symbol, shape_id.map(|id| id.0)),
279 Export::Codec { symbol, codec_id } => ("codec", symbol, codec_id.map(|id| id.0)),
280 Export::NumberDomain {
281 symbol,
282 number_domain_id,
283 } => ("number-domain", symbol, number_domain_id.map(|id| id.0)),
284 Export::Value { symbol } => ("value", symbol, None),
285 Export::Site { symbol, runtime_id } => {
286 let stable_id = match runtime_id {
287 Some(RuntimeId::Site(id)) => Some(id.0),
288 _ => None,
289 };
290 ("site", symbol, stable_id)
291 }
292 };
293 node(
294 "export",
295 vec![
296 ("kind", Datum::Symbol(Symbol::new(kind))),
297 ("symbol", Datum::Symbol(symbol.clone())),
298 ("stable-id", stable_id.map(u32_datum).unwrap_or(Datum::Nil)),
299 ],
300 )
301}
302
303fn export_from_datum(datum: &Datum) -> Result<Export> {
304 let fields = expect_node(datum, "export")?;
305 let kind = expect_symbol(required_field(fields, "kind")?)?;
306 let symbol = expect_symbol(required_field(fields, "symbol")?)?.clone();
307 let stable_id = optional_u32(required_field(fields, "stable-id")?)?;
308 match kind.name.as_ref() {
309 "class" if kind.namespace.is_none() => Ok(Export::Class {
310 symbol,
311 class_id: stable_id.map(ClassId),
312 }),
313 "function" if kind.namespace.is_none() => Ok(Export::Function {
314 symbol,
315 function_id: stable_id.map(FunctionId),
316 }),
317 "macro" if kind.namespace.is_none() => Ok(Export::Macro {
318 symbol,
319 macro_id: stable_id.map(MacroId),
320 }),
321 "shape" if kind.namespace.is_none() => Ok(Export::Shape {
322 symbol,
323 shape_id: stable_id.map(ShapeId),
324 }),
325 "codec" if kind.namespace.is_none() => Ok(Export::Codec {
326 symbol,
327 codec_id: stable_id.map(CodecId),
328 }),
329 "number-domain" if kind.namespace.is_none() => Ok(Export::NumberDomain {
330 symbol,
331 number_domain_id: stable_id.map(NumberDomainId),
332 }),
333 "value" if kind.namespace.is_none() => Ok(Export::Value { symbol }),
334 "site" if kind.namespace.is_none() => Ok(Export::Site {
335 symbol,
336 runtime_id: stable_id.map(|id| RuntimeId::Site(SiteId(id))),
337 }),
338 _ => Err(Error::Lib(format!("unknown export kind {kind}"))),
339 }
340}
341
342fn export_record_datum(record: &ExportRecord) -> Datum {
343 node(
344 "export-record",
345 vec![
346 ("kind", Datum::Symbol(record.kind.symbol().clone())),
347 ("symbol", Datum::Symbol(record.symbol.clone())),
348 ("state", export_state_datum(&record.state)),
349 ],
350 )
351}
352
353fn export_record_from_datum(datum: &Datum) -> Result<ExportRecord> {
354 let fields = expect_node(datum, "export-record")?;
355 Ok(ExportRecord {
356 kind: ExportKind::new(expect_symbol(required_field(fields, "kind")?)?.clone()),
357 symbol: expect_symbol(required_field(fields, "symbol")?)?.clone(),
358 state: export_state_from_datum(required_field(fields, "state")?)?,
359 })
360}
361
362fn export_state_datum(state: &ExportState) -> Datum {
363 match state {
364 ExportState::Resolved { id } => node(
365 "export-state",
366 vec![
367 ("kind", Datum::Symbol(Symbol::new("resolved"))),
368 ("runtime-id", runtime_id_datum(*id)),
369 ],
370 ),
371 ExportState::Declared => state_node("declared", Datum::Nil),
372 ExportState::Unsupported { reason } => {
373 state_node("unsupported", Datum::String(reason.clone()))
374 }
375 ExportState::Invalid { error } => state_node("invalid", Datum::String(error.clone())),
376 }
377}
378
379fn export_state_from_datum(datum: &Datum) -> Result<ExportState> {
380 let fields = expect_node(datum, "export-state")?;
381 let kind = expect_symbol(required_field(fields, "kind")?)?;
382 let value = fields
383 .iter()
384 .find_map(|(field, value)| (field.name.as_ref() == "value").then_some(value));
385 match kind.name.as_ref() {
386 "resolved" if kind.namespace.is_none() => Ok(ExportState::Resolved {
387 id: runtime_id_from_datum(required_field(fields, "runtime-id")?)?,
388 }),
389 "declared" if kind.namespace.is_none() => Ok(ExportState::Declared),
390 "unsupported" if kind.namespace.is_none() => Ok(ExportState::Unsupported {
391 reason: expect_string(value.unwrap_or(&Datum::Nil))?.to_owned(),
392 }),
393 "invalid" if kind.namespace.is_none() => Ok(ExportState::Invalid {
394 error: expect_string(value.unwrap_or(&Datum::Nil))?.to_owned(),
395 }),
396 _ => Err(Error::Lib(format!("unknown export state {kind}"))),
397 }
398}
399
400fn runtime_id_datum(id: RuntimeId) -> Datum {
401 let (kind, value) = match id {
402 RuntimeId::Class(id) => ("class", Some(id.0)),
403 RuntimeId::Function(id) => ("function", Some(id.0)),
404 RuntimeId::Macro(id) => ("macro", Some(id.0)),
405 RuntimeId::Shape(id) => ("shape", Some(id.0)),
406 RuntimeId::Codec(id) => ("codec", Some(id.0)),
407 RuntimeId::NumberDomain(id) => ("number-domain", Some(id.0)),
408 RuntimeId::Site(id) => ("site", Some(id.0)),
409 RuntimeId::Value => ("value", None),
410 };
411 node(
412 "runtime-id",
413 vec![
414 ("kind", Datum::Symbol(Symbol::new(kind))),
415 ("value", value.map(u32_datum).unwrap_or(Datum::Nil)),
416 ],
417 )
418}
419
420fn runtime_id_from_datum(datum: &Datum) -> Result<RuntimeId> {
421 let fields = expect_node(datum, "runtime-id")?;
422 let kind = expect_symbol(required_field(fields, "kind")?)?;
423 let value = optional_u32(required_field(fields, "value")?)?;
424 match kind.name.as_ref() {
425 "class" if kind.namespace.is_none() => {
426 Ok(RuntimeId::Class(ClassId(required_id(value, kind)?)))
427 }
428 "function" if kind.namespace.is_none() => {
429 Ok(RuntimeId::Function(FunctionId(required_id(value, kind)?)))
430 }
431 "macro" if kind.namespace.is_none() => {
432 Ok(RuntimeId::Macro(MacroId(required_id(value, kind)?)))
433 }
434 "shape" if kind.namespace.is_none() => {
435 Ok(RuntimeId::Shape(ShapeId(required_id(value, kind)?)))
436 }
437 "codec" if kind.namespace.is_none() => {
438 Ok(RuntimeId::Codec(CodecId(required_id(value, kind)?)))
439 }
440 "number-domain" if kind.namespace.is_none() => Ok(RuntimeId::NumberDomain(NumberDomainId(
441 required_id(value, kind)?,
442 ))),
443 "site" if kind.namespace.is_none() => {
444 Ok(RuntimeId::Site(SiteId(required_id(value, kind)?)))
445 }
446 "value" if kind.namespace.is_none() => Ok(RuntimeId::Value),
447 _ => Err(Error::Lib(format!("unknown runtime id kind {kind}"))),
448 }
449}
450
451fn required_id(value: Option<u32>, kind: &Symbol) -> Result<u32> {
452 value.ok_or_else(|| Error::Lib(format!("runtime id kind {kind} requires an id")))
453}
454
455fn source_node(kind: &'static str, value: Datum) -> Datum {
456 node(
457 "lib-source",
458 vec![("kind", Datum::Symbol(Symbol::new(kind))), ("value", value)],
459 )
460}
461
462fn state_node(kind: &'static str, value: Datum) -> Datum {
463 node(
464 "export-state",
465 vec![("kind", Datum::Symbol(Symbol::new(kind))), ("value", value)],
466 )
467}
468
469fn node(tag: &'static str, fields: Vec<(&'static str, Datum)>) -> Datum {
470 Datum::Node {
471 tag: Symbol::qualified("library", tag),
472 fields: fields
473 .into_iter()
474 .map(|(field, value)| (Symbol::new(field), value))
475 .collect(),
476 }
477}
478
479fn expect_node<'a>(datum: &'a Datum, tag: &'static str) -> Result<&'a [(Symbol, Datum)]> {
480 let Datum::Node {
481 tag: actual,
482 fields,
483 } = datum
484 else {
485 return Err(Error::TypeMismatch {
486 expected: "datum node",
487 found: datum_kind(datum),
488 });
489 };
490 let expected = Symbol::qualified("library", tag);
491 if actual != &expected {
492 return Err(Error::Lib(format!(
493 "expected datum node {expected}, found {actual}"
494 )));
495 }
496 Ok(fields)
497}
498
499fn required_field<'a>(fields: &'a [(Symbol, Datum)], name: &'static str) -> Result<&'a Datum> {
500 fields
501 .iter()
502 .find_map(|(field, value)| {
503 (field.namespace.is_none() && field.name.as_ref() == name).then_some(value)
504 })
505 .ok_or_else(|| Error::Lib(format!("missing boot datum field {name}")))
506}
507
508fn expect_list(datum: &Datum) -> Result<&[Datum]> {
509 match datum {
510 Datum::List(items) => Ok(items),
511 other => Err(Error::TypeMismatch {
512 expected: "datum list",
513 found: datum_kind(other),
514 }),
515 }
516}
517
518fn expect_symbol(datum: &Datum) -> Result<&Symbol> {
519 match datum {
520 Datum::Symbol(symbol) => Ok(symbol),
521 other => Err(Error::TypeMismatch {
522 expected: "datum symbol",
523 found: datum_kind(other),
524 }),
525 }
526}
527
528fn expect_string(datum: &Datum) -> Result<&str> {
529 match datum {
530 Datum::String(value) => Ok(value),
531 other => Err(Error::TypeMismatch {
532 expected: "datum string",
533 found: datum_kind(other),
534 }),
535 }
536}
537
538fn expect_bytes(datum: &Datum) -> Result<&[u8]> {
539 match datum {
540 Datum::Bytes(value) => Ok(value),
541 other => Err(Error::TypeMismatch {
542 expected: "datum bytes",
543 found: datum_kind(other),
544 }),
545 }
546}
547
548fn u16_datum(value: u16) -> Datum {
549 Datum::String(value.to_string())
550}
551
552fn u32_datum(value: u32) -> Datum {
553 Datum::String(value.to_string())
554}
555
556fn expect_u16(datum: &Datum) -> Result<u16> {
557 expect_string(datum)?
558 .parse::<u16>()
559 .map_err(|err| Error::Lib(format!("invalid u16 boot datum: {err}")))
560}
561
562fn expect_u32(datum: &Datum) -> Result<u32> {
563 expect_string(datum)?
564 .parse::<u32>()
565 .map_err(|err| Error::Lib(format!("invalid u32 boot datum: {err}")))
566}
567
568fn optional_u32(datum: &Datum) -> Result<Option<u32>> {
569 match datum {
570 Datum::Nil => Ok(None),
571 other => expect_u32(other).map(Some),
572 }
573}
574
575fn datum_kind(datum: &Datum) -> &'static str {
576 match datum {
577 Datum::Nil => "datum nil",
578 Datum::Bool(_) => "datum bool",
579 Datum::Number(_) => "datum number",
580 Datum::Symbol(_) => "datum symbol",
581 Datum::String(_) => "datum string",
582 Datum::Bytes(_) => "datum bytes",
583 Datum::List(_) => "datum list",
584 Datum::Vector(_) => "datum vector",
585 Datum::Map(_) => "datum map",
586 Datum::Set(_) => "datum set",
587 Datum::Node { .. } => "datum node",
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 fn sample_manifest(target: LibTarget) -> LibManifest {
596 LibManifest {
597 id: Symbol::qualified("demo", "lib"),
598 version: Version("1.0.0".to_owned()),
599 abi: AbiVersion { major: 0, minor: 1 },
600 target,
601 requires: Vec::new(),
602 capabilities: Vec::new(),
603 exports: Vec::new(),
604 }
605 }
606
607 #[test]
608 fn codec_source_target_round_trips_through_the_manifest_codec() {
609 let target = LibTarget::CodecSource(Symbol::qualified("codec", "lisp"));
610 let manifest = sample_manifest(target.clone());
611 let decoded = manifest_from_datum(&manifest_datum(&manifest)).expect("decode manifest");
612 assert_eq!(decoded.target, target);
613 }
614
615 #[test]
616 fn legacy_lisp_source_tag_still_decodes_to_codec_source() {
617 let datum = manifest_datum(&sample_manifest(LibTarget::HostRegistered));
620 let Datum::Node { tag, fields } = datum else {
621 panic!("manifest datum is a node");
622 };
623 let patched = Datum::Node {
624 tag,
625 fields: fields
626 .into_iter()
627 .map(|(field, value)| {
628 if field.name.as_ref() == "target" {
629 (field, Datum::Symbol(Symbol::new("lisp-source")))
630 } else {
631 (field, value)
632 }
633 })
634 .collect(),
635 };
636 let decoded = manifest_from_datum(&patched).expect("decode legacy manifest");
637 assert_eq!(
638 decoded.target,
639 LibTarget::CodecSource(Symbol::qualified("codec", "lisp"))
640 );
641 }
642
643 #[test]
644 fn closed_targets_round_trip() {
645 for target in [
646 LibTarget::Native,
647 LibTarget::WasmComponent,
648 LibTarget::DataOnly,
649 LibTarget::HostRegistered,
650 ] {
651 let manifest = sample_manifest(target.clone());
652 let decoded = manifest_from_datum(&manifest_datum(&manifest)).expect("decode manifest");
653 assert_eq!(decoded.target, target);
654 }
655 }
656}