[HN Gopher] Crabtime: Zig's Comptime in Rust
       ___________________________________________________________________
        
       Crabtime: Zig's Comptime in Rust
        
       Author : klaussilveira
       Score  : 378 points
       Date   : 2025-03-19 18:44 UTC (3 days ago)
        
 (HTM) web link (crates.io)
 (TXT) w3m dump (crates.io)
        
       | weinzierl wrote:
       | I love the logo, it is brilliant.
       | 
       | Making Rust's macros easier is laudable. Purely from a user's
       | perspective I find it especially annoying, that proc macros need
       | their own crate, even if I understand the reasons for it. If I
       | read Crabtime correctly it solves that problem, which is nice.
       | 
       | That being said Crabtime looks more like compile time eval on
       | steroids to me than an analogon to Zig's comptime.
       | 
       | One (maybe _the_ ) distinguishing feature between comptime in Zig
       | and Rust macros seems to me to be access to type information. In
       | Zig you have it[1] in Rust you don't and that makes a big
       | difference. It doesn't look like we will get that in Rust anytime
       | soon and the projects that need it (e.g. cargo semver check) use
       | dirty tricks (parsing RustDoc from the macro) to accomplish it. I
       | did not see anything like that in Crabtime, but I might have
       | missed it. At any rate, I'd expect compile time reflection for
       | anything that claims to bring comptime to Rust.
       | 
       | [1] I think, but I am not a Zig expert, so please correct me if I
       | am wrong.
        
         | nukem222 wrote:
         | How on earth does zig resolve types before macros? Must be some
         | ~~nuts~~ novel order of evaluation to get that behavior. How is
         | this intended to function? Are there multiple layers of macros?
         | Do you have to declare said level or is it derived? How do you
         | use macros to define types or declare types of variables? Can
         | you use said types in other macros?
        
           | pfg_ wrote:
           | Zig doesn't have macros, it has functions which can be run at
           | comptime. You can make a function that returns a type and
           | call it from another function. All declarations are only
           | analyzed when they are first used, and functions when called
           | at comptime are memoized based on their arguments. The order
           | of evaluation is really simple and predictable.
        
             | nukem222 wrote:
             | > Zig doesn't have macros, it has functions which can be
             | run at comptime
             | 
             | You raised my hopes and dashed them quite expertly, sir.
             | Bravo!
        
               | gliptic wrote:
               | You're probably underestimating what you can do with
               | these.
        
             | weinzierl wrote:
             | Just for completeness: Rust has functions which can be run
             | at comptime as well. They are called _const fn_ and Rust
             | has them out of the box, no crate required. They are also
             | true Rust and not macros with a separate syntax.
             | 
             | They are still not an adequate substitute for Zig's
             | comptime feature. For one and in a sense they are much more
             | limited than comptime functions in Zig but for another (and
             | for better or worse) they also have much higher aspirations
             | than Zig.
             | 
             | const fn must always be able to be run at compile time or
             | run time and always produce bit-identical results. This is
             | much harder than it looks at first glance because it must
             | also uphold in a cross-compiling scenario where the compile
             | time environment can be vastly different from the run time
             | environment.
             | 
             | This requirement also forbids any kind of side effect, so
             | Rust const fn are essentially pure functions and I've heard
             | them called like that.
        
               | kzrdude wrote:
               | Rust has compile time environment variables and const fns
               | can parse them. It's one very nice and easy way to
               | experiment with or configure rust code at compile time,
               | and should be explored more.
        
               | Zambyte wrote:
               | Reading environment variables contradicts my mental model
               | of pure functions. Are they just not pure functions then?
               | Or pure-ish?
        
               | kzrdude wrote:
               | The functions are pure but they take an input from a
               | built-in (looks like a macro) that reads the environment
               | variable at compile time.
               | 
               | Also, you only compile once, so how could you tell the
               | difference? You could say - if it was using const fn that
               | it's a "templated" function that depends on compile time
               | settings.
        
               | Zambyte wrote:
               | I see, I guess if you can't set environment variables
               | during build time that makes it pure-ish enough.
        
               | vlovich123 wrote:
               | You can set environment variables in your build.rs / the
               | user sets it like A=b cargo build.
        
               | dwattttt wrote:
               | There's two lookups that can occur, distinguishing them
               | makes it clearer.
               | 
               | Looking up the value of an environment variable at
               | runtime is not a const operation, and produces an error
               | if you try to do it in a const fn.
               | 
               | Looking up the value of an environment variable during
               | compile time _can_ be done in a const context, but it'll
               | only happen once. The environment should be considered an
               | input to a const fn, and that makes it "pure".
               | 
               | EDIT: These two operations can both be done in non-const
               | functions too, they're different functions (well, one's a
               | macro).
        
               | johnisgood wrote:
               | So if Rust has Zig's comptime feature, what is this
               | crate? How does it differ, what does it add?
        
               | IshKebab wrote:
               | Rust _doesn 't_ have Zig's comptime feature. Rust's const
               | fn's are normal functions that are _capable_ of running
               | at compile time. It 's an optional optimisation; it
               | doesn't exist any additional semantic capabilities
               | because they also need to be able to run at runtime.
               | 
               | Zig's comptime functions _only_ run at compile time, so
               | they can do extra things - in particular manipulating
               | types - that you can 't do if your function needs to run
               | at runtime. (Don't mention dependent types.)
        
               | johnisgood wrote:
               | Okay, so this crate adds Zig's actual comptime?
        
               | weinzierl wrote:
               | No, it doesn't because it is based on Rust macros which
               | are strictly less capable than Zig comptime in a crucial
               | way (compile time reflection).
               | 
               | Neither Rust macros nor const fn are 100% what Zig
               | comptime is but they have other properties that Zig
               | comptime lacks. Apples and oranges.
        
               | zozbot234 wrote:
               | Note that dependently-typed code also effectively "runs
               | at compile-time", it's inherent to that programming
               | model. You can "extract" an ordinary program from
               | dependently-typed code which you can then compile to a
               | binary and run as usual, but then that program will not
               | feature dependent types in their full generality.
        
               | bsder wrote:
               | > Zig's comptime functions only run at compile time, so
               | they can do extra things - in particular manipulating
               | types - that you can't do if your function needs to run
               | at runtime.
               | 
               | Careful, I'm not sure this is true. I haven't found a Zig
               | comptime function that doesn't also work just as well at
               | runtime function.
               | 
               | This is, in fact, the primary characteristic that makes
               | Zig comptime easier to reason about than any "macro"
               | system. If something is wrong in my comptime function, I
               | can normally make a small adjustment to force it to be a
               | runtime function that I can step through and probe and
               | debug.
               | 
               | It's sort of a unification of compile time and run time
               | semantics and it is _long_ overdue. The late John Shutt
               | 's Scheme-alike Kernel
               | (https://web.cs.wpi.edu/~jshutt/kernel.html) sort of
               | approached this as did old-school Tcl.
        
               | IshKebab wrote:
               | Yeah I don't really understand why Rust copied that from
               | C++, where constexpr functions only _might_ run at
               | compile time.
               | 
               | C++ ended up having to add consteval and constinit which
               | really are compile-time.
        
               | kibwen wrote:
               | It's the other way around. Rust has always had contexts
               | that are guaranteed to be compile-time (const and static
               | items), and gradually added the ability to run some
               | subset of the language at compile-time (const fn)
               | specifically to accommodate const/static items (e.g. to
               | replace the old lazy_static with the modern LazyLock),
               | and naturally also allows these functions to run at
               | runtime if you want (and in what would otherwise be a
               | runtime context, you can enforce compile-time evaluation
               | with a const block).
        
               | tialaramex wrote:
               | Huh? What is it you think Rust copied here? I agree that
               | the choice in C++ is essentially worthless, so that in
               | practice you can write functions which are definitely
               | never executed at compile time and aren't constant in any
               | sense, label them constexpr and that compiles anyway. It
               | just becomes yet more noise C++ programmers learn to type
               | by reflex to get the correct behaviour from their
               | compiler, joining explicit.
               | 
               | But in Rust that's not what you're getting. Rust's const
               | fn is none of the options C++ decided it needed, Rust
               | says _if_ the parameters are themselves constants _then_
               | we promise we can evaluate this at compile time and if
               | appropriate we will -- this means we can use Rust 's
               | const fn where we'd use C++ consteval, but the function
               | can also be called at runtime with variable parameters -
               | and we can use Rust's const where we'd use C++ constinit,
               | calling these const fn with constant parameters.
               | 
               | Because Rust is more explicit about safety of course, we
               | can often get away with claiming some value is "constant"
               | in C++ despite actually figuring out what it is at
               | runtime, and Rust isn't OK with that, for example in my
               | code                  const OVERSIZE: u32 =
               | SIG_BITS.next_power_of_two() << 1;
               | 
               | We can just calculate what power of two is bigger than
               | SIG_BITS and shift it left at compile time. But...
               | pub(super) static SHORT_80: LazyLock<Rational> =
               | LazyLock::new(|| Rational::fraction(1, 80).unwrap());
               | 
               | The Rational type is a big rational, it owns heap
               | allocations so we'll just make one once, at runtime, and
               | then re-use it whenever we need this particular fraction
               | (it's for calculating natural logarithms of arbitrary
               | computable real numbers).
        
               | IshKebab wrote:
               | > Rust says if the parameters are themselves constants
               | then we promise we can evaluate this at compile time and
               | if appropriate we will
               | 
               | Well exactly. "if appropriate". So like C++'s
               | `constexpr`, Rust doesn't make any guarantees about
               | compile-time evaluation.
               | 
               | Zig's `comptime` _must_ be evaluated at compile time.
        
               | kibwen wrote:
               | _> Zig 's `comptime` must be evaluated at compile time._
               | 
               | Yes, and the equivalent in Rust is any constant context,
               | such as a const item, or a const block inside of a non-
               | const function. Anything in a constant context is
               | guaranteed to run at compile-time.
        
               | tialaramex wrote:
               | If we want a constant context, we can say so. Because
               | Rust is expression oriented we can write for example a
               | loop (though if you're unfamiliar with Rust it may not be
               | clear why a _for_ loop can 't work yet, other loops are
               | fine) and wrap the whole expression in a const block and
               | that'll be evaluated at compile time. For example:
               | let a = const {             let mut x: u64 = 0;
               | let mut k = 5;             loop {                 if k ==
               | 0 {                     break x;                 }
               | k -= 1;                 x += 2;                 x *= x;
               | }         };
        
               | tyilo wrote:
               | Floats are not guaranteed to be bit-identical at compile
               | time and run time in Rust.
        
               | weinzierl wrote:
               | Last time I checked the float functions that have no bit-
               | identical results (mostly transcendental functions) were
               | missing from Rust's const fn for exactly that reason.
        
               | kibwen wrote:
               | It's not quite as bad as it sounds, because the only
               | difference is that the representation of NaN (the sign
               | and the payload bits) isn't guaranteed to be stable. If
               | you're not relying on any specific representation of NaN,
               | then floating-point math in const fn is identical, and
               | observed differences would be considered a soundness bug
               | in the Rust compiler.
        
               | weinzierl wrote:
               | I had to look this up because it is a while that I tried
               | to use floating point math in a const fn and it seems
               | that the differences you described have been decided to
               | be acceptable.
               | 
               | Looks like floating point math in const fn is coming.
               | Here is the respective tracking issue:
               | https://github.com/rust-lang/rust/issues/128288
        
               | kibwen wrote:
               | It's actually already here, it stabilized last year in
               | 1.82: https://blog.rust-
               | lang.org/2024/10/17/Rust-1.82.0.html#float...
        
               | weinzierl wrote:
               | Oh, nice. Sometime I find it really hard to track the
               | status of new Rust features. For example the tracking
               | issue I linked is still open with "Stabilize" missing.
        
               | throwawaymaths wrote:
               | what happens if you compile on a system that has a
               | different precision than the system you run on? like
               | suppose you compile on a 64 bit system targetting 32 bit
               | embedded with an fp accelerator or a 16 bit system with
               | softfloat?
        
               | kibwen wrote:
               | I'm not personally familiar with the implementation, but
               | Rust's const fn is evaluated using an interpreter called
               | MIRI with its own softfloat implementation, and therefore
               | isn't limited by the precision of the host platform. The
               | act of cross-compilation shouldn't pose a problem, and
               | would be a soundness issue in the compiler if it did.
        
               | throwawaymaths wrote:
               | no the soundness of the compiler is at risk because the
               | _target_ has limitations, not the host.
        
               | codedokode wrote:
               | They are not guaranteed to be precise anyway.
        
               | WhyNotHugo wrote:
               | What Rust is missing is reflection and the ability to
               | define types and functions via code. Zig's comptime is
               | often used for this: to generate code (for example, a
               | serialiser for a type) or to generate types (generics
               | being a typical thing, but lots of other usages are
               | viable).
        
               | vlovich123 wrote:
               | You can define types and functions via code via macros.
               | For example, [1] which creates a new sibling type and
               | injects a ::builder method into your type.
               | 
               | And you can add reflection [2]. So if you can add what
               | you need via crates, is the language actually missing it
               | or is it just not as ergonomic / performant as it needs
               | to be or is it an education problem?
               | 
               | [1] https://docs.rs/typed-builder/latest/typed_builder/
               | 
               | [2] https://docs.rs/reflect/latest/reflect/
        
           | wavemode wrote:
           | The answer is that zig doesn't have macros (i.e. syntactic
           | transformations). Comptime functions in zig are just that -
           | functions which run at compile time. They run after
           | typechecking of existing types, but they are capable of
           | creating new types. Types in Zig are just values. But they're
           | values that don't exist at runtime.
        
           | deredede wrote:
           | Zig's comptime is not macros, it's staged programming /
           | multi-stage programming.
        
             | Ygg2 wrote:
             | Zig comptime is a C++ template done better. It also suffers
             | from similar issues as C++ templates. You can't know
             | function is comptime unless you put it in comptime and it
             | passes.
             | 
             | See https://typesanitizer.com/blog/zig-generics.html
        
               | pjmlp wrote:
               | C++98 templates done better, we are past that now in
               | C++23.
               | 
               | Also D and Circle, done it before Zig.
        
               | andrepd wrote:
               | >we are past that now
               | 
               | Jesus, we very much aren't. The only real step
               | improvement was in C++11 which added constexpr which (in
               | the following years) gradually obviated the need for
               | C++98 style TMP. But there really hasn't been much of a
               | generational improvement since (OK, concepts I guess),
               | and it remains cumbersome and error prone and difficult
               | to debug.
        
               | pjmlp wrote:
               | If I compare C++98 template metaprogramming with tag
               | dispatch, ADL, and SFINAE, with what C++23 offers, it is
               | already a complete different world, even if there are
               | still warts to improve.
        
               | samatman wrote:
               | This blog post is disqualified from any serious
               | discussion, because it doesn't know the distinction
               | between templates, which Zig's comptime constructs are
               | not, and partial evaluation with reified types, which
               | Zig's comptime constructs are.
               | 
               | It's not possible to make a positive contribution after a
               | mistake that basic.
               | 
               | Here's an example of someone getting the design space
               | correct, and therefore contributing to the discussion in
               | a positive way. He doesn't end up liking Zig, for reasons
               | I disagree with, but he does completely evade being not-
               | even-wrong, which is table stakes.
               | 
               | https://hirrolot.github.io/posts/why-static-languages-
               | suffer...
        
               | Ygg2 wrote:
               | > This blog post is disqualified from any serious
               | discussion, because it doesn't know the distinction
               | between templates
               | 
               | Just because a blog doesn't go full type theory doesn't
               | disqualify it from drawing conclusions based on
               | experience and limitations incurred during actual use.
               | 
               | Something can be very well typed but still suck to use.
               | 
               | Intution doesn't need to be based on formal
               | understanding. See Table of elements. Created by grouping
               | elements by behavior, it turned out to be based on
               | electron orbital configuration.
        
               | samatman wrote:
               | The central claim is that Zig's use of comptime is
               | similar enough to templates to conflate them. That's
               | simply incorrect. There's no value in trying to extract
               | information from something which makes such a basic
               | mistake as that, it doesn't contribute to a discussion,
               | it distracts that discussion down a blind alley.
        
               | Ygg2 wrote:
               | I think it's insightful to some extent. The problems
               | encountered in C++ templates apply to Zig's comptime as
               | well. And their solution seem to be along same lines,
               | i.e. add constraints.
               | 
               | Edit: on re-reading the author doesn't understand why
               | negative traits in Rust are a problem (not is basic
               | boolean operation). I think they are abstracting too much
               | and saying cows should be roughly spherical and water
               | should roughly be a superconductor.
        
           | littlestymaar wrote:
           | This isn't a macro, it works as both macros and templates in
           | C++, and regarding types it works the same way as templates
           | in C++.
        
           | SkiFire13 wrote:
           | I'll leave this here, try guessing what this prints:
           | const std = @import("std");              const myType =
           | struct {             const info = @typeInfo(myType);
           | const before = info.Struct.decls.len;                  pub
           | usingnamespace (if (before == 0) struct { pub fn baz() void
           | {} } else struct {});                  const after1 =
           | info.Struct.decls.len;             const after2 =
           | @typeInfo(myType).Struct.decls.len;         };
           | pub fn main() void {             std.debug.print("Hello, {}
           | {} {}!\n", .{ myType.before, myType.after1, myType.after2 });
           | }
        
             | throwawaymaths wrote:
             | well i suppose this is a good part of the reason why
             | usingnamespace is likely to go the way of the dodo, though
             | if i had to guess:
             | 
             | hello: 4, 5, 5
        
         | pjmlp wrote:
         | That is what I really like about the evolution of
         | metaprogramming in C++.
         | 
         | While it started as a hack on how to use templates back in
         | C++98, it has gotten quite usable nowadays in C++23, and the
         | compile time reflection will make it even better.
         | 
         | All without having another language to learn about, as it
         | happens with Rust macros, with its variations, or reliance on
         | 3rd party crates (syn).
        
           | msk-lywenn wrote:
           | How is C++'s template metaprogramming not another language
           | inside C++ today? AFAIK, the syntax and even general logic is
           | still extremely different than regular C++
        
             | pjmlp wrote:
             | constexpr, consteval, if constexpr, requires, auto,... are
             | quite regular C++.
        
           | codedokode wrote:
           | The problem with C++ metaprogramming is that it is pain to
           | read and understand, unless it is your daily job.
        
         | pron wrote:
         | > One (maybe the) distinguishing feature between comptime in
         | Zig and Rust macros seems to me to be access to type
         | information. In Zig you have it[1] in Rust you don't and that
         | makes a big difference.
         | 
         | There are other differences. First, comptime functions _aren
         | 't_ syntactic macros. This makes them much easier to reason
         | about and debug. You could think about them as if they were
         | regular functions running at runtime in a partially-typed
         | language with powerful reflection (their simplicity also means
         | they're weaker than macros, but the point is that you can get
         | very far with that, without taking on the difficulties
         | associated with macros). Second, I think that comptime's
         | uniqueness comes not from what it does in isolation, but that
         | it makes other language features redundant, keeping the entire
         | language simple. This means that with _one_ simple yet just-
         | powerful-enough feature you can do away with several other
         | features.
         | 
         | The end result is that Zig is a very simple language with the
         | expressivity of far more complicated languages. That on its own
         | is not super unusual; in a way, JavaScript is like that, too.
         | But Zig does it in a _low-level_ language, and that 's
         | revolutionary. It is because of its simplicity that people
         | compare Zig to C, but it's as expressive as C++ while also
         | being safer than C++, let alone C.
         | 
         | Adding comptime to an already-complex language misses out on
         | its greatest benefit.
        
           | forks wrote:
           | What are some examples of other language features that
           | comptime makes redundant?
        
             | dhruvrajvanshi wrote:
             | It's generic system for example, is built on top of
             | comptime. A generic struct is just a function that takes a
             | type as an argument and returns a struct.
             | 
             | ``` fn Vec(comptime T: anytype) {                 return
             | struct {               // ...       }
             | 
             | }
             | 
             | ```
             | 
             | IMO having a first class generic type parameter syntax is
             | better but this demonstrates OP's point.
        
               | Wumpnot wrote:
               | It just looks like C++ templates with a slightly
               | different syntax ..
        
               | azakai wrote:
               | Exactly, the point is that C++ added templates as a huge
               | new language feature, while in Zig it is just one of the
               | things that is immediately possible thanks to comptime.
        
               | throwawaymaths wrote:
               | well, not quite since you can pass non-type things of
               | generally _any_ level of data type complexity (as long as
               | it 's comptime-valid, which only excludes certain types
               | of mutation), and do stuff with them that you couldnt in
               | c++.                   fn MyType(T: type, comptime tag:
               | [] u8, comptime count: usize) type {           const
               | capitalized = some_module.capitalize(tag);
               | return struct{             fn name() []const u8 {
               | return capitalized;             }             array:
               | [count]T,           };         }
               | 
               | for example
        
               | Wumpnot wrote:
               | That example looks easy enough to replicate in C++ with
               | consteval + template, basically the same except a few
               | minor syntax changes.
        
               | Maxatar wrote:
               | You absolutely can't do that in C++ with consteval +
               | template. C++ would need support for reflection to do
               | that, and maybe it will get it in 10 years, maybe not,
               | but as of today this would not be possible.
               | 
               | Furthermore, the original argument wasn't about whether
               | something can or can't be done in C++, it was that this
               | one feature in Zig subsumes what would require a
               | multitude of features from C++, such as consteval,
               | templates, SFINAE, type traits, so on so forth...
               | 
               | Instead of having all these disparate features all of
               | which work in subtly different ways, you have one single
               | feature that unifies all of this functionality together.
        
               | steveklabnik wrote:
               | You're not wrong in general here, but C++ is going to get
               | the core of reflection in C++26. I'm not sure enough of
               | the details to know if it supports doing this, however.
               | 
               | Rust on the other hand... that might be ten years.
        
               | throwawaymaths wrote:
               | i didn't use reflection in this example, but note that
               | consteval shouldn't be able to do this because I _mutate_
               | the string; it 's not const at comptime.
        
               | SkiFire13 wrote:
               | Nit: comptime does not replace a proper generic system
               | (i.e. a polymorphic type system), but acts more like a
               | templating system (like the one in C++).
        
             | pron wrote:
             | Generics, interfaces/traits/concepts, macros, conditional
             | compilation, const functions/constexpr. These are four or
             | five different features in C++ or Rust, some of which are
             | quite complex, all expressible as one simple construct:
             | comptime.
        
               | kobebrookskC3 wrote:
               | how does zig express a trait like Send https://doc.rust-
               | lang.org/std/marker/trait.Send.html which ensures that
               | values can safely be moved to another thread, for example
               | when spawning a thread? https://doc.rust-
               | lang.org/std/thread/fn.spawn.html
        
               | throwawaymaths wrote:
               | if you want that you will need a proof checker or
               | something.
        
               | pcwalton wrote:
               | How do you typecheck generics, with type inference, with
               | comptime?
               | 
               | Or, more generally, address all the issues raised in [1].
               | You're saying that comptime can fully replicate all the
               | features that a proper generics system has, which is
               | plainly false.
               | 
               | [1]: https://typesanitizer.com/blog/zig-generics.html
        
               | creata wrote:
               | I don't think pron was saying that Zig has a feature-by-
               | feature match for everything that Rust's generics can do.
               | I think his point is that comptime can handle what the
               | target audience of Zig wants from generics. In that
               | regard, I don't think the criticisms there are that big a
               | deal.
        
               | throwawaymaths wrote:
               | 1. if you wish, you absolutely can check for "extra
               | constraints" on a passed type (or even an anytype
               | parameter) using comptime reflection and the
               | @comptimeError builtin.
               | 
               | 2. if you want to restrict the use of a function to
               | comptime (why you would want to is beyond me) it is
               | possible to do with @inComptime builtin.
               | 
               | the only tricky bit is that your function _could_ try to
               | call a function inaccessible to you because it 's
               | transitively restricted and you'd have a hard time
               | noticing that from the code but it's not possible for
               | that code to be compiled (barring errors by the zig team)
               | so its more of an annoyance than a problem.
        
               | pron wrote:
               | I would say these are more differences than issues, and
               | that some of those presented as more fundamental ones are
               | actually quite small. Suppose that instead of `fn foo
               | (comptime T : type, ...) { typecheck(T); ...}` Zig
               | introduced just a tiny bit of new syntax to allow you to
               | write something like `fn foo (comptime T : typecheck(T),
               | ...) { ... }` -- i.e. the type constraints would be part
               | of the signature -- would you then say it had generics
               | rather than templates? Personally, I have not made up my
               | mind on whether or not such an addition would be very
               | valuable, but even if it is, it can be done later. That
               | small addition would address most "issues" in the
               | article, which I would say are more about IDE support
               | than anything else. But even without it, what you want to
               | know is known at compile time, and the article admits
               | that the compilation errors are already better than those
               | you get with C++ templates (I would say much better).
               | 
               | Now, I'm not saying that Zig's choices always dominate
               | and that all languages would be better off with its
               | approach; far from it. I am saying that it introduces a
               | novel tradeoff that is especially compelling in cases
               | where not only generics but also macros, conditional
               | compilation, and constexprs are otherwise required. In a
               | language like Java these extra features are not required,
               | and so Zig-style comptime would not simplify the language
               | nearly as much.
               | 
               | But even in cases where all these features are needed, I
               | don't think everyone would take Zig's choices over C++'s
               | or Rust's, or vice-versa. To those, like me, for whom
               | language complexity is the biggest problem with C++ or
               | Ada (I used Ada in the nineties), Zig is a revolutionary
               | step forward. I don't think any low-level language has
               | ever been this simple while also being this expressive.
        
               | SkiFire13 wrote:
               | Comptime can only properly express half of them:
               | 
               | - generics: comptime can implement some kind of
               | polymorphism, but not at the type level. In other words
               | it implements a templating system, not a polymorphic type
               | system;
               | 
               | - interfaces/traits/concepts: comptime implements none of
               | that, it is plain duck typing, just like "old" C++
               | templates. In fact C++ introduced concepts to improve its
               | situation with templates, while Zig is still behind on
               | that front!
               | 
               | - macros: comptime solves some of the usecases where
               | macros are used, but it cannot produce arbitrary tokens
               | and hence cannot fully replace macros.
               | 
               | I do agree that it can neatly replace conditional
               | compilation and const functions/const expr, but let's not
               | make it seem like comptime solves everything in the
               | world.
        
               | throwawaymaths wrote:
               | in practice aside from interfaces the only thing you
               | can't do at comptime is to generically attach
               | declarations (member functions, consts) to a container
               | type (the best you can do is to do it on a case by case
               | basis).
               | 
               | you could probably cobble together an interface system
               | with @comptimeError, but because of the time order of
               | compilation stages, a failed method call will trigger the
               | compiler error _before_ your custom interface code,
               | making it effectively useless for the 90% of cases you
               | care about.
               | 
               | if I'm not mistaken in principle a small change to the
               | compiler could amend this situation
        
           | creata wrote:
           | > This makes them much easier to reason about and debug.
           | 
           | Can you give an example of something that's easier to reason
           | about (e.g., an error that's easier to spot) with Zig's
           | comptime than with macros?
           | 
           | > it makes other language features redundant
           | 
           | I'm guessing (so I might be wrong) that IDEs and users still
           | need to be aware of the common idioms, so why does it matter
           | whether or not those common idioms are implemented in the
           | compiler or using comptime? (I'm not saying it _doesn 't_
           | matter, I'm wondering what benefits you have in mind.)
        
             | WhyNotHugo wrote:
             | > Can you give an example of something that's easier to
             | reason about (e.g., an error that's easier to spot) with
             | Zig's comptime than with macros?
             | 
             | Rust proc_macros takes a stream of tokens and return a
             | stream of tokens. If your macro meant to return an instance
             | of a specific type, it must output the correct tokens which
             | create that instance via existing interfaces. There's some
             | really ugly indirection in trying to understand what's
             | going on.
             | 
             | This is always harder to reason about than Zig's
             | equivalent, because in Zig you just return the thing that
             | you want to return.
        
               | NobodyNada wrote:
               | How does it work if I wanted to construct a type (and
               | maybe a set of helper types. some related functions,
               | etc.), rather than an instance of a type?
               | 
               | If I just wanted to construct an instance of a specific
               | type at compile time in Rust, I'd probably be using a
               | const fn instead of a macro.
        
               | cgh wrote:
               | You return the type directly. You can then declare things
               | to be of this type. Eg, from the Zig docs, here's how to
               | construct a generic List type (note the comptime
               | declaration of the generic parameter):                 fn
               | List(comptime T: type) type {           return struct {
               | items: []T,               len: usize,           };
               | }            // The generic List data structure can be
               | instantiated by passing in a type:       var buffer:
               | [10]i32 = undefined;       var list = List(i32){
               | .items = &buffer,           .len = 0,       };
        
             | samatman wrote:
             | You can't reason about macros, that's not how they work.
             | 
             | You can read their definition, you can expand them, but
             | there's no way to look at a macro call and reason about it,
             | it can do anything at all. In C you don't even know what is
             | and isn't a macro, so Rust has a modest edge in that
             | respect.
             | 
             | Zig just doesn't have this problem to begin with.
        
               | creata wrote:
               | Reading a macro's definition and reasoning about its
               | effect is... reasoning. It's not the same as reasoning
               | about something using its inherent limitations, which is
               | the kind of reasoning that I think you're referring to,
               | but it's still reasoning.
        
               | samatman wrote:
               | Ok, sure, we can reason about anything. We could reason
               | about machine code, if we had the time and inclination.
               | 
               | I barely participate in Hacker News anymore because it
               | seems to have collectively lost the ability to extract
               | meaning from words, unless an exhausting and totally
               | excessive amount of attention is put into satisfying a
               | misplaced sense of precision. There's no intellectual
               | charity left and it sucks.
        
           | GrantMoyer wrote:
           | Nit-pick: Javascript is not simple; I personally think it has
           | among the most complex language semantics out of all commonly
           | used languages.
        
             | HelloNurse wrote:
             | Javascript would be a simple hybrid of object oriented and
             | functional principles if backward compatibility with old
             | hacks and "robust" use for web page scripting didn't
             | require a host of redundant features, syntactic bizarre
             | special cases and and evil semantic choices: -- comments,
             | iterating objects and arrays, the absurd equality operators
             | and type conversions, and so on.
        
               | efnx wrote:
               | Prototypal inheritance can be pretty awkward and
               | complicated. It's evident in that few young JS devs ever
               | use it, and many don't even know about it!
        
             | Lerc wrote:
             | Can you give an example outside of the weirdness caused by
             | automaticy converting types? (Or 'with', which is kind-of
             | in purgatory now)
             | 
             | I have considered a non backwards compatible JavaScript
             | descendant to clean things up. It would be interesting to
             | hear what you consider to be problems.
        
               | GrantMoyer wrote:
               | Off the top of my head:
               | 
               | - There's both undefined and null
               | 
               | - There's three ways to declare variable
               | 
               | - typeof vs instanceof
               | 
               | - for in vs for of
               | 
               | - the way `this` works
               | 
               | - semantics of functions called with the wrong number of
               | args
               | 
               | There's far more that I can't immediately recall.
        
               | throwawaymaths wrote:
               | prototypes vs class
        
               | panzi wrote:
               | Empty slots in arrays are weird.                   >>>
               | let xs = [,'foo']         <<< undefined         >>> xs
               | <<< Array [ <1 empty slot>, "foo" ]         >>>
               | xs.map((x, index) => index)         <<< Array [ <1 empty
               | slot>, 1 ]         >>> for (let x of xs) console.log(x);
               | undefined         foo         <<< undefined         >>>
               | for (let x in xs) console.log(x);         1         <<<
               | undefined         >>> xs.length         <<< 2
               | >>> delete xs[1]         <<< true         >>> xs.length
               | <<< 2         >>> xs         <<< Array [ <2 empty slots>
               | ]
               | 
               | Empty slots are skipped in map() and for-in, but not in
               | for-of and the new array from map() will have the same
               | empty slots. delete will change a slot to empty, it won't
               | change the length of the array.
               | 
               | Still, _much_ saner than PHP arrays.
        
               | codedokode wrote:
               | Javacripts has lot of low-level functions like [[Get]],
               | [[Call]], [[ToPrimitive]] which can be redefined. If you
               | believe that JS is easy, do you remember how these low-
               | level functions work? Also, JS has prototypes.
        
       | vlovich123 wrote:
       | Wow this is so neat. Has anyone had any experience with it /
       | feedback? This looks so much nicer than existing macros.
        
         | weinzierl wrote:
         | No experience. I agree that it looks nice and useful, but I
         | don't think it is much like Zig's comptime.
        
         | nindalf wrote:
         | The author announced a previous version of this crate a couple
         | of weeks ago and this new version two days ago. So there might
         | not be that many users.
         | 
         | That said I did replace a declarative macro with it. Supposedly
         | I wrote the declarative macro according to git blame but I only
         | have a vague idea how it works. I replaced it with crabtime and
         | I got something that I can understand and maintain.
         | 
         | Overall I'd say I'm very pleased with crabtime. Previously I
         | would have avoided Rust metaprogramming as much as possible,
         | but now I'd feel confident to use it where appropriate.
        
         | Ygg2 wrote:
         | It's neat, but I don't think it does the same as Zig's
         | comptime. For one it doesn't have Zig's dynamic behavior, nor
         | more practically compile time reflection.
         | 
         | Rust can't have Zig's comptime dynamic properties, same way Zig
         | will never have Rust's compile time guarantees*.
         | 
         | You can't simultaneously have your dynamic cake and eat it at
         | compile time.
         | 
         | * Theoretically you can have it, but it would require changing
         | language to such extent it's not recognizable.
        
       | norman784 wrote:
       | This looks nice, just yesterday I was trying to make my code more
       | concise by using some macro_rules magic, but it was a bit more
       | than what macro_rules can handle, so I ended up just writing the
       | whole thing. I avoid whenever I can proc macros, I wrote my fair
       | share of macros, but I hate them, you need to add most of the
       | time 3 new dependencies, syn, quote and proc_macros2, that adds
       | up to the compilation times.
       | 
       | This looks worth the playing with and see if they can solve my
       | issue, one thing I avoid as much as possible is to add
       | unnecessary dependencies, didn't check how many dependencies this
       | will add overall to the project.
        
         | lifthrasiir wrote:
         | It depends on proc-macro2, syn, quote, toml and rustc_version
         | [1]. First three are legitimately expected for any complex
         | enough procedural macros. Toml and rustc_version are apparently
         | for automatic Cargo configuration and fairly harmless by their
         | own. Their transitive dependencies are also not bad: unicode-
         | ident (from proc-macro2), serde, serde_spanned, toml_datetime
         | (from toml), and semver (from rustc_version).
         | 
         | [1] https://crates.io/crates/crabtime-
         | internal/1.1.1/dependencie...
        
       | nindalf wrote:
       | I tried the library out and it worked pretty well for me.
       | 
       | I had previously written a declarative macro to generate
       | benchmark functions [1]. It worked, but I didn't enjoy the
       | process of getting it working. Nor did I feel confident about
       | making changes to it.
       | 
       | When I rewrote it using crabtime I found the experience much
       | better. I was mostly writing Rust code now, something I was
       | familiar with. The code is much more readable and customisable
       | [2]. For example, instead of having to pass in the names of the
       | modules each time I added a new one, I simply read the files from
       | disk at compile time.
       | 
       | To compare the two see what the code looks like in within the
       | braces of paste!{} in the first one and crabtime::output!{} in
       | the second one. The main difference is that I can _construct_ the
       | strings using Rust code and drop them in with a simple {{ str }}.
       | With paste!, I don 't know exactly what I did, but I kept messing
       | around until it worked.
       | 
       | Or compare the two loops. In the first one we have
       | `($($year:ident {$($day:ident),+ $(,)?}),+ $(,)?)` while with
       | crabtime we have plain Rust code - `for (year, day) in
       | years_and_days`. I find the latter more readable.
       | 
       | Overall I'm quite pleased with crabtime. Earlier I'd avoid Rust
       | metaprogramming as much as possible, but now I'd be open to
       | writing a macro if the situation called for it.
       | 
       | [1] -
       | https://github.com/nindalf/advent/blob/13ff13/benches/benche...
       | 
       | [2] -
       | https://github.com/nindalf/advent/blob/b72b98/benches/benche...
        
       | jpgvm wrote:
       | This is cursed in the most wonderful way, kudos.
        
         | Sharlin wrote:
         | It's what the word "blursed" was coined for.
        
           | airstrike wrote:
           | I need to use that more often...
        
       | stared wrote:
       | I love these kinds of acknowledgements, as they not only show
       | gratitude, but also give a glimpse into the collaborative,
       | creative process:
       | 
       | > We would like to extend our heartfelt gratitude to the
       | following individuals for their valuable contributions to this
       | project:
       | 
       | > timonv - For discovering and suggesting the brilliant name for
       | this crate. Read more about it here (https://www.reddit.com/r/rus
       | t/comments/1j42fgi/comment/mg6pw...).
       | 
       | > Polanas - For their invaluable assistance with testing, design,
       | and insightful feedback that greatly improved the project.
       | 
       | > Your support and contributions have played a vital role in
       | making this crate better--thank you!
        
       | the__alchemist wrote:
       | Deos anyone have an example beyond the one on that page? I'm
       | having a hard time understanding.
       | 
       | So, I'm interested in some metaprogramming right now. I'm setting
       | up Vec3 SIMD types, and it requires a lot of repetition to manage
       | the various variants: f32::Vec3x8, f64::Vec3x16 etc that are all
       | similar internally. This could be handled using traditional
       | macros, procedural macros, or something called "code gen", which
       | I think is string manipulation of code. Could I use crabtime to
       | do this instead? Should I?
        
         | CGamesPlay wrote:
         | Honestly, this long document is probably a better link than the
         | crates.io page: https://docs.rs/crabtime/latest/crabtime/
         | 
         | > This could be handled using traditional macros, procedural
         | macros, or something called "code gen", which I think is string
         | manipulation of code. Could I use crabtime to do this instead?
         | Should I?
         | 
         | You could, it seems. Crabtime supports both the procedural
         | macros and "code gen" approaches you are talking about.
        
         | codedokode wrote:
         | For simply copy-pasting code you could start with using
         | simplest traditional macros. No matter what approach you choose
         | your code will be pain to read and understand (maybe we need to
         | have "show generated code" button in our IDEs).
        
           | conaclos wrote:
           | Actually we have a command to do exactly what you want:
           | `expand macro`. Crabtime claims to have the same thing.
        
       | jgalt212 wrote:
       | Does anyone else find macros make it hard to grep a code base?
       | This does seem like something semantic grep could solve, but I'm
       | unaware of any semantic grep macros use cases.
        
         | mplanchard wrote:
         | This is why I generally avoid making new structs via macros,
         | and why I personally dislike the popular error library snafu:
         | breaking "go to definition" and codebase search really needs to
         | be worth it IMO. It doesn't feel as bad for proc macros that
         | add methods for whatever reason for me, but having to use a
         | type with an opaque definition hidden behind a macro really
         | bugs me.
        
           | Nullabillity wrote:
           | Snafu works fine with at least rust-analyzer's gotodef
           | (though go-to-references is indeed broken by it :/).
        
         | loeg wrote:
         | Moreso than calling an ordinary subroutine?
        
           | dymk wrote:
           | The code demo in the crabtime readme is actually a good
           | example of something that is now hard to grep. Let's say you
           | see a usage of `Position1`, and you want to find where and
           | how it's defined - well, there is no `enum Position1` in the
           | codebase, because the identifier is concatenated from two
           | separate parts. You lose out on some IDE niceties as well -
           | can't command-click on the definition site to find usages,
           | because there is no definition site (available to you, at
           | least).
        
         | codedokode wrote:
         | The problem is not macros, it is concatenation of identifiers.
         | I stumbled upon this a lot when working with CSS preprocessors
         | which allow you to write code like this:
         | .user {             &--profile { color: red; }          }
         | 
         | Now, searching for "user--profile" CSS class becomes imposible.
         | Despite this, SASS and similar preprocessor seem to be popular
         | and used almost everywhere. Well, I never had high expectations
         | of front-end developers, so I am not very disappointed.
         | 
         | So I think as long as you don't break identifiers, the code
         | should be searchable. But, your IDE will probably not able to
         | help you with auto-complete and navigation.
        
           | jgalt212 wrote:
           | > The problem is not macros, it is concatenation of
           | identifiers.
           | 
           | Yes, that's also a problem. As is MySQLCursorDict Class. Now
           | you have to grep every column name every table your code base
           | accesses.
        
       | mplanchard wrote:
       | At first I was like wait this looks just like eval_macro, which I
       | discovered a couple of weeks ago. Looks like it is just renamed!
       | The new name is great, congrats on the improved branding :)
        
       | KolmogorovComp wrote:
       | A better link https://docs.rs/crabtime/1.1.1/crabtime/
        
       | dymk wrote:
       | This looks cool, but how it impacts project compile times? They
       | talk about how caching works for multiple invocations of the same
       | macro with different arguments. It would be nice to have some
       | approximate numbers for how long it takes to create, compile, and
       | execute one of its generated projects.
        
       | cyber1 wrote:
       | No, this is not Zig comptime at all. Zig's comptime work is on
       | another level, and it's amazing.
        
         | metaltyphoon wrote:
         | Instead of just saying its not. Explain what's so much
         | different here.
        
       | jedisct1 wrote:
       | It's nothing like Zig's comptime.
        
       | codedokode wrote:
       | The problem with macros in Rust is that they have full access to
       | your computer. This is literally an invitation for exploitation.
       | I think we will see the attacks based on this vulnerability once
       | Rust becomes more popular.
        
         | cmrx64 wrote:
         | There's already a runtime for sandboxing macros with wasm:
         | https://github.com/dtolnay/watt
        
           | codedokode wrote:
           | So you need to use hacks, like compiling code into a web-
           | browser language and messing with config files instead of
           | having security out-of-box?
           | 
           | But thank you for letting me learn something useful.
        
             | cmrx64 wrote:
             | It's a demonstration. wasm is a portable ISA more than a
             | "language". Surely it makes sense to build things
             | incrementally, in layers? https://internals.rust-
             | lang.org/t/pre-rfc-sandboxed-determin...
             | 
             | But go off, king.
        
         | jkelleyrtp wrote:
         | `make myfile.mk` -> pwned
         | 
         | I do share the sentiment - and complain about this frequently -
         | but any environment with build scripts can wreck your computer.
         | Encrypt what you can, I guess, but software engineering is an
         | extremely dangerous job wrt security.
        
         | hypeatei wrote:
         | Do other languages have a security model for this? I've always
         | assumed that building arbitrary code could execute something in
         | most languages.
         | 
         | I think using something like the pledge syscall from OpenBSD in
         | the compiler could be useful. That way, it's controlled at the
         | process level which things can be accessed on the system.
        
           | codedokode wrote:
           | C macros and gcc do not allow to run arbitrary code during
           | compilation.
        
       | cchance wrote:
       | I don't know if i'm too dumb i never understood what comptime
       | gives, i get what macros are for but how does something like
       | crabtime improve things ?
       | 
       | Does this basically allow us to write normal rust code instead of
       | procmacros, with even fewer constraints?
        
       | tdhz77 wrote:
       | Moments like this realize I don't understand programming
       | fundamentals. Imposter syndrome sits in and I realize that my
       | lack of formal education is costing me.
        
       ___________________________________________________________________
       (page generated 2025-03-22 23:01 UTC)