sim-run-core 0.1.0

Core command entry API for the SIM bootloader.
Documentation
use std::{fs, path::PathBuf};

use sim_kernel::{
    AbiVersion, CatalogSource, Cx, Error as KernelError, Export, Lib, LibLoader, LibManifest,
    LibTarget, Linker, LoadCx, Symbol, Version,
    library::{LibSource as KernelLibSource, LibTarget::HostRegistered},
};

use crate::{CliBoot, CratesIoResolver, LibSourceSpec, LoadReceiptRole, LoadSession, Payload};

fn codec_manifest(id: &str, codec_name: &str, target: LibTarget) -> LibManifest {
    LibManifest {
        id: Symbol::new(id),
        version: Version("0.1.0".to_owned()),
        abi: AbiVersion { major: 0, minor: 1 },
        target,
        requires: Vec::new(),
        capabilities: Vec::new(),
        exports: vec![Export::Codec {
            symbol: Symbol::qualified("codec", codec_name),
            codec_id: None,
        }],
    }
}

fn value_manifest(id: &str, export: &str, target: LibTarget) -> LibManifest {
    LibManifest {
        id: Symbol::new(id),
        version: Version("0.1.0".to_owned()),
        abi: AbiVersion { major: 0, minor: 1 },
        target,
        requires: Vec::new(),
        capabilities: Vec::new(),
        exports: vec![Export::Value {
            symbol: Symbol::new(export),
        }],
    }
}

struct FixtureCodecLib {
    manifest: LibManifest,
    codec_symbol: Symbol,
}

impl FixtureCodecLib {
    fn new(id: &str, codec_name: &str, target: LibTarget) -> Self {
        Self {
            manifest: codec_manifest(id, codec_name, target),
            codec_symbol: Symbol::qualified("codec", codec_name),
        }
    }
}

impl Lib for FixtureCodecLib {
    fn manifest(&self) -> LibManifest {
        self.manifest.clone()
    }

    fn load(&self, cx: &mut LoadCx, linker: &mut Linker) -> sim_kernel::Result<()> {
        linker
            .codec_value(self.codec_symbol.clone(), cx.factory().bool(true).unwrap())
            .map(|_| ())
    }
}

struct FixtureValueLib {
    manifest: LibManifest,
    export: Symbol,
}

impl FixtureValueLib {
    fn new(id: &str, export: &str, target: LibTarget) -> Self {
        Self {
            manifest: value_manifest(id, export, target),
            export: Symbol::new(export),
        }
    }
}

impl Lib for FixtureValueLib {
    fn manifest(&self) -> LibManifest {
        self.manifest.clone()
    }

    fn load(&self, cx: &mut LoadCx, linker: &mut Linker) -> sim_kernel::Result<()> {
        linker.value(self.export.clone(), cx.factory().bool(true).unwrap())
    }
}

struct ArtifactLoader;

impl LibLoader for ArtifactLoader {
    fn can_load(&self, source: &KernelLibSource) -> bool {
        matches!(source, KernelLibSource::Bytes(_) | KernelLibSource::Path(_))
    }

    fn load(&self, _cx: &mut Cx, source: KernelLibSource) -> sim_kernel::Result<Box<dyn Lib>> {
        let bytes = match source {
            KernelLibSource::Bytes(bytes) => bytes,
            KernelLibSource::Path(path) => {
                fs::read(path).map_err(|err| KernelError::Lib(format!("read artifact: {err}")))?
            }
            _ => return Err(KernelError::Lib("unsupported fixture source".to_owned())),
        };
        match bytes.as_slice() {
            b"codec-lisp" => Ok(Box::new(FixtureCodecLib::new(
                "codec-lisp",
                "lisp",
                LibTarget::DataOnly,
            ))),
            b"codec-json" => Ok(Box::new(FixtureCodecLib::new(
                "codec-json",
                "json",
                LibTarget::DataOnly,
            ))),
            b"codec-test-crate" => Ok(Box::new(FixtureCodecLib::new(
                "codec-test-crate",
                "test",
                LibTarget::DataOnly,
            ))),
            b"codec-catalog" => Ok(Box::new(FixtureCodecLib::new(
                "codec-catalog",
                "lisp",
                LibTarget::DataOnly,
            ))),
            b"ordinary-lib" => Ok(Box::new(FixtureValueLib::new(
                "ordinary-lib",
                "ordinary-value",
                LibTarget::DataOnly,
            ))),
            _ => Err(KernelError::Lib("artifact rejected".to_owned())),
        }
    }
}

