# 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" which can be used to define strategies. These
macros are only available inside `Skitter.DSL.Strategy.defhook/2`, where they are automatically
imported.
Internally, these macros call functions defined in various other modules. The information stored
in the `t:Skitter.Strategy.context/0` passed to the hook is used to pass the appropriate
information to these functions.
"""
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.
## Examples
iex> defstrategy Example do
...> defhook example, do: error("An error message")
...> end
iex> Example.example(%Context{strategy: Example, operation: Foo})
** (Skitter.StrategyError) Raised by Skitter.DSL.Strategy.HelpersTest.Example handling Foo:
An error message
"""
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, role, placement \\ nil) do
quote do
Skitter.Worker.create_remote(context(), unquote(state), unquote(role), 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, role) do
quote do
Skitter.Worker.create_local(context(), unquote(state), unquote(role))
end
end
@doc """
Send a message to a worker using 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 """
Create an empty state for the operation.
This macro creates an initial state for an operation, by using
`Skitter.Operation.initial_state/2`
## Examples
iex> defoperation ExampleOperation do
...> initial_state :initial_state
...> end
iex> defstrategy ExampleStrategy do
...> defhook example(), do: initial_state()
...> end
iex> ExampleStrategy.example(%Context{operation: ExampleOperation})
:initial_state
"""
defmacro initial_state(config \\ nil) do
quote do
Skitter.Operation.initial_state(operation(), unquote(config))
end
end
@doc """
Call `callback` of the current operation.
Uses `Skitter.Operation.call/5`. The state, configuration and arguments can be passed through
opts. If they are not provided, `:args` defaults to the empty list while `:state` and `:config`
default to `nil`.
## Examples
iex> defoperation ExampleOperation do
...> defcb example(arg), do: {arg, state(), config()}
...> end
iex> defstrategy ExampleStrategy do
...> defhook empty_example(), do: call(:example).result
...> defhook arg_example(), do: call(:example, args: [:foo]).result
...> defhook state_example(), do: call(:example, args: [nil], state: :foo).result
...> defhook config_example(), do: call(:example, args: [nil], config: :foo).result
...> defhook all_example(), do: call(:example, args: [:foo], state: :foo, config: :foo).result
...> end
iex> ExampleStrategy.arg_example(%Context{operation: ExampleOperation})
{:foo, nil, nil}
iex> ExampleStrategy.state_example(%Context{operation: ExampleOperation})
{nil, :foo, nil}
iex> ExampleStrategy.config_example(%Context{operation: ExampleOperation})
{nil, nil, :foo}
iex> ExampleStrategy.all_example(%Context{operation: ExampleOperation})
{:foo, :foo, :foo}
iex> ExampleStrategy.empty_example(%Context{operation: ExampleOperation})
** (UndefinedFunctionError) function Skitter.DSL.Strategy.HelpersTest.ExampleOperation.example/2 is undefined or private
"""
defmacro call(callback, opts \\ []) do
quote do
Skitter.Operation.call(
operation(),
unquote(callback),
unquote(Keyword.get(opts, :state, nil)),
unquote(Keyword.get(opts, :config, nil)),
unquote(Keyword.get(opts, :args, []))
)
end
end
@doc """
Call `callback` of the current operation if it exists.
Uses `Skitter.Operation.call_if_exists/5`. The state, configuration and arguments can be passed
through opts (as in `call/3`). If they are not provided, `:args` defaults to the empty list
while `:state` and `:config` default to `nil`.
## Examples
iex> defoperation ExampleOperation, out: port do
...> defcb exists do
...> state <~ :exists
...> :exists ~> port
...> :exists
...> end
...> end
iex> defstrategy ExampleStrategy do
...> defhook exists_example(), do: call_if_exists(:exists)
...> defhook does_not_exist_example(), do: call_if_exists(:does_not_exist)
...> end
iex> ExampleStrategy.exists_example(%Context{operation: ExampleOperation})
%Result{result: :exists, state: :exists, emit: [port: [:exists]]}
iex> ExampleStrategy.does_not_exist_example(%Context{operation: ExampleOperation})
%Result{result: nil, state: nil, emit: []}
"""
defmacro call_if_exists(callback, opts \\ []) do
quote do
Skitter.Operation.call_if_exists(
operation(),
unquote(callback),
unquote(Keyword.get(opts, :state, nil)),
unquote(Keyword.get(opts, :config, nil)),
unquote(Keyword.get(opts, :args, []))
)
end
end
@doc """
Get the name of the in port with the given `index`.
Calls `Skitter.Operation.index_to_in_port/2`.
## Examples
iex> defoperation ExampleOperation, in: [foo, bar] do
...> end
iex> defstrategy ExampleStrategy do
...> defhook example(), do: index_to_in_port(1)
...> end
iex> ExampleStrategy.example(%Context{operation: ExampleOperation})
:bar
"""
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`.
## Examples
iex> defoperation ExampleOperation, out: [foo, bar] do
...> end
iex> defstrategy ExampleStrategy do
...> defhook example(), do: index_to_out_port(0)
...> end
iex> ExampleStrategy.example(%Context{operation: ExampleOperation})
:foo
"""
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`.
## Examples
iex> defoperation ExampleOperation, in: [foo, bar] do
...> end
iex> defstrategy ExampleStrategy do
...> defhook example(), do: in_port_to_index(:bar)
...> end
iex> ExampleStrategy.example(%Context{operation: ExampleOperation})
1
"""
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`.
## Examples
iex> defoperation ExampleOperation, out: [foo, bar] do
...> end
iex> defstrategy ExampleStrategy do
...> defhook example(), do: out_port_to_index(:foo)
...> end
iex> ExampleStrategy.example(%Context{operation: ExampleOperation})
0
"""
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.
## Examples
iex> defoperation ExampleOperation, out: [foo, bar] do
...> end
iex> defstrategy ExampleStrategy do
...> defhook example(), do: to_port(0, [1, 2, 3])
...> end
iex> ExampleStrategy.example(%Context{operation: ExampleOperation})
[foo: [1, 2, 3]]
"""
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.
## Examples
iex> defoperation ExampleOperation, out: [foo, bar] do
...> end
iex> defstrategy ExampleStrategy do
...> defhook example(), do: to_all_ports([1, 2, 3])
...> end
iex> ExampleStrategy.example(%Context{operation: ExampleOperation})
[foo: [1, 2, 3], bar: [1, 2, 3]]
"""
defmacro to_all_ports(list) do
quote do
operation()
|> Skitter.Operation.out_ports()
|> Enum.map(&{&1, unquote(list)})
end
end
end