///|
fn metadata_rules(instructions : Array[Instruction]) -> Array[Diagnostic] {
  let out : Array[Diagnostic] = []
  let mut healthchecks = 0
  let mut defined : Map[String, Bool] = Map([])
  let saved : Map[String, Map[String, Bool]] = Map([])
  let mut stage_name : String? = None
  for ins in instructions {
    fn emit(code : String, message : String) -> Unit {
      if !out.iter().any(d => d.line == ins.line && d.code == code) {
        out.push({ line: ins.line, code, message, })
      }
    }
    if ins.name == "FROM" {
      if stage_name is Some(name) {
        saved[name] = defined.copy()
      }
      let values = ins.split_arguments().values catch { _ => [] }
      defined = if values.is_empty() {
        Map([])
      } else {
        saved.get(values[0].to_lower()).unwrap_or(Map([])).copy()
      }
      stage_name = if values.length() == 3 {
        Some(values[2].to_lower())
      } else {
        None
      }
      healthchecks = 0
      if ins.arguments.has_prefix("--platform=") {
        emit("DL3029", "Avoid fixing the build platform in FROM")
      }
    }
    if ins.name == "HEALTHCHECK" {
      healthchecks += 1
      if healthchecks > 1 {
        emit("DL3012", "Earlier HEALTHCHECK in this stage is overridden")
      }
      let args = words(ins.arguments)
      let mut cmd = -1
      for i, value in args {
        if value.to_upper() == "CMD" {
          cmd = i
          break
        }
      }
      if cmd >= 0 && cmd + 1 < args.length() && !args[cmd + 1].has_prefix("[") {
        emit("DL3025", "Prefer JSON exec form for signal forwarding")
      }
    }
    if ins.name == "EXPOSE" {
      for spec in words(ins.arguments) {
        if spec.contains("$") {
          continue
        }
        let parts = spec.split("/").collect()
        let mut valid = parts.length() <= 2 &&
          (parts.length() == 1 || parts[1] == "tcp" || parts[1] == "udp")
        let mut oversized = false
        let ports = parts[0].split("-").collect()
        let mut previous = -1
        if ports.length() > 2 {
          valid = false
        }
        for port in ports {
          let mut number = 0
          if port.is_empty() {
            valid = false
          }
          for c in port.iter() {
            if c < '0' || c > '9' || number > 65535 {
              valid = false
              break
            }
            number = number * 10 + c.to_int() - 48
          }
          if number > 65535 {
            oversized = true
          }
          if number > 65535 || number < previous {
            valid = false
          }
          previous = number
        }
        if !valid {
          emit(
            if oversized {
              "DL3011"
            } else {
              "ML1012"
            },
            "EXPOSE requires a port or ordered range from 0 to 65535, optionally /tcp or /udp",
          )
        }
      }
    }
    if ins.name == "ENV" {
      let tokens = raw_arguments(ins.arguments) catch { _ => [] }
      let names = tokens
        .filter(t => t.contains("="))
        .map(t => t.split("=").collect()[0].to_owned())
      if names.length() > 1 {
        for token in tokens {
          let parts = token.split("=").collect()
          if parts.length() < 2 {
            continue
          }
          let used = expand_variables(
            parts[1:].map(s => s.to_owned()).join("="),
            Map([]),
          ) catch {
            _ => { value: "", unresolved: [], }
          }
          if used.unresolved
            .iter()
            .any(name => names.contains(name) && !defined.contains(name)) {
            emit(
              "DL3044", "ENV assignments on one instruction read the previous environment",
            )
          }
        }
      }
      for name in names {
        defined[name] = true
      }
    }
  }
  out
}