A 2D dataflow language

Layout is grammar.

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

Playground

The real interpreter, compiled to WebAssembly with Pyodide. Edit anything — it re-runs as you type.

Loading Python…
Program.chimni
InputsJSON object
Result

      
The same program in Python and pandastested equal in CI
Dataflowdata moves → along a row · ' reads the cell above · rows without ties run side by side

Read it in two minutes

In Sanskrit, a word's ending tells you its role in the sentence. Chimni borrows that idea: position is grammar.

→

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 new row is an independent chain

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)  ^
'

A quote reads the cell above

' is one row up, '' two rows up, same column. Joins happen by position, not by name.

@sales  #(cat)  +s(amount)
@sales  .       +s(amount)
.       .       ''/'        ^
$

Signatures are contracts

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}
Diagram: right means depends on, down means independent, a quote reads the cell above

Every cell, on one screen

CellMeaningPython-ish
$[in]{out}signature: inputs, and the shape of each outputdef f(in) -> out
@xstart a row from input xfor _ in x
@x(a=b)join input x where x.a == item.bJOIN … ON
#(k) #()group by field / by the itemgroupby
%(k)keep the first item per keyseen = set() loop
?(f)>[v]filter (> < >= <= = !=)WHERE
?>=[90:A, 75:B, _:C]ladder, first match winsif / elif / else
*(f) *(a,b)pick field(s)x["f"]
+s +n +a +m +isum / count / average / max item / min itemsum len mean max min
+rrunning totalaccumulate
~/ ~\sort ascending / descendingsorted
_l _u ;wlowercase / uppercase / split into words.lower() .upper() .split()
^ ^[n] ^[1](f)emit: all / first n / first one, then a fieldreturn, [: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

The fair comparison

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.

ExamplePythonAlternativeAltChimniCharacters py alt chimni

Lines of code, comments excluded. Click a row to open it in the playground.

Where the others win

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.

Where Chimni is different

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.

Where Chimni is weaker

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.

Limits & tricks

What the grid is good at, where it runs out, and one bug we found and fixed along the way.

trick

Ties can do more than they look like they can

"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
.        .     .      '+''  ^
fixed in this version

Grouped rows now line up by key

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)
.         .            ''/'          ^
limit

Four everyday questions Chimni can't ask

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.

limit

Ties need somewhere to land

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