///|
pub suberror VerifyError {
  MissingTerminator(block_id~ : Int)
  EmptyFunction
  UndefinedValue(value_id~ : Int)
  ForeignValue(value_id~ : Int)
  ForeignBlock(block_id~ : Int)
  ForeignInstruction(inst_id~ : Int)
  ForeignGlobalValue(global_value_id~ : Int)
  DuplicateBlockId(block_id~ : Int)
  DuplicateValueDefinition(value_id~ : Int)
  DuplicateInstructionId(inst_id~ : Int)
  UseBeforeDefinition(value_id~ : Int)
  NonDominatingUse(value_id~ : Int, defining_block~ : Int, use_block~ : Int)
  InstructionOperandMismatch(inst_id~ : Int)
  InvalidBlockTarget(block_id~ : Int)
  ArityMismatch(message~ : String)
  TypeMismatch(message~ : String)
  UnverifiableInstruction(message~ : String)
} derive(Debug, Eq)

///|
priv enum ValueDefinition {
  FunctionParameter(Type)
  BlockParameter(Type, Int)
  InstructionResult(Type, Int, Int)
}

///|
pub impl Show for VerifyError with fn output(self, logger) {
  logger.write_string(Repr(self).to_string())
}

///|
/// Construct a structured error for a consumer seam that cannot validate or
/// lower an otherwise well-formed MilkIR function.
pub fn VerifyError::unverifiable_instruction(message : String) -> VerifyError {
  UnverifiableInstruction(message~)
}

///|
pub fn Function::verify(self : Function) -> Unit raise VerifyError {
  if self.owner.val.construction_error is Some(error) {
    raise error
  }
  if self.blocks.is_empty() {
    raise EmptyFunction
  }
  for index, entry in self.global_values {
    let (global_value, data) = entry
    if !self.owns_global_value(global_value) || global_value.id != index {
      raise ForeignGlobalValue(global_value_id=global_value.id)
    }
    verify_global_value_data(data)
  }
  let mut max_block_id = -1
  for block in self.blocks {
    if !self.owns_block(block) {
      raise ForeignBlock(block_id=block.id)
    }
    if block.id < 0 {
      raise InvalidBlockTarget(block_id=block.id)
    }
    if block.id > max_block_id {
      max_block_id = block.id
    }
  }
  let blocks : Array[Block?] = Array::make(max_block_id + 1, None)
  for block in self.blocks {
    if blocks[block.id] is Some(_) {
      raise DuplicateBlockId(block_id=block.id)
    }
    blocks[block.id] = Some(block)
    if block.terminator is None {
      raise MissingTerminator(block_id=block.id)
    }
  }
  let definitions : @hashmap.HashMap[Int, ValueDefinition] = HashMap([])
  for item in self.params {
    let (param, ty) = item
    require_owned_value(self, param)
    record_definition(param, ty, FunctionParameter(ty), definitions)
  }
  let instruction_ids : @hashset.HashSet[Int] = HashSet([])
  for block in self.blocks {
    for item in block.params {
      let (param, ty) = item
      require_owned_value(self, param)
      record_definition(param, ty, BlockParameter(ty, block.id), definitions)
    }
    for inst_index, inst in block.instructions {
      if !self.owns_inst(inst) {
        raise ForeignInstruction(inst_id=inst.id)
      }
      for operand in inst.args {
        require_owned_value(self, operand)
      }
      for operand in inst.operands {
        require_owned_value(self, operand)
      }
      for result in inst.results {
        require_owned_value(self, result)
      }
      if instruction_ids.contains(inst.id) {
        raise DuplicateInstructionId(inst_id=inst.id)
      }
      instruction_ids.add(inst.id)
      verify_instruction_operands(inst)
      for result in inst.results {
        record_definition(
          result,
          result.ty,
          InstructionResult(result.ty, block.id, inst_index),
          definitions,
        )
      }
    }
    if block.terminator is Some(term) {
      require_owned_terminator_values(self, term)
    }
  }
  let dominance = CFG::build(self).compute_dominance()
  for block in self.blocks {
    for inst_index, inst in block.instructions {
      verify_inst(self, inst, definitions, block.id, inst_index, dominance)
    }
    if block.terminator is Some(term) {
      verify_terminator(
        term,
        definitions,
        blocks,
        self.results,
        block.id,
        block.instructions.length(),
        dominance,
      )
    }
  }
}

///|
fn require_owned_value(
  func : Function,
  value : Value,
) -> Unit raise VerifyError {
  if !func.owns_value(value) {
    raise ForeignValue(value_id=value.id)
  }
}

///|
fn require_owned_values(
  func : Function,
  values : Array[Value],
) -> Unit raise VerifyError {
  for value in values {
    require_owned_value(func, value)
  }
}

///|
fn require_owned_terminator_values(
  func : Function,
  term : Terminator,
) -> Unit raise VerifyError {
  match term {
    Jump(_, values) | Return(values) => require_owned_values(func, values)
    Branch(cond, _, true_args, _, false_args) => {
      require_owned_value(func, cond)
      require_owned_values(func, true_args)
      require_owned_values(func, false_args)
    }
    Brz(cond, _, _) | Brnz(cond, _, _) | BrTable(cond, _, _) =>
      require_owned_value(func, cond)
    Trap(_) | TrapExit(_) => ()
  }
}

