stoffel-vm 0.1.0

Register-based virtual machine runtime and MPC execution engine for Stoffel programs
Documentation
use stoffel_vm_types::core_types::Value;

pub(crate) type ValueConversionResult<T> = Result<T, ValueConversionError>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ValueConversionError {
    ExpectedNonNegativeInteger { name: String },
    ExpectedInteger { name: String },
    TooLargeForPlatform { name: String },
    TooLargeForU64 { name: String },
    ExceedsI64Range { name: String },
    ExceedsVmIntegerRange { name: String, value: u128 },
}

impl ValueConversionError {
    fn expected_non_negative_integer(name: &str) -> Self {
        Self::ExpectedNonNegativeInteger {
            name: name.to_owned(),
        }
    }

    fn expected_integer(name: &str) -> Self {
        Self::ExpectedInteger {
            name: name.to_owned(),
        }
    }

    fn too_large_for_platform(name: &str) -> Self {
        Self::TooLargeForPlatform {
            name: name.to_owned(),
        }
    }

    fn too_large_for_u64(name: &str) -> Self {
        Self::TooLargeForU64 {
            name: name.to_owned(),
        }
    }

    fn exceeds_i64_range(name: &str) -> Self {
        Self::ExceedsI64Range {
            name: name.to_owned(),
        }
    }

    fn exceeds_vm_integer_range(name: &str, value: impl Into<u128>) -> Self {
        Self::ExceedsVmIntegerRange {
            name: name.to_owned(),
            value: value.into(),
        }
    }
}

impl std::fmt::Display for ValueConversionError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ValueConversionError::ExpectedNonNegativeInteger { name } => {
                write!(f, "{name} must be a non-negative integer")
            }
            ValueConversionError::ExpectedInteger { name } => {
                write!(f, "{name} must be an integer")
            }
            ValueConversionError::TooLargeForPlatform { name } => {
                write!(f, "{name} is too large for this platform")
            }
            ValueConversionError::TooLargeForU64 { name } => {
                write!(f, "{name} is too large for u64")
            }
            ValueConversionError::ExceedsI64Range { name } => {
                write!(f, "{name} exceeds i64 range")
            }
            ValueConversionError::ExceedsVmIntegerRange { name, value } => {
                write!(f, "{name} {value} exceeds VM integer range")
            }
        }
    }
}

impl std::error::Error for ValueConversionError {}

impl From<ValueConversionError> for String {
    fn from(error: ValueConversionError) -> Self {
        error.to_string()
    }
}

pub(crate) fn value_to_usize(value: &Value, name: &str) -> ValueConversionResult<usize> {
    match value {
        Value::I64(v) if *v >= 0 => {
            usize::try_from(*v).map_err(|_| ValueConversionError::too_large_for_platform(name))
        }
        Value::I32(v) if *v >= 0 => {
            usize::try_from(*v).map_err(|_| ValueConversionError::too_large_for_platform(name))
        }
        Value::I16(v) if *v >= 0 => {
            usize::try_from(*v).map_err(|_| ValueConversionError::too_large_for_platform(name))
        }
        Value::I8(v) if *v >= 0 => {
            usize::try_from(*v).map_err(|_| ValueConversionError::too_large_for_platform(name))
        }
        Value::U64(v) => {
            usize::try_from(*v).map_err(|_| ValueConversionError::too_large_for_platform(name))
        }
        Value::U32(v) => {
            usize::try_from(*v).map_err(|_| ValueConversionError::too_large_for_platform(name))
        }
        Value::U16(v) => Ok((*v).into()),
        Value::U8(v) => Ok((*v).into()),
        _ => Err(ValueConversionError::expected_non_negative_integer(name)),
    }
}

