Thanks for explaining. Is
https://github.com/kalai-transpiler/kalai the latest incarnation of Kalai? iiuc, you're using two translation strategies for different target languages. The first two backends work by doing more-or-less a single pass, and the two later backends use a more traditional multi-pass compiler architecture.
Your observation on "approach sounds very common and reasonable at first blush" brought to mind this from Rachit's ["transpiler" rant](
https://people.csail.mit.edu/rachit/post/transpiler/):
> Lie #4: Transpilers don't have backends ... Compilers already do things that “transpilers” are supposed to do. And they do it better because they are built on the foundation of language semantics instead of syntactic manipulation.
Yeah, on string random access, most languages have it. Swift is an outlier, but closest to what we ended up going with.
https://temperlang.github.io/tld/6977ee7c59772b42/draftsite/blog/2025/03/25/a-tangle-of-strings/ captures a lot of our API design work for strings.
tldr: the need for consistent semantics despite native code unit differences led us to try both slice semantics and by-construction index types for low-level string processing. The former allows perfect consistency but is confusing and inefficient on dynlangs. The latter looks like indexing and is trivial to translate efficiently.
The collections design space is really gnarly, but since your user community seems to skew towards FP people, persistence definitely seems the way to go.
Defining semantics for iteration over mutable data structures is really hard to get sane much less consistent. Java there tried ConcurrentModificationException as a principled way to get fail-stop semantics but I think there are holes in that you can drive a truck through.
On top/any types, I think what really drove home for me how hard it is to get both idiomaticity and semantic consistency was this Python dict with heterogenous keys.
Python 3.14.0 (main, Oct 7 2025, 09:34:52) [Clang 17.0.0 (clang-1700.0.13.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> m = { True: "True", 1: "1", 1.0: "1.0" }
>>> m[True]
'1.0'
Even ignoring dynlangs' tendency to just have one scalar type for (numbers, strings, booleans), in key expressions, the conflation becomes super apparent. And List.find operators tends to recreate those problems in non-relational contexts.
Yeah. On JSON {de,}serialization, that was a major design story for us too, but not one that drove us towards heterogeneity. We allow
@json
class Price(
public currencyCode: String,
public amount: Int32,
) { ... }
@json is a decorator that applies at compile time to define a JSON adapter helper. That allows doing explicit, type-directed decoding.
Price.jsonAdapter().decodeFromJson(...)
Since we can't allow runtime type introspection, we do it at compile time. Meta-programming lets us avoid the need for heterogeneity. That's super important for things like:
let someBooleans = [false, true];
// Should encode `[false, true]` not `[0, 1]` even on runtimes that represent booleans using ints.
List.jsonAdapter(Boolean.jsonAdapter()).encodeToJson(someBooleans, jsonOut);
Some older dynlangs (eg Perl5 & PHP) have added affordances to allow for JSON encoding (
https://www.php.net/manual/en/function.is-bool.php ), but they're super brittle in practice.
Yeah, Rust's linearity is definitely an outlier. I was a huge fan of the Cyclone language back in the day, so I'm a fan of Rust too.
One thing we did was decide that there's no default reference identity operator, à la Clojure (identical? ...).