Skip to main content

sim_run_core/
introspect.rs

1use sim_kernel::{Args, ExportKind, ExportRecord, ExportState, LibManifest, Symbol};
2
3use crate::{
4    CliBoot, CliError, LibSourceSpec, LoadReceipt, LoadReceiptRole, LoadSession,
5    crates_io::{CratesIoListing, CratesIoListingSource, fallback_spec_for_symbol},
6    source::symbol_from_text,
7};
8
9const CLI_LIST_ENTRYPOINT: &str = "cli/list";
10const CLI_INSPECT_ENTRYPOINT: &str = "cli/inspect";
11
12impl LoadSession {
13    /// Loads a boot session and returns list/inspect output for loader metadata.
14    ///
15    /// Listing or inspecting loaded libraries does not require a boot codec. When
16    /// the codec is unavailable, fall back to loading just the explicit `--load`
17    /// sources so they can still be introspected. (`load_boot` resolves the boot
18    /// codec first and returns before loading `--load` sources when that codec is
19    /// unavailable, so an empty receipt set means the codec was the failure.)
20    pub fn run_loaded_introspection(&mut self, boot: &CliBoot) -> Result<String, CliError> {
21        if let Err(codec_err) = self.load_boot(boot) {
22            if !self.receipts().is_empty() || boot.loads.is_empty() {
23                return Err(codec_err);
24            }
25            for source in &boot.loads {
26                self.load_source(source)?;
27            }
28        }
29        self.loaded_introspection_output(boot)
30    }
31
32    pub(crate) fn loaded_introspection_output(
33        &mut self,
34        boot: &CliBoot,
35    ) -> Result<String, CliError> {
36        let mut sections = Vec::new();
37        if boot.list {
38            sections.push(self.list_output()?);
39        }
40        if let Some(target) = &boot.inspect {
41            sections.push(self.inspect_output(target)?);
42        }
43        Ok(join_sections(sections))
44    }
45
46    fn list_output(&mut self) -> Result<String, CliError> {
47        if let Some(delegate) = select_delegate(self.receipts(), CLI_LIST_ENTRYPOINT) {
48            return self.call_delegate(&delegate, Vec::new());
49        }
50        Ok(format_list(self))
51    }
52
53    fn inspect_output(&mut self, target: &str) -> Result<String, CliError> {
54        if let Some(delegate) = select_delegate(self.receipts(), CLI_INSPECT_ENTRYPOINT) {
55            let target = self
56                .cx_mut()
57                .factory()
58                .string(target.to_owned())
59                .map_err(|err| CliError::new(format!("build inspect target: {err}")))?;
60            return self.call_delegate(&delegate, vec![target]);
61        }
62        self.inspect_target(target)
63    }
64
65    fn call_delegate(
66        &mut self,
67        delegate: &IntrospectionDelegate,
68        args: Vec<sim_kernel::Value>,
69    ) -> Result<String, CliError> {
70        let result = self
71            .cx_mut()
72            .call_function(&delegate.symbol, Args::new(args))
73            .map_err(|err| {
74                CliError::new(format!(
75                    "introspection delegate {} from {} failed: {err}",
76                    delegate.symbol, delegate.lib
77                ))
78            })?;
79        let mut output = result.object().display(self.cx_mut()).map_err(|err| {
80            CliError::new(format!("display introspection delegate result: {err}"))
81        })?;
82        if !output.ends_with('\n') {
83            output.push('\n');
84        }
85        Ok(output)
86    }
87
88    fn inspect_target(&mut self, target: &str) -> Result<String, CliError> {
89        let symbol = symbol_from_text(target);
90        if let Some(receipt) = self
91            .receipts()
92            .iter()
93            .find(|receipt| receipt.manifest.id == symbol)
94        {
95            return Ok(format_receipt(receipt));
96        }
97
98        let export_matches = self
99            .receipts()
100            .iter()
101            .flat_map(|receipt| {
102                receipt
103                    .exports
104                    .iter()
105                    .filter(|record| record.symbol == symbol)
106                    .map(move |record| (receipt, record))
107            })
108            .collect::<Vec<_>>();
109        if !export_matches.is_empty() {
110            return Ok(format_export_matches(target, &export_matches));
111        }
112
113        let source = parse_inspect_source(target);
114        self.inspect_source_or_fallback(&source)
115    }
116
117    fn inspect_source_or_fallback(&mut self, source: &LibSourceSpec) -> Result<String, CliError> {
118        match self.inspect_source(source) {
119            Ok(report) => Ok(report),
120            Err(err) => match source {
121                LibSourceSpec::Symbol(symbol) => {
122                    let Some(spec) = fallback_spec_for_symbol(symbol) else {
123                        return Err(err);
124                    };
125                    self.inspect_source(&LibSourceSpec::CratesIo(spec))
126                }
127                _ => Err(err),
128            },
129        }
130    }
131
132    fn inspect_source(&mut self, source: &LibSourceSpec) -> Result<String, CliError> {
133        match source {
134            LibSourceSpec::Host(name) => {
135                let lib = self.hosts().instantiate(name, self.config_state())?;
136                Ok(format_manifest_source(source, source, &lib.manifest()))
137            }
138            LibSourceSpec::CratesIo(spec) => {
139                let resolved = self.crates_io().resolve(spec)?;
140                let resolved_source = LibSourceSpec::Path(resolved.artifact.clone());
141                let manifest = self.inspect_data_source_manifest(
142                    sim_run_loaders::path_source_spec(resolved.artifact),
143                )?;
144                Ok(format_manifest_source(source, &resolved_source, &manifest))
145            }
146            _ => {
147                let requested = source
148                    .to_kernel_data_source()
149                    .ok_or_else(|| CliError::new(format!("cannot inspect source {source}")))?;
150                let resolved = self.resolve_data_source(requested.clone());
151                let manifest = self.inspect_data_source_manifest(resolved.clone())?;
152                Ok(format_manifest_source(
153                    &LibSourceSpec::from_kernel_data_source(requested),
154                    &LibSourceSpec::from_kernel_data_source(resolved),
155                    &manifest,
156                ))
157            }
158        }
159    }
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
163struct IntrospectionDelegate {
164    lib: Symbol,
165    symbol: Symbol,
166}
167
168fn select_delegate(receipts: &[LoadReceipt], name: &str) -> Option<IntrospectionDelegate> {
169    receipts
170        .iter()
171        .filter(|receipt| matches!(receipt.role, LoadReceiptRole::Library))
172        .find_map(|receipt| delegate_for_receipt(receipt, name))
173        .or_else(|| {
174            receipts
175                .iter()
176                .filter(|receipt| matches!(receipt.role, LoadReceiptRole::BootCodec { .. }))
177                .find_map(|receipt| delegate_for_receipt(receipt, name))
178        })
179}
180
181fn delegate_for_receipt(receipt: &LoadReceipt, name: &str) -> Option<IntrospectionDelegate> {
182    receipt
183        .exports
184        .iter()
185        .find(|record| record_is_resolved_function(record, name))
186        .map(|record| IntrospectionDelegate {
187            lib: receipt.manifest.id.clone(),
188            symbol: record.symbol.clone(),
189        })
190}
191
192fn record_is_resolved_function(record: &ExportRecord, name: &str) -> bool {
193    record.kind == ExportKind::named(ExportKind::FUNCTION)
194        && matches!(record.state, ExportState::Resolved { .. })
195        && record.symbol.as_qualified_str() == name
196}
197
198fn format_list(session: &LoadSession) -> String {
199    let mut output = String::new();
200    output.push_str("catalog sources:\n");
201    if session.catalog_sources().is_empty() {
202        output.push_str("- none\n");
203    } else {
204        for (symbol, source) in session.catalog_sources() {
205            output.push_str(&format!("- symbol:{symbol} -> {source}\n"));
206        }
207    }
208
209    output.push_str("crates.io artifacts:\n");
210    match session.crates_io().available_artifacts() {
211        Ok(listings) if listings.is_empty() => output.push_str("- none\n"),
212        Ok(listings) => {
213            for listing in listings {
214                output.push_str(&format_crates_listing(&listing));
215            }
216        }
217        Err(err) => output.push_str(&format!("- unavailable: {err}\n")),
218    }
219
220    output.push_str("loaded libs:\n");
221    if session.receipts().is_empty() {
222        output.push_str("- none\n");
223    } else {
224        for receipt in session.receipts() {
225            output.push_str(&format!(
226                "- role={} lib={} version={} requested={} resolved={} exports={}\n",
227                role_label(&receipt.role),
228                receipt.manifest.id,
229                receipt.manifest.version.0,
230                receipt.requested_source,
231                receipt.resolved_source,
232                receipt.exports.len()
233            ));
234        }
235    }
236    output
237}
238
239fn format_crates_listing(listing: &CratesIoListing) -> String {
240    format!(
241        "- crates.io:{}@{} source={} artifact={}\n",
242        listing.package,
243        listing.version,
244        crates_listing_source_label(&listing.source),
245        listing.artifact.display()
246    )
247}
248
249fn crates_listing_source_label(source: &CratesIoListingSource) -> &'static str {
250    match source {
251        CratesIoListingSource::Cache => "cache",
252        CratesIoListingSource::Registry => "registry",
253    }
254}
255
256fn format_receipt(receipt: &LoadReceipt) -> String {
257    let mut output = String::new();
258    output.push_str(&format!("lib {}\n", receipt.manifest.id));
259    output.push_str(&format!("version {}\n", receipt.manifest.version.0));
260    output.push_str(&format!("target {:?}\n", receipt.manifest.target));
261    output.push_str(&format!("role {}\n", role_label(&receipt.role)));
262    output.push_str(&format!("requested {}\n", receipt.requested_source));
263    output.push_str(&format!("resolved {}\n", receipt.resolved_source));
264    output.push_str(&format_dependencies(receipt));
265    output.push_str(&format_exports(&receipt.exports));
266    output
267}
268
269fn format_manifest_source(
270    requested: &LibSourceSpec,
271    resolved: &LibSourceSpec,
272    manifest: &LibManifest,
273) -> String {
274    let mut output = String::new();
275    output.push_str(&format!("source {}\n", requested));
276    output.push_str(&format!("resolved {}\n", resolved));
277    output.push_str(&format!("lib {}\n", manifest.id));
278    output.push_str(&format!("version {}\n", manifest.version.0));
279    output.push_str(&format!("target {:?}\n", manifest.target));
280    output.push_str("exports:\n");
281    for record in manifest.declared_export_records() {
282        output.push_str(&format_export(&record));
283    }
284    if manifest.exports.is_empty() {
285        output.push_str("- none\n");
286    }
287    output
288}
289
290fn format_export_matches(target: &str, matches: &[(&LoadReceipt, &ExportRecord)]) -> String {
291    let mut output = String::new();
292    output.push_str(&format!("export {target}\n"));
293    for (receipt, record) in matches {
294        output.push_str(&format!("- lib={} ", receipt.manifest.id));
295        output.push_str(format_export(record).trim_start_matches("- "));
296    }
297    output
298}
299
300fn format_dependencies(receipt: &LoadReceipt) -> String {
301    let mut output = String::new();
302    output.push_str("dependencies:\n");
303    if receipt.dependencies.is_empty() {
304        output.push_str("- none\n");
305    } else {
306        for dependency in &receipt.dependencies {
307            output.push_str(&format!(
308                "- lib_id={} symbol={}\n",
309                dependency.lib_id.0, dependency.symbol
310            ));
311        }
312    }
313    output
314}
315
316fn format_exports(exports: &[ExportRecord]) -> String {
317    let mut output = String::new();
318    output.push_str("exports:\n");
319    if exports.is_empty() {
320        output.push_str("- none\n");
321    } else {
322        for record in exports {
323            output.push_str(&format_export(record));
324        }
325    }
326    output
327}
328
329fn format_export(record: &ExportRecord) -> String {
330    format!(
331        "- kind={} symbol={} state={}\n",
332        record.kind.symbol(),
333        record.symbol,
334        state_label(&record.state)
335    )
336}
337
338fn state_label(state: &ExportState) -> String {
339    match state {
340        ExportState::Resolved { id } => format!("resolved:{id:?}"),
341        ExportState::Declared => "declared".to_owned(),
342        ExportState::Unsupported { reason } => format!("unsupported:{reason}"),
343        ExportState::Invalid { error } => format!("invalid:{error}"),
344    }
345}
346
347fn role_label(role: &LoadReceiptRole) -> String {
348    match role {
349        LoadReceiptRole::Library => "library".to_owned(),
350        LoadReceiptRole::BootCodec { name, symbol } => format!("boot-codec:{name}:{symbol}"),
351    }
352}
353
354fn parse_inspect_source(target: &str) -> LibSourceSpec {
355    target
356        .parse::<LibSourceSpec>()
357        .unwrap_or_else(|_| LibSourceSpec::Symbol(target.to_owned()))
358}
359
360fn join_sections(sections: Vec<String>) -> String {
361    let mut output = sections
362        .into_iter()
363        .map(|mut section| {
364            if !section.ends_with('\n') {
365                section.push('\n');
366            }
367            section
368        })
369        .collect::<Vec<_>>()
370        .join("\n");
371    if !output.ends_with('\n') {
372        output.push('\n');
373    }
374    output
375}