Skip to content(if available)orjump to list(if available)

Partially Matching Zig Enums

Partially Matching Zig Enums

82 comments

·August 9, 2025

loeg

Just having a comptime unreachable feature seems pretty cool. Common C++ compilers have the worst version of this with __builtin_unreachable() -- they don't do any verification the site is unreachable, and just let the optimizer go to town. (I use/recommend runtime assert/fatal/abort over that behavior most days of the week.)

judofyr

This is one the reasons I find it so silly when people disregard Zig «because it’s just another memory unsafe language»: There’s plenty of innovation within Zig, especially related to comptime and metaprogramming. I really hope other languages are paying attention and steals some of these ideas.

«inline else» is also very powerful tool to easily abstract away code with no runtime cost.

chrismorgan

What I’ve seen isn’t people disregarding Zig because it’s just another memory-unsafe language, but rather disqualifying Zig because it’s memory-unsafe, and they don’t want to deal with that, even if some other aspects of the language are rather interesting and compelling. But once you’re sold on memory safety, it’s hard to go back.

Sytten

This is really the crust of the argument. I absolutely love the Rust compiler for example, going back to Zig would feel a regression to me. There is a whole class of bugs that my brain now assumes the compiler will handle for me.

pron

Problem is, like they say the stock market has predicted nine of the last five recessions, the Rust compiler stops nine of every five memory safety issues. Put another way, while both Rust and Zig prevent memory safety issues, Zig does it with false negatives while Rust does it with false positives. This is by necessity when using the type system for that job, but it does come at a cost that disqualifies Rust for others...

Nobody knows whether Rust and/or Zig themselves are the future of low-level programming, but I think it's likely that the future of low-level programming is that programmers who prefer one approach would use a Rust-like language, while those who prefer the other approach would use a Zig-like language. It will be intesting to see whether the preferences are evenly split, though, or one of them has a clear majority support.

dnautics

would you be satisfied if there was a static safety checker? (or if it were a compiler plugin that you trigger by running a slightly different command?). Note that zig compiles as a single object, so if you import a library and the library author does not do safety checking, your program would still do the safety checking if it doesn't cross a C abi boundary.

https://www.youtube.com/watch?v=ZY_Z-aGbYm8

Zambyte

In practice, almost all memory safety related bugs caught by the Rust compiler are caught by the Zig safe build modes at run time. This is strictly worse in isolation, but when you factor in the fact that the rest of the language is much easier to reason about, the better C interop, the simple yet powerful metaprogramming, and the great built in testing tools, the tradeoffs start to become a lot more interesting.

munchler

Nit: I think you want crux in that phrase, not crust.

conorbergin

I think the problem with this attitude is the compiler becomes a middle manager you have to appease rather than a collaborator. Certainly there are advantages to having a manager, but if you go off the beaten track with Rust, you will not have a good time. I write most of my code in Zig these days and I think being able to segfault is a small price to pay to never have to see `Arc<RefCell<Foo<Bar<Whatever>>>` again.

diegocg

As someone who uses D and has been doing things like what you see in the post for a long time, I wonder why other languages would put attention to these tricks and steal them when they have been completely ignoring them forever when done in D. Perhaps Zig will make these features more popular, but I'm skeptic.

brabel

I was trying to implement this trick in D using basic enum, but couldn't find a solution that works at compile-time, like in Zig. Could you show how to do that?

Snarwin

  import std.meta: AliasSeq;

  enum E { a, b, c }

  void handle(E e)
  {
      // Need label to break out of 'static foreach'
      Lswitch: final switch (e)
      {
          static foreach (ab; AliasSeq!(E.a, E.b))
          {
              case ab:
                  handleAB();
                  // No comptime switch in D
                  static if (ab == E.a)
                      handleA();
                  else static if (ab == E.b)
                      handleB();
                  else
                      static assert(false, "unreachable");
                  break Lswitch;
          }
          case E.c:
              handleC();
              break;
      }
  }

ozgrakkurt

This perspective that many people take on memory-safety of Rust seems really "interesting".

Unfortunately for all fanatics, language really doesn't matter that much.

I have been using KDE for years now and it works perfectly good for me. It has no issues/crashes, it has many features in terms of desktop environment and also many programs that come with it like music player, video player, text editor, terminal etc. and they all work perfectly well for me. Almost all of this is written in C++. No need to mention the classic linux/chromium etc. etc which are all written in c++/c.

I use Ghostty which is written in zig, it is amazingly polished and works super well as well.

I have built and used a lot of software written in Rust as well and they worked really well too.

At some point you have to admit, what matters is the people writing software, the amount of effort that goes into it etc. it is not the langauge.

