1use std::sync::Arc;
2
3use sim_kernel::{
4 AbiVersion, Args, Callable, Cx, Datum, Error, Export, Expr, Lib, LibManifest, LibTarget,
5 Linker, LoadCx, Object, ObjectCompat, Result, Symbol, Value, Version,
6};
7
8use crate::WorldProduct;
9
10pub const WORLD_VERB: &str = "world";
12
13#[derive(Clone)]
15pub struct WorldCommandLib {
16 product: WorldProduct,
17}
18
19impl WorldCommandLib {
20 pub fn new() -> std::result::Result<Self, crate::WorldError> {
22 Ok(Self {
23 product: WorldProduct::bundled()?,
24 })
25 }
26}
27
28impl Lib for WorldCommandLib {
29 fn manifest(&self) -> LibManifest {
30 LibManifest {
31 id: Symbol::qualified("lib", "world-command"),
32 version: Version(env!("CARGO_PKG_VERSION").into()),
33 abi: AbiVersion { major: 0, minor: 1 },
34 target: LibTarget::HostRegistered,
35 requires: Vec::new(),
36 capabilities: Vec::new(),
37 exports: vec![Export::Function {
38 symbol: sim_run_core::cli_main_entrypoint_symbol(WORLD_VERB),
39 function_id: None,
40 }],
41 }
42 }
43
44 fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
45 linker.function_value(
46 sim_run_core::cli_main_entrypoint_symbol(WORLD_VERB),
47 cx.factory().opaque(Arc::new(WorldEntrypoint {
48 product: self.product.clone(),
49 }))?,
50 )?;
51 Ok(())
52 }
53}
54
55#[derive(Clone)]
56struct WorldEntrypoint {
57 product: WorldProduct,
58}
59
60impl Object for WorldEntrypoint {
61 fn display(&self, _: &mut Cx) -> Result<String> {
62 Ok("cli/main/world".into())
63 }
64
65 fn as_any(&self) -> &dyn std::any::Any {
66 self
67 }
68}
69
70impl ObjectCompat for WorldEntrypoint {
71 fn as_callable(&self) -> Option<&dyn Callable> {
72 Some(self)
73 }
74}
75
76impl Callable for WorldEntrypoint {
77 fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
78 let argv = envelope_args(cx, args.values().first())?;
79 let value = match argv.get(1).map(String::as_str) {
80 Some("project") if argv.len() == 5 => self
81 .product
82 .project(&argv[2], &argv[3], Datum::String(argv[4].clone()), None)
83 .map(|projection| projection.value),
84 Some("diff") if argv.len() == 6 => self.product.diff(
85 &argv[2],
86 &argv[3],
87 Datum::String(argv[4].clone()),
88 Datum::String(argv[5].clone()),
89 ),
90 Some("why") if argv.len() == 4 => self.product.why(&argv[2], &argv[3]),
91 _ => {
92 return Err(Error::Eval(
93 "usage: sim world project KIND FACT VALUE | diff KIND FACT BEFORE AFTER | why CONCLUSION FACT".into(),
94 ));
95 }
96 }
97 .map_err(|error| Error::Eval(error.to_string()))?;
98 println!("{}", render(&value));
99 cx.factory().bool(true)
100 }
101}
102
103fn envelope_args(cx: &mut Cx, envelope: Option<&Value>) -> Result<Vec<String>> {
104 let envelope = envelope.ok_or_else(|| Error::Eval("missing world envelope".into()))?;
105 let table = envelope
106 .object()
107 .as_table_impl()
108 .ok_or_else(|| Error::Eval("world envelope is not a table".into()))?;
109 let value = table.get(cx, Symbol::new("args"))?;
110 let Expr::List(values) = value.object().as_expr(cx)? else {
111 return Err(Error::Eval("world args are not a list".into()));
112 };
113 values
114 .into_iter()
115 .map(|value| match value {
116 Expr::String(value) => Ok(value),
117 _ => Err(Error::Eval("world arg is not a string".into())),
118 })
119 .collect()
120}
121
122fn render(value: &Datum) -> String {
123 match value {
124 Datum::Nil => "nil".to_owned(),
125 Datum::Bool(value) => value.to_string(),
126 Datum::String(value) => format!("\"{}\"", value.replace('"', "\\\"")),
127 Datum::Symbol(value) => value.to_string(),
128 Datum::Vector(values) => format!(
129 "[{}]",
130 values.iter().map(render).collect::<Vec<_>>().join(" ")
131 ),
132 Datum::List(values) => format!(
133 "({})",
134 values.iter().map(render).collect::<Vec<_>>().join(" ")
135 ),
136 Datum::Node { tag, fields } => {
137 let fields = fields
138 .iter()
139 .map(|(name, value)| format!("({} {})", name, render(value)))
140 .collect::<Vec<_>>()
141 .join(" ");
142 format!("#({tag} {fields})")
143 }
144 other => format!("{other:?}"),
145 }
146}