harn-hostlib 0.8.65

Opt-in code-intelligence and deterministic-tool host builtins for the Harn VM
Documentation
use std::collections::BTreeMap;

use harn_vm::VmValue;

use crate::error::HostlibError;

pub(crate) fn describe(value: &VmValue) -> &'static str {
    match value {
        VmValue::Int(_) => "int",
        VmValue::Float(_) => "float",
        VmValue::String(_) => "string",
        VmValue::Bytes(_) => "bytes",
        VmValue::Bool(_) => "bool",
        VmValue::Nil => "nil",
        VmValue::List(_) => "list",
        VmValue::Dict(_) => "dict",
        VmValue::Set(_) => "set",
        _ => "other",
    }
}

pub(crate) fn require_string(
    builtin: &'static str,
    dict: &BTreeMap<String, VmValue>,
    key: &'static str,
) -> Result<String, HostlibError> {
    optional_string(builtin, dict, key)?.ok_or(HostlibError::MissingParameter {
        builtin,
        param: key,
    })
}

pub(crate) fn optional_string(
    builtin: &'static str,
    dict: &BTreeMap<String, VmValue>,
    key: &'static str,
) -> Result<Option<String>, HostlibError> {
    match dict.get(key) {
        None | Some(VmValue::Nil) => Ok(None),
        Some(VmValue::String(s)) => Ok(Some(s.to_string())),
        Some(other) => Err(HostlibError::InvalidParameter {
            builtin,
            param: key,
            message: format!("expected string, got {}", describe(other)),
        }),
    }
}

pub(crate) fn optional_bool(
    builtin: &'static str,
    dict: &BTreeMap<String, VmValue>,
    key: &'static str,
) -> Result<Option<bool>, HostlibError> {
    match dict.get(key) {
        None | Some(VmValue::Nil) => Ok(None),
        Some(VmValue::Bool(value)) => Ok(Some(*value)),
        Some(other) => Err(HostlibError::InvalidParameter {
            builtin,
            param: key,
            message: format!("expected bool, got {}", describe(other)),
        }),
    }
}

pub(crate) fn optional_i64(
    builtin: &'static str,
    dict: &BTreeMap<String, VmValue>,
    key: &'static str,
    default: i64,
) -> Result<i64, HostlibError> {
    match optional_i64_no_default(builtin, dict, key)? {
        Some(value) => Ok(value),
        None => Ok(default),
    }
}

pub(crate) fn optional_i64_no_default(
    builtin: &'static str,
    dict: &BTreeMap<String, VmValue>,
    key: &'static str,
) -> Result<Option<i64>, HostlibError> {
    match dict.get(key) {
        None | Some(VmValue::Nil) => Ok(None),
        Some(value) => coerce_i64(builtin, key, value).map(Some),
    }
}

pub(crate) fn optional_u64(
    builtin: &'static str,
    dict: &BTreeMap<String, VmValue>,
    key: &'static str,
) -> Result<Option<u64>, HostlibError> {
    match optional_i64_no_default(builtin, dict, key)? {
        None => Ok(None),
        Some(value) if value >= 0 => Ok(Some(value as u64)),
        Some(value) => Err(HostlibError::InvalidParameter {
            builtin,
            param: key,
            message: format!("expected non-negative integer, got {value}"),
        }),
    }
}

pub(crate) fn optional_string_list(
    builtin: &'static str,
    dict: &BTreeMap<String, VmValue>,
    key: &'static str,
) -> Result<Option<Vec<String>>, HostlibError> {
    match dict.get(key) {
        None | Some(VmValue::Nil) => Ok(None),
        Some(VmValue::List(items)) => items
            .iter()
            .enumerate()
            .map(|(idx, item)| match item {
                VmValue::String(value) => Ok(value.to_string()),
                other => Err(HostlibError::InvalidParameter {
                    builtin,
                    param: key,
                    message: format!("expected string at index {idx}, got {}", describe(other)),
                }),
            })
            .collect::<Result<Vec<_>, _>>()
            .map(Some),
        Some(other) => Err(HostlibError::InvalidParameter {
            builtin,
            param: key,
            message: format!("expected list of strings, got {}", describe(other)),
        }),
    }
}

pub(crate) fn coerce_i64(
    builtin: &'static str,
    key: &'static str,
    value: &VmValue,
) -> Result<i64, HostlibError> {
    match value {
        VmValue::Int(value) => Ok(*value),
        VmValue::Float(value) if value.is_finite() && value.fract() == 0.0 => {
            const I64_MAX_EXCLUSIVE: f64 = 9_223_372_036_854_775_808.0;
            if *value < i64::MIN as f64 || *value >= I64_MAX_EXCLUSIVE {
                return Err(HostlibError::InvalidParameter {
                    builtin,
                    param: key,
                    message: format!("integer is outside i64 range: {value}"),
                });
            }
            Ok(*value as i64)
        }
        VmValue::Float(value) if !value.is_finite() => Err(HostlibError::InvalidParameter {
            builtin,
            param: key,
            message: format!("expected finite integer, got {value}"),
        }),
        other => Err(HostlibError::InvalidParameter {
            builtin,
            param: key,
            message: format!("expected integer, got {}", describe(other)),
        }),
    }
}