harn-vm 0.8.50

Async bytecode virtual machine for the Harn programming language
Documentation
use std::rc::Rc;

use crate::stdlib::macros::{harn_builtin, VmBuiltinDef};
use crate::value::{VmError, VmValue};
use crate::vm::Vm;

fn runtime_error(message: impl Into<String>) -> VmError {
    VmError::Runtime(message.into())
}

fn expect_bytes<'a>(args: &'a [VmValue], index: usize, builtin: &str) -> Result<&'a [u8], VmError> {
    match args.get(index) {
        Some(VmValue::Bytes(bytes)) => Ok(bytes.as_slice()),
        Some(other) => Err(runtime_error(format!(
            "{builtin}: expected bytes at argument {}, got {}",
            index + 1,
            other.type_name()
        ))),
        None => Err(runtime_error(format!(
            "{builtin}: missing argument {}",
            index + 1
        ))),
    }
}

fn expect_string<'a>(args: &'a [VmValue], index: usize, builtin: &str) -> Result<&'a str, VmError> {
    match args.get(index) {
        Some(VmValue::String(text)) => Ok(text.as_ref()),
        Some(other) => Err(runtime_error(format!(
            "{builtin}: expected string at argument {}, got {}",
            index + 1,
            other.type_name()
        ))),
        None => Err(runtime_error(format!(
            "{builtin}: missing argument {}",
            index + 1
        ))),
    }
}

fn expect_int(args: &[VmValue], index: usize, builtin: &str) -> Result<i64, VmError> {
    match args.get(index) {
        Some(VmValue::Int(value)) => Ok(*value),
        Some(other) => Err(runtime_error(format!(
            "{builtin}: expected int at argument {}, got {}",
            index + 1,
            other.type_name()
        ))),
        None => Err(runtime_error(format!(
            "{builtin}: missing argument {}",
            index + 1
        ))),
    }
}

pub(crate) fn register_bytes_builtins(vm: &mut Vm) {
    for def in MODULE_BUILTINS {
        vm.register_builtin_def(def);
    }
}

#[harn_builtin(sig = "bytes_from_string(text: string?) -> bytes", category = "bytes")]
fn bytes_from_string_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let text = expect_string(args, 0, "bytes_from_string")?;
    Ok(VmValue::Bytes(Rc::new(text.as_bytes().to_vec())))
}

#[harn_builtin(sig = "bytes_to_string(input: bytes) -> string", category = "bytes")]
fn bytes_to_string_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let bytes = expect_bytes(args, 0, "bytes_to_string")?;
    let text = std::str::from_utf8(bytes)
        .map_err(|error| runtime_error(format!("bytes_to_string: {error}")))?;
    Ok(VmValue::String(Rc::from(text)))
}

#[harn_builtin(
    sig = "bytes_to_string_lossy(input: bytes) -> string",
    category = "bytes"
)]
fn bytes_to_string_lossy_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let bytes = expect_bytes(args, 0, "bytes_to_string_lossy")?;
    Ok(VmValue::String(Rc::from(
        String::from_utf8_lossy(bytes).into_owned(),
    )))
}

#[harn_builtin(sig = "bytes_to_hex(input: bytes) -> string", category = "bytes")]
fn bytes_to_hex_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let bytes = expect_bytes(args, 0, "bytes_to_hex")?;
    Ok(VmValue::String(Rc::from(hex::encode(bytes))))
}

#[harn_builtin(sig = "bytes_from_hex(text: string?) -> bytes", category = "bytes")]
fn bytes_from_hex_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let text = expect_string(args, 0, "bytes_from_hex")?;
    let bytes =
        hex::decode(text).map_err(|error| runtime_error(format!("bytes_from_hex: {error}")))?;
    Ok(VmValue::Bytes(Rc::new(bytes)))
}

#[harn_builtin(sig = "bytes_to_base64(input: bytes) -> string", category = "bytes")]
fn bytes_to_base64_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    use base64::Engine;

    let bytes = expect_bytes(args, 0, "bytes_to_base64")?;
    Ok(VmValue::String(Rc::from(
        base64::engine::general_purpose::STANDARD.encode(bytes),
    )))
}

#[harn_builtin(sig = "bytes_from_base64(text: string?) -> bytes", category = "bytes")]
fn bytes_from_base64_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    use base64::Engine;

    let text = expect_string(args, 0, "bytes_from_base64")?;
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(text.as_bytes())
        .map_err(|error| runtime_error(format!("bytes_from_base64: {error}")))?;
    Ok(VmValue::Bytes(Rc::new(bytes)))
}

