kossnocorp / superstate

Type-safe JavaScript statecharts library
https://superstate.io
MIT License
76 stars 0 forks source link

MermaidJS to Superstate #1

Open tomByrer opened 3 months ago

tomByrer commented 3 months ago

FYI I might make another target to translate Mermaid JS to Superstate.

I'm already 1/2 way there; since I knew there would be multiple types of sources & targets for statecharts, I made an intervening output (sort of simple AST), so the only step is to go from that Object notation to Superstate. EG: source:

stateDiagram-v2
id mermaidJS state diagram
%% inspiration: https://mermaid.js.org/syntax/stateDiagram.html
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still : STOP %% named transition / event
Moving --> Crash
Crash --> [*]

to OSM:

{
  "final": [
    "Still",
    "Crash",
  ],
  "id": "mermaidJS state diagram",
  "initial": "Still",
  "isConcurrent": false,
  "states": {
    "Crash": {
      "Crash--FINIS": "FINIS",
    },
    "FINIS": "final",
    "Moving": {
      "Moving--Crash": "Crash",
      "STOP": "Still",
    },
    "Still": {
      "Still--FINIS": "FINIS",
      "Still--Moving": "Moving",
    },
  },
}

Which in tern is used to make a XState machine

import { createMachine } from "xstate";

export const machinemermaidJSstatediagram = createMachine({
    id: "mermaidJS state diagram",
    initial: "Still",
    states: {
        Still: {
            on: {
                "Still--Moving": {
                    target: "Moving"
                },
                "Still--FINIS": {
                    target: "FINIS"
                }
            }
        },
        Moving: {
            on: {
                STOP: {
                    target: "Still"
                },
                "Moving--Crash": {
                    target: "Crash"
                }
            }
        },
        Crash: {
            on: {
                "Crash--FINIS": {
                    target: "FINIS"
                }
            }
        },
        FINIS: {
            type: "final"
        }
    }
},);
kossnocorp commented 3 months ago

Hey! This is fascinating! Can you tell me more about how this can be applied? I imagine this can be used to initialize a state chart, but then Superstate will lose all the type safety.

If it's to generate code, I'm curious if there's a way to synchronize and define substrates separately from the root statechart?

kossnocorp commented 3 months ago

By the way, I plan to do the vice-versa translation out of the box, so you can get Mermaid code from any statechart, which is admittedly much easier.

tomByrer commented 3 months ago

type safety

Yea, it was weird enough to add branching & "actions" to MermaidJS, & I still don't have context. + I'm quoting "actions" because there are no functions in MermaidJS, so I only allow to tag where the actions & filters get called. I expect folks have to hand-code actual code after conversion.

vice-versa translation

Well, I didn't think of OSM to Mermaid transcoding, but here is other example of the intervening pseudo-AST: https://github.com/tomByrer/markdown2statemachine/blob/master/test/__snapshots__/snap.test.js.snap#L908C1-L944C5 Might be a bit verbose since I tend to keep in many empty values, & order is reversed. I also keep in the weird markup I invented for actions & guards in the OSM. But otherwise readable if you're familiar with XState.

Maybe there is a better way to deal with actions & guards better in the OSM?

If there was a common AST-ish for all things StateMachine, then not only documentation would be easier for all StateMachines (eg SuperState -> Mermaid), but different SM implementations can be used in the same workflow.

(Prior art for front-end development: Mitosis)

davidkpiano commented 3 months ago

If there was a common AST-ish for all things StateMachine, then not only documentation would be easier for all StateMachines (eg SuperState -> Mermaid), but different SM implementations can be used in the same workflow.

I'd be happy working towards this goal. I have started a statechart schema based on XState here: https://github.com/statelyai/specification/blob/main/schemas/statechart.json

kossnocorp commented 2 months ago

@tomByrer I just shipped superstate@1.0.0-beta.2 with the toMermaid function, which I think might be helpful. I used the XState approach to formatting actions, events, and guards.

I don't think I have access to the repo (https://github.com/tomByrer/markdown2statemachine), so I can't check how close it is to what you got right now.

Even though it could be beneficial for Superstate to render events as eventName() and actions as actionName! (as that's the convention in the code), having compatibility with XState, at least behind an option, will be the best bet.

@davidkpiano I'm not sure if you plan to change it or expand it, but I would appreciate your input.

I'm talking about those (plus guarded events if [long]):

image

If there was a common AST-ish for all things StateMachine, then not only documentation would be easier for all StateMachines (eg SuperState -> Mermaid), but different SM implementations can be used in the same workflow.

That's a fascinating idea. I'm unsure if @davidkpiano designed the spec to be universal, but we can at least use a subset of it.


Here's how the Mermaid rendering works:

import { superstate } from "superstate";
import { toMermaid } from "superstate/mermaid";

type VolumeState = "low" | "medium" | "high";

const volumeState = superstate<VolumeState>("volume")
  .state("low", "up() -> medium")
  .state("medium", ["up() -> high", "down() -> low"])
  .state("high", "down() -> medium");

type PlayerState = "stopped" | "playing" | "paused";

const playerState = superstate<PlayerState>("player")
  .state("stopped", "play() -> playing")
  .state("playing", ["pause() -> paused", "stop() -> stopped"], ($) =>
    $.sub("volume", volumeState)
  )
  .state("paused", ["play() -> playing", "stop() -> stopped"]);

const mermaid = toMermaid(playerState);

And produces:

%% Generated with Superstate
stateDiagram-v2
    state "player" as player {
        [*] --> player.stopped
        player.stopped --> player.playing : play
        player.playing --> player.paused : pause
        player.playing --> player.stopped : stop
        player.paused --> player.playing : play
        player.paused --> player.stopped : stop
        state "stopped" as player.stopped
        state "playing" as player.playing {
            [*] --> player.playing.low
            player.playing.low --> player.playing.medium : up
            player.playing.medium --> player.playing.high : up
            player.playing.medium --> player.playing.low : down
            player.playing.high --> player.playing.medium : down
            state "low" as player.playing.low
            state "medium" as player.playing.medium
            state "high" as player.playing.high
        }
        state "paused" as player.paused
    }

Mermaid code:

%% Generated with Superstate
stateDiagram-v2
    state "player" as player {
        [*] --> player.stopped
        player.stopped --> player.playing : play
        player.playing --> player.paused : pause
        player.playing --> player.stopped : stop
        player.paused --> player.playing : play
        player.paused --> player.stopped : stop
        state "stopped" as player.stopped
        state "playing" as player.playing {
            [*] --> player.playing.low
            player.playing.low --> player.playing.medium : up
            player.playing.medium --> player.playing.high : up
            player.playing.medium --> player.playing.low : down
            player.playing.high --> player.playing.medium : down
            state "low" as player.playing.low
            state "medium" as player.playing.medium
            state "high" as player.playing.high
        }
        state "paused" as player.paused
    }
davidkpiano commented 2 months ago

That's a fascinating idea. I'm unsure if @davidkpiano designed the spec to be universal, but we can at least use a subset of it.

Yes, I intend for the specification to be universal 👍