Notes
2026/01/28
I'm revisiting some of Kombucha's syntax. The goal is to have a syntax that is both minimal and general (in the sense that almost no syntax is privileged and everything is just a user-defined function) while also being less of a soup of parentheses than most Lisps are.
The basic idea is that...
foo(bar, baz),((x * y) + z) as long as parens are used to disambiguate,x + y + z, which don't need parens),The idea of minimal-but-general syntax and trailing arguments are borrowed from Koka (where you can write foo { ... } without needing to wrap the block as foo({...})), while keyword arguments such as if (condition) do: { ... } else: { ... } are borrowed from Elixir (via Ruby) and desugar to if(condition, [["do", {...}], ["else", {...}]]).
In Elixir, keywords such as do: and else: don't need postfix colons in most situations, which leads to pretty nice syntax, but raises the question of how to disambiguate a combination of infix calls and keyword arguments (in a language like Kombucha which doesn't restrict infix operators to a fixed set).
Here are two options for keyword arguments:
// Option 1: `:` for keywords, infix allowed after keywords
try {
// ...
} catch: error! as: [resume, arg] => {
// ...
}
vs.
// Option 2: no `:` for keywords, no infix allowed after keywords
try {
// ...
} catch error! as ([resume, arg] => {
// ...
})
Advantages of keywords-with-colons:
key: value syntax can be used as a general key-value syntaxAdvantages of keywords-without-colons:
do { ... } while { ... }): is exclusively used for other features such as type annotationsSince being explicit about syntax and being able to use infix calls as keyword arguments is more important to me than sticking as closely as possible to existing language syntax, Kombucha is using option 1.
Previously, Kombucha used (x, y, ...) exclusively for function arguments and a single expression like (expression) to disambiguate nested infix calls. In other words, (expression) was semantically the same as expression, and multiple items wrapped in parens were only allowed as function call syntax. A collection of values was always written as [x, y, ...]. As a result, function parameters in anonymous functions had to be wrapped in [...], like [x, y] => f(x, y).
But what if the language needs to support different kinds of sequential data, such as arrays of boxed elements (which thus each take up the same space in memory, allowing for O(1) access) as well as tuples of fields with differents lengths? What if we wanted to use [...] for arrays and (...) for tuples?
Rust uses (...) for both tuples and grouping, which works because (x) is not a tuple, it's the same as x. Single element tuples have to be written with a trailing , in Rust, like (x,).
Here's another option for Kombucha: Anything wrapped in `(...)` is always a tuple, except when it's used on the left or right hand side of an infix function and contains a single element, in which case it's interpreted as parens meant to disambiguate. To use a single element tuple as an argument of an infix function, it needs to be wrapped in additional parens, like ((x)) f y, which is the same as f((x), y).
This also leads to a pleasant syntax for anonymous functions, because the (x, y) in (x, y) => { ... } is a two element tuple, whereas the (x) in (x) => { ... } can be simplified to x => { ... }, just like in JS.
Putting it all together (trailing arguments + keyword calls + infix calls + tuples):
try {
// ...
} catch: error! as: (resume, arg) => {
// ...
}
if (x == y) {
// ...
} elif: (x == z) do: {
// ...
} else: {
// ...
}
do {
// ...
} while: { ... }
while { ... } {
// ...
}
All while remaining minimal and general.