Skip to main content

yulang_native/
cps_compare.rs

1use std::fmt;
2
3use yulang_runtime as runtime;
4
5use crate::cps_eval::{CpsEvalError, eval_cps_module};
6use crate::cps_lower::{CpsLowerError, lower_cps_module};
7use crate::cps_repr_cranelift::{CpsReprCraneliftError, compile_runtime_module_to_cps_repr_jit};
8use crate::cps_validate::{CpsValidateError, validate_cps_module};
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum CpsCompareError {
12    Lower(CpsLowerError),
13    Validate(CpsValidateError),
14    Eval(CpsEvalError),
15    Cranelift(CpsReprCraneliftError),
16    Vm(runtime::VmError),
17    ResidualRequest {
18        index: usize,
19        request: runtime::VmRequest,
20    },
21    RootCountMismatch {
22        vm: usize,
23        cps: usize,
24    },
25    ValueMismatch {
26        index: usize,
27        vm: runtime::VmValue,
28        cps: runtime::VmValue,
29    },
30    I64Mismatch {
31        index: usize,
32        vm: i64,
33        cps_cranelift: i64,
34    },
35    NonI64Value {
36        index: usize,
37        value: runtime::VmValue,
38    },
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct CpsReprI64CompareReport {
43    pub roots: Vec<CpsReprI64RootCompare>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct CpsReprI64RootCompare {
48    pub index: usize,
49    pub vm: i64,
50    pub cps_cranelift: i64,
51}
52
53impl fmt::Display for CpsCompareError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            CpsCompareError::Lower(error) => write!(f, "{error}"),
57            CpsCompareError::Validate(error) => write!(f, "{error}"),
58            CpsCompareError::Eval(error) => write!(f, "{error}"),
59            CpsCompareError::Cranelift(error) => write!(f, "{error}"),
60            CpsCompareError::Vm(error) => write!(f, "{error}"),
61            CpsCompareError::ResidualRequest { index, request } => write!(
62                f,
63                "VM root {index} produced a host/effect request instead of a value: {request:?}"
64            ),
65            CpsCompareError::RootCountMismatch { vm, cps } => {
66                write!(f, "VM produced {vm} roots but CPS produced {cps}")
67            }
68            CpsCompareError::ValueMismatch { index, vm, cps } => {
69                write!(f, "CPS root {index} mismatch: VM {vm:?}, CPS {cps:?}")
70            }
71            CpsCompareError::I64Mismatch {
72                index,
73                vm,
74                cps_cranelift,
75            } => write!(
76                f,
77                "CPS repr Cranelift root {index} mismatch: VM {vm}, Cranelift {cps_cranelift}"
78            ),
79            CpsCompareError::NonI64Value { index, value } => {
80                write!(f, "VM root {index} produced non-i64 value {value:?}")
81            }
82        }
83    }
84}
85
86impl std::error::Error for CpsCompareError {}
87
88impl From<CpsLowerError> for CpsCompareError {
89    fn from(value: CpsLowerError) -> Self {
90        Self::Lower(value)
91    }
92}
93
94impl From<CpsValidateError> for CpsCompareError {
95    fn from(value: CpsValidateError) -> Self {
96        Self::Validate(value)
97    }
98}
99
100impl From<CpsEvalError> for CpsCompareError {
101    fn from(value: CpsEvalError) -> Self {
102        Self::Eval(value)
103    }
104}
105
106impl From<CpsReprCraneliftError> for CpsCompareError {
107    fn from(value: CpsReprCraneliftError) -> Self {
108        Self::Cranelift(value)
109    }
110}
111
112impl From<runtime::VmError> for CpsCompareError {
113    fn from(value: runtime::VmError) -> Self {
114        Self::Vm(value)
115    }
116}
117
118pub fn compare_cps_module(module: &runtime::Module) -> Result<(), CpsCompareError> {
119    let cps_module = lower_cps_module(module)?;
120    validate_cps_module(&cps_module)?;
121    let cps_values = eval_cps_module(&cps_module)?;
122    let vm_results = runtime::compile_vm_module(module.clone())?.eval_roots()?;
123    if vm_results.len() != cps_values.len() {
124        return Err(CpsCompareError::RootCountMismatch {
125            vm: vm_results.len(),
126            cps: cps_values.len(),
127        });
128    }
129    for (index, (vm_result, cps)) in vm_results.into_iter().zip(cps_values).enumerate() {
130        let vm = match vm_result {
131            runtime::VmResult::Value(value) => value,
132            runtime::VmResult::Request(request) => {
133                return Err(CpsCompareError::ResidualRequest { index, request });
134            }
135        };
136        if vm != cps {
137            return Err(CpsCompareError::ValueMismatch { index, vm, cps });
138        }
139    }
140    Ok(())
141}
142
143pub fn compare_cps_repr_cranelift_i64(module: &runtime::Module) -> Result<(), CpsCompareError> {
144    compare_cps_repr_cranelift_i64_report(module).map(|_| ())
145}
146
147pub fn compare_cps_repr_cranelift_i64_report(
148    module: &runtime::Module,
149) -> Result<CpsReprI64CompareReport, CpsCompareError> {
150    let mut jit = compile_runtime_module_to_cps_repr_jit(module)?;
151    let cps_values = jit.run_roots_i64()?;
152    let vm_results = runtime::compile_vm_module(module.clone())?.eval_roots()?;
153    if vm_results.len() != cps_values.len() {
154        return Err(CpsCompareError::RootCountMismatch {
155            vm: vm_results.len(),
156            cps: cps_values.len(),
157        });
158    }
159    let mut roots = Vec::with_capacity(vm_results.len());
160    for (index, (vm_result, cps_cranelift)) in vm_results.into_iter().zip(cps_values).enumerate() {
161        let value = match vm_result {
162            runtime::VmResult::Value(value) => value,
163            runtime::VmResult::Request(request) => {
164                return Err(CpsCompareError::ResidualRequest { index, request });
165            }
166        };
167        let vm = match value {
168            runtime::VmValue::Int(value) => {
169                value
170                    .parse::<i64>()
171                    .map_err(|_| CpsCompareError::NonI64Value {
172                        index,
173                        value: runtime::VmValue::Int(value),
174                    })?
175            }
176            runtime::VmValue::Bool(value) => i64::from(value),
177            value => return Err(CpsCompareError::NonI64Value { index, value }),
178        };
179        if vm != cps_cranelift {
180            return Err(CpsCompareError::I64Mismatch {
181                index,
182                vm,
183                cps_cranelift,
184            });
185        }
186        roots.push(CpsReprI64RootCompare {
187            index,
188            vm,
189            cps_cranelift,
190        });
191    }
192    Ok(CpsReprI64CompareReport { roots })
193}
194
195#[cfg(test)]
196mod tests {
197    use yulang_typed_ir as typed_ir;
198
199    use crate::compare::compare_module;
200
201    use super::*;
202
203    fn unknown_lit(lit: typed_ir::Lit) -> runtime::Expr {
204        runtime::Expr::typed(runtime::ExprKind::Lit(lit), runtime::Type::unknown())
205    }
206
207    fn primitive(op: typed_ir::PrimitiveOp) -> runtime::Expr {
208        runtime::Expr::typed(runtime::ExprKind::PrimitiveOp(op), runtime::Type::unknown())
209    }
210
211    fn apply(callee: runtime::Expr, arg: runtime::Expr) -> runtime::Expr {
212        runtime::Expr::typed(
213            runtime::ExprKind::Apply {
214                callee: Box::new(callee),
215                arg: Box::new(arg),
216                evidence: None,
217                instantiation: None,
218            },
219            runtime::Type::unknown(),
220        )
221    }
222
223    fn if_expr(
224        cond: runtime::Expr,
225        then_branch: runtime::Expr,
226        else_branch: runtime::Expr,
227    ) -> runtime::Expr {
228        runtime::Expr::typed(
229            runtime::ExprKind::If {
230                cond: Box::new(cond),
231                then_branch: Box::new(then_branch),
232                else_branch: Box::new(else_branch),
233                evidence: None,
234            },
235            runtime::Type::unknown(),
236        )
237    }
238
239    fn var(name: &str) -> runtime::Expr {
240        runtime::Expr::typed(
241            runtime::ExprKind::Var(typed_ir::Path::from_name(typed_ir::Name(name.to_string()))),
242            runtime::Type::unknown(),
243        )
244    }
245
246    fn effect_op(name: &str) -> runtime::Expr {
247        runtime::Expr::typed(
248            runtime::ExprKind::EffectOp(effect_operation_path(name)),
249            runtime::Type::unknown(),
250        )
251    }
252
253    fn effect_operation_path(name: &str) -> typed_ir::Path {
254        typed_ir::Path {
255            segments: vec![
256                typed_ir::Name(name.to_string()),
257                typed_ir::Name(name.to_string()),
258            ],
259        }
260    }
261
262    fn bind_pattern(name: &str) -> runtime::Pattern {
263        runtime::Pattern::Bind {
264            name: typed_ir::Name(name.to_string()),
265            ty: runtime::Type::unknown(),
266        }
267    }
268
269    fn handle_once(
270        effect: &str,
271        payload: &str,
272        resume: &str,
273        body: runtime::Expr,
274        arm_body: runtime::Expr,
275    ) -> runtime::Expr {
276        let effect_namespace = typed_ir::Path::from_name(typed_ir::Name(effect.to_string()));
277        let effect_operation = effect_operation_path(effect);
278        runtime::Expr::typed(
279            runtime::ExprKind::Handle {
280                body: Box::new(body),
281                arms: vec![runtime::HandleArm {
282                    effect: effect_operation,
283                    payload: bind_pattern(payload),
284                    resume: Some(runtime::ResumeBinding {
285                        name: typed_ir::Name(resume.to_string()),
286                        ty: runtime::Type::unknown(),
287                    }),
288                    guard: None,
289                    body: arm_body,
290                }],
291                evidence: runtime::JoinEvidence {
292                    result: typed_ir::Type::Unknown,
293                },
294                handler: runtime::HandleEffect {
295                    consumes: vec![effect_namespace],
296                    residual_before: None,
297                    residual_after: None,
298                },
299            },
300            runtime::Type::unknown(),
301        )
302    }
303
304    fn block(stmts: Vec<runtime::Stmt>, tail: runtime::Expr) -> runtime::Expr {
305        runtime::Expr::typed(
306            runtime::ExprKind::Block {
307                stmts,
308                tail: Some(Box::new(tail)),
309            },
310            runtime::Type::unknown(),
311        )
312    }
313
314    fn thunk(expr: runtime::Expr) -> runtime::Expr {
315        runtime::Expr::typed(
316            runtime::ExprKind::Thunk {
317                effect: typed_ir::Type::Unknown,
318                value: runtime::Type::unknown(),
319                expr: Box::new(expr),
320            },
321            runtime::Type::thunk(typed_ir::Type::Unknown, runtime::Type::unknown()),
322        )
323    }
324
325    fn handled_body(expr: runtime::Expr) -> runtime::Expr {
326        thunk(expr)
327    }
328
329    fn lambda(param: &str, body: runtime::Expr) -> runtime::Expr {
330        runtime::Expr::typed(
331            runtime::ExprKind::Lambda {
332                param: typed_ir::Name(param.to_string()),
333                param_effect_annotation: None,
334                param_function_allowed_effects: None,
335                body: Box::new(body),
336            },
337            runtime::Type::unknown(),
338        )
339    }
340
341    fn binding(name: &str, body: runtime::Expr) -> runtime::Binding {
342        runtime::Binding {
343            name: typed_ir::Path::from_name(typed_ir::Name(name.to_string())),
344            type_params: Vec::new(),
345            scheme: typed_ir::Scheme {
346                requirements: Vec::new(),
347                body: typed_ir::Type::Unknown,
348            },
349            body,
350        }
351    }
352
353    fn module_with_root(expr: runtime::Expr) -> runtime::Module {
354        module_with_bindings_and_root(Vec::new(), expr)
355    }
356
357    fn module_with_bindings_and_root(
358        bindings: Vec<runtime::Binding>,
359        expr: runtime::Expr,
360    ) -> runtime::Module {
361        runtime::Module {
362            path: typed_ir::Path::default(),
363            bindings,
364            root_exprs: vec![expr],
365            roots: vec![runtime::Root::Expr(0)],
366            role_impls: Vec::new(),
367        }
368    }
369
370    fn primitive_call(op: typed_ir::PrimitiveOp, args: Vec<runtime::Expr>) -> runtime::Expr {
371        args.into_iter()
372            .fold(primitive(op), |callee, arg| apply(callee, arg))
373    }
374
375    fn list_expr(items: Vec<i64>) -> runtime::Expr {
376        list_of_exprs(
377            items
378                .into_iter()
379                .map(|item| unknown_lit(typed_ir::Lit::Int(item.to_string())))
380                .collect(),
381        )
382    }
383
384    fn list_of_exprs(items: Vec<runtime::Expr>) -> runtime::Expr {
385        items
386            .into_iter()
387            .map(|item| primitive_call(typed_ir::PrimitiveOp::ListSingleton, vec![item]))
388            .fold(
389                primitive_call(
390                    typed_ir::PrimitiveOp::ListEmpty,
391                    vec![unknown_lit(typed_ir::Lit::Unit)],
392                ),
393                |acc, item| primitive_call(typed_ir::PrimitiveOp::ListMerge, vec![acc, item]),
394            )
395    }
396
397    fn record(fields: Vec<(&str, runtime::Expr)>) -> runtime::Expr {
398        runtime::Expr::typed(
399            runtime::ExprKind::Record {
400                fields: fields
401                    .into_iter()
402                    .map(|(name, value)| runtime::RecordExprField {
403                        name: typed_ir::Name(name.to_string()),
404                        value,
405                    })
406                    .collect(),
407                spread: None,
408            },
409            runtime::Type::unknown(),
410        )
411    }
412
413    fn variant(tag: &str, value: Option<runtime::Expr>) -> runtime::Expr {
414        runtime::Expr::typed(
415            runtime::ExprKind::Variant {
416                tag: typed_ir::Name(tag.to_string()),
417                value: value.map(Box::new),
418            },
419            runtime::Type::unknown(),
420        )
421    }
422
423    fn compare_all(module: &runtime::Module) {
424        compare_module(module).expect("native control matches VM");
425        compare_cps_module(module).expect("CPS matches VM");
426    }
427
428    fn compare_cps_cranelift_i64(module: &runtime::Module) {
429        compare_cps_repr_cranelift_i64(module).expect("CPS repr Cranelift matches VM");
430    }
431
432    fn cps_cranelift_display_roots(module: &runtime::Module) -> Vec<String> {
433        let mut jit =
434            compile_runtime_module_to_cps_repr_jit(module).expect("compiled CPS repr Cranelift");
435        jit.run_roots_display().expect("ran CPS repr Cranelift")
436    }
437
438    #[test]
439    fn compares_pure_int_add_with_vm_and_native_control() {
440        let expr = apply(
441            apply(
442                primitive(typed_ir::PrimitiveOp::IntAdd),
443                unknown_lit(typed_ir::Lit::Int("20".to_string())),
444            ),
445            unknown_lit(typed_ir::Lit::Int("22".to_string())),
446        );
447        compare_all(&module_with_root(expr));
448    }
449
450    #[test]
451    fn compares_pure_int_add_with_cps_repr_cranelift() {
452        let expr = apply(
453            apply(
454                primitive(typed_ir::PrimitiveOp::IntAdd),
455                unknown_lit(typed_ir::Lit::Int("20".to_string())),
456            ),
457            unknown_lit(typed_ir::Lit::Int("22".to_string())),
458        );
459        compare_cps_cranelift_i64(&module_with_root(expr));
460    }
461
462    #[test]
463    fn compares_effect_handler_with_cps_repr_cranelift() {
464        let body = apply(
465            effect_op("choose"),
466            unknown_lit(typed_ir::Lit::Int("1".to_string())),
467        );
468        let arm_body = apply(var("k"), var("x"));
469
470        compare_cps_cranelift_i64(&module_with_root(handle_once(
471            "choose",
472            "x",
473            "k",
474            handled_body(body),
475            arm_body,
476        )));
477    }
478
479    #[test]
480    fn compares_multishot_effect_handler_with_cps_repr_cranelift() {
481        let body = apply(
482            effect_op("choose"),
483            unknown_lit(typed_ir::Lit::Int("1".to_string())),
484        );
485        let resume_x = apply(var("k"), var("x"));
486        let resume_two = apply(var("k"), unknown_lit(typed_ir::Lit::Int("2".to_string())));
487        let arm_body = apply(
488            apply(primitive(typed_ir::PrimitiveOp::IntAdd), resume_x),
489            resume_two,
490        );
491
492        compare_cps_cranelift_i64(&module_with_root(handle_once(
493            "choose",
494            "x",
495            "k",
496            handled_body(body),
497            arm_body,
498        )));
499    }
500
501    #[test]
502    fn compares_effect_handler_rest_continuation_with_cps_repr_cranelift() {
503        let choose_one = apply(
504            effect_op("choose"),
505            unknown_lit(typed_ir::Lit::Int("1".to_string())),
506        );
507        let body = apply(
508            apply(primitive(typed_ir::PrimitiveOp::IntAdd), choose_one),
509            unknown_lit(typed_ir::Lit::Int("10".to_string())),
510        );
511        let resume_x = apply(var("k"), var("x"));
512        let resume_two = apply(var("k"), unknown_lit(typed_ir::Lit::Int("2".to_string())));
513        let arm_body = apply(
514            apply(primitive(typed_ir::PrimitiveOp::IntAdd), resume_x),
515            resume_two,
516        );
517
518        compare_cps_cranelift_i64(&module_with_root(handle_once(
519            "choose",
520            "x",
521            "k",
522            handled_body(body),
523            arm_body,
524        )));
525    }
526
527    #[test]
528    fn compares_effect_handler_branch_with_cps_repr_cranelift() {
529        let then_branch = apply(
530            apply(
531                primitive(typed_ir::PrimitiveOp::IntAdd),
532                apply(
533                    effect_op("choose"),
534                    unknown_lit(typed_ir::Lit::Int("1".to_string())),
535                ),
536            ),
537            unknown_lit(typed_ir::Lit::Int("10".to_string())),
538        );
539        let else_branch = apply(
540            apply(
541                primitive(typed_ir::PrimitiveOp::IntAdd),
542                apply(
543                    effect_op("choose"),
544                    unknown_lit(typed_ir::Lit::Int("2".to_string())),
545                ),
546            ),
547            unknown_lit(typed_ir::Lit::Int("20".to_string())),
548        );
549        let body = if_expr(
550            unknown_lit(typed_ir::Lit::Bool(true)),
551            then_branch,
552            else_branch,
553        );
554        let resume_x = apply(var("k"), var("x"));
555        let resume_three = apply(var("k"), unknown_lit(typed_ir::Lit::Int("3".to_string())));
556        let arm_body = apply(
557            apply(primitive(typed_ir::PrimitiveOp::IntAdd), resume_x),
558            resume_three,
559        );
560
561        compare_cps_cranelift_i64(&module_with_root(handle_once(
562            "choose",
563            "x",
564            "k",
565            handled_body(body),
566            arm_body,
567        )));
568    }
569
570    #[test]
571    fn compares_non_scalar_effect_payloads_with_cps_repr_cranelift() {
572        let cases = vec![
573            (
574                unknown_lit(typed_ir::Lit::String("ok".to_string())),
575                "ok".to_string(),
576            ),
577            (list_expr(vec![1, 2, 3]), "[1, 2, 3]".to_string()),
578            (
579                record(vec![(
580                    "answer",
581                    unknown_lit(typed_ir::Lit::Int("42".to_string())),
582                )]),
583                "{ answer: 42 }".to_string(),
584            ),
585            (
586                variant(
587                    "just",
588                    Some(unknown_lit(typed_ir::Lit::Int("7".to_string()))),
589                ),
590                ":just 7".to_string(),
591            ),
592        ];
593
594        for (payload, expected) in cases {
595            let body = apply(effect_op("choose"), payload);
596            let arm_body = apply(var("k"), var("x"));
597            let module = module_with_root(handle_once(
598                "choose",
599                "x",
600                "k",
601                handled_body(body),
602                arm_body,
603            ));
604
605            compare_cps_module(&module).expect("CPS matches VM");
606            assert_eq!(cps_cranelift_display_roots(&module), vec![expected]);
607        }
608    }
609
610    #[test]
611    fn carries_runtime_values_through_cps_repr_control_edges() {
612        let branch = module_with_root(if_expr(
613            unknown_lit(typed_ir::Lit::Bool(true)),
614            list_expr(vec![1, 2]),
615            list_expr(vec![3]),
616        ));
617        compare_cps_module(&branch).expect("branch CPS matches VM");
618        assert_eq!(
619            cps_cranelift_display_roots(&branch),
620            vec!["[1, 2]".to_string()]
621        );
622
623        let direct_call = module_with_bindings_and_root(
624            vec![binding("make", lambda("u", list_expr(vec![4, 5])))],
625            apply(var("make"), unknown_lit(typed_ir::Lit::Unit)),
626        );
627        compare_cps_module(&direct_call).expect("direct call CPS matches VM");
628        assert_eq!(
629            cps_cranelift_display_roots(&direct_call),
630            vec!["[4, 5]".to_string()]
631        );
632
633        let closure_apply = module_with_root(block(
634            vec![runtime::Stmt::Let {
635                pattern: bind_pattern("f"),
636                value: lambda("u", list_expr(vec![6, 7])),
637            }],
638            apply(var("f"), unknown_lit(typed_ir::Lit::Unit)),
639        ));
640        compare_cps_module(&closure_apply).expect("closure apply CPS matches VM");
641        assert_eq!(
642            cps_cranelift_display_roots(&closure_apply),
643            vec!["[6, 7]".to_string()]
644        );
645    }
646
647    #[test]
648    fn carries_large_closure_environment_through_cps_repr() {
649        let closure_apply = module_with_root(block(
650            vec![
651                runtime::Stmt::Let {
652                    pattern: bind_pattern("a"),
653                    value: unknown_lit(typed_ir::Lit::Int("1".to_string())),
654                },
655                runtime::Stmt::Let {
656                    pattern: bind_pattern("b"),
657                    value: unknown_lit(typed_ir::Lit::Int("2".to_string())),
658                },
659                runtime::Stmt::Let {
660                    pattern: bind_pattern("c"),
661                    value: unknown_lit(typed_ir::Lit::Int("3".to_string())),
662                },
663                runtime::Stmt::Let {
664                    pattern: bind_pattern("d"),
665                    value: unknown_lit(typed_ir::Lit::Int("4".to_string())),
666                },
667                runtime::Stmt::Let {
668                    pattern: bind_pattern("e"),
669                    value: unknown_lit(typed_ir::Lit::Int("5".to_string())),
670                },
671                runtime::Stmt::Let {
672                    pattern: bind_pattern("f"),
673                    value: lambda(
674                        "u",
675                        handle_once(
676                            "choose",
677                            "x",
678                            "k",
679                            handled_body(apply(
680                                effect_op("choose"),
681                                list_of_exprs(vec![
682                                    var("a"),
683                                    var("b"),
684                                    var("c"),
685                                    var("d"),
686                                    var("e"),
687                                ]),
688                            )),
689                            apply(var("k"), var("x")),
690                        ),
691                    ),
692                },
693            ],
694            apply(var("f"), unknown_lit(typed_ir::Lit::Unit)),
695        ));
696
697        compare_cps_module(&closure_apply).expect("large closure env CPS matches VM");
698        assert_eq!(
699            cps_cranelift_display_roots(&closure_apply),
700            vec!["[1, 2, 3, 4, 5]".to_string()]
701        );
702    }
703
704    #[test]
705    fn applies_partial_top_level_function_through_cps_repr() {
706        let add = binding(
707            "add",
708            lambda(
709                "x",
710                lambda(
711                    "y",
712                    apply(
713                        apply(primitive(typed_ir::PrimitiveOp::IntAdd), var("x")),
714                        var("y"),
715                    ),
716                ),
717            ),
718        );
719        let root = apply(
720            apply(var("add"), unknown_lit(typed_ir::Lit::Int("1".to_string()))),
721            unknown_lit(typed_ir::Lit::Int("41".to_string())),
722        );
723        let module = module_with_bindings_and_root(vec![add], root);
724
725        compare_cps_module(&module).expect("partial function CPS matches VM");
726        assert_eq!(cps_cranelift_display_roots(&module), vec!["42".to_string()]);
727    }
728
729    #[test]
730    fn compares_if_with_vm_and_native_control() {
731        let expr = if_expr(
732            apply(
733                apply(
734                    primitive(typed_ir::PrimitiveOp::IntLt),
735                    unknown_lit(typed_ir::Lit::Int("1".to_string())),
736                ),
737                unknown_lit(typed_ir::Lit::Int("2".to_string())),
738            ),
739            unknown_lit(typed_ir::Lit::String("then".to_string())),
740            unknown_lit(typed_ir::Lit::String("else".to_string())),
741        );
742        compare_all(&module_with_root(expr));
743    }
744
745    #[test]
746    fn compares_block_binding_with_vm_and_native_control() {
747        let expr = block(
748            vec![runtime::Stmt::Let {
749                pattern: bind_pattern("x"),
750                value: unknown_lit(typed_ir::Lit::Int("21".to_string())),
751            }],
752            apply(
753                apply(primitive(typed_ir::PrimitiveOp::IntAdd), var("x")),
754                var("x"),
755            ),
756        );
757        compare_all(&module_with_root(expr));
758    }
759
760    #[test]
761    fn compares_recursive_binding_with_vm_and_native_control() {
762        let countdown = binding(
763            "countdown",
764            lambda(
765                "n",
766                if_expr(
767                    apply(
768                        apply(primitive(typed_ir::PrimitiveOp::IntLe), var("n")),
769                        unknown_lit(typed_ir::Lit::Int("0".to_string())),
770                    ),
771                    unknown_lit(typed_ir::Lit::Int("0".to_string())),
772                    apply(
773                        var("countdown"),
774                        apply(
775                            apply(primitive(typed_ir::PrimitiveOp::IntSub), var("n")),
776                            unknown_lit(typed_ir::Lit::Int("1".to_string())),
777                        ),
778                    ),
779                ),
780            ),
781        );
782        let root = apply(
783            var("countdown"),
784            unknown_lit(typed_ir::Lit::Int("3".to_string())),
785        );
786        compare_all(&module_with_bindings_and_root(vec![countdown], root));
787    }
788}