├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── cli ├── Cargo.toml └── src │ └── bin │ └── fsmentry.rs ├── core ├── Cargo.toml ├── src │ ├── args.rs │ ├── dsl.rs │ ├── graph.rs │ └── lib.rs └── tests │ ├── check_compile │ ├── full.rs │ └── simple.rs │ └── test.rs └── src ├── example.dsl ├── example.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell", 76 | "windows-sys", 77 | ] 78 | 79 | [[package]] 80 | name = "anyhow" 81 | version = "1.0.97" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 84 | 85 | [[package]] 86 | name = "assert_cmd" 87 | version = "2.0.16" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 90 | dependencies = [ 91 | "anstyle", 92 | "bstr", 93 | "doc-comment", 94 | "libc", 95 | "predicates", 96 | "predicates-core", 97 | "predicates-tree", 98 | "wait-timeout", 99 | ] 100 | 101 | [[package]] 102 | name = "backtrace" 103 | version = "0.3.74" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 106 | dependencies = [ 107 | "addr2line", 108 | "cfg-if", 109 | "libc", 110 | "miniz_oxide", 111 | "object", 112 | "rustc-demangle", 113 | "windows-targets", 114 | ] 115 | 116 | [[package]] 117 | name = "backtrace-ext" 118 | version = "0.2.1" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" 121 | dependencies = [ 122 | "backtrace", 123 | ] 124 | 125 | [[package]] 126 | name = "bitflags" 127 | version = "2.9.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 130 | 131 | [[package]] 132 | name = "bstr" 133 | version = "1.10.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" 136 | dependencies = [ 137 | "memchr", 138 | "regex-automata", 139 | "serde", 140 | ] 141 | 142 | [[package]] 143 | name = "cfg-if" 144 | version = "1.0.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 147 | 148 | [[package]] 149 | name = "clap" 150 | version = "4.5.32" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" 153 | dependencies = [ 154 | "clap_builder", 155 | "clap_derive", 156 | ] 157 | 158 | [[package]] 159 | name = "clap_builder" 160 | version = "4.5.32" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" 163 | dependencies = [ 164 | "anstream", 165 | "anstyle", 166 | "clap_lex", 167 | "strsim", 168 | ] 169 | 170 | [[package]] 171 | name = "clap_derive" 172 | version = "4.5.32" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 175 | dependencies = [ 176 | "heck", 177 | "proc-macro2", 178 | "quote", 179 | "syn", 180 | ] 181 | 182 | [[package]] 183 | name = "clap_lex" 184 | version = "0.7.4" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 187 | 188 | [[package]] 189 | name = "colorchoice" 190 | version = "1.0.3" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 193 | 194 | [[package]] 195 | name = "derive-quickcheck-arbitrary" 196 | version = "0.1.3" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "697d85c38ac8f4dad3129d38d0d40060a98fd2557bfaf0bc8c071ecfce884ce5" 199 | dependencies = [ 200 | "proc-macro2", 201 | "quote", 202 | "structmeta", 203 | "syn", 204 | ] 205 | 206 | [[package]] 207 | name = "difflib" 208 | version = "0.4.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 211 | 212 | [[package]] 213 | name = "dissimilar" 214 | version = "1.0.9" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" 217 | 218 | [[package]] 219 | name = "doc-comment" 220 | version = "0.3.3" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 223 | 224 | [[package]] 225 | name = "env_logger" 226 | version = "0.8.4" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" 229 | dependencies = [ 230 | "log", 231 | "regex", 232 | ] 233 | 234 | [[package]] 235 | name = "equivalent" 236 | version = "1.0.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 239 | 240 | [[package]] 241 | name = "errno" 242 | version = "0.3.10" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 245 | dependencies = [ 246 | "libc", 247 | "windows-sys", 248 | ] 249 | 250 | [[package]] 251 | name = "expect-test" 252 | version = "1.5.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" 255 | dependencies = [ 256 | "dissimilar", 257 | "once_cell", 258 | ] 259 | 260 | [[package]] 261 | name = "fsmentry" 262 | version = "0.4.0" 263 | dependencies = [ 264 | "assert_cmd", 265 | "derive-quickcheck-arbitrary", 266 | "expect-test", 267 | "fsmentry-core", 268 | "prettyplease", 269 | "proc-macro2", 270 | "quickcheck", 271 | "quote", 272 | "syn", 273 | "trybuild", 274 | ] 275 | 276 | [[package]] 277 | name = "fsmentry-cli" 278 | version = "0.4.0" 279 | dependencies = [ 280 | "anyhow", 281 | "clap", 282 | "fsmentry-core", 283 | "prettyplease", 284 | "quote", 285 | "syn", 286 | "syn-miette", 287 | ] 288 | 289 | [[package]] 290 | name = "fsmentry-core" 291 | version = "0.4.0" 292 | dependencies = [ 293 | "expect-test", 294 | "prettyplease", 295 | "proc-macro2", 296 | "quote", 297 | "syn", 298 | ] 299 | 300 | [[package]] 301 | name = "getrandom" 302 | version = "0.2.15" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 305 | dependencies = [ 306 | "cfg-if", 307 | "libc", 308 | "wasi", 309 | ] 310 | 311 | [[package]] 312 | name = "gimli" 313 | version = "0.31.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 316 | 317 | [[package]] 318 | name = "glob" 319 | version = "0.3.1" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 322 | 323 | [[package]] 324 | name = "hashbrown" 325 | version = "0.15.1" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" 328 | 329 | [[package]] 330 | name = "heck" 331 | version = "0.5.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 334 | 335 | [[package]] 336 | name = "indexmap" 337 | version = "2.6.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 340 | dependencies = [ 341 | "equivalent", 342 | "hashbrown", 343 | ] 344 | 345 | [[package]] 346 | name = "is_ci" 347 | version = "1.2.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" 350 | 351 | [[package]] 352 | name = "is_terminal_polyfill" 353 | version = "1.70.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 356 | 357 | [[package]] 358 | name = "itoa" 359 | version = "1.0.11" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 362 | 363 | [[package]] 364 | name = "libc" 365 | version = "0.2.162" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" 368 | 369 | [[package]] 370 | name = "linux-raw-sys" 371 | version = "0.4.15" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 374 | 375 | [[package]] 376 | name = "log" 377 | version = "0.4.22" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 380 | 381 | [[package]] 382 | name = "memchr" 383 | version = "2.7.4" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 386 | 387 | [[package]] 388 | name = "miette" 389 | version = "7.5.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" 392 | dependencies = [ 393 | "backtrace", 394 | "backtrace-ext", 395 | "cfg-if", 396 | "miette-derive", 397 | "owo-colors", 398 | "supports-color", 399 | "supports-hyperlinks", 400 | "supports-unicode", 401 | "terminal_size", 402 | "textwrap", 403 | "thiserror", 404 | "unicode-width 0.1.14", 405 | ] 406 | 407 | [[package]] 408 | name = "miette-derive" 409 | version = "7.5.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" 412 | dependencies = [ 413 | "proc-macro2", 414 | "quote", 415 | "syn", 416 | ] 417 | 418 | [[package]] 419 | name = "miniz_oxide" 420 | version = "0.8.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 423 | dependencies = [ 424 | "adler2", 425 | ] 426 | 427 | [[package]] 428 | name = "object" 429 | version = "0.36.7" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 432 | dependencies = [ 433 | "memchr", 434 | ] 435 | 436 | [[package]] 437 | name = "once_cell" 438 | version = "1.20.3" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 441 | 442 | [[package]] 443 | name = "owo-colors" 444 | version = "4.2.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" 447 | 448 | [[package]] 449 | name = "predicates" 450 | version = "3.1.2" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" 453 | dependencies = [ 454 | "anstyle", 455 | "difflib", 456 | "predicates-core", 457 | ] 458 | 459 | [[package]] 460 | name = "predicates-core" 461 | version = "1.0.8" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" 464 | 465 | [[package]] 466 | name = "predicates-tree" 467 | version = "1.0.11" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" 470 | dependencies = [ 471 | "predicates-core", 472 | "termtree", 473 | ] 474 | 475 | [[package]] 476 | name = "prettyplease" 477 | version = "0.2.31" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" 480 | dependencies = [ 481 | "proc-macro2", 482 | "syn", 483 | ] 484 | 485 | [[package]] 486 | name = "proc-macro2" 487 | version = "1.0.94" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 490 | dependencies = [ 491 | "unicode-ident", 492 | ] 493 | 494 | [[package]] 495 | name = "quickcheck" 496 | version = "1.0.3" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" 499 | dependencies = [ 500 | "env_logger", 501 | "log", 502 | "rand", 503 | ] 504 | 505 | [[package]] 506 | name = "quote" 507 | version = "1.0.40" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 510 | dependencies = [ 511 | "proc-macro2", 512 | ] 513 | 514 | [[package]] 515 | name = "rand" 516 | version = "0.8.5" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 519 | dependencies = [ 520 | "rand_core", 521 | ] 522 | 523 | [[package]] 524 | name = "rand_core" 525 | version = "0.6.4" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 528 | dependencies = [ 529 | "getrandom", 530 | ] 531 | 532 | [[package]] 533 | name = "regex" 534 | version = "1.11.1" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 537 | dependencies = [ 538 | "aho-corasick", 539 | "memchr", 540 | "regex-automata", 541 | "regex-syntax", 542 | ] 543 | 544 | [[package]] 545 | name = "regex-automata" 546 | version = "0.4.9" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 549 | dependencies = [ 550 | "aho-corasick", 551 | "memchr", 552 | "regex-syntax", 553 | ] 554 | 555 | [[package]] 556 | name = "regex-syntax" 557 | version = "0.8.5" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 560 | 561 | [[package]] 562 | name = "rustc-demangle" 563 | version = "0.1.24" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 566 | 567 | [[package]] 568 | name = "rustix" 569 | version = "0.38.44" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 572 | dependencies = [ 573 | "bitflags", 574 | "errno", 575 | "libc", 576 | "linux-raw-sys", 577 | "windows-sys", 578 | ] 579 | 580 | [[package]] 581 | name = "ryu" 582 | version = "1.0.18" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 585 | 586 | [[package]] 587 | name = "serde" 588 | version = "1.0.215" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" 591 | dependencies = [ 592 | "serde_derive", 593 | ] 594 | 595 | [[package]] 596 | name = "serde_derive" 597 | version = "1.0.215" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" 600 | dependencies = [ 601 | "proc-macro2", 602 | "quote", 603 | "syn", 604 | ] 605 | 606 | [[package]] 607 | name = "serde_json" 608 | version = "1.0.132" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 611 | dependencies = [ 612 | "itoa", 613 | "memchr", 614 | "ryu", 615 | "serde", 616 | ] 617 | 618 | [[package]] 619 | name = "serde_spanned" 620 | version = "0.6.8" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 623 | dependencies = [ 624 | "serde", 625 | ] 626 | 627 | [[package]] 628 | name = "strsim" 629 | version = "0.11.1" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 632 | 633 | [[package]] 634 | name = "structmeta" 635 | version = "0.2.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d" 638 | dependencies = [ 639 | "proc-macro2", 640 | "quote", 641 | "structmeta-derive", 642 | "syn", 643 | ] 644 | 645 | [[package]] 646 | name = "structmeta-derive" 647 | version = "0.2.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" 650 | dependencies = [ 651 | "proc-macro2", 652 | "quote", 653 | "syn", 654 | ] 655 | 656 | [[package]] 657 | name = "supports-color" 658 | version = "3.0.2" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" 661 | dependencies = [ 662 | "is_ci", 663 | ] 664 | 665 | [[package]] 666 | name = "supports-hyperlinks" 667 | version = "3.1.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" 670 | 671 | [[package]] 672 | name = "supports-unicode" 673 | version = "3.0.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" 676 | 677 | [[package]] 678 | name = "syn" 679 | version = "2.0.100" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 682 | dependencies = [ 683 | "proc-macro2", 684 | "quote", 685 | "unicode-ident", 686 | ] 687 | 688 | [[package]] 689 | name = "syn-miette" 690 | version = "0.3.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "dd1a2bfae2df81406f8d21baa0253d34ddd0ddafcd1e7aa12aa24279bb76a24b" 693 | dependencies = [ 694 | "miette", 695 | "proc-macro2", 696 | "syn", 697 | ] 698 | 699 | [[package]] 700 | name = "target-triple" 701 | version = "0.1.3" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" 704 | 705 | [[package]] 706 | name = "termcolor" 707 | version = "1.4.1" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 710 | dependencies = [ 711 | "winapi-util", 712 | ] 713 | 714 | [[package]] 715 | name = "terminal_size" 716 | version = "0.4.1" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" 719 | dependencies = [ 720 | "rustix", 721 | "windows-sys", 722 | ] 723 | 724 | [[package]] 725 | name = "termtree" 726 | version = "0.4.1" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 729 | 730 | [[package]] 731 | name = "textwrap" 732 | version = "0.16.2" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 735 | dependencies = [ 736 | "unicode-linebreak", 737 | "unicode-width 0.2.0", 738 | ] 739 | 740 | [[package]] 741 | name = "thiserror" 742 | version = "1.0.69" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 745 | dependencies = [ 746 | "thiserror-impl", 747 | ] 748 | 749 | [[package]] 750 | name = "thiserror-impl" 751 | version = "1.0.69" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 754 | dependencies = [ 755 | "proc-macro2", 756 | "quote", 757 | "syn", 758 | ] 759 | 760 | [[package]] 761 | name = "toml" 762 | version = "0.8.19" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 765 | dependencies = [ 766 | "serde", 767 | "serde_spanned", 768 | "toml_datetime", 769 | "toml_edit", 770 | ] 771 | 772 | [[package]] 773 | name = "toml_datetime" 774 | version = "0.6.8" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 777 | dependencies = [ 778 | "serde", 779 | ] 780 | 781 | [[package]] 782 | name = "toml_edit" 783 | version = "0.22.22" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 786 | dependencies = [ 787 | "indexmap", 788 | "serde", 789 | "serde_spanned", 790 | "toml_datetime", 791 | "winnow", 792 | ] 793 | 794 | [[package]] 795 | name = "trybuild" 796 | version = "1.0.101" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" 799 | dependencies = [ 800 | "dissimilar", 801 | "glob", 802 | "serde", 803 | "serde_derive", 804 | "serde_json", 805 | "target-triple", 806 | "termcolor", 807 | "toml", 808 | ] 809 | 810 | [[package]] 811 | name = "unicode-ident" 812 | version = "1.0.13" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 815 | 816 | [[package]] 817 | name = "unicode-linebreak" 818 | version = "0.1.5" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 821 | 822 | [[package]] 823 | name = "unicode-width" 824 | version = "0.1.14" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 827 | 828 | [[package]] 829 | name = "unicode-width" 830 | version = "0.2.0" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 833 | 834 | [[package]] 835 | name = "utf8parse" 836 | version = "0.2.2" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 839 | 840 | [[package]] 841 | name = "wait-timeout" 842 | version = "0.2.0" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 845 | dependencies = [ 846 | "libc", 847 | ] 848 | 849 | [[package]] 850 | name = "wasi" 851 | version = "0.11.0+wasi-snapshot-preview1" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 854 | 855 | [[package]] 856 | name = "winapi-util" 857 | version = "0.1.9" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 860 | dependencies = [ 861 | "windows-sys", 862 | ] 863 | 864 | [[package]] 865 | name = "windows-sys" 866 | version = "0.59.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 869 | dependencies = [ 870 | "windows-targets", 871 | ] 872 | 873 | [[package]] 874 | name = "windows-targets" 875 | version = "0.52.6" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 878 | dependencies = [ 879 | "windows_aarch64_gnullvm", 880 | "windows_aarch64_msvc", 881 | "windows_i686_gnu", 882 | "windows_i686_gnullvm", 883 | "windows_i686_msvc", 884 | "windows_x86_64_gnu", 885 | "windows_x86_64_gnullvm", 886 | "windows_x86_64_msvc", 887 | ] 888 | 889 | [[package]] 890 | name = "windows_aarch64_gnullvm" 891 | version = "0.52.6" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 894 | 895 | [[package]] 896 | name = "windows_aarch64_msvc" 897 | version = "0.52.6" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 900 | 901 | [[package]] 902 | name = "windows_i686_gnu" 903 | version = "0.52.6" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 906 | 907 | [[package]] 908 | name = "windows_i686_gnullvm" 909 | version = "0.52.6" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 912 | 913 | [[package]] 914 | name = "windows_i686_msvc" 915 | version = "0.52.6" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 918 | 919 | [[package]] 920 | name = "windows_x86_64_gnu" 921 | version = "0.52.6" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 924 | 925 | [[package]] 926 | name = "windows_x86_64_gnullvm" 927 | version = "0.52.6" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 930 | 931 | [[package]] 932 | name = "windows_x86_64_msvc" 933 | version = "0.52.6" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 936 | 937 | [[package]] 938 | name = "winnow" 939 | version = "0.6.20" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 942 | dependencies = [ 943 | "memchr", 944 | ] 945 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fsmentry" 3 | version.workspace = true 4 | license.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | documentation.workspace = true 8 | homepage.workspace = true 9 | readme.workspace = true 10 | description.workspace = true 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | fsmentry-core.workspace = true 17 | proc-macro2 = "1" 18 | quote = "1" 19 | syn = { version = "2" } 20 | 21 | [dev-dependencies] 22 | derive-quickcheck-arbitrary = "0.1.3" 23 | proc-macro2 = { version = "1.0.68", default-features = false } 24 | quickcheck = "1.0.3" 25 | syn = { version = "2", features = ["extra-traits"] } 26 | trybuild = { version = "1.0.85", features = ["diff"] } 27 | assert_cmd = "2.0.12" 28 | prettyplease = "0.2.31" 29 | expect-test = "1.5.1" 30 | 31 | 32 | [workspace.package] 33 | version = "0.4.0" 34 | license = "MIT OR Apache-2.0" 35 | edition = "2021" 36 | repository = "https://github.com/aatifsyed/fsmentry" 37 | documentation = "https://docs.rs/fsmentry" 38 | homepage = "https://crates.io/crates/fsmentry" 39 | readme = "README.md" 40 | description = "Finite State Machines with an entry API and data storage" 41 | 42 | [workspace] 43 | members = ["core", "cli"] 44 | 45 | [workspace.dependencies] 46 | fsmentry-core = { version = "0.4.0", path = "core" } 47 | 48 | [package.metadata.docs.rs] 49 | rustdoc-args = ["--document-private-items"] 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # `fsmentry` 4 | 5 | ```rust 6 | fsmentry! { 7 | enum TrafficLight { 8 | Red -> RedAmber -> Green -> Amber -> Red 9 | } 10 | } 11 | ``` 12 | 13 | A code generator for finite state machines (FSMs) with the following features: 14 | - An `entry` api to transition the state machine. 15 | - Illegal states and transitions can be made unrepresentable. 16 | - States can contain data. 17 | - Generic over user types. 18 | - Custom `#[derive(..)]` support. 19 | - Inline SVG diagrams in docs. 20 | - Generated code is `#[no_std]` compatible. 21 | 22 | ```rust 23 | // define the machine. 24 | fsmentry! { 25 | /// This is a state machine for a traffic light 26 | // Documentation on nodes and states will appear in the generated code 27 | pub enum TrafficLight { 28 | /// Documentation for the [`Red`] state. 29 | Red, // this is a state 30 | Green(String), // this state has data inside it. 31 | 32 | Red -> RedAmber -> Green, 33 | // ^ states can be defined inline. 34 | 35 | Green -custom_method_name-> Amber 36 | /// Custom method documentation 37 | -> Red, 38 | } 39 | } 40 | 41 | // instantiate the machine 42 | let mut state = TrafficLight::Red; 43 | loop { 44 | match state.entry() { 45 | TrafficLightEntry::Red(to) => to.red_amber(), // transition the state machine 46 | // when you transition to a state with data, 47 | // you must provide the data 48 | TrafficLightEntry::RedAmber(to) => to.green(String::from("this is some data")), 49 | TrafficLightEntry::Green(mut to) => { 50 | // you can inspect or mutate the data in a state... 51 | let data: &String = to.as_ref(); 52 | let data: &mut String = to.as_mut(); 53 | // ...and you get it back when you transition out of a state 54 | let data: String = to.custom_method_name(); 55 | }, 56 | TrafficLightEntry::Amber(_) => break, 57 | } 58 | } 59 | ``` 60 | 61 | # About the generated code. 62 | 63 | This macro has three main outputs: 64 | - A "state" enum, which reflects the enum you pass in. 65 | - An "entry" enum, with variants that reflect. 66 | - Data contained in the state (if any). 67 | - Transitions to a different state variant (if any) - see below. 68 | - "transition" structs, which access the data in a variant and allow only legal transitions via methods. 69 | - Transition structs expose their mutable reference to the "state" above, 70 | to allow you to write e.g your own pinning logic. 71 | It is recommended that you wrap each machine in its own module to keep 72 | this reference private, lest you seed panics by manually creating a 73 | transition struct with the wrong underlying state. 74 | 75 | ```rust 76 | mod my_state { // recommended to create a module per machine. 77 | fsmentry::fsmentry! { 78 | /// These attributes are passed through to the state enum. 79 | #[derive(Debug)] 80 | #[fsmentry( 81 | mermaid(true), // Embed mermaid-js into the rustdoc to render a diagram. 82 | entry(pub(crate) MyEntry), // Override the default visibility and name 83 | unsafe(false), // By default, transition structs will panic if constructed incorrectly. 84 | // If you promise to only create valid transition structs, 85 | // or hide the transition structs in their own module, 86 | // you can make these panics unreachable_unchecked instead. 87 | rename_methods(false), // By default, non-overridden methods are given 88 | // snake_case names according to their destination 89 | // but you can turn this off. 90 | )] 91 | pub enum MyState<'a, T> { 92 | Start -> GenericData(&'a mut T) -> Stop, 93 | Start & GenericData -> Error, 94 | // ^ This is shorthand for the following: 95 | // Start -> Error, 96 | // GenericData -> Error, 97 | } 98 | }} 99 | 100 | assert_impl_debug::>(); 101 | ``` 102 | 103 | ## Hierarchical state machines 104 | 105 | `fsmentry` needs no special considerations for sub-state machines - simply store one 106 | on the relevant node! 107 | Here is the example from the [`statig`](https://crates.io/crates/statig) crate: 108 | ```text 109 | ┌─────────────────────────┐ 110 | │ Blinking │🞀─────────┐ 111 | │ ┌───────────────┐ │ │ 112 | │ ┌─🞂│ LedOn │──┐ │ ┌───────────────┐ 113 | │ │ └───────────────┘ │ │ │ NotBlinking │ 114 | │ │ ┌───────────────┐ │ │ └───────────────┘ 115 | │ └──│ LedOff │🞀─┘ │ 🞁 116 | │ └───────────────┘ │──────────┘ 117 | └─────────────────────────┘ 118 | ``` 119 | 120 | ```rust 121 | fsmentry! { 122 | enum Webcam { 123 | NotBlinking -> Blinking(Led) -> NotBlinking 124 | } 125 | } 126 | fsmentry! { 127 | enum Led { 128 | On -> Off -> On, 129 | } 130 | } 131 | 132 | let mut webcam = Webcam::NotBlinking; 133 | loop { 134 | match webcam.entry() { // transition the outer machine 135 | WebcamEntry::Blinking(mut webcam) => match webcam.as_mut().entry() { // transition the inner machine 136 | LedEntry::Off(led) => led.on(), 137 | LedEntry::On(led) => { 138 | led.off(); 139 | webcam.not_blinking(); 140 | } 141 | }, 142 | WebcamEntry::NotBlinking(webcam) => { 143 | webcam.blinking(Led::On) 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | # Comparison with other state machine libraries 150 | 151 | | Crate | Illegal states/transitions unrepresentable | States contain data | State machine definition | Comments | 152 | | ----------------------------------------------------- | ------------------------------------------ | ------------------- | --------------------------- | ---------------- | 153 | | [`fsmentry`](https://crates.io/crates/fsmentry) | Yes | Yes | Graph | | 154 | | [`sm`](https://crates.io/crates/sm) | Yes | No | States, events, transitions | | 155 | | [`rust-fsm`](https://crates.io/crates/rust-fsm) | No | Yes (manually) | States, events, transitions | | 156 | | [`finny`](https://crates.io/crates/finny) | No | Yes | Builder | | 157 | | [`sfsm`](https://crates.io/crates/sfsm) | No | No | States and transitions | | 158 | | [`statig`](https://crates.io/crates/statig) | ? | ? | ? | Complicated API! | 159 | | [`sad_machine`](https://crates.io/crates/sad_machine) | Yes | No | States, events, transitions | | 160 | | [`machine`](https://crates.io/crates/machine) | No | Yes | States, events, transitions | | 161 | 162 | 163 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fsmentry-cli" 3 | version.workspace = true 4 | license.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | documentation.workspace = true 8 | homepage.workspace = true 9 | readme.workspace = true 10 | description.workspace = true 11 | 12 | [dependencies] 13 | anyhow = "1" 14 | clap = { version = "4.5", features = ["derive"] } 15 | fsmentry-core.workspace = true 16 | prettyplease = "0.2" 17 | quote = { version = "1", default-features = false } 18 | syn = { version = "2", default-features = false } 19 | syn-miette = "0.3.0" 20 | -------------------------------------------------------------------------------- /cli/src/bin/fsmentry.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Read as _, Write as _}, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use anyhow::{bail, Context as _}; 7 | use clap::Parser; 8 | use fsmentry_core::{FsmEntry, Mermaid}; 9 | use quote::ToTokens as _; 10 | use syn::parse::{Parse as _, Parser as _}; 11 | 12 | /// Read a file in a DSL, and generate rust code for a state machine. 13 | #[derive(Parser)] 14 | struct Args { 15 | /// Input file to generate from. 16 | /// If `-` or not supplied, read from stdin. 17 | file: Option, 18 | } 19 | 20 | fn main() -> anyhow::Result<()> { 21 | let Args { file } = Args::parse(); 22 | let input = match file { 23 | Some(path) if path == Path::new("-") => get_stdin()?, 24 | Some(path) => std::fs::read_to_string(path).context("error reading file")?, 25 | None => get_stdin()?, 26 | }; 27 | let parser = FsmEntry::parse; 28 | let generator = match parser.parse_str(&input) { 29 | Ok(generator) => generator.map_mermaid(|()| Mermaid::default()), 30 | Err(error) => { 31 | let s = syn_miette::Error::new(error, input).render(); 32 | bail!("\n{}", s); 33 | } 34 | }; 35 | 36 | let printme = prettyplease::unparse( 37 | &syn::parse2(generator.to_token_stream()).context("unexpected output from codegen")?, 38 | ); 39 | writeln!( 40 | io::stdout(), 41 | "// generated by fsmentry {}\n{printme}", 42 | env!("CARGO_PKG_VERSION") 43 | )?; 44 | Ok(()) 45 | } 46 | 47 | fn get_stdin() -> anyhow::Result { 48 | let mut s = String::new(); 49 | io::stdin() 50 | .read_to_string(&mut s) 51 | .context("error reading from stdin")?; 52 | Ok(s) 53 | } 54 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fsmentry-core" 3 | version.workspace = true 4 | license.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | documentation.workspace = true 8 | homepage.workspace = true 9 | readme.workspace = true 10 | description.workspace = true 11 | 12 | [dependencies] 13 | proc-macro2 = { version = "1", default-features = false } 14 | quote = { version = "1", default-features = false } 15 | syn = { version = "2", features = [ 16 | "clone-impls", 17 | "full", 18 | "parsing", 19 | "printing", 20 | ], default-features = false } 21 | 22 | [dev-dependencies] 23 | prettyplease = "0.2.15" 24 | expect-test = "1.5.1" 25 | -------------------------------------------------------------------------------- /core/src/args.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::collections::BTreeMap; 4 | 5 | use syn::{ 6 | meta::ParseNestedMeta, 7 | parenthesized, 8 | parse::{Parse, ParseStream}, 9 | token, Attribute, LitBool, Token, 10 | }; 11 | 12 | /// Simple argument parser for `#[foo(bar = .., baz(..))]` arguments. 13 | // This file is designed to be transplantable. 14 | #[derive(Default)] 15 | pub struct Parser<'a> { 16 | #[expect(clippy::type_complexity)] 17 | inner: BTreeMap< 18 | String, 19 | Either< 20 | Option) -> syn::Result<()> + 'a>>, 21 | Box) -> syn::Result<()> + 'a>, 22 | >, 23 | >, 24 | } 25 | 26 | impl<'a> Parser<'a> { 27 | pub fn new() -> Self { 28 | Self::default() 29 | } 30 | /// Parse this argument at most once. 31 | pub fn once( 32 | mut self, 33 | key: impl Into, 34 | f: impl FnOnce(ParseStream<'_>) -> syn::Result<()> + 'a, 35 | ) -> Self { 36 | let clobbered = self 37 | .inner 38 | .insert(key.into(), Either::Left(Some(Box::new(f)))); 39 | assert!(clobbered.is_none()); 40 | self 41 | } 42 | /// Parse this argument many times. 43 | pub fn many( 44 | mut self, 45 | key: impl Into, 46 | f: impl FnMut(ParseStream<'_>) -> syn::Result<()> + 'a, 47 | ) -> Self { 48 | let clobbered = self.inner.insert(key.into(), Either::Right(Box::new(f))); 49 | assert!(clobbered.is_none()); 50 | self 51 | } 52 | /// Use with [`Attribute::parse_nested_meta`]. 53 | pub fn parse(&mut self, meta: ParseNestedMeta<'_>) -> syn::Result<()> { 54 | for (k, e) in &mut self.inner { 55 | if meta.path.is_ident(k) { 56 | return match e { 57 | Either::Left(o) => match o.take() { 58 | Some(f) => f(meta.input), 59 | None => Err(meta.error("duplicate value for key")), 60 | }, 61 | Either::Right(m) => m(meta.input), 62 | }; 63 | } 64 | } 65 | Err(meta.error(format!("Expected one of {:?}", self.inner.keys()))) 66 | } 67 | /// Filter out attributes with the given `ident`, parsing them as appropriate. 68 | pub fn extract(&mut self, ident: &str, attrs: &mut Vec) -> syn::Result<()> { 69 | let mut error = None; 70 | attrs.retain(|attr| { 71 | if attr.path().is_ident(ident) { 72 | if let Err(e) = attr.parse_nested_meta(|meta| self.parse(meta)) { 73 | match &mut error { 74 | None => error = Some(e), 75 | Some(already) => already.combine(e), 76 | } 77 | }; 78 | false // we've parsed - filter it out 79 | } else { 80 | true // keep it 81 | } 82 | }); 83 | match error { 84 | Some(e) => Err(e), 85 | None => Ok(()), 86 | } 87 | } 88 | } 89 | 90 | enum Either { 91 | Left(L), 92 | Right(R), 93 | } 94 | 95 | fn _on_value( 96 | input: ParseStream<'_>, 97 | f: impl FnOnce(ParseStream<'_>) -> syn::Result<()>, 98 | ) -> syn::Result<()> { 99 | match input.peek(Token![=]) { 100 | true => { 101 | input.parse::()?; 102 | f(input) 103 | } 104 | false => { 105 | let content; 106 | parenthesized!(content in input); 107 | f(&content) 108 | } 109 | } 110 | } 111 | 112 | /// Calls `f` on the following portions of `input`: 113 | /// ```text 114 | /// foo = bar 115 | /// ^^^ 116 | /// *or* 117 | /// foo(bar) 118 | /// ^^^ 119 | /// ``` 120 | pub fn on_value<'a>( 121 | mut f: impl FnMut(ParseStream<'_>) -> syn::Result<()> + 'a, 122 | ) -> impl FnMut(ParseStream<'_>) -> syn::Result<()> + 'a { 123 | move |input| { 124 | let f = &mut f; 125 | _on_value(input, f) 126 | } 127 | } 128 | 129 | /// Create a parser which assigns the given bool. 130 | pub fn bool(dst: &mut bool) -> impl FnMut(ParseStream<'_>) -> syn::Result<()> + '_ { 131 | |input| { 132 | *dst = input.parse::()?.value; 133 | Ok(()) 134 | } 135 | } 136 | 137 | /// Create a parser which assigns to the given item. 138 | pub fn parse(dst: &mut T) -> impl FnMut(ParseStream<'_>) -> syn::Result<()> + '_ { 139 | |input| { 140 | *dst = input.parse()?; 141 | Ok(()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /core/src/dsl.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::ToTokens; 3 | use syn::{ 4 | braced, bracketed, custom_keyword, parenthesized, 5 | parse::{Parse, ParseStream}, 6 | punctuated::{Pair, Punctuated}, 7 | spanned::Spanned as _, 8 | token, Attribute, Generics, Ident, LitStr, Token, Type, Visibility, 9 | }; 10 | 11 | pub(crate) struct Root { 12 | pub attrs: Vec, 13 | pub vis: Visibility, 14 | #[allow(unused)] 15 | pub r#enum: Token![enum], 16 | pub ident: Ident, 17 | pub generics: Generics, 18 | #[allow(unused)] 19 | pub brace: token::Brace, 20 | pub stmts: Punctuated, 21 | } 22 | 23 | #[test] 24 | fn state_enum() { 25 | let _: Root = syn::parse_quote! { 26 | pub enum State<'a, T> 27 | where 28 | T: Ord 29 | { 30 | PopulatedIsland(String), 31 | DesertIsland, 32 | 33 | Fountain(&'a mut T) 34 | /// Go over the water 35 | -fountain2bridge-> BeautifulBridge(Vec) 36 | /// Reuse the rocks 37 | -bridge2tombstone-> Tombstone(char), 38 | /// This fountain is so pretty! 39 | Fountain -> Plank -> 40 | /// This grave is simple, and beautiful in its simplicity. 41 | UnmarkedGrave, 42 | 43 | Stream -> BeautifulBridge, 44 | Stream -> Plank, 45 | }}; 46 | } 47 | impl Parse for Root { 48 | fn parse(input: ParseStream) -> syn::Result { 49 | let content; 50 | Ok(Self { 51 | attrs: Attribute::parse_outer(input)?, 52 | vis: input.parse()?, 53 | r#enum: input.parse()?, 54 | ident: input.parse()?, 55 | generics: { 56 | let mut it = input.parse::()?; 57 | it.where_clause = input.parse()?; 58 | it 59 | }, 60 | brace: braced!(content in input), 61 | stmts: { 62 | let mut stmts = Punctuated::new(); 63 | while !content.is_empty() { 64 | let fork = content.fork(); 65 | fork.parse::()?; 66 | if fork.peek(Token![,]) || fork.is_empty() { 67 | stmts.push(Statement::Node(content.parse()?)); 68 | } else { 69 | let first = content.parse()?; 70 | let mut rest = vec![]; 71 | while content.peek(Token![-]) || content.peek(Token![#]) { 72 | let arrow = content.parse::()?; 73 | let next = content.parse::()?; 74 | if next.into_iter().len() > 1 75 | && matches!(arrow.kind, ArrowKind::Named { .. }) 76 | { 77 | let msg = "Named transitions (`-name->`) to node groups (`A & B`) are not supported, since it requires duplicate method names"; 78 | return Err(syn::Error::new(arrow.kind.span(), msg)); 79 | } 80 | rest.push((arrow, next)); 81 | } 82 | if rest.is_empty() { 83 | return Err(content.error( 84 | "Node groups (`A & B`) must be followed by transitions (`->`)", 85 | )); 86 | } 87 | stmts.push(Statement::Transition { first, rest }); 88 | } 89 | if !content.is_empty() { 90 | stmts.push_punct(content.parse()?); 91 | } 92 | } 93 | stmts 94 | }, 95 | }) 96 | } 97 | } 98 | 99 | pub(crate) enum Statement { 100 | Node(Node), 101 | Transition { 102 | first: NodeGroup, 103 | rest: Vec<(Arrow, NodeGroup)>, 104 | }, 105 | } 106 | 107 | pub(crate) struct Node { 108 | pub doc: Vec, 109 | pub name: Ident, 110 | pub ty: Option<(token::Paren, Type)>, 111 | } 112 | impl Parse for Node { 113 | fn parse(input: ParseStream) -> syn::Result { 114 | Ok(Self { 115 | doc: parse_docs(input)?, 116 | name: input.parse()?, 117 | ty: match input.peek(token::Paren) { 118 | true => { 119 | let content; 120 | Some((parenthesized!(content in input), content.parse()?)) 121 | } 122 | false => None, 123 | }, 124 | }) 125 | } 126 | } 127 | 128 | pub(crate) struct NodeGroup(Punctuated); 129 | 130 | impl Parse for NodeGroup { 131 | fn parse(input: ParseStream) -> syn::Result { 132 | Ok(Self(Punctuated::parse_separated_nonempty(input)?)) 133 | } 134 | } 135 | 136 | impl<'a> IntoIterator for &'a NodeGroup { 137 | type Item = &'a Node; 138 | type IntoIter = <&'a Punctuated as IntoIterator>::IntoIter; 139 | fn into_iter(self) -> Self::IntoIter { 140 | self.0.iter() 141 | } 142 | } 143 | 144 | pub(crate) struct Arrow { 145 | pub doc: Vec, 146 | pub kind: ArrowKind, 147 | } 148 | impl Parse for Arrow { 149 | fn parse(input: ParseStream) -> syn::Result { 150 | Ok(Self { 151 | doc: parse_docs(input)?, 152 | kind: input.parse()?, 153 | }) 154 | } 155 | } 156 | impl ToTokens for Arrow { 157 | fn to_tokens(&self, tokens: &mut TokenStream) { 158 | let Self { doc, kind } = self; 159 | docs_to_tokens(doc, tokens); 160 | kind.to_tokens(tokens); 161 | } 162 | } 163 | 164 | pub(crate) enum ArrowKind { 165 | Plain(Token![->]), 166 | Named { 167 | start: Token![-], 168 | ident: Ident, 169 | end: Token![->], 170 | }, 171 | } 172 | impl Parse for ArrowKind { 173 | fn parse(input: ParseStream) -> syn::Result { 174 | if input.peek(Token![->]) { 175 | return Ok(Self::Plain(input.parse()?)); 176 | } 177 | Ok(Self::Named { 178 | start: input.parse()?, 179 | ident: input.parse()?, 180 | end: input.parse()?, 181 | }) 182 | } 183 | } 184 | impl ToTokens for ArrowKind { 185 | fn to_tokens(&self, tokens: &mut TokenStream) { 186 | match self { 187 | ArrowKind::Plain(it) => it.to_tokens(tokens), 188 | ArrowKind::Named { start, ident, end } => { 189 | start.to_tokens(tokens); 190 | ident.to_tokens(tokens); 191 | end.to_tokens(tokens); 192 | } 193 | } 194 | } 195 | } 196 | 197 | custom_keyword!(doc); 198 | 199 | #[derive(Clone)] 200 | pub(crate) struct DocAttr { 201 | pub pound: Token![#], 202 | pub bracket: token::Bracket, 203 | pub doc: doc, 204 | pub eq: Token![=], 205 | pub str: LitStr, 206 | } 207 | impl DocAttr { 208 | pub fn new(s: &str, span: Span) -> Self { 209 | Self { 210 | pound: Token![#](span), 211 | bracket: token::Bracket(span), 212 | doc: doc(span), 213 | eq: Token![=](span), 214 | str: LitStr::new(s, span), 215 | } 216 | } 217 | pub fn empty() -> Self { 218 | Self::new("", Span::call_site()) 219 | } 220 | } 221 | impl Parse for DocAttr { 222 | fn parse(input: ParseStream) -> syn::Result { 223 | let content; 224 | Ok(Self { 225 | pound: input.parse()?, 226 | bracket: bracketed!(content in input), 227 | doc: content.parse()?, 228 | eq: content.parse()?, 229 | str: content.parse()?, 230 | }) 231 | } 232 | } 233 | impl ToTokens for DocAttr { 234 | fn to_tokens(&self, tokens: &mut TokenStream) { 235 | let Self { 236 | pound, 237 | bracket, 238 | doc, 239 | eq, 240 | str, 241 | } = self; 242 | pound.to_tokens(tokens); 243 | bracket.surround(tokens, |tokens| { 244 | doc.to_tokens(tokens); 245 | eq.to_tokens(tokens); 246 | str.to_tokens(tokens); 247 | }); 248 | } 249 | } 250 | 251 | fn parse_docs(input: ParseStream) -> syn::Result> { 252 | let mut parsed = vec![]; 253 | while input.peek(Token![#]) { 254 | parsed.push(input.parse()?); 255 | } 256 | Ok(parsed) 257 | } 258 | fn docs_to_tokens(docs: &[DocAttr], tokens: &mut TokenStream) { 259 | for doc in docs { 260 | doc.to_tokens(tokens); 261 | } 262 | } 263 | 264 | pub(crate) struct VisIdent { 265 | pub vis: Visibility, 266 | pub ident: Ident, 267 | } 268 | impl Parse for VisIdent { 269 | fn parse(input: ParseStream) -> syn::Result { 270 | Ok(Self { 271 | vis: input.parse()?, 272 | ident: input.parse()?, 273 | }) 274 | } 275 | } 276 | impl ToTokens for VisIdent { 277 | fn to_tokens(&self, tokens: &mut TokenStream) { 278 | let Self { vis, ident } = self; 279 | vis.to_tokens(tokens); 280 | ident.to_tokens(tokens); 281 | } 282 | } 283 | 284 | pub(crate) struct ModulePath { 285 | leading_colon: Option, 286 | segments: Punctuated, 287 | } 288 | impl Parse for ModulePath { 289 | fn parse(input: ParseStream) -> syn::Result { 290 | let syn::Path { 291 | leading_colon, 292 | segments, 293 | } = syn::Path::parse_mod_style(input)?; 294 | Ok(Self { 295 | leading_colon, 296 | segments: segments 297 | .into_pairs() 298 | .map(|it| match it { 299 | Pair::Punctuated(seg, sep) => Pair::Punctuated(seg.ident, sep), 300 | Pair::End(seg) => Pair::End(seg.ident), 301 | }) 302 | .collect(), 303 | }) 304 | } 305 | } 306 | impl ToTokens for ModulePath { 307 | fn to_tokens(&self, tokens: &mut TokenStream) { 308 | let Self { 309 | leading_colon, 310 | segments, 311 | } = self; 312 | leading_colon.to_tokens(tokens); 313 | segments.to_tokens(tokens); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /core/src/graph.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::collections::BTreeMap; 3 | 4 | use proc_macro2::{Ident, TokenStream}; 5 | use quote::ToTokens; 6 | 7 | use crate::dsl::DocAttr; 8 | 9 | #[derive(Hash, PartialEq, Eq, Debug, Clone, PartialOrd, Ord)] 10 | pub(crate) struct NodeId(pub Ident); 11 | impl ToTokens for NodeId { 12 | fn to_tokens(&self, tokens: &mut TokenStream) { 13 | self.0.to_tokens(tokens); 14 | } 15 | } 16 | impl fmt::Display for NodeId { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | self.0.fmt(f) 19 | } 20 | } 21 | 22 | pub(crate) struct NodeData { 23 | pub doc: Vec, 24 | /// Stored as a single tuple member in the state enum. 25 | pub ty: Option, 26 | } 27 | pub(crate) struct EdgeData { 28 | pub doc: Vec, 29 | pub method_name: syn::Ident, 30 | } 31 | 32 | // Don't want to take a dependency on petgraph 33 | pub(crate) struct Graph { 34 | /// All nodes referenced in `edges` are here. 35 | pub nodes: BTreeMap, 36 | /// Directed L -> R. 37 | /// 38 | /// [`EdgeData::method_name`]s MUST be unique. 39 | pub edges: BTreeMap<(NodeId, NodeId), EdgeData>, 40 | } 41 | 42 | impl Graph { 43 | pub fn outgoing<'a>(&'a self, from: &NodeId) -> Vec<(&'a NodeId, &'a NodeData, &'a EdgeData)> { 44 | self.edges 45 | .iter() 46 | .filter_map(move |((it, to), edge_data)| { 47 | (it == from).then_some((to, &self.nodes[to], edge_data)) 48 | }) 49 | .collect() 50 | } 51 | pub fn incoming<'a>(&'a self, to: &NodeId) -> Vec<(&'a NodeId, &'a NodeData, &'a EdgeData)> { 52 | self.edges 53 | .iter() 54 | .filter_map(move |((from, it), edge_data)| { 55 | (it == to).then_some((from, &self.nodes[to], edge_data)) 56 | }) 57 | .collect() 58 | } 59 | pub fn nodes(&self) -> impl Iterator)> { 60 | self.nodes.iter().map(|(nid, data)| { 61 | let incoming = self.incoming(nid); 62 | let outgoing = self.outgoing(nid); 63 | ( 64 | nid, 65 | data, 66 | match (incoming.is_empty(), outgoing.is_empty()) { 67 | (true, true) => Kind::Isolate, 68 | (true, false) => Kind::Source(outgoing), 69 | (false, true) => Kind::Sink(incoming), 70 | (false, false) => Kind::NonTerminal { incoming, outgoing }, 71 | }, 72 | ) 73 | }) 74 | } 75 | } 76 | 77 | pub(crate) enum Kind<'a> { 78 | /// `*` 79 | Isolate, 80 | /// `* -> ...` 81 | Source(Vec<(&'a NodeId, &'a NodeData, &'a EdgeData)>), 82 | /// `... -> *` 83 | Sink(Vec<(&'a NodeId, &'a NodeData, &'a EdgeData)>), 84 | /// `... -> * -> ...` 85 | NonTerminal { 86 | incoming: Vec<(&'a NodeId, &'a NodeData, &'a EdgeData)>, 87 | outgoing: Vec<(&'a NodeId, &'a NodeData, &'a EdgeData)>, 88 | }, 89 | } 90 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A code generator for state machines with an entry API. 2 | //! 3 | //! See the [`fsmentry` crate](https://docs.rs/fsmentry). 4 | 5 | mod args; 6 | mod dsl; 7 | mod graph; 8 | 9 | use std::{ 10 | collections::{BTreeMap, BTreeSet}, 11 | fmt::Write as _, 12 | iter, 13 | }; 14 | 15 | use args::*; 16 | use proc_macro2::{Span, TokenStream}; 17 | use quote::quote; 18 | use quote::ToTokens; 19 | use syn::{ 20 | parse::{Parse, ParseStream}, 21 | parse_quote, 22 | punctuated::Punctuated, 23 | spanned::Spanned as _, 24 | Arm, Attribute, Expr, Generics, Ident, ImplGenerics, ItemImpl, ItemStruct, Lifetime, Token, 25 | Type, TypeGenerics, Variant, Visibility, WhereClause, 26 | }; 27 | 28 | use crate::dsl::*; 29 | use crate::graph::*; 30 | 31 | macro_rules! bail_at { 32 | ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { 33 | return Err(syn::Error::new($span, format!($fmt, $($arg,)*))) 34 | }; 35 | } 36 | 37 | /// Renderer for mermaid diagrams. 38 | pub trait Renderer { 39 | /// Return [`None`] to skip rendering. 40 | fn render(&self, diagram: &str) -> Option; 41 | } 42 | 43 | /// Skip rendering entirely. 44 | impl Renderer for () { 45 | fn render(&self, _: &str) -> Option { 46 | None 47 | } 48 | } 49 | 50 | /// Forward to the inner [`Renderer`], if present. 51 | impl Renderer for Option { 52 | fn render(&self, diagram: &str) -> Option { 53 | self.as_ref().and_then(|it| it.render(diagram)) 54 | } 55 | } 56 | 57 | /// Call the provided function. 58 | impl Option> Renderer for F { 59 | fn render(&self, diagram: &str) -> Option { 60 | self(diagram) 61 | } 62 | } 63 | 64 | /// A [`Renderer`] which embeds a script to load `mermaidjs` into the docs. 65 | pub struct Mermaid( 66 | /// The URL to import mermaid from. 67 | pub String, 68 | ); 69 | 70 | impl Default for Mermaid { 71 | fn default() -> Self { 72 | Self(String::from( 73 | "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs", 74 | )) 75 | } 76 | } 77 | 78 | impl Renderer for Mermaid { 79 | fn render(&self, diagram: &str) -> Option { 80 | Some(format!( 81 | "\ 82 |
 83 | {diagram}
 84 | 
