lib/lockstep/process.ex

defmodule Lockstep.Process do
  @moduledoc """
  Controller-aware wrappers for `Process.whereis/1`, `Process.register/2`,
  `Process.unregister/1`, and `Process.registered/0`.

  These rewrite the per-name registry to be **per-node** rather than
  BEAM-global, so atom names like `:counter` can refer to different
  pids on different simulated cluster nodes. Without this, every name
  lives in a single BEAM-wide table and you get spurious
  `:already_registered` collisions when the same registered name
  legitimately exists on multiple nodes.

  Falls through to BEAM's vanilla `Process.*` calls when no Lockstep
  controller is in scope.

  ## Rewriter integration

  `Lockstep.Rewriter` rewrites bare `Process.whereis/1`,
  `Process.register/2`, `Process.unregister/1`, and `Process.registered/0`
  to the corresponding `Lockstep.Process.*` calls.
  """

  @doc """
  Look up a process registered under `name` on the calling process's
  notional cluster node. Returns the pid or `nil`. Mirrors
  `Process.whereis/1`.
  """
  @spec whereis(atom()) :: pid() | nil
  def whereis(name) when is_atom(name) do
    case Process.get(:lockstep_controller) do
      nil ->
        Process.whereis(name)

      ctl when is_pid(ctl) ->
        case Lockstep.Controller.cluster_whereis_name(ctl, self(), name) do
          :undefined -> nil
          pid -> pid
        end
    end
  end

  @doc """
  Register `pid_or_name` under `name` on the registered process's
  notional node. Mirrors `Process.register/2`.

  Returns `true` on success. Raises `ArgumentError` if the name is
  already registered on that node, or if the pid is already
  registered under another name.
  """
  @spec register(pid() | atom(), atom()) :: true
  def register(pid_or_name, name) when is_atom(name) do
    pid = resolve_pid(pid_or_name)

    case Process.get(:lockstep_controller) do
      nil ->
        Process.register(pid, name)

      ctl when is_pid(ctl) ->
        case Lockstep.Controller.cluster_register_name(ctl, pid, name) do
          :yes ->
            true

          {:already, _existing} ->
            raise ArgumentError,
                  "name #{inspect(name)} already registered on node " <>
                    inspect(Lockstep.Controller.cluster_node_of(ctl, pid))
        end
    end
  end

  @doc """
  Unregister `name` on the calling process's node. Mirrors
  `Process.unregister/1`.

  Returns `true` on success. Raises `ArgumentError` if no such name.
  """
  @spec unregister(atom()) :: true
  def unregister(name) when is_atom(name) do
    case Process.get(:lockstep_controller) do
      nil ->
        Process.unregister(name)

      ctl when is_pid(ctl) ->
        case Lockstep.Controller.cluster_whereis_name(ctl, self(), name) do
          :undefined ->
            raise ArgumentError, "no such name registered: #{inspect(name)}"

          _pid ->
            :ok = Lockstep.Controller.cluster_unregister_name(ctl, self(), name)
            true
        end
    end
  end

  @doc """
  List atom names registered on the calling process's node. Mirrors
  `Process.registered/0`.
  """
  @spec registered() :: [atom()]
  def registered do
    case Process.get(:lockstep_controller) do
      nil ->
        Process.registered()

      ctl when is_pid(ctl) ->
        Lockstep.Controller.cluster_registered_names(ctl, self())
    end
  end

  @doc """
  Set a debug label on the current process. Mirrors OTP 27+'s
  `set_label/1` from the `Process` module but works on older OTPs by
  no-op'ing -- the label is purely cosmetic for `:observer` / debug
  tooling, so it's safe to ignore.
  """
  @spec set_label(any()) :: :ok
  def set_label(label) do
    if function_exported?(Process, :set_label, 1) do
      apply(Process, :set_label, [label])
    end

    :ok
  end

  defp resolve_pid(pid) when is_pid(pid), do: pid

  defp resolve_pid(name) when is_atom(name) do
    case whereis(name) do
      nil -> raise ArgumentError, "no such name registered: #{inspect(name)}"
      pid -> pid
    end
  end
end