Thoughts on Zig Syntax

Post at — May 10, 2025

In a search for a new programming language to learn, I have recently come across the language Zig. At first glance, I had little to no interest in looking into this language. It seemed like just another new language trying to make it big as many languages have tried, with varying success.

My points here center on Zig and its similarities and differences syntactically between other languages I have experience with. I will go over features in later articles.

Initial thoughts

Zig presents itself as a simple, fast, and extensive low-level programming language. In many ways, it hits the mark on these goals. I find that it presents simple solutions to problems like sorting, memory allocations and generic typing. In my initial learning period, I gained a decent understanding of the language and what it stands for, this took much longer for Rust, albeit it doesn’t have as many features.

Syntactic Similarities

There first thing of consideration is the language Syntax. Zig uses a simple C-like syntax, that’s easy to understand as a programmer coming from C and other structured programming languages like Rust.

Zig and C syntax

Take a look at the standard ‘Hello World’ exemplar code.

1
2
3
4
5
6
// Zig
const std = @import("std");

pub fn main() void {
  std.debug.print("Hello {s}\n", .{"World!"});
}

This code doesn’t look too different from the same code in C.

1
2
3
4
5
6
// C
#include <stdio>

int main(void) {
  printf("Hello %s\n", "World");
}

Zig syntax is personally fun to work with. It looks got and simplifies complex sections of code in my perspective. The syntax is reminiscent of C but holds some common qualities with Delphi, particularly newer versions of Delphi and freepascal.

Check out my comparison between Delphi and Zig.

Broader observations

Function declarations

Zig syntax does a great job at representing code in a simple and readable fashion, along with it’s distinct features. Function declarations look similar to those in Rust, with the fn prefix and return type indicated after the parameter list.

1
2
3
4
// Zig
fn LoadShader(path: [_]const u8) anyerrror!Shader {
  // ...
}
1
2
3
4
// Rust
fn LoadShader(path: &str) -> Result<Shader, ShaderError> {
  // ...
}

Here errors are a part of the function return type, this shies away from the Rust convention of the Result<T,E> type. I find that this is easier to understand than the Result type but also much more useful than the Exceptions that Delphi, Java, C++ and who knows how many more languages confidently sport.

Function scopes

Standard function scopes using { } parenthesis are used in Zig, much like other structured programming languages. They can also be embedded in existing scopes for in-scope scopes which can have their own variables. This is not a feature that only Zig has, but one that I am a fan of.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const glx = @import("glx.zig");
const gpa_instance = std.heap.GeneralPurposeAllocator(.{}){};

fn LoadShader(path: []const u8) anyerror!Shader {
  var allocator = gpa_instance.backing_allocator();

  defer _ = allocator.deinit();

  {
    // Inner scope
    var loader = glx.ShaderLoader().init(allocator);

    defer _ = loader.deinit();

    {
      // Inner inner scope

      // ...
    }
  }

  // ...

  return shader;
}

Loops and if-statements

A baffling syntactic choice is the need for ( ) parenthesis for if statements, for loops and while loops. Much like C-like languages.

1
2
3
4
5
{
  if(path.len > 10) {
    // ...
  }
}

This is a personal gripe, spurred on by the syntax of Rust, which I prefer.

1
2
3
4
5
{
  if path.length > 10 {
    // ...
  }
}

Payload captures

Payload captures are a unique capturing method used in Zig. Instead of the for item in syntax common in languages, Zig uses a payload capture.

Payload captures are a method of capturing values in a list or enum and treating them as variables available within the scope of the loop or catch. These are normally constants.

1
2
3
4
5
{
  for (SHADER_LIST) | shader | {
    DeleteShader(shader);
  }
}

These work with while loops, for loops and other structures such as returning errors.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  // Payload captures in while loops
  while (SHADER_LIST) | shader | {
    DeleteShader(shader);
  }

  // Payload captures in error handling with `catch`
  LoadShader(shader) catch | err | {
    std.log.err("Failed to load shader: {}",.{@errorName(err)});
  }
}

Counter-example

This is a standard example of the for x in y syntax in Lua.

1
2
3
for shader in shaders do
  DeleteShader(shader);
end

Pointers

Pointers work the way the do in C. Indicated by *var. Dereference with var.*.

1
2
3
4
5
6
7
8
9
{
  // Declararing a pointer
  var shader: *Shader = try LoadShader(shader);

  ShaderList.append(shader);

  // Dereferencing
  ShaderList.IndexOf(shader.*);
}

Imports

Import with the @import syntax. Importing a module doesn’t import structs and declarations globally. Declare as constants by treating them as properties of the module.

1
2
const raylib = @import("raylib");
const initWindow = raylib.initWindow;

Final observations

These syntactic features of Zig give the language the unique style that appears in code written in Zig. I find these qualities pleasant and interesting. Zig is a great, simple language thats simplicity presents itself in syntax and features. The language is still in rapid development, so who knows what it may look like in the future?