├── .clippy.toml ├── typestate-book ├── src │ ├── annex_1.md │ ├── features │ │ ├── features.md │ │ ├── macro_attributes.md │ │ └── compilation_flags.md │ ├── SUMMARY.md │ ├── chapter_1.md │ ├── chapter_4.md │ ├── chapter_2.md │ ├── static │ │ ├── DotLightBulb.svg │ │ ├── UmlLightBulb.svg │ │ ├── DotSmartBulb.svg │ │ └── UmlSmartBulb.svg │ └── chapter_3.md ├── .gitignore └── book.toml ├── .vscode └── settings.json ├── .gitignore ├── articles ├── cola22.pdf ├── sblp21.pdf └── thesis.pdf ├── tests ├── pass │ ├── empty_module.rs │ ├── stateful_automata.rs │ ├── empty_enumerate.rs │ ├── empty_constructors.rs │ ├── stateful_automata_enumerate.rs │ ├── stateful_automata_constructors.rs │ ├── generics_automata.rs │ ├── non-deterministic_initial_state.rs │ └── lifetimes_automata.rs ├── fail │ ├── duplicate_state_attr.stderr │ ├── empty_automata.rs │ ├── nested_enums.stderr │ ├── duplicate_automata_attr.stderr │ ├── conflicting_state_attr.stderr │ ├── undeclared_variant.stderr │ ├── duplicate_automata_attr.rs │ ├── unsupported_variant.stderr │ ├── missing_initial_final_states.rs │ ├── conflicting_automata_attr.stderr │ ├── duplicate_state_attr.rs │ ├── conflicting_state_attr.rs │ ├── conflicting_automata_attr.rs │ ├── complex_unreachable_state.stderr │ ├── undeclared_variant.rs │ ├── unsupported_variant.rs │ ├── complex_unreachable_state.rs │ ├── empty_automata.stderr │ ├── missing_initial_final_states.stderr │ └── nested_enums.rs └── macro_tests.rs ├── typestate-proc-macro ├── src │ ├── visitors │ │ ├── mod.rs │ │ ├── decision.rs │ │ ├── state.rs │ │ └── transition.rs │ ├── igraph │ │ ├── mod.rs │ │ ├── validate.rs │ │ └── export.rs │ └── lib.rs ├── Cargo.toml ├── LICENSE-MIT └── LICENSE-APACHE ├── examples ├── generics.rs ├── bulbs │ ├── dead_bulb.rs │ ├── smart_bulb.rs │ └── light_bulb.rs └── traffic_light.rs ├── CITATION.cff ├── .github └── workflows │ ├── deploy-book.yml │ └── ci.yml ├── justfile ├── LICENSE-MIT ├── Cargo.toml ├── README.md ├── CONTRIBUTING.md ├── assets ├── DotLightBulb.svg ├── UmlLightBulb.svg ├── DotSmartBulb.svg └── UmlSmartBulb.svg ├── src └── lib.rs └── LICENSE-APACHE /.clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.42.0" -------------------------------------------------------------------------------- /typestate-book/src/annex_1.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /typestate-book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /typestate-book/src/features/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [100] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | Cargo.lock 3 | *.svg 4 | *.dot 5 | *.png 6 | *.uml -------------------------------------------------------------------------------- /articles/cola22.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustype/typestate-rs/HEAD/articles/cola22.pdf -------------------------------------------------------------------------------- /articles/sblp21.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustype/typestate-rs/HEAD/articles/sblp21.pdf -------------------------------------------------------------------------------- /articles/thesis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustype/typestate-rs/HEAD/articles/thesis.pdf -------------------------------------------------------------------------------- /tests/pass/empty_module.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m {} 5 | 6 | fn main() {} -------------------------------------------------------------------------------- /typestate-proc-macro/src/visitors/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod decision; 2 | pub(crate) mod state; 3 | pub(crate) mod transition; 4 | -------------------------------------------------------------------------------- /tests/fail/duplicate_state_attr.stderr: -------------------------------------------------------------------------------- 1 | error: Duplicate attribute. 2 | --> $DIR/duplicate_state_attr.rs:9:5 3 | | 4 | 9 | #[state] 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /typestate-book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["José Duarte"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "The Typestate Book" 7 | -------------------------------------------------------------------------------- /tests/fail/empty_automata.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[automaton] 6 | struct S {} 7 | } 8 | 9 | fn main() {} -------------------------------------------------------------------------------- /tests/fail/nested_enums.stderr: -------------------------------------------------------------------------------- 1 | error: `enum` variants cannot refer to other `enum`s. 2 | --> $DIR/nested_enums.rs:34:9 3 | | 4 | 34 | A 5 | | ^ 6 | -------------------------------------------------------------------------------- /tests/fail/duplicate_automata_attr.stderr: -------------------------------------------------------------------------------- 1 | error: Duplicate attribute. 2 | --> $DIR/duplicate_automata_attr.rs:6:5 3 | | 4 | 6 | #[automaton] 5 | | ^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/fail/conflicting_state_attr.stderr: -------------------------------------------------------------------------------- 1 | error: Conflicting attributes are declared. 2 | --> $DIR/conflicting_state_attr.rs:9:5 3 | | 4 | 9 | #[state] 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/fail/undeclared_variant.stderr: -------------------------------------------------------------------------------- 1 | error: `enum` variant is not a declared state. 2 | --> $DIR/undeclared_variant.rs:19:9 3 | | 4 | 19 | Second 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/fail/duplicate_automata_attr.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[automaton] 6 | #[automaton] 7 | struct S {} 8 | } 9 | 10 | fn main() {} -------------------------------------------------------------------------------- /tests/fail/unsupported_variant.stderr: -------------------------------------------------------------------------------- 1 | error: Only unit (C-like) `enum` variants are supported. 2 | --> $DIR/unsupported_variant.rs:19:9 3 | | 4 | 19 | Second(B) 5 | | ^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/macro_tests.rs: -------------------------------------------------------------------------------- 1 | #[rustversion::stable] 2 | #[test] 3 | fn compile() { 4 | let t = trybuild::TestCases::new(); 5 | t.compile_fail("tests/fail/*.rs"); 6 | t.pass("tests/pass/*.rs"); 7 | } 8 | -------------------------------------------------------------------------------- /tests/fail/missing_initial_final_states.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[automaton] 6 | struct S {} 7 | 8 | #[state] 9 | struct A {} 10 | } 11 | 12 | fn main() {} -------------------------------------------------------------------------------- /tests/fail/conflicting_automata_attr.stderr: -------------------------------------------------------------------------------- 1 | error: Conflicting attributes are declared. 2 | --> $DIR/conflicting_automata_attr.rs:6:5 3 | | 4 | 6 | #[automaton] // this should error because it is the second one 5 | | ^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/fail/duplicate_state_attr.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[automaton] 6 | struct S {} 7 | 8 | #[state] 9 | #[state] 10 | struct A {} 11 | } 12 | 13 | fn main() {} -------------------------------------------------------------------------------- /tests/fail/conflicting_state_attr.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[automaton] 6 | struct S {} 7 | 8 | #[automaton] 9 | #[state] 10 | struct A {} 11 | } 12 | 13 | fn main() {} -------------------------------------------------------------------------------- /tests/fail/conflicting_automata_attr.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[state] 6 | #[automaton] // this should error because it is the second one 7 | struct S {} 8 | } 9 | 10 | fn main() {} -------------------------------------------------------------------------------- /tests/fail/complex_unreachable_state.stderr: -------------------------------------------------------------------------------- 1 | error: Non-productive state. For a state to be productive, a path from the state to a final state is required to exist. 2 | --> $DIR/complex_unreachable_state.rs:20:16 3 | | 4 | 20 | pub struct RedA; 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /tests/pass/stateful_automata.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[automaton] 6 | struct S {} 7 | 8 | #[state] 9 | struct A {} 10 | 11 | trait A { 12 | fn start() -> A; 13 | fn end(self); 14 | } 15 | } 16 | 17 | fn main() {} -------------------------------------------------------------------------------- /tests/pass/empty_enumerate.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate( 4 | enumerate = "" 5 | )] 6 | mod m { 7 | #[automaton] 8 | struct S {} 9 | 10 | #[state] 11 | struct A {} 12 | 13 | trait A { 14 | fn start() -> A; 15 | fn end(self); 16 | } 17 | } 18 | 19 | fn main() {} -------------------------------------------------------------------------------- /tests/pass/empty_constructors.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate( 4 | state_constructors = "" 5 | )] 6 | mod m { 7 | #[automaton] 8 | struct S {} 9 | 10 | #[state] 11 | struct A {} 12 | 13 | trait A { 14 | fn start() -> A; 15 | fn end(self); 16 | } 17 | } 18 | 19 | fn main() {} -------------------------------------------------------------------------------- /tests/pass/stateful_automata_enumerate.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate( 4 | enumerate = "E" 5 | )] 6 | mod m { 7 | #[automaton] 8 | struct S {} 9 | 10 | #[state] 11 | struct A {} 12 | 13 | trait A { 14 | fn start() -> A; 15 | fn end(self); 16 | } 17 | } 18 | 19 | fn main() {} -------------------------------------------------------------------------------- /typestate-book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Quick Intro](./chapter_1.md) 4 | - [What are typestates?](./chapter_2.md) 5 | - [Basic Guide to Typestates](./chapter_3.md) 6 | - [Advanced Guide](./chapter_4.md) 7 | - [Features](./features/features.md) 8 | - [Macro Attributes](./features/macro_attributes.md) 9 | - [Compilation Flags](./features/compilation_flags.md) -------------------------------------------------------------------------------- /examples/generics.rs: -------------------------------------------------------------------------------- 1 | use typestate::typestate; 2 | 3 | #[typestate] 4 | mod my_state { 5 | #[automaton] 6 | pub struct MyState; 7 | 8 | #[state] 9 | pub struct State1 { 10 | data: T, 11 | } 12 | 13 | trait State1 { 14 | fn new() -> State1; 15 | 16 | fn done(self); 17 | } 18 | } 19 | 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /tests/pass/stateful_automata_constructors.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate( 4 | state_constructors = "new_state" 5 | )] 6 | mod m { 7 | #[automaton] 8 | struct S {} 9 | 10 | #[state] 11 | struct A {} 12 | 13 | trait A { 14 | fn start() -> A; 15 | fn end(self); 16 | } 17 | } 18 | 19 | fn main() {} -------------------------------------------------------------------------------- /tests/pass/generics_automata.rs: -------------------------------------------------------------------------------- 1 | use typestate::typestate; 2 | 3 | #[typestate] 4 | mod my_state { 5 | #[automaton] 6 | pub struct MyState; 7 | 8 | #[state] 9 | pub struct State1 { 10 | data: T, 11 | } 12 | 13 | trait State1 { 14 | fn new() -> State1; 15 | 16 | fn done(self); 17 | } 18 | } 19 | 20 | fn main() {} -------------------------------------------------------------------------------- /tests/fail/undeclared_variant.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | fn main() {} 4 | 5 | #[typestate] 6 | mod undeclared_variant { 7 | #[automaton] 8 | pub struct A; 9 | 10 | #[state] 11 | pub struct First; 12 | pub trait First { 13 | fn new() -> First; 14 | fn end(self); 15 | } 16 | 17 | enum E { 18 | First, 19 | Second 20 | } 21 | } -------------------------------------------------------------------------------- /tests/fail/unsupported_variant.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | fn main() {} 4 | 5 | #[typestate] 6 | mod invalid_variant { 7 | #[automaton] 8 | pub struct A; 9 | 10 | #[state] 11 | pub struct First; 12 | pub trait First { 13 | fn new() -> First; 14 | fn end(self); 15 | } 16 | 17 | enum E { 18 | First, 19 | Second(B) 20 | } 21 | } -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.1.0 2 | message: "If you use this library in your project, please cite it as below." 3 | authors: 4 | - family-names: "Duarte" 5 | given-names: "José" 6 | - family-names: "Ravara" 7 | given-names: "António" 8 | orcid: https://orcid.org/0000-0001-8074-0380 9 | title: "Retrofitting Typestates into Rust" 10 | doi: 10.1145/3475061.3475082 11 | version: 0.8.0 12 | release-date: 2021-07-16 13 | url: "https://github.com/rustype/typestate-rs/" -------------------------------------------------------------------------------- /tests/pass/non-deterministic_initial_state.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[automaton] 6 | struct S {} 7 | 8 | #[state] 9 | struct A {} 10 | 11 | #[state] 12 | struct B {} 13 | 14 | #[state] 15 | struct Error {} 16 | 17 | trait A { 18 | fn start() -> Fallible; 19 | fn next(self) -> B; 20 | } 21 | 22 | trait B { 23 | fn end(self); 24 | } 25 | 26 | trait Error { 27 | fn consume(self); 28 | } 29 | 30 | enum Fallible { 31 | A, 32 | Error, 33 | } 34 | } 35 | 36 | fn main() {} 37 | -------------------------------------------------------------------------------- /tests/pass/lifetimes_automata.rs: -------------------------------------------------------------------------------- 1 | use typestate::typestate; 2 | 3 | #[typestate] 4 | mod m { 5 | #[automaton] 6 | struct Player {} 7 | 8 | #[state] 9 | struct Alive<'name> { 10 | name: &'name str 11 | } 12 | 13 | #[state] 14 | struct Dead<'name> { 15 | name: &'name str 16 | } 17 | 18 | enum PlayerLifeState<'name> { 19 | Alive, 20 | Dead, 21 | } 22 | 23 | trait Alive<'name> { 24 | fn start(name: &str) -> Alive; 25 | fn damage(self) -> PlayerLifeState; 26 | fn suicide(self) -> Dead<'name>; 27 | } 28 | 29 | trait Dead { 30 | fn end(self); 31 | } 32 | } 33 | 34 | fn main() {} -------------------------------------------------------------------------------- /.github/workflows/deploy-book.yml: -------------------------------------------------------------------------------- 1 | name: Deploy mdBook 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup mdBook 15 | uses: peaceiris/actions-mdbook@v1 16 | with: 17 | mdbook-version: '0.4.14' 18 | 19 | - name: Build mdBook 20 | run: | 21 | cd ./typestate-book/ 22 | mdbook build 23 | 24 | - name: Deploy mdBook 25 | uses: peaceiris/actions-gh-pages@v3 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: ./typestate-book/book -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: 2 | @just --list 3 | 4 | build: 5 | cargo build 6 | 7 | preinstall: 8 | rustup toolchain install nightly 9 | cargo install cargo-expand 10 | 11 | clean-diagrams: 12 | rm *.uml *.dot 13 | 14 | clean-build: 15 | cargo clean 16 | 17 | @clean: 18 | just clean-diagrams 19 | just clean-build 20 | 21 | test: 22 | cargo test --all 23 | 24 | fmt: 25 | cargo fmt --all 26 | 27 | # pre-commit only 28 | 29 | fmt-check: 30 | git diff --name-only --cached | grep ".rs" | xargs rustfmt --check --edition 2018 -l 31 | 32 | clippy: 33 | cargo clippy --tests -- -Dclippy::all 34 | 35 | # book 36 | 37 | serve: 38 | mdbook serve typestate-book/ --open 39 | -------------------------------------------------------------------------------- /tests/fail/complex_unreachable_state.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate] 4 | mod traffic_light { 5 | #[automaton] 6 | pub struct TrafficLight { 7 | pub cycles: u64, 8 | } 9 | 10 | #[state] 11 | pub struct Green; 12 | 13 | #[state] 14 | pub struct Yellow; 15 | 16 | #[state] 17 | pub struct Red; 18 | 19 | #[state] 20 | pub struct RedA; 21 | 22 | pub trait Green { 23 | fn to_yellow(self) -> Yellow; 24 | } 25 | 26 | pub trait Yellow { 27 | fn to_red(self) -> Red; 28 | } 29 | 30 | pub trait Red { 31 | fn to_green(self) -> Green; 32 | fn turn_on() -> Red; 33 | fn turn_off(self); 34 | } 35 | } 36 | 37 | fn main() {} 38 | -------------------------------------------------------------------------------- /tests/fail/empty_automata.stderr: -------------------------------------------------------------------------------- 1 | error: Missing initial state. To declare an initial state you can use a function with signature like `fn f() -> T` where `T` is a declared state. 2 | --> $DIR/empty_automata.rs:3:1 3 | | 4 | 3 | #[typestate] 5 | | ^^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the attribute macro `typestate` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: Missing final state. To declare a final state you can use a function with signature like `fn f(self) -> T` where `T` is not a declared state. 10 | --> $DIR/empty_automata.rs:3:1 11 | | 12 | 3 | #[typestate] 13 | | ^^^^^^^^^^^^ 14 | | 15 | = note: this error originates in the attribute macro `typestate` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | -------------------------------------------------------------------------------- /examples/bulbs/dead_bulb.rs: -------------------------------------------------------------------------------- 1 | use light_bulb::*; 2 | use typestate::typestate; 3 | 4 | #[typestate] 5 | mod light_bulb { 6 | #[automaton] 7 | pub struct LightBulb; 8 | 9 | #[state] 10 | pub struct Off; 11 | pub trait Off { 12 | fn screw() -> Off; 13 | fn unscrew(self); 14 | fn turn_on(self) -> AfterOn; 15 | } 16 | 17 | #[state] 18 | pub struct On; 19 | pub trait On { 20 | fn turn_off(self) -> Off; 21 | } 22 | 23 | #[state] 24 | pub struct Dead; 25 | pub trait Dead { 26 | fn turn_off(self) -> Off; 27 | } 28 | 29 | pub enum AfterOn { 30 | #[metadata(label="'Bulb is On'")] 31 | On, 32 | #[metadata(label="'Bulb is Dead'")] 33 | Dead 34 | } 35 | } 36 | 37 | 38 | fn main() { 39 | 40 | } 41 | -------------------------------------------------------------------------------- /tests/fail/missing_initial_final_states.stderr: -------------------------------------------------------------------------------- 1 | error: Missing initial state. To declare an initial state you can use a function with signature like `fn f() -> T` where `T` is a declared state. 2 | --> $DIR/missing_initial_final_states.rs:3:1 3 | | 4 | 3 | #[typestate] 5 | | ^^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the attribute macro `typestate` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: Missing final state. To declare a final state you can use a function with signature like `fn f(self) -> T` where `T` is not a declared state. 10 | --> $DIR/missing_initial_final_states.rs:3:1 11 | | 12 | 3 | #[typestate] 13 | | ^^^^^^^^^^^^ 14 | | 15 | = note: this error originates in the attribute macro `typestate` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | -------------------------------------------------------------------------------- /examples/bulbs/smart_bulb.rs: -------------------------------------------------------------------------------- 1 | use typestate::typestate; 2 | 3 | #[typestate] 4 | mod smart_bulb { 5 | #[automaton] 6 | struct SmartBulb { 7 | cycles: u64, 8 | } 9 | 10 | #[state] 11 | struct Off; 12 | trait Off { 13 | fn screw() -> Off; 14 | fn unscrew(self); 15 | fn turn_on(self) -> On; // Off => On transition 16 | } 17 | 18 | #[state] 19 | struct On { 20 | color: [u8; 3] 21 | } 22 | trait On { 23 | fn turn_off(self) -> Off; 24 | fn get_color(&self); 25 | fn set_color(self, color: (u8, u8, u8)) -> Unknown; 26 | } 27 | 28 | enum Unknown { 29 | #[metadata(label = "bulb changed color successfully")] 30 | On, 31 | #[metadata(label = "bulb failed and turned off")] 32 | Off, 33 | } 34 | } 35 | 36 | fn main() {} 37 | -------------------------------------------------------------------------------- /tests/fail/nested_enums.rs: -------------------------------------------------------------------------------- 1 | use typestate_proc_macro::typestate; 2 | 3 | #[typestate(enumerate="ETrafficLight")] 4 | mod traffic_light { 5 | #[automaton] 6 | pub struct TrafficLight { 7 | pub cycles: u64, 8 | } 9 | #[state] 10 | pub struct Green; 11 | #[state] 12 | pub struct Yellow; 13 | #[state] 14 | pub struct Red; 15 | 16 | // #[transition] 17 | pub trait Green { 18 | fn to_yellow(self) -> Yellow; 19 | } 20 | pub trait Yellow { 21 | fn to_red(self) -> Red; 22 | } 23 | pub trait Red { 24 | fn to_green(self) -> Green; 25 | fn turn_on() -> Red; 26 | fn turn_off(self); 27 | fn to_either(self) -> Either; 28 | } 29 | pub enum A {} 30 | 31 | pub enum Either { 32 | Yellow, 33 | Red, 34 | A 35 | } 36 | } 37 | 38 | fn main() {} -------------------------------------------------------------------------------- /typestate-proc-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typestate-proc-macro" 3 | version = "0.8.0" 4 | edition = "2018" 5 | authors = ["José Duarte "] 6 | description = "A proc macro DSL for typestates" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["typestate"] 9 | categories = ["development-tools"] 10 | homepage = "https://github.com/rustype/typestate-rs" 11 | repository = "https://github.com/rustype/typestate-rs" 12 | autotests = false 13 | 14 | [features] 15 | default = ["std"] 16 | 17 | # to deprecate in the future 18 | std = [] 19 | 20 | # file export utilities 21 | export-dot = [] 22 | export-plantuml = [] 23 | export-mermaid = [] 24 | 25 | # documentation export utilities 26 | docs-mermaid = [] 27 | 28 | [lib] 29 | proc-macro = true 30 | 31 | [dependencies] 32 | quote = "1.0" 33 | syn = { version="1.0", features=["extra-traits", "visit-mut", "parsing", "full"] } 34 | proc-macro2 = "1.0" 35 | darling = "0.13" 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | pull_request: 8 | schedule: [cron: "0 0 1,15 * *"] 9 | 10 | jobs: 11 | test: 12 | name: Rust ${{matrix.rust}} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | rust: [beta, stable, nightly] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: dtolnay/rust-toolchain@master 21 | with: 22 | toolchain: ${{matrix.rust}} 23 | - run: cargo test --all 24 | env: 25 | RUSTFLAGS: ${{matrix.rustflags}} 26 | 27 | msrv: 28 | name: Rust 1.42.0 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: dtolnay/rust-toolchain@1.42.0 33 | - run: cargo check 34 | 35 | clippy: 36 | name: Clippy 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: dtolnay/rust-toolchain@clippy 41 | - run: cargo clippy --tests -- -Dclippy::all -------------------------------------------------------------------------------- /examples/bulbs/light_bulb.rs: -------------------------------------------------------------------------------- 1 | use light_bulb::*; 2 | use typestate::typestate; 3 | 4 | #[typestate] 5 | mod light_bulb { 6 | #[automaton] 7 | pub struct LightBulb; 8 | 9 | #[state] 10 | pub struct Off; 11 | pub trait Off { 12 | fn screw() -> Off; 13 | fn unscrew(self); 14 | fn turn_on(self) -> On; // Off => On transition 15 | } 16 | 17 | #[state] 18 | pub struct On; 19 | pub trait On { 20 | fn turn_off(self) -> Off; 21 | } 22 | } 23 | 24 | impl OffState for LightBulb { 25 | fn screw() -> LightBulb { 26 | Self { state: Off } 27 | } 28 | fn unscrew(self) {} 29 | fn turn_on(self) -> LightBulb { 30 | LightBulb:: { state: On } 31 | } 32 | } 33 | 34 | impl OnState for LightBulb { 35 | fn turn_off(self) -> LightBulb { 36 | LightBulb:: { state: Off } 37 | } 38 | } 39 | 40 | fn main() { 41 | let bulb = LightBulb::::screw(); 42 | let bulb = bulb.turn_on(); 43 | let bulb = bulb.turn_off(); 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /typestate-proc-macro/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /typestate-book/src/chapter_1.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Typestates allow you to define *safe* usage protocols for your objects. 4 | The compiler will help you on your journey and disallow errors on given states. 5 | You will no longer be able to try and read from closed streams. 6 | 7 | `#[typestate]` builds on ideas from the [`state_machine_future`](https://github.com/fitzgen/state_machine_future) crate. 8 | 9 | ## First steps 10 | 11 | Before you start your typestate development journey you need to declare your dependencies, 12 | you can start using the `typestate` crate by adding the following line to your `Cargo.toml` file. 13 | 14 | ```toml 15 | typestate = "0.8.0" 16 | ``` 17 | 18 | ## Citing `typestate` 19 | 20 | If you find `typestate` useful in your work, we kindly request you cite the following paper: 21 | 22 | ```bibtex 23 | @inproceedings{10.1145/3475061.3475082, 24 | author = {Duarte, Jos\'{e} and Ravara, Ant\'{o}nio}, 25 | title = {Retrofitting Typestates into Rust}, 26 | year = {2021}, 27 | url = {https://doi.org/10.1145/3475061.3475082}, 28 | doi = {10.1145/3475061.3475082}, 29 | booktitle = {25th Brazilian Symposium on Programming Languages}, 30 | pages = {83–91}, 31 | numpages = {9}, 32 | series = {SBLP'21} 33 | } 34 | ``` -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typestate" 3 | version = "0.8.0" 4 | edition = "2018" 5 | authors = ["José Duarte "] 6 | description = "A proc macro DSL for typestates" 7 | readme = "README.md" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["typestate"] 10 | categories = ["development-tools"] 11 | homepage = "https://github.com/rustype/typestate-rs" 12 | repository = "https://github.com/rustype/typestate-rs" 13 | autotests = false 14 | 15 | [features] 16 | default = ["std", "docs-mermaid"] 17 | 18 | # should probably deprecate this feature 19 | # until someone needs it, it only adds complexity 20 | std = ["typestate-proc-macro/std"] 21 | 22 | # features to export diagrams 23 | export-dot = ["typestate-proc-macro/export-dot"] 24 | export-plantuml = ["typestate-proc-macro/export-plantuml"] 25 | export-mermaid = ["typestate-proc-macro/export-mermaid"] 26 | 27 | # export mermaid state machines inside the docs 28 | docs-mermaid = ["typestate-proc-macro/docs-mermaid", "aquamarine"] 29 | 30 | [[test]] 31 | name = "tests" 32 | path = "tests/macro_tests.rs" 33 | 34 | [dev-dependencies] 35 | trybuild = { version = "1.0", features = ["diff"] } 36 | rustversion = "1.0" 37 | 38 | [dependencies.typestate-proc-macro] 39 | version = "0.8.0" 40 | path = "typestate-proc-macro" 41 | package = "typestate-proc-macro" 42 | 43 | [dependencies] 44 | aquamarine = { version = "^0.1.9", optional = true } 45 | 46 | [workspace] 47 | members = ["typestate-proc-macro"] 48 | -------------------------------------------------------------------------------- /typestate-book/src/features/macro_attributes.md: -------------------------------------------------------------------------------- 1 | # Macro Attributes 2 | 3 | The `#[typestate]` macro exposes some extra features through attribute parameters. 4 | This chapter introduces them and provides simple examples. 5 | 6 | ## `enumerate` 7 | 8 | The `enumerate` parameter will generate an additional `enum` containing all states; 9 | this is useful when dealing with anything that requires a more "general" concept of state. 10 | 11 | Consider the file [`examples/light_bulb.rs`](../examples/light_bulb.rs): 12 | 13 | ```rust 14 | #[typestate(enumerate = "LightBulbStates")] 15 | mod light_bulb { 16 | #[state] struct On; 17 | #[state] struct Off; 18 | // ... 19 | } 20 | ``` 21 | 22 | Using the `enumerate` attribute will add the following `enum` to the expansion: 23 | 24 | ```rust 25 | pub enum LightBulbStates { 26 | Off(LightBulb), 27 | On(LightBulb), 28 | } 29 | ``` 30 | 31 | ## `state_constructors` 32 | 33 | The `state_constructors` parameter will generate additional constructors 34 | for each state *with fields*; this is useful when declaring states inside the automaton. 35 | 36 | Consider the following example state: 37 | 38 | ```rust 39 | #[typestate(state_constructors = "new_state")] 40 | mod light_bulb { 41 | #[state] struct On { 42 | color: [u8; 3] 43 | } 44 | // ... 45 | } 46 | ``` 47 | 48 | When compiled, the following constructor is generated: 49 | 50 | ```rust 51 | impl On { 52 | pub fn new_state(color: [u8; 3]) -> Self { 53 | Self { color } 54 | } 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /typestate-book/src/chapter_4.md: -------------------------------------------------------------------------------- 1 | # Advanced Guide 2 | 3 | There are some features which may be helpful when describing a typestate. 4 | There are two main features that weren't discussed yet. 5 | 6 | ## Self-transitioning functions 7 | Putting it simply, states may require to mutate themselves without transitioning, or maybe we require a simple getter. 8 | To declare methods for that purpose, we can use functions that take references (mutable or not) to `self`. 9 | 10 | Consider the following example where we have a flag that can be up or not. 11 | We have two functions, one checks if the flag is up, the other, sets the flag up. 12 | 13 | ```rust,noplaypen 14 | #[state] struct Flag { 15 | up: bool 16 | } 17 | 18 | impl Flag { 19 | fn is_up(&self) -> bool; 20 | fn set_up(&mut self); 21 | } 22 | ``` 23 | 24 | As these functions do not change the typestate state, 25 | they transition back to the current state. 26 | 27 | ## Non-deterministic transitions 28 | Consider that a typestate relies on an external component that can fail, to model that, one would use `Result`. 29 | However, we need our typestate to transition between known states, so we declare two things: 30 | - An `Error` state along with the other states. 31 | - An `enum` to represent the bifurcation of states. 32 | 33 | ```rust,noplaypen 34 | #[state] struct Error { 35 | message: String 36 | } 37 | 38 | enum OperationResult { 39 | State, Error 40 | } 41 | ``` 42 | 43 | Inside the enumeration there can only be other valid states and only `Unit` style variants are supported. 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `#[typestate]` 2 | 3 | [github](https://github.com/rustype/typestate-rs) 4 | [](https://docs.rs/typestate) 5 | [](https://crates.io/crates/typestate) 6 | 7 | This library provides developers with a macro to design typestated objects. 8 | 9 | ```toml 10 | [dependencies] 11 | typestate = "0.8.0" 12 | ``` 13 | 14 | *Compiler support: requires rustc 1.42+* 15 | 16 | ## Documentation 17 | 18 | If you're only interested in getting up and running with `typestate`, 19 | the documentation might be more useful for you. 20 | You can consult it in 21 | 22 | If you're interested in learning more about the `typestate` crate, or typestates in Rust, 23 | you can read *The Typestate Book* in . 24 | 25 | ## Citing `typestate` 26 | 27 | If you find `typestate` useful in your work, we kindly request you cite the following paper: 28 | 29 | ```bibtex 30 | @inproceedings{10.1145/3475061.3475082, 31 | author = {Duarte, Jos\'{e} and Ravara, Ant\'{o}nio}, 32 | title = {Retrofitting Typestates into Rust}, 33 | year = {2021}, 34 | url = {https://doi.org/10.1145/3475061.3475082}, 35 | doi = {10.1145/3475061.3475082}, 36 | booktitle = {25th Brazilian Symposium on Programming Languages}, 37 | pages = {83–91}, 38 | numpages = {9}, 39 | series = {SBLP'21} 40 | } 41 | ``` 42 | 43 | Alternatively, you can cite the extended version: 44 | 45 | ```bibtex 46 | @article{10.1016/j.cola.2022.101154, 47 | title = {Taming stateful computations in Rust with typestates}, 48 | journal = {Journal of Computer Languages}, 49 | pages = {101154}, 50 | year = {2022}, 51 | issn = {2590-1184}, 52 | doi = {10.1016/j.cola.2022.101154}, 53 | url = {https://doi.org/10.1016/j.cola.2022.101154}, 54 | author = {Duarte, Jos\'{e} and Ravara, Ant\'{o}nio}, 55 | ``` 56 | 57 | ## Publications 58 | 59 | - [Retrofitting Typestates into Rust (MSc Thesis)](articles/thesis.pdf) 60 | - [Retrofitting Typestates into Rust (SBLP'21)](articles/sblp21.pdf) 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions under (but not limited to) the following categories: 4 | 5 | - Code 6 | - `typestate` macro codebase 7 | - The actions pipeline 8 | - Other code present in the repository 9 | (e.g. `justfile` or in the future, scripts to "compile" diagrams) 10 | - Documentation 11 | - Examples 12 | - Documentation for the macro 13 | - *The Typestate Book* 14 | - Reports 15 | - Bugs 16 | - Missing functionality (e.g. 17 | [#5](https://github.com/rustype/typestate-rs/issues/5) & 18 | [#8](https://github.com/rustype/typestate-rs/issues/8)) 19 | - Suggestions (e.g. 20 | [#2](https://github.com/rustype/typestate-rs/issues/2)) 21 | - Relevant discussions (e.g. 22 | [#3](https://github.com/rustype/typestate-rs/issues/3)) 23 | - Questions 24 | 25 | > A single contribution may fall under one or more of the previous categories. 26 | 27 | ## Git Message Style 28 | 29 | The repository git message style is based on Karma's style (read more in ), 30 | to reduce confusion, we use the following subset: 31 | 32 | - `feat` for a new feature. 33 | - `fix` for a bug fix or other minor code modifications. 34 | This includes the following labels from Karma's style: 35 | - `perf` 36 | - `style` 37 | - `refactor` 38 | - `test` 39 | - `build` 40 | - `chore` 41 | - `docs` for documentation modifications. 42 | - `book` for *The Typestate Book* modifications. (Note: these may be included under `docs`) 43 | 44 | ### Addressing Issues 45 | 46 | Commits addressing specific issues *must* contain the addressed issue. 47 | 48 | As an example, consider you were fixing issue `#10`, the message should look as follows: 49 | 50 | ``` 51 | feat(#10): message goes here 52 | ``` 53 | 54 | > Addressing multiple issues in a single commit is discouraged, 55 | > in such cases, a bigger issue which tracks the progress on the smaller tasks should be created. 56 | 57 | ### Breaking Changes 58 | 59 | Commits implementing breaking changes should contain a `!` in the end of the commit label (e.g. `feat!(#10):...`). 60 | 61 | ## Final Note 62 | 63 | The objective of this document is to *reduce friction*, 64 | as such the contents in this document constitute a *guide* rather than a set of rules. 65 | -------------------------------------------------------------------------------- /typestate-book/src/chapter_2.md: -------------------------------------------------------------------------------- 1 | # What are typestates? 2 | 3 | In a nutshell, typestates are finite state machines described at the type-level. 4 | They aim to tame stateful computations by allowing the compiler to reason about the state of the program. 5 | 6 | Consider the following Java example: 7 | 8 | ```java 9 | public class ScannerFail { 10 | void main(String... args) { 11 | Scanner s = new Scanner(System.in); 12 | s.close(); 13 | s.nextLine(); 14 | } 15 | } 16 | ``` 17 | 18 | The example will compile and run, however it will crash during runtime, throwing an `IllegalStateException`, 19 | this happens because we tried to read a line after closing the `Scanner`. 20 | 21 | If you thought: "*The compiler should have told me!*" - then, typestates are for you! 22 | 23 | In a typestated language, `Scanner` would have its state be a first-class citizen of the code. 24 | Consider the following example in *typestated-Java*: 25 | 26 | ```java 27 | public class ScannerFail { 28 | void main(String... args) { 29 | Scanner[Open] s = new Scanner(System.in); 30 | // s now has type Scanner[Closed] 31 | s = s.close(); 32 | // compilation error: Scanner[Closed] does not have a nextLine method 33 | s.nextLine(); 34 | } 35 | } 36 | ``` 37 | 38 | As made evident by the comments, the example would not compile because the `Scanner` 39 | transitions to the `Closed` state after the `.close()` call. 40 | 41 | ## Typestates in Rust 42 | 43 | Typestates are not a new concept to Rust. 44 | There are several blog posts on the subject 45 | [[1](https://yoric.github.io/post/rust-typestate/), 46 | [2](http://cliffle.com/blog/rust-typestate/), 47 | [3](https://rustype.github.io/notes/notes/rust-typestate-series/rust-typestate-index)] 48 | as well as a [chapter](https://docs.rust-embedded.org/book/static-guarantees/typestate-programming.html) in *The Embedded Rust Book*. 49 | 50 | In short, we can write typestates by hand, we add some generics here and there, 51 | declare them as a "*state*" and in the end we can keep living our lives with our new state machine. 52 | 53 | This approach however is *error-prone* and *verbose* (especially with bigger automata). 54 | It also provides *no* guarantees about the automata, unless of course, you designed and tested the design previously. 55 | 56 | As programmers, we want to automate this cumbersome job and to do so, we use Rust's powerful procedural macros! -------------------------------------------------------------------------------- /assets/DotLightBulb.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | Automata 11 | 12 | 13 | 14 | _initial_0 15 | 16 | 17 | 18 | Off 19 | 20 | Off 21 | 22 | 23 | 24 | _initial_0->Off 25 | 26 | 27 | screw 28 | 29 | 30 | 31 | Off->Off 32 | 33 | 34 | unscrew 35 | 36 | 37 | 38 | On 39 | 40 | On 41 | 42 | 43 | 44 | Off->On 45 | 46 | 47 | turn_on 48 | 49 | 50 | 51 | On->Off 52 | 53 | 54 | turn_off 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /typestate-book/src/static/DotLightBulb.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | Automata 11 | 12 | 13 | 14 | _initial_0 15 | 16 | 17 | 18 | Off 19 | 20 | Off 21 | 22 | 23 | 24 | _initial_0->Off 25 | 26 | 27 | screw 28 | 29 | 30 | 31 | Off->Off 32 | 33 | 34 | unscrew 35 | 36 | 37 | 38 | On 39 | 40 | On 41 | 42 | 43 | 44 | Off->On 45 | 46 | 47 | turn_on 48 | 49 | 50 | 51 | On->Off 52 | 53 | 54 | turn_off 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /typestate-book/src/features/compilation_flags.md: -------------------------------------------------------------------------------- 1 | # Compilation Flags 2 | 3 | The `typestate` macro provides several `cargo` features, 4 | mostly focused on the visualization of your typestate's automata. 5 | 6 | ## Mermaid Diagrams 7 | 8 | `docs-mermaid` will embed [Mermaid.js](https://mermaid-js.github.io/mermaid/#/) diagrams in your documentation. 9 | 10 | This feature is activated by default, regarless, see below how you can explicitly activate it. 11 | 12 | In the terminal, for each run: 13 | ```bash 14 | cargo doc --features docs-mermaid 15 | ``` 16 | 17 | Or by declaring it in `Cargo.toml`: 18 | ```toml 19 | typestate = { version = "0.8.0", features = [ "docs-mermaid" ] } 20 | ``` 21 | 22 | ## DOT Diagrams 23 | 24 | `export-dot` - will generate a `.dot` file, describing your typestate's state machine. 25 | You can customize certain `.dot` parameters through the following environment variables: 26 | 27 | - `DOT_PAD` - specifies how much, in inches, to extend the drawing area around the minimal area needed to draw the graph. 28 | - `DOT_NODESEP` - `nodesep` specifies the minimum space between two adjacent nodes in the same rank, in inches. 29 | - `DOT_RANKSEP` - sets the desired rank separation, in inches. 30 | - `EXPORT_FOLDER` - declare the target folder for exported files. 31 | 32 | This feature is not activated by default, see below how you can activate it. 33 | 34 | In the terminal, for each run: 35 | ```bash 36 | cargo doc --features export-dot 37 | ``` 38 | 39 | Or by declaring it in `Cargo.toml`: 40 | ```toml 41 | typestate = { version = "0.8.0", features = [ "export-dot" ] } 42 | ``` 43 | 44 | > For more information on DOT configuration, I recommend you read through [DOT documentation](https://graphviz.org/doc/info/attrs.html). 45 | 46 | ### Examples 47 | 48 | These examples are present in the `examples/` folder in the repository's root. 49 | 50 | | `LightBulb` | `SmartBulb` | 51 | | ---------------------------------------------------- | ---------------------------------------------------- | 52 | | ![`examples/light_bulb.rs`](../static/DotLightBulb.svg) | ![`examples/smart_bulb.rs`](../static/DotSmartBulb.svg) | 53 | 54 | ## PlantUML Diagrams 55 | 56 | `export-plantuml` will generate a PlantUML state diagram (`.uml` file) of your state machine. 57 | Like the previous feature, you can also customize this one through the following environment variables: 58 | 59 | - `PLANTUML_NODESEP` - `nodesep` specifies the minimum space between two adjacent nodes in the same rank. 60 | - `PLANTUML_RANKSEP` - Sets the desired rank separation. 61 | - `EXPORT_FOLDER` - Declare the target folder for exported files. 62 | 63 | This feature is not activated by default, see below how you can activate it. 64 | 65 | In the terminal, for each run: 66 | ```bash 67 | cargo doc --features export-plantuml 68 | ``` 69 | 70 | Or by declaring it in `Cargo.toml`: 71 | ```toml 72 | typestate = { version = "0.8.0", features = [ "export-plantuml" ] } 73 | ``` 74 | 75 | > For more information on PlantUML configuration, I recommend you read through [PlantUML Hitchhiker's Guide](https://crashedmind.github.io/PlantUMLHitchhikersGuide/layout/layout.html#nodesep-and-ranksep). 76 | 77 | ### Examples 78 | 79 | These examples are present in the `examples/` folder in the repository's root. 80 | 81 | | `LightBulb` | `SmartBulb` | 82 | | ---------------------------------------------------- | ---------------------------------------------------- | 83 | | ![`examples/light_bulb.rs`](../static/UmlLightBulb.svg) | ![`examples/smart_bulb.rs`](../static/UmlSmartBulb.svg) | -------------------------------------------------------------------------------- /assets/UmlLightBulb.svg: -------------------------------------------------------------------------------- 1 | OffOnscrewunscrewturn_onturn_off -------------------------------------------------------------------------------- /typestate-book/src/static/UmlLightBulb.svg: -------------------------------------------------------------------------------- 1 | OffOnscrewunscrewturn_onturn_off -------------------------------------------------------------------------------- /typestate-book/src/chapter_3.md: -------------------------------------------------------------------------------- 1 | # Basic Guide to Typestates 2 | 3 | Consider we are tasked with building the firmware for a traffic light, 4 | we can turn it on and off and cycle between Green, Yellow and Red. 5 | 6 | We first declare a module with the `#[typestate]` macro attached to it. 7 | ```rust,noplaypen 8 | #[typestate] 9 | mod traffic_light {} 10 | ``` 11 | 12 | This of course does nothing, in fact it will provide you an error, 13 | saying that we haven't declared an *automaton*. 14 | 15 | And so, our next task is to do that. 16 | Inside our `traffic_light` module we declare a structure annotated with `#[automaton]`. 17 | ```rust,noplaypen 18 | #[automaton] 19 | pub struct TrafficLight; 20 | ``` 21 | 22 | Our next step is to declare the states. 23 | We declare three empty structures annotated with `"[state]`. 24 | ```rust,noplaypen 25 | #[state] pub struct Green; 26 | #[state] pub struct Yellow; 27 | #[state] pub struct Red; 28 | ``` 29 | 30 | So far so good, however some errors should appear, regarding the lack of initial and final states. 31 | 32 | To declare initial and final states we need to see them as describable by transitions. 33 | Whenever an object is created, the method that created leaves the object in the *initial* state. 34 | Equally, whenever a method consumes an object and does not return it (or a similar version of it), 35 | it made the object reach the *final* state. 36 | 37 | With this in mind we can lay down the following rules: 38 | - Functions that *do not* take a valid state (i.e. `self`) and return a valid state, describe an initial state. 39 | - Functions that take a valid state (i.e. `self`) and *do not* return a valid state, describe a final state. 40 | 41 | So we write the following function signatures: 42 | ```rust,noplaypen 43 | fn turn_on() -> Red; 44 | fn turn_off(self); 45 | ``` 46 | 47 | However, these are *free* functions, meaning that `self` relates to nothing. 48 | To attach them to a state we wrap them around a `trait` with the name of the state they are supposed to be attached to. 49 | So our previous example becomes: 50 | ```rust,noplaypen 51 | trait Red { 52 | fn turn_on() -> Red; 53 | fn turn_off(self); 54 | } 55 | ``` 56 | 57 | *Before we go further, a quick review:* 58 | > - The module is annotated with `#[typestate]` enabling the DSL. 59 | > - To declare the main automaton we attach `#[automaton]` to a structure. 60 | > - The states are declared by attaching `#[state]`. 61 | > - State functions are declared through traits that share the same name. 62 | > - Initial and final states are declared by functions with a "special" signature. 63 | 64 | Finally, we need to address how states transition between each other. 65 | An astute reader might have inferred that we can consume one state and return another, 66 | such reader would be 100% correct. 67 | 68 | For example, to transition between the `Red` state and the `Green` we do: 69 | ```rust,noplaypen 70 | trait Red { 71 | fn to_green(self) -> Green; 72 | } 73 | ``` 74 | 75 | Building on this we can finish the other states: 76 | ```rust,noplaypen 77 | pub trait Green { 78 | fn to_yellow(self) -> Yellow; 79 | } 80 | 81 | pub trait Yellow { 82 | fn to_red(self) -> Red; 83 | } 84 | 85 | pub trait Red { 86 | fn to_green(self) -> Green; 87 | fn turn_on() -> Red; 88 | fn turn_off(self); 89 | } 90 | ``` 91 | 92 | And the full code becomes: 93 | 94 | ```rust,noplaypen 95 | #[typestate] 96 | mod traffic_light { 97 | #[automaton] 98 | pub struct TrafficLight { 99 | pub cycles: u64, 100 | } 101 | 102 | #[state] pub struct Green; 103 | #[state] pub struct Yellow; 104 | #[state] pub struct Red; 105 | 106 | pub trait Green { 107 | fn to_yellow(self) -> Yellow; 108 | } 109 | 110 | pub trait Yellow { 111 | fn to_red(self) -> Red; 112 | } 113 | 114 | pub trait Red { 115 | fn to_green(self) -> Green; 116 | fn turn_on() -> Red; 117 | fn turn_off(self); 118 | } 119 | } 120 | ``` 121 | 122 | The code above will generate: 123 | - Expand the main structure with a `state: State` field. 124 | - A sealed trait which disallows states from being added *externally*. 125 | - Traits for each state, providing the described functions. -------------------------------------------------------------------------------- /assets/DotSmartBulb.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | Automata 11 | 12 | 13 | 14 | _initial_0 15 | 16 | 17 | 18 | Off 19 | 20 | Off 21 | 22 | 23 | 24 | _initial_0->Off 25 | 26 | 27 | screw 28 | 29 | 30 | 31 | Off->Off 32 | 33 | 34 | unscrew 35 | 36 | 37 | 38 | On 39 | 40 | On 41 | 42 | 43 | 44 | Off->On 45 | 46 | 47 | turn_on 48 | 49 | 50 | 51 | On->Off 52 | 53 | 54 | turn_off 55 | 56 | 57 | 58 | On->Off 59 | 60 | 61 | set_color 62 | 63 | 64 | 65 | On->On 66 | 67 | 68 | get_color 69 | 70 | 71 | 72 | On->On 73 | 74 | 75 | set_color 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /typestate-book/src/static/DotSmartBulb.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | Automata 11 | 12 | 13 | 14 | _initial_0 15 | 16 | 17 | 18 | Off 19 | 20 | Off 21 | 22 | 23 | 24 | _initial_0->Off 25 | 26 | 27 | screw 28 | 29 | 30 | 31 | Off->Off 32 | 33 | 34 | unscrew 35 | 36 | 37 | 38 | On 39 | 40 | On 41 | 42 | 43 | 44 | Off->On 45 | 46 | 47 | turn_on 48 | 49 | 50 | 51 | On->Off 52 | 53 | 54 | turn_off 55 | 56 | 57 | 58 | On->Off 59 | 60 | 61 | set_color 62 | 63 | 64 | 65 | On->On 66 | 67 | 68 | get_color 69 | 70 | 71 | 72 | On->On 73 | 74 | 75 | set_color 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/traffic_light.rs: -------------------------------------------------------------------------------- 1 | // use maintenance::*; 2 | use traffic_light::*; 3 | use typestate::typestate; 4 | 5 | const N_CYCLES_MAINTENANCE: u64 = 1000; 6 | 7 | fn main() { 8 | let red_light = TrafficLight::::turn_on(); 9 | let mut green_light = red_light.to_green(); 10 | if green_light.requires_maintenance() { 11 | green_light.reset_cycles() 12 | } 13 | let yellow_light = green_light.to_yellow(); 14 | let red_light = yellow_light.to_red(); 15 | red_light.turn_off(); 16 | } 17 | 18 | // #[typestate] 19 | // mod test { 20 | // #[automaton] 21 | // pub struct Automata {} 22 | // } 23 | 24 | // #[typestate] 25 | // mod maintenance { 26 | // use super::traffic_light::*; 27 | // #[automaton] 28 | // pub struct Maintenance { 29 | // pub tl: TrafficLight, 30 | // } 31 | 32 | // #[state] 33 | // pub struct In; 34 | // #[state] 35 | // pub struct Out; 36 | 37 | // pub trait In { 38 | // fn new() -> In; 39 | // fn perform(self) -> Out; 40 | // } 41 | 42 | // pub trait Out { 43 | // fn test(self) -> Out; 44 | // fn end(self); 45 | // } 46 | // } 47 | 48 | // impl InState for Maintenance { 49 | // fn new() -> Maintenance { 50 | // Maintenance:: { 51 | // tl: TrafficLight::::turn_on(), 52 | // state: In, 53 | // } 54 | // } 55 | // fn perform(self) -> Maintenance { 56 | // Maintenance:: { 57 | // tl: self.tl, 58 | // state: Out, 59 | // } 60 | // } 61 | // } 62 | 63 | // impl OutState for Maintenance { 64 | // fn test(mut self) -> Maintenance { 65 | // let green = self.tl.to_green(); 66 | // let yellow = green.to_yellow(); 67 | // let red = yellow.to_red(); 68 | // self.tl = red; 69 | // self 70 | // } 71 | // fn end(self) {} 72 | // } 73 | 74 | #[typestate(enumerate = "ETrafficLight", state_constructors = "new_state")] 75 | mod traffic_light { 76 | #[derive(Debug)] 77 | #[automaton] 78 | pub struct TrafficLight { 79 | pub cycles: u64, 80 | } 81 | #[state] 82 | pub struct Green; 83 | #[state] 84 | pub struct Yellow; 85 | #[state] 86 | pub struct Red; 87 | 88 | pub trait Green { 89 | fn to_yellow(self) -> Yellow; 90 | } 91 | pub trait Yellow { 92 | fn to_red(self) -> Red; 93 | } 94 | pub trait Red { 95 | fn to_green(self) -> Green; 96 | fn turn_on() -> Red; 97 | fn turn_off(self); 98 | fn to_either(self) -> Either; 99 | } 100 | 101 | pub enum Either { 102 | #[metadata(label = "test")] 103 | Yellow, 104 | Red, 105 | } 106 | } 107 | 108 | impl GreenState for TrafficLight { 109 | fn to_yellow(self) -> TrafficLight { 110 | println!("Green -> Yellow"); 111 | TrafficLight:: { 112 | cycles: self.cycles, 113 | state: Yellow, 114 | } 115 | } 116 | } 117 | 118 | impl YellowState for TrafficLight { 119 | fn to_red(self) -> TrafficLight { 120 | println!("Yellow -> Red"); 121 | TrafficLight:: { 122 | // increment the cycle 123 | cycles: self.cycles + 1, 124 | state: Red, 125 | } 126 | } 127 | } 128 | 129 | impl RedState for TrafficLight { 130 | fn to_either(self) -> Either { 131 | Either::Yellow(TrafficLight:: { 132 | cycles: self.cycles, 133 | state: Yellow, 134 | }) 135 | } 136 | 137 | fn to_green(self) -> TrafficLight { 138 | println!("Red -> Green"); 139 | TrafficLight:: { 140 | cycles: self.cycles, 141 | state: Green, 142 | } 143 | } 144 | 145 | fn turn_on() -> TrafficLight { 146 | println!("Turning on..."); 147 | TrafficLight:: { 148 | cycles: 0, 149 | state: Red, 150 | } 151 | } 152 | 153 | fn turn_off(self) { 154 | println!("Turning off..."); 155 | // ... consume 156 | } 157 | } 158 | 159 | impl TrafficLight 160 | where 161 | State: TrafficLightState, 162 | { 163 | fn requires_maintenance(&self) -> bool { 164 | self.cycles > N_CYCLES_MAINTENANCE 165 | } 166 | 167 | fn reset_cycles(&mut self) { 168 | self.cycles = 0; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /typestate-proc-macro/src/igraph/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod export; 2 | pub mod validate; 3 | 4 | use darling::FromMeta; 5 | use std::{ 6 | collections::{HashMap, HashSet}, 7 | fmt::{Debug, Display}, 8 | hash::Hash, 9 | }; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct StateNode { 13 | state: Option, 14 | metadata: Metadata, 15 | } 16 | 17 | impl StateNode { 18 | pub fn new(state: Option) -> Self { 19 | Self { 20 | state, 21 | metadata: Metadata::empty(), 22 | } 23 | } 24 | 25 | pub fn update_metadata(&mut self, metadata: Metadata) { 26 | self.metadata = metadata; 27 | } 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub enum Node 32 | where 33 | S: Hash + Eq + Debug + Clone + Display, 34 | { 35 | State(StateNode), 36 | Decision(Vec>), // NOTE: instead of Vec<_>, HashSet<_> would probably be better 37 | } 38 | 39 | impl From for Node 40 | where 41 | S: Hash + Eq + Debug + Clone + Display, 42 | { 43 | fn from(s: S) -> Self { 44 | Node::State(StateNode::new(Some(s))) 45 | } 46 | } 47 | 48 | impl From> for Node 49 | where 50 | S: Hash + Eq + Debug + Clone + Display, 51 | { 52 | fn from(s: Option) -> Self { 53 | Node::State(StateNode::new(s)) 54 | } 55 | } 56 | 57 | impl From> for Node 58 | where 59 | S: Hash + Eq + Debug + Clone + Display, 60 | { 61 | fn from(s: Vec) -> Self { 62 | Node::Decision(s.into_iter().map(|s| StateNode::new(Some(s))).collect()) 63 | } 64 | } 65 | 66 | impl From>> for Node 67 | where 68 | S: Hash + Eq + Debug + Clone + Display, 69 | { 70 | fn from(s: Vec>) -> Self { 71 | Node::Decision(s) 72 | } 73 | } 74 | 75 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 76 | pub struct Transition 77 | where 78 | T: Hash + Eq + Debug + Clone + Display, 79 | { 80 | transition: T, 81 | } 82 | 83 | impl Transition 84 | where 85 | T: Hash + Eq + Debug + Clone + Display, 86 | { 87 | pub fn new(transition: T) -> Self { 88 | Self { transition } 89 | } 90 | } 91 | 92 | impl Display for Transition 93 | where 94 | T: Hash + Eq + Debug + Clone + Display, 95 | { 96 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 97 | f.write_fmt(format_args!("{}", self.transition)) 98 | } 99 | } 100 | 101 | impl From for Transition 102 | where 103 | T: Hash + Eq + Debug + Clone + Display, 104 | { 105 | fn from(t: T) -> Self { 106 | Self::new(t) 107 | } 108 | } 109 | 110 | /// Metadata associated with nodes and transitions, 111 | /// to be used as additional annotations. 112 | #[derive(Debug, Clone, Hash, PartialEq, Eq, FromMeta)] 113 | pub struct Metadata { 114 | #[darling(rename = "label")] 115 | transition_label: Option, 116 | } 117 | 118 | impl Metadata { 119 | fn empty() -> Self { 120 | Self { 121 | transition_label: None, 122 | } 123 | } 124 | } 125 | 126 | impl Default for Metadata { 127 | fn default() -> Self { 128 | Self::empty() 129 | } 130 | } 131 | 132 | #[derive(Debug, Clone)] 133 | pub struct IntermediateGraph 134 | where 135 | // State type parameter. 136 | S: Hash + Eq + Debug + Clone + Display, 137 | // Transition type parameter. 138 | T: Hash + Eq + Debug + Clone + Display, 139 | { 140 | states: HashSet, 141 | choices: HashSet, 142 | delta: HashMap, HashMap, Node>>, 143 | } 144 | 145 | impl IntermediateGraph 146 | where 147 | S: Hash + Eq + Debug + Clone + Display, 148 | T: Hash + Eq + Debug + Clone + Display, 149 | { 150 | pub fn new() -> Self { 151 | Self { 152 | states: HashSet::new(), 153 | choices: HashSet::new(), 154 | delta: HashMap::new(), 155 | } 156 | } 157 | 158 | pub fn add_state(&mut self, state: S) -> bool { 159 | self.states.insert(state) 160 | } 161 | 162 | pub fn add_choice(&mut self, choice: S) -> bool { 163 | self.choices.insert(choice) 164 | } 165 | 166 | pub fn add_transition( 167 | &mut self, 168 | source: Option, 169 | transition: Transition, 170 | destinations: Node, 171 | ) { 172 | if let Some(source_value) = self.delta.get_mut(&source) { 173 | // NOTE: multi-valued transitions are disallowed because Rust does not support overloading, 174 | // thus, one cannot write function `f` for the same `Self` type with different signatures. 175 | source_value.insert(transition, destinations); 176 | } else { 177 | let mut transitions = HashMap::new(); 178 | transitions.insert(transition, destinations); 179 | self.delta.insert(source, transitions); 180 | } 181 | } 182 | } 183 | 184 | impl Default for IntermediateGraph 185 | where 186 | S: Hash + Eq + Debug + Clone + Display, 187 | T: Hash + Eq + Debug + Clone + Display, 188 | { 189 | fn default() -> Self { 190 | Self::new() 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /typestate-proc-macro/src/visitors/decision.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use darling::FromMeta; 4 | use syn::{visit_mut::VisitMut, Error, Fields, Generics, Ident, ItemEnum, ItemMod, Variant}; 5 | 6 | use crate::{ 7 | igraph::{Metadata, StateNode}, 8 | StateMachineInfo, TypestateError, 9 | }; 10 | 11 | macro_rules! bail_if_any { 12 | ( $errors:expr ) => { 13 | match $errors { 14 | errors => { 15 | if !errors.is_empty() { 16 | return errors; 17 | } 18 | } 19 | } 20 | }; 21 | } 22 | 23 | pub(crate) fn visit_non_deterministic( 24 | module: &mut ItemMod, 25 | state_machine_info: &mut StateMachineInfo, 26 | ) -> Vec { 27 | let mut decision_visitor = DecisionVisitor::new(state_machine_info); 28 | decision_visitor.visit_item_mod_mut(module); 29 | // report non_det_state_visitor errors and return 30 | bail_if_any!(decision_visitor.errors); 31 | vec![] 32 | } 33 | 34 | struct DecisionVisitor<'sm> { 35 | state_machine_info: &'sm mut StateMachineInfo, 36 | decision_generics: HashSet, 37 | errors: Vec, 38 | } 39 | 40 | impl<'sm> DecisionVisitor<'sm> { 41 | fn new(state_machine_info: &'sm mut StateMachineInfo) -> Self { 42 | Self { 43 | state_machine_info, 44 | decision_generics: HashSet::new(), 45 | errors: vec![], 46 | } 47 | } 48 | 49 | fn visit_variant_mut(&mut self, variant: &mut Variant) -> Option> { 50 | if let Fields::Unit = &variant.fields { 51 | let det_states = &self.state_machine_info.det_states; 52 | let ident = &variant.ident; 53 | // check if the current ident is a valid state or another decision node 54 | if self 55 | .state_machine_info 56 | .non_det_transitions 57 | .contains_key(ident) 58 | { 59 | self.push_unsupported_state_error(ident); 60 | } else if let Some(it_struct) = det_states.get(ident) { 61 | let mut state = StateNode::new(Some(ident.clone())); 62 | let mut errors = vec![]; 63 | variant.attrs.retain(|attr| { 64 | if attr.path.is_ident("metadata") { 65 | match attr.parse_meta() { 66 | Ok(meta) => match Metadata::from_meta(&meta) { 67 | Ok(metadata) => state.update_metadata(metadata), 68 | Err(err) => { 69 | // TODO fix this hack 70 | errors.push(Error::new_spanned(attr, err.to_string())) 71 | } 72 | }, 73 | Err(err) => errors.push(err), 74 | } 75 | false 76 | } else { 77 | true 78 | } 79 | }); 80 | self.errors.append(&mut errors); 81 | 82 | let automata_ident = self.state_machine_info.get_automaton_ident(); 83 | let generics = &it_struct.generics; 84 | self.decision_generics.insert(generics.clone()); 85 | variant.fields = Fields::Unnamed(::syn::parse_quote!( 86 | /* Variant */ ( 87 | #automata_ident<#ident #generics> 88 | ) 89 | )); 90 | 91 | return Some(state); 92 | } else { 93 | self.push_undeclared_variant_error(ident); 94 | } 95 | } else { 96 | self.push_unsupported_variant_error(variant); 97 | } 98 | None 99 | } 100 | 101 | /// Add `undeclared state` error to the error vector. 102 | fn push_undeclared_variant_error(&mut self, ident: &Ident) { 103 | self.errors 104 | .push(TypestateError::UndeclaredVariant(ident.clone()).into()); 105 | } 106 | 107 | /// Add `unsupported variant` error to the error vector. 108 | fn push_unsupported_variant_error(&mut self, variant: &Variant) { 109 | self.errors 110 | .push(TypestateError::UnsupportedVariant(variant.clone()).into()); 111 | } 112 | 113 | /// Add `unsupported state` error to the error vector. 114 | fn push_unsupported_state_error(&mut self, ident: &Ident) { 115 | self.errors 116 | .push(TypestateError::UnsupportedState(ident.clone()).into()); 117 | } 118 | } 119 | 120 | impl<'sm> VisitMut for DecisionVisitor<'sm> { 121 | fn visit_item_enum_mut(&mut self, i: &mut ItemEnum) { 122 | let enum_ident = i.ident.clone(); 123 | 124 | let destination_idents: Option> = i 125 | .variants 126 | .iter_mut() 127 | .map(|v| self.visit_variant_mut(v)) 128 | .collect(); 129 | 130 | // NOTE: this could be `Option` 131 | 132 | if let Some(dest) = destination_idents { 133 | self.state_machine_info 134 | .intermediate_automaton 135 | .add_choice(enum_ident.clone()); 136 | self.state_machine_info 137 | .intermediate_automaton 138 | .add_transition( 139 | enum_ident.clone().into(), 140 | enum_ident.clone().into(), 141 | dest.into(), 142 | ); 143 | 144 | self.state_machine_info 145 | .non_det_transitions 146 | .insert(enum_ident, i.clone()); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /assets/UmlSmartBulb.svg: -------------------------------------------------------------------------------- 1 | OffOnget_colorscrewunscrewturn_onturn_offset_color -------------------------------------------------------------------------------- /typestate-book/src/static/UmlSmartBulb.svg: -------------------------------------------------------------------------------- 1 | OffOnget_colorscrewunscrewturn_onturn_offset_color -------------------------------------------------------------------------------- /typestate-proc-macro/src/igraph/validate.rs: -------------------------------------------------------------------------------- 1 | // TODO document module 2 | 3 | use super::{IntermediateGraph, Transition}; 4 | use std::{ 5 | collections::{HashMap, HashSet, VecDeque}, 6 | fmt::{Debug, Display}, 7 | hash::Hash, 8 | }; 9 | 10 | pub trait Property {} 11 | 12 | pub trait Validate { 13 | type Out; 14 | fn validate(&self, _: P) -> Self::Out; 15 | } 16 | 17 | type Delta = HashMap, HashSet>>; 18 | 19 | pub struct GenericAutomaton 20 | where 21 | S: Hash + Eq + Debug + Clone + Display, 22 | T: Hash + Eq + Debug + Clone + Display, 23 | { 24 | initial_states: HashSet, 25 | final_states: HashSet, 26 | // TODO: consider if making this pub is a decent solution 27 | pub states: HashSet, 28 | delta: Delta, 29 | idelta: Delta, 30 | } 31 | 32 | impl Default for GenericAutomaton 33 | where 34 | S: Hash + Eq + Debug + Clone + Display, 35 | T: Hash + Eq + Debug + Clone + Display, 36 | { 37 | fn default() -> Self { 38 | Self { 39 | initial_states: HashSet::new(), 40 | final_states: HashSet::new(), 41 | states: HashSet::new(), 42 | delta: Delta::new(), 43 | idelta: Delta::new(), 44 | } 45 | } 46 | } 47 | 48 | impl GenericAutomaton 49 | where 50 | S: Hash + Eq + Debug + Clone + Display, 51 | T: Hash + Eq + Debug + Clone + Display, 52 | { 53 | #[allow(clippy::shadow_unrelated)] 54 | fn add_transition(&mut self, src: S, transition: Transition, dst: S) { 55 | // HACK: since `delta` and `idelta` are inside `self`, use the macro to as a "function" 56 | macro_rules! add_transition { 57 | ($delta:ident, $src:expr, $transition:expr, $dst:expr) => { 58 | let src = $src; 59 | let transition = $transition; 60 | let dst = $dst; 61 | if let Some(t_neighbors) = self.$delta.get_mut(&src) { 62 | if let Some(n_neighbors) = t_neighbors.get_mut(&transition) { 63 | n_neighbors.insert(dst); 64 | } else { 65 | // create the neighbor set 66 | let mut v = HashSet::new(); 67 | v.insert(dst); 68 | // connect them with the transition 69 | t_neighbors.insert(transition, v); 70 | } 71 | } else { 72 | // create the neighbor set 73 | let mut v = HashSet::new(); 74 | v.insert(dst); 75 | // create the transition to neighbor map 76 | let mut v_ = HashMap::new(); 77 | v_.insert(transition, v); 78 | // connect them with the source 79 | self.$delta.insert(src, v_); 80 | } 81 | }; 82 | } 83 | 84 | add_transition!(delta, src.clone(), transition.clone(), dst.clone()); 85 | add_transition!(idelta, dst, transition, src); 86 | } 87 | } 88 | 89 | impl From> for GenericAutomaton 90 | where 91 | S: Hash + Eq + Debug + Clone + Display, 92 | T: Hash + Eq + Debug + Clone + Display, 93 | { 94 | fn from(i: IntermediateGraph) -> Self { 95 | let mut s = Self { 96 | states: i.states, 97 | ..GenericAutomaton::default() 98 | }; 99 | // NOTE: maybe add the choices 100 | for (src, sigmas) in i.delta { 101 | for (sigma, dst) in sigmas { 102 | match (src.clone(), dst) { 103 | (None, super::Node::State(state)) => { 104 | // safety: if src == None then state.state != None 105 | s.initial_states.insert(state.state.unwrap()); 106 | } 107 | (None, super::Node::Decision(_)) => { 108 | unreachable!("the initial state cannot lead to a decision") 109 | } 110 | (Some(src), super::Node::State(state)) => match state.state { 111 | None => { 112 | s.final_states.insert(src); 113 | } 114 | Some(dst) => s.add_transition(src, sigma, dst), 115 | }, 116 | (Some(src), super::Node::Decision(states)) => { 117 | states.into_iter().for_each(|state| { 118 | // safety: destinations cannot point to None 119 | s.add_transition(src.clone(), sigma.clone(), state.state.unwrap()) 120 | }) 121 | } 122 | } 123 | } 124 | } 125 | s 126 | } 127 | } 128 | 129 | /// Productive states property type. 130 | pub struct ProductiveStates; 131 | 132 | impl Property for ProductiveStates {} 133 | 134 | impl Validate for GenericAutomaton 135 | where 136 | S: Hash + Eq + Debug + Clone + Display, 137 | T: Hash + Eq + Debug + Clone + Display, 138 | { 139 | type Out = HashSet; 140 | 141 | fn validate(&self, _: ProductiveStates) -> Self::Out { 142 | let mut stack: VecDeque<_> = self.final_states.iter().collect(); 143 | // productive == visited 144 | let mut productive = HashSet::new(); 145 | while let Some(state) = stack.pop_back() { 146 | if productive.insert(state.clone()) { 147 | if let Some(states) = self.idelta.get(state).map(|transitions| { 148 | transitions 149 | .values() 150 | .flat_map(std::collections::HashSet::iter) 151 | }) { 152 | stack.extend(states) 153 | } 154 | } 155 | } 156 | productive 157 | } 158 | } 159 | 160 | /// Non-productive states property type. 161 | pub struct NonProductiveStates; 162 | 163 | impl Property for NonProductiveStates {} 164 | 165 | impl Validate for GenericAutomaton 166 | where 167 | S: Hash + Eq + Debug + Clone + Display, 168 | T: Hash + Eq + Debug + Clone + Display, 169 | { 170 | type Out = HashSet; 171 | 172 | fn validate(&self, _: NonProductiveStates) -> Self::Out { 173 | let productive = self.validate(ProductiveStates); 174 | self.states.difference(&productive).cloned().collect() 175 | } 176 | } 177 | 178 | /// Useful states property type. 179 | pub struct UsefulStates; 180 | 181 | impl Property for UsefulStates {} 182 | 183 | impl Validate for GenericAutomaton 184 | where 185 | S: Hash + Eq + Debug + Clone + Display, 186 | T: Hash + Eq + Debug + Clone + Display, 187 | { 188 | type Out = HashSet; 189 | 190 | fn validate(&self, _: UsefulStates) -> Self::Out { 191 | // TODO this could benefit from some "caching" of results on productive 192 | let productive = self.validate(ProductiveStates); 193 | let mut stack: VecDeque<_> = self.initial_states.iter().collect(); 194 | // productive == visited 195 | let mut reachable = HashSet::new(); 196 | while let Some(state) = stack.pop_back() { 197 | if reachable.insert(state.clone()) { 198 | if let Some(states) = self.delta.get(state).map(|transitions| { 199 | transitions 200 | .values() 201 | .flat_map(std::collections::HashSet::iter) 202 | }) { 203 | stack.extend(states) 204 | } 205 | } 206 | } 207 | productive.intersection(&reachable).cloned().collect() 208 | } 209 | } 210 | 211 | /// Non-useful states property type. 212 | pub struct NonUsefulStates; 213 | 214 | impl Property for NonUsefulStates {} 215 | 216 | impl Validate for GenericAutomaton 217 | where 218 | S: Hash + Eq + Debug + Clone + Display, 219 | T: Hash + Eq + Debug + Clone + Display, 220 | { 221 | type Out = HashSet; 222 | 223 | fn validate(&self, _: NonUsefulStates) -> Self::Out { 224 | self.states 225 | .difference(&self.validate(UsefulStates)) 226 | .cloned() 227 | .collect() 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [github](https://github.com/rustype/typestate-rs) 2 | //! [docs](https://docs.rs/typestate) 3 | //! [crates](https://crates.io/crates/typestate) 4 | //! 5 | //! Are you frustrated with `IllegalStateException`s in Java? 6 | //! 7 | //! Typestates allow you to define *safe* usage protocols for your objects. 8 | //! The compiler will help you on your journey and disallow errors on given states. 9 | //! You will no longer be able to try and read from closed streams. 10 | //! 11 | //! `#[typestate]` builds on ideas from the [`state_machine_future`](https://github.com/fitzgen/state_machine_future) crate. 12 | //! If typestates are so useful, why not use them with limit them to `Future`s? 13 | //! 14 | //! ### Typestates in Rust 15 | //! 16 | //! Typestates are not a new concept to Rust. 17 | //! There are several blog posts on the subject 18 | //! [[1](https://yoric.github.io/post/rust-typestate/), 19 | //! [2](http://cliffle.com/blog/rust-typestate/), 20 | //! [3](https://rustype.github.io/notes/notes/rust-typestate-series/rust-typestate-index)] 21 | //! as well as a [chapter](https://docs.rust-embedded.org/book/static-guarantees/typestate-programming.html) in *The Embedded Rust Book*. 22 | //! 23 | //! In short, we can write typestates by hand, we add some generics here and there, 24 | //! declare them as a "*state*" and in the end we can keep living our lives with our new state machine. 25 | //! 26 | //! This approach however is *error-prone* and *verbose* (especially with bigger automata). 27 | //! It also provides *no* guarantees about the automata, unless of course, you designed and tested the design previously. 28 | //! 29 | //! As programmers, we want to automate this cumbersome job and to do so, we use Rust's powerful procedural macros! 30 | //! 31 | //! ## Basic Guide 32 | //! 33 | //! Consider we are tasked with building the firmware for a traffic light, 34 | //! we can turn it on and off and cycle between Green, Yellow and Red. 35 | //! 36 | //! We first declare a module with the `#[typestate]` macro attached to it. 37 | //! ```rust,ignore 38 | //! #[typestate] 39 | //! mod traffic_light {} 40 | //! ``` 41 | //! 42 | //! This of course does nothing, in fact it will provide you an error, 43 | //! saying that we haven't declared an *automaton*. 44 | //! 45 | //! And so, our next task is to do that. 46 | //! Inside our `traffic_light` module we declare a structure annotated with `#[automaton]`. 47 | //! ```rust,ignore 48 | //! #[automaton] 49 | //! pub struct TrafficLight; 50 | //! ``` 51 | //! 52 | //! Our next step is to declare the states. 53 | //! We declare three empty structures annotated with `"[state]`. 54 | //! ```rust,ignore 55 | //! #[state] pub struct Green; 56 | //! #[state] pub struct Yellow; 57 | //! #[state] pub struct Red; 58 | //! ``` 59 | //! 60 | //! So far so good, however some errors should appear, regarding the lack of initial and final states. 61 | //! 62 | //! To declare initial and final states we need to see them as describable by transitions. 63 | //! Whenever an object is created, the method that created leaves the object in the *initial* state. 64 | //! Equally, whenever a method consumes an object and does not return it (or a similar version of it), 65 | //! it made the object reach the *final* state. 66 | //! 67 | //! With this in mind we can lay down the following rules: 68 | //! - Functions that *do not* take a valid state (i.e. `self`) and return a valid state, describe an initial state. 69 | //! - Functions that take a valid state (i.e. `self`) and *do not* return a valid state, describe a final state. 70 | //! 71 | //! So we write the following function signatures: 72 | //! ```rust,ignore 73 | //! fn turn_on() -> Red; 74 | //! fn turn_off(self); 75 | //! ``` 76 | //! 77 | //! However, these are *free* functions, meaning that `self` relates to nothing. 78 | //! To attach them to a state we wrap them around a `trait` with the name of the state they are supposed to be attached to. 79 | //! So our previous example becomes: 80 | //! ```rust,ignore 81 | //! trait Red { 82 | //! fn turn_on() -> Red; 83 | //! fn turn_off(self); 84 | //! } 85 | //! ``` 86 | //! 87 | //! *Before we go further, a quick review:* 88 | //! > - The module is annotated with `#[typestate]` enabling the DSL. 89 | //! > - To declare the main automaton we attach `#[automaton]` to a structure. 90 | //! > - The states are declared by attaching `#[state]`. 91 | //! > - State functions are declared through traits that share the same name. 92 | //! > - Initial and final states are declared by functions with a "special" signature. 93 | //! 94 | //! Finally, we need to address how states transition between each other. 95 | //! An astute reader might have inferred that we can consume one state and return another, 96 | //! such reader would be 100% correct. 97 | //! 98 | //! For example, to transition between the `Red` state and the `Green` we do: 99 | //! ```rust,ignore 100 | //! trait Red { 101 | //! fn to_green(self) -> Green; 102 | //! } 103 | //! ``` 104 | //! 105 | //! Building on this we can finish the other states: 106 | //! ```rust,ignore 107 | //! pub trait Green { 108 | //! fn to_yellow(self) -> Yellow; 109 | //! } 110 | //! 111 | //! pub trait Yellow { 112 | //! fn to_red(self) -> Red; 113 | //! } 114 | //! 115 | //! pub trait Red { 116 | //! fn to_green(self) -> Green; 117 | //! fn turn_on() -> Red; 118 | //! fn turn_off(self); 119 | //! } 120 | //! ``` 121 | //! 122 | //! And the full code becomes: 123 | //! 124 | //! ```rust,ignore 125 | //! #[typestate] 126 | //! mod traffic_light { 127 | //! #[automaton] 128 | //! pub struct TrafficLight { 129 | //! pub cycles: u64, 130 | //! } 131 | //! 132 | //! #[state] pub struct Green; 133 | //! #[state] pub struct Yellow; 134 | //! #[state] pub struct Red; 135 | //! 136 | //! pub trait Green { 137 | //! fn to_yellow(self) -> Yellow; 138 | //! } 139 | //! 140 | //! pub trait Yellow { 141 | //! fn to_red(self) -> Red; 142 | //! } 143 | //! 144 | //! pub trait Red { 145 | //! fn to_green(self) -> Green; 146 | //! fn turn_on() -> Red; 147 | //! fn turn_off(self); 148 | //! } 149 | //! } 150 | //! ``` 151 | //! 152 | //! The code above will generate: 153 | //! - Expand the main structure with a `state: State` field. 154 | //! - A sealed trait which disallows states from being added *externally*. 155 | //! - Traits for each state, providing the described functions. 156 | //! 157 | //! ## Advanced Guide 158 | //! 159 | //! There are some features which may be helpful when describing a typestate. 160 | //! There are two main features that weren't discussed yet. 161 | //! 162 | //! ### Self-transitioning functions 163 | //! Putting it simply, states may require to mutate themselves without transitioning, or maybe we require a simple getter. 164 | //! To declare methods for that purpose, we can use functions that take references (mutable or not) to `self`. 165 | //! 166 | //! Consider the following example where we have a flag that can be up or not. 167 | //! We have two functions, one checks if the flag is up, the other, sets the flag up. 168 | //! 169 | //! ```rust,ignore 170 | //! #[state] struct Flag { 171 | //! up: bool 172 | //! } 173 | //! 174 | //! impl Flag { 175 | //! fn is_up(&self) -> bool; 176 | //! fn set_up(&mut self); 177 | //! } 178 | //! ``` 179 | //! 180 | //! As these functions do not change the typestate state, 181 | //! they transition back to the current state. 182 | //! 183 | //! ### Non-deterministic transitions 184 | //! Consider that a typestate relies on an external component that can fail, to model that, one would use `Result`. 185 | //! However, we need our typestate to transition between known states, so we declare two things: 186 | //! - An `Error` state along with the other states. 187 | //! - An `enum` to represent the bifurcation of states. 188 | //! 189 | //! ```rust,ignore 190 | //! #[state] struct Error { 191 | //! message: String 192 | //! } 193 | //! 194 | //! enum OperationResult { 195 | //! State, Error 196 | //! } 197 | //! ``` 198 | //! 199 | //! Inside the enumeration there can only be other valid states and only `Unit` style variants are supported. 200 | //! 201 | //! ## Attributes 202 | //! 203 | //! This is the list of attributes that can be used along `#[typestate]`: 204 | //! - `#[typestate]`: the main attribute macro, without attribute parameters. 205 | //! - `#[typestate(enumerate = "...")]`: this option makes the macro generate an additional `enum`, 206 | //! the `enum` enables working with variables and structures "generic" to the state. 207 | //! - The parameter can be declared *with* or *without* a string literal, if declared with the string, 208 | //! that string will be used as identifier to the `enum`. 209 | //! - If the parameter is used with an *empty string* or *without* a string, 210 | //! the default behavior is to prepend an `E` to the automata name. 211 | //! - `#[typestate(state_constructors = "...")`: this option generates basic constructors for states with fields. 212 | //! 213 | //! ## Features 214 | //! The cargo features you can enable: 215 | //! - `debug_dot` will generate a `.dot` file of your state machine. 216 | //! - This feature can be customized through the following environment variables (taken from the [DOT documentation](https://graphviz.org/doc/info/attrs.html)): 217 | //! - `DOT_PAD` - Specifies how much, in inches, to extend the drawing area around the minimal area needed to draw the graph. 218 | //! - `DOT_NODESEP` - In `dot`, `nodesep` specifies the minimum space between two adjacent nodes in the same rank, in inches. 219 | //! - `DOT_RANKSEP` - In `dot`, sets the desired rank separation, in inches. 220 | //! - `debug_plantuml` will generate a PlantUML state diagram (`.uml` file) of your state machine. 221 | 222 | pub extern crate typestate_proc_macro; 223 | 224 | pub use ::typestate_proc_macro::typestate; 225 | 226 | #[doc(hidden)] 227 | pub mod __private__ { 228 | #[cfg(feature = "docs-mermaid")] 229 | pub use ::aquamarine; 230 | } 231 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /typestate-proc-macro/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 | -------------------------------------------------------------------------------- /typestate-proc-macro/src/visitors/state.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use crate::{generated_attr, StateMachineInfo, TypestateError}; 4 | 5 | use parse::Parser; 6 | use syn::{ 7 | parse, visit_mut::VisitMut, Attribute, Error, Field, Fields, Ident, Item, ItemMod, ItemStruct, 8 | Path, 9 | }; 10 | 11 | pub(crate) const AUTOMATA_ATTR_IDENT: &str = "automaton"; 12 | pub(crate) const STATE_ATTR_IDENT: &str = "state"; 13 | 14 | type Result = ::core::result::Result; 15 | 16 | // HACK return type 17 | pub(crate) fn visit_states( 18 | module: &mut ItemMod, 19 | state_machine_info: &mut StateMachineInfo, 20 | constructor_ident: Option, 21 | ) -> Vec { 22 | // start visitor 23 | let mut state_visitor = StateVisitor::new(state_machine_info, constructor_ident); 24 | state_visitor.visit_item_mod_mut(module); 25 | // report state_visitor errors and return 26 | if !state_visitor.errors.is_empty() { 27 | return state_visitor.errors; 28 | } 29 | 30 | let mut constructors = state_visitor.constructors; 31 | if let Some((_, v)) = &mut module.content { 32 | v.append(&mut constructors); 33 | } 34 | 35 | let sealed_trait = state_visitor.sealed_trait; 36 | if sealed_trait.trait_ident.is_none() { 37 | return vec![TypestateError::MissingAutomata.into()]; 38 | } 39 | 40 | match &mut module.content { 41 | Some((_, v)) => { 42 | v.append(&mut sealed_trait.into()); // HACK unwrap is safe because otherwise errors would've bailed 43 | } 44 | None => {} 45 | } 46 | 47 | vec![] 48 | } 49 | 50 | struct StateVisitor<'sm> { 51 | /// State machine required information 52 | state_machine_info: &'sm mut StateMachineInfo, 53 | /// Sealed trait information 54 | sealed_trait: SealedPattern, 55 | /// Default constructors 56 | constructors: Vec, 57 | /// Default constructor ident 58 | constructor_ident: Option, 59 | /// Errors found during expansion 60 | errors: Vec, 61 | } 62 | 63 | impl<'sm> StateVisitor<'sm> { 64 | fn new( 65 | state_machine_info: &'sm mut StateMachineInfo, 66 | constructor_ident: Option, 67 | ) -> Self { 68 | Self { 69 | state_machine_info, 70 | sealed_trait: SealedPattern::default(), 71 | constructors: vec![], 72 | constructor_ident, 73 | errors: vec![], 74 | } 75 | } 76 | 77 | /// Add `multiple attributes` error to the error vector. 78 | fn push_multiple_attr_error(&mut self, attr: &Attribute) { 79 | self.errors 80 | .push(TypestateError::ConflictingAttributes(attr.clone()).into()); 81 | } 82 | 83 | /// Add `duplicate attribute` error to the error vector. 84 | fn push_multiple_decl_error(&mut self, attr: &Attribute) { 85 | self.errors 86 | .push(TypestateError::DuplicateAttributes(attr.clone()).into()); 87 | } 88 | 89 | /// Add `multiple automata` error to the error vector. 90 | fn push_multiple_automata_decl_error(&mut self, it: &ItemStruct) { 91 | self.errors 92 | .push(TypestateError::AutomataRedefinition(it.clone()).into()); 93 | } 94 | } 95 | 96 | impl<'sm> VisitMut for StateVisitor<'sm> { 97 | fn visit_item_struct_mut(&mut self, it_struct: &mut ItemStruct) { 98 | let attributes = &mut it_struct.attrs; 99 | let mut main_attr = None; 100 | attributes.retain(|attr| { 101 | Attr::Retain == { 102 | let ts_attr = TypestateAttr::try_from(&attr.path); 103 | match ts_attr { 104 | Ok(inner_ts_attr) => { 105 | match main_attr { 106 | Some(ref prev_attr) => { 107 | if *prev_attr == inner_ts_attr { 108 | self.push_multiple_decl_error(attr); 109 | } else { 110 | self.push_multiple_attr_error(attr); 111 | } 112 | } 113 | ref mut at_none @ None => { 114 | // only if it wasnt previously assigned we can assign a new value 115 | *at_none = Some(inner_ts_attr) 116 | } 117 | } 118 | Attr::Discard 119 | } 120 | Err(()) => Attr::Retain, 121 | } 122 | } 123 | }); 124 | 125 | // if errors were reported stop processing 126 | if !self.errors.is_empty() { 127 | return; 128 | } 129 | 130 | match main_attr { 131 | Some(TypestateAttr::Automata) => { 132 | // check for multiple automata definitions 133 | match self.state_machine_info.automaton_ident { 134 | Some(_) => { 135 | self.push_multiple_automata_decl_error(it_struct); 136 | return; 137 | } 138 | None => self.state_machine_info.automaton_ident = Some(it_struct.clone()), 139 | }; 140 | match it_struct.expand_state_type_parameter() { 141 | Ok(bound_ident) => match self.sealed_trait.trait_ident { 142 | Some(_) => unreachable!("this should have been checked previously"), 143 | None => self.sealed_trait.trait_ident = Some(bound_ident), 144 | }, 145 | Err(e) => { 146 | self.errors.push(e); 147 | } 148 | } 149 | } 150 | Some(TypestateAttr::State) => { 151 | self.state_machine_info 152 | .intermediate_automaton 153 | .add_state(it_struct.ident.clone()); 154 | 155 | // TODO: remove the call below 156 | self.state_machine_info.add_state(it_struct.clone().into()); 157 | self.sealed_trait.states.push(it_struct.clone()); 158 | if let Some(ident) = &self.constructor_ident { 159 | self.constructors 160 | .expand_state_constructors(ident, it_struct); 161 | } 162 | } 163 | None => { 164 | // empty attribute list 165 | } 166 | } 167 | } 168 | } 169 | 170 | trait ExpandStateConstructors { 171 | fn expand_state_constructors(&mut self, constructor_ident: &Ident, item_struct: &ItemStruct); 172 | } 173 | 174 | impl ExpandStateConstructors for Vec { 175 | fn expand_state_constructors(&mut self, constructor_ident: &Ident, item_struct: &ItemStruct) { 176 | if let Fields::Named(named) = &item_struct.fields { 177 | let struct_ident = &item_struct.ident; 178 | let field_ident = named.named.iter().map(|field| &field.ident); 179 | let field_ident2 = named.named.iter().map(|field| &field.ident); // HACK 180 | let field_ty = named.named.iter().map(|field| &field.ty); 181 | let generated_attr = generated_attr(); 182 | self.push(::syn::parse_quote! { 183 | #generated_attr 184 | impl #struct_ident { 185 | pub fn #constructor_ident(#(#field_ident: #field_ty,)*) -> Self { 186 | Self { 187 | #(#field_ident2,)* 188 | } 189 | } 190 | } 191 | }); 192 | } 193 | } 194 | } 195 | 196 | trait ExpandState { 197 | /// Expand the state type parameter in a structure or other kind of item. 198 | fn expand_state_type_parameter(&mut self) -> syn::Result; 199 | } 200 | 201 | impl ExpandState for ItemStruct { 202 | fn expand_state_type_parameter(&mut self) -> syn::Result { 203 | // TODO make the suffix custom 204 | let type_param_ident = ::quote::format_ident!("{}State", self.ident); 205 | self.generics 206 | .params 207 | .push(::syn::parse_quote!(State: #type_param_ident)); 208 | 209 | let field_to_add = ::quote::quote!( 210 | pub state: State 211 | ); 212 | 213 | match &mut self.fields { 214 | syn::Fields::Named(named) => { 215 | named 216 | .named 217 | .push(Field::parse_named.parse2(field_to_add).unwrap()); 218 | } 219 | syn::Fields::Unnamed(_) => { 220 | return syn::Result::Err(TypestateError::UnsupportedStruct(self.clone()).into()); 221 | } 222 | syn::Fields::Unit => { 223 | self.fields = Fields::Named(::syn::parse_quote!({ #field_to_add })); 224 | } 225 | }; 226 | 227 | Ok(type_param_ident) 228 | } 229 | } 230 | 231 | #[derive(Debug, PartialEq)] 232 | enum TypestateAttr { 233 | Automata, 234 | State, 235 | } 236 | 237 | impl TryFrom<&Ident> for TypestateAttr { 238 | // TODO take care of this error type 239 | type Error = (); 240 | 241 | fn try_from(ident: &Ident) -> Result { 242 | if ident == AUTOMATA_ATTR_IDENT { 243 | Ok(Self::Automata) 244 | } else if ident == STATE_ATTR_IDENT { 245 | Ok(Self::State) 246 | } else { 247 | Err(()) 248 | } 249 | } 250 | } 251 | 252 | impl TryFrom<&Path> for TypestateAttr { 253 | type Error = (); 254 | 255 | fn try_from(path: &Path) -> Result { 256 | if path.is_ident(AUTOMATA_ATTR_IDENT) { 257 | Ok(Self::Automata) 258 | } else if path.is_ident(STATE_ATTR_IDENT) { 259 | Ok(Self::State) 260 | } else { 261 | Err(()) 262 | } 263 | } 264 | } 265 | 266 | #[derive(PartialEq)] 267 | enum Attr { 268 | Retain, 269 | Discard, 270 | } 271 | 272 | #[derive(Default)] 273 | pub(crate) struct SealedPattern { 274 | /// Ident for the sealed pattern public trait 275 | trait_ident: Option, // late init 276 | /// Item structs for the states. 277 | states: Vec, 278 | } 279 | 280 | // TODO rework this as an ExpandX trait 281 | impl From for Vec { 282 | /// Convert the [`SealedTrait`] into a vector of Item. 283 | /// This enables the addition of new items to the main module. 284 | fn from(sealed_pattern: SealedPattern) -> Self { 285 | let trait_ident = sealed_pattern.trait_ident.expect("missing `.trait_ident`"); 286 | let private_mod_ident = ::quote::format_ident!("__private"); 287 | // or `Private` or `Sealed` or `format_ident!("{}Sealed", …)` 288 | // take into account that `trait_ident` may have already been used 289 | let private_mod_trait = &trait_ident; 290 | 291 | let generated_attr = generated_attr(); 292 | 293 | let mut ret = vec![ 294 | // Sealed trait 295 | ::syn::parse_quote! { 296 | #generated_attr 297 | #[doc(hidden)] 298 | /* private */ mod #private_mod_ident { 299 | /* to avoid the nested item being processed */ 300 | #generated_attr 301 | pub trait #private_mod_trait {} 302 | } 303 | }, 304 | // State trait 305 | ::syn::parse_quote! { 306 | #generated_attr 307 | pub trait #trait_ident: #private_mod_ident::#private_mod_trait {} 308 | }, 309 | // Blanket impl of state trait from sealed implementors 310 | // This frees us from having to provide concrete impls for each type. 311 | ::syn::parse_quote! { 312 | #generated_attr 313 | impl<__T : ?::core::marker::Sized> #trait_ident 314 | for __T 315 | where 316 | __T : #private_mod_ident::#private_mod_trait, 317 | {} 318 | }, 319 | ]; 320 | 321 | let states = &sealed_pattern.states; 322 | 323 | // Sealed trait impls 324 | ret.extend(states.iter().map(|each_state| { 325 | let struct_ident = &each_state.ident; 326 | let (impl_generics, type_generics, where_clause) = each_state.generics.split_for_impl(); 327 | ::syn::parse_quote! { 328 | #generated_attr 329 | impl #impl_generics #private_mod_ident::#private_mod_trait for #struct_ident #type_generics #where_clause {} 330 | } 331 | })); 332 | 333 | ret 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /typestate-proc-macro/src/visitors/transition.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::{IsGeneratedAttr, StateMachineInfo, Transition, TypestateError}; 4 | use syn::Attribute; 5 | use syn::{ 6 | visit_mut::VisitMut, Error, FnArg, Ident, ItemMod, ItemTrait, Receiver, ReturnType, Signature, 7 | TraitItemMethod, Type, 8 | }; 9 | 10 | macro_rules! bail_if_any { 11 | ( $errors:expr ) => { 12 | match $errors { 13 | errors => { 14 | if !errors.is_empty() { 15 | return errors; 16 | } 17 | } 18 | } 19 | }; 20 | } 21 | 22 | pub(crate) fn visit_transitions( 23 | module: &mut ItemMod, 24 | state_machine_info: &mut StateMachineInfo, 25 | ) -> Vec { 26 | // Visit transitions 27 | let mut transition_visitor = TransitionVisitor::new(state_machine_info); 28 | transition_visitor.visit_item_mod_mut(module); 29 | 30 | // report transition_visitor errors and return 31 | bail_if_any!(transition_visitor.errors); 32 | bail_if_any!(state_machine_info.check_missing()); 33 | bail_if_any!(state_machine_info.check_unused_non_det_transitions()); 34 | vec![] 35 | } 36 | 37 | struct TransitionVisitor<'sm> { 38 | current_state: Option, 39 | state_machine_info: &'sm mut StateMachineInfo, 40 | errors: Vec, 41 | } 42 | 43 | impl<'sm> TransitionVisitor<'sm> { 44 | fn new(state_machine_info: &'sm mut StateMachineInfo) -> Self { 45 | Self { 46 | current_state: None, 47 | state_machine_info, 48 | errors: vec![], 49 | } 50 | } 51 | 52 | /// Add `unknown state` error to the error vector. 53 | fn push_unknown_state_error(&mut self, ident: &Ident) { 54 | self.errors 55 | .push(TypestateError::UnknownState(ident.clone()).into()); 56 | } 57 | 58 | fn push_invalid_trait_error(&mut self, it: &ItemTrait) { 59 | self.errors 60 | .push(TypestateError::InvalidAssocFuntions(it.clone()).into()); 61 | } 62 | } 63 | 64 | impl<'sm> VisitMut for TransitionVisitor<'sm> { 65 | fn visit_item_trait_mut(&mut self, i: &mut ItemTrait) { 66 | let attributes = &i.attrs; 67 | // check if there is a `#[generated]` attribute 68 | // if there is, do not process this trait 69 | if attributes.iter().any(Attribute::is_generated_attr) { 70 | return; 71 | } 72 | 73 | let ident = &i.ident; 74 | 75 | if self 76 | .state_machine_info 77 | .non_det_transitions 78 | .contains_key(ident) 79 | { 80 | self.push_invalid_trait_error(i); 81 | return; 82 | } 83 | 84 | if self.state_machine_info.det_states.contains_key(ident) { 85 | self.current_state = Some(ident.clone()); 86 | i.ident = ::quote::format_ident!("{}State", ident); 87 | // go deeper 88 | for item in &mut i.items { 89 | self.visit_trait_item_mut(item); 90 | } 91 | } else { 92 | self.push_unknown_state_error(ident); 93 | } 94 | } 95 | 96 | fn visit_trait_item_method_mut(&mut self, i: &mut TraitItemMethod) { 97 | let attrs = &mut i.attrs; 98 | let sig = &mut i.sig; 99 | let mut states = HashSet::new(); 100 | self.state_machine_info.det_states.keys().for_each(|k| { 101 | states.insert(k.clone()); // HACK clone 102 | }); 103 | self.state_machine_info 104 | .non_det_transitions 105 | .keys() 106 | .for_each(|k| { 107 | states.insert(k.clone()); // HACK clone 108 | }); 109 | let fn_kind = sig.extract_signature_kind(&states); 110 | let fn_ident = sig.ident.clone(); 111 | sig.expand_signature_state(self.state_machine_info); // TODO check for correct expansion 112 | 113 | match fn_kind { 114 | FnKind::Initial(return_ty_ident) => { 115 | // add a transition to an initial state 116 | // BOOK 117 | self.state_machine_info 118 | .intermediate_automaton 119 | .add_transition( 120 | None, 121 | fn_ident.clone().into(), 122 | return_ty_ident.clone().into(), 123 | ); 124 | 125 | self.state_machine_info 126 | .insert_initial(return_ty_ident.clone(), fn_ident); 127 | // mark non det transition as used 128 | if self 129 | .state_machine_info 130 | .non_det_transitions 131 | .contains_key(&return_ty_ident) 132 | { 133 | self.state_machine_info 134 | .used_non_det_transitions 135 | .insert(return_ty_ident); 136 | } 137 | } 138 | FnKind::Final => { 139 | // add #[must_use] 140 | // attrs.push(::syn::parse_quote!(#[must_use])); 141 | let state = self.current_state.as_ref().unwrap().clone(); 142 | 143 | // BOOK 144 | self.state_machine_info 145 | .intermediate_automaton 146 | .add_transition(Some(state.clone()), fn_ident.clone().into(), None.into()); 147 | 148 | self.state_machine_info.insert_final(state, fn_ident); 149 | } 150 | FnKind::Transition(return_ty_ident) => { 151 | // add #[must_use] 152 | attrs.push(::syn::parse_quote!(#[must_use])); 153 | let source = self.current_state.as_ref().unwrap().clone(); 154 | // BOOK 155 | self.state_machine_info 156 | .intermediate_automaton 157 | .add_transition( 158 | source.clone().into(), 159 | fn_ident.clone().into(), 160 | return_ty_ident.clone().into(), 161 | ); 162 | 163 | let transition = Transition::new(source, return_ty_ident.clone(), fn_ident); 164 | 165 | self.state_machine_info.transitions.insert(transition); 166 | // mark non det transition as used 167 | if self 168 | .state_machine_info 169 | .non_det_transitions 170 | .contains_key(&return_ty_ident) 171 | { 172 | self.state_machine_info 173 | .used_non_det_transitions 174 | .insert(return_ty_ident); 175 | } 176 | } 177 | FnKind::SelfTransition => { 178 | let state = self.current_state.as_ref().unwrap().clone(); 179 | 180 | // BOOK 181 | self.state_machine_info 182 | .intermediate_automaton 183 | .add_transition( 184 | state.clone().into(), 185 | fn_ident.clone().into(), 186 | state.clone().into(), 187 | ); 188 | 189 | let transition = Transition::new(state.clone(), state.clone(), fn_ident); 190 | self.state_machine_info.transitions.insert(transition); 191 | // mark non det transition as used 192 | if self 193 | .state_machine_info 194 | .non_det_transitions 195 | .contains_key(&state) 196 | { 197 | self.state_machine_info 198 | .used_non_det_transitions 199 | .insert(state); 200 | } 201 | } 202 | FnKind::Other => {} 203 | }; 204 | } 205 | } 206 | 207 | /// Enumeration describing a function's receiver kind. 208 | /// 209 | /// Possible kinds are: 210 | /// - `self` 211 | /// - `mut self` 212 | /// - `&self` 213 | /// - `&mut self` 214 | /// - `T`/`&T`/`&mut T` 215 | #[derive(Debug)] 216 | enum ReceiverKind { 217 | /// Receiver takes ownership of `self`. 218 | OwnedSelf, 219 | /// Receiver takes mutable ownership of `self`. 220 | MutOwnedSelf, 221 | /// Receiver takes a reference to `self`. 222 | RefSelf, 223 | /// Receiver takes a mutable reference to `self`. 224 | MutRefSelf, 225 | /// Receiver takes any other type. 226 | Other, 227 | } 228 | 229 | /// Enumeration describing a function's output kind in regards to existing states. 230 | /// 231 | /// Possible kinds are: 232 | /// - `()` 233 | /// - `State` 234 | /// - `T` 235 | #[derive(Debug)] 236 | enum OutputKind { 237 | /// Function does not return a value (i.e. Java's `void`). 238 | Unit, 239 | /// Function returns a `T` which is a valid state. 240 | /// 241 | /// Note: `&T` or `&mut T` are not valid states. 242 | State(Ident), 243 | /// Any other `T`. 244 | Other, 245 | } 246 | 247 | /// Enumeration describing a function's kind in regard to the typestate state machine. 248 | /// 249 | /// Possible kinds are: 250 | /// - `fn() -> State` 251 | /// - `fn(self) -> T` 252 | /// - `fn(self) -> State` 253 | /// - `fn(&self) -> T` or `fn(&mut self) -> T` 254 | #[derive(Debug)] 255 | enum FnKind { 256 | /// Function that does not take `self` and returns a valid state. 257 | Initial(Ident), 258 | /// Function that consumes `self` and does not return a valid state. 259 | Final, 260 | /// Function that consumes `self` and returns a valid state. 261 | Transition(Ident), 262 | /// Function that takes a reference (mutable or not) to `self`, it cannot return a state. 263 | SelfTransition, 264 | /// Other kinds of functions 265 | Other, 266 | } 267 | 268 | /// Provides a series of utility methods to be used on [`syn::Signature`]. 269 | trait SignatureKind { 270 | /// Extract a [`ReceiverKind`] from a [`syn::Signature`]. 271 | fn extract_receiver_kind(&self) -> ReceiverKind; 272 | /// Extract a [`OutputKind`] from a [`syn::Signature`]. 273 | fn extract_output_kind(&self, states: &HashSet) -> OutputKind; 274 | /// Extract a [`FnKind`] from a [`syn::Signature`]. 275 | /// Takes a set of states to check for valid states. 276 | fn extract_signature_kind(&self, states: &HashSet) -> FnKind; 277 | /// Expands a signature 278 | /// (e.g. `fn f() -> State => fn f() -> Automata`). 279 | fn expand_signature_state(&mut self, info: &StateMachineInfo); 280 | } 281 | 282 | impl SignatureKind for Signature { 283 | fn extract_receiver_kind(&self) -> ReceiverKind { 284 | let fn_in = &self.inputs; 285 | if let Some(FnArg::Receiver(Receiver { 286 | reference, 287 | mutability, 288 | .. 289 | })) = fn_in.first() 290 | { 291 | match (reference, mutability) { 292 | (None, None) => ReceiverKind::OwnedSelf, 293 | (None, Some(_)) => ReceiverKind::MutOwnedSelf, 294 | (Some(_), None) => ReceiverKind::RefSelf, 295 | (Some(_), Some(_)) => ReceiverKind::MutRefSelf, 296 | } 297 | } else { 298 | ReceiverKind::Other 299 | } 300 | } 301 | 302 | // the `states: HashMap` kinda sucks 303 | // making a `Contains` trait with a `contains` method and implement that for `HashMap` 304 | // would probably be better 305 | fn extract_output_kind(&self, states: &HashSet) -> OutputKind { 306 | let fn_out = &self.output; 307 | match fn_out { 308 | ReturnType::Default => OutputKind::Unit, 309 | ReturnType::Type(_, ty) => match **ty { 310 | Type::Path(ref path) => { 311 | if let Some(ident) = path.path.segments.first() { 312 | let ident = &ident.ident; 313 | if states.contains(ident) { 314 | return OutputKind::State(ident.clone()); 315 | } 316 | } 317 | OutputKind::Other 318 | } 319 | _ => OutputKind::Other, 320 | }, 321 | } 322 | } 323 | 324 | fn extract_signature_kind(&self, states: &HashSet) -> FnKind { 325 | let recv = self.extract_receiver_kind(); 326 | let out = self.extract_output_kind(states); 327 | match (recv, out) { 328 | (ReceiverKind::OwnedSelf, OutputKind::State(ident)) 329 | | (ReceiverKind::MutOwnedSelf, OutputKind::State(ident)) => FnKind::Transition(ident), 330 | (ReceiverKind::OwnedSelf, _) | (ReceiverKind::MutOwnedSelf, _) => FnKind::Final, 331 | (ReceiverKind::RefSelf, _) | (ReceiverKind::MutRefSelf, _) => FnKind::SelfTransition, 332 | (ReceiverKind::Other, OutputKind::State(ident)) => FnKind::Initial(ident), 333 | (ReceiverKind::Other, _) => FnKind::Other, 334 | } 335 | } 336 | 337 | fn expand_signature_state(&mut self, info: &StateMachineInfo) { 338 | let fn_out = &mut self.output; 339 | let det_states = &info.det_states; 340 | let non_det_states = &info.non_det_transitions; 341 | 342 | if let ReturnType::Type(_, ty) = fn_out { 343 | if let Type::Path(ref mut path) = **ty { 344 | if let Some(ident) = path.path.get_ident() { 345 | if det_states.contains_key(ident) { 346 | let automata_ident = info.get_automaton_ident(); 347 | path.path = ::syn::parse_quote!(#automata_ident<#ident>); 348 | } else if let Some(it_enum) = non_det_states.get(ident) { 349 | let generics = &it_enum.generics; 350 | path.path = ::syn::parse_quote!(#ident #generics); 351 | } 352 | } 353 | } 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /typestate-proc-macro/src/igraph/export.rs: -------------------------------------------------------------------------------- 1 | //! The `export` module contains the code which "compiles" 2 | //! typestate graphs into formats other tools understand. 3 | //! 4 | //! Currently, the following formats are supported: 5 | //! - `DOT` 6 | //! - `PlantUML` 7 | //! - `MermaidJS` 8 | //! 9 | //! To add a new format you will need to create a new `struct` 10 | //! representing the format, implement `Format` for it and then 11 | //! implement `Export` for `IntermediateGraph` where the generic type 12 | //! `Export` takes, is your format. 13 | //! 14 | //! ```rust,ignore 15 | //! impl Export for IntermediateGraph { ... } 16 | //! ``` 17 | 18 | /// Type alias for `()` or [`std::error::Error`]. 19 | #[allow(dead_code)] 20 | type Result = std::result::Result<(), Box>; 21 | 22 | /// Blanket trait for [`Export`] implementations. 23 | pub trait Format { 24 | /// The current format file extension. 25 | fn file_extension<'a>() -> &'a str; 26 | } 27 | 28 | /// Used to declare output formats for the [`IntermediateAutomaton`]. 29 | pub trait Export { 30 | /// Export the implementing type as format `F` to the output stream `w`. 31 | fn export(&self, w: &mut W, _: F) -> Result; 32 | } 33 | 34 | /// The Mermaid format module, containing the marker type and implementation for the respective export trait. 35 | #[cfg(feature = "docs-mermaid")] 36 | pub mod mermaid { 37 | use super::{Export, Result}; 38 | use crate::igraph::{IntermediateGraph, Node, Transition}; 39 | use std::{ 40 | fmt::{Debug, Display}, 41 | hash::Hash, 42 | }; 43 | 44 | /// The mermaid format struct. 45 | #[derive(Clone, Copy)] 46 | pub struct Mermaid; 47 | 48 | /// Blanket implementation for the [`Mermaid`] format. 49 | impl super::Format for Mermaid { 50 | fn file_extension<'a>() -> &'a str { 51 | "mermaid" 52 | } 53 | } 54 | 55 | impl Export for IntermediateGraph 56 | where 57 | S: Hash + Eq + Debug + Clone + Display, 58 | T: Hash + Eq + Debug + Clone + Display, 59 | { 60 | fn export(&self, w: &mut W, f: Mermaid) -> Result { 61 | writeln!(w, "stateDiagram-v2")?; 62 | 63 | if let Some(v) = self.delta.get(&None) { 64 | for (t, dst) in v { 65 | (t, dst).export(w, f)? 66 | } 67 | } 68 | 69 | for choice in &self.choices { 70 | writeln!(w, "state {} <<choice>>", choice)?; 71 | } 72 | 73 | for (src, v) in &self.delta { 74 | if let Some(src) = src { 75 | for (t, dst) in v { 76 | (src, t, dst).export(w, f)? 77 | } 78 | } 79 | } 80 | 81 | Ok(()) 82 | } 83 | } 84 | 85 | impl Export for (&Transition, &Node) 86 | where 87 | S: Hash + Eq + Debug + Clone + Display, 88 | T: Hash + Eq + Debug + Clone + Display, 89 | { 90 | fn export(&self, w: &mut W, _: Mermaid) -> Result { 91 | let t = &self.0.transition; 92 | let dst = self.1; 93 | match dst { 94 | Node::State(state) => match &state.state { 95 | None => unreachable!("invalid transition: None -> None"), 96 | Some(s) => { 97 | // if there is a transition label, use that instead of the existing label 98 | if let Some(label) = &state.metadata.transition_label { 99 | writeln!(w, "[*] --> {} : {}", label, t)? 100 | } else { 101 | writeln!(w, "[*] --> {} : {}", s, t)? 102 | } 103 | } 104 | }, 105 | Node::Decision(_) => { 106 | // NOTE: unsure about this 107 | unreachable!("invalid transition: None -> Decision") 108 | } 109 | } 110 | Ok(()) 111 | } 112 | } 113 | 114 | impl Export for (&S, &Transition, &Node) 115 | where 116 | S: Hash + Eq + Debug + Clone + Display, 117 | T: Hash + Eq + Debug + Clone + Display, 118 | { 119 | fn export(&self, w: &mut W, _: Mermaid) -> Result { 120 | let src = self.0; 121 | let t = &self.1.transition; 122 | let dst = self.2; 123 | 124 | match dst { 125 | Node::State(state) => match &state.state { 126 | None => writeln!(w, "{} --> [*] : {}", src, t)?, 127 | Some(s) => { 128 | // if there is a transition label, use that instead of the existing label 129 | if let Some(label) = &state.metadata.transition_label { 130 | writeln!(w, "{} --> {} : {}", src, label, t)? 131 | } else { 132 | writeln!(w, "{} --> {} : {}", src, s, t)? 133 | } 134 | } 135 | }, 136 | Node::Decision(decision) => { 137 | for s in decision { 138 | if let Some(state) = &s.state { 139 | if let Some(label) = &s.metadata.transition_label { 140 | writeln!(w, "{} --> {} : {}", src, state, label)? 141 | } else { 142 | writeln!(w, "{} --> {}", src, state)? 143 | } 144 | } else if let Some(label) = &s.metadata.transition_label { 145 | writeln!(w, "{} --> [*] : {}", src, label)? 146 | } else { 147 | writeln!(w, "{} --> [*]", src)? 148 | } 149 | } 150 | } 151 | } 152 | 153 | Ok(()) 154 | } 155 | } 156 | } 157 | 158 | /// The PlantUML format module, containing the marker type and implementation for the respective export trait. 159 | #[cfg(feature = "export-plantuml")] 160 | pub mod plantuml { 161 | use super::{Export, Result}; 162 | use crate::igraph::{IntermediateGraph, Node, Transition}; 163 | use std::{ 164 | fmt::{Debug, Display}, 165 | hash::Hash, 166 | }; 167 | 168 | #[derive(Clone, Copy)] 169 | pub struct PlantUml; 170 | 171 | impl super::Format for PlantUml { 172 | fn file_extension<'a>() -> &'a str { 173 | "uml" 174 | } 175 | } 176 | 177 | impl Export for IntermediateGraph 178 | where 179 | S: Hash + Eq + Debug + Clone + Display, 180 | T: Hash + Eq + Debug + Clone + Display, 181 | { 182 | fn export(&self, w: &mut W, f: PlantUml) -> Result { 183 | writeln!(w, "@startuml")?; 184 | writeln!(w, "hide empty description")?; 185 | 186 | if let Some(s) = ::std::env::var_os("PLANTUML_NODESEP") { 187 | w.write_fmt(format_args!( 188 | "skinparam nodesep {}\n", 189 | s.into_string().unwrap_or_else(|_| "30".to_string()) 190 | ))?; 191 | } 192 | 193 | if let Some(s) = ::std::env::var_os("PLANTUML_RANKSEP") { 194 | w.write_fmt(format_args!( 195 | "skinparam ranksep {}\n", 196 | s.into_string().unwrap_or_else(|_| "30".to_string()) 197 | ))?; 198 | } 199 | 200 | if let Some(v) = self.delta.get(&None) { 201 | for (t, dst) in v { 202 | (t, dst).export(w, f)? 203 | } 204 | } 205 | 206 | for choice in &self.choices { 207 | writeln!(w, "state {} <>", choice)?; 208 | } 209 | 210 | for (src, v) in &self.delta { 211 | if let Some(src) = src { 212 | for (t, dst) in v { 213 | (src, t, dst).export(w, f)? 214 | } 215 | } 216 | } 217 | 218 | writeln!(w, "@end")?; 219 | 220 | Ok(()) 221 | } 222 | } 223 | 224 | impl Export for (&Transition, &Node) 225 | where 226 | S: Hash + Eq + Debug + Clone + Display, 227 | T: Hash + Eq + Debug + Clone + Display, 228 | { 229 | fn export(&self, w: &mut W, _: PlantUml) -> Result { 230 | let t = &self.0.transition; 231 | let dst = self.1; 232 | 233 | match dst { 234 | Node::State(state) => match &state.state { 235 | None => unreachable!("invalid transition: None -> None"), 236 | Some(s) => { 237 | // if there is a transition label, use that instead of the existing label 238 | if let Some(label) = &state.metadata.transition_label { 239 | writeln!(w, "[*] --> {} : {}", label, t)? 240 | } else { 241 | writeln!(w, "[*] --> {} : {}", s, t)? 242 | } 243 | } 244 | }, 245 | Node::Decision(_) => { 246 | // NOTE: unsure about this 247 | unreachable!("invalid transition: None -> Decision") 248 | } 249 | } 250 | 251 | Ok(()) 252 | } 253 | } 254 | 255 | impl Export for (&S, &Transition, &Node) 256 | where 257 | S: Hash + Eq + Debug + Clone + Display, 258 | T: Hash + Eq + Debug + Clone + Display, 259 | { 260 | fn export(&self, w: &mut W, _: PlantUml) -> Result { 261 | let src = self.0; 262 | let t = &self.1.transition; 263 | let dst = self.2; 264 | 265 | match dst { 266 | Node::State(state) => match &state.state { 267 | None => writeln!(w, "{} --> [*] : {}", src, t)?, 268 | Some(s) => { 269 | // if there is a transition label, use that instead of the existing label 270 | if let Some(label) = &state.metadata.transition_label { 271 | writeln!(w, "{} --> {} : {}", src, label, t)? 272 | } else { 273 | writeln!(w, "{} --> {} : {}", src, s, t)? 274 | } 275 | } 276 | }, 277 | Node::Decision(decision) => { 278 | for s in decision { 279 | if let Some(state) = &s.state { 280 | if let Some(label) = &s.metadata.transition_label { 281 | writeln!(w, "{} --> {} : {}", src, state, label)? 282 | } else { 283 | writeln!(w, "{} --> {}", src, state)? 284 | } 285 | } else if let Some(label) = &s.metadata.transition_label { 286 | writeln!(w, "{} --> [*] : {}", src, label)? 287 | } else { 288 | writeln!(w, "{} --> [*]", src)? 289 | } 290 | } 291 | } 292 | } 293 | 294 | Ok(()) 295 | } 296 | } 297 | } 298 | 299 | /// The DOT format module, containing the marker type and implementation for the respective export trait. 300 | #[cfg(feature = "export-dot")] 301 | pub mod dot { 302 | use super::{Export, Result}; 303 | use crate::igraph::{IntermediateGraph, Node, Transition}; 304 | use std::{ 305 | fmt::{Debug, Display}, 306 | hash::Hash, 307 | }; 308 | 309 | fn var_or_default(var_name: &str, var_default: &str) -> String { 310 | ::std::env::var_os(var_name) 311 | .and_then(|s| s.into_string().ok()) 312 | .unwrap_or_else(|| var_default.to_string()) 313 | } 314 | 315 | #[derive(Clone, Copy)] 316 | pub struct Dot; 317 | 318 | impl super::Format for Dot { 319 | fn file_extension<'a>() -> &'a str { 320 | "dot" 321 | } 322 | } 323 | 324 | const DOT_SPECIAL_NODE: &str = 325 | r#"label="", fillcolor=black, fixedsize=true, height=0.25, style=filled"#; 326 | 327 | impl Export for IntermediateGraph 328 | where 329 | S: Hash + Eq + Debug + Clone + Display, 330 | T: Hash + Eq + Debug + Clone + Display, 331 | { 332 | fn export(&self, w: &mut W, f: Dot) -> Result { 333 | writeln!(w, "digraph Automata {{")?; 334 | 335 | w.write_fmt(format_args!( 336 | " graph [pad=\"{}\", nodesep=\"{}\", ranksep=\"{}\"];\n", 337 | var_or_default("DOT_PAD", "0.25"), 338 | var_or_default("DOT_NODESEP", "0.75"), 339 | var_or_default("DOT_RANKSEP", "1"), 340 | ))?; 341 | 342 | writeln!(w, " _initial_ [{}, shape=circle];", DOT_SPECIAL_NODE)?; 343 | writeln!(w, " _final_ [{}, shape=doublecircle];", DOT_SPECIAL_NODE)?; 344 | 345 | for s in &self.choices { 346 | writeln!(w, " {} [shape=diamond];", s)? 347 | } 348 | for (src, v) in &self.delta { 349 | for (t, dst) in v { 350 | (src, t, dst).export(w, f)? 351 | } 352 | } 353 | 354 | write!(w, "}}")?; 355 | Ok(()) 356 | } 357 | } 358 | 359 | impl Export for (&Option, &Transition, &Node) 360 | where 361 | S: Hash + Eq + Debug + Clone + Display, 362 | T: Hash + Eq + Debug + Clone + Display, 363 | { 364 | fn export(&self, w: &mut W, _: Dot) -> Result { 365 | let src = self.0; 366 | let t = &self.1.transition; 367 | let dst = self.2; 368 | 369 | if let Some(src) = src { 370 | match dst { 371 | Node::State(state) => match &state.state { 372 | None => writeln!(w, " {} -> _final_ [label=\"{}\"];", src, t)?, 373 | Some(s) => { 374 | // if there is a transition label, use that instead of the existing label 375 | if let Some(label) = &state.metadata.transition_label { 376 | writeln!(w, " {} -> {} [label=\"{}\"];", src, label, t)? 377 | } else { 378 | writeln!(w, " {} -> {} [label=\"{}\"];", src, s, t)? 379 | } 380 | } 381 | }, 382 | Node::Decision(decision) => { 383 | for s in decision { 384 | if let Some(state) = &s.state { 385 | if let Some(label) = &s.metadata.transition_label { 386 | writeln!(w, " {} -> {} [label=\"{}\"];", src, state, label)? 387 | } else { 388 | writeln!(w, " {} -> {};", src, state)? 389 | } 390 | } else if let Some(label) = &s.metadata.transition_label { 391 | writeln!(w, " {} -> _final_ [label=\"{}\"];", src, label)? 392 | } else { 393 | writeln!(w, " {} -> _final_;", src)? 394 | } 395 | } 396 | } 397 | } 398 | } else { 399 | match dst { 400 | Node::State(state) => match &state.state { 401 | None => unreachable!("invalid transition: None -> None"), 402 | Some(s) => { 403 | // if there is a transition label, use that instead of the existing label 404 | if let Some(label) = &state.metadata.transition_label { 405 | writeln!(w, " _initial_ -> {} [label=\"{}\"];", label, t)? 406 | } else { 407 | writeln!(w, " _initial_ -> {} [label=\"{}\"];", s, t)? 408 | } 409 | } 410 | }, 411 | Node::Decision(_) => { 412 | // NOTE: unsure about this 413 | unreachable!("invalid transition: None -> Decision") 414 | } 415 | } 416 | } 417 | 418 | Ok(()) 419 | } 420 | } 421 | } 422 | 423 | #[cfg(any( 424 | feature = "export-dot", 425 | feature = "export-plantuml", 426 | feature = "export-mermaid" 427 | ))] 428 | use { 429 | super::IntermediateGraph, 430 | std::{ 431 | env, 432 | fs::{create_dir_all, File}, 433 | path::Path, 434 | }, 435 | syn::Ident, 436 | }; 437 | 438 | #[cfg(any( 439 | feature = "export-dot", 440 | feature = "export-plantuml", 441 | feature = "export-mermaid" 442 | ))] 443 | pub(crate) fn export( 444 | file_name: &str, 445 | igraph: &IntermediateGraph, 446 | format: F, 447 | ) -> Result 448 | where 449 | IntermediateGraph: Export, 450 | { 451 | let folder = { 452 | let cwd = env::current_dir()?; 453 | env::var_os("EXPORT_FOLDER").map_or_else(|| cwd, |dir| Path::new(&dir).to_path_buf()) 454 | }; 455 | if !folder.exists() { 456 | create_dir_all(&folder)?; 457 | } 458 | 459 | let mut f = File::create(folder.join(file_name).with_extension(F::file_extension()))?; 460 | igraph.export(&mut f, format)?; 461 | Ok(()) 462 | } 463 | -------------------------------------------------------------------------------- /typestate-proc-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod igraph; 2 | mod visitors; 3 | 4 | use crate::{ 5 | igraph::{ 6 | validate::{GenericAutomaton, NonProductiveStates, NonUsefulStates, Validate}, 7 | IntermediateGraph, 8 | }, 9 | visitors::state::AUTOMATA_ATTR_IDENT, 10 | }; 11 | use darling::FromMeta; 12 | use proc_macro::TokenStream; 13 | use proc_macro2::{Span, TokenStream as TokenStream2}; 14 | use quote::{format_ident, ToTokens}; 15 | use std::{ 16 | collections::{HashMap, HashSet}, 17 | hash::Hash, 18 | }; 19 | use syn::{ 20 | parse_macro_input, Attribute, AttributeArgs, Error, Ident, Item, ItemEnum, ItemMod, ItemStruct, 21 | ItemTrait, Variant 22 | }; 23 | 24 | const CRATE_NAME: &str = "typestate_proc_macro"; 25 | const GENERATED_ATTR_IDENT: &str = "generated"; 26 | 27 | #[doc(hidden)] 28 | #[proc_macro_attribute] 29 | pub fn generated(_: TokenStream, input: TokenStream) -> TokenStream { 30 | input 31 | } 32 | 33 | fn generated_attr() -> TokenStream2 { 34 | let crate_ident = ::quote::format_ident!("{}", crate::CRATE_NAME); 35 | let generated_ident = ::quote::format_ident!("{}", crate::GENERATED_ATTR_IDENT); 36 | ::quote::quote!(#[::typestate::#crate_ident::#generated_ident]) 37 | } 38 | 39 | trait IsGeneratedAttr { 40 | fn is_generated_attr(&self) -> bool; 41 | } 42 | 43 | impl IsGeneratedAttr for syn::Attribute { 44 | fn is_generated_attr(&self) -> bool { 45 | let segments = &self.path.segments.iter().collect::>(); 46 | if segments.len() == 2 { 47 | // support ::typestate_proc_macro::generated 48 | segments[0].ident == CRATE_NAME && segments[1].ident == GENERATED_ATTR_IDENT 49 | } else if segments.len() == 3 { 50 | // support ::typestate::typestate_proc_macro::generated 51 | segments[0].ident == "typestate" 52 | && segments[1].ident == CRATE_NAME 53 | && segments[2].ident == GENERATED_ATTR_IDENT 54 | } else { 55 | false 56 | } 57 | } 58 | } 59 | 60 | 61 | /// See the module documentation for a full featured tutorial on how to use `#[typestate]`. 62 | #[allow(clippy::too_many_lines)] // TODO handle this 63 | #[proc_macro_attribute] 64 | pub fn typestate(args: TokenStream, input: TokenStream) -> TokenStream { 65 | macro_rules! bail_if_any { 66 | ( $errors:expr ) => { 67 | match $errors { 68 | errors => { 69 | if !errors.is_empty() { 70 | return errors.into_compile_error().into(); 71 | } 72 | } 73 | } 74 | }; 75 | } 76 | 77 | // Parse attribute arguments 78 | let attr_args: AttributeArgs = parse_macro_input!(args); 79 | 80 | let args = match MacroAttributeArguments::from_list(&attr_args) { 81 | Ok(v) => v, 82 | Err(e) => { 83 | return TokenStream::from(e.write_errors()); 84 | } 85 | }; 86 | 87 | // parse the input as a mod 88 | let mut module: ItemMod = parse_macro_input!(input); 89 | 90 | // eprint!("{:#?}", module); 91 | 92 | if let Some((_, content)) = &module.content { 93 | if content.is_empty() { 94 | return quote::quote!(const _: () = { 95 | #[deprecated(note = "Empty module detected.")] 96 | #[allow(nonstandard_style)] 97 | enum compile_warning {} 98 | 99 | let _: compile_warning; 100 | };).into_token_stream().into(); 101 | } 102 | } 103 | 104 | let mut state_machine_info = StateMachineInfo::new(); 105 | 106 | bail_if_any!(visitors::state::visit_states( 107 | &mut module, 108 | &mut state_machine_info, 109 | if !args.state_constructors.is_empty() { 110 | Some(format_ident!("{}", args.state_constructors)) 111 | } else { 112 | None 113 | } 114 | )); 115 | 116 | // Visit non-deterministic transitions 117 | bail_if_any!(visitors::decision::visit_non_deterministic( 118 | &mut module, 119 | &mut state_machine_info 120 | )); 121 | 122 | // Visit transitions 123 | bail_if_any!(visitors::transition::visit_transitions( 124 | &mut module, 125 | &mut state_machine_info 126 | )); 127 | 128 | #[cfg(any( 129 | feature = "export-dot", 130 | feature = "export-plantuml", 131 | feature = "export-mermaid" 132 | ))] 133 | export_diagram_files(&state_machine_info); 134 | 135 | #[cfg(feature = "docs-mermaid")] 136 | { 137 | use igraph::export::{mermaid::Mermaid, Export}; 138 | // NOTE: hacky bypass to avoid rewriting the ::Write 139 | let mut f = Vec::::new(); 140 | 141 | // TODO: handle the unwrap 142 | state_machine_info 143 | .intermediate_automaton 144 | .export(&mut f, Mermaid) 145 | .unwrap(); 146 | 147 | let doc_string = String::from_utf8(f).unwrap(); 148 | let doc_string_iter = doc_string.split('\n').filter(|s| !s.is_empty()); 149 | 150 | module = ::syn::parse_quote!( 151 | #[cfg_attr(doc, ::typestate::__private__::aquamarine::aquamarine)] 152 | #[doc = "```mermaid"] 153 | #(#[doc = #doc_string_iter])* 154 | #[doc = "```"] 155 | #module 156 | ); 157 | } 158 | 159 | // TODO: deal with the unwrap later 160 | let automata_ident = state_machine_info.automaton_ident.unwrap(); 161 | 162 | let ga = GenericAutomaton::from(state_machine_info.intermediate_automaton); 163 | let errors: Vec = ga 164 | .validate(NonProductiveStates) 165 | .into_iter() 166 | .map(|ident| TypestateError::NonProductiveState(ident).into()) 167 | .collect(); 168 | bail_if_any!(errors); 169 | 170 | let errors: Vec = ga 171 | .validate(NonUsefulStates) 172 | .into_iter() 173 | .map(|ident| TypestateError::NonUsefulState(ident).into()) 174 | .collect(); 175 | bail_if_any!(errors); 176 | 177 | let states = ga.states.iter().collect::>(); 178 | 179 | // match the `Option` 180 | let mut enumerate_tokens = if !args.enumerate.is_empty() { 181 | let mut res: Vec = vec![]; 182 | let enum_ident = &format_ident!("{}", &args.enumerate); 183 | res.expand_enumerate(&automata_ident, enum_ident, &states); 184 | res 185 | } else { 186 | vec![] 187 | }; 188 | 189 | if let Some((_, v)) = &mut module.content { 190 | v.append(&mut enumerate_tokens); 191 | } 192 | 193 | // if errors do not exist, return the token stream 194 | module.into_token_stream().into() 195 | } 196 | 197 | #[cfg(any( 198 | feature = "export-dot", 199 | feature = "export-plantuml", 200 | feature = "export-mermaid" 201 | ))] 202 | fn export_diagram_files(state_machine_info: &StateMachineInfo) { 203 | use crate::igraph::export; 204 | 205 | #[cfg(feature = "export-dot")] 206 | { 207 | use igraph::export::dot::Dot; 208 | export::export( 209 | &state_machine_info 210 | .automaton_ident 211 | .clone() 212 | .unwrap() 213 | .ident 214 | .to_string(), 215 | &state_machine_info.intermediate_automaton, 216 | Dot, 217 | ) 218 | .unwrap(); 219 | } 220 | 221 | #[cfg(feature = "export-plantuml")] 222 | { 223 | use igraph::export::plantuml::PlantUml; 224 | export::export( 225 | &state_machine_info 226 | .automaton_ident 227 | .clone() 228 | .unwrap() 229 | .ident 230 | .to_string(), 231 | &state_machine_info.intermediate_automaton, 232 | PlantUml, 233 | ) 234 | .unwrap(); 235 | } 236 | 237 | #[cfg(feature = "export-mermaid")] 238 | { 239 | use igraph::export::mermaid::Mermaid; 240 | export::export( 241 | &state_machine_info 242 | .automaton_ident 243 | .clone() 244 | .unwrap() 245 | .ident 246 | .to_string(), 247 | &state_machine_info.intermediate_automaton, 248 | Mermaid, 249 | ) 250 | .unwrap(); 251 | } 252 | } 253 | 254 | trait ExpandEnumerate { 255 | fn expand_enumerate(&mut self, automata: &ItemStruct, automata_enum: &Ident, states: &[&Ident]); 256 | /// Expand the [`ToString`] implentation for enumeration. 257 | /// Only available with `std` and when `enumerate` is used. 258 | fn expand_to_string(&mut self, automata_enum: &Ident, states: &[&Ident]); 259 | /// Expand the enumeration containing all states. 260 | /// Only available when `enumerate` is used. 261 | fn expand_enum(&mut self, automata: &ItemStruct, automata_enum: &Ident, states: &[&Ident]); 262 | /// Expand the [`From`] implementation to convert from states to enumeration and back. 263 | /// Only available when `enumerate` is used. 264 | fn expand_from(&mut self, automata: &ItemStruct, automata_enum: &Ident, states: &[&Ident]); 265 | } 266 | 267 | impl ExpandEnumerate for Vec { 268 | fn expand_enumerate( 269 | &mut self, 270 | automata: &ItemStruct, 271 | automata_enum: &Ident, 272 | states: &[&Ident], 273 | ) { 274 | // expand the enumeration 275 | self.expand_enum(automata, automata_enum, states); 276 | 277 | // expand conversion traits: `From` 278 | self.expand_from(automata, automata_enum, states); 279 | 280 | // if std is present, generate `to_string` implementations 281 | #[cfg(feature = "std")] 282 | self.expand_to_string(automata_enum, states); 283 | } 284 | 285 | fn expand_to_string(&mut self, automata_enum: &Ident, states: &[&Ident]) { 286 | let to_string = ::quote::quote! { 287 | impl ::std::string::ToString for #automata_enum { 288 | fn to_string(&self) -> String { 289 | match &self { 290 | #(#automata_enum::#states(_) => stringify!(#states).to_string(),)* 291 | } 292 | } 293 | } 294 | }; 295 | self.push(::syn::parse_quote!(#to_string)); 296 | } 297 | 298 | fn expand_from(&mut self, automata: &ItemStruct, automata_enum: &Ident, states: &[&Ident]) { 299 | let automata_ident = &automata.ident; 300 | let from_tokens = states.iter().map(|state| { 301 | ::syn::parse_quote! { 302 | impl ::core::convert::From<#automata_ident<#state>> for #automata_enum { 303 | fn from(value: #automata_ident<#state>) -> Self { 304 | Self::#state(value) 305 | } 306 | } 307 | } 308 | }); 309 | self.extend(from_tokens); 310 | } 311 | 312 | fn expand_enum(&mut self, automata: &ItemStruct, automata_enum: &Ident, states: &[&Ident]) { 313 | let automata_ident = &automata.ident; 314 | let automata_vis = &automata.vis; 315 | self.push(::syn::parse_quote! { 316 | #automata_vis enum #automata_enum { 317 | #(#states(#automata_ident<#states>),)* 318 | } 319 | }); 320 | } 321 | } 322 | 323 | #[derive(Debug, FromMeta)] 324 | struct MacroAttributeArguments { 325 | /// Optional arguments. 326 | #[darling(default)] 327 | enumerate: String, 328 | #[darling(default)] 329 | state_constructors: String, 330 | } 331 | 332 | /// A value to `proc_macro2::TokenStream2` conversion. 333 | /// More precisely into 334 | trait IntoCompileError { 335 | fn into_compile_error(self) -> TokenStream2; 336 | } 337 | 338 | impl IntoCompileError for Vec { 339 | fn into_compile_error(mut self) -> TokenStream2 { 340 | if !self.is_empty() { 341 | // if errors exist, return all errors 342 | let fst_err = self.swap_remove(0); 343 | return self 344 | .into_iter() 345 | .fold(fst_err, |mut all, curr| { 346 | all.combine(curr); 347 | all 348 | }) 349 | .to_compile_error(); 350 | } 351 | TokenStream2::new() 352 | } 353 | } 354 | 355 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 356 | struct Transition { 357 | source: Ident, 358 | destination: Ident, 359 | symbol: Ident, 360 | } 361 | 362 | impl Transition { 363 | fn new(source: Ident, destination: Ident, symbol: Ident) -> Self { 364 | Self { 365 | source, 366 | destination, 367 | symbol, 368 | } 369 | } 370 | } 371 | 372 | /// Extracted information from the states 373 | #[derive(Debug, Clone)] 374 | struct StateMachineInfo { 375 | /// Main structure (aka Automata ?) 376 | automaton_ident: Option, // late init 377 | 378 | /// Deterministic states (`struct`s) 379 | det_states: HashMap, 380 | 381 | /// Non-deterministic transitions (`enum`s) 382 | non_det_transitions: HashMap, 383 | 384 | /// Non-deterministic transitions present in this collection are used. 385 | /// This is just so we can throw an error on unused enumerations. 386 | used_non_det_transitions: HashSet, 387 | 388 | /// Set of transitions. 389 | /// Extracted from functions with a signature like `(State) -> State`. 390 | transitions: HashSet, 391 | 392 | /// Set of initial states. 393 | /// Extracted from functions with a signature like `() -> State`. 394 | initial_states: HashMap>, 395 | 396 | /// Set of final states. 397 | /// Extracted from functions with a signature like `(State) -> ()`. 398 | final_states: HashMap>, 399 | 400 | pub intermediate_automaton: IntermediateGraph, 401 | } 402 | 403 | impl StateMachineInfo { 404 | /// Construct a new [`StateMachineInfo`]. 405 | fn new() -> Self { 406 | Self { 407 | automaton_ident: None, 408 | intermediate_automaton: IntermediateGraph::new(), 409 | det_states: HashMap::new(), 410 | non_det_transitions: HashMap::new(), 411 | used_non_det_transitions: HashSet::new(), 412 | transitions: HashSet::new(), 413 | initial_states: HashMap::new(), 414 | final_states: HashMap::new(), 415 | } 416 | } 417 | 418 | /// Add a generic state to the [`StateMachineInfo`] 419 | fn add_state(&mut self, state: Item) { 420 | match state { 421 | Item::Struct(item_struct) => { 422 | self.det_states 423 | .insert(item_struct.ident.clone(), item_struct); 424 | } 425 | Item::Enum(item_enum) => { 426 | self.non_det_transitions 427 | .insert(item_enum.ident.clone(), item_enum); 428 | } 429 | _ => unreachable!("invalid state"), 430 | } 431 | } 432 | 433 | /// Return the main state identifier. 434 | /// This is an utility function. 435 | // maybe the unwrap could be converted into a check 436 | // if none -> comp time error 437 | fn get_automaton_ident(&self) -> &Ident { 438 | &self.automaton_ident.as_ref().unwrap().ident 439 | } 440 | 441 | /// Check for missing initial or final states. 442 | fn check_missing(&self) -> Vec { 443 | let mut errors = vec![]; 444 | if self.initial_states.is_empty() { 445 | errors.push(TypestateError::MissingInitialState.into()); 446 | } 447 | if self.final_states.is_empty() { 448 | errors.push(TypestateError::MissingFinalState.into()); 449 | } 450 | errors 451 | } 452 | 453 | /// Check for unused non-deterministic transitions 454 | fn check_unused_non_det_transitions(&self) -> Vec { 455 | self.non_det_transitions 456 | .keys() 457 | .collect::>() 458 | .difference( 459 | // HACK 460 | &self.used_non_det_transitions.iter().collect::>(), 461 | ) 462 | .collect::>() 463 | .iter() 464 | .map(|i| TypestateError::UnusedTransition((***i).clone()).into()) 465 | .collect::>() 466 | } 467 | 468 | fn insert_initial(&mut self, state: Ident, transition: Ident) { 469 | if let Some(transitions) = self.initial_states.get_mut(&state) { 470 | transitions.insert(transition); 471 | } else { 472 | let mut transitions = HashSet::new(); 473 | transitions.insert(transition); 474 | self.initial_states.insert(state, transitions); 475 | } 476 | } 477 | 478 | fn insert_final(&mut self, state: Ident, transition: Ident) { 479 | if let Some(transitions) = self.final_states.get_mut(&state) { 480 | transitions.insert(transition); 481 | } else { 482 | let mut transitions = HashSet::new(); 483 | transitions.insert(transition); 484 | self.final_states.insert(state, transitions); 485 | } 486 | } 487 | } 488 | 489 | enum TypestateError { 490 | MissingAutomata, 491 | NonProductiveState(Ident), 492 | NonUsefulState(Ident), 493 | MissingInitialState, 494 | MissingFinalState, 495 | ConflictingAttributes(Attribute), 496 | DuplicateAttributes(Attribute), 497 | AutomataRedefinition(ItemStruct), 498 | UndeclaredVariant(Ident), 499 | UnsupportedVariant(Variant), 500 | UnknownState(Ident), 501 | InvalidAssocFuntions(ItemTrait), 502 | UnsupportedStruct(ItemStruct), 503 | UnsupportedState(Ident), 504 | UnusedTransition(Ident), 505 | } 506 | 507 | impl From for syn::Error { 508 | fn from(err: TypestateError) -> Self { 509 | match err { 510 | TypestateError::MissingAutomata => Error::new(Span::call_site(), format!("Missing `#[{}]` struct.", AUTOMATA_ATTR_IDENT)), 511 | TypestateError::NonProductiveState(ident) => Error::new_spanned(ident, "Non-productive state. For a state to be productive, a path from the state to a final state is required to exist."), 512 | TypestateError::NonUsefulState(ident) => Error::new_spanned(ident, "Non-useful state. For a state to be useful it must first be productive and a path from initial state to the state is required to exist."), 513 | TypestateError::MissingInitialState => Error::new(Span::call_site(), "Missing initial state. To declare an initial state you can use a function with signature like `fn f() -> T` where `T` is a declared state."), 514 | TypestateError::MissingFinalState => Error::new(Span::call_site(), "Missing final state. To declare a final state you can use a function with signature like `fn f(self) -> T` where `T` is not a declared state."), 515 | TypestateError::ConflictingAttributes(attr) => Error::new_spanned(attr, "Conflicting attributes are declared."), // TODO add which attributes are conflicting 516 | TypestateError::DuplicateAttributes(attr) => Error::new_spanned(attr, "Duplicate attribute."), 517 | TypestateError::AutomataRedefinition(item_struct) => Error::new_spanned(item_struct, format!("`#[{}]` redefinition here.", AUTOMATA_ATTR_IDENT)), 518 | TypestateError::UndeclaredVariant(ident) => Error::new_spanned(&ident, "`enum` variant is not a declared state."), 519 | TypestateError::UnsupportedVariant(variant) => Error::new_spanned(&variant, "Only unit (C-like) `enum` variants are supported."), 520 | TypestateError::UnknownState(ident) => Error::new_spanned(&ident, format!("`{}` is not a declared state.", ident)), 521 | TypestateError::InvalidAssocFuntions(item_trait) => Error::new_spanned(&item_trait, "Non-deterministic states cannot have associated functions"), 522 | TypestateError::UnsupportedStruct(item_struct) => Error::new_spanned(&item_struct, "Tuple structures are not supported."), 523 | TypestateError::UnsupportedState(ident) => Error::new_spanned(&ident, "`enum` variants cannot refer to other `enum`s."), 524 | TypestateError::UnusedTransition(ident) => Error::new_spanned(&ident, "Unused transitions are not allowed."), 525 | } 526 | } 527 | } 528 | 529 | impl IntoCompileError for TypestateError { 530 | fn into_compile_error(self) -> TokenStream2 { 531 | let err: syn::Error = self.into(); 532 | err.to_compile_error() 533 | } 534 | } 535 | --------------------------------------------------------------------------------