Skip to main content

native/rx_rust_nif/src/terms.rs

use rustler::{Encoder, Env, Term};

pub fn binary<'a>(env: Env<'a>, bytes: &[u8]) -> Term<'a> {
    if let Some(mut out) = rustler::types::binary::OwnedBinary::new(bytes.len()) {
        out.as_mut_slice().copy_from_slice(bytes);
        out.release(env).encode(env)
    } else {
        "".encode(env)
    }
}

pub fn try_binary<'a>(env: Env<'a>, bytes: &[u8]) -> Result<Term<'a>, String> {
    let Some(mut out) = rustler::types::binary::OwnedBinary::new(bytes.len()) else {
        return Err("failed to allocate native plot page binary".to_owned());
    };

    out.as_mut_slice().copy_from_slice(bytes);
    Ok(out.release(env).encode(env))
}

pub fn encode_output_map<'a>(env: Env<'a>, output: &crate::work::EvalOutput) -> Term<'a> {
    map_from_terms(
        env,
        &[
            crate::atoms::stdout().encode(env),
            crate::atoms::messages().encode(env),
            crate::atoms::warnings().encode(env),
        ],
        &[
            binary(env, &output.stdout),
            binary(env, &output.messages),
            binary(env, &output.warnings),
        ],
    )
}

pub fn encode_eval_success<'a>(env: Env<'a>, success: crate::work::EvalSuccess) -> Term<'a> {
    let result = match success.result {
        Some(resource) => resource.encode(env),
        None => crate::atoms::nil().encode(env),
    };

    let globals = success
        .globals
        .into_iter()
        .map(|(name, resource)| (binary(env, &name), resource.encode(env)))
        .collect::<Vec<_>>();

    (
        crate::atoms::ok(),
        (result, globals, encode_output_map(env, &success.output)),
    )
        .encode(env)
}

pub fn encode_plot_success<'a>(
    env: Env<'a>,
    success: crate::work::PlotSuccess,
) -> Result<Term<'a>, String> {
    let pages = success
        .pages
        .iter()
        .map(|page| try_binary(env, page))
        .collect::<Result<Vec<_>, _>>()?;

    Ok((
        crate::atoms::ok(),
        (
            success.width,
            success.height,
            pages,
            encode_output_map(env, &success.output),
        ),
    )
        .encode(env))
}

pub fn encode_structured_error<'a>(env: Env<'a>, error: crate::work::StructuredError) -> Term<'a> {
    let class_terms = error
        .r_class
        .iter()
        .map(|class| binary(env, class))
        .collect::<Vec<_>>();

    let call = match error.call {
        Some(call) => binary(env, &call),
        None => crate::atoms::nil().encode(env),
    };

    let map = map_from_terms(
        env,
        &[
            crate::atoms::message().encode(env),
            crate::atoms::r_class().encode(env),
            crate::atoms::call().encode(env),
            crate::atoms::traceback().encode(env),
            crate::atoms::output().encode(env),
        ],
        &[
            binary(env, &error.message),
            class_terms.encode(env),
            call,
            Vec::<Term>::new().encode(env),
            encode_output_map(env, &error.output),
        ],
    );

    (crate::atoms::error(), map).encode(env)
}

pub fn encode_init_config<'a>(env: Env<'a>, config: &crate::work::InitConfig) -> Term<'a> {
    let lib_paths = config
        .lib_paths
        .iter()
        .map(|path| path.as_str())
        .collect::<Vec<_>>();

    map_from_terms(
        env,
        &[
            crate::atoms::r_home().encode(env),
            crate::atoms::lib_r_path().encode(env),
            crate::atoms::lib_paths().encode(env),
        ],
        &[
            config.r_home.as_str().encode(env),
            config.lib_r_path.as_str().encode(env),
            lib_paths.encode(env),
        ],
    )
}

pub fn encode_init_failure<'a>(env: Env<'a>, failure: &crate::work::InitFailure) -> Term<'a> {
    map_from_terms(
        env,
        &[
            crate::atoms::stage().encode(env),
            crate::atoms::message().encode(env),
            crate::atoms::retryable().encode(env),
            crate::atoms::restart_required().encode(env),
        ],
        &[
            stage_atom(env, failure.stage),
            failure.message.as_str().encode(env),
            failure.retryable.encode(env),
            failure.restart_required.encode(env),
        ],
    )
}

pub fn encode_init_mismatch<'a>(env: Env<'a>, mismatch: &crate::work::InitMismatch) -> Term<'a> {
    let mismatches = mismatch
        .mismatches
        .iter()
        .map(|name| field_atom(env, name))
        .collect::<Vec<_>>();

    map_from_terms(
        env,
        &[
            crate::atoms::message().encode(env),
            crate::atoms::mismatches().encode(env),
            crate::atoms::current().encode(env),
            crate::atoms::requested().encode(env),
            crate::atoms::restart_required().encode(env),
        ],
        &[
            mismatch.message.as_str().encode(env),
            mismatches.encode(env),
            encode_init_config(env, &mismatch.current),
            encode_init_config(env, &mismatch.requested),
            true.encode(env),
        ],
    )
}

