sim-run-core 0.3.0

Core command entry API for the SIM bootloader.
Documentation
use sim_kernel::{Args, Cx, ExportKind, ExportRecord, ExportState, Symbol};

use crate::{
    CliBoot, CliEnvelope, CliError, LoadReceipt, LoadReceiptRole, LoadSession,
    envelope::cli_envelope_value, exit::value_to_exit_code,
};

/// Symbol prefix a loaded lib claims to own command-line execution.
pub const CLI_MAIN_ENTRYPOINT: &str = "cli/main";

/// A loaded function that owns command-line execution.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CliEntrypoint {
    /// Library that exported the entrypoint.
    pub lib: Symbol,
    /// Exported function symbol invoked for the handoff.
    pub symbol: Symbol,
}

impl LoadSession {
    /// Loads a boot session and runs the selected loaded CLI entrypoint.
    pub fn run_loaded_boot(&mut self, boot: &CliBoot) -> Result<i32, CliError> {
        if boot.config_report.is_some() {
            print!("{}", self.run_config_report(boot)?);
            return Ok(0);
        }
        if boot.list || boot.inspect.is_some() {
            print!("{}", self.run_loaded_introspection(boot)?);
            return Ok(0);
        }
        self.load_boot(boot)?;
        self.run_loaded_handoff(&boot.envelope())
    }

    /// Runs the selected loaded CLI entrypoint for an already-loaded session.
    pub fn run_loaded_handoff(&mut self, envelope: &CliEnvelope) -> Result<i32, CliError> {
        let entrypoint = select_cli_entrypoint(self.receipts(), envelope.verb.as_deref())?;
        run_loaded_cli(self.cx_mut(), &entrypoint, envelope)
    }
}

/// Builds the qualified `cli/main/NAME` entrypoint symbol for a named lib.
pub fn cli_main_entrypoint_symbol(name: &str) -> Symbol {
    Symbol::qualified("cli", format!("main/{name}"))
}

/// Selects the loaded entrypoint that claims [`CLI_MAIN_ENTRYPOINT`].
///
/// Prefers a `--load` library over the boot codec, and returns an error
/// when no loaded lib claims the entrypoint.
pub fn select_cli_entrypoint(
    receipts: &[LoadReceipt],
    verb: Option<&str>,
) -> Result<CliEntrypoint, CliError> {
    select_from_role(
        receipts,
        |role| matches!(role, LoadReceiptRole::Library),
        verb,
    )
    .or_else(|| {
        select_from_role(
            receipts,
            |role| matches!(role, LoadReceiptRole::BootCodec { .. }),
            verb,
        )
    })
    .ok_or_else(|| no_entrypoint_error(receipts, verb))
}

/// Calls a loaded entrypoint with the boot envelope and returns its exit code.
pub fn run_loaded_cli(
    cx: &mut Cx,
    entrypoint: &CliEntrypoint,
    envelope: &CliEnvelope,
) -> Result<i32, CliError> {
    let envelope = cli_envelope_value(cx, envelope)?;
    let result = cx
        .call_function(&entrypoint.symbol, Args::new(vec![envelope]))
        .map_err(|err| {
            CliError::new(format!(
                "cli handoff failed for {} from {}: {err}",
                entrypoint.symbol, entrypoint.lib
            ))
        })?;
    value_to_exit_code(cx, result)
}

fn select_from_role(
    receipts: &[LoadReceipt],
    role_matches: impl Fn(&LoadReceiptRole) -> bool + Copy,
    verb: Option<&str>,
) -> Option<CliEntrypoint> {
    if let Some(verb) = verb {
        find_entrypoint(receipts, role_matches, |record| {
            record_claims_exact_cli_main(record, verb)
        })
        .or_else(|| find_entrypoint(receipts, role_matches, record_claims_generic_cli_main))
    } else {
        find_entrypoint(receipts, role_matches, record_claims_cli_main)
    }
}

fn find_entrypoint(
    receipts: &[LoadReceipt],
    role_matches: impl Fn(&LoadReceiptRole) -> bool,
    record_matches: impl Fn(&ExportRecord) -> bool + Copy,
) -> Option<CliEntrypoint> {
    receipts
        .iter()
        .filter(|receipt| role_matches(&receipt.role))
        .find_map(|receipt| entrypoint_for_receipt(receipt, record_matches))
}

fn entrypoint_for_receipt(
    receipt: &LoadReceipt,
    record_matches: impl Fn(&ExportRecord) -> bool + Copy,
) -> Option<CliEntrypoint> {
    receipt
        .exports
        .iter()
        .find(|record| record_matches(record))
        .map(|record| CliEntrypoint {
            lib: receipt.manifest.id.clone(),
            symbol: record.symbol.clone(),
        })
}

fn record_claims_exact_cli_main(record: &ExportRecord, verb: &str) -> bool {
    record.kind == ExportKind::named(ExportKind::FUNCTION)
        && matches!(record.state, ExportState::Resolved { .. })
        && record.symbol == cli_main_entrypoint_symbol(verb)
}

fn record_claims_generic_cli_main(record: &ExportRecord) -> bool {
    record.kind == ExportKind::named(ExportKind::FUNCTION)
        && matches!(record.state, ExportState::Resolved { .. })
        && record.symbol == Symbol::new(CLI_MAIN_ENTRYPOINT)
}

fn record_claims_cli_main(record: &ExportRecord) -> bool {
    record.kind == ExportKind::named(ExportKind::FUNCTION)
        && matches!(record.state, ExportState::Resolved { .. })
        && symbol_claims_cli_main(&record.symbol)
}

fn symbol_claims_cli_main(symbol: &Symbol) -> bool {
    let symbol = symbol.as_qualified_str();
    symbol == CLI_MAIN_ENTRYPOINT || symbol.starts_with(&format!("{CLI_MAIN_ENTRYPOINT}/"))
}

fn no_entrypoint_error(receipts: &[LoadReceipt], verb: Option<&str>) -> CliError {
    let loaded = if receipts.is_empty() {
        "none".to_owned()
    } else {
        receipts
            .iter()
            .map(|receipt| receipt.manifest.id.to_string())
            .collect::<Vec<_>>()
            .join(", ")
    };
    let requested = verb
        .map(|verb| format!(" {CLI_MAIN_ENTRYPOINT}/{verb} or {CLI_MAIN_ENTRYPOINT}"))
        .unwrap_or_else(|| format!(" {CLI_MAIN_ENTRYPOINT}"));
    CliError::new(format!(
        "no loaded lib claims{requested}; loaded libs: {loaded}; load one with --load"
    ))
}