geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use num_bigint::BigInt;

use super::super::list::{ListValue, ListValueKind};
use super::{bit_array, custom, function, tuple};

pub(super) fn write(output: &mut String, value: &ListValue) {
    if let ListValueKind::Int(values) = value.kind()
        && let Some(chars) = printable_charlist(values)
    {
        output.push_str("charlist.from_string(\"");
        output.push_str(&chars);
        output.push_str("\")");
        return;
    }

    match value.kind() {
        ListValueKind::Parameter(_) => output.push_str("[]"),
        ListValueKind::Int(values) => write_items(output, values, |output, value| {
            output.push_str(&value.to_string());
        }),
        ListValueKind::String(values) => write_items(output, values, |output, value| {
            output.push_str(&format!("{value:?}"));
        }),
        ListValueKind::BitArray(values) => write_items(output, values, bit_array::write),
        ListValueKind::UtfCodepoint(values) => {
            write_items(output, values, |output, value| {
                output.push_str(&format!("{value:?}"));
            });
        }
        ListValueKind::Custom { values, .. } => write_items(output, values, custom::write),
        ListValueKind::External { values, .. } => {
            write_items(output, values, |output, value| {
                output.push_str(value.inspection());
            });
        }
        ListValueKind::Float(values) => write_items(output, values, |output, value| {
            output.push_str(&format!("{value:?}"));
        }),
        ListValueKind::Bool(values) => write_items(output, values, |output, value| {
            output.push_str(if *value { "True" } else { "False" });
        }),
        ListValueKind::Nil(len) => {
            output.push('[');
            for index in 0..*len {
                if index != 0 {
                    output.push_str(", ");
                }
                output.push_str("Nil");
            }
            output.push(']');
        }
        ListValueKind::Tuple { values, .. } => write_items(output, values, |output, values| {
            tuple::write(output, values)
        }),
        ListValueKind::List { values, .. } => write_items(output, values, write),
        ListValueKind::Function { values, .. } => write_items(output, values, function::write),
    }
}

fn printable_charlist(values: &[BigInt]) -> Option<String> {
    if values.is_empty() {
        return None;
    }

    let mut chars = String::with_capacity(values.len());
    for value in values {
        match u8::try_from(value) {
            Ok(byte @ 32..=126) => chars.push(char::from(byte)),
            Ok(_) | Err(_) => return None,
        }
    }
    Some(chars)
}

fn write_items<T>(output: &mut String, values: &[T], mut write_item: impl FnMut(&mut String, &T)) {
    output.push('[');
    let mut separator = "";
    for value in values {
        output.push_str(separator);
        write_item(output, value);
        separator = ", ";
    }
    output.push(']');
}

#[cfg(test)]
mod tests {
    use super::super::super::{BitArrayValue, CustomValue, FunctionValue, ListValue, Value};
    use crate::host::HostExternalStore;
    use crate::plan::execution::function::{
        CoreRuntimeFunctionId, IntFunctionId, RuntimeFunctionId,
    };
    use crate::plan::{
        CustomType, CustomTypeName, ExternalType, ExternalTypeName, FunctionType, TypeParameterId,
        ValueType,
    };
    use crate::runtime::ExternalValue;

    #[test]
    fn writes_empty_printable_and_non_printable_int_lists() {
        let cases = [
            (
                ListValue::empty(ValueType::Parameter(TypeParameterId(0))),
                "[]",
            ),
            (ListValue::int(Vec::new()), "[]"),
            (
                ListValue::int(vec![72.into(), 105.into(), 33.into()]),
                r#"charlist.from_string("Hi!")"#,
            ),
            (
                ListValue::int(vec![31.into(), 126.into(), 256.into()]),
                "[31, 126, 256]",
            ),
        ];

        for (value, expected) in cases {
            assert_eq!(Value::List(value).inspect().to_string(), expected);
        }
    }

