-module(scamper@transition). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/scamper/transition.gleam"). -export([execute/5, can_execute/4, available_events/2]). -export_type([transition_result/3]). -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( " Transition engine for scamper FSMs.\n" "\n" " Handles guard evaluation, callback execution, invariant checking,\n" " and rollback on failure. This is the core algorithmic module.\n" ). -type transition_result(HBV, HBW, HBX) :: {transition_result, HBV, HBX, list(scamper@history:transition_record(HBV, HBW, HBX)), integer()}. -file("src/scamper/transition.gleam", 125). -spec find_matching_rule_loop( list(scamper@config:transition_rule(HEM, HEN, HEO)), HEN, HEO, gleam@option:option(scamper@config:transition_rule(HEM, HEN, HEO)) ) -> gleam@option:option(scamper@config:transition_rule(HEM, HEN, HEO)). find_matching_rule_loop(Rules, Context, Event, Fallback) -> case Rules of [] -> Fallback; [Rule | Rest] -> case erlang:element(5, Rule) of none -> case Fallback of none -> find_matching_rule_loop( Rest, Context, Event, {some, Rule} ); {some, _} -> find_matching_rule_loop( Rest, Context, Event, Fallback ) end; {some, Guard_fn} -> case Guard_fn(Context, Event) of true -> {some, Rule}; false -> find_matching_rule_loop( Rest, Context, Event, Fallback ) end end end. -file("src/scamper/transition.gleam", 117). ?DOC( " Find the first matching rule by evaluating guards top-to-bottom.\n" " Rules with Some(guard) are tested; rules with None (fallback) are collected.\n" " First passing guarded rule wins. If none pass, use fallback if available.\n" ). -spec find_matching_rule( list(scamper@config:transition_rule(HEB, HEC, HED)), HEC, HED ) -> gleam@option:option(scamper@config:transition_rule(HEB, HEC, HED)). find_matching_rule(Rules, Context, Event) -> find_matching_rule_loop(Rules, Context, Event, none). -file("src/scamper/transition.gleam", 241). ?DOC(" Run global state callbacks, then state-specific callback.\n"). -spec run_state_callbacks( HGC, HGD, list(fun((HGC, HGD) -> {ok, HGD} | {error, binary()})), gleam@option:option(fun((HGC, HGD) -> {ok, HGD} | {error, binary()})), scamper@error:callback_stage() ) -> {ok, HGD} | {error, scamper@error:transition_error(HGC, any())}. run_state_callbacks( Target_state, Context, Global_callbacks, State_callback, Stage ) -> gleam@result:'try'( gleam@list:try_fold( Global_callbacks, Context, fun(Ctx, Cb) -> _pipe = Cb(Target_state, Ctx), gleam@result:map_error( _pipe, fun(Reason) -> {callback_failed, Stage, Reason} end ) end ), fun(Ctx@1) -> case State_callback of none -> {ok, Ctx@1}; {some, Cb@1} -> _pipe@1 = Cb@1(Target_state, Ctx@1), gleam@result:map_error( _pipe@1, fun(Reason@1) -> {callback_failed, Stage, Reason@1} end ) end end ). -file("src/scamper/transition.gleam", 270). ?DOC(" Run global transition callbacks, then state-specific transition callback.\n"). -spec run_transition_callbacks( HGP, HGQ, HGP, HGR, list(fun((HGP, HGQ, HGP, HGR) -> {ok, HGR} | {error, binary()})), gleam@option:option(fun((HGP, HGQ, HGP, HGR) -> {ok, HGR} | {error, binary()})), scamper@error:callback_stage() ) -> {ok, HGR} | {error, scamper@error:transition_error(HGP, HGQ)}. run_transition_callbacks( From, Event, To, Context, Global_callbacks, State_callback, Stage ) -> gleam@result:'try'( gleam@list:try_fold( Global_callbacks, Context, fun(Ctx, Cb) -> _pipe = Cb(From, Event, To, Ctx), gleam@result:map_error( _pipe, fun(Reason) -> {callback_failed, Stage, Reason} end ) end ), fun(Ctx@1) -> case State_callback of none -> {ok, Ctx@1}; {some, Cb@1} -> _pipe@1 = Cb@1(From, Event, To, Ctx@1), gleam@result:map_error( _pipe@1, fun(Reason@1) -> {callback_failed, Stage, Reason@1} end ) end end ). -file("src/scamper/transition.gleam", 301). ?DOC(" Find a state-specific callback for a given state.\n"). -spec find_state_callback( list({HHE, fun((HHE, HHF) -> {ok, HHF} | {error, binary()})}), HHE ) -> gleam@option:option(fun((HHE, HHF) -> {ok, HHF} | {error, binary()})). find_state_callback(Callbacks, Target) -> case gleam@list:find( Callbacks, fun(Entry) -> erlang:element(1, Entry) =:= Target end ) of {ok, {_, Cb}} -> {some, Cb}; {error, _} -> none end. -file("src/scamper/transition.gleam", 312). ?DOC(" Find a state-specific transition callback for a given from-state.\n"). -spec find_transition_callback( list({HHM, fun((HHM, HHO, HHM, HHN) -> {ok, HHN} | {error, binary()})}), HHM ) -> gleam@option:option(fun((HHM, HHO, HHM, HHN) -> {ok, HHN} | {error, binary()})). find_transition_callback(Callbacks, Target) -> case gleam@list:find( Callbacks, fun(Entry) -> erlang:element(1, Entry) =:= Target end ) of {ok, {_, Cb}} -> {some, Cb}; {error, _} -> none end. -file("src/scamper/transition.gleam", 203). ?DOC( " Run all callbacks in the guaranteed order:\n" " 1. Global on_exit → 2. State-specific on_exit(from)\n" " 3. Global on_transition → 4. State-specific on_transition(from)\n" " 5. Global on_enter → 6. State-specific on_enter(to)\n" ). -spec run_all_callbacks( HFS, HFT, HFS, HFU, scamper@config:config(HFS, HFU, HFT) ) -> {ok, HFU} | {error, scamper@error:transition_error(HFS, HFT)}. run_all_callbacks(From, Event, To, Context, Config) -> gleam@result:'try'( run_state_callbacks( From, Context, scamper@config:get_global_on_exit(Config), find_state_callback(scamper@config:get_on_exit(Config), From), on_exit ), fun(Ctx) -> gleam@result:'try'( run_transition_callbacks( From, Event, To, Ctx, scamper@config:get_global_on_transition(Config), find_transition_callback( scamper@config:get_on_transition_state(Config), From ), on_transition ), fun(Ctx@1) -> run_state_callbacks( To, Ctx@1, scamper@config:get_global_on_enter(Config), find_state_callback( scamper@config:get_on_enter(Config), To ), on_enter ) end ) end ). -file("src/scamper/transition.gleam", 323). ?DOC(" Run all context invariants. Returns Ok or InvariantViolation.\n"). -spec run_invariants(HHX, scamper@config:config(HHY, HHX, HHZ)) -> {ok, nil} | {error, scamper@error:transition_error(HHY, HHZ)}. run_invariants(Context, Config) -> Invariants = scamper@config:get_invariants(Config), gleam@list:try_fold(Invariants, nil, fun(_, Inv) -> _pipe = Inv(Context), gleam@result:map_error( _pipe, fun(Reason) -> {invariant_violation, Reason} end ) end). -file("src/scamper/transition.gleam", 151). ?DOC(" Execute the transition with callbacks and invariants.\n"). -spec execute_transition( HFB, HFC, HFD, HFB, scamper@config:config(HFB, HFC, HFD), list(scamper@history:transition_record(HFB, HFD, HFC)) ) -> {ok, transition_result(HFB, HFD, HFC)} | {error, scamper@error:transition_error(HFB, HFD)}. execute_transition(From, Context, Event, To, Config, Current_history) -> gleam@result:'try'( run_all_callbacks(From, Event, To, Context, Config), fun(Ctx_after_callbacks) -> gleam@result:'try'( run_invariants(Ctx_after_callbacks, Config), fun(_) -> Timestamp = scamper@config:get_timestamp(Config), Snapshot = case scamper@config:get_history_snapshots(Config) of true -> {some, Ctx_after_callbacks}; false -> none end, Record = scamper@history:new_record( From, Event, To, Timestamp, Snapshot ), New_history = scamper@history:append( Current_history, Record, scamper@config:get_history_limit(Config) ), {ok, {transition_result, To, Ctx_after_callbacks, New_history, Timestamp}} end ) end ). -file("src/scamper/transition.gleam", 90). -spec resolve_guards( HDG, HDH, HDI, list(scamper@config:transition_rule(HDG, HDH, HDI)), scamper@config:config(HDG, HDH, HDI), list(scamper@history:transition_record(HDG, HDI, HDH)) ) -> {ok, transition_result(HDG, HDI, HDH)} | {error, scamper@error:transition_error(HDG, HDI)}. resolve_guards(From, Context, Event, Rules, Config, Current_history) -> case find_matching_rule(Rules, Context, Event) of {some, Rule} -> execute_transition( From, Context, Event, erlang:element(4, Rule), Config, Current_history ); none -> {error, {guard_rejected, From, Event, <<"All guards rejected"/utf8>>}} end. -file("src/scamper/transition.gleam", 57). -spec execute_non_final( HCP, HCQ, HCR, scamper@config:config(HCP, HCQ, HCR), list(scamper@history:transition_record(HCP, HCR, HCQ)) ) -> {ok, transition_result(HCP, HCR, HCQ)} | {error, scamper@error:transition_error(HCP, HCR)}. execute_non_final(State, Context, Event, Config, Current_history) -> Matching_rules = begin _pipe = scamper@config:get_transitions(Config), gleam@list:filter( _pipe, fun(Rule) -> (erlang:element(2, Rule) =:= State) andalso (erlang:element( 3, Rule ) =:= Event) end ) end, case Matching_rules of [] -> case scamper@config:get_event_policy(Config) of ignore -> {ok, {transition_result, State, Context, Current_history, scamper@config:get_timestamp(Config)}}; reject -> {error, {invalid_transition, State, Event}} end; Rules -> resolve_guards( State, Context, Event, Rules, Config, Current_history ) end. -file("src/scamper/transition.gleam", 39). ?DOC( " Execute a state transition.\n" "\n" " Algorithm:\n" " 1. Check if in final state → AlreadyFinal\n" " 2. Find matching rules for (from, event)\n" " 3. No rules → check event policy (Reject/Ignore)\n" " 4. Evaluate guards top-to-bottom, first passing wins\n" " 5. Execute callbacks: on_exit → on_transition → on_enter\n" " 6. Run invariants on new context\n" " 7. Record history\n" ). -spec execute( HBY, HBZ, HCA, scamper@config:config(HBY, HBZ, HCA), list(scamper@history:transition_record(HBY, HCA, HBZ)) ) -> {ok, transition_result(HBY, HCA, HBZ)} | {error, scamper@error:transition_error(HBY, HCA)}. execute(State, Context, Event, Config, Current_history) -> Final_states = scamper@config:get_final_states(Config), case gleam@list:contains(Final_states, State) of true -> {error, {already_final, State}}; false -> execute_non_final(State, Context, Event, Config, Current_history) end. -file("src/scamper/transition.gleam", 336). ?DOC( " Check whether a transition is possible without executing it.\n" " Only checks final state, matching rules, and guards.\n" ). -spec can_execute(HIH, HII, HIJ, scamper@config:config(HIH, HIJ, HII)) -> boolean(). can_execute(State, Event, Context, Config) -> Final_states = scamper@config:get_final_states(Config), case gleam@list:contains(Final_states, State) of true -> false; false -> Matching_rules = begin _pipe = scamper@config:get_transitions(Config), gleam@list:filter( _pipe, fun(Rule) -> (erlang:element(2, Rule) =:= State) andalso (erlang:element( 3, Rule ) =:= Event) end ) end, case Matching_rules of [] -> case scamper@config:get_event_policy(Config) of ignore -> true; reject -> false end; Rules -> case find_matching_rule(Rules, Context, Event) of {some, _} -> true; none -> false end end end. -file("src/scamper/transition.gleam", 367). ?DOC( " Get all events that have at least one matching transition rule\n" " from the current state. Does not evaluate guards.\n" ). -spec available_events(HIN, scamper@config:config(HIN, any(), HIP)) -> list(HIP). available_events(State, Config) -> _pipe = scamper@config:get_transitions(Config), _pipe@1 = gleam@list:filter( _pipe, fun(Rule) -> erlang:element(2, Rule) =:= State end ), _pipe@2 = gleam@list:map( _pipe@1, fun(Rule@1) -> erlang:element(3, Rule@1) end ), gleam@list:unique(_pipe@2).