sim-run-core 0.2.0

Core command entry API for the SIM bootloader.
Documentation
use std::sync::Arc;

use sim_config::{ConfigDir, ConfigRoots, ConfigSource, ConfigView};
use sim_kernel::{
    AbiVersion, Args, Callable, Cx, DefaultFactory, Error, Export, Expr, Lib, LibManifest,
    LibTarget, Linker, LoadCx, NoopEvalPolicy, Object, ObjectCompat, Result, Symbol, Value,
    Version,
};

use crate::{
    CliBoot, ConfigLoadOptions, LibSourceSpec, LoadSession, SourceStatus, load_config_sources,
};

fn lib(namespace: &str, name: &str) -> Symbol {
    Symbol::qualified(namespace, name)
}

fn test_cx() -> Cx {
    Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory))
}

fn site_config_dir_expr(target: &Symbol) -> Expr {
    ConfigDir::one(
        target.clone(),
        Expr::Map(vec![(
            Expr::Symbol(Symbol::new("mode")),
            Expr::String("site".to_owned()),
        )]),
    )
    .unwrap()
    .to_expr()
}

#[derive(Clone)]
struct ConfigDirSite {
    site: Symbol,
    dir: Expr,
}

impl ConfigDirSite {
    fn new(site: Symbol, dir: Expr) -> Self {
        Self { site, dir }
    }
}

impl Object for ConfigDirSite {
    fn display(&self, _cx: &mut Cx) -> Result<String> {
        Ok(format!("#<config-site {}>", self.site))
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

impl ObjectCompat for ConfigDirSite {
    fn as_callable(&self) -> Option<&dyn Callable> {
        Some(self)
    }

    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
        Ok(Expr::Symbol(self.site.clone()))
    }
}

impl Callable for ConfigDirSite {
    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
        let [request] = args.values() else {
            return Err(Error::Eval("config site expects one request".to_owned()));
        };
        let request = request.object().as_expr(cx)?;
        if request != Expr::Symbol(Symbol::qualified("config", "dir")) {
            return Err(Error::Eval(format!(
                "unsupported config site request {request:?}"
            )));
        }
        cx.factory().expr(self.dir.clone())
    }
}

struct BasicLib {
    id: Symbol,
}

impl BasicLib {
    fn new(id: &str) -> Self {
        Self {
            id: crate::source::symbol_from_text(id),
        }
    }
}

impl Lib for BasicLib {
    fn manifest(&self) -> LibManifest {
        LibManifest {
            id: self.id.clone(),
            version: Version("0.1.0".to_owned()),
            abi: AbiVersion { major: 0, minor: 1 },
            target: LibTarget::HostRegistered,
            requires: Vec::new(),
            capabilities: Vec::new(),
            exports: Vec::new(),
        }
    }

    fn load(&self, _cx: &mut LoadCx, _linker: &mut Linker<'_>) -> Result<()> {
        Ok(())
    }
}

struct ConfigSiteLib {
    id: Symbol,
    site: Symbol,
    dir: Expr,
}

impl ConfigSiteLib {
    fn new(site: Symbol, dir: Expr) -> Self {
        Self {
            id: lib("config", "site-lib"),
            site,
            dir,
        }
    }
}

impl Lib for ConfigSiteLib {
    fn manifest(&self) -> LibManifest {
        LibManifest {
            id: self.id.clone(),
            version: Version("0.1.0".to_owned()),
            abi: AbiVersion { major: 0, minor: 1 },
            target: LibTarget::HostRegistered,
            requires: Vec::new(),
            capabilities: Vec::new(),
            exports: vec![Export::Site {
                symbol: self.site.clone(),
                runtime_id: None,
            }],
        }
    }

    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
        let site = cx.factory().opaque(Arc::new(ConfigDirSite::new(
            self.site.clone(),
            self.dir.clone(),
        )))?;
        linker.site_value(self.site.clone(), site).map(|_| ())
    }
}

#[test]
fn site_backed_dir_calls_config_dir_operation() {
    let mut cx = test_cx();
    let cookbook = lib("sim", "cookbook");
    let site = lib("config", "runtime");
    let site_value = cx
        .factory()
        .opaque(Arc::new(ConfigDirSite::new(
            site.clone(),
            site_config_dir_expr(&cookbook),
        )))
        .unwrap();
    cx.registry_mut()
        .register_site_value(site.clone(), site_value)
        .unwrap();
    let opts = ConfigLoadOptions {
        roots: ConfigRoots::new(None, std::env::temp_dir().join("sim-run-site-unused")),
        read_files: false,
        single_file: None,
        site_sources: vec![site.clone()],
    };

    let state = load_config_sources(&mut cx, &opts, &[]);

    assert!(state.diagnostics().is_empty(), "{:?}", state.diagnostics());
    assert!(matches!(
        state.layers().first().unwrap().source,
        ConfigSource::Site { site: ref layer_site } if layer_site == &site
    ));
    let table = state.effective().dir.table(&cookbook).unwrap();
    assert_eq!(ConfigView::new(table).string("mode"), Some("site"));
}

#[test]
fn config_site_loaded_by_boot_source_is_visible_before_config_factory() {
    let app = lib("app", "serve");
    let site = lib("config", "runtime");
    let site_for_factory = site.clone();
    let app_for_factory = app.clone();
    let boot = CliBoot {
        config: ConfigLoadOptions {
            roots: ConfigRoots::new(None, std::env::temp_dir().join("sim-run-site-boot-unused")),
            read_files: false,
            single_file: None,
            site_sources: vec![site.clone()],
        },
        payload: crate::Payload {
            args: vec!["serve".into()],
            ..crate::Payload::default()
        },
        ..CliBoot::default()
    };
    let mut session = LoadSession::new()
        .with_host_factory("codec/lisp", || Box::new(BasicLib::new("codec/lisp")))
        .with_host_factory("config/site", move || {
            Box::new(ConfigSiteLib::new(
                site_for_factory.clone(),
                site_config_dir_expr(&app_for_factory),
            ))
        })
        .with_host_factory_with_config("app/serve", |config| {
            let id = if config.effective().dir.table(&lib("app", "serve")).is_some() {
                "app/configured"
            } else {
                "app/unconfigured"
            };
            Box::new(BasicLib::new(id))
        })
        .with_default_verb_sources(
            "serve",
            vec![
                LibSourceSpec::Host("config/site".to_owned()),
                LibSourceSpec::Host("app/serve".to_owned()),
            ],
        );

    session.load_boot(&boot).unwrap();

    assert!(session.receipts().iter().any(|receipt| {
        receipt.requested_source == LibSourceSpec::Host("app/serve".to_owned())
            && receipt.manifest.id == lib("app", "configured")
    }));
    assert!(session.config_state().source_reports().iter().any(|row| {
        matches!(row.source, ConfigSource::Site { ref site } if site == &lib("config", "runtime"))
            && row.status == SourceStatus::Found
    }));
    let table = session.config_state().effective().dir.table(&app).unwrap();
    assert_eq!(ConfigView::new(table).string("mode"), Some("site"));
}