pub(crate) fn value_to_u64(value: &Value, name: &str) -> ValueConversionResult<u64> {
    match value {
        Value::I64(v) if *v >= 0 => {
            u64::try_from(*v).map_err(|_| ValueConversionError::too_large_for_u64(name))
        }
        Value::I32(v) if *v >= 0 => {
            u64::try_from(*v).map_err(|_| ValueConversionError::too_large_for_u64(name))
        }
        Value::I16(v) if *v >= 0 => {
            u64::try_from(*v).map_err(|_| ValueConversionError::too_large_for_u64(name))
        }
        Value::I8(v) if *v >= 0 => {
            u64::try_from(*v).map_err(|_| ValueConversionError::too_large_for_u64(name))
        }
        Value::U64(v) => Ok(*v),
        Value::U32(v) => Ok((*v).into()),
        Value::U16(v) => Ok((*v).into()),
        Value::U8(v) => Ok((*v).into()),
        _ => Err(ValueConversionError::expected_non_negative_integer(name)),
    }
}

pub(crate) fn value_to_i64(value: &Value, name: &str) -> ValueConversionResult<i64> {
    match value {
        Value::I64(v) => Ok(*v),
        Value::I32(v) => Ok((*v).into()),
        Value::I16(v) => Ok((*v).into()),
        Value::I8(v) => Ok((*v).into()),
        Value::U64(v) => {
            i64::try_from(*v).map_err(|_| ValueConversionError::exceeds_i64_range(name))
        }
        Value::U32(v) => Ok((*v).into()),
        Value::U16(v) => Ok((*v).into()),
        Value::U8(v) => Ok((*v).into()),
        _ => Err(ValueConversionError::expected_integer(name)),
    }
}

pub(crate) fn usize_to_vm_i64(value: usize, name: &str) -> ValueConversionResult<i64> {
    i64::try_from(value)
        .map_err(|_| ValueConversionError::exceeds_vm_integer_range(name, value as u128))
}

pub(crate) fn u64_to_vm_i64(value: u64, name: &str) -> ValueConversionResult<i64> {
    i64::try_from(value)
        .map_err(|_| ValueConversionError::exceeds_vm_integer_range(name, u128::from(value)))
}

#[cfg(test)]
mod tests {
    use super::*;
    use stoffel_vm_types::core_types::F64;

    #[test]
    fn integer_conversions_accept_vm_integer_widths() {
        assert_eq!(value_to_usize(&Value::U8(7), "count"), Ok(7));
        assert_eq!(value_to_u64(&Value::U32(9), "timeout_ms"), Ok(9));
        assert_eq!(value_to_i64(&Value::I16(-3), "scalar"), Ok(-3));
    }

    #[test]
    fn integer_conversions_report_typed_argument_errors() {
        assert_eq!(
            value_to_usize(&Value::I64(-1), "index"),
            Err(ValueConversionError::ExpectedNonNegativeInteger {
                name: "index".to_string()
            })
        );
        assert_eq!(
            value_to_i64(&Value::Float(F64::new(1.5)), "scalar"),
            Err(ValueConversionError::ExpectedInteger {
                name: "scalar".to_string()
            })
        );
    }

    #[test]
    fn integer_conversions_report_range_errors_without_losing_context() {
        assert_eq!(
            value_to_i64(&Value::U64(i64::MAX as u64 + 1), "scalar"),
            Err(ValueConversionError::ExceedsI64Range {
                name: "scalar".to_string()
            })
        );
        assert_eq!(
            u64_to_vm_i64(i64::MAX as u64 + 1, "session_id"),
            Err(ValueConversionError::ExceedsVmIntegerRange {
                name: "session_id".to_string(),
                value: i64::MAX as u128 + 1
            })
        );
    }

    #[test]
    fn conversion_errors_preserve_existing_display_messages() {
        let error = value_to_usize(&Value::String("x".to_string()), "index").unwrap_err();
        assert_eq!(error.to_string(), "index must be a non-negative integer");

        let error = usize_to_vm_i64(usize::MAX, "array length").unwrap_err();
        if usize::MAX > i64::MAX as usize {
            assert_eq!(
                error.to_string(),
                format!("array length {} exceeds VM integer range", usize::MAX)
            );
        }
    }
}