The core idea
A compiler is a shape, not a language.
Every compiler does the same four things: read source, understand it, transform it, and print it back out. racklr keeps reading and writing language-specific, generates the parser from a grammar, and shares the tree in the middle, along with everything that operates on it.
The consequence is that adding a language stops being a big project. You write down what the language looks like, you describe how its ideas map to the shared tree, and you describe how to print the tree back out. The rest of the pipeline already exists.
Generation
Write the spec, get the parser.
You do not hand-write a lexer or a parser. You write a grammar, which is a description of what the language looks like, and racklr generates the parser from it.
The generated parser does what a hand-written one would: it turns source text into a tree, and it handles the fiddly parts for you. Modes for lexing, recursive structures that would trip up a naive parser, and indentation-sensitive languages with the right lexer support.
// Lisp.g4 grammar Lisp; form : list_ | vector | atom ; list_ : '(' form* ')' ; vector : '[' form* ']' ; atom : SYMBOL | NUMBER ; // the lexer rules SYMBOL : [a-zA-Z+*/=<>!?-]+ ; NUMBER : [0-9]+ ; WS : [ \t\r\n]+ -> skip ;
The shared tree
One tree, many languages.
The parser hands you a tree in the shape of the source language. racklr lowers that into a single shared tree, the universal intermediate representation, and every language agrees on what lives in it.
The shared tree has a small set of shapes that cover what programs actually do. A function, a call, a branch, a loop, an assignment. A Clojure function and a Python function are different on the surface and identical underneath. Once something is in the shared tree, every language that can read it becomes a target.
;; Clojure (defn greet [name] (str "hello " name)) ;; Python def greet(name): return "hello " + name ;; both become the same shape uir-fn(name=greet, params=[name], body=call(+, "hello ", name))
Adding a language
Three small pieces.
Everything above is shared. A new language only brings its own three pieces.
The description of the language, which generates its parser.
How the source tree maps onto the shared tree.
How the shared tree prints back out as text.
Once those three exist, the language reaches every other language through the shared tree. That is the whole bet: do the transformation once, on the shared tree, and let every language meet there.
The contract
Side effects get a type system.
An effect row says what a function is allowed to do. It names the capabilities the function may use, and it says how much and within what scope.
;; this function may call the LLM, at most 5 times ^{:effects {:llm {:max 5}}} (defn summarize [q] (llm/query q))
Inference walks the call graph and composes those rows. A function that calls something with effects inherits them, and the join has to stay inside the caller's own row. If a call oversteps, inference rejects it at compile time. Where the call graph is not statically resolvable, racklr emits a generated guard wrapper that validates the row before the function runs. The contract is checked at the boundary, not guessed at in review.
That places effect rows between the two familiar extremes. Formal verification proves properties outright, at a cost that keeps it rare. Ad hoc specs are cheap but drift from the code. An effect row sits in the middle: it does not try to read the code, it states what the code may do, and it enforces that at the seam.
Ready to try it? The front page has the install and run commands. Back to the landing page.