    #[test]
    fn writes_every_non_int_list_storage_family() {
        fn source_hash(
            context: &crate::host::HostExternalHashing<'_>,
            value: &crate::host::HostStoredValue<num_bigint::BigInt>,
        ) -> u64 {
            context.stored_value_hash(value)
        }

        fn inspect(
            context: &crate::host::HostExternalInspection<'_>,
            value: &crate::host::HostStoredValue<num_bigint::BigInt>,
        ) -> ecow::EcoString {
            context.inspect_stored_value(value)
        }

        let custom_type = CustomType::new(
            CustomTypeName::new("geam".into(), "main".into(), "Boxed".into()),
            Vec::new(),
        );
        let custom =
            CustomValue::from_evaluated(custom_type.clone(), "Boxed".into(), 0, Vec::new());
        let function = FunctionValue::new(
            RuntimeFunctionId::Core(CoreRuntimeFunctionId::Int(IntFunctionId(0))),
            Vec::new(),
            FunctionType::new(Vec::new(), ValueType::Int),
        );
        let external_type = ExternalType::new(
            ExternalTypeName::new("application".into(), "main".into(), "Resource".into()),
            Vec::new(),
        );
        let store = HostExternalStore::default();
        let source_equal =
            |context: &crate::host::HostExternalEquality<'_>,
             left: &crate::host::HostStoredValue<num_bigint::BigInt>,
             right: &crate::host::HostStoredValue<num_bigint::BigInt>| {
                context.stored_values_equal(left, right)
            };
        let first = store.insert(
            crate::host::HostStoredValue::new(crate::runtime::StoredRuntimeValue::test_int(
                7.into(),
            )),
            source_equal,
            source_hash,
            inspect,
        );
        let equal = store.insert(
            crate::host::HostStoredValue::new(crate::runtime::StoredRuntimeValue::test_int(
                7.into(),
            )),
            source_equal,
            source_hash,
            inspect,
        );
        let stored_equal =
            |left: &crate::runtime::StoredRuntimeValue,
             right: &crate::runtime::StoredRuntimeValue| left.value() == right.value();
        let equality = crate::host::HostExternalEquality::new(&stored_equal);
        assert!(first.source_equal(&equality, &equal));
        let stored_hash = |_: &crate::runtime::StoredRuntimeValue| 7;
        let stored_inspect = |_: &crate::runtime::StoredRuntimeValue| "Resource(7)".into();
        assert_eq!(
            first.source_hash(&crate::host::HostExternalHashing::new(&stored_hash)),
            7,
        );
        assert_eq!(
            first.inspection(&crate::host::HostExternalInspection::new(&stored_inspect)),
            "Resource(7)",
        );
        let external =
            ExternalValue::from_evaluated(external_type.clone(), first, "Resource(7)".into());
        let cases = [
            (ListValue::string(vec!["one".into()]), r#"["one"]"#),
            (
                ListValue::bit_array(vec![BitArrayValue::from_bytes(vec![1])]),
                "[<<1>>]",
            ),
            (ListValue::utf_codepoint(vec!['A']), "['A']"),
            (
                ListValue::from_evaluated_custom(custom_type, vec![custom]),
                "[Boxed]",
            ),
            (
                ListValue::from_evaluated_external(external_type, vec![external]),
                "[Resource(7)]",
            ),
            (ListValue::float(vec![1.5]), "[1.5]"),
            (ListValue::bool(vec![true, false]), "[True, False]"),
            (ListValue::nil(2), "[Nil, Nil]"),
            (
                ListValue::from_evaluated_tuple(
                    vec![ValueType::Int],
                    vec![vec![Value::Int(1.into())]],
                ),
                "[#(1)]",
            ),
            (
                ListValue::from_evaluated_list(
                    ValueType::Int,
                    vec![ListValue::int(vec![1.into()])],
                ),
                "[[1]]",
            ),
            (
                ListValue::from_evaluated_function(function.type_(), vec![function]),
                "[//fn() { ... }]",
            ),
        ];

        for (value, expected) in cases {
            assert_eq!(Value::List(value).inspect().to_string(), expected);
        }
    }
}