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