use std::{ffi::OsString, path::PathBuf, sync::Arc};
use sim_kernel::{
AbiVersion, Callable, Cx, Error, Export, Lib, LibLoader, LibManifest, LibTarget, Linker,
LoadCx, Object, ObjectCompat, Symbol, Value, Version, library::LibSource as KernelLibSource,
object::Args,
};
use crate::{
Bootloader, CliBoot, LibSourceSpec, LoadSession, Payload,
handoff::{CLI_MAIN_ENTRYPOINT, cli_main_entrypoint_symbol},
};
fn manifest(id: &str, exports: Vec<Export>) -> LibManifest {
LibManifest {
id: Symbol::new(id),
version: Version("0.1.0".to_owned()),
abi: AbiVersion { major: 0, minor: 1 },
target: LibTarget::HostRegistered,
requires: Vec::new(),
capabilities: Vec::new(),
exports,
}
}
fn cli_main_export(symbol: Symbol) -> Export {
Export::Function {
symbol,
function_id: None,
}
}
fn codec_export(name: &str) -> Export {
Export::Codec {
symbol: Symbol::qualified("codec", name),
codec_id: None,
}
}
struct FixtureLib {
manifest: LibManifest,
codec: Option<String>,
entrypoints: Vec<(Symbol, FixtureEntrypoint)>,
}
impl FixtureLib {
fn codec(name: &str, entrypoint: Option<FixtureEntrypoint>) -> Self {
let entrypoint_symbol = cli_main_entrypoint_symbol(&format!("codec-{name}"));
let mut exports = vec![codec_export(name)];
if entrypoint.is_some() {
exports.push(cli_main_export(entrypoint_symbol.clone()));
}
Self {
manifest: manifest(&format!("codec-{name}"), exports),
codec: Some(name.to_owned()),
entrypoints: entrypoint
.into_iter()
.map(|entrypoint| (entrypoint_symbol.clone(), entrypoint))
.collect(),
}
}
fn app(id: &str, entrypoint: FixtureEntrypoint) -> Self {
let entrypoint_symbol = cli_main_entrypoint_symbol(id);
Self::with_entrypoints(id, vec![(entrypoint_symbol, entrypoint)])
}
fn app_for_verb(id: &str, verb: &str, entrypoint: FixtureEntrypoint) -> Self {
Self::with_entrypoints(id, vec![(cli_main_entrypoint_symbol(verb), entrypoint)])
}
fn generic(id: &str, entrypoint: FixtureEntrypoint) -> Self {
Self::with_entrypoints(id, vec![(Symbol::new(CLI_MAIN_ENTRYPOINT), entrypoint)])
}
fn with_entrypoints(id: &str, entrypoints: Vec<(Symbol, FixtureEntrypoint)>) -> Self {
Self {
manifest: manifest(
id,
entrypoints
.iter()
.map(|(symbol, _)| cli_main_export(symbol.clone()))
.collect(),
),
codec: None,
entrypoints,
}
}
fn plain(id: &str) -> Self {
Self {
manifest: manifest(id, Vec::new()),
codec: None,
entrypoints: Vec::new(),
}
}
}
impl Lib for FixtureLib {
fn manifest(&self) -> LibManifest {
self.manifest.clone()
}
fn load(&self, cx: &mut LoadCx, linker: &mut Linker) -> sim_kernel::Result<()> {
if let Some(codec) = &self.codec {
linker.codec_value(
Symbol::qualified("codec", codec.as_str()),
cx.factory().bool(true)?,
)?;
}
for (symbol, entrypoint) in &self.entrypoints {
linker.function_value(
symbol.clone(),
cx.factory().opaque(Arc::new(entrypoint.clone()))?,
)?;
}
Ok(())
}
}
struct VerbCatalogLoader;
impl LibLoader for VerbCatalogLoader {
fn can_load(&self, source: &KernelLibSource) -> bool {
sim_run_loaders::bytes_from_source(source).is_ok_and(|bytes| bytes.is_some())
}
fn load(&self, _cx: &mut Cx, source: KernelLibSource) -> sim_kernel::Result<Box<dyn Lib>> {
let Some(bytes) = sim_run_loaders::bytes_from_source(&source)? else {
return Err(Error::Lib("verb catalog loader needs bytes".to_owned()));
};
let payload = String::from_utf8(bytes)
.map_err(|err| Error::Lib(format!("verb catalog bytes are not UTF-8: {err}")))?;
let (verb, expected_args) = catalog_payload(payload);
let manifest_id = verb.clone();
let expected_verb: &'static str = Box::leak(verb.clone().into_boxed_str());
let expected_args = expected_args
.into_iter()
.map(|arg| Box::leak(arg.into_boxed_str()) as &'static str)
.collect();
Ok(Box::new(FixtureLib::app(
&manifest_id,
FixtureEntrypoint::echo(ExpectedEnvelope {
codec: "codec/test",
verb: expected_verb,
args: expected_args,
eval: "nil",
script: "nil",
stdin: "nil",
}),
)))
}
}
fn catalog_payload(payload: String) -> (String, Vec<String>) {
let mut parts = payload.split('\0').map(str::to_owned).collect::<Vec<_>>();
let verb = parts.remove(0);
let args = if parts.is_empty() {
vec![verb.clone(), "--dry-run".to_owned()]
} else {
parts
};
(verb, args)
}
fn catalog_bytes(verb: &str, args: &[&str]) -> Vec<u8> {
std::iter::once(verb)
.chain(args.iter().copied())
.collect::<Vec<_>>()
.join("\0")
.into_bytes()
}
#[derive(Clone)]
struct FixtureEntrypoint {
mode: EntrypointMode,
}
impl FixtureEntrypoint {
fn success() -> Self {
Self {
mode: EntrypointMode::Return(true),
}
}
fn failure() -> Self {
Self {
mode: EntrypointMode::Return(false),
}
}
fn echo(expected: ExpectedEnvelope) -> Self {
Self {
mode: EntrypointMode::Echo(expected),
}
}
}
#[derive(Clone)]
enum EntrypointMode {
Return(bool),
Echo(ExpectedEnvelope),
}
impl Object for FixtureEntrypoint {
fn display(&self, _cx: &mut Cx) -> sim_kernel::Result<String> {
Ok(CLI_MAIN_ENTRYPOINT.to_owned())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ObjectCompat for FixtureEntrypoint {
fn as_callable(&self) -> Option<&dyn Callable> {
Some(self)
}
}
impl Callable for FixtureEntrypoint {
fn call(&self, cx: &mut Cx, args: Args) -> sim_kernel::Result<Value> {
match &self.mode {
EntrypointMode::Return(success) => cx.factory().bool(*success),
EntrypointMode::Echo(expected) => {
let Some(envelope) = args.values().first() else {
return Err(Error::Eval("missing envelope argument".to_owned()));
};
let matched = expected.matches(cx, envelope)?;
cx.factory().bool(matched)
}
}
}
}
#[derive(Clone)]
struct ExpectedEnvelope {
codec: &'static str,
verb: &'static str,
args: Vec<&'static str>,
eval: &'static str,
script: &'static str,
stdin: &'static str,
}
impl ExpectedEnvelope {
fn matches(&self, cx: &mut Cx, envelope: &Value) -> sim_kernel::Result<bool> {
let args = table_value(cx, envelope, "args")?;
Ok(table_display(cx, envelope, "codec")? == self.codec
&& table_display(cx, envelope, "verb")? == self.verb
&& list_displays(cx, &args)? == self.args
&& table_display(cx, envelope, "eval")? == self.eval
&& table_display(cx, envelope, "script")? == self.script
&& table_display(cx, envelope, "stdin")? == self.stdin)
}
}
#[test]
fn explicit_loaded_lib_entrypoint_wins_over_codec_entrypoint() {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Host("app/success".to_owned())],
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || {
Box::new(FixtureLib::codec(
"test",
Some(FixtureEntrypoint::failure()),
))
})
.with_host_factory("app/success", || {
Box::new(FixtureLib::app("app-success", FixtureEntrypoint::success()))
});
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0);
}
#[test]
fn exact_verb_entrypoint_wins_within_loaded_library() {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Host("app/multi".to_owned())],
payload: Payload {
args: vec![OsString::from("repl")],
..Payload::default()
},
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_host_factory("app/multi", || {
Box::new(FixtureLib::with_entrypoints(
"app-multi",
vec![
(
cli_main_entrypoint_symbol("serve"),
FixtureEntrypoint::failure(),
),
(
cli_main_entrypoint_symbol("repl"),
FixtureEntrypoint::success(),
),
],
))
});
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0);
}
#[test]
fn generic_entrypoint_fallback_runs_when_exact_verb_is_absent() {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Host("app/generic".to_owned())],
payload: Payload {
args: vec![OsString::from("repl")],
..Payload::default()
},
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_host_factory("app/generic", || {
Box::new(FixtureLib::with_entrypoints(
"app-generic",
vec![
(
Symbol::new(CLI_MAIN_ENTRYPOINT),
FixtureEntrypoint::success(),
),
(
cli_main_entrypoint_symbol("serve"),
FixtureEntrypoint::failure(),
),
],
))
});
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0);
}
#[test]
fn default_verb_sources_load_when_verb_has_no_explicit_loads() {
let boot = CliBoot {
codec: Some("test".to_owned()),
payload: Payload {
args: vec![OsString::from("repl")],
..Payload::default()
},
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_host_factory("app/repl", || {
Box::new(FixtureLib::app_for_verb(
"app-repl",
"repl",
FixtureEntrypoint::success(),
))
})
.with_default_verb_sources("repl", vec![LibSourceSpec::Host("app/repl".to_owned())]);
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0);
assert_eq!(session.receipts().len(), 2);
assert_eq!(session.receipts()[1].manifest.id, Symbol::new("app-repl"));
}
#[test]
fn explicit_loads_override_default_verb_sources() {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Host("app/explicit".to_owned())],
payload: Payload {
args: vec![OsString::from("repl")],
..Payload::default()
},
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_host_factory("app/default", || {
Box::new(FixtureLib::app_for_verb(
"app-default",
"repl",
FixtureEntrypoint::failure(),
))
})
.with_host_factory("app/explicit", || {
Box::new(FixtureLib::app_for_verb(
"app-explicit",
"repl",
FixtureEntrypoint::success(),
))
})
.with_default_verb_sources("repl", vec![LibSourceSpec::Host("app/default".to_owned())]);
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0);
assert_eq!(session.receipts().len(), 2);
assert_eq!(
session.receipts()[1].manifest.id,
Symbol::new("app-explicit")
);
}
#[test]
fn boot_codec_entrypoint_is_fallback() {
let boot = CliBoot {
codec: Some("test".to_owned()),
..CliBoot::default()
};
let mut session = LoadSession::new().with_host_factory("codec/test", || {
Box::new(FixtureLib::codec(
"test",
Some(FixtureEntrypoint::success()),
))
});
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0);
}
#[test]
fn false_result_maps_to_failure_exit_code() {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Host("app/fail".to_owned())],
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_host_factory("app/fail", || {
Box::new(FixtureLib::app("app-fail", FixtureEntrypoint::failure()))
});
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 1);
}
#[test]
fn entrypoint_receives_cli_envelope_value() {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Host("app/echo".to_owned())],
native_audio_provider: None,
config: crate::ConfigLoadOptions::default(),
list: false,
inspect: None,
config_report: None,
payload: Payload {
args: vec![
OsString::from("run"),
OsString::from("--port"),
OsString::from("9"),
],
eval: Some("(+ 1 2)".to_owned()),
script: Some(PathBuf::from("demo.sim")),
stdin: Some("input".to_owned()),
},
};
let expected = ExpectedEnvelope {
codec: "codec/test",
verb: "run",
args: vec!["run", "--port", "9"],
eval: "(+ 1 2)",
script: "demo.sim",
stdin: "input",
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_host_factory("app/echo", move || {
Box::new(FixtureLib::generic(
"app-echo",
FixtureEntrypoint::echo(expected.clone()),
))
});
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0);
}
#[test]
fn target_verbs_dispatch_from_symbol_loaded_libs_without_baked_cli() {
for verb in ["server", "atelier", "cookbook", "browse", "agent", "forge"] {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Symbol(verb.to_owned())],
payload: Payload {
args: vec![OsString::from(verb), OsString::from("--dry-run")],
..Payload::default()
},
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_loader(VerbCatalogLoader)
.with_catalog_source(
verb,
sim_run_loaders::catalog_bytes_source(verb.as_bytes().to_vec()),
);
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0, "{verb} should dispatch to its loaded lib");
}
}
#[test]
fn forge_verb_boots_through_bootloader() {
let expected = ExpectedEnvelope {
codec: "codec/lisp",
verb: "forge",
args: vec!["forge", "lift", "summarize the contract"],
eval: "nil",
script: "nil",
stdin: "nil",
};
let code = Bootloader::standard()
.host_lib("codec/lisp", || Box::new(FixtureLib::codec("lisp", None)))
.host_verb("forge", "app/forge", move || {
Box::new(FixtureLib::app(
"forge",
FixtureEntrypoint::echo(expected.clone()),
))
})
.run(["sim", "forge", "lift", "summarize the contract"])
.unwrap();
assert_eq!(code, 0);
}
#[test]
fn documented_command_equivalents_preserve_mode_payloads() {
let cases: &[(&str, &str, &[&str])] = &[
(
"server-start",
"server",
&["server", "--port", "4500", "--serve", "agent://planner"],
),
("server-eval", "server", &["server", "--eval", "(+ 1 2)"]),
(
"server-trigger",
"server",
&["server", "--trigger", "cron://daily"],
),
(
"atelier-shell",
"atelier",
&[
"atelier",
"--control-root",
".",
"--cache",
".sim/atelier/shell.json",
],
),
("cookbook-list", "cookbook", &["cookbook", "list"]),
(
"cookbook-run",
"cookbook",
&["cookbook", "run", "codec/lisp/hello-world"],
),
];
for (label, verb, args) in cases {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Symbol((*verb).to_owned())],
payload: Payload {
args: args.iter().map(OsString::from).collect(),
..Payload::default()
},
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_loader(VerbCatalogLoader)
.with_catalog_source(
*verb,
sim_run_loaders::catalog_bytes_source(catalog_bytes(verb, args)),
);
let code = session.run_loaded_boot(&boot).unwrap();
assert_eq!(code, 0, "{label} should dispatch through loaded lib");
}
}
#[test]
fn missing_entrypoint_lists_loaded_libs_and_load_hint() {
let boot = CliBoot {
codec: Some("test".to_owned()),
loads: vec![LibSourceSpec::Host("app/plain".to_owned())],
..CliBoot::default()
};
let mut session = LoadSession::new()
.with_host_factory("codec/test", || Box::new(FixtureLib::codec("test", None)))
.with_host_factory("app/plain", || Box::new(FixtureLib::plain("app-plain")));
let err = session.run_loaded_boot(&boot).unwrap_err().to_string();
assert!(err.contains("no loaded lib claims cli/main"));
assert!(err.contains("codec-test"));
assert!(err.contains("app-plain"));
assert!(err.contains("load one with --load"));
}
fn table_value(cx: &mut Cx, table: &Value, field: &str) -> sim_kernel::Result<Value> {
let Some(table) = table.object().as_table_impl() else {
return Err(Error::Eval("envelope is not a table".to_owned()));
};
table.get(cx, Symbol::new(field))
}
fn table_display(cx: &mut Cx, table: &Value, field: &str) -> sim_kernel::Result<String> {
let value = table_value(cx, table, field)?;
value.object().display(cx)
}
fn list_displays(cx: &mut Cx, value: &Value) -> sim_kernel::Result<Vec<String>> {
let Some(list) = value.object().as_list() else {
return Err(Error::Eval("field is not a list".to_owned()));
};
list.to_vec(cx, Some(16))?
.into_iter()
.map(|value| value.object().display(cx))
.collect()
}