To the surprise of literally no one, I'm working on implementing a programming language all my own
Inspired by conversation at a recent Future of Coding event, I decided I’d write up a little something about the programming language I’ve been working on (for what feels like forever) before I’ve gotten it to a totally shareable state. I have a working interpreter that I’m pretty pleased with, but I don’t yet have an interactive environment for creating, exploring, debugging, and running code — I have this idea for a Smalltalk-flavored infinite canvas dev experience that’ll work in the browser. Hoping that’ll be ready soon(ish)!
Author’s note: Cutting in real fast from the future with an after-the-fact update! I pulled together a relatively simple, standalone playground for folks to explore Baba Yaga a bit more!
Meet Baba Yaga!
Baba Yaga started as a purely aesthetic endeavor. Like a beaver drawn to slowing the flow of a river, I had this idea that was haunting me about what I wanted a language to look like on screen and kinda worked backwards from there. To start, I wrote a few fantasy programs, and then started to think through how to get that code to run.
I have no intention whatsoever of Baba Yaga becoming anything but a curiosity and an exploratory project for me. Because of this, it is really, really biased towards the things I adore in a programming experience, including:
immutability
functional-first everything everywhere
minimal syntax to learn
some batteries included
There is no real groundbreaking single feature that I think is especially notable about Baba Yaga. Instead, it mixes a bunch of familiar functional programming concepts into a hopefully cohesive and expressive combination that is orders of magnitude simpler than other such functional languages. Baba Yaga is kinda like Toki Pona but for Haskell.
Basic Syntax and Data Types
The syntax aims for visual clarity with minimal punctuation. Over the last few years, I’ve really fallen for typed languages, but I also don’t have a big capital T type theory brain. So, Baba Yaga only has some core types that fell out of its approach to control flow with pattern matching.
Declarations
Variables and functions use the same declaration pattern. Function calls don’t require parentheses, though you can use them for grouping and disambiguation.
// Variables transport : "Chicken House"; number : 42; // Functions add : x y -> x + y; square : x -> x * x; // Function calls result : add 5 3; nested : add (square 4) 2;
Functions can also be curried without additional ceremony:
// Curried function addCurried : x -> y -> x + y; add5 : addCurried 5; result : add5 3; // 8
Data Types
The language includes a few essential, immutable data types.
// Numbers (Int and Float are distinct) age : 36; temperature : 32.6; accountBalance: -12000; // Strings with the '..' concatenation operator fullName : "Lucy" .. " " .. "Snowe"; // Booleans isActive : true; // Lists (immutable) numbers : [1, 2, 3, 4, 5]; firstItem : numbers.0; // zero-indexed access // Tables (immutable key-value structures) person : {name: "Lucy Snowe", age: 23, city: "Villette"}; personName : person.name;
Types and Validation
Types are optional but can be explicitly declared. Without declarations, values carry runtime type tags ( Int , Float , String , Bool , List , Table ). Parameter and return type validation happens at call time only for that stuff which includes annotations. Anything without type annotations is left to do what it likes. Slip slip slip-and-slide.
// Type declaration (name Type) myValue Int; myOtherValue Int; // Assignment must match declared type myValue : 10; // OK myOtherValue : "x"; // Error: expected Int
Control Flow with Pattern Matching
The when expression is the language’s only control flow construct. No if / else or switch statements exist. I did this mostly to see if I could, but also to help encourage thinking about the shape of data rather than imperative conditions. Philosophically speaking:
if/else asks “ is this condition true?”
asks is this condition true?” switch asks “ is this value equal to one of these constants?”
asks is this value equal to one of these constants?” pattern matching asks “ does this data look like this? If so, give me the pieces.”
You can match on simple values, types, and use guards for more complex conditions.
// Match on simple values describe : x -> when x is 0 then "Zero" 1 then "One" _ then "Something else"; // _ is the wildcard // Match on types processValue : value -> when value is Int then "Got an integer" String then "Got text" _ then "Got something else"; // Use pattern guards for complex conditions categorize : n -> when n is x if (x > 0 and x < 10) then "small positive" x if (x >= 10) then "large positive" x if (x < 0) then "negative" 0 then "zero";
Baba Yaga also supports matching against multiple discriminants and destructuring data structures like tables.
// Multiple discriminants checkPosition : x y -> when x y is 0 0 then "Origin" 1 1 then "Diagonal" _ _ then "Somewhere else"; // Destructuring and nested matching processUser : user -> when user is {name: n, age: a, active: true} then when a is age if (age < 18) then "Minor: " .. n age if (age >= 18 and age < 65) then "Adult: " .. n _ then "Senior: " .. n {name: n, active: false} then "Inactive: " .. n _ then "Invalid user";
Here’s a chunk of Conway’s Game of Life that I used to test if the language could handle non-trivial algorithms. Like the famous implementation in APL, each generation is a pure transformation of the previous state.
// Apply Game of Life rules nextCell : grid row col -> with ( current : grid.(row).(col); neighbors : countNeighbors grid row col; ) -> when current is 1 then when (neighbors = 2 or neighbors = 3) is true then 1 _ then 0 _ then when (neighbors = 3) is true then 1 _ then 0;
Working with Collections
All list operations are immutable and return new lists. This includes standard functional staples as well as utilities inspired by other array programming languages.
Functional Staples
The higher-order functions that are the bread and butter of most functional programming work as expected.
// Basic list operations original : [1, 2, 3]; withFour : append original 4; // [1, 2, 3, 4] withZero : prepend 0 original; // [0, 1, 2, 3] combined : concat [1, 2] [3, 4]; // [1, 2, 3, 4] // Higher-order functions numbers : [1, 2, 3, 4, 5, 6]; doubled : map (x -> x * 2) numbers; // [2, 4, 6, 8, 10, 12] evens : filter (x -> x % 2 = 0) numbers; // [2, 4, 6] sum : reduce (acc x -> acc + x) 0 numbers; // 21
Array Programming Utilities
The array programming features follow the same pattern as the other higher-order functions.
data : [10, 21, 30, 43, 50]; // Find indices where a predicate is true evenIndices : where (x -> x % 2 = 0) data; // [0, 2, 4] // Select elements at specific indices selected : at [0, 2, 4] data; // [10, 30, 50] // Cumulative operations (like APL's scan) cumulative : cumsum [1, 2, 3, 4, 5]; // [0, 1, 3, 6, 10, 15] // Broadcasting (apply a function with a scalar to each element) addTen : broadcast (x y -> x + y) 10 [1, 2, 3]; // [11, 12, 13] // Reshape a flat array into a matrix matrix : reshape [2, 3] [1, 2, 3, 4, 5, 6]; // [[1, 2, 3], [4, 5, 6]]
These operations compose pretty well with the functional style. You can chain where to find indices, at to select values, and then process those with map without experiencing any wild dissonance, I don’t think.
Error Handling
Instead of exceptions, Baba Yaga uses a Result type ( Ok value or Err message ) for operations that might fail. This introduces explicit error representation and handling.
divide : x y -> when y is 0 then Err "Cannot divide by zero" _ then Ok (x / y); handleDivision : x y -> when (divide x y) is Ok result then result Err message then 0; // Provide a default value on failure
Local Bindings and Utilities
The with keyword creates immutable local bindings within an expression. For mutually recursive local functions, use with rec . Mutually recursive functions work without any special syntax at the global scope.
// 'with' for local, immutable bindings processOrder : order -> with ( taxRate : 0.08; subtotal : order.items * order.price; total : subtotal + (subtotal * taxRate); ) -> {subtotal: subtotal, total: total}; // 'with rec' for mutual recursion evenOdd : n -> with rec ( isEven : x -> when x is 0 then true _ then isOdd (x - 1); isOdd : x -> when x is 0 then false _ then isEven (x - 1); ) -> {even: isEven n, odd: isOdd n};
The language also comes with a handful of utilities organized in namespaces like str , math , and validate .
words : str.split "hello,world,banana" ","; // ["hello", "world", "banana"] absolute : math.abs -5; // 5 valid : validate.range 1 10 5; // true debug.print "Processing" someValue;
JavaScript Interoperability
Since Baba Yaga runs on JavaScript, it needs a way to call JS functions…or, like, I wanted to have a way to call JS from Baba Yaga as a sort of escape hatch so that I didn’t have to remake the whole entire world. But JS doesn’t have the same kinda functional guarantees as Baba Yaga is trying to ensure, so all JS calls return Result types. This helps to maintain the functional programming model even when crossing into imperative territory. This means you can’t accidentally throw exceptions from JS that crash your functional program.
// Call JavaScript functions (returns a Result type) jsonResult : io.callJS "JSON.parse" ["{\"x\": 10}"]; // Convert between JS and Baba Yaga data jsArray : io.listToJSArray [1, 2, 3]; babaTable : when (io.callJS "JSON.parse" ["{}"]) is Ok obj then io.objectToTable obj Err msg then Err msg;
This is likely the roughest edge of the entire language and where I need to spend the most time.
Don’t ya know / They’re talking about a implementation?
In the past I’ve implemented a lot of toy languages. Working on Baba Yaga has been a great learning experience on how to implement a quote real language unquote. I’ve tried not to be slap-dash about my approach, and, while I don’t want Baba Yaga to take the world by storm, nor do I really expect many/any folks to really ever use it, I wanted it to be performant enough to use for games and sketches and maybe some live code stuff.
To help meet those goals, some things that I considered during th implementation:
Two engines : The project has a stable “ legacy” engine and an optimized one. The optimized one doesn’t work super duper well, though, so, it is turned off by default, but I should revisit that.
: The project has a stable legacy” engine and an optimized one. The optimized one doesn’t work super duper well, though, so, it is turned off by default, but I should revisit that. Enormous test suite : Working on rawk I realized the only way to test a language is to use that language a lot, so having a test suite that covers all of the language features is really really useful…but also even a tiny language ends up having a whole freaking lot of surface area to test when it has non-trivial syntax rules more complicated than you get in a forth or a scheme.
: Working on rawk I realized the only way to test a language is to use that language a lot, so having a test suite that covers all of the language features is really really useful…but also even a tiny language ends up having a whole freaking lot of surface area to test when it has non-trivial syntax rules more complicated than you get in a forth or a scheme. Developer-focused, Elm-inspired error handling : The error system provides detailed messages with source location, visual pointers, and suggestions…because Elm poisoned me, and because I think error handling shouldn’t be an implementation after thought but like the whole-freaking-point of the exercise.
: The error system provides detailed messages with source location, visual pointers, and suggestions…because Elm poisoned me, and because I think error handling shouldn’t be an implementation after thought but like the whole-freaking-point of the exercise. Security-focused interop : The JavaScript integration includes function allowlisting, execution timeouts, and memory limits to maintain functional guarantees and also to prevent ya from just writing JS all the time (looking at myself).
: The JavaScript integration includes function allowlisting, execution timeouts, and memory limits to maintain functional guarantees and also to prevent ya from just writing all the time (looking at myself). Performance-aware design : This is a space I had literally no understanding of before starting this. I read a bunch of blog posts and combed through a number of implementations of other languages. The implementation uses stuff like object pooling for AST nodes and includes benchmarks to try and measure the impact of optimizations, like using arrays instead of hash maps for variable lookups…but I’ll be honest, this is the bit of the project where I likely have the most to learn, still.
: This is a space I had literally no understanding of before starting this. I read a bunch of blog posts and combed through a number of implementations of other languages. The implementation uses stuff like object pooling for nodes and includes benchmarks to try and measure the impact of optimizations, like using arrays instead of hash maps for variable lookups…but I’ll be honest, this is the bit of the project where I likely have the most to learn, still. Runtime configuration: Because Baba Yaga targets a handful of JavaScript runtimes, the engine supports type-safe presets for the different environments (development, production, sandbox), allowing for different recursion depths and memory limits. I haven’t actually ever needed to tune these away from the default, but the option exists!
Full Language Reference Card
BABA YAGA LANGUAGE REFERENCE ============================ SYNTAX ------ var : value; // assignment var Type; var : value; // typed assignment f : x -> body; // function f : x y -> body; // multi-param function f : (x: Type) -> Type -> body; // typed function f : x -> y -> body; // curried function f : x -> with (locals) -> body; // with locals f : x -> with rec (fns) -> body;// with mutual recursion LITERALS -------- 42 // Int 3.14 // Float "text" // String true false // Bool [1,2,3] // List {a:1, b:2} // Table PI INFINITY // constants OPERATORS (precedence high→low) ------------------------------- f x, obj.prop // call, access - ! // unary minus, not * / % // multiply, divide, modulo + - // add, subtract = != < <= > >= // comparison and or // logical .. // string concat CONTROL FLOW ------------ when x is // pattern match 0 then "zero" Int then "number" _ then "other"; when x y is // multi-discriminant 0 0 then "origin" _ _ then "other"; x if (condition) then result // pattern guard _ then "fallback"; TYPES ----- Int Float Number String Bool List Table Result Function Int ⊂ Float ⊂ Number // type hierarchy Ok value | Err message // Result variants ARRAY OPERATIONS ---------------- // Core HOFs map f xs // [f x | x <- xs] filter p xs // [x | x <- xs, p x] reduce f z xs // f(...f(f(z,x1),x2)...,xn) // Array programming scan f z xs // cumulative reduce cumsum xs // cumulative sum cumprod xs // cumulative product at indices xs // xs[indices] where p xs // indices where p x is true take n xs // first n elements drop n xs // drop first n elements broadcast f scalar xs // f scalar to each x zipWith f xs ys // [f x y | (x,y) <- zip xs ys] reshape dims xs // reshape flat array to matrix flatMap f xs // concat (map f xs) // List manipulation append xs x // xs ++ [x] prepend x xs // [x] ++ xs concat xs ys // xs ++ ys update xs i x // xs with xs[i] = x removeAt xs i // xs without xs[i] slice xs start end // xs[start:end] length xs // |xs| // Utilities chunk xs n // split xs into chunks of size n range start end // [start..end] repeat n x // [x,x,...] (n times) sort.by xs f // sort xs by key function f group.by xs f // group xs by key function f TABLE OPERATIONS ---------------- set tbl k v // tbl with tbl[k] = v remove tbl k // tbl without tbl[k] merge tbl1 tbl2 // tbl1 ∪ tbl2 keys tbl // [k | k <- tbl] values tbl // [tbl[k] | k <- tbl] shape x // metadata: kind, rank, shape, size STRING OPERATIONS ----------------- str.concat s1 s2 ... // s1 + s2 + ... str.split s delim // split s by delim str.join xs delim // join xs with delim str.length s // |s| str.substring s start end // s[start:end] str.replace s old new // replace old with new in s str.trim s // strip whitespace str.upper s // uppercase str.lower s // lowercase text.lines s // split by newlines text.words s // split by whitespace MATH OPERATIONS --------------- // Arithmetic math.abs x // |x| math.sign x // -1, 0, or 1 math.min x y, math.max x y // min/max math.clamp x lo hi // clamp x to [lo,hi] // Rounding math.floor x, math.ceil x // ⌊x⌋, ⌈x⌉ math.round x, math.trunc x // round, truncate // Powers & logs math.pow x y // x^y math.sqrt x // √x math.exp x, math.log x // e^x, ln(x) // Trigonometry math.sin x, math.cos x, math.tan x math.asin x, math.acos x, math.atan x, math.atan2 y x math.deg x, math.rad x // degrees ↔ radians // Random math.random // [0,1) math.randomInt lo hi // [lo,hi] FUNCTION COMBINATORS -------------------- flip f // λx y. f y x apply f x // f x pipe x f // f x (reverse apply) compose f g // λx. f (g x) (binary compose) VALIDATION & DEBUG ------------------ // Validation validate.notEmpty x // x is not empty validate.range lo hi x // lo ≤ x ≤ hi validate.type "Type" x // x has type Type validate.email x // x is valid email // Debugging debug.print [name] value // print with optional name debug.inspect x // detailed inspection assert condition message // throw if condition false I/O --- io.out value // print value io.in // read stdin JAVASCRIPT INTEROP ------------------ io.callJS fnName args // call JS function synchronously io.callJSAsync fnName args // call JS function asynchronously io.getProperty obj propName // get JS object property io.setProperty obj propName val // set JS object property io.hasProperty obj propName // check if JS property exists io.jsArrayToList jsArray // convert JS array to Baba Yaga list io.listToJSArray list // convert Baba Yaga list to JS array io.objectToTable jsObj // convert JS object to Baba Yaga table io.tableToObject table // convert Baba Yaga table to JS object io.getLastJSError // get last JS error (if available) io.clearJSError // clear last JS error (if available) EXAMPLES -------- // Fibonacci fib : n -> when n is 0 then 0 1 then 1 _ then (fib (n-1)) + (fib (n-2)); // Array processing pipeline process : xs -> with ( filtered : filter (x -> (x % 2) = 0) xs; doubled : map (x -> x * 2) filtered; summed : reduce (acc x -> acc + x) 0 doubled; ) -> summed; // Result handling safeDivide : x y -> when y is 0 then Err "div by zero" _ then Ok (x / y); use : r -> when r is Ok v then v Err _ then 0; // Pattern matching with guards classify : x -> when x is n if ((n > 0) and (n < 10)) then "small positive" n if (n >= 10) then "large positive" n if (n < 0) then "negative" _ then "zero"; // Mutual recursion evenOdd : n -> with rec ( even : x -> when x is 0 then true _ then odd (x - 1); odd : x -> when x is 0 then false _ then even (x - 1); ) -> {even: even n, odd: odd n}; // Array programming matrix : reshape [2,3] [1,2,3,4,5,6]; // [[1,2,3],[4,5,6]] indices : where (x -> x > 3) [1,2,3,4,5]; // [3,4] selected : at indices [10,20,30,40,50]; // [40,50]
Published August 27, 2025