yulang-runtime-refine 0.1.0

Runtime type refinement, validation, invariant checks, and hygiene printing for Yulang.
Documentation
use yulang_typed_ir as typed_ir;

use yulang_runtime_types::ir::{
    EffectIdRef, EffectIdVar, Expr, ExprKind, HandleEffect, Module, Stmt,
};
use yulang_runtime_types::types::{effect_path, effect_paths};

pub fn format_hygiene_module(module: &Module) -> String {
    let mut out = String::new();
    for binding in &module.bindings {
        out.push_str(&format_path(&binding.name));
        out.push('\n');
        format_hygiene_expr_into(&binding.body, 1, &mut out);
    }
    for (index, expr) in module.root_exprs.iter().enumerate() {
        out.push_str("<root ");
        out.push_str(&index.to_string());
        out.push_str(">\n");
        format_hygiene_expr_into(expr, 1, &mut out);
    }
    out
}

pub fn format_hygiene_expr(expr: &Expr) -> String {
    let mut out = String::new();
    format_hygiene_expr_into(expr, 0, &mut out);
    out
}

fn format_hygiene_expr_into(expr: &Expr, indent: usize, out: &mut String) {
    match &expr.kind {
        ExprKind::LocalPushId { id, body } => {
            line(
                indent,
                out,
                &format!("local_push_id {}", format_id_var(*id)),
            );
            format_hygiene_expr_into(body, indent + 1, out);
        }
        ExprKind::AddId {
            id, allowed, thunk, ..
        } => {
            line(
                indent,
                out,
                &format!("add_id[{}, {}]", format_id_ref(*id), format_effect(allowed)),
            );
            format_hygiene_expr_into(thunk, indent + 1, out);
        }
        ExprKind::FindId { id } => {
            line(indent, out, &format!("find_id {}", format_id_ref(*id)));
        }
        ExprKind::PeekId => line(indent, out, "peek_id"),
        ExprKind::Handle {
            body,
            arms,
            handler,
            ..
        } => {
            line(indent, out, &format_handle(handler));
            format_hygiene_expr_into(body, indent + 1, out);
            for arm in arms {
                if let Some(guard) = &arm.guard {
                    line(indent + 1, out, "guard");
                    format_hygiene_expr_into(guard, indent + 2, out);
                }
                line(indent + 1, out, "arm");
                format_hygiene_expr_into(&arm.body, indent + 2, out);
            }
        }
        ExprKind::BindHere { expr } => {
            line(indent, out, "bind_here");
            format_hygiene_expr_into(expr, indent + 1, out);
        }
        ExprKind::Thunk { effect, expr, .. } => {
            line(indent, out, &format!("thunk[{}]", format_effect(effect)));
            format_hygiene_expr_into(expr, indent + 1, out);
        }
        ExprKind::Lambda { body, .. } => {
            line(indent, out, "lambda");
            format_hygiene_expr_into(body, indent + 1, out);
        }
        ExprKind::Apply { callee, arg, .. } => {
            line(indent, out, "apply");
            format_hygiene_expr_into(callee, indent + 1, out);
            format_hygiene_expr_into(arg, indent + 1, out);
        }
        ExprKind::If {
            cond,
            then_branch,
            else_branch,
            ..
        } => {
            line(indent, out, "if");
            format_hygiene_expr_into(cond, indent + 1, out);
            format_hygiene_expr_into(then_branch, indent + 1, out);
            format_hygiene_expr_into(else_branch, indent + 1, out);
        }
        ExprKind::Tuple(items) => {
            line(indent, out, "tuple");
            for item in items {
                format_hygiene_expr_into(item, indent + 1, out);
            }
        }
        ExprKind::Record { fields, spread } => {
            line(indent, out, "record");
            for field in fields {
                format_hygiene_expr_into(&field.value, indent + 1, out);
            }
            if let Some(spread) = spread {
                match spread {
                    yulang_runtime_types::ir::RecordSpreadExpr::Head(expr)
                    | yulang_runtime_types::ir::RecordSpreadExpr::Tail(expr) => {
                        format_hygiene_expr_into(expr, indent + 1, out);
                    }
                }
            }
        }
        ExprKind::Variant { value, .. } => {
            line(indent, out, "variant");
            if let Some(value) = value {
                format_hygiene_expr_into(value, indent + 1, out);
            }
        }
        ExprKind::Select { base, .. } => {
            line(indent, out, "select");
            format_hygiene_expr_into(base, indent + 1, out);
        }
        ExprKind::Match {
            scrutinee, arms, ..
        } => {
            line(indent, out, "match");
            format_hygiene_expr_into(scrutinee, indent + 1, out);
            for arm in arms {
                if let Some(guard) = &arm.guard {
                    line(indent + 1, out, "guard");
                    format_hygiene_expr_into(guard, indent + 2, out);
                }
                line(indent + 1, out, "arm");
                format_hygiene_expr_into(&arm.body, indent + 2, out);
            }
        }
        ExprKind::Block { stmts, tail } => {
            line(indent, out, "block");
            for stmt in stmts {
                format_hygiene_stmt_into(stmt, indent + 1, out);
            }
            if let Some(tail) = tail {
                format_hygiene_expr_into(tail, indent + 1, out);
            }
        }
        ExprKind::Coerce { expr, .. } | ExprKind::Pack { expr, .. } => {
            format_hygiene_expr_into(expr, indent, out);
        }
        ExprKind::Var(_) | ExprKind::EffectOp(_) | ExprKind::PrimitiveOp(_) | ExprKind::Lit(_) => {}
    }
}

