use crate::canonical::{
CExpr, Effect, FnDecl, Param, Pattern, Stage, TypeExpr,
};
use crate::ids::NodeId;
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TransformError {
#[error("unknown node id `{at}`")]
UnknownNode { at: String },
#[error("expected a Match expression at `{at}` but found `{found_kind}`")]
NotAMatch { at: String, found_kind: &'static str },
#[error("expected a Let expression at `{at}` but found `{found_kind}`")]
NotALet { at: String, found_kind: &'static str },
#[error("arm index {requested} out of range (arm count = {arm_count}) at `{at}`")]
ArmIndexOutOfRange { at: String, arm_count: usize, requested: usize },
#[error("malformed NodeId `{0}`")]
BadNodeId(String),
#[error("cannot transform inside `{stage_kind}` — only FnDecl bodies are transformable")]
NonFnTarget { stage_kind: &'static str },
#[error("rename is a no-op: old and new name are both `{name}`")]
RenameNoOp { name: String },
#[error("inline_let refused: `{reason}`")]
InlineLetRefused { reason: String },
#[error("extract_function refused: `{reason}`")]
ExtractFnRefused { reason: String },
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExtractFnSpec {
pub name: String,
pub type_params: Vec<String>,
pub params: Vec<Param>,
pub return_type: TypeExpr,
pub effects: Vec<Effect>,
}
pub fn replace_match_arm(
stage: &Stage,
match_node: &NodeId,
arm_index: usize,
new_body: CExpr,
) -> Result<Stage, TransformError> {
let mut out = stage.clone();
let (body, n_params) = match &mut out {
Stage::FnDecl(fd) => {
let n = fd.params.len();
(&mut fd.body, n)
}
Stage::TypeDecl(_) => return Err(TransformError::NonFnTarget { stage_kind: "TypeDecl" }),
Stage::Import(_) => return Err(TransformError::NonFnTarget { stage_kind: "Import" }),
};
let path = parse_node_id(match_node.as_str())?;
if path.is_empty() {
return Err(TransformError::NotAMatch {
at: match_node.as_str().into(),
found_kind: "stage_root",
});
}
if path[0] != n_params + 1 {
return Err(TransformError::UnknownNode { at: match_node.as_str().into() });
}
let inner = &path[1..];
let target = navigate_to_expr(body, inner, match_node.as_str())?;
let CExpr::Match { scrutinee: _, arms } = target else {
return Err(TransformError::NotAMatch {
at: match_node.as_str().into(),
found_kind: cexpr_kind(target),
});
};
if arm_index >= arms.len() {
return Err(TransformError::ArmIndexOutOfRange {
at: match_node.as_str().into(),
arm_count: arms.len(),
requested: arm_index,
});
}
arms[arm_index].body = new_body;
Ok(out)
}
pub fn rename_local(
stage: &Stage,
let_node: &NodeId,
new_name: &str,
) -> Result<Stage, TransformError> {
let mut out = stage.clone();
let (body, n_params) = match &mut out {
Stage::FnDecl(fd) => {
let n = fd.params.len();
(&mut fd.body, n)
}
Stage::TypeDecl(_) => return Err(TransformError::NonFnTarget { stage_kind: "TypeDecl" }),
Stage::Import(_) => return Err(TransformError::NonFnTarget { stage_kind: "Import" }),
};
let path = parse_node_id(let_node.as_str())?;
if path.is_empty() {
return Err(TransformError::NotALet {
at: let_node.as_str().into(),
found_kind: "stage_root",
});
}
if path[0] != n_params + 1 {
return Err(TransformError::UnknownNode { at: let_node.as_str().into() });
}
let inner = &path[1..];
let target = navigate_to_expr(body, inner, let_node.as_str())?;
let CExpr::Let { name, body: let_body, .. } = target else {
return Err(TransformError::NotALet {
at: let_node.as_str().into(),
found_kind: cexpr_kind(target),
});
};
if name == new_name {
return Err(TransformError::RenameNoOp { name: name.clone() });
}
let old_name = std::mem::replace(name, new_name.to_string());
rewrite_var_in_expr(let_body, &old_name, new_name);
Ok(out)
}
pub fn inline_let(
stage: &Stage,
let_node: &NodeId,
) -> Result<Stage, TransformError> {
let mut out = stage.clone();
let (body, n_params) = match &mut out {
Stage::FnDecl(fd) => {
let n = fd.params.len();
(&mut fd.body, n)
}
Stage::TypeDecl(_) => return Err(TransformError::NonFnTarget { stage_kind: "TypeDecl" }),
Stage::Import(_) => return Err(TransformError::NonFnTarget { stage_kind: "Import" }),
};
let path = parse_node_id(let_node.as_str())?;
if path.is_empty() {
return Err(TransformError::NotALet {
at: let_node.as_str().into(),
found_kind: "stage_root",
});
}
if path[0] != n_params + 1 {
return Err(TransformError::UnknownNode { at: let_node.as_str().into() });
}
let inner = &path[1..];
if inner.is_empty() {
let CExpr::Let { name, value, body: let_body, .. } = body.clone() else {
return Err(TransformError::NotALet {
at: let_node.as_str().into(),
found_kind: cexpr_kind(body),
});
};
check_inlinable(&value)?;
let captures = free_vars(&value);
check_no_capture(&let_body, &captures)?;
let mut replaced = *let_body;
substitute_in_expr(&mut replaced, &name, &value);
*body = replaced;
return Ok(out);
}
let target = navigate_to_expr(body, inner, let_node.as_str())?;
let CExpr::Let { name, value, body: let_body, .. } = target.clone() else {
return Err(TransformError::NotALet {
at: let_node.as_str().into(),
found_kind: cexpr_kind(target),
});
};
check_inlinable(&value)?;
let captures = free_vars(&value);
check_no_capture(&let_body, &captures)?;
let mut replaced = *let_body;
substitute_in_expr(&mut replaced, &name, &value);
*target = replaced;
Ok(out)
}
fn check_inlinable(v: &CExpr) -> Result<(), TransformError> {
match v {
CExpr::Literal { .. } | CExpr::Var { .. } => Ok(()),
CExpr::FieldAccess { value, .. } => check_inlinable(value),
CExpr::BinOp { lhs, rhs, .. } => {
check_inlinable(lhs)?;
check_inlinable(rhs)
}
CExpr::UnaryOp { expr, .. } => check_inlinable(expr),
CExpr::TupleLit { items } | CExpr::ListLit { items } => {
for it in items { check_inlinable(it)?; }
Ok(())
}
other => Err(TransformError::InlineLetRefused {
reason: format!(
"let value contains a `{}` expression; slice 3 only inlines literal/var/field/binop/unaryop/tuple/list trees",
cexpr_kind(other)
),
}),
}
}
fn free_vars(v: &CExpr) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
collect_free_vars(v, &mut out);
out
}
fn collect_free_vars(e: &CExpr, out: &mut std::collections::BTreeSet<String>) {
match e {
CExpr::Var { name } => { out.insert(name.clone()); }
CExpr::Literal { .. } => {}
CExpr::Call { callee, args } => {
collect_free_vars(callee, out);
for a in args { collect_free_vars(a, out); }
}
CExpr::Let { value, body, name, .. } => {
collect_free_vars(value, out);
let mut inner = std::collections::BTreeSet::new();
collect_free_vars(body, &mut inner);
inner.remove(name);
out.extend(inner);
}
CExpr::Match { scrutinee, arms } => {
collect_free_vars(scrutinee, out);
for arm in arms {
let mut inner = std::collections::BTreeSet::new();
collect_free_vars(&arm.body, &mut inner);
let bound = pattern_bindings(&arm.pattern);
for b in bound { inner.remove(&b); }
out.extend(inner);
}
}
CExpr::Block { statements, result } => {
for s in statements { collect_free_vars(s, out); }
collect_free_vars(result, out);
}
CExpr::Constructor { args, .. } => {
for a in args { collect_free_vars(a, out); }
}
CExpr::RecordLit { fields } => {
for f in fields { collect_free_vars(&f.value, out); }
}
CExpr::TupleLit { items } | CExpr::ListLit { items } => {
for i in items { collect_free_vars(i, out); }
}
CExpr::FieldAccess { value, .. } => collect_free_vars(value, out),
CExpr::Lambda { params, body, .. } => {
let mut inner = std::collections::BTreeSet::new();
collect_free_vars(body, &mut inner);
for p in params { inner.remove(&p.name); }
out.extend(inner);
}
CExpr::BinOp { lhs, rhs, .. } => {
collect_free_vars(lhs, out);
collect_free_vars(rhs, out);
}
CExpr::UnaryOp { expr, .. } => collect_free_vars(expr, out),
CExpr::Return { value } => collect_free_vars(value, out),
}
}
fn pattern_bindings(p: &Pattern) -> Vec<String> {
let mut out = Vec::new();
collect_pattern_bindings(p, &mut out);
out
}
fn collect_pattern_bindings(p: &Pattern, out: &mut Vec<String>) {
match p {
Pattern::PVar { name } => out.push(name.clone()),
Pattern::PLiteral { .. } | Pattern::PWild => {}
Pattern::PConstructor { args, .. } => for p in args { collect_pattern_bindings(p, out); }
Pattern::PRecord { fields } => for f in fields { collect_pattern_bindings(&f.pattern, out); }
Pattern::PTuple { items } => for p in items { collect_pattern_bindings(p, out); }
}
}
fn check_no_capture(
body: &CExpr,
captures: &std::collections::BTreeSet<String>,
) -> Result<(), TransformError> {
let mut conflict: Option<String> = None;
walk_binders(body, &mut |name| {
if captures.contains(name) && conflict.is_none() {
conflict = Some(name.to_string());
}
});
if let Some(name) = conflict {
return Err(TransformError::InlineLetRefused {
reason: format!(
"value's free var `{name}` is re-bound in the body; inlining would capture"
),
});
}
Ok(())
}
fn walk_binders(e: &CExpr, on_binder: &mut dyn FnMut(&str)) {
match e {
CExpr::Let { name, value, body, .. } => {
on_binder(name);
walk_binders(value, on_binder);
walk_binders(body, on_binder);
}
CExpr::Lambda { params, body, .. } => {
for p in params { on_binder(&p.name); }
walk_binders(body, on_binder);
}
CExpr::Match { scrutinee, arms } => {
walk_binders(scrutinee, on_binder);
for arm in arms {
for b in pattern_bindings(&arm.pattern) { on_binder(&b); }
walk_binders(&arm.body, on_binder);
}
}
CExpr::Call { callee, args } => {
walk_binders(callee, on_binder);
for a in args { walk_binders(a, on_binder); }
}
CExpr::Block { statements, result } => {
for s in statements { walk_binders(s, on_binder); }
walk_binders(result, on_binder);
}
CExpr::Constructor { args, .. } => for a in args { walk_binders(a, on_binder); }
CExpr::RecordLit { fields } => for f in fields { walk_binders(&f.value, on_binder); }
CExpr::TupleLit { items } | CExpr::ListLit { items } => {
for i in items { walk_binders(i, on_binder); }
}
CExpr::FieldAccess { value, .. } => walk_binders(value, on_binder),
CExpr::BinOp { lhs, rhs, .. } => {
walk_binders(lhs, on_binder); walk_binders(rhs, on_binder);
}
CExpr::UnaryOp { expr, .. } => walk_binders(expr, on_binder),
CExpr::Return { value } => walk_binders(value, on_binder),
CExpr::Var { .. } | CExpr::Literal { .. } => {}
}
}
fn substitute_in_expr(e: &mut CExpr, name: &str, replacement: &CExpr) {
match e {
CExpr::Var { name: n } if n == name => {
*e = replacement.clone();
}
CExpr::Var { .. } | CExpr::Literal { .. } => {}
CExpr::Call { callee, args } => {
substitute_in_expr(callee, name, replacement);
for a in args { substitute_in_expr(a, name, replacement); }
}
CExpr::Let { name: binder, value, body, .. } => {
substitute_in_expr(value, name, replacement);
if binder != name {
substitute_in_expr(body, name, replacement);
}
}
CExpr::Match { scrutinee, arms } => {
substitute_in_expr(scrutinee, name, replacement);
for arm in arms {
if !pattern_binds(&arm.pattern, name) {
substitute_in_expr(&mut arm.body, name, replacement);
}
}
}
CExpr::Block { statements, result } => {
for s in statements { substitute_in_expr(s, name, replacement); }
substitute_in_expr(result, name, replacement);
}
CExpr::Constructor { args, .. } => {
for a in args { substitute_in_expr(a, name, replacement); }
}
CExpr::RecordLit { fields } => {
for f in fields { substitute_in_expr(&mut f.value, name, replacement); }
}
CExpr::TupleLit { items } | CExpr::ListLit { items } => {
for i in items { substitute_in_expr(i, name, replacement); }
}
CExpr::FieldAccess { value, .. } => substitute_in_expr(value, name, replacement),
CExpr::Lambda { params, body, .. } => {
if !params.iter().any(|p| p.name == name) {
substitute_in_expr(body, name, replacement);
}
}
CExpr::BinOp { lhs, rhs, .. } => {
substitute_in_expr(lhs, name, replacement);
substitute_in_expr(rhs, name, replacement);
}
CExpr::UnaryOp { expr, .. } => substitute_in_expr(expr, name, replacement),
CExpr::Return { value } => substitute_in_expr(value, name, replacement),
}
}
fn rewrite_var_in_expr(e: &mut CExpr, old: &str, new: &str) {
match e {
CExpr::Var { name } => {
if name == old { *name = new.into(); }
}
CExpr::Literal { .. } => {}
CExpr::Call { callee, args } => {
rewrite_var_in_expr(callee, old, new);
for a in args { rewrite_var_in_expr(a, old, new); }
}
CExpr::Let { name, value, body, .. } => {
rewrite_var_in_expr(value, old, new);
if name != old {
rewrite_var_in_expr(body, old, new);
}
}
CExpr::Match { scrutinee, arms } => {
rewrite_var_in_expr(scrutinee, old, new);
for arm in arms {
if !pattern_binds(&arm.pattern, old) {
rewrite_var_in_expr(&mut arm.body, old, new);
}
}
}
CExpr::Block { statements, result } => {
for s in statements { rewrite_var_in_expr(s, old, new); }
rewrite_var_in_expr(result, old, new);
}
CExpr::Constructor { args, .. } => {
for a in args { rewrite_var_in_expr(a, old, new); }
}
CExpr::RecordLit { fields } => {
for f in fields { rewrite_var_in_expr(&mut f.value, old, new); }
}
CExpr::TupleLit { items } | CExpr::ListLit { items } => {
for i in items { rewrite_var_in_expr(i, old, new); }
}
CExpr::FieldAccess { value, .. } => rewrite_var_in_expr(value, old, new),
CExpr::Lambda { params, body, .. } => {
if !params.iter().any(|p| p.name == old) {
rewrite_var_in_expr(body, old, new);
}
}
CExpr::BinOp { lhs, rhs, .. } => {
rewrite_var_in_expr(lhs, old, new);
rewrite_var_in_expr(rhs, old, new);
}
CExpr::UnaryOp { expr, .. } => rewrite_var_in_expr(expr, old, new),
CExpr::Return { value } => rewrite_var_in_expr(value, old, new),
}
}
fn pattern_binds(p: &Pattern, name: &str) -> bool {
match p {
Pattern::PVar { name: n } => n == name,
Pattern::PLiteral { .. } | Pattern::PWild => false,
Pattern::PConstructor { args, .. } => args.iter().any(|p| pattern_binds(p, name)),
Pattern::PRecord { fields } => fields.iter().any(|f| pattern_binds(&f.pattern, name)),
Pattern::PTuple { items } => items.iter().any(|p| pattern_binds(p, name)),
}
}
pub fn extract_function(
stage: &Stage,
expr_node: &NodeId,
spec: ExtractFnSpec,
) -> Result<(Stage, Stage), TransformError> {
let mut modified = stage.clone();
let (body, n_params) = match &mut modified {
Stage::FnDecl(fd) => {
let n = fd.params.len();
(&mut fd.body, n)
}
Stage::TypeDecl(_) => return Err(TransformError::NonFnTarget { stage_kind: "TypeDecl" }),
Stage::Import(_) => return Err(TransformError::NonFnTarget { stage_kind: "Import" }),
};
let path = parse_node_id(expr_node.as_str())?;
if path.is_empty() {
return Err(TransformError::UnknownNode { at: expr_node.as_str().into() });
}
if path[0] != n_params + 1 {
return Err(TransformError::UnknownNode { at: expr_node.as_str().into() });
}
let inner = &path[1..];
let target = navigate_to_expr(body, inner, expr_node.as_str())?;
let extracted_expr = target.clone();
let free = free_vars(&extracted_expr);
let declared: std::collections::BTreeSet<String> =
spec.params.iter().map(|p| p.name.clone()).collect();
if free != declared {
let only_in_free: Vec<&String> = free.difference(&declared).collect();
let only_in_declared: Vec<&String> = declared.difference(&free).collect();
return Err(TransformError::ExtractFnRefused {
reason: format!(
"free vars {free:?} differ from declared params {declared:?}: \
missing {only_in_free:?}, extra {only_in_declared:?}"
),
});
}
let call = CExpr::Call {
callee: Box::new(CExpr::Var { name: spec.name.clone() }),
args: spec.params.iter()
.map(|p| CExpr::Var { name: p.name.clone() })
.collect(),
};
*target = call;
let new_fn = Stage::FnDecl(FnDecl {
name: spec.name,
type_params: spec.type_params,
params: spec.params,
effects: spec.effects,
effect_row_var: None,
return_type: spec.return_type,
body: extracted_expr,
examples: Vec::new(),
});
Ok((modified, new_fn))
}
fn parse_node_id(id: &str) -> Result<Vec<usize>, TransformError> {
let s = id.strip_prefix("n_").ok_or_else(|| TransformError::BadNodeId(id.into()))?;
let mut parts = s.split('.');
let head = parts.next().ok_or_else(|| TransformError::BadNodeId(id.into()))?;
if head != "0" {
return Err(TransformError::BadNodeId(id.into()));
}
let mut out = Vec::new();
for p in parts {
out.push(p.parse::<usize>().map_err(|_| TransformError::BadNodeId(id.into()))?);
}
Ok(out)
}
fn navigate_to_expr<'a>(
root: &'a mut CExpr,
path: &[usize],
target_id: &str,
) -> Result<&'a mut CExpr, TransformError> {
let mut current = root;
for &idx in path {
current = step_expr(current, idx)
.ok_or_else(|| TransformError::UnknownNode { at: target_id.into() })?;
}
Ok(current)
}
fn step_expr(e: &mut CExpr, idx: usize) -> Option<&mut CExpr> {
match e {
CExpr::Call { callee, args } => {
if idx == 0 { return Some(callee); }
args.get_mut(idx - 1)
}
CExpr::Let { value, body, .. } => {
match idx {
0 => Some(value),
1 => Some(body),
_ => None,
}
}
CExpr::Match { scrutinee, arms } => {
if idx == 0 { return Some(scrutinee); }
let arm_off = idx - 1;
if arm_off % 2 != 1 {
return None;
}
let arm_index = arm_off / 2;
arms.get_mut(arm_index).map(|a| &mut a.body)
}
CExpr::Block { statements, result } => {
if idx < statements.len() {
statements.get_mut(idx)
} else if idx == statements.len() {
Some(result)
} else {
None
}
}
CExpr::Constructor { args, .. } | CExpr::TupleLit { items: args, .. }
| CExpr::ListLit { items: args, .. } => args.get_mut(idx),
CExpr::RecordLit { fields } => fields.get_mut(idx).map(|f| &mut f.value),
CExpr::FieldAccess { value, .. } => if idx == 0 { Some(value) } else { None },
CExpr::Lambda { body, .. } => if idx == 0 { Some(body) } else { None },
CExpr::BinOp { lhs, rhs, .. } => match idx {
0 => Some(lhs), 1 => Some(rhs), _ => None,
},
CExpr::UnaryOp { expr, .. } => if idx == 0 { Some(expr) } else { None },
CExpr::Return { value } => if idx == 0 { Some(value) } else { None },
_ => None,
}
}
fn cexpr_kind(e: &CExpr) -> &'static str {
match e {
CExpr::Literal { .. } => "Literal",
CExpr::Var { .. } => "Var",
CExpr::Call { .. } => "Call",
CExpr::Let { .. } => "Let",
CExpr::Match { .. } => "Match",
CExpr::Block { .. } => "Block",
CExpr::Constructor { .. } => "Constructor",
CExpr::RecordLit { .. } => "RecordLit",
CExpr::TupleLit { .. } => "TupleLit",
CExpr::ListLit { .. } => "ListLit",
CExpr::FieldAccess { .. } => "FieldAccess",
CExpr::Lambda { .. } => "Lambda",
CExpr::BinOp { .. } => "BinOp",
CExpr::UnaryOp { .. } => "UnaryOp",
CExpr::Return { .. } => "Return",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canonical::{Arm, CLit, FnDecl, Param, Pattern, TypeExpr};
fn let_stage() -> Stage {
let body = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::BinOp {
op: "+".into(),
lhs: Box::new(CExpr::Var { name: "n".into() }),
rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
}),
body: Box::new(CExpr::BinOp {
op: "+".into(),
lhs: Box::new(CExpr::Var { name: "x".into() }),
rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
}),
};
Stage::FnDecl(FnDecl {
name: "outer".into(),
type_params: Vec::new(),
params: vec![Param {
name: "n".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}],
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
})
}
fn let_node_id() -> NodeId { NodeId("n_0.2".into()) }
#[test]
fn rename_local_renames_binding_and_body_reference() {
let stage = let_stage();
let out = rename_local(&stage, &let_node_id(), "y").unwrap();
let Stage::FnDecl(fd) = out else { panic!() };
let CExpr::Let { name, value, body, .. } = fd.body else { panic!() };
assert_eq!(name, "y", "binding renamed");
let CExpr::BinOp { lhs, .. } = *value else { panic!() };
assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "n"));
let CExpr::BinOp { lhs, .. } = *body else { panic!() };
assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "y"));
}
#[test]
fn rename_local_refuses_no_op() {
let stage = let_stage();
let err = rename_local(&stage, &let_node_id(), "x").unwrap_err();
assert!(matches!(err, TransformError::RenameNoOp { .. }));
}
#[test]
fn rename_local_respects_inner_let_shadowing() {
let inner = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
body: Box::new(CExpr::Var { name: "x".into() }),
};
let body = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
body: Box::new(inner),
};
let stage = Stage::FnDecl(FnDecl {
name: "f".into(),
type_params: Vec::new(),
params: Vec::new(),
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
});
let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
let Stage::FnDecl(fd) = out else { panic!() };
let CExpr::Let { name: outer_name, body: outer_body, .. } = fd.body else { panic!() };
assert_eq!(outer_name, "y", "outer let renamed");
let CExpr::Let { name: inner_name, body: inner_body, .. } = *outer_body else { panic!() };
assert_eq!(inner_name, "x");
assert!(matches!(*inner_body, CExpr::Var { name: ref n } if n == "x"));
}
#[test]
fn rename_local_respects_lambda_param_shadowing() {
let lambda = CExpr::Lambda {
params: vec![Param {
name: "x".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}],
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
effects: Vec::new(),
effect_row_var: None,
body: Box::new(CExpr::Var { name: "x".into() }),
};
let body = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
body: Box::new(lambda),
};
let stage = Stage::FnDecl(FnDecl {
name: "f".into(),
type_params: Vec::new(),
params: Vec::new(),
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Function {
params: vec![TypeExpr::Named { name: "Int".into(), args: Vec::new() }],
effects: Vec::new(),
effect_row_var: None,
ret: Box::new(TypeExpr::Named { name: "Int".into(), args: Vec::new() }),
},
body,
examples: Vec::new(),
});
let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
let Stage::FnDecl(fd) = out else { panic!() };
let CExpr::Let { name, body: outer_body, .. } = fd.body else { panic!() };
assert_eq!(name, "y");
let CExpr::Lambda { body: lam_body, .. } = *outer_body else { panic!() };
assert!(matches!(*lam_body, CExpr::Var { name: ref n } if n == "x"));
}
#[test]
fn rename_local_respects_match_pattern_shadowing() {
let match_expr = CExpr::Match {
scrutinee: Box::new(CExpr::Var { name: "foo".into() }),
arms: vec![
Arm {
pattern: Pattern::PVar { name: "x".into() },
body: CExpr::Var { name: "x".into() },
},
Arm {
pattern: Pattern::PWild,
body: CExpr::Var { name: "x".into() },
},
],
};
let body = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
body: Box::new(match_expr),
};
let stage = Stage::FnDecl(FnDecl {
name: "f".into(),
type_params: Vec::new(),
params: Vec::new(),
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
});
let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
let Stage::FnDecl(fd) = out else { panic!() };
let CExpr::Let { body: outer_body, .. } = fd.body else { panic!() };
let CExpr::Match { arms, .. } = *outer_body else { panic!() };
assert!(matches!(arms[0].body, CExpr::Var { name: ref n } if n == "x"));
assert!(matches!(arms[1].body, CExpr::Var { name: ref n } if n == "y"));
}
#[test]
fn rename_local_not_a_let_errors() {
let stage = match_stage_with_two_arms();
let err = rename_local(&stage, &NodeId("n_0.2".into()), "y").unwrap_err();
assert!(matches!(err, TransformError::NotALet { found_kind: "Match", .. }),
"got {err:?}");
}
fn inlinable_stage() -> Stage {
let body = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Literal { value: CLit::Int { value: 5 } }),
body: Box::new(CExpr::BinOp {
op: "+".into(),
lhs: Box::new(CExpr::Var { name: "x".into() }),
rhs: Box::new(CExpr::Var { name: "n".into() }),
}),
};
Stage::FnDecl(FnDecl {
name: "f".into(),
type_params: Vec::new(),
params: vec![Param {
name: "n".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}],
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
})
}
#[test]
fn inline_let_substitutes_literal_value() {
let stage = inlinable_stage();
let out = inline_let(&stage, &NodeId("n_0.2".into())).unwrap();
let Stage::FnDecl(fd) = out else { panic!() };
let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
assert!(matches!(*lhs, CExpr::Literal { value: CLit::Int { value: 5 } }));
}
#[test]
fn inline_let_refuses_call_in_value() {
let body = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Call {
callee: Box::new(CExpr::Var { name: "f".into() }),
args: Vec::new(),
}),
body: Box::new(CExpr::Var { name: "x".into() }),
};
let stage = Stage::FnDecl(FnDecl {
name: "g".into(),
type_params: Vec::new(),
params: Vec::new(),
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
});
let err = inline_let(&stage, &NodeId("n_0.1".into())).unwrap_err();
assert!(matches!(err, TransformError::InlineLetRefused { .. }), "got {err:?}");
}
#[test]
fn inline_let_refuses_capture() {
let inner = CExpr::Let {
name: "y".into(),
ty: None,
value: Box::new(CExpr::Literal { value: CLit::Int { value: 7 } }),
body: Box::new(CExpr::BinOp {
op: "+".into(),
lhs: Box::new(CExpr::Var { name: "x".into() }),
rhs: Box::new(CExpr::Var { name: "y".into() }),
}),
};
let body = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Var { name: "y".into() }),
body: Box::new(inner),
};
let stage = Stage::FnDecl(FnDecl {
name: "g".into(),
type_params: Vec::new(),
params: vec![Param {
name: "y".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}],
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
});
let err = inline_let(&stage, &NodeId("n_0.2".into())).unwrap_err();
assert!(matches!(err, TransformError::InlineLetRefused { .. }), "got {err:?}");
}
#[test]
fn inline_let_substitutes_under_shadowing() {
let inner = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Var { name: "n".into() }),
body: Box::new(CExpr::BinOp {
op: "+".into(),
lhs: Box::new(CExpr::Var { name: "x".into() }),
rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
}),
};
let body = CExpr::Let {
name: "x".into(),
ty: None,
value: Box::new(CExpr::Literal { value: CLit::Int { value: 5 } }),
body: Box::new(inner),
};
let stage = Stage::FnDecl(FnDecl {
name: "g".into(),
type_params: Vec::new(),
params: vec![Param {
name: "n".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}],
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
});
let out = inline_let(&stage, &NodeId("n_0.2".into())).unwrap();
let Stage::FnDecl(fd) = out else { panic!() };
let CExpr::Let { name, .. } = fd.body else { panic!() };
assert_eq!(name, "x", "inner let preserved");
}
#[test]
fn inline_let_not_a_let_target_errors() {
let stage = match_stage_with_two_arms();
let err = inline_let(&stage, &NodeId("n_0.2".into())).unwrap_err();
assert!(matches!(err, TransformError::NotALet { found_kind: "Match", .. }));
}
fn extract_stage() -> Stage {
let body = CExpr::BinOp {
op: "+".into(),
lhs: Box::new(CExpr::BinOp {
op: "*".into(),
lhs: Box::new(CExpr::Var { name: "n".into() }),
rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
}),
rhs: Box::new(CExpr::Var { name: "m".into() }),
};
Stage::FnDecl(FnDecl {
name: "caller".into(),
type_params: Vec::new(),
params: vec![
Param {
name: "n".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
},
Param {
name: "m".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
},
],
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
})
}
fn double_n_spec() -> ExtractFnSpec {
ExtractFnSpec {
name: "double_n".into(),
type_params: Vec::new(),
params: vec![Param {
name: "n".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}],
effects: Vec::new(),
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}
}
#[test]
fn extract_function_replaces_subexpression_with_call() {
let stage = extract_stage();
let (modified, new_fn) = extract_function(
&stage,
&NodeId("n_0.3.0".into()),
double_n_spec(),
).unwrap();
let Stage::FnDecl(fd) = modified else { panic!() };
let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
let CExpr::Call { callee, args } = *lhs else { panic!() };
assert!(matches!(*callee, CExpr::Var { name: ref n } if n == "double_n"));
assert_eq!(args.len(), 1);
assert!(matches!(args[0], CExpr::Var { name: ref n } if n == "n"));
let Stage::FnDecl(new_fd) = new_fn else { panic!() };
assert_eq!(new_fd.name, "double_n");
assert_eq!(new_fd.params.len(), 1);
assert_eq!(new_fd.params[0].name, "n");
let CExpr::BinOp { op, lhs, rhs, .. } = new_fd.body else { panic!() };
assert_eq!(op, "*");
assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "n"));
assert!(matches!(*rhs, CExpr::Literal { value: CLit::Int { value: 2 } }));
}
#[test]
fn extract_function_refuses_extra_params() {
let stage = extract_stage();
let mut spec = double_n_spec();
spec.params.push(Param {
name: "z".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
});
let err = extract_function(&stage, &NodeId("n_0.3.0".into()), spec).unwrap_err();
assert!(matches!(err, TransformError::ExtractFnRefused { .. }), "got {err:?}");
}
#[test]
fn extract_function_refuses_missing_params() {
let stage = extract_stage();
let spec = ExtractFnSpec {
name: "no_args".into(),
type_params: Vec::new(),
params: Vec::new(),
effects: Vec::new(),
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
};
let err = extract_function(&stage, &NodeId("n_0.3.0".into()), spec).unwrap_err();
assert!(matches!(err, TransformError::ExtractFnRefused { .. }), "got {err:?}");
}
#[test]
fn extract_function_handles_zero_free_vars() {
let body = CExpr::BinOp {
op: "+".into(),
lhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
};
let stage = Stage::FnDecl(FnDecl {
name: "caller".into(),
type_params: Vec::new(),
params: Vec::new(),
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
});
let spec = ExtractFnSpec {
name: "one".into(),
type_params: Vec::new(),
params: Vec::new(),
effects: Vec::new(),
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
};
let (modified, new_fn) = extract_function(
&stage, &NodeId("n_0.1.0".into()), spec,
).unwrap();
let Stage::FnDecl(fd) = modified else { panic!() };
let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
let CExpr::Call { args, .. } = *lhs else { panic!() };
assert_eq!(args.len(), 0, "no args for zero-free-var extract");
let Stage::FnDecl(new_fd) = new_fn else { panic!() };
assert!(matches!(new_fd.body, CExpr::Literal { value: CLit::Int { value: 1 } }));
}
#[test]
fn extract_function_typedecl_target_errors() {
let stage = Stage::TypeDecl(crate::canonical::TypeDecl {
name: "T".into(),
params: Vec::new(),
definition: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
});
let err = extract_function(&stage, &NodeId("n_0.0".into()), double_n_spec())
.unwrap_err();
assert!(matches!(err, TransformError::NonFnTarget { stage_kind: "TypeDecl" }));
}
fn match_stage_with_two_arms() -> Stage {
let body = CExpr::Match {
scrutinee: Box::new(CExpr::Var { name: "n".into() }),
arms: vec![
Arm {
pattern: Pattern::PLiteral { value: CLit::Int { value: 0 } },
body: CExpr::Literal { value: CLit::Int { value: 1 } },
},
Arm {
pattern: Pattern::PWild,
body: CExpr::Literal { value: CLit::Int { value: 2 } },
},
],
};
Stage::FnDecl(FnDecl {
name: "pick".into(),
type_params: Vec::new(),
params: vec![Param {
name: "n".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}],
effects: Vec::new(),
effect_row_var: None,
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
body,
examples: Vec::new(),
})
}
fn match_node_id() -> NodeId {
NodeId("n_0.2".into())
}
#[test]
fn replace_first_arm_body_succeeds() {
let stage = match_stage_with_two_arms();
let new_body = CExpr::Literal { value: CLit::Int { value: 42 } };
let out = replace_match_arm(&stage, &match_node_id(), 0, new_body).unwrap();
let Stage::FnDecl(fd) = out else { panic!() };
let CExpr::Match { arms, .. } = fd.body else { panic!() };
assert_eq!(arms.len(), 2);
assert!(matches!(arms[0].body, CExpr::Literal { value: CLit::Int { value: 42 } }));
assert!(matches!(arms[1].body, CExpr::Literal { value: CLit::Int { value: 2 } }));
assert!(matches!(arms[0].pattern, Pattern::PLiteral { .. }));
}
#[test]
fn replace_second_arm_preserves_first() {
let stage = match_stage_with_two_arms();
let new_body = CExpr::Literal { value: CLit::Int { value: 99 } };
let out = replace_match_arm(&stage, &match_node_id(), 1, new_body).unwrap();
let Stage::FnDecl(fd) = out else { panic!() };
let CExpr::Match { arms, .. } = fd.body else { panic!() };
assert!(matches!(arms[0].body, CExpr::Literal { value: CLit::Int { value: 1 } }));
assert!(matches!(arms[1].body, CExpr::Literal { value: CLit::Int { value: 99 } }));
}
#[test]
fn arm_index_out_of_range_errors() {
let stage = match_stage_with_two_arms();
let new_body = CExpr::Literal { value: CLit::Unit };
let err = replace_match_arm(&stage, &match_node_id(), 5, new_body).unwrap_err();
assert!(matches!(err, TransformError::ArmIndexOutOfRange { arm_count: 2, requested: 5, .. }));
}
#[test]
fn non_match_target_errors() {
let stage = match_stage_with_two_arms();
let new_body = CExpr::Literal { value: CLit::Unit };
let err = replace_match_arm(&stage, &NodeId("n_0.2.0".into()), 0, new_body)
.unwrap_err();
assert!(matches!(err, TransformError::NotAMatch { found_kind: "Var", .. }),
"got {err:?}");
}
#[test]
fn unknown_node_errors() {
let stage = match_stage_with_two_arms();
let new_body = CExpr::Literal { value: CLit::Unit };
let err = replace_match_arm(&stage, &NodeId("n_0.99".into()), 0, new_body)
.unwrap_err();
assert!(matches!(err, TransformError::UnknownNode { .. }), "got {err:?}");
}
#[test]
fn typedecl_target_errors() {
let stage = Stage::TypeDecl(crate::canonical::TypeDecl {
name: "T".into(),
params: Vec::new(),
definition: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
});
let err = replace_match_arm(&stage, &match_node_id(), 0,
CExpr::Literal { value: CLit::Unit }).unwrap_err();
assert!(matches!(err, TransformError::NonFnTarget { stage_kind: "TypeDecl" }));
}
}