defmodule Spex.ImplModel do
@moduledoc """
Represents the observed implementation model built from runtime transitions.
"""
alias Spex.BisimilarityChecker
@type t :: %__MODULE__{
specification: Spex.Specification.t(),
transitions: MapSet.t(Spex.transition()),
learning_mode?: boolean()
}
@type serialisation :: String.t()
defstruct [:specification, :transitions, :learning_mode?]
@doc """
Creates an empty implementation model for a specification in learning mode.
"""
@spec initialise(Spex.Specification.t()) :: t()
def initialise(specification) do
%__MODULE__{
specification: specification,
transitions: MapSet.new(),
learning_mode?: true
}
end
@type observation_status ::
:ok
| :deviation_still_bisimilar
| :deviation_not_bisimilar
@doc """
Observes a transition and returns its status and resulting model.
In learning mode, the transition is added. Outside learning mode, the model
is checked for bisimilarity impact without mutating stored transitions.
"""
@spec observe_transition(t(), Spex.transition()) :: {observation_status(), t()}
def observe_transition(impl_model, transition)
def observe_transition(
%__MODULE__{transitions: transitions, learning_mode?: true} = impl_model,
transition
) do
{:ok, %{impl_model | transitions: MapSet.put(transitions, transition)}}
end
def observe_transition(
%__MODULE__{transitions: transitions, learning_mode?: false} =
impl_model,
transition
) do
observation_status =
cond do
MapSet.member?(transitions, transition) ->
:ok
%{impl_model | transitions: MapSet.put(transitions, transition)}
|> BisimilarityChecker.bisimilar_to_specification?() ->
:deviation_still_bisimilar
true ->
:deviation_not_bisimilar
end
{observation_status, impl_model}
end
@doc """
Serialises an implementation model into `.spex` text format.
"""
@spec serialise(t()) :: serialisation()
def serialise(impl_model) do
transitions =
Enum.map_join(impl_model.transitions, "\n", fn {from_state, action, to_state} ->
"#{Atom.to_string(from_state)} --[#{Atom.to_string(action)}]-> #{Atom.to_string(to_state)}"
end)
"""
Specification: #{impl_model.specification}
Learning mode: #{impl_model.learning_mode?}
Transitions:
#{transitions}
"""
end
@doc """
Deserialises `.spex` content into an implementation model.
"""
@spec deserialise(serialisation()) :: {:ok, t()} | {:error, Spex.Errors.ImplModelError.t()}
def deserialise(serialisation) do
[spec_line, learning_mode_line, _transitions_heading | transition_lines] =
String.split(serialisation, "\n")
specification = spec_line |> String.replace("Specification: ", "") |> String.to_atom()
learning_mode? = learning_mode_line |> String.replace("Learning mode: ", "") == "true"
transitions =
transition_lines
|> Enum.reject(&(&1 == ""))
|> Enum.map(fn line ->
[from_state, action, to_state] =
Regex.run(~r/^(.+) --\[(.+)\]-> (.+)$/, line, capture: :all_but_first)
{String.to_atom(from_state), String.to_atom(action), String.to_atom(to_state)}
end)
|> MapSet.new()
{:ok,
%__MODULE__{
specification: specification,
learning_mode?: learning_mode?,
transitions: transitions
}}
rescue
e ->
{:error,
%Spex.Errors.ImplModelError{reason: :deserialisation_failed, context: %{original_error: e}}}
end
end