Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Installing jank on macOS and Linux
  2. Writing a program which prints Hello, world!
  3. 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 new without specifying org.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.

  1. The project name hello_lein and version 0.1-SNAPSHOT
  2. :license: The license of our project, which defaults to MPL since jank uses it. You are free to change this.
  3. :dependencies: Our project dependencies. More on this in another chapter.
  4. :plugins: The lein-jank plugin, and its :middleware, which is used to configure our project for jank instead of Clojure JVM.
  5. :main: The entrypoint of our program, which contains our -main function.
  6. :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-port file 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:

  1. Calling a Clojure function
  2. 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/raw strings 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:

  • rand becomes cpp/rand
  • std::this_thread::get_id becomes cpp/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.

std::map<std::string, int*>
(std.map std.string (:* int))

A normal C++ array template instantiation.

std::array<char, 64>::value_type
(:member (std.array char 64) value_type)

A sized C-style array.

unsigned char[1024]
(:array (:unsigned char) 1024)

A reference to an unsized C-style array.

unsigned char(&)[]
(:& (:array (:unsigned char)))

A pointer to a C++ function.

int (*)(std::string const &)
(:* (:fn int [(:& (:const std.string))]))

A pointer to a C++ member function.

int (Foo::*)(std::string const &)
(:member* Foo (:fn int [(:& (:const std.string))]))

A pointer to a C++ member which is itself a pointer to a function.

void (*Foo::*)()
(:member* Foo (:* (:fn void [])))

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
std::basic_string<char>::npos
#cpp (:member (std.basic_string char) npos)
std::numeric_limits<long long>::max()
(#cpp (:member (std.numeric_limits (:long (:long int))) max))
&std::pair<int, bool>::first
#cpp (:&member (std.pair int bool) first)

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.

  1. cpp/cast
  2. cpp/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 #include in 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.

  1. To contain all special C++ forms like cpp/new, cpp/cast, cpp/&, etc.
  2. 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.

  1. Print directives to stdout which tell the jank build system to add flags to its jank invocation
  2. Find or build files and install them in the provided out directory

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:

  1. Build flags
  2. 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.

Adds the -L path flag to the jank invocation. The path should be absolute.

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.

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:

  1. Source directory (:src-dir) – This is where the package’s source files are.
  2. Build directory (:build-dir) – This is a temporary directory where build files can be placed.
  3. 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:

  1. 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.
  2. The optimization level to use (:optimization-level) – This is an integer from 0 to 3 inclusive. 0 means no optimizations.
  3. 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:

  1. Does this library need any preprocessor defines? Generally, the answer is no.
  2. Where are the include headers stored?
  3. Where are the libraries stored?
  4. 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:

  1. A project.clj, defining how the package will be named and versioned
  2. 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 your PKG_CONFIG_PATH can 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:foo syntax here
  • When using jank run or jank run-main, -- is needed to separate args for jank from args for your program
    • Example: jank -I include run test.jank -- a b c

Parser

  • No load operation for data_readers.(cljc|jank) at start-up to extend supported tags
  • jank allows using reader conditionals in .jank files as well
  • No automatic method to disable read by setting the clojure.core/*read-eval* at start-up. The value can be overriden via a binding operation

clojure.core

  • Baked into the jank binary, not shipped separately
  • No nested require support (same as ClojureScript)
  • No import
  • (hash-map) returns a hash map, not an array map
  • aget is a special form
  • aset is a macro
  • keyword is more strict about valid inputs
  • future can only forward exceptions which are std::exception or object_ref
    • Other exceptions will be forwarded as "Unknown exception"
  • future-cancel returns nil, not the result of the cancellation
  • future-cancelled? always returns false on 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_string is expected to be UTF-8
  • No records (yet)
  • No protocols (yet)
  • Sequences
    • Support for in-place operations (fresh-seq, next-in-place)

Compilation model

  • Source-only distribution
    • .o files 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 .class files
    • A module is backed by either a .jank or .cljc source file, optionally with a .o file cached for quick loading

Math

  • Signed integer overflow is well defined
  • Unsigned integer overflow is well defined
  • Division by integer 0 is undefined behavior
  • Division by floating point 0.0 is 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:

  1. How to check the health of your jank install
  2. How to get a stack trace for a jank crash
  3. Where to ask for help and report issues
  4. 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=1 will print out formatted IR for each compiled jank function.
  • JANK_PRINT_CODEGEN=1 will 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 -v to 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 throw command in gdb before you run.

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 you run.

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 def just 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 #js reader tag
    • And so on
  • Clojure Dart
    • Lazily initializes def, to aid in tree shaking
    • Doesn’t have multi-methods
    • Extends catch syntax 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 #dart reader tag
    • And so on
  • Basilisp
    • All numbers are unlimited precision
    • Doesn’t have refs or software transactional memory (STM)
    • Doesn’t have a character type
    • Supports a custom #py reader 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:

  1. The jank compiler is significantly simpler
  2. It’s possible to compile entire jank projects to just .cpp files and then feed that into a normal C++ build system. At that point, it’s just a C++ project
  3. 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.

  1. You can wrap the native value in an opaque box, if its lifetime allows that.
  2. You can define a conversion trait for your native type.
  3. 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"}