#[test]
fn default_lisp_boots_from_crates_io_cache_and_records_role() {
    let cache = temp_cache("default-lisp-codec");
    let artifact = temp_artifact("default-lisp-codec");
    fs::write(&artifact, b"codec-lisp").unwrap();
    let resolver = CratesIoResolver::new(cache.clone()).with_registry_artifact(
        "sim-codec-lisp",
        "0.1.0",
        artifact.clone(),
    );
    let mut session = LoadSession::new()
        .with_loader(ArtifactLoader)
        .with_crates_io_resolver(resolver);

    let receipts = session.load_boot(&CliBoot::default()).unwrap().to_vec();

    assert_eq!(receipts.len(), 1);
    assert_eq!(
        receipts[0].role,
        LoadReceiptRole::BootCodec {
            name: "lisp".to_owned(),
            symbol: "codec/lisp".to_owned(),
        }
    );
    assert_eq!(
        receipts[0].requested_source,
        LibSourceSpec::CratesIo("sim-codec-lisp@^0.1".parse().unwrap())
    );
    assert_eq!(receipts[0].manifest.id, Symbol::new("codec-lisp"));
    assert!(
        session
            .cx()
            .registry()
            .codec_by_symbol(&Symbol::qualified("codec", "lisp"))
            .is_some()
    );
    let _ = fs::remove_dir_all(cache);
    let _ = fs::remove_file(artifact);
}

#[test]
fn codec_override_uses_named_codec() {
    let cache = temp_cache("json-codec");
    let artifact = temp_artifact("json-codec");
    fs::write(&artifact, b"codec-json").unwrap();
    let resolver = CratesIoResolver::new(cache.clone()).with_registry_artifact(
        "sim-codec-json",
        "0.1.0",
        artifact.clone(),
    );
    let boot = CliBoot {
        codec: Some("json".to_owned()),
        ..CliBoot::default()
    };
    let mut session = LoadSession::new()
        .with_loader(ArtifactLoader)
        .with_crates_io_resolver(resolver);

    let receipts = session.load_boot(&boot).unwrap().to_vec();

    assert_eq!(
        receipts[0].role,
        LoadReceiptRole::BootCodec {
            name: "json".to_owned(),
            symbol: "codec/json".to_owned(),
        }
    );
    assert_eq!(
        receipts[0].requested_source,
        LibSourceSpec::CratesIo("sim-codec-json@^0.1".parse().unwrap())
    );
    assert!(
        session
            .cx()
            .registry()
            .codec_by_symbol(&Symbol::qualified("codec", "json"))
            .is_some()
    );
    let _ = fs::remove_dir_all(cache);
    let _ = fs::remove_file(artifact);
}

#[test]
fn missing_codec_has_clear_error() {
    let cache = temp_cache("missing-codec");
    let mut session =
        LoadSession::new().with_crates_io_resolver(CratesIoResolver::new(cache.clone()));

    let err = session.load_boot(&CliBoot::default()).unwrap_err();

    assert_eq!(
        err.to_string(),
        "no codec 'lisp' available; provide one with --load \
         (crates.io network fetch is not implemented (cache-only resolver); \
         seed the cache for sim-codec-lisp@^0.1)"
    );
    let _ = fs::remove_dir_all(cache);
}

