Is Nim a transpiler?

19th October 2021 - Language design , Nim , Programming

This is a question that has come up time and time again in the IRC channel, when talking to people in person, and in the comment section pretty much every time Nim has an article on Hackernews or one of the bigger programming subreddits. It’s also a question that has been answered a lot of times, both with a short and efficient “no”, but also in longer form. This article will go into some detail about why the answer is “no” and can hopefully serve as a reference the next time this question gets asked.

What is a compiler anyways?

Those who ask the question “is Nim a transpiler?” usually means “as opposed to a compiler”, so to understand why the answer is “no” we need to understand the difference between a compiler and a transpiler. While these words are sometimes used interchangeably there is a subtle yet important distinction in their meaning. But before we get into that, what exactly is a compiler? Compilers have existed almost as long as computers have been in use. The first ones started to crop up in the early ’50s as computers became more and more capable. The job of a compiler is to take a program written in an abstract language, and convert it into something more concrete. Many compilers will compile from your language of choice directly into binary code, like GCC, or into a particular VM bytecode format, like the Java Compiler. Other compilers will take a language of higher abstraction, and simply compile it into a more concrete implementation in a different language. An example of this is TypeScript, which compiles its code into JavaScript, running all the static type checks and such before outputting only valid JavaScript code.

But I thought a transpiler just converted between languages?

This seems to be the misconception that leads to people asking the question which titles this article. While it is true that a transpiler converts from one language to another language the distinction lies in levels of abstraction. By compiling C to assembly you’re essentially using information in the C language to make decisions about what assembly code to generate, discard the information in the process. A transpiler on the other hand is a tool that converts between two languages while keeping the same level of abstraction. A theoretical perfect transpilation is therefore a two-way process where you could go back and forth between languages. This is typically not the case however because of structural changes and the assumptions required to transpile between two different languages. Examples of transpilers include JSweet which converts from Java to TypeScript, f2c which transpiles Fortran 77 to C, or J2ObjC which converts from Java to Objective-C, or even the tool c2nim which ships with Nim and compiles from C to Nim. Transpilers are typically used when you want to use code from one language which is on the same level of abstraction and use it in a language of the same or higher abstraction. And even this list is using the term a bit loosely, f2c for example was a part of the chain to compile Fortran to machine code, and is therefore translating Fortran features into C.

So what makes Nim not a transpiler?

The Nim compiler takes Nim code, and outputs C, C++, Objective-C, or JavaScript code. This might sound suspiciously much like a transpiler. Especially when it’s compiling to the typed languages. But again the key point is levels of abstraction. Nim not only has a much stricter type system than for example C, but it also supports best-in-class metaprogramming. This means that the Nim compiler will not only convert Nim code to C code, but it allows the program itself to generate Nim code with Nim code. This is done through templates and macros and is a huge part of what makes Nim so flexible. In fact you could write a transpiler in Nim which read a code file from a different language and converted it to Nim code while it was compiling. So no, Nim is not merely a transpiler, the code it outputs is not convertible back to the Nim that created it and the level of abstraction has dropped quite a fair bit. In fact the C code that Nim outputs isn’t even intended to be read or understood by a human, it is generated to be as fast as possible, and Nim does some clever tricks to make the C compiler spit out more efficient machine code. These features puts Nim comfortably in the compiler space.

So is there nothing which is a true transpiler?

The above definition is fairly strict, and after reading it many people will ask themselves this question. And it all comes down to how strict you want to be in your definition. Is a different syntax an abstraction? Are Java and Objective-C similar enough for it to not cross a boundry of abstraction? The reality of the situation is that all languages have some differences, after all what would be the reason to create an exact copy of a language? But in general the term is most commonly applied to stand-alone tools that takes an already complete project in one language, and converts it to another language. Sometimes these tools might not even do the conversion 100% accurately or even fail outright on parts that can’t be translated accurately. These tools are often used to do a one-time conversion of a language to another, or simply to be able to use a certain library in another language than it was written. However the term is also used in more relaxed terms when describing e.g. Babel which converts from next-generation JavaScript to browser compatible JavaScript (Babel itself refers to itself as a compiler), or even CoffeeScript which is mostly an elaborate syntax transformation into JavaScript (and also refers to itself as “compiling to JavaScript”). As user “mst” put in while discussing this on IRC:

transpiler, n.: Somebody else’s compiler

Final remarks

In summary the transpiler word has been quite overused lately. And while a more relaxed definition than the one I’ve used here can certainly be applied you will need to relax it almost to the point of absurdity in order to categorize Nim as a transpiler. Nim uses C in much the same way that Rust uses the LLVM for example, simply a target for compilation that will optimise well and run on a lot of different targets. After all, standing on the shoulders of giants is a great way to reach a long way without having to do a lot of climbing.