Skip to main content

sim_lib_compute_cli/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3//! Loadable compute command surface.
4//!
5//! The crate exports `cli/main/compute` as a host-registered callable. It
6//! inspects installed compute sites and profile storage supplied as a Table/Dir
7//! value by the embedding host; it never opens filesystem paths or creates a
8//! separate bootstrap.
9
10mod args;
11mod envelope;
12mod evidence;
13mod render;
14
15use std::sync::Arc;
16
17use sim_kernel::{
18    AbiVersion, Args, Callable, CapabilityName, Cx, Error, Export, Lib, LibManifest, LibTarget,
19    Linker, LoadCx, Object, ObjectCompat, Result, Symbol, Value, Version,
20};
21
22pub use args::{ComputeCommand, OutputMode, ProfileAction, parse_compute_args};
23pub use render::help;
24
25use crate::{
26    envelope::envelope_args,
27    evidence::{profile_evidence, provider_rows, recipe_evidence},
28    render::{render_profile, render_providers, render_recipe},
29};
30
31/// Capability required for device inspection and probe evidence.
32pub fn compute_device_capability() -> CapabilityName {
33    CapabilityName::new("compute.device")
34}
35
36/// Capability required for profile reads.
37pub fn compute_profile_read_capability() -> CapabilityName {
38    CapabilityName::new("compute.profile.read")
39}
40
41/// Capability required for profile writes.
42pub fn compute_profile_write_capability() -> CapabilityName {
43    CapabilityName::new("compute.profile.write")
44}
45
46/// Runtime library symbol for the compute CLI.
47pub fn compute_cli_lib_symbol() -> Symbol {
48    Symbol::qualified("lib", "compute-cli")
49}
50
51/// Symbol exported by the compute command entrypoint.
52pub fn compute_entrypoint_symbol() -> Symbol {
53    Symbol::qualified("cli", "main/compute")
54}
55
56/// Error returned by compute command parsing and rendering.
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct ComputeCliError {
59    message: String,
60}
61
62impl ComputeCliError {
63    /// Builds an error from a user-facing message.
64    pub fn new(message: impl Into<String>) -> Self {
65        Self {
66            message: message.into(),
67        }
68    }
69
70    fn from_kernel(error: Error) -> Self {
71        Self::new(error.to_string())
72    }
73}
74
75impl std::fmt::Display for ComputeCliError {
76    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        formatter.write_str(&self.message)
78    }
79}
80
81impl std::error::Error for ComputeCliError {}
82
83/// Host-registered library that exports the compute command entrypoint.
84#[derive(Clone, Default)]
85pub struct ComputeCliLib {
86    profile_store: Option<Value>,
87}
88
89impl ComputeCliLib {
90    /// Builds the library without profile storage.
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// Builds the library with caller-supplied Table/Dir profile storage.
96    pub fn with_profile_store(profile_store: Value) -> Self {
97        Self {
98            profile_store: Some(profile_store),
99        }
100    }
101}
102
103impl Lib for ComputeCliLib {
104    fn manifest(&self) -> LibManifest {
105        LibManifest {
106            id: compute_cli_lib_symbol(),
107            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
108            abi: AbiVersion { major: 0, minor: 1 },
109            target: LibTarget::HostRegistered,
110            requires: Vec::new(),
111            capabilities: Vec::new(),
112            exports: vec![Export::Function {
113                symbol: compute_entrypoint_symbol(),
114                function_id: None,
115            }],
116        }
117    }
118
119    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
120        linker.function_value(
121            compute_entrypoint_symbol(),
122            cx.factory().opaque(Arc::new(ComputeEntrypoint {
123                profile_store: self.profile_store.clone(),
124            }))?,
125        )?;
126        Ok(())
127    }
128}
129
130#[derive(Clone)]
131struct ComputeEntrypoint {
132    profile_store: Option<Value>,
133}
134
135impl Object for ComputeEntrypoint {
136    fn display(&self, _cx: &mut Cx) -> Result<String> {
137        Ok("cli/main/compute".to_owned())
138    }
139
140    fn as_any(&self) -> &dyn std::any::Any {
141        self
142    }
143}
144
145impl ObjectCompat for ComputeEntrypoint {
146    fn as_callable(&self) -> Option<&dyn Callable> {
147        Some(self)
148    }
149}
150
151impl Callable for ComputeEntrypoint {
152    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
153        let Some(envelope) = args.values().first() else {
154            return Err(Error::Eval("missing compute envelope".to_owned()));
155        };
156        let args = envelope_args(cx, envelope)?;
157        let command = parse_compute_args(&args).map_err(|err| Error::Eval(err.to_string()))?;
158        let output = run_command(cx, self.profile_store.as_ref(), &command)
159            .map_err(|err| Error::Eval(err.to_string()))?;
160        print!("{output}");
161        cx.factory().bool(true)
162    }
163}
164
165/// Runs a parsed compute command and returns rendered output.
166pub fn run_command(
167    cx: &mut Cx,
168    profile_store: Option<&Value>,
169    command: &ComputeCommand,
170) -> std::result::Result<String, ComputeCliError> {
171    match command {
172        ComputeCommand::Help => Ok(help().to_owned()),
173        ComputeCommand::Devices(selection) | ComputeCommand::Probe(selection) => {
174            cx.require(&compute_device_capability())
175                .map_err(ComputeCliError::from_kernel)?;
176            Ok(render_providers(command, &provider_rows(cx, selection)))
177        }
178        ComputeCommand::Profile(request) => {
179            let capability = match request.action {
180                ProfileAction::Save => compute_profile_write_capability(),
181                ProfileAction::List | ProfileAction::Read => compute_profile_read_capability(),
182            };
183            cx.require(&capability)
184                .map_err(ComputeCliError::from_kernel)?;
185            let evidence = profile_evidence(cx, profile_store, request)?;
186            Ok(render_profile(command, &evidence))
187        }
188        ComputeCommand::Explain(request) => {
189            cx.require(&compute_device_capability())
190                .map_err(ComputeCliError::from_kernel)?;
191            cx.require(&compute_profile_read_capability())
192                .map_err(ComputeCliError::from_kernel)?;
193            let evidence = profile_evidence(cx, profile_store, request)?;
194            Ok(render_profile(command, &evidence))
195        }
196        ComputeCommand::Recipe(_) => {
197            cx.require(&compute_device_capability())
198                .map_err(ComputeCliError::from_kernel)?;
199            Ok(render_recipe(command, &recipe_evidence()))
200        }
201    }
202}
203
204/// Cookbook recipes for this lib, embedded at build time.
205pub static RECIPES: sim_cookbook::EmbeddedDir =
206    include!(concat!(env!("OUT_DIR"), "/cookbook_recipes.rs"));
207
208#[cfg(test)]
209mod tests;