#[test]
fn explicit_host_codec_loads_before_other_sources() {
    let cache = temp_cache("explicit-host-codec");
    let artifact = temp_artifact("explicit-host-codec");
    fs::write(&artifact, b"codec-test-crate").unwrap();
    let resolver = CratesIoResolver::new(cache.clone()).with_registry_artifact(
        "sim-codec-test",
        "0.1.0",
        artifact.clone(),
    );
    let boot = CliBoot {
        codec: Some("test".to_owned()),
        loads: vec![
            LibSourceSpec::Host("codec/test".to_owned()),
            LibSourceSpec::Bytes(b"ordinary-lib".to_vec()),
        ],
        native_audio_provider: None,
        list: false,
        inspect: None,
        payload: Payload::default(),
    };
    let mut session = LoadSession::new()
        .with_loader(ArtifactLoader)
        .with_catalog_source(
            "codec/test",
            CatalogSource::Bytes(b"codec-catalog".to_vec()),
        )
        .with_crates_io_resolver(resolver)
        .with_host_factory("codec/test", || {
            Box::new(FixtureCodecLib::new(
                "codec-test-host",
                "test",
                HostRegistered,
            ))
        });

    let receipts = session.load_boot(&boot).unwrap().to_vec();

    assert_eq!(receipts.len(), 2);
    assert_eq!(
        receipts[0].requested_source,
        LibSourceSpec::Host("codec/test".to_owned())
    );
    assert_eq!(receipts[0].manifest.id, Symbol::new("codec-test-host"));
    assert_eq!(receipts[1].role, LoadReceiptRole::Library);
    assert_eq!(receipts[1].manifest.id, Symbol::new("ordinary-lib"));
    assert!(
        session
            .cx()
            .registry()
            .value_by_symbol(&Symbol::new("ordinary-value"))
            .is_some()
    );
    let _ = fs::remove_dir_all(cache);
    let _ = fs::remove_file(artifact);
}

#[test]
fn catalog_source_precedes_crates_io_and_host_fallbacks() {
    let cache = temp_cache("catalog-codec");
    let artifact = temp_artifact("catalog-codec");
    fs::write(&artifact, b"codec-lisp").unwrap();
    let resolver = CratesIoResolver::new(cache.clone()).with_registry_artifact(
        "sim-codec-lisp",
        "0.1.0",
        artifact.clone(),
    );
    let mut session = LoadSession::new()
        .with_loader(ArtifactLoader)
        .with_catalog_source(
            "codec/lisp",
            CatalogSource::Bytes(b"codec-catalog".to_vec()),
        )
        .with_crates_io_resolver(resolver)
        .with_host_factory("codec/lisp", || {
            Box::new(FixtureCodecLib::new(
                "codec-lisp-host",
                "lisp",
                HostRegistered,
            ))
        });

    let receipts = session.load_boot(&CliBoot::default()).unwrap().to_vec();

    assert_eq!(
        receipts[0].requested_source,
        LibSourceSpec::Symbol("codec/lisp".to_owned())
    );
    assert_eq!(
        receipts[0].resolved_source,
        LibSourceSpec::Bytes(b"codec-catalog".to_vec())
    );
    assert_eq!(receipts[0].manifest.id, Symbol::new("codec-catalog"));
    let _ = fs::remove_dir_all(cache);
    let _ = fs::remove_file(artifact);
}

#[test]
fn bundled_host_codec_is_last_fallback() {
    let mut session = LoadSession::new().with_host_factory("codec/lisp", || {
        Box::new(FixtureCodecLib::new(
            "codec-lisp-host",
            "lisp",
            HostRegistered,
        ))
    });

    let receipts = session.load_boot(&CliBoot::default()).unwrap().to_vec();

    assert_eq!(
        receipts[0].requested_source,
        LibSourceSpec::Host("codec/lisp".to_owned())
    );
    assert_eq!(receipts[0].manifest.id, Symbol::new("codec-lisp-host"));
}

fn temp_artifact(label: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "sim-run-core-codec-{}-{label}.artifact",
        std::process::id()
    ))
}

fn temp_cache(label: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "sim-run-core-codec-cache-{}-{label}",
        std::process::id()
    ))
}