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.
Ah yes, he is the author of Kingly which is another state-machine Javascript library that has nice integration with several visualization tools. I highly recommend checking the docs.
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)
Readable
This can be achieved by utilizing symbols (and ligature-symbol) to mimic the diagram. However, the symbol should be meaningful. Let me take an example from smcat language:
instead of
"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)
Writeable
This part can be divided into 2:
conciseness
refactorability
Conciseness can be achieved by having syntactic sugar. For example:
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 🤔
Transferable
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):
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:
repl terminal (already implemented but not yet polished)
analyzer for various usages. One of them is to get information how many times a state is visited and events that need to be triggered when traveling into a specific state.
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.
Conclusion
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:
Logic State - A State Machine Language
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.
However...
👇 Reality 👇
This is what we get 😂 (sorry @brucou)
History
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.
Ah yes, he is the author of Kingly which is another state-machine Javascript library that has nice integration with several visualization tools. I highly recommend checking the docs.
Design Philosophy
Logic State share the same philosophy with Scdlang, which is:
Readable
This can be achieved by utilizing symbols (and ligature-symbol) to mimic the diagram. However, the symbol should be meaningful. Let me take an example from smcat language:
instead of
just write it as
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:
[[
,)))
, etc) like in lisp. However, ligature-symbol like multiple dashes (-----
) or such are allowed.Writeable
This part can be divided into 2:
Conciseness can be achieved by having syntactic sugar. For example:
will desugar into
also (work in progress)
desugar into
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 🤔
Transferable
This is the trickiest part. Let's try to relate state-machine between its use cases and the programming languages it commonly used:
Hmm, but that doesn't answer anything 🤔
Now relate them how state machine are implemented (mostly):
That's much better 😲
However, Logic State is a domain-specific language and there are several ways how DSLs are used:
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:
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,orWe 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:
Roadmap
Short term
Long Term
Conclusion
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:
Team
Please let me know if anyone figures out how to display the diagram side-by-side in the demo 🥺
@Vandesm14 thank you. This is the first time I use Nim in a project 🤠