Skip to main content

vm/builtins/runtime/
print.rs

1use crate::vm::{CallOutcome, CallReturn, HostFunction, Value, Vm, VmResult};
2
3pub fn format_value(value: &Value) -> String {
4    match value {
5        Value::Null => "null".to_string(),
6        Value::Int(value) => value.to_string(),
7        Value::Float(value) => value.to_string(),
8        Value::Bool(value) => value.to_string(),
9        Value::String(value) => value.as_str().to_string(),
10        Value::Bytes(value) => format_bytes(value.as_ref()),
11        Value::Array(values) => {
12            let parts = values
13                .iter()
14                .map(format_value)
15                .collect::<Vec<_>>()
16                .join(", ");
17            format!("[{parts}]")
18        }
19        Value::Map(entries) => {
20            let parts = entries
21                .iter()
22                .map(|(key, value)| format!("{}: {}", format_value(key), format_value(value)))
23                .collect::<Vec<_>>()
24                .join(", ");
25            format!("{{{parts}}}")
26        }
27    }
28}
29
30fn format_bytes(bytes: &[u8]) -> String {
31    let preview_len = bytes.len().min(16);
32    let mut preview = String::with_capacity(preview_len * 2);
33    for byte in &bytes[..preview_len] {
34        preview.push(hex_nibble(byte >> 4));
35        preview.push(hex_nibble(byte & 0x0F));
36    }
37    if bytes.len() > preview_len {
38        format!("bytes[len={} hex={}..]", bytes.len(), preview)
39    } else {
40        format!("bytes[len={} hex={}]", bytes.len(), preview)
41    }
42}
43
44fn hex_nibble(value: u8) -> char {
45    match value {
46        0..=9 => char::from(b'0' + value),
47        10..=15 => char::from(b'a' + (value - 10)),
48        _ => unreachable!("hex nibble out of range"),
49    }
50}
51
52fn format_values(args: &[Value]) -> String {
53    args.iter().map(format_value).collect::<Vec<_>>().join(" ")
54}
55
56fn borrowed_args_return(args: &[Value]) -> CallReturn {
57    match args {
58        [] => CallReturn::none(),
59        [value] => CallReturn::one(value.clone()),
60        _ => CallReturn::one(Value::array(args.to_vec())),
61    }
62}
63
64pub struct PrintHostFunction<F>
65where
66    F: FnMut(String) + Send + 'static,
67{
68    sink: F,
69}
70
71impl<F> PrintHostFunction<F>
72where
73    F: FnMut(String) + Send + 'static,
74{
75    pub fn new(sink: F) -> Self {
76        Self { sink }
77    }
78}
79
80impl<F> HostFunction for PrintHostFunction<F>
81where
82    F: FnMut(String) + Send + 'static,
83{
84    fn call(&mut self, _vm: &mut Vm, args: &[Value]) -> VmResult<CallOutcome> {
85        let rendered = format_values(args);
86        (self.sink)(rendered);
87        Ok(CallOutcome::Return(borrowed_args_return(args)))
88    }
89}
90
91pub struct PrintlnHostFunction<F>
92where
93    F: FnMut(String) + Send + 'static,
94{
95    sink: F,
96}
97
98impl<F> PrintlnHostFunction<F>
99where
100    F: FnMut(String) + Send + 'static,
101{
102    pub fn new(sink: F) -> Self {
103        Self { sink }
104    }
105}
106
107impl<F> HostFunction for PrintlnHostFunction<F>
108where
109    F: FnMut(String) + Send + 'static,
110{
111    fn call(&mut self, _vm: &mut Vm, args: &[Value]) -> VmResult<CallOutcome> {
112        let mut rendered = format_values(args);
113        rendered.push('\n');
114        (self.sink)(rendered);
115        Ok(CallOutcome::Return(borrowed_args_return(args)))
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::sync::{Arc, Mutex};
122
123    use crate::bytecode::Program;
124    use crate::vm::{HostFunction, Value, Vm};
125
126    use super::{PrintHostFunction, PrintlnHostFunction, format_value};
127
128    fn vm_for_host_call() -> Vm {
129        Vm::new(Program::new(
130            Vec::new(),
131            vec![crate::bytecode::OpCode::Ret as u8],
132        ))
133    }
134
135    #[test]
136    fn format_value_renders_nested_values() {
137        let value = Value::map(vec![(
138            Value::string("items"),
139            Value::array(vec![Value::Int(1), Value::Bool(true)]),
140        )]);
141        assert_eq!(format_value(&value), "{items: [1, true]}");
142    }
143
144    #[test]
145    fn print_host_function_writes_to_sink() {
146        let lines = Arc::new(Mutex::new(Vec::<String>::new()));
147        let sink_lines = Arc::clone(&lines);
148        let mut host = PrintHostFunction::new(move |rendered| {
149            if let Ok(mut guard) = sink_lines.lock() {
150                guard.push(rendered);
151            }
152        });
153        let mut vm = vm_for_host_call();
154
155        host.call(&mut vm, &[Value::Int(2), Value::string("ok")])
156            .expect("print host call should succeed");
157
158        let guard = lines.lock().expect("sink should be lockable");
159        assert_eq!(guard.as_slice(), ["2 ok"]);
160    }
161
162    #[test]
163    fn println_host_function_appends_newline() {
164        let lines = Arc::new(Mutex::new(Vec::<String>::new()));
165        let sink_lines = Arc::clone(&lines);
166        let mut host = PrintlnHostFunction::new(move |rendered| {
167            if let Ok(mut guard) = sink_lines.lock() {
168                guard.push(rendered);
169            }
170        });
171        let mut vm = vm_for_host_call();
172
173        host.call(&mut vm, &[Value::string("line")])
174            .expect("println host call should succeed");
175
176        let guard = lines.lock().expect("sink should be lockable");
177        assert_eq!(guard.as_slice(), ["line\n"]);
178    }
179}