lib/type_check/type.ex

defmodule TypeCheck.Type do
  @moduledoc """
  TODO
  """
  import TypeCheck.Internals.Bootstrap.Macros

  if_recompiling? do
    use TypeCheck
  end

  @typedoc """
  Something is a TypeCheck.Type if it implements the TypeCheck.Protocols.ToCheck protocol.

  It is also expected to implement the TypeCheck.Protocols.Inspect protocol (although that has an `Any` fallback).

  In practice, this type means 'any of the' structs in the `TypeCheck.Builtin.*` modules.
  """
  if_recompiling? do
    import TypeCheck.Type.StreamData

    @type! t() ::
             (x :: any() when TypeCheck.Type.type?(x))
             |> wrap_with_gen(&TypeCheck.Type.StreamData.arbitrary_type_gen/0)
  else
    @type t() :: any()
  end

  # To allow types to refer to this type
  # @doc false
  # def t do
  #   import TypeCheck.Builtin
  #   any()
  # end

  @typedoc """
  Indicates that we expect a 'type AST' that will be expanded
  to a proper type. This means that it might contain essentially the full syntax that Elixir Typespecs
  allow, which will be rewritten to calls to the functions in `TypeCheck.Builtin`.

  See `TypeCheck.Builtin` for the precise syntax you are allowed to use.
  """
  @type expandable_type() :: any()

  @doc """
  Constructs a concrete type from the given `type_ast`.

  This means that you can pass type-syntax to this macro,
  which will be transformed into explicit calls to the functions in `TypeCheck.Builtin`.

      iex> res = TypeCheck.Type.build(:ok | :error)
      iex> res
      #TypeCheck.Type< :ok | :error >
      iex> # This is the same as:
      iex> import TypeCheck.Builtin, only: [one_of: 2, literal: 1]
      iex> explicit = one_of(literal(:ok), literal(:error))
      iex> res == explicit
      true

      iex> res = TypeCheck.Type.build({a :: number(), b :: number()} when a <= b)
      iex> res
      #TypeCheck.Type< ({a :: number(), b :: number()} when a <= b) >
      iex> # This is the same as:
      iex> import TypeCheck.Builtin, only: [fixed_tuple: 1, number: 0, guarded_by: 2, named_type: 2]
      iex> explicit = guarded_by(fixed_tuple([named_type(:a, number()), named_type(:b, number())]), quote do a <= b end)
      iex> explicit
      #TypeCheck.Type< ({a :: number(), b :: number()} when a <= b) >

  Of course, you can refer to your own local and remote types as well.
  """
  defmacro build(type_ast, options \\ TypeCheck.Options.new()) do
    options = TypeCheck.Options.new(options)

    type_ast
    |> build_unescaped(__CALLER__, options)
    |> Macro.escape(unquote: true)
  end

  @doc false
  # Building block of macros that take an unexpanded type-AST as input.
  #
  # Transforms `type_ast` (which is expected to be a quoted Elixir AST) into a type value.
  # The result is _not_ escaped
  # assuming that you'd want to do further compile-time work with the type.
  def build_unescaped(type_ast, caller, typecheck_options, add_typecheck_module \\ false) do
    type_ast = TypeCheck.Internals.PreExpander.rewrite(type_ast, caller, typecheck_options)

    code =
      if add_typecheck_module do
        compile_time_imports_module_name =
          Module.concat(TypeCheck.Internals.UserTypes, caller.module)

        quote generated: true, location: :keep do
          import unquote(compile_time_imports_module_name)
          unquote(type_ast)
        end
      else
        type_ast
      end

    {type, []} = Code.eval_quoted(code, [], caller)
    type
  end

  def type?(possibly_a_type) do
    TypeCheck.Protocols.ToCheck.impl_for(possibly_a_type) != nil
  end

  @doc false
  def ensure_type!(possibly_a_type) do
    case TypeCheck.Protocols.ToCheck.impl_for(possibly_a_type) do
      nil ->
        raise TypeCheck.CompileError, """
        Invalid value passed to a function expecting a type!
        `#{inspect(possibly_a_type)}` is not a valid TypeCheck type.
        You probably tried to use a TypeCheck type as a function directly.

        Instead, either implement named types using the `type`, `typep`, `opaque` macros,
        or use TypeCheck.Type.build/1 to construct a one-off type.

        Both of these will perform the necessary conversions to turn 'normal' datatypes to types.

        (If you _really_ want to build a type manually,
        be sure to use `TypeCheck.Builtin.literal/1`
        to turn a literal value into a type.)
        """

      _other ->
        :ok
    end
  end
end