Crow, a Programming Language
Crow is a small general-purpose language. It reads like Python, compiles to Rust, and the same file runs two ways: at once in the interpreter, and at native speed as a binary. Loops run in parallel without being asked. A failure is a value that says why. Tests sit under the function they check. A web application is written in the same language, with no JavaScript.
Why another language
Languages that are easy to write tend to run slowly, and languages that run fast ask a lot of the writer. Anyone who works with data pays for that trade twice: explore in one language, ship in another, and add parallelism, failure handling, testing and the web layer from four separate frameworks with four sets of rules. Crow is one simple language that runs two ways, with those rules in the language rather than in libraries, the web page written in Crow itself rather than in JavaScript, and its batteries where data and applications need them. It does not try to replace the established ecosystems; it tries to be a coherent place for the programs that sit between them. Here is what one looks like.
A whole program
use math
prices = [101.5, 102.1, 99.8, 103.4]
avg = mean[prices]
returns = for p in prices
(p - avg) / avg
log['average: `fmt[avg, decimals = 2]`']
Four things happened that nothing in the file asked for. No type was written: the compiler inferred every one from use. The loop ran on every core, because its body only computes. Its results came back in input order. And the backticks carried a number into the text. Calls are f[x]
,
blocks are indentation, strings use one quote, and comparison and logic are the words eq
,
lt
,
and
and not
,
so almost nothing needs the shift key.
Parallel without asking
A for
loop runs in parallel unless its body could reveal the order it ran in: writing a variable from outside the loop, printing, touching a file, a database, the network, the clock or the random generator, or leaving the function early. Then it runs in order. The compiler decides once, from the program, and both execution modes read the same decision; a collecting loop keeps input order either way. So total = total + x
is correct and sequential, and the idiom that keeps a loop parallel is the one the book teaches: compute in the loop, print after. while
is always sequential, and cpu[1]
makes everything that follows single-threaded.
Statements, by contrast, run in the order written. They did not always: the runtime once ran independent assignments in parallel waves, until a seeded script drawing three random numbers in three statements gave five different answers in thirty runs. The analysis saw variables and not the generator, the clock or the file. The reversal is written into the specification as a design note, and the benchmark programs did not slow down when the waves went.
Failure is a value
bad = int['oops']
worse = bad + 10
log[worse.ok] --- false
log[worse.reason] --- cannot convert 'oops' to int
A failed operation returns a value that knows it failed and says why. It flows through arithmetic and calls like any other value until something tries to act on it, and there the language refuses to guess: a failure cannot decide a branch, cannot be assigned over a good value, and cannot be silenced inline. There is no try
,
no catch
,
no Result
type, no ?
,
and no null; a miss is a failed value too.
One way to say each thing
One call syntax, one string quote, one loop, one way to build a map. No +=
,
no ternary, no double-quoted strings, no iterator chains, no lambdas: find[items, is_big]
names its predicate, and a closure would capture state that silently breaks a parallel loop. Every one of those absences is a design note in the specification saying what was considered and why it lost, and when the compiler refuses a habit from another language, the error names the Crow form and the note behind it.
Tests live under the function
fn fee[notional]
if notional lt 0
return error['negative notional', 'validation']
return max[1.0, notional * 0.001]
test[n]
fee[50000.0] eq 50.0 --- an exact fact, checked once
fee[-5.0].kind eq 'validation' --- the error path
if n gte 0.0
fee[n] gte 1.0 --- a law: n is generated, many values
No framework, no test directory. The block's parameter is a generated input, boundaries first, so the last line is a law checked on many values. A block that reaches no truth fails as "asserts nothing". Builds strip test blocks. crow check
goes further: static analysis for purity, unused names, unreachable code and SQL against the real schema, then generated edge cases run through the pure functions and kept as snapshots. Answers from databases, HTTP services and language models can be recorded once and replayed, and a web page is tested by a headless session that clicks it, with no browser.
Web applications, all in Crow
use web, ui
count = 0
fn inc[]
count = count + 1
view render[]
return div[
h1['Count: `count`'],
button['+', click = inc]
]
serve[3000]
page['/', render]
That is a whole web application: a server, a page, no other files, and no JavaScript. A view
is a function from state to a tree, and inside it the HTML vocabulary is ordinary Crow functions, so the tree you write is the tree you get. Top-level variables are the page's state. A click runs inc
on the server, the page renders again, and the browser only streams events back over a socket; each tab is its own session, and what pages share is the database. A view with parameters is a component. derive
declares a value that follows what it reads, so a query is recomputed when a handler writes the table it reads. chart[frame, candles = …]
draws SVG on the server, and crow dev
reloads the program as its files change without dropping a session. There is no build step, no client framework and no API layer between the data and the page. A page is tested like any function: app[render]
opens a headless session, click[s, inc]
runs the handler, and html[s]
is what the reader would see, in both execution modes.
Interpreted and compiled
crow run -i file.co
starts at once; it is also the REPL, the notebook kernel and the dev server. crow build file.co
prints the same program as Rust and builds it at full optimisation. Both run one front end to one intermediate form, and the kernels under frames and tensors are one Rust crate both call. The two are held to one behaviour mechanically: 244 parity programs run both ways with their output diffed, every example in the book runs both ways on every change, and a differential fuzzer generates programs that must run alike or be refused alike. A disagreement is a bug in Crow, never a semantics to learn. That holds the compiler. Your program is held by its own tests, by crow check
and by its recorded fixtures, and parity carries what they establish to the binary.
How fast
Measured on 2026-10-13 on an Apple M4 Max with sixteen cores, crow 0.6.0, rustc 1.93.1, Go 1.23.0 and CPython 3.12.12: the median of five runs, with the range as a percentage of the median in brackets, wall time of the whole process, every port's output byte-identical. The Rust and Go ports used sixteen threads where they take a thread count.
| Program, size | Crow compiled | Rust | Go | CPython |
|---|---|---|---|---|
| fannkuch-redux, n = 10 | 0.11 s [13%] | 0.13 s [5%] | 0.12 s [5%] | 3.00 s [16%] |
| spectral norm, n = 5,500 | 0.086 s [14%] | 0.099 s [5%] | 0.13 s [4%] | 9.74 s [10%] |
| pidigits, 5,000 digits | 0.31 s [2%] | 0.85 s [32%] | 0.29 s [2%] | 1.01 s [3%] |
| n-body, 1 million steps | 0.088 s [8%] | 0.032 s [8%] | 0.033 s [4%] | 2.41 s [2%] |
| k-nucleotide, 5 million | 0.056 s [12%] | 0.029 s [6%] | 0.14 s [6%] | 4.48 s [6%] |
| binary trees, depth 18 | 0.10 s [3%] | 0.77 s [6%] | 0.65 s [3%] | 0.68 s [2%] |
Where the generated code is tight, fannkuch, spectral norm and pidigits, the compiled program runs with Rust and Go; on n-body it is nearly three times behind and on k-nucleotide about twice, and the roadmap names the reasons. Binary trees is mostly an allocator story: the Rust port linked against mimalloc, which Crow's programs use, runs in 0.16 s rather than 0.77 s. The interpreter is faster than CPython on five of the seven programs CPython ran. The frames, whose kernels are Rust, load a ten-million-row CSV in 0.41 s against polars's 1.04 s and pandas's 4.67 s, and beat pandas on every operation measured and polars on most. Every program, the frame and web tables, the method and the sources to rerun it are in the companion article.
What exists today
One binary holds the interpreter, the compiler, the test runner, the checker, the language server, a dev server that reloads a web program and keeps every session, and the language's own book with a run button on every example. Inside it are tensors with automatic differentiation, a column-oriented frame, drivers for SQLite, Postgres and ClickHouse, HTTP and S3 readers, a web framework in which a page is a view over facts, rendered on the server, and a language model as a connection. Eight packages add models, graphs, optimisation, retrieval, vector search, symbolic regression, the exchange calendar and a small GPT, each shipped with the reference script it is held to; the companion article lists what each holds. The largest Crow program is a trading research platform's simulation page, 30 files and about 7,400 lines, served by the compiled binary; the book is a Crow program too. What you cannot yet build is also plain: the models are small ones that train on a laptop, there are no channels, actors or async, and a complex domain without sum types or pattern matching is the wrong fit.
Try it
curl -fsSL https://raw.githubusercontent.com/agncrow/crow/main/install.sh | bash
crow new hello && cd hello && crow run
One line on a Mac or a Linux machine, and the interpreter, the tests, the checker and served apps work at once; the first native build fetches a pinned Rust toolchain. Paste the fee function above into fee.co
and run crow test fee.co
.
The repository is at github.com/agncrow/crow, the book at agncrow.github.io/crow, and the next article
is the first hour: the editor, the two ways of running, a page, projects and packages.
Crow is an alpha at version 0.6.0, open source under the MIT or Apache-2.0 licence. The syntax may change between releases, the changelog says what moved and crow fix
migrates what it can; bug reports are welcome and pull requests wait until the alpha ends. The path to 1.0 is language stability, compatibility guarantees, regression coverage, sustained production testing and validation through independently developed applications, with performance and the interpreter's overhead the open work.
