Compiler

The slim command supports the creation of state machines that are optimized for production. The Kingly state machine library may end up contributing between 5 and 12 KB of your production bundles. The slim compiler allows you to reduce further the footprint of your state machines by compiling away the Kingly library. Real-life, large machines compile to ~2 KB size. Additionally, the compiled code is plain, zero-dependency JavaScript that will work in older browsers (IE > 8).

The slim compiling command is designed to work with and complement the yed graph editor. You may find more information on yEd in the Graph editor section of this documentation.

slim takes a .grapml yEd file as input and outputs JavaScript files that define and export a state machine factory function. Developers can then import the machine factory function in their program and create a Kingly state machine by calling the factory with the required parameters.

You are invited to review the Password meter tutorial for a guided example of turning a machine drawing into a JavaScript function with slim.

From a drawing to a JavaScript function

A Kingly state machine computes outputs as a result of being passed an input — we will often say that the machine processes or receives an event. The high-level specifications of the machine computation can be represented with a graph. Nodes in the graph correspond to control states of the machine. Edges connecting two nodes represent transitions between control states of the machine.

We said specifications because the drawing let us know how the machine will compute in response to an input depending on the state it is in. We said high-level specifications because the drawing does not allow the actual computing of a machine response — it is a drawing. Let’s look at a simple such drawing from the tutorials:

simple machine

This machine drawing tells us that when processing a click input, the modeled JavaScript machine should trigger the incrementing of a counter, and some rendering. It does not tell us how exactly that works. To turn the drawing into an actual JavaScript function, you need to provide the missing JavaScript objects:

The .graphml file contains the following pieces of information:

The slim CLI extracts the pieces of information contained in the .graphml file that it receives as a parameter and produces two almost identical JavaScript files. One is destined to be used in a browser context (.js file), the other in a Node.js context (.cjs file that can be required but not imported).

The produced JavaScript files import/require the Kingly library, and export a machine factory that can be used by other modules to construct the actual JavaScript machine we seek. The machine factory must be passed the following pieces of information that are missing from the graph:

You are invited to review the Password meter tutorial for a guided example of turning a machine drawing into a JavaScript machine with yed2kingly.

How does it work?

In a typical process, you draw a machine with the yEd editor. When done or ready to test, you save the file in the default .graphml format in the same directory in which you want to use the target state machine. You run the slim command on the newly saved file. That generates the compiled JavaScript file which exports a machine factory function. The factory is passed parameters to create a Kingly state machine.

Get started

If you haven’t yet installed the yEd editor, please do so by following the instructions here.

To use slim in the shell terminal, you will need to install the package globally:

npm install -g slim

Usage

slim filename.graphml

Running the converter produces two files, targeted at consumption in a browser and Node.js environment. Assuming the file src/graphs/file.graphml is passed to slim, the following two files are created: src/graphs/file.graphml.fsm.compiled.js, and src/graphs/file.graphml.fsm.compiled.cjs.

Examples

The following machine graph:

top-level-init test graph

when compiled with slim leads to the following JavaScript file:

// Generated automatically by Kingly, version 0.29
// http://github.com/brucou/Kingly
// Copy-paste help
// For debugging purposes, guards and actions functions should all have a name
// Using natural language sentences for labels in the graph is valid
// guard and action functions name still follow JavaScript rules though
// -----Guards------
/**
 * @param {E} extendedState
 * @param {D} eventData
 * @param {X} settings
 * @returns Boolean
 */
// const guards = {
//   "isNumber": function (extendedState, eventData, settings){},
//   "not(isNumber)": function (extendedState, eventData, settings){},
// };
// -----Actions------
/**
 * @param {E} extendedState
 * @param {D} eventData
 * @param {X} settings
 * @returns {{updates: U[], outputs: O[]}}
 * (such that updateState:: E -> U[] -> E)
 */
// const actions = {
//   "logNumber": function (extendedState, eventData, settings){},
//   "logOther": function (extendedState, eventData, settings){},
// };
// -------Control states---------
/*
      {"0":"nok","1":"Numberღn0","2":"Otherღn2","3":"Doneღn3"}
      */
