[HN Gopher] Compile time evaluation in Nim, Zig, Rust and C++
       ___________________________________________________________________
        
       Compile time evaluation in Nim, Zig, Rust and C++
        
       Author : lukastyrychtr
       Score  : 117 points
       Date   : 2022-04-25 20:34 UTC (1 days ago)
        
 (HTM) web link (castillodel.github.io)
 (TXT) w3m dump (castillodel.github.io)
        
       | [deleted]
        
       | jbandela1 wrote:
       | Here is a complete example how to generate a compile time
       | FizzBuzz array in C++. This includes converting the numbers to
       | string. This example does through 50.
       | 
       | https://gcc.godbolt.org/z/1nTcdvWM5
       | #include <limits>             #include <array>
       | #include <algorithm>             #include <utility>
       | #include <string_view>                  template<unsigned int
       | Num>             constexpr auto to_array(){
       | constexpr auto digits =
       | std::numeric_limits<unsigned int>::digits10;
       | std::array<char,digits> ar{};                 auto x = Num;
       | int pos = 0;                 while(x > 0){
       | ar[pos] = '0' + x % 10;                     x /= 10;
       | ++pos;                 }
       | std::reverse(ar.begin(),ar.begin() + pos);                 return
       | std::make_pair(pos,ar);             }
       | template<unsigned int Num>             struct to_string{
       | constexpr static auto p = to_array<Num>();
       | constexpr static std::string_view get(){
       | return std::string_view(p.second.data(),p.first);
       | }             };                  template<unsigned int Num>
       | constexpr std::string_view get_fizz_buzz(){                  if
       | constexpr (Num % 15 == 0) {                     return
       | "FizzBuzz";                 } else if constexpr (Num % 3 == 0) {
       | return "Fizz";                 } else if constexpr (Num % 5 == 0)
       | {                     return "Buzz";                 } else{
       | return to_string<Num>::get();                  }             }
       | template<std::size_t... I>             constexpr auto
       | get_fizz_buzz_array_impl(std::index_sequence<I...>){
       | return std::array{get_fizz_buzz<I+1>()...};             }
       | template<unsigned int Val>             constexpr auto
       | get_fizz_buzz_array(){                 return
       | get_fizz_buzz_array_impl(std::make_index_sequence<Val>());
       | }             #include <iostream>             int main(){
       | static constexpr auto ar = get_fizz_buzz_array<50>();
       | for(auto s:ar){                     std::cout << s << "\n";
       | }                       }
        
       | pjmlp wrote:
       | With C++20, the C++ example can actually be written as
       | constexpr std::string get_fizzbuzz(int number) {             if
       | (number % 15 == 0) {                 return "FizzBuzz";
       | } else if (number % 3 == 0) {                 return "Fizz";
       | } else if (number % 5 == 0) {                 return "Buzz";
       | }             return std::to_string(number); // convert to string
       | }
        
         | jmyeet wrote:
         | Based on the other comments to this, isn't it a problem that
         | we're even debating the "correct" way to implement FizzBuzz in
         | C++20? Like, does no one else see a problem with that?
        
           | secondcoming wrote:
           | Like, no. Have you never programmed Python?
        
             | jmyeet wrote:
             | I've seen many debates about whether or not a given Python
             | code snippet was "best" or "Pythonic" but nowhere near the
             | level of discourse (compared to C++) about whether or not
             | it's correct. Perfect forwarding, anyone?
        
               | secondcoming wrote:
               | Oh I see, I thought you were referring to language
               | features rather than code correctness.
        
               | [deleted]
        
         | andreidd wrote:
         | No, it can't. std::to_string isn't constexpr. And even if it
         | was, it still wouldn't work because the std::string needs to be
         | destroyed inside the constexpr context.
         | 
         | The article is also wrong because std::to_chars isn't constexpr
         | so you can't use that.
        
           | pjmlp wrote:
           | Compiler Explorer is your friend,
           | https://godbolt.org/z/94hzK8svE
           | 
           | In one thing you're actually right, I should have used a
           | string_view for the return value instead.
           | #include <vector>         #include <string>         #include
           | <algorithm>         #include <string>         #include
           | <cstdio>              constexpr std::string_view
           | get_fizzbuzz(int number) {             if (number % 15 == 0)
           | {                 return "FizzBuzz";             } else if
           | (number % 3 == 0) {                 return "Fizz";
           | } else if (number % 5 == 0) {                 return "Buzz";
           | }             return std::to_string(number); // convert to
           | string         }              int main() {             static
           | constexpr auto value = get_fizzbuzz(15);
           | puts(value.data());         }
           | 
           | The template metaprogramming to expand all values for
           | get_fizzbuzz() is left as exercise.
        
             | cornstalks wrote:
             | You're gonna have a bad time if you return a string_view to
             | a locally created string.
        
               | pjmlp wrote:
               | It is compile time, so the expectation is that the
               | compiler replaces it anyway, although you might be right.
        
               | MaulingMonkey wrote:
               | `constexpr` isn't "compile time". It's _potentially_
               | compile time at best. Debug builds in particular will go
               | out of their way to evaluate things at runtime,
               | presumably so you can set breakpoints and step through
               | code, even when it 's completely pointless. I have seen
               | the following function show up in a profiler:
               | template < bool value >         bool constexpr
               | IsEnabled() { return value; }
               | 
               | This was used to silence warnings about unconditional
               | branches in a macro. There are ways to force such
               | functions to be evaluated at compile time, but they're
               | pretty awkward, and limited to integral types (you can't
               | use them on string_view s):                   #define
               | EVAL_AT_COMPILE_TIME(x)
               | std::integral_constant<decltype(x), x>::value
               | const bool x = EVAL_AT_COMPILE_TIME(IsEnabled<true>());
        
               | pjmlp wrote:
               | That is why main() has a static constexpr, to force its
               | execution at compile time. A trick I learned from Jason's
               | screencasts.
               | 
               | Although, you're right. I tried it back at home on VC++
               | and its static analyser wasn't happy.
               | 
               | My idea was to make use of template metaprogramming to
               | generate the string buffer, there are a couple of
               | examples of such attempts.
               | 
               | However, I guess with such amount of additional code, I
               | should declare defeat on the original comment.
        
               | tialaramex wrote:
               | Seems to me the _good news_ is that your compiler told
               | you this doesn 't work.
               | 
               | In languages where you can't _tell_ the compiler you
               | think this is constant, there 's a risk you delude
               | yourself, especially because computers are very fast, and
               | you think you've got O(1) when it's actually O(N) or even
               | O(N^2) and one day N gets big enough and you're in real
               | trouble.
        
               | deschutes wrote:
               | String literals are specified to have static storage
               | duration. That means references are valid at least until
               | main returns. In practice string literals are immortal
               | and references to them are always valid.
        
               | cornstalks wrote:
               | I didn't say string literal. I said "locally created
               | string" i.e. std::string
               | 
               | In particular, my comment was referring to "return
               | std::to_string(number);"
        
               | deschutes wrote:
               | I missed that. You're right.
        
             | nemothekid wrote:
             | > _The template metaprogramming to expand all values for
             | get_fizzbuzz() is left as exercise._
             | 
             | There's some joke here about authors using "left as
             | exercise to the reader" to skip debugging their broken
             | code.
        
               | pjmlp wrote:
               | Or like I don't need to fully implement it for the next
               | interview.
        
             | andreidd wrote:
             | Your example only works because the optimizer eliminates
             | your call to std::to_string.
             | 
             | Call get_fizzbuzz(11) and you'll see the error.
        
               | pjmlp wrote:
               | I stand corrected, however I have a couple of ideas to
               | try later on.
        
             | deschutes wrote:
             | Have to defer conversion to std string to runtime
             | https://godbolt.org/z/rWKEr4P6h
        
               | pjmlp wrote:
               | Yeah, I was wrong on that regard.
               | 
               | Although I still think with a mix of template
               | metaprogramming and constexpr might be possible, however
               | it would be a very low ROI on such example.
               | 
               | And it would still be worse than the other languages, so
               | defeat accepted.
        
       | treeform wrote:
       | You can do a lot with Nim at compile time, check out my talk on
       | Nim Metaprogramming not just for FizzBuzz, but real world
       | applications:
       | https://fosdem.org/2022/schedule/event/nim_metaprogramming/
       | 
       | I am working an a macro to compile Nim code into GLSL. So not
       | only can you write Nim to C or Nim to JS, it can also (in limited
       | way) do Nim to GLSL GPU Shaders. See here:
       | https://github.com/treeform/shady
       | 
       | I am also working on a macro system similar to SWIG, where using
       | a some macros one can write a Nim library and generate wrappers
       | for your NIM library for many languages like C, Python, JS, Ruby.
       | See here: https://github.com/treeform/genny
        
       | tjoff wrote:
       | Great, now do it with C-macros! ;)
        
       | valcron1000 wrote:
       | Another language that supports (arbitrary) compile time code
       | execution is Haskell through Template Haskell. Ex. using
       | FizzBuzz:                 -- FizzBuzz.hs              module
       | FizzBuzz (fizzBuzz) where              fizzBuzz :: Int -> String
       | fizzBuzz n         | n `mod` 15 == 0 = "FizzBuzz"         | n
       | `mod` 3 == 0 = "Fizz"         | n `mod` 5 == 0 = "Buzz"         |
       | otherwise = show n              -- Main.hs              import
       | FizzBuzz (fizzBuzz)       import Language.Haskell.TH.Syntax
       | (lift)              main :: IO ()       main = do         print
       | $(lift $ map fizzBuzz [1 .. 100])
       | 
       | As you can see, the fizzbuzz function does not need any special
       | syntax or annotations. You can use any code at compile time. The
       | only downside is that you need to separate the code into two
       | modules.
       | 
       | Other solutions involve using the type system to do this kind of
       | computation at compile time, but I think that TH is very powerful
       | (maybe too powerful since you can run arbitrary IO at compile
       | time).
        
       | Cloudef wrote:
       | The compile time in zig and nim is breath of fresh air, while
       | rust seems similar spaghetti mess as c++
        
         | lionkor wrote:
         | I know its super hip to hate on C++, but how is adding one
         | keyword to make a function compiletime evaluated "spaghetti
         | mess"? Because it's C++ and its hard?
        
           | mhh__ wrote:
           | constexpr should just try to evaluate stuff at compile time.
           | In D, most sensible code works at compile time automatically,
           | I don't think about it.
           | 
           | Having to mark _everything_ as evaluatable at compile time is
           | a stupid, stupid, decision that only C++ could think was a
           | good idea.
        
             | epage wrote:
             | One downside to implicit constexpr is its harder to know
             | when an API intends it as a user and harder to enforce it
             | works as an author. Seems like it could be easy to break
             | compatibility.
        
               | layer8 wrote:
               | At least users would notice at compile time when it
               | breaks. With both implicit and explicit constexpr,
               | authors who care could use the explicit form, and any
               | implicit constexpr would be "use at your own risk", but
               | would still be guaranteed to work as long as you don't
               | upgrade the dependency.
        
               | mhh__ wrote:
               | That doesn't really happen in D because basically
               | everything is "constexpr" unless it does something like
               | inline ASM or unions with pointers in them
        
             | [deleted]
        
             | pjmlp wrote:
             | D should worry less how great language it is, focus on
             | fixing long standing DIPs and compiler bugs, and actually
             | have an ecosystem that makes it worthwhile using in the
             | industries where C++ is the first choice.
        
               | rat9988 wrote:
               | Somehow it feels like languages are clashing here.
               | 
               | >D should worry less how great language it is,
               | 
               | You are just replying to some guy expressing an opinion
               | on internet about how to do something better, and using D
               | as example.
        
               | pjmlp wrote:
               | Mostly because unless I am going crazy, the comment was
               | different when I posted my remark.
        
               | bachmeier wrote:
               | The HN guidelines state:
               | 
               | > Eschew flamebait. Avoid unrelated controversies and
               | generic tangents.
               | 
               | You responded to
               | 
               | > Having to mark everything as evaluatable at compile
               | time is a stupid, stupid, decision that only C++ could
               | think was a good idea.
               | 
               | with
               | 
               | > D should worry less how great language it is, focus on
               | fixing long standing DIPs and compiler bugs, and actually
               | have an ecosystem that makes it worthwhile using in the
               | industries where C++ is the first choice.
               | 
               | Definitely qualifies as both "flamebait" and "unrelated
               | controversies".
        
               | pjmlp wrote:
               | The comment was edited, it was about the virtues of
               | static if in D versus C++.
        
               | mhh__ wrote:
               | My comment?
        
               | pjmlp wrote:
               | If I recall correctly it had something about not doing if
               | constexpr if being lesser than static if, and how greater
               | it makes D over C++, overlooking the fact that since
               | Andrei published its book that has mattered very little.
               | 
               | Now if I happened to reply to the wrong comment, sorry
               | about that, and I should pay more attention before
               | replying.
        
       | stefano_c wrote:
       | One possible solution in Rust could be:                   enum
       | Value {             Fizz,             Buzz,             FizzBuzz,
       | Number(usize),         }              impl std::fmt::Display for
       | Value {             fn fmt(&self, f: &mut std::fmt::Formatter) ->
       | std::fmt::Result {                 match self {
       | Value::Fizz => write!(f, "Fizz"),                     Value::Buzz
       | => write!(f, "Buzz"),                     Value::FizzBuzz =>
       | write!(f, "FizzBuzz"),                     Value::Number(num) =>
       | write!(f, "{}", num),                 }             }         }
       | const fn get_fizzbuzz_equivalent(number: usize) -> Value {
       | if number % 15 == 0 {                 Value::FizzBuzz
       | } else if number % 3 == 0 {                 Value::Fizz
       | } else if number % 5 == 0 {                 Value::Buzz
       | } else {                 Value::Number(number)             }
       | }              fn main() {             (1..100).for_each(|num|
       | println!("{}", get_fizzbuzz_equivalent(num)));         }
        
         | tialaramex wrote:
         | While this implements FizzBuzz it does not actually end up
         | doing the work at compile time.
         | 
         | You annotate get_fizzbuzz_equivalent() with const, so Rust
         | _would_ evaluate that on constant inputs at compile time, but
         | that 's not very interesting since it's basically a switch.
         | 
         | The use of const here does _not_ oblige Rust to somehow figure
         | out everywhere you can use this function and do the work at
         | compile time since the inputs might be (and are here)
         | variables. Sure enough if you try in Godbolt you will see that
         | eliding const makes no real difference.
         | 
         | Rust's const today is far less powerful than something like C++
         | constexpr, I suspect that you can't really do what Nim did in a
         | reasonable way with Rust. You could I'm sure get there with
         | build.rs and/or proc macros, but that's not really in the
         | spirit of this exercise.
        
           | steveklabnik wrote:
           | To elaborate, the parent doesn't call get_fizzbuzz_equivalent
           | in a "const context", which would _require_ it to be
           | evaluated at compile time. So it 's called at runtime like it
           | didn't have `const`.
           | 
           | You can do something _like_ the nim without build.rs or proc
           | macros:                   #[derive(Debug, Copy, Clone)]
           | enum Value {             Fizz,             Buzz,
           | FizzBuzz,             Number(usize),         }
           | const fn get_fizzbuzz_equivalent<const N: usize>() -> [Value;
           | N] {             let mut result = [Value::FizzBuzz; N];
           | let mut i: usize = 0;             while i < N {
           | let n = i + 1;                 if n % 15 == 0 {
           | result[i] = Value::FizzBuzz;                 } else if n % 3
           | == 0 {                     result[i] = Value::Fizz;
           | } else if n % 5 == 0 {                     result[i] =
           | Value::Buzz;                 } else {
           | result[i] = Value::Number(n);                 };
           | i += 1;             }                      result         }
           | fn main() {             const FIZZBUZZ: [Value; 31] =
           | get_fizzbuzz_equivalent();
           | println!("{:?}", FIZZBUZZ);         }
           | 
           | There are certainly some ergonomic issues here; having to use
           | while because for isn't usable in const contexts yet, which
           | is annoying. But this does compute the whole array at compile
           | time.
           | 
           | (Shout-out to
           | https://stackoverflow.com/questions/67538438/const-array-
           | fro... which I basically smashed together with OP's code to
           | produce the above example.)
        
       | adamrezich wrote:
       | "jai" is pretty cool in this regard, you can just huck whatever
       | you want into a #run {} block and it'll get evaluated at compile
       | time.
        
       | winrid wrote:
       | Nim continues to impress.
        
       | vegai_ wrote:
       | Nim is an odd language insofar that every time it's pitted
       | against other languages, it performs brilliantly on nearly every
       | level. Yet almost nobody uses it. Really weird juxtaposition.
        
         | pjmlp wrote:
         | Because comparing grammars and semantics is meaningless without
         | an ecosystem and killer use case to come along with the
         | language.
        
         | skywal_l wrote:
         | Quickly scanning Nim page, there is a couple of things that
         | bothers me in Nim. The python style blocks making multi line
         | lambda awkward, Garbage collection, "Identifier equality"
         | (some_variable == someVariable)...
         | 
         | So because of the quirkiness I would understand why it is not a
         | universally adopted language.
         | 
         | Do you have pointers to the comparison with other languages?
        
           | elcritch wrote:
           | I think the biggest issue is that Nim took a long time to
           | "find its feet". There was a lot of experimentation before
           | the current sweet spot. Due to its new GC system 'ARC' it's
           | now broadly useable as a systems language, which wasn't true
           | before with a regular GC. ARC is non-atomic/non-locking
           | reference count based, meaning it can be used to as a systems
           | language (no GC pauses) or for system libraries (C programs
           | can use it).
           | 
           | There's a few awkward-ish parts of the syntax, but in
           | practice those items aren't big issues (compared to say Rust
           | async syntax issue). For example multi-line lambdas can be
           | done with 'do' blocks or just wrapping parens. Identifier
           | equality has actually saved me from a few bugs by
           | accidentally creating a new similarly named variable, say an
           | 'isDone' when I already had an 'is_done' following the style
           | from C FFI. Though the compiler warns by default when mix
           | naming styles, and can be set as an error if desired.
           | 
           | Iterators usage can be a bit annoying though. I also really
           | enjoy UFCS:
           | https://en.m.wikipedia.org/wiki/Uniform_Function_Call_Syntax
           | 
           | Python comparison example:
           | https://narimiran.github.io//2018/05/10/python-numpy-
           | nim.htm...
        
             | coliveira wrote:
             | ARC is not new, it was used by Apple in their languages for
             | at least 10 years now.
        
               | ______-_-______ wrote:
               | Obviously parent is not claiming that Nim invented the
               | concept of reference counting. They're saying that it's a
               | recent addition to Nim, and Nim's other memory management
               | strategies have been part of the compiler for longer.
        
           | dom96 wrote:
           | What's so awkward about multi line lambdas? You can achieve
           | them in the same way you can in JavaScript, can't get easier
           | than that.                   import sequtils              let
           | x = [1,2,3,4,5]         echo x.map(           proc (num:
           | int): int =             num*2         )
        
           | poulpy123 wrote:
           | I just started to (very slowly) learn Nim, and it's quite
           | nice for the moment. Of course there are some points of
           | friction, but which language doesn't have ? The documentation
           | page has comparison with C, python and typescript/JavaScript
           | https://nim-lang.org/documentation.html
        
         | indymike wrote:
         | It takes time for the ecosystem to emerge... and I think Nim's
         | future is very bright.
        
         | renox wrote:
         | Same with D..
        
         | mrweasel wrote:
         | It lacks the branding of something like Rust, which it self
         | tried to piggy back on Go years ago, until it got traction.
         | 
         | C++, for better or worse continues to be a safe choice. A
         | reasonably large user base, and it will be around for a long
         | time. My co-worker joked that if you want your software to be
         | able to run in the future, pick Windows and C++ as your
         | platform. Then you'll be good for a few decades.
        
         | polotics wrote:
         | Patiently waiting for the right moment to try and introduce it
         | at $JOB, but it's kind of hard so many developers like only the
         | familiar...
        
         | ryukoposting wrote:
         | People don't use Nim because the ecosystem is small, and the
         | ecosystem is small because people don't use Nim.
         | 
         | The solution is to make the ecosystem bigger by contributing to
         | it. Good news! The ecosystem is small, so there's a lot of low-
         | hanging fruit.
         | 
         | I'm a "random guy" writing Nim in my spare time, and I develop
         | & maintain a handful of Nimble packages. Someone might actually
         | be using them, too. None of my packages require regular
         | maintenance because Nim's tooling is dead simple and the
         | language is really easy to work with. It's a great opportunity
         | for an enthusiastic hobbyist to be a big fish in a small pond.
        
           | vaylian wrote:
           | It's been a while since I last played around with Nim (back
           | when it was still called Nimrod). But doesn't Nim compile to
           | C and therefore it can interact with the entire C ecosystem?
           | And afaik it is also camel-case-insensitive, so that you can
           | call C functions without having to worry about different
           | naming conventions?
        
             | ryukoposting wrote:
             | > But doesn't Nim compile to C and therefore it can
             | interact with the entire C ecosystem?
             | 
             | You can directly call C code, yes. You can even work with
             | macros! It's a lot nicer when you can import a package that
             | provides an abstraction layer over the C code, though (for
             | example, my `ruby' package)
             | 
             | > call C functions without having to worry about different
             | naming conventions
             | 
             | Yes. In fact, if you hand-write your C bindings instead of
             | using something like c2nim, you can completely change the
             | name of the function. This is a snippet from my
             | aforementioned `ruby' package:                 proc
             | evalString*(code: cstring): RawValue {.importc:
             | "rb_eval_string".}
        
           | jdrek1 wrote:
           | The ecosystem might be small but as you said it can grow and
           | I think there's enough people out there willing to give a new
           | language a try if it seems appealing enough, it's quite
           | common to try a new language for Advent of Code for example.
           | 
           | That being said, honestly the thing that stops me from
           | trying/using Nim that the compiler enforces a style and that
           | style is just wrong (it enforces spaces). Same goes for Go
           | which enforces the opening brace on same line style. The
           | people writing those languages can have whatever style they
           | like for their code, but forcing me to use it is very off-
           | putting to say the least. I get that this is probably a minor
           | issue for most people but I can't deal with change that well
           | and things like this are super annoying, especially since
           | there is not one single reason for forcing a certain style on
           | everyone.
           | 
           | I'm aware that `#? replace(sub = "\t", by = " ")` works, but
           | it's a hack and I'd have to inject it everywhere, not a good
           | solution. But at least it's better than Go in this regard.
        
             | ryukoposting wrote:
             | > That being said, honestly the thing that stops me from
             | trying/using Nim that the compiler enforces a style and
             | that style is just wrong (it enforces spaces)
             | 
             | I agree that the spaces thing is weird. Personally, I don't
             | care what style anyone uses in any language. Nim doesn't
             | care either (besides tabs, for some reason). All I ask is
             | that your style is readable and consistent.
             | 
             | It seems like every developer has hot takes about code
             | style. With Nim, no matter what hot takes you have about
             | code style, I can import your module without you forcing
             | your style into my code.
             | 
             | I'm a firmware engineer by day, and it seems like every
             | embedded C codebase on earth uses a different style. For
             | me, it's refreshing to be able to write code that's styled
             | consistently, regardless of the styles used by
             | dependencies.
        
         | verdagon wrote:
         | I say this as a fellow language developer: Nim is impressive as
         | hell!
         | 
         | I'm particularly excited about their region isolation [0]. A
         | lot of new languages are exploring it, and it has a lot of
         | potential to change the way we program over the next decade.
         | 
         | [0] https://github.com/nim-lang/RFCs/issues/244
        
       | pabs3 wrote:
       | Are there any compilers that let you run arbitrary code (like
       | running external processes) at compile time?
        
         | valcron1000 wrote:
         | Haskell's GHC allows you to do anything at compile time.
         | Literally anything.
        
       ___________________________________________________________________
       (page generated 2022-04-26 23:02 UTC)