defmodule CPSolver.Variable do defstruct [:id, :name, :domain, :space] @type t :: %__MODULE__{ id: reference(), name: String.t(), domain: Domain.t() } alias CPSolver.Variable alias CPSolver.Store.Registry, as: Store alias CPSolver.Utils alias CPSolver.DefaultDomain, as: Domain require Logger @callback new(values :: Enum.t(), opts :: Keyword.t()) :: Variable.t() defmacro __using__(_) do quote do @behaviour CPSolver.Variable def new(values, opts \\ default_opts()) do %Variable{ id: make_ref(), domain: Domain.new(values) } end defp default_opts() do [domain_impl: CPSolver.DefaultDomain] end defoverridable new: 2 end end def domain(variable) do store_op(:domain, variable) end def size(variable) do store_op(:size, variable) end def fixed?(%{domain: domain} = variable) do Domain.fixed?(domain) || store_op(:fixed?, variable) end def fixed?(variable) do store_op(:fixed?, variable) end def min(variable) do store_op(:min, variable) end def max(variable) do store_op(:max, variable) end def contains?(variable, value) do store_op(:contains?, variable, value) end def remove(variable, value) do store_op(:remove, variable, value) end def removeAbove(variable, value) do store_op(:removeAbove, variable, value) end def removeBelow(variable, value) do store_op(:removeBelow, variable, value) end def fix(variable, value) do store_op(:fix, variable, value) end defp store_op(op, variable, value) when op in [:remove, :removeAbove, :removeBelow, :fix] do Store.update(variable.space, variable, op, [value]) end defp store_op(op, variable, value) when op in [:contains?] do Store.get(variable.space, variable, op, [value]) end defp store_op(op, variable) when op in [:size, :fixed?, :min, :max] do Store.get(variable.space, variable, op) end defp store_op(:domain, variable) do Store.domain(variable.space, variable) end def subscribers(variable) do Utils.subscribers({:variable, variable.id}) end def bind_variables(space, variables) do Enum.map(variables, fn var -> bind(var, space) end) end def bind(variable, space) do Map.put(variable, :space, space) end def set_store(variable, store) do Map.put(variable, :store, store) end end defmodule CPSolver.Variable.Agent do alias CPSolver.DefaultDomain, as: Domain alias CPSolver.Store.Registry, as: StoreRegistry alias CPSolver.Utils require Logger @behaviour GenServer def create(space, %{id: id} = variable) do {:ok, _} = Registry.register(StoreRegistry, id, space) {:ok, _pid} = GenServer.start_link(__MODULE__, variable, name: StoreRegistry.variable_proc_id(variable)) end def stop(variable) do GenServer.stop(StoreRegistry.variable_proc_id(variable)) end @impl true @spec init(atom | %{:domain => any, optional(any) => any}) :: {:ok, :gb_sets.set(any)} def init(variable) do {:ok, Domain.new(variable.domain)} end def operation(var, operation, args \\ []) do GenServer.call(StoreRegistry.variable_proc_id(var), {var, operation, args}) end @impl true def handle_call({var, op, _args}, _from, :fail) do handle_op_on_failed_var(var, op) {:reply, :fail, :fail} end ## Read ops def handle_call({_var, :domain, _args}, _from, domain) do {:reply, domain, domain} end def handle_call({_var, op, args}, _from, domain) when op in [:size, :fixed?, :min, :max, :contains?] do {:reply, apply(Domain, op, [domain | args]), domain} end ## Write ops def handle_call({var, op, args}, _from, domain) when op in [:remove, :removeAbove, :removeBelow, :fix] do {result, updated_domain} = case apply(Domain, op, [domain | args]) do :fail -> {:fail, :fail} |> tap(fn _ -> handle_failure(var) end) :none -> {:no_change, domain} |> tap(fn _ -> handle_domain_no_change(var) end) {domain_change, new_domain} -> {domain_change, new_domain} |> tap(fn _ -> handle_domain_change(domain_change, var, new_domain) end) end {:reply, result, updated_domain} end defp handle_op_on_failed_var(var, operation) do Logger.warning( "Attempt to request #{inspect(operation)} on failed variable #{inspect(var.id)}" ) end defp handle_failure(var) do Logger.debug("Failure for variable #{inspect(var.id)}") ## TODO: notify space (and maybe don't notify propagators) publish(var, {:fail, var.id}) end defp handle_domain_no_change(_var) do :ok end defp handle_domain_change(domain_change, var, _domain) do publish(var, {domain_change, var.id}) |> tap(fn _ -> Logger.debug("Domain change (#{domain_change}) for #{inspect(var.id)}") end) end defp publish(var, message) do Utils.publish({:variable, var.id}, message) end end