Ritchie Playground is now live for you to try out the Ritchie syntax and features currently available.
Gli Antipasti
On the appetizer menu today is the core structure of the language. We have the usual built-in scalar data types, strings, arrays, dictionaries and a Decimal data type. User-defined types and methods on them are supported.
We also have key language semantics like scope and visibility management, mutability, function calling and control flow. There are also nullable types and some Ritchie-special error handling that's detailed below.
You’re here because you want to see how a 20-line program turns into a distributed system with auto-scaling and persistence. All you get so far is some array flipping capability? Why?
Well, I have two good reasons:
One, the distributed system bit, I know fairly well. That’s pretty much all I’ve done for a quarter of a century :o What I haven’t done is build a language of any strength (other than some toys) before. So I needed to convince myself that it’s something that I can do.
Second, if you’re going to write all your business logic in Ritchie — because that’s the whole point of it1 — then the language needs to have the strength to support a wide range of business requirements.
The best thing is, you can now see the language evolving over the coming weeks and months. Next on the cards for me is tackling effects, I/O and dataflow-driven concurrency, which paves the way towards @Service types. Exciting!
Highlights of What’s Released
If Ritchie were a culinary dish, it wouldn’t claim to be authentic this-or-that cuisine. It’s a fusion dish with (I hope you’ll agree) a tasteful blend of elements from different programming styles and languages, rooted in C-like syntax and imperative logic.
Type Declarations Are Back!
When you see something like _, data := json.Marshal(payload), how do you know what you’re getting back in data? Unless you’re familiar with conventions and lore, or you look up the docs, you wouldn’t know that you just discarded the marshalled output and you should have instead written data, _ :=. This is a problem that I’ve faced many times while reading code and now that writing code is near-zero cost, the burden of effort is on reading — whether the reader is human or AI. Neither would want to look up the docs to figure out the data types that a function returns.
Lexically Significant ASCII Source
Another thing that I’ve always found to hamper readability is the littering of code with qualifiers like type, fn, const, public static void… They’re like little speed-bumps that you have to bounce over to get to the point. In Ritchie, the lexical conventions take care of most of it.
A Typename is capitalised. CONSTANT is ALL_CAPS. A function() starts with lowercase but has parentheses. A variable (we call it binding) is lowercase without parentheses. Methods are Typename.function(...). Function result types are denoted by ->. Oh, and we have errors that look like This!. Yes, with a bang at the end because… error!
Error Handling
Speaking of errors, Ritchie has implicit error handling, which is literally Go’s if err != nil { return err } stuck everywhere an error can materialise. Of course, we do a bit more than just return the error. The default handling is “bubbling” the error up, which captures local context and attaches it to the prior context before passing it back to the caller. There’s also some control-flow acrobatics that the compiler does in order to make error handling behaviour intuitive but powerful. Here’s an example you can try in the playground:
DebitFailed! {
Int balance
Int attempted
}
Wallet {
Int balance
}
Wallet.debit(Int amt) {
if self.balance < amt {
return DebitFailed!{self.balance, amt}
}
self.balance = self.balance - amt
}
OrderId = Int // type alias
Logistics {}
Logistics.ship(OrderId o_id) {
log(f"Shipped Order: {o_id}")
}
checkout(Wallet w, Int bill_amt) -> OrderId {
w.debit(bill_amt)
return 909
}
main() {
Wallet w = Wallet{20}
OrderId o_id = checkout(w, 22)
Logistics.ship(o_id)
}
Here main() calls checkout(), which in turn calls Wallet.debit(), unfortunately with a bill that’s greater than the balance in the wallet. So Wallet.debit() raises a DebitFailed!, which nobody remembers to check or handle. Will we end up shipping a digi-bomb with a bogus Order ID? Nope. When you run the program, you should see something like:
DebitFailed! { balance: 20, attempted: 22 }
at main
raised by Wallet.debit(amt: 22)
Immutable Value Semantics
You can freely change any Ritchie variable any time because Ritchie values are immutable. If you didn’t blink, you’re a programming languages nerd. Otherwise, I’ll join you in being surprised by that statement. I designed Ritchie variables to be like “C without pointers”. C without pointers is all pass-by-value semantics. The interesting property of that is, if you hold a variable, you can pass it around to ordinary functions willy-nilly. Those functions can assign to it or pass it on to other functions. No matter how much they modify the variable you passed to a function, when the function returns, your variable remains unchanged. It is immutable by such operations. You can only assign other values to it.
And so, ordinary function calls don’t require you to worry about lifetimes, const, mut, borrow, etc. etc. Methods are the exception: a value’s own methods are permitted to mutate it. That’s what Wallet.debit() does in the preceding example, through self. self is the only reference in Ritchie and it can only exist within a method body and only point to the instance on which the method is called. Assigning a new value to self actually modifies the receiver itself. You can’t return self either. The language won’t stop you from writing return self, but it will actually return a copy of the instance to the caller rather than a reference to the instance.
Decimal Type
Ritchie also contains a built-in 128-bit Decimal type. Decimal is special in that it doesn’t behave like traditional Int or Float types. They have their quirks, like integer arithmetic eventually going from a very large positive value to a very large negative value all of a sudden while you’re repeatedly adding to it. Binary floating-point cannot exactly represent many decimal fractions and can sometimes result in unexpected values like 0.1 + 0.2 becoming 0.30000000000000004 when you expect 0.3. Decimal behaves nicely in all these situations. It raises errors such as OutOfRange! or DivisionByZero! when challenged with weird calculations instead of flipping its sign or becoming something unnatural like NaN.
Language Architecture
Ritchie is implemented as a custom-built recursive-descent parser that emits Go intermediate code which then is compiled by the Go compiler to native code. A lot of the Ritchie tricks are actually calls into a rlr package (Ritchie Language Runtime) that allows me to define Ritchie semantics as Go logic and keep the size of the generated Go code down. It also allows me to leverage the immense, highly mature Go standard library and avoid third party dependencies in Ritchie compiler toolchain.
In terms of performance, Ritchie is within a few percentage points of Go and faster than Python or JS (NodeJS). In a small suite of equivalent programs, Ritchie revision e115792c9648 ran every measured workload faster than Node.js 22.22.2 and CPython 3.14.0. Median workload times were 1.4–3.1× faster than Node and 2.9–82× faster than CPython on my Linux/amd64 Intel NUC, with compilation excluded and startup approximately subtracted. If we include startup times, Ritchie is even faster.
Try It Now
You’ve heard enough about the language — now try it. Download the Ritchie Skill for your coding agent, then have it teach Ritchie to you — or put it straight to work.
