harn-vm 0.7.49

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

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) {
    vm.register_builtin("bytes_from_string", |args, _out| {
        let text = expect_string(args, 0, "bytes_from_string")?;
        Ok(VmValue::Bytes(Rc::new(text.as_bytes().to_vec())))
    });

    vm.register_builtin("bytes_to_string", |args, _out| {
        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)))
    });

    vm.register_builtin("bytes_to_string_lossy", |args, _out| {
        let bytes = expect_bytes(args, 0, "bytes_to_string_lossy")?;
        Ok(VmValue::String(Rc::from(
            String::from_utf8_lossy(bytes).into_owned(),
        )))
    });

    vm.register_builtin("bytes_to_hex", |args, _out| {
        let bytes = expect_bytes(args, 0, "bytes_to_hex")?;
        Ok(VmValue::String(Rc::from(hex::encode(bytes))))
    });

    vm.register_builtin("bytes_from_hex", |args, _out| {
        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)))
    });

    vm.register_builtin("bytes_to_base64", |args, _out| {
        use base64::Engine;

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

    vm.register_builtin("bytes_from_base64", |args, _out| {
        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)))
    });

    vm.register_builtin("bytes_len", |args, _out| {
        let bytes = expect_bytes(args, 0, "bytes_len")?;
        Ok(VmValue::Int(bytes.len() as i64))
    });

    vm.register_builtin("bytes_concat", |args, _out| {
        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)))
    });

    vm.register_builtin("bytes_slice", |args, _out| {
        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)))
    });

    vm.register_builtin("bytes_eq", |args, _out| {
        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))))
    });
}

#[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]);
    }
}