As far as memory-safety goes, it really isn't close to being the most important thing unless you are writing security critical stuff. Even then just using Rust isn't as good as you might think, I uncountered a decent amount of segfaults, random crashes etc. using very popular Rust libraries as well. In the end just need to put in the effort.

I'm not saying language doesn't matter but it isn't even close to being the most important thing.

Eliah_Lakhin

> As far as memory-safety goes, it really isn't close to being the most important thing unless you are writing security critical stuff.

Safety is the selling point of Rust, but it's not the only benefit from a technical point of view.

The language semantics force you to write programs in a way that is most convenient for the optimizing compiler.

Not always, but in many cases, it's likely that a program written in Rust will be highly and deeply optimized. Of course, you can follow the same rules in C or Zig, but you would have to control more things manually, and you'd always have to think about what the compiler is doing under the hood.

It's true that neither safety nor performance are critical for many applications, but from this perspective, you could just use a high-level environment such as the JVM. The JVM is already very safe, just less performant.

bobajeff

I've seen a few new languages come along that were inspired by zig's comptime/metaprogramming in the same language concept.

Zig I think has potential but it hasn't stabilized enough yet for broad adoption. That means it'll be awhile before it's built an ecosystem (libraries, engines etc.) that is useful to developers that don't care about language design.

the__alchemist

Concur. This is a great feature I wish rust had. I've been bitten by the unpleasant syntax this article laments.

pron

> just another memory unsafe language

Also, treating all languages that don't ensure full memory safety as if they're equally problematic is silly. The reason not ensuring memory safety is bad is because memory unsafety as at the root of some bugs that are both common, dangerous, and hard to catch. Only not all kinds of memory unsafety are equally problematic, Zig does ensure the lack of the the most dangerous kind of unsafety (out-of-bounds access) while making the other kind (use-after-free) easier to find.

That the distinction between "fully memory safe" and "not fully memory safe" is binary is also silly not just because of the above, but because no lanugage, not even Java, is truly "fully memory safe", as programs continue to employ components not written in memory safe languages.

Furthermore, Zig has (or intends to have) novel features (among low-level languages) that help reduce bugs beyond those caused by memory unsafety.

hitekker

If you one day write a blog, I would want to subscribe.

Your writing feels accessible. I find it makes complex topics approachable. Or at least, it gives me a feel of concepts that I would otherwise have no grasp on. Other online writing tends to be permeated by a thick lattice of ideology or hyper-technical arcanery that inhibits understanding.

pron

Thank you!

