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 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}