Little Language Case Study - Generating Code for Simple State Machines
Why draw a diagram when you could write 50+ lines of boilerplate instead? 2026-04-08
There’s a moment every programmer knows: you’re staring at a blob of nested if statements and callback hell, and you think — I know exactly what this is supposed to do, I just can’t see it anymore.
State machines are one of the classic cures. They’re one of computing’s oldest and cleanest ideas: a system exists in one of a finite number of states, and transitions between them are triggered by events or conditions. Compilers use them. Network protocols are built on them. Game AI runs on them. Traffic lights run on them.
And yet, when we go to implement a state machine in a general-purpose language, something unfortunate happens.
The Diagram vs. The Code
Consider a simple looping state machine — the kind you might use to track a value bouncing between two bounds. Here’s the design, expressed as a diagram:
(three states — idle, wait for w recrossing, wait for zero recrossing — with guarded transitions between them)
The logic is immediately legible:
From idle: if x > w, call reverse() and move to wait for w recrossing. If x < 0, call reverse() and move to wait for zero recrossing.
From wait for w recrossing: if x < w, return to idle.
From wait for zero recrossing: if x > 0, return to idle.
That’s the whole machine. You could explain it to a non-programmer in two minutes.
A practical note: the code annotating each transition (the guards and actions) is written in raw Python. The state machine runtime expects an object — called e — through which the surrounding code exposes variables and functions like x, w, and reverse(). This is a deliberate separation: the diagram owns the control flow, and the host code owns the domain logic.
Now look at the Python that implements it — over 50 lines generated from that exact diagram:
It’s correct. It works. And it’s a completely opaque rendering of something that was obvious in the diagram while the Python code buries the intended control flow under a layer of low-level detail, forcing a functional representation onto something that is fundamentally a control flow structure.
The control flow architecture has been obscured. The states are still there if you squint, but the shape of the machine — the gestalt that makes state machines so powerful for reasoning — has been smeared across a class definition.
The Pattern Hidden in the Boilerplate
The generated code reveals a consistent pattern. Every state is represented by exactly three functions:
enter_<state>— called once when entering the statestep_<state>— called repeatedly; evaluates guards and fires transitionsexit_<state>— called once when leaving the state
A transition from one state to the next follows a fixed multi-step ritual inside the current state’s step function:
Evaluate the guard (e.g. if x > w)
If true: call exit from the current state
Call any transition action (e.g. reverse())
Call enter into the next state
The machine itself is a class with a state variable and a dispatch table that routes step() calls to the right function for the current state.
This is all perfectly mechanical. There’s no creativity in writing it. It’s transcription work — and that’s exactly what code generation is for.
Little Languages, Big Leverage
The diagram isn’t just documentation. It is the program — expressed in what I call a DPL (Diagrammatic Programming Language).
DPLs are “little languages.” They make no pretense of Turing completeness. They don’t try to solve every possible problem. They solve one problem — in this case, expressing state-machine control flow — and they solve it well.
The key insight is that a DPL doesn’t replace your general-purpose language. It generates snippets for it. The Python code above is intended to be embedded inside a larger project, like a component you’d pull off a shelf. The diagram specifies the control flow skeleton; the surrounding Python code supplies the domain logic (reverse(), the bounds checking, whatever the machine needs to operate).
This is a genuinely useful division of labour:
The diagram expresses what states exist and when to transition between them.
The surrounding code expresses what to do on entry, exit, and transition.
Neither half needs to know too much about the other.
Treating Python Like Assembly
Here’s the provocative framing: once you have a code generator, your general-purpose programming language becomes an assembly language for your project.
This isn’t an insult to Python. Assembly languages are precise, powerful, and absolutely essential. But nobody argues that you should write your entire project in assembly — you want to work at a higher level of abstraction, and compile down to assembly for execution.
The same logic applies here. When your project has components that are naturally expressed as state machines (and many do), you should be able to draw those components and compile down to Python — not transcribe them by hand.
The code generator at github.com/guitarvydas/sm does exactly this. Drop a .drawio diagram of your state machine into the repo and run @make. Out comes the boilerplate Python. You supply the guards and actions; the tool supplies the structure.
[See README.md for the exact recipe.]
The Workbench We Should Want
There’s a bigger argument lurking here.
We’ve spent decades building programming languages that try to do everything — general-purpose, Turing-complete, expressive, fast, safe, concurrent. And those languages are remarkable achievements.
But the ambition to make one language handle every problem has a shadow side: it makes us reach for that one language even when a smaller, more targeted tool would serve us better. We hand-write state machine boilerplate when we could draw diagrams. We build elaborate data transformation pipelines in imperative code when a declarative notation would be cleaner. We implement parsers by hand when a grammar DSL would be more readable and more maintainable.
The alternative is a programming workbench: a composition of tools, each doing one thing well, connected by code generation and interoperability. You draw where drawing is clearest. You write where writing is clearest. The workbench stitches it together.
That’s not a new idea — it’s the Unix philosophy applied to the act of programming itself. Small tools. Sharp edges. Composable outputs.
The state machine generator is a small demonstration of what that workbench could feel like. The diagram is unambiguous. The generated code is mechanical. The human writes only the interesting parts.
Try It
The tool is at github.com/guitarvydas/sm. It’s lightly tested — consider this early-stage and experimental — but the concept is solid and the generated code for a diagram like the one above works as expected.
If you’re reaching for nested conditionals to track some mode in your program, draw the states first. You might find the diagram says everything you need — and that the code writes itself.
What would your programming workflow look like if you could compose it from little languages, each suited to its part of the problem? I’d love to hear what you’d reach for first.
Further: Ideas and Observations
Ideas and observations about other little languages might be of interest:
What I’ve Learned from Forth Haiku Thus Far and other related Forth Haiku articles on my substack.
Forth Haiku playlist on YouTube
Experiments with Text to Text Transpilation - along with articles on my substack containing the keyword ‘T2T’
PBP Part Basics - along with articles containing the keywords ‘PBP’, ‘Parts Based Programming’, ‘0D’
See Also
Email: ptcomputingsimplicity@gmail.com
Substack: paultarvydas.substack.com
Videos: https://www.youtube.com/@programmingsimplicity2980
Discord: https://discord.gg/65YZUh6Jpq
Leanpub: [WIP] https://leanpub.com/u/paul-tarvydas
Twitter: @paul_tarvydas
Bluesky: @paultarvydas.bsky.social
Mastodon: @paultarvydas
(earlier) Blog: guitarvydas.github.io
References: https://guitarvydas.github.io/2024/01/06/References.html




That was a good read. I'm just learning Automata and Graph Theory. I scanned the GitHub but I need to explore more... I don't enter the conversation because I don't feel I have enough experience to bring a relevant argument. I didn't know what .sm was...