///|
pub(all) struct AnalysisResult[Data] {
  data : Map[Int32, Data]
  converged : Bool
  iterations : Int32
}

///|
/// Computes e-class analysis to a bounded fixed point. `make` observes the
/// current optional child data, and `merge` joins contributions. Both callbacks
/// must be pure; merge must be associative, commutative, idempotent, and monotone.
/// Data values must be immutable. A nonconverged result must not be treated as
/// complete. Run this from the saturation hook to add derived constants/unions.
pub fn[Op : Eq + Hash, Data : Eq] EGraph::analyze(
  self : EGraph[Op],
  make : (Node[Op], Array[Data?]) -> Data?,
  merge : (Data, Data) -> Data,
  iteration_limit~ : Int32 = 1000,
) -> AnalysisResult[Data] {
  self.require_clean()
  let data : Map[Int32, Data] = Map([])
  let ids = self.class_ids()
  for iteration in 0.. data.get(self.find(child))))
          is Some(value) {
          let merged = match data.get(id) {
            None => value
            Some(old) => merge(old, value)
          }
          if data.get(id) != Some(merged) {
            data[id] = merged
            changed = true
          }
        }
      }
    }
    if !changed {
      return { data, converged: true, iterations: iteration + 1, }
    }
  }
  { data, converged: false, iterations: iteration_limit, }
}