I did have one once (https://pron.github.io) but I don't know how accessible it is :) (two post series are book-length)

Ygg2

> Your writing feels accessible. I find it makes complex topics approachable

Yeah. By omitting a large swath of nuance. It reeks of "you can approximate cow with a sphere the size of Jupiter". It's baffling ludicrous.

Any rhetorical device that equates Java/C# (any memory safe Turing language ) safety with C is most likely a fallacy.

IshKebab

Right but I think people are disappointed because we finally got a language that has memory safety without GC, so Zig seems like a step backwards. Even if it is much much better than C (clearly), it's hard to get excited about a language that "unsolves" a longstanding problem.

> not even Java, is truly "fully memory safe", as programs continue to employ components not written in memory safe languages.

This is a silly point.

pron

> I think people are disappointed because we finally got a language that has memory safety without GC, so Zig seems like a step backwards

Memory safety (like soundly ensuring any non-trivial property) must come at a cost (that's just complexity theory). You can pay for it with added footprint (Java) or with added effort (Rust). Some people are disappointed that Zig offer more safety than C++ but less than Rust in exchange for other important benefits, while others are disappointed that the price you have to pay for even more safety in Rust is not a price they're happy to pay.

BTW, many Rust programs do use GC (that's what Rc/Arc are), it's just one that optimises for footprint rather than speed (which is definitely okay when you don't use the GC as much as in Java, but it's not really "without GC", either, when many programs do rely on GC to some extent).

> This is a silly point.

Why? It shows that even those who wish to make the distinction seem binary themselves accept that it isn't, and really believe that it matters just how much risk you take and how much you pay to reduce it.

(You could even point out that memory corruption can occur at the hardware level, so not only is the promise of zero memory corruption not necessarily worth any price, but it is also unattainable, even in principle, and if that were truly the binary line, then all of software is on the same side of it.)

CyberDildonics

If you make advancements but disregard the advancements that came before you, you have a research language, not a modern usable language.

loeg

By this definition, every major programming language in use today (C, C++, Java, Python, ...) is merely a research language.

surajrmal

I can't take zig as seriously as rust due to lack of data race safety. There are just too many bugs that can happen when you have threads, share state between those threads and manually manage memory. There are so many bugs I've written because I did this wrong for many years but didn't realize until I wrote rust. I don't trust myself or anyone to get this right.

agons

Is there a reason the Zig compiler can't perform type-narrowing for `u` within the `U::A(_) | U::B(_)` "guard", rendering just the set of 2 cases entirely necessary and sufficient (obviating the need for any of the solutions in the blog post)?

I'm not familiar with Zig, but also ready to find out I'm not as familiar with type systems as I thought.

rvrb

it can narrow the payload: https://zigbin.io/7cb79d

that doesn't help when you want to run methods or logic on the union type in the switch branch, since you would have to check the tag in every method

it might be helpful for the post to demonstrate that a/b can have different payload types in their example. the reason being on line 9:

  inline .a, .b => |_, ab| {
the `_` is discarding the payload; `ab` is the tag, that compiler has narrowed to `.a` or `.b`.

I think using a concrete example would also be helpful. let's say a bytecode VM, which is top of mind for me right now:

  dispatch: switch (instruction) {
      inline .load0, .load1, .load2, .load3, .load => |_, tag| {
          const slot = switch (tag) {
              .load0 => 0,
              .load1 => 1,
              .load2 => 2,
              .load3 => 3,
              .load => self.read(u8),
              else => comptime unreachable,
          };
          self.locals[slot] = self.pop();
          continue :dispatch self.read(Instruction);
      },
      // ...
  }

uecker

It is great to see other languages getting the same compile-time meta programming features as C ;-)

https://godbolt.org/z/P1r49nTWo

loeg

I didn't know godbolt let you include URLs. Fascinating: https://raw.githubusercontent.com/uecker/noplate/main/src/co...

I think Zig gets some points here for legibility ;-).

null

[deleted]

spiffyk

This post shows how versatile Zig's comptime is not only in terms of expressing what to pre-compute before the program ever runs, but also for doing arbitrary compile time bug-checks like these. At least to me, the former is a really obvious use-case and I have no problem using that to my advantage like that. But I often seem to overlook the latter, even though it could prove really valuable.

dwattttt

I love the idea, but something being "provable" in this way feels like relying on optimisations.

If a dead code elimination pass didn't remove the 'comptime unreachable' statement, you'll now fail to compile (I expect?)

teiferer

It's inherently an incomplete heutistic. Cf. the halting problem.

Doesn't mean it's not useful.

anonymoushn

A lot of Zig relies on compilation being lazy in the same sort of way.

dwattttt

For the validity of the program? As in, a program will fail to compile (or compile but be incorrect) if an optimisation misbehaves?

That sounds as bad as relying on undefined behaviour in C.

rvrb

I did not realize you could inline anything other than an `else` branch! This is a very cool use for that.

the__alchemist

I love how this opens with the acknowledgement we've made a mess of choice-like data structure terminology!

veber-alex

I don't understand. Isn't this only useful if the value you match on is known at compile time?

sekao

The code example will work even if `u` is only known at runtime. That's because the inner switch is not matching on `u`, it's matching on `ab`, which is known at compile time due to the use of `inline`.

That may be confusing, but basically `inline` is generating different code for the branches .a and .b, so in those cases the value of `ab` is known at compile time. So, the inner switch is running at compile time too. In the .a branch it just turns into a call to handle_a(), and in the .b branch it turns into a call to handle_b().

alpinisme

The problem this is meant to solve is that sometimes a human thinking about the logic of the program can see it is impossible to reach some code (ie it is statically certain) but the language syntax and type system alone would not see the impossibility. So you can help the compiler along.

It is not meant for asserting dynamic “unreachability” (which is more like an assertion than a proof).

dlahoda

fn main() {

    if false {

        const _:() =  panic!();

    }
}

Fails to compile in Rust.

Sharlin

Sure, because it's compile-time code inside a (semantically) run-time check. In recent Rust versions you can do

    fn main() {
        const {
            if false {
                let _:() = panic!();
            }
        }
    }
which compiles as expected. (Note that if the binding were `const` instead of `let`, it'd still have failed to compile, because the semantics don't change.)

tialaramex

Perhaps more succinctly:

    fn main() {
       const _:() = const { if false { panic!() } };
    }
It's fine that we want a constant, it's fine that this constant would, when being computed at compile time, panic if false was true, because it is not.

dlahoda

not sure it is to be equivalent to zig.

in zig they have one brach const.

in rust example from you, whole control flow ix is const. which is not rquivalent to zig. so how to have non const branches?

the__alchemist

I have no idea what that's trying to do. A demonstration that rust is a large language with different dialects! A terse statement with multiple things I don't understand:

  - Assigning a const conditionally?
  - Naming a const _ ?
  - () as a type?
  - Assigning a panic to a constant (or variable) ?
To me it might as well be:

  fn main() {
    match let {
        if ()::<>unimplemented!() -> else;
    }
}

Ygg2

Why would it? If I recall correctly, const and static stuff basically gets inlined at the beginning of the program.

38

[dead]