lib/type_check/builtin/maybe_improper_list.ex

defmodule TypeCheck.Builtin.MaybeImproperList do
  defstruct [:element_type, :terminator_type]

  use TypeCheck
  @type! t() :: t(TypeCheck.Type.t(), TypeCheck.Type.t())
  @type! t(element_type, terminator_type) :: %__MODULE__{
           element_type: element_type,
           terminator_type: terminator_type
         }

  @type! problem_tuple ::
           {t(), :not_a_list, %{}, any()}
           | {t(), :element_error,
              %{
                problem: lazy(TypeCheck.TypeError.Formatter.problem_tuple()),
                index: non_neg_integer()
              }, any()}
           | {t(), :terminator_error,
              %{problem: lazy(TypeCheck.TypeError.Formatter.problem_tuple())}, any()}

  defimpl TypeCheck.Protocols.Escape do
    def escape(s) do
      %{
        s
        | element_type: TypeCheck.Protocols.Escape.escape(s.element_type),
          terminator_type: TypeCheck.Protocols.Escape.escape(s.terminator_type)
      }
    end
  end

  defimpl TypeCheck.Protocols.ToCheck do
    def to_check(s, param) do
      quote generated: true, location: :keep do
        case unquote(param) do
          x when not is_list(x) ->
            {:error, {unquote(Macro.escape(s)), :not_a_list, %{}, unquote(param)}}

          _ ->
            unquote(build_element_check(s, param))
        end
      end
    end

    defp build_element_check(s, param) do
      element_check =
        TypeCheck.Protocols.ToCheck.to_check(s.element_type, Macro.var(:single_param, __MODULE__))

      terminator_check =
        TypeCheck.Protocols.ToCheck.to_check(
          s.terminator_type,
          Macro.var(:single_param, __MODULE__)
        )

      quote generated: true, location: :keep do
        orig_param = unquote(param)

        res =
          orig_param
          |> TypeCheck.Builtin.MaybeImproperList.with_index_improper()
          |> TypeCheck.Builtin.MaybeImproperList.reduce_while_improper(
            {:ok, [], []},
            fn {input, index}, {:ok, bindings, altered_param}, terminator? ->
              var!(single_param, unquote(__MODULE__)) = input

              if terminator? do
                case unquote(terminator_check) do
                  {:ok, terminator_bindings, altered_terminator} ->
                    {:ok_terminator, terminator_bindings ++ bindings, altered_param,
                     altered_terminator}

                  {:error, problem} ->
                    {:error,
                     {unquote(Macro.escape(s)), :terminator_error,
                      %{problem: problem, index: index}, orig_param}}
                end
              else
                case unquote(element_check) do
                  {:ok, element_bindings, altered_element} ->
                    {:cont,
                     {:ok, element_bindings ++ bindings, [altered_element | altered_param]}}

                  {:error, problem} ->
                    problem =
                      {:error,
                       {unquote(Macro.escape(s)), :element_error,
                        %{problem: problem, index: index}, orig_param}}

                    {:halt, problem}
                end
              end
            end
          )

        case res do
          {:ok_terminator, bindings, altered_param, altered_terminator} ->
            altered_improper_list = :lists.reverse(altered_param) ++ altered_terminator
            {:ok, bindings, altered_improper_list}

          {:ok, bindings, altered_param} ->
            altered_list = :lists.reverse(altered_param)
            {:ok, bindings, altered_list}

          other ->
            other
        end
      end
    end
  end

  @doc false
  # Similar to Enum.with_index, but works on improper lists
  @typep elem() :: term()
  @typep terminator() :: term()
  @spec with_index_improper(maybe_improper_list(elem(), terminator())) ::
          maybe_improper_list({elem(), non_neg_integer()}, {terminator(), non_neg_integer()})
  def with_index_improper(maybe_improper_list) do
    do_with_index_improper(maybe_improper_list, [], 0)
  end

  defp do_with_index_improper(maybe_improper_list, result, count) do
    case maybe_improper_list do
      [] ->
        :lists.reverse(result)

      [head | tail] ->
        do_with_index_improper(tail, [{head, count} | result], count + 1)

      terminator ->
        rest_list = :lists.reverse(result)
        terminator_with_index = {terminator, count}
        rest_list ++ terminator_with_index
    end
  end

  @doc false
  # Works similarly to `Enum.reduce_while`, but is able to handle improper lists.
  # The reduction fun is given a boolean as third parameter,
  # which is true when the first parameter is the terminator (the non-`[]` at the end of an improper list)
  @typep acc() :: term()
  @typep reduction_fun :: (elem(), acc(), is_terminator :: boolean() -> acc())
  @spec reduce_while_improper(maybe_improper_list(), acc(), reduction_fun()) :: acc()
  def reduce_while_improper(maybe_improper_list, acc, fun) do
    case maybe_improper_list do
      [] ->
        acc

      [head | tail] ->
        case fun.(head, acc, false) do
          {:cont, new_acc} ->
            reduce_while_improper(tail, new_acc, fun)

          {:halt, final_acc} ->
            final_acc
        end

      terminator ->
        fun.(terminator, acc, true)
    end
  end

  def empty?(list) do
    case list do
      [] -> true
      _other -> false
    end
  end

  defimpl TypeCheck.Protocols.Inspect do
    def inspect(list, opts) do
      Inspect.Algebra.container_doc(
        Inspect.Algebra.color("maybe_improper_list(", :builtin_type, opts),
        [
          TypeCheck.Protocols.Inspect.inspect(list.element_type, opts),
          TypeCheck.Protocols.Inspect.inspect(list.terminator_type, opts)
        ],
        Inspect.Algebra.color(")", :builtin_type, opts),
        opts,
        fn x, _ -> x end,
        separator: ",",
        break: :maybe
      )
      |> Inspect.Algebra.color(:builtin_type, opts)
    end
  end

  if Code.ensure_loaded?(StreamData) do
    defimpl TypeCheck.Protocols.ToStreamData do
      def to_gen(s) do
        # require Blocked
        # Blocked.by("https://github.com/whatyouhide/stream_data/issues/169", "StreamData.maybe_improper_list_of is incorrect") do
        element_gen = TypeCheck.Protocols.ToStreamData.to_gen(s.element_type)
        terminator_gen = TypeCheck.Protocols.ToStreamData.to_gen(s.terminator_type)
        gen = StreamData.maybe_improper_list_of(element_gen, terminator_gen)

        StreamData.filter(gen, fn list ->
          case list do
            [_elem] -> false
            _other -> true
          end
        end)

        # else
        #   element_gen =  TypeCheck.Protocols.ToStreamData.to_gen(s.element_type)
        #   terminator_gen = TypeCheck.Protocols.ToStreamData.to_gen(s.terminator_type)
        #   StreamData.maybe_improper_list_of(element_gen, terminator_gen)
        # end
      end
    end
  end
end