///|
/// Verify that a function is self-contained core MilkIR. Dialect-bearing
/// instructions must be validated and lowered by their owning adapter.
pub fn Function::verify_core(self : Function) -> Unit raise VerifyError {
  self.verify()
  if !self.global_values.is_empty() {
    raise UnverifiableInstruction(
      message="context global values require a dialect adapter",
    )
  }
  for block in self.blocks {
    for inst in block.instructions {
      if inst.opcode is Ext(ext, _) {
        raise UnverifiableInstruction(
          message="unresolved dialect operation \{ext.dialect}.\{ext.opcode} is not core MilkIR",
        )
      }
    }
  }
}

///|
/// Verify generic MilkIR structure, require every extension and context global
/// to belong to the owning dialect, and apply that adapter's validators before
/// crossing its seam.
pub fn Function::verify_with_dialect_validator(
  self : Function,
  dialect : String,
  validate_extension : (ExtensionInstView) -> String?,
  validate_global_value : (GlobalValueData) -> String?,
) -> Unit raise VerifyError {
  self.verify()
  for entry in self.global_values {
    if entry.1 is ContextField(field, _, _) {
      if field.dialect != dialect {
        raise UnverifiableInstruction(
          message="context field dialect '\{field.dialect}' is not owned by adapter '\{dialect}'",
        )
      }
      if validate_global_value(entry.1) is Some(message) {
        raise UnverifiableInstruction(message~)
      }
    }
  }
  for block in self.blocks {
    for inst in block.instructions {
      if inst.opcode is Ext(_, _) {
        let view = extension_inst_view(inst)
        if view.op.dialect != dialect {
          raise UnverifiableInstruction(
            message="extension dialect '\{view.op.dialect}' is not owned by adapter '\{dialect}'",
          )
        }
        if validate_extension(view) is Some(message) {
          raise UnverifiableInstruction(message~)
        }
      }
    }
  }
}

///|
fn record_definition(
  value : Value,
  declared_type : Type,
  definition : ValueDefinition,
  definitions : @hashmap.HashMap[Int, ValueDefinition],
) -> Unit raise VerifyError {
  if value.ty != declared_type {
    raise TypeMismatch(
      message="value \{value.id} has type \{value.ty}, but its declaration has type \{declared_type}",
    )
  }
  if definitions.get(value.id) is Some(_) {
    raise DuplicateValueDefinition(value_id=value.id)
  }
  definitions.set(value.id, definition)
}

///|
fn definition_type(definition : ValueDefinition) -> Type {
  match definition {
    FunctionParameter(ty)
    | BlockParameter(ty, _)
    | InstructionResult(ty, _, _) => ty
  }
}

///|
fn verify_defined(
  value : Value,
  definitions : @hashmap.HashMap[Int, ValueDefinition],
  use_block : Int,
  use_position : Int,
  dominance : Dominance,
) -> Unit raise VerifyError {
  match definitions.get(value.id) {
    None => raise UndefinedValue(value_id=value.id)
    Some(definition) => {
      let defined_type = definition_type(definition)
      if value.ty != defined_type {
        raise TypeMismatch(
          message="value \{value.id} has type \{value.ty}, but its definition has type \{defined_type}",
        )
      }
      match definition {
        FunctionParameter(_) => ()
        BlockParameter(_, defining_block) =>
          if defining_block != use_block &&
            !dominance.dominates(defining_block, use_block) {
            raise NonDominatingUse(
              value_id=value.id,
              defining_block~,
              use_block~,
            )
          }
        InstructionResult(_, defining_block, defining_position) =>
          if defining_block == use_block {
            if defining_position >= use_position {
              raise UseBeforeDefinition(value_id=value.id)
            }
          } else if !dominance.dominates(defining_block, use_block) {
            raise NonDominatingUse(
              value_id=value.id,
              defining_block~,
              use_block~,
            )
          }
      }
    }
  }
}

