Hacker News new | ask | show | jobs
by WalterBright 424 days ago
It's not novel. D pioneered compile time function execution (CTFE) back around 2007. The idea has since been adopted in many other languages, like C++.

One thing it is used for is generating string literals, which then can be fed to the compiler. This takes the place of macros.

CTFE is one of D's most popular and loved features.

5 comments

It is novel to the point of being revolutionary. As I wrote in my comment, "the novelty isn't in the feature itself, but in the place it has in the language". It's one thing to come up with a feature. It's a whole other thing to position it within the language. Various compile-time evaluations are not even remotely positioned in D, Nim, or C++ as they are in Zig. The point of Zig's comptime is not that it allows you to do certain computations at compile-time, but that it replaces more specialised features such as templates/generics, interfaces, macros, and conditional compilation. That creates a completely novel simplicity/power balance.

If the presence of features is how we judge design, then the product with the most features would be considered the best design. Of course, often the opposite is the case. The absence of features is just as crucial for design as their presence. It's like saying that a device with a touchscreen and a physical keyboard has essentially the same properties as a device with only a touchscreen.

If a language has a mechanism that can do exactly what Zig's comptime does but it also has generics or templates, macros, and/or conditional compilation, then it doesn't have anything resembling Zig's comptime.

> Various compile-time evaluations are not even remotely positioned in D, Nim, or C++ as they are in Zig.

See my other reply. I don't understand your comment.

https://news.ycombinator.com/item?id=43748490

The revolution in Zig isn't in what the comptime mechanism is able to do, but how it allows the language to not have other features, which is what gives that language its power to simplicity ratio.

Let me put it like this: Zig's comptime is a general compilation time computation mechanism that has introspection capabilities and replaces generics/templates, interfaces/typeclasses, macros, and conditional compilation.

It's like that the main design feature of some devices is that they have a touchscreen but not a keyboard. The novelty isn't the touchscreen; it's in the touchscreen eliminating the keyboard. The touchscreen itself doesn't have to be novel; the novelty is how it's used to eliminate the keyboard. If your device has a touchscreen and a keyboard, then it does not have the same design feature.

Zig's novel comptime is a mechanism that eliminates other specialised features, and if these features are still present, then your language doesn't have Zig's comptime. It has a touchscreen and a keyboard, whereas Zig's novelty is a touchscreen without a keyboard.

The example of a comptime parameter to a function is a template, whether you call it that or not :-/ A function template is a function with compile time parameters.

The irony here is back in the 2000's, many programmers were put off by C++ templates, and found them to be confusing. Myself included. But when I (belatedly) realized that function templates were functions with compile time parameters, I had an epiphany:

Don't call them templates! Call them functions with compile time parameters. The people who were confused by templates understood that immediately. Then later, after realizing that they had been using templates all along, became comfortable with templates.

BTW, I wholeheartedly agree that it is better to have a small set of features that can do the same thing as a larger set of features. But I'm not seeing how comptime is accomplishing that.

> But I'm not seeing how comptime is accomplishing that.

Because Zig does the work of C++'s templates, macros, conditional compilation, constexprs, and concepts with one relatively simple feature.

