Turning Code into Machine Language

#ProgrammingThu Sep 26 2024

Have you ever felt that Babel’s build time was too slow? Here’s an example of someone who felt the same frustration and decided to create a new compiler—SWC. In this article, we will dive into what compilers, Babel, and SWC are all about. To start, we first need to understand the difference between high-level and low-level programming languages.

High-Level vs. Low-Level Languages

A high-level programming language is closer to human language than machine code. Since it's easier to read and maintain, most programming today is done using high-level languages. However, to execute this code, it must be translated into a low-level language. Low-level languages, like machine code or assembly language, are used by the computer's hardware. This translation process is handled by compilers or interpreters.

Compilers

A compiler is a program that scans the entire source code and converts it into machine code. There are several types of compilers:

  • Static Compilation
    • Directly converts source code into machine code
  • Bytecode Compilation
    • Translates source code into bytecode
  • AOT Compilation (Ahead-Of-Time)
    • Compiles intermediate code (like bytecode) into machine code
  • JIT Compilation (Just-In-Time)
    • Compiles code in real-time as it’s needed

Examples of compiled languages: C, C++, Rust, Go

Interpreters

An interpreter is a program that reads and executes the source code line by line. Unlike compiled languages, interpreted languages don’t generate an executable file, and the conversion to machine code happens as each line is executed, making it slower than compiled code. Additionally, compilers report all errors after the code is converted, while interpreters stop as soon as they encounter an error, which can be beneficial for security.

Examples of interpreted languages: JavaScript, SQL, Python, Ruby

The V8 Engine: JavaScript’s Powerhouse

The V8 Engine is an open-source JavaScript engine that powers Google Chrome, Deno, and Node.js. Written in C++, it uses JIT (Just-In-Time) compilation to convert JavaScript into machine code that runs on the web.

v8 engine

Babel

Babel is a JavaScript compiler that allows ECMAScript 2015+ code to run in older browsers. It also transforms JSX syntax using the React preset. To ensure that JavaScript runs safely across all browsers, tools like Babel are essential for converting code to ES5 syntax before deployment. If you use Create React App (CRA) to generate a React project, you’ll notice that Babel is part of the build process. Running the build generates a .dist folder, which is Babel’s output. You can also configure Babel through a .babelrc file to use presets like minify or specify the JavaScript version to be supported.

For functions that aren't handled by browser engines, Babel includes a polyfill, allowing those functions to run even if the browser doesn’t natively support them. Polyfills are different from Babel in that they are added to the script and executed at runtime, whereas Babel executes during compilation.

SWC

So, what is SWC? It's used by tools like Next.js and Deno, SWC boasts significantly faster build times compared to Babel. Unlike Babel, which is JavaScript-based and uses only one CPU core, SWC’s creator opted for Rust, a language well-suited for parallel processing, which dramatically improves performance. The acronym SWC stands for Speedy Web Compiler, which gives you an idea of the motivation behind its creation. Rust was chosen for its ability to handle parallel processes and because its FFI (Foreign Function Interface) and generics make development easier.

Existing web build tools tend to terminate after execution, meaning they can't optimize JavaScript with a compiler’s help. The creator of SWC believes JIT compilers aren’t the solution for this issue. Instead, using Rust, many build tools could migrate toward more efficient workflows, solving this long-standing problem.

Node.js now allows direct execution of TypeScript files without external dependencies, thanks to the new --experimental-strip-types flag. (Reference: Node.js GitHub PR #53725)