pub fn encode_native_diagnostics<'a>(
    env: Env<'a>,
    diagnostics: crate::work::NativeDiagnostics,
) -> Term<'a> {
    map_from_terms(
        env,
        &[
            crate::atoms::init_state().encode(env),
            crate::atoms::init_config().encode(env),
            crate::atoms::attempted_init_config().encode(env),
            crate::atoms::last_init_error().encode(env),
            crate::atoms::init_attempt_count().encode(env),
            crate::atoms::init_mismatch_count().encode(env),
            crate::atoms::owner_thread_id().encode(env),
            crate::atoms::release_enqueued_count().encode(env),
            crate::atoms::release_success_count().encode(env),
            crate::atoms::release_failure_count().encode(env),
            crate::atoms::release_skipped_uninitialized_count().encode(env),
            crate::atoms::last_release_thread_id().encode(env),
            crate::atoms::last_release_resource_id().encode(env),
            crate::atoms::last_release_error().encode(env),
        ],
        &[
            init_state_atom(env, diagnostics.init_state),
            optional_config(env, diagnostics.init_config.as_ref()),
            optional_config(env, diagnostics.attempted_init_config.as_ref()),
            optional_failure(env, diagnostics.last_init_error.as_ref()),
            diagnostics.init_attempt_count.encode(env),
            diagnostics.init_mismatch_count.encode(env),
            diagnostics.owner_thread_id.encode(env),
            diagnostics.release_enqueued_count.encode(env),
            diagnostics.release_success_count.encode(env),
            diagnostics.release_failure_count.encode(env),
            diagnostics.release_skipped_uninitialized_count.encode(env),
            diagnostics.last_release_thread_id.encode(env),
            optional_u64(env, diagnostics.last_release_resource_id),
            optional_string(env, diagnostics.last_release_error.as_ref()),
        ],
    )
}

fn field_atom<'a>(env: Env<'a>, name: &str) -> Term<'a> {
    match name {
        "r_home" => crate::atoms::r_home().encode(env),
        "lib_r_path" => crate::atoms::lib_r_path().encode(env),
        "lib_paths" => crate::atoms::lib_paths().encode(env),
        _ => name.encode(env),
    }
}

fn stage_atom<'a>(env: Env<'a>, stage: &str) -> Term<'a> {
    match stage {
        "dlopen_lib_r" => crate::atoms::dlopen_lib_r().encode(env),
        "resolve_boot_symbols" => crate::atoms::resolve_boot_symbols().encode(env),
        "rf_initialize_r" => crate::atoms::rf_initialize_r().encode(env),
        "setup_rmainloop" => crate::atoms::setup_rmainloop().encode(env),
        "load_r_api" => crate::atoms::load_r_api().encode(env),
        "apply_lib_paths" => crate::atoms::apply_lib_paths().encode(env),
        "ensure_eval_helper" => crate::atoms::ensure_eval_helper().encode(env),
        _ => stage.encode(env),
    }
}

fn init_state_atom<'a>(env: Env<'a>, state: crate::work::InitState) -> Term<'a> {
    match state {
        crate::work::InitState::Uninitialized => crate::atoms::uninitialized().encode(env),
        crate::work::InitState::Initialized => crate::atoms::initialized().encode(env),
        crate::work::InitState::Failed => crate::atoms::failed().encode(env),
    }
}

fn optional_config<'a>(env: Env<'a>, config: Option<&crate::work::InitConfig>) -> Term<'a> {
    match config {
        Some(config) => encode_init_config(env, config),
        None => crate::atoms::nil().encode(env),
    }
}

fn optional_failure<'a>(env: Env<'a>, failure: Option<&crate::work::InitFailure>) -> Term<'a> {
    match failure {
        Some(failure) => encode_init_failure(env, failure),
        None => crate::atoms::nil().encode(env),
    }
}

fn optional_u64<'a>(env: Env<'a>, value: Option<u64>) -> Term<'a> {
    match value {
        Some(value) => value.encode(env),
        None => crate::atoms::nil().encode(env),
    }
}

fn optional_string<'a>(env: Env<'a>, value: Option<&String>) -> Term<'a> {
    match value {
        Some(value) => value.as_str().encode(env),
        None => crate::atoms::nil().encode(env),
    }
}

pub fn map_from_terms<'a>(env: Env<'a>, keys: &[Term<'a>], values: &[Term<'a>]) -> Term<'a> {
    Term::map_from_term_arrays(env, keys, values).unwrap_or_else(|_| Term::map_new(env))
}