// ------------------------------

function createStateMachine(fsmDefForCompile, stg) {
  var actions = fsmDefForCompile.actionFactories;
  var guards = fsmDefForCompile.guards;
  var updateState = fsmDefForCompile.updateState;
  var initialExtendedState = fsmDefForCompile.initialExtendedState;

  // Initialize machine state,
  // Start with pre-initial state "nok"
  var cs = 0;
  var es = initialExtendedState;

  var eventHandlers = [
    ...
  ];

  function process(event) {
    ...
  }

  // Start the machine
  process({ ["init"]: initialExtendedState });

  return process;
}

export { createStateMachine };

Let’s illustrate the parameters received by the createStateMachine factory function:

// require the js file
const { createStateMachine } = require(`${graphMlFile}.fsm.compiled.cjs`);

// Build the machine
const guards = {
  'not(isNumber)': (s, e, stg) => typeof s.n !== 'number',
  isNumber: (s, e, stg) => typeof s.n === 'number',
};
const actionFactories = {
  logOther: (s, e, stg) => ({ outputs: [`logOther run on ${s.n}`], updates: {} }),
  logNumber: (s, e, stg) => ({ outputs: [`logNumber run on ${s.n}`], updates: {} }),
};
const fsm1 = createStateMachine({
  initialExtendedState: { n: 0 },
  actionFactories,
  guards,
  updateState,
}, settings);

As the example illustrates, the factory function’s first parameter consists of four objects:

Note that the compiled machine does not offer error messages, protection against malformed inputs, devtool support, or logging functionality. This is only possible when using the Kingly library — and its extra kilobytes.

Note also that, as much as possible, slim refrains from using advanced JavaScript language features in the generated code to be compatible with older browsers without polyfilling or babel-parsing. This, however, has not been thoroughly tested so far.

You will find additional examples in the /tests directory of the slim Github repository.

Tips

Size of the generated file

This section contains rather technical considerations that do not impact your usage of Kingly or its tooling. Feel free to skip if you have more pressing matters to consider.

Assuming the machine has no isolated states (i.e. states which are not reached by any transitions), the size of the compiled file roughly follows the shape a + b x Number of transitions, i.e. is mostly proportional to the number of transitions of the graph. The proportional coefficient b seems to be fairly low and the compression factor increases with the size of the machine. In short, you need to write a really large graph to get to 5Kb just for the machine.

Minification is performed online with the javascript-minifier tool. Lines of code are counted with an online tool.

We give a few data points:

Machine graph Machine graph size Compiled machine size
counter machine graph 1 control state, 1 transition ~50 loc, 0.5 KB
[password meter modelization] 4 control state, 5 transitions ~80 loc, 0.6 KB
Conduit average-sized application ~35 control states, 75 transitions ~2.3 KB

We estimate that writing logic by hand for average-size machines may shave 100 (extra code due to the compiler) + 400 bytes (extra code due to using a graph editor) for a total of 0.5 KB.

Note that this size does not (and cannot) include the actions and guards but does represent the size of the logic encoded in the machine.

Those preliminary results are fairly consistent. Assuming 20 bytes per transitions (computed from the previous data points), with a baseline of 500 bytes, to reach 5 KB (i.e. the size of the core Kingly library) we need a machine with over 200 transitions!!

In summary, with the slim compiler, Kingly proposes state machines as a near-zero-cost abstraction. This means that if you would have written that logic by hand, you would not have been able to achieve a significantly improved min.gzipped size.

Troubleshooting

The script will exit with an error code whenever:

Known limitations

The .graphml format for yEd is not publicly documented. The specifications for the format have not changed in many years. However, our parser may still have holes or break if the format specifications change. It is thus important that you log issues if you encounter any errors while running the compiler.

Feedback welcome!

Kingly tooling exist to address pain points from real use cases, and drive your productivity up. Your feedback is welcome and may result in the improvement or extension of the existing set of tools. If there is anything you feel should be addressed and is not, or should be addressed differently, get in touch by opening an issue on Github.