Effect rows
Side effects, type-checked.
Generated code is a black box: you can read what it says, but not what it will do. An effect row puts a readable boundary around it. Annotate a function with the capabilities it may use, and the compiler checks that its calls stay inside the row at compile time, with a runtime guard as a backstop.
;; this function may call the LLM, at most 5 times ^{:effects {:llm {:max 5}}} (defn summarize [q] (llm/query q))
;; declared with no effects, then it calls the LLM ^{:effects {}} (defn summarize [q] (llm/query q)) ;; error: llm/query exceeds the declared effect row
Proves properties outright, and often costs more than teams can justify.
Cheap and everywhere, but they stop at the surface and drift from the code.
The middle ground. A checked boundary: what it may do, how often, in what scope.
The box stays dark. The boundary is what you read and verify.
The foundation
One tree, every language.
Every front end lowers to the same 40+ node types. A Clojure function and a Python function are different on the surface and identical underneath. Adding a language is two small passes, not a rewrite.
A working lexer and parser module, generated straight from the spec.
The shared intermediate representation every language meets in the middle.
Parsing, lowering, and emitting through UIR today.
Who it's for
Two jobs, one tool.
If you build compilers, a grammar becomes a working front end instead of a hand-written parser. If you run generated code, an effect row becomes a readable boundary on what that code may do. The two meet on the shared tree.
What's next
Six front ends today. More when you join in.
Six languages parse, lower, and emit through UIR today: JSON, Python, JavaScript, TypeScript, TSX / React, and Clojure. Early days. The foundation and the effect layer are the pieces that exist now; the rest is open ground.
- Cross-compilation between any two front ends. Python to JavaScript, TypeScript to Python, Clojure to anything. One shared tree makes each pair two small passes; the roadmap is the full, tested matrix.
- Rust. A parser, a lowering pass, and an emitter, with a design goal of Rust's ownership rules feeding into the effect layer.
- HTML and CSS. A second-tier lowering so markup and styles join the same tree as the code.
- More frameworks. Svelte, Vue, and server components, following the TSX path that already turns hooks into static files.
- A language registry. Drop in a grammar and a lowering pass, and once it exists every emitter becomes a target in principle.
If one of these is close to your heart, or your favorite language is missing, the door is open. A front end is a grammar, a lowering pass, and an emitter. Everything else is already shared. Contribute on GitHub.
Try it
A library, not a service.
It's Racket. Install it, then run the suites or wire a grammar through the pipeline yourself.
git clone https://github.com/racklr-dev/racklr cd racklr raco pkg install --link . # link the package raco test racklr-test/*.rkt # run the suites
Why Racket? Macros and a parser ecosystem made it feasible to treat a grammar as data and generate a parser from it. That generation is the lever the rest of the toolchain rests on.