///|
/// Infer a single token rewrite only when all the other tokens align with an
/// observed output segment. Neither task names nor future rows are available.
fn normalization_bases(
  examples : Array[Example],
  column : Int32,
) -> Array[Expr] {
  let base = Field(column)
  let result : Array[Expr] = [base]
  let seen : Map[String, Bool] = Map([])
  for separator in [",", ";", "|", "/"] {
    for e in examples {
      let before = e.input[column]
        .split(separator)
        .map(s => s.trim().to_owned())
        .collect()
      let after = e.output
        .split(separator)
        .map(s => s.trim().to_owned())
        .collect()
      if before.length() < 2 ||
        before.length() > 8 ||
        after.length() < before.length() {
        continue
      }
      for offset in 0..<(after.length() - before.length() + 1) {
        for index in 0.. 32 ||
            to.length() > 32 {
            continue
          }
          let mut aligned = true
          for k in 0..= 17 {
              return result
            }
          }
        }
      }
    }
  }
  result
}

///|
/// A shared output suffix can be an optional field suffix. A guard makes
/// applying the learned normalization twice idempotent.
fn learn_normalizations(
  pool : Pool,
  examples : Array[Example],
  column : Int32,
  options : SearchOptions,
) -> Unit {
  if examples.length() < 2 {
    return
  }
  let targets = examples.map(e => e.output.to_array())
  let first = targets[0]
  let suffixes : Array[String] = []
  for length in 1..<(Int32::min(32, first.length()) + 1) {
    let suffix = char_slice(first, first.length() - length, first.length())
    if !examples.all(e => e.output.has_suffix(suffix)) {
      break
    }
    suffixes.push(suffix)
  }
  let bases = normalization_bases(examples, column)
  if bases.length() >= 17 {
    pool.truncated = true
  }
  for base in bases {
    offer_atom(pool, base, expression_cost(base), examples, options)
    for suffix in suffixes {
      if pool.attempts >= options.max_atom_evaluations {
        pool.truncated = true
        return
      }
      let values : Array[String] = []
      for e in examples {
        match attempt(base, e.input) {
          Ok(value) => values.push(value)
          Err(_) => break
        }
      }
      if values.length() != examples.length() {
        continue
      }
      let present = values.filter(v => v.has_suffix(suffix)).length()
      if present == 0 || present == values.length() {
        continue
      }
      let expression = If(
        Matches(base, regex_literal(suffix) + "$", false),
        base,
        Concat([base, Literal(suffix)]),
      )
      offer_atom(
        pool,
        expression,
        12 + expression_cost(base) + suffix.char_length(),
        examples,
        options,
      )
    }
  }
}