Getty Images/iStockphoto

Tip

Top benefits of the Zig programming language

Does the world really need another programming language? Yes, say developers behind Zig. Here are five of the top features Zig brings to the programming world.

There's a new programming language that is capturing the hearts and minds of both Java and C programmers alike. Its name is Zig, and while it's not yet hit a full version 1.0 increment, development activity and enthusiasm for this new programming language is growing.

But why does the world need a new programming language? More specifically, what are the advantages of Zig over other languages such as C++, Python or Java?

Benefits of Zig

While a full discussion of how Zig approaches software development differently would be quite extensive, Zig software developers will appreciate several interesting benefits, including the following:

  • A simple, succinct grammar and syntax.
  • Interpreted language usability with fully compiled outputs.
  • A user-friendly set of build commands.
  • Features that simplify and thus encourage unit testing.
  • Simplified debugging and runtime troubleshooting.

Zig has a simple and succinct syntax

The Zig language's low complexity is one of its most compelling benefits.

Zig's formal language specification is articulated entirely within a 500-line parsing expression grammar (PEG) file. This simplicity and succinctness is a point of pride for the Zig community.

Zig does not require generics or preprocessors -- everything is written in Zig itself, not in a side-language. That's a huge improvement over C, and C++ in particular.

Dynamic features in a compiled language

A common dividing line between programming languages is how they turn high-level code into instructions that the processor executes. This boils down to the following two points:

  • In interpreted languages, the parts of a dynamic code that must run through a software interpreter, rather than be compiled on the fly by a just-in-time compiler, run more slowly than the code generated by a compiler in compiled languages which is already entirely in machine code.
  • Compiled languages such as C and C++ compile directly into machine code, which is what the CPU actually executes.

Even though machine code generated by compiled languages traditionally performs better, it is easier to test, troubleshoot and debug in interpreted languages.

Zig refutes that formal subdivision. Although Zig compiles directly to machine language, Zig code can also be executed at compilation time when values are known during the compilation, and thus offer a lot of the flexibility normally found in interpreted languages. This can be rather puzzling to programmers who are versed in compiled languages in general, and particularly C or C++.

This not only eliminates the need for preprocessors or generics, but it also pushes the power of both features and others to their full extent into the language itself. It's not a restriction, but rather a generalization.

With Zig, you get the performance benefits of a compiled language without sacrificing many important characteristics that provide the flexibility of interpreted languages.

Simplified build and test commands

To create a highly optimized executable file in Zig, a developer merely issues the following simple and single command, where prog_name.zig is the name of the source file:

zig build-exe -O ReleaseFast prog_name.zig

The build command zig test doesn't generate an executable file, but instead compiles and executes the various test blocks contained within a source code file. For example, with a source file called module.zig, one would compile and execute its test blocks thusly:

zig test module.zig

With Zig, developers can quickly run test blocks in an individual source file or generate an executable file which could involve several source files.

Support for test blocks

Test blocks are one of the most revolutionary traits of Zig.

Test blocks let developers write tests inside any Zig source file and then execute these test blocks in isolation. This is another feature that makes Zig resemble an interpreted language, but with advantages.

Not only are test blocks usable to test anything in the source file they are in without the need to build an executable file, they also enable easy and fast prototyping.

Consider the following source file named test.zig:

const std = @import("std");

test " => testing integer div built-in" {
    std.debug.print("\n" ++
      "0 div 3 {}\n" ++
      "1 div 3 {}\n" ++
      "2 div 3 {}\n" ++
      "3 div 3 {}\n" ++
      "4 div 3 {}\n" ++
      "5 div 3 {}\n" ++
      "6 div 3 {}\n" ++
      "7 div 3 {}\n" ++
      "8 div 3 {}\n",
      .{ @divTrunc(0, 3), 
         @divTrunc(1, 3),
         @divTrunc(2, 3),
         @divTrunc(3, 3), 
         @divTrunc(4, 3),
         @divTrunc(5, 3),
         @divTrunc(6, 3),
         @divTrunc(7, 3),
         @divTrunc(8, 3)
       }
    );
}

To compile and execute the test block, one uses the zig test command as illustrated in Figure 1.

Screenshot of testing integer divisions.
Figure 1. Testing integer divisions.

Zig makes debugging easier

Debugging may very well be easier in Zig than in any other programming language.

A big problem with debuggers in general is that to execute a program step by step, one must generate symbols and integrate them with the debugger. This is often a hassle because one must create another version of the program by compiling it in Debug mode, which traditionally yields notoriously slower executables.

In Zig, a developer can integrate breakpoints in the source code by using the built-in @breakpoint(). For example, one may want to insert a @breakpoint() just after a call to a variable-displaying print function that displays the variables one wishes to trace. Setting the breakpoints in the source code streamlines the debugging process and enables use of the debugger with code in Release mode. In this setup, the breakpoints are already preset in the executable file, as are the commands to print the variables' contents, which should conveniently precede the breakpoints.

The following code, assumed from now on to be in a file named main.zig, demonstrates the inclusion of the @breakpoint() built-in function within a for loop:

const std = @import("std");

pub fn main() void {
    for (0..3) |i| {
       std.debug.print("i = {}", .{i});
       @breakpoint();
    }
}

Placing the print function call just before the breakpoint displays the value of the variable holding the iteration index right before the debugger stops.

This program is compiled and run in the command tool as shown in Figure 2. First, the executable is generated in ReleaseFast mode. Then, the debugger -- in this case GDB -- is called with the executable name; its extension is usually unnecessary. To run the program for the first time, one uses the r command followed by an Enter. The program then prints the value of i and stops at the breakpoint as expected. The c command followed by an Enter continues the process. For further iterations, to make the debugger repeat the c command (the last command typed) just keep hitting Enter.

Screenshot of debugging in Zig.
Figure 2. Debugging in Zig.

More advantages of Zig: Following the recent trends

An increasing number of features previously reserved for interpreted languages are now being integrated into compiled languages. This suggests a shift toward improved performance while maintaining some benefits of interpreted languages. This trend highlights interesting impending prospects in programming languages.

Zig also follows this trend relentlessly. It pushes the limits of what's possible for compiled programming languages, and at the same time remains a simple, but powerful language.

Nilo Stolte has extensive experience in computer graphics, computer architecture and parallel processing, as well as high-level programming languages including C, C++ and Java.

Dig Deeper on Core Java APIs and programming techniques