Skip to main content

yulang_runtime/
host.rs

1use yulang_typed_ir as typed_ir;
2
3use crate::vm::{VmError, VmModule, VmProfile, VmRequest, VmResult, VmValue};
4
5pub struct HostRunOutput {
6    pub results: Vec<VmResult>,
7    pub stdout: String,
8    pub vm_profile: VmProfile,
9}
10
11pub fn eval_roots_with_basic_host(module: &VmModule) -> Result<HostRunOutput, VmError> {
12    let mut results = Vec::new();
13    let mut stdout = String::new();
14    let mut vm_profile = VmProfile::default();
15
16    for index in 0..module.module().root_exprs.len() {
17        let result = eval_root_with_basic_host(module, index, &mut stdout)?;
18        vm_profile.merge(result.1);
19        let result = result.0;
20        results.push(result);
21    }
22
23    Ok(HostRunOutput {
24        results,
25        stdout,
26        vm_profile,
27    })
28}
29
30pub fn eval_root_with_basic_host(
31    module: &VmModule,
32    index: usize,
33    stdout: &mut String,
34) -> Result<(VmResult, VmProfile), VmError> {
35    let (mut result, mut vm_profile) = module.eval_root_expr_profiled(index)?;
36    loop {
37        match result {
38            VmResult::Value(value @ VmValue::Thunk(_)) => {
39                let forced = module.force_value_profiled(value)?;
40                result = forced.0;
41                vm_profile.merge(forced.1);
42            }
43            VmResult::Value(_) => return Ok((result, vm_profile)),
44            VmResult::Request(request) => {
45                let Some(value) = handle_host_request(&request, stdout) else {
46                    return Ok((VmResult::Request(request), vm_profile));
47                };
48                let resumed = module.resume_request_profiled(request, value)?;
49                result = resumed.0;
50                vm_profile.merge(resumed.1);
51            }
52        }
53    }
54}
55
56fn handle_host_request(request: &VmRequest, stdout: &mut String) -> Option<VmValue> {
57    handle_out_request(request, stdout)
58        .or_else(|| handle_err_request(request))
59        .or_else(|| handle_warn_request(request))
60        .or_else(|| handle_die_request(request))
61        .or_else(|| handle_debug_request(request))
62        .or_else(|| handle_fs_request(request))
63}
64
65fn handle_out_request(request: &VmRequest, stdout: &mut String) -> Option<VmValue> {
66    if !out_effect_is(&request.effect, "out", "write") {
67        return None;
68    }
69    stdout.push_str(&host_string(&request.payload));
70    Some(VmValue::Unit)
71}
72
73fn handle_err_request(request: &VmRequest) -> Option<VmValue> {
74    if !out_effect_is(&request.effect, "err", "write") {
75        return None;
76    }
77    eprint!("{}", host_string(&request.payload));
78    Some(VmValue::Unit)
79}
80
81fn handle_warn_request(request: &VmRequest) -> Option<VmValue> {
82    if !out_effect_is(&request.effect, "warn", "warn") {
83        return None;
84    }
85    eprintln!("warning: {}", host_string(&request.payload));
86    Some(VmValue::Unit)
87}
88
89fn handle_die_request(request: &VmRequest) -> Option<VmValue> {
90    if !out_effect_is(&request.effect, "die", "die") {
91        return None;
92    }
93    eprintln!("die: {}", host_string(&request.payload));
94    std::process::exit(1);
95}
96
97fn out_effect_is(path: &typed_ir::Path, act: &str, op: &str) -> bool {
98    let [std, mod_seg, act_seg, operation] = path.segments.as_slice() else {
99        return false;
100    };
101
102    std.0 == "std" && mod_seg.0 == "out" && act_seg.0 == act && operation.0 == op
103}
104
105#[cfg(not(target_arch = "wasm32"))]
106fn handle_fs_request(request: &VmRequest) -> Option<VmValue> {
107    use std::fs;
108
109    let op = fs_effect_operation(&request.effect)?;
110    match op {
111        "read_at" => {
112            let (path, range) = host_path_range_pair(&request.payload)?;
113            Some(match fs::read(path.as_ref()) {
114                Ok(bytes) => host_read_at_result(&bytes, &range),
115                Err(error) => host_fs_error_result(host_fs_error_code(&error)),
116            })
117        }
118        "write_at" => {
119            let (path, range, text) = host_path_range_text_triple(&request.payload)?;
120            let code = match fs::read(path.as_ref()) {
121                Ok(mut bytes) => {
122                    let (start, end) = host_normalized_int_range(&range, bytes.len())?;
123                    bytes.splice(start..end, text.as_bytes().iter().copied());
124                    fs::write(path.as_ref(), bytes)
125                        .err()
126                        .map_or(0, |error| host_fs_error_code(&error))
127                }
128                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
129                    fs::write(path.as_ref(), text.as_bytes())
130                        .err()
131                        .map_or(0, |error| host_fs_error_code(&error))
132                }
133                Err(error) => host_fs_error_code(&error),
134            };
135            Some(VmValue::Int(code.to_string()))
136        }
137        "read_text" => {
138            let path = host_path_string(&request.payload)?;
139            Some(match fs::read_to_string(path) {
140                Ok(text) => host_opt_some(VmValue::String(text.into())),
141                Err(_) => host_opt_none(),
142            })
143        }
144        "write_text" => {
145            let (path, text) = host_path_text_pair(&request.payload)?;
146            Some(VmValue::Bool(fs::write(path, text).is_ok()))
147        }
148        "exists" => {
149            let path = host_path_value(&request.payload)?;
150            Some(VmValue::Bool(path.exists()))
151        }
152        "is_file" => {
153            let path = host_path_value(&request.payload)?;
154            Some(VmValue::Bool(path.is_file()))
155        }
156        "is_dir" => {
157            let path = host_path_value(&request.payload)?;
158            Some(VmValue::Bool(path.is_dir()))
159        }
160        _ => None,
161    }
162}
163
164#[cfg(target_arch = "wasm32")]
165fn handle_fs_request(_request: &VmRequest) -> Option<VmValue> {
166    None
167}
168
169fn fs_effect_operation(path: &typed_ir::Path) -> Option<&str> {
170    let [std, fs_module, fs_act, operation] = path.segments.as_slice() else {
171        return None;
172    };
173
174    (std.0 == "std" && fs_module.0 == "fs" && fs_act.0 == "fs").then_some(operation.0.as_str())
175}
176
177fn handle_debug_request(request: &VmRequest) -> Option<VmValue> {
178    debug_effect_is(&request.effect)
179        .then(|| VmValue::String(host_debug_string(&request.payload).into()))
180}
181
182fn debug_effect_is(path: &typed_ir::Path) -> bool {
183    let [std, prelude, role, method] = path.segments.as_slice() else {
184        return false;
185    };
186
187    std.0 == "std" && prelude.0 == "prelude" && role.0 == "Debug" && method.0 == "debug"
188}
189
190fn host_path_string(value: &VmValue) -> Option<String> {
191    match value {
192        VmValue::String(value) => Some(value.to_flat_string()),
193        _ => None,
194    }
195}
196
197fn host_path_value(value: &VmValue) -> Option<std::rc::Rc<std::path::PathBuf>> {
198    match value {
199        VmValue::Path(value) => Some(value.clone()),
200        VmValue::String(value) => Some(std::rc::Rc::new(std::path::PathBuf::from(
201            value.to_flat_string(),
202        ))),
203        _ => None,
204    }
205}
206
207fn host_path_range_pair(value: &VmValue) -> Option<(std::rc::Rc<std::path::PathBuf>, VmValue)> {
208    let VmValue::Tuple(items) = value else {
209        return None;
210    };
211    let [path, range] = items.as_slice() else {
212        return None;
213    };
214    Some((host_path_value(path)?, range.clone()))
215}
216
217fn host_path_range_text_triple(
218    value: &VmValue,
219) -> Option<(std::rc::Rc<std::path::PathBuf>, VmValue, String)> {
220    let VmValue::Tuple(items) = value else {
221        return None;
222    };
223    let [path, range, text] = items.as_slice() else {
224        return None;
225    };
226    Some((
227        host_path_value(path)?,
228        range.clone(),
229        host_path_string(text)?,
230    ))
231}
232
233fn host_path_text_pair(value: &VmValue) -> Option<(String, String)> {
234    let VmValue::Tuple(items) = value else {
235        return None;
236    };
237    let [path, text] = items.as_slice() else {
238        return None;
239    };
240    Some((host_path_string(path)?, host_path_string(text)?))
241}
242
243fn host_read_at_result(bytes: &[u8], range: &VmValue) -> VmValue {
244    let Some((start, end)) = host_normalized_int_range(range, bytes.len()) else {
245        return host_fs_error_result(3);
246    };
247    let slice = &bytes[start..end];
248    let valid_len = match std::str::from_utf8(slice) {
249        Ok(text) => {
250            return VmValue::Tuple(vec![
251                VmValue::Int("0".to_string()),
252                VmValue::String(text.into()),
253                host_range_value(start, end),
254            ]);
255        }
256        Err(error) => error.valid_up_to(),
257    };
258    let valid_end = start + valid_len;
259    let text = std::str::from_utf8(&bytes[start..valid_end]).unwrap_or("");
260    VmValue::Tuple(vec![
261        VmValue::Int("0".to_string()),
262        VmValue::String(text.into()),
263        host_range_value(start, valid_end),
264    ])
265}
266
267fn host_fs_error_result(code: i64) -> VmValue {
268    VmValue::Tuple(vec![
269        VmValue::Int(code.to_string()),
270        VmValue::String("".into()),
271        host_range_value(0, 0),
272    ])
273}
274
275fn host_fs_error_code(error: &std::io::Error) -> i64 {
276    match error.kind() {
277        std::io::ErrorKind::NotFound => 1,
278        std::io::ErrorKind::PermissionDenied => 2,
279        _ => 3,
280    }
281}
282
283fn host_normalized_int_range(value: &VmValue, len: usize) -> Option<(usize, usize)> {
284    let VmValue::Variant { tag, value } = value else {
285        return None;
286    };
287    if tag.0 != "within" {
288        return None;
289    }
290    let VmValue::Tuple(items) = value.as_deref()? else {
291        return None;
292    };
293    let [start, end] = items.as_slice() else {
294        return None;
295    };
296    let start = host_normalized_start_bound(start)?;
297    let end = host_normalized_end_bound(end, len)?;
298    (start <= end && end <= len).then_some((start, end))
299}
300
301fn host_normalized_start_bound(value: &VmValue) -> Option<usize> {
302    let VmValue::Variant { tag, value } = value else {
303        return None;
304    };
305    match tag.0.as_str() {
306        "unbounded" => Some(0),
307        "included" => usize::try_from(host_bound_int(value.as_deref()?)?).ok(),
308        "excluded" => usize::try_from(host_bound_int(value.as_deref()?)? + 1).ok(),
309        _ => None,
310    }
311}
312
313fn host_normalized_end_bound(value: &VmValue, len: usize) -> Option<usize> {
314    let VmValue::Variant { tag, value } = value else {
315        return None;
316    };
317    match tag.0.as_str() {
318        "unbounded" => Some(len),
319        "included" => usize::try_from(host_bound_int(value.as_deref()?)? + 1).ok(),
320        "excluded" => usize::try_from(host_bound_int(value.as_deref()?)?).ok(),
321        _ => None,
322    }
323}
324
325fn host_bound_int(value: &VmValue) -> Option<i64> {
326    match value {
327        VmValue::Int(value) => value.parse().ok(),
328        _ => None,
329    }
330}
331
332fn host_range_value(start: usize, end: usize) -> VmValue {
333    VmValue::Variant {
334        tag: typed_ir::Name("within".to_string()),
335        value: Some(Box::new(VmValue::Tuple(vec![
336            host_bound_value("included", start),
337            host_bound_value("excluded", end),
338        ]))),
339    }
340}
341
342fn host_bound_value(tag: &str, value: usize) -> VmValue {
343    VmValue::Variant {
344        tag: typed_ir::Name(tag.to_string()),
345        value: Some(Box::new(VmValue::Int(value.to_string()))),
346    }
347}
348
349fn host_opt_some(value: VmValue) -> VmValue {
350    VmValue::Variant {
351        tag: typed_ir::Name("just".to_string()),
352        value: Some(Box::new(value)),
353    }
354}
355
356fn host_opt_none() -> VmValue {
357    VmValue::Variant {
358        tag: typed_ir::Name("nil".to_string()),
359        value: None,
360    }
361}
362
363fn host_string(value: &VmValue) -> String {
364    match value {
365        VmValue::String(value) => value.to_flat_string(),
366        VmValue::Int(value) | VmValue::Float(value) => value.clone(),
367        VmValue::Bool(value) => value.to_string(),
368        VmValue::Unit => "()".to_string(),
369        VmValue::Bytes(value) => format!("<bytes len={}>", value.len()),
370        VmValue::Path(value) => value.display().to_string(),
371        _ => format!("{value:?}"),
372    }
373}
374
375fn host_debug_string(value: &VmValue) -> String {
376    match value {
377        VmValue::Int(value) | VmValue::Float(value) => value.clone(),
378        VmValue::String(value) => format!("{:?}", value.to_flat_string()),
379        VmValue::Bytes(value) => format!("<bytes len={}>", value.len()),
380        VmValue::Path(value) => format!("{:?}", value.display().to_string()),
381        VmValue::Bool(value) => value.to_string(),
382        VmValue::Unit => "()".to_string(),
383        VmValue::List(items) => format!(
384            "[{}]",
385            items
386                .to_vec()
387                .iter()
388                .map(|item| host_debug_string(item.as_ref()))
389                .collect::<Vec<_>>()
390                .join(", ")
391        ),
392        VmValue::Tuple(items) => format!(
393            "({})",
394            items
395                .iter()
396                .map(host_debug_string)
397                .collect::<Vec<_>>()
398                .join(", ")
399        ),
400        VmValue::Record(fields) => format!(
401            "{{{}}}",
402            fields
403                .iter()
404                .map(|(name, value)| format!("{}: {}", name.0, host_debug_string(value)))
405                .collect::<Vec<_>>()
406                .join(", ")
407        ),
408        VmValue::Variant { tag, value } => match value {
409            Some(value) => format!("{} {}", tag.0, host_debug_string(value)),
410            None => tag.0.clone(),
411        },
412        VmValue::EffectOp(path) => format!("<effect-op {}>", host_format_path(path)),
413        VmValue::PrimitiveOp(_) => "<primitive>".to_string(),
414        VmValue::Resume(_) => "<resume>".to_string(),
415        VmValue::Closure(_) => "<closure>".to_string(),
416        VmValue::Thunk(_) => "<thunk>".to_string(),
417        VmValue::EffectId(id) => format!("<effect-id {id}>"),
418    }
419}
420
421fn host_format_path(path: &typed_ir::Path) -> String {
422    path.segments
423        .iter()
424        .map(|segment| segment.0.as_str())
425        .collect::<Vec<_>>()
426        .join("::")
427}