Skip to main content

sim_run_core/
handoff.rs

1use sim_kernel::{Args, Cx, ExportKind, ExportRecord, ExportState, Symbol};
2
3use crate::{
4    CliBoot, CliEnvelope, CliError, LoadReceipt, LoadReceiptRole, LoadSession,
5    envelope::cli_envelope_value, exit::value_to_exit_code,
6};
7
8/// Symbol prefix a loaded lib claims to own command-line execution.
9pub const CLI_MAIN_ENTRYPOINT: &str = "cli/main";
10
11/// A loaded function that owns command-line execution.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct CliEntrypoint {
14    /// Library that exported the entrypoint.
15    pub lib: Symbol,
16    /// Exported function symbol invoked for the handoff.
17    pub symbol: Symbol,
18}
19
20impl LoadSession {
21    /// Loads a boot session and runs the selected loaded CLI entrypoint.
22    pub fn run_loaded_boot(&mut self, boot: &CliBoot) -> Result<i32, CliError> {
23        if boot.config_report.is_some() {
24            print!("{}", self.run_config_report(boot)?);
25            return Ok(0);
26        }
27        if boot.list || boot.inspect.is_some() {
28            print!("{}", self.run_loaded_introspection(boot)?);
29            return Ok(0);
30        }
31        self.load_boot(boot)?;
32        self.run_loaded_handoff(&boot.envelope())
33    }
34
35    /// Runs the selected loaded CLI entrypoint for an already-loaded session.
36    pub fn run_loaded_handoff(&mut self, envelope: &CliEnvelope) -> Result<i32, CliError> {
37        let entrypoint = select_cli_entrypoint(self.receipts(), envelope.verb.as_deref())?;
38        run_loaded_cli(self.cx_mut(), &entrypoint, envelope)
39    }
40}
41
42/// Builds the qualified `cli/main/NAME` entrypoint symbol for a named lib.
43pub fn cli_main_entrypoint_symbol(name: &str) -> Symbol {
44    Symbol::qualified("cli", format!("main/{name}"))
45}
46
47/// Selects the loaded entrypoint that claims [`CLI_MAIN_ENTRYPOINT`].
48///
49/// Prefers a `--load` library over the boot codec, and returns an error
50/// when no loaded lib claims the entrypoint.
51pub fn select_cli_entrypoint(
52    receipts: &[LoadReceipt],
53    verb: Option<&str>,
54) -> Result<CliEntrypoint, CliError> {
55    select_from_role(
56        receipts,
57        |role| matches!(role, LoadReceiptRole::Library),
58        verb,
59    )
60    .or_else(|| {
61        select_from_role(
62            receipts,
63            |role| matches!(role, LoadReceiptRole::BootCodec { .. }),
64            verb,
65        )
66    })
67    .ok_or_else(|| no_entrypoint_error(receipts, verb))
68}
69
70/// Calls a loaded entrypoint with the boot envelope and returns its exit code.
71pub fn run_loaded_cli(
72    cx: &mut Cx,
73    entrypoint: &CliEntrypoint,
74    envelope: &CliEnvelope,
75) -> Result<i32, CliError> {
76    let envelope = cli_envelope_value(cx, envelope)?;
77    let result = cx
78        .call_function(&entrypoint.symbol, Args::new(vec![envelope]))
79        .map_err(|err| {
80            CliError::new(format!(
81                "cli handoff failed for {} from {}: {err}",
82                entrypoint.symbol, entrypoint.lib
83            ))
84        })?;
85    value_to_exit_code(cx, result)
86}
87
88fn select_from_role(
89    receipts: &[LoadReceipt],
90    role_matches: impl Fn(&LoadReceiptRole) -> bool + Copy,
91    verb: Option<&str>,
92) -> Option<CliEntrypoint> {
93    if let Some(verb) = verb {
94        find_entrypoint(receipts, role_matches, |record| {
95            record_claims_exact_cli_main(record, verb)
96        })
97        .or_else(|| find_entrypoint(receipts, role_matches, record_claims_generic_cli_main))
98    } else {
99        find_entrypoint(receipts, role_matches, record_claims_cli_main)
100    }
101}
102
103fn find_entrypoint(
104    receipts: &[LoadReceipt],
105    role_matches: impl Fn(&LoadReceiptRole) -> bool,
106    record_matches: impl Fn(&ExportRecord) -> bool + Copy,
107) -> Option<CliEntrypoint> {
108    receipts
109        .iter()
110        .filter(|receipt| role_matches(&receipt.role))
111        .find_map(|receipt| entrypoint_for_receipt(receipt, record_matches))
112}
113
114fn entrypoint_for_receipt(
115    receipt: &LoadReceipt,
116    record_matches: impl Fn(&ExportRecord) -> bool + Copy,
117) -> Option<CliEntrypoint> {
118    receipt
119        .exports
120        .iter()
121        .find(|record| record_matches(record))
122        .map(|record| CliEntrypoint {
123            lib: receipt.manifest.id.clone(),
124            symbol: record.symbol.clone(),
125        })
126}
127
128fn record_claims_exact_cli_main(record: &ExportRecord, verb: &str) -> bool {
129    record.kind == ExportKind::named(ExportKind::FUNCTION)
130        && matches!(record.state, ExportState::Resolved { .. })
131        && record.symbol == cli_main_entrypoint_symbol(verb)
132}
133
134fn record_claims_generic_cli_main(record: &ExportRecord) -> bool {
135    record.kind == ExportKind::named(ExportKind::FUNCTION)
136        && matches!(record.state, ExportState::Resolved { .. })
137        && record.symbol == Symbol::new(CLI_MAIN_ENTRYPOINT)
138}
139
140fn record_claims_cli_main(record: &ExportRecord) -> bool {
141    record.kind == ExportKind::named(ExportKind::FUNCTION)
142        && matches!(record.state, ExportState::Resolved { .. })
143        && symbol_claims_cli_main(&record.symbol)
144}
145
146fn symbol_claims_cli_main(symbol: &Symbol) -> bool {
147    let symbol = symbol.as_qualified_str();
148    symbol == CLI_MAIN_ENTRYPOINT || symbol.starts_with(&format!("{CLI_MAIN_ENTRYPOINT}/"))
149}
150
151fn no_entrypoint_error(receipts: &[LoadReceipt], verb: Option<&str>) -> CliError {
152    let loaded = if receipts.is_empty() {
153        "none".to_owned()
154    } else {
155        receipts
156            .iter()
157            .map(|receipt| receipt.manifest.id.to_string())
158            .collect::<Vec<_>>()
159            .join(", ")
160    };
161    let requested = verb
162        .map(|verb| format!(" {CLI_MAIN_ENTRYPOINT}/{verb} or {CLI_MAIN_ENTRYPOINT}"))
163        .unwrap_or_else(|| format!(" {CLI_MAIN_ENTRYPOINT}"));
164    CliError::new(format!(
165        "no loaded lib claims{requested}; loaded libs: {loaded}; load one with --load"
166    ))
167}