-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.