Skip to main content

steel_core/command/execution/
queue.rs

1#![cfg_attr(
2    not(test),
3    expect(
4        dead_code,
5        reason = "custom scheduler control hooks are exercised by tests and reserved for keyed runtimes"
6    )
7)]
8
9use std::{collections::VecDeque, sync::Arc};
10
11use crate::command::brigadier::{CommandSyntaxError, ContextChainStage};
12
13use super::{
14    CommandResultCallback, ExecutionCommandSource, SteelContextChain, SteelExecutor, SteelModifier,
15};
16
17const MAX_COMMAND_QUEUE_DEPTH: usize = 10_000_000;
18
19/// Flags accumulated while traversing a command context chain.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub(crate) struct ChainModifiers(u8);
22
23impl ChainModifiers {
24    const FORKED: u8 = 1;
25    const RETURN: u8 = 2;
26
27    pub(crate) const fn is_forked(self) -> bool {
28        self.0 & Self::FORKED != 0
29    }
30
31    pub(crate) const fn is_return(self) -> bool {
32        self.0 & Self::RETURN != 0
33    }
34
35    pub(crate) const fn with_forked(self) -> Self {
36        Self(self.0 | Self::FORKED)
37    }
38
39    pub(crate) const fn with_return(self) -> Self {
40        Self(self.0 | Self::RETURN)
41    }
42}
43
44/// Why a command queue stopped running.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub(crate) enum ExecutionStop {
47    Completed,
48    Suspended,
49    CommandLimit,
50    QueueOverflow,
51}
52
53#[derive(Clone)]
54pub(crate) struct Frame {
55    depth: usize,
56    return_value_consumer: CommandResultCallback,
57    discard: FrameDiscard,
58}
59
60#[derive(Clone, Copy)]
61enum FrameDiscard {
62    All,
63    AtOrAbove(usize),
64}
65
66impl Frame {
67    pub(crate) const fn depth(&self) -> usize {
68        self.depth
69    }
70
71    pub(crate) fn return_success(&self, value: i32) {
72        self.return_value_consumer.on_result(true, value);
73    }
74
75    pub(crate) fn return_failure(&self) {
76        self.return_value_consumer.on_result(false, 0);
77    }
78}
79
80pub(crate) trait EntryAction<S>: Send + 'static
81where
82    S: ExecutionCommandSource,
83{
84    fn execute(self: Box<Self>, context: &mut CommandExecutionContext<S>, frame: Frame);
85
86    fn runs_after_command_limit(&self) -> bool {
87        false
88    }
89
90    fn cancel(&mut self) {}
91}
92
93/// Poll result for a normal command whose result is produced across ticks.
94pub(crate) enum CommandResultSuspensionPoll {
95    Pending,
96    Ready(Result<i32, CommandSyntaxError>),
97}
98
99/// Ordering barrier retained while a top-level command is suspended.
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub enum CommandSuspensionOrder {
102    /// Only later commands from the same source wait for this suspension.
103    #[default]
104    Source,
105    /// Every later command waits because the suspended work mutates shared command authority.
106    Global,
107}
108
109/// Cross-tick work that retains ordinary command result and error semantics.
110pub(crate) trait CommandResultSuspension: Send + 'static {
111    fn order(&self) -> CommandSuspensionOrder {
112        CommandSuspensionOrder::Source
113    }
114
115    fn poll(&mut self) -> CommandResultSuspensionPoll;
116
117    fn cancel(&mut self) {}
118}
119
120/// Poll result for work that suspended a command execution.
121pub(crate) enum CommandSuspensionPoll<S>
122where
123    S: ExecutionCommandSource,
124{
125    Pending,
126    Ready(Box<dyn EntryAction<S>>),
127}
128
129impl<S> CommandSuspensionPoll<S>
130where
131    S: ExecutionCommandSource,
132{
133    pub(crate) fn resume(action: impl EntryAction<S>) -> Self {
134        Self::Ready(Box::new(action))
135    }
136}
137
138/// Cross-tick work that eventually produces the next action for the same command frame.
139pub(crate) trait CommandSuspension<S>: Send + 'static
140where
141    S: ExecutionCommandSource,
142{
143    fn order(&self) -> CommandSuspensionOrder {
144        CommandSuspensionOrder::Source
145    }
146
147    fn poll(&mut self) -> CommandSuspensionPoll<S>;
148
149    fn cancel(&mut self) {}
150}
151
152struct CommandQueueEntry<S>
153where
154    S: ExecutionCommandSource,
155{
156    frame: Frame,
157    action: Box<dyn EntryAction<S>>,
158}
159
160struct ActiveSuspension<S>
161where
162    S: ExecutionCommandSource,
163{
164    frame: Frame,
165    suspension: Box<dyn CommandSuspension<S>>,
166}
167
168struct SuspensionResumeAction<S>
169where
170    S: ExecutionCommandSource,
171{
172    action: Box<dyn EntryAction<S>>,
173}
174
175impl<S> EntryAction<S> for SuspensionResumeAction<S>
176where
177    S: ExecutionCommandSource,
178{
179    fn execute(self: Box<Self>, context: &mut CommandExecutionContext<S>, frame: Frame) {
180        self.action.execute(context, frame);
181    }
182
183    fn runs_after_command_limit(&self) -> bool {
184        true
185    }
186
187    fn cancel(&mut self) {
188        self.action.cancel();
189    }
190}
191
192/// Vanilla-style command action queue retained only while explicitly suspended.
193pub(crate) struct CommandExecutionContext<S>
194where
195    S: ExecutionCommandSource,
196{
197    command_limit: usize,
198    fork_limit: usize,
199    queue_limit: usize,
200    command_quota: usize,
201    queue_overflow: bool,
202    command_queue: VecDeque<CommandQueueEntry<S>>,
203    new_top_commands: Vec<CommandQueueEntry<S>>,
204    suspension: Option<ActiveSuspension<S>>,
205    current_frame_depth: usize,
206}
207
208impl<S> CommandExecutionContext<S>
209where
210    S: ExecutionCommandSource,
211{
212    pub(crate) fn new(command_limit: usize, fork_limit: usize) -> Self {
213        let command_limit = command_limit.max(1);
214        Self {
215            command_limit,
216            fork_limit,
217            queue_limit: MAX_COMMAND_QUEUE_DEPTH,
218            command_quota: command_limit,
219            queue_overflow: false,
220            command_queue: VecDeque::new(),
221            new_top_commands: Vec::new(),
222            suspension: None,
223            current_frame_depth: 0,
224        }
225    }
226
227    #[cfg(test)]
228    pub(super) fn with_queue_limit(
229        command_limit: usize,
230        fork_limit: usize,
231        queue_limit: usize,
232    ) -> Self {
233        let mut context = Self::new(command_limit, fork_limit);
234        context.queue_limit = queue_limit;
235        context
236    }
237
238    pub(crate) fn queue_initial_command(
239        &mut self,
240        chain: SteelContextChain<S>,
241        source: S,
242        return_value_consumer: CommandResultCallback,
243    ) {
244        let source = Arc::new(source);
245        let frame = self.create_top_frame(return_value_consumer);
246        self.queue_entry(CommandQueueEntry {
247            frame,
248            action: Box::new(BuildContextsAction {
249                chain,
250                original_source: Arc::clone(&source),
251                sources: vec![source],
252                modifiers: ChainModifiers::default(),
253            }),
254        });
255    }
256
257    pub(crate) fn run(&mut self) -> ExecutionStop {
258        if self.suspension.is_some() {
259            return ExecutionStop::Suspended;
260        }
261        if self.queue_overflow {
262            log::error!(
263                "Command execution stopped due to command queue overflow (max {})",
264                self.queue_limit
265            );
266            return ExecutionStop::QueueOverflow;
267        }
268
269        self.push_new_commands();
270        let stop = loop {
271            if self.command_quota == 0
272                && !self
273                    .command_queue
274                    .front()
275                    .is_some_and(|entry| entry.action.runs_after_command_limit())
276            {
277                log::info!(
278                    "Command execution stopped due to limit (executed {} commands)",
279                    self.command_limit
280                );
281                break ExecutionStop::CommandLimit;
282            }
283
284            let Some(entry) = self.command_queue.pop_front() else {
285                break ExecutionStop::Completed;
286            };
287            self.current_frame_depth = entry.frame.depth;
288            entry.action.execute(self, entry.frame);
289            if self.queue_overflow {
290                log::error!(
291                    "Command execution stopped due to command queue overflow (max {})",
292                    self.queue_limit
293                );
294                break ExecutionStop::QueueOverflow;
295            }
296            if self.suspension.is_some() {
297                break ExecutionStop::Suspended;
298            }
299            self.push_new_commands();
300        };
301        self.current_frame_depth = 0;
302        stop
303    }
304
305    /// Polls the active suspension once and resumes the retained queue when it is ready.
306    pub(crate) fn poll_suspension(&mut self) -> ExecutionStop {
307        let Some(mut active) = self.suspension.take() else {
308            return self.run();
309        };
310
311        match active.suspension.poll() {
312            CommandSuspensionPoll::Pending => {
313                self.suspension = Some(active);
314                ExecutionStop::Suspended
315            }
316            CommandSuspensionPoll::Ready(action) => {
317                self.queue_next(active.frame, SuspensionResumeAction { action });
318                self.run()
319            }
320        }
321    }
322
323    pub(crate) fn suspension_order(&self) -> Option<CommandSuspensionOrder> {
324        self.suspension
325            .as_ref()
326            .map(|active| active.suspension.order())
327    }
328
329    /// Cancels active and queued suspension work and discards the retained command queue.
330    pub(crate) fn cancel(&mut self) {
331        if let Some(mut active) = self.suspension.take() {
332            active.suspension.cancel();
333        }
334        self.cancel_command_queue();
335        self.cancel_new_top_commands();
336        self.queue_overflow = false;
337        self.current_frame_depth = 0;
338    }
339
340    pub(crate) const fn fork_limit(&self) -> usize {
341        self.fork_limit
342    }
343
344    pub(crate) const fn increment_cost(&mut self) {
345        self.command_quota = self.command_quota.saturating_sub(1);
346    }
347
348    const fn create_top_frame(&self, return_value_consumer: CommandResultCallback) -> Frame {
349        if self.current_frame_depth == 0 {
350            return Frame {
351                depth: 0,
352                return_value_consumer,
353                discard: FrameDiscard::All,
354            };
355        }
356
357        let depth = self.current_frame_depth + 1;
358        Frame {
359            depth,
360            return_value_consumer,
361            discard: FrameDiscard::AtOrAbove(depth),
362        }
363    }
364
365    fn queue_next(&mut self, frame: Frame, action: impl EntryAction<S>) {
366        self.queue_entry(CommandQueueEntry {
367            frame,
368            action: Box::new(action),
369        });
370    }
371
372    fn queue_boxed(&mut self, frame: Frame, action: Box<dyn EntryAction<S>>) {
373        self.queue_entry(CommandQueueEntry { frame, action });
374    }
375
376    fn queue_entry(&mut self, mut entry: CommandQueueEntry<S>) {
377        if self
378            .new_top_commands
379            .len()
380            .saturating_add(self.command_queue.len())
381            > self.queue_limit
382        {
383            self.queue_overflow = true;
384            self.cancel_new_top_commands();
385            self.cancel_command_queue();
386        }
387        if self.queue_overflow {
388            entry.action.cancel();
389            return;
390        }
391        self.new_top_commands.push(entry);
392    }
393
394    fn push_new_commands(&mut self) {
395        while let Some(command) = self.new_top_commands.pop() {
396            self.command_queue.push_front(command);
397        }
398    }
399
400    fn discard(&mut self, frame: &Frame) {
401        match frame.discard {
402            FrameDiscard::All => self.cancel_command_queue(),
403            FrameDiscard::AtOrAbove(depth) => {
404                while self
405                    .command_queue
406                    .front()
407                    .is_some_and(|entry| entry.frame.depth >= depth)
408                {
409                    if let Some(mut entry) = self.command_queue.pop_front() {
410                        entry.action.cancel();
411                    }
412                }
413            }
414        }
415    }
416
417    fn cancel_command_queue(&mut self) {
418        for mut entry in self.command_queue.drain(..) {
419            entry.action.cancel();
420        }
421    }
422
423    fn cancel_new_top_commands(&mut self) {
424        for mut entry in self.new_top_commands.drain(..) {
425            entry.action.cancel();
426        }
427    }
428}
429
430impl<S> Drop for CommandExecutionContext<S>
431where
432    S: ExecutionCommandSource,
433{
434    fn drop(&mut self) {
435        self.cancel();
436    }
437}
438
439/// Queue and frame operations available only to custom internal executors.
440pub(crate) struct ExecutionControl<'context, S>
441where
442    S: ExecutionCommandSource,
443{
444    context: &'context mut CommandExecutionContext<S>,
445    frame: Frame,
446}
447
448impl<'context, S> ExecutionControl<'context, S>
449where
450    S: ExecutionCommandSource,
451{
452    pub(crate) const fn new(
453        context: &'context mut CommandExecutionContext<S>,
454        frame: Frame,
455    ) -> Self {
456        Self { context, frame }
457    }
458
459    pub(crate) const fn current_frame(&self) -> &Frame {
460        &self.frame
461    }
462
463    pub(crate) fn queue_next(&mut self, action: impl EntryAction<S>) {
464        self.context.queue_next(self.frame.clone(), action);
465    }
466
467    /// Suspends at this queue position until the supplied work produces a resume action.
468    pub(crate) fn suspend(&mut self, suspension: impl CommandSuspension<S>) {
469        self.queue_next(SuspendAction {
470            suspension: Box::new(suspension),
471        });
472    }
473
474    pub(crate) fn queue_contexts(
475        &mut self,
476        chain: SteelContextChain<S>,
477        original_source: Arc<S>,
478        sources: Vec<Arc<S>>,
479        modifiers: ChainModifiers,
480    ) {
481        self.context.queue_next(
482            self.frame.clone(),
483            BuildContextsAction {
484                chain,
485                original_source,
486                sources,
487                modifiers,
488            },
489        );
490    }
491
492    pub(crate) fn discard_frame(&mut self) {
493        self.context.discard(&self.frame);
494    }
495
496    pub(crate) fn queue_fallthrough(&mut self) {
497        self.queue_next(FallthroughAction);
498    }
499
500    pub(crate) fn return_success(&mut self, result: i32) {
501        self.frame.return_success(result);
502        self.context.discard(&self.frame);
503    }
504
505    pub(crate) fn return_failure(&mut self) {
506        self.frame.return_failure();
507        self.context.discard(&self.frame);
508    }
509}
510
511struct SuspendAction<S>
512where
513    S: ExecutionCommandSource,
514{
515    suspension: Box<dyn CommandSuspension<S>>,
516}
517
518impl<S> EntryAction<S> for SuspendAction<S>
519where
520    S: ExecutionCommandSource,
521{
522    fn execute(self: Box<Self>, context: &mut CommandExecutionContext<S>, frame: Frame) {
523        assert!(
524            context.suspension.is_none(),
525            "a command execution cannot activate two suspensions at once"
526        );
527        context.suspension = Some(ActiveSuspension {
528            frame,
529            suspension: self.suspension,
530        });
531    }
532
533    fn runs_after_command_limit(&self) -> bool {
534        true
535    }
536
537    fn cancel(&mut self) {
538        self.suspension.cancel();
539    }
540}
541
542struct BuildContextsAction<S>
543where
544    S: ExecutionCommandSource,
545{
546    chain: SteelContextChain<S>,
547    original_source: Arc<S>,
548    sources: Vec<Arc<S>>,
549    modifiers: ChainModifiers,
550}
551
552impl<S> EntryAction<S> for BuildContextsAction<S>
553where
554    S: ExecutionCommandSource,
555{
556    fn execute(self: Box<Self>, context: &mut CommandExecutionContext<S>, frame: Frame) {
557        let Self {
558            mut chain,
559            original_source,
560            mut sources,
561            mut modifiers,
562        } = *self;
563
564        while chain.stage() == ContextChainStage::Modify {
565            sources.retain(|source| source.execution_is_current());
566            if sources.is_empty() {
567                if modifiers.is_return() {
568                    context.queue_next(frame, FallthroughAction);
569                }
570                return;
571            }
572            if chain.top_context().is_forked() {
573                modifiers = modifiers.with_forked();
574            }
575
576            match chain.top_context().modifier() {
577                Some(SteelModifier::Custom(modifier)) => {
578                    let mut control = ExecutionControl::new(context, frame);
579                    modifier.apply(original_source, sources, &chain, modifiers, &mut control);
580                    return;
581                }
582                Some(SteelModifier::Standard(modifier)) => {
583                    context.increment_cost();
584                    let mut next_sources = Vec::new();
585                    for source in sources {
586                        let command_context = chain.top_context().copy_for(Arc::clone(&source));
587                        let new_sources = match modifier(&command_context) {
588                            Ok(sources) => sources,
589                            Err(error) => {
590                                if modifiers.is_forked() {
591                                    continue;
592                                }
593                                source.handle_error(&error, false);
594                                return;
595                            }
596                        };
597                        if next_sources.len().saturating_add(new_sources.len())
598                            >= context.fork_limit()
599                        {
600                            let error = CommandSyntaxError::dynamic(format!(
601                                "Command fork limit reached ({})",
602                                context.fork_limit()
603                            ));
604                            original_source.handle_error(&error, modifiers.is_forked());
605                            return;
606                        }
607                        next_sources.extend(new_sources.into_iter().map(Arc::new));
608                    }
609                    sources = next_sources;
610                }
611                None => {}
612            }
613
614            let Some(next_stage) = chain.next_stage() else {
615                unreachable!("a modifying command stage always has a following stage")
616            };
617            chain = next_stage;
618        }
619
620        sources.retain(|source| source.execution_is_current());
621        if sources.is_empty() {
622            if modifiers.is_return() {
623                context.queue_next(frame, FallthroughAction);
624            }
625            return;
626        }
627
628        let Some(executor) = chain.top_context().executor() else {
629            unreachable!("a context chain's final stage is always executable")
630        };
631        match executor {
632            SteelExecutor::Custom(executor) => {
633                for source in sources {
634                    let mut control = ExecutionControl::new(context, frame.clone());
635                    executor.run(source, &chain, modifiers, &mut control);
636                }
637            }
638            SteelExecutor::Standard(_) | SteelExecutor::Suspended(_) => {
639                if modifiers.is_return() {
640                    let Some(source) = sources.into_iter().next() else {
641                        unreachable!("empty source lists return before terminal scheduling")
642                    };
643                    let callback = CommandResultCallback::chain(
644                        source.callback(),
645                        frame.return_value_consumer.clone(),
646                    );
647                    let source = Arc::new(source.with_callback(callback));
648                    schedule_executions(context, frame, chain, vec![source], modifiers);
649                } else {
650                    schedule_executions(context, frame, chain, sources, modifiers);
651                }
652            }
653        }
654    }
655}
656
657fn schedule_executions<S>(
658    context: &mut CommandExecutionContext<S>,
659    frame: Frame,
660    chain: SteelContextChain<S>,
661    sources: Vec<Arc<S>>,
662    modifiers: ChainModifiers,
663) where
664    S: ExecutionCommandSource,
665{
666    match sources.len() {
667        0 => {}
668        1 | 2 => {
669            for source in sources {
670                context.queue_next(
671                    frame.clone(),
672                    ExecuteAction {
673                        chain: chain.clone(),
674                        source,
675                        modifiers,
676                    },
677                );
678            }
679        }
680        _ => context.queue_next(
681            frame,
682            ExecuteContinuation {
683                chain,
684                sources: sources.into(),
685                modifiers,
686            },
687        ),
688    }
689}
690
691struct ExecuteContinuation<S>
692where
693    S: ExecutionCommandSource,
694{
695    chain: SteelContextChain<S>,
696    sources: VecDeque<Arc<S>>,
697    modifiers: ChainModifiers,
698}
699
700impl<S> EntryAction<S> for ExecuteContinuation<S>
701where
702    S: ExecutionCommandSource,
703{
704    fn execute(mut self: Box<Self>, context: &mut CommandExecutionContext<S>, frame: Frame) {
705        let Some(source) = self.sources.pop_front() else {
706            return;
707        };
708        context.queue_next(
709            frame.clone(),
710            ExecuteAction {
711                chain: self.chain.clone(),
712                source,
713                modifiers: self.modifiers,
714            },
715        );
716        if !self.sources.is_empty() {
717            context.queue_boxed(frame, self);
718        }
719    }
720}
721
722struct ExecuteAction<S>
723where
724    S: ExecutionCommandSource,
725{
726    chain: SteelContextChain<S>,
727    source: Arc<S>,
728    modifiers: ChainModifiers,
729}
730
731impl<S> EntryAction<S> for ExecuteAction<S>
732where
733    S: ExecutionCommandSource,
734{
735    fn execute(self: Box<Self>, context: &mut CommandExecutionContext<S>, frame: Frame) {
736        let Self {
737            chain,
738            source,
739            modifiers,
740        } = *self;
741        if !source.execution_is_current() {
742            source.callback().on_result(false, 0);
743            return;
744        }
745        context.increment_cost();
746        let command_context = chain.top_context().copy_for(Arc::clone(&source));
747        let Some(executor) = command_context.executor() else {
748            unreachable!("a scheduled execute action always has a terminal executor")
749        };
750        match executor {
751            SteelExecutor::Standard(executor) => {
752                complete_command_result(source.as_ref(), modifiers, executor(&command_context));
753            }
754            SteelExecutor::Suspended(executor) => match executor(&command_context) {
755                Ok(suspension) => context.queue_next(
756                    frame,
757                    SuspendAction {
758                        suspension: Box::new(CommandResultSuspensionAdapter {
759                            suspension,
760                            source,
761                            modifiers,
762                        }),
763                    },
764                ),
765                Err(error) => complete_command_result(source.as_ref(), modifiers, Err(error)),
766            },
767            SteelExecutor::Custom(_) => {
768                unreachable!("custom executors run directly while building contexts")
769            }
770        }
771    }
772}
773
774struct CommandResultSuspensionAdapter<S>
775where
776    S: ExecutionCommandSource,
777{
778    suspension: Box<dyn CommandResultSuspension>,
779    source: Arc<S>,
780    modifiers: ChainModifiers,
781}
782
783impl<S> CommandSuspension<S> for CommandResultSuspensionAdapter<S>
784where
785    S: ExecutionCommandSource,
786{
787    fn order(&self) -> CommandSuspensionOrder {
788        self.suspension.order()
789    }
790
791    fn poll(&mut self) -> CommandSuspensionPoll<S> {
792        if !self.source.execution_is_current() {
793            self.suspension.cancel();
794            return CommandSuspensionPoll::resume(UnavailableCommandResultAction {
795                source: Arc::clone(&self.source),
796            });
797        }
798        match self.suspension.poll() {
799            CommandResultSuspensionPoll::Pending => CommandSuspensionPoll::Pending,
800            CommandResultSuspensionPoll::Ready(result) => {
801                CommandSuspensionPoll::resume(CompleteCommandResultAction {
802                    source: Arc::clone(&self.source),
803                    modifiers: self.modifiers,
804                    result,
805                })
806            }
807        }
808    }
809
810    fn cancel(&mut self) {
811        self.suspension.cancel();
812    }
813}
814
815struct UnavailableCommandResultAction<S>
816where
817    S: ExecutionCommandSource,
818{
819    source: Arc<S>,
820}
821
822impl<S> EntryAction<S> for UnavailableCommandResultAction<S>
823where
824    S: ExecutionCommandSource,
825{
826    fn execute(self: Box<Self>, _context: &mut CommandExecutionContext<S>, _frame: Frame) {
827        self.source.callback().on_result(false, 0);
828    }
829}
830
831struct CompleteCommandResultAction<S>
832where
833    S: ExecutionCommandSource,
834{
835    source: Arc<S>,
836    modifiers: ChainModifiers,
837    result: Result<i32, CommandSyntaxError>,
838}
839
840impl<S> EntryAction<S> for CompleteCommandResultAction<S>
841where
842    S: ExecutionCommandSource,
843{
844    fn execute(self: Box<Self>, _context: &mut CommandExecutionContext<S>, _frame: Frame) {
845        complete_command_result(self.source.as_ref(), self.modifiers, self.result);
846    }
847}
848
849fn complete_command_result<S>(
850    source: &S,
851    modifiers: ChainModifiers,
852    result: Result<i32, CommandSyntaxError>,
853) where
854    S: ExecutionCommandSource,
855{
856    match result {
857        Ok(result) => source.callback().on_result(true, result),
858        Err(error) => {
859            source.callback().on_result(false, 0);
860            if !modifiers.is_forked() {
861                source.handle_error(&error, false);
862            }
863        }
864    }
865}
866
867struct FallthroughAction;
868
869impl<S> EntryAction<S> for FallthroughAction
870where
871    S: ExecutionCommandSource,
872{
873    fn execute(self: Box<Self>, context: &mut CommandExecutionContext<S>, frame: Frame) {
874        frame.return_failure();
875        context.discard(&frame);
876    }
877}