///|
fn verify_instruction_operands(inst : Inst) -> Unit raise VerifyError {
  if inst.args.length() != inst.operands.length() {
    raise InstructionOperandMismatch(inst_id=inst.id)
  }
  for i in 0.. Unit raise VerifyError {
  if actual != expected {
    raise ArityMismatch(
      message="\{context} expects \{expected} operands, got \{actual}",
    )
  }
}

///|
fn require_same_type(
  a : Value,
  b : Value,
  context : String,
) -> Unit raise VerifyError {
  if a.ty != b.ty {
    raise TypeMismatch(message="\{context} operands have mismatched types")
  }
}

///|
fn require_block(
  block_id : Int,
  blocks : Array[Block?],
) -> Block raise VerifyError {
  if block_id < 0 || block_id >= blocks.length() {
    raise InvalidBlockTarget(block_id~)
  }
  match blocks[block_id] {
    Some(block) => block
    None => raise InvalidBlockTarget(block_id~)
  }
}

///|
fn verify_block_args(
  block_id : Int,
  args : Array[Value],
  blocks : Array[Block?],
  definitions : @hashmap.HashMap[Int, ValueDefinition],
  edge : String,
  use_block : Int,
  use_position : Int,
  dominance : Dominance,
) -> Unit raise VerifyError {
  let block = require_block(block_id, blocks)
  for arg in args {
    verify_defined(arg, definitions, use_block, use_position, dominance)
  }
  if args.length() != block.params.length() {
    raise ArityMismatch(
      message="\{edge} to block \{block_id} expects \{block.params.length()} arguments, got \{args.length()}",
    )
  }
  for i, arg in args {
    let (_, expected_type) = block.params[i]
    if arg.ty != expected_type {
      raise TypeMismatch(
        message="\{edge} to block \{block_id} argument \{i} has type \{arg.ty}, expected \{expected_type}",
      )
    }
  }
}

///|
fn verify_i32(value : Value, context : String) -> Unit raise VerifyError {
  if value.ty != I32 {
    raise TypeMismatch(message="\{context} must be i32, got \{value.ty}")
  }
}

///|
fn verify_inst(
  func : Function,
  inst : Inst,
  definitions : @hashmap.HashMap[Int, ValueDefinition],
  use_block : Int,
  use_position : Int,
  dominance : Dominance,
) -> Unit raise VerifyError {
  for arg in inst.args {
    verify_defined(arg, definitions, use_block, use_position, dominance)
  }
  verify_opcode_contract(func, inst)
}

///|
fn verify_global_value_data(data : GlobalValueData) -> Unit raise VerifyError {
  match data {
    ContextField(field, _, region) => {
      if field.dialect == "" {
        raise UnverifiableInstruction(
          message="context fields require a dialect name",
        )
      }
      if field.key < 0 {
        raise UnverifiableInstruction(
          message="context field keys must be non-negative",
        )
      }
      match region {
        Heap(index) | Table(index) =>
          if index < 0 {
            raise UnverifiableInstruction(
              message="context field alias-region indices must be non-negative",
            )
          }
        Context | Other => ()
      }
    }
  }
}

///|
fn extension_inst_view(inst : Inst) -> ExtensionInstView {
  let op = match inst.opcode {
    Ext(op, _) => op
    _ => abort("extension_inst_view requires an extension instruction")
  }
  {
    op,
    operand_types: ReadOnlyArray::makei(inst.args.length(), i => inst.args[i].ty),
    result_types: ReadOnlyArray::makei(inst.results.length(), i => {
      inst.results[i].ty
    }),
  }
}

///|
fn verify_terminator(
  term : Terminator,
  definitions : @hashmap.HashMap[Int, ValueDefinition],
  blocks : Array[Block?],
  result_types : Array[Type],
  use_block : Int,
  use_position : Int,
  dominance : Dominance,
) -> Unit raise VerifyError {
  match term {
    Return(values) => {
      for value in values {
        verify_defined(value, definitions, use_block, use_position, dominance)
      }
      if values.length() != result_types.length() {
        raise ArityMismatch(
          message="return expects \{result_types.length()} values, got \{values.length()}",
        )
      }
      for i, value in values {
        let expected_type = result_types[i]
        if value.ty != expected_type {
          raise TypeMismatch(
            message="return value \{i} has type \{value.ty}, expected \{expected_type}",
          )
        }
      }
    }
    Jump(target, values) =>
      verify_block_args(
        target, values, blocks, definitions, "jump", use_block, use_position, dominance,
      )
    Branch(cond, true_target, true_args, false_target, false_args) => {
      verify_defined(cond, definitions, use_block, use_position, dominance)
      verify_i32(cond, "branch condition")
      verify_block_args(
        true_target, true_args, blocks, definitions, "branch", use_block, use_position,
        dominance,
      )
      verify_block_args(
        false_target, false_args, blocks, definitions, "branch", use_block, use_position,
        dominance,
      )
    }
    Brz(cond, true_target, false_target)
    | Brnz(cond, true_target, false_target) => {
      verify_defined(cond, definitions, use_block, use_position, dominance)
      verify_i32(cond, "branch condition")
      verify_block_args(
        true_target,
        [],
        blocks,
        definitions,
        "branch",
        use_block,
        use_position,
        dominance,
      )
      verify_block_args(
        false_target,
        [],
        blocks,
        definitions,
        "branch",
        use_block,
        use_position,
        dominance,
      )
    }
    BrTable(index, targets, default_target) => {
      verify_defined(index, definitions, use_block, use_position, dominance)
      verify_i32(index, "branch table index")
      for target in targets {
        verify_block_args(
          target,
          [],
          blocks,
          definitions,
          "branch table",
          use_block,
          use_position,
          dominance,
        )
      }
      verify_block_args(
        default_target,
        [],
        blocks,
        definitions,
        "branch table",
        use_block,
        use_position,
        dominance,
      )
    }
    Trap(_) | TrapExit(_) => ()
  }
}