85 | ", 90 | self.0 91 | )) 92 | } 93 | } 94 | 95 | /// A [`Parse`]-able and [printable](ToTokens) representation of a state machine. 96 | pub struct FsmEntry { 97 | state_attrs: Vec, 98 | state_vis: Visibility, 99 | state_ident: Ident, 100 | state_generics: Generics, 101 | 102 | r#unsafe: bool, 103 | path_to_core: ModulePath, 104 | 105 | entry_vis: Visibility, 106 | entry_ident: Ident, 107 | entry_lifetime: Lifetime, 108 | 109 | graph: Graph, 110 | 111 | render_mermaid: bool, 112 | mermaid_renderer: MermaidR, 113 | } 114 | 115 | impl FsmEntry { 116 | /// Change the mermaid renderer. 117 | pub fn map_mermaid(self, f: F) -> FsmEntry 118 | where 119 | F: FnOnce(MermaidR) -> MermaidR2, 120 | { 121 | let Self { 122 | state_attrs, 123 | state_vis, 124 | state_ident, 125 | state_generics, 126 | r#unsafe, 127 | path_to_core, 128 | entry_vis, 129 | entry_ident, 130 | entry_lifetime, 131 | graph, 132 | render_mermaid, 133 | mermaid_renderer, 134 | } = self; 135 | FsmEntry { 136 | state_attrs, 137 | state_vis, 138 | state_ident, 139 | state_generics, 140 | r#unsafe, 141 | path_to_core, 142 | entry_vis, 143 | entry_ident, 144 | entry_lifetime, 145 | graph, 146 | render_mermaid, 147 | mermaid_renderer: f(mermaid_renderer), 148 | } 149 | } 150 | fn nodes(&self) -> impl Iterator { 151 | self.graph.nodes.keys().map(|NodeId(ident)| ident) 152 | } 153 | fn edges(&self) -> impl Iterator { 154 | self.graph.edges.keys().map(|(NodeId(f), NodeId(t))| (f, t)) 155 | } 156 | pub fn dot(&self) -> String { 157 | let mut s = format!("digraph {}{{\n", self.state_ident); 158 | for draw in self.draw() { 159 | match draw { 160 | Draw::Edge(l, r) => s.write_fmt(format_args!(" {l} -> {r};\n")), 161 | Draw::Node(it) => s.write_fmt(format_args!(" {it};\n")), 162 | } 163 | .unwrap(); 164 | } 165 | s.push_str("}\n"); 166 | s 167 | } 168 | pub fn mermaid(&self) -> String { 169 | let mut s = String::from("graph LR\n"); 170 | for draw in self.draw() { 171 | match draw { 172 | Draw::Edge(l, r) => s.write_fmt(format_args!(" {l} --> {r};\n")), 173 | Draw::Node(it) => s.write_fmt(format_args!(" {it};\n")), 174 | } 175 | .unwrap() 176 | } 177 | s 178 | } 179 | fn draw(&self) -> impl Iterator> { 180 | let mut nodes = self.nodes().collect::>(); 181 | let edges = self 182 | .edges() 183 | .map(|(l, r)| { 184 | nodes.remove(l); 185 | nodes.remove(r); 186 | Draw::Edge(l, r) 187 | }) 188 | .collect::>(); 189 | edges.into_iter().chain(nodes.into_iter().map(Draw::Node)) 190 | } 191 | } 192 | enum Draw<'a> { 193 | Edge(&'a Ident, &'a Ident), 194 | Node(&'a Ident), 195 | } 196 | 197 | impl ToTokens for FsmEntry { 198 | fn to_tokens(&self, tokens: &mut TokenStream) { 199 | let Self { 200 | state_attrs, 201 | state_vis, 202 | state_ident, 203 | state_generics, 204 | r#unsafe, 205 | path_to_core, 206 | entry_vis, 207 | entry_ident, 208 | entry_lifetime, 209 | graph, 210 | mermaid_renderer, 211 | render_mermaid, 212 | } = self; 213 | let mut state_variants: Vec = vec![]; 214 | let mut entry_variants: Vec = vec![]; 215 | let mut entry_structs: Vec = vec![]; 216 | let mut match_ctor: Vec = vec![]; 217 | let mut as_ref_as_mut: Vec = vec![]; 218 | let mut transition: Vec = vec![]; 219 | 220 | let replace: ModulePath = parse_quote!(#path_to_core::mem::replace); 221 | let panik: &Expr = &match r#unsafe { 222 | true => parse_quote!(unsafe { #path_to_core::hint::unreachable_unchecked() }), 223 | false => { 224 | parse_quote!(#path_to_core::panic!("entry struct was instantiated with a mismatched state")) 225 | } 226 | }; 227 | 228 | let entry_generics = { 229 | let mut it = state_generics.clone(); 230 | it.params.insert(0, parse_quote!(#entry_lifetime)); 231 | it 232 | }; 233 | let (state_impl_generics, state_type_generics, _) = state_generics.split_for_impl(); 234 | let (entry_impl_generics, entry_type_generics, where_clause) = 235 | entry_generics.split_for_impl(); 236 | 237 | for (node, NodeData { doc, ty }, ref kind) in graph.nodes() { 238 | state_variants.push(match ty { 239 | Some(ty) => parse_quote!(#(#doc)* #node(#ty)), 240 | None => parse_quote!(#(#doc)* #node), 241 | }); 242 | match_ctor.push(match (ty, kind) { 243 | (Some(_), Kind::Isolate | Kind::Sink(_)) => { 244 | parse_quote!(#state_ident::#node(it) => #entry_ident::#node(it)) 245 | } 246 | (None, Kind::Isolate | Kind::Sink(_)) => { 247 | parse_quote!(#state_ident::#node => #entry_ident::#node) 248 | } 249 | (Some(_), Kind::NonTerminal { .. } | Kind::Source(_)) => { 250 | parse_quote!(#state_ident::#node(_) => #entry_ident::#node(#node(value))) 251 | } 252 | (None, Kind::NonTerminal { .. } | Kind::Source(_)) => { 253 | parse_quote!(#state_ident::#node => #entry_ident::#node(#node(value))) 254 | } 255 | }); 256 | let reachability = reachability_docs(&node.0, state_ident, kind); 257 | entry_variants.push(match kind { 258 | Kind::Isolate | Kind::Sink(_) => match ty { 259 | Some(ty) => parse_quote!(#(#reachability)* #node(&#entry_lifetime mut #ty)), 260 | None => parse_quote!(#(#reachability)* #node), 261 | }, 262 | Kind::Source(_) | Kind::NonTerminal { .. } => { 263 | parse_quote!(#(#reachability)* #node(#node #entry_type_generics)) 264 | } 265 | }); 266 | if let Kind::Source(outgoing) | Kind::NonTerminal { outgoing, .. } = kind { 267 | let outer_doc = format!(" See [`{entry_ident}::{node}`]"); 268 | let field_doc = format!(" MUST match [`{entry_ident}::{node}`]"); 269 | entry_structs.push(parse_quote! { 270 | #[doc = #outer_doc] 271 | #entry_vis struct #node #entry_type_generics( 272 | #[doc = #field_doc] 273 | & #entry_lifetime mut #state_ident #state_type_generics 274 | ) 275 | #where_clause; 276 | }); 277 | for (dst, NodeData { ty: dst_ty, .. }, EdgeData { method_name, doc }) in outgoing { 278 | let body = make_body( 279 | state_ident, 280 | node, 281 | ty.as_ref(), 282 | dst, 283 | dst_ty.as_ref(), 284 | method_name, 285 | &replace, 286 | panik, 287 | ); 288 | let pointer = DocAttr::new( 289 | &format!(" Transition to [`{state_ident}::{}`]", dst.0), 290 | Span::call_site(), 291 | ); 292 | let pointer = match doc.is_empty() { 293 | true => vec![pointer], 294 | false => vec![DocAttr::empty(), pointer], 295 | }; 296 | transition.push(parse_quote! { 297 | #[allow(clippy::needless_lifetimes)] 298 | impl #entry_impl_generics #node #entry_type_generics 299 | #where_clause 300 | { 301 | #(#doc)* 302 | #(#pointer)* 303 | #body 304 | } 305 | }); 306 | } 307 | 308 | if let Some(ty) = ty { 309 | as_ref_as_mut.extend(make_as_ref_mut( 310 | &entry_impl_generics, 311 | path_to_core, 312 | ty, 313 | state_ident, 314 | &node.0, 315 | &entry_type_generics, 316 | where_clause, 317 | panik, 318 | )); 319 | } 320 | } 321 | } 322 | 323 | let mut entry_attrs: Vec = vec![{ 324 | let doc = format!(" Progress through variants of [`{state_ident}`], created by its [`entry`]({state_ident}::entry) method."); 325 | parse_quote!(#[doc = #doc]) 326 | }]; 327 | 328 | if *render_mermaid { 329 | if let Some(rendered) = mermaid_renderer.render(&self.mermaid()) { 330 | if !entry_attrs.is_empty() { 331 | entry_attrs.push(parse_quote!(#[doc = ""])); 332 | } 333 | entry_attrs.push(parse_quote!(#[doc = #rendered])); 334 | } 335 | } 336 | 337 | tokens.extend(quote! { 338 | #(#state_attrs)* 339 | #state_vis enum #state_ident #state_generics #where_clause { 340 | #(#state_variants),* 341 | } 342 | #(#entry_attrs)* 343 | #entry_vis enum #entry_ident #entry_generics #where_clause { 344 | #(#entry_variants),* 345 | } 346 | impl #entry_impl_generics 347 | #path_to_core::convert::From<& #entry_lifetime mut #state_ident #state_generics> 348 | for #entry_ident #entry_type_generics 349 | #where_clause { 350 | fn from(value: & #entry_lifetime mut #state_ident #state_generics) -> Self { 351 | match value { 352 | #(#match_ctor),* 353 | } 354 | } 355 | } 356 | impl #state_impl_generics #state_ident #state_type_generics 357 | #where_clause { 358 | #[allow(clippy::needless_lifetimes)] 359 | #entry_vis fn entry<#entry_lifetime>(& #entry_lifetime mut self) -> #entry_ident #entry_type_generics { 360 | self.into() 361 | } 362 | } 363 | #(#entry_structs)* 364 | #(#as_ref_as_mut)* 365 | #(#transition)* 366 | }); 367 | } 368 | } 369 | 370 | impl Parse for FsmEntry { 371 | fn parse(input: ParseStream) -> syn::Result { 372 | let Root { 373 | attrs: mut state_attrs, 374 | vis: state_vis, 375 | r#enum: _, 376 | ident: state_ident, 377 | generics: state_generics, 378 | brace: _, 379 | stmts, 380 | } = input.parse()?; 381 | 382 | let mut rename_methods = true; 383 | let mut entry = VisIdent { 384 | vis: state_vis.clone(), 385 | ident: Ident::new(&format!("{}Entry", state_ident), Span::call_site()), 386 | }; 387 | let mut r#unsafe = false; 388 | let mut path_to_core: ModulePath = parse_quote!(::core); 389 | let mut render_mermaid = false; 390 | let mut parser = Parser::new() 391 | .once("rename_methods", on_value(bool(&mut rename_methods))) 392 | .once("entry", on_value(parse(&mut entry))) 393 | .once("unsafe", on_value(bool(&mut r#unsafe))) 394 | .once("path_to_core", on_value(parse(&mut path_to_core))) 395 | .once("mermaid", on_value(bool(&mut render_mermaid))); 396 | parser.extract("fsmentry", &mut state_attrs)?; 397 | drop(parser); 398 | let graph = stmts2graph(&stmts, rename_methods)?; 399 | if graph.edges.is_empty() { 400 | bail_at!(state_ident.span(), "must define at least one edge `A -> B`"); 401 | } 402 | let VisIdent { 403 | vis: entry_vis, 404 | ident: entry_ident, 405 | } = entry; 406 | 407 | Ok(Self { 408 | state_attrs, 409 | state_vis, 410 | state_ident, 411 | state_generics, 412 | r#unsafe, 413 | path_to_core, 414 | entry_vis, 415 | entry_ident, 416 | entry_lifetime: parse_quote!('state), 417 | graph, 418 | mermaid_renderer: (), 419 | render_mermaid, 420 | }) 421 | } 422 | } 423 | 424 | fn stmts2graph( 425 | stmts: &Punctuated, 426 | rename_methods: bool, 427 | ) -> syn::Result { 428 | use std::collections::btree_map::Entry::{Occupied, Vacant}; 429 | 430 | let mut nodes = BTreeMap::::new(); 431 | let mut edges = BTreeMap::<(NodeId, NodeId), EdgeData>::new(); 432 | 433 | // Define all the nodes upfront. 434 | // Note that transition definitions may include types, at any location. 435 | for Node { name, ty, doc } in stmts.iter().flat_map(|it| match it { 436 | Statement::Node(it) => Box::new(iter::once(it)) as Box>, 437 | Statement::Transition { first, rest, .. } => Box::new( 438 | first 439 | .into_iter() 440 | .chain(rest.iter().flat_map(|(_, grp)| grp)), 441 | ), 442 | }) { 443 | let ty = ty.as_ref().map(|(_, it)| it); 444 | match nodes.entry(NodeId(name.clone())) { 445 | Occupied(mut occ) => match (&occ.get().ty, ty) { 446 | (None, Some(_)) | (Some(_), None) | (None, None) => { 447 | append_docs(&mut occ.get_mut().doc, doc) 448 | } 449 | // don't compile `syn` with `extra-traits` 450 | (Some(l), Some(r)) 451 | if l.to_token_stream().to_string() == r.to_token_stream().to_string() => 452 | { 453 | append_docs(&mut occ.get_mut().doc, doc) 454 | } 455 | (Some(_), Some(_)) => bail_at!(name.span(), "incompatible redefinition"), 456 | }, 457 | Vacant(v) => { 458 | v.insert(NodeData { 459 | ty: ty.cloned(), 460 | doc: doc.clone(), 461 | }); 462 | } 463 | }; 464 | } 465 | 466 | for stmt in stmts { 467 | let Statement::Transition { first, rest } = stmt else { 468 | continue; // handled above 469 | }; 470 | 471 | let mut grp_left = first; 472 | 473 | for (Arrow { doc, kind }, grp_right) in rest { 474 | for from in grp_left { 475 | for to in grp_right { 476 | match edges.entry((NodeId(from.name.clone()), NodeId(to.name.clone()))) { 477 | Occupied(already) => { 478 | let (a, b) = already.key(); 479 | bail_at!(kind.span(), "duplicate edge definition between {a} and {b}") 480 | } 481 | Vacant(v) => { 482 | v.insert(EdgeData { 483 | doc: doc.clone(), 484 | method_name: match kind { 485 | ArrowKind::Plain(_) => match rename_methods { 486 | true => snake_case(&to.name), 487 | false => to.name.clone(), 488 | }, 489 | ArrowKind::Named { ident, .. } => ident.clone(), 490 | }, 491 | }); 492 | } 493 | } 494 | } 495 | } 496 | grp_left = grp_right; 497 | } 498 | } 499 | 500 | Ok(Graph { nodes, edges }) 501 | } 502 | 503 | fn reachability_docs(node_ident: &Ident, state_ident: &Ident, kind: &Kind<'_>) -> Vec { 504 | let span = Span::call_site(); 505 | let mut dst = vec![DocAttr::new( 506 | &format!(" Represents [`{state_ident}::{node_ident}`]"), 507 | span, 508 | )]; 509 | if let Kind::Sink(incoming) | Kind::NonTerminal { incoming, .. } = kind { 510 | dst.extend([ 511 | DocAttr::empty(), 512 | DocAttr::new(" This state is reachable from the following:", span), 513 | ]); 514 | dst.extend(incoming.iter().map(|(NodeId(other), _, EdgeData { method_name, .. })| { 515 | let s = format!(" - [`{other}`]({state_ident}::{other}) via [`{method_name}`]({other}::{method_name})"); 516 | DocAttr::new(&s, Span::call_site()) 517 | })); 518 | } 519 | if let Kind::Source(outgoing) | Kind::NonTerminal { outgoing, .. } = kind { 520 | dst.extend([ 521 | DocAttr::empty(), 522 | DocAttr::new(" This state can transition to the following:", span), 523 | ]); 524 | dst.extend(outgoing.iter().map(|(NodeId(other), _, EdgeData { method_name, .. })| { 525 | let s = format!(" - [`{other}`]({state_ident}::{other}) via [`{method_name}`]({node_ident}::{method_name})"); 526 | DocAttr::new(&s, Span::call_site()) 527 | })); 528 | } 529 | dst 530 | } 531 | 532 | fn append_docs(dst: &mut Vec, src: &[DocAttr]) { 533 | match (dst.is_empty(), src.is_empty()) { 534 | (true, true) => {} 535 | (true, false) => dst.extend_from_slice(src), 536 | (false, true) => {} 537 | (false, false) => { 538 | dst.push(DocAttr::empty()); 539 | dst.extend_from_slice(src); 540 | } 541 | } 542 | } 543 | 544 | fn snake_case(ident: &Ident) -> Ident { 545 | let ident = ident.to_string(); 546 | let mut snake = String::new(); 547 | for (i, ch) in ident.char_indices() { 548 | if i > 0 && ch.is_uppercase() { 549 | snake.push('_'); 550 | } 551 | snake.push(ch.to_ascii_lowercase()); 552 | } 553 | match (syn::parse_str(&snake), { 554 | snake.insert_str(0, "r#"); 555 | syn::parse_str(&snake) 556 | }) { 557 | (Ok(it), _) | (_, Ok(it)) => it, 558 | _ => panic!("bad ident {ident}"), 559 | } 560 | } 561 | 562 | #[allow(clippy::too_many_arguments)] 563 | fn make_body( 564 | state_ident: &Ident, 565 | node: &NodeId, 566 | ty: Option<&Type>, 567 | dst: &NodeId, 568 | dst_ty: Option<&Type>, 569 | method_name: &Ident, 570 | replace: &ModulePath, 571 | panik: &Expr, 572 | ) -> TokenStream { 573 | match (ty, dst_ty) { 574 | (None, None) => quote! { 575 | pub fn #method_name(self) { 576 | match #replace(self.0, #state_ident::#dst) { 577 | #state_ident::#node => {}, 578 | _ => #panik, 579 | } 580 | } 581 | }, 582 | (None, Some(dst_ty)) => quote! { 583 | pub fn #method_name(self, next: #dst_ty) { 584 | match #replace(self.0, #state_ident::#dst(next)) { 585 | #state_ident::#node => {}, 586 | _ => #panik, 587 | } 588 | } 589 | }, 590 | (Some(ty), None) => quote! { 591 | pub fn #method_name(self) -> #ty { 592 | match #replace(self.0, #state_ident::#dst) { 593 | #state_ident::#node(it) => it, 594 | _ => #panik, 595 | } 596 | } 597 | }, 598 | (Some(ty), Some(dst_ty)) => quote! { 599 | pub fn #method_name(self, next: #dst_ty) -> #ty { 600 | match #replace(self.0, #state_ident::#dst(next)) { 601 | #state_ident::#node(it) => it, 602 | _ => #panik, 603 | } 604 | } 605 | }, 606 | } 607 | } 608 | 609 | #[allow(clippy::too_many_arguments)] 610 | fn make_as_ref_mut( 611 | entry_impl_generics: &ImplGenerics, 612 | path_to_core: &ModulePath, 613 | ty: &Type, 614 | state_ident: &Ident, 615 | node_ident: &Ident, 616 | entry_type_generics: &TypeGenerics, 617 | where_clause: Option<&WhereClause>, 618 | panik: &Expr, 619 | ) -> [ItemImpl; 2] { 620 | let as_ref = parse_quote! { 621 | #[allow(clippy::needless_lifetimes)] 622 | impl #entry_impl_generics #path_to_core::convert::AsRef<#ty> for #node_ident #entry_type_generics 623 | #where_clause 624 | { 625 | fn as_ref(&self) -> &#ty { 626 | match &self.0 { 627 | #state_ident::#node_ident(it) => it, 628 | _ => #panik 629 | } 630 | } 631 | } 632 | }; 633 | let as_mut = parse_quote! { 634 | #[allow(clippy::needless_lifetimes)] 635 | impl #entry_impl_generics #path_to_core::convert::AsMut<#ty> for #node_ident #entry_type_generics 636 | #where_clause 637 | { 638 | fn as_mut(&mut self) -> &mut #ty { 639 | match &mut self.0 { 640 | #state_ident::#node_ident(it) => it, 641 | _ => #panik 642 | } 643 | } 644 | } 645 | }; 646 | [as_ref, as_mut] 647 | } 648 | -------------------------------------------------------------------------------- /core/tests/check_compile/full.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(rustfmt, rustfmt_skip)] 2 | /// This is a state machine that explores all vertex types 3 | #[derive(Debug)] 4 | pub enum State<'a, T> 5 | where 6 | T: Ord, 7 | { 8 | /// A non-terminal vertex with data 9 | BeautifulBridge(Vec), 10 | /// An isolated vertex without data. 11 | DesertIsland, 12 | /// A source vertex with data. 13 | Fountain(&'a mut T), 14 | Plank, 15 | /// An isolated vertex with data. 16 | PopulatedIsland(String), 17 | Stream, 18 | /// A sink vertex with data 19 | Tombstone(char), 20 | UnmarkedGrave, 21 | } 22 | /// Progress through variants of [`State`], created by its [`entry`](State::entry) method. 23 | pub(crate) enum MyEntry<'state, 'a, T> 24 | where 25 | T: Ord, 26 | { 27 | /// Represents [`State::BeautifulBridge`] 28 | /// 29 | /// This state is reachable from the following: 30 | /// - [`Fountain`](State::Fountain) via [`fountain2bridge`](Fountain::fountain2bridge) 31 | /// - [`Stream`](State::Stream) via [`beautiful_bridge`](Stream::beautiful_bridge) 32 | /// 33 | /// This state can transition to the following: 34 | /// - [`Tombstone`](State::Tombstone) via [`bridge2tombstone`](BeautifulBridge::bridge2tombstone) 35 | BeautifulBridge(BeautifulBridge<'state, 'a, T>), 36 | /// Represents [`State::DesertIsland`] 37 | DesertIsland, 38 | /// Represents [`State::Fountain`] 39 | /// 40 | /// This state can transition to the following: 41 | /// - [`BeautifulBridge`](State::BeautifulBridge) via [`fountain2bridge`](Fountain::fountain2bridge) 42 | /// - [`Plank`](State::Plank) via [`plank`](Fountain::plank) 43 | Fountain(Fountain<'state, 'a, T>), 44 | /// Represents [`State::Plank`] 45 | /// 46 | /// This state is reachable from the following: 47 | /// - [`Fountain`](State::Fountain) via [`plank`](Fountain::plank) 48 | /// - [`Stream`](State::Stream) via [`plank`](Stream::plank) 49 | /// 50 | /// This state can transition to the following: 51 | /// - [`UnmarkedGrave`](State::UnmarkedGrave) via [`unmarked_grave`](Plank::unmarked_grave) 52 | Plank(Plank<'state, 'a, T>), 53 | /// Represents [`State::PopulatedIsland`] 54 | PopulatedIsland(&'state mut String), 55 | /// Represents [`State::Stream`] 56 | /// 57 | /// This state can transition to the following: 58 | /// - [`BeautifulBridge`](State::BeautifulBridge) via [`beautiful_bridge`](Stream::beautiful_bridge) 59 | /// - [`Plank`](State::Plank) via [`plank`](Stream::plank) 60 | Stream(Stream<'state, 'a, T>), 61 | /// Represents [`State::Tombstone`] 62 | /// 63 | /// This state is reachable from the following: 64 | /// - [`BeautifulBridge`](State::BeautifulBridge) via [`bridge2tombstone`](BeautifulBridge::bridge2tombstone) 65 | Tombstone(&'state mut char), 66 | /// Represents [`State::UnmarkedGrave`] 67 | /// 68 | /// This state is reachable from the following: 69 | /// - [`Plank`](State::Plank) via [`unmarked_grave`](Plank::unmarked_grave) 70 | UnmarkedGrave, 71 | } 72 | impl<'state, 'a, T> ::core::convert::From<&'state mut State<'a, T>> 73 | for MyEntry<'state, 'a, T> 74 | where 75 | T: Ord, 76 | { 77 | fn from(value: &'state mut State<'a, T>) -> Self { 78 | match value { 79 | State::BeautifulBridge(_) => MyEntry::BeautifulBridge(BeautifulBridge(value)), 80 | State::DesertIsland => MyEntry::DesertIsland, 81 | State::Fountain(_) => MyEntry::Fountain(Fountain(value)), 82 | State::Plank => MyEntry::Plank(Plank(value)), 83 | State::PopulatedIsland(it) => MyEntry::PopulatedIsland(it), 84 | State::Stream => MyEntry::Stream(Stream(value)), 85 | State::Tombstone(it) => MyEntry::Tombstone(it), 86 | State::UnmarkedGrave => MyEntry::UnmarkedGrave, 87 | } 88 | } 89 | } 90 | impl<'a, T> State<'a, T> 91 | where 92 | T: Ord, 93 | { 94 | #[allow(clippy::needless_lifetimes)] 95 | pub(crate) fn entry<'state>(&'state mut self) -> MyEntry<'state, 'a, T> { 96 | self.into() 97 | } 98 | } 99 | /// See [`MyEntry::BeautifulBridge`] 100 | pub(crate) struct BeautifulBridge<'state, 'a, T>( 101 | /// MUST match [`MyEntry::BeautifulBridge`] 102 | &'state mut State<'a, T>, 103 | ) 104 | where 105 | T: Ord; 106 | /// See [`MyEntry::Fountain`] 107 | pub(crate) struct Fountain<'state, 'a, T>( 108 | /// MUST match [`MyEntry::Fountain`] 109 | &'state mut State<'a, T>, 110 | ) 111 | where 112 | T: Ord; 113 | /// See [`MyEntry::Plank`] 114 | pub(crate) struct Plank<'state, 'a, T>( 115 | /// MUST match [`MyEntry::Plank`] 116 | &'state mut State<'a, T>, 117 | ) 118 | where 119 | T: Ord; 120 | /// See [`MyEntry::Stream`] 121 | pub(crate) struct Stream<'state, 'a, T>( 122 | /// MUST match [`MyEntry::Stream`] 123 | &'state mut State<'a, T>, 124 | ) 125 | where 126 | T: Ord; 127 | #[allow(clippy::needless_lifetimes)] 128 | impl<'state, 'a, T> ::core::convert::AsRef> for BeautifulBridge<'state, 'a, T> 129 | where 130 | T: Ord, 131 | { 132 | fn as_ref(&self) -> &Vec { 133 | match &self.0 { 134 | State::BeautifulBridge(it) => it, 135 | _ => unsafe { ::core::hint::unreachable_unchecked() } 136 | } 137 | } 138 | } 139 | #[allow(clippy::needless_lifetimes)] 140 | impl<'state, 'a, T> ::core::convert::AsMut> for BeautifulBridge<'state, 'a, T> 141 | where 142 | T: Ord, 143 | { 144 | fn as_mut(&mut self) -> &mut Vec { 145 | match &mut self.0 { 146 | State::BeautifulBridge(it) => it, 147 | _ => unsafe { ::core::hint::unreachable_unchecked() } 148 | } 149 | } 150 | } 151 | #[allow(clippy::needless_lifetimes)] 152 | impl<'state, 'a, T> ::core::convert::AsRef<&'a mut T> for Fountain<'state, 'a, T> 153 | where 154 | T: Ord, 155 | { 156 | fn as_ref(&self) -> &&'a mut T { 157 | match &self.0 { 158 | State::Fountain(it) => it, 159 | _ => unsafe { ::core::hint::unreachable_unchecked() } 160 | } 161 | } 162 | } 163 | #[allow(clippy::needless_lifetimes)] 164 | impl<'state, 'a, T> ::core::convert::AsMut<&'a mut T> for Fountain<'state, 'a, T> 165 | where 166 | T: Ord, 167 | { 168 | fn as_mut(&mut self) -> &mut &'a mut T { 169 | match &mut self.0 { 170 | State::Fountain(it) => it, 171 | _ => unsafe { ::core::hint::unreachable_unchecked() } 172 | } 173 | } 174 | } 175 | #[allow(clippy::needless_lifetimes)] 176 | impl<'state, 'a, T> BeautifulBridge<'state, 'a, T> 177 | where 178 | T: Ord, 179 | { 180 | /// Transition to [`State::Tombstone`] 181 | pub fn bridge2tombstone(self, next: char) -> Vec { 182 | match ::core::mem::replace(self.0, State::Tombstone(next)) { 183 | State::BeautifulBridge(it) => it, 184 | _ => unsafe { ::core::hint::unreachable_unchecked() } 185 | } 186 | } 187 | } 188 | #[allow(clippy::needless_lifetimes)] 189 | impl<'state, 'a, T> Fountain<'state, 'a, T> 190 | where 191 | T: Ord, 192 | { 193 | /// I've overridden transition method name 194 | /// 195 | /// Transition to [`State::BeautifulBridge`] 196 | pub fn fountain2bridge(self, next: Vec) -> &'a mut T { 197 | match ::core::mem::replace(self.0, State::BeautifulBridge(next)) { 198 | State::Fountain(it) => it, 199 | _ => unsafe { ::core::hint::unreachable_unchecked() } 200 | } 201 | } 202 | } 203 | #[allow(clippy::needless_lifetimes)] 204 | impl<'state, 'a, T> Fountain<'state, 'a, T> 205 | where 206 | T: Ord, 207 | { 208 | /// Transition to [`State::Plank`] 209 | pub fn plank(self) -> &'a mut T { 210 | match ::core::mem::replace(self.0, State::Plank) { 211 | State::Fountain(it) => it, 212 | _ => unsafe { ::core::hint::unreachable_unchecked() } 213 | } 214 | } 215 | } 216 | #[allow(clippy::needless_lifetimes)] 217 | impl<'state, 'a, T> Plank<'state, 'a, T> 218 | where 219 | T: Ord, 220 | { 221 | /// Transition to [`State::UnmarkedGrave`] 222 | pub fn unmarked_grave(self) { 223 | match ::core::mem::replace(self.0, State::UnmarkedGrave) { 224 | State::Plank => {} 225 | _ => unsafe { ::core::hint::unreachable_unchecked() } 226 | } 227 | } 228 | } 229 | #[allow(clippy::needless_lifetimes)] 230 | impl<'state, 'a, T> Stream<'state, 'a, T> 231 | where 232 | T: Ord, 233 | { 234 | /// Transition to [`State::BeautifulBridge`] 235 | pub fn beautiful_bridge(self, next: Vec) { 236 | match ::core::mem::replace(self.0, State::BeautifulBridge(next)) { 237 | State::Stream => {} 238 | _ => unsafe { ::core::hint::unreachable_unchecked() } 239 | } 240 | } 241 | } 242 | #[allow(clippy::needless_lifetimes)] 243 | impl<'state, 'a, T> Stream<'state, 'a, T> 244 | where 245 | T: Ord, 246 | { 247 | /// Transition to [`State::Plank`] 248 | pub fn plank(self) { 249 | match ::core::mem::replace(self.0, State::Plank) { 250 | State::Stream => {} 251 | _ => unsafe { ::core::hint::unreachable_unchecked() } 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /core/tests/check_compile/simple.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(rustfmt, rustfmt_skip)] 2 | enum Road { 3 | End, 4 | Fork, 5 | Start, 6 | } 7 | /// Progress through variants of [`Road`], created by its [`entry`](Road::entry) method. 8 | enum RoadEntry<'state> { 9 | /// Represents [`Road::End`] 10 | /// 11 | /// This state is reachable from the following: 12 | /// - [`Fork`](Road::Fork) via [`end`](Fork::end) 13 | End, 14 | /// Represents [`Road::Fork`] 15 | /// 16 | /// This state is reachable from the following: 17 | /// - [`Start`](Road::Start) via [`fork`](Start::fork) 18 | /// 19 | /// This state can transition to the following: 20 | /// - [`End`](Road::End) via [`end`](Fork::end) 21 | /// - [`Start`](Road::Start) via [`start`](Fork::start) 22 | Fork(Fork<'state>), 23 | /// Represents [`Road::Start`] 24 | /// 25 | /// This state is reachable from the following: 26 | /// - [`Fork`](Road::Fork) via [`start`](Fork::start) 27 | /// 28 | /// This state can transition to the following: 29 | /// - [`Fork`](Road::Fork) via [`fork`](Start::fork) 30 | Start(Start<'state>), 31 | } 32 | impl<'state> ::core::convert::From<&'state mut Road> for RoadEntry<'state> { 33 | fn from(value: &'state mut Road) -> Self { 34 | match value { 35 | Road::End => RoadEntry::End, 36 | Road::Fork => RoadEntry::Fork(Fork(value)), 37 | Road::Start => RoadEntry::Start(Start(value)), 38 | } 39 | } 40 | } 41 | impl Road { 42 | #[allow(clippy::needless_lifetimes)] 43 | fn entry<'state>(&'state mut self) -> RoadEntry<'state> { 44 | self.into() 45 | } 46 | } 47 | /// See [`RoadEntry::Fork`] 48 | struct Fork<'state>( 49 | /// MUST match [`RoadEntry::Fork`] 50 | &'state mut Road, 51 | ); 52 | /// See [`RoadEntry::Start`] 53 | struct Start<'state>( 54 | /// MUST match [`RoadEntry::Start`] 55 | &'state mut Road, 56 | ); 57 | #[allow(clippy::needless_lifetimes)] 58 | impl<'state> Fork<'state> { 59 | /// Transition to [`Road::End`] 60 | pub fn end(self) { 61 | match ::core::mem::replace(self.0, Road::End) { 62 | Road::Fork => {} 63 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 64 | } 65 | } 66 | } 67 | #[allow(clippy::needless_lifetimes)] 68 | impl<'state> Fork<'state> { 69 | /// Transition to [`Road::Start`] 70 | pub fn start(self) { 71 | match ::core::mem::replace(self.0, Road::Start) { 72 | Road::Fork => {} 73 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 74 | } 75 | } 76 | } 77 | #[allow(clippy::needless_lifetimes)] 78 | impl<'state> Start<'state> { 79 | /// Transition to [`Road::Fork`] 80 | pub fn fork(self) { 81 | match ::core::mem::replace(self.0, Road::Fork) { 82 | Road::Start => {} 83 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/tests/test.rs: -------------------------------------------------------------------------------- 1 | macro_rules! tests { 2 | ($( 3 | $ident:ident { 4 | $($tt:tt)* 5 | } 6 | )*) => { 7 | $( 8 | #[test] 9 | fn $ident() { 10 | let entry: fsmentry_core::FsmEntry = syn::parse_quote! { 11 | $($tt)* 12 | }; 13 | let mut pretty = prettyplease::unparse(&syn::parse_quote! { 14 | #entry 15 | }); 16 | pretty.insert_str(0, "#![cfg_attr(rustfmt, rustfmt_skip)]\n"); 17 | expect_test::expect_file![ 18 | concat!("check_compile/", stringify!($ident), ".rs") 19 | ].assert_eq(&pretty); 20 | } 21 | )* 22 | 23 | #[allow(unused)] 24 | mod check_compile { 25 | $( 26 | mod $ident; 27 | )* 28 | } 29 | }; 30 | } 31 | 32 | tests! { 33 | full { 34 | /// This is a state machine that explores all vertex types 35 | #[derive(Debug)] 36 | #[fsmentry( 37 | entry = pub(crate) MyEntry, 38 | unsafe(true), 39 | )] 40 | pub enum State<'a, T> 41 | where 42 | T: Ord 43 | { 44 | /// An isolated vertex with data. 45 | PopulatedIsland(String), 46 | /// An isolated vertex without data. 47 | DesertIsland, 48 | 49 | /// A source vertex with data. 50 | Fountain(&'a mut T) 51 | /// I've overridden transition method name 52 | -fountain2bridge-> 53 | /// A non-terminal vertex with data 54 | BeautifulBridge(Vec) 55 | -bridge2tombstone-> 56 | /// A sink vertex with data 57 | Tombstone(char), 58 | 59 | Fountain -> Plank -> UnmarkedGrave, 60 | 61 | Stream -> BeautifulBridge, 62 | Stream -> Plank, 63 | } 64 | } 65 | simple { 66 | enum Road { 67 | Start -> Fork -> End, 68 | Fork -> Start, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/example.dsl: -------------------------------------------------------------------------------- 1 | /// This explores all vertex types. 2 | #[derive(Debug)] 3 | #[fsmentry( 4 | mermaid(true), 5 | )] 6 | pub enum State<'a, T> 7 | where 8 | T: Ord 9 | { 10 | /// Isolated vertex, with data. 11 | IsolatedWithData(String), 12 | /// Isolated vertex, without data. 13 | IsolatedEmpty, 14 | 15 | /// Source vertex, with data. 16 | SourceWithData(&'a mut T) 17 | /// Method documentation on renamed method. 18 | -to_non_terminal_with_data-> 19 | /// Non-terminal vertex, with data. 20 | NonTerminalWithData(Vec) 21 | /// Method documentation on a non-renamed method. 22 | -> 23 | /// Sink vertex, with data. 24 | SinkWithData(char), 25 | 26 | SourceWithData -> NonTerminalEmpty -> SinkEmpty, 27 | 28 | SourceEmpty -> NonTerminalWithData, 29 | SourceEmpty -> NonTerminalEmpty, 30 | } 31 | -------------------------------------------------------------------------------- /src/example.rs: -------------------------------------------------------------------------------- 1 | /// This explores all vertex types. 2 | #[derive(Debug)] 3 | pub enum State<'a, T> 4 | where 5 | T: Ord, 6 | { 7 | /// Isolated vertex, without data. 8 | IsolatedEmpty, 9 | /// Isolated vertex, with data. 10 | IsolatedWithData(String), 11 | NonTerminalEmpty, 12 | /// Non-terminal vertex, with data. 13 | NonTerminalWithData(Vec), 14 | SinkEmpty, 15 | /// Sink vertex, with data. 16 | SinkWithData(char), 17 | SourceEmpty, 18 | /// Source vertex, with data. 19 | SourceWithData(&'a mut T), 20 | } 21 | /// Progress through variants of [`State`], created by its [`entry`](State::entry) method. 22 | /// 23 | /**
 24 | graph LR
 25 |   NonTerminalEmpty --> SinkEmpty;
 26 |   NonTerminalWithData --> SinkWithData;
 27 |   SourceEmpty --> NonTerminalEmpty;
 28 |   SourceEmpty --> NonTerminalWithData;
 29 |   SourceWithData --> NonTerminalEmpty;
 30 |   SourceWithData --> NonTerminalWithData;
 31 |   IsolatedEmpty;
 32 |   IsolatedWithData;
 33 | 
 34 | 
35 | */ 40 | pub enum StateEntry<'state, 'a, T> 41 | where 42 | T: Ord, 43 | { 44 | /// Represents [`State::IsolatedEmpty`] 45 | IsolatedEmpty, 46 | /// Represents [`State::IsolatedWithData`] 47 | IsolatedWithData(&'state mut String), 48 | /// Represents [`State::NonTerminalEmpty`] 49 | /// 50 | /// This state is reachable from the following: 51 | /// - [`SourceEmpty`](State::SourceEmpty) via [`non_terminal_empty`](SourceEmpty::non_terminal_empty) 52 | /// - [`SourceWithData`](State::SourceWithData) via [`non_terminal_empty`](SourceWithData::non_terminal_empty) 53 | /// 54 | /// This state can transition to the following: 55 | /// - [`SinkEmpty`](State::SinkEmpty) via [`sink_empty`](NonTerminalEmpty::sink_empty) 56 | NonTerminalEmpty(NonTerminalEmpty<'state, 'a, T>), 57 | /// Represents [`State::NonTerminalWithData`] 58 | /// 59 | /// This state is reachable from the following: 60 | /// - [`SourceEmpty`](State::SourceEmpty) via [`non_terminal_with_data`](SourceEmpty::non_terminal_with_data) 61 | /// - [`SourceWithData`](State::SourceWithData) via [`to_non_terminal_with_data`](SourceWithData::to_non_terminal_with_data) 62 | /// 63 | /// This state can transition to the following: 64 | /// - [`SinkWithData`](State::SinkWithData) via [`sink_with_data`](NonTerminalWithData::sink_with_data) 65 | NonTerminalWithData(NonTerminalWithData<'state, 'a, T>), 66 | /// Represents [`State::SinkEmpty`] 67 | /// 68 | /// This state is reachable from the following: 69 | /// - [`NonTerminalEmpty`](State::NonTerminalEmpty) via [`sink_empty`](NonTerminalEmpty::sink_empty) 70 | SinkEmpty, 71 | /// Represents [`State::SinkWithData`] 72 | /// 73 | /// This state is reachable from the following: 74 | /// - [`NonTerminalWithData`](State::NonTerminalWithData) via [`sink_with_data`](NonTerminalWithData::sink_with_data) 75 | SinkWithData(&'state mut char), 76 | /// Represents [`State::SourceEmpty`] 77 | /// 78 | /// This state can transition to the following: 79 | /// - [`NonTerminalEmpty`](State::NonTerminalEmpty) via [`non_terminal_empty`](SourceEmpty::non_terminal_empty) 80 | /// - [`NonTerminalWithData`](State::NonTerminalWithData) via [`non_terminal_with_data`](SourceEmpty::non_terminal_with_data) 81 | SourceEmpty(SourceEmpty<'state, 'a, T>), 82 | /// Represents [`State::SourceWithData`] 83 | /// 84 | /// This state can transition to the following: 85 | /// - [`NonTerminalEmpty`](State::NonTerminalEmpty) via [`non_terminal_empty`](SourceWithData::non_terminal_empty) 86 | /// - [`NonTerminalWithData`](State::NonTerminalWithData) via [`to_non_terminal_with_data`](SourceWithData::to_non_terminal_with_data) 87 | SourceWithData(SourceWithData<'state, 'a, T>), 88 | } 89 | impl<'state, 'a, T> ::core::convert::From<&'state mut State<'a, T>> 90 | for StateEntry<'state, 'a, T> 91 | where 92 | T: Ord, 93 | { 94 | fn from(value: &'state mut State<'a, T>) -> Self { 95 | match value { 96 | State::IsolatedEmpty => StateEntry::IsolatedEmpty, 97 | State::IsolatedWithData(it) => StateEntry::IsolatedWithData(it), 98 | State::NonTerminalEmpty => { 99 | StateEntry::NonTerminalEmpty(NonTerminalEmpty(value)) 100 | } 101 | State::NonTerminalWithData(_) => { 102 | StateEntry::NonTerminalWithData(NonTerminalWithData(value)) 103 | } 104 | State::SinkEmpty => StateEntry::SinkEmpty, 105 | State::SinkWithData(it) => StateEntry::SinkWithData(it), 106 | State::SourceEmpty => StateEntry::SourceEmpty(SourceEmpty(value)), 107 | State::SourceWithData(_) => StateEntry::SourceWithData(SourceWithData(value)), 108 | } 109 | } 110 | } 111 | impl<'a, T> State<'a, T> 112 | where 113 | T: Ord, 114 | { 115 | #[allow(clippy::needless_lifetimes)] 116 | pub fn entry<'state>(&'state mut self) -> StateEntry<'state, 'a, T> { 117 | self.into() 118 | } 119 | } 120 | /// See [`StateEntry::NonTerminalEmpty`] 121 | pub struct NonTerminalEmpty<'state, 'a, T>( 122 | /// MUST match [`StateEntry::NonTerminalEmpty`] 123 | &'state mut State<'a, T>, 124 | ) 125 | where 126 | T: Ord; 127 | /// See [`StateEntry::NonTerminalWithData`] 128 | pub struct NonTerminalWithData<'state, 'a, T>( 129 | /// MUST match [`StateEntry::NonTerminalWithData`] 130 | &'state mut State<'a, T>, 131 | ) 132 | where 133 | T: Ord; 134 | /// See [`StateEntry::SourceEmpty`] 135 | pub struct SourceEmpty<'state, 'a, T>( 136 | /// MUST match [`StateEntry::SourceEmpty`] 137 | &'state mut State<'a, T>, 138 | ) 139 | where 140 | T: Ord; 141 | /// See [`StateEntry::SourceWithData`] 142 | pub struct SourceWithData<'state, 'a, T>( 143 | /// MUST match [`StateEntry::SourceWithData`] 144 | &'state mut State<'a, T>, 145 | ) 146 | where 147 | T: Ord; 148 | #[allow(clippy::needless_lifetimes)] 149 | impl<'state, 'a, T> ::core::convert::AsRef> 150 | for NonTerminalWithData<'state, 'a, T> 151 | where 152 | T: Ord, 153 | { 154 | fn as_ref(&self) -> &Vec { 155 | match &self.0 { 156 | State::NonTerminalWithData(it) => it, 157 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 158 | } 159 | } 160 | } 161 | #[allow(clippy::needless_lifetimes)] 162 | impl<'state, 'a, T> ::core::convert::AsMut> 163 | for NonTerminalWithData<'state, 'a, T> 164 | where 165 | T: Ord, 166 | { 167 | fn as_mut(&mut self) -> &mut Vec { 168 | match &mut self.0 { 169 | State::NonTerminalWithData(it) => it, 170 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 171 | } 172 | } 173 | } 174 | #[allow(clippy::needless_lifetimes)] 175 | impl<'state, 'a, T> ::core::convert::AsRef<&'a mut T> for SourceWithData<'state, 'a, T> 176 | where 177 | T: Ord, 178 | { 179 | fn as_ref(&self) -> &&'a mut T { 180 | match &self.0 { 181 | State::SourceWithData(it) => it, 182 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 183 | } 184 | } 185 | } 186 | #[allow(clippy::needless_lifetimes)] 187 | impl<'state, 'a, T> ::core::convert::AsMut<&'a mut T> for SourceWithData<'state, 'a, T> 188 | where 189 | T: Ord, 190 | { 191 | fn as_mut(&mut self) -> &mut &'a mut T { 192 | match &mut self.0 { 193 | State::SourceWithData(it) => it, 194 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 195 | } 196 | } 197 | } 198 | #[allow(clippy::needless_lifetimes)] 199 | impl<'state, 'a, T> NonTerminalEmpty<'state, 'a, T> 200 | where 201 | T: Ord, 202 | { 203 | /// Transition to [`State::SinkEmpty`] 204 | pub fn sink_empty(self) { 205 | match ::core::mem::replace(self.0, State::SinkEmpty) { 206 | State::NonTerminalEmpty => {} 207 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 208 | } 209 | } 210 | } 211 | #[allow(clippy::needless_lifetimes)] 212 | impl<'state, 'a, T> NonTerminalWithData<'state, 'a, T> 213 | where 214 | T: Ord, 215 | { 216 | /// Method documentation on a non-renamed method. 217 | /// 218 | /// Transition to [`State::SinkWithData`] 219 | pub fn sink_with_data(self, next: char) -> Vec { 220 | match ::core::mem::replace(self.0, State::SinkWithData(next)) { 221 | State::NonTerminalWithData(it) => it, 222 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 223 | } 224 | } 225 | } 226 | #[allow(clippy::needless_lifetimes)] 227 | impl<'state, 'a, T> SourceEmpty<'state, 'a, T> 228 | where 229 | T: Ord, 230 | { 231 | /// Transition to [`State::NonTerminalEmpty`] 232 | pub fn non_terminal_empty(self) { 233 | match ::core::mem::replace(self.0, State::NonTerminalEmpty) { 234 | State::SourceEmpty => {} 235 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 236 | } 237 | } 238 | } 239 | #[allow(clippy::needless_lifetimes)] 240 | impl<'state, 'a, T> SourceEmpty<'state, 'a, T> 241 | where 242 | T: Ord, 243 | { 244 | /// Transition to [`State::NonTerminalWithData`] 245 | pub fn non_terminal_with_data(self, next: Vec) { 246 | match ::core::mem::replace(self.0, State::NonTerminalWithData(next)) { 247 | State::SourceEmpty => {} 248 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 249 | } 250 | } 251 | } 252 | #[allow(clippy::needless_lifetimes)] 253 | impl<'state, 'a, T> SourceWithData<'state, 'a, T> 254 | where 255 | T: Ord, 256 | { 257 | /// Transition to [`State::NonTerminalEmpty`] 258 | pub fn non_terminal_empty(self) -> &'a mut T { 259 | match ::core::mem::replace(self.0, State::NonTerminalEmpty) { 260 | State::SourceWithData(it) => it, 261 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 262 | } 263 | } 264 | } 265 | #[allow(clippy::needless_lifetimes)] 266 | impl<'state, 'a, T> SourceWithData<'state, 'a, T> 267 | where 268 | T: Ord, 269 | { 270 | /// Method documentation on renamed method. 271 | /// 272 | /// Transition to [`State::NonTerminalWithData`] 273 | pub fn to_non_terminal_with_data(self, next: Vec) -> &'a mut T { 274 | match ::core::mem::replace(self.0, State::NonTerminalWithData(next)) { 275 | State::SourceWithData(it) => it, 276 | _ => ::core::panic!("entry struct was instantiated with a mismatched state"), 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # `fsmentry` 2 | //! 3 | //! ``` 4 | //! # use fsmentry::fsmentry; 5 | //! fsmentry! { 6 | //! enum TrafficLight { 7 | //! Red -> RedAmber -> Green -> Amber -> Red 8 | //! } 9 | //! } 10 | //! ``` 11 | //! 12 | //! A code generator for finite state machines (FSMs) with the following features: 13 | //! - An `entry` api to transition the state machine. 14 | //! - Illegal states and transitions can be made unrepresentable. 15 | //! - States can contain data. 16 | //! - Generic over user types. 17 | //! - Custom `#[derive(..)]` support. 18 | //! - Inline SVG diagrams in docs. 19 | //! - Generated code is `#[no_std]` compatible. 20 | // TODO: 21 | // - Define your machine as a graph in e.g [`DOT`](https://en.wikipedia.org/wiki/DOT_%28graph_description_language%29). 22 | //! 23 | //! ``` 24 | //! # use fsmentry::fsmentry; 25 | //! // define the machine. 26 | // TODO 27 | // // you can also use the DOT language if you prefer. 28 | //! fsmentry! { 29 | //! /// This is a state machine for a traffic light 30 | //! // Documentation on nodes and states will appear in the generated code 31 | //! pub enum TrafficLight { 32 | //! /// Documentation for the [`Red`] state. 33 | //! Red, // this is a state 34 | //! Green(String), // this state has data inside it. 35 | //! 36 | //! Red -> RedAmber -> Green, 37 | //! // ^ states can be defined inline. 38 | //! 39 | //! Green -custom_method_name-> Amber 40 | //! /// Custom method documentation 41 | //! -> Red, 42 | //! } 43 | //! } 44 | //! 45 | //! // instantiate the machine 46 | //! let mut state = TrafficLight::Red; 47 | //! loop { 48 | //! match state.entry() { 49 | //! TrafficLightEntry::Red(to) => to.red_amber(), // transition the state machine 50 | //! // when you transition to a state with data, 51 | //! // you must provide the data 52 | //! TrafficLightEntry::RedAmber(to) => to.green(String::from("this is some data")), 53 | //! TrafficLightEntry::Green(mut to) => { 54 | //! // you can inspect or mutate the data in a state... 55 | //! let data: &String = to.as_ref(); 56 | //! let data: &mut String = to.as_mut(); 57 | //! // ...and you get it back when you transition out of a state 58 | //! let data: String = to.custom_method_name(); 59 | //! }, 60 | //! TrafficLightEntry::Amber(_) => break, 61 | //! } 62 | //! } 63 | //! ``` 64 | //! 65 | //! # About the generated code. 66 | //! 67 | //! This macro has three main outputs: 68 | //! - A "state" enum, which reflects the enum you pass in. 69 | //! - An "entry" enum, with variants that reflect. 70 | //! - Data contained in the state (if any). 71 | //! - Transitions to a different state variant (if any) - see below. 72 | //! - "transition" structs, which access the data in a variant and allow only legal transitions via methods. 73 | //! - Transition structs expose their mutable reference to the "state" above, 74 | //! to allow you to write e.g your own pinning logic. 75 | //! It is recommended that you wrap each machine in its own module to keep 76 | //! this reference private, lest you seed panics by manually creating a 77 | //! transition struct with the wrong underlying state. 78 | //! 79 | //! ``` 80 | //! mod my_state { // recommended to create a module per machine. 81 | //! fsmentry::fsmentry! { 82 | //! /// These attributes are passed through to the state enum. 83 | //! #[derive(Debug)] 84 | //! #[fsmentry( 85 | //! mermaid(true), // Embed mermaid-js into the rustdoc to render a diagram. 86 | //! entry(pub(crate) MyEntry), // Override the default visibility and name 87 | //! unsafe(false), // By default, transition structs will panic if constructed incorrectly. 88 | //! // If you promise to only create valid transition structs, 89 | //! // or hide the transition structs in their own module, 90 | //! // you can make these panics unreachable_unchecked instead. 91 | //! rename_methods(false), // By default, non-overridden methods are given 92 | //! // snake_case names according to their destination 93 | //! // but you can turn this off. 94 | //! )] 95 | //! pub enum MyState<'a, T> { 96 | //! Start -> GenericData(&'a mut T) -> Stop, 97 | //! Start & GenericData -> Error, 98 | //! // ^ This is shorthand for the following: 99 | //! // Start -> Error, 100 | //! // GenericData -> Error, 101 | //! } 102 | //! }} 103 | //! 104 | //! # fn assert_impl_debug() {}; 105 | //! assert_impl_debug::>(); 106 | //! ``` 107 | //! 108 | //! ## Hierarchical state machines 109 | //! 110 | //! `fsmentry` needs no special considerations for sub-state machines - simply store one 111 | //! on the relevant node! 112 | //! Here is the example from the [`statig`](https://crates.io/crates/statig) crate: 113 | //! ```text 114 | //! ┌─────────────────────────┐ 115 | //! │ Blinking │🞀─────────┐ 116 | //! │ ┌───────────────┐ │ │ 117 | //! │ ┌─🞂│ LedOn │──┐ │ ┌───────────────┐ 118 | //! │ │ └───────────────┘ │ │ │ NotBlinking │ 119 | //! │ │ ┌───────────────┐ │ │ └───────────────┘ 120 | //! │ └──│ LedOff │🞀─┘ │ 🞁 121 | //! │ └───────────────┘ │──────────┘ 122 | //! └─────────────────────────┘ 123 | //! ``` 124 | //! 125 | //! ```no_run 126 | //! # use fsmentry::fsmentry; 127 | //! fsmentry! { 128 | //! enum Webcam { 129 | //! NotBlinking -> Blinking(Led) -> NotBlinking 130 | //! } 131 | //! } 132 | //! fsmentry! { 133 | //! enum Led { 134 | //! On -> Off -> On, 135 | //! } 136 | //! } 137 | //! 138 | //! let mut webcam = Webcam::NotBlinking; 139 | //! loop { 140 | //! match webcam.entry() { // transition the outer machine 141 | //! WebcamEntry::Blinking(mut webcam) => match webcam.as_mut().entry() { // transition the inner machine 142 | //! LedEntry::Off(led) => led.on(), 143 | //! LedEntry::On(led) => { 144 | //! led.off(); 145 | //! webcam.not_blinking(); 146 | //! } 147 | //! }, 148 | //! WebcamEntry::NotBlinking(webcam) => { 149 | //! webcam.blinking(Led::On) 150 | //! } 151 | //! } 152 | //! } 153 | //! ``` 154 | //! 155 | //! # Comparison with other state machine libraries 156 | //! 157 | //! | Crate | Illegal states/transitions unrepresentable | States contain data | State machine definition | Comments | 158 | //! | ----------------------------------------------------- | ------------------------------------------ | ------------------- | --------------------------- | ---------------- | 159 | //! | [`fsmentry`](https://crates.io/crates/fsmentry) | Yes | Yes | Graph | | 160 | //! | [`sm`](https://crates.io/crates/sm) | Yes | No | States, events, transitions | | 161 | //! | [`rust-fsm`](https://crates.io/crates/rust-fsm) | No | Yes (manually) | States, events, transitions | | 162 | //! | [`finny`](https://crates.io/crates/finny) | No | Yes | Builder | | 163 | //! | [`sfsm`](https://crates.io/crates/sfsm) | No | No | States and transitions | | 164 | //! | [`statig`](https://crates.io/crates/statig) | ? | ? | ? | Complicated API! | 165 | //! | [`sad_machine`](https://crates.io/crates/sad_machine) | Yes | No | States, events, transitions | | 166 | //! | [`machine`](https://crates.io/crates/machine) | No | Yes | States, events, transitions | | 167 | 168 | #![cfg_attr(docsrs, feature(doc_cfg))] 169 | 170 | /// This is an example state machine that is only included on [`docs.rs`](https://docs.rs). 171 | /// It is generated from the following definition: 172 | /// ```rust,ignore 173 | #[doc = include_str!("example.dsl")] 174 | /// ``` 175 | #[cfg(docsrs)] 176 | mod example; 177 | 178 | use fsmentry_core::{FsmEntry, Mermaid}; 179 | use proc_macro2::TokenStream; 180 | use quote::ToTokens as _; 181 | use syn::parse_macro_input; 182 | 183 | /// Accept a state machine definition as follows: 184 | /// ``` 185 | /// # use fsmentry::fsmentry; 186 | /// fsmentry! { 187 | /// pub enum TrafficLight { 188 | /// Red -> RedAmber -> Green(T) -> Amber -> Red, 189 | /// } 190 | /// } 191 | /// ``` 192 | /// 193 | /// For more, see the [crate documentation](mod@self). 194 | #[proc_macro] 195 | pub fn fsmentry(item: proc_macro::TokenStream) -> proc_macro::TokenStream { 196 | // hide from docs.rs 197 | fn _dsl(entry: FsmEntry) -> syn::Result { 198 | Ok(entry.map_mermaid(|()| Mermaid::default()).to_token_stream()) 199 | } 200 | 201 | let item = parse_macro_input!(item as FsmEntry); 202 | _dsl(item) 203 | .unwrap_or_else(syn::Error::into_compile_error) 204 | .into() 205 | } 206 | 207 | // #[cfg(feature = "std")] 208 | // #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 209 | // #[doc(inline)] 210 | // pub use fsmentry_core::FSMGenerator; 211 | 212 | // #[cfg(feature = "macros")] 213 | // #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] 214 | // #[doc(inline)] 215 | // pub use fsmentry_macros::{dot, dsl}; 216 | 217 | #[cfg(test)] 218 | mod tests { 219 | use expect_test::expect_file; 220 | use fsmentry_core::{FsmEntry, Mermaid}; 221 | use quote::ToTokens; 222 | 223 | #[test] 224 | fn trybuild() { 225 | let t = trybuild::TestCases::new(); 226 | t.pass("trybuild/pass/**/*.rs"); 227 | t.compile_fail("trybuild/fail/**/*.rs") 228 | } 229 | 230 | #[test] 231 | fn example() { 232 | let file = syn::parse2( 233 | syn::parse_str::(include_str!("example.dsl")) 234 | .unwrap() 235 | .map_mermaid(|()| Mermaid::default()) 236 | .to_token_stream(), 237 | ) 238 | .unwrap(); 239 | expect_file!["example.rs"].assert_eq(&prettyplease::unparse(&file)); 240 | } 241 | 242 | #[test] 243 | fn readme() { 244 | assert!( 245 | std::process::Command::new("cargo") 246 | .args(["rdme", "--check"]) 247 | .output() 248 | .expect("couldn't run `cargo rdme`") 249 | .status 250 | .success(), 251 | "README.md is out of date - bless the new version by running `cargo rdme`" 252 | ) 253 | } 254 | } 255 | --------------------------------------------------------------------------------