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}