You need a diagram to picture the soundness of your system
A picture is worth a thousand words while a symbol is worth a hundred words
Let me start with meme-like
👇 Goal 👇
conversation between me and @brucou
The plan for this jam is to make the scope as minimal as possible and provide a real usage demo.
👇 Reality 👇
This is what we get 😂 (sorry @brucou)
This language is a rewrite (and also redesign) of my previous prototype called Scdlang. It starts from my curiosity on how to define state machine in Rust which I'm new to that language back then (more info). As I experiment, the design of the language grow and I found a lot of interesting tools for the state machine. However, I can't find a language that similar to drawing-language like Graphviz or Mermaid but has semantics analysis and can generate code or be embedded in another programming language.
I've been discussing the design in the past with @brucou (see issue #29) but then I just realize that the design of this language become too wild and Rust macro is too slow. So I took this chance to rewrite it from scratch with a different approach.
- Readable just like you read then visualize a state diagram
- Writeable just like you write code which is concise and can be refactored
- Transferable to any implementation (e.g platform, programming language, runtime, etc)
"on backlog" => doing : working on it;
just write it as
on_backlog -> doing @ working_on_it
which can be read as
on backlog into doing at working on it
The thing that needs to be considered is not having too many symbols in one-line.
My rule thumbs for this are:
- the ratio between words and symbols/ligature-symbols is >5:<5 - Mean that the word count of one file/line must be same or less than the symbol or ligature-symbol count
- avoid repetitive symbol - For example, there no things like double bracket (e.g
))), etc) like in lisp. However, ligature-symbol like multiple dashes (
-----) or such are allowed.
:: there are 2 rules cause I only have 2 thumbs 🥁 (pun intended)
This part can be divided into 2:
Conciseness can be achieved by having syntactic sugar. For example:
when <-> where @ ask
will desugar into
when -> where @ ask when <- where @ ask
also (work in progress)
when,what,why -----> who @ conclusion
who <- when @ conclusion who <- what @ conclusion who <- why @ conclusion
As for refactorability, the language might have a module/import system (not decided) and hierarchical states (#29) in the future. However, this will not happen in a short time to keep the scope of this project as small as possible so it can be embedded in another programming language. Maybe there are better alternatives 🤔
this part has relations on why full rewrite to Nim is necessary
This is the trickiest part. Let's try to relate state-machine between its use cases and the programming languages it commonly used:
- Game - C#, Lua, ... , and Webassembly
- Embedded System - C/C++/Rust for microcontroller; many HDL variants for ASIC design;
- Apps - Java/Typescript, Kotlin, Dart, ... , and Webassembly
- Server/Services - Erlang, Elixir, Akka, ...
Hmm, but that doesn't answer anything 🤔
Now relate them how state machine are implemented (mostly):
- Game - State Pattern and/or Conditional Statement
- Embedded System - Typestate
- Server/Services - Actor Model
That's much better 😲
However, Logic State is a domain-specific language and there are several ways how DSLs are used:
- embedded into programming language (e.g GraphpQL, lit-html, LINQ, regex)
- single source of truth (e.g GraphQL, protobuf, EBNF)
- sent over a wire (e.g GraphQL)
To make state machine language that can last in the long term, we need to support all those scenarios. That sound impossible but we can chip it down gradually as long as we use the right approach 😉
There is 2 aspect of how we can design this language:
- State Machine as a runtime - this is a case when you need a system with dynamic behavior
- State Machine as a generated code - this is a case when you need a system with unchanged/static behavior
And due to that complexity, we need to design Logic State as embeddable DSL first. Compiler for generating code can be implemented in any language whatever it uses VM or not. However, to make it compile at runtime is a different matter. We need to approach it on how PCRE (regex) or Rosie does. In short, we also need to package it as a static library, dynamic (shared) library, and webassembly module. This means the most suitable language for writing the compiler is: C/C++, Rust, Nim, or anything that can do manual memory management or have a very slim garbage collector.
Making 2 separate compilers is also an option. Where one is for generating code and the other is to be used at runtime. However, maintaining 2 implementations without a single source of truth can be quite difficult. And so there is 3 way we can approach this:
create an AST, use parser combinator,or
- just make composable grammar.
We use the later. Composable grammar is much easier to work with but there are very limited libraries or languages that support this. Raku is the best language for writing composable grammar but it needs MoarVM. Luckily Nim has a library called NPeg to write composable grammar.
In conclusion, Nim should be used to write the compiler because it brings these benefits:
- we can write composable grammar using NPeg
- we can compile it to:
- we can choose various memory management strategy ranging from not using any GC into using Go's garbage collector
[side-note] Nim macros are faster than Rust macros.
- rich toolings
- real usage demo
- expand the compiler to support 3 types of DSL:
- embedded DSL which means it can be embedded into other languages or platforms. The DSL should have the most minimal subset syntax (e.g NO: import system, hierarchial state, etc) to keep it performant.
- external DSL which means it can be used as a single source of truth. However, it needs a module or import system to make it composable.
- extendable DSL, similiar to markdown maskfile or kakoune config which use special syntax to integrate with another languages.
- integrate into various programming languages, including actor-model languages like Erlang/Elixir
- better packaging or have plugin systems. Anything that can make non-core contribution and integration easier.
A bit out of topic but I've been participating in various game jams this past month here and learn a lot from another. So I've been thinking that I need an outlet to experiment and grow this language in a fun way. The game that I make mostly is vehicular combat (though I prefer to call it car battle) and I'm thinking to integrate this language where the player must constantly write a state machine to control the vehicle and defeat the enemy. So that even if I can't develop this language in fulltime, I can still develop it for my entertainment since using it in production might be too risky and can put people off.
Currently, this language supports generating code for 3 programming languages:
|Typescript Interface||Type State, Record/Collection|
|Rust Trait||Type State|
|Rust||Type State, Conditional Statement|
- Source: https://github.com/logic-state/logicstate
- Demo: https://repl.it/@logicstate/logicstate#examples/detective.logic
P.S: it takes some time to Run ▶ the demo (
examples/detective.logic) because it also needs to compile the source first (approx 5-15 seconds)
Please let me know if anyone figures out how to display the diagram side-by-side in the demo 🥺