///|
/// Iterative admission of host-built ASTs before validation/canonicalization.
/// Units count declarations, instructions, locals, type members and payload
/// bytes. They bound accepted shape, not the optimizer's exact instruction count.
fn ResourceBudget::admit_module(
  self : ResourceBudget,
  module_ : @types.Module,
) -> ResourceLease raise RuntimeError {
  let lease = self.reserve(CompilationUnits, 0L)
  errdefer lease.release()
  let used = Ref(0L)
  fn charge(amount : Int32) -> Unit raise RuntimeError {
    let next = used.val + amount.to_int64()
    guard lease.reservation.resize(next) else {
      raise ResourceLimitExceeded(CompilationUnits)
    }
    used.val = next
  }
  fn expression(
    body : ReadOnlyArray[@types.Instruction],
  ) -> Unit raise RuntimeError {
    let stack : Array[(ReadOnlyArray[@types.Instruction], Int32, Int32)] = [
      (body, 0, 0),
    ]
    while stack.pop() is Some((body, index, depth)) {
      if index >= body.length() {
        continue
      }
      charge(1)
      stack.push((body, index + 1, depth))
      fn child(
        body : ReadOnlyArray[@types.Instruction],
      ) -> Unit raise RuntimeError {
        guard depth < self.compilation_depth else {
          raise ResourceLimitExceeded(CompilationUnits)
        }
        stack.push((body, 0, depth + 1))
      }
      match body[index] {
        Block(_, body) | Loop(_, body) => child(body)
        If(_, yes, no) => {
          child(no)
          child(yes)
        }
        TryTable(_, handlers, body) => {
          charge(handlers.length())
          child(body)
        }
        BrTable(labels, _) => charge(labels.length())
        SelectTyped(types) => charge(types.length())
        Resume(_, handlers)
        | ResumeThrow(_, _, handlers)
        | ResumeThrowRef(_, handlers) => charge(handlers.length())
        _ => ()
      }
    }
  }
  charge(module_.types.length())
  charge(module_.type_rec_groups.length())
  charge(module_.imports.length())
  charge(module_.funcs.length())
  charge(module_.tables.length())
  charge(module_.memories.length())
  charge(module_.globals.length())
  charge(module_.exports.length())
  charge(module_.elems.length())
  charge(module_.codes.length())
  charge(module_.datas.length())
  charge(module_.tags.length())
  charge(module_.func_names.length())
  for type_ in module_.types {
    charge(type_.supertypes.length())
    match type_.composite {
      Func(type_) => {
        charge(type_.params.length())
        charge(type_.results.length())
      }
      Struct(type_) => charge(type_.fields.length())
      Array(_) | Cont(_) => charge(1)
    }
  }
  for import_ in module_.imports {
    charge(import_.mod_name.length())
    charge(import_.name.length())
  }
  for export_ in module_.exports {
    charge(export_.name.length())
  }
  for _, name in module_.func_names {
    charge(name.length())
  }
  for table in module_.tables {
    if table.init is Some(body) {
      expression(body)
    }
  }
  for global in module_.globals {
    expression(global.init)
  }
  for element in module_.elems {
    if element.mode is Active(_, offset) {
      expression(offset)
    }
    charge(element.init.length())
    for body in element.init {
      expression(body)
    }
  }
  for data in module_.datas {
    charge(data.init.length())
    expression(data.offset)
  }
  for code in module_.codes {
    charge(code.locals.length())
    expression(code.body)
  }
  lease
}

///|
pub fn Store::check_instance_admission(
  self : Store,
  admission : InstanceAdmission,
  validated? : @validator.ValidatedModule,
) -> Unit raise RuntimeError {
  self.ensure_open()
  guard physical_equal(admission.owner, self.closed) else { raise ForeignStore }
  guard !admission.consumed.val else { raise InvalidHandle }
  if validated is Some(validated) && self.resource_budget is Some(budget) {
    if admission.validated is Some(accepted) &&
      physical_equal(accepted, validated) {
      return
    }
    // A token for a smaller module cannot authorize a different validated AST.
    // Return this token's provisional work charge before replacing it.
    if admission.compilation is Some(previous) {
      previous.release()
    }
    admission.compilation = None
    admission.compilation = Some(budget.admit_module(validated.snapshot()))
    admission.validated = Some(validated)
  }
}

///|
/// Bound binary decoding before expanded locals/type vectors are allocated.
/// The returned AST belongs to the caller; its lifetime is not charged to the
/// Store. Instantiation separately admits and retains its compilation units.
pub fn Store::decode_module(self : Store, bytes : Bytes) -> @types.Module raise {
  self.ensure_open()
  guard self.resource_budget is Some(budget) else {
    return @parser.parse_module(bytes)
  }
  let input = budget.reserve(InputBytes, bytes.length().to_int64())
  defer input.release()
  let work = budget.reserve(CompilationUnits, 0L)
  defer work.release()
  let used = Ref(0L)
  @parser.parse_module(
    bytes,
    limits={
      max_input_bytes: bytes.length().to_int64(),
      max_work: 0x7FFF_FFFF_FFFF_FFFFL,
      max_depth: budget.compilation_depth,
    },
    admit_work=Some(fn(amount) {
      let next = used.val + amount
      if !work.reservation.resize(next) {
        return false
      }
      used.val = next
      true
    }),
  ) catch {
    error if error.is_resource_exhaustion() =>
      raise ResourceLimitExceeded(CompilationUnits)
    error => raise error
  }
}

///|
/// Temporarily admit validation or compilation of a caller-owned AST.
pub fn Store::admit_module_work(
  self : Store,
  module_ : @types.Module,
) -> ResourceLease? raise RuntimeError {
  self.ensure_open()
  self.resource_budget.map(budget => budget.admit_module(module_))
}