Welcome to the jank alpha!
by Jeaye Wilkerson, with feedback from the jank community.
This book is written for jank’s alpha release. It is incomplete, although its incompleteness matches jank’s. It’s still early to jump into jank, but your time and patience is welcome.
Important
jank is alpha quality software. It will crash. It will leak. It will be slow. There are huge areas of functionality which haven’t been implemented. Your help getting us past this stage is greatly appreciated.
What is jank?
jank is a general purpose programming language. It’s a dialect of Clojure, which is itself a dialect of Lisp. jank is functional-first, but it supports adhoc mutations and effects. All data structures are persistent and immutable by default and jank, following Clojure’s design, provides mechanisms for safe mutations for easy concurrency.
Beyond Clojure, jank is brethren to C++ and it can reach into C++ arbitrarily to both access and define new C++ types, functions, and templates, at runtime. This is done by JIT (just in time) compiling C++, using Clang and LLVM. The result is that you can write Clojure code which can access C and C++ libraries trivially.
For more details on jank’s status, please read the foreword.
Foreword
jank is a personal creation made public. It’s a passion project which aspires to push the boundaries of two languages very dear to me: C++ and Clojure. These two languages could not be further apart, in their syntax, paradigms, culture, adoption, and typical use cases. Still, I aim to bind them.
jank is currently alpha quality software. Most importantly, that means that you would be crazy to ship it into production for anything that matters. More specifically, it means that jank, and its related programs, will crash, leak memory, provide incorrect results, and in general surprise, confound, and frustrate until we can implement all remaining pieces and iron out all remaining bugs. I need your help with this.
Before jank, achieving this level of seamless C++ interop, with JIT (just in time) compiled C++, and full AOT (ahead of time) compilation support had never been done, from any dynamic language. Swift has come closest, though it’s not dynamically typed and it lacks an official JIT compiler. Cppyy, for Python, strives to compete, but it lacks AOT compilation support. Because of this trail blazing, we have faced many bugs in Clang. Clang is the main challenge for both compile-time performance and overall memory usage. This continues to be a limiter for jank’s success, due to the size and complexity of the Clang code base and my limited time. To ensure jank’s success, this challenge will need to be tackled directly, most likely by finding and employing a part-time Clang developer. If you are able to help with this, please reach out.
Moving on.
The performance of jank, during this alpha stage, will be quite bad. Depending on the benchmark, you might find jank to be 2x or even 10x slower than Clojure JVM. Maybe more. We have continuous benchmarks tracking jank’s performance.
Do not be concerned by this. I am not concerned by this.
I have not had the luxury to focus on performance much beyond some early design decisions. Clojure JVM, aside from leaning on the JVM for much of its performance, is doing many more optimizations than jank is currently doing. A lot of functionality, such as the GC (BDWGC), sorted containers, and others are currently placeholder. What’s most important is that jank works and that it’s correct. Once we achieve that, I will endeaver to show that jank can be the fastest Clojure dialect around. Right now, that is not a priority, no matter how much you may want it to be.
Lastly, the Clojure community is empowered by backward compatibility. I respect and appreciate this goal for both Clojure and jank. I will be codifying stable APIs for embedding jank, ensuring the stability of jank’s special forms, developing an integrated build system to improve the longevity of jank libraries which wrap native libraries, and pursuing binary compatibility as much as the native world and all of its quirks allows. However, during the alpha release stage, and until we have our first production release, anything goes.
Now, install jank. Build some software. Report all of your bugs on Slack or Github! Engage in the community. Work with us as we stabilize and forge this language into what others in the future will know it to be.
Jeaye
Getting Started
Let’s jump into jank! There’s a lot to learn, but we all have to start somewhere. In this chapter, we’ll discuss:
- Installing jank on macOS and Linux
- Writing a program which prints
Hello, world! - Using Leiningen to manage jank projects
Installation
jank has continuous builds for macOS, Ubuntu, and Arch. These builds are bleeding edge and you’re encouraged to update regularly. If you’re on any of the supported systems, you can install jank using your system’s package manager. If not, you can still build jank yourself.
Homebrew (macOS, aarch64)
We have a binary jank package in brew, so installation is quick and easy.
brew install jank-lang/jank/jank
To update jank, you can run the following.
brew update
brew reinstall jank-lang/jank/jank
If you’d like to install from source using brew, you can use jank-lang/jank/jank-git instead.
Note
We don’t yet have x86 binaries in the Homebrew package. If you’d like to help with this, please reach out.
Ubuntu Linux (24.04, 24.10, 25.04, 25.10, 26.04)
We have a binary jank package in our own repo, so installation is quick and easy.
sudo apt install -y sudo curl gnupg lsb-release
curl -s "https://ppa.jank-lang.org/KEY.gpg" | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/jank.gpg >/dev/null
echo "deb [signed-by=/etc/apt/trusted.gpg.d/jank.gpg] https://ppa.jank-lang.org $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/jank.list >/dev/null
sudo apt update
sudo apt install -y jank
To update jank, you can run the following.
sudo apt update
sudo apt reinstall jank
Note
Older versions of Ubuntu, like 22.04, will not work with jank. This is because jank requires C++20 to work and the libstdc++ on those systems is too old.
Arch Linux (AUR)
We have a binary jank package in AUR, so installation is quick and easy.
yay -S jank-bin
To update jank, you can run the following.
yay -Syy
yay -S jank-bin
If you’d like to install from source on Arch, you can install jank-git instead.
Something else?
Don’t see your preferred system here? Help us with packaging! We want jank to be everywhere.
Hello, world!
Now that you’ve installed jank, it’s time to write your first jank program.
Following tradition, we’ll write a trivial program which prints Hello, world!
to the screen.
Project directory setup
jank doesn’t require any particular directory structure. It can work with files directly. However, in order to keep our systems clean, we’ll create a new project directory for this example. Run these commands in a terminal.
$ mkdir -p ~/projects/hello_world
$ cd ~/projects/hello_world
Next, use your text editor to create a file called hello.jank in the
hello_world directory. The contents should look like this.
(println "Hello, world!")
Finally, we can run this file!
$ jank run hello.jank
Hello, world!
Hello, Leiningen!
jank, on its own, is just a compiler and runtime. For non-trivial projects, you will want a tool to manage your dependencies, profiles, resources, and development workflows. For this, we use Leiningen (LINE-ing-en). Leiningen is a project management tool for Clojure and jank is a dialect of Clojure.
Note
In the future, jank’s recommended workflow will be to use the default Clojure CLI tool, but it’s still being improved and it doesn’t yet offer the excellent user experience that Leiningen does. For now, it is not recommended for jank projects.
Installing Leiningen
Leiningen is available in basically every package manager as leiningen.
# If you're on macOS.
$ brew install leiningen
# If you're on Ubuntu (or similar).
$ sudo apt install -y leiningen
# If you're on Arch (or similar).
$ yay -S leiningen
For more details, see the Leiningen docs.
Creating a project with Leiningen
Now that Leiningen is installed, we can use it to create a new jank project. Let’s create another hello world style program.
$ mkdir ~/projects
$ cd ~/projects
$ lein new org.jank-lang/jank hello_lein
$ cd hello_lein
Note
If you use
lein newwithout specifyingorg.jank-lang/jank, you will get a Clojure JVM project, not a jank project. Make sure you get a jank project.
The layout of a Leiningen project
Inside the hello_lein directory, you will find some files have already been
created.
$ ls
LICENSE project.clj src test
Most importantly, the project.clj is the file which controls Leiningen and
stores all meta information about your project. To start with, our project.clj
will look similar to this:
(defproject hello_lein "0.1-SNAPSHOT"
:license {:name "MPL 2.0"
:url "https://www.mozilla.org/en-US/MPL/2.0/"}
:dependencies []
:plugins [[org.jank-lang/lein-jank "2026.06-2"]]
:middleware [leiningen.jank/middleware]
:main hello-lein.main
:profiles {:debug {:target-dir "target/debug"
:jank {:optimization-level 0}}
:release {:target-dir "target/release"
:jank {:optimization-level 3}}})
Your versions may differ, but the overall structure will remain. Our
project.clj defines some useful aspects to Leiningen.
- The project name
hello_leinand version0.1-SNAPSHOT :license: The license of our project, which defaults to MPL since jank uses it. You are free to change this.:dependencies: Our project dependencies. More on this in another chapter.:plugins: Thelein-jankplugin, and its:middleware, which is used to configure our project for jank instead of Clojure JVM.:main: The entrypoint of our program, which contains our-mainfunction.:profiles, which allows us to enable different flags and build modes.
Inside src/hello_lein/main.jank, we will see the code for our project.
(ns hello-lein.main)
(defn -main [& args]
(println "Hello, world!"))
Running a Leiningen project
Running your project involves starting at our :main file, loading all required
files, and then calling the -main function. Leiningen will help us with
setting everything up so that jank can do this.
$ lein run
<maybe some first time output for lein to fetch dependencies>
Hello, world!
Testing a Leiningen project
Leiningen has support for easily running all tests for a project. Tests are
written in the test/ directory. The jank template provided us with an example
test which will fail.
$ lein test
If tests exist in directories other than test/, you can use the :test
profile to point Leiningen to them. Leiningen will find all jank source files
within those directories, recursively, and ensure their tests run.
(defproject hello_lein "0.1-SNAPSHOT"
;; ...
:profiles {;; ...
:test {:test-paths ["src/test" "other/test"]}})
You can read more about testing projects here.
Compiling a Leiningen project
It’s possible to AOT (ahead of time) compile our whole Leiningen project to an executable. This involves compiling every one of our source files and dependencies and then linking them all together. Leiningen makes this easy.
$ lein compile
$ ./target/debug/hello_lein
Hello, world!
Our binary has been named based on our project and is placed within our target directory. When we invoke it, we see our printed hello world.
You can read more about AOT compiling projects here.
Hello, nREPL!
jank has nREPL support both via Leiningen projects and directly from the jank
command. This has been tested to work with both CIDER and Conjure.
Within a Leiningen project
To start up an nREPL server for your project, you can just run the following.
$ lein repl
jank nREPL server is running on nrepl://127.0.0.1:55765
my-project.main=>
Note
Generally, you don’t need to copy the address or port yourself, since jank writes a
.nrepl-portfile which is found by most nREPL tooling.
Using jank directly
If you’re not within a Leiningen project, you can still quickly spin up an nREPL server as part of any jank terminal nREPL client.
$ jank repl
jank nREPL server is running on nrepl://127.0.0.1:67563
user=>
Reaching into C++
jank is designed to be able to reach right into C++ and access variables, types,
functions, templates, and even preprocessor values. All C++ related special
forms live in the special cpp/ namespace.
In this chapter, we’ll dive into all of the various use cases of working with C++ types, functions, values, and exceptions.
Working with native values
C++ values, references, and pointers can be used directly within jank. However,
there are some limitations due to how C++’s object model works. The primary
factor here is that C++ has no base object type for all classes and structs.
Each top-level type is standalone. This is different from the JVM, CLR, and JS
environments where there is a base Object for all class types. This means that
we need to take some extra steps in order to store arbitrary C++ values within
the jank runtime. There are a few ways this can be done and jank tries to make
this as easy as possible.
let is special
Within a let, you can bind any native value to a name and jank will do no
conversions. Furthermore, each let represents a lexical scope, so any values
with non-trivial destructors will have those destructors called at the end of
the let. This behaves just the same as explicit C++ scoping via { }.
As soon as you leave the let, but trying to pass the value as an argument, or
return the value from the let, you may be crossing a boundary out of C++ land
and into Clojure land. In general, this happens in two cases:
- Calling a Clojure function
- Returning a value in a Clojure function
If you use native values in a let and you only call C and C++ functions, no
conversions will happen. No hidden allocations will happen. However, when you
start passing native values into Clojure functions or returning them from
Clojure functions, jank will do what it can to make that work, which may involve
conversions. More on that below.
Named literals
jank also has explicit support cpp/nullptr, which corresponds to the C++ primitive.
Member values
Member values can be accessed from a native object using the .-foo syntax. The
cpp/ prefix for members is optional. For example, let’s create a person and
then pull out the name.
(cpp/raw "struct person
{ std::string name; };")
(defn create-person [name]
(let [p (cpp/person (cpp/cast cpp/std.string name))
n (.-name p)]
))
Whenever a member is accessed, you will get a reference to it, not a copy. Also, note that members can be accessed through a pointer to the native object, without needing an explicit dereference.
(defn create-person [name]
(let [p (cpp/new cpp/person (cpp/cast cpp/std.string name))
n (.-name p)]
))
Trait-convertible
Some C++ types are automatically and implicitly convertible to/from
jank objects. These include all C++ intrinsic intregral types, bools, C strings,
and even some C++ standard libary types like std::string. For these types,
the jank compiler will detect if conversion is necessary and will implicitly
handle conversions as needed. For example, let’s take a look at this jank code
which calls a C++ function which operates on std::string.
(cpp/raw "std::string to_upper(std::string const &s)
{
std::string ret;
for(auto const c : s)
{ ret += ::toupper(c); }
return ret;
}")
(defn to-upper [o]
(let [upper (cpp/to_upper o)]
upper))
In this code, o is a jank::runtime::object_ref. This is basically like
Clojure’s Object type. It’s a garbage collected, type-erased value. When the
jank compiler analyzes the call to (cpp/to_upper o), it resolves that
to_upper expects a std::string and that there is a conversion trait for it.
So the jank compiler will automatically handle converting from object_ref into
a std::string. On the other side, upper is a std::string, which is the
result of to_upper. However, when we return it from the let, the jank
compiler sees that it can implicitly create an object_ref for us, so there’s
nothing we need to do.
Non-trait-convertible
Aside from the built-in supported trait conversions, every other C++ type will not be convertible. If you try to pass such a value as a jank function argument or if you try to return such a value from a jank function, you will get a compiler error. For example, given this source:
(cpp/raw "struct person
{ std::string name; };")
(defn create-person [name]
(let [p (cpp/person (cpp/cast cpp/std.string name))]
p))
If we try to run this file, we’ll get a compiler error telling us that we can’t
return a value of type person from our function, since it’s not convertible to
a jank runtime object.
$ jank run person.jank
─ analyze/invalid-cpp-conversion ────────────────────────────
error: This function is returning a native object of type
'person', which is not convertible to a jank runtime
object.
─────┬───────────────────────────────────────────────────────
│ test.jank
─────┼───────────────────────────────────────────────────────
2 │ { std::string name; };")
3 │
4 │ (defn create-person [name]
│ ^ Expanded from this macro.
─────┴───────────────────────────────────────────────────────
Implementing your own trait
To build on the person type defined above, we could extend the conversion
trait to teach jank how to convert to/from person and jank maps. This does
require C++ template metaprogramming, which is an advanced concept that’s only
intended for C++ developers who’re using jank.
(cpp/raw "struct person
{ std::string name; };
namespace jank::runtime
{
template <>
struct convert<person>
{
static obj::keyword_ref name_kw;
static obj::persistent_hash_map_ref into_object(person const &p)
{
return obj::persistent_hash_map::create_unique(std::make_pair(name_kw, make_box(p.name)));
}
static person from_object(object_ref const o)
{
auto const name{ try_object<obj::persistent_string>(get(o, name_kw)) };
return person{ name->data };
}
};
obj::keyword_ref convert<person>::name_kw{ __rt_ctx->intern_keyword(\"name\").expect_ok() };
}")
(defn create-person [name]
(let [p (cpp/person (cpp/cast cpp/std.string name))]
p))
(println (create-person "foo"))
Now, if we’re to run this, we can see that the person was implicitly converted
into a jank hash map.
$ jank run person.jank
{:name foo}
Note
Although this works, consider moving this C++ into a header file and including it instead. Writing large amounts of C++ in
cpp/rawstrings doesn’t scale very well, in terms of maintainability.
Opaque boxes
There is a performance cost to the convenience of implicit conversions. For pure data, and trivial types, this may be preferred. However, if you want to store something like a C++ database handle, which is managing network resources, a thread pool, and other state, converting this to a jank runtime object is not practical. In these cases, you can use an opaque box to pass the data through the jank runtime instead.
Opaque boxes are jank runtime objects which basically store a void*, which is
an untyped native pointer. The key part here is that the data you store in the
opaque box must be a pointer. Since the opaque box could be stored in a
container, captured in a closure, or otherwise kept alive, it’s important that
the data within is also dynamically allocated. However, opaque boxes track the
name of the type at compile-time and ensure that unboxing uses the correct type.
Given a hypothetical my_db C++ database library, boxing is done like this:
(defn query! [db-box q]
(let [; db-box is an object_ref
; db is a my_db.connection*
db (cpp/unbox (:* my_db.connection) db-box)]
(.query db q)))
(defn -main [& args]
(let [; db is a my_db.connection*
db (cpp/new my_db.connection "localhost:5758")
; db-box is a opaque_box_ref
db-box (cpp/box db)]
))
If you unbox the incorrect type, jank will surface a runtime error with helpful
source information describing the type that was in the opaque box and the type
you expected. For example, let’s say we box a connection*, but we try to unbox
it as a secure_connection*.
❯ jank run test.jank
─ runtime/invalid-unbox ───────────────────────────────────────────────────────
error: This opaque box holds a 'my_db::connection*', but it was unboxed as a
'my_db::secure_connection*'.
─────┬─────────────────────────────────────────────────────────────────────────
│ test.jank
─────┼─────────────────────────────────────────────────────────────────────────
21 │ (let [; db-box is an object_ref
22 │ ; db is a my_db.connection*
23 │ db (cpp/unbox (:* my_db.secure_connection) db-box)]
│ ^^^^^^^^^ Unboxed here.
│ …
28 │ db (cpp/new my_db.connection "localhost:5758")
29 │ ; db-box is a opaque_box_ref
30 │ db-box (cpp/box db)]
│ ^^^^^^^ Boxed here.
31 │ (query! db-box "meow")))
32 │
33 │ (-main)
│ ^^^^^^^ Used here.
─────┴─────────────────────────────────────────────────────────────────────────
Complex literal values
If your C++ value is not representable using just a symbol, due to
template arguments or other shenanigans, you can use jank’s C++ domain specific language (DSL).
Documentation on that is here. For example, here’s how we grab
std::basic_string<char>::npos:
(let [m #cpp (:member (std.basic_string char) npos)]
)
No implicit boxing will happen here, unless you use this value in a way which requires it. jank will give you a reference to the value you specified. If you need a copy, you will need to manually do that.
Working with native types
Accessing C++ types
C++ types are available within the cpp namespace, but you must replace :: with
.. For example, std::string becomes cpp/std.string. This also works for
type aliases. Given a type, a value can be constructed by calling the type. This
supports both constructor overload resolution and aggregate initialization.
C++ initializer lists are not currently supported.
(let [i (cpp/int)] ; Stack-allocates an int.
)
Complex literal types
For complex types like template instantiations, pointers to members, and so on,
jank supports a C++ domain-specific language (DSL).
This is available implicitly when in type position, but it can be
explicitly requested by using the special #cpp tag. The documentation for this
DSL is here.
(let [p (#cpp (:* void))] ; Stack-allocates a void*.
)
Defining new types
There isn’t yet a way to define new types using jank’s syntax, but you can
always drop to cpp/raw to either include headers or define some C++ types
inline. Improved support for extending jank’s object model with JIT (just in
time) compiled types will be coming soon.
(cpp/raw "struct person
{
std::string name;
};")
Better yet, write the C++ code in a header file and include it in your jank project.
(ns my.app
(:include "person.hpp"))
Working with native functions
Global functions
C++ has a huge range of function scenarios and jank tries to capture them all.
The simplest case is global functions, as well as static member functions. This
applies to both C and C++ functions. In order to call these, just take the fully
qualified name of the function and replace :: with .. For example:
randbecomescpp/randstd::this_thread::get_idbecomescpp/std.this_thread.get_id
For example, we can use the C functions srand, time, and rand to seed the
pseudo-random number generator with the current time and then get a number.
(defn -main [& args]
(cpp/srand (cpp/time cpp/nullptr))
(println "rand:" (cpp/rand)))
If you need to access a function within a more complex type, such as a template instantiation, you can use the C++ domain specific language (DSL). It’s documented here.
Overload resolution
Once we get out of C land and into C++ territory, the function name alone doesn’t necessarily make it unique. C++ functions can be overloaded with different arities and different parameter types. jank will resolve each function call at compile-time. There is no runtime reflection. If a call can’t be resolved to a known overload, or is ambiguous between multiple overloads, jank will raise a compiler error.
For example, the
std::to_string
function has many different overloads. Here, we specifically create i to be an
int, so overload resolution can happen.
(defn -main [& args]
(let [i 42
s (cpp/std.to_string i)]
s))
However, if we try to rely on implicit trait conversions, or we pass an unsupported type, we’ll get a compiler error.
(defn -main [& args]
(let [i 42
s (cpp/std.to_string i)]
s))
$ jank run test.jank
─ analyze/invalid-cpp-function-call ───────────────────────────────────────────
error: No normal overload match was found. When considering automatic trait
conversions, this call is ambiguous.
─────┬─────────────────────────────────────────────────────────────────────────
│ test.jank
─────┼─────────────────────────────────────────────────────────────────────────
1 │ (defn -main [& args]
2 │ (let [i 42
│ ^ Expanded from this macro.
3 │ s (cpp/std.to_string i)]
│ ^^^^^^^^^^^^^^^^^ Found here.
─────┴─────────────────────────────────────────────────────────────────────────
We could opt into a specific conversion, and thus a specific overload, by using cpp/cast.
(defn -main [& args]
(let [i 42
s (cpp/std.to_string (cpp/cast cpp/int i))]
s))
Member functions
Member functions can be accessed using the .foo syntax. The cpp/ prefix for
members is optional. For example, let’s convert a jank object to a std::string
and then see if it’s empty.
(defn empty? [o]
(let [s (str o)
native-s (cpp/cast cpp/std.string s)]
(.empty native-s)))
Also note that member functions can be called through a pointer to the native object, without the need for an explicit dereference.
Arbitrary callables
In C++, we also deal with pointers to functions and custom types which implement the call operator. jank supports both of these scenarios using the normal call syntax. For example, we can implement our own callable which captures some data and then returns it when called.
(cpp/raw "struct call_me
{
jank::runtime::object_ref data;
jank::runtime::object_ref operator()()
{ return data; }
};")
(defn -main [& args]
(let [f (cpp/call_me. "meow")]
(f)))
Operators
C++ operators are special language features for primitives, but they can also be
overloaded for custom types. Their semantics are much more complicated than
Clojure’s function calls, but basically all of them are available under the
cpp/ namespace within jank.
Note
C++20 does operator rewriting for comparison operators, to use the
<=>spaceship operator, or perhaps others. jank doesn’t currently support this. If you’re porting C++ code to jank which fails to find the appropriate operator, chances are that operator never existed and Clang used rewriting to use a different operator instead. You can mitigate this by ensuring you use operators that actually exist.
The C++ DSL
jank has a domain specific language (DSL) for acessing arbitrary C++ types and
values. This DSL is automatically enabled when the jank compiler is expecting a
type, such as the first argument to cpp/new, cpp/unbox, or cpp/cast. The
DSL can also be explicitly enabled using the #cpp tag.
DSL overview
The C++ DSL can be used to look up both types and values. The jank compiler will ensure that you use a type when you need a type and a value when you need a value.
(:* t)adds a pointer(:& t)adds an lvalue reference(:&& t)adds an rvalue reference(:const t)and(:volatile t)add the corresponding C++ qualification(:signed t)and(:unsigned t)add the corresponding C++ qualification(:long t)and(:short t)add the corresponding C++ qualification(:array t s?)turns a type into a sized (or unsized) array(:fn ret [a1 a2...])builds a function type(t a1 a2...)builds a template instantiation(:member t name)nests into a type(:member* t mt)builds a pointer to member type(:&member t mt)builds a pointer to member value
Let’s take a look at some examples, comparing the C++ representation and the jank representation.
Type examples
| C++ | jank |
|
A normal C++ map template instantiation.
|
|
|
A normal C++ array template instantiation.
|
|
|
A sized C-style array.
|
|
|
A reference to an unsized C-style array.
|
|
|
A pointer to a C++ function.
|
|
|
A pointer to a C++ member function.
|
|
|
A pointer to a C++ member which is itself a pointer to a function.
|
|
Value examples
jank will never implicitly analyze the C++ DSL for values, like it does for
types. You must always tag your DSL form with #cpp to separate it from normal
jank code.
| C++ | jank |
|
|
|
|
|
|
Throwing and catching exceptions
jank integrates tightly into C++’s exception model. C++ allows values of any type to be thrown, caught, and rethrown. So does jank.
Throwing and catching jank objects
When you throw a value from jank, regardless of its type, the value will be
type-erased into an object_ref. It doesn’t matter if you throw a keyword or a
hash map or a string, or any other jank runtime object, you catch it as an
object_ref. For example:
(defn -main [& args]
(try
(throw :ok!)
(catch cpp/jank.runtime.object_ref e
(println :caught e))))
Throwing and catching native values
Many C++ libraries will throw values which are not jank runtime objects. A very
common type to throw is a class derived from std::exception. From jank, we can
catch any C++ type and, just like in C++, we can catch exceptions via their base
type as well.
In this example, calling .at on a std::vector, with an invalid index, will
throw a std::out_of_range exception, which derives from std::exception. We
can catch the exception by the base type and then rely on the virtual .what
member function to get the exception message.
(let [v (#cpp (std::vector int))]
(try
; This will throw.
(.at v 0)
(catch cpp/std.exception e
(println :caught (.what e)))))
Note
jank doesn’t yet support providing native values to
(throw ...), but it will soon.Also, jank doesn’t yet support the equivalent of C++’s catch all, which catches any exception type, but doesn’t provide the value. We will support this, too.
Casting between native types
jank has two primary means of casting between native types.
cpp/castcpp/unsafe-cast
They both have the same syntax, but they perform different actions.
cpp/cast
This is the most common style of cast which you’ll see in jank. It closely maps
to C++ static_cast, but it has additional functionality to support jank’s
trait conversions. This will allow you
to cast to/from jank runtime object and supported native C++ values.
(fn [o]
(let [f 3.14
; Normal static_cast support.
i (cpp/cast cpp/int f)
; Explicit trait conversion, since `o` is a jank object.
oi (cpp/cast cpp/int o)])
cpp/unsafe-cast
When cpp/cast is not enough, jank supports a more cutting cast which is the
equivalent of C-style casting in C++. This is a combination of static_cast,
reinterpret_cast, and const_cast. For example, if you need to cast between unrelated
pointer types, cpp/cast will not work, but cpp/unsafe-cast will. However,
note that cpp/unsafe-cast does not support trait conversions. It is solely
dedicated to native type construction and reinterpretation.
(let [s "meow"
us (cpp/unsafe-cast (:* (:unsigned char)) s)])
Embedding raw C++
jank has a special cpp/raw form which accepts a single string containing
literal C++ code. This can be used for bringing in pretty much anything.
jank will always compile the included C++ source in a global scope, even if you
put the cpp/raw form within a nested scope, such as within a function or a
let. For example, this code will have the same effect, even if this function
is never called.
(defn foo []
(cpp/raw "struct bar{ };"))
The cpp/raw form always evaluates to nil. At runtime, foo will do nothing
but return nil, since the JIT compilation is where the effect of cpp/raw
actually happens.
Note
Unlike in C++, you will not need to
#includein every source file, since the global C++ environment is affected by each file inclusion. This is simply due to how Clang’s JIT compilation works. However, this means you should be even more careful with how much you include and how name collisions might happen within your jank projects.
A helpful idiom
Hopefully this becomes a less common idiom simply by not being needed, but for
now it’s common enough. If you run into issues trying to access a member, call a
function, etc using normal C++ interop, you can write a wrapper in cpp/raw
which will do the trick. For example, let’s say we have the following code.
(let [s (cpp/std.string)
; Let's say that this interop call isn't compiling correctly, due to a
; jank bug.
size (.size s)]
(println "The size is" size))
You can work around this issue by defining a helper function which does the C++ work for you. In this case, we could do the following.
(cpp/raw "size_t get_string_size(std::string const &s)
{ return s.size(); }")
(let [s (cpp/std.string)
size (cpp/get_string_size s)]
(println "The size is" size))
Of course, if you need to use this, please also report a bug on jank’s Github which describes what you tried to do and why it didn’t work.
The cpp namespace
The special cpp/ namespace has two purposes, in jank.
- To contain all special C++ forms like
cpp/new,cpp/cast,cpp/&, etc. - To provide access to all C and C++ symbols.
If you want to access a C or C++ symbol without the cpp prefix, you can refer
it into your current namespace using clojure.core/refer-global. This is also
accepted as part of the ns macro. For example:
(ns my-lib.core
(:include "gl/gl.h")
(:refer-global :only [glBindBuffer GL_ARRAY_BUFFER]))
(defn bind-array-buffer! [buffer]
; These symbols can just be used directly, without a cpp/ prefix.
(glBindBuffer GL_ARRAY_BUFFER buffer))
Working with projects
jank uses Leiningen to manage projects. It’s possible to use other tooling, such as Clojure CLI, but Leiningen currently provides a much better user experience.
Leiningen projects in jank manage your dependencies, compilation settings, runtime settings, distribution settings, and more.
Testing
Testing a Leiningen project
Leiningen has support for easily running all tests for a project. Tests are
written in the test/ directory. The jank template provided us with an example
test which will fail.
$ lein test
If tests exist in directories other than test/, you can use the :test
profile to point Leiningen to them. Leiningen will find all jank source files
within those directories, recursively, and ensure their tests run.
(defproject hello_lein "0.1-SNAPSHOT"
;; ...
:profiles {;; ...
:test {:test-paths ["src/test" "other/test"]}})
Running specific tests
You can run tests in specific namespaces by specifying them as command-line arguments.
$ lein test project.ns1 project.ns2
You can also run just specific tests.
$ lein test :only project.ns1/my-test
Test selectors
Test selectors allow you to define filters for which tests to run.
When you execute lein test, Leiningen uses the :default selector
to select the tests, by default. However, you can add new selectors in
project.clj and specify them when running lein test.
For example, let’s say our integration tests are slow and we want to separate
them from our other tests. First we add a new :integration test selector to
our project.
(defproject hello_lein "0.1-SNAPSHOT"
;; ...
:profiles {;; ...
:test {:test-selectors {:default (complement :integration)
:integration :integration}}})
Then we tag our integration tests with the :integration metadata.
(deftest ^:integration network-heavy-test
(is (= [1 2 3] (:numbers (network-operation)))))
Finally, we can run only the integration tests.
$ lein test :integration
AOT compiling
Given a jank Leiningen project, you can ahead of time (AOT) compile all of your code and dependencies to an executable using this command.
lein compile
Your executable will be named based on your current project and can be found
within target/debug/<project name> by default. When you invoke your
executable, your -main function will be called.
Building for release
By default, lein compile will build you a debug executable with fewer
optimizations enabled. To get a release executable, you can enable the release
profile.
lein with-profile +release compile
AOT runtime selection
By default, AOT compilation will target jank’s static runtime. This means that
the compiled binary will not link to Clang/LLVM and all of its functionality
will be baked in. Since Clang/LLVM is not linked in, JIT compilation is not
possible at runtime. Calling something like eval will throw. This is very
similar to a Graal native image.
If you need to be able to JIT compile code from your AOT compiled binary, you’ll
need to enable the dynamic runtime. From jank’s command line, you can use
--runtime dynamic, but you can also just set this in your Leiningen project.
:profiles {:release {:jank {:runtime :dynamic}}}
The jank build system
jank provides a powerful, sandboxed, cross-platform build system. It enables us to build jank programs on top of existing C and C++ libraries, in a unified way, regardless of the build system those libraries use. Check out the enclosed sections and guides to learn more!
Build system overview
jank provides a custom native build system with the goal of making it easy to build your jank programs on top of C and C++ libraries. Cargo is a big inspiration for the design of jank’s build system.
Some dependencies are installed by the system and others need to be built from source. jank’s build system handles both of these cases. It’s important to note that jank’s build system doesn’t aim to replace existing native build systems like CMake, but it does integrate with them via custom build scripts.
The foundational aspect of jank’s build system is the jank-build.bb script.
Each jank package may have one jank-build.bb script and its presence indicates
to the jank build system that there’s work to be done to build that package.
The script itself is executed with Babashka and may use any Babashka APIs available. The jank build system provides some particular APIs to aid in finding system packages and building projects from source.
Each script has two jobs, which we’ll explain separately.
- Print directives to
stdoutwhich tell the jank build system to add flags to its jank invocation - Find or build files and install them in the provided
outdirectory
By default, each build script for a dependency will run whenever any of the build flags change. For example, if you change your optimization level, all dependencies will be rebuilt. However, build scripts can also print directives which tell the jank build system to rebuild that package when other things change.
Directives
Each printed directive is a single line printed to stdout by the
jank-build.bb script. Any lines printed which don’t start with jank-build::
are ignored by the build system. Every directive can be provided any number of
times and will always build on the previously printed directives.
The directives fit into two categories:
- Build flags
- Re-run conditions
Build flags
For the build flags, you can specify preprocessor defines, include directories which contain header files, link directories which contain libraries, and the library names to actually link.
jank-build::define=FOO
Adds the -DFOO flag to the jank invocation. This can also be used with a value,
with jank-build::define=FOO=1, which then adds the -DFOO=1 flag.
jank-build::include-dir=path
Adds the -I path flag to the jank invocation. The path should be absolute.
jank-build::link-dir=path
Adds the -L path flag to the jank invocation. The path should be absolute.
jank-build::link-library=lib
Adds the -l lib flag to the jank invocation, which supports both static and dynamic libraries.
If both are present, dynamic libraries will be preferred, which matches Clang’s
behavior. Library names, relative paths, file names, and absolute paths are supported.
jank-build::link-static-library=lib
Adds the -l:lib flag to the jank invocation, which is similar to the -l flag
but it forces the linked library to be static. If no static library is found, an error is raised.
Library names, relative paths, file names, and absolute paths are supported.
Re-run conditions
Re-run condition directives tell the jank build system when to re-run the build script. By default, it will happen whenever any files within the source directory of the package change.
jank-build::rerun-if-changed=path
Informs the jank build system to re-run this build script if a particular file
changes. The path is expected to be relative to the source directory of the
package. If the path is a directory, all files within that directory will be
watched.
Note: If no rerun-if-changed directive is provided, all files within the source
directory will be watched.
jank-build::rerun-if-env-changed=FOO
Informs the jank build system to re-run this build script if the environment
variable FOO changes. This is mainly expected to be used for variables like
CC and CXX, but anything can work.
Note: The following variables will be watched by default and cannot be unwatched.
PKG_CONFIG_PATH
Finding or building things
The second thing build scripts can do is prepare files for jank consumption. This may involve building projects with CMake or similar. In order to do this effectively, the jank build system has the concept of three key directories per build script invocation:
- Source directory (
:src-dir) – This is where the package’s source files are. - Build directory (
:build-dir) – This is a temporary directory where build files can be placed. - Out directory (
:out-dir) – This is where the final artifacts must be installed in order to be used by jank.
On top of that, build scripts are provided with the following:
- A map of build inputs (
:inputs) which maps from package name to out directory – This is useful when your package depends on the output of another package. - The optimization level to use (
:optimization-level) – This is an integer from0to3inclusive.0means no optimizations. - Whether or not to build a static lib (
:static?).
The inputs to a build script are available via *input*. Here’s an example:
{:src-dir "/path/to/project"
:build-dir "/tmp/jank-build-6616793965093062497"
:out-dir "/path/to/project/target/debug/_cache/imgui+glfw-out-XXX"
:inputs {"org.jank-lang.commons/imgui-sys" "/path/to/project/target/debug/_cache/imgui-sys-2026.06-6-out-HsZD6cjvMzkPAeaJZZQAKw"
"org.jank-lang.commons/glfw-sys" "/path/to/project/target/debug/_cache/glfw-sys-2026.06-1-out-zCGlvgSQ65jN8ZQwixkVIA"
"org.jank-lang.commons/imgui-glfw-sys" "/path/to/project/target/debug/_cache/imgui-glfw-sys-2026.06-6-out-DY2lSndonGjZEkhw7JE1TQ"
"org.jank-lang.commons/gl-sys" "/path/to/project/target/debug/_cache/gl-sys-2026.06-1-out-qCuPOQm5Ch-yJlIvZOBPqA"
"org.jank-lang.commons/imgui-opengl2-sys" "/path/to/project/target/debug/_cache/imgui-opengl2-sys-2026.06-6-out-OisnnGXhsy1cebbvy8iM9A"}
:optimization-level 0
:static? true}
Sandboxing
By default, build scripts run in a sandbox which only has write access to the
build and out directories. In-source builds will not work. Read access is given
to system-level directories, but not /home. To disable the sandbox, you can
run Leiningen with --disable-sandbox. For example: lein run --disable-sandbox.
Build script dependencies
Your build scripts may have dependencies of their own, which are not available
to your jank application. This is handled by adding :build-dependencies to
your project.clj. For example:
:build-dependencies [[org.jank-lang.commons/jank-build-cmake "2026.06-6"]]
Top-level build scripts
Your jank project may also have a jank-build.bb of its own, stored in the
top-level directory of your project, adjacent to your project.clj. By default,
this build script always runs, unless you provide any rerun-if-changed
directives in it. If you just want to add some -D, -I, -L, or -l flags
to your project, this is where you should do it.
Troubleshooting build failures
If a build script fails, the jank build system will automatically print the
stdout and stderr from the build. You can also opt into verbose mode by
providing -v to your Leiningen invocation. For example: lein compile -v.
The build cache
jank and the jank build system store the output of builds into the “target” and
“build” directories. By default, the target directory is target and the build
directory is target/_cache, both relative to the project.clj or current
directory of the jank execution. Within Leiningen projects, the jank
template automatically sets up target directories per-profile. This changes the
debug profile target directory to target/debug, for example. To change the
target directory with jank directly you can use the --target-dir flag.
Similarly, the --build-dir flag can be used, but it’s not currently
possible to change the build directory within project.clj.
There are two types of artifacts:
- Final build artifacts
- These files are meant for your consumption and are placed within the target directory.
- Intermediate build artifacts
- These are internal files to the jank build system and are generally not consumed directly. They are stored within the build directory.
Example trees
Leiningen project
For a Leiningen project, json-query, an example tree with a debug build looks like this:
target/
├── debug
│ ├── _cache
│ │ └── json-query
│ │ ├── x86_64-unknown-linux-gnu-211eea4be7942af843e902b894e93b6cc665b2aa6943f36f5bca4ddde080972b
│ │ │ └── json_query
│ │ │ └── main.o
│ │ └── x86_64-unknown-linux-gnu-5e3948b46dee3bb4f17fc699e821182481fb5465155bdbc359678f7e6a8db9da
│ │ └── json_query
│ │ └── main.o
│ └── json-query
└── stale
└── leiningen.core.classpath.extract-native-dependencies
Note that there are two builds of json-query, with two different hashes. This
happens when your compiler flags change.
Also note that the final executable is at target/debug/json-query.
Direct jank invocation
If you invoke jank directly to compile code, you’ll still have a target and
build directory created for you. Here’s an example of compiling a local
hello.jank.
.
├── hello.jank
└── target
├── a.out
└── _cache
└── x86_64-unknown-linux-gnu-deb515fb6fcde162c8cefee396e3090f348d5ec8e2d1a863abf325fa79682321
└── hello.o
Guide: Packaging a system library
Packaging a system library is generally very straight-forward. We need to identify these things:
- Does this library need any preprocessor defines? Generally, the answer is no.
- Where are the include headers stored?
- Where are the libraries stored?
- What are the libraries named?
That’s it! Fortunately, pkg-config handles this for most system libraries.
Before we write a build script, let’s first answer all four questions needed to
package sqlite3. If we invoke pkg-config and ask for both C flags and libs,
we’ll actually get an answer to every single question above.
❯ pkg-config --cflags --libs sqlite3
-I/nix/store/vyd6g9viqafhzr97dq8zsbksdf4w5avm-sqlite-3.51.2-dev/include -L/nix/store/yg1gv8db04ldrnmdhykq8zjqqg6pg5kd-sqlite-3.51.2/lib -lsqlite3
Note
If you see an error like
Package 'sqlite3' not found, make sure that you have sqlite3 installed.
Note that this is the output for my particular NixOS system. On a different
distro, like Arch, the output may simply be -lsqlite3 and nothing else. This is an
important thing to note about pkg-config. You cannot just invoke it on your
machine and then hard-code the results in your build script. You have to
actually invoke pkg-config in the build script, so that it can fetch the
correct flags for the user’s machine.
A note on -sys packages
When we wrap system packages, we end up creating a special -sys package. The
goal of -sys packages is just to make the system library available, without
providing any higher level Clojure-style wrapper for the API. This allows -sys
packages to be reused by different higher level abstraction libraries and it
separates the concerns of the libraries. In our example here, for sqlite3, we’re
writing the sqlite3-sys package.
If you’re creating a -sys package, please consider adding it to the jank commons.
Writing a small package
Our sqlite3-sys package only needs two things:
- A
project.clj, defining how the package will be named and versioned - A
jank-build.bb, defining how the package will be “built”
For us, “building” just means running pkg-config to answer our four questions.
Here’s an example project.clj:
(defproject org.jank-lang.guide/sqlite3-sys "2026.07-1"
:description "Raw package for sqlite3."
:license {:name "MPL 2.0"
:url "https://www.mozilla.org/en-US/MPL/2.0/"}
:plugins [[org.jank-lang/lein-jank "2026.07-1"]]
:middleware [leiningen.jank/middleware]
:build-dependencies [[org.jank-lang.commons/jank-build-pkg-config "2026.06-1"]])
Note that we add :build-dependencies so that we can grab
jank-build-pkg-config. This is a helper for the jank-build.bb we’re going to
write. It’s going to call pkg-config for us and then turn the output of that
into jank build system directives. Here’s our jank-build.bb:
(require '[jank.build.pkg-config :refer [pkg-config]])
(pkg-config "sqlite3")
That’s it! Now, if we wanted to use this package locally, we could install it
with lein install and then add it as a dependency to another project.
❯ lein install
Created /home/jeaye/projects/sqlite3-sys/target/sqlite3-sys-2026.07-1.jar
Wrote /home/jeaye/projects/sqlite3-sys/pom.xml
Installed jar and pom into local repo.
Using the new package
Let’s create a new project and use our sqlite3-sys library.
❯ lein new org.jank-lang/jank hello-sqlite3
Generating a project called hello-sqlite3 based on the 'jank' template.
Then we need to add our dependency to the project.clj:
:dependencies [[org.jank-lang.guide/sqlite3-sys "2026.07-1"]]
And we need to call sqlite3 from our jank code:
(ns hello-sqlite3.main
(:include "sqlite3.h"))
(defn -main [& args]
(println (cpp/sqlite3_libversion)))
Now, when we try to run our program, the jank build system will find the
sqlite3-sys package, extract it, run the jank-build.bb script, and propagate
the pkg-config flags up to our jank invocation.
❯ lein run
Extracting [org.jank-lang.guide/sqlite3-sys 2026.07-1]
Compiling [org.jank-lang.guide/sqlite3-sys 2026.07-1]
3.51.2
# Future runs won't need to extract/build anything.
❯ lein run
3.51.2
Note that your sqlite3 version may be different from mine here. That’s ok.
Note
If you see an error like
Failed to find library 'sqlite3', make sure that you have sqlite3 installed. If you’re on macOS, make sure that yourPKG_CONFIG_PATHcan find the homebrew config for sqlite3.export PKG_CONFIG_PATH="/opt/homebrew/opt/sqlite/lib/pkgconfig:$PKG_CONFIG_PATH"
Summary
Packaging a system library for jank involves answering the four key questions.
Tools like pkg-config can do a lot of this for us! Some packages don’t have
pkg-config entries, which can make this work more manual. Other packages have
their own version of pkg-config, like curl-config, which accept similar
flags. Take a look at the pkg-config build script helper
here
for a peek behind the scenes of what we used for this guide.
Also, take a look at the official sqlite3-sys jank commons package, since it looks just like the one we made here!
Guide: Packaging a source library
jank can package libraries which need building from source. There is no limitation on the build system used, such as CMake, Automake, GNU Make, Scons, etc. The jank build system can work with any build system, since we just invoke it externally.
Note
If you can package a library using an installed system package and
pkg-config, prefer that. It’s much simpler, more efficient, and more portable than building from source.
Before continuing, please read the guide on packaging system libraries, since it teaches the foundational knowledge of how packaging works with jank’s build system.
Hello raylib
For this guide, we’re going to package
raylib from source. Although raylib has a
package in Nix and Homebrew, there is no Ubuntu/Debian package, which makes the
pkg-config approach far less portable. Instead, we’ll build raylib from
source.
A note on -sys packages
Even when we build from source, the pattern around -sys packages applies, so
be sure to read about that here.
Building raylib manually
To start with, let’s make sure we can build raylib manually. Then we’ll deal
with hooking that same flow into the jank build system. The first step in
building raylib is to just download it. For your package, it likely makes sense
to add raylib as a git submodule. You could also just clone it or download a
release from Github, as long as you have a raylib-sys directory for your
package with a raylib directory inside, which has the actual raylib
repository.
In our raylib directory, we’ll make our own build directory, just like the
jank build system’s build directory.
❯ cd raylib
❯ mkdir build
mkdir: created directory 'build'
❯ cd build
❯ cmake .. -DBUILD_EXAMPLES=off -DBUILD_SHARED_LIBS=on
<CMake configure output>
❯ make
<Make output>
# On macOS, you'll see libraylib.dylib instead!
❯ ls raylib/libraylib.*
raylib/libraylib.so
❯ cd ../../
❯ rm -r raylib/build
That wasn’t so bad! Now we just need to do the same from our jank-build.bb.
Packaging raylib
Let’s add our basic project:
(defproject org.jank-lang.guide/raylib-sys "2026.07-1"
:description "Raw package for raylib."
:license {:name "MPL 2.0"
:url "https://www.mozilla.org/en-US/MPL/2.0/"}
:plugins [[org.jank-lang/lein-jank "2026.07-1"]]
:middleware [leiningen.jank/middleware]
:build-dependencies [[org.jank-lang.commons/jank-build-cmake "2026.06-6"]]
:verbatim-paths ["raylib"])
Note the jank-build-cmake helper in the :build-dependencies. This is going
to do the heavy lifting for us. Also note the :verbatim-paths, which tells
Leiningen that we want the entire raylib directory to be included in our
raylib-sys package.
Now we just need our jank-build.bb.
;; Part 1.
(require '[babashka.fs :as fs]
'[jank.build.cmake :as cmake])
;; Part 2.
(let [out-dir (:out-dir *input*)
src-dir (fs/path (:src-dir *input*) "raylib")
input (assoc *input* :src-dir src-dir)]
(cmake/build input {:defines {"BUILD_EXAMPLES" false}})
;; Part 3.
(println (str "jank-build::include-dir=" (fs/path out-dir "include")))
(println (str "jank-build::link-dir=" (fs/path out-dir "lib")))
(println (str "jank-build::link-dir=" (fs/path out-dir "lib64")))
(println (str "jank-build::link-library=" "raylib")))
We’ll go over this part by part. Firstly, we require babashka.fs so we can do
some path manipulation. This namespace is available to all Babashka programs
without needing to add a dependency. Then we require jank.build.cmake, which
we added to our :build-dependencies above.
In part two, we invoke the jank.build.cmake helper, but we change the source
directory to be the nested raylib directory. The CMake helper expects the
CMakeLists.txt file to be within the source directory. The CMake helper will
automatically handle building the project in the build directory and installing
the final artifacts to the output directory.
In part three, we just print the necessary directives to tell the jank build system where to find headers, libraries, and which libraries to link.
That’s it! Now, if we wanted to use this package locally, we could install it
with lein install and then add it as a dependency to another project.
❯ lein install
Created /home/jeaye/projects/raylib-sys/target/raylib-sys-2026.07-1.jar
Wrote /home/jeaye/projects/raylib-sys/pom.xml
Installed jar and pom into local repo.
Using the new package
Let’s create a new project and use our raylib-sys library.
❯ lein new org.jank-lang/jank hello-raylib
Generating a project called hello-raylib based on the 'jank' template.
Then we need to add our dependency to the project.clj:
:dependencies [[org.jank-lang.guide/raylib-sys "2026.07-1"]]
Before we add any raylib code, let’s try to run our project and make sure everything builds correctly.
❯ lein run
Extracting [org.jank-lang.guide/raylib-sys 2026.07-1]
Compiling [org.jank-lang.guide/raylib-sys 2026.07-1]
Hello, world!
Nice! Let’s take a look at the generated output directory, to see what’s inside. Note that your directory name may be different, due to the hashing, but it’ll be in the same place:
❯ ls target/debug/_cache/raylib-sys-2026.07-1-out-cv799vKey7U76tllczT0Hw/
include jank-build-cache.txt jank-build-fingerprint.txt lib64
Our headers will be in the include directory and, in this case, our libraries
will be in lib64. These may end up being named different things on your
machine, too. That’s the flexibility of the jank build system: all of this is
handled by CMake and then the jank build system just connects the dots.
Speaking of which, let’s wrap this up by writing some raylib code!
(ns hello-raylib.main
(:include "raylib.h"))
(defn -main [& args]
(cpp/InitWindow 200 100 "raylib demo")
(cpp/SetTargetFPS 60)
(while (cpp/! (cpp/WindowShouldClose))
(let [time (cpp/GetTime)
color (cpp/ColorFromHSV (mod (* 100.0 time) 360.0) 0.5 0.5)]
(cpp/BeginDrawing)
(cpp/ClearBackground cpp/RAYWHITE)
(cpp/DrawText "Hello jank!" 50 30 20 color)
(cpp/EndDrawing)))
(cpp/CloseWindow))
Now you should be able to run this and see a raylib window rendering some colored text.
❯ lein run
INFO: Initializing raylib 6.1-dev
INFO: Platform backend: DESKTOP (GLFW)
<bunch of other raylib output>
Summary
Packaging a source library for jank involves answering the same four key questions as with system libraries. On top of that, we just need to utilize our source directory, build directory, and out directory with the project’s build system in order to put the files where jank can use them. Take a look at the CMake build script helper here for a peek behind the scenes of what we used for this guide.
Also, take a look at the official raylib-sys jank commons package, since it looks just like the one we made here!
Differences from Clojure
jank is meant to be Clojure, but Clojure itself has no specification. There are differences between Clojure JVM, ClojureScript, Clojure CLR, ClojureDart, and others. Part of being a Clojure is embracing one’s host and being transparent about it. This is where most of the differences come into play.
jank does not try to hide its C++ host. That would defeat the point of being Clojure.
Command line
- You will find no Clojure CLI
-X:foosyntax here - When using
jank runorjank run-main,--is needed to separate args for jank from args for your program- Example:
jank -I include run test.jank -- a b c
- Example:
Parser
- No load operation for
data_readers.(cljc|jank)at start-up to extend supported tags - jank allows using reader conditionals in
.jankfiles as well - No automatic method to disable read by setting the
clojure.core/*read-eval*at start-up. The value can be overriden via abindingoperation
clojure.core
- Baked into the
jankbinary, not shipped separately - No nested
requiresupport (same as ClojureScript) - No
import (hash-map)returns a hash map, not an array mapagetis a special formasetis a macrokeywordis more strict about valid inputsfuturecan only forward exceptions which arestd::exceptionorobject_ref- Other exceptions will be forwarded as
"Unknown exception"
- Other exceptions will be forwarded as
future-cancelreturnsnil, not the result of the cancellationfuture-cancelled?always returnsfalseon macOS, since there is no reliable way to check this with pthread
Object model
- No stable boxes for small integers (the JVM pre-allocates
1,2,3, etc) persistent_stringis expected to be UTF-8- No records (yet)
- No protocols (yet)
- Sequences
- Support for in-place operations (
fresh-seq,next-in-place)
- Support for in-place operations (
Compilation model
- Source-only distribution
.ofiles found in JARs will not be used- Git deps are an exception here; if someone commits a .o file into a git dep on your module path, jank will load it
- jank uses the term “module path” instead of “class path”
- We don’t have
.classfiles - A module is backed by either a
.jankor.cljcsource file, optionally with a.ofile cached for quick loading
- We don’t have
Math
- Signed integer overflow is well defined
- Unsigned integer overflow is well defined
- Division by integer
0is undefined behavior - Division by floating point
0.0is well defined
Troubleshooting
jank is not yet stable. Chances are, you’re going to hit some crashes, bugs, leaks, or other issues. In this chapter, we’ll learn the following:
- How to check the health of your jank install
- How to get a stack trace for a jank crash
- Where to ask for help and report issues
- Other frequently asked questions
Checking jank’s health
Once jank is installed, you can query its health at any time. Here’s an example output of jank installed via Homebrew on macOS.
$ jank check-health
─ system ───────────────────────────────────────────────────────────────────────────────────────────
─ ✅ operating system: macos
─ ✅ default triple: arm64-apple-darwin25.0.0
─ jank install ─────────────────────────────────────────────────────────────────────────────────────
─ ✅ jank version: jank-0.1-768f8310ce0f3d61b01f2df0f0e66ab9c9df1984
─ ✅ jank resource dir: ../lib/jank/0.1
─ ✅ jank resolved resource dir: /opt/homebrew/Cellar/jank/0.1/bin/../lib/jank/0.1 (found)
─ ✅ jank user cache dir: /Users/jeaye/.cache/jank/arm64-apple-darwin25.0.0-f33ec85999b436c281e9fba631425b57189670f96ba3166f2d327cd1543b516d (found)
─ clang install ────────────────────────────────────────────────────────────────────────────────────
─ ⚠️ configured clang path: /Users/runner/work/jank/jank/compiler+runtime/build/llvm-install/usr/local/bin/clang++ (not found)
─ ✅ runtime clang path: /opt/homebrew/Cellar/jank/0.1/bin/../lib/jank/0.1/bin/clang++ (found)
─ ⚠️ configured clang resource dir: /Users/runner/work/jank/jank/compiler+runtime/build/llvm-install/usr/local/lib/clang/22 (not found)
─ ✅ runtime clang resource dir: /opt/homebrew/Cellar/jank/0.1/lib/jank/0.1/lib/clang/22 (found)
─ jank runtime ─────────────────────────────────────────────────────────────────────────────────────
─ ✅ jank runtime initialized
─ ✅ jank pch path: /Users/jeaye/.cache/jank/arm64-apple-darwin25.0.0-f33ec85999b436c281e9fba631425b57189670f96ba3166f2d327cd1543b516d (found)
─ ✅ jank can jit compile c++
─ ✅ jank can aot compile working binaries
─ support ──────────────────────────────────────────────────────────────────────────────────────────
If you're having issues with jank, please either ask the jank community on the Clojurians Slack or report the issue on Github.
─ Slack: https://clojurians.slack.com/archives/C03SRH97FDK
─ Github: https://github.com/jank-lang/jank
How to read the output
jank’s health check will provide essential information about the jank installation, Clang installation, and current system. In general, if you don’t see any ❌ then you’re good to go. However, for more subtle issues, you may need to look at the particular paths which jank has determined to ensure they match up with your expectations.
Either way, when you’re reporting a bug or submitting system information, including your health check output is very useful.
Printing jank’s IR or codegen
You can glimpse under the hood of jank and have it print out both the generated jank IR and the generated C++ code, separately. These are accomplished via environment variables.
JANK_PRINT_IR=1will print out formatted IR for each compiled jank function.JANK_PRINT_CODEGEN=1will print out formatted C++ code for each compiled function.
Note that not all evaluated code is compiled. If you want to be sure some code is compiled, wrap it in a function and call it.
How to get a stack trace
If jank is crashing, or your AOT compiled program is, you may be asked to provide a stack trace. In case you’re not familiar with how to do this, here’s a quick rundown for both Linux (gdb) and macOS (lldb).
Note
Getting a stack trace requires invoking a debugger with your jank command. If you’re using Leiningen to invoke jank, you can get the underlying command by passing
-vto Leiningen. For example,lein run -v.
Let’s say we’re trying to run jank run foo.jank.
Linux
Make sure you have gdb installed. This is likely already installed, but if
it’s not, it is definitely in your package manager’s repos and it’s likely just
called gdb. Once you have gdb, you can use the following.
$ gdb --args jank run foo.jank
> run
# Do whatever is needed to cause the crash.
> backtrace
# Copy this to share with others.
> quit
Note
If you want to break when an exception is thrown, use the
catch throwcommand in gdb before yourun.
macOS
On macOS, you should have lldb installed as part of your developer tools.
However, you can get newer versions from Homebrew as part of the llvm package.
$ lldb -- jank run foo.jank
> run
# Do whatever is needed to cause the crash.
> backtrace
# Copy this to share with others.
> quit
Note
If you want to break when an exception is thrown, use the
breakpoint set -E c++command in lldb before yourun.
Where to get help
Note
Before reaching out with questions, check the FAQ.
The jank community, which is the Clojure community, is known to be welcoming. You are encouraged to reach out if you have any issues. Firstly, drop into Slack and ask your questions or explain your problems there. This will often be all that you need. However, if you’d like to report an issue for us to work on, please create a Discussion first, on Github.
In either case, please explain to the best of your ability and try to reproduce any issues with the smallest amount of code possible. Most of the time, a jank bug can be reproduced in fewer than 5 lines of code, but it can take some effort to get there.
Frequently asked questions (FAQ)
Is jank compatible with Clojure?
Yes! jank is a Clojure dialect. Furthermore, I am working directly with the Clojure core team and jank is sponsored both by Nubank and Clojurists Together. That’s leads us to the next question, though.
What is a Clojure dialect?
Now this is the question. Let’s survey the landscape of Clojure dialects.
- ClojureScript
- Doesn’t have reified vars and
defjust create JS globals - Doesn’t have refs or software transactional memory (STM)
- Doesn’t have a character type
- Doesn’t have a ratio, big decimal, or big integer type
- Runs macros in a different compilation stage (in Clojure JVM)
- Supports a custom
#jsreader tag - And so on
- Doesn’t have reified vars and
- Clojure Dart
- Lazily initializes
def, to aid in tree shaking - Doesn’t have multi-methods
- Extends
catchsyntax to support stack traces - Runs macros in a different compilation stage (in Clojure JVM)
- Doesn’t have array maps
- Supports named parameters
- Supports a custom
#dartreader tag - And so on
- Lazily initializes
- Basilisp
- All numbers are unlimited precision
- Doesn’t have refs or software transactional memory (STM)
- Doesn’t have a character type
- Supports a custom
#pyreader tag - Python builtins are available under the special
python/namespace - And so on
- jank
- You can read the differences here
The same sorts of differences can be found for most Clojure dialects. A crucial reason that each Clojure dialect is different is that Clojure is designed to embrace its host runtime. By that I mean that Clojure JVM leans into the JVM. ClojureScript leans into JavaScript. Clojure Dart leans into the Dart world. In each of these, attributes and behaviors of the host runtime show transparently through the Clojure dialect. They’re not hidden. This, on the surface, makes Clojure dialects different, but actually it’s for this reason that they’re all more Clojure-like.
So what’s common across all of these? That’s not currently defined by the Clojure core team. But the common space across all of these is where jank aims to meet. A good mantra for this is “If it works in Clojure JVM and ClojureScript, it should work in jank.”
For more info on the differences between dialects and how they’re tracked, take a look at the clojure test suite, which is a jank-lead initiative to find unexpected discrepancies across dialects.
Why does jank have its own file type?
That’s what all Clojure dialects do. If you want code which can run on multiple
dialects, use a .cljc (Clojure Common) file with reader conditionals.
- Clojure JVM:
.clj - ClojureScript:
.cljs - Clojure CLR:
.cljr - Clojure Dart:
.cljd - Babashka:
.bb - Basilisp:
.lpy - jank:
.jank
How is jank’s memory managed?
The Clojure side of jank is garbage collected, using the Boehm GC (BDWGC).
However, any C++ interop uses normal C++ idioms, including RAII. For example, if
you stack allocate a C++ value which has a destructor, jank will ensure that
destructor runs at the end of the object’s scope. Also, you can use cpp/new
and cpp/delete or cpp/malloc and cpp/free, if you so desire.
Why does jank compile to C++?
jank is both a Clojure dialect and a C++ dialect, which is unique among most programming languages. In order for us to consider jank a C++ dialect, we must have excellent interop with C++, such as:
- Including C++ headers
- Instantiating templates
- Calling virtual member functions
- Throwing and catching exceptions
- Providing the same RAII destructor guarantees as C++
- In general being able to handle any normal C++ library
If we were to not target C++ for code generation and instead target something like LLVM IR directly, we would need to duplicate thousands upon thousands of lines of code from Clang in order to properly encode C++ semantics and ABI nuances in LLVM IR. We know, since we have done this. It was not worth it. Even when it worked, we would need to keep up with Clang as C++ evolved, we would need to deal with more portability issues, and, in general, we would need to build not only a Clojure compiler but a C++ compiler.
Clang is already a C++ compiler and it’s a better C++ compiler than we’re going to build, so instead we generate C++ and let Clang do what it does best. This same some huge benefits:
- The jank compiler is significantly simpler
- It’s possible to compile entire jank projects to just
.cppfiles and then feed that into a normal C++ build system. At that point, it’s just a C++ project - Inspecting the generated code is a breeze
The only real downside is worse compilation performance. This is a tradeoff we accept.
Why does Clojure not compile to Java then?
The Clojure JVM compiler compiles to JVM bytecode, rather than Java. It can do this because the JVM bytecode operates at the nearly semantic level of Java. Clojure can represent Java objects, virtual calls, exceptions, and so on, using JVM bytecode.
LLVM IR, on the other hand, is much, much lower level than C++. The only way to turn C++ semantics into LLVM IR is with a C++ compiler, which is why jank generates C++ and uses Clang to then turn the C++ into LLVM IR.
How do I redefine C++ types, values, and functions?
C++ does not support redefining types, values, functions, etc. That violates the one definition rule.
Clojure, on the other hand, allows redefining just about everything. That’s how we do REPL-driven development. When using Clojure JVM, you can’t jump into Java and start redefining Java classes. That’s not how Java works. But you can still do REPL-driven development. Similarly, in jank, you can’t jump into C++ and start redefining things. But you can do it the Clojure way.
For example, if you define a NEW C++ function, and you have a stable Clojure-side var which holds a function referring to your C++ function, you can then redefine what’s inside the var. That will work.
(cpp/raw "int my_fn1(){ return 1; }")
(defn my-fn []
(cpp/my_fn1))
(cpp/raw "int my_fn2(){ return 2; }") ; NEW fn
(defn my-fn [] ; Redefined
(cpp/my_fn2))
So, the Clojure side of things can act as a proxy into updated C++ code. This is actually how jank’s C++ code generation works. But this is not the recommended approach.
The recommended approach is to keep anything you’re redefining on the Clojure side. That will play very nicely with the REPL. Leave the C++ stuff to be static, if you can. However, if you need to update C++ stuff, consider adding a version to the symbol or namespace so that you’re always defining new things and not violating the one definition rule.
Reference
Errors
Lex
lex/unexpected-eof
This uncommon error usually means the source file contains corrupt Unicode. Normal unterminated lists or strings produce different errors.
Mitigations
Verify the integrity of the file before proceeding.
lex/expecting-whitespace
This happens when two forms that must be separated are written back to back without whitespace.
Mitigations
Add a space or newline between the two forms. If they are meant to be one token, rewrite them as a single valid token.
lex/invalid-unicode
This usually means the source file contains invalid or incomplete Unicode text. It is usually not a normal unfinished form.
Mitigations
Save the file as valid UTF-8. Then remove or retype the corrupted characters.
lex/incomplete-character
This happens when a character literal starts but no complete character value follows.
Mitigations
Complete the character literal. Examples include \a, \space, \newline. Otherwise remove the partial literal.
lex/invalid-number
This happens when a numeric literal is malformed. Common causes include mixed number syntax. They also include digits that do not match the base. Another cause is an unsupported base. Another cause is an unfinished literal.
Mitigations
Rewrite the literal using one valid number format. Make sure every required digit is present.
lex/invalid-ratio
This happens when a ratio literal is not written as two integers separated by /. Decimal points are not allowed. Scientific notation is not allowed. Arbitrary-radix notation is not allowed. Non-integer denominators are not allowed.
Mitigations
Write the ratio as numerator/denominator with integers on both sides. If you want a decimal value, use a decimal number instead.
lex/invalid-symbol
This most often means the symbol starts with /. That is not allowed.
Mitigations
Either remove the leading / or add a namespace before it, such as my.ns/name.
lex/invalid-keyword
This happens when a keyword literal is missing its name or has an invalid namespace or name shape. Common cases include :, ::, :/foo, ::/foo. Another common case is too many leading colons.
Mitigations
Use one of these shapes: :name, :ns/name, ::name, ::alias/name.
lex/unterminated-string
A string literal was started with " but never closed.
Mitigations
Either add the missing closing quote or remove the accidental opening quote.
lex/unexpected-character
This happens when the source contains a character that cannot start any valid jank token in that position.
Mitigations
Either remove or replace the stray character. If it was intentional, put it inside a string or another valid form.
lex/internal-failure
This indicates an internal problem while reading source code. It is not a normal syntax mistake.
Mitigations
Try to simplify the source. Then report it as a jank bug.
Parse
parse/invalid-unicode
This happens when a character literal or Unicode escape does not represent a valid Unicode character.
Mitigations
Use a valid character literal or a valid Unicode escape.
parse/invalid-character
This happens when a character literal is not one of the supported character forms.
Mitigations
Use a single character. You can also use a supported named character such as \newline or \space. A valid Unicode character form also works.
parse/invalid-string-escape
This happens when jank does not recognize a string escape sequence. It can also happen for a malformed Unicode escape inside a string literal.
Mitigations
Use a supported escape sequence such as \n, \t, \\, \". You can also use
a valid \uXXXX escape.
parse/unexpected-closing-character
This happens when one of ), ], } appears without a matching opening delimiter.
Mitigations
Either remove the extra closing delimiter or add the matching opening delimiter earlier in the form.
parse/unterminated-list
A list was started with ( but was not closed with ).
Mitigations
Add the missing ) to close the list.
parse/unterminated-vector
A vector was started with [ but was not closed with ].
Mitigations
Add the missing ] to close the vector.
parse/unterminated-map
A map was started with { but was not closed with }.
Mitigations
Add the missing } to close the map.
parse/unterminated-set
A set was started with #{ but was not closed with }.
Mitigations
Add the missing } to close the set.
parse/odd-entries-in-map
This happens when a map literal has a key without a corresponding value. For example:
{:a }
Mitigations
Make sure every key in the map is followed by a value.
parse/duplicate-keys-in-map
This happens when the same key appears more than once in a map literal. For example:
{:a 1
:a 2}
Mitigations
Either remove the duplicate key or rename it so each key appears only once.
parse/duplicate-items-in-set
This happens when the same item appears more than once in a set literal. For example:
#{:foo :foo}
This also applies to repeated forms such as:
#{(rand) (rand)}
Mitigations
Remove the duplicate item so each set element appears only once. If you need separate computed values, bind them in a let first and build the set from those locals.
(let [a (rand)
b (rand)]
#{a b})
parse/invalid-quote
This usually means a quote form like ' is missing the value it should quote.
Mitigations
Place a form immediately after the quote.
parse/invalid-meta-hint-value
This happens when the form after ^ is missing or is not a valid metadata value.
Mitigations
Use a valid metadata form after ^. A keyword or map works.
parse/invalid-meta-hint-target
This happens when metadata is attached to a missing form or to a value that cannot carry metadata. Some values, such as numbers and strings, do not support metadata.
Mitigations
Put the metadata before a valid target form that supports metadata. If needed, wrap the value in something that can carry metadata, such as an atom or a container.
parse/unsupported-reader-macro
This happens when # is followed by a reader macro that jank does not support.
Mitigations
Either use a supported reader macro or rewrite the form without that reader syntax.
parse/nested-shorthand-function
This happens when one shorthand anonymous function form is placed inside another #() form. The %1-style placeholders would be ambiguous between the two functions.
Mitigations
Either rewrite the nested shorthand function as a normal fn or move it outside the outer #() form.
parse/invalid-shorthand-function-parameter
This happens when a % parameter inside #() is not one of the supported forms.
Mitigations
Use one of the supported forms: %, %&, %n where n is 1 or greater. % means %1. %& means all parameters as a sequence.
parse/invalid-reader-var
This happens when #' is not followed by a symbol. For example, #'foo is valid if foo resolves in the current namespace. #'123 is not valid.
Mitigations
Put a symbol immediately after #'.
parse/invalid-reader-comment
This happens when #_ is not followed by a form to skip.
Mitigations
Either place a form immediately after #_ or remove the reader comment. For example:
(let [a (get-a!)]
#_(println :a a)
a)
parse/invalid-reader-conditional
This happens when a reader conditional such as #? is not allowed in the current context. It can also happen when it is not followed by a list. Malformed feature or value pairs can trigger it too.
Mitigations
Use a valid reader conditional list with keyword features and matching forms. Enable reader conditionals if needed. A valid reader conditional looks like this:
#?(:jank (println "hi jank!")
:default (println "hi other clojure!"))
parse/invalid-reader-splice
This happens when #?@ is used where splicing is not allowed. It can also happen when the selected form is not a sequence.
Mitigations
Use #?@ only in a place where splicing is allowed. Make sure the selected form is a sequence. A valid reader conditional splice looks like this:
(def v [1 2 #?@(:jank [3 4]
:default [])])
parse/invalid-reader-gensym
This happens when a gensym-style symbol ending in # is used outside a syntax-quoted form.
Mitigations
Use gensym literals only inside syntax quote. Otherwise replace them with an ordinary symbol.
parse/invalid-reader-symbolic-value
This happens when a reader symbolic value is not one of the supported forms. Supported forms include ##Inf, ##-Inf, ##NaN. It may also appear for unsupported built-in tagged-reader cases.
Mitigations
Use one of the supported symbolic values. Otherwise switch to a supported tagged literal form.
parse/invalid-reader-tag-value
This happens when a tagged literal is missing its following form. It can also happen when the following form has the wrong kind for that tag.
Mitigations
Place a valid form immediately after the tag. Make sure it matches what the tag expects.
parse/invalid-regex
This happens when a regex literal contains an invalid regular expression.
Mitigations
Fix the regex pattern so it is valid.
parse/invalid-uuid
This happens when #uuid is not followed by a valid UUID string.
Mitigations
Use a valid UUID string after #uuid.
parse/invalid-inst
This happens when #inst is not followed by a supported date or time string.
Mitigations
Use a supported date or time string after #inst.
parse/invalid-syntax-quote
This usually means a syntax quote form like ` is missing the value it should quote.
Mitigations
Place a form immediately after the syntax quote.
parse/invalid-syntax-unquote
This happens when an unquote form such as ~ is missing its value or is otherwise used incorrectly.
Mitigations
Use ~ only with a following form in a valid syntax-quote context.
parse/invalid-syntax-unquote-splice
This happens when ~@ is used somewhere syntax-quote splicing is not allowed.
Mitigations
Use ~@ only inside a sequence position where splicing makes sense.
parse/invalid-reader-deref
This happens when @ is not followed by a form to dereference.
Mitigations
Place a form immediately after @.
parse/invalid-ratio
This most often means the ratio’s denominator is zero.
Mitigations
Use a non-zero denominator.
parse/invalid-keyword
This happens when a keyword form is lexically valid but still cannot be resolved or interned as a keyword.
Mitigations
Use a valid keyword name. For auto-resolved keywords, make sure the namespace or alias exists.
parse/invalid-data-reader
This happens when tagged-literal data reader configuration is invalid. For example, *data-readers* may not be a map. A data reader may also not be a function.
Mitigations
Fix the tagged-literal reader configuration before reading the form again.
parse/internal-failure
This indicates an internal problem while building forms from tokens. It is not a normal syntax mistake.
Mitigations
Try to simplify the source. Then report it as a jank bug.
Analyze
analyze/invalid-case
This happens when the low-level case* form is used directly with missing arguments or with arguments of the wrong type.
Mitigations
Prefer the normal case macro instead of calling case* directly. If you do use case*, make sure each required argument is present and has the expected type.
analyze/invalid-def
This happens when a def form is malformed. It may be missing its name. It may have extra forms. The name may not be an unqualified symbol.
Mitigations
Write def as (def name value) or (def name). Make sure name is an unqualified symbol.
analyze/invalid-fn
This happens when an fn form has an invalid overall shape. Malformed arities can trigger it. Duplicate arities can trigger it too. Invalid variadic usage can also trigger it.
Mitigations
Rewrite the function so each arity has a valid parameter vector and body. If you use a variadic arity, make sure it is well formed. fn comes in two forms.
Single arity
(fn foo [a1 a2]
(println a1 a2))
Multi-arity
(fn foo
([a1]
(println a1))
([a1 a2]
(println a1 a2)))
analyze/invalid-fn-parameters
This happens when a function parameter vector is malformed. Common cases include non-symbol parameters. Qualified parameter names can also trigger it. Incorrect & usage can trigger it too.
Mitigations
Use a parameter vector of unqualified symbols. Use & only once. Put it immediately before the variadic parameter name.
analyze/invalid-recur-position
This happens when recur is used outside the tail position of a fn or loop.
Mitigations
Move the recur call to the tail position of the enclosing fn or loop.
analyze/invalid-recur-from-try
This happens when recur appears within a try path. The same rule applies inside catch and finally.
Mitigations
Restructure the code so the recur happens outside the try form.
analyze/invalid-recur-args
This happens when recur is given the wrong number of arguments for the enclosing fn or loop.
Mitigations
Pass exactly one new value for each local that the enclosing fn arity or loop binds.
analyze/invalid-let
This happens when a let binding form is malformed. The binding form may be missing. It may not be a vector. It may have an odd number of entries. The binding names may be invalid.
Mitigations
Use a binding vector with pairs of unqualified symbol names and values.
analyze/invalid-letfn
This happens when a letfn* binding form is malformed. The bindings may be missing. They may be uneven. The names may be invalid. The bound values may not be functions.
Mitigations
Use a binding vector of function-name and function-value pairs. Make sure each name is an unqualified symbol.
analyze/invalid-if
This happens when an if form is malformed. It may be missing the then branch. It may also have too many forms.
Mitigations
Write if as (if test then) or (if test then else). If you need multiple forms in the then branch or the else branch, wrap them in a do form.
analyze/invalid-quote
This happens when quote is given anything other than exactly one form.
Mitigations
Pass exactly one form to quote.
analyze/invalid-var-reference
This happens when (var ...) is malformed. It may have the wrong number of arguments. The argument may not be a symbol.
Mitigations
Use (var some-symbol) with exactly one symbol argument.
analyze/unresolved-var
This happens when (var some-symbol) refers to a var that jank cannot resolve. The same error can appear for #'some-symbol.
Mitigations
Make sure the var exists. Check the spelling. Make sure it is available in the current namespace or use a fully qualified name. You can write a fully qualified var reference as #'some.ns/foo.
analyze/unresolved-symbol
This happens when a symbol is not a local. It may also not be a named recursion target. In that case jank cannot resolve it to a known var or supported C++ global.
Mitigations
Check the spelling. Check the namespace. Check the scope. Make sure any required namespace has been loaded or aliased.
analyze/macro-expansion-exception
This happens when a macro throws an exception or another error while jank expands it.
Mitigations
Inspect the macro call. Inspect the macro itself. Reduce the input to a smaller example and fix the failing expansion path.
analyze/invalid-cpp-operator-call
This happens when a C++ operator form is called with the wrong number of arguments. It can also happen when the operand types do not support that operator.
Mitigations
Call the operator with the required number of arguments. Make sure the operand types support it.
analyze/invalid-cpp-constructor-call
This happens when a C++ constructor call cannot be formed. The target may not be constructible. Required template information may also be missing.
Mitigations
Make sure you are constructing a valid concrete type. Make sure the constructor arguments match an available constructor.
analyze/invalid-cpp-member-call
This happens when a .member call is missing its target object. It can also happen when the target type does not have a matching callable member function.
Mitigations
Pass the target object first. Make sure the member function exists for that type and argument list. The member function must be public.
analyze/invalid-cpp-capture
This happens when a local native C++ value is captured in a context where it cannot be safely copied.
Mitigations
Avoid capturing that value directly. If possible, change the code so the captured value has a copyable type. You can also capture a pointer to it by using cpp/& in a let. Verify that the pointed-to value will outlive the closure.
analyze/invalid-cpp-position
This happens when a C++ operator or member-style form is used as a plain value instead of being called directly.
Mitigations
Call the form directly in place, such as (.member obj ...). Do not try to pass it around as a value.
analyze/mismatched-if-types
This happens when the then branch and else branch of an if produce native C++ values with incompatible types. jank needs both branches to produce a compatible result type for the full expression.
This usually is not a problem for ordinary jank objects. It most often appears when native C++ values are involved.
Mitigations
Change the branches so they produce compatible types. jank can handle some implicit conversions and trait conversions automatically. If that is not enough, wrap both branches in a common type such as std::variant.
analyze/invalid-cpp-call
This happens when jank cannot form a valid indirect or generic C++ call. That can include calls through function pointers. It can also include calls through functors or other callable values.
Mitigations
Make sure the target is actually callable. Make sure the argument list matches an available call operator or function type.
analyze/invalid-cpp-conversion
This happens when there’s an unsupported implicit conversion needed between two
types. This can happen between two native types, such as when you try to use a
native value for an if condition and it can’t implicitly convert to bool.
This can also happen for unsupported trait conversions. In that case, a jank object is expected and a native value is provided, but there is no conversion trait which allows jank to get from the native value to the jank object.
Mitigations
If you’re implicitly casting between two native types, you’ll need to sort out how that can be done more explicitly.
If you’re converting a native value into a jank object, you have a few options.
- You can wrap the native value in an opaque box, if its lifetime allows that.
- You can define a conversion trait for your native type.
- You can also build a jank object from the native value’s members.
For more information on all of this see here.
analyze/invalid-cpp-symbol
This happens when a C++ symbol form is malformed. It may refer to a namespace as a value. It may use an invalid dotted name. It may use constructor syntax on something that is not a type.
Mitigations
Rewrite the symbol as a valid C++ namespace, type, function, member name. Only use constructor syntax with actual types.
analyze/unresolved-cpp-symbol
This happens when a referenced C++ name cannot be resolved to any known symbol.
Mitigations
Check the spelling. Check the qualification. Check the includes. Check the surrounding scope. Make sure the C++ name is available where it is used.
analyze/invalid-cpp-raw
This happens when cpp/raw is missing its string argument. It can also happen when it has extra arguments or when it is given something other than a string literal of C++ code.
Mitigations
Call cpp/raw with exactly one string literal containing the raw C++ code.
analyze/invalid-cpp-type
This happens when a form that requires a C++ type is malformed or when it receives a value form instead of a type form.
Mitigations
Pass a valid C++ type form in that position.
analyze/invalid-cpp-type-position
This happens when a C++ type name is used where jank expects a value instead of a type.
Mitigations
Move the type form to a type position. Otherwise use a value-producing form instead.
Types cannot be used as first-class values in jank because there is no runtime reflection.
analyze/invalid-cpp-dsl
This happens when a #cpp type form is malformed. Common cases include invalid modifiers. They also include invalid template usage. Malformed function type forms can trigger it. Malformed array type forms can trigger it. Using a value form where a type form is required can trigger it too.
Mitigations
Rewrite the #cpp form so each part is a valid C++ type description. Make sure each part appears in the correct position.
For full documentation on the C++ DSL, see here.
analyze/invalid-cpp-value
This happens when a resolved C++ form is not usable as a value in the current position.
Mitigations
Make sure the form names an actual value. Otherwise rewrite it as the correct type form or call form for that position.
analyze/invalid-cpp-cast
This happens when cpp/cast is malformed or when the requested cast is not allowed for the source type and target type.
Mitigations
Call cpp/cast with exactly a target type and a value. Choose a conversion that is valid for those types.
cpp/cast behaves like C++ static_cast. If you need a less restricted cast, use cpp/unsafe-cast.
analyze/invalid-cpp-unsafe-cast
This happens when cpp/unsafe-cast is malformed or when even an unsafe C-style cast cannot be formed for the given types.
Mitigations
Call cpp/unsafe-cast with exactly a target type and a value. Make sure the cast is actually possible.
analyze/invalid-cpp-box
This happens when cpp/box is malformed. It can also happen when it receives the wrong number of arguments or when the value cannot be boxed this way.
cpp/box must receive a raw pointer value. The pointed-to value must outlive the returned opaque box.
Mitigations
Call cpp/box with exactly one suitable pointer value.
analyze/invalid-cpp-unbox
This happens when cpp/unbox is malformed. It can also happen when it receives the wrong number of arguments. Using a non-pointer target type triggers it too. Applying it to something that is not a boxed jank object also triggers it.
Mitigations
Call cpp/unbox with a pointer target type and a compatible boxed jank value.
analyze/invalid-cpp-new
This happens when cpp/new is missing the type it should allocate.
Mitigations
Pass the type to allocate, followed by any constructor arguments.
analyze/invalid-cpp-delete
This happens when cpp/delete is malformed. It can also happen when it receives the wrong number of arguments or when the value is not a pointer.
Mitigations
Call cpp/delete with exactly one pointer value. The pointed-to value must have been allocated via cpp/new.
analyze/invalid-cpp-member-access
This happens when a .-member form is malformed. It can also happen when the target object is missing. Extra arguments can trigger it too. A missing field on the target type can also trigger it.
Mitigations
Pass exactly one target object. Make sure the named member exists on that type. Make sure it is accessible. The member must be public.
analyze/internal-failure
This indicates an internal analyzer problem or inconsistent internal state. It is not a normal code mistake.
Mitigations
Try to simplify the source. Then report it as a jank bug.
Codegen
codegen/internal-failure
This indicates an internal code generation bug rather than a normal user-facing code mistake.
Mitigations
Try to simplify the source. Then report it as a jank bug.
AOT
aot/unresolved-main
This happens when jank is building an executable and the target module does not define -main.
Mitigations
Define -main in the target module. Make sure you are compiling the module that
is meant to be the program entrypoint. A normal -main looks like this:
(ns my-app.main)
(defn -main [& args]
)
Note that it’s defn -main and not defn- main.
aot/internal-failure
This indicates an internal AOT build-state problem.
Mitigations
Try to simplify the source. Then report it as a jank bug.
Runtime
runtime/module-not-found
This happens when jank cannot find the requested module. Common causes include a missing dependency, a wrong module path, or asking for a module that is not available in the current build.
Mitigations
Make sure the module exists. Then check that the module path, build mode, and current dependencies make it available to jank.
Note that it’s very common to have a module name with a - in it, but the
corresponding file system path will use _ instead. So my-app.foo-bar becomes
my_app/foo_bar.jank on the file system.
runtime/module-binary-without-source
This error happens when a required module has no source file on the module path, but it does have an object file. jank will refuse to load object files for modules which don’t have a corresponding source, since jank is a source-first language.
Mitigations
Verify that the module’s source is present on the module path.
runtime/unable-to-open-file
This happens when jank cannot open or read a required file. Common causes include a missing file, a bad path, or a filesystem access problem.
Mitigations
Check that the file exists and is readable. Then verify that the path being used is the one you intended.
runtime/invalid-cpp-eval
This happens when runtime C++ evaluation fails. The provided C++ may be invalid or the JIT toolchain may have failed while compiling it.
Mitigations
Check the accompanying compiler output first. Then fix the C++ source or the local Clang and JIT setup before trying again.
runtime/unable-to-load-module
This happens when jank finds a module but fails while loading or initializing it.
Mitigations
Read the accompanying error details closely. Then fix the exception or nested error that occurred during module loading.
runtime/invalid-unbox
This happens when a boxed foreign value is unboxed as the wrong type.
Mitigations
Make sure the unbox operation uses the same type that was originally boxed. The jank error output should provide you with source information, as well as the expected type for the given box.
runtime/non-metadatable-value
This happens when metadata is applied to a value type that cannot carry metadata.
Mitigations
Only use metadata with values that support it. If needed, wrap the value in a metadatable form first, such as an atom.
runtime/invalid-referred-global-symbol
This happens when a referred C++ global name is malformed.
Mitigations
Use a simple symbol name for the referred C++ global. Avoid namespace-qualified names in that position.
runtime/invalid-referred-global-rename
This happens when a requested local rename for a referred C++ global would conflict with an existing name in the namespace.
Mitigations
Choose a different local name that does not collide with an existing var.
runtime/unsupported-behavior
This happens when an operation is used on a value whose type does not support that behavior. Common examples include sequence operations, associative lookup, indexed access, comparison, or numeric conversion on the wrong kind of value.
Mitigations
Use the operation only with values that support it. If needed, convert the value to a compatible type first.
runtime/static-feature-disabled
This happens when a dynamic-only feature is used in a static runtime build.
Mitigations
Avoid that feature in a static runtime, or switch to a dynamic runtime if you need it.
runtime/uncaught-exception
This error happens when an exception, of some kind, was thrown and then not caught by your program’s code. The jank runtime caught the exception and then surfaced the stack trace, along with the exception message.
Mitigations
Inspect the stack trace to find where the exception was thrown and whether or
not that was intended. If the exception was intended, add a try to your
program so that you can catch the exception yourself.
runtime/internal-failure
This indicates an internal runtime problem or a low-level loading failure rather than a normal language mistake. Current uses include impossible loader states, missing internal artifacts, jar-reading failures, and unsupported generated arities.
Mitigations
Try again from a clean state if the problem involves loading or compilation artifacts. If it still happens, report it as a jank bug with the exact command and input.
System
system/clang-executable-not-found
This happens when jank cannot find a suitable Clang executable with the required major version.
Mitigations
jank should be installed with its own Clang version, so this error indicates an issue with the jank install. The first step to troubleshoot this would be a health check.
system/failure
This covers external tool or environment failures. Common causes include missing Clang resources, missing SDK or Xcode tooling, missing precompiled-header inputs, or Clang itself failing during a build step.
Mitigations
Read the accompanying error output closely. Then fix the reported system or toolchain problem before trying again.
Internal
internal/failure
This is a generic fallback for internal failures. It usually indicates a jank bug or an unexpected internal state.
Mitigations
Try to simplify the source. Then report it as a jank bug.
Developing jank
This documentation is for the compiler hackers working on jank itself. Anything documented here is not intended to be stable unless otherwise stated, since these are the internals of jank’s compiler and runtime. jank aims to offer stable language syntax and semantics, and a stable C API, while leaving the underlying C++ API unburdened with backward compatibility.
IR reference
jank has its own custom SSA-based intermediate representation (IR), which it uses for all compiled jank code. Even though jank is tightly coupled with Clang/LLVM, using LLVM IR directly rules out large classes of optimizations, since LLVM IR is significantly lower level than the semantics of Clojure. However, jank’s IR is exactly at the level of Clojure’s semantics, which allows us to optimize things like var derefs, persistent data structures, transients, closure captures, and so on.
Compilation model
Every compiled function gets turned into jank IR after it’s analyzed into the jank abstract syntax tree (AST). From there, we optimize the IR and then finally generate C++ code from the IR, which we give to Clang to compile into LLVM IR.
%% Enable JavaScript to see this diagram rendered nicely. :)
flowchart TD
source[jank source] --> ast[AST]
ast --> jank-ir[jank IR]
jank-ir --> cpp[Generated C++]
cpp --> llvm-ir[LLVM IR]
Overview
jank’s IR is represented as an IR module, at the highest level. IR modules contain the following:
- Module name
- Lifted vars
- Lifted constants
- Functions
IR modules are stored in memory as C++ objects, but they can be rendered to Clojure data for easy debugging or for writing tests. Round trip serialization is not supported, since there’s more information that we store than we can reasonably render to Clojure data, including some Clang AST internals needed for C++ interop. As an example of some jank IR, here’s a simple jank function and its corresponding IR.
(defn greet [name]
(if (= "jeaye" name)
(println "Are you me?!")
(println (str "Hello, " name "!"))))
We can get the IR printed for each compiled function by setting
JANK_PRINT_IR=1 when we invoke jank.
{:name user_greet_1
:lifted-vars {clojure.core/println clojure_core_SLASH_println_5
clojure.core/str clojure_core_SLASH_str_8
clojure.core/= clojure_core_SLASH__EQ__3}
:lifted-constants {"!" const_7
"Are you me?!" const_4
"Hello, " const_6
"jeaye" const_2}
:functions [{:name user_greet_1_1
:blocks [{:name entry
:instructions [{:name greet :op :parameter :type "jank::runtime::object_ref"}
{:name name :op :parameter :type "jank::runtime::object_ref"}
{:name v3 :op :literal :value "jeaye" :type "jank::runtime::obj::persistent_string_ref"}
{:name v4 :op :var-deref :var clojure.core/= :type "jank::runtime::object_ref"}
{:name v5 :op :dynamic-call :fn v4 :args [v3 name] :type "jank::runtime::object_ref"}
{:name v7 :op :truthy :value v5 :type "bool"}
{:name v8 :op :branch :condition v7 :then if0 :else else1 :merge nil :shadow nil :type "void"}]}
{:name if0
:instructions [{:name v9 :op :literal :value "Are you me?!" :type "jank::runtime::obj::persistent_string_ref"}
{:name v10 :op :var-deref :var clojure.core/println :type "jank::runtime::object_ref"}
{:name v11 :op :dynamic-call :fn v10 :args [v9] :type "jank::runtime::object_ref"}
{:name v12 :op :ret :value v11 :type "jank::runtime::object_ref"}]}
{:name else1
:instructions [{:name v13 :op :literal :value "Hello, " :type "jank::runtime::obj::persistent_string_ref"}
{:name v14 :op :literal :value "!" :type "jank::runtime::obj::persistent_string_ref"}
{:name v15 :op :var-deref :var clojure.core/str :type "jank::runtime::object_ref"}
{:name v16 :op :dynamic-call :fn v15 :args [v13 name v14] :type "jank::runtime::object_ref"}
{:name v17 :op :var-deref :var clojure.core/println :type "jank::runtime::object_ref"}
{:name v18 :op :dynamic-call :fn v17 :args [v16] :type "jank::runtime::object_ref"}
{:name v19 :op :ret :value v18 :type "jank::runtime::object_ref"}]}]}]}
Modules have at least one function, where each function corresponds with a
single arity of a jank function. IR functions are broken into basic blocks,
which are a common fundamental principle in control flow graphs (CFGs). Each
basic block has exactly one terminator, which must be the last instruction in
the block. We start at the first block, which is generally called entry.
The remainder of this document will describe each IR instruction.
Data structures
literal
The literal instruction introduces a lifted constant into the scope. This
instruction is only used for boxed jank values, not unboxed C++ literals.
{:name v8 :op :literal :value 1 :type "jank::runtime::obj::integer_ref"}
persistent-list
The persistent-list instruction creates a list object, given the values
provided. This is generally only used for packing arguments to variadic
functions, since otherwise a list would just be a literal.
{:name v12 :op :persistent-list :values [v10 v11] :type "jank::runtime::obj::persistent_list_ref"}
persistent-vector
The persistent-vector instruction creates a vector object, given the values
provided. This is only used for vectors which aren’t literals.
{:name v3 :op :persistent-vector :values [v0 v1 v2] :type "jank::runtime::obj::persistent_vector_ref"}
persistent-array-map
The persistent-array-map instruction creates an array map object, given the values
provided. This is only used for maps which aren’t literals and only if they’re
small enough to fit in an array map. Otherwise, a hash map is used.
{:name v4 :op :persistent-array-map :values [[v1 v0] [v2 v3]] :type "jank::runtime::obj::persistent_array_map_ref"}
persistent-hash-map
The persistent-hash-map instruction creates a hash map object, given the values
provided. This is only used for maps which aren’t literals and only if they’re
too large to fit in an array map.
{:name v4 :op :persistent-hash-map :values [[v1 v0] [v2 v3]] :type "jank::runtime::obj::persistent_hash_map_ref"}
persistent-hash-set
The persistent-hash-set instruction creates a hash set object, given the values
provided. This is only used for maps which aren’t literals.
{:name v3 :op :persistent-hash-set :values [v0 v1 v2] :type "jank::runtime::obj::persistent_hash_set_ref"}
Functions
function
The function instruction creates a new function object, given all of its
arities and the arity flags. Each arity is its own C function. Note that
function objects don’t support captured values. For those, we use closure.
{:name v0 :op :function :arities {0 user_foo_82687_0} :arity-flags 0 :type "jank::runtime::obj::jit_function_ref"}
closure
The closure instruction creates a new closure object, given all of its
arities, arity flags, and captures. Each arity is its own C function. Note that
closure objects are used when we have captures. If there are no captures, we use
a function. The context of a closure is the name of the struct which is
created just for this closure, to hold its captures.
{:name v1 :op :closure :context user_foo_82688_ctx :arities {0 user_foo_82688_0} :captures {a {:name v0 :type jank::runtime::object_ref}} :arity-flags 0 :type "jank::runtime::obj::jit_closure_ref"}
parameter
The parameter instruction introduces a named parameter of the function into
the local scope. jank will automatically generate one of these for each
parameter a function has. Unlike most instructions, the name used for this
instruction is not auto-generated. The munged name of the actual parameter is
used. Parameters will always have the type object_ref.
{:name n :op :parameter :type "jank::runtime::object_ref"}
capture
The capture instruction is similar to the parameter instruction, in that it
introduces a named closure capture into the scope. jank will automatically
generate one of these for each capture a function has. Like parameter, the
munged name of the actual capture is used for the instruction name. Captures may
have the type object_ref or any other type.
{:name n :op :capture :type "jank::runtime::object_ref"}
dynamic-call
The dynamic-call instruction will invoke the call behavior on the provided
:fn. This works for any jank runtime object which implements the call
behavior, such as functions, closures, keywords, maps, and so on. Any number of
arguments can be provided and the runtime will handle packing them as necessary.
{:name v2 :op :dynamic-call :fn v1 :args [v0] :type "jank::runtime::object_ref"}
named-recursion
The named-recursion instruction will recur into the current function, without
performing argument packing. This recursion doesn’t need to be in tail position,
unlike normal recur usage. It’s separated from dynamic-call as an
optimization.
{:name v1 :op :named-recursion :fn foo :args [v0] :type "jank::runtime::object_ref"}
recursion-reference
The recursion-reference instruction will store a reference to the current
function. This is only used when capturing or returning the function object.
When the recursion reference is called, a named-recursion instruction is used
instead.
Note that recursion references that cross function boundaries are represented as
a capture instruction instead. So a recursion-reference is only ever used
for the immediate function.
{:name v0 :op :recursion-reference :type "jank::runtime::object_ref"}
Vars
def
The def instruction interns a var, sets is root, and updates its metadata. The
name of this instruction will refer to the var itself. Note the the :value is
optional. If there is no value present, the var will still be interned and have
its metadata updated, but its root will not be updated.
{:name v2 :op :def :var user/foo :value v0 :meta v1 :type "jank::runtime::obj::var_ref"}
var-deref
The var-deref instruction grabs the latest value out of an interned var. This
is implicitly done, via Clojure’s semantics, whenever a var is referenced by
its name. For example, (println :meow) implicitly derefs
clojure.core/println.
{:name v1 :op :var-deref :var clojure.core/println :type "jank::runtime::object_ref"}
var-ref
The var-ref instruction grabs an interned var directly, without dereferencing
it. This is analogous to the #'foo syntax, in Clojure.
{:name v1 :op :var-ref :var clojure.core/println :type "jank::runtime::obj::var_ref"}
Control flow
jump
The jump instruction is a terminator which will unconditionally jump to a
different IR block. The instruction also knows whether it’s part of a loop, in
which case it’s effectively a continue;.
{:name v9 :op :jump :block if-merge3 :loop false :type "void"}
branch-set
The branch-set instruction is half of the branch-set/branch-get pair,
which is used to store the expression result of branching so that the merge
block can have a value to use. In other IRs, branch-get is called phi.
jank’s IR usage of branch-set/branch-get is based on the Pizlo-style
upsilon/phi, just without the Greek naming.
Each branch-set will refer to a “shadow” name, without assigning it into the
SSA semantics. The shadow gets assigned into a proper instruction name when
branch-get is used.
The name of the branch-set instruction is never used and the type is always
void.
{:name v11 :op :branch-set :shadow s4 :value v10 :type "void"}
branch-get
The branch-get instruction is half of the branch-set/branch-get pair,
which is used to store the expression result of branching so that the merge
block can have a value to use. See the branch-set docs for more info.
The name of the branch-get is always the same as the shadow variable used in
the branch-set. The type is the actual type of the shadow variable, which
could be any jank object or native type.
{:name s4 :op :branch-get :type "jank::runtime::obj::keyword_ref"}
branch
The branch instruction is a terminator which conditionally jumps to either the
:then or :else block, depending on whether the :condition value is true.
The branch instruction also tracks the resulting merge block, if there is one,
and the shadow variable used.
The name of this instruction is never used and its type is always void.
{:name v6 :op :branch :condition v5 :then if1 :else else2 :merge if-merge3 :shadow s4 :type "void"}
loop
The loop instruction is a terminator which sets up mutable bindings and then
jumps to the provided merge block. Each mutable binding gets a shadow variable
and the result of the loop also has one.
The name of this instruction is never used and its type is always void.
{:name v6 :op :loop :loop-block loop4 :merge loop-merge5 :shadow s2 :shadows {{:name s3 :value v1 :type "jank::runtime::object_ref"}} :type "void"}
case
The case instruction is a terminator which branches to one of the
corresponding case blocks, depending on the provided value. The shift and mask
are set up by the clojure.core/case macro and each case block disambiguates
hash collisions. This instruction also tracks whether there’s a merge block and
shadow variable.
The name of this instruction is never used and its type is always void.
{:name v54 :op :case :shift 0 :mask 0 :value v3 :case-blocks [{:value 3 :block case36} {:value 2 :block case21} {:value 1 :block case6}] :default-block default51 :merge-block nil :shadow nil :type "void"}
ret
The ret instruction is a block terminator which returns a value from the
current function. It has a name, like all instructions, but that name will never
be used. The type of a ret instruction is the type of the returned data.
{:name v7 :op :ret :value v4 :type "jank::runtime::object_ref"}
Exceptions
try
The try instruction is a starter which denotes that the rest of the basic
block will be part of a try body. This instruction notes the various catch
types and their corresponding blocks, as well as whether there is a finally
block. A try instruction will always have a merge block and a shadow variable
for its result.
The name of this instruction is never used and its type is always void.
{:name v11 :op :try :catches [{:type "jank::runtime::oref<jank::runtime::object> &" :block catch4}] :merge try-merge2 :shadow s3 :finally nil :type "void"}
catch
The catch instruction is a starter which denotes that the rest of the basic
block will be part of a catch body. This instruction also knows the merge
block of the corresponding try, as well as the shadow of the try.
The name of this instruction, and its type, correspond with the caught exception value.
{:name v5 :op :catch :merge try-merge2 :shadow s3 :type "jank::runtime::oref<jank::runtime::object> &"}
finally
The finally instruction is a starter which denotes that the rest of the basic
block will be part of a finally body. This instruction also knows the merge
block of the corresponding try.
The name of this instruction is never used and its type is always void.
{:name v5 :op :finally :merge try-merge2 :type "void"}
throw
The throw instruction is a terminator which throws an exeption value. Any
value type can be thrown.
The name of this instruction is never used and its type is always void.
{:name v19 :op :throw :value v18 :type "void"}
Utilities
truthy
The truthy instruction will convert a boxed jank object to a bool, generally
for branching. This is avoided whenever we already have a bool, such as when
working with native values.
{:name v5 :op :truthy :value v0 :type "bool"}
type-erase
The type-erase instruction will strip typed object information away from boxed
jank runtime objects. This is used specifically for mutable values, such as loop
bindings, since they may be initialized with an empty vector, but we have no
idea what type of value each iteration will bring, so we need to represent the
value as a type-erased object_ref instead. This instruction is only meant to
be used on boxed jank objects.
{:name v1 :op :type-erase :value v0 :type "jank::runtime::object_ref"}
letfn
The letfn instruction introduces one or more function/closure locals
simultaneously. It’s expected that for N bindings, there are N instructions
which follow the letfn instruction, one to create each binding. The name of
this instruction will not be used.
{:name v0 :op :letfn :bindings [foo bar] :type "void"}
{:name v1 :op :function :arities {0 user_foo_82687_0} :arity-flags 0 :type "jank::runtime::obj::jit_function_ref"}
{:name v2 :op :function :arities {0 user_bar_82688_0} :arity-flags 0 :type "jank::runtime::obj::jit_function_ref"}
Mutual recursion is handled via :defer for the capture. For example, if both
foo and bar functions do nothing but return each other, we would get this
IR. Notice how the bar capture’s value name is :defer.
{:name v0 :op :letfn :bindings [foo bar] :type "void"}
{:name v1 :op :closure :context user_foo_82687_ctx :arities {0 user_foo_82687_0} :captures {bar {:name :defer :type jank::runtime::object_ref}} :arity-flags 0 :type "jank::runtime::obj::jit_closure_ref"}
{:name v2 :op :closure :context user_bar_82688_ctx :arities {0 user_bar_82688_0} :captures {foo {:name v1 :type jank::runtime::object_ref}} :arity-flags 0 :type "jank::runtime::obj::jit_closure_ref"}
C++ interop
cpp/scope-open
The cpp/scope-open instruction introduces a new C++ scope, which controls the
lifetimes of values defined within the scope. This is used to ensure C++ value
destructors are executed at the correct time. Each cpp/scope-open has a
corresponding cpp/scope-close. The name of scopes is unique in that they start
with s, rather than v.
This instruction always has the type void.
{:name s0 :op :cpp/scope-open :type "void"}
cpp/scope-close
The cpp/scope-close instruction corresponds to a cpp/scope-open, which then
closes the corresponding C++ scope, representing the lexically location where
destructors will run for values defined within that scope. Note that every scope
must only be closed exactly once.
While cpp/scope-close is not a terminator, it is permitted to appear after
terminators, within IR blocks.
This instruction always has the type void.
{:name v11 :op :cpp/scope-close :scope s0 :type "void"}
cpp/raw
The cpp/raw instruction introduces some global C++ which needs to be compiled
alongside this module.
This instruction always has the type void.
{:name v0 :op :cpp/raw :type "void"}
cpp/value
The cpp/value instruction accesses an arbitrary C++ value. The value could be
a C++ constant, function, variable, or member access.
{:name v0 :op :cpp/value :scope "std::basic_string<char>::npos" :type "const std::basic_string<char>::size_type &"}
cpp/into-object
The cpp/into-object instruction converts a native C++ value into a jank
runtime object using the jank::runtime::convert trait.
{:name v2 :op :cpp/into-object :value v1 :type "jank::runtime::object_ref"}
cpp/from-object
The cpp/from-object instruction converts a boxed jank object into a native C++
value using the jank::runtime::convert trait.
{:name v2 :op :cpp/from-object :value v1 :type "int"}
cpp/unsafe-cast
The cpp/unsafe-cast instruction performs a C-style cast from one type to
another.
The type of this instruction is the resulting type of the data.
{:name v1 :op :cpp/unsafe-cast :value v0 :type "int *"}
cpp/call
The cpp/call instruction calls a C++ function, pointer to function, or
functor, with the given arguments. The value of this call will render as nil
if it’s inlined as a C++ symbol.
The type of this instruction is the return type of the function.
{:name v1 :op :cpp/call :value nil :args [v0] :type "std::string"}
For example, if you have a cpp/call for an IR value, it will be set.
{:name v1 :op :cpp/call :value v0 :args [] :type "int"}
cpp/constructor-call
The cpp/constructor-call instruction constructs a stack-allocated C++ value.
This is used for both overloaded constructors and aggregate initialization.
The type of this instruction is the type of the object being constructed.
{:name v1 :op :cpp/constructor-call :args [v0] :type "foo"}
cpp/member-call
The cpp/member-call instruction calls a member function or operator on an invoking object.
This instruction supports direct members as well as base members. Virtual
functions are supported, as well as default arguments.
The type of this instruction is the return type of the function.
{:name v1 :op :cpp/member-call :fn "std::function<void ()>::operator()" :args [v0] :type "void"}
cpp/member-access
The cpp/member-access instruction reaches into a member of the provided C++
object and grabs a reference to it. This instruction supports direct members as
well as base members.
The type of this instruction is the return type of the function.
{:name v3 :op :cpp/member-access :value v2 :member "std::pair<long long, long long>::first" :type "long long &"}
cpp/builtin-operator-call
The cpp/builtin-operator-call instruction performs a C++ operator on primitive
types. Overloaded operators and operators defined for custom types will use
cpp/call or cpp/member-call, depending on whether the operator is defined
as a member.
The type of this instruction is the return type of the operator.
{:name v2 :op :cpp/builtin-operator-call :op "+" :args [v0 v1] :type "jtl::f64"}
cpp/box
The cpp/box instruction creates an opaque boxed jank object to store a native
pointer as a void*. This value can then be retrieved via cpp/unbox, but the
type must be provided, so a cast can be performed. The type will be verified at
runtime.
{:name v2 :op :cpp/box :value v1 :type "jank::runtime::object_ref"}
cpp/unbox
The cpp/unbox instruction extracts the void* from an opaque boxed jank
object and casts it to the specified type. The type will be verified at runtime.
The type of this instruction is the extracted type of the native pointer.
{:name v4 :op :cpp/unbox :value v2 :type "jtl::i64 *"}
cpp/new
The cpp/new instruction GC allocates a new C++ value. The result will be a
pointer to that value.
The type of this instruction is the extracted type of the native pointer.
{:name v2 :op :cpp/new :value v1 :type "int *"}
cpp/delete
The cpp/delete instruction frees memory allocated via cpp/new. This is not
required, since all values allocated via cpp/new are tracked by jank’s GC.
This instruction always has the type void.
{:name v3 :op :cpp/delete :value v2 :type "void"}