lib/spex/bisimilarity_checker.ex

defmodule Spex.BisimilarityChecker do
  @moduledoc """
  Compares implementation models against specifications via branching bisimilarity.
  """

  defmodule MercWrapper do
    @moduledoc """
    Rust NIF wrapper used to execute bisimilarity checks.
    """

    use Rustler, otp_app: :spex, crate: "spex_merc_wrapper"

    @doc """
    Compares implementation and specification LTS data for branching bisimilarity.
    """
    @spec compare_bisimilarity(impl_data, spec_data) :: boolean()
          when impl_data: {[Spex.transition()], Spex.state()},
               spec_data: {[Spex.state()], [Spex.action()], [Spex.transition()], Spex.state()}
    def compare_bisimilarity(impl_data, spec_data)
    def compare_bisimilarity(_, _), do: :erlang.nif_error(:nif_not_loaded)
  end

  @doc """
  Compares an ImplModel against a Specification using bisimilarity checking.

  Returns `true` if the implementation model is bisimilar to the specification,
  `false` otherwise.

  ## Parameters

  - `impl_model`: A `%Spex.ImplModel{}` struct containing observed transitions
  - `specification`: A module implementing the `Spex.Specification` behaviour

  ## Examples

      iex> impl_model = %Spex.ImplModel{...}
      iex> Spex.BisimilarityChecker.bisimilar_to_specification?(impl_model, MySpecification)
      true
  """
  @spec bisimilar_to_specification?(Spex.ImplModel.t()) :: boolean()
  def bisimilar_to_specification?(%Spex.ImplModel{specification: specification} = impl_model) do
    # Extract data from Specification module
    spec_states = specification.states()
    spec_actions = specification.actions()
    spec_transitions = specification.transitions()
    spec_initial_state = specification.initial_state()

    spec_data = {spec_states, spec_actions, spec_transitions, spec_initial_state}

    # Extract data from ImplModel
    impl_transitions = impl_model.transitions |> MapSet.to_list()

    {initialisation_transitions, impl_transitions_proper} =
      Enum.split_with(impl_transitions, fn
        {nil, :__initialisation__, _} -> true
        _ -> false
      end)

    case initialisation_transitions do
      [{nil, :__initialisation__, impl_initial_state}] ->
        impl_data = {impl_transitions_proper, impl_initial_state}

        # Call the Rust NIF
        MercWrapper.compare_bisimilarity(impl_data, spec_data)

      _ ->
        false
    end
  end
end