Packages

Type-safe finite state machine library for Gleam

Current section

Files

Jump to
scamper src scamper@validation.erl
Raw

src/scamper@validation.erl

-module(scamper@validation).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/scamper/validation.gleam").
-export([detect_deadlocks/1, reachable_states/2, validate_config/2]).
-export_type([validation_warning/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(" Configuration validation, deadlock detection, and reachability analysis.\n").
-type validation_warning(IQA) :: {unreachable_state, IQA} |
{transition_from_final_state, IQA} |
{missing_fallback, IQA}.
-file("src/scamper/validation.gleam", 37).
?DOC(" Find non-final states that have no outgoing transitions (deadlock states).\n").
-spec detect_deadlocks(scamper@config:config(IQL, any(), any())) -> list(IQL).
detect_deadlocks(Config) ->
Transitions = scamper@config:get_transitions(Config),
Final_states = scamper@config:get_final_states(Config),
All_to_states = begin
_pipe = Transitions,
_pipe@1 = gleam@list:map(
_pipe,
fun(Rule) -> erlang:element(4, Rule) end
),
gleam@list:unique(_pipe@1)
end,
States_with_outgoing = begin
_pipe@2 = Transitions,
_pipe@3 = gleam@list:map(
_pipe@2,
fun(Rule@1) -> erlang:element(2, Rule@1) end
),
gleam@list:unique(_pipe@3)
end,
_pipe@4 = All_to_states,
gleam@list:filter(
_pipe@4,
fun(S) ->
not gleam@list:contains(Final_states, S) andalso not gleam@list:contains(
States_with_outgoing,
S
)
end
).
-file("src/scamper/validation.gleam", 70).
-spec bfs(
list(IQZ),
list(IQZ),
list(scamper@config:transition_rule(IQZ, any(), any()))
) -> list(IQZ).
bfs(Queue, Visited, Transitions) ->
case Queue of
[] ->
Visited;
[Current | Rest] ->
Neighbors = begin
_pipe = Transitions,
_pipe@1 = gleam@list:filter(
_pipe,
fun(Rule) -> erlang:element(2, Rule) =:= Current end
),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(Rule@1) -> erlang:element(4, Rule@1) end
),
_pipe@3 = gleam@list:unique(_pipe@2),
gleam@list:filter(
_pipe@3,
fun(S) -> not gleam@list:contains(Visited, S) end
)
end,
New_visited = lists:append(Visited, Neighbors),
New_queue = lists:append(Rest, Neighbors),
bfs(New_queue, New_visited, Transitions)
end.
-file("src/scamper/validation.gleam", 62).
?DOC(
" Find all states reachable from the initial state via BFS.\n"
" Ignores guards (assumes all guards could pass).\n"
).
-spec reachable_states(scamper@config:config(IQS, any(), any()), IQS) -> list(IQS).
reachable_states(Config, Initial_state) ->
Transitions = scamper@config:get_transitions(Config),
bfs([Initial_state], [Initial_state], Transitions).
-file("src/scamper/validation.gleam", 95).
-spec check_unreachable(scamper@config:config(IRJ, any(), any()), IRJ) -> list(validation_warning(IRJ)).
check_unreachable(Config, Initial_state) ->
Transitions = scamper@config:get_transitions(Config),
Reachable = reachable_states(Config, Initial_state),
All_states = begin
_pipe = Transitions,
_pipe@1 = gleam@list:flat_map(
_pipe,
fun(Rule) -> [erlang:element(2, Rule), erlang:element(4, Rule)] end
),
gleam@list:unique(_pipe@1)
end,
_pipe@2 = All_states,
_pipe@3 = gleam@list:filter(
_pipe@2,
fun(S) -> not gleam@list:contains(Reachable, S) end
),
gleam@list:map(_pipe@3, fun(S@1) -> {unreachable_state, S@1} end).
-file("src/scamper/validation.gleam", 113).
-spec check_transitions_from_final(scamper@config:config(IRR, any(), any())) -> list(validation_warning(IRR)).
check_transitions_from_final(Config) ->
Transitions = scamper@config:get_transitions(Config),
Final_states = scamper@config:get_final_states(Config),
_pipe = Transitions,
_pipe@1 = gleam@list:filter(
_pipe,
fun(Rule) ->
gleam@list:contains(Final_states, erlang:element(2, Rule))
end
),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(Rule@1) -> erlang:element(2, Rule@1) end
),
_pipe@3 = gleam@list:unique(_pipe@2),
gleam@list:map(_pipe@3, fun(S) -> {transition_from_final_state, S} end).
-file("src/scamper/validation.gleam", 126).
-spec check_missing_fallbacks(scamper@config:config(IRZ, any(), any())) -> list(validation_warning(IRZ)).
check_missing_fallbacks(Config) ->
Transitions = scamper@config:get_transitions(Config),
From_states = begin
_pipe = Transitions,
_pipe@1 = gleam@list:map(
_pipe,
fun(Rule) -> erlang:element(2, Rule) end
),
gleam@list:unique(_pipe@1)
end,
_pipe@2 = From_states,
_pipe@3 = gleam@list:filter(
_pipe@2,
fun(From) ->
Rules = gleam@list:filter(
Transitions,
fun(Rule@1) -> erlang:element(2, Rule@1) =:= From end
),
Has_guarded = gleam@list:any(
Rules,
fun(Rule@2) ->
gleam@option:is_some(erlang:element(5, Rule@2))
end
),
Has_fallback = gleam@list:any(
Rules,
fun(Rule@3) -> erlang:element(5, Rule@3) =:= none end
),
Has_guarded andalso not Has_fallback
end
),
gleam@list:map(_pipe@3, fun(S) -> {missing_fallback, S} end).
-file("src/scamper/validation.gleam", 19).
?DOC(
" Validate a config for common issues.\n"
" Returns Ok(Nil) if no warnings, or Error with a list of warnings.\n"
).
-spec validate_config(scamper@config:config(IQB, any(), any()), IQB) -> {ok,
nil} |
{error, list(validation_warning(IQB))}.
validate_config(Config, Initial_state) ->
Warnings = lists:append(
[check_unreachable(Config, Initial_state),
check_transitions_from_final(Config),
check_missing_fallbacks(Config)]
),
case Warnings of
[] ->
{ok, nil};
_ ->
{error, Warnings}
end.