#[harn_builtin(sig = "bytes_len(input: bytes) -> int", category = "bytes")]
fn bytes_len_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let bytes = expect_bytes(args, 0, "bytes_len")?;
    Ok(VmValue::Int(bytes.len() as i64))
}

#[harn_builtin(
    sig = "bytes_concat(left: bytes, right: bytes) -> bytes",
    category = "bytes"
)]
fn bytes_concat_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let left = expect_bytes(args, 0, "bytes_concat")?;
    let right = expect_bytes(args, 1, "bytes_concat")?;
    let mut out = Vec::with_capacity(left.len() + right.len());
    out.extend_from_slice(left);
    out.extend_from_slice(right);
    Ok(VmValue::Bytes(Rc::new(out)))
}

#[harn_builtin(
    sig = "bytes_slice(input: bytes, start: int, end: int) -> bytes",
    category = "bytes"
)]
fn bytes_slice_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let bytes = expect_bytes(args, 0, "bytes_slice")?;
    let len = bytes.len() as i64;
    let start = expect_int(args, 1, "bytes_slice")?.clamp(0, len) as usize;
    let end = expect_int(args, 2, "bytes_slice")?.clamp(0, len) as usize;
    let slice = if start >= end {
        Vec::new()
    } else {
        bytes[start..end].to_vec()
    };
    Ok(VmValue::Bytes(Rc::new(slice)))
}

#[harn_builtin(
    sig = "bytes_eq(left: bytes, right: bytes) -> bool",
    category = "bytes"
)]
fn bytes_eq_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    use subtle::ConstantTimeEq;

    let left = expect_bytes(args, 0, "bytes_eq")?;
    let right = expect_bytes(args, 1, "bytes_eq")?;
    Ok(VmValue::Bool(bool::from(left.ct_eq(right))))
}

pub(crate) const MODULE_BUILTINS: &[&VmBuiltinDef] = &[
    &BYTES_FROM_STRING_IMPL_DEF,
    &BYTES_TO_STRING_IMPL_DEF,
    &BYTES_TO_STRING_LOSSY_IMPL_DEF,
    &BYTES_TO_HEX_IMPL_DEF,
    &BYTES_FROM_HEX_IMPL_DEF,
    &BYTES_TO_BASE64_IMPL_DEF,
    &BYTES_FROM_BASE64_IMPL_DEF,
    &BYTES_LEN_IMPL_DEF,
    &BYTES_CONCAT_IMPL_DEF,
    &BYTES_SLICE_IMPL_DEF,
    &BYTES_EQ_IMPL_DEF,
];

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

    fn vm() -> Vm {
        let mut vm = Vm::new();
        register_bytes_builtins(&mut vm);
        vm
    }

    fn call(vm: &mut Vm, name: &str, args: Vec<VmValue>) -> Result<VmValue, VmError> {
        let f = vm.builtins.get(name).unwrap().clone();
        let mut out = String::new();
        f(&args, &mut out)
    }

    fn s(v: &str) -> VmValue {
        VmValue::String(Rc::from(v))
    }

    fn b(v: &[u8]) -> VmValue {
        VmValue::Bytes(Rc::new(v.to_vec()))
    }

    #[test]
    fn bytes_round_trip_utf8() {
        let mut vm = vm();
        let bytes = call(&mut vm, "bytes_from_string", vec![s("héllo")]).unwrap();
        let text = call(&mut vm, "bytes_to_string", vec![bytes]).unwrap();
        assert_eq!(text.display(), "héllo");
    }

    #[test]
    fn bytes_hex_round_trip() {
        let mut vm = vm();
        let bytes = call(&mut vm, "bytes_from_hex", vec![s("0001ff")]).unwrap();
        let hex = call(&mut vm, "bytes_to_hex", vec![bytes]).unwrap();
        assert_eq!(hex.display(), "0001ff");
    }

    #[test]
    fn bytes_base64_round_trip() {
        let mut vm = vm();
        let encoded = call(&mut vm, "bytes_to_base64", vec![b(&[0, 1, 2, 255])]).unwrap();
        let decoded = call(&mut vm, "bytes_from_base64", vec![encoded]).unwrap();
        assert_eq!(decoded.as_bytes().unwrap(), &[0, 1, 2, 255]);
    }

    #[test]
    fn bytes_slice_clamps() {
        let mut vm = vm();
        let sliced = call(
            &mut vm,
            "bytes_slice",
            vec![b(&[1, 2, 3, 4]), VmValue::Int(-5), VmValue::Int(99)],
        )
        .unwrap();
        assert_eq!(sliced.as_bytes().unwrap(), &[1, 2, 3, 4]);
    }
}