lib/util/term_util.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

use Croma

defmodule Antikythera.TermUtil do
  @moduledoc """
  Utils for calculating the actual size of terms.

  These utils traverse terms and accumulate the size of terms including the actual size of binary.
  """

  @doc """
  Returns the actual size of `term` in bytes.
  """
  defun size(term :: term) :: non_neg_integer do
    :erts_debug.flat_size(term) * :erlang.system_info(:wordsize) + total_binary_size(term)
  end

  defunp total_binary_size(term :: term) :: non_neg_integer do
    b when is_bitstring(b) -> byte_size(b)
    t when is_tuple(t) -> total_binary_size_in_list(Tuple.to_list(t))
    l when is_list(l) -> total_binary_size_in_list(l)
    # including non-enumerable structs
    m when is_map(m) -> total_binary_size_in_list(Map.to_list(m))
    _ -> 0
  end

  defunp total_binary_size_in_list(list :: v[[term]]) :: non_neg_integer do
    Enum.reduce(list, 0, fn e, acc -> total_binary_size(e) + acc end)
  end

  @doc """
  Returns whether actual size of `term` exceeds `limit` bytes.

  This is more efficient than deciding by using `size/1` because this function returns immediately after exceeding `limit`, not traverses the entire term.
  """
  defun size_smaller_or_equal?(term :: term, limit :: v[non_neg_integer]) :: boolean do
    case limit - :erts_debug.flat_size(term) * :erlang.system_info(:wordsize) do
      new_limit when new_limit >= 0 -> limit_minus_total_binary_size(term, new_limit) >= 0
      _ -> false
    end
  end

  defunp limit_minus_total_binary_size(term :: term, limit :: v[non_neg_integer]) :: integer do
    case term do
      b when is_bitstring(b) -> limit - byte_size(b)
      t when is_tuple(t) -> reduce_while_positive(Tuple.to_list(t), limit)
      l when is_list(l) -> reduce_while_positive(l, limit)
      # including non-enumerable structs
      m when is_map(m) -> reduce_while_positive(Map.to_list(m), limit)
      _ -> limit
    end
  end

  defunp reduce_while_positive(list :: v[[term]], limit :: v[non_neg_integer]) :: integer do
    Enum.reduce_while(list, limit, fn e, l1 ->
      l2 = limit_minus_total_binary_size(e, l1)
      if l2 < 0, do: {:halt, l2}, else: {:cont, l2}
    end)
  end
end