Skip to main content

sim_run_core/
introspect.rs

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