A few years ago I wrote a SQL parser in C++, before there was any AI to help. Recently I wrote it again, in Rust — sqlparser-ranger. I chose Rust because it has a benchmark that compares a parser against all the others, and I wanted a number to measure mine by. The number came out well. But that is not what this post is about.
Before writing either of them, I read all of Alibaba's Druid parser to understand how it works. I had no other way in. I jumped from function to function, from the bottom up, guessing what the authors had been thinking. I was not them, and the code did not tell me.
Writing my own taught me what I was missing while reading theirs. The code itself is not the important part. Code is what a design becomes when you write it down. What matters is the thinking above it: what the components are, what each one is for, and how they connect.
What the thinking looked like
My parser has two components at the top.
The Scanner reads characters and turns them into tokens. Each of its functions is one branch of that decision.
The Parser reads tokens and builds a syntax tree. It follows SQLite's official railroad diagrams exactly — both in how the work is split into functions and in what each function does. There is one function per diagram, named after it: sql_stmt_list, select_core, window_defn, frame_spec. Because of that, changing it is a matter of minutes. When the grammar changes, I know which function to open before I open anything.
There is one trick I only found after I understood the whole thing. A token's kind is a number, and I put those numbers in ranges by what the tokens are. Operators are spaced 25 apart by precedence, so the precedence is the value:
let min_prec = left_op as i32 + 25;
let qualifies = (TokenKind::Or..=TokenKind::Collate).contains(&kind)
&& kind as i32 >= min_prec;
There is no precedence table. The same idea turns "is this a join keyword" into a range check. It has a cost — the order of the enum now means something, and reordering it breaks things — but a parser does not change often, and when it does, the ranges move with it.
None of this is visible if you read one function at a time.
What I wanted
A tool that shows the components, what they mean, and how they connect — top-down instead of bottom-up. If I had had one while reading Druid, I would have started from the middle layer, the main types, worked out what each one is for, and only then read the code.
It is made for people, not for AI. I know most code is written by AI now and fewer people read it, so I do not expect many people to need this. I am trying anyway. I built it with an AI coding assistant myself, and I do not think that is a contradiction: the more code is written for us, the more of our work is reading it.
Planisphere
So I made Planisphere, a VS Code and Cursor extension. It reads a Python, TypeScript, Go, Rust or Java project and draws its types as one radial map: a centre, and rings of what hangs off it.
- Click a type and what it touches stays lit; the panel shows its comment and its methods. Click again to open the source.
- Right-click it to see only what it points to.
- Open a type, and its methods are drawn as the tree their calls make, around the type. The rest of the project moves outward to make room.
TypeScript and Go need nothing installed. Rust, Python and Java use the toolchain already on your machine. Everything runs locally; nothing is uploaded.
Drawing my own parser
The first drawing of my parser showed me the modules, every kind of statement and expression, and that every name follows SQLite's diagrams — though someone who does not know the diagrams would not see that.
It could not show the token ranges. Those are values of an enum, not structure, and a picture of structure has nothing to say about them.
And at first it could not show the grammar either. Parser was one node with a list of 69 names beside it. Those 69 methods call each other 342 times, and none of it was recorded. Now it is: opened, Parser shows sql_stmt_list next to it, the statements around them, and the clauses further out.
Trade-offs, not special cases
Every design is a set of trade-offs; I think everyone who builds things feels the same. What I tried to keep to is that a rule is a trade-off stated once, not a fix for the project in front of me.
- Crowded, unless the reader asks. When a ring of the drawing cannot hold what is on it, it stays crowded instead of growing outward, because growing drags the whole drawing with it. Opening a type does the opposite and pushes things outward — because there, the reader asked for the room.
-
A place is a distance, not an owner. Calls between methods are a graph, and a tree gives each method only one place.
expris called by 23 of my parser's rules. It is drawn next to one of them, and the lines from the other 22 are drawn too. Its place only says how few calls it is from the entry point. - No rules that read one project's habits. I could have made my own parser look better with rules tuned to it. The next project would not follow my habits.
- Honest about what it is. The lines come from names in the source, not from running it. Some are missed and some are wrong, and the legend says so.
What it does not do
- Very large projects are still a hairball. VS Code is 35,235 nodes.
- A type with 150 methods does not read well when opened in place; it has a separate view for that.
- It cannot show what is not structure, like the token ranges above.
Try it
It is free on the VS Code Marketplace and on Open VSX for Cursor. It is early — 0.x — and the drawing will keep changing. Open a folder, right-click, Planisphere: Analyze Folder.
If you draw your own project, I would love to see the picture.
I wrote this from my own notes in Chinese; it was translated into English with the help of AI.

Top comments (0)