[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)