https://github.com/drsensor/scdlang
Scdlang is a description language for describing Statecharts that later can be used to generate code or just transpile it into another format. Although the syntax itself inspired from text-based drawing (e.g mermaid or graphviz), 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.
Statecharts (or State Machine in general) has a lot of benefit ranging from modeling User Experience (UX) to NPC behavior. While it has a lot of benefits, each implementation is different and the only single-source-of-truth is the diagram itself. A visual diagram is great but it's time consuming when you want to refactor it (one of many reasons why visual programming not popular). As far as I know, there is 3 way to implement Statecharts model in the programming language:
| Approach | Pro | Con |
|---|---|---|
| via key-value pair | fast (almost O(1)) | cost more memory |
| via pattern-matching/switch-case/if-else statement | less memory | has runtime overhead |
| typestate | type-safe, fast, no memory overhead | only applicable on static type language that has generic type |
Each implementation has its pros and cons which suitable depend on the context. For example:
template (I'm still not sure if it's categorized as typestate programming or pattern-matching 🤔)Hopefully (in the future), Scdlang can be used as a single source of truth to model a state machine regardless of the interpreter, platform, or programming language you use.
This project is still in really early stage and it's a one-man show for several reasons. For now, this language can only be transpiled into XState format and only support Finite State Machine representation. Regardless of that, here is the current features:
This description-language can produce beautiful error messages similar to Rust out of the box (thanks to Pest). Although Pest give me beautiful error messages for free, some adjustment needs to be done to make the error messages less confusing (see packages/core//error/format.rs). This adjustment is a never-ending process which I need to document on how to fine-tune that in the Contribution Guidelines.
As for the syntax highlighting, it handled using prettyprint and I only use it in the CLI layer. (I also contribute a bit on that package to make it fit into this project 😋)
Because Scdlang is a description-language (not programming-lang), it doesn't mean there is no need to add semantics analysis. This language needs to have semantics analysis because StateMachine and Statecharts have some caveats which most of them are related to a state transition. Currently, my only source about prohibited transition is in the OMG UML specification (it has more details than W3C SCXML spec).
For more info about the semantics error (still in draft), see files in the folder tests/fixtures/semantic_errors
TL;DR see examples/simple.scl
//line and /*block*/ comments', double-quote ", backtick `/*
bunch of expressions
*/
Z ---------------------------> A @ Reset
It's not exactly a feature. I use this technique when choosing a symbol and designing fluent API
Transient transition 👇
"get🆙" -> Walk
Eventful/Triggered transition 👇
On -> Off @ Shutdown
Reverse arrow 👇
On -> Off @ Toggle
On <- Off @ Toggle
For a new language, only having core functionality without having a tool to play around is a bit unexciting. So here I create the CLI to help me evaluate it in interactive manners. The CLI itself implemented using clap (without additional args-parsers-helper like Structopt or others). The CLI is called scrap which is stand for Statecharts Rhapsody (in respect to Rational Rhapsody). Currently, scrap has 2 main subcommands:
scrap code|generate for code generationscrap eval|repl to enter REPL modeThis CLI is also pipe friendly which mean the output between interactive and non-interactive shell is different. This is useful when you want to log the error into a file or preprocess the results via stdout|>stdin.
The essential feature of many compiler/transpiler is to output the result. In scrap code command, the output is base on --format flag as long as there is no syntax or semantic error. By default, it parses the whole file and outputs it as an XState format (in JSON). To parse it line-by-line, you need to provide --stream flag which also gives you the partial results.
Ideally, when someone wants to showcase a new language, they will create a playground site (e.g playrust or smcat). Because I'm focusing this first implementation on CLI, I create a REPL feature instead of the playground site. The caveat is when someone wants to try this, they need to install the CLI first 😋.
This REPL subcommand is also pipe friendly just like scrap code. The only differences are it always outputs line numbers when running on the interactive shell. It also can receive input from stdin when piped (see Fig 3.).
Current toolchains:
| Description | Tools |
|---|---|
| Task runner | Justfile |
| Linter / Code checker | clippy, flake8 + mypy |
| Code formatter | rustfmt, black |
| Dependency manager | cargo, pipenv |
Key Dependencies:
| Dependency | Use on |
|---|---|
| Pest | core |
| clap | cli |
| serde | transpiler/* |
The CI will play a big part in detecting bugs and performance degradation early on (hopefully). Currently, I'm using 2 CI service:
The CI configuration is still far from complete. A lot of things need to be done like publishing to package repository (e.g PPA, AUR, crates.io, etc) and others. At least this will help to do regression tests and quickly download the release binary without compiling it first.
As for what performance metrics I measure, I just measure the CPU Load and Peek Memory when parsing 1000 lines of Scdlang code without empty lines or comments (see Perf CLI release action). I also measure the build time in non-release mode because I need to be aware of which dependencies are bloated. Unlike Javascript, detecting bloated dependencies is not as simple as getting the bundle size. By measuring the compile time, I can guess when I add a bloated dependency by comparing the compile time of before and after I add it. It also gives me a clue about duplicated instantiation.
Ideally, continuous benchmarks should be run in the dedicated machine to achieve consistency.
Well, I just need it to be cheap 💸
This project currently separated into 3 modules:
| Module | Description | Test strategy |
|---|---|---|
| cli | module that compiled into binary CLI | integration test |
| core | module that implements the core features | unit test |
| transpiler/* | bunch of modules to transpile Scdlang code into another format | unit test |
On <-> Off @ Toggle)scrap eval --format smcat --pipe-to 'smcat --input-type json [stdin] | open-in-browser'
I'm still figuring out how to document the project architecture and also on how to fine-tune and make some changes (especially for the error message). However, feel free to open a github issue if you have some ideas or feature requests.
In the beginning, I create this project just to evaluate Pest parser before I use it in another project (I need decent context-free grammar for data cleansing). In the experiment (see branch prototype), I make text-based drawing language as a problem that I want to solve. I also took a chance to play around with Github Actions and test the idea of storing custom metadata on each commit using git-notes (in this case I store the macro-benchmark results). Now I realize that seems I've gone too far drilling the problems 😂. I want to push this project forward and see what it will become 🥺.
The development of this project is kinda slow and less productive. Hopefully, Rust compiler and the type-checker will get faster while the language server consumes less memory over time 😢