Skip to main content

sim_kernel/library/
boot_codec.rs

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    /// Encodes the boot state as canonical SIM data.
18    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    /// Decodes a boot state from canonical SIM data.
32    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    /// Encodes the receipt as canonical SIM data.
50    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    /// Decodes the receipt from canonical SIM data.
76    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    /// Encodes the source spec as canonical SIM data.
120    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    /// Decodes the source spec from canonical SIM data.
132    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        // A pre-CodecSource manifest serialized the lisp codec as the closed
618        // symbol `lisp-source`; it must still load.
619        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}