lib/zig/type/integer.ex

defmodule Zig.Type.Integer do
  use Zig.Type, inspect?: true

  defstruct [:signedness, :bits]

  @type t :: %__MODULE__{
          signedness: :unsigned | :signed,
          bits: 0..65_535 | :big
        }

  def parse("u" <> number_str = t) do
    %__MODULE__{signedness: :unsigned, bits: get_bits!(number_str, t)}
  end

  def parse("i" <> number_str = t) do
    %__MODULE__{signedness: :signed, bits: get_bits!(number_str, t)}
  end

  def parse(other), do: raise(Zig.Type.ParseError, source: other)

  @signs %{"signed" => :signed, "unsigned" => :unsigned}
  def from_json(%{"signedness" => s, "bits" => b}) do
    %__MODULE__{signedness: Map.fetch!(@signs, s), bits: b}
  end

  def get_bits!(number_str, full_type) do
    case Integer.parse(number_str) do
      {n, ""} when n in 0..65_535 ->
        n

      {n, ""} ->
        raise Zig.Type.ParseError,
          source: full_type,
          reason: "#{n} is out of range of zig bit lengths"

      _ ->
        raise Zig.Type.ParseError, source: full_type
    end
  end

  def to_string(integer) do
    case integer.signedness do
      :unsigned -> "u#{integer.bits}"
      :signed -> "i#{integer.bits}"
    end
  end

  def to_call(integer), do: to_string(integer)

  def inspect(type, _opts) do
    import Inspect.Algebra
    concat(["~t(", to_string(type), ")"])
  end

  def marshals_param?(%{bits: bits}), do: bits > 64
  def marshals_return?(%{bits: bits}), do: bits > 64

  def marshal_param(type, variable, index, :elixir) do
    marshal_param_elixir(type, variable, index)
  end

  def marshal_param(type, variable, index, :erlang) do
    marshal_param_erlang(type, variable, index)
  end

  defp marshal_param_elixir(type, variable, index) do
    max = typemax(type)
    min = typemin(type)

    guards =
      quote bind_quoted: [
              variable: variable,
              name: to_string(type),
              index: index,
              max: max,
              min: min
            ] do
        unless is_integer(variable) do
          :erlang.error(
            {:argument_error, index,
             [{"expected: integer (for `#{name}`)"}, {"got: `#{inspect(variable)}`"}]}
          )
        end

        unless variable >= min do
          :erlang.error(
            {:argument_error, index,
             [
               {"expected: integer (for `#{name}`)"},
               {"got: `#{inspect(variable)}`"},
               {"note: out of bounds (#{min}..#{max})"}
             ]}
          )
        end

        unless variable <= max do
          :erlang.error(
            {:argument_error, index,
             [
               {"expected: integer (for `#{name}`)"},
               {"got: `#{inspect(variable)}`"},
               {"note: out of bounds (#{min}..#{max})"}
             ]}
          )
        end
      end

    size = _next_power_of_two_ceil(type.bits)

    case type.signedness do
      :unsigned ->
        quote do
          unquote(guards)
          unquote(variable) = <<unquote(variable)::unsigned-integer-size(unquote(size))-native>>
        end

      :signed ->
        quote do
          unquote(guards)
          unquote(variable) = <<unquote(variable)::signed-integer-size(unquote(size))-native>>
        end
    end
  end

  defp marshal_param_erlang(type, variable, _index) do
    max = typemax(type)
    min = typemin(type)

    size = _next_power_of_two_ceil(type.bits)

    """
    #{variable}_m = case #{variable} of
      X when not is_integer(X) ->
        erlang:error(badarg);
      X when X < #{min} ->
        erlang:error(badarg);
      X when X > #{max} ->
        erlang:error(badarg);
      X -> <<X:#{size}/native-#{type.signedness}-integer-unit:1>>
    end,
    """
  end

  def marshal_return(type, variable, :elixir) do
    marshal_return_elixir(type, variable)
  end

  def marshal_return(type, variable, :erlang) do
    marshal_return_erlang(type, variable)
  end

  defp marshal_return_elixir(type, variable) do
    size = _next_power_of_two_ceil(type.bits)

    case type.signedness do
      :unsigned ->
        quote do
          <<result::unsigned-integer-size(unquote(size))-native>> = unquote(variable)
          result
        end

      :signed ->
        quote do
          <<result::signed-integer-size(unquote(size))-native>> = unquote(variable)
          result
        end
    end
  end

  defp marshal_return_erlang(type, variable) do
    size = _next_power_of_two_ceil(type.bits)

    """
    <<NumberResult:#{size}/native-#{type.signedness}-integer>> = #{variable},
    NumberResult
    """
  end

  def _next_power_of_two_ceil(bits), do: _next_power_of_two_ceil(bits, 1, true)

  defp _next_power_of_two_ceil(bits, so_far, all_zeros) do
    import Bitwise
    shifted = bits >>> 1

    case {shifted, all_zeros} do
      {1, true} ->
        1 <<< so_far

      {1, false} ->
        2 <<< so_far

      _ ->
        all_zeros = all_zeros && (bits &&& 1) == 0
        _next_power_of_two_ceil(shifted, so_far + 1, all_zeros)
    end
  end

  def spec(%{bits: 0}, _, _), do: 0

  def spec(%{signedness: :unsigned} = type, _, _opts) do
    quote context: Elixir do
      0..unquote(typemax(type))
    end
  end

  def spec(%{signedness: :signed} = type, _, _opts) do
    neg_typemin = -typemin(type)

    quote context: Elixir do
      -unquote(neg_typemin)..unquote(typemax(type))
    end
  end

  # note that we're currently not using this function for unsigned ints, which we know is zero
  defp typemin(%{signedness: :signed, bits: 0}), do: 0
  defp typemin(%{signedness: :signed, bits: bits}), do: -Bitwise.<<<(1, bits - 1)
  defp typemin(%{signedness: :unsigned}), do: 0

  defp typemax(%{bits: 0}), do: 0
  defp typemax(%{signedness: :unsigned, bits: bits}), do: Bitwise.<<<(1, bits) - 1
  defp typemax(%{signedness: :signed, bits: 1}), do: 0
  defp typemax(%{signedness: :signed, bits: bits}), do: Bitwise.<<<(1, bits - 1) - 1

  def return_allowed?(_), do: true
end