Skip to main content

tidepool_effect/
machine.rs

1use crate::dispatch::{DispatchEffect, EffectContext};
2use crate::error::EffectError;
3use tidepool_eval::heap::Heap;
4use tidepool_eval::value::Value;
5use tidepool_repr::{CoreExpr, DataConId, DataConTable};
6
7pub struct EffectMachine<'a> {
8    table: &'a DataConTable,
9    heap: &'a mut dyn Heap,
10    val_id: DataConId,
11    e_id: DataConId,
12    leaf_id: DataConId,
13    node_id: DataConId,
14    union_id: DataConId,
15}
16
17impl<'a> EffectMachine<'a> {
18    pub fn new(table: &'a DataConTable, heap: &'a mut dyn Heap) -> Result<Self, EffectError> {
19        let val_id = table
20            .get_by_name("Val")
21            .ok_or_else(|| EffectError::MissingConstructor { name: "Val" })?;
22        let e_id = table
23            .get_by_name("E")
24            .ok_or_else(|| EffectError::MissingConstructor { name: "E" })?;
25        let leaf_id = table
26            .get_by_name("Leaf")
27            .ok_or_else(|| EffectError::MissingConstructor { name: "Leaf" })?;
28        let node_id = table
29            .get_by_name("Node")
30            .ok_or_else(|| EffectError::MissingConstructor { name: "Node" })?;
31        let union_id = table
32            .get_by_name("Union")
33            .ok_or_else(|| EffectError::MissingConstructor { name: "Union" })?;
34        Ok(Self {
35            table,
36            heap,
37            val_id,
38            e_id,
39            leaf_id,
40            node_id,
41            union_id,
42        })
43    }
44
45    /// Run an Eff expression to completion with the given handler HList.
46    /// Backward-compatible: uses U=() (no user data).
47    pub fn run<H: DispatchEffect>(
48        &mut self,
49        expr: &CoreExpr,
50        handlers: &mut H,
51    ) -> Result<Value, EffectError> {
52        self.run_with_user(expr, handlers, &())
53    }
54
55    /// Run an Eff expression with user data threaded through to handlers.
56    pub fn run_with_user<U, H: DispatchEffect<U>>(
57        &mut self,
58        expr: &CoreExpr,
59        handlers: &mut H,
60        user: &U,
61    ) -> Result<Value, EffectError> {
62        let env = tidepool_eval::eval::env_from_datacon_table(self.table);
63        let mut current = tidepool_eval::eval::eval(expr, &env, self.heap)?;
64
65        loop {
66            let forced = tidepool_eval::eval::force(current, self.heap)?;
67            match forced {
68                Value::Con(id, ref fields) if id == self.val_id => {
69                    // Val x — pure result, done. Deep force to eliminate any ThunkRefs.
70                    let result = fields
71                        .first()
72                        .cloned()
73                        .unwrap_or(Value::Lit(tidepool_repr::Literal::LitInt(0)));
74                    return Ok(tidepool_eval::eval::deep_force(result, self.heap)?);
75                }
76                Value::Con(id, ref fields) if id == self.e_id => {
77                    // E (Union tag# req) k
78                    if fields.len() != 2 {
79                        return Err(EffectError::FieldCountMismatch {
80                            constructor: "E",
81                            expected: 2,
82                            got: fields.len(),
83                        });
84                    }
85                    let union_val = tidepool_eval::eval::deep_force(fields[0].clone(), self.heap)?;
86                    let k = tidepool_eval::eval::force(fields[1].clone(), self.heap)?;
87
88                    // Destructure Union(tag, req)
89                    let (tag, request) = match union_val {
90                        Value::Con(uid, ref ufields) if uid == self.union_id => {
91                            if ufields.len() != 2 {
92                                return Err(EffectError::FieldCountMismatch {
93                                    constructor: "Union",
94                                    expected: 2,
95                                    got: ufields.len(),
96                                });
97                            }
98                            let tag = match &ufields[0] {
99                                Value::Lit(tidepool_repr::Literal::LitWord(w)) => *w,
100                                Value::Lit(tidepool_repr::Literal::LitInt(i)) => *i as u64,
101                                other => {
102                                    return Err(EffectError::UnexpectedValue {
103                                        context: "Union tag (Word#/Int#)",
104                                        got: format!("{:?}", other),
105                                    })
106                                }
107                            };
108                            // deep_force the request so FromCore never sees ThunkRef
109                            let req = ufields[1].clone();
110                            (tag, req)
111                        }
112                        other => {
113                            return Err(EffectError::UnexpectedValue {
114                                context: "Union constructor",
115                                got: format!("{:?}", other),
116                            })
117                        }
118                    };
119
120                    // Dispatch to handler
121                    let cx = EffectContext::with_user(self.table, user);
122                    let response = handlers.dispatch(tag, &request, &cx)?;
123
124                    // Apply continuation
125                    current = self.apply_cont(k, response)?;
126                }
127                other => {
128                    return Err(EffectError::UnexpectedValue {
129                        context: "Val or E constructor",
130                        got: format!("{:?}", other),
131                    });
132                }
133            }
134        }
135    }
136
137    /// Async version of run_with_user. Same interpreter loop, but awaits handler dispatch.
138    /// Not Send (because &mut dyn Heap is not Send). Consumer wraps in a task that owns the machine.
139    #[cfg(feature = "async")]
140    pub async fn run_async<U: Sync, H: crate::dispatch::AsyncDispatchEffect<U>>(
141        &mut self,
142        expr: &CoreExpr,
143        handlers: &mut H,
144        user: &U,
145    ) -> Result<Value, EffectError> {
146        let env = tidepool_eval::eval::env_from_datacon_table(self.table);
147        let mut current = tidepool_eval::eval::eval(expr, &env, self.heap)?;
148
149        loop {
150            let forced = tidepool_eval::eval::force(current, self.heap)?;
151            match forced {
152                Value::Con(id, ref fields) if id == self.val_id => {
153                    let result = fields
154                        .first()
155                        .cloned()
156                        .unwrap_or(Value::Lit(tidepool_repr::Literal::LitInt(0)));
157                    return Ok(tidepool_eval::eval::deep_force(result, self.heap)?);
158                }
159                Value::Con(id, ref fields) if id == self.e_id => {
160                    if fields.len() != 2 {
161                        return Err(EffectError::FieldCountMismatch {
162                            constructor: "E",
163                            expected: 2,
164                            got: fields.len(),
165                        });
166                    }
167                    let union_val = tidepool_eval::eval::deep_force(fields[0].clone(), self.heap)?;
168                    let k = tidepool_eval::eval::force(fields[1].clone(), self.heap)?;
169
170                    let (tag, request) = match union_val {
171                        Value::Con(uid, ref ufields) if uid == self.union_id => {
172                            if ufields.len() != 2 {
173                                return Err(EffectError::FieldCountMismatch {
174                                    constructor: "Union",
175                                    expected: 2,
176                                    got: ufields.len(),
177                                });
178                            }
179                            let tag = match &ufields[0] {
180                                Value::Lit(tidepool_repr::Literal::LitWord(w)) => *w,
181                                Value::Lit(tidepool_repr::Literal::LitInt(i)) => *i as u64,
182                                other => {
183                                    return Err(EffectError::UnexpectedValue {
184                                        context: "Union tag (Word#/Int#)",
185                                        got: format!("{:?}", other),
186                                    })
187                                }
188                            };
189                            let req = ufields[1].clone();
190                            (tag, req)
191                        }
192                        other => {
193                            return Err(EffectError::UnexpectedValue {
194                                context: "Union constructor",
195                                got: format!("{:?}", other),
196                            })
197                        }
198                    };
199
200                    let cx = EffectContext::with_user(self.table, user);
201                    let response = handlers.dispatch(tag, &request, &cx).await?;
202
203                    current = self.apply_cont(k, response)?;
204                }
205                other => {
206                    return Err(EffectError::UnexpectedValue {
207                        context: "Val or E constructor",
208                        got: format!("{:?}", other),
209                    });
210                }
211            }
212        }
213    }
214
215    /// Apply a Leaf/Node continuation tree to a value.
216    fn apply_cont(&mut self, k: Value, arg: Value) -> Result<Value, EffectError> {
217        let k = tidepool_eval::eval::force(k, self.heap)?;
218        match k {
219            Value::Con(id, ref fields) if id == self.leaf_id => {
220                // Leaf(f) — apply f to arg
221                if fields.len() != 1 {
222                    return Err(EffectError::FieldCountMismatch {
223                        constructor: "Leaf",
224                        expected: 1,
225                        got: fields.len(),
226                    });
227                }
228                let f = tidepool_eval::eval::force(fields[0].clone(), self.heap)?;
229                Ok(self.apply_closure(f, arg)?)
230            }
231            Value::Con(id, ref fields) if id == self.node_id => {
232                // Node(k1, k2) — apply k1, then compose with k2
233                if fields.len() != 2 {
234                    return Err(EffectError::FieldCountMismatch {
235                        constructor: "Node",
236                        expected: 2,
237                        got: fields.len(),
238                    });
239                }
240                let k1 = fields[0].clone();
241                let k2 = fields[1].clone();
242                let result = self.apply_cont(k1, arg)?;
243                let forced = tidepool_eval::eval::force(result, self.heap)?;
244
245                match forced {
246                    Value::Con(vid, ref vfields) if vid == self.val_id => {
247                        // k1 returned Val(y) — feed y to k2
248                        let y = vfields
249                            .first()
250                            .cloned()
251                            .unwrap_or(Value::Lit(tidepool_repr::Literal::LitInt(0)));
252                        self.apply_cont(k2, y)
253                    }
254                    Value::Con(eid, ref efields) if eid == self.e_id => {
255                        // k1 yielded E(union, k') — compose: E(union, Node(k', k2))
256                        if efields.len() != 2 {
257                            return Err(EffectError::FieldCountMismatch {
258                                constructor: "E (continuation)",
259                                expected: 2,
260                                got: efields.len(),
261                            });
262                        }
263                        let union_val = efields[0].clone();
264                        let k_prime = efields[1].clone();
265                        let new_k = Value::Con(self.node_id, vec![k_prime, k2]);
266                        Ok(Value::Con(self.e_id, vec![union_val, new_k]))
267                    }
268                    other => Err(EffectError::UnexpectedValue {
269                        context: "Val or E after applying k1",
270                        got: format!("{:?}", other),
271                    }),
272                }
273            }
274            Value::Closure(..) => {
275                // Raw closure (degenerate continuation)
276                Ok(self.apply_closure(k, arg)?)
277            }
278            other => Err(EffectError::UnexpectedValue {
279                context: "Leaf or Node continuation",
280                got: format!("{:?}", other),
281            }),
282        }
283    }
284
285    /// Apply a single closure to a value.
286    fn apply_closure(&mut self, closure: Value, arg: Value) -> Result<Value, EffectError> {
287        match closure {
288            Value::Closure(env, binder, body) => {
289                let new_env = env.update(binder, arg);
290                Ok(tidepool_eval::eval::eval(&body, &new_env, self.heap)?)
291            }
292            Value::ConFun(tag, arity, mut args) => {
293                args.push(arg);
294                if args.len() == arity {
295                    Ok(Value::Con(tag, args))
296                } else {
297                    Ok(Value::ConFun(tag, arity, args))
298                }
299            }
300            other => Err(EffectError::UnexpectedValue {
301                context: "closure",
302                got: format!("{:?}", other),
303            }),
304        }
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use tidepool_eval::heap::VecHeap;
312    use tidepool_repr::datacon::DataCon;
313    use tidepool_repr::datacon_table::DataConTable;
314    use tidepool_repr::types::{DataConId, Literal, VarId};
315    use tidepool_repr::{CoreExpr, CoreFrame, RecursiveTree};
316
317    fn make_test_table() -> DataConTable {
318        let mut table = DataConTable::new();
319        table.insert(DataCon {
320            id: DataConId(1),
321            name: "Val".to_string(),
322            tag: 1,
323            rep_arity: 1,
324            field_bangs: vec![],
325        });
326        table.insert(DataCon {
327            id: DataConId(2),
328            name: "E".to_string(),
329            tag: 2,
330            rep_arity: 2,
331            field_bangs: vec![],
332        });
333        table.insert(DataCon {
334            id: DataConId(3),
335            name: "Leaf".to_string(),
336            tag: 1,
337            rep_arity: 1,
338            field_bangs: vec![],
339        });
340        table.insert(DataCon {
341            id: DataConId(4),
342            name: "Node".to_string(),
343            tag: 2,
344            rep_arity: 2,
345            field_bangs: vec![],
346        });
347        table.insert(DataCon {
348            id: DataConId(5),
349            name: "Union".to_string(),
350            tag: 1,
351            rep_arity: 2,
352            field_bangs: vec![],
353        });
354        table
355    }
356
357    #[test]
358    fn test_effect_machine_pure_val() {
359        // Eff expression that is just Val(42)
360        let table = make_test_table();
361        let mut heap = VecHeap::new();
362
363        // Build: Con(Val, [Lit(42)])
364        let expr: CoreExpr = RecursiveTree {
365            nodes: vec![
366                CoreFrame::Lit(Literal::LitInt(42)),
367                CoreFrame::Con {
368                    tag: DataConId(1), // Val
369                    fields: vec![0],
370                },
371            ],
372        };
373
374        let mut handlers = frunk::HNil;
375        let mut machine = EffectMachine::new(&table, &mut heap).unwrap();
376        let result = machine.run(&expr, &mut handlers).unwrap();
377
378        match result {
379            Value::Lit(Literal::LitInt(n)) => assert_eq!(n, 42),
380            other => panic!("Expected Lit(42), got {:?}", other),
381        }
382    }
383
384    #[test]
385    fn test_effect_machine_single_effect() {
386        // Build: E(Union(0, Lit(99)), Leaf(\x -> Val(x)))
387        // Handler at tag 0 receives Lit(99), returns Lit(100)
388        let table = make_test_table();
389        let mut heap = VecHeap::new();
390
391        let expr: CoreExpr = RecursiveTree {
392            nodes: vec![
393                // 0: Var(x) -- will be the Val payload
394                CoreFrame::Var(VarId(100)),
395                // 1: Con(Val, [Var(x)]) -- Val(x)
396                CoreFrame::Con {
397                    tag: DataConId(1), // Val
398                    fields: vec![0],
399                },
400                // 2: Lam(x, Con(Val, [Var(x)])) -- \x -> Val(x)
401                CoreFrame::Lam {
402                    binder: VarId(100),
403                    body: 1,
404                },
405                // 3: Con(Leaf, [lam]) -- Leaf(\x -> Val(x))
406                CoreFrame::Con {
407                    tag: DataConId(3), // Leaf
408                    fields: vec![2],
409                },
410                // 4: Lit(99) -- the request
411                CoreFrame::Lit(Literal::LitInt(99)),
412                // 5: Lit(0) -- tag Word# 0
413                CoreFrame::Lit(Literal::LitWord(0)),
414                // 6: Con(Union, [tag, req]) -- Union(0, 99)
415                CoreFrame::Con {
416                    tag: DataConId(5), // Union
417                    fields: vec![5, 4],
418                },
419                // 7: Con(E, [union, k]) -- E(Union(0, 99), Leaf(\x -> Val(x)))
420                CoreFrame::Con {
421                    tag: DataConId(2), // E
422                    fields: vec![6, 3],
423                },
424            ],
425        };
426
427        // Simple handler: receives any value, returns Lit(100)
428        use crate::dispatch::{EffectContext, EffectHandler};
429        use tidepool_bridge::FromCore;
430
431        struct TestReq(i64);
432        impl FromCore for TestReq {
433            fn from_value(
434                value: &Value,
435                _table: &DataConTable,
436            ) -> Result<Self, tidepool_bridge::BridgeError> {
437                match value {
438                    Value::Lit(Literal::LitInt(n)) => Ok(TestReq(*n)),
439                    _ => Err(tidepool_bridge::BridgeError::TypeMismatch {
440                        expected: "LitInt".into(),
441                        got: format!("{:?}", value),
442                    }),
443                }
444            }
445        }
446
447        struct TestHandler;
448        impl EffectHandler for TestHandler {
449            type Request = TestReq;
450            fn handle(
451                &mut self,
452                req: TestReq,
453                _cx: &EffectContext,
454            ) -> Result<Value, EffectError> {
455                // Echo back the request + 1
456                Ok(Value::Lit(Literal::LitInt(req.0 + 1)))
457            }
458        }
459
460        let mut handlers = frunk::hlist![TestHandler];
461        let mut machine = EffectMachine::new(&table, &mut heap).unwrap();
462        let result = machine.run(&expr, &mut handlers).unwrap();
463
464        match result {
465            Value::Lit(Literal::LitInt(n)) => assert_eq!(n, 100),
466            other => panic!("Expected Lit(100), got {:?}", other),
467        }
468    }
469
470    #[test]
471    fn test_run_with_user_data() {
472        // Same as single_effect but handler reads user data to compute response
473        let table = make_test_table();
474        let mut heap = VecHeap::new();
475
476        let expr: CoreExpr = RecursiveTree {
477            nodes: vec![
478                CoreFrame::Var(VarId(100)),
479                CoreFrame::Con {
480                    tag: DataConId(1),
481                    fields: vec![0],
482                },
483                CoreFrame::Lam {
484                    binder: VarId(100),
485                    body: 1,
486                },
487                CoreFrame::Con {
488                    tag: DataConId(3),
489                    fields: vec![2],
490                },
491                CoreFrame::Lit(Literal::LitInt(10)),
492                CoreFrame::Lit(Literal::LitWord(0)),
493                CoreFrame::Con {
494                    tag: DataConId(5),
495                    fields: vec![5, 4],
496                },
497                CoreFrame::Con {
498                    tag: DataConId(2),
499                    fields: vec![6, 3],
500                },
501            ],
502        };
503
504        use crate::dispatch::{EffectContext, EffectHandler};
505        use tidepool_bridge::FromCore;
506
507        struct TestReq(i64);
508        impl FromCore for TestReq {
509            fn from_value(
510                value: &Value,
511                _table: &DataConTable,
512            ) -> Result<Self, tidepool_bridge::BridgeError> {
513                match value {
514                    Value::Lit(Literal::LitInt(n)) => Ok(TestReq(*n)),
515                    _ => Err(tidepool_bridge::BridgeError::TypeMismatch {
516                        expected: "LitInt".into(),
517                        got: format!("{:?}", value),
518                    }),
519                }
520            }
521        }
522
523        struct UserData {
524            multiplier: i64,
525        }
526
527        struct UserHandler;
528        impl EffectHandler<UserData> for UserHandler {
529            type Request = TestReq;
530            fn handle(
531                &mut self,
532                req: TestReq,
533                cx: &EffectContext<'_, UserData>,
534            ) -> Result<Value, EffectError> {
535                Ok(Value::Lit(Literal::LitInt(req.0 * cx.user().multiplier)))
536            }
537        }
538
539        let user = UserData { multiplier: 5 };
540        let mut handlers = frunk::hlist![UserHandler];
541        let mut machine = EffectMachine::new(&table, &mut heap).unwrap();
542        let result = machine.run_with_user(&expr, &mut handlers, &user).unwrap();
543
544        match result {
545            Value::Lit(Literal::LitInt(n)) => assert_eq!(n, 50), // 10 * 5
546            other => panic!("Expected Lit(50), got {:?}", other),
547        }
548    }
549}
550
551#[cfg(all(test, feature = "async"))]
552mod async_tests {
553    use super::*;
554    use crate::dispatch::{AsyncEffectHandler, EffectContext};
555    use tidepool_bridge::FromCore;
556    use tidepool_eval::heap::VecHeap;
557    use tidepool_eval::value::Value;
558    use tidepool_repr::datacon::DataCon;
559    use tidepool_repr::datacon_table::DataConTable;
560    use tidepool_repr::types::{DataConId, Literal, VarId};
561    use tidepool_repr::{CoreExpr, CoreFrame, RecursiveTree};
562
563    fn make_test_table() -> DataConTable {
564        let mut table = DataConTable::new();
565        table.insert(DataCon {
566            id: DataConId(1),
567            name: "Val".to_string(),
568            tag: 1,
569            rep_arity: 1,
570            field_bangs: vec![],
571        });
572        table.insert(DataCon {
573            id: DataConId(2),
574            name: "E".to_string(),
575            tag: 2,
576            rep_arity: 2,
577            field_bangs: vec![],
578        });
579        table.insert(DataCon {
580            id: DataConId(3),
581            name: "Leaf".to_string(),
582            tag: 1,
583            rep_arity: 1,
584            field_bangs: vec![],
585        });
586        table.insert(DataCon {
587            id: DataConId(4),
588            name: "Node".to_string(),
589            tag: 2,
590            rep_arity: 2,
591            field_bangs: vec![],
592        });
593        table.insert(DataCon {
594            id: DataConId(5),
595            name: "Union".to_string(),
596            tag: 1,
597            rep_arity: 2,
598            field_bangs: vec![],
599        });
600        table
601    }
602
603    fn make_single_effect_expr() -> CoreExpr {
604        // E(Union(0, Lit(99)), Leaf(\x -> Val(x)))
605        RecursiveTree {
606            nodes: vec![
607                CoreFrame::Var(VarId(100)),
608                CoreFrame::Con {
609                    tag: DataConId(1),
610                    fields: vec![0],
611                },
612                CoreFrame::Lam {
613                    binder: VarId(100),
614                    body: 1,
615                },
616                CoreFrame::Con {
617                    tag: DataConId(3),
618                    fields: vec![2],
619                },
620                CoreFrame::Lit(Literal::LitInt(99)),
621                CoreFrame::Lit(Literal::LitWord(0)),
622                CoreFrame::Con {
623                    tag: DataConId(5),
624                    fields: vec![5, 4],
625                },
626                CoreFrame::Con {
627                    tag: DataConId(2),
628                    fields: vec![6, 3],
629                },
630            ],
631        }
632    }
633
634    struct TestReq(i64);
635    impl FromCore for TestReq {
636        fn from_value(
637            value: &Value,
638            _table: &DataConTable,
639        ) -> Result<Self, tidepool_bridge::BridgeError> {
640            match value {
641                Value::Lit(Literal::LitInt(n)) => Ok(TestReq(*n)),
642                _ => Err(tidepool_bridge::BridgeError::TypeMismatch {
643                    expected: "LitInt".into(),
644                    got: format!("{:?}", value),
645                }),
646            }
647        }
648    }
649
650    struct AsyncTestHandler;
651    impl AsyncEffectHandler for AsyncTestHandler {
652        type Request = TestReq;
653        fn handle(
654            &mut self,
655            req: TestReq,
656            _cx: &EffectContext<'_, ()>,
657        ) -> impl std::future::Future<Output = Result<Value, EffectError>> + Send + '_ {
658            async move { Ok(Value::Lit(Literal::LitInt(req.0 + 1))) }
659        }
660    }
661
662    #[tokio::test]
663    async fn test_async_single_effect() {
664        let table = make_test_table();
665        let mut heap = VecHeap::new();
666        let expr = make_single_effect_expr();
667
668        let mut handlers = frunk::hlist![AsyncTestHandler];
669        let mut machine = EffectMachine::new(&table, &mut heap).unwrap();
670        let result = machine.run_async(&expr, &mut handlers, &()).await.unwrap();
671
672        match result {
673            Value::Lit(Literal::LitInt(n)) => assert_eq!(n, 100),
674            other => panic!("Expected Lit(100), got {:?}", other),
675        }
676    }
677
678    #[tokio::test]
679    async fn test_async_with_user_data() {
680        let table = make_test_table();
681        let mut heap = VecHeap::new();
682        let expr: CoreExpr = RecursiveTree {
683            nodes: vec![
684                CoreFrame::Var(VarId(100)),
685                CoreFrame::Con {
686                    tag: DataConId(1),
687                    fields: vec![0],
688                },
689                CoreFrame::Lam {
690                    binder: VarId(100),
691                    body: 1,
692                },
693                CoreFrame::Con {
694                    tag: DataConId(3),
695                    fields: vec![2],
696                },
697                CoreFrame::Lit(Literal::LitInt(10)),
698                CoreFrame::Lit(Literal::LitWord(0)),
699                CoreFrame::Con {
700                    tag: DataConId(5),
701                    fields: vec![5, 4],
702                },
703                CoreFrame::Con {
704                    tag: DataConId(2),
705                    fields: vec![6, 3],
706                },
707            ],
708        };
709
710        struct UserData {
711            multiplier: i64,
712        }
713
714        struct AsyncUserHandler;
715        impl AsyncEffectHandler<UserData> for AsyncUserHandler {
716            type Request = TestReq;
717            fn handle(
718                &mut self,
719                req: TestReq,
720                cx: &EffectContext<'_, UserData>,
721            ) -> impl std::future::Future<Output = Result<Value, EffectError>> + Send + '_ {
722                let result = req.0 * cx.user().multiplier;
723                async move { Ok(Value::Lit(Literal::LitInt(result))) }
724            }
725        }
726
727        let user = UserData { multiplier: 7 };
728        let mut handlers = frunk::hlist![AsyncUserHandler];
729        let mut machine = EffectMachine::new(&table, &mut heap).unwrap();
730        let result = machine
731            .run_async(&expr, &mut handlers, &user)
732            .await
733            .unwrap();
734
735        match result {
736            Value::Lit(Literal::LitInt(n)) => assert_eq!(n, 70), // 10 * 7
737            other => panic!("Expected Lit(70), got {:?}", other),
738        }
739    }
740
741    #[tokio::test]
742    async fn test_async_pure_val() {
743        let table = make_test_table();
744        let mut heap = VecHeap::new();
745        let expr: CoreExpr = RecursiveTree {
746            nodes: vec![
747                CoreFrame::Lit(Literal::LitInt(42)),
748                CoreFrame::Con {
749                    tag: DataConId(1),
750                    fields: vec![0],
751                },
752            ],
753        };
754
755        let mut handlers = frunk::HNil;
756        let mut machine = EffectMachine::new(&table, &mut heap).unwrap();
757        let result = machine.run_async(&expr, &mut handlers, &()).await.unwrap();
758
759        match result {
760            Value::Lit(Literal::LitInt(n)) => assert_eq!(n, 42),
761            other => panic!("Expected Lit(42), got {:?}", other),
762        }
763    }
764}