Skip to main content

sim_lib_world/
command.rs

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
10/// CLI verb contributed by this loaded library.
11pub const WORLD_VERB: &str = "world";
12
13/// Loadable read-only `sim world` command library.
14#[derive(Clone)]
15pub struct WorldCommandLib {
16    product: WorldProduct,
17}
18
19impl WorldCommandLib {
20    /// Constructs the command library with the qualified bundled providers.
21    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}