use crate::canonical::{CExpr, Stage};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Patch {
Replace {
target: String,
with: CExpr,
},
Delete {
target: String,
},
WrapWith {
target: String,
wrapper: CExpr,
},
}
#[derive(Debug, Clone, thiserror::Error, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PatchError {
#[error("unknown node id `{at}`")]
UnknownNode { at: String },
#[error("cannot patch non-expression at `{at}` ({reason})")]
NonExprTarget { at: String, reason: String },
#[error("Delete target is not in a list-shaped parent at `{at}`")]
DeleteNotInList { at: String },
#[error("WrapWith fragment must contain exactly one `_HOLE_` var")]
WrapWithMissingHole,
#[error("malformed NodeId `{0}`")]
BadNodeId(String),
}
pub fn apply_patch(stage: &Stage, patch: &Patch) -> Result<Stage, PatchError> {
let mut out = stage.clone();
let target = match patch {
Patch::Replace { target, .. } | Patch::Delete { target } | Patch::WrapWith { target, .. } => target,
};
let path = parse_node_id(target)?;
let body = match &mut out {
Stage::FnDecl(fd) => &mut fd.body,
Stage::TypeDecl(_) | Stage::Import(_) => {
return Err(PatchError::NonExprTarget {
at: target.clone(),
reason: "only fn-decl bodies are patchable".into(),
});
}
};
if path.is_empty() {
return Err(PatchError::NonExprTarget {
at: target.clone(),
reason: "cannot replace the stage root".into(),
});
}
let body_path = &path[1..];
apply_inside_expr(body, body_path, patch, target)?;
Ok(out)
}
fn parse_node_id(id: &str) -> Result<Vec<usize>, PatchError> {
let s = id.strip_prefix("n_").ok_or_else(|| PatchError::BadNodeId(id.into()))?;
let mut parts = s.split('.');
let head = parts.next().ok_or_else(|| PatchError::BadNodeId(id.into()))?;
if head != "0" {
return Err(PatchError::BadNodeId(id.into()));
}
let mut out = Vec::new();
for p in parts {
out.push(p.parse::<usize>().map_err(|_| PatchError::BadNodeId(id.into()))?);
}
Ok(out)
}
fn apply_inside_expr(
e: &mut CExpr,
path: &[usize],
patch: &Patch,
target_id: &str,
) -> Result<(), PatchError> {
if path.is_empty() {
match patch {
Patch::Replace { with, .. } => {
*e = with.clone();
Ok(())
}
Patch::Delete { .. } => Err(PatchError::DeleteNotInList { at: target_id.into() }),
Patch::WrapWith { wrapper, .. } => {
let mut wrapped = wrapper.clone();
if !substitute_hole(&mut wrapped, e) {
return Err(PatchError::WrapWithMissingHole);
}
*e = wrapped;
Ok(())
}
}
} else {
let i = path[0];
let rest = &path[1..];
descend(e, i, rest, patch, target_id)
}
}
fn descend(
e: &mut CExpr,
i: usize,
rest: &[usize],
patch: &Patch,
target_id: &str,
) -> Result<(), PatchError> {
let unknown = || PatchError::UnknownNode { at: target_id.into() };
match e {
CExpr::Call { callee, args } => {
if i == 0 { return apply_inside_expr(callee, rest, patch, target_id); }
let idx = i - 1;
args.get_mut(idx).ok_or_else(unknown)
.and_then(|c| apply_inside_expr(c, rest, patch, target_id))
}
CExpr::Let { value, body, .. } => match i {
0 => apply_inside_expr(value, rest, patch, target_id),
1 => apply_inside_expr(body, rest, patch, target_id),
_ => Err(unknown()),
},
CExpr::Match { scrutinee, arms } => {
if i == 0 { return apply_inside_expr(scrutinee, rest, patch, target_id); }
let arm_pos = i - 1;
let arm_idx = arm_pos / 2;
let is_pat = arm_pos.is_multiple_of(2);
let arm = arms.get_mut(arm_idx).ok_or_else(unknown)?;
if is_pat {
Err(PatchError::NonExprTarget {
at: target_id.into(),
reason: "patches on patterns are not supported (use Replace on the arm body or WrapWith)".into(),
})
} else {
apply_inside_expr(&mut arm.body, rest, patch, target_id)
}
}
CExpr::Block { statements, result } => {
if i < statements.len() {
if rest.is_empty() {
match patch {
Patch::Delete { .. } => {
statements.remove(i);
Ok(())
}
Patch::Replace { with, .. } => {
statements[i] = with.clone();
Ok(())
}
Patch::WrapWith { wrapper, .. } => {
let mut wrapped = wrapper.clone();
let original = std::mem::replace(&mut statements[i], CExpr::Literal { value: crate::canonical::CLit::Unit });
if !substitute_hole(&mut wrapped, &original) {
statements[i] = original; return Err(PatchError::WrapWithMissingHole);
}
statements[i] = wrapped;
Ok(())
}
}
} else {
apply_inside_expr(&mut statements[i], rest, patch, target_id)
}
} else if i == statements.len() {
if matches!(patch, Patch::Delete { .. }) {
Err(PatchError::DeleteNotInList { at: target_id.into() })
} else {
apply_inside_expr(result, rest, patch, target_id)
}
} else {
Err(unknown())
}
}
CExpr::Constructor { args, .. } => {
args.get_mut(i).ok_or_else(unknown)
.and_then(|c| apply_inside_expr(c, rest, patch, target_id))
}
CExpr::RecordLit { fields } => {
fields.get_mut(i).ok_or_else(unknown)
.and_then(|f| apply_inside_expr(&mut f.value, rest, patch, target_id))
}
CExpr::TupleLit { items } | CExpr::ListLit { items } => {
if rest.is_empty() && matches!(patch, Patch::Delete { .. }) {
if i >= items.len() { return Err(unknown()); }
items.remove(i);
return Ok(());
}
items.get_mut(i).ok_or_else(unknown)
.and_then(|c| apply_inside_expr(c, rest, patch, target_id))
}
CExpr::FieldAccess { value, .. } => {
if i == 0 { apply_inside_expr(value, rest, patch, target_id) } else { Err(unknown()) }
}
CExpr::Lambda { body, .. } => {
if i == 0 { apply_inside_expr(body, rest, patch, target_id) } else { Err(unknown()) }
}
CExpr::BinOp { lhs, rhs, .. } => match i {
0 => apply_inside_expr(lhs, rest, patch, target_id),
1 => apply_inside_expr(rhs, rest, patch, target_id),
_ => Err(unknown()),
},
CExpr::UnaryOp { expr, .. } => {
if i == 0 { apply_inside_expr(expr, rest, patch, target_id) } else { Err(unknown()) }
}
CExpr::Return { value } => {
if i == 0 { apply_inside_expr(value, rest, patch, target_id) } else { Err(unknown()) }
}
CExpr::Literal { .. } | CExpr::Var { .. } => Err(unknown()),
}
}
fn substitute_hole(node: &mut CExpr, target: &CExpr) -> bool {
let mut count = 0;
walk_substitute(node, target, &mut count);
count == 1
}
fn walk_substitute(e: &mut CExpr, target: &CExpr, count: &mut u32) {
if let CExpr::Var { name } = e {
if name == "_HOLE_" {
*e = target.clone();
*count += 1;
return;
}
}
match e {
CExpr::Literal { .. } | CExpr::Var { .. } => {}
CExpr::Call { callee, args } => {
walk_substitute(callee, target, count);
for a in args { walk_substitute(a, target, count); }
}
CExpr::Let { value, body, .. } => {
walk_substitute(value, target, count);
walk_substitute(body, target, count);
}
CExpr::Match { scrutinee, arms } => {
walk_substitute(scrutinee, target, count);
for a in arms { walk_substitute(&mut a.body, target, count); }
}
CExpr::Block { statements, result } => {
for s in statements { walk_substitute(s, target, count); }
walk_substitute(result, target, count);
}
CExpr::Constructor { args, .. } => for a in args { walk_substitute(a, target, count); },
CExpr::RecordLit { fields } => for f in fields { walk_substitute(&mut f.value, target, count); },
CExpr::TupleLit { items } | CExpr::ListLit { items } => {
for i in items { walk_substitute(i, target, count); }
}
CExpr::FieldAccess { value, .. } => walk_substitute(value, target, count),
CExpr::Lambda { body, .. } => walk_substitute(body, target, count),
CExpr::BinOp { lhs, rhs, .. } => {
walk_substitute(lhs, target, count);
walk_substitute(rhs, target, count);
}
CExpr::UnaryOp { expr, .. } => walk_substitute(expr, target, count),
CExpr::Return { value } => walk_substitute(value, target, count),
}
}