///|
/// Produce a deterministic semantic patch; array changes replace the array.
/// Dictionary order alone produces no operations. Input trees are validated.
pub fn diff(
  before : Value,
  after : Value,
  limits~ : Limits = Limits::default(),
) -> Array[Patch] raise PlistError {
  validate(before, limits~)
  validate(after, limits~)
  let result : Array[Patch] = []
  fn append(op : Patch, path : String) -> Unit raise PlistError {
    if path.length() > limits.max_bytes {
      raise Limit("diff pointer budget")
    }
    if result.length() >= limits.max_items {
      raise Limit("diff operation budget")
    }
    result.push(op)
  }
  fn walk(a : Value, b : Value, path : String) -> Unit raise PlistError {
    if equivalent(a, b) {
      return
    }
    match (a, b) {
      (Dict(old), Dict(fresh)) => {
        for (key, _) in old {
          if b.get(key) == None {
            let child = path + format_pointer([key])
            append(Remove(child), child)
          }
        }
        for (key, value) in fresh {
          let child = path + format_pointer([key])
          match a.get(key) {
            None => append(Add(child, value), child)
            Some(previous) => walk(previous, value, child)
          }
        }
      }
      _ => append(Replace(path, b), path)
    }
  }
  walk(before, after, "")
  // Intermediate edits must fit the same budgets as the two endpoint trees.
  ignore(apply_patch(before, result, limits~))
  result
}