fn format_hygiene_stmt_into(stmt: &Stmt, indent: usize, out: &mut String) {
    match stmt {
        Stmt::Let { value, .. } | Stmt::Expr(value) | Stmt::Module { body: value, .. } => {
            format_hygiene_expr_into(value, indent, out);
        }
    }
}

fn format_handle(handler: &HandleEffect) -> String {
    let consumes = handler
        .consumes
        .iter()
        .map(format_path)
        .collect::<Vec<_>>()
        .join(", ");
    let before = handler
        .residual_before
        .as_ref()
        .map(format_effect)
        .unwrap_or_else(|| "_".to_string());
    let after = handler
        .residual_after
        .as_ref()
        .map(format_effect)
        .unwrap_or_else(|| "_".to_string());
    format!("handle consumes=[{consumes}] residual={before}->{after}")
}

fn line(indent: usize, out: &mut String, text: &str) {
    for _ in 0..indent {
        out.push_str("  ");
    }
    out.push_str(text);
    out.push('\n');
}

fn format_id_var(id: EffectIdVar) -> String {
    format!("ae{}", id.0)
}

fn format_id_ref(id: EffectIdRef) -> String {
    match id {
        EffectIdRef::Var(id) => format_id_var(id),
        EffectIdRef::Peek => "peek".to_string(),
    }
}

fn format_effect(effect: &typed_ir::Type) -> String {
    let paths = effect_paths(effect);
    if !paths.is_empty() {
        return paths.iter().map(format_path).collect::<Vec<_>>().join("; ");
    }
    effect_path(effect)
        .map(|path| format_path(&path))
        .unwrap_or_else(|| format!("{effect:?}"))
}

fn format_path(path: &typed_ir::Path) -> String {
    path.segments
        .iter()
        .map(|segment| segment.0.as_str())
        .collect::<Vec<_>>()
        .join("::")
}

#[cfg(test)]
mod tests {
    use super::*;
    use yulang_runtime_types::ir::{Expr, ExprKind, Type as RuntimeType};

    fn named_type(name: &str) -> typed_ir::Type {
        typed_ir::Type::Named {
            path: typed_ir::Path::from_name(typed_ir::Name(name.to_string())),
            args: Vec::new(),
        }
    }

    fn row(name: &str) -> typed_ir::Type {
        typed_ir::Type::Row {
            items: vec![named_type(name)],
            tail: Box::new(typed_ir::Type::Never),
        }
    }

    #[test]
    fn shows_local_push_and_add_id_scope() {
        let int = RuntimeType::value(named_type("int"));
        let effect = row("io");
        let thunk = Expr::typed(
            ExprKind::Thunk {
                effect: effect.clone(),
                value: int.clone(),
                expr: Box::new(Expr::typed(
                    ExprKind::Lit(typed_ir::Lit::Int("1".to_string())),
                    int,
                )),
            },
            RuntimeType::thunk(effect.clone(), RuntimeType::value(named_type("int"))),
        );
        let expr = Expr::typed(
            ExprKind::LocalPushId {
                id: EffectIdVar(0),
                body: Box::new(Expr::typed(
                    ExprKind::AddId {
                        id: EffectIdRef::Peek,
                        allowed: effect,
                        active: false,
                        thunk: Box::new(thunk),
                    },
                    RuntimeType::thunk(row("io"), RuntimeType::value(named_type("int"))),
                )),
            },
            RuntimeType::thunk(row("io"), RuntimeType::value(named_type("int"))),
        );

        let text = format_hygiene_expr(&expr);

        assert!(text.contains("local_push_id ae0"));
        assert!(text.contains("add_id[peek, io]"));
        assert!(text.contains("thunk[io]"));
    }

    #[test]
    fn shows_handler_effect_summary_and_find_id() {
        let unit = RuntimeType::value(named_type("unit"));
        let effect_path = typed_ir::Path::from_name(typed_ir::Name("io".to_string()));
        let expr = Expr::typed(
            ExprKind::Handle {
                body: Box::new(Expr::typed(
                    ExprKind::FindId {
                        id: EffectIdRef::Var(EffectIdVar(1)),
                    },
                    RuntimeType::value(named_type("__effect_id")),
                )),
                arms: Vec::new(),
                evidence: yulang_runtime_types::ir::JoinEvidence {
                    result: named_type("unit"),
                },
                handler: HandleEffect {
                    consumes: vec![effect_path],
                    residual_before: Some(row("io")),
                    residual_after: Some(typed_ir::Type::Never),
                },
            },
            unit,
        );

        let text = format_hygiene_expr(&expr);

        assert!(text.contains("handle consumes=[io] residual=io->"));
        assert!(text.contains("find_id ae1"));
    }
}