From the article:

    fn print(comptime T: type, value: T) void {
That's a template. In D it looks like:

    void print(T)(T value) {
which is also a template.
If I understand TFA correctly, the author claims that D’s approach is actually different: https://matklad.github.io/2025/04/19/things-zig-comptime-won...

“In contrast, there’s absolutely no facility for dynamic source code generation in Zig. You just can’t do that, the feature isn’t! [sic]

Zig has a completely different feature, partial evaluation/specialization, which, none the less, is enough to cover most of use-cases for dynamic code generation.”

The partial evaluation/specialization is accomplished in D using a template. The example from the link:

    fn f(comptime x: u32, y: u32) u32 {
        if (x == 0) return y + 1;
        if (x == 1) return y * 2;
        return y;
    }
and in D:

    uint f(uint x)(uint y) {
        if (x == 0) return y + 1;
        if (x == 1) return y * 2;
        return y;
    }
The two parameter lists make it a function template, the first set of parameters are the template parameters, which are compile time. The second set are the runtime parameters. The compile time parameters can also be types, and aliased symbols.
Here is, I think, an interesting example of the kind of thing TFA is talking about. In case you’re not already familiar, there’s an issue that game devs sometimes struggle with, where, in C/C++, an array of structs (AoS) has a nice syntactic representation in the language and is easy to work with/avoid leaks, but a struct of arrays (SoA) has a more compact layout in memory and better performance.

Zig has a library to that allows you to have an AoS that is laid out in memory like a SoA: https://zig.news/kristoff/struct-of-arrays-soa-in-zig-easy-i... . If you read the implementation (https://github.com/ziglang/zig/blob/master/lib/std/multi_arr...) the SoA is an elaborately specialized type, parameterized on a struct type that it introspects at compile time.

It’s neat because one might reach for macros for this sort of the thing (and I’d expect the implementation to be quite complex, if it’s even possible) but the details of Zig’s comptime—you can inspect the fields of the type parameter struct, and the SoA can be highly flexible about its own fields—mean that you don’t need a macro system, and the Zig implementation is actually simpler than a macro approach probably would be.

D doesn't have a macro system, either, so I don't understand what you mean.
IIUC, it does have code generation—the ability to generate strings at compile-time and feed them back into the compiler.

The argument that the author of TFA is making is that Zig’s comptime is a very limited feature (which, they argue, is good. It restricts users from introducing architecture dependencies/cross-compilation bugs, is more amenable to optimization, etc), and yet it allows users to do most of the things that more general alternatives (such as code generation or a macro system) are often used for.

In other words, while Zig of course didn’t invent compile-time functions (see lisp macros), it is notable and useful from a PL perspective if Zig users are doing things that seem to require macros or code generation without actually having those features. D users, in contrast, do have code generation.

Or, alternatively, while many languages support metaprogramming of some kind, Zig’s metaprogramming language is at a unique maxima of safety (which macros and code generation lack) and utility (which e.g. Java/Go runtime reflection, which couldn’t do the AoS/SoA thing, lack)

Edit Ok, I think Zig comptime expressions are just like D templates, like you said. The syntax is nicer than C++ templates. Zig’s “No host leakage” (to guarantee cross-compile-ability) looks like the one possibly substantively different thing.

> Zig’s “No host leakage” (to guarantee cross-compile-ability) looks like the one possibly substantively different thing.

That is a good idea, but could be problematic if one relies on size_t, which changes in size from 32 to 64 bit. D's CTFE adds checks for undefined behavior, such as shifting by more bits than are in the type being shifted. These checks are not done at runtime for performance reasons.

D's CTFE also does not allow calling the operating system, and only works on functions that are "pure".

Using a different type vs. a different syntax can be an important usability consideration, particularly since D also has templates and other features, where Zig provides only the comptime type for all of them. Homogeneity can also be a nice usability win, though there are downsides as well.
Zig's use of comptime in a function argument makes it a template :-/

I bet if you use such a function with different comptime arguments, compile it, and dump the assembler you'll see that function appearing multiple times, each with somewhat different code generated for it.

> Zig's use of comptime in a function argument makes it a template :-/

That you can draw an isomorphism between two things does not mean they are ergonomically identical.

When we're responding to quite valid points about other languages having essentially the same features as Zig with subjective claims about ergonomics, the idea that Zig comptime is "revolutionary" is looking awfully flimsy. I agree with Walter: Zig isn't doing anything novel. Picking some features while leaving others out is something that every language does; if doing that is enough to make a language "revolutionary", then every language is revolutionary. The reality is a lot simpler and more boring: for Zig enthusiasts, the set of features that Zig has appeals to them. Just like enthusiasts of every programming language.
that's a comically archaic way of using the verb 'to be', not a grammatical error. you see it in phrases like "to be or not to be", or "i think, therefore i am". "the feature isn't" just means it doesn't exist.
Damn, beat me by half a day.
Sure, CTFE can be used to generate strings, then later "mixed-in" as source code, but also can be used to execute normal functions and then the result can be stored in a compile-time constant (in D that's the `enum` storage class), for example generating an array using a function literal called at compile-time:

   enum arr = { return iota(5).map!(i => i * 10).array; }();
   static assert(arr == [0,10,20,30,40]);
> the feature isn’t! [sic]

To be, or not to be... The feature is not.

(IOW, English may not be the author's native language. I'm fairly sure it means "The feature doesn't exist".)

A little bit out of context, I just want to thank you and all the contributors for the D programming language.
That means a lot to us. Thanks!
> D pioneered compile time function execution (CTFE) back around 2007

Pioneered? Forth had that in the 1970s, lisp somewhere in the 1960s (I’m not sure whether the first versions of either had it, so I won’t say 1970 respectively 1960), and there may be other or even older examples.

True, but consider that Forth and Lisp started out as interpreted languages, meaning the whole thing can be done at compile time. I haven't seen this feature before in a language that was designed to be compiled to machine code, such as C, Pascal, Fortran, etc.

BTW, D's ImportC C compiler does CTFE, too!! CTFE is a natural fit for C, and works like a champ. Standard C should embrace it.

Nitpick: Lisp didn’t start out as an interpreted language. It started as an idea from a theoretical computer scientist, and wasn’t supposed to be implemented. https://en.wikipedia.org/wiki/Lisp_(programming_language)#Hi...:

"Steve Russell said, look, why don't I program this eval ... and I said to him, ho, ho, you're confusing theory with practice, this eval is intended for reading, not for computing. But he went ahead and did it. That is, he compiled the eval in my paper into IBM 704 machine code, fixing bugs, and then advertised this as a Lisp interpreter, which it certainly was. So at that point Lisp had essentially the form that it has today”

Steve Russell comment is about writing the actual interpreter for Lisp, so the comment is correct on that Lisp started out as an interpreted language, and even mentions that in the comment that you quote.

The interpreter (i.e. Lisp READ-EVAL-PRINT loop) was written in IBM 704 machine code, which just was a reimplementation of the EVAL function as JMC described it in his paper.

The part that "wasn't supposed to be implemented" was about the form of Lisp, M-expressions vs. S-expressions not Lisp it self. Which was supposed to be implemented as a programming system from day one.

You're missing the point. If anything D is littered with features and feature bloat (CTFE included). Zig (as the author of the blog mentions) is more than somewhat defined by what it can't do.
I fully agree that the difference is a matter of taste.

All living languages accrete features over time. D started out as a much more modest language. It originally eschewed templates and operator overloading, for example.

Some features were abandoned, too, like complex numbers and the "bit" data type.