///|
/// Serialize `value` to an in-memory byte buffer, returning the bytes.
///
/// A fresh session state (string dedup table, reference table) is created
/// for the call, matching the top-level APIs of both existing
/// implementations: ids never leak across calls.
pub fn[T : BinarySerializer] serialize_to_bytes(
  value : T,
  options~ : Options = Options::default(),
) -> Bytes raise DesertFailure {
  let out = ByteOutput::new()
  let ctx : SerializationContext = {
    output: out,
    output_stack: [],
    state: SerializerState::new(),
    options,
  }
  value.serialize(ctx)
  out.to_bytes()
}

///|
/// Serialize `value` into a caller-provided `ByteOutput`.
pub fn[T : BinarySerializer] serialize_to_output(
  value : T,
  out : ByteOutput,
  options~ : Options = Options::default(),
) -> Unit raise DesertFailure {
  let ctx : SerializationContext = {
    output: out,
    output_stack: [],
    state: SerializerState::new(),
    options,
  }
  value.serialize(ctx)
}

///|
/// Compute the serialized size of `value` in bytes (the bytes are written
/// to a scratch output and discarded). Two-pass exact-size serialization
/// can be built on this.
pub fn[T : BinarySerializer] serialized_size(
  value : T,
  options~ : Options = Options::default(),
) -> Int32 raise DesertFailure {
  let out = ByteOutput::new()
  let ctx : SerializationContext = {
    output: out,
    output_stack: [],
    state: SerializerState::new(),
    options,
  }
  value.serialize(ctx)
  out.length()
}

///|
/// Deserialize a value of type `T` from `data`.
///
/// The input must have been produced by a compatible serializer (same
/// options, same schema version). Underruns and format violations raise
/// `DesertFailure`.
pub fn[T : BinaryDeserializer] deserialize(
  data : Bytes,
  options~ : Options = Options::default(),
) -> T raise DesertFailure {
  let ctx : DeserializationContext = {
    input: BinaryInput::new(data),
    state: SerializerState::new(),
    options,
  }
  T::deserialize(ctx)
}

///|
/// Deserialize a value of type `T` from `data`, additionally checking that
/// the whole input was consumed. Trailing bytes fail with
/// `DeserializationFailure`, which catches framing mistakes early.
pub fn[T : BinaryDeserializer] deserialize_exhaustive(
  data : Bytes,
  options~ : Options = Options::default(),
) -> T raise DesertFailure {
  let ctx : DeserializationContext = {
    input: BinaryInput::new(data),
    state: SerializerState::new(),
    options,
  }
  let value = T::deserialize(ctx)
  if !ctx.input.is_empty() {
    raise DeserializationFailure("Trailing bytes after deserialized value")
  }
  value
}