Hacker News new | ask | show | jobs
by pron 432 days ago
Yes!

To me, the uniqueness of Zig's comptime is a combination of two things:

1. comtpime replaces many other features that would be specialised in other languages with or without rich compile-time (or runtime) metaprogramming, and

2. comptime is referentially transparent [1], that makes it strictly "weaker" than AST macros, but simpler to understand; what's surprising is just how capable you can be with a comptime mechanism with access to introspection yet without the referentially opaque power of macros.

These two give Zig a unique combination of simplicity and power. We're used to seeing things like that in Scheme and other Lisps, but the approach in Zig is very different. The outcome isn't as general as in Lisp, but it's powerful enough while keeping code easier to understand.

You can like it or not, but it is very interesting and very novel (the novelty isn't in the feature itself, but in the place it has in the language). Languages with a novel design and approach that you can learn in a couple of days are quite rare.

[1]: In short, this means that you get no access to names or expressions, only the values they yield.

6 comments

I was a bit confused by the remark that comptime is referentially transparent. I'm familiar with the term as it's used in functional programming to mean that an expression can be replaced by its value (stemming from it having no side-effects). However, from a quick search I found an old related comment by you [1] that clarified this for me.

If I understand correctly you're using the term in a different (perhaps more correct/original?) sense where it roughly means that two expressions with the same meaning/denotation can be substituted for each other without changing the meaning/denotation of the surrounding program. This property is broken by macros. A macro in Rust, for instance, can distinguish between `1 + 1` and `2`. The comptime system in Zig in contrast does not break this property as it only allows one to inspect values and not un-evaluated ASTs.

[1]: https://news.ycombinator.com/item?id=36154447

Yes, I am using the term more correctly (or at least more generally), although the way it's used in functional programming is a special case. A referentially transparent term is one whose sub-terms can be replaced by their references without changing the reference of the term as a whole. A functional programming language is simply one where all references are values or "objects" in the programming language itself.

The expression `i++` in C is not a value in C (although it is a "value" in some semantic descriptions of C), yet a C expression that contains `i++` and cannot distinguish between `i++` and any other C operation that increments i by 1, is referentially transparent, which is pretty much all C expressions except for those involving C macros.

Macros are not referentially transparent because they can distinguish between, say, a variable whose name is `foo` and is equal to 3 and a variable whose name is `bar` and is equal to 3. In other words, their outcome may differ not just by what is being referenced (3) but also by how it's referenced (`foo` or `bar`), hence they're referentially opaque.

Those are equivalent, I think. If you can replace an expression by its value, any two expressions with the same value are indistinguishable (and conversely a value is an expression which is its own value).
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.

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.

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.

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.

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.

Comptime is often pushed as being something extraordinarily special, when it's not. Many other languages have similar. Jai, Vlang, Dlang, etc...

What could be argued, is if Zig's version of it is comparatively better, but that is a very difficult argument to make. Not only in terms of how different languages are used, but something like an overall comparison of features looks to be needed in order to make any kind of convincing case, beyond hyping a particular feature.

You didn't read the article because that's the argument being made (whether you think these points have merit) :

> My understanding is that Jai, for example, doesn’t do this, and runs comptime code on the host.

> Many powerful compile-time meta programming systems work by allowing you to inject arbitrary strings into compilation, sort of like #include whose argument is a shell-script that generates the text to include dynamically. For example, D mixins work that way:

> And Rust macros, while technically producing a token-tree rather than a string, are more or less the same

The comment made by me, is a reply to another reader, not of the article directly. The push back was on the nature of their comment.

> the uniqueness of Zig's comptime... > You can like it or not, but it is very interesting and very novel...

While true, such features in Zig can be interesting, they are not particularly novel (as other highly knowledgeable readers have pointed out). Zig's comptime is often marketed or hyped as being special, while overlooking that other languages often do similar, but have their own perspectives and reasoning on how metaprogramming and those type of features fit into their language. Not to mention, metaprogramming has its downsides too. It's not all roses.

The article does seek to make comparisons with other languages, but arguably out of context, as to what those languages are trying to achieve with their feature sets. Comptime should not be looked at in a bubble, but as part of the language as a whole.

A language creator with an interesting take on metaprogramming in general, is Ginger Bill (of Odin). Who often has enthusiasts attempt to pressure him into making more extensive use of it in his language, but he pushes back because of various problems it can cause, and has argued he often comes up with optimal solutions without it. There are different sides to the story, in regards to usage and goals, relative to the various languages being considered.

I’ve never managed to understand your year-long[1] manic praise over this feature. Given that you’re a language implementer.

It’s very cool to be able to just say “Y is just X”. You know in a museum. Or at a distance. Not necessarily as something you have to work with daily. Because I would rather take something ranging from Java’s interface to Haskell’s typeclasses since once implemented, they’ll just work. With comptime types, according to what I’ve read, you’ll have to bring your T to the comptime and find out right then and there if it will work. Without enough foresight it might not.

That’s not something I want. I just want generics or parametric polymorphism or whatever it is to work once it compiles. If there’s a <T> I want to slot in T without any surprises. And whether Y is just X is a very distant priority at that point. Another distant priority is if generics and whatever else is all just X undernea... I mean just let me use the language declaratively.

I felt like I was on the idealistic end of the spectrum when I saw you criticizing other languages that are not installed on 3 billion devices as too academic.[2] Now I’m not so sure?

[1] https://news.ycombinator.com/item?id=24292760

[2] But does Scala technically count since it’s on the JVM though?

My "manic praise" extends to the novelty of the feature as Zig's design is revolutionary. It is exciting because it's very rare to see completely novel designs in programming languages, especially in a language that is both easy to learn and intended for low-level programming.

I wait 10-15 years before judging if a feature is "good"; determining that a feature is bad is usually quicker.

> With comptime types, according to what I’ve read, you’ll have to bring your T to the comptime and find out right then and there if it will work. Without enough foresight it might not.

But the point is that all that is done at compile time, which is also the time when all more specialised features are checked.

> That’s not something I want. I just want generics or parametric polymorphism or whatever it is to work once it compiles.

Again, everything is checked at compile-time. Once it compiles it will work just like generics.

> I mean just let me use the language declaratively.

That's fine and expected. I believe that most language preferences are aesthetic, and there have been few objective reasons to prefer some designs over others, and usually it's a matter of personal preference or "extra-linguistic" concerns, such as availability of developers and libraries, maturity, etc..

> Now I’m not so sure?

Personally, I wouldn't dream of using Zig or Rust for important software because they're so unproven. But I do find novel designs fascinating. Some even match my own aesthetic preferences.

> But the point is that all that is done at compile time, which is also the time when all more specialised features are checked.

> ...

> Again, everything is checked at compile-time. Once it compiles it will work just like generics.

No. My compile-time when using a library with a comptime type in Zig is not guaranteed to work because my user experience could depend on if the library writer tested with the types (or compile-time input) that I am using.[1] That’s not a problem in Java or Haskell: if the library works for Mary it will work for John no matter what the type-inputs are.

> That's fine and expected. I believe that most language preferences are aesthetic, and there have been few objective reasons to prefer some designs over others, and usually it's a matter of personal preference or "extra-linguistic" concerns, such as availability of developers and libraries, maturity, etc..

Please don’t retreat to aesthetics. What I brought up is a concrete and objective user experience tradeoff.

[1] based on https://strongly-typed-thoughts.net/blog/zig-2025#comptime-i...

> No. My compile-time when using a library with a comptime type in Zig is not guaranteed to work because my user experience could depend on if the library writer tested with the types (or compile-time input) that I am using.[1] That’s not a problem in Java or Haskell: if the library works for Mary it will work for John no matter what the type-inputs are.

What you're saying isn't very meaningful. Even generics may impose restrictions on their type parameters (e.g. typeclasses in Zig or type bounds in Java) and don't necessarily work for all types. In both cases you know at compile-time whether your types fit the bounds or not.

It is true that the restrictions in Haskell/Java are more declarative, but the distinction is more a matter of personal aesthetic preference, which is exactly what's expressed in that blog post (although comptime is about as different from C++ templates as it is from Haskell/Java generics). Like anything, and especially truly novel approaches, it's not for everyone's tastes, but neither are Java, Haskell, or Rust, for that matter. That doesn't make Zig's approach any less novel or interesting, even if you don't like it. I find Rust's design unpalatable, but that doesn't mean it's not interesting or impressive, and Zig's approach -- again, like it or not -- is even more novel.

> What you're saying isn't very meaningful. Even generics may impose restrictions on their type parameters (e.g. typeclasses in Zig or type bounds in Java) and don't necessarily work for all types. In both cases you know at compile-time whether your types fit the bounds or not.

Java type-bounds is what I mean with declarative. The library author wrote them, I know them, I have to follow them. It’s all spelled out. According to the link that’s not the case with the Zig comptime machinery. It’s effectively duck-typed from the point of view of the client (declaration).

I also had another source in mind which explicitly described how Zig comptime is “duck typed” but I can’t seem to find it. Really annoying.

> It is true that the restrictions in Haskell/Java are more declarative, but the distinction is more a matter of personal aesthetic preference, which is exactly what's expressed in that blog post (although comptime is about as different from C++ templates as it is from Haskell/Java generics).

It’s about as aesthetic as having spelled out reasons (usability) for preferring static typing over dynamic typing or vice versa. It’s really not. At all.

> , but that doesn't mean it's not interesting or impressive, and Zig's approach -- again, like it or not -- is even more novel.

I prefer meaningful leaps forward in programming language usability over supposed most-streamlined and clever approaches (comptime all the way down). I guess I’m just a pragmatist in that very narrow area.

> According to the link that’s not the case with the Zig comptime machinery. It’s effectively duck-typed from the point of view of the client (declaration).

It is "duck-typed", but it is checked at compile time. Unlike ducktyping in JS, you know whether or not your type is a valid argument just as you would for Java type bounds -- the compiler lets you know. Everything is also all spelled out, just in a different way.

> It’s about as aesthetic as having spelled out reasons (usability) for preferring static typing over dynamic typing or vice versa. It’s really not. At all.

But everything is checked statically, so all the arguments of failing fast apply here, too.

> I prefer meaningful leaps forward in programming language usability over supposed most-streamlined and clever approaches (comptime all the way down). I guess I’m just a pragmatist in that very narrow area.

We haven't had "meaningful leaps forward in programming language usability" in a very long time (and there are fundamental reasons for that, and indeed the situation was predicted decades ago). But if we were to have a meaningful leap forward, first we'd need some leap forward and then we could try learning how meaningful it is (which usually takes a very long time). I don't know that Zig's comptime is a meaningful leap forward or not, but as one of the most novel innovations in programming languages in a very long time, at least it's something that's worth a look.

Do you have a source for "criticizing other languages not installed on 3 billion devices as too academic" ?

Without more context, this comment sounds like rehashing old (personal?) drama.

pron has been posting about programming languages for years and years, here, in public, for all to see. I guess reading them makes it personal? (We don’t know each other)

The usual persona is the hard-nosed pragmatist[1] who thinks language choice doesn’t matter and that PL preference is mostly about “programmer enjoyment”.

[1] https://news.ycombinator.com/item?id=16889706

Edit: The original claim might have been skewed. Due to occupation the PL discussions often end up being about Java related things, and the JVM language which is criticized has often been Scala specifically. Here he recommends Kotlin over Scala (not Java): https://news.ycombinator.com/item?id=9948798

> Because I would rather take something ranging from Java’s interface to Haskell’s typeclasses since once implemented, they’ll just work. With comptime types, according to what I’ve read, you’ll have to bring your T to the comptime and find out right then and there if it will work. Without enough foresight it might not.

This was perhaps a bad comparison and I should have compared e.g. Java generics to Zig’s comptime T.

I'm sorry but I don't understand what you're complaining about comptime. All the stuff you said you wanted to work (generic, parametric polymorphism, slotting <T>, etc) just work with comptime. People are praising about comptime because it's a simple mechanism that replacing many features in other languages that require separate language features. Comptime is very simple and natural to use. It can just float with your day to day programming without much fuss.
comptime can’t outright replace many language features because it chooses different tradeoffs to get to where it wants. You get a “one thing to rule all” at the expense of less declarative use.

Which I already said in my original comment. But here’s a source that I didn’t find last time: https://strongly-typed-thoughts.net/blog/zig-2025#comptime-i...

Academics have thought about evaluating things at compile time (or any time) for decades. No, you can’t just slot in eval at a weird place that no one ever thought of (they did) and immediately solve a suite of problems that other languages use multiple discrete features for (there’s a reason they do that).

> comptime can’t outright replace many language features because it chooses different tradeoffs to get to where it wants.

You're missing the point. I don't have any theory to qualify this, but:

I've worked in a language with lisp-ey macros, and I absolutely hate hate hate when people build too-clever DSLs that hide a lot of weird shit like creating variable names or pluralizing database tables for me, swapping camel-case and snake case, creating a ton of logic under the hood that's hard to chase.

Zig's comptime for the most part shys you away from those sorts of things. So yes, it's not fully feature parity in the language theory sense, but it really blocks you or discourages you away from shit you don't need to do, please for the love of god don't. Hard to justify theoretically. it's real though.

It's just something you notice after working with it for while.

No, you are clearly missing the point because I laid out concrete critiques about how Zig doesn’t replace certain concrete language features with One Thing to Rule Them All. All in reply to someone complimenting Zig on that same subject.

That you want to make a completely different point about macros gone wild is not my problem.

Regarding 2. How are comptime values restricted to total computations? Is it just by the fact that the compiler actually finished, or are there any restrictions on comptime evaluations?
Yes, comptime evaluation is restricted to a configurable number of back-branches. 1000 by default, I think.
They don't need to be restricted to total computation to be referentially transparent. Non-termination is also a reference.
Has anyone grafted Zig style macros into Common Lisp?
That wouldn't be very meaningful. The semantics of Zig's comptime is more like that of subroutines in a dynamic language - say, JavaScript functions - than that of macros. The point is that it's executed, and yields errors, at a different phase, i.e. compile time.
You can do that, I think, with EVAL-WHEN.
The Scopes language might be similar to what you're asking about. Its notion of "spices" which complement the "sugars" feature is a similar kind of constant evaluation. It's not a Common Lisp dialect, though, but it is sexp based.
Isn’t this kind of thing sort of the default thing in Lisp? Code is data so you can transform it.
There are no limitations on the transformations in lisp. That can make macros very hard to understand. And hard for later program transformers to deal with.

The innovation in Zig is the restrictions that limit the power of macros.

Lisp is so powerful, but without static types you can't even do basic stuff like overloading, and have to invent a way to even check the type(for custom types) so you can branch on type.
> Lisp is so powerful, but <tired old shit from someone who's never used Lisp>.

You use defmethod for overloading. Types check themselves.

And a modern compiler will jmp past the type checks if the inferencer OKs it!
Which the inferencer probably can't do because of how dynamic standard-class can be. Also, if we want to get pedantic, method-dispatch does not dispatch on types in Common Lisp, but rather via EQL or the class of the argument. Since sub-classes can be made (and methods added or even removed) after a method invocation is compiled, there is no feasible way in a typical lisp implementation[1] to do compile-time dispatch of any type that is a subtype of standard-object.

Now none of this prevents you from extending lisp in such a way that lets you freeze the method dispatch (see e.g. https://github.com/alex-gutev/static-dispatch), but "a modern compiler will jmp past the type checks" is false for all of the CLOS implementations I'm familiar with.

1: SICL is a research implementation that has first-class global environments. If you save the global-environment of a method invocation, you can re-compile the invocation whenever a method is defined (or removed) and get static-dispatch. There's a paper somewhere (probably on metamodular?) that discusses this possibility.

> but without static types

So add static types.

https://github.com/coalton-lang/coalton

No need for overloading when you have CLOS and multi-method dispatch.
There isn't really as clear of a distinction between "runtime" and "compile time" in Lisp. The comptime keyword is essentially just the opposite of quote in Lisp. Instead of using comptime to say what should be evaluated early, you use quote to say what should be evaluated later. Adding comptime to Lisp would be weird (though obviously not impossible, because it's Lisp), because that is essentially the default for expressions.
Since we are specifically speaking about Common Lisp, there certainly is; see e.g. https://www.lispworks.com/documentation/HyperSpec/Body/s_eva...
The truth of this varies between Lisp based languages.