diff --git a/.github/action/perf/Dockerfile b/.github/action/perf/Dockerfile index 3cbcba6c..c1c50d05 100644 --- a/.github/action/perf/Dockerfile +++ b/.github/action/perf/Dockerfile @@ -1,4 +1,4 @@ -FROM python:slim AS helper +FROM python:slim-buster AS helper RUN apt-get update && \ apt-get install --no-install-recommends -y \ @@ -12,7 +12,7 @@ RUN pip install pyinstaller && \ pyinstaller wrap-args.py --onefile --distpath /bin # ----------------- Github Action ------------------------ -FROM rust:slim +FROM rust:slim-buster LABEL "name"="perf" \ "maintainer"="Fahmi Akbar Wildana " \ @@ -23,12 +23,12 @@ LABEL "com.github.actions.name"="GitHub Action for Measuring Performance" \ "com.github.actions.icon"="clock" \ "com.github.actions.color"="orange" -ADD https://github.com/sharkdp/hyperfine/releases/download/v1.5.0/hyperfine_1.5.0_amd64.deb \ +ADD https://github.com/sharkdp/hyperfine/releases/download/v1.6.0/hyperfine_1.6.0_amd64.deb \ https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \ /tmp/ RUN chmod +x /tmp/* && \ - dpkg -i /tmp/hyperfine_1.5.0_amd64.deb && \ + dpkg -i /tmp/hyperfine_1.6.0_amd64.deb && \ mv /tmp/jq-linux64 /usr/bin/jq && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/.github/action/perf/wrap-args.py b/.github/action/perf/wrap-args.py index 5a5a503f..aec599b9 100755 --- a/.github/action/perf/wrap-args.py +++ b/.github/action/perf/wrap-args.py @@ -1,29 +1,35 @@ #!/usr/bin/env python -import re,sys +import re +import sys from itertools import zip_longest from subprocess import run assert len(sys.argv) > 1 + # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" def grouper(iterable, n, fillvalue=None): "Collect data into fixed-length chunks or blocks" args = [iter(iterable)] * n return zip_longest(*args, fillvalue=fillvalue) + def group_command(flatten_commands, commands): "Group string by it's command name" separator = f"({'|'.join(list_command)})" - separated_cmd_params = [s.strip() for s in re.split(separator, flatten_commands)[1:]] - return [' '.join(s) for s in grouper(separated_cmd_params, 2)] + separated_cmd_params = [ + s.strip() for s in re.split(separator, flatten_commands)[1:] + ] + return [" ".join(s) for s in grouper(separated_cmd_params, 2)] + -cargo_list = run(['cargo', '--list'], capture_output=True) -stdout = cargo_list.stdout.decode('utf-8') +cargo_list = run(["cargo", "--list"], capture_output=True) +stdout = cargo_list.stdout.decode("utf-8") -command_help = stdout.split('\n')[1:-1] # remove endline and title string -list_command =[s.split(maxsplit=1)[0] for s in command_help] # remove help description +command_help = stdout.split("\n")[1:-1] # remove endline and title string +list_command = [s.split(maxsplit=1)[0] for s in command_help] # remove help description -grouped = group_command(' '.join(sys.argv[1:]), list_command) +grouped = group_command(" ".join(sys.argv[1:]), list_command) prefix_command = lambda f: '"{prefix} {}"'.format(f'" "{f} '.join(grouped), prefix=f) -print(prefix_command('cargo')) +print(prefix_command("cargo")) diff --git a/.github/entrypoint.sh b/.github/entrypoint.sh index ce1cb7ae..bfc6a899 100755 --- a/.github/entrypoint.sh +++ b/.github/entrypoint.sh @@ -1,12 +1,13 @@ #!/bin/sh set -e -export PATH="$HOME/.cargo/bin:$PATH" +mkdir --parents ${HOME}/.bin/ +export PATH="$HOME/.cargo/bin:${HOME}/.bin:$PATH" +[ -z $WORKDIR ] || cd $WORKDIR for cmd in "$@"; do echo "Running '$cmd'..." if sh -c "$cmd"; then - # no op echo echo "Successfully ran '$cmd'" else diff --git a/.github/main.workflow b/.github/main.workflow index 5eccfaf6..b55a73e8 100644 --- a/.github/main.workflow +++ b/.github/main.workflow @@ -1,6 +1,6 @@ workflow "Testing" { on = "push" - resolves = ["Test all rust project"] + resolves = ["Test all rust project", "Smoke tests"] } workflow "Measure Performance" { @@ -41,6 +41,32 @@ action "Summarize perf" { # --------------------------------------------------------------- # ------------------------ Process ------------------------------ +action "Test all rust project" { + uses = "docker://rust:slim-buster" + runs = "./.github/entrypoint.sh" + args = [ + "cargo install just", + "just test", + "mv target/debug/${BIN} ${HOME}/.bin/${BIN}", + ] + env = { BIN = "scrap" } +} + +action "Smoke tests" { + needs = "Test all rust project" + uses = "docker://node:buster" + runs = "./.github/entrypoint.sh" + args = [ + "git submodule update --init --recursive", + "npm install", + "scrap generate src/${filestem}.scl --format xstate --as typescript > src/fsm/${filestem}.ts", + "scrap generate src/${filestem}.scl --format xstate --as javascript >> src/fsm/${filestem}.ts", + "npx tsc --build", + "node dist/index.js", + ] + env = { WORKDIR = "examples/xstate/nodejs", filestem = "light" } +} + action "Perf cargo" { needs = "On Push" uses = "./.github/action/perf" @@ -52,17 +78,6 @@ action "Perf cargo" { ] } -action "Test all rust project" { - uses = "docker://rust:slim" - runs = "./.github/entrypoint.sh" - args = [ - "cargo install just", - "just unit", - "just integration || true", - ] - env = { PWD = "/github/workspace" } -} - action "Build Release cli as musl" { needs = "On Push" uses = "docker://rust:slim" @@ -71,7 +86,6 @@ action "Build Release cli as musl" { "rustup target add x86_64-unknown-linux-musl", "apt-get update && apt-get install -y musl-tools", "cargo build --target x86_64-unknown-linux-musl --release --bin ${BIN}", - "mkdir -p ${HOME}/.bin/", "mv target/x86_64-unknown-linux-musl/release/${BIN} ${HOME}/.bin/${BIN}", ] env = { BIN = "scrap" } @@ -82,10 +96,10 @@ action "Perf CLI release" { uses = "docker://python:alpine" runs = "./.github/profiler.sh" args = [ - "${HOME}/.bin/scrap code examples/simple.scl --format xstate", - "${HOME}/.bin/scrap code examples/simple.scl --format xstate --stream", - "${HOME}/.bin/scrap code examples/simple.scl --format smcat", - "${HOME}/.bin/scrap code examples/simple.scl --format smcat --stream", + "scrap code examples/simple.scl --format xstate", + "scrap code examples/simple.scl --format xstate --stream", + "scrap code examples/simple.scl --format smcat", + "scrap code examples/simple.scl --format smcat --stream", ], env = { PREPARE = "./scripts/gensample.py 1000 > examples/simple.scl" } } diff --git a/.github/profiler.sh b/.github/profiler.sh index a59f72bc..59897bff 100755 --- a/.github/profiler.sh +++ b/.github/profiler.sh @@ -6,6 +6,8 @@ # 4. Add environment variable for preparation (e.g execute script to generate sample data) set -e +export PATH="$HOME/.cargo/bin:${HOME}/.bin:$PATH" + json='{ "command": "%C", "memory": { diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..046dcdf5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "examples/xstate/nodejs"] + path = examples/xstate/nodejs + url = https://github.com/DrSensor/nodejs-scdlang_xstate.git diff --git a/Cargo.lock b/Cargo.lock index 80698d49..8f806d0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,6 +638,14 @@ dependencies = [ "walkdir 2.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "html5ever" version = "0.21.0" @@ -869,6 +877,11 @@ name = "numtoa" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "once_cell" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "onig" version = "4.3.2" @@ -1399,6 +1412,7 @@ dependencies = [ "scdlang 0.2.1", "scdlang_smcat 0.2.1", "scdlang_xstate 0.2.1", + "voca_rs 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "which 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1420,9 +1434,10 @@ name = "scdlang" version = "0.2.1" dependencies = [ "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "pest 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "pest_derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "static_assertions 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1434,6 +1449,8 @@ dependencies = [ "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", "serde_with 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "strum 0.15.0 (git+https://github.com/Peternator7/strum.git)", + "strum_macros 0.15.0 (git+https://github.com/Peternator7/strum.git)", ] [[package]] @@ -1444,6 +1461,9 @@ dependencies = [ "scdlang 0.2.1", "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_with 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "strum 0.15.0 (git+https://github.com/Peternator7/strum.git)", + "strum_macros 0.15.0 (git+https://github.com/Peternator7/strum.git)", "voca_rs 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1549,6 +1569,11 @@ name = "spin" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "static_assertions" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "stfu8" version = "0.2.4" @@ -1599,6 +1624,22 @@ name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "strum" +version = "0.15.0" +source = "git+https://github.com/Peternator7/strum.git#efed58502a40ac101691068bbc6c20a0b351f3fd" + +[[package]] +name = "strum_macros" +version = "0.15.0" +source = "git+https://github.com/Peternator7/strum.git#efed58502a40ac101691068bbc6c20a0b351f3fd" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "syn" version = "0.11.11" @@ -1991,6 +2032,7 @@ dependencies = [ "checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55" "checksum globset 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "925aa2cac82d8834e2b2a4415b6f6879757fb5c0928fc445ae76461a12eed8f2" "checksum globwalk 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89fa2e29859da05acd066bd45996f05c271b271d7ec4a781f909682328f65d25" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum html5ever 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba3a1fd1857a714d410c191364c5d7bf8a6487c0ab5575146d37dd7eb17ef523" "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" "checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" @@ -2021,6 +2063,7 @@ dependencies = [ "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +"checksum once_cell 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1824583b0e4dc0c1716eea4fb51a9ca2634943f0b07fd929e79af6aeb5a513cc" "checksum onig 4.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a646989adad8a19f49be2090374712931c3a59835cb5277b4530f48b417f26e7" "checksum onig_sys 69.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388410bf5fa341f10e58e6db3975f4bea1ac30247dd79d37a9e5ced3cb4cc3b0" "checksum opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93f5bb2e8e8dec81642920ccff6b61f1eb94fa3020c5a325c9851ff604152409" @@ -2094,12 +2137,15 @@ dependencies = [ "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" "checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" "checksum spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44363f6f51401c34e7be73db0db371c04705d35efbe9f7d6082e03a921a32c55" +"checksum static_assertions 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" "checksum stfu8 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf70433e3300a3c395d06606a700cdf4205f4f14dbae2c6833127c6bb22db77" "checksum string_cache 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "413fc7852aeeb5472f1986ef755f561ddf0c789d3d796e65f0b6fe293ecd4ef8" "checksum string_cache_codegen 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eea1eee654ef80933142157fdad9dd8bc43cf7c74e999e369263496f04ff4da" "checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum strum 0.15.0 (git+https://github.com/Peternator7/strum.git)" = "" +"checksum strum_macros 0.15.0 (git+https://github.com/Peternator7/strum.git)" = "" "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" "checksum syn 0.15.39 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d960b829a55e56db167e861ddb43602c003c7be0bee1d345021703fac2fb7c" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" diff --git a/Justfile b/Justfile index 78f61f57..75e63ef7 100644 --- a/Justfile +++ b/Justfile @@ -18,7 +18,7 @@ check: clear watchexec --restart --clear 'just {{command}} {{args}}' # Run all kind of tests -test: unit integration +test: unit integration smoke # Generate and open the documentation docs +args='': @@ -63,14 +63,20 @@ release version: build args='': cargo build {{args}} -# Run all unit test +# Run all unit tests unit: cargo test --lib --all --exclude s-crap -- --test-threads=1 -# Run all integration test +# Run all integration tests integration: cargo test --tests -p s-crap -- --test-threads=1 +# Run all examples and doc-tests +smoke: + cargo test --doc --all --exclude s-crap + cargo test --examples +# TODO: `mask --maskfile examples/xstate/nodejs/maskfile.md start` should link `scrap` to target/debug/scrap + # Show reports of macro-benchmark @stats git-flags='': ./scripts/summary.sh {{git-flags}} | ./scripts/perfsum.py diff --git a/README.md b/README.md index 5f14ed9f..1fc7b611 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![License](https://img.shields.io/github/license/drsensor/scdlang.svg)](./LICENSE) [![Chats](https://img.shields.io/badge/community-grey.svg?logo=matrix)](https://matrix.to/#/+statecharts:matrix.org) -> 🚧 Still **Work in Progress** πŸ—οΈ +> 🚧 Slowly **Work in Progress** πŸ—οΈ ## About Scdlang (pronounced `/ˈesˌsi:ˈdi:ˈlΓ¦Ε‹/`) is a description language for describing Statecharts that later can be used to generate code or just transpile it into another format. This project is more focus on how to describe Statecharts universally that can be used in another language/platform rather than drawing a Statecharts diagram. For drawing, see [State Machine Cat][]. @@ -35,11 +35,12 @@ Scdlang (pronounced `/ˈesˌsi:ˈdi:ˈlΓ¦Ε‹/`) is a description language for des - [ ] [WaveDrom](https://observablehq.com/@drom/wavedrom) - Compile into other formats (hopefully, no promise): - [ ] WebAssembly (using [parity-wasm](https://github.com/paritytech/parity-wasm)) -- Code generation πŸ€” - - [ ] Julia via [`@generated`](https://docs.julialang.org/en/v1/manual/metaprogramming/#Generated-functions-1) implemented as [parametric](https://docs.julialang.org/en/v1/manual/methods/#Parametric-Methods-1) [multiple-dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch#Julia) [functors](https://docs.julialang.org/en/v1/manual/methods/#Function-like-objects-1) - - [ ] Rust via [`#[proc_macro_attribute]`](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros) implemented as [typestate programming](https://rust-embedded.github.io/book/static-guarantees/typestate-programming.html)? (I'm still afraid if it will conflict with another crates) - - [ ] Elixir via [`use`](https://elixir-lang.org/getting-started/alias-require-and-import.html#use) macro which desugar into [gen_statem](https://andrealeopardi.com/posts/connection-managers-with-gen_statem/) πŸ’ͺ - - [ ] Flutter via [`builder_factories`](https://github.com/flutter/flutter/wiki/Code-generation-in-Flutter) (waiting for the [FFI](https://github.com/dart-lang/sdk/issues/34452) to be stable) +- Code generation (all of them can be non-embedded and it will be the priority) πŸ€” + - [ ] Julia via [`@generated`](https://docs.julialang.org/en/v1/manual/metaprogramming/#Generated-functions-1) implemented as [parametric](https://docs.julialang.org/en/v1/manual/methods/#Parametric-Methods-1) [multiple-dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch#Julia) [functors](https://docs.julialang.org/en/v1/manual/methods/#Function-like-objects-1) [non-embedded until there is a project like PyO3 but for Julia] + - [ ] Rust via [`#[proc_macro_attribute]`](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros) implemented as [typestate programming](https://rust-embedded.github.io/book/static-guarantees/typestate-programming.html)? (Need to figure out how to support async, non-async, and no-std in single abstraction) [embedded] + - [ ] Elixir via [`use`](https://elixir-lang.org/getting-started/alias-require-and-import.html#use) macro and [rustler](https://github.com/rusterlium/rustler) which desugar into [gen_statem](https://andrealeopardi.com/posts/connection-managers-with-gen_statem/) [embedded] + - [ ] Flutter via [`builder_factories`](https://github.com/flutter/flutter/wiki/Code-generation-in-Flutter) (waiting for the [FFI](https://github.com/dart-lang/sdk/issues/34452) to be stable) [embedded] + - [ ] Typescript or **AssemblyScript** implemented as [typestate interface or abstract-class](https://spectrum.chat/statecharts/general/typestate-guard~d1ec4eb1-6db7-45bb-8b79-836c9a22cd5d) (usefull for building smart contract) [non-embedded] > For more info, see the changelog in the [release page][] diff --git a/docs/hook_cli.dot b/docs/hook_cli.dot index 74c36326..fe0666bd 100644 --- a/docs/hook_cli.dot +++ b/docs/hook_cli.dot @@ -8,8 +8,8 @@ digraph { node [shape=box] initial -> smcat [label=<json>] + smcat -> {"" [shape=point]} [dir=none, label="[ if no graphviz ]"] smcat -> dot [label=<dot>] - smcat -> {"" [shape=point]} [dir=none, label="[ no graphviz ]"] "" -> "graph-easy" [label=<dot>] image [shape=record, label="bmp|gif|jpg|png|tif"] @@ -17,11 +17,15 @@ digraph { terminal [shape=record, label="ascii|boxart"] lang1 [shape=record, label="smcat|json|scxml|xmi"] lang2 [shape=record, label="eps|fig|dot_json|pic|tk|vml|vrml"] + lang3 [shape=record, label="vcg|gdl|graphml", color=limegreen] compressed [shape=record, label="gd|gd2|svgz|vmlz|wbmp"] all [shape=record, label="svg|dot"] smcat -> all,lang1 [label=<-f smcat --as>] dot,"graph-easy" -> all,image,document [label=<-f graph --as>] - "graph-easy" -> terminal,pdf [label=<-f graph --as>] + "graph-easy" -> terminal,pdf,lang3 [label=<-f graph --as>] + lang3:gml -> cytoscape [color=grey] dot -> compressed,lang2 [label=<-f graph --as>] + + cytoscape [shape=ellipse,color=grey,fontcolor=slategrey] } \ No newline at end of file diff --git a/examples/xstate/nodejs b/examples/xstate/nodejs new file mode 160000 index 00000000..3709f43f --- /dev/null +++ b/examples/xstate/nodejs @@ -0,0 +1 @@ +Subproject commit 3709f43f833c3c82fd7d094e590e1db1f61234b5 diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 5ef098c7..664b8449 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -12,7 +12,8 @@ edition = "2018" [[bin]] name = "scrap" path = "src/main.rs" -test = false +test = false # TODO: remove integration test and replace it with evlish or bats + # CLI tests should be readable and close to shell scripting environment doc = false doctest = false @@ -30,23 +31,11 @@ prettyprint = "0.*" colored = "1" which = "2" regex = "1" +voca_rs = "1" [dependencies.clap] version = "2" -features = ["wrap_help"] - -# WARNING: This make compilation time doubled!! Seems cargo has serious issue here 😠 -[build-dependencies] -clap = "2" -scdlang = { path = "../core", version = "0.2.1" } -scdlang_xstate = { path = "../transpiler/xstate", version = "0.2.1" } -scdlang_smcat = { path = "../transpiler/smcat", version = "0.2.1" } -atty = "0.2" -rustyline = "5" -prettyprint = "0.*" -colored = "1" -which = "2" -regex = "1" +features = ["wrap_help", "color"] [dev-dependencies] predicates = "1" diff --git a/packages/cli/README.md b/packages/cli/README.md index 96717428..e5066e9e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -53,9 +53,9 @@ Unlocked features: - [x] `svg` -> Scalable Vector Graphics - [x] `dot` -> the DOT language - [x] `txt` -> Graph::Easy text - - [ ] `vcg` -> VCG (Visualizing Compiler Graphs - a subset of GDL) text - - [ ] `gdl` -> GDL (Graph Description Language) text - - [ ] `graphml` -> GraphML + - [x] `vcg` -> VCG (Visualizing Compiler Graphs - a subset of GDL) text + - [x] `gdl` -> GDL (Graph Description Language) text + - [x] `graphml` -> GraphML - [x] `bmp` -> Windows bitmap - [x] `gif` -> GIF - [ ] `hpgl` -> HP-GL/2 vector graphic diff --git a/packages/cli/build.rs b/packages/cli/build.rs deleted file mode 100644 index 05732f3c..00000000 --- a/packages/cli/build.rs +++ /dev/null @@ -1,27 +0,0 @@ -#![allow(unused_imports)] - -extern crate clap; -use clap::Shell::*; -use std::{env, error::Error, path::Path}; - -#[cfg(not(debug_assertions))] -include!("src/lib.rs"); - -#[cfg(not(debug_assertions))] -fn main() -> Result<(), Box> { - let ref out_dir = Path::new(&env::var("OUT_DIR")?).join("../../../"); - - let mut app = cli::build(); - let mut generate_completions = |shell| app.gen_completions(env!("CARGO_PKG_NAME"), shell, out_dir); - - for shell in &[Bash, Fish, Zsh, PowerShell, Elvish] { - generate_completions(*shell); - } - - Ok(()) -} - -#[cfg(debug_assertions)] -fn main() { - // skip if not build as release binary to save development time -} diff --git a/packages/cli/src/arg.rs b/packages/cli/src/arg.rs index e1e16051..6a674881 100644 --- a/packages/cli/src/arg.rs +++ b/packages/cli/src/arg.rs @@ -54,8 +54,6 @@ pub mod output { pub const FORMAT: &str = "format"; pub fn format<'o>() -> Arg<'o, 'o> { Arg::from_usage("[format] --as 'Select parser output'") - .requires(TARGET) - .hidden(which("smcat").is_err()) // TODO: don't hide it when support another output (e.g typescript) .possible_values(&{ let mut possible_formats = Vec::new(); possible_formats.merge_from_slice(&format::XSTATE); @@ -77,4 +75,14 @@ pub mod output { (TARGET, Some("graph"), "boxart"), ]) } + + // TODO: report bug that .requires(FORMAT) not work if .default_value_ifs() in format<'o>() is specify + + pub const EXPORT_NAME: &str = "export"; + pub const EXPORT_NAME_LIST: [&str; 3] = ["typescript", "javascript", "dts"]; + pub fn export_name<'o>() -> Arg<'o, 'o> { + Arg::from_usage("[export] --name 'Export name'") + .requires(FORMAT) + .empty_values(false) + } } diff --git a/packages/cli/src/cli.rs b/packages/cli/src/cli.rs index 05440735..46fbb316 100644 --- a/packages/cli/src/cli.rs +++ b/packages/cli/src/cli.rs @@ -1,5 +1,6 @@ use crate::{arg, commands::*}; -use clap::{App, ArgMatches, SubCommand}; +use atty::{self, Stream}; +use clap::{App, AppSettings::*, ArgMatches, SubCommand}; use std::error; #[rustfmt::skip] @@ -10,7 +11,7 @@ pub fn build<'a, 'b>() -> App<'a, 'b> { } pub fn run(matches: &ArgMatches) -> Result<()> { - Main::run_on(&matches)?; + Main::invoke(&matches)?; Eval::run_on(&matches)?; Code::run_on(&matches)?; Ok(()) @@ -25,7 +26,12 @@ pub trait CLI<'c> { fn additional_usage<'s>(cmd: App<'s, 'c>) -> App<'s, 'c>; fn command<'s: 'c>() -> App<'c, 's> { let cmd = SubCommand::with_name(Self::NAME); - Self::additional_usage(cmd).args_from_usage(Self::USAGE) + let app = Self::additional_usage(cmd).args_from_usage(Self::USAGE); + if atty::is(Stream::Stdout) { + app.setting(ColoredHelp) + } else { + app.setting(ColorNever) + } } fn invoke(args: &ArgMatches) -> Result<()>; diff --git a/packages/cli/src/commands/code/mod.rs b/packages/cli/src/commands/code/mod.rs index ecdd9eaf..1cdf935b 100644 --- a/packages/cli/src/commands/code/mod.rs +++ b/packages/cli/src/commands/code/mod.rs @@ -17,8 +17,10 @@ use scdlang_xstate as xstate; use std::{ fs::{self, File}, io::{BufRead, BufReader}, + path::Path, str, }; +use voca_rs::*; use which::which; pub struct Code; @@ -32,26 +34,42 @@ impl<'c> CLI<'c> for Code { fn additional_usage<'s>(cmd: App<'s, 'c>) -> App<'s, 'c> { cmd.visible_aliases(&["generate", "gen", "declaration", "declr"]) .about("Generate from scdlang file declaration to another format") - .args(&[output::dist(), output::target(), output::format()]) + .args(&[output::dist(), output::target(), output::format(), output::export_name()]) } fn invoke(args: &ArgMatches) -> Result<()> { - let filepath = args.value_of("FILE").unwrap_or_default(); - let target = args.value_of(output::TARGET).unwrap_or_default(); - let output_format = args.value_of(output::FORMAT).unwrap_or_default(); - let mut print = PRINTER(output_format); + let value_of = |arg| args.value_of(arg).unwrap_or_default(); + let (target, output_format) = (value_of(output::TARGET), value_of(output::FORMAT)); + let (mut print, filepath) = (PRINTER(output_format), value_of("FILE")); + let export_name = match args.value_of(output::EXPORT_NAME) { + Some(export_name) => export_name.to_string(), + None => { + let stem = Path::new(filepath).file_stem().expect("").to_str().unwrap(); + match output_format { + "dts" | "typescript" => case::pascal_case(stem), + "javascript" => case::camel_case(stem), + _ => stem.to_string(), + } + } + }; let mut machine: Box = match target { - "xstate" => Box::new(match output_format { - "json" => xstate::Machine::new(), - "typescript" => unreachable!("TODO: on the next update"), - _ => unreachable!("{} --as {:?}", Self::NAME, args.value_of(output::FORMAT)), + "xstate" => Box::new({ + use xstate::Config; + let mut machine = xstate::Machine::new(); + let config = machine.configure(); + config.set(&Config::Output, &output_format); + if output_format.one_of(&output::EXPORT_NAME_LIST) { + config.set(&Config::ExportName, &export_name); + } + machine }), "smcat" | "graph" => { + use smcat::{option::Mode::*, Config}; let mut machine = Box::new(smcat::Machine::new()); let config = machine.configure(); match output_format { - "ascii" | "boxart" => config.with_err_semantic(true), + "ascii" | "boxart" => config.with_err_semantic(true).set(&Config::Mode, &BlackboxState), _ => config.with_err_semantic(false), }; machine @@ -86,6 +104,10 @@ impl<'c> CLI<'c> for Code { machine.parse(&file)?; } + if let Some(warnings) = machine.collect_warnings()? { + PRINTER("erlang").change(Mode::Warning).prompt(&warnings, "WARNING"); + } + let machine = machine.to_string(); let result = if which("smcat").is_ok() && target.one_of(&["smcat", "graph"]) { use format::ext; diff --git a/packages/cli/src/commands/eval/mod.rs b/packages/cli/src/commands/eval/mod.rs index 8251fb61..8c7e777a 100644 --- a/packages/cli/src/commands/eval/mod.rs +++ b/packages/cli/src/commands/eval/mod.rs @@ -3,6 +3,7 @@ mod console; use crate::{ arg::output, cli::{Result, CLI}, + error::*, format, iter::*, print::*, @@ -42,21 +43,38 @@ If file => It will be overwriten everytime the REPL produce output, especially i ), output::target(), output::format(), + output::export_name().required_ifs( + output::EXPORT_NAME_LIST + .iter() + .map(|&v| ("format", v)) + .collect::>() + .as_slice(), + ), ]) } fn invoke(args: &ArgMatches) -> Result<()> { - let output_format = args.value_of(output::FORMAT).unwrap_or_default(); - let target = args.value_of(output::TARGET).unwrap_or_default(); - let mut repl: REPL = Editor::with_config(prompt::CONFIG()); + let value_of = |arg| args.value_of(arg).unwrap_or_default(); + let (target, output_format) = (value_of(output::TARGET), value_of(output::FORMAT)); + let (export_name, mut repl) = (value_of(output::EXPORT_NAME), Editor::with_config(prompt::CONFIG()) as REPL); let mut machine: Box = match target { - "xstate" => Box::new(xstate::Machine::new()), + "xstate" => Box::new({ + use xstate::*; + let mut machine = xstate::Machine::new(); + let config = machine.configure(); + config.set(&Config::Output, &output_format); + if output_format.one_of(&output::EXPORT_NAME_LIST) { + config.set(&Config::ExportName, &export_name); + } + machine + }), "smcat" | "graph" => { + use smcat::{option::Mode::*, Config}; let mut machine = Box::new(smcat::Machine::new()); let config = machine.configure(); match output_format { - "ascii" | "boxart" => config.with_err_semantic(true), + "ascii" | "boxart" => config.with_err_semantic(true).set(&Config::Mode, &BlackboxState), _ => config.with_err_semantic(false), }; machine @@ -146,6 +164,9 @@ If file => It will be overwriten everytime the REPL produce output, especially i }, line ); + if let Some(warnings) = machine.collect_warnings()? { + PRINTER("erlang").change(Mode::Warning).prompt(&warnings, "WARNING"); + } output(machine.to_string(), Some(header))?; } } diff --git a/packages/cli/src/commands/mod.rs b/packages/cli/src/commands/mod.rs index 6b0670f5..e5bf14e7 100644 --- a/packages/cli/src/commands/mod.rs +++ b/packages/cli/src/commands/mod.rs @@ -5,23 +5,44 @@ pub use code::*; pub use eval::*; use crate::cli::*; -use clap::{crate_description, crate_version, App, AppSettings::*, ArgMatches}; +use clap::{crate_description, crate_version, App, AppSettings::*, Arg, ArgMatches, Shell}; +use std::{env, io}; pub struct Main; impl<'c> CLI<'c> for Main { const NAME: &'c str = "Statecharts Rhapsody"; const USAGE: &'c str = ""; - fn command<'s: 'c>() -> App<'c, 's> { - let cmd = App::new(Self::NAME); - cmd.settings(&[VersionlessSubcommands, SubcommandRequiredElseHelp]) - } - fn additional_usage<'s>(cmd: App<'s, 'c>) -> App<'s, 'c> { - cmd.version(crate_version!()).about(crate_description!()) + cmd.settings(&[VersionlessSubcommands, ArgRequiredElseHelp]) + .version(crate_version!()) + .about(crate_description!()) + .arg( + Arg::from_usage("--shell-completion [shell] 'Generate shell completion'").possible_values(&[ + "bash", + "zsh", + "fish", + "elvish", + "powershell", + ]), + ) } - fn invoke(_matches: &ArgMatches) -> Result<()> { + fn invoke(args: &ArgMatches) -> Result<()> { + if let (Some(shell), Ok(bin_path)) = (args.value_of("shell-completion"), env::current_exe()) { + build().gen_completions_to( + bin_path.file_stem().map(|s| s.to_str().expect("utf-8")).expect("appname"), + match shell { + "bash" => Shell::Bash, + "zsh" => Shell::Zsh, + "fish" => Shell::Fish, + "elvish" => Shell::Elvish, + "powershell" => Shell::PowerShell, + _ => unreachable!("shell {} not supported", shell), + }, + &mut io::stdout(), + ) + } Ok(()) } } diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index 31084d6b..6a6668a8 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -6,6 +6,8 @@ use prettyprint::PrettyPrint; use scdlang; use std::*; +const HEADER: &str = "ERROR"; + #[derive(Debug)] pub enum Error<'s> { Count { @@ -55,7 +57,7 @@ impl Report for Box { use scdlang::Error::*; match err { Deadlock => prompting(&err.to_string()), - _ => print.prompt(&err.to_string(), "can't parse"), + _ => print.prompt(&err.to_string(), HEADER), } } else { prompting(&self.to_string()) @@ -71,7 +73,7 @@ impl Report for Error<'_> { fn report_and_exit(&self, default_exit_code: Option, _: Option) { let print = PRINTER("haskell").change(Mode::Error); match self { - Error::StreamParse(err) => print.prompt(&err.to_string(), "can't parse"), + Error::StreamParse(err) => print.prompt(&err.to_string(), HEADER), _ => prompting(&self.to_string()), }; if let Some(exit_code) = default_exit_code { @@ -107,7 +109,7 @@ impl fmt::Display for Error<'_> { fn error(message: &str) -> String { if atty::is(Stream::Stderr) { - format!("{} {}", prompt::ERROR.red().bold(), message.magenta()) + format!("{} {}", prompt::ERROR.cyan(), message.italic().bold()) } else { format!("{} {}", prompt::ERROR, message) } @@ -119,7 +121,7 @@ impl Prompt for PrettyPrint { } } -trait Prompt { +pub trait Prompt { fn prompt(&self, message: &str, title: &str) { if atty::is(Stream::Stderr) { self.print(message, title) diff --git a/packages/cli/src/lib.rs b/packages/cli/src/lib.rs index a2367575..2ec688bd 100644 --- a/packages/cli/src/lib.rs +++ b/packages/cli/src/lib.rs @@ -27,7 +27,7 @@ pub mod prompt { use rustyline::config::{self, *}; pub const REPL: &str = "Β»"; - pub const ERROR: &str = "ERROR:"; + pub const ERROR: &str = "severity:"; // TODO: PR are welcome πŸ˜† pub const CONFIG: fn() -> config::Config = || { @@ -47,6 +47,7 @@ pub mod print { REPL, Debug, Error, + Warning, MultiLine, UseHeader, @@ -62,18 +63,22 @@ pub mod print { impl PrinterChange for PrettyPrint { fn change(&self, mode: Mode) -> Self { + let stderr = |theme| { + self.configure() + .grid(true) + .header(true) + .theme(theme) + .paging_mode(PagingMode::Error) + .build() + }; (match mode { Mode::Plain => self.configure().grid(false).header(false).line_numbers(false).build(), Mode::UseHeader => self.configure().grid(true).header(true).build(), Mode::MultiLine => self.configure().grid(true).build(), Mode::REPL => self.configure().line_numbers(true).grid(true).build(), Mode::Debug => self.configure().line_numbers(true).grid(true).header(true).build(), - Mode::Error => self - .configure() - .grid(true) - .theme("Sublime Snazzy") - .paging_mode(PagingMode::Error) - .build(), + Mode::Error => stderr("Sublime Snazzy"), + Mode::Warning => stderr("OneHalfDark"), }) .unwrap() // because it only throw error if field not been initialized } @@ -95,6 +100,7 @@ pub mod print { .theme("TwoDark") .language(match lang { "smcat" => "perl", + "dts" => "typescript", "scxml" | "xmi" => "xml", "ascii" | "boxart" => "txt", _ => lang, @@ -149,7 +155,7 @@ pub mod iter { // TODO: Hacktoberfest pub mod format { - pub const XSTATE: [&str; 1] = ["json" /*, typescript*/]; + pub const XSTATE: [&str; 4] = ["json", "dts", "javascript", "typescript"]; pub const SMCAT: &str = "json"; #[rustfmt::skip] pub const BLOB: [&str; 13] = ["bmp", "gd", "gd2", "gif", "jpg", "jpeg", "jpe", "png", "svgz", "tif", "tiff", "vmlz", "webmp"]; @@ -158,7 +164,7 @@ pub mod format { pub mod ext { pub const SMCAT: [&str; 7] = ["svg", "dot", "smcat", "json", "html", "scxml", "xmi"]; pub const DOT: [&str; 32] = ["bmp", "canon", "dot", "gv", "xdot", "eps", "fig", "gd", "gd2", "gif", "jpg", "jpeg", "jpe", "json", "json0", "dot_json", "xdot_json", "pic", "plain", "plain-ext", "png", "ps", "ps2", "svg", "svgz", "tif", "tiff", "tk", "vml", "vmlz", "vrml", "wbmp"]; - pub const GRAPH_EASY: [&str; 13] = ["ascii", "boxart", "svg", "dot", "txt", "bmp", "gif", "jpg", "pdf", "png", "ps", "ps2", "tif"]; + pub const GRAPH_EASY: [&str; 16] = ["ascii", "boxart", "svg", "dot", "txt", "vcg", "gdl", "graphml", "bmp", "gif", "jpg", "pdf", "png", "ps", "ps2", "tif"]; } pub fn into_legacy_dot(input: &str) -> String { diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index ed7cc610..1125aa38 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -11,10 +11,16 @@ categories = ["parsing"] edition = "2018" [dependencies] +# parser + context-free-grammar pest = "2" pest_derive = "2" -lazy_static = "1" -either = "1" +# helper +either = "1" # a workaround for iterator.filter that return Result +static_assertions = "0.3" # design-by-contract at compile time + +[dependencies.once_cell] # to define the global caches +version = "0.2" +default-features = false # disable parking_lot [badges] maintenance = { status = "actively-developed" } \ No newline at end of file diff --git a/packages/core/src/cache.rs b/packages/core/src/cache.rs index 14383818..dfbd3c91 100644 --- a/packages/core/src/cache.rs +++ b/packages/core/src/cache.rs @@ -1,53 +1,62 @@ //! Collection of cache variables which help detecting semantics error. // TODO: πŸ€” use hot reload https://crates.rs/crates/warmy when import statement is introduced use crate::error::Error; -use lazy_static::lazy_static; -use std::{ - any::Any, - collections::HashMap, - hash::Hash, - sync::{Mutex, MutexGuard}, -}; - -const NUM_OF_CACHES: usize = 1; /*update πŸ‘ˆ when adding another type of caches*/ +use once_cell::sync::Lazy; +use std::{collections::HashMap, sync::*}; // TODO: replace with https://github.com/rust-lang-nursery/lazy-static.rs/issues/111 when resolved // πŸ€” or is there any better way? // pub static mut TRANSITION: Option> = None; // doesn't work! // type LazyMut = Mutex>; -lazy_static! { - static ref TRANSITION: Mutex = Mutex::new(HashMap::new()); - /*reserved for another caches*/ -} +static TRANSITION: Lazy> = Lazy::new(|| Mutex::new(HashMap::new())); +static WARNING: Lazy> = Lazy::new(|| RwLock::new(WarningMap::new())); +/*reserved for another caches*/ /// Access cached transition safely -pub fn transition<'a>() -> Result, Error> { +pub fn transition<'a>() -> Result, Error> { TRANSITION.lock().map_err(|_| Error::Deadlock) } +/// write only caches +pub mod write { + use super::*; + /// Access cached warnings safely + pub fn warning<'a>() -> Result, Error> { + WARNING.write().map_err(|_| Error::Deadlock) + } +} + +/// read only caches +pub mod read { + use super::*; + /// Access cached warnings safely + pub fn warning<'a>() -> Result, Error> { + WARNING.read().map_err(|_| Error::Deadlock) + } +} + /// Clear cache data but preserve the allocated memory for reuse. -pub fn clear<'c>() -> Result, Error> { - let mut cache_list = [ - TRANSITION.lock().map_err(|_| Error::Deadlock)?, - /*reserved for another caches*/ - ]; - cache_list.iter_mut().for_each(|c| c.clear()); - Ok(Shrink(cache_list)) +pub fn clear() -> Result { + transition()?.clear(); + write::warning()?.clear(); + /*reserved for another caches*/ + Ok(Shrink) } -pub struct Shrink<'c, K: Eq + Hash, V: Any>(CacheList<'c, K, V>); -impl Shrink<'_, K, V> { +pub struct Shrink; +impl Shrink { /// Shrinks the allocated memory as much as possible - pub fn shrink(mut self) -> Result<(), Error> { - self.0.iter_mut().for_each(|c| c.shrink_to_fit()); + pub fn shrink(self) -> Result<(), Error> { + transition()?.shrink_to_fit(); + write::warning()?.shrink_to_fit(); + /*reserved for another caches*/ Ok(()) } } // TODO: πŸ€” consider using this approach http://idubrov.name/rust/2018/06/01/tricking-the-hashmap.html -type CacheList<'c, K, V> = [MutexGuard<'c, HashMap>; NUM_OF_CACHES]; -type MapTransition = HashMap>; - -type CurrentState = String; -type NextState = String; -type Trigger = Option; +pub(crate) type TransitionMap = HashMap>; +pub(crate) type WarningMap = HashMap; +pub(crate) type CurrentState = String; +pub(crate) type NextState = String; +pub(crate) type Trigger = Option; diff --git a/packages/core/src/core/builder.rs b/packages/core/src/core/builder.rs index 07860276..bb19460d 100644 --- a/packages/core/src/core/builder.rs +++ b/packages/core/src/core/builder.rs @@ -1,7 +1,8 @@ use crate::{cache, error::Error, external::Builder}; use pest_derive::Parser; +use std::collections::*; -#[derive(Parser, Default, Clone)] // πŸ€” is it wise to derive from Copy&Clone ? +#[derive(Debug, Parser, Default, Clone)] // πŸ€” is it wise to derive from Copy&Clone ? #[grammar = "grammar.pest"] /** __Core__ parser and also [`Builder`]. @@ -33,15 +34,18 @@ pub struct Scdlang<'g> { pub(super) clear_cache: bool, //-|in case for program that need to disable…| pub semantic_error: bool, //-|…then enable semantic error at runtime| + + derive_config: Option>, } impl<'s> Scdlang<'s> { /// This method is prefered for instantiating /// than using [`Default::default()`](https://doc.rust-lang.org/std/default/trait.Default.html#tymethod.default) pub fn new() -> Self { - Self { + Scdlang { clear_cache: true, semantic_error: true, + derive_config: Option::default(), ..Default::default() } } @@ -77,12 +81,29 @@ impl<'g> Builder<'g> for Scdlang<'g> { self.clear_cache = default; self } + + fn set(&mut self, key: &'g dyn AsRef, value: &'g dyn AsRef) -> &mut dyn Builder<'g> { + match self.derive_config.as_mut() { + Some(config) => { + config + .entry(key.as_ref()) + .and_modify(|val| *val = value.as_ref()) + .or_insert_with(|| value.as_ref()); + } + None => self.derive_config = Some([(key.as_ref(), value.as_ref())].iter().cloned().collect()), + }; + self + } + + fn get(&self, key: &'g dyn AsRef) -> Option<&'g str> { + self.derive_config.as_ref()?.get(key.as_ref()).cloned() + } } impl<'g> Drop for Scdlang<'g> { fn drop(&mut self) { if self.clear_cache { - clear_cache().expect("Deadlock") + clear_cache().expect("no Deadlock") } } } diff --git a/packages/core/src/core/parser.rs b/packages/core/src/core/parser.rs index 82b48e6c..461b6b06 100644 --- a/packages/core/src/core/parser.rs +++ b/packages/core/src/core/parser.rs @@ -11,8 +11,12 @@ use std::{fmt, *}; # Examples ``` +# use std::error::Error; +use scdlang::parse; + let token_pairs = parse("A -> B")?; -println("{:#?}", token_pairs); +println!("{:#?}", token_pairs); +# Ok::<(), Box>(()) ``` */ pub fn parse(source: &str) -> Result, RuleError> { >::parse(Rule::DescriptionFile, source) diff --git a/packages/core/src/error/format.rs b/packages/core/src/error/format.rs index 2001f300..b04114d2 100644 --- a/packages/core/src/error/format.rs +++ b/packages/core/src/error/format.rs @@ -9,10 +9,9 @@ impl FineTune for PestError { if let ParsingError { positives, negatives } = self.variant { let negatives = negatives.excludes(&[EOI, PASCAL_CASE, QUOTED]); - let mut positives = positives.excludes(&[EOI]); + let mut positives = positives.excludes(&[EOI, expression]); positives = match &positives[..] { [Symbol::at, Name::state] => vec![Symbol::at], - [Rule::expression, Symbol::at] => vec![Symbol::at], _ => positives, }; @@ -41,6 +40,7 @@ impl<'t> Scdlang<'t> { Symbol::arrow::right => "->".to_string(), Symbol::arrow::left => "<-".to_string(), Symbol::at => "@".to_string(), + Symbol::triangle::right => "|>".to_string(), Rule::transition => "-->, <--, ->>, <<-, >->, <-<, or <->".to_string(), _ => format!("{:?}", rule), }) diff --git a/packages/core/src/error/mod.rs b/packages/core/src/error/mod.rs index 6652ce8e..dd364f41 100644 --- a/packages/core/src/error/mod.rs +++ b/packages/core/src/error/mod.rs @@ -6,7 +6,6 @@ use pest; pub type PestError = pest::error::Error; -#[allow(deprecated)] // false alarm on clippy πŸ˜… #[derive(Debug)] /// Parse-related error type. // WARNING: πŸ‘‡ adding lifetime annotation can cause lifetime refactoring hell πŸ’’ (it will break Parser trait) @@ -14,10 +13,18 @@ pub enum Error { /// Happen when there is syntax or semantics error Parse(Box), + /// Used internally on severity: WARNING + WithId { id: u64, error: Box }, + /// Can happen when accessing caches unsafely Deadlock, } +pub(crate) struct ErrorMap { + pub id: u64, + pub message: String, +} + #[cfg(test)] mod variant { #![allow(clippy::unit_arg)] @@ -43,7 +50,7 @@ mod variant { | "A -> B->" | "A -> B PascalCase" | "A -> B 'quoted'" - | "A -> B invalid name" => assert_eq!("expected @", message), + | "A -> B invalid name" => assert_eq!("expected @ or |>", message), _ => unreachable!("{}", expression), }, ParsingError { .. } => unimplemented!("TODO: implement this if there is any case for that"), diff --git a/packages/core/src/external.rs b/packages/core/src/external.rs index 05d8d7cf..45ca4484 100644 --- a/packages/core/src/external.rs +++ b/packages/core/src/external.rs @@ -5,10 +5,19 @@ Useful for creating transpiler, codegen, or even compiler. 1. Implement trait [`Parser`] on struct with [`Scdlang`] field (or any type that implement [`Builder`]). ```no_run -#[derive(Default)] +use scdlang::{*, prelude::*}; +use std::{error, fmt}; +pub mod prelude { + pub use scdlang::external::*; +} + +#[derive(Debug, Default)] +struct Schema {} + +#[derive(Debug, Default)] pub struct Machine<'a> { builder: Scdlang<'a>, // or any type that implmenet trait `Builder` - schema: std::any::Any, + schema: Schema, } impl Machine<'_> { @@ -20,12 +29,18 @@ impl Machine<'_> { } } +impl fmt::Display for Machine<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.schema) + } +} + impl<'a> Parser<'a> for Machine<'a> { - fn configure(&mut self) -> &mut Builder<'a> { + fn configure(&mut self) -> &mut dyn Builder<'a> { &mut self.builder } - fn parse(&mut self, source: &str) -> Result<(), DynError> { + fn parse(&mut self, source: &str) -> Result<(), Box> { self.clean_cache()?; unimplemented!(); } @@ -42,6 +57,8 @@ impl<'a> Parser<'a> for Machine<'a> { 2. Then it can be used like this: ```ignore +use scdlang::external::*; + let parser: Box = Box::new(match args { Some(text) => module_a::Machine::try_parse(text)?, None => module_b::Machine::new(), @@ -64,7 +81,6 @@ parser.parse("Off -> On @ Power")?; use crate::{cache, Scdlang}; use std::{error::Error, fmt}; -#[rustfmt::skip] /** A Trait which external parser must implement. This trait was mean to be used outside the core. @@ -74,11 +90,22 @@ pub trait Parser<'t>: fmt::Display { fn parse(&mut self, source: &str) -> Result<(), BoxError>; /// Parse `source` then insert/append the results. fn insert_parse(&mut self, source: &str) -> Result<(), BoxError>; - /// Parse `source` while instantiate the Parser. - fn try_parse(source: &str, options: Scdlang<'t>) -> Result where Self: Sized; + fn try_parse(source: &str, options: Scdlang<'t>) -> Result + where + Self: Sized; + /// Configure the parser. fn configure(&mut self) -> &mut dyn Builder<'t>; + /// Get all warnings messages (all messages are prettified) + fn collect_warnings<'e>(&self) -> Result, DynError<'e>> { + let messages = cache::read::warning()? + .values() + .map(|s| s.as_str()) + .collect::>() + .join("\n\n"); + Ok(Some(messages).filter(|s| !s.is_empty())) + } /// Completely clear the caches which also deallocate the memory. fn flush_cache<'e>(&'t self) -> Result<(), DynError<'e>> { @@ -108,6 +135,13 @@ pub trait Builder<'t> { fn with_err_path(&mut self, path: &'t str) -> &mut dyn Builder<'t>; /// Set the line_of_code offset of the error essages. fn with_err_line(&mut self, line: usize) -> &mut dyn Builder<'t>; + + // WARNING: `Any` is not supported because trait object can't have generic methods + + /// Set custom config. Used on derived Parser. + fn set(&mut self, key: &'t dyn AsRef, value: &'t dyn AsRef) -> &mut dyn Builder<'t>; + /// Get custom config. Used on derived Parser. + fn get(&self, key: &'t dyn AsRef) -> Option<&'t str>; } type DynError<'t> = Box; diff --git a/packages/core/src/grammar.pest b/packages/core/src/grammar.pest index 2fdc32a8..555233de 100644 --- a/packages/core/src/grammar.pest +++ b/packages/core/src/grammar.pest @@ -4,13 +4,16 @@ DescriptionFile = _{ SOI ~ CRLF? ~ ( ) ~ NEWLINE* )* ~ EOI } -expression = { (self_transition|(StateName ~ transition)) ~ trigger? } - self_transition = { LoopTo ~ StateName } - transition = { ( TransitionToggle - | TransientLoopTo | LoopTo | TransitionTo - | TransientLoopFrom | LoopFrom | TransitionFrom - ) ~ StateName } - trigger = { TriggerAt ~ EventName } +expression = { ( internal_transition | ((self_transition | (StateName ~ transition)) ~ trigger? ~ action?) ) } + self_transition = { LoopTo ~ StateName } + internal_transition = { StateName ~ trigger ~ action } + transition = { ( TransitionToggle + | TransientLoopTo | LoopTo | TransitionTo + | TransientLoopFrom | LoopFrom | TransitionFrom + ) ~ StateName } + trigger = { TriggerAt ~ (guard|(EventName ~ guard?)) } + guard = _{ "[" ~ guardName ~ "]" } + action = { PlayNext ~ actionName } // #region symbol/operator TransitionTo = @{ "-"+ ~ ">" } @@ -21,14 +24,18 @@ LoopFrom = @{ "<<" ~ "-"+ } TransientLoopTo = @{ ">" ~ "-"+ ~ ">" } TransientLoopFrom = @{ "<" ~ "-"+ ~ "<" } TriggerAt = @{ "@" } +PlayNext = @{ "|>" } // #endregion // #region name -StateName = ${ PASCAL_CASE|QUOTED } -EventName = ${ PASCAL_CASE|QUOTED } +StateName = ${ PASCAL_CASE|QUOTED } +EventName = ${ PASCAL_CASE|QUOTED } +actionName = ${ CAMEL_CASE|QUOTED } +guardName = ${ CAMEL_CASE|QUOTED } // #endregion PASCAL_CASE = @{ ASCII_ALPHA_UPPER ~ ASCII_ALPHANUMERIC* } +CAMEL_CASE = @{ ASCII_ALPHA_LOWER ~ ASCII_ALPHANUMERIC* } QUOTED = @{ single_quote|double_quote|backtick } single_quote = _{ "'" ~ (!(NEWLINE|"'") ~ "\\"? ~ ANY)* ~ "'" } double_quote = _{ "\"" ~ (!(NEWLINE|"\"") ~ "\\"? ~ ANY)* ~ "\"" } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index facdec9a..9e7a3779 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -10,6 +10,8 @@ pub mod utils; pub use crate::core::{parse, Scdlang}; pub use error::Error; pub use external::Parser as Transpiler; +pub use external::Parser as Compiler; +pub use external::Parser as Codegen; /// A prelude providing convenient access to commonly-used features of scdlang core parser. pub mod prelude { @@ -65,6 +67,12 @@ pub mod grammar { TransientLoopFrom as left, }; } + + pub mod triangle { + pub use crate::core::Rule::{ + PlayNext as right, + }; + } } #[allow(non_snake_case)] @@ -73,7 +81,9 @@ pub mod grammar { pub mod Name { pub use super::Rule::{ StateName as state, - EventName as event + EventName as event, + actionName as action, + guardName as guard, }; } } diff --git a/packages/core/src/semantics/graph.rs b/packages/core/src/semantics/graph.rs index ca0a43e4..e2b0acf8 100644 --- a/packages/core/src/semantics/graph.rs +++ b/packages/core/src/semantics/graph.rs @@ -1,5 +1,6 @@ //! Module that contain semantics graph of Scdlang which modeled as [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph) #![allow(dead_code)] +use std::fmt::{self, Display}; #[derive(Debug, Clone)] /// SCXML equivalent: @@ -10,8 +11,9 @@ /// ``` pub struct Transition<'t> { pub from: State<'t>, - pub to: State<'t>, + pub to: Option>, pub at: Option>, + pub run: Option>, pub kind: TransitionType<'t>, // πŸ€” maybe I should hide it then implement kind() method } @@ -30,7 +32,9 @@ pub enum TransitionType<'t> { state: &'t State<'t>, kind: &'t TransitionType<'t>, }, - Normal, // πŸ€” should I implement Default trait? + Internal, + // FIXME: encapsulate πŸ‘‡ as External variant + Normal, Toggle, Loop { transient: bool, @@ -59,17 +63,37 @@ impl Into for &State<'_> { } } -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] /// SCXML equivalent: /// ```scxml /// /// ``` -pub struct Event<'s> { +pub struct Event<'at> { // pub kind: &'s EventType, // πŸ€” probably should not be a field but more like kind() method because the type can be deduce on the available field - pub name: &'s str, // TODO: should be None when it only have a Guard or it just an Internal Event + pub name: Option<&'at str>, // should be None when it only have a Guard or "it just an Internal Event" + pub guard: Option<&'at str>, +} + +impl Display for Event<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{event}{guard}", + event = self.name.unwrap_or(""), + guard = match self.guard { + Some(guard_name) => ["[", guard_name, "]"].concat(), + None => String::new(), + } + ) + } +} + +#[derive(Debug, Default, Clone)] +pub struct Action<'play> { + pub name: &'play str, } -impl Into for &Event<'_> { +impl Into for &Action<'_> { fn into(self) -> String { self.name.to_string() } diff --git a/packages/core/src/semantics/kind.rs b/packages/core/src/semantics/kind.rs index 66e9992c..44fb685c 100644 --- a/packages/core/src/semantics/kind.rs +++ b/packages/core/src/semantics/kind.rs @@ -1,6 +1,8 @@ use crate::{utils::naming::Name, Error}; use std::{any::Any, fmt::Debug}; +// TODO: test this in BDD style (possibly via cucumber-rust) + #[derive(Debug)] /// An enum returned by [Scdlang.iter_from](../struct.Scdlang.html#method.iter_from) /// to access semantics type/kind @@ -24,6 +26,10 @@ pub enum Found { None, } +pub trait Check { + fn semantic_check(&self) -> Result; +} + #[rustfmt::skip] /** Everything that can change state @@ -31,14 +37,12 @@ Example: ```scl A -> B ``` */ -pub trait Expression: Debug { +pub trait Expression: Debug + Check { fn current_state(&self) -> Name; - fn next_state(&self) -> Name; + fn next_state(&self) -> Option; fn event(&self) -> Option; - fn semantic_check(&self) -> Result; - fn action(&self) -> Option<&Any/*πŸ‘ˆTBD*/> { - unimplemented!("TBD") - } + fn guard(&self) -> Option; + fn action(&self) -> Option; } /** [UNIMPLEMENTED] Mostly everything that use curly braces. @@ -48,7 +52,7 @@ Example: state A {} ``` πŸ€” I wonder if curly braces that can expand into multiple transition is included */ -pub trait Declaration: Debug { +pub trait Declaration: Debug + Check { /// e.g: `@entry |> doSomething` fn statements(&self) -> Option>; @@ -65,8 +69,8 @@ Example: A |> doSomething ``` or just a shorthand for writing a declaration in one line */ -pub trait Statement: Debug { +pub trait Statement: Debug + Check { fn state(&self) -> Option; - fn action(&self) -> Option<&Any /*πŸ‘ˆTBD*/>; + fn action(&self) -> Option<&dyn Any /*πŸ‘ˆTBD*/>; fn event(&self) -> Option; } diff --git a/packages/core/src/semantics/mod.rs b/packages/core/src/semantics/mod.rs index 3f2bca65..eb409161 100644 --- a/packages/core/src/semantics/mod.rs +++ b/packages/core/src/semantics/mod.rs @@ -11,26 +11,41 @@ pub use kind::*; pub(super) mod analyze { // WARNING: move this on separate file when it became more complex use super::*; - use crate::{error::Error, grammar::Rule, Scdlang}; + use crate::{cache, error::*, grammar::Rule, Scdlang}; use pest::{iterators::Pair, Span}; pub type TokenPair<'i> = Pair<'i, Rule>; - pub trait SemanticCheck: Expression { + // TODO: report visibility of pub(crate) inside pub(super) as bugs + // it's a workaround for ErrorMap + pub(crate) trait SemanticCheck: Expression { fn check_error(&self) -> Result, Error>; + fn check_warning(&self) -> Result, Error> { + Ok(None) + } } /// A Trait that must be implmented for doing semantics checking. pub trait SemanticAnalyze<'c>: From> { + fn analyze_warning(&self, _span: Span<'c>, _options: &'c Scdlang) -> Result<(), Error> { + Ok(()) + } + // span is not borrowed because PestError::new_from_span(..) is consumable fn analyze_error(&self, span: Span<'c>, options: &'c Scdlang) -> Result<(), Error>; /// Perform full semantics analysis from pest::iterators::Pair. fn analyze_from(pair: TokenPair<'c>, options: &'c Scdlang) -> Result { - let span = pair.as_span(); - let sc = pair.into(); - Self::analyze_error(&sc, span, options)?; + let this = pair.clone().into(); + // WARNING: there is possibility that one expression can contain both error and warning because of sugar syntax (<->, ->>, >->) + Self::analyze_error(&this, pair.as_span(), options)?; + if let Err(Error::WithId { id, error }) = Self::analyze_warning(&this, pair.as_span(), options) { + cache::write::warning()? + .entry(id) + .and_modify(|e| *e = error.to_string()) + .or_insert_with(|| error.to_string()); + } // reserved for another analysis! πŸ’ͺ - Ok(sc) + Ok(this) } fn into_kinds(self) -> Vec>; diff --git a/packages/core/src/semantics/transition/analyze.rs b/packages/core/src/semantics/transition/analyze.rs index 6045d3e2..0d74c8ec 100644 --- a/packages/core/src/semantics/transition/analyze.rs +++ b/packages/core/src/semantics/transition/analyze.rs @@ -1,26 +1,28 @@ -use super::helper::prelude::*; -use crate::{cache, semantics, utils::naming::sanitize, Error}; +use super::helper::{prelude::*, transform_key::*}; +use crate::{cache, error::*, semantics, utils::*}; use semantics::{analyze::*, Kind, Transition}; impl SemanticCheck for Transition<'_> { fn check_error(&self) -> Result, Error> { - let mut cache_transition = cache::transition()?; - - let (current, target) = (sanitize(self.from.name), sanitize(self.to.name)); - let t_cache = cache_transition.entry(current).or_default(); + // (key, value) = (EventName + guardName, NextState) + let (mut cache, target) = ( + cache::transition()?, + naming::sanitize(self.to.as_ref().unwrap_or(&self.from).name), + ); + let t_cache = self.cache_current_state(&mut cache); Ok(match &self.at { - Some(trigger) => { - if t_cache.contains_key(&None) { + Some(event) => { + if t_cache.contains_key(&None) && event.name.is_some() { Some(self.warn_conflict(&t_cache)) - } else if let Some(prev_target) = t_cache.insert(Some(trigger.into()), target) { + } else if let Some(prev_target) = t_cache.insert(Some(event.into()), target) { Some(self.warn_duplicate(&prev_target)) } else { None } } None => { - if t_cache.keys().any(Option::is_some) { + if t_cache.keys().any(EventKey::has_trigger) { Some(self.warn_conflict(&t_cache)) } else if let Some(prev_target) = t_cache.insert(None, target) { Some(self.warn_duplicate(&prev_target)) @@ -30,6 +32,33 @@ impl SemanticCheck for Transition<'_> { } }) } + + fn check_warning(&self) -> Result, Error> { + // (key, value) = (EventName + guardName, NextState) + let (mut cache, target) = ( + cache::transition()?, + naming::sanitize(self.to.as_ref().unwrap_or(&self.from).name), + ); + let t_cache = self.cache_current_state(&mut cache); + + // WARNING: don't insert anything since the insertion is already done in analyze_error >-down-to-> check_error + Ok(self.at.as_ref().and_then(|event| { + let has_same_event = t_cache + .keys() + .filter(|&key| key != &Some(event.into())) + .any(|key| key.get_trigger() == event.name); + let has_different_target = t_cache.values().any(|key| key != &target); + + if t_cache.keys().any(EventKey::has_guard) && event.guard.is_some() && has_same_event && has_different_target { + Some(ErrorMap { + id: (self.from.name, self.at.as_ref().and_then(|e| e.name)).get_hash(), + message: self.warn_nondeterministic(t_cache), + }) + } else { + None + } + })) + } } impl<'t> SemanticAnalyze<'t> for Transition<'t> { @@ -43,6 +72,19 @@ impl<'t> SemanticAnalyze<'t> for Transition<'t> { Ok(()) } + fn analyze_warning(&self, span: Span<'t>, options: &'t Scdlang) -> Result<(), Error> { + let make_error = |message| options.err_from_span(span, message).into(); + for transition in self.clone().into_iter() { + if let Some(err) = transition.check_warning()? { + return Err(Error::WithId { + id: err.id, + error: make_error(err.message), + }); + } + } + Ok(()) + } + fn into_kinds(self) -> Vec> { let mut kinds = Vec::new(); for transition in self.into_iter() { @@ -53,39 +95,74 @@ impl<'t> SemanticAnalyze<'t> for Transition<'t> { } use std::collections::HashMap; +type CacheMap = HashMap, String>; +type CachedTransition<'state> = MutexGuard<'state, cache::TransitionMap>; + +impl<'t> Transition<'t> { + fn cache_current_state<'a>(&self, cache: &'t mut CachedTransition<'a>) -> &'t mut CacheMap { + cache.entry(naming::sanitize(self.from.name)).or_default() + } +} + impl Transition<'_> { fn warn_duplicate(&self, prev_target: &str) -> String { match &self.at { Some(trigger) => format!( "duplicate transition: {} -> {},{} @ {}", - self.from.name, self.to.name, prev_target, trigger.name + self.from.name, + self.to.as_ref().unwrap_or(&self.from).name, + prev_target, + trigger ), None => format!( - "duplicate transient transition: {} -> {},{}", - self.from.name, self.to.name, prev_target + "duplicate transient-transition: {} -> {},{}", + self.from.name, + self.to.as_ref().unwrap_or(&self.from).name, + prev_target ), } } - fn warn_conflict(&self, cache_target: &HashMap, String>) -> String { + fn warn_conflict(&self, cache: &CacheMap) -> String { + const REASON: &str = "never had a chance to trigger"; match &self.at { - Some(_) => { - let prev_target = cache_target.get(&None).unwrap(); - format!("conflict with: {} -> {}", self.from.name, prev_target) + Some(event) => { + let (prev_target, trigger) = ( + cache.get(&None).expect("cache without trigger"), + event.name.expect("not auto-transition"), + ); + format!("{} {} because: {} -> {}", trigger, REASON, self.from.name, prev_target) } None => { - let prev_targets: Vec<&str> = cache_target - .iter() - .filter_map(|(trigger, target)| trigger.as_ref().and(Some(target.as_str()))) - .collect(); - let prev_triggers: Vec = cache_target.keys().filter_map(ToOwned::to_owned).collect(); - format!( - "conflict with: {} -> {} @ {}", - self.from.name, - prev_targets.join(","), - prev_triggers.join(",") - ) + let caches = cache.iter().filter(|(event, _)| event.has_trigger()); + + let mut messages = format!("conflict with {} {{\n", self.from.name); + for (event, target) in caches.clone() { + writeln!(&mut messages, "\t -> {}{}", target, event.as_expression()).expect("utf-8"); + } + messages += " }"; + + let triggers = caches.filter_map(|(event, _)| event.get_trigger()).collect::>(); + write!(&mut messages, "\n because {} {}", triggers.join(","), REASON).expect("utf-8"); + messages + } + } + } + + fn warn_nondeterministic(&self, cache: &CacheMap) -> String { + let mut messages = String::from("non-deterministic transition of "); + match &self.at { + Some(event) => { + let guards = cache.keys().filter_map(|k| k.guards_with_same_trigger(event.name)); + + writeln!(&mut messages, "{}{} {{", self.from.name, event.name.as_expression()).expect("utf-8"); + for guard in guards { + let target = cache.get(&event.name.as_key(&guard)).map(String::as_str).unwrap_or(""); + writeln!(&mut messages, "\t-> {} @ [{}]", target, guard).expect("utf-8"); + } + messages + " }" } + None => unreachable!("there is no such thing as \"non-deterministic transient transition\""), } } } diff --git a/packages/core/src/semantics/transition/convert.rs b/packages/core/src/semantics/transition/convert.rs index 5160555a..c3938402 100644 --- a/packages/core/src/semantics/transition/convert.rs +++ b/packages/core/src/semantics/transition/convert.rs @@ -1,13 +1,13 @@ #![allow(deprecated)] use super::helper::{get, prelude::*}; -use crate::semantics::{Event, StateType, Transition, TransitionType}; +use crate::semantics::{Action, StateType, Transition, TransitionType}; impl<'t> From> for Transition<'t> { fn from(pair: TokenPair<'t>) -> Self { - let mut lhs = ""; - let mut ops = Rule::EOI; - let mut rhs = ""; + let (mut lhs, mut rhs) = ("", ""); + let mut ops = None; let mut event = None; + let mut action = None; // determine the lhs, rhs, and operators for span in pair.into_inner() { @@ -17,19 +17,23 @@ impl<'t> From> for Transition<'t> { // TODO: waiting for https://github.com/rust-lang/rfcs/pull/2649 (Destructuring without `let`) let (operators, target) = get::transition(span); rhs = target; - ops = operators; + ops = Some(operators); } Rule::self_transition => { let (operators, target) = get::transition(span); rhs = target; lhs = rhs; - ops = operators; + ops = Some(operators); } - Rule::trigger => { - event = Some(Event { - name: get::trigger(span), - }) + Rule::internal_transition => { + let (state, ev, act) = get::arrowless_transition(span); + lhs = state; + rhs = lhs; + event = Some(ev); + action = Some(act); } + Rule::trigger => event = Some(get::trigger(span)), + Rule::action => action = Some(Action { name: get::action(span) }), _ => unreachable!( "Rule::{:?} not found when determine the lhs, rhs, and operators", span.as_rule() @@ -39,26 +43,27 @@ impl<'t> From> for Transition<'t> { // determine the current, next, and type of the State let (transition_type, (current_state, next_state)) = match ops { - Symbol::double_arrow::right => ( + None => (TransitionType::Internal, get::state(lhs, None, &StateType::Atomic)), + Some(Symbol::double_arrow::right) => ( TransitionType::Loop { transient: false }, - get::state(lhs, rhs, &StateType::Atomic), + get::state(lhs, Some(rhs), &StateType::Atomic), ), - Symbol::tail_arrow::right => ( + Some(Symbol::tail_arrow::right) => ( TransitionType::Loop { transient: true }, - get::state(lhs, rhs, &StateType::Atomic), + get::state(lhs, Some(rhs), &StateType::Atomic), ), - Symbol::arrow::right => (TransitionType::Normal, get::state(lhs, rhs, &StateType::Atomic)), - Symbol::arrow::both => (TransitionType::Toggle, get::state(lhs, rhs, &StateType::Atomic)), - Symbol::arrow::left => (TransitionType::Normal, get::state(rhs, lhs, &StateType::Atomic)), - Symbol::tail_arrow::left => ( + Some(Symbol::arrow::right) => (TransitionType::Normal, get::state(lhs, Some(rhs), &StateType::Atomic)), + Some(Symbol::arrow::both) => (TransitionType::Toggle, get::state(lhs, Some(rhs), &StateType::Atomic)), + Some(Symbol::arrow::left) => (TransitionType::Normal, get::state(rhs, Some(lhs), &StateType::Atomic)), + Some(Symbol::tail_arrow::left) => ( TransitionType::Loop { transient: true }, - get::state(rhs, lhs, &StateType::Atomic), + get::state(rhs, Some(lhs), &StateType::Atomic), ), - Symbol::double_arrow::left => ( + Some(Symbol::double_arrow::left) => ( TransitionType::Loop { transient: false }, - get::state(rhs, lhs, &StateType::Atomic), + get::state(rhs, Some(lhs), &StateType::Atomic), ), - _ => unreachable!( + Some(_) => unreachable!( "Rule::{:?} not found when determine the current, next, and type of the State", &ops ), @@ -69,6 +74,7 @@ impl<'t> From> for Transition<'t> { from: current_state, to: next_state, at: event, + run: action, kind: transition_type, } } diff --git a/packages/core/src/semantics/transition/iter.rs b/packages/core/src/semantics/transition/desugar.rs similarity index 68% rename from packages/core/src/semantics/transition/iter.rs rename to packages/core/src/semantics/transition/desugar.rs index fb60366d..4a042aaa 100644 --- a/packages/core/src/semantics/transition/iter.rs +++ b/packages/core/src/semantics/transition/desugar.rs @@ -1,3 +1,5 @@ +//! Code for desugaring expression into multiple transition + use crate::semantics; use semantics::{Transition, TransitionType}; use std::iter::FromIterator; @@ -8,26 +10,26 @@ impl<'i> IntoIterator for Transition<'i> { fn into_iter(mut self) -> Self::IntoIter { TransitionIterator(match self.kind { - TransitionType::Normal => [self].to_vec(), + TransitionType::Normal | TransitionType::Internal => vec![self], TransitionType::Toggle => { self.kind = TransitionType::Normal; let (mut left, right) = (self.clone(), self); - left.from = right.to.clone(); - left.to = right.from.clone(); - [left, right].to_vec() + left.from = right.to.clone().expect("not Internal"); + left.to = right.from.clone().into(); + vec![left, right] } TransitionType::Loop { transient } => { /* A ->> B @ C */ - if self.from.name != self.to.name { + if self.from.name != self.to.as_ref().expect("not Internal").name { let (mut self_loop, mut normal) = (self.clone(), self); - self_loop.from = self_loop.to.clone(); + self_loop.from = self_loop.to.as_ref().expect("not Internal").clone(); normal.kind = TransitionType::Normal; - normal.at = if transient { None } else { normal.at }; - [normal, self_loop].to_vec() + normal.at = normal.at.filter(|_| !transient); + vec![normal, self_loop] } /* ->> B @ C */ else { - [self].to_vec() // reason: see Symbol::double_arrow::right => (..) in convert.rs + vec![self] // reason: see Symbol::double_arrow::right => (..) in convert.rs } } TransitionType::Inside { .. } => unreachable!("TODO: when support StateType::Compound"), @@ -62,6 +64,6 @@ where where T: IntoIterator>, { - unimplemented!("TODO: on the next update") + unimplemented!("TODO: on the next update when const generic is stabilized") } } diff --git a/packages/core/src/semantics/transition/helper.rs b/packages/core/src/semantics/transition/helper.rs index f18a0070..b31c0340 100644 --- a/packages/core/src/semantics/transition/helper.rs +++ b/packages/core/src/semantics/transition/helper.rs @@ -6,15 +6,15 @@ pub(super) mod prelude { Scdlang, }; pub use pest::{error::ErrorVariant, iterators::Pair, Span}; - pub use std::convert::TryInto; + pub use std::{convert::TryInto, fmt::Write, sync::MutexGuard}; } pub(super) mod get { use super::prelude::*; use crate::semantics::*; - pub fn state<'t>(current: &'t str, next: &'t str, kind: &'t StateType) -> (State<'t>, State<'t>) { - (State { name: current, kind }, State { name: next, kind }) + pub fn state<'t>(current: &'t str, next: Option<&'t str>, kind: &'t StateType) -> (State<'t>, Option>) { + (State { name: current, kind }, next.map(|name| State { name, kind })) } type Tuple<'target> = (Rule, &'target str); @@ -39,17 +39,118 @@ pub(super) mod get { (ops, target) } - pub fn trigger(pair: TokenPair) -> &str { - let mut event = ""; + pub fn arrowless_transition(pair: TokenPair) -> (&str, Event, Action) { + use super::get; + let (mut state, mut event, mut action) = ("", Event::default(), Action::default()); for span in pair.into_inner() { match span.as_rule() { - Name::event => event = span.as_str(), + Rule::StateName => state = span.as_str(), + Rule::trigger => event = get::trigger(span), + Rule::action => action = Action { name: get::action(span) }, + _ => unreachable!("Rule::{:?}", span.as_rule()), + } + } + + (state, event, action) + } + + pub fn action(pair: TokenPair) -> &str { + let mut action = ""; + + for span in pair.into_inner() { + match span.as_rule() { + Name::action => action = span.as_str(), + Symbol::triangle::right => { /* reserved when exit/entry is implemented */ } + _ => unreachable!("Rule::{:?}", span.as_rule()), + } + } + + action + } + + pub fn trigger(pair: TokenPair) -> Event { + let (mut event, mut guard) = (None, None); + + for span in pair.into_inner() { + match span.as_rule() { + Name::event => event = Some(span.as_str()), + Name::guard => guard = Some(span.as_str()), Symbol::at => { /* reserved when Internal Event is implemented */ } _ => unreachable!("Rule::{:?}", span.as_rule()), } } - event + Event { name: event, guard } + } +} + +/// analyze.rs helpers for transforming key for caches +pub(super) mod transform_key { + use crate::semantics::*; + + // WARNING: not performant because of using concatenated String as a key which cause filtering + impl From<&Event<'_>> for String { + fn from(event: &Event<'_>) -> Self { + format!("{}?{}", event.name.unwrap_or(""), event.guard.unwrap_or("")) + } + } + + impl<'i> EventKey<'i> for &'i Option {} + pub trait EventKey<'i>: Into> { + fn has_trigger(self) -> bool { + self.into().filter(|e| is_empty(e.rsplit('?'))).is_some() + } + fn has_guard(self) -> bool { + self.into().filter(|e| is_empty(e.split('?'))).is_some() + } + fn get_guard(self) -> Option<&'i str> { + self.into().and_then(|e| none_empty(e.split('?'))) + } + fn get_trigger(self) -> Option<&'i str> { + self.into().and_then(|e| none_empty(e.rsplit('?'))) + } + fn guards_with_same_trigger(self, trigger: Option<&'i str>) -> Option<&'i str> { + self.into() + .filter(|e| none_empty(e.rsplit('?')) == trigger) + .and_then(|e| none_empty(e.split('?'))) + } + fn triggers_with_same_guard(self, guard: Option<&'i str>) -> Option<&'i str> { + self.into() + .filter(|e| none_empty(e.split('?')) == guard) + .and_then(|e| none_empty(e.rsplit('?'))) + } + fn as_expression(self) -> String { + self.into().map(String::as_str).as_expression() + } + } + + impl<'o> Trigger<'o> for &'o Option<&'o str> {} + pub trait Trigger<'o>: Into> { + fn as_expression(self) -> String { + self.into() + .map(|s| { + format!( + " @ {trigger}{guard}", + trigger = none_empty(s.rsplit('?')).unwrap_or_default(), + guard = none_empty(s.split('?')) + .filter(|_| s.contains('?')) + .map(|g| format!("[{}]", g)) + .unwrap_or_default(), + ) + }) + .unwrap_or_default() + } + fn as_key(self, guard: &str) -> Option { + Some(format!("{}?{}", self.into().unwrap_or(&""), guard)) + } + } + + fn is_empty<'a>(split: impl Iterator) -> bool { + none_empty(split).is_some() + } + + fn none_empty<'a>(split: impl Iterator) -> Option<&'a str> { + split.last().filter(|s| !s.is_empty()) } } diff --git a/packages/core/src/semantics/transition/mod.rs b/packages/core/src/semantics/transition/mod.rs index fd15b09e..5cd9f362 100644 --- a/packages/core/src/semantics/transition/mod.rs +++ b/packages/core/src/semantics/transition/mod.rs @@ -1,31 +1,53 @@ +//! parse -> convert -> desugar -> analyze -> consume + mod analyze; mod convert; +mod desugar; mod helper; -mod iter; use crate::{ - semantics::{analyze::SemanticCheck, Expression, Found, Kind, Transition}, + semantics::{analyze::*, Check, Expression, Found, Kind, Transition}, utils::naming::Name, Error, }; +use static_assertions::assert_impl_all; + +assert_impl_all!(r#for; Transition, + SemanticAnalyze<'static>, + SemanticCheck, + From>, + IntoIterator // because `A <-> B` can be desugared into 2 transition +); impl Expression for Transition<'_> { fn current_state(&self) -> Name { self.from.name.into() } - fn next_state(&self) -> Name { - self.to.name.into() + fn next_state(&self) -> Option { + self.to.as_ref().map(|state| state.name.into()) } fn event(&self) -> Option { - self.at.as_ref().map(|event| event.name.into()) + self.at.as_ref().and_then(|event| event.name).map(Into::into) + } + + fn guard(&self) -> Option { + self.at.as_ref().and_then(|event| event.guard).map(Into::into) + } + + fn action(&self) -> Option { + self.run.as_ref().map(|action| action.name.into()) } +} +impl Check for Transition<'_> { fn semantic_check(&self) -> Result { - Ok(match self.check_error()? { - Some(message) => Found::Error(message), - None => Found::None, + // WARNING: there is possibility that one expression can contain both error and warning because of sugar syntax (<->, ->>, >->) + Ok(match (self.check_error()?, self.check_warning()?) { + (Some(message), _) => Found::Error(message), + (_, Some(err)) => Found::Warning(err.message), + (None, None) => Found::None, }) } } @@ -55,15 +77,15 @@ mod pair { "A <- D" => { let state: Transition = expression.into(); assert_eq!(state.from.name, "D"); - assert_eq!(state.to.name, "A"); + assert_eq!(state.to.map(|n| n.name), Some("A")); assert!(state.at.is_none()); } "A -> D @ C" => { let state: Transition = expression.into(); let event = state.at.expect("struct Event"); assert_eq!(state.from.name, "A"); - assert_eq!(state.to.name, "D"); - assert_eq!(event.name, "C"); + assert_eq!(state.to.map(|n| n.name), Some("D")); + assert_eq!(event.name, Some("C")); } _ => unreachable!("{}", expression.as_str()), }) @@ -84,24 +106,24 @@ mod pair { // contains state name B or E let (mut states, mut transitions) = (["B", "E"].iter(), Transition::from(expression.clone()).into_iter()); - assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.name == *s))); + assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.map(|n| n.name) == Some(*s)))); let [state_b, state_e] = Transition::from(expression).into_iter().collect::<[Transition; 2]>(); - assert_eq!(state_b.from.name, state_e.to.name); - assert_eq!(state_b.to.name, state_e.from.name); + assert_eq!(Some(state_b.from.name), state_e.to.map(|n| n.name)); + assert_eq!(state_b.to.map(|n| n.name), Some(state_e.from.name)); assert_eq!(state_b.at.is_none(), state_e.at.is_none()); } "A <-> D @ C" => { // contains state name A or D let (mut states, mut transitions) = (["A", "D"].iter(), Transition::from(expression.clone()).into_iter()); - assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.name == *s))); + assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.map(|n| n.name) == Some(*s)))); let [state_a, state_d] = Transition::from(expression).into_iter().collect::<[Transition; 2]>(); let [event_a, event_d] = [state_a.at.expect("struct Event"), state_d.at.expect("struct Event")]; - assert_eq!(state_a.from.name, state_d.to.name); - assert_eq!(state_a.to.name, state_d.from.name); - assert!([event_a.name, event_d.name].iter().all(|e| *e == "C")); + assert_eq!(Some(state_a.from.name), state_d.to.map(|n| n.name)); + assert_eq!(state_a.to.map(|n| n.name), Some(state_d.from.name)); + assert!([event_a.name, event_d.name].iter().all(|e| *e == Some("C"))); } _ => unreachable!("{}", expression.as_str()), }) @@ -125,46 +147,48 @@ mod pair { // contains state name X or Z let (mut states, mut transitions) = (["X", "Z"].iter(), Transition::from(expression.clone()).into_iter()); - assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.name == *s))); + assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.map(|n| n.name) == Some(*s)))); let [state_z, state_x] = Transition::from(expression).into_iter().collect::<[Transition; 2]>(); assert_eq!(state_z.from.name, "Z"); - assert_eq!(state_z.from.name, state_z.to.name); + assert_eq!(Some(state_z.from.name), state_z.to.map(|n| n.name)); assert_eq!(state_x.from.name, "X"); - assert_eq!(state_x.to.name, state_z.from.name); + assert_eq!(state_x.to.map(|n| n.name), Some(state_z.from.name)); assert_eq!(state_z.at.is_none(), state_x.at.is_none()); } "A ->> D @ C" => { // contains state name A or D let (mut states, mut transitions) = (["A", "D"].iter(), Transition::from(expression.clone()).into_iter()); - assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.name == *s))); + assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.map(|n| n.name) == Some(*s)))); let [state_d, state_a] = Transition::from(expression).into_iter().collect::<[Transition; 2]>(); let [event_d, event_a] = [state_d.at.expect("struct Event"), state_a.at.expect("struct Event")]; assert_eq!(state_d.from.name, "D"); - assert_eq!(state_d.from.name, state_d.to.name); + assert_eq!(Some(state_d.from.name), state_d.to.map(|n| n.name)); assert_eq!(state_a.from.name, "A"); - assert_eq!(state_a.to.name, state_d.from.name); - assert!([event_d.name, event_a.name].iter().all(|e| *e == "C")); + assert_eq!(state_a.to.map(|n| n.name), Some(state_d.from.name)); + assert!([event_d.name, event_a.name].iter().all(|e| *e == Some("C"))); } "D <<- E @ B" => { // contains state name E or D let (mut states, mut transitions) = (["E", "D"].iter(), Transition::from(expression.clone()).into_iter()); - assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.name == *s))); + assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.map(|n| n.name) == Some(*s)))); let [state_d, state_e] = Transition::from(expression).into_iter().collect::<[Transition; 2]>(); let [event_d, event_e] = [state_d.at.expect("struct Event"), state_e.at.expect("struct Event")]; assert_eq!(state_d.from.name, "D"); - assert_eq!(state_d.from.name, state_e.to.name); + assert_eq!(Some(state_d.from.name), state_e.to.as_ref().map(|n| n.name)); assert_eq!(state_e.from.name, "E"); - assert_eq!(state_e.to.name, state_d.from.name); - assert!([event_d.name, event_e.name].iter().all(|e| *e == "B")); + assert_eq!(state_e.to.map(|n| n.name), Some(state_d.from.name)); + assert!([event_d.name, event_e.name].iter().all(|e| *e == Some("B"))); } "->> E @ C" => Transition::from(expression).into_iter().for_each(|transition| { - assert!([transition.from.name, transition.to.name].iter().all(|e| *e == "E")); - assert_eq!(transition.at.expect("struct Event").name, "C"); + assert!([Some(transition.from.name), transition.to.map(|n| n.name)] + .iter() + .all(|e| *e == Some("E"))); + assert_eq!(transition.at.expect("struct Event").name, Some("C")); }), _ => unreachable!("{}", expression.as_str()), }) @@ -186,41 +210,41 @@ mod pair { // contains state name X or Z let (mut states, mut transitions) = (["X", "Z"].iter(), Transition::from(expression.clone()).into_iter()); - assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.name == *s))); + assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.map(|n| n.name) == Some(*s)))); let [state_z, state_x] = Transition::from(expression).into_iter().collect::<[Transition; 2]>(); assert_eq!(state_z.from.name, "Z"); - assert_eq!(state_z.from.name, state_z.to.name); + assert_eq!(Some(state_z.from.name), state_z.to.map(|n| n.name)); assert_eq!(state_x.from.name, "X"); - assert_eq!(state_x.to.name, state_z.from.name); + assert_eq!(state_x.to.map(|n| n.name), Some(state_z.from.name)); assert_eq!(state_z.at.is_none(), state_x.at.is_none()); } "A >-> D @ C" => { // contains state name A or D let (mut states, mut transitions) = (["A", "D"].iter(), Transition::from(expression.clone()).into_iter()); - assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.name == *s))); + assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.map(|n| n.name) == Some(*s)))); let [state_d, state_a] = Transition::from(expression).into_iter().collect::<[Transition; 2]>(); assert_eq!(state_d.from.name, "D"); - assert_eq!(state_d.from.name, state_d.to.name); - assert_eq!(state_d.at.expect("struct Event").name, "C"); + assert_eq!(Some(state_d.from.name), state_d.to.map(|n| n.name)); + assert_eq!(state_d.at.expect("struct Event").name, Some("C")); assert_eq!(state_a.from.name, "A"); - assert_eq!(state_a.to.name, state_d.from.name); + assert_eq!(state_a.to.map(|n| n.name), Some(state_d.from.name)); assert!(state_a.at.is_none()); } "D <-< E @ B" => { // contains state name E or D let (mut states, mut transitions) = (["E", "D"].iter(), Transition::from(expression.clone()).into_iter()); - assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.name == *s))); + assert!(states.any(|s| transitions.any(|t| t.from.name == *s || t.to.map(|n| n.name) == Some(*s)))); let [state_d, state_e] = Transition::from(expression).into_iter().collect::<[Transition; 2]>(); assert_eq!(state_d.from.name, "D"); - assert_eq!(state_d.from.name, state_e.to.name); - assert_eq!(state_d.at.expect("struct Event").name, "B"); + assert_eq!(Some(state_d.from.name), state_e.to.as_ref().map(|n| n.name)); + assert_eq!(state_d.at.expect("struct Event").name, Some("B")); assert_eq!(state_e.from.name, "E"); - assert_eq!(state_e.to.name, state_d.from.name); + assert_eq!(state_e.to.map(|n| n.name), Some(state_d.from.name)); assert!(state_e.at.is_none()); } _ => unreachable!("{}", expression.as_str()), @@ -229,6 +253,46 @@ mod pair { ) } + #[test] + #[ignore] + fn guard_transition() -> ParseResult { + test::parse::expression( + r#" + A -> B @ D[valid] + A -> F @ D + A -> C @ D[exist] + "#, + |_expression| unimplemented!(), + ) + } + + #[test] + #[ignore] + fn auto_transient_transition() -> ParseResult { + test::parse::expression( + r#" + A -> B @ [valid] + A -> F + A -> C @ [exist] + "#, + |_expression| unimplemented!(), + ) + } + + #[test] + #[ignore] + fn auto_transition_with_trigger() -> ParseResult { + test::parse::expression( + r#" + A -> B @ D + A -> B @ [valid] + A -> C @ [exist] + A -> C @ E + "#, + |_expression| unimplemented!(), + ) + } + mod fix_issues { use super::*; use crate::semantics::analyze::SemanticAnalyze; @@ -307,6 +371,22 @@ mod pair { ) } + mod redundant_transition { + use super::*; + + #[test] + #[ignore] + fn event_guard_target_same_state() -> ParseResult { + test::parse::expression( + r#" + A -> B @ D[valid] + A -> B @ D + "#, + |_expression| unimplemented!(), + ) + } + } + mod ambigous_transition { use super::*; diff --git a/packages/core/src/utils/mod.rs b/packages/core/src/utils/mod.rs index 5d098a75..59f54e61 100644 --- a/packages/core/src/utils/mod.rs +++ b/packages/core/src/utils/mod.rs @@ -1,3 +1,17 @@ //! Collections of helper module. pub mod naming; + +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +impl CalculateHash for T {} +pub(crate) trait CalculateHash: Hash { + fn get_hash(&self) -> u64 { + let mut s = DefaultHasher::new(); + self.hash(&mut s); + s.finish() + } +} diff --git a/packages/transpiler/smcat/Cargo.toml b/packages/transpiler/smcat/Cargo.toml index 2fd58dbd..d6f4d659 100644 --- a/packages/transpiler/smcat/Cargo.toml +++ b/packages/transpiler/smcat/Cargo.toml @@ -14,6 +14,9 @@ scdlang = { path = "../../core", version = "0.2.1" } serde_json = "1" serde = { version = "1", features = ["derive"] } serde_with = { version = "1", features = ["json"] } +# for custom options +strum = { git = "https://github.com/Peternator7/strum.git" } +strum_macros = { git = "https://github.com/Peternator7/strum.git" } [dev-dependencies] assert-json-diff = "1" \ No newline at end of file diff --git a/packages/transpiler/smcat/src/lib.rs b/packages/transpiler/smcat/src/lib.rs index c76db41f..4716d28a 100644 --- a/packages/transpiler/smcat/src/lib.rs +++ b/packages/transpiler/smcat/src/lib.rs @@ -1,117 +1,66 @@ #![allow(clippy::unit_arg)] +extern crate strum; +mod parser; mod schema; mod utils; -pub use scdlang::Transpiler; -use scdlang::{ - prelude::*, - semantics::{Found, Kind}, - Scdlang, -}; -use schema::*; +use scdlang::{prelude::*, Scdlang}; +use schema::Coordinate; use serde::Serialize; -use std::{error, fmt, mem::ManuallyDrop}; -use utils::*; + +pub mod prelude { + pub use scdlang::external::*; +} + +pub use option::Config; +pub mod option { + use strum_macros::*; + + #[derive(AsRefStr)] + #[strum(serialize_all = "lowercase")] + pub enum Config { + Mode, + } + + #[derive(AsRefStr, EnumString)] + #[strum(serialize_all = "kebab-case")] + pub enum Mode { + BlackboxState, + } +} #[derive(Default, Serialize)] /** Transpiler Scdlang β†’ State Machine Cat (JSON). # Examples ```no_run -let smcat = Machine::new(); +# use std::error::Error; +use scdlang_smcat::{prelude::*, Machine}; + +let mut parser = Machine::new(); -smcat.configure().with_err_path("test.scl"); +parser.configure().with_err_path("test.scl"); parser.parse("A -> B")?; println!("{}", parser.to_string()); +# Ok::<(), Box>(()) ``` */ pub struct Machine<'a> { #[serde(skip)] - builder: Scdlang<'a>, + builder: Scdlang<'a>, // TODO: refactor this as specialized builder #[serde(flatten)] schema: Coordinate, // TODO: replace with πŸ‘‡ when https://github.com/serde-rs/serde/issues/1507 resolved // schema: mem::ManuallyDrop, } -impl<'a> Parser<'a> for Machine<'a> { - fn configure(&mut self) -> &mut Builder<'a> { - &mut self.builder - } - - fn parse(&mut self, source: &str) -> Result<(), DynError> { - self.clean_cache()?; - let ast = ManuallyDrop::new(Self::try_parse(source, self.builder.to_owned())?); - Ok(self.schema = ast.schema.to_owned()) // FIXME: expensive clone - } - - fn insert_parse(&mut self, source: &str) -> Result<(), DynError> { - let mut ast = ManuallyDrop::new(Self::try_parse(source, self.builder.to_owned())?); - self.schema.states.merge(&ast.schema.states); - match (&mut self.schema.transitions, &mut ast.schema.transitions) { - (Some(origin), Some(parsed)) => origin.extend_from_slice(parsed), - (None, _) => self.schema.transitions = ast.schema.transitions.to_owned(), - _ => {} - } - Ok(()) - } - - #[allow(clippy::match_bool)] - fn try_parse(source: &str, builder: Scdlang<'a>) -> Result { - use StateType::*; - let mut schema = Coordinate::default(); - - for kind in builder.iter_from(source)? { - match kind { - Kind::Expression(expr) => { - let (color, note) = match builder.semantic_error { - false => match expr.semantic_check()? { - Found::Error(message) => (Some("red".to_string()), Some(message.split_to_vec('\n'))), - _ => (None, None), - }, - true => (None, None), - }; - schema.states.merge(&{ - let mut states = [expr.current_state().into_type(Regular), expr.next_state().into_type(Regular)]; - if let Some(color) = &color { - states.iter_mut().for_each(|s| { - s.with_color(color); - }); - } - states - }); - let transition = Transition { - from: expr.current_state().into(), - to: expr.next_state().into(), - event: expr.event().map(|e| e.into()), - label: expr.event().map(|e| e.into()), - color, - note, - ..Default::default() - }; - match &mut schema.transitions { - Some(transitions) => transitions.push(transition), - None => schema.transitions = Some(vec![transition]), - }; - } - _ => unimplemented!("TODO: implement the rest on the next update"), - } - } - - Ok(Machine { schema, builder }) - } -} - impl Machine<'_> { /// Create new StateMachine. /// Use this over `Machine::default()`❗ pub fn new() -> Self { - let mut builder = Scdlang::new(); + let (mut builder, schema) = (Scdlang::new(), Coordinate::default()); builder.auto_clear_cache(false); - Self { - builder, - schema: Coordinate::default(), - } + Self { builder, schema } } } @@ -121,13 +70,7 @@ impl Drop for Machine<'_> { } } -impl fmt::Display for Machine<'_> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", serde_json::to_string_pretty(&self.schema).map_err(|_| fmt::Error)?) - } -} - -type DynError = Box; +type DynError = Box; #[cfg(test)] mod test { @@ -249,6 +192,7 @@ mod test { } #[test] + #[ignore] fn disable_semantic_error() -> Result<(), DynError> { let mut machine = Machine::new(); machine.configure().with_err_semantic(false); @@ -277,7 +221,8 @@ mod test { "from": "A", "to": "B", "color": "red", - "note": ["duplicate transient transition: A -> B,C"] + // FIXME: πŸ‘‡ should be tested using regex + "note": ["duplicate transient-transition: A -> B,C"] }] }), json!(machine) diff --git a/packages/transpiler/smcat/src/parser.rs b/packages/transpiler/smcat/src/parser.rs new file mode 100644 index 00000000..bf727b78 --- /dev/null +++ b/packages/transpiler/smcat/src/parser.rs @@ -0,0 +1,117 @@ +use super::{schema::*, utils::*, *}; +use scdlang::{ + prelude::*, + semantics::{Found, Kind}, + Scdlang, +}; +use std::{fmt, mem::ManuallyDrop}; + +impl fmt::Display for Machine<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let json = serde_json::to_string_pretty(&self.schema).map_err(|_| fmt::Error)?; + write!(f, "{}", json.trim()) + } +} + +impl<'a> Parser<'a> for Machine<'a> { + fn try_parse(source: &str, builder: Scdlang<'a>) -> Result { + use StateType::*; + let mut schema = Coordinate::default(); + let get = |key| builder.get(key); + + for kind in builder.iter_from(source)? { + match kind { + Kind::Expression(expr) => { + let (color, note) = match expr.semantic_check()? { + Found::Error(ref message) if !builder.semantic_error => ( + Some("red".to_string()), + Some(message.split('\n').map(|s| s.trim_start_matches(' ').to_string()).collect()), + ), + _ => (None, None), + }; + let (event, cond, action) = ( + expr.event().map(|e| e.into()), + expr.guard().map(|e| e.into()), + expr.action().map(|e| e.into()), + ); + + schema.states.merge(&{ + let (mut current, mut next) = ( + expr.current_state().into_type(Regular), + expr.next_state().map(|s| s.into_type(Regular)), + ); + if let/* mark error */Some(color) = &color { + current.with_color(color); + next = next.map(|mut s| s.with_color(color).clone()) + } + use option::Mode; + match next { + Some(next) => vec![current, next], // external transition + None if get(&Config::Mode) == Some(Mode::BlackboxState.as_ref()) => vec![/*WARNING:wasted*/], // ignore anything inside state + None => { + if let (Some(event), Some(action)) = (event.as_ref(), action.as_ref()) { + current.actions = Some(vec![ActionType { + r#type: ActionTypeType::Activity, + body: match cond.as_ref() { + None => format!("{} / {}", event, action), + Some(cond) => format!("{} [{}] / {}", event, cond, action), + }, + }]); + } + vec![current] // internal transition + } + } + }); + + if let/* external transition */Some(next_state) = expr.next_state() { + #[rustfmt::skip] + let transition = Transition { + from: expr.current_state().into(), + to: next_state.into(), + label: if event.is_some() || cond.is_some() || action.is_some() { + let action_cond = action.is_some() || cond.is_some(); + let (event, cond, action) = (event.clone(), cond.clone(), action.clone()); + Some(format!( // add spacing on each token + "{on}{is}{run}", + on = event.map(|event| format!("{}{spc}", event, spc = if action_cond { " " } else { "" },)) + .unwrap_or_default(), + is = cond.map(|guard| format!("[{}]{spc}", guard, spc = if action.is_some() { " " } else { "" },)) + .unwrap_or_default(), + run = action.map(|act| format!("/ {}", act)).unwrap_or_default() + )) + } else { None }, event, cond, action, color, note + }; + match &mut schema.transitions { + Some(transitions) => transitions.push(transition), + None => schema.transitions = Some(vec![transition]), + }; + } + } + _ => unimplemented!("TODO: implement the rest on the next update"), + } + } + + Ok(Machine { schema, builder }) + } + + fn configure(&mut self) -> &mut dyn Builder<'a> { + &mut self.builder + } + + fn parse(&mut self, source: &str) -> Result<(), DynError> { + self.clean_cache()?; + let ast = ManuallyDrop::new(Self::try_parse(source, self.builder.to_owned())?); + Ok(self.schema = ast.schema.to_owned()) // FIXME: expensive clone + } + + fn insert_parse(&mut self, source: &str) -> Result<(), DynError> { + let mut ast = ManuallyDrop::new(Self::try_parse(source, self.builder.to_owned())?); + self.schema.states.merge(&ast.schema.states); + match (&mut self.schema.transitions, &mut ast.schema.transitions) { + (Some(origin), Some(parsed)) => origin.extend_from_slice(parsed), + (None, _) => self.schema.transitions = ast.schema.transitions.to_owned(), + _ => {} + } + Ok(()) + } +} diff --git a/packages/transpiler/smcat/src/schema.rs b/packages/transpiler/smcat/src/schema.rs index b7ca10e9..bc11d9ec 100644 --- a/packages/transpiler/smcat/src/schema.rs +++ b/packages/transpiler/smcat/src/schema.rs @@ -5,7 +5,7 @@ use serde::Serialize; use serde_with::skip_serializing_none; -// TODO: is it necessary to watch smcat-ast.schema.json then generate this automatically on each release πŸ€” +// TODO: replace Vec with HashSet #[skip_serializing_none] #[derive(Debug, Default, Clone, Serialize)] @@ -51,13 +51,13 @@ pub struct Coordinate { pub transitions: Option>, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize)] pub struct ActionType { #[serde(rename = "body")] pub body: String, #[serde(rename = "type")] - pub action_type_type: ActionTypeType, + pub r#type: ActionTypeType, } #[skip_serializing_none] @@ -88,7 +88,7 @@ pub struct Transition { pub to: String, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize)] pub enum ActionTypeType { #[serde(rename = "activity")] Activity, diff --git a/packages/transpiler/smcat/src/utils.rs b/packages/transpiler/smcat/src/utils.rs index d9f76212..b668333e 100644 --- a/packages/transpiler/smcat/src/utils.rs +++ b/packages/transpiler/smcat/src/utils.rs @@ -1,4 +1,4 @@ -use super::{State, StateType}; +use super::schema::{State, StateType}; use scdlang::utils::naming::Name; use std::iter::FromIterator; @@ -42,12 +42,21 @@ impl MergeStates for Vec { for state in states { if !self.iter().any(|s| s.name == state.name) { self.push(state.to_owned()); - } else if state.color.is_some() { + } else { let pos = self .iter() .position(|s| s.name == state.name) .expect("any(|s| s.name == state.name)"); - self[pos].color = state.color.clone(); + if let Some(color) = state.color.clone() { + self[pos].color = Some(color); + } + if let Some(new_actions) = state.actions.clone() { + let mut actions = self[pos].actions.clone().unwrap_or_default(); + actions.extend(new_actions); + actions.sort_unstable(); + actions.dedup(); + self[pos].actions = Some(actions); + } } } } diff --git a/packages/transpiler/xstate/Cargo.toml b/packages/transpiler/xstate/Cargo.toml index ef919505..6017e242 100644 --- a/packages/transpiler/xstate/Cargo.toml +++ b/packages/transpiler/xstate/Cargo.toml @@ -13,7 +13,11 @@ edition = "2018" scdlang = { path = "../../core", version = "0.2.1" } serde_json = "1" serde = { version = "1", features = ["derive"] } -voca_rs = "1" +serde_with = { version = "1", features = ["json"] } +voca_rs = "1" # helper to convert Scdlang naming convention into DavidKPiano naming convention +# for custom options +strum = { git = "https://github.com/Peternator7/strum.git" } +strum_macros = { git = "https://github.com/Peternator7/strum.git" } [dev-dependencies] assert-json-diff = "1" \ No newline at end of file diff --git a/packages/transpiler/xstate/src/lib.rs b/packages/transpiler/xstate/src/lib.rs index 3e9ae65e..4143bd43 100644 --- a/packages/transpiler/xstate/src/lib.rs +++ b/packages/transpiler/xstate/src/lib.rs @@ -1,4 +1,175 @@ -pub use scdlang::Transpiler; +#![allow(clippy::unit_arg)] +extern crate strum; +mod parser; +mod schema; +mod typescript; -mod machine; -pub use machine::Machine; +use scdlang::{prelude::*, Scdlang}; +use schema::StateChart; +use serde::Serialize; + +pub mod prelude { + pub use scdlang::external::*; +} + +pub use option::Config; +pub mod option { + use strum_macros::*; + + #[derive(AsRefStr)] + #[strum(serialize_all = "lowercase")] + pub enum Config { + Output, + ExportName, + } + + #[derive(AsRefStr, EnumString)] + #[strum(serialize_all = "lowercase")] + pub enum Output { + JSON, + TypeScript, + JavaScript, + } +} + +#[derive(Default, Serialize)] +/** Transpiler Scdlang β†’ XState. + +# Examples +```no_run +# use std::error::Error; +use scdlang_xstate::{prelude::*, Machine}; + +let mut parser = Machine::new(); + +parser.configure().with_err_path("test.scl"); +parser.parse("A -> B")?; + +println!("{}", parser.to_string()); +# Ok::<(), Box>(()) +``` */ +pub struct Machine<'a> { + #[serde(skip)] + builder: Scdlang<'a>, // TODO:refactor this as specialized builder + + #[serde(flatten)] + schema: StateChart, // TODO: replace with πŸ‘‡ when https://github.com/serde-rs/serde/issues/1507 resolved + // schema: mem::ManuallyDrop, +} + +impl Machine<'_> { + /* Create new StateMachine in default mode + + ##### custom config + * "output": "json" | "typescript" (default: "json") */ + pub fn new() -> Self { + use option::*; + let (mut builder, schema) = (Scdlang::new(), StateChart::default()); + builder.auto_clear_cache(false); + builder.set(&Config::Output, &Output::JSON); + Self { builder, schema } + } +} + +impl Drop for Machine<'_> { + fn drop(&mut self) { + self.flush_cache().expect("xstate: Deadlock"); + } +} + +type DynError = Box; + +#[cfg(test)] +mod test { + use super::*; + use assert_json_diff::assert_json_eq; + use serde_json::json; + + #[test] + fn transient_transition() -> Result<(), DynError> { + let mut machine = Machine::new(); + machine.parse("AlphaGo -> BetaRust")?; + + Ok(assert_json_eq!( + json!({ + "states": { + "alphaGo": { + "on": { + "": "betaRust" + } + } + } + }), + json!(machine) + )) + } + + #[test] + fn eventful_transition() -> Result<(), DynError> { + let mut machine = Machine::new(); + machine.parse( + "A -> B @ CarlieCaplin + A <- B @ CarlieCaplin + A -> D @ EnhancedErlang", + )?; + + Ok(assert_json_eq!( + json!({ + "states": { + "a": { + "on": { + "CARLIE_CAPLIN": "b", + "ENHANCED_ERLANG": "d" + } + }, + "b": { + "on": { + "CARLIE_CAPLIN": "a" + } + } + } + }), + json!(machine) + )) + } + + #[test] + fn no_clear_cache() { + let mut machine = Machine::new(); + machine.parse("A -> B").expect("Nothing happened"); + machine.insert_parse("A -> C").expect_err("Duplicate transition"); + + assert_json_eq!( + json!({ + "states": { + "a": { + "on": { + "": "b" + } + } + } + }), + json!(machine) + ) + } + + #[test] + fn clear_cache() { + let mut machine = Machine::new(); + machine.insert_parse("A -> B").expect("Nothing happened"); + machine.parse("A -> C").expect("Clear cache and replace schema"); + + assert_json_eq!( + json!({ + "states": { + "a": { + "on": { + "": "c" + } + } + } + }), + json!(machine) + ) + } +} diff --git a/packages/transpiler/xstate/src/machine/mod.rs b/packages/transpiler/xstate/src/machine/mod.rs deleted file mode 100644 index 9466fea4..00000000 --- a/packages/transpiler/xstate/src/machine/mod.rs +++ /dev/null @@ -1,203 +0,0 @@ -#![allow(clippy::unit_arg)] -mod schema; -use schema::*; - -use scdlang::{prelude::*, semantics::Kind, Scdlang}; -use serde::Serialize; -use serde_json::json; -use std::{error, fmt, mem::ManuallyDrop}; -use voca_rs::case::{camel_case, shouty_snake_case}; - -#[derive(Default, Serialize)] -/** Transpiler Scdlang β†’ XState. - -# Examples -```no_run -let xstate = Machine::new(); - -xstate.configure().with_err_path("test.scl"); -parser.parse("A -> B")?; - -println!("{}", parser.to_string()); -``` */ -pub struct Machine<'a> { - #[serde(skip)] - builder: Scdlang<'a>, - - #[serde(flatten)] - schema: StateChart, // TODO: replace with πŸ‘‡ when https://github.com/serde-rs/serde/issues/1507 resolved - // schema: mem::ManuallyDrop, -} - -impl<'a> Parser<'a> for Machine<'a> { - fn configure(&mut self) -> &mut Builder<'a> { - &mut self.builder - } - - fn parse(&mut self, source: &str) -> Result<(), DynError> { - self.clean_cache()?; - let ast = ManuallyDrop::new(Self::try_parse(source, self.builder.to_owned())?); - Ok(self.schema = ast.schema.to_owned()) // FIXME: expensive clone - } - - fn insert_parse(&mut self, source: &str) -> Result<(), DynError> { - let ast = ManuallyDrop::new(Self::try_parse(source, self.builder.to_owned())?); - for (current_state, transition) in ast.schema.states.to_owned(/*FIXME: expensive clone*/) { - self.schema - .states - .entry(current_state) - .and_modify(|t| t.on.extend(transition.on.clone())) - .or_insert(transition); - } - Ok(()) - } - - fn try_parse(source: &str, builder: Scdlang<'a>) -> Result { - let mut schema = StateChart::default(); - - for kind in builder.iter_from(source)? { - match kind { - Kind::Expression(expr) => { - let current_state = expr.current_state().map(camel_case); - let next_state = expr.next_state().map(camel_case); - let event_name = expr.event().map(|e| e.map(shouty_snake_case)).unwrap_or_default(); - - schema - .states - .entry(current_state) - .and_modify(|t| { - t.on.entry(event_name.to_string()).or_insert_with(|| json!(next_state)); - }) - .or_insert(Transition { - // TODO: waiting for map macros https://github.com/rust-lang/rfcs/issues/542 - on: [(event_name.to_string(), json!(next_state))].iter().cloned().collect(), - }); - } - _ => unimplemented!("TODO: implement the rest on the next update"), - } - } - - Ok(Machine { schema, builder }) - } -} - -impl Machine<'_> { - /// Create new StateMachine. - /// Use this over `Machine::default()`❗ - pub fn new() -> Self { - let mut builder = Scdlang::new(); - builder.auto_clear_cache(false); - Self { - builder, - schema: StateChart::default(), - } - } -} - -impl Drop for Machine<'_> { - fn drop(&mut self) { - self.flush_cache().expect("xstate: Deadlock"); - } -} - -impl fmt::Display for Machine<'_> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", serde_json::to_string_pretty(&self.schema).map_err(|_| fmt::Error)?) - } -} - -type DynError = Box; - -#[cfg(test)] -mod test { - use super::*; - use assert_json_diff::assert_json_eq; - - #[test] - fn transient_transition() -> Result<(), DynError> { - let mut machine = Machine::new(); - machine.parse("AlphaGo -> BetaRust")?; - - Ok(assert_json_eq!( - json!({ - "states": { - "alphaGo": { - "on": { - "": "betaRust" - } - } - } - }), - json!(machine) - )) - } - - #[test] - fn eventful_transition() -> Result<(), DynError> { - let mut machine = Machine::new(); - machine.parse( - "A -> B @ CarlieCaplin - A <- B @ CarlieCaplin - A -> D @ EnhancedErlang", - )?; - - Ok(assert_json_eq!( - json!({ - "states": { - "a": { - "on": { - "CARLIE_CAPLIN": "b", - "ENHANCED_ERLANG": "d" - } - }, - "b": { - "on": { - "CARLIE_CAPLIN": "a" - } - } - } - }), - json!(machine) - )) - } - - #[test] - fn no_clear_cache() { - let mut machine = Machine::new(); - machine.parse("A -> B").expect("Nothing happened"); - machine.insert_parse("A -> C").expect_err("Duplicate transition"); - - assert_json_eq!( - json!({ - "states": { - "a": { - "on": { - "": "b" - } - } - } - }), - json!(machine) - ) - } - - #[test] - fn clear_cache() { - let mut machine = Machine::new(); - machine.insert_parse("A -> B").expect("Nothing happened"); - machine.parse("A -> C").expect("Clear cache and replace schema"); - - assert_json_eq!( - json!({ - "states": { - "a": { - "on": { - "": "c" - } - } - } - }), - json!(machine) - ) - } -} diff --git a/packages/transpiler/xstate/src/machine/schema.rs b/packages/transpiler/xstate/src/machine/schema.rs deleted file mode 100644 index db7a46cf..00000000 --- a/packages/transpiler/xstate/src/machine/schema.rs +++ /dev/null @@ -1,15 +0,0 @@ -use serde::Serialize; -use serde_json::Value; -use std::collections::HashMap; - -#[derive(Debug, Clone, Serialize)] -pub struct Transition { - pub on: HashMap, - // πŸ€”β˜οΈ how about convert it to struct of #[derive(Hash, Eq, PartialEq, Debug)] - // see https://doc.rust-lang.org/nightly/std/collections/struct.HashMap.html#examples -} - -#[derive(Debug, Clone, Default, Serialize)] -pub struct StateChart { - pub states: HashMap, -} diff --git a/packages/transpiler/xstate/src/parser.rs b/packages/transpiler/xstate/src/parser.rs new file mode 100644 index 00000000..2161a313 --- /dev/null +++ b/packages/transpiler/xstate/src/parser.rs @@ -0,0 +1,109 @@ +use super::{schema::*, *}; +use scdlang::{prelude::*, semantics::Kind, Scdlang}; +use std::{fmt, mem::ManuallyDrop}; +use voca_rs::case::{camel_case, shouty_snake_case}; + +impl fmt::Display for Machine<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let get = |key| self.builder.get(key).ok_or(fmt::Error); + let (output, export_name) = (get(&Config::Output)?, get(&Config::ExportName)); + match output { + "json" | "javascript" | "js" => { + let json = serde_json::to_string_pretty(&self.schema).map_err(|_| fmt::Error)?; + if let "javascript" | "js" = output { + return write!(f, "export const {} = {}", export_name?, json.trim()); + } + write!(f, "{}", json.trim()) + } + "dts" | "typescript" | "ts" => { + let mut dts = self.to_typescript().map_err(|_| fmt::Error)?; + if output == "dts" { + dts = dts.replace("export type ", "type "); + } + write!(f, "{}", dts.trim()) + } + _ => Ok(()), + } + } +} + +impl<'a> Parser<'a> for Machine<'a> { + fn try_parse(source: &str, builder: Scdlang<'a>) -> Result { + let mut schema = StateChart::default(); + + for kind in builder.iter_from(source)? { + match kind { + Kind::Expression(expr) => { + let (current_state, next_state) = ( + expr.current_state().map(camel_case), + expr.next_state().map(|e| e.map(camel_case)), + ); + let (event_name, guard) = ( + expr.event().map(|e| e.map(shouty_snake_case)).unwrap_or_default(), + expr.guard().map(|e| e.map(camel_case)), + ); + let action = expr.action().map(|e| e.map(camel_case)); + + let (not_obj, t_obj) = ( + guard.is_none() && action.is_none(), + TransitionObject { + target: next_state, + actions: action, + cond: guard, + }, + ); + + let transition = if not_obj { + Transition::Target(t_obj.target.clone()) + } else { + Transition::Object(t_obj.clone()) + }; + + let t = schema.states.entry(current_state).or_insert(State { + // TODO: waiting for map macros https://github.com/rust-lang/rfcs/issues/542 + on: [(event_name.to_string(), transition.clone())].iter().cloned().collect(), + }); + t.on.entry(event_name.to_string()) + .and_modify(|target| { + match target { + Transition::ListObject(objects) => objects.push(t_obj), + _ if target != &transition => { + *target = Transition::ListObject(vec![ + (if let Transition::Object(obj) = target { obj } else { &t_obj }).clone(), + t_obj, + ]) + } + _ => {} + }; + }) + .or_insert(transition); + } + _ => unimplemented!("TODO: implement the rest on the next update"), + } + } + + Ok(Machine { schema, builder }) + } + + fn configure(&mut self) -> &mut dyn Builder<'a> { + &mut self.builder + } + + fn parse(&mut self, source: &str) -> Result<(), DynError> { + self.clean_cache()?; + let ast = ManuallyDrop::new(Self::try_parse(source, self.builder.to_owned())?); + Ok(self.schema = ast.schema.to_owned()) // FIXME: expensive clone + } + + fn insert_parse(&mut self, source: &str) -> Result<(), DynError> { + let ast = ManuallyDrop::new(Self::try_parse(source, self.builder.to_owned())?); + for (current_state, transition) in ast.schema.states.to_owned(/*FIXME: expensive clone*/) { + self.schema + .states + .entry(current_state) + .and_modify(|t| t.on.extend(transition.on.clone())) + .or_insert(transition); + } + Ok(()) + } +} diff --git a/packages/transpiler/xstate/src/schema.rs b/packages/transpiler/xstate/src/schema.rs new file mode 100644 index 00000000..399bc934 --- /dev/null +++ b/packages/transpiler/xstate/src/schema.rs @@ -0,0 +1,37 @@ +use serde::Serialize; +use serde_with::skip_serializing_none; +use std::collections::HashMap; + +#[skip_serializing_none] +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize)] +pub struct TransitionObject { + pub target: Option, + pub actions: Option, // TODO: should be Option> in the future + pub cond: Option, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +#[serde(untagged)] +pub enum Transition { + Target(Option), + Object(TransitionObject), + ListObject(Vec), +} + +type Event = String; + +// #[skip_serializing_none] +#[derive(Debug, Clone, Serialize)] +pub struct State { + // #[serde(flatten)] + // pub child: Option, + pub on: HashMap, + // πŸ€”β˜οΈ how about convert it to struct of #[derive(Hash, Eq, PartialEq, Debug)] + // see https://doc.rust-lang.org/nightly/std/collections/struct.HashMap.html#examples +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct StateChart { + pub states: HashMap, +} diff --git a/packages/transpiler/xstate/src/typescript/.hierarchy.ts b/packages/transpiler/xstate/src/typescript/.hierarchy.ts new file mode 100644 index 00000000..3e51b0f8 --- /dev/null +++ b/packages/transpiler/xstate/src/typescript/.hierarchy.ts @@ -0,0 +1,17 @@ +// TODO: finish this template when compound state is implemented + +//#region {#each states as $state_ +//@ts-ignore +export type $state_StateIn = StateInState<$state_> + +//@ts-ignore +export type $state_EventIn = EventInState<$state_> +//#endregion {/each} + +type StateInState = { + readonly [source in keyof Machine["states"]]: keyof Machine["states"][source]["states"]; +} + +type EventInState = { + readonly [source in keyof Machine["states"]]: keyof Machine["states"][source]["on"]; +} diff --git a/packages/transpiler/xstate/src/typescript/mod.rs b/packages/transpiler/xstate/src/typescript/mod.rs new file mode 100644 index 00000000..3febb0fe --- /dev/null +++ b/packages/transpiler/xstate/src/typescript/mod.rs @@ -0,0 +1,27 @@ +use super::*; +use serde_json; + +const TEMPLATE: &str = include_str!("schema.ts"); + +impl Machine<'_> { + pub(super) fn to_typescript(&self) -> Result { + if let Some(export_name) = self.builder.get(&Config::ExportName) { + Ok(TEMPLATE + .replace("$name", export_name) + .replace("$schema", &serde_json::to_string_pretty(&self.schema)?) + .replace( + "/*each*/EventIn", + &self + .schema + .states + .keys() + .map(|key| format!(r#"EventIn["{state}"]"#, state = key)) + .collect::>() + .join(" | "), + ) + .replace("//@ts-ignore", "")) + } else { + Err(format!("\"{config}\" must be defined", config = Config::ExportName.as_ref()).into()) + } + } +} diff --git a/packages/transpiler/xstate/src/typescript/schema.ts b/packages/transpiler/xstate/src/typescript/schema.ts new file mode 100644 index 00000000..3378e69b --- /dev/null +++ b/packages/transpiler/xstate/src/typescript/schema.ts @@ -0,0 +1,16 @@ +//@ts-ignore +type $name = $schema + +export type $nameState = keyof $name["states"] + +export type $nameEvent = {type: /*each*/EventIn} + +export type $nameSchema = { + states: {[source in $nameState]: {}} +} + +export type EventIn = EventInState<$name> + +type EventInState = { + readonly [source in keyof Machine["states"]]: keyof Machine["states"][source]["on"]; +} diff --git a/pnpm-workspace.yml b/pnpm-workspace.yml new file mode 100644 index 00000000..15d0d7bf --- /dev/null +++ b/pnpm-workspace.yml @@ -0,0 +1,2 @@ +packages: + - 'examples/**' \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 06c1b6ae..e12dde9e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,7 @@ disallow_untyped_defs=False check_untyped_defs=False [flake8] -ignore = E203, E266, E501, W503 +ignore = E203, E266, E501, W503, E731 max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 \ No newline at end of file diff --git a/tests/fixtures/semantic_errors/event.md b/tests/fixtures/semantic_errors/event.md index af52e291..ef0453f4 100644 --- a/tests/fixtures/semantic_errors/event.md +++ b/tests/fixtures/semantic_errors/event.md @@ -1,5 +1,5 @@ --- -title: Semantics Error on Transition with Event +title: Semantics Error/Warning on Transition with Event references: --- @@ -9,7 +9,34 @@ references: ##### 1. Have more than one transition with same trigger βœ” Transition with specific event must only occur once. ```scl,error -A -> B @ D -A -> C @ D +A -> B @ E +A -> C @ E +``` +Which state should `A` transtition to when `E` is triggered? + +##### 2. Event both **with** and **without** guard pointing to same state βœ” +This expression is redundant. +```scl,error +A -> B @ E[valid] +A -> B @ E +``` +Regardless `valid` is true or false, `A` will transition to `B` when `E` is triggered. +This should be rewritten as: +```scl +A -> B @ E +``` + +##### 3. Multiple guards on same event +This expression can cause unpredictable transition. +```scl,warning +A -> B @ E[valid] +A -> C @ E[exist] +``` +Which state should `A` transtition to when `E` is triggered despite both `valid` and `exist` is true? +Formal verification should be used for extra precautions: +```scl +assume [guards <= 2] in A -> * + +A -> B @ E[valid] +A -> C @ E[exist] ``` -Which state should `A` transtition to when event `D` is triggered? \ No newline at end of file diff --git a/tests/fixtures/semantic_errors/transient_transition.md b/tests/fixtures/semantic_errors/transient_transition.md index ef2606d1..b3c3b9a3 100644 --- a/tests/fixtures/semantic_errors/transient_transition.md +++ b/tests/fixtures/semantic_errors/transient_transition.md @@ -1,5 +1,5 @@ --- -title: Semantics Error on Transient Transition +title: Semantics Error/Warning on Transient Transition references: --- @@ -29,8 +29,23 @@ A -> C @ D [isAllowed] ``` Even `isAllowed` is true, the program that implement this state machine will likely don't have enough time to trigger event `D`. However, guarded event with no trigger is allowed because guard is precomputed (hence it written in camelCase). -```scl +```scl,warning A -> B A -> C @ [isAllowed] ``` -`A` will transition to `C` if `isAllowed` else it will transition to `B`. \ No newline at end of file +`A` will transition to `C` if `isAllowed` else it will transition to `B`. + +##### 3. Have multiple guards (auto transition) +This expression can cause unpredictable transition. +```scl,warning +A -> B @ [valid] +A -> C @ [exist] +``` +Which state should `A` transtition to when `valid` and `exist` is true? +Formal verification should be used for extra precautions: +```scl +assume [guards <= 2] in A -> * + +A -> B @ [valid] +A -> C @ [exist] +``` diff --git a/tests/fixtures/syntax/action.md b/tests/fixtures/syntax/action.md index 61c0deba..ca287b87 100644 --- a/tests/fixtures/syntax/action.md +++ b/tests/fixtures/syntax/action.md @@ -132,11 +132,11 @@ Read as: "decrement `x` when entering *state **Alpha*** and increment `x` if exi #### activity ```scl -state Beta { do |> beeping } +state Beta { do <> beeping } ``` or ```scl -Beta >< beeping +Beta <> beeping ``` Read as: "perform *activity **beeping*** when on *state **Beta***" @@ -158,18 +158,17 @@ state Beta { @ Click |> something } ``` or ```scl -Beta @ Click |> something +Beta @ Click |> something // βœ” ``` -Read as: "execute *action **something*** when *event **Click*** occurred while in *state **Beta***" +Read as: "while in *state **Beta*** and *event **Click*** occurred, execute *action **something***" -##### with guard πŸ€” +##### with guard ```scl -state Beta { @ Click[x > 0 & In(A)] |> something } +state Beta { @ Click[allGreen] |> something } ``` or ```scl -Beta @ Click[x > 0 & In(A)] |> something +Beta @ Click[allGreen] |> something // βœ” ``` -Read as: "execute *action **something*** *event **Click*** can occurred while in *state **Beta*** only if in *state **Alpha***" - +Read as: "while in *state **Beta*** and *event **Click*** occurred, execute *action **something*** only if *condition **allGreen*** is true" --- diff --git a/tests/fixtures/syntax/event.md b/tests/fixtures/syntax/event.md index 01e85094..43ae2fd9 100644 --- a/tests/fixtures/syntax/event.md +++ b/tests/fixtures/syntax/event.md @@ -188,7 +188,7 @@ A -> B @ C[*] |> * ```scl A -> B @ C[isD] ``` -Read as: "*state **A*** transition to *state **B*** at *event **C*** only if *condition **isD*** is true" +Read as: "*state **A*** transition to *state **B*** at *event **C*** only if *condition **isD*** is true" βœ” ##### use $expression ```scl @@ -200,19 +200,24 @@ A -> B @ C[x > y] Read as: "*state **A*** transition to *state **B*** at *event **C*** only if `x > y`" ##### use boolean operator +> doesn't map well with xstate - symbol: `|`,`&`,`!`,`^` ```scl A -> B @ C[D|!E] ``` -or +Read as: "*state **A*** transition to *state **B*** at *event **C*** only if *condition **D*** is true or *condition **E*** is false" + ```scl let VarX as x let VarY as y A -> B @ C[x > y & y > 0] ``` +Read as: "*state **A*** transition to *state **B*** at *event **C*** only if `x > y` and `y > 0`" ##### use "in state" guards +> Only valid if it's transition from compound/parallel state + ```scl let VarX as x @@ -220,7 +225,7 @@ state A { E -> G @ F G -> E @ F } -A -> B @ C[] //TODO: consider to use `in` keyword πŸ€” +A -> B @ C[in G] ``` Read as: "*state **A*** transition to *state **B*** at *event **C*** only if **A** is in *state **G***" @@ -229,7 +234,7 @@ Read as: "*state **A*** transition to *state **B*** at *event **C*** only if **A ```scl A -> B @ C |> f ``` -Read as: "*state **A*** transition to *state **B*** at *event **C*** will execute *action **f***" +Read as: "*state **A*** transition to *state **B*** at *event **C*** will execute *action **f***" βœ” ```scl |> f { @@ -244,12 +249,12 @@ Read as: "execute *action **f*** ##### with guards ```scl -A -> B @ C[D] |> f +A -> B @ C[isD] |> f ``` -Read as: "*state **A*** transition to *state **B*** at *event **C*** only if *condition **D*** is true will execute *action **f***" +Read as: "*state **A*** transition to *state **B*** at *event **C*** only if *condition **isD*** is true will execute *action **f***" βœ” ```scl -@ [D] { +@ [isD] { A -> J @ D |> g A ->> B @ C[isEmergency] |> f }