///|
fn decode_at(
  schema : Schema,
  ty : Type,
  r : Reader,
  budget : Budget,
  depth : Int,
) -> Value raise XdrError {
  budget.visit(depth, r.position())
  match ty {
    TRef(name) => decode_at(schema, schema.lookup(name), r, budget, depth + 1)
    TVoid => VVoid
    TInt => VInt(r.read_int())
    TUInt => VUInt(r.read_uint())
    THyper => VHyper(r.read_hyper())
    TUHyper => VUHyper(r.read_uhyper())
    TBool => VBool(r.read_bool())
    TFloat => VFloat(r.read_float())
    TDouble => VDouble(r.read_double())
    TString(bound) => VString(r.read_string(bound~))
    TOpaque(bound, variable) =>
      VOpaque(
        if variable {
          r.read_opaque(bound~)
        } else {
          r.read_fixed(bound)
        },
      )
    TArray(inner, bound, variable) => {
      let n = if variable { r.read_length(bound) } else { bound }
      if n > budget.left {
        raise Invalid(r.position(), "aggregate value item limit")
      }
      let out : Array[Value] = []
      for i = 0; i < n; i = i + 1 {
        out.push(decode_at(schema, inner, r, budget, depth + 1))
      }
      VArray(out)
    }
    TOptional(inner) =>
      if r.read_bool() {
        VOptional(Some(decode_at(schema, inner, r, budget, depth + 1)))
      } else {
        VOptional(None)
      }
    TEnum(labels) => {
      let n = r.read_int()
      validate_tag(TEnum(labels), n)
      VEnum(n)
    }
    TStruct(fields) => {
      let out : Array[(String, Value)] = []
      for field in fields {
        out.push(
          (field.name, decode_at(schema, field.ty, r, budget, depth + 1)),
        )
      }
      VStruct(out)
    }
    TUnion(tag, arms, fallback) => {
      let n = r.read_int()
      validate_tag(schema.resolve(tag), n)
      VUnion(
        n,
        decode_at(schema, union_arm(arms, fallback, n), r, budget, depth + 1),
      )
    }
  }
}

///|
/// Decode exactly one datum; trailing bytes are rejected, not silently discarded.
pub fn decode(
  schema : Schema,
  type_name : String,
  bytes : Array[Byte],
) -> Value raise XdrError {
  let r = Reader::new(bytes, limits=schema.limits)
  let budget : Budget = {
    left: schema.limits.max_items,
    limits: schema.limits,
  }
  let value = decode_at(schema, schema.lookup(type_name), r, budget, 0)
  r.finish()
  value
}