lib/dsl/strategy/helpers.ex

# Copyright 2018 - 2022, Mathijs Saey, Vrije Universiteit Brussel

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

defmodule Skitter.DSL.Strategy.Helpers do
  @moduledoc """
  Macros to be used in strategy hooks.

  This module defines various macro "primitives" to be used in `Skitter.DSL.Strategy.defhook/2`.
  The contents of this module are automatically available inside `defhook`.

  The macros defined in this module do not offer new functionality. Instead, they provide
  syntactic sugar over calling existing functions with arguments based on the context passed to
  the strategy hook.
  """

  defmacro __using__(_) do
    quote do
      import Kernel, except: [send: 2]
      import unquote(__MODULE__)
      alias Skitter.{Operation, Worker, Remote}
    end
  end

  @doc """
  Raise a `Skitter.StrategyError`

  The error is automatically annotated with the current context, which is used to retrieve the
  current operation and strategy.
  """
  defmacro error(message) do
    quote do
      raise Skitter.StrategyError,
        message: unquote(message),
        context: context()
    end
  end

  @doc """
  Create a worker using `Skitter.Worker.create_remote/4`.

  This macro creates a remote worker, automatically passing the current context.
  """
  defmacro remote_worker(state, tag, placement \\ nil) do
    quote do
      Skitter.Worker.create_remote(context(), unquote(state), unquote(tag), unquote(placement))
    end
  end

  @doc """
  Create a worker using `Skitter.Worker.create_local/3`.

  This macro creates a local worker, automatically passing the current context.
  """
  defmacro local_worker(state, tag) do
    quote do
      Skitter.Worker.create_local(context(), unquote(state), unquote(tag))
    end
  end

  @doc """
  Send a message to a worker using the Elixir's `Kernel.send/2`.

  `send/2` and `send/3` defined in this module call `Skitter.Worker.send/2` which will send a
  message to a worker, eventually causing its `c:Skitter.Strategy.Operation.process/4` hook to be
  called.

  In contrast, this function uses the built-in `Kernel.send/2` of Elixir, which sends a message to
  a pid. This is useful when you need to use `Kernel.SpecialForms.receive/1` inside a hook.
  """
  defmacro plain_send(pid, message) do
    quote do
      Kernel.send(unquote(pid), unquote(message))
    end
  end

  @doc """
  Send a message to a worker with `Skitter.Worker.send/2`
  """
  defmacro send(worker, message) do
    quote(do: Skitter.Worker.send(unquote(worker), unquote(message)))
  end

  @doc """
  Emit data for the current context.

  Uses `Skitter.Strategy.Operation.emit/2`
  """
  defmacro emit(emit) do
    quote(do: Skitter.Strategy.Operation.emit(context(), unquote(emit)))
  end

  @doc """
  Stop the given worker using `Skitter.Worker.stop/1`
  """
  defmacro stop_worker(worker) do
    quote(do: Skitter.Worker.stop(unquote(worker)))
  end

  @doc """
  Stop the current worker using `Skitter.Worker.stop/1`
  """
  defmacro stop_worker do
    quote(do: Skitter.Worker.stop(self()))
  end

  @doc """
  Call `callback` of the current operation with `state`, `config` and `args`.

  Uses `Skitter.Operation.call/5`.
  """
  defmacro call(callback, state, config, args) do
    quote do
      Skitter.Operation.call(
        operation(),
        unquote(callback),
        unquote(state),
        unquote(config),
        unquote(args)
      )
    end
  end

  @doc """
  Call `callback` of the current operation with `args` and `config`.

  Uses `Skitter.Operation.call/4`.
  """
  defmacro call(callback, config, args) do
    quote do
      Skitter.Operation.call(operation(), unquote(callback), unquote(config), unquote(args))
    end
  end

  @doc """
  Call `callback` of the current operation with `args`.

  Uses `Skitter.Operation.call/3`.
  """
  defmacro call(callback, args) do
    quote(do: Skitter.Operation.call(operation(), unquote(callback), unquote(args)))
  end

  @doc """
  Call `callback` of the current operation with `args`.

  Uses `Skitter.Operation.call/2`.
  """
  defmacro call(callback) do
    quote(do: Skitter.Operation.call(operation(), unquote(callback)))
  end

  @doc """
  Call `callback` of the current operation if it exists.

  Uses `Skitter.Operation.call_if_exists/5`.
  """
  defmacro call_if_exists(callback, state, config, args) do
    quote do
      Skitter.Operation.call_if_exists(
        operation(),
        unquote(callback),
        unquote(state),
        unquote(config),
        unquote(args)
      )
    end
  end

  @doc """
  Call `callback` of the current operation if it exists.

  Uses `Skitter.Operation.call_if_exists/4`.
  """
  defmacro call_if_exists(callback, config, args) do
    quote do
      Skitter.Operation.call_if_exists(
        operation(),
        unquote(callback),
        unquote(config),
        unquote(args)
      )
    end
  end

  @doc """
  Call `callback` of the current operation if it exists.

  Uses `Skitter.Operation.call_if_exists/3`.
  """
  defmacro call_if_exists(callback, args) do
    quote do
      Skitter.Operation.call_if_exists(operation(), unquote(callback), unquote(args))
    end
  end

  @doc """
  Call `callback` of the current operation if it exists.

  Uses `Skitter.Operation.call_if_exists/2`.
  """
  defmacro call_if_exists(callback) do
    quote do
      Skitter.Operation.call_if_exists(operation(), unquote(callback))
    end
  end

  @doc """
  Get the name of the in port with the given `index`.

  Calls `Skitter.Operation.index_to_in_port/2`.
  """
  defmacro index_to_in_port(index) do
    quote do
      Skitter.Operation.index_to_in_port(operation(), unquote(index))
    end
  end

  @doc """
  Get the name of the out port with the given `index`.

  Calls `Skitter.Operation.index_to_out_port/2`.
  """
  defmacro index_to_out_port(index) do
    quote do
      Skitter.Operation.index_to_out_port(operation(), unquote(index))
    end
  end

  @doc """
  Get the index of the given in `port`.

  Calls `Skitter.Operation.in_port_to_index/2`.
  """
  defmacro in_port_to_index(port) do
    quote do
      Skitter.Operation.in_port_to_index(operation(), unquote(port))
    end
  end

  @doc """
  Get the index of the given out `port`.

  Calls `Skitter.Operation.out_port_to_index/2`.
  """
  defmacro out_port_to_index(port) do
    quote do
      Skitter.Operation.out_port_to_index(operation(), unquote(port))
    end
  end

  @doc """
  Programmatically create output for the out port with `index`.

  The data must be wrapped in a list.
  """
  defmacro to_port(index, list) do
    quote do
      [{index_to_out_port(unquote(index)), unquote(list)}]
    end
  end

  @doc """
  Programmatically create output for all out ports.

  The data must be wrapped in a list.
  """
  defmacro to_all_ports(list) do
    quote do
      operation()
      |> Skitter.Operation.out_ports()
      |> Enum.map(&{&1, unquote(list)})
    end
  end

  @doc """
  Create an empty state for the operation.

  This macro creates an initial state for an operation, by using
  `Skitter.Operation.initial_state/1`
  """
  defmacro initial_state do
    quote do
      Skitter.Operation.initial_state(operation())
    end
  end
end