├── .github └── workflows │ ├── build.yml │ ├── changelog.yml │ ├── links.yml │ └── properties │ ├── build.properties.json │ └── docs.properties.json ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── ci ├── install.sh └── script.sh ├── docs ├── dsl.md ├── sm1.png ├── sm2.png └── sm3.png ├── examples ├── async.rs ├── context.rs ├── dominos.rs ├── event_with_data.rs ├── event_with_mutable_data.rs ├── event_with_reference_data.rs ├── ex1.rs ├── ex2.rs ├── ex3.rs ├── guard_action_syntax.rs ├── guard_action_syntax_with_temporary_context.rs ├── guard_custom_error.rs ├── input_state_pattern_match.rs ├── named_async.rs ├── named_dominos.rs ├── named_ex1.rs ├── named_ex2.rs ├── named_ex3.rs ├── named_input_state_pattern_match.rs ├── named_state_with_data.rs ├── named_state_with_reference_data.rs ├── on_entry_on_exit_generic.rs ├── reuse_action.rs ├── starting_state_with_data.rs ├── state_machine_logger.rs ├── state_with_data.rs └── state_with_reference_data.rs ├── macros ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT └── src │ ├── codegen.rs │ ├── diagramgen.rs │ ├── lib.rs │ ├── parser │ ├── data.rs │ ├── event.rs │ ├── input_state.rs │ ├── lifetimes.rs │ ├── mod.rs │ ├── output_state.rs │ ├── state_machine.rs │ └── transition.rs │ └── validation.rs ├── src └── lib.rs └── tests ├── compile-fail ├── double_state_event.rs ├── double_state_event.stderr ├── duplicate_action.rs ├── duplicate_action.stderr ├── duplicate_guard.rs ├── duplicate_guard.stderr ├── guarded_transition_after_unguarded.rs ├── guarded_transition_after_unguarded.stderr ├── multiple_starting_state.rs ├── multiple_starting_state.stderr ├── no_action_with_state_data.rs ├── no_action_with_state_data.stderr ├── no_starting_state.rs ├── no_starting_state.stderr ├── wildcard_before_input_state.rs ├── wildcard_before_input_state.stderr ├── wildcard_no_effect.rs ├── wildcard_no_effect.stderr ├── wildcard_with_pattern.rs └── wildcard_with_pattern.stderr └── test.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | check: 13 | name: Check 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: dtolnay/rust-toolchain@stable 19 | 20 | - name: Run cargo check 21 | run: cargo check 22 | 23 | test: 24 | name: Test Suite 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: dtolnay/rust-toolchain@stable 29 | 30 | - name: Run cargo test 31 | run: cargo test 32 | 33 | fmt: 34 | name: Rustfmt 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: dtolnay/rust-toolchain@stable 39 | with: 40 | components: rustfmt 41 | 42 | - name: Run cargo fmt 43 | run: cargo fmt --all -- --check 44 | 45 | clippy: 46 | name: Clippy 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: dtolnay/rust-toolchain@stable 51 | with: 52 | components: clippy 53 | 54 | - name: Run cargo clippy 55 | run: cargo clippy --all -- -D warnings 56 | 57 | docs: 58 | runs-on: ubuntu-latest 59 | 60 | steps: 61 | - uses: actions/checkout@v4 62 | 63 | - name: Set up Python 3.x 64 | uses: actions/setup-python@v5 65 | with: 66 | # Semantic version range syntax or exact version of a Python version 67 | python-version: '3.x' 68 | # Optional - x64 or x86 architecture, defaults to x64 69 | architecture: 'x64' 70 | 71 | # You can test your matrix by printing the current Python version 72 | - name: Display Python version 73 | run: python -c "import sys; print(sys.version)" 74 | 75 | - name: Install dependencies 76 | run: pip install git+https://github.com/linkchecker/linkchecker.git 77 | 78 | - name: Remove cargo-config 79 | run: rm -f .cargo/config 80 | 81 | - name: Build docs 82 | run: cargo doc 83 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | # Check that the changelog is updated for all changes. 2 | # 3 | # This is only run for PRs. 4 | 5 | on: 6 | pull_request: 7 | # opened, reopened, synchronize are the default types for pull_request. 8 | # labeled, unlabeled ensure this check is also run if a label is added or removed. 9 | types: [opened, reopened, labeled, unlabeled, synchronize] 10 | 11 | name: Changelog 12 | 13 | jobs: 14 | changelog: 15 | name: Changelog 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v4 20 | 21 | - name: Check that changelog updated 22 | uses: dangoslen/changelog-enforcer@v3 23 | with: 24 | changeLogPath: CHANGELOG.md 25 | skipLabels: 'needs-changelog, skip-changelog' 26 | missingUpdateErrorMessage: 'Please add a changelog entry in the CHANGELOG.md file.' 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/links.yml: -------------------------------------------------------------------------------- 1 | name: Links 2 | 3 | on: 4 | repository_dispatch: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: "00 18 * * *" 8 | 9 | jobs: 10 | linkChecker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Link Checker 16 | id: lychee 17 | uses: lycheeverse/lychee-action@v1 18 | env: 19 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 20 | 21 | - name: Create Issue From File 22 | if: steps.lychee.outputs.exit_code != 0 23 | uses: peter-evans/create-issue-from-file@v4 24 | with: 25 | title: Link Checker Report 26 | content-filepath: ./lychee/out.md 27 | labels: report, automated issue 28 | -------------------------------------------------------------------------------- /.github/workflows/properties/build.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Build", 3 | "description": "Build and test a Rust project with Cargo.", 4 | "iconName": "rust", 5 | "categories": ["Rust"] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/properties/docs.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Docs", 3 | "description": "Build the books.", 4 | "iconName": "rust", 5 | "categories": ["Rust"] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | macros/target 3 | **/*.rs.bk 4 | Cargo.lock 5 | *.gv 6 | *.svg 7 | .vscode 8 | .idea/ 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - Add support for defining States and Events attributes using `states_attr` and `events_attr` fields 13 | - Add support for async on_entry_* and on_exit_* hooks with flag `entry_exit_async: true` 14 | 15 | ### Changed 16 | 17 | - [breaking] Remove `derive_states` and `derive_events` fields in lieu of `states_attr` and `events_attr` to define attributes generically 18 | - Bumped `syn` dependency to version 2 19 | 20 | ## [v0.8.0] - 2024-08-07 21 | 22 | ### Added 23 | 24 | - Add transition callback. A function which is called for every transition. It has default empty 25 | implementation. 26 | - Add support for implicit and wildcard internal transitions 27 | 28 | ### Changed 29 | 30 | - [breaking] Renamed custom_guard_error flag to custom_error as it is not guard specific anymore 31 | - [breaking] Re-ordered on_exit/on_entry hooks calls 32 | - [breaking] New `transition_callback` has replaced `log_state_change` as it is more flexible. 33 | 34 | ## [0.7.0] - 2024-07-03 35 | 36 | ### Added 37 | 38 | - Add support for async guards and actions 39 | - Add name to statemachine and make dot output stable and unique ([issue-62](https://github.com/korken89/smlang-rs/pull/62)) 40 | - Add derive macros to states and events ([issue-62](https://github.com/korken89/smlang-rs/pull/62)) 41 | - Add hooks to `StateMachineContext` for logging events, guards, actions, and state changes 42 | - Add support multiple guarded transitions for a triggering event 43 | - Add support for guard boolean expressions in the state machine declaration 44 | - There are now `on_entry_` and `on_entry_` functions 45 | defined to allow handling entry and exit from all state machine states. These have a default empty 46 | implementation. 47 | * The expanded code is now written to `target/smlang-expansion-.rs` during the build 48 | process. 49 | 50 | ### Fixed 51 | 52 | - Fixes multiple issues with lifetimes ([issue-57](https://github.com/korken89/smlang-rs/issues/57), [issue-58](https://github.com/korken89/smlang-rs/pull/58)) 53 | 54 | ### Changed 55 | 56 | - `StateMachine::new` and `StateMachine::new_with_state` are now const functions 57 | - Fixed clippy warnings 58 | - [breaking] Changed guard functions return type from Result<(),_> to Result 59 | - [breaking] Changed action functions return type from () to Result 60 | - [breaking] Disallow guards mutable access to the context 61 | - [breaking] Renamed GuardError to Error as it is now used for both guards and actions 62 | 63 | ## [v0.6.0] - 2022-11-02 64 | 65 | ### Fixed 66 | 67 | - Updated the link checker in the Github actions to use [lychee](https://github.com/lycheeverse/lychee). 68 | 69 | ### Added 70 | 71 | - Starting state can now contain data ([issue-34](https://github.com/korken89/smlang-rs/issues/34)) 72 | - Allow explicit input states before wildcard input state([issue-47](https://github.com/korken89/smlang-rs/pull/47) 73 | 74 | ### Changed 75 | - Custom guard error types are now specified as a type of the `StateMachineContext` to allow for 76 | more versatile types. 77 | 78 | ## [v0.5.1] 79 | 80 | ### Fixed 81 | * [#36](https://github.com/korken89/smlang-rs/issues/36) Attempts to use actions and guards with 82 | inconsistent input, event, and output state data will be flagged as compiler errors. 83 | 84 | ### Added 85 | 86 | ## [v0.5.0] 87 | 88 | ### Added 89 | 90 | - Changelog enforcer added to CI 91 | - State data now supports lifetimes ([issue-26](https://github.com/korken89/smlang-rs/issues/26)) 92 | - New example [dominos.rs](https://github.com/korken89/smlang-rs/blob/master/examples/dominos.rs) illustrating a method of event propagation ([issue-17](https://github.com/korken89/smlang-rs/issues/17)) 93 | - Input states support pattern matching and wildcards ([issue-29](https://github.com/korken89/smlang-rs/issues/29)) 94 | 95 | ### Fixed 96 | - PartialEq for States and Events based on discriminant only ([issue-21](https://github.com/korken89/smlang-rs/issues/21)) 97 | - Updated the CI badges ([issue-30](https://github.com/korken89/smlang-rs/issues/30)) 98 | 99 | ## [v0.4.2] 100 | 101 | ### Fixed 102 | 103 | ### Added 104 | 105 | - Initial state can be specified at runtime. 106 | 107 | ### Changes 108 | 109 | ## [v0.4.1] -- YANKED 110 | 111 | ## [v0.4.0] 112 | 113 | ### Added 114 | 115 | - Introduce a new named syntax, supporting `guard_error`, `transitions` and `temporary_context` 116 | - The ability to define custom guard errors 117 | 118 | ## [v0.3.5] 119 | 120 | ### Fixed 121 | 122 | - No longer needed to define each action, the same action can now be reused. 123 | 124 | ### Added 125 | 126 | ## [v0.3.4] 127 | 128 | ### Fixed 129 | 130 | ### Added 131 | 132 | - Added syntax and support for a temporary context which is propagated from `process_event`. This 133 | allows for usage in systems where the normal context cannot take ownership, such as when having a 134 | reference which is only valid during the invocation of the state machine. For an example of this 135 | feature see `examples/guard_action_syntax_with_temporary_context.rs`. 136 | 137 | ### Changes 138 | 139 | ## [v0.3.3] 140 | 141 | ### Fixed 142 | 143 | - Now compatible with `#![deny(missing_docs)]`. 144 | 145 | ## [v0.3.2] 146 | 147 | ### Fixed 148 | 149 | - Having states with data associated, but no action to set this data, caused arcane errors. This is now fixed. 150 | 151 | ### Added 152 | 153 | - Destination state may now have a type associated with it 154 | 155 | ### Changes 156 | 157 | ## [v0.3.1] 158 | 159 | ### Changes 160 | 161 | * Better documentation and examples 162 | * Graphviz diagrams only generated if feature is enabled 163 | 164 | ## [v0.3.0] 165 | 166 | ### Fixed 167 | 168 | * API documentation should now be correctly generated in a project 169 | 170 | ### Changes 171 | 172 | * [breaking] Most derives on `States`, `Events` (`Copy`, `Clone`, `Debug`) and trait bounds on 173 | `StateMachineContext` are removed. 174 | * [breaking] All returns of state are now by reference 175 | * [breaking] Guards now take self my mutable reference, this to allow for context modifications. Quite common 176 | when receiving the same event N times can be accepted as a transition. Before one would have to have 177 | a long list of states to go through. 178 | * Most function are made `#[inline]` 179 | 180 | ## [v0.2.2] 181 | 182 | ### Added 183 | 184 | * Lifetime support added to guards and actions 185 | 186 | ## [v0.2.1] 187 | 188 | ### Added 189 | 190 | * Basic lifetime support for event data 191 | 192 | ## v0.2.0 193 | 194 | ### Added 195 | 196 | * Support for generating a graphviz file over the state machine 197 | * Support for data in events 198 | * Support for data in states 199 | * Change log added 200 | 201 | [Unreleased]: https://github.com/korken89/smlang-rs/compare/v0.8.0...master 202 | [v0.8.0]: https://github.com/korken89/smlang-rs/compare/v0.7.0...v0.8.0 203 | [v0.7.0]: https://github.com/korken89/smlang-rs/compare/v0.6.0...v0.7.0 204 | [v0.6.0]: https://github.com/korken89/smlang-rs/compare/v0.5.1...v0.6.0 205 | [v0.5.1]: https://github.com/korken89/smlang-rs/compare/v0.5.0...v0.5.1 206 | [v0.5.0]: https://github.com/korken89/smlang-rs/compare/v0.4.2...v0.5.0 207 | [v0.4.2]: https://github.com/korken89/smlang-rs/compare/v0.4.1...v0.4.2 208 | [v0.4.1]: https://github.com/korken89/smlang-rs/compare/v0.4.0...v0.4.1 209 | [v0.4.0]: https://github.com/korken89/smlang-rs/compare/v0.3.5...v0.4.0 210 | [v0.3.5]: https://github.com/korken89/smlang-rs/compare/v0.3.4...v0.3.5 211 | [v0.3.4]: https://github.com/korken89/smlang-rs/compare/v0.3.3...v0.3.4 212 | [v0.3.3]: https://github.com/korken89/smlang-rs/compare/v0.3.2...v0.3.3 213 | [v0.3.2]: https://github.com/korken89/smlang-rs/compare/v0.3.1...v0.3.2 214 | [v0.3.1]: https://github.com/korken89/smlang-rs/compare/v0.3.0...v0.3.1 215 | [v0.3.0]: https://github.com/korken89/smlang-rs/compare/v0.2.2...v0.3.0 216 | [v0.2.2]: https://github.com/korken89/smlang-rs/compare/v0.2.1...v0.2.2 217 | [v0.2.1]: https://github.com/korken89/smlang-rs/compare/v0.2.0...v0.2.1 218 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smlang" 3 | categories = ["embedded", "no-std"] 4 | authors = ["Emil Fresk ", "Donny Zimmanck "] 5 | description = "A no-std state machine language DSL" 6 | keywords = ["dsl", "statemachine"] 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/korken89/smlang-rs" 9 | version = "0.8.0" 10 | edition = "2018" 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | smlang-macros = { path = "macros", version = "0.8.0" } 15 | 16 | [dev-dependencies] 17 | smol = "1" 18 | derive_more = "0.99.17" 19 | serde = {version = "1",features = ["derive"]} 20 | 21 | [target.'cfg(not(target_os = "none"))'.dev-dependencies] 22 | trybuild = "1.0" 23 | 24 | [[test]] 25 | name = "test" 26 | 27 | [profile.release] 28 | codegen-units = 1 29 | lto = true 30 | 31 | [workspace] 32 | members = ["macros"] 33 | 34 | [features] 35 | default = [] 36 | 37 | graphviz = ["smlang-macros/graphviz"] 38 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smlang: A `no_std` State Machine Language DSL in Rust 2 | 3 | ![Build Status](https://github.com/korken89/smlang-rs/actions/workflows/build.yml/badge.svg) 4 | ![Documentation](https://github.com/korken89/smlang-rs/actions/workflows/docs.yml/badge.svg) 5 | 6 | > A state machine language DSL based on the syntax of [Boost-SML](https://boost-ext.github.io/sml/). 7 | 8 | `smlang` is a procedural macro library creating a state machine language DSL, whose aim to facilitate the 9 | use of state machines, as they quite fast can become overly complicated to write and get an 10 | overview of. 11 | 12 | The library supports both `async` and non-`async` code. 13 | 14 | ## Transition DSL 15 | 16 | Below is a sample of the DSL. For a full description of the `statemachine` macro, please reference 17 | the [DSL document](docs/dsl.md). 18 | 19 | ```rust 20 | statemachine!{ 21 | transitions: { 22 | *SrcState1 + Event1 [ guard1 ] / action1 = DstState2, // * denotes starting state 23 | SrcState2 + Event2 [ guard2 ] / action2 = DstState1, 24 | } 25 | // ... 26 | } 27 | ``` 28 | 29 | Where `guard` and `action` are optional and can be left out. A `guard` is a function which returns 30 | `Ok(true)` if the state transition should happen - otherwise, the transition should not happen. 31 | The `action` functions are run during the state machine transition. 32 | 33 | > This implies that any state machine must be written as a list of transitions. 34 | 35 | The DSL supports wildcards and pattern matching for input states similar to rust pattern matching: 36 | 37 | ```rust 38 | statemachine!{ 39 | transitions: { 40 | *State1 | State3 + ToState2 = State2, 41 | State1 | State2 + ToState3 = State3, 42 | _ + ToState4 = State4, 43 | State4 + ToState1 = State1, 44 | } 45 | // ... 46 | } 47 | ``` 48 | 49 | Which is equivalent to: 50 | 51 | ```rust 52 | statemachine!{ 53 | transitions: { 54 | *State1 + ToState2 = State2, 55 | State3 + ToState2 = State2, 56 | 57 | State1 + ToState3 = State3, 58 | State2 + ToState3 = State3, 59 | 60 | State1 + ToState4 = State4, 61 | State2 + ToState4 = State4, 62 | State3 + ToState4 = State4, 63 | State4 + ToState4 = State4, 64 | 65 | State4 + ToState1 = State1, 66 | } 67 | // ... 68 | } 69 | ``` 70 | See example `examples/input_state_pattern_match.rs` for a usage example. 71 | 72 | #### Internal transitions 73 | 74 | The DSL supports internal transitions. 75 | Internal transition allow to accept an event and process an action, 76 | and then stay in the current state. 77 | Internal transitions can be specified explicitly, e.g. 78 | ```plantuml 79 | State2 + Event2 / event2_action = State2, 80 | ``` 81 | or 82 | ```plantuml 83 | State2 + Event2 / event2_action = _, 84 | ``` 85 | or implicitly, by omitting the target state including '='. 86 | ```plantuml 87 | State2 + Event2 / event2_action, 88 | ``` 89 | It is also possible to define wildcard implicit (or explicit using '_') internal transitions. 90 | 91 | ```rust 92 | statemachine! { 93 | transitions: { 94 | *State1 + Event2 = State2, 95 | State1 + Event3 = State3, 96 | State1 + Event4 = State4, 97 | 98 | _ + Event2 / event2_action, 99 | }, 100 | } 101 | ``` 102 | The example above demonstrates how you could make Event2 acceptable for any state, 103 | not covered by any of the previous transitions, and to do an action to process it. 104 | 105 | It is equivalent to: 106 | 107 | ```rust 108 | statemachine! { 109 | transitions: { 110 | *State1 + Event2 = State2, 111 | State1 + Event3 = State3, 112 | State1 + Event4 = State4, 113 | 114 | State2 + Event2 / event2_action = State2, 115 | State3 + Event2 / event2_action = State3, 116 | State4 + Event2 / event2_action = State4, 117 | }, 118 | } 119 | ``` 120 | 121 | See also tests: `test_internal_transition_with_data()` or `test_wildcard_states_and_internal_transitions()` for a usage example. 122 | 123 | #### Guard expressions 124 | 125 | Guard expression in square brackets [] allows to define a boolean expressions of multiple guard functions. 126 | For example: 127 | ```rust 128 | statemachine! { 129 | transitions: { 130 | *Init + Login(Entry) [valid_entry] / attempt = LoggedIn, 131 | Init + Login(Entry) [!valid_entry && !too_many_attempts] / attempt = Init, 132 | Init + Login(Entry) [!valid_entry && too_many_attempts] / attempt = LoginDenied, 133 | LoggedIn + Logout / reset = Init, 134 | } 135 | } 136 | ``` 137 | Guard expressions may consist of guard function names, and their combinations with &&, || and ! operations. 138 | 139 | #### Multiple guarded transitions for the same state and triggering event 140 | Multiple guarded transitions for the same state and triggering event are supported (see the example above). 141 | It is assumed that only one guard is enabled in such a case to avoid a conflict over which transition should be selected. 142 | However, if there is a conflict and more than one guard is enabled, the first enabled transition, 143 | in the order they appear in the state machine definition, will be selected. 144 | 145 | ### State machine context 146 | 147 | The state machine needs a context to be defined. 148 | The `StateMachineContext` is generated from the `statemachine!` proc-macro and is what implements 149 | guards and actions, and data that is available in all states within the state machine and persists 150 | between state transitions: 151 | 152 | ```rust 153 | statemachine!{ 154 | transitions: { 155 | State1 + Event1 = State2, 156 | } 157 | // ... 158 | } 159 | 160 | pub struct Context; 161 | 162 | impl StateMachineContext for Context {} 163 | 164 | fn main() { 165 | let mut sm = StateMachine::new(Context); 166 | 167 | // ... 168 | } 169 | ``` 170 | 171 | See example `examples/context.rs` for a usage example. 172 | 173 | 174 | ### State data 175 | 176 | Any state may have some data associated with it: 177 | 178 | ```rust 179 | pub struct MyStateData(pub u32); 180 | 181 | statemachine!{ 182 | transitions: { 183 | State1(MyStateData) + Event1 = State2, 184 | } 185 | // ... 186 | } 187 | ``` 188 | 189 | See example `examples/state_with_data.rs` for a usage example. 190 | 191 | If the starting state contains data, this data must be provided after the context when creating a new machine. 192 | 193 | ```rust 194 | pub struct MyStateData(pub u32); 195 | 196 | statemachine!{ 197 | transitions: { 198 | State2 + Event2 / action = State1(MyStateData), 199 | *State1(MyStateData) + Event1 = State2, 200 | // ... 201 | } 202 | // ... 203 | } 204 | 205 | // ... 206 | 207 | let mut sm = StateMachine::new(Context, MyStateData(42)); 208 | ``` 209 | 210 | State data may also have associated lifetimes which the `statemachine!` macro will pick up and add the `States` enum and `StateMachine` structure. This means the following will also work: 211 | 212 | ```rust 213 | pub struct MyStateData<'a>(&'a u32); 214 | 215 | statemachine! { 216 | transitions: { 217 | *State1 + Event1 / action = State2, 218 | State2(MyStateData<'a>) + Event2 = State1, 219 | // ... 220 | } 221 | // ... 222 | } 223 | ``` 224 | 225 | See example `examples/state_with_reference_data.rs` for a usage example. 226 | 227 | ### Event data 228 | 229 | Data may be passed along with an event into the `guard` and `action`: 230 | 231 | ```rust 232 | pub struct MyEventData(pub u32); 233 | 234 | statemachine!{ 235 | transitions: { 236 | State1 + Event1(MyEventData) [guard] = State2, 237 | } 238 | // ... 239 | } 240 | ``` 241 | 242 | Event data may also have associated lifetimes which the `statemachine!` macro will pick up and add the `Events` enum. This means the following will also work: 243 | 244 | ```rust 245 | pub struct MyEventData<'a>(pub &'a u32); 246 | 247 | statemachine!{ 248 | transitions: { 249 | State1 + Event1(MyEventData<'a>) [guard1] = State2, 250 | State1 + Event2(&'a [u8]) [guard2] = State3, 251 | } 252 | // ... 253 | } 254 | ``` 255 | 256 | See example `examples/event_with_data.rs` for a usage example. 257 | 258 | ### Guard and Action syntax 259 | 260 | See example `examples/guard_action_syntax.rs` for a usage-example. 261 | 262 | ### Async Guard, Action And Entry/Exit 263 | 264 | Guards and actions may both be optionally `async`: 265 | ```rust 266 | use smlang::{async_trait, statemachine}; 267 | 268 | statemachine! { 269 | entry_exit_async: true, 270 | transitions: { 271 | *State1 + Event1 [guard1] / async action1 = State2, 272 | State2 + Event2 [async guard2] / action2 = State3, 273 | } 274 | } 275 | 276 | 277 | pub struct Context { 278 | // ... 279 | } 280 | 281 | impl StateMachineContext for Context { 282 | async fn action1(&mut self) -> () { 283 | // ... 284 | } 285 | 286 | async fn guard2(&mut self) -> Result<(), ()> { 287 | // ... 288 | } 289 | 290 | fn guard1(&mut self) -> Result<(), ()> { 291 | // ... 292 | } 293 | 294 | fn action2(&mut self) -> () { 295 | // ... 296 | } 297 | 298 | async fn on_entry_state1(&mut self) { 299 | // ... 300 | } 301 | 302 | async fn on_exit_state2(&mut self) { 303 | // ... 304 | } 305 | } 306 | ``` 307 | 308 | See example `examples/async.rs` for a usage-example. 309 | 310 | ## State Machine Examples 311 | 312 | Here are some examples of state machines converted from UML to the State Machine Language DSL. 313 | Runnable versions of each example is available in the `examples` folder. The `.png`s are generated 314 | with the `graphviz` feature. 315 | 316 | ### Linear state machine 317 | 318 | ![alt text](./docs/sm1.png "") 319 | 320 | DSL implementation: 321 | 322 | ```rust 323 | statemachine!{ 324 | transitions: { 325 | *State1 + Event1 = State2, 326 | State2 + Event2 = State3, 327 | } 328 | } 329 | ``` 330 | 331 | This example is available in `ex1.rs`. 332 | 333 | ### Looping state machine 334 | 335 | ![alt text](./docs/sm2.png "") 336 | 337 | DSL implementation: 338 | 339 | ```rust 340 | statemachine!{ 341 | transitions: { 342 | *State1 + Event1 = State2, 343 | State2 + Event2 = State3, 344 | State3 + Event3 = State2, 345 | } 346 | } 347 | ``` 348 | 349 | This example is available in `ex2.rs`. 350 | 351 | ### Using guards and actions 352 | 353 | ![alt text](./docs/sm3.png "") 354 | 355 | DSL implementation: 356 | 357 | ```rust 358 | statemachine!{ 359 | transitions: { 360 | *State1 + Event1 [guard] / action = State2, 361 | } 362 | } 363 | ``` 364 | 365 | This example is available in `ex3.rs`. 366 | 367 | ### Using entry and exit functions in transitions 368 | 369 | The statemachine will create for all states an `on_entry_` and `on_exit_` function. 370 | If the are not used, they will be optimized away by the compiler. An example be 371 | found in `on_entry_on_exit_generic`. 372 | 373 | ### Transition callback 374 | 375 | The statemachine will call for every transition a transition callback. This function 376 | is called with both the old state and new state as arguments. An example can be found 377 | in `dominos`. 378 | 379 | ## Helpers 380 | 381 | ### Specify attributes for states and events 382 | 383 | Setting `events_attr` and `states_attr` fields to a list of attributes to `Events` and `States` enums respectively. To derive Display, use `derive_more::Display`. 384 | 385 | 386 | ```rust 387 | use core::Debug; 388 | use derive_more::Display; 389 | // ... 390 | statemachine!{ 391 | states_attr: #[derive(Debug, Display)], 392 | events_attr: #[derive(Debug, Display)], 393 | transitions: { 394 | *State1 + Event1 = State2, 395 | } 396 | } 397 | 398 | // ... 399 | 400 | println!("Current state: {}", sm.state().unwrap()); 401 | println!("Expected state: {}", States::State1); 402 | println!("Sending event: {}", Events::Event1); 403 | 404 | // ... 405 | 406 | ``` 407 | 408 | ### Hooks for logging events, guards, actions, and state transitions 409 | 410 | The `StateMachineContext` trait defines (and provides default, no-op implementations for) functions that are called for each event, guard, action, and state transition. You can provide your 411 | own implementations which plug into your preferred logging mechanism. 412 | 413 | ```rust 414 | fn log_process_event(&self, current_state: &States, event: &Events) {} 415 | fn log_guard(&self, guard: &'static str, result: &Result<(), ()>) {} 416 | fn log_action(&self, action: &'static str) {} 417 | fn log_state_change(&self, new_state: &States) {} 418 | ``` 419 | 420 | See `examples/state_machine_logger.rs` for an example which uses `states_attr` and `events_attr` to derive `Debug` implementations for easy logging. 421 | 422 | ## Contributors 423 | 424 | List of contributors in alphabetical order: 425 | 426 | * Emil Fresk ([@korken89](https://github.com/korken89)) 427 | * Mathias Koch ([@MathiasKoch](https://github.com/MathiasKoch)) 428 | * Ryan Summers ([@ryan-summers](https://github.com/ryan-summers)) 429 | * Donny Zimmanck ([@dzimmanck](https://github.com/dzimmanck)) 430 | 431 | --- 432 | 433 | ## License 434 | 435 | Licensed under either of 436 | 437 | - Apache License, Version 2.0 [LICENSE-APACHE](LICENSE-APACHE) or 438 | - MIT license [LICENSE-MIT](LICENSE-MIT) or 439 | 440 | at your option. 441 | 442 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | set -euxo pipefail 2 | 3 | main() { 4 | if [ $TARGET = docs ]; then 5 | mkdir mdcheck 6 | curl https://raw.githubusercontent.com/mike42/mdcheckr/master/mdcheckr -o mdcheck/mdcheckr 7 | chmod +x mdcheck/mdcheckr 8 | fi 9 | } 10 | 11 | main 12 | -------------------------------------------------------------------------------- /ci/script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | main() { 6 | cargo test 7 | cargo check 8 | } 9 | 10 | main 11 | -------------------------------------------------------------------------------- /docs/dsl.md: -------------------------------------------------------------------------------- 1 | ## Transition DSL 2 | 3 | The state machine macro DSL is defined as follows: 4 | 5 | ```rust 6 | use smlang::statemachine; 7 | 8 | statemachine!{ 9 | // [Optional] An optional prefix to name the generated state machine trait code. This 10 | // can be used to allow multiple state machines to exist in the same source 11 | // file. The generated trait and types are `States`, `Events`, 12 | // and `StateMachine` respectively. 13 | name: Name, 14 | 15 | // [Optional] Can be used if a temporary context is needed within the state machine 16 | // API. When specified, the temporary context is provided in 17 | // `StateMachine::process_event()` and is exposed in guards and actions as 18 | // the second argument. 19 | temporary_context: u32, 20 | 21 | // [Optional] Can be optionally specified to add a new `type Error` to the 22 | // generated `StateMachineContext` trait to allow guards to return a custom 23 | // error type instead of `()`. 24 | custom_error: false, 25 | 26 | // [Optional] A list of attributes for the generated `States` and `Events` 27 | // enumerations respectively. For example, to `#[derive(Debug)]` and `#[repr(u8)], these 28 | // would both be specified in a list as follows: 29 | states_attr: #[derive(Debug)] #[repr(u8)], 30 | events_attr: #[derive(Debug)] #[repr(u8)], 31 | 32 | transitions: { 33 | // * denotes the starting state 34 | *StartState + Event1 [ guard1] / action1 = DstState1, 35 | 36 | // Guards and actions can be async functions. 37 | SrcState2 + Event2 [ async guard2 ] / async action2 = DstState2, 38 | 39 | // Pattern matching can be used to support multiple states with the same 40 | // transition event. 41 | StartState | SrcState2 + Event3 [ guard3] / action3 = DstState3, 42 | 43 | // ..or wildcarding can be used to allow all states to share a 44 | // transition event. 45 | _ + Event4 = DstState4, 46 | 47 | // States can contain data 48 | StateWithData(u32) + Event = DstState5, 49 | StateWithOtherData(&'a u32) + Event = DstState5, 50 | 51 | // Guards can be logically combined using `!`, `||`, and `&&`. 52 | SrcState6 + Event6 [ async guard6 || other_guard6 ] / action6 = DstState6, 53 | SrcState7 + Event7 [ async guard7 && !other_guard7 ] / action7 = DstState7, 54 | } 55 | // ... 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/sm1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korken89/smlang-rs/1550d3df4202f990463f618ebfe7cdca9304a65e/docs/sm1.png -------------------------------------------------------------------------------- /docs/sm2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korken89/smlang-rs/1550d3df4202f990463f618ebfe7cdca9304a65e/docs/sm2.png -------------------------------------------------------------------------------- /docs/sm3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korken89/smlang-rs/1550d3df4202f990463f618ebfe7cdca9304a65e/docs/sm3.png -------------------------------------------------------------------------------- /examples/async.rs: -------------------------------------------------------------------------------- 1 | //! Async guards and actions example 2 | //! 3 | //! An example of using async guards and actions mixed with standard ones. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | statemachine! { 10 | transitions: { 11 | *State1 + Event1 [guard1] / async action1 = State2, 12 | State2 + Event2 [async guard2 && guard3] / async action2 = State3, 13 | State3 + Event3 / action3 = State4(bool), 14 | } 15 | } 16 | 17 | /// Context with member 18 | pub struct Context { 19 | lock: smol::lock::RwLock, 20 | done: bool, 21 | } 22 | 23 | impl StateMachineContext for Context { 24 | fn guard3(&self) -> Result { 25 | println!("`guard3` called from async context"); 26 | Ok(true) 27 | } 28 | 29 | async fn guard2(&self) -> Result { 30 | println!("`guard2` called from async context"); 31 | let mut lock = self.lock.write().await; 32 | *lock = false; 33 | Ok(true) 34 | } 35 | 36 | fn guard1(&self) -> Result { 37 | println!("`guard1` called from sync context"); 38 | Ok(true) 39 | } 40 | 41 | async fn action2(&mut self) -> Result<(), ()> { 42 | println!("`action2` called from async context"); 43 | if !*self.lock.read().await { 44 | self.done = true; 45 | } 46 | Ok(()) 47 | } 48 | 49 | async fn action1(&mut self) -> Result<(), ()> { 50 | println!("`action1` called from async context"); 51 | let mut lock = self.lock.write().await; 52 | *lock = true; 53 | Ok(()) 54 | } 55 | 56 | fn action3(&mut self) -> Result { 57 | println!("`action3` called from sync context, done = `{}`", self.done); 58 | Ok(self.done) 59 | } 60 | } 61 | 62 | fn main() { 63 | smol::block_on(async { 64 | let mut sm = StateMachine::new(Context { 65 | lock: smol::lock::RwLock::new(false), 66 | done: false, 67 | }); 68 | assert!(matches!(sm.state(), &States::State1)); 69 | 70 | let r = sm.process_event(Events::Event1).await; 71 | assert!(matches!(r, Ok(&States::State2))); 72 | 73 | let r = sm.process_event(Events::Event2).await; 74 | assert!(matches!(r, Ok(&States::State3))); 75 | 76 | let r = sm.process_event(Events::Event3).await; 77 | assert!(matches!(r, Ok(&States::State4(true)))); 78 | 79 | // Now all events will not give any change of state 80 | let r = sm.process_event(Events::Event1).await; 81 | assert!(matches!(r, Err(Error::InvalidEvent))); 82 | assert!(matches!(sm.state(), &States::State4(_))); 83 | 84 | let r = sm.process_event(Events::Event2).await; 85 | assert!(matches!(r, Err(Error::InvalidEvent))); 86 | assert!(matches!(sm.state(), &States::State4(_))); 87 | }); 88 | 89 | // ... 90 | } 91 | -------------------------------------------------------------------------------- /examples/context.rs: -------------------------------------------------------------------------------- 1 | //! Context with members example 2 | //! 3 | //! An example of using the context structure with members for counting the number of transitions. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | statemachine! { 10 | transitions: { 11 | *State1 + Event1 / count_transition1 = State2, 12 | State2 + Event2 / count_transition2 = State1, 13 | } 14 | } 15 | 16 | /// Context with member 17 | pub struct Context { 18 | /// Number of transitions 19 | pub num_transitions: usize, 20 | } 21 | 22 | impl StateMachineContext for Context { 23 | fn count_transition1(&mut self) -> Result<(), ()> { 24 | self.num_transitions += 1; 25 | Ok(()) 26 | } 27 | 28 | fn count_transition2(&mut self) -> Result<(), ()> { 29 | self.num_transitions += 1; 30 | Ok(()) 31 | } 32 | } 33 | 34 | fn main() { 35 | let mut sm = StateMachine::new(Context { num_transitions: 0 }); 36 | 37 | sm.process_event(Events::Event1).ok(); // ++ 38 | sm.process_event(Events::Event1).ok(); // Will fail 39 | sm.process_event(Events::Event2).ok(); // ++ 40 | 41 | assert_eq!(sm.context().num_transitions, 2); 42 | 43 | // ... 44 | } 45 | -------------------------------------------------------------------------------- /examples/dominos.rs: -------------------------------------------------------------------------------- 1 | //! An example of using state data to propagate events (See issue-17) 2 | 3 | #![deny(missing_docs)] 4 | 5 | use smlang::statemachine; 6 | 7 | statemachine! { 8 | states_attr: #[derive(Debug)], 9 | events_attr: #[derive(Debug)], 10 | transitions: { 11 | *D0 + ToD1 / to_d2 = D1, 12 | D1(Option) + ToD2 / to_d3 = D2, 13 | D2(Option) + ToD3 / to_d4 = D3, 14 | D3(Option) + ToD4 / to_d5 = D4, 15 | D4(Option) + ToD5 = D5, 16 | } 17 | } 18 | 19 | /// Context 20 | pub struct Context; 21 | 22 | impl StateMachineContext for Context { 23 | fn to_d2(&mut self) -> Result, ()> { 24 | Ok(Some(Events::ToD2)) 25 | } 26 | 27 | fn to_d3(&mut self, _state_data: &Option) -> Result, ()> { 28 | Ok(Some(Events::ToD3)) 29 | } 30 | 31 | fn to_d4(&mut self, _state_data: &Option) -> Result, ()> { 32 | Ok(Some(Events::ToD4)) 33 | } 34 | 35 | fn to_d5(&mut self, _state_data: &Option) -> Result, ()> { 36 | Ok(Some(Events::ToD5)) 37 | } 38 | fn transition_callback(&self, exit: &States, entry: &States) { 39 | println!("Domino {:?} fell. Next up: {:?}", exit, entry); 40 | } 41 | } 42 | 43 | // The macros does not derive Copy/Clone traits to the events, so we need to add them so that the 44 | // event can be moved out of the state data 45 | impl Copy for Events {} 46 | impl Clone for Events { 47 | fn clone(&self) -> Self { 48 | *self 49 | } 50 | } 51 | 52 | fn main() { 53 | let mut sm = StateMachine::new(Context); 54 | 55 | // first event starts the dominos 56 | let mut event = Some(Events::ToD1); 57 | 58 | // use a while let loop to let the events propagate and the dominos fall 59 | while let Some(e) = event { 60 | let state = sm.process_event(e).unwrap(); 61 | 62 | // use pattern matching to extract the event from any state with an action that fires one 63 | // good practice here NOT to use a wildcard to ensure you don't miss any states 64 | event = match state { 65 | States::D0 => None, 66 | States::D1(event) => *event, 67 | States::D2(event) => *event, 68 | States::D3(event) => *event, 69 | States::D4(event) => *event, 70 | States::D5 => None, 71 | }; 72 | } 73 | 74 | // All the dominos fell! 75 | assert!(matches!(sm.state(), &States::D5)); 76 | } 77 | -------------------------------------------------------------------------------- /examples/event_with_data.rs: -------------------------------------------------------------------------------- 1 | //! Event data example 2 | //! 3 | //! An example of using event data together with a guard and action. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// Event data 10 | #[derive(PartialEq)] 11 | pub struct MyEventData(pub u32); 12 | 13 | statemachine! { 14 | transitions: { 15 | *State1 + Event1(MyEventData) [guard] / action = State2, 16 | // ... 17 | } 18 | } 19 | 20 | /// Context 21 | pub struct Context; 22 | 23 | impl StateMachineContext for Context { 24 | fn guard(&self, event_data: &MyEventData) -> Result { 25 | Ok(event_data == &MyEventData(42)) 26 | } 27 | 28 | fn action(&mut self, event_data: MyEventData) -> Result<(), ()> { 29 | println!("Got valid Event Data = {}", event_data.0); 30 | Ok(()) 31 | } 32 | } 33 | 34 | fn main() { 35 | let mut sm = StateMachine::new(Context); 36 | let result = sm.process_event(Events::Event1(MyEventData(1))); // Guard will fail 37 | 38 | assert!(matches!(result, Err(Error::TransitionsFailed))); 39 | 40 | let result = sm.process_event(Events::Event1(MyEventData(42))); // Guard will pass 41 | 42 | assert!(matches!(result, Ok(&States::State2))); 43 | } 44 | -------------------------------------------------------------------------------- /examples/event_with_mutable_data.rs: -------------------------------------------------------------------------------- 1 | //! Event data example 2 | //! 3 | //! An example of using event data together with a guard and action. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// Event data 10 | #[derive(PartialEq)] 11 | pub struct MyEventData(pub u32); 12 | 13 | statemachine! { 14 | transitions: { 15 | *State1 + Event1(&'a mut MyEventData) [guard] / action = State2, 16 | // ... 17 | } 18 | } 19 | 20 | /// Context 21 | pub struct Context; 22 | 23 | impl StateMachineContext for Context { 24 | fn guard(&self, event_data: &mut MyEventData) -> Result { 25 | event_data.0 = 55; 26 | Ok(true) 27 | } 28 | 29 | fn action(&mut self, event_data: &mut MyEventData) -> Result<(), ()> { 30 | println!("Got valid Event Data = {}", event_data.0); 31 | Ok(()) 32 | } 33 | } 34 | 35 | fn main() { 36 | let mut sm = StateMachine::new(Context); 37 | 38 | let result = sm.process_event(Events::Event1(&mut MyEventData(42))); // Guard will pass 39 | 40 | assert!(matches!(result, Ok(&States::State2))); 41 | } 42 | -------------------------------------------------------------------------------- /examples/event_with_reference_data.rs: -------------------------------------------------------------------------------- 1 | //! Reference types in events 2 | //! 3 | //! A simple example of a state machine which will get events that contain references. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// Reference wrapper 10 | #[derive(Clone, Copy, PartialEq, Debug)] 11 | pub struct MyReferenceWrapper<'a>(pub &'a u32); 12 | 13 | statemachine! { 14 | transitions: { 15 | *State1 + Event1(&'a [u8]) [guard1] / action1 = State2, 16 | State2 + Event2(MyReferenceWrapper<'b>) [guard2] / action2 = State3, 17 | } 18 | } 19 | 20 | /// Context 21 | pub struct Context; 22 | 23 | impl StateMachineContext for Context { 24 | fn guard1(&self, event_data: &[u8]) -> Result { 25 | // Only ok if the slice is not empty 26 | Ok(!event_data.is_empty()) 27 | } 28 | 29 | fn action1(&mut self, event_data: &[u8]) -> Result<(), ()> { 30 | println!("Got valid Event Data = {:?}", event_data); 31 | Ok(()) 32 | } 33 | 34 | fn guard2(&self, event_data: &MyReferenceWrapper) -> Result { 35 | Ok(*event_data.0 > 9000) 36 | } 37 | 38 | fn action2(&mut self, event_data: MyReferenceWrapper) -> Result<(), ()> { 39 | println!("Got valid Event Data = {}", event_data.0); 40 | Ok(()) 41 | } 42 | } 43 | 44 | fn main() { 45 | let mut sm = StateMachine::new(Context); 46 | 47 | let result = sm.process_event(Events::Event1(&[])); // Guard will fail 48 | assert!(matches!(result, Err(Error::TransitionsFailed))); 49 | let result = sm.process_event(Events::Event1(&[1, 2, 3])); // Guard will pass 50 | assert!(matches!(result, Ok(&States::State2))); 51 | 52 | let r = 42; 53 | let result = sm.process_event(Events::Event2(MyReferenceWrapper(&r))); // Guard will fail 54 | assert!(matches!(result, Err(Error::TransitionsFailed))); 55 | 56 | let r = 9001; 57 | let result = sm.process_event(Events::Event2(MyReferenceWrapper(&r))); // Guard will pass 58 | assert!(matches!(result, Ok(States::State3))); 59 | } 60 | -------------------------------------------------------------------------------- /examples/ex1.rs: -------------------------------------------------------------------------------- 1 | //! Linear state machine 2 | //! 3 | //! A simple example of a state machine which will get stuck in the final state. 4 | //! A picture depicting the state machine can be found in the README. 5 | 6 | #![deny(missing_docs)] 7 | 8 | use smlang::statemachine; 9 | 10 | statemachine! { 11 | transitions: { 12 | *State1 + Event1 = State2, 13 | State2 + Event2 = State3, 14 | }, 15 | } 16 | 17 | /// Context 18 | pub struct Context; 19 | 20 | impl StateMachineContext for Context {} 21 | 22 | fn main() { 23 | let mut sm = StateMachine::new(Context); 24 | assert!(matches!(sm.state(), &States::State1)); 25 | 26 | let r = sm.process_event(Events::Event1); 27 | assert!(matches!(r, Ok(&States::State2))); 28 | 29 | let r = sm.process_event(Events::Event2); 30 | assert!(matches!(r, Ok(&States::State3))); 31 | 32 | // Now all events will not give any change of state 33 | let r = sm.process_event(Events::Event1); 34 | assert!(matches!(r, Err(Error::InvalidEvent))); 35 | assert!(matches!(sm.state(), &States::State3)); 36 | 37 | let r = sm.process_event(Events::Event2); 38 | assert!(matches!(r, Err(Error::InvalidEvent))); 39 | assert!(matches!(sm.state(), &States::State3)); 40 | } 41 | -------------------------------------------------------------------------------- /examples/ex2.rs: -------------------------------------------------------------------------------- 1 | //! Looping state machine 2 | //! 3 | //! An example of a state machine which will loop between State 2 and State 3. 4 | //! A picture depicting the state machine can be found in the README. 5 | 6 | #![deny(missing_docs)] 7 | 8 | use smlang::statemachine; 9 | 10 | statemachine! { 11 | transitions: { 12 | *State1 + Event1 = State2, 13 | State2 + Event2 = State3, 14 | State3 + Event3 = State2, 15 | } 16 | } 17 | 18 | /// Context 19 | pub struct Context; 20 | 21 | impl StateMachineContext for Context {} 22 | 23 | fn main() { 24 | let mut sm = StateMachine::new(Context); 25 | assert!(matches!(sm.state(), &States::State1)); 26 | 27 | let r = sm.process_event(Events::Event1); 28 | assert!(matches!(r, Ok(&States::State2))); 29 | 30 | let r = sm.process_event(Events::Event2); 31 | assert!(matches!(r, Ok(&States::State3))); 32 | 33 | // Go back in the loop a few time 34 | let r = sm.process_event(Events::Event3); 35 | assert!(matches!(r, Ok(&States::State2))); 36 | 37 | let r = sm.process_event(Events::Event2); 38 | assert!(matches!(r, Ok(&States::State3))); 39 | 40 | let r = sm.process_event(Events::Event3); 41 | assert!(matches!(r, Ok(&States::State2))); 42 | 43 | // Now we cannot use Event1 again, as it is outside the state machine loop 44 | let r = sm.process_event(Events::Event1); 45 | assert!(matches!(r, Err(Error::InvalidEvent))); 46 | assert!(matches!(sm.state(), &States::State2)); 47 | } 48 | -------------------------------------------------------------------------------- /examples/ex3.rs: -------------------------------------------------------------------------------- 1 | //! Looping state machine 2 | //! 3 | //! An example of using guards and actions. 4 | //! A picture depicting the state machine can be found in the README. 5 | 6 | #![deny(missing_docs)] 7 | 8 | use smlang::statemachine; 9 | 10 | statemachine! { 11 | transitions: { 12 | *State1 + Event1 [guard] / action1 = State2, 13 | State2 + Event2 [guard_fail] / action2 = State3, 14 | } 15 | } 16 | 17 | /// Context 18 | pub struct Context; 19 | 20 | impl StateMachineContext for Context { 21 | fn guard(&self) -> Result { 22 | // Always ok 23 | Ok(true) 24 | } 25 | 26 | fn guard_fail(&self) -> Result { 27 | // Always fail 28 | Ok(false) 29 | } 30 | 31 | fn action1(&mut self) -> Result<(), ()> { 32 | //println!("Action 1"); 33 | Ok(()) 34 | } 35 | 36 | fn action2(&mut self) -> Result<(), ()> { 37 | //println!("Action 1"); 38 | Ok(()) 39 | } 40 | } 41 | 42 | fn main() { 43 | let mut sm = StateMachine::new(Context); 44 | assert!(matches!(sm.state(), &States::State1)); 45 | 46 | println!("Before action 1"); 47 | 48 | // Go through the first guard and action 49 | let r = sm.process_event(Events::Event1); 50 | assert!(matches!(r, Ok(&States::State2))); 51 | 52 | println!("After action 1"); 53 | 54 | println!("Before action 2"); 55 | 56 | // The action will never run as the guard will fail 57 | let r = sm.process_event(Events::Event2); 58 | assert!(matches!(r, Err(Error::TransitionsFailed))); 59 | 60 | println!("After action 2"); 61 | 62 | // Now we are stuck due to the guard never returning true 63 | assert!(matches!(sm.state(), &States::State2)); 64 | } 65 | -------------------------------------------------------------------------------- /examples/guard_action_syntax.rs: -------------------------------------------------------------------------------- 1 | //! Guard and action syntax example 2 | //! 3 | //! An example of using guards and actions with state and event data. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// Event data 10 | #[derive(PartialEq)] 11 | pub struct MyEventData(pub u32); 12 | 13 | /// State data 14 | #[derive(PartialEq)] 15 | pub struct MyStateData(pub u32); 16 | 17 | statemachine! { 18 | transitions: { 19 | *State1 + Event1(MyEventData) [guard1] / action1 = State2, 20 | State2(MyStateData) + Event2 [guard2] / action2 = State3, 21 | // ... 22 | } 23 | } 24 | 25 | /// Context 26 | pub struct Context; 27 | 28 | impl StateMachineContext for Context { 29 | // Guard1 has access to the data from Event1 30 | fn guard1(&self, _event_data: &MyEventData) -> Result { 31 | todo!() 32 | } 33 | 34 | // Action1 has access to the data from Event1, and need to return the state data for State2 35 | fn action1(&mut self, _event_data: MyEventData) -> Result { 36 | todo!() 37 | } 38 | 39 | // Guard2 has access to the data from State2 40 | fn guard2(&self, _state_data: &MyStateData) -> Result { 41 | todo!() 42 | } 43 | 44 | // Action2 has access to the data from State2 45 | fn action2(&mut self, _state_data: &MyStateData) -> Result<(), ()> { 46 | todo!() 47 | } 48 | } 49 | 50 | fn main() {} 51 | -------------------------------------------------------------------------------- /examples/guard_action_syntax_with_temporary_context.rs: -------------------------------------------------------------------------------- 1 | //! Guard and action syntax example 2 | //! 3 | //! An example of using guards and actions with state and event data. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// Event data 10 | #[derive(PartialEq)] 11 | pub struct MyEventData(pub u32); 12 | 13 | /// State data 14 | #[derive(PartialEq)] 15 | pub struct MyStateData(pub u32); 16 | 17 | statemachine! { 18 | temporary_context: &mut u16, 19 | transitions: { 20 | *State1 + Event1(MyEventData) [guard1] / action1 = State2, 21 | State2(MyStateData) + Event2 [guard2] / action2 = State3, 22 | // ... 23 | }, 24 | } 25 | 26 | /// Context 27 | pub struct Context; 28 | 29 | impl StateMachineContext for Context { 30 | // Guard1 has access to the data from Event1 31 | fn guard1(&self, temp_context: &mut u16, _event_data: &MyEventData) -> Result { 32 | *temp_context += 1; 33 | 34 | Ok(true) 35 | } 36 | 37 | // Action1 has access to the data from Event1, and need to return the state data for State2 38 | fn action1( 39 | &mut self, 40 | temp_context: &mut u16, 41 | _event_data: MyEventData, 42 | ) -> Result { 43 | *temp_context += 1; 44 | 45 | Ok(MyStateData(1)) 46 | } 47 | 48 | // Guard2 has access to the data from State2 49 | fn guard2(&self, temp_context: &mut u16, _state_data: &MyStateData) -> Result { 50 | *temp_context += 1; 51 | 52 | Ok(true) 53 | } 54 | 55 | // Action2 has access to the data from State2 56 | fn action2(&mut self, temp_context: &mut u16, _state_data: &MyStateData) -> Result<(), ()> { 57 | *temp_context += 1; 58 | Ok(()) 59 | } 60 | } 61 | 62 | fn main() { 63 | let mut sm = StateMachine::new(Context {}); 64 | let mut val = 0; 65 | 66 | // This invocation will go through 1 guard and one action. 67 | let r = sm 68 | .process_event(&mut val, Events::Event1(MyEventData(1))) 69 | .unwrap(); 70 | 71 | assert!(r == &States::State2(MyStateData(1))); 72 | assert_eq!(val, 2); 73 | } 74 | -------------------------------------------------------------------------------- /examples/guard_custom_error.rs: -------------------------------------------------------------------------------- 1 | //! Guard and action syntax example 2 | //! 3 | //! An example of using guards and actions with state and event data. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// Custom guard errors 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 11 | pub enum GuardError { 12 | /// This is a custom guard error variant 13 | Custom, 14 | } 15 | 16 | /// Event data 17 | #[derive(PartialEq)] 18 | pub struct MyEventData(pub u32); 19 | 20 | /// State data 21 | #[derive(PartialEq)] 22 | pub struct MyStateData(pub u32); 23 | 24 | statemachine! { 25 | transitions: { 26 | *State1 + Event1(MyEventData) [guard1] / action1 = State2, 27 | State2(MyStateData) + Event2 [guard2] / action2 = State3, 28 | // ... 29 | }, 30 | custom_error: true, 31 | } 32 | 33 | /// Context 34 | pub struct Context; 35 | 36 | impl StateMachineContext for Context { 37 | type Error = GuardError; // Guard1 has access to the data from Event1 38 | fn guard1(&self, _event_data: &MyEventData) -> Result { 39 | Err(GuardError::Custom) 40 | } 41 | 42 | // Action1 has access to the data from Event1, and need to return the state data for State2 43 | fn action1(&mut self, _event_data: MyEventData) -> Result { 44 | todo!() 45 | } 46 | 47 | // Guard2 has access to the data from State2 48 | fn guard2(&self, _state_data: &MyStateData) -> Result { 49 | todo!() 50 | } 51 | 52 | // Action2 has access to the data from State2 53 | fn action2(&mut self, _state_data: &MyStateData) -> Result<(), Self::Error> { 54 | todo!() 55 | } 56 | } 57 | 58 | fn main() { 59 | let mut sm = StateMachine::new(Context {}); 60 | 61 | let r = sm.process_event(Events::Event1(MyEventData(1))); 62 | 63 | assert!(matches!(r, Err(Error::GuardFailed(GuardError::Custom)))); 64 | } 65 | -------------------------------------------------------------------------------- /examples/input_state_pattern_match.rs: -------------------------------------------------------------------------------- 1 | //! Pattern Matching State Machine 2 | //! 3 | //! This demonstrates the use of input state pattern matching so that states that share a common 4 | //! transition to the same output state can be described more succinctly 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | // statemachine! { 10 | // transitions: { 11 | // *Idle + Charge = Charging, 12 | // Idle + Discharge = Discharging, 13 | // Charging + ChargeComplete = Charged, 14 | // Discharging + DischargeComplete = Discharged, 15 | // Charged + Discharge = Discharging, 16 | // Dischaged + Charge = Charging, 17 | // Charging + Discharge = Discharging, 18 | // Discharging + Charge = Charging, 19 | // Idle + FaultDetected = Fault, 20 | // Charging + FaultDetected = Fault, 21 | // Discharging + FaultDetected = Fault, 22 | // Charged + FaultDetected = Fault, 23 | // Discharged + FaultDetected = Fault, 24 | // Fault + FaultCleard = Idle, 25 | // }, 26 | // } 27 | 28 | // A simple charge/discharge state machine that has a dedicated "Fault" state 29 | statemachine! { 30 | transitions: { 31 | *Idle | Discharging | Discharged + Charge = Charging, 32 | Idle | Charging | Charged + Discharge = Discharging, 33 | Charging + ChargeComplete = Charged, 34 | Discharging + DischargeComplete = Discharged, 35 | _ + FaultDetected = Fault, 36 | Fault + FaultCleard = Idle, 37 | }, 38 | } 39 | 40 | /// Context 41 | pub struct Context; 42 | 43 | impl StateMachineContext for Context {} 44 | 45 | fn main() { 46 | let mut sm = StateMachine::new(Context); 47 | 48 | assert!(matches!(sm.state(), &States::Idle)); 49 | 50 | let r = sm.process_event(Events::Charge); 51 | assert!(matches!(r, Ok(&States::Charging))); 52 | 53 | let r = sm.process_event(Events::Discharge); 54 | assert!(matches!(r, Ok(&States::Discharging))); 55 | 56 | let r = sm.process_event(Events::Charge); 57 | assert!(matches!(r, Ok(&States::Charging))); 58 | 59 | let r = sm.process_event(Events::ChargeComplete); 60 | assert!(matches!(r, Ok(&States::Charged))); 61 | 62 | let r = sm.process_event(Events::Charge); 63 | assert!(matches!(r, Err(Error::InvalidEvent))); 64 | assert!(matches!(sm.state(), &States::Charged)); 65 | 66 | let r = sm.process_event(Events::Discharge); 67 | assert!(matches!(r, Ok(&States::Discharging))); 68 | 69 | let r = sm.process_event(Events::DischargeComplete); 70 | assert!(matches!(r, Ok(&States::Discharged))); 71 | 72 | let r = sm.process_event(Events::Discharge); 73 | assert!(matches!(r, Err(Error::InvalidEvent))); 74 | assert!(matches!(sm.state(), &States::Discharged)); 75 | 76 | sm = StateMachine::new_with_state(Context, States::Idle); 77 | let r = sm.process_event(Events::FaultDetected); 78 | assert!(matches!(r, Ok(&States::Fault))); 79 | 80 | sm = StateMachine::new_with_state(Context, States::Charging); 81 | let r = sm.process_event(Events::FaultDetected); 82 | assert!(matches!(r, Ok(&States::Fault))); 83 | 84 | sm = StateMachine::new_with_state(Context, States::Charged); 85 | let r = sm.process_event(Events::FaultDetected); 86 | assert!(matches!(r, Ok(&States::Fault))); 87 | 88 | sm = StateMachine::new_with_state(Context, States::Discharging); 89 | let r = sm.process_event(Events::FaultDetected); 90 | assert!(matches!(r, Ok(&States::Fault))); 91 | 92 | sm = StateMachine::new_with_state(Context, States::Discharged); 93 | let r = sm.process_event(Events::FaultDetected); 94 | assert!(matches!(r, Ok(&States::Fault))); 95 | 96 | let r = sm.process_event(Events::Charge); 97 | assert!(matches!(r, Err(Error::InvalidEvent))); 98 | assert!(matches!(sm.state(), &States::Fault)); 99 | 100 | let r = sm.process_event(Events::Discharge); 101 | assert!(matches!(r, Err(Error::InvalidEvent))); 102 | assert!(matches!(sm.state(), &States::Fault)); 103 | 104 | let r = sm.process_event(Events::ChargeComplete); 105 | assert!(matches!(r, Err(Error::InvalidEvent))); 106 | assert!(matches!(sm.state(), &States::Fault)); 107 | 108 | let r = sm.process_event(Events::DischargeComplete); 109 | assert!(matches!(r, Err(Error::InvalidEvent))); 110 | assert!(matches!(sm.state(), &States::Fault)); 111 | } 112 | -------------------------------------------------------------------------------- /examples/named_async.rs: -------------------------------------------------------------------------------- 1 | //! Async guards and actions example 2 | //! 3 | //! An example of using async guards and actions mixed with standard ones. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | statemachine! { 10 | name: AsyncSimple, 11 | transitions: { 12 | *State1 + Event1 [guard1] / async action1 = State2, 13 | State2 + Event2 [async guard2] / async action2 = State3, 14 | State3 + Event3 / action3 = State4(bool), 15 | } 16 | } 17 | 18 | /// Context with member 19 | pub struct Context { 20 | lock: smol::lock::RwLock, 21 | done: bool, 22 | } 23 | 24 | impl AsyncSimpleStateMachineContext for Context { 25 | fn guard1(&self) -> Result { 26 | println!("`guard1` called from sync context"); 27 | Ok(true) 28 | } 29 | 30 | async fn guard2(&self) -> Result { 31 | println!("`guard2` called from async context"); 32 | let mut lock = self.lock.write().await; 33 | *lock = false; 34 | Ok(true) 35 | } 36 | 37 | fn action3(&mut self) -> Result { 38 | println!("`action3` called from sync context, done = `{}`", self.done); 39 | Ok(self.done) 40 | } 41 | 42 | async fn action1(&mut self) -> Result<(), ()> { 43 | println!("`action1` called from async context"); 44 | let mut lock = self.lock.write().await; 45 | *lock = true; 46 | Ok(()) 47 | } 48 | 49 | async fn action2(&mut self) -> Result<(), ()> { 50 | println!("`action2` called from async context"); 51 | if !*self.lock.read().await { 52 | self.done = true; 53 | } 54 | Ok(()) 55 | } 56 | } 57 | 58 | fn main() { 59 | smol::block_on(async { 60 | let mut sm = AsyncSimpleStateMachine::new(Context { 61 | lock: smol::lock::RwLock::new(false), 62 | done: false, 63 | }); 64 | assert!(matches!(sm.state(), &AsyncSimpleStates::State1)); 65 | 66 | let r = sm.process_event(AsyncSimpleEvents::Event1).await; 67 | assert!(matches!(r, Ok(&AsyncSimpleStates::State2))); 68 | 69 | let r = sm.process_event(AsyncSimpleEvents::Event2).await; 70 | assert!(matches!(r, Ok(&AsyncSimpleStates::State3))); 71 | 72 | let r = sm.process_event(AsyncSimpleEvents::Event3).await; 73 | assert!(matches!(r, Ok(&AsyncSimpleStates::State4(true)))); 74 | 75 | // Now all events will not give any change of state 76 | let r = sm.process_event(AsyncSimpleEvents::Event1).await; 77 | assert!(matches!(r, Err(AsyncSimpleError::InvalidEvent))); 78 | assert!(matches!(sm.state(), &AsyncSimpleStates::State4(_))); 79 | 80 | let r = sm.process_event(AsyncSimpleEvents::Event2).await; 81 | assert!(matches!(r, Err(AsyncSimpleError::InvalidEvent))); 82 | assert!(matches!(sm.state(), &AsyncSimpleStates::State4(_))); 83 | }); 84 | 85 | // ... 86 | } 87 | -------------------------------------------------------------------------------- /examples/named_dominos.rs: -------------------------------------------------------------------------------- 1 | //! An example of using state data to propagate events (See issue-17) 2 | 3 | #![deny(missing_docs)] 4 | 5 | use smlang::statemachine; 6 | 7 | statemachine! { 8 | name: Dominos, 9 | transitions: { 10 | *D0 + ToD1 / to_d2 = D1, 11 | D1(Option) + ToD2 / to_d3 = D2, 12 | D2(Option) + ToD3 / to_d4 = D3, 13 | D3(Option) + ToD4 / to_d5 = D4, 14 | D4(Option) + ToD5 = D5, 15 | } 16 | } 17 | 18 | /// Context 19 | pub struct Context; 20 | 21 | impl DominosStateMachineContext for Context { 22 | fn to_d2(&mut self) -> Result, ()> { 23 | Ok(Some(DominosEvents::ToD2)) 24 | } 25 | 26 | fn to_d3(&mut self, _state_data: &Option) -> Result, ()> { 27 | Ok(Some(DominosEvents::ToD3)) 28 | } 29 | 30 | fn to_d4(&mut self, _state_data: &Option) -> Result, ()> { 31 | Ok(Some(DominosEvents::ToD4)) 32 | } 33 | 34 | fn to_d5(&mut self, _state_data: &Option) -> Result, ()> { 35 | Ok(Some(DominosEvents::ToD5)) 36 | } 37 | } 38 | 39 | // The macros does not derive Copy/Clone traits to the events, so we need to add them so that the 40 | // event can be moved out of the state data 41 | impl Copy for DominosEvents {} 42 | impl Clone for DominosEvents { 43 | fn clone(&self) -> Self { 44 | *self 45 | } 46 | } 47 | 48 | fn main() { 49 | let mut sm = DominosStateMachine::new(Context); 50 | 51 | // first event starts the dominos 52 | let mut event = Some(DominosEvents::ToD1); 53 | 54 | // use a while let loop to let the events propagate and the dominos fall 55 | while let Some(e) = event { 56 | let state = sm.process_event(e).unwrap(); 57 | 58 | // use pattern matching to extract the event from any state with an action that fires one 59 | // good practice here NOT to use a wildcard to ensure you don't miss any states 60 | event = match state { 61 | DominosStates::D0 => None, 62 | DominosStates::D1(event) => *event, 63 | DominosStates::D2(event) => *event, 64 | DominosStates::D3(event) => *event, 65 | DominosStates::D4(event) => *event, 66 | DominosStates::D5 => None, 67 | }; 68 | } 69 | 70 | // All the dominos fell! 71 | assert!(matches!(sm.state(), &DominosStates::D5)); 72 | } 73 | -------------------------------------------------------------------------------- /examples/named_ex1.rs: -------------------------------------------------------------------------------- 1 | //! Linear state machine 2 | //! 3 | //! A simple example of a state machine which will get stuck in the final state. 4 | //! A picture depicting the state machine can be found in the README. 5 | 6 | #![deny(missing_docs)] 7 | 8 | use smlang::statemachine; 9 | 10 | statemachine! { 11 | name: Linear, 12 | states_attr: #[derive(Debug)], 13 | transitions: { 14 | *State1 + Event1 = State2, 15 | State2 + Event2 = State3, 16 | }, 17 | } 18 | 19 | /// Context 20 | pub struct Context; 21 | 22 | impl LinearStateMachineContext for Context {} 23 | 24 | fn main() { 25 | let mut sm = LinearStateMachine::new(Context); 26 | assert!(matches!(sm.state(), &LinearStates::State1)); 27 | 28 | let r = sm.process_event(LinearEvents::Event1); 29 | assert!(matches!(r, Ok(&LinearStates::State2))); 30 | 31 | let r = sm.process_event(LinearEvents::Event2); 32 | assert!(matches!(r, Ok(&LinearStates::State3))); 33 | 34 | // Now all events will not give any change of state 35 | let r = sm.process_event(LinearEvents::Event1); 36 | assert!(matches!(r, Err(LinearError::InvalidEvent))); 37 | assert!(matches!(sm.state(), &LinearStates::State3)); 38 | 39 | let r = sm.process_event(LinearEvents::Event2); 40 | assert!(matches!(r, Err(LinearError::InvalidEvent))); 41 | assert!(matches!(sm.state(), &LinearStates::State3)); 42 | } 43 | -------------------------------------------------------------------------------- /examples/named_ex2.rs: -------------------------------------------------------------------------------- 1 | //! Looping state machine 2 | //! 3 | //! An example of a state machine which will loop between State 2 and State 3. 4 | //! A picture depicting the state machine can be found in the README. 5 | 6 | #![deny(missing_docs)] 7 | 8 | use smlang::statemachine; 9 | 10 | statemachine! { 11 | name: Looping, 12 | transitions: { 13 | *State1 + Event1 = State2, 14 | State2 + Event2 = State3, 15 | State3 + Event3 = State2, 16 | } 17 | } 18 | 19 | /// Context 20 | pub struct Context; 21 | 22 | impl LoopingStateMachineContext for Context {} 23 | 24 | fn main() { 25 | let mut sm = LoopingStateMachine::new(Context); 26 | assert!(matches!(sm.state(), &LoopingStates::State1)); 27 | 28 | let r = sm.process_event(LoopingEvents::Event1); 29 | assert!(matches!(r, Ok(&LoopingStates::State2))); 30 | 31 | let r = sm.process_event(LoopingEvents::Event2); 32 | assert!(matches!(r, Ok(&LoopingStates::State3))); 33 | 34 | // Go back in the loop a few time 35 | let r = sm.process_event(LoopingEvents::Event3); 36 | assert!(matches!(r, Ok(&LoopingStates::State2))); 37 | 38 | let r = sm.process_event(LoopingEvents::Event2); 39 | assert!(matches!(r, Ok(&LoopingStates::State3))); 40 | 41 | let r = sm.process_event(LoopingEvents::Event3); 42 | assert!(matches!(r, Ok(&LoopingStates::State2))); 43 | 44 | // Now we cannot use Event1 again, as it is outside the state machine loop 45 | let r = sm.process_event(LoopingEvents::Event1); 46 | assert!(matches!(r, Err(LoopingError::InvalidEvent))); 47 | assert!(matches!(sm.state(), &LoopingStates::State2)); 48 | } 49 | -------------------------------------------------------------------------------- /examples/named_ex3.rs: -------------------------------------------------------------------------------- 1 | //! Looping state machine 2 | //! 3 | //! An example of using guards and actions. 4 | //! A picture depicting the state machine can be found in the README. 5 | 6 | #![deny(missing_docs)] 7 | 8 | use smlang::statemachine; 9 | 10 | statemachine! { 11 | name: LoopingWithGuards, 12 | transitions: { 13 | *State1 + Event1 [guard] / action1 = State2, 14 | State2 + Event2 [guard_fail] / action2 = State3, 15 | } 16 | } 17 | 18 | /// Context 19 | pub struct Context; 20 | 21 | impl LoopingWithGuardsStateMachineContext for Context { 22 | fn guard(&self) -> Result { 23 | // Always ok 24 | Ok(true) 25 | } 26 | 27 | fn guard_fail(&self) -> Result { 28 | // Always fail 29 | Ok(false) 30 | } 31 | 32 | fn action1(&mut self) -> Result<(), ()> { 33 | //println!("Action 1"); 34 | Ok(()) 35 | } 36 | 37 | fn action2(&mut self) -> Result<(), ()> { 38 | //println!("Action 1"); 39 | Ok(()) 40 | } 41 | } 42 | 43 | fn main() { 44 | let mut sm = LoopingWithGuardsStateMachine::new(Context); 45 | assert!(matches!(sm.state(), &LoopingWithGuardsStates::State1)); 46 | 47 | println!("Before action 1"); 48 | 49 | // Go through the first guard and action 50 | let r = sm.process_event(LoopingWithGuardsEvents::Event1); 51 | assert!(matches!(r, Ok(&LoopingWithGuardsStates::State2))); 52 | 53 | println!("After action 1"); 54 | 55 | println!("Before action 2"); 56 | 57 | // The action will never run as the guard will fail 58 | let r = sm.process_event(LoopingWithGuardsEvents::Event2); 59 | assert!(matches!(r, Err(LoopingWithGuardsError::TransitionsFailed))); 60 | 61 | println!("After action 2"); 62 | 63 | // Now we are stuck due to the guard never returning true 64 | assert!(matches!(sm.state(), &LoopingWithGuardsStates::State2)); 65 | } 66 | -------------------------------------------------------------------------------- /examples/named_input_state_pattern_match.rs: -------------------------------------------------------------------------------- 1 | //! Pattern Matching State Machine 2 | //! 3 | //! This demonstrates the use of input state pattern matching so that states that share a common 4 | //! transition to the same output state can be described more succinctly 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | // statemachine! { 10 | // transitions: { 11 | // *Idle + Charge = Charging, 12 | // Idle + Discharge = Discharging, 13 | // Charging + ChargeComplete = Charged, 14 | // Discharging + DischargeComplete = Discharged, 15 | // Charged + Discharge = Discharging, 16 | // Dischaged + Charge = Charging, 17 | // Charging + Discharge = Discharging, 18 | // Discharging + Charge = Charging, 19 | // Idle + FaultDetected = Fault, 20 | // Charging + FaultDetected = Fault, 21 | // Discharging + FaultDetected = Fault, 22 | // Charged + FaultDetected = Fault, 23 | // Discharged + FaultDetected = Fault, 24 | // Fault + FaultCleard = Idle, 25 | // }, 26 | // } 27 | 28 | // A simple charge/discharge state machine that has a dedicated "Fault" state 29 | statemachine! { 30 | name: Battery, 31 | transitions: { 32 | *Idle | Discharging | Discharged + Charge = Charging, 33 | Idle | Charging | Charged + Discharge = Discharging, 34 | Charging + ChargeComplete = Charged, 35 | Discharging + DischargeComplete = Discharged, 36 | _ + FaultDetected = Fault, 37 | Fault + FaultCleard = Idle, 38 | }, 39 | } 40 | 41 | /// Context 42 | pub struct Context; 43 | 44 | impl BatteryStateMachineContext for Context {} 45 | 46 | fn main() { 47 | let mut sm = BatteryStateMachine::new(Context); 48 | 49 | assert!(matches!(sm.state(), &BatteryStates::Idle)); 50 | 51 | let r = sm.process_event(BatteryEvents::Charge); 52 | assert!(matches!(r, Ok(&BatteryStates::Charging))); 53 | 54 | let r = sm.process_event(BatteryEvents::Discharge); 55 | assert!(matches!(r, Ok(&BatteryStates::Discharging))); 56 | 57 | let r = sm.process_event(BatteryEvents::Charge); 58 | assert!(matches!(r, Ok(&BatteryStates::Charging))); 59 | 60 | let r = sm.process_event(BatteryEvents::ChargeComplete); 61 | assert!(matches!(r, Ok(&BatteryStates::Charged))); 62 | 63 | let r = sm.process_event(BatteryEvents::Charge); 64 | assert!(matches!(r, Err(BatteryError::InvalidEvent))); 65 | assert!(matches!(sm.state(), &BatteryStates::Charged)); 66 | 67 | let r = sm.process_event(BatteryEvents::Discharge); 68 | assert!(matches!(r, Ok(&BatteryStates::Discharging))); 69 | 70 | let r = sm.process_event(BatteryEvents::DischargeComplete); 71 | assert!(matches!(r, Ok(&BatteryStates::Discharged))); 72 | 73 | let r = sm.process_event(BatteryEvents::Discharge); 74 | assert!(matches!(r, Err(BatteryError::InvalidEvent))); 75 | assert!(matches!(sm.state(), &BatteryStates::Discharged)); 76 | 77 | sm = BatteryStateMachine::new_with_state(Context, BatteryStates::Idle); 78 | let r = sm.process_event(BatteryEvents::FaultDetected); 79 | assert!(matches!(r, Ok(&BatteryStates::Fault))); 80 | 81 | sm = BatteryStateMachine::new_with_state(Context, BatteryStates::Charging); 82 | let r = sm.process_event(BatteryEvents::FaultDetected); 83 | assert!(matches!(r, Ok(&BatteryStates::Fault))); 84 | 85 | sm = BatteryStateMachine::new_with_state(Context, BatteryStates::Charged); 86 | let r = sm.process_event(BatteryEvents::FaultDetected); 87 | assert!(matches!(r, Ok(&BatteryStates::Fault))); 88 | 89 | sm = BatteryStateMachine::new_with_state(Context, BatteryStates::Discharging); 90 | let r = sm.process_event(BatteryEvents::FaultDetected); 91 | assert!(matches!(r, Ok(&BatteryStates::Fault))); 92 | 93 | sm = BatteryStateMachine::new_with_state(Context, BatteryStates::Discharged); 94 | let r = sm.process_event(BatteryEvents::FaultDetected); 95 | assert!(matches!(r, Ok(&BatteryStates::Fault))); 96 | 97 | let r = sm.process_event(BatteryEvents::Charge); 98 | assert!(matches!(r, Err(BatteryError::InvalidEvent))); 99 | assert!(matches!(sm.state(), &BatteryStates::Fault)); 100 | 101 | let r = sm.process_event(BatteryEvents::Discharge); 102 | assert!(matches!(r, Err(BatteryError::InvalidEvent))); 103 | assert!(matches!(sm.state(), &BatteryStates::Fault)); 104 | 105 | let r = sm.process_event(BatteryEvents::ChargeComplete); 106 | assert!(matches!(r, Err(BatteryError::InvalidEvent))); 107 | assert!(matches!(sm.state(), &BatteryStates::Fault)); 108 | 109 | let r = sm.process_event(BatteryEvents::DischargeComplete); 110 | assert!(matches!(r, Err(BatteryError::InvalidEvent))); 111 | assert!(matches!(sm.state(), &BatteryStates::Fault)); 112 | } 113 | -------------------------------------------------------------------------------- /examples/named_state_with_data.rs: -------------------------------------------------------------------------------- 1 | //! State data example 2 | //! 3 | //! An example of using state data together with an action. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// State data 10 | #[derive(PartialEq)] 11 | pub struct MyStateData(pub u32); 12 | 13 | statemachine! { 14 | name: StatesWithData, 15 | transitions: { 16 | *State1 + Event1 / action = State2, 17 | State2(MyStateData) + Event2 = State1, 18 | // ... 19 | } 20 | } 21 | 22 | /// Context 23 | pub struct Context; 24 | 25 | impl StatesWithDataStateMachineContext for Context { 26 | fn action(&mut self) -> Result { 27 | Ok(MyStateData(42)) 28 | } 29 | } 30 | 31 | fn main() { 32 | let mut sm = StatesWithDataStateMachine::new(Context); 33 | let result = sm.process_event(StatesWithDataEvents::Event1); 34 | 35 | assert!(matches!( 36 | result, 37 | Ok(&StatesWithDataStates::State2(MyStateData(42))) 38 | )); 39 | } 40 | -------------------------------------------------------------------------------- /examples/named_state_with_reference_data.rs: -------------------------------------------------------------------------------- 1 | //! State data example 2 | //! 3 | //! An example of using referenced state data with lifetimes together with an action. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// State data 10 | #[derive(PartialEq)] 11 | pub struct MyStateData<'a>(&'a u32); 12 | 13 | statemachine! { 14 | name: StatesWithRefData, 15 | transitions: { 16 | *State1 + Event1 / action = State2, 17 | State2(MyStateData<'a>) + Event2 = State1, 18 | // ... 19 | } 20 | } 21 | 22 | /// Context 23 | pub struct Context; 24 | 25 | impl StatesWithRefDataStateMachineContext for Context { 26 | fn action<'a>(&mut self) -> Result, ()> { 27 | Ok(MyStateData(&42)) 28 | } 29 | } 30 | 31 | fn main() { 32 | let mut sm = StatesWithRefDataStateMachine::new(Context); 33 | let result = sm.process_event(StatesWithRefDataEvents::Event1); 34 | 35 | assert!(matches!( 36 | result, 37 | Ok(&StatesWithRefDataStates::State2(MyStateData(&42))) 38 | )); 39 | } 40 | -------------------------------------------------------------------------------- /examples/on_entry_on_exit_generic.rs: -------------------------------------------------------------------------------- 1 | //! An example of using state data to propagate events (See issue-17) 2 | 3 | #![deny(missing_docs)] 4 | 5 | use smlang::statemachine; 6 | 7 | statemachine! { 8 | name: OnEntryExample, 9 | transitions: { 10 | *D0 + ToD1 = D1, 11 | D0 + ToD3 = D3, 12 | D1 + ToD2 = D2, 13 | D2 + ToD1 = D1, 14 | D1 + ToD0 = D0, 15 | }, 16 | } 17 | 18 | /// Context 19 | pub struct Context { 20 | exited_d0: i32, 21 | entered_d1: i32, 22 | } 23 | 24 | impl OnEntryExampleStateMachineContext for Context { 25 | fn on_exit_d0(&mut self) { 26 | self.exited_d0 += 1; 27 | } 28 | fn on_entry_d1(&mut self) { 29 | self.entered_d1 += 1; 30 | } 31 | } 32 | 33 | fn main() { 34 | let mut sm = OnEntryExampleStateMachine::new(Context { 35 | exited_d0: 0, 36 | entered_d1: 0, 37 | }); 38 | 39 | // first event starts the dominos 40 | let _ = sm.process_event(OnEntryExampleEvents::ToD1).unwrap(); 41 | 42 | assert!(matches!(sm.state(), &OnEntryExampleStates::D1)); 43 | assert_eq!(sm.context().exited_d0, 1); 44 | assert_eq!(sm.context().entered_d1, 1); 45 | 46 | let _ = sm.process_event(OnEntryExampleEvents::ToD2).unwrap(); 47 | 48 | assert!(matches!(sm.state(), &OnEntryExampleStates::D2)); 49 | assert_eq!(sm.context().exited_d0, 1); 50 | assert_eq!(sm.context().entered_d1, 1); 51 | 52 | let _ = sm.process_event(OnEntryExampleEvents::ToD1).unwrap(); 53 | 54 | assert!(matches!(sm.state(), &OnEntryExampleStates::D1)); 55 | assert_eq!(sm.context().exited_d0, 1); 56 | assert_eq!(sm.context().entered_d1, 2); 57 | 58 | let _ = sm.process_event(OnEntryExampleEvents::ToD0).unwrap(); 59 | 60 | assert!(matches!(sm.state(), &OnEntryExampleStates::D0)); 61 | assert_eq!(sm.context().exited_d0, 1); 62 | assert_eq!(sm.context().entered_d1, 2); 63 | 64 | let _ = sm.process_event(OnEntryExampleEvents::ToD3).unwrap(); 65 | 66 | assert!(matches!(sm.state(), &OnEntryExampleStates::D3)); 67 | assert_eq!(sm.context().exited_d0, 2); 68 | assert_eq!(sm.context().entered_d1, 2); 69 | } 70 | -------------------------------------------------------------------------------- /examples/reuse_action.rs: -------------------------------------------------------------------------------- 1 | //! Reuse the same aciton more than once 2 | //! 3 | //! This example shows how to use the same action in multiple transitions. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | statemachine! { 10 | transitions: { 11 | *State1 + Event1 / action = State2, 12 | State1 + Event2 / action = State3, 13 | State2 + Event2 = State1, 14 | } 15 | } 16 | 17 | /// Action will increment our context 18 | pub struct Context(usize); 19 | 20 | impl StateMachineContext for Context { 21 | fn action(&mut self) -> Result<(), ()> { 22 | self.0 += 1; 23 | Ok(()) 24 | } 25 | } 26 | 27 | fn main() { 28 | let mut sm = StateMachine::new(Context(0)); 29 | assert!(matches!(sm.state(), &States::State1)); 30 | assert!(sm.context.0 == 0); 31 | 32 | // triggers action 33 | let r = sm.process_event(Events::Event1); 34 | assert!(matches!(r, Ok(&States::State2))); 35 | assert!(sm.context.0 == 1); 36 | 37 | let r = sm.process_event(Events::Event2); 38 | assert!(matches!(r, Ok(&States::State1))); 39 | assert!(sm.context.0 == 1); 40 | 41 | // triggers the same action 42 | let r = sm.process_event(Events::Event2); 43 | assert!(matches!(r, Ok(&States::State3))); 44 | assert!(sm.context.0 == 2); 45 | } 46 | -------------------------------------------------------------------------------- /examples/starting_state_with_data.rs: -------------------------------------------------------------------------------- 1 | //! State data example 2 | //! 3 | //! An example of using state data together with an action. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// State data 10 | #[derive(PartialEq)] 11 | pub struct MyStateData(pub u32); 12 | 13 | statemachine! { 14 | transitions: { 15 | State2 + Event2 / action = State1, 16 | *State1(MyStateData) + Event1 = State2, 17 | // ... 18 | } 19 | } 20 | 21 | /// Context 22 | pub struct Context; 23 | 24 | impl StateMachineContext for Context { 25 | fn action(&mut self) -> Result { 26 | Ok(MyStateData(42)) 27 | } 28 | } 29 | 30 | fn main() { 31 | let mut sm = StateMachine::new(Context, MyStateData(42)); 32 | let _ = sm.process_event(Events::Event1); 33 | let result = sm.process_event(Events::Event2); 34 | 35 | assert!(matches!(result, Ok(&States::State1(MyStateData(42))))); 36 | } 37 | -------------------------------------------------------------------------------- /examples/state_machine_logger.rs: -------------------------------------------------------------------------------- 1 | //! State machine logger example 2 | //! 3 | //! An example of using the logging hooks on the `StateMachineContext` trait to automatically log 4 | //! events, guards, actions, and state transitions 5 | 6 | #![deny(missing_docs)] 7 | 8 | use smlang::statemachine; 9 | 10 | /// Event data 11 | #[derive(PartialEq, Debug)] 12 | pub struct MyEventData(pub u32); 13 | 14 | /// State data 15 | #[derive(PartialEq, Debug)] 16 | pub struct MyStateData(pub u32); 17 | 18 | statemachine! { 19 | states_attr: #[derive(Debug)], 20 | events_attr: #[derive(Debug)], 21 | transitions: { 22 | *State1 + Event1(MyEventData) [guard1] / action1 = State2, 23 | State2(MyStateData) + Event2 [guard2] / action2 = State3, 24 | // ... 25 | } 26 | } 27 | 28 | /// Context 29 | pub struct Context; 30 | 31 | impl StateMachineContext for Context { 32 | // Guard1 has access to the data from Event1 33 | fn guard1(&self, event_data: &MyEventData) -> Result { 34 | Ok(event_data.0 % 2 == 0) 35 | } 36 | 37 | // Action1 has access to the data from Event1, and need to return the state data for State2 38 | fn action1(&mut self, event_data: MyEventData) -> Result { 39 | println!("Creating state data for next state"); 40 | Ok(MyStateData(event_data.0)) 41 | } 42 | 43 | // Guard2 has access to the data from State2 44 | fn guard2(&self, state_data: &MyStateData) -> Result { 45 | Ok(state_data.0 % 2 == 0) 46 | } 47 | 48 | // Action2 has access to the data from State2 49 | fn action2(&mut self, state_data: &MyStateData) -> Result<(), ()> { 50 | println!("Printing state data {:?}", state_data); 51 | Ok(()) 52 | } 53 | 54 | fn log_process_event(&self, current_state: &States, event: &Events) { 55 | println!( 56 | "[StateMachineLogger]\t[{:?}] Processing event {:?}", 57 | current_state, event 58 | ); 59 | } 60 | 61 | fn log_guard(&self, guard: &'static str, result: bool) { 62 | if result { 63 | println!("[StateMachineLogger]\tEnabled `{}`", guard); 64 | } else { 65 | println!("[StateMachineLogger]\tDisabled `{}`", guard); 66 | } 67 | } 68 | 69 | fn log_action(&self, action: &'static str) { 70 | println!("[StateMachineLogger]\tRunning `{}`", action); 71 | } 72 | 73 | fn transition_callback(&self, old_state: &States, new_state: &States) { 74 | println!( 75 | "[StateMachineLogger]\tTransitioning {:?} -> {:?}", 76 | old_state, new_state 77 | ); 78 | } 79 | } 80 | 81 | fn main() { 82 | let mut sm = StateMachine::new(Context); 83 | 84 | let events = [ 85 | Events::Event1(MyEventData(1)), 86 | Events::Event1(MyEventData(0)), 87 | Events::Event2, 88 | ]; 89 | 90 | for event in events { 91 | let _ = sm.process_event(event); 92 | } 93 | 94 | /* $ cargo run --example state_machine_logger 95 | [StateMachineLogger][State1] Processing event Event1(MyEventData(1)) 96 | [StateMachineLogger] Failed `guard1` 97 | [StateMachineLogger][State1] Processing event Event1(MyEventData(0)) 98 | [StateMachineLogger] Passed `guard1` 99 | Creating state data for next state 100 | [StateMachineLogger] Running `action1` 101 | [StateMachineLogger] Transitioning to State2(MyStateData(0)) 102 | [StateMachineLogger][State2(MyStateData(0))] Processing event Event2 103 | [StateMachineLogger] Passed `guard2` 104 | Printing state data MyStateData(0) 105 | [StateMachineLogger] Running `action2` 106 | [StateMachineLogger] Transitioning to State3 107 | */ 108 | } 109 | -------------------------------------------------------------------------------- /examples/state_with_data.rs: -------------------------------------------------------------------------------- 1 | //! State data example 2 | //! 3 | //! An example of using state data together with an action. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// State data 10 | #[derive(PartialEq)] 11 | pub struct MyStateData(pub u32); 12 | 13 | statemachine! { 14 | transitions: { 15 | *State1 + Event1 / action = State2, 16 | State2(MyStateData) + Event2 = State1, 17 | // ... 18 | } 19 | } 20 | 21 | /// Context 22 | pub struct Context; 23 | 24 | impl StateMachineContext for Context { 25 | fn action(&mut self) -> Result { 26 | Ok(MyStateData(42)) 27 | } 28 | } 29 | 30 | fn main() { 31 | let mut sm = StateMachine::new(Context); 32 | let result = sm.process_event(Events::Event1); 33 | 34 | assert!(matches!(result, Ok(&States::State2(MyStateData(42))))); 35 | } 36 | -------------------------------------------------------------------------------- /examples/state_with_reference_data.rs: -------------------------------------------------------------------------------- 1 | //! State data example 2 | //! 3 | //! An example of using referenced state data with lifetimes together with an action. 4 | 5 | #![deny(missing_docs)] 6 | 7 | use smlang::statemachine; 8 | 9 | /// State data 10 | #[derive(PartialEq)] 11 | pub struct MyStateData<'a>(&'a u32); 12 | 13 | statemachine! { 14 | transitions: { 15 | *State1 + Event1 / action = State2, 16 | State2(MyStateData<'a>) + Event2 = State1, 17 | // ... 18 | } 19 | } 20 | 21 | /// Context 22 | pub struct Context; 23 | 24 | impl StateMachineContext for Context { 25 | fn action<'a>(&mut self) -> Result, ()> { 26 | Ok(MyStateData(&42)) 27 | } 28 | } 29 | 30 | fn main() { 31 | let mut sm = StateMachine::new(Context); 32 | let result = sm.process_event(Events::Event1); 33 | 34 | assert!(matches!(result, Ok(&States::State2(MyStateData(&42))))); 35 | } 36 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smlang-macros" 3 | categories = ["embedded", "no-std"] 4 | authors = ["Emil Fresk ", "Donny Zimmanck "] 5 | description = "Procedual macros for the smlang crate" 6 | keywords = ["dsl", "statemachine"] 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/korken89/smlang-rs" 9 | version = "0.8.0" 10 | edition = "2018" 11 | readme = "../README.md" 12 | 13 | [dependencies] 14 | quote = "1" 15 | proc-macro2 = "1" 16 | string_morph = "0.1.0" 17 | syn = "2" 18 | 19 | [lib] 20 | proc-macro = true 21 | 22 | [features] 23 | graphviz = ["syn/extra-traits"] 24 | -------------------------------------------------------------------------------- /macros/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /macros/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /macros/src/diagramgen.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::*; 2 | 3 | /// Generates a string containing 'dot' syntax to generate a statemachine diagram with graphviz. 4 | pub fn generate_diagram(sm: &ParsedStateMachine) -> String { 5 | let transitions = &sm.states_events_mapping; 6 | 7 | let mut diagram_states = sm.states.iter().map(|s| s.0).collect::>(); 8 | diagram_states.sort(); 9 | let diagram_states = diagram_states.into_iter(); 10 | let mut diagram_events = vec![]; 11 | let mut diagram_transitions = vec![]; 12 | for (state, event) in transitions { 13 | for eventmapping in event.values() { 14 | for transition in &eventmapping.transitions { 15 | diagram_events.push(( 16 | eventmapping.event.to_string(), 17 | transition 18 | .guard 19 | .as_ref() 20 | .map(|i| i.to_string()) 21 | .unwrap_or_else(|| "_".to_string()), 22 | transition 23 | .action 24 | .as_ref() 25 | .map(|i| i.ident.to_string()) 26 | .unwrap_or_else(|| "_".to_string()), 27 | )); 28 | diagram_transitions.push(( 29 | state, 30 | transition.out_state.to_string(), 31 | eventmapping.event.to_string(), 32 | )); 33 | } 34 | } 35 | } 36 | // Sorting is needed to ensure stable (ie not changing between runs of 37 | // the same sm code) dot file contents. This is needed to ensure stable 38 | // hash sum, which is used to name unnamed diagrams. If done without sorting, 39 | // the output is polluted with lots of similar svg files with different names. 40 | // This ensures that new files will only occur upon changing the structure of the code. 41 | diagram_events.sort(); 42 | diagram_transitions.sort(); 43 | 44 | let state_string = diagram_states 45 | .map(|s| { 46 | format!( 47 | "\t{} [shape=box color=\"red\" fillcolor=\"#ffbb33\" style=filled]", 48 | s 49 | ) 50 | }) 51 | .collect::>(); 52 | let event_string = diagram_events 53 | .iter() 54 | .map(|s| { 55 | format!( 56 | "\t{0} [shape=box label=\"{0}\\n[{1}] / {2}\"]", 57 | s.0, s.1, s.2 58 | ) 59 | }) 60 | .collect::>(); 61 | let transition_string = diagram_transitions 62 | .iter() 63 | .map(|t| format!("\t{0} -> {1} [color=blue label={2}];", t.0, t.1, t.2)) 64 | .collect::>(); 65 | 66 | format!( 67 | "digraph G {{ 68 | rankdir=\"LR\"; 69 | node [fontname=Arial]; 70 | edge [fontname=Arial]; 71 | s [shape=circle size=2 color=\"black\" style=filled] 72 | 73 | s -> {} 74 | {} 75 | 76 | {} 77 | 78 | {} 79 | }}", 80 | sm.starting_state, 81 | state_string.join("\n"), 82 | event_string.join("\n"), 83 | transition_string.join("\n") 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "512"] 2 | 3 | extern crate proc_macro; 4 | 5 | mod codegen; 6 | #[cfg(feature = "graphviz")] 7 | mod diagramgen; 8 | mod parser; 9 | mod validation; 10 | 11 | use syn::parse_macro_input; 12 | 13 | // dot -Tsvg statemachine.gv -o statemachine.svg 14 | 15 | #[proc_macro] 16 | pub fn statemachine(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 17 | // Parse the syntax into structures 18 | let input = parse_macro_input!(input as parser::state_machine::StateMachine); 19 | 20 | // Validate syntax 21 | match parser::ParsedStateMachine::new(input) { 22 | // Generate code and hand the output tokens back to the compiler 23 | Ok(sm) => { 24 | #[cfg(feature = "graphviz")] 25 | { 26 | use std::hash::{Hash, Hasher}; 27 | use std::io::Write; 28 | 29 | // Generate dot syntax for the statemachine. 30 | let diagram = diagramgen::generate_diagram(&sm); 31 | let diagram_name = if let Some(name) = &sm.name { 32 | name.to_string() 33 | } else { 34 | let mut diagram_hasher = std::collections::hash_map::DefaultHasher::new(); 35 | diagram.hash(&mut diagram_hasher); 36 | format!("smlang{:010x}", diagram_hasher.finish()) 37 | }; 38 | 39 | // Start the 'dot' process. 40 | let mut process = std::process::Command::new("dot") 41 | .args(["-Tsvg", "-o", &format!("statemachine_{diagram_name}.svg")]) 42 | .stdin(std::process::Stdio::piped()) 43 | .spawn() 44 | .expect("Failed to execute 'dot'. Are you sure graphviz is installed?"); 45 | 46 | // Write the dot syntax string to the 'dot' process stdin. 47 | process 48 | .stdin 49 | .as_mut() 50 | .map(|s| s.write_all(diagram.as_bytes())); 51 | 52 | // Check the graphviz return status to see if it was successful. 53 | match process.wait() { 54 | Ok(status) => { 55 | if !status.success() { 56 | panic!("'dot' failed to run. Are you sure graphviz is installed?"); 57 | } 58 | } 59 | Err(_) => panic!("'dot' failed to run. Are you sure graphviz is installed?"), 60 | } 61 | } 62 | 63 | // Validate the parsed state machine before generating code. 64 | if let Err(e) = validation::validate(&sm) { 65 | return e.to_compile_error().into(); 66 | } 67 | 68 | codegen::generate_code(&sm).into() 69 | } 70 | Err(error) => error.to_compile_error().into(), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /macros/src/parser/data.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::lifetimes::Lifetimes; 2 | use std::collections::HashMap; 3 | use syn::{parse, spanned::Spanned, Type}; 4 | 5 | pub type DataTypes = HashMap; 6 | 7 | #[derive(Debug)] 8 | pub struct DataDefinitions { 9 | pub data_types: DataTypes, 10 | pub all_lifetimes: Lifetimes, 11 | pub lifetimes: HashMap, 12 | } 13 | 14 | impl DataDefinitions { 15 | pub fn new() -> Self { 16 | Self { 17 | data_types: DataTypes::new(), 18 | all_lifetimes: Lifetimes::new(), 19 | lifetimes: HashMap::new(), 20 | } 21 | } 22 | 23 | // helper function for adding a new data type to a data descriptions struct 24 | fn add(&mut self, key: String, data_type: Type) -> Result<(), parse::Error> { 25 | // retrieve any lifetimes used in this data-type 26 | let lifetimes = Lifetimes::from_type(&data_type)?; 27 | 28 | // add the data to the collection 29 | self.data_types.insert(key.clone(), data_type); 30 | 31 | // if any new lifetimes were used in the type definition, we add those as well 32 | if !lifetimes.is_empty() { 33 | self.all_lifetimes.extend(&lifetimes); 34 | self.lifetimes.insert(key, lifetimes); 35 | } 36 | Ok(()) 37 | } 38 | 39 | // helper function for collecting data types and adding them to a data descriptions struct 40 | pub fn collect(&mut self, key: String, data_type: Option) -> Result<(), parse::Error> { 41 | // check to see if there was every a previous data-type associated with this transition 42 | let prev = self.data_types.get(&key); 43 | 44 | // if there was a previous data definition for this key, may sure it is consistent 45 | if let Some(prev) = prev { 46 | if let Some(ref data_type) = data_type { 47 | if prev != &data_type.clone() { 48 | return Err(parse::Error::new( 49 | data_type.span(), 50 | format!( 51 | "This event's type {} does not match its previous definition", 52 | key 53 | ), 54 | )); 55 | } 56 | } else { 57 | return Err(parse::Error::new( 58 | data_type.span(), 59 | format!( 60 | "This event's type {} does not match its previous definition", 61 | key 62 | ), 63 | )); 64 | } 65 | } 66 | 67 | if let Some(data_type) = data_type { 68 | self.add(key, data_type)?; 69 | } 70 | Ok(()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /macros/src/parser/event.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::transition::GuardExpression; 2 | use crate::parser::AsyncIdent; 3 | use syn::{parenthesized, parse, spanned::Spanned, token, Ident, Token, Type}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Event { 7 | pub ident: Ident, 8 | pub data_type: Option, 9 | } 10 | 11 | #[derive(Debug)] 12 | pub struct EventMapping { 13 | pub in_state: Ident, 14 | pub event: Ident, 15 | pub transitions: Vec, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct Transition { 20 | pub guard: Option, 21 | pub action: Option, 22 | pub out_state: Ident, 23 | } 24 | 25 | impl parse::Parse for Event { 26 | fn parse(input: parse::ParseStream) -> syn::Result { 27 | // Event 28 | input.parse::()?; 29 | let ident: Ident = input.parse()?; 30 | 31 | // Possible type on the event 32 | let data_type = if input.peek(token::Paren) { 33 | let content; 34 | parenthesized!(content in input); 35 | let input: Type = content.parse()?; 36 | 37 | // Check so the type is supported 38 | match &input { 39 | Type::Array(_) 40 | | Type::Path(_) 41 | | Type::Ptr(_) 42 | | Type::Reference(_) 43 | | Type::Slice(_) 44 | | Type::Tuple(_) => (), 45 | _ => { 46 | return Err(parse::Error::new( 47 | input.span(), 48 | "This is an unsupported type for events.", 49 | )) 50 | } 51 | } 52 | 53 | Some(input) 54 | } else { 55 | None 56 | }; 57 | 58 | Ok(Self { ident, data_type }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /macros/src/parser/input_state.rs: -------------------------------------------------------------------------------- 1 | use syn::{parenthesized, parse, spanned::Spanned, token, Ident, Token, Type}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct InputState { 5 | pub start: bool, 6 | pub wildcard: bool, 7 | pub ident: Ident, 8 | pub data_type: Option, 9 | } 10 | 11 | impl parse::Parse for InputState { 12 | fn parse(input: parse::ParseStream) -> syn::Result { 13 | // Check for starting state definition 14 | let start = input.parse::().is_ok(); 15 | 16 | // check to see if this is a wildcard state, which is denoted with "underscore" 17 | let underscore = input.parse::(); 18 | let wildcard = underscore.is_ok(); 19 | 20 | // wildcards can't be used as starting states 21 | if start && wildcard { 22 | return Err(parse::Error::new( 23 | input.span(), 24 | "Wildcards can't be used as the starting state.", 25 | )); 26 | } 27 | 28 | // Input State 29 | let ident: Ident = if let Ok(underscore) = underscore { 30 | underscore.into() 31 | } else { 32 | input.parse()? 33 | }; 34 | 35 | // Possible type on the input state 36 | let data_type = if input.peek(token::Paren) { 37 | let content; 38 | parenthesized!(content in input); 39 | let input: Type = content.parse()?; 40 | 41 | // Wilcards should not have data associated, as data will already be defined 42 | if wildcard { 43 | return Err(parse::Error::new( 44 | input.span(), 45 | "Wildcard states cannot have data associated with it.", 46 | )); 47 | } 48 | 49 | // Check so the type is supported 50 | match &input { 51 | Type::Array(_) 52 | | Type::Path(_) 53 | | Type::Ptr(_) 54 | | Type::Reference(_) 55 | | Type::Slice(_) 56 | | Type::Tuple(_) => (), 57 | _ => { 58 | return Err(parse::Error::new( 59 | input.span(), 60 | "This is an unsupported type for states.", 61 | )) 62 | } 63 | } 64 | 65 | Some(input) 66 | } else { 67 | None 68 | }; 69 | 70 | Ok(Self { 71 | start, 72 | wildcard, 73 | ident, 74 | data_type, 75 | }) 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | 82 | use super::*; 83 | use syn::parse_quote; 84 | 85 | #[test] 86 | #[should_panic(expected = "Wildcards can't be used as the starting state.")] 87 | fn wildcard_used_as_start() { 88 | let _: InputState = parse_quote! { 89 | *_ 90 | }; 91 | } 92 | 93 | #[test] 94 | fn input_state_with_data() { 95 | let state: InputState = parse_quote! { 96 | *Start(u8) 97 | }; 98 | 99 | assert!(state.start); 100 | assert!(!state.wildcard); 101 | assert!(state.data_type.is_some()); 102 | } 103 | 104 | #[test] 105 | #[should_panic(expected = "Wildcard states cannot have data associated with it.")] 106 | fn wildcard_with_data() { 107 | let _: InputState = parse_quote! { 108 | _(u8) 109 | }; 110 | } 111 | 112 | #[test] 113 | #[should_panic(expected = "This is an unsupported type for states.")] 114 | fn unsupported_type() { 115 | let _: InputState = parse_quote! { 116 | State1(!) 117 | }; 118 | } 119 | 120 | #[test] 121 | fn wildcard() { 122 | let wildcard: InputState = parse_quote! { 123 | _ 124 | }; 125 | 126 | assert!(wildcard.wildcard); 127 | assert!(!wildcard.start); 128 | assert!(wildcard.data_type.is_none()); 129 | } 130 | 131 | #[test] 132 | fn start() { 133 | let start: InputState = parse_quote! { 134 | *Start 135 | }; 136 | 137 | assert!(start.start); 138 | assert!(!start.wildcard); 139 | assert!(start.data_type.is_none()); 140 | } 141 | 142 | #[test] 143 | fn state_without_data() { 144 | let state: InputState = parse_quote! { 145 | State 146 | }; 147 | 148 | assert!(!state.start); 149 | assert!(!state.wildcard); 150 | assert!(state.data_type.is_none()); 151 | } 152 | 153 | #[test] 154 | fn state_with_data() { 155 | let state: InputState = parse_quote! { 156 | State(u8) 157 | }; 158 | 159 | assert!(!state.start); 160 | assert!(!state.wildcard); 161 | assert!(state.data_type.is_some()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /macros/src/parser/lifetimes.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | use std::ops::Sub; 4 | use syn::{parse, spanned::Spanned, GenericArgument, Lifetime, PathArguments, Type}; 5 | 6 | #[derive(Default, Debug, Clone)] 7 | pub struct Lifetimes { 8 | lifetimes: Vec, 9 | } 10 | 11 | impl Lifetimes { 12 | pub fn new() -> Lifetimes { 13 | Lifetimes { 14 | lifetimes: Vec::new(), 15 | } 16 | } 17 | 18 | pub fn from_type(data_type: &Type) -> Result { 19 | let mut lifetimes = Lifetimes::new(); 20 | lifetimes.insert_from_type(data_type)?; 21 | Ok(lifetimes) 22 | } 23 | 24 | pub fn insert(&mut self, lifetime: &Lifetime) { 25 | if !self.lifetimes.contains(lifetime) { 26 | self.lifetimes.push(lifetime.to_owned()); 27 | } 28 | } 29 | 30 | pub fn extend(&mut self, other: &Lifetimes) { 31 | for lifetime in other.lifetimes.iter() { 32 | self.insert(lifetime); 33 | } 34 | } 35 | 36 | pub fn is_empty(&self) -> bool { 37 | self.lifetimes.is_empty() 38 | } 39 | 40 | pub fn as_slice(&self) -> &[Lifetime] { 41 | &self.lifetimes[..] 42 | } 43 | 44 | /// Extracts lifetimes from a [`Type`] 45 | pub fn insert_from_type(&mut self, data_type: &Type) -> Result<(), parse::Error> { 46 | match data_type { 47 | Type::Reference(tr) => { 48 | if let Some(lifetime) = &tr.lifetime { 49 | self.insert(lifetime); 50 | } else { 51 | return Err(parse::Error::new( 52 | data_type.span(), 53 | "This event's data lifetime is not defined, consider adding a lifetime.", 54 | )); 55 | } 56 | } 57 | Type::Path(tp) => { 58 | let punct = &tp.path.segments; 59 | for p in punct.iter() { 60 | if let PathArguments::AngleBracketed(abga) = &p.arguments { 61 | for arg in &abga.args { 62 | if let GenericArgument::Lifetime(lifetime) = &arg { 63 | self.insert(lifetime); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | Type::Tuple(tuple) => { 70 | for elem in tuple.elems.iter() { 71 | self.insert_from_type(elem)?; 72 | } 73 | } 74 | _ => {} 75 | } 76 | 77 | Ok(()) 78 | } 79 | } 80 | 81 | impl ToTokens for Lifetimes { 82 | fn to_tokens(&self, tokens: &mut TokenStream) { 83 | if self.is_empty() { 84 | return; 85 | } 86 | 87 | let lifetimes = self.as_slice(); 88 | tokens.extend(quote! { #(#lifetimes),* ,}); 89 | } 90 | } 91 | 92 | impl Sub<&Lifetimes> for Lifetimes { 93 | type Output = Lifetimes; 94 | 95 | fn sub(mut self, rhs: &Lifetimes) -> Lifetimes { 96 | self.lifetimes.retain(|lt| !rhs.lifetimes.contains(lt)); 97 | self 98 | } 99 | } 100 | 101 | impl Sub for Lifetimes { 102 | type Output = Lifetimes; 103 | 104 | fn sub(self, rhs: Lifetimes) -> Lifetimes { 105 | self.sub(&rhs) 106 | } 107 | } 108 | 109 | impl Sub<&Lifetimes> for &Lifetimes { 110 | type Output = Lifetimes; 111 | 112 | fn sub(self, rhs: &Lifetimes) -> Lifetimes { 113 | self.to_owned().sub(rhs) 114 | } 115 | } 116 | 117 | impl Sub for &Lifetimes { 118 | type Output = Lifetimes; 119 | 120 | fn sub(self, rhs: Lifetimes) -> Lifetimes { 121 | self.to_owned().sub(&rhs) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /macros/src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod event; 3 | pub mod input_state; 4 | pub mod lifetimes; 5 | pub mod output_state; 6 | pub mod state_machine; 7 | pub mod transition; 8 | 9 | use data::DataDefinitions; 10 | use event::EventMapping; 11 | use state_machine::StateMachine; 12 | 13 | use input_state::InputState; 14 | use proc_macro2::{Span, TokenStream}; 15 | 16 | use crate::parser::event::Transition; 17 | use std::collections::{hash_map, HashMap}; 18 | use std::fmt; 19 | use syn::{parse, Attribute, Ident, Type}; 20 | use transition::StateTransition; 21 | pub type TransitionMap = HashMap>; 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct AsyncIdent { 25 | pub ident: Ident, 26 | pub is_async: bool, 27 | } 28 | impl AsyncIdent { 29 | pub fn to_token_stream(&self, visit: &mut F) -> TokenStream 30 | where 31 | F: FnMut(&AsyncIdent) -> TokenStream, 32 | { 33 | visit(self) 34 | } 35 | } 36 | impl fmt::Display for AsyncIdent { 37 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 38 | if self.is_async { 39 | write!(f, "{}().await", self.ident) 40 | } else { 41 | write!(f, "{}()", self.ident) 42 | } 43 | } 44 | } 45 | 46 | #[derive(Debug)] 47 | pub struct ParsedStateMachine { 48 | pub name: Option, 49 | pub states_attr: Vec, 50 | pub events_attr: Vec, 51 | pub temporary_context_type: Option, 52 | pub custom_error: bool, 53 | pub states: HashMap, 54 | pub starting_state: Ident, 55 | pub state_data: DataDefinitions, 56 | pub events: HashMap, 57 | pub event_data: DataDefinitions, 58 | pub states_events_mapping: HashMap>, 59 | pub entry_exit_async: bool, 60 | } 61 | 62 | // helper function for adding a transition to a transition event map 63 | fn add_transition( 64 | transition: &StateTransition, 65 | transition_map: &mut TransitionMap, 66 | state_data: &DataDefinitions, 67 | ) -> Result<(), parse::Error> { 68 | let p = transition_map 69 | .get_mut(&transition.in_state.ident.to_string()) 70 | .unwrap(); 71 | 72 | match p.entry(transition.event.ident.to_string()) { 73 | hash_map::Entry::Vacant(entry) => { 74 | let mapping = EventMapping { 75 | in_state: transition.in_state.ident.clone(), 76 | event: transition.event.ident.clone(), 77 | transitions: vec![Transition { 78 | guard: transition.guard.clone(), 79 | action: transition.action.clone(), 80 | out_state: transition.out_state.ident.clone(), 81 | }], 82 | }; 83 | entry.insert(mapping); 84 | } 85 | hash_map::Entry::Occupied(mut entry) => { 86 | let mapping = entry.get_mut(); 87 | mapping.transitions.push(Transition { 88 | guard: transition.guard.clone(), 89 | action: transition.action.clone(), 90 | out_state: transition.out_state.ident.clone(), 91 | }); 92 | } 93 | } 94 | 95 | // Check for actions when states have data a 96 | if state_data 97 | .data_types 98 | .contains_key(&transition.out_state.ident.to_string()) 99 | { 100 | // This transition goes to a state that has data associated, check so it has an 101 | // action 102 | 103 | if transition.action.is_none() { 104 | return Err(parse::Error::new( 105 | transition.out_state.ident.span(), 106 | "This state has data associated, but not action is define here to provide it.", 107 | )); 108 | } 109 | } 110 | Ok(()) 111 | } 112 | 113 | impl ParsedStateMachine { 114 | pub fn new(mut sm: StateMachine) -> parse::Result { 115 | // Derive out_state for internal non-wildcard transitions 116 | for transition in sm.transitions.iter_mut() { 117 | if transition.out_state.internal_transition && !transition.in_state.wildcard { 118 | transition.out_state.ident = transition.in_state.ident.clone(); 119 | transition 120 | .out_state 121 | .data_type 122 | .clone_from(&transition.in_state.data_type); 123 | transition.out_state.internal_transition = false; 124 | } 125 | } 126 | 127 | // Check the initial state definition 128 | let mut starting_transitions_iter = sm.transitions.iter().filter(|sm| sm.in_state.start); 129 | 130 | let starting_transition = starting_transitions_iter.next().ok_or(parse::Error::new( 131 | Span::call_site(), 132 | "No starting state defined, indicate the starting state with a *.", 133 | ))?; 134 | 135 | if starting_transitions_iter.next().is_some() { 136 | return Err(parse::Error::new( 137 | Span::call_site(), 138 | "More than one starting state defined (indicated with *), remove duplicates.", 139 | )); 140 | } 141 | 142 | // Extract the starting state 143 | let starting_state = starting_transition.in_state.ident.clone(); 144 | 145 | let mut states = HashMap::new(); 146 | let mut state_data = DataDefinitions::new(); 147 | let mut events = HashMap::new(); 148 | let mut event_data = DataDefinitions::new(); 149 | let mut states_events_mapping = TransitionMap::new(); 150 | 151 | for transition in sm.transitions.iter() { 152 | // Collect states 153 | let in_state_name = transition.in_state.ident.to_string(); 154 | if !transition.in_state.wildcard { 155 | states.insert(in_state_name.clone(), transition.in_state.ident.clone()); 156 | state_data.collect(in_state_name.clone(), transition.in_state.data_type.clone())?; 157 | } 158 | if !transition.out_state.internal_transition { 159 | let out_state_name = transition.out_state.ident.to_string(); 160 | states.insert(out_state_name.clone(), transition.out_state.ident.clone()); 161 | state_data.collect( 162 | out_state_name.clone(), 163 | transition.out_state.data_type.clone(), 164 | )?; 165 | } 166 | 167 | // Collect events 168 | let event_name = transition.event.ident.to_string(); 169 | events.insert(event_name.clone(), transition.event.ident.clone()); 170 | event_data.collect(event_name.clone(), transition.event.data_type.clone())?; 171 | 172 | // add input and output states to the mapping HashMap 173 | if !transition.in_state.wildcard { 174 | states_events_mapping.insert(transition.in_state.ident.to_string(), HashMap::new()); 175 | } 176 | if !transition.out_state.internal_transition { 177 | states_events_mapping 178 | .insert(transition.out_state.ident.to_string(), HashMap::new()); 179 | } 180 | } 181 | 182 | for transition in sm.transitions.iter() { 183 | // if input state is a wildcard, we need to add this transition for all states 184 | if transition.in_state.wildcard { 185 | let mut transition_added = false; 186 | 187 | for (name, in_state) in &states { 188 | // skip already set input state 189 | let p = states_events_mapping 190 | .get_mut(&in_state.to_string()) 191 | .unwrap(); 192 | 193 | if p.contains_key(&transition.event.ident.to_string()) { 194 | continue; 195 | } 196 | 197 | // create a new input state from wildcard 198 | let in_state = InputState { 199 | start: false, 200 | wildcard: false, 201 | ident: in_state.clone(), 202 | data_type: state_data.data_types.get(name).cloned(), 203 | }; 204 | 205 | // create the transition 206 | let mut out_state = transition.out_state.clone(); 207 | if out_state.internal_transition { 208 | out_state.ident = in_state.ident.clone(); 209 | out_state.data_type.clone_from(&in_state.data_type); 210 | } 211 | let wildcard_transition = StateTransition { 212 | in_state, 213 | event: transition.event.clone(), 214 | guard: transition.guard.clone(), 215 | action: transition.action.clone(), 216 | out_state, 217 | }; 218 | 219 | // add the wildcard transition to the transition map 220 | // TODO: Need to work on the span of this error, as it is being caused by the wildcard 221 | // but won't show up at that line 222 | add_transition( 223 | &wildcard_transition, 224 | &mut states_events_mapping, 225 | &state_data, 226 | )?; 227 | 228 | transition_added = true; 229 | } 230 | 231 | // No transitions were added by expanding the wildcard, 232 | // so emit an error to the user 233 | if !transition_added { 234 | return Err(parse::Error::new( 235 | transition.in_state.ident.span(), 236 | "Wildcard has no effect", 237 | )); 238 | } 239 | } else { 240 | add_transition(transition, &mut states_events_mapping, &state_data)?; 241 | } 242 | } 243 | 244 | Ok(ParsedStateMachine { 245 | name: sm.name, 246 | states_attr: sm.states_attr, 247 | events_attr: sm.events_attr, 248 | temporary_context_type: sm.temporary_context_type, 249 | custom_error: sm.custom_error, 250 | states, 251 | starting_state, 252 | state_data, 253 | events, 254 | event_data, 255 | states_events_mapping, 256 | entry_exit_async: sm.entry_exit_async, 257 | }) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /macros/src/parser/output_state.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::{parenthesized, parse, spanned::Spanned, token, Ident, Token, Type}; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct OutputState { 6 | pub ident: Ident, 7 | pub internal_transition: bool, 8 | pub data_type: Option, 9 | } 10 | 11 | impl parse::Parse for OutputState { 12 | fn parse(input: parse::ParseStream) -> syn::Result { 13 | if input.peek(Token![=]) { 14 | input.parse::()?; 15 | let (internal_transition, ident) = if input.peek(Token![_]) { 16 | // Underscore ident here is used to represent an internal transition 17 | let underscore = input.parse::()?; 18 | (true, underscore.into()) 19 | } else { 20 | (false, input.parse()?) 21 | }; 22 | 23 | // Possible type on the output state 24 | let data_type = if !internal_transition && input.peek(token::Paren) { 25 | let content; 26 | parenthesized!(content in input); 27 | let input: Type = content.parse()?; 28 | 29 | // Check so the type is supported 30 | match &input { 31 | Type::Array(_) 32 | | Type::Path(_) 33 | | Type::Ptr(_) 34 | | Type::Reference(_) 35 | | Type::Slice(_) 36 | | Type::Tuple(_) => (), 37 | _ => { 38 | return Err(parse::Error::new( 39 | input.span(), 40 | "This is an unsupported type for states.", 41 | )) 42 | } 43 | } 44 | 45 | Some(input) 46 | } else { 47 | None 48 | }; 49 | 50 | Ok(Self { 51 | ident, 52 | internal_transition, 53 | data_type, 54 | }) 55 | } else { 56 | // Internal transition 57 | Ok(Self { 58 | ident: Ident::new("_", Span::call_site()), 59 | internal_transition: true, 60 | data_type: None, 61 | }) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /macros/src/parser/state_machine.rs: -------------------------------------------------------------------------------- 1 | use super::transition::{StateTransition, StateTransitions}; 2 | use syn::{braced, parse, spanned::Spanned, token, Attribute, Ident, Token, Type}; 3 | 4 | #[derive(Debug)] 5 | pub struct StateMachine { 6 | pub temporary_context_type: Option, 7 | pub custom_error: bool, 8 | pub transitions: Vec, 9 | pub name: Option, 10 | pub states_attr: Vec, 11 | pub events_attr: Vec, 12 | pub entry_exit_async: bool, 13 | } 14 | 15 | impl StateMachine { 16 | pub fn new() -> Self { 17 | StateMachine { 18 | temporary_context_type: None, 19 | custom_error: false, 20 | transitions: Vec::new(), 21 | name: None, 22 | states_attr: Vec::new(), 23 | events_attr: Vec::new(), 24 | entry_exit_async: false, 25 | } 26 | } 27 | 28 | pub fn add_transitions(&mut self, transitions: StateTransitions) { 29 | for in_state in transitions.in_states { 30 | let transition = StateTransition { 31 | in_state, 32 | event: transitions.event.clone(), 33 | guard: transitions.guard.clone(), 34 | action: transitions.action.clone(), 35 | out_state: transitions.out_state.clone(), 36 | }; 37 | self.transitions.push(transition); 38 | } 39 | } 40 | } 41 | 42 | impl parse::Parse for StateMachine { 43 | fn parse(input: parse::ParseStream) -> parse::Result { 44 | let mut statemachine = StateMachine::new(); 45 | 46 | loop { 47 | // If the last line ends with a comma this is true 48 | if input.is_empty() { 49 | break; 50 | } 51 | 52 | match input.parse::()?.to_string().as_str() { 53 | "transitions" => { 54 | input.parse::()?; 55 | if input.peek(token::Brace) { 56 | let content; 57 | braced!(content in input); 58 | loop { 59 | if content.is_empty() { 60 | break; 61 | } 62 | 63 | let transitions: StateTransitions = content.parse()?; 64 | statemachine.add_transitions(transitions); 65 | 66 | // No comma at end of line, no more transitions 67 | if content.is_empty() { 68 | break; 69 | } 70 | 71 | if content.parse::().is_err() { 72 | break; 73 | }; 74 | } 75 | } 76 | } 77 | "custom_error" => { 78 | input.parse::()?; 79 | let custom_error: syn::LitBool = input.parse()?; 80 | if custom_error.value { 81 | statemachine.custom_error = true 82 | } 83 | } 84 | "temporary_context" => { 85 | input.parse::()?; 86 | let temporary_context_type: Type = input.parse()?; 87 | 88 | // Check so the type is supported 89 | match &temporary_context_type { 90 | Type::Array(_) 91 | | Type::Path(_) 92 | | Type::Ptr(_) 93 | | Type::Reference(_) 94 | | Type::Slice(_) 95 | | Type::Tuple(_) => (), 96 | _ => { 97 | return Err(parse::Error::new( 98 | temporary_context_type.span(), 99 | "This is an unsupported type for the temporary state.", 100 | )) 101 | } 102 | } 103 | 104 | // Store the temporary context type 105 | statemachine.temporary_context_type = Some(temporary_context_type); 106 | } 107 | "name" => { 108 | input.parse::()?; 109 | statemachine.name = Some(input.parse::()?); 110 | } 111 | 112 | "states_attr" => { 113 | input.parse::()?; 114 | statemachine.states_attr = Attribute::parse_outer(input)?; 115 | } 116 | 117 | "events_attr" => { 118 | input.parse::()?; 119 | statemachine.events_attr = Attribute::parse_outer(input)?; 120 | } 121 | 122 | "entry_exit_async" => { 123 | input.parse::()?; 124 | let entry_exit_async: syn::LitBool = input.parse()?; 125 | if entry_exit_async.value { 126 | statemachine.entry_exit_async = true; 127 | } 128 | } 129 | 130 | keyword => { 131 | return Err(parse::Error::new( 132 | input.span(), 133 | format!( 134 | "Unknown keyword {}. Support keywords: [\"name\", \ 135 | \"transitions\", \ 136 | \"temporary_context\", \ 137 | \"custom_error\", \ 138 | \"states_attr\", \ 139 | \"events_attr\", \ 140 | \"entry_exit_async\" 141 | ]", 142 | keyword 143 | ), 144 | )) 145 | } 146 | } 147 | 148 | // No comma at end of line, no more transitions 149 | if input.is_empty() { 150 | break; 151 | } 152 | 153 | if input.parse::().is_err() { 154 | break; 155 | }; 156 | } 157 | 158 | Ok(statemachine) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /macros/src/parser/transition.rs: -------------------------------------------------------------------------------- 1 | use super::event::Event; 2 | use super::input_state::InputState; 3 | use super::output_state::OutputState; 4 | use super::AsyncIdent; 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | use std::fmt; 8 | use syn::{bracketed, parse, token, Ident, Token}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct StateTransition { 12 | pub in_state: InputState, 13 | pub event: Event, 14 | pub guard: Option, 15 | pub action: Option, 16 | pub out_state: OutputState, 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct StateTransitions { 21 | pub in_states: Vec, 22 | pub event: Event, 23 | pub guard: Option, 24 | pub action: Option, 25 | pub out_state: OutputState, 26 | } 27 | 28 | impl parse::Parse for StateTransitions { 29 | fn parse(input: parse::ParseStream) -> syn::Result { 30 | // parse the input pattern 31 | let mut in_states = Vec::new(); 32 | loop { 33 | let in_state: InputState = input.parse()?; 34 | in_states.push(in_state); 35 | if input.parse::().is_err() { 36 | break; 37 | }; 38 | } 39 | 40 | // Make sure that if a wildcard is used, it is the only input state 41 | if in_states.len() > 1 { 42 | for in_state in &in_states { 43 | if in_state.wildcard { 44 | return Err(parse::Error::new( 45 | in_state.ident.span(), 46 | "Wildcards already include all states, so should not be used with input state patterns.", 47 | )); 48 | } 49 | } 50 | } 51 | // Event 52 | let event: Event = input.parse()?; 53 | 54 | // Possible guard 55 | let guard = if input.peek(token::Bracket) { 56 | let content; 57 | bracketed!(content in input); 58 | Some(GuardExpression::parse(&content)?) 59 | } else { 60 | None 61 | }; 62 | 63 | // Possible action 64 | let action = if input.parse::().is_ok() { 65 | let is_async = input.parse::().is_ok(); 66 | let action: Ident = input.parse()?; 67 | Some(AsyncIdent { 68 | ident: action, 69 | is_async, 70 | }) 71 | } else { 72 | None 73 | }; 74 | 75 | let out_state: OutputState = input.parse()?; 76 | 77 | Ok(Self { 78 | in_states, 79 | event, 80 | guard, 81 | action, 82 | out_state, 83 | }) 84 | } 85 | } 86 | #[derive(Debug, Clone)] 87 | pub enum GuardExpression { 88 | Guard(AsyncIdent), 89 | Not(Box), 90 | Group(Box), 91 | And(Box, Box), 92 | Or(Box, Box), 93 | } 94 | impl fmt::Display for GuardExpression { 95 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 96 | match self { 97 | GuardExpression::Guard(async_ident) => write!(f, "{}", async_ident), 98 | GuardExpression::Not(expr) => write!(f, "!{}", expr), 99 | GuardExpression::Group(expr) => write!(f, "({})", expr), 100 | GuardExpression::And(lhs, rhs) => { 101 | write!(f, "{} && {}", lhs, rhs) 102 | } 103 | GuardExpression::Or(lhs, rhs) => { 104 | write!(f, "{} || {}", lhs, rhs) 105 | } 106 | } 107 | } 108 | } 109 | impl GuardExpression { 110 | pub fn to_token_stream(&self, visit: &mut F) -> TokenStream 111 | where 112 | F: FnMut(&AsyncIdent) -> TokenStream, 113 | { 114 | match self { 115 | GuardExpression::Guard(async_ident) => async_ident.to_token_stream(visit), 116 | GuardExpression::Not(expr) => { 117 | let expr_tokens = expr.to_token_stream(visit); 118 | quote! { !#expr_tokens } 119 | } 120 | GuardExpression::Group(expr) => { 121 | let expr_tokens = expr.to_token_stream(visit); 122 | quote! { (#expr_tokens) } 123 | } 124 | GuardExpression::And(lhs, rhs) => { 125 | let lhs_tokens = lhs.to_token_stream(visit); 126 | let rhs_tokens = rhs.to_token_stream(visit); 127 | quote! { #lhs_tokens && #rhs_tokens } 128 | } 129 | GuardExpression::Or(lhs, rhs) => { 130 | let lhs_tokens = lhs.to_token_stream(visit); 131 | let rhs_tokens = rhs.to_token_stream(visit); 132 | quote! { #lhs_tokens || #rhs_tokens } 133 | } 134 | } 135 | } 136 | } 137 | 138 | pub fn visit_guards(expr: &GuardExpression, mut visit_guard: F) -> Result<(), parse::Error> 139 | where 140 | F: FnMut(&AsyncIdent) -> Result<(), parse::Error>, 141 | { 142 | let mut stack = vec![expr]; 143 | while let Some(node) = stack.pop() { 144 | match node { 145 | GuardExpression::Guard(guard) => { 146 | visit_guard(guard)?; 147 | } 148 | GuardExpression::Not(inner) | GuardExpression::Group(inner) => { 149 | stack.push(inner.as_ref()); 150 | } 151 | GuardExpression::And(left, right) | GuardExpression::Or(left, right) => { 152 | stack.push(left.as_ref()); 153 | stack.push(right.as_ref()); 154 | } 155 | } 156 | } 157 | Ok(()) 158 | } 159 | 160 | impl parse::Parse for GuardExpression { 161 | fn parse(input: parse::ParseStream) -> syn::Result { 162 | parse_or(input) 163 | } 164 | } 165 | 166 | fn parse_or(input: parse::ParseStream) -> syn::Result { 167 | let mut left = parse_and(input)?; 168 | while input.peek(Token![||]) { 169 | let _or: Token![||] = input.parse()?; 170 | let right = parse_and(input)?; 171 | left = GuardExpression::Or(Box::new(left), Box::new(right)); 172 | } 173 | Ok(left) 174 | } 175 | 176 | fn parse_and(input: parse::ParseStream) -> syn::Result { 177 | let mut left = parse_not(input)?; 178 | while input.peek(Token![&&]) { 179 | let _and: Token![&&] = input.parse()?; 180 | let right = parse_not(input)?; 181 | left = GuardExpression::And(Box::new(left), Box::new(right)); 182 | } 183 | Ok(left) 184 | } 185 | 186 | fn parse_not(input: parse::ParseStream) -> syn::Result { 187 | if input.peek(Token![!]) { 188 | let _not: Token![!] = input.parse()?; 189 | let expr = parse_primary(input)?; 190 | return Ok(GuardExpression::Not(Box::new(expr))); 191 | } 192 | parse_primary(input) 193 | } 194 | 195 | fn parse_primary(input: parse::ParseStream) -> syn::Result { 196 | if input.peek(token::Paren) { 197 | let content; 198 | syn::parenthesized!(content in input); 199 | let expr = parse_or(&content)?; 200 | return Ok(GuardExpression::Group(Box::new(expr))); 201 | } 202 | 203 | if input.peek(Token![async]) { 204 | let _async: Token![async] = input.parse()?; 205 | let ident: Ident = input.parse()?; 206 | return Ok(GuardExpression::Guard(AsyncIdent { 207 | ident, 208 | is_async: true, 209 | })); 210 | } 211 | 212 | let ident: Ident = input.parse()?; 213 | Ok(GuardExpression::Guard(AsyncIdent { 214 | ident, 215 | is_async: false, 216 | })) 217 | } 218 | 219 | #[cfg(test)] 220 | mod test { 221 | use crate::parser::transition::GuardExpression; 222 | use syn::parse_str; 223 | 224 | #[test] 225 | fn bad_guard_expression() { 226 | let guard_expression = "a && b c"; 227 | assert!(parse_str::(guard_expression).is_err()); 228 | } 229 | #[test] 230 | fn guard_expressions() -> Result<(), syn::Error> { 231 | for (guard_expression_str, expected) in vec![ 232 | ("guard", "guard()"), 233 | ("async guard", "guard().await"), 234 | ("async a || async b", "a().await || b().await"), 235 | ("!guard", "!guard()"), 236 | ("a && b", "a() && b()"), 237 | ("a || b", "a() || b()"), 238 | ("a || b || c", "a() || b() || c()"), 239 | ("a || b && c || d", "a() || b() && c() || d()"), 240 | ("(a || b) && (c || d)", "(a() || b()) && (c() || d())"), 241 | ("a && b || c && d", "a() && b() || c() && d()"), 242 | ( 243 | "a && ( !b && c ) || d && e", 244 | "a() && (!b() && c()) || d() && e()", 245 | ), 246 | ] { 247 | let guard_expression: GuardExpression = parse_str(guard_expression_str)?; 248 | assert_eq!(guard_expression.to_string(), expected); 249 | println!("{:?}", guard_expression); 250 | } 251 | Ok(()) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /macros/src/validation.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::transition::visit_guards; 2 | use crate::parser::{AsyncIdent, ParsedStateMachine}; 3 | use proc_macro2::Span; 4 | use std::collections::HashMap; 5 | use syn::parse; 6 | 7 | /// A basic representation an action call signature. 8 | #[derive(PartialEq, Clone)] 9 | struct FunctionSignature { 10 | // Function input arguments. 11 | arguments: Vec, 12 | 13 | // Function result (if any). 14 | result: Option, 15 | 16 | // Is the function async 17 | is_async: bool, 18 | } 19 | 20 | impl FunctionSignature { 21 | pub fn new( 22 | input_data: Option<&syn::Type>, 23 | event_data: Option<&syn::Type>, 24 | output_data: Option<&syn::Type>, 25 | is_async: bool, 26 | ) -> Self { 27 | let mut input_arguments = vec![]; 28 | 29 | if let Some(datatype) = input_data { 30 | input_arguments.push(datatype.clone()); 31 | } 32 | 33 | if let Some(datatype) = event_data { 34 | input_arguments.push(datatype.clone()); 35 | } 36 | 37 | let result = output_data.cloned(); 38 | 39 | Self { 40 | arguments: input_arguments, 41 | result, 42 | is_async, 43 | } 44 | } 45 | 46 | pub fn new_guard( 47 | input_state: Option<&syn::Type>, 48 | event: Option<&syn::Type>, 49 | is_async: bool, 50 | ) -> Self { 51 | // Guards never have output data. 52 | Self::new(input_state, event, None, is_async) 53 | } 54 | } 55 | 56 | // Verify action and guard function signatures. 57 | fn validate_action_signatures(sm: &ParsedStateMachine) -> Result<(), parse::Error> { 58 | // Collect all of the action call signatures. 59 | let mut actions = HashMap::new(); 60 | 61 | let all_transitions = &sm.states_events_mapping; 62 | 63 | for (in_state_name, from_transitions) in all_transitions.iter() { 64 | let in_state_data = sm.state_data.data_types.get(in_state_name); 65 | 66 | for (out_state_name, event_mapping) in from_transitions.iter() { 67 | let out_state_data = sm.state_data.data_types.get(out_state_name); 68 | 69 | // Get the data associated with this event. 70 | let event_data = sm 71 | .event_data 72 | .data_types 73 | .get(&event_mapping.event.to_string()); 74 | for transition in &event_mapping.transitions { 75 | if let Some(AsyncIdent { 76 | ident: action, 77 | is_async, 78 | }) = &transition.action 79 | { 80 | let signature = FunctionSignature::new( 81 | in_state_data, 82 | event_data, 83 | out_state_data, 84 | *is_async, 85 | ); 86 | 87 | // If the action is not yet known, add it to our tracking list. 88 | actions 89 | .entry(action.to_string()) 90 | .or_insert_with(|| signature.clone()); 91 | 92 | // Check that the call signature is equivalent to the recorded signature for this 93 | // action. 94 | if actions.get(&action.to_string()).unwrap() != &signature { 95 | return Err(parse::Error::new( 96 | Span::call_site(), 97 | format!("Action `{}` can only be reused when all input states, events, and output states have the same data", action), 98 | )); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | Ok(()) 106 | } 107 | 108 | fn validate_guard_signatures(sm: &ParsedStateMachine) -> Result<(), parse::Error> { 109 | // Collect all of the guard call signatures. 110 | let mut guards = HashMap::new(); 111 | 112 | let all_transitions = &sm.states_events_mapping; 113 | 114 | for (in_state_name, from_transitions) in all_transitions.iter() { 115 | let in_state_data = sm.state_data.data_types.get(in_state_name); 116 | 117 | for (_out_state_name, event_mapping) in from_transitions.iter() { 118 | // Get the data associated with this event. 119 | let event_data = sm 120 | .event_data 121 | .data_types 122 | .get(&event_mapping.event.to_string()); 123 | for transition in &event_mapping.transitions { 124 | if let Some(guard_expression) = &transition.guard { 125 | let res = visit_guards(guard_expression, |guard| { 126 | let signature = 127 | FunctionSignature::new_guard(in_state_data, event_data, guard.is_async); 128 | 129 | // If the action is not yet known, add it to our tracking list. 130 | guards 131 | .entry(guard.ident.to_string()) 132 | .or_insert_with(|| signature.clone()); 133 | 134 | // Check that the call signature is equivalent to the recorded signature for this 135 | // guard. 136 | if guards.get(&guard.ident.to_string()).unwrap() != &signature { 137 | return Err(parse::Error::new( 138 | Span::call_site(), 139 | format!("Guard `{}` can only be reused when all input states and events have the same data", guard.ident), 140 | )); 141 | } 142 | Ok(()) 143 | }); 144 | res?; 145 | } 146 | } 147 | } 148 | } 149 | 150 | Ok(()) 151 | } 152 | fn validate_unreachable_transitions(sm: &ParsedStateMachine) -> Result<(), parse::Error> { 153 | let all_transitions = &sm.states_events_mapping; 154 | for (in_state, event_mappings) in all_transitions { 155 | for (event, event_mapping) in event_mappings { 156 | // more than single transition for (in_state,event) 157 | if event_mapping.transitions.len() > 1 { 158 | let mut unguarded_count = 0; 159 | for t in &event_mapping.transitions { 160 | if let Some(g) = &t.guard { 161 | if unguarded_count > 0 { 162 | // Guarded transition AFTER an unguarded one 163 | return Err(parse::Error::new( 164 | Span::call_site(), 165 | format!("{} + {}: [{}] : guarded transition is unreachable because it follows an unguarded transition, which handles all cases", 166 | in_state, event, g), 167 | )); 168 | } 169 | } else { 170 | // unguarded 171 | unguarded_count += 1; 172 | if unguarded_count > 1 { 173 | return Err(parse::Error::new( 174 | Span::call_site(), 175 | format!("{} + {}: State and event combination specified multiple times, remove duplicates.", in_state, event), 176 | )); 177 | } 178 | } 179 | } 180 | } 181 | } 182 | } 183 | Ok(()) 184 | } 185 | 186 | /// Validate coherency of the state machine. 187 | pub fn validate(sm: &ParsedStateMachine) -> Result<(), parse::Error> { 188 | validate_action_signatures(sm)?; 189 | validate_guard_signatures(sm)?; 190 | validate_unreachable_transitions(sm)?; 191 | Ok(()) 192 | } 193 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # smlang 2 | //! 3 | //! `smlang` is a procedural macro library creating a state machine language DSL is to facilitate the 4 | //! use of state machines, as they quite fast can become overly complicated to write and get an 5 | //! overview of. 6 | //! 7 | //! # Project dependent documentation 8 | //! 9 | //! When this crate is used in a project the documentation will be auto generated in the 10 | //! **documentation of the project**, this comes from the procedural macro also generating 11 | //! documentation. 12 | //! 13 | #![doc = include_str!("../docs/dsl.md")] 14 | //! 15 | //! # Example 16 | //! 17 | //! Below is an example of the state machine macro in use along with the code that would be 18 | //! generated for this sample to demonstrate how this library is used. 19 | //! 20 | //! ```rust 21 | //! use smlang::statemachine; 22 | //! 23 | //! statemachine! { 24 | //! name: Sample, 25 | //! states_attr: #[derive(Debug)], 26 | //! events_attr: #[derive(Clone, Debug)], 27 | //! transitions: { 28 | //! *Init + InitEvent [ guard_init ] / action_init = Ready, 29 | //! } 30 | //! } 31 | //! ``` 32 | //! 33 | //! Results in the following code: 34 | //! ```ignore 35 | //! #[derive(Debug)] 36 | //! enum SampleStates { 37 | //! Init, 38 | //! Ready, 39 | //! } 40 | //! 41 | //! #[derive(Clone, Debug)] 42 | //! enum SampleEvents { 43 | //! InitEvent, 44 | //! } 45 | //! 46 | //! struct SampleStateMachine { 47 | //! // ... 48 | //! } 49 | //! 50 | //! enum SampleError { 51 | //! InvalidEvent, 52 | //! GuardFailed, 53 | //! // ... 54 | //! } 55 | //! 56 | //! impl SampleStateMachine { 57 | //! /// Creates a state machine with the starting state 58 | //! pub fn new() -> Self { /**/ } 59 | //! 60 | //! /// Returns the current state 61 | //! pub fn state(&self) -> States { /**/ } 62 | //! 63 | //! /// Process an event 64 | //! /// 65 | //! /// # Returns 66 | //! /// `Ok(NextState)` if the transition was successful or `Err()` if the transition failed. 67 | //! /// guard failed 68 | //! pub fn process_event(&mut self, event: Events) -> Result { 69 | //! # Err(SampleError::InvalidEvent); 70 | //! /**/ 71 | //! } 72 | //! } 73 | //! 74 | //! trait SampleStateMachineContext { 75 | //! // Called to guard the transition to `Ready`. Returns an `Err` if the guard fails. 76 | //! fn guard_init(&mut self) -> Result<(), ()>; 77 | //! 78 | //! // Called when transitioning to `Ready`. 79 | //! fn action_init(&mut self); 80 | //! } 81 | //! ``` 82 | #![no_std] 83 | 84 | pub use smlang_macros::statemachine; 85 | -------------------------------------------------------------------------------- /tests/compile-fail/double_state_event.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use smlang::statemachine; 4 | 5 | statemachine! { 6 | transitions: { 7 | *State1 + Event1 = State2, 8 | State1 + Event1 = State3, //~ State and event combination specified multiple times, remove duplicates. 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /tests/compile-fail/double_state_event.stderr: -------------------------------------------------------------------------------- 1 | error: State1 + Event1: State and event combination specified multiple times, remove duplicates. 2 | --> tests/compile-fail/double_state_event.rs:5:1 3 | | 4 | 5 | / statemachine! { 5 | 6 | | transitions: { 6 | 7 | | *State1 + Event1 = State2, 7 | 8 | | State1 + Event1 = State3, //~ State and event combination specified multiple times, remove duplicates. 8 | 9 | | } 9 | 10 | | } 10 | | |_^ 11 | | 12 | = note: this error originates in the macro `statemachine` (in Nightly builds, run with -Z macro-backtrace for more info) 13 | -------------------------------------------------------------------------------- /tests/compile-fail/duplicate_action.rs: -------------------------------------------------------------------------------- 1 | use smlang::statemachine; 2 | 3 | statemachine! { 4 | transitions: { 5 | *Init + Event / action = State1(u32), 6 | 7 | // This transition is not valid because `action` would have different input arguments from 8 | // earlier. 9 | State1(u32) + Event / action = State2(u32), 10 | } 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /tests/compile-fail/duplicate_action.stderr: -------------------------------------------------------------------------------- 1 | error: Action `action` can only be reused when all input states, events, and output states have the same data 2 | --> tests/compile-fail/duplicate_action.rs:3:1 3 | | 4 | 3 | / statemachine! { 5 | 4 | | transitions: { 6 | 5 | | *Init + Event / action = State1(u32), 7 | ... | 8 | 10 | | } 9 | 11 | | } 10 | | |_^ 11 | | 12 | = note: this error originates in the macro `statemachine` (in Nightly builds, run with -Z macro-backtrace for more info) 13 | -------------------------------------------------------------------------------- /tests/compile-fail/duplicate_guard.rs: -------------------------------------------------------------------------------- 1 | use smlang::statemachine; 2 | 3 | statemachine! { 4 | transitions: { 5 | *Init + Event [guard] / action = State1(u32), 6 | 7 | // This transition is not valid because `guard` would have different input arguments from 8 | // earlier. 9 | State1(u32) + Event [guard] / action2 = State2(u32), 10 | } 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /tests/compile-fail/duplicate_guard.stderr: -------------------------------------------------------------------------------- 1 | error: Guard `guard` can only be reused when all input states and events have the same data 2 | --> tests/compile-fail/duplicate_guard.rs:3:1 3 | | 4 | 3 | / statemachine! { 5 | 4 | | transitions: { 6 | 5 | | *Init + Event [guard] / action = State1(u32), 7 | ... | 8 | 10 | | } 9 | 11 | | } 10 | | |_^ 11 | | 12 | = note: this error originates in the macro `statemachine` (in Nightly builds, run with -Z macro-backtrace for more info) 13 | -------------------------------------------------------------------------------- /tests/compile-fail/guarded_transition_after_unguarded.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use smlang::statemachine; 4 | 5 | statemachine! { 6 | transitions: { 7 | State1 + Event1 = Fault, 8 | *State1 + Event1 [guard] = State2, 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /tests/compile-fail/guarded_transition_after_unguarded.stderr: -------------------------------------------------------------------------------- 1 | error: State1 + Event1: [guard()] : guarded transition is unreachable because it follows an unguarded transition, which handles all cases 2 | --> tests/compile-fail/guarded_transition_after_unguarded.rs:5:1 3 | | 4 | 5 | / statemachine! { 5 | 6 | | transitions: { 6 | 7 | | State1 + Event1 = Fault, 7 | 8 | | *State1 + Event1 [guard] = State2, 8 | 9 | | } 9 | 10 | | } 10 | | |_^ 11 | | 12 | = note: this error originates in the macro `statemachine` (in Nightly builds, run with -Z macro-backtrace for more info) 13 | -------------------------------------------------------------------------------- /tests/compile-fail/multiple_starting_state.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use smlang::statemachine; 4 | 5 | statemachine! { 6 | transitions: { 7 | //~ More than one starting state defined (indicated with *), remove duplicates. 8 | *State1 + Event1 = State2, 9 | *State2 + Event2 = State3, 10 | } 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /tests/compile-fail/multiple_starting_state.stderr: -------------------------------------------------------------------------------- 1 | error: More than one starting state defined (indicated with *), remove duplicates. 2 | --> tests/compile-fail/multiple_starting_state.rs:5:1 3 | | 4 | 5 | / statemachine! { 5 | 6 | | transitions: { 6 | 7 | | //~ More than one starting state defined (indicated with *), remove duplicates. 7 | 8 | | *State1 + Event1 = State2, 8 | 9 | | *State2 + Event2 = State3, 9 | 10 | | } 10 | 11 | | } 11 | | |_^ 12 | | 13 | = note: this error originates in the macro `statemachine` (in Nightly builds, run with -Z macro-backtrace for more info) 14 | -------------------------------------------------------------------------------- /tests/compile-fail/no_action_with_state_data.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use smlang::statemachine; 4 | 5 | statemachine! { 6 | transitions: { 7 | *State1 + Event1 = State2(u32), //~ This state has data associated, but not action is define here to provide it. 8 | } 9 | } 10 | 11 | fn main() {} 12 | 13 | -------------------------------------------------------------------------------- /tests/compile-fail/no_action_with_state_data.stderr: -------------------------------------------------------------------------------- 1 | error: This state has data associated, but not action is define here to provide it. 2 | --> $DIR/no_action_with_state_data.rs:7:28 3 | | 4 | 7 | *State1 + Event1 = State2(u32), //~ This state has data associated, but not action is define here to provide it. 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/compile-fail/no_starting_state.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use smlang::statemachine; 4 | 5 | statemachine! { 6 | transitions: { 7 | //~ ERROR No starting state defined, indicate the starting state with a * 8 | State1 + Event1 = State2, 9 | State2 + Event2 = State3, 10 | } 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /tests/compile-fail/no_starting_state.stderr: -------------------------------------------------------------------------------- 1 | error: No starting state defined, indicate the starting state with a *. 2 | --> $DIR/no_starting_state.rs:5:1 3 | | 4 | 5 | / statemachine! { 5 | 6 | | transitions: { 6 | 7 | | //~ ERROR No starting state defined, indicate the starting state with a * 7 | 8 | | State1 + Event1 = State2, 8 | 9 | | State2 + Event2 = State3, 9 | 10 | | } 10 | 11 | | } 11 | | |_^ 12 | | 13 | = note: this error originates in the macro `statemachine` (in Nightly builds, run with -Z macro-backtrace for more info) 14 | -------------------------------------------------------------------------------- /tests/compile-fail/wildcard_before_input_state.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use smlang::statemachine; 4 | 5 | statemachine! { 6 | transitions: { 7 | _ + Event1 = Fault, //~ State and event combination specified multiple times, remove duplicates. 8 | *State1 + Event1 = State2, 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /tests/compile-fail/wildcard_before_input_state.stderr: -------------------------------------------------------------------------------- 1 | error: State1 + Event1: State and event combination specified multiple times, remove duplicates. 2 | --> tests/compile-fail/wildcard_before_input_state.rs:5:1 3 | | 4 | 5 | / statemachine! { 5 | 6 | | transitions: { 6 | 7 | | _ + Event1 = Fault, //~ State and event combination specified multiple times, remove duplicates. 7 | 8 | | *State1 + Event1 = State2, 8 | 9 | | } 9 | 10 | | } 10 | | |_^ 11 | | 12 | = note: this error originates in the macro `statemachine` (in Nightly builds, run with -Z macro-backtrace for more info) 13 | -------------------------------------------------------------------------------- /tests/compile-fail/wildcard_no_effect.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use smlang::statemachine; 4 | 5 | statemachine! { 6 | transitions: { 7 | *State1 + Event1 = State2, 8 | _ + Event1 = Fault, 9 | _ + Event1 = State3, //~ Wildcard has no effect 10 | } 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /tests/compile-fail/wildcard_no_effect.stderr: -------------------------------------------------------------------------------- 1 | error: Wildcard has no effect 2 | --> tests/compile-fail/wildcard_no_effect.rs:9:9 3 | | 4 | 9 | _ + Event1 = State3, //~ Wildcard has no effect 5 | | ^ 6 | -------------------------------------------------------------------------------- /tests/compile-fail/wildcard_with_pattern.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use smlang::statemachine; 4 | 5 | statemachine! { 6 | transitions: { 7 | *State1 + Event1 = State2, 8 | _ | State2 + Event2 = State1, //~ Wildcards already include all states, so should not be used with input state patterns. 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /tests/compile-fail/wildcard_with_pattern.stderr: -------------------------------------------------------------------------------- 1 | error: Wildcards already include all states, so should not be used with input state patterns. 2 | --> tests/compile-fail/wildcard_with_pattern.rs:8:9 3 | | 4 | 8 | _ | State2 + Event2 = State1, //~ Wildcards already include all states, so should not be used with input state patterns. 5 | | ^ 6 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | extern crate smlang; 2 | 3 | use derive_more::Display; 4 | 5 | use smlang::statemachine; 6 | 7 | mod internal_macros { 8 | #[macro_export] 9 | macro_rules! assert_transition { 10 | ($sm:expr, $event:expr, $expected_state:expr, $expected_count:expr) => {{ 11 | let prev_state = $sm.state; 12 | $sm.process_event($event).unwrap(); 13 | println!("{:?} -> {:?} : {:?}", prev_state, $sm.state, $sm.context()); 14 | assert_eq!($expected_state, $sm.state); 15 | assert_eq!($expected_count, $sm.context().count); 16 | }}; 17 | } 18 | #[macro_export] 19 | macro_rules! assert_transition_ok { 20 | ($sm:expr, $event:expr, $expected_action:expr, $expected_result:pat) => {{ 21 | let prev_state = $sm.state; 22 | if let Ok(result) = $sm.process_event($event) { 23 | let result = result.clone(); 24 | println!("{:?} -> {:?} : {:?}", prev_state, result, $sm.context()); 25 | match result { 26 | $expected_result => {} 27 | _ => panic!( 28 | "Assertion failed:\n expected: {:?},\n actual: {:?}", 29 | stringify!($expected_result), 30 | result 31 | ), 32 | } 33 | assert_eq!($expected_action, $sm.context().action); 34 | } else { 35 | panic!("assert_transition_ok failed") 36 | } 37 | }}; 38 | } 39 | } 40 | 41 | #[test] 42 | fn compile_fail_tests() { 43 | let t = trybuild::TestCases::new(); 44 | t.compile_fail("tests/compile-fail/*.rs"); 45 | } 46 | #[test] 47 | fn wildcard_after_input_state() { 48 | statemachine! { 49 | transitions: { 50 | *State1 + Event1 = State2, 51 | _ + Event1 = Fault, 52 | } 53 | } 54 | 55 | struct Context; 56 | impl StateMachineContext for Context {} 57 | 58 | let mut sm = StateMachine::new(Context); 59 | 60 | sm.process_event(Events::Event1).unwrap(); 61 | assert!(matches!(sm.state(), &States::State2)); 62 | 63 | sm.process_event(Events::Event1).unwrap(); 64 | assert!(matches!(sm.state(), &States::Fault)); 65 | } 66 | 67 | #[test] 68 | fn multiple_lifetimes() { 69 | pub struct X; 70 | pub struct Y; 71 | pub struct Z; 72 | 73 | statemachine! { 74 | transitions: { 75 | *State1 + Event1(&'a X) [guard1] / action1 = State2(&'a X), 76 | State2(&'a X) + Event2(&'b Y) [guard2] / action2 = State3((&'a X, &'b Y)), 77 | State4 + Event(&'c Z) [guard3] / action3 = State5, 78 | } 79 | } 80 | 81 | #[allow(dead_code)] 82 | struct Context; 83 | 84 | impl StateMachineContext for Context { 85 | fn guard1(&self, _event_data: &X) -> Result { 86 | Ok(true) 87 | } 88 | 89 | fn guard2(&self, _state_data: &X, _event_data: &Y) -> Result { 90 | Ok(true) 91 | } 92 | 93 | fn guard3(&self, _event_data: &Z) -> Result { 94 | Ok(true) 95 | } 96 | 97 | fn action1<'a>(&mut self, event_data: &'a X) -> Result<&'a X, ()> { 98 | Ok(event_data) 99 | } 100 | 101 | fn action2<'a, 'b>( 102 | &mut self, 103 | state_data: &'a X, 104 | event_data: &'b Y, 105 | ) -> Result<(&'a X, &'b Y), ()> { 106 | Ok((state_data, event_data)) 107 | } 108 | 109 | fn action3(&mut self, _event_data: &Z) -> Result<(), ()> { 110 | Ok(()) 111 | } 112 | } 113 | 114 | #[allow(dead_code)] 115 | struct WrappedStates<'a, 'b>(States<'a, 'b>); 116 | 117 | #[allow(dead_code)] 118 | struct WrappedEvents<'a, 'b, 'c>(Events<'a, 'b, 'c>); 119 | } 120 | 121 | #[test] 122 | fn derive_display_events_states() { 123 | statemachine! { 124 | events_attr: #[derive(Debug,Display)], 125 | states_attr: #[derive(Debug,Display)], 126 | transitions: { 127 | *Init + Event = End, 128 | } 129 | } 130 | 131 | struct Context; 132 | impl StateMachineContext for Context {} 133 | 134 | let mut sm = StateMachine::new(Context); 135 | assert!(matches!(sm.state(), &States::Init)); 136 | 137 | let event = Events::Event; 138 | assert_eq!(format!("{}", event), "Event"); 139 | 140 | sm.process_event(event).unwrap(); 141 | assert!(matches!(sm.state(), &States::End)); 142 | } 143 | 144 | #[test] 145 | fn named_derive_display_events_states() { 146 | statemachine! { 147 | name: SM, 148 | events_attr: #[derive(Debug,Display)], 149 | states_attr: #[derive(Debug,Display)], 150 | transitions: { 151 | *Init + Event = End, 152 | } 153 | } 154 | 155 | struct Context; 156 | impl SMStateMachineContext for Context {} 157 | 158 | let mut sm = SMStateMachine::new(Context); 159 | assert!(matches!(sm.state(), &SMStates::Init)); 160 | 161 | let event = SMEvents::Event; 162 | assert_eq!(format!("{}", event), "Event"); 163 | 164 | sm.process_event(event).unwrap(); 165 | assert!(matches!(sm.state(), &SMStates::End)); 166 | } 167 | 168 | #[test] 169 | fn async_guards_and_actions() { 170 | use smol; 171 | 172 | smol::block_on(async { 173 | statemachine! { 174 | transitions: { 175 | *State1 + Event1 [async guard1] / async action1 = State2, 176 | _ + Event1 = Fault, 177 | } 178 | } 179 | 180 | struct Context; 181 | 182 | impl StateMachineContext for Context { 183 | async fn guard1(&self) -> Result { 184 | Ok(true) 185 | } 186 | 187 | async fn action1(&mut self) -> Result<(), ()> { 188 | Ok(()) 189 | } 190 | } 191 | 192 | let mut sm = StateMachine::new(Context); 193 | 194 | sm.process_event(Events::Event1).await.unwrap(); 195 | assert!(matches!(sm.state(), &States::State2)); 196 | 197 | sm.process_event(Events::Event1).await.unwrap(); 198 | assert!(matches!(sm.state(), &States::Fault)); 199 | }); 200 | } 201 | 202 | #[test] 203 | fn async_on_entry_and_exit() { 204 | use smol; 205 | 206 | smol::block_on(async { 207 | statemachine! { 208 | entry_exit_async: true, 209 | transitions: { 210 | *State1 + Event1 = State2, 211 | State2 + Event2 = State3, 212 | } 213 | } 214 | 215 | struct Context; 216 | 217 | impl StateMachineContext for Context { 218 | async fn on_entry_state1(&mut self) {} 219 | 220 | async fn on_exit_state1(&mut self) {} 221 | 222 | async fn on_entry_state2(&mut self) {} 223 | 224 | async fn on_exit_state2(&mut self) {} 225 | } 226 | 227 | let mut sm = StateMachine::new(Context); 228 | 229 | sm.process_event(Events::Event1).await.unwrap(); 230 | assert!(matches!(sm.state(), &States::State2)); 231 | 232 | sm.process_event(Events::Event2).await.unwrap(); 233 | assert!(matches!(sm.state(), &States::State3)); 234 | }); 235 | } 236 | 237 | #[test] 238 | fn guard_expressions() { 239 | #[derive(PartialEq, Display)] 240 | pub struct Entry(pub u32); 241 | 242 | statemachine! { 243 | states_attr: #[derive(Display, Debug)], 244 | transitions: { 245 | *Init + Login(&'a Entry) [valid_entry] / attempt = LoggedIn, 246 | Init + Login(&'a Entry) [!valid_entry && !too_many_attempts] / attempt = Init, 247 | Init + Login(&'a Entry) [!valid_entry && too_many_attempts] / attempt = LoginDenied, 248 | LoggedIn + Logout / reset = Init, 249 | } 250 | } 251 | 252 | /// Context 253 | pub struct Context { 254 | password: u32, 255 | attempts: u32, 256 | } 257 | impl StateMachineContext for Context { 258 | fn valid_entry(&self, e: &Entry) -> Result { 259 | Ok(e.0 == self.password) 260 | } 261 | fn too_many_attempts(&self, _e: &Entry) -> Result { 262 | Ok(self.attempts >= 3) 263 | } 264 | fn reset(&mut self) -> Result<(), ()> { 265 | self.attempts = 0; 266 | Ok(()) 267 | } 268 | fn attempt(&mut self, _e: &Entry) -> Result<(), ()> { 269 | self.attempts += 1; 270 | Ok(()) 271 | } 272 | } 273 | 274 | let mut sm = StateMachine::new(Context { 275 | password: 42, 276 | attempts: 0, 277 | }); 278 | assert!(matches!(sm.state(), &States::Init)); 279 | 280 | let bad_entry = Entry(10); 281 | let good_entry = Entry(42); 282 | 283 | let _ = sm.process_event(Events::Login(&bad_entry)); 284 | assert_eq!(sm.context().attempts, 1); 285 | assert!(matches!(sm.state(), &States::Init)); 286 | 287 | let _ = sm.process_event(Events::Login(&bad_entry)); 288 | assert_eq!(sm.context().attempts, 2); 289 | assert!(matches!(sm.state(), &States::Init)); 290 | 291 | let _ = sm.process_event(Events::Login(&good_entry)); 292 | assert_eq!(sm.context().attempts, 3); 293 | assert!(matches!(sm.state(), &States::LoggedIn)); 294 | 295 | let _ = sm.process_event(Events::Logout); 296 | assert_eq!(sm.context().attempts, 0); 297 | assert!(matches!(sm.state(), &States::Init)); 298 | 299 | let _ = sm.process_event(Events::Login(&bad_entry)); 300 | let _ = sm.process_event(Events::Login(&bad_entry)); 301 | let _ = sm.process_event(Events::Login(&bad_entry)); 302 | assert_eq!(sm.context().attempts, 3); 303 | assert!(matches!(sm.state(), &States::Init)); 304 | 305 | // exhausted attempts 306 | let _ = sm.process_event(Events::Login(&bad_entry)); 307 | assert_eq!(sm.context().attempts, 4); 308 | assert!(matches!(sm.state(), &States::LoginDenied)); 309 | 310 | // Invalid event, as we are in a final state 311 | assert_eq!( 312 | sm.process_event(Events::Login(&good_entry)), 313 | Err(Error::InvalidEvent) 314 | ); 315 | assert_eq!(sm.context().attempts, 4); 316 | assert!(matches!(sm.state(), &States::LoginDenied)); 317 | } 318 | #[test] 319 | fn guarded_transition_before_unguarded() { 320 | use smlang::statemachine; 321 | statemachine! { 322 | transitions: { 323 | *State1 + Event1 [guard] / disable = State2, 324 | State1 + Event1 = Fault, 325 | State2 + Event1 = State1, 326 | } 327 | } 328 | struct Context { 329 | pub enabled: bool, 330 | } 331 | impl StateMachineContext for Context { 332 | fn guard(&self) -> Result { 333 | Ok(self.enabled) 334 | } 335 | 336 | fn disable(&mut self) -> Result<(), ()> { 337 | self.enabled = false; 338 | Ok(()) 339 | } 340 | } 341 | let mut sm = StateMachine::new(Context { enabled: true }); 342 | sm.process_event(Events::Event1).unwrap(); 343 | assert!(matches!(sm.state(), &States::State2)); 344 | 345 | sm.process_event(Events::Event1).unwrap(); 346 | assert!(matches!(sm.state(), &States::State1)); 347 | 348 | sm.process_event(Events::Event1).unwrap(); 349 | assert!(matches!(sm.state(), &States::Fault)); 350 | } 351 | 352 | #[test] 353 | fn guard_errors() { 354 | use smlang::statemachine; 355 | statemachine! { 356 | transitions: { 357 | *Init + Event1 [guard] = Done, 358 | } 359 | } 360 | 361 | struct Context { 362 | pub guard_passable: bool, 363 | pub guard_errors: bool, 364 | } 365 | impl StateMachineContext for Context { 366 | fn guard(&self) -> Result { 367 | if self.guard_errors { 368 | Err(()) 369 | } else { 370 | Ok(self.guard_passable) 371 | } 372 | } 373 | } 374 | 375 | let mut sm = StateMachine::new(Context { 376 | guard_passable: false, 377 | guard_errors: true, 378 | }); 379 | 380 | // Test attempting to transition when the guard fails. 381 | sm.context_mut().guard_errors = true; 382 | assert!(sm.process_event(Events::Event1).is_err()); 383 | assert!(matches!(sm.state(), &States::Init)); 384 | 385 | // Test attempting to transition when the guard is not passable. 386 | sm.context_mut().guard_errors = false; 387 | assert!(sm.process_event(Events::Event1).is_err()); 388 | assert!(matches!(sm.state(), &States::Init)); 389 | 390 | assert!(sm.process_event(Events::Event1).is_err()); 391 | assert!(matches!(sm.state(), &States::Init)); 392 | 393 | sm.context_mut().guard_passable = true; 394 | sm.process_event(Events::Event1).unwrap(); 395 | assert!(matches!(sm.state(), &States::Done)); 396 | } 397 | #[test] 398 | fn test_internal_transition_with_data() { 399 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 400 | pub struct State1Data(pub ActionId); 401 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 402 | pub struct State3Data(pub ActionId); 403 | 404 | statemachine! { 405 | transitions: { 406 | *State1(State1Data) + Event2 / action12 = State2, 407 | State1(State1Data) + Event3 / action13 = State3(State3Data), 408 | State1(State1Data) + Event4 / action14 = State4(State3Data), 409 | 410 | State2 + Event3 / action23 = State3(State3Data), 411 | State4(State3Data) + Event1 / action44 = _, // Same as State4(State3Data) + Event1 / action44 412 | 413 | // TRANSITION : _ + Event3 / increment_count = _, IS EQUIVALENT TO THE FOLLOWING TWO: 414 | // State3(State3Data) + Event3 / action_3 = State3(State3Data), 415 | // State4(State3Data) + Event3 / action_3 = State4(State3Data), 416 | _ + Event3 / action_3 = _, 417 | }, 418 | states_attr: #[derive(Debug, Clone, Copy, Eq)] 419 | } 420 | /// Context 421 | #[derive(Default, Debug, PartialEq, Eq)] 422 | pub struct Context { 423 | action: ActionId, 424 | } 425 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] 426 | enum ActionId { 427 | #[default] 428 | Init, 429 | Action3, 430 | Action12, 431 | Action13, 432 | Action14, 433 | Action23, 434 | Action44, 435 | } 436 | impl StateMachineContext for Context { 437 | fn action_3(&mut self, d: &State3Data) -> Result { 438 | self.action = ActionId::Action3; 439 | Ok(*d) 440 | } 441 | 442 | fn action44(&mut self, _d: &State3Data) -> Result { 443 | self.action = ActionId::Action44; 444 | Ok(State3Data(ActionId::Action44)) 445 | } 446 | fn action14(&mut self, _d: &State1Data) -> Result { 447 | self.action = ActionId::Action14; 448 | Ok(State3Data(ActionId::Action14)) 449 | } 450 | fn action12(&mut self, _d: &State1Data) -> Result<(), ()> { 451 | self.action = ActionId::Action12; 452 | Ok(()) 453 | } 454 | fn action13(&mut self, _d: &State1Data) -> Result { 455 | self.action = ActionId::Action13; 456 | Ok(State3Data(ActionId::Action13)) 457 | } 458 | fn action23(&mut self) -> Result { 459 | self.action = ActionId::Action23; 460 | Ok(State3Data(ActionId::Action23)) 461 | } 462 | } 463 | 464 | { 465 | let mut sm = StateMachine::new(Context::default(), State1Data(ActionId::Init)); 466 | matches!(States::State2, States::State2); 467 | assert_transition_ok!(sm, Events::Event2, ActionId::Action12, States::State2); 468 | assert!(sm.process_event(Events::Event1).is_err()); 469 | assert!(sm.process_event(Events::Event2).is_err()); 470 | assert!(sm.process_event(Events::Event4).is_err()); 471 | assert_transition_ok!(sm, Events::Event3, ActionId::Action23, States::State3(_)); 472 | assert_transition_ok!(sm, Events::Event3, ActionId::Action3, States::State3(_)); 473 | assert_transition_ok!(sm, Events::Event3, ActionId::Action3, States::State3(_)); 474 | assert!(sm.process_event(Events::Event1).is_err()); 475 | assert!(sm.process_event(Events::Event2).is_err()); 476 | assert!(sm.process_event(Events::Event4).is_err()); 477 | } 478 | { 479 | let mut sm = StateMachine::new(Context::default(), State1Data(ActionId::Init)); 480 | assert_transition_ok!(sm, Events::Event3, ActionId::Action13, States::State3(_)); 481 | assert!(sm.process_event(Events::Event1).is_err()); 482 | assert!(sm.process_event(Events::Event2).is_err()); 483 | assert!(sm.process_event(Events::Event4).is_err()); 484 | } 485 | { 486 | let mut sm = StateMachine::new(Context::default(), State1Data(ActionId::Init)); 487 | assert_transition_ok!(sm, Events::Event4, ActionId::Action14, States::State4(_)); 488 | assert_transition_ok!(sm, Events::Event1, ActionId::Action44, States::State4(_)); 489 | assert_transition_ok!(sm, Events::Event3, ActionId::Action3, States::State4(_)); 490 | } 491 | } 492 | #[test] 493 | fn test_wildcard_states_and_internal_transitions() { 494 | statemachine! { 495 | transitions: { 496 | *State1 + Event2 = State2, 497 | State2 + Event3 = State3, 498 | _ + Event1 / increment_count, // Internal transition (implicit: omitting target state) 499 | _ + Event3 / increment_count = _ , // Internal transition (explicit: using _ as target state) 500 | }, 501 | states_attr: #[derive(Debug, Clone, Copy)] 502 | } 503 | #[derive(Debug)] 504 | pub struct Context { 505 | count: u32, 506 | } 507 | impl StateMachineContext for Context { 508 | fn increment_count(&mut self) -> Result<(), ()> { 509 | self.count += 1; 510 | Ok(()) 511 | } 512 | } 513 | 514 | let mut sm = StateMachine::new(Context { count: 0 }); 515 | 516 | assert_transition!(sm, Events::Event1, States::State1, 1); 517 | assert_transition!(sm, Events::Event2, States::State2, 1); 518 | assert_transition!(sm, Events::Event3, States::State3, 1); 519 | assert_transition!(sm, Events::Event1, States::State3, 2); 520 | assert_transition!(sm, Events::Event3, States::State3, 3); 521 | 522 | assert!(sm.process_event(Events::Event2).is_err()); // InvalidEvent 523 | assert_eq!(States::State3, sm.state); 524 | } 525 | #[test] 526 | fn test_specify_attrs() { 527 | #![deny(non_camel_case_types)] 528 | use serde::Serialize; 529 | statemachine! { 530 | transitions: { 531 | *State1 + tostate2 = State2, 532 | State2 + tostate3 / increment_count = State3 533 | }, 534 | states_attr: #[derive(Debug, Clone, Copy, Serialize)] #[non_exhaustive] #[repr(u8)] #[serde(tag="type")], 535 | events_attr: #[derive(Debug)] #[allow(non_camel_case_types)] 536 | } 537 | 538 | #[derive(Debug, Default)] 539 | pub struct Context { 540 | count: u32, 541 | } 542 | 543 | impl StateMachineContext for Context { 544 | fn increment_count(&mut self) -> Result<(), ()> { 545 | self.count += 1; 546 | Ok(()) 547 | } 548 | } 549 | let mut sm = StateMachine::new(Context::default()); 550 | 551 | assert_eq!(sm.state().clone(), States::State1); 552 | assert_transition!(sm, Events::tostate2, States::State2, 0); 553 | assert_transition!(sm, Events::tostate3, States::State3, 1); 554 | } 555 | --------------------------------------------------------------------------------