Right means “consume the cell on my left”
Each cell takes the previous cell's output. A row is a pipeline, read left to right.
@users ?(age)>=[18] *(name) ~/ ^
A 2D dataflow language
Most languages tell the machine how. Chimni says what flows where. A cell consumes the cell on its left. A new row is an independent chain. Inputs and outputs live in the syntax. There are no loops and no temporary variables.
$[orders,min_total]{(customer,total)} @orders #(customer) +s(amount) ?>[min_total] ~\(total) ^
take orders → group by customer → sum amount → keep totals above min_total → sort descending → emit
The real interpreter, compiled to WebAssembly with Pyodide. Edit anything — it re-runs as you type.
' reads the cell above · rows without ties run side by sideIn Sanskrit, a word's ending tells you its role in the sentence. Chimni borrows that idea: position is grammar.
Each cell takes the previous cell's output. A row is a pipeline, read left to right.
@users ?(age)>=[18] *(name) ~/ ^
Rows can't see each other unless they say so, so a runtime may run them in parallel. Each ^ fills the next output.
@orders #(customer) +s(amount) ^ @orders . +s(amount) ^
' is one row up, '' two rows up, same column. Joins happen by position, not by name.
@sales #(cat) +s(amount) @sales . +s(amount) . . ''/' ^
Inputs in [ ], output shapes in { }. Emit the wrong shape, or none where it isn't allowed, and it's an error.
$[transactions,limit]{id|none}
| Cell | Meaning | Python-ish |
|---|---|---|
$[in]{out} | signature: inputs, and the shape of each output | def f(in) -> out |
@x | start a row from input x | for _ in x |
@x(a=b) | join input x where x.a == item.b | JOIN … ON |
#(k) #() | group by field / by the item | groupby |
%(k) | keep the first item per key | seen = set() loop |
?(f)>[v] | filter (> < >= <= = !=) | WHERE |
?>=[90:A, 75:B, _:C] | ladder, first match wins | if / elif / else |
*(f) *(a,b) | pick field(s) | x["f"] |
+s +n +a +m +i | sum / count / average / max item / min item | sum len mean max min |
+r | running total | accumulate |
~/ ~\ | sort ascending / descending | sorted |
_l _u ;w | lowercase / uppercase / split into words | .lower() .upper() .split() |
^ ^[n] ^[1](f) | emit: all / first n / first one, then a field | return, [:n] |
. | empty cell (passes data through) | — |
' '' ''/' | tie: read the cell 1 / 2 rows up; arithmetic on ties | — |
Full reference: SPEC.md · Ten-minute tutorial: TUTORIAL.md · What didn't work: DESIGN.md
Comparing Chimni with loop-and-dict Python flatters it. So every example also has an idiomatic pandas, SQL or jq version, written the way someone fluent would write it. All three versions are run against each other on hundreds of random inputs in CI, so they really do compute the same thing.
| Example | Python | Alternative | Alt | Chimni | Characters py alt chimni |
|---|
Lines of code, comments excluded. Click a row to open it in the playground.
jq and SQL are as short as Chimni, sometimes shorter. SELECT name FROM users ORDER BY age DESC LIMIT 1 is
hard to beat. Most of Chimni's lead over Python disappears once you compare it with a tool built for the job.
The output shape is part of the grammar, so “I said pairs but built a triple” fails with a row and column.
Independent rows are visibly independent. Ties are deterministic: the SQL versions need an explicit
ORDER BY …, MIN(rowid) to agree with the others when totals are equal.
No OR in a filter, no outer joins, no picking an item by position. Some everyday questions can't be asked at all, and the section below lists them rather than hiding them.
What the grid is good at, where it runs out, and one bug we found and fixed along the way.
"Second-highest distinct score" looks impossible without indexing. Four rows and three ties do it: subtract the maximum, keep the negatives, take their maximum, add the maximum back. Powerful, though not easy to read.
-- second-highest distinct score: subtract the max, keep the negatives,
-- take their max, add the max back
$[scores]{second|none}
@scores . . .
@scores +m . .
. ''-' ?<[0] +m
. . . '+'' ^
Tying two grouped rows used to pair groups by position. If the groups came out in a different order,
the answer was silently wrong. Now they pair by key, and rows whose keys differ are an error that names the
missing keys. This one gives a → 1.0, b → 100.0, as it should.
$[sales,returns]{(category,per_return)}
@sales #(category) +s(revenue)
@returns #(category) +s(units)
. . ''/' ^
An OR filter ("refunds or anything over $100"), an anti-join ("customers with no orders"), a median, and day-to-day differences. There's no OR, the only join is an inner join, and nothing can address an item by its position.
A tie reads the cell above in the same column, and that cell can't be empty. So rows that a later row
reads are padded with . out to the right column. chimni fmt lines the columns up
so you can see which cell a tie reads.
Design notes and open questions: DESIGN.md