What Makes the Zig Programming Language Unique?
erikexplores.substack.com
erikexplores.substack.com
Programming language experts told Andrew Kelley, the creator of the Zig
programming language, that having code which could run at compile time was a
really dumb idea. But he went ahead and implemented it anyway. Years later,
this has proven to be one of the killer features of Zig. In the Zig world, we
call it comptime, from the keyword used to mark code required to run at compile
time or variables to be known at compile time.
Which experts? The "comptime" is just macro-expansion from Scheme/Lisp which has been around for a long time. Aren't C++ templates also "code that runs at compile time"?Circle, mentioned elsewhere, is a C++ dialect with fully working compile time metaprogramming.
And the error handling is even only 10% working. clang is fine, gcc is broken for years.
Or is it still for in-house use only?
reportedly his game "The Witness" was written in it. He has a series of video's where he explores what the language will look like as he's developing it, and has claimed he'll open source it once he feels it's ready. He wants to maintain control until he feels it's no longer half-baked.
IIRC, he demonstrates things like a music player running during compile time. It's a core tenet of his language.
Probably the best place to see it in it's current form is his development streams on Twitch: https://www.twitch.tv/j_blow. Right now the most active streamer besides Jon is Raphael, who does a lot of work on the Jai compiler: https://www.twitch.tv/raphael_luba. Sadly, the other people I know who stream their work with it haven't been streaming much lately.
No that was still written in C++. The new game that he's working on is written in Jai.
I agree with this, but there may still be times when it is needed.
I'm working on a system where users can run a build in an interpreter that kills the build if it tries to do anything they don't allow. An important and trusted project that needs a special permission will probably be allowed to do that, but random packages? Not at all.
And yes, this is a trillion dollar problem. I may be just some bloke with an idea, trying to make it happen. However, I have the free time to try, so why not?
However, I think that if an interpreter does the execution, the interpreter can check every "instruction" that is executed. If every instruction is checked, how could that be evaded?
Honest question because I can't see how, and I need to.
The context for this is an interpreter that doesn't allow direct syscalls; only syscalls through the interpreter.
It seems even D has that because dynamic allocation could call `mmap()`, and you mentioned that D allows comptime dynamic allocation. So I'm confused.
Whether this is desirable is dubious. But fundamentally it's not any more dangerous than Mara's nightly_crimes! Rust proc macro (which during compilation replaces your running compiler with a compiler that believes it is compiling the standard library and thus allows non-stable "nightly" features even though you aren't running a nightly build, then casually alters the compiler output to say that this was fine and there's nothing to worry about), or any number of other tricks which result from being powerful enough at compile time.
I doubt that D is as powerful as people would want and yet manages to ensure this can't happen. There are Rust people thinking about WASM sandboxing for this, but it's tricky.
with the GC, which is safe
Saying the compiler can't run user code and make system calls while compiling is is like plugging a whole the size of a bus with your pinky. You prevented absolutely nothing. You just make devs jump through hoops (external tools in their build) to do the things they need to do and in doing so force them to add dependencies which expand their surface area of attacks.
Maybe a command-line switch to say which files can execute code at compile time but still, having worked on a lisp system that ran code and made system calls at compile time it was a huge time saver. Example:
constexpr max_buffer_size = getSizeOfLargestFile("assets/*.gltf");
enum Modes = enumerateSupportedModes("modes/*.el");So at least some IDEs are more cautious about this these days.
Does there have to be? Is there a language that has absorbed its build system into it?
In a language like D you could just execute the same system calls in the executable that was compiled. The assumption that you compile something and then not run it doesn't make this any safer, does it?
Zig's use of comptime is fairly revolutionary in a c-like language. Compared to the shenanigans required by c++, the entire language* is (should be) available at comptime. Compared to the backtick/quote/macro magic available in lisp... the awesomeness isn't that it's comptime, the awesomeness is that it's type safe.
But, I'd say it isn't even the use of comptime that's particularly noteworthy, it's the lack of pretty much every feature that isn't comptime. This wouldn't be a good idea without comptime being as robust and featureful as it is.
* nope, see puffoflogic's reply
I haven't used Zig (yet, maybe one day), but does anyone know if there's a difference between Haxe macros and Zig comptime? AFAIK Circle also offers something similar for C++ (https://www.circle-lang.org/)
This makes it less expressive, but it also means it is easier to learn and use. (It's a lot like C++'s constexpr)
The interesting part comes from recognizing that by making constructs like structs first-class-values in comptime, you can achieve most of the common things that templates, macros, conditional compilation, etc give you, but with a much simpler feature (which is just running code at compile time)
Like with Zig, you are limited mostly by what is available in the environment at macro expansion time; unlike Zig, you can do things that generate allocations.
> but with a much simpler feature (which is just running code at compile time)
Common Lisp does that with the EVAL-WHEN construct, which allows to run arbitrary code at compile time.
That’s exactly what Haxe macros are. It’s regular Haxe code that runs during compile, and it has access to all Haxe language features and the standard library.
The only special thing about them is that they can directly manipulate the AST before it gets passed on to the compiler for final code generation.
In a sense, they’re like “shaders” for the compiler.
Fixed-size allocators are not composable. You cannot write any kind of comptime routine which calls further routines which need to do allocation, and how much allocation they do is based on arguments to your routine - except by forcing your caller to calculate how much allocation needs to be done. But that leaks your implementation details to your caller, largely invalidating the point of making some kind of comptime routine in the first place. Therefore comptime can be used as a toy, or in C-style language-enforced NIH mode (where you [re]write every single routine you need yourself, or at least know the intimate implementation details of all of them), but not in a serious programming stance.
Runtime allocators also have a limit to how much memory you can allocate. It's usually higher, but there's still a limit. Your computer doesn't have an infinite amount of memory.
Overall I am not the right guy for this conversation because I am too used to never calling mmap at runtime either.
I want to give zig a fair assessment so yes, this is absolutely right. This is why I have elsewhere said that zig is basically two separate but superficially similar languages. You can probably accomplish just about anything in comptime zig, if you're willing to completely reimplement it from the core language up. The limitations I mention come in if you try to write code to be used in either comptime or runtime zig. So comptime zig has no growable arrays, no dictionaries, no string searching, no regexes, etc. etc. until these are reimplemented specifically for comptime zig. Mostly this is just sad, that a decent idea was implemented with such a terrible downside that would be so easy to fix: just add allocators. Just. Add. Allocators. But no, the zig team has explicitly rejected this.
I'm torn on this. To me, comptime is the C preprocessor but sane. Anything you can do above that is bonus. I don't need comptime to be totally Turing complete.
And, I do NOT want macros in the language. Macros are a bottomless well of suck. Lisps/Schemes still tie themselves into knots with macro hygeine. Rust's procmacros are terrible and debugging them is worse--just try figuring out what some intermediate expansion did.
The point of Zig is to NOT be Rust/C++/etc. The point is to be C with a bunch of the sharp edges and corners filed off. It's not for everybody. If you want web services, use Go. ML/AI--Python for ecosystem. Rust for strong safety guarantees.
Zig has some nice things while still remaining relatively small. It, like most modern languages, understands that ecosystem is important. It does a better job than almost everybody at cross compiling. comptime gets you a lot of the benefits of templates while dodging lots of the suckage.
However, at some point, you have to declare "Stop, that isn't going to be part of the language" or you wind up with the ungodly mess that is C++.
Sure, if it's all code you're writing yourself then it's doable to avoid using an allocator. If you want to reuse existing code that does expect an allocator, however, then being able to use a FBA is handy.
One major use is to create strings at compile time, which are then fed back into the compiler as D code. I.e. metaprogramming.
[1]: https://ziglang.org/documentation/master/std/#root;fmt.compt... [2]: https://ziglang.org/documentation/master/#compileError
https://github.com/ziglang/zig/issues/1291
This is an open issue from 2018 that I opened that specifically acknowledges this as a valuable use case that should work.
Don't let puffoflogic put words in my mouth. They are a troll account.
I might phrase that as "statically-typed language". Most (if not all) of the dynamically-typed languages are technically programs that run at the only "compile time" there is and can do arbitrarily whacky things as a result. It is generally a good idea not to think of them that way. I've removed quite a few top-level Perl statements in my time and enforced rules that a module really should be nothing but declarations of constants and subroutines unless there's an absolute emergency. But dynamically typed languages will generally let you read some files off a disk, query a database schema, and hit an HTTP API to build a "module" if you want to.
And in this case the entire language really is available. The limitations tend to come with the increasingly fragile order of operations the modules impose on each other, rather than technical capability.
I don't say this to slag on Zig. Increasing the range of what static languages can do is a legitimately interesting topic. This is to shed light on the general space of programming langauges.
But yeah, dynamically typed languages have been doing this for decades, particularly prevalent in Perl circles.
Sometimes it's legitimately an innovation just to put it into a standard library from the very beginning. I've discussed on HN a couple of times how useful it was for Go to ship with an "io.Reader" definition for "things that emit bytestreams" in the standard library. It has very little to do with language features; many other languages could theoretically have done it. Many languages have a sort of half-usable "file" abstraction, but you can never quite tell what calls will work on which type of thing. Having a single concrete interface in the standard library from day one of the Go language drove the entire ecosystem in a single coherent direction, though, and in Go, you can pretty much count on that interface and can nest it quite deeply without hitting a corner case.
I expect it may be something like that for Zig... it's not that nobody has ever integrated code generation at the compile step for a static language before, it is merely that I'm not sure anyone's ever pushed it out like from the very beginning at this level. I may be wrong. I can come up with several examples of much more dynamic languages doing it. I'm sure out there in the world you can combine Lisp macros with some static type system for the same language. But that would be a bolt-on, not something that was in the standard lib from day one.
Thinking of languages not just in terms of features, but in terms of what community the standard library affords is probably the next major frontier in language design over the next 20-30 years. Yeah, I hope that we still see some sort of revolutionary language that solves all our problems, but I expect to see a lot of languages that don't necessarily have any new features per se but are just better standard libraries from day one.
It it is not revolutionary in the context of such languages. Out of the mainstream but not revolutionary.
The D programming language popularized it in 2006 or 2007. Here's it in action:
int square(int x) { return x * x; }
int[square(3)] a; // declare array `a` of 9 ints
The embedded C compiler in D, ImportC, also can do CTFE: int square(int x) { return x * x; }
_Static_assert(square(3) == 9, "Error, Will Robinson!");
You'll see a lot of that in the test suite for ImportC, because the test suite runs a lot faster if it doesn't need to link and load. int[4] squares1 = [0, 1, 4, 9]; // doing it by hand
int[4] squares2 = () {
int[4] a;
foreach (i; 0 .. 4)
a[i] = square(i);
return a;
} (); // doing it with CTFE
squares2 declares a lambda, which returns the initialized array, and then executes the lambda.I actually include myself in that camp, so thanks for taking the stage :D
https://www.wildernesslabs.co/
https://nikolayk.medium.com/getting-started-with-unity-dots-...
Had Remedy experiment worked out, maybe DOTS would be using D instead of having their own compiler infrastructure for HPC#.
Nor Java even,
https://www.ptc.com/en/products/developer-tools/perc
https://www.aicas.com/wp/products-services/jamaicavm/
The main reasons are not having a big name company that would push D no matter what, and lack of focus where D should aim for.
You can ensure your D code doesn't use the GC by adding the `@nogc` attribute.
But really, all this concern about the GC is misplaced. It's just another tool available. You can use it in D, or use RAII, or your own allocation system. It's your choice as a D programmer.
I am giving this analogy since Walter has a degree in mechanical engineering and he has correctly made the GC a default in D but not mandatory, but the choice somehow is not popular in the software industry.
int squares1[4] = {0, 1, 4, 9}; // doing it by hand
std::array squares2 = [] {
std::array<int, 4> a;
for (int i = 0; i < 4; i++)
a[i] = i * i;
return a;
} (); // doing it with CTF
Is as pretty? Not as much, but it does the job. string cat(string s) {
return "int " ~ s ~ ";";
}
int test() {
mixin(cat("i"));
return i + 3;
}So what if D wins at code golf against C++?
What matters is being good enough for most compile time use cases, while enjoying the large ecosystem and IDE tooling, perfect is the enemy of good.
Indeed C# and C++ don't give me all nice features from D, when doing language comparison tables, D wins on that table listing.
Yet they give me their library ecosystem, graphical IDEs, and first day avail on any major company SDK.
I will take that productivity win over the few features that the D language happens to have.
Why is Andrei Alexandrescu helping to improve C++ at NVidia instead of using D, when the language is so great?
Ecosystem, that is why.
const std = @import("std");
export fn square(x: c_long) c_longlong {
comptime std.debug.assert(@sizeOf(c_longlong) > @sizeOf(c_long));
return @as(c_longlong, x) * x;
}
If I run `zig build-obj` on that on my i9 Mac, compilation fails with an assertion error. If I run `zig build-obj -target arm-freestanding-gnu` instead, it compiles since sizeof(long) is 4 and sizeof(long long) is 8.Yes:
c_longlong square(c_long x) {
static assert(c_longlong.sizeof > c_long.sizeof);
return cast(c_longlong)x * x;
} template MakePtrType(T) { alias MakePtrType = T*; }
MakePtrType!int p;
pragma(msg, "p is type ", typeof(p));
dmd -c test2.d
p is type int*
or perhaps I am misunderstanding you?Edit: So the main difference seems to be that types are first class citizens in Zig, you can write a normal comptime function that returns a type and either use it to do more metaprogramming or everywhere you would declare a type for a function or variable. I am not aware of any static typed languages that lets you do this with normal language syntax.
C++ does it with templates, but in Zig you can do what C++ templates can but with if statements and loops.
Please show me an example where it differs. D's metaprogramming is very good at constructing types. A member of the D community tried very hard to implement type functions, but it turned out to be redundant with D's existing capability.
Not revolutionary. Before Zig came out, which was 2016, there was Jonathan Blow talking about and arguably repopularizing it. Lots of people (who knows how many developers) and languages were influenced by him too.
2014, Jonathan Blow, who made lots of videos and gave lots of talks, before going on to officially promote Jai (2016). And it appears he was playing around with prototypes much earlier and talking about it (from 2014).
https://youtu.be/UTqZNujQOlA (Demo: Base language, compile-time execution)
It's not, it's just fairly insecure. comptime is known forever, in static and dynamic languages. Better languages do know about the security and usage implications and do something about it.
See AIM-57: http://publications.csail.mit.edu/ai/browse/0000browse.shtml
And for what, exactly? Your audience is the same audience you're claiming told Andrew it was a dumb idea, you're basically calling your audience dumb.
I choose to par-phrase him, and had no idea that this would get anyone this upset and lead to me being accused of being a liar. How much of experts these people where I cannot vouch for. Maybe they were second rate language designers or maybe Kelley referred to older papers advising against it.
Either way I cannot find the exact video anywhere where he made this remark despite looking through several. Does it really matter? Can you say for a fact that no language designer expert ever told Andrew Kelley this? I am not claim this is the general opinion among designers, only that this is what Andrew got told.
If anyone has the correct quote, I'll be happy to update the article.
> I was writing that intro based on recalling Andrew Kelley stating in a video
When you are writing for a technical crowd, it is good to be prepared with references and evidence. Unsourced information is likely to be questioned. Unsourced information making dubious claims that go against half a century of computer programming and practice is even more likely to be questioned.
> Can you say for a fact that no language designer expert ever told Andrew Kelley this?
That's not a valid counter-argument. Can you say for a fact that Russell's teapot does not exist? That does not make the existence of Russell's teapot any more likely! The burden of proof lies on you to show that the bold claim you made in your article holds up against scrutiny.
Making this into a huge topic that requires evidence and citations seems a bit over the top. I would say you guys don’t know how journalism works. You equate article writing with scientific journals and papers. They are not the same thing.
I think what is at the core is getting the language description correct.
> There are a lot of people in this thread saying that I said stuff I didn't say. It's maddening.
I have done it in the past and what I learned is that it can give a temporary boost of endorphins, but it's very short-lived and it doesn't really help anyone in the long term, including yourself.
I will try to lookup this video when I have time. I was a bit surprised that this remark was a such a turn-off. Had I known, I should have looked up the video to get the exact quote.
It was just meant as a bit of a funny dramatic entry to discussing an unusual language design choice for a statically type language. Sure we are seeing more things like const expressions in C++ today, but at the time this wasn't very common.
People can dismiss language ideas and concepts that "have been known and used for many decades" just the same.
For a trivial example: significant whitespace.
So, e.g. "Lisp has had that since forever" is not a guarantee people would be OK with it, unless they also like Lisp - not to mention they might not even know about it in Lisp.
As for C++, many people consider templates - at least as implemented - a bad idea. So some people knowing that it existed in C++ already, and considering it a bad idea to have in Zig is not contradictory.
The phrase "They said it couldn't be done!" is an evocative proxy for technological novelty. If the experts are not representative, the claim of novelty will be misleading.
Well, it did create a big mess and overhead in C++ for one (not to mention adding another turing-complete language on top of it)...
This was actually a schism in Lisp and why FEXPRs died out. CS theory wanted to compile things for speed and FEXPRs very much didn't fit into that. This is why macros took over--they were very much targeted at compile time.
The late John Shutt's thesis and work on the language he called Kernel goes into quite a bit of detail and the history about it: https://web.cs.wpi.edu/~jshutt/kernel.html
Sadly, Shutt never really "completed" the Kernel language--the language is very interesting but is extremely slow and unwieldy in the descriptions he gave. It generates a lot of garbage and extra "environments"; it resists compilation and memoization; it sometimes seems to go out of its way to deal with things that make life very difficult (circular and infinite lists).
A couple of good passes of optimization might have produced something very interesting.
I was personally attracted to it for very small embedded devices (<16K RAM/16K Flash) as it delgates a lot of macrology to interpretation--sadly I just couldn't get the footprint down far enough given all the garbage and environments the language generates. I suspect it's possible, but it would take someone who knew the landscape intimately to figure out what needed to be dropped.
> infinite lists
Sounds like an early attempt at Haskell. Haskell is lazy by default so you can write your own "if" as a function and it will be just as short circuiting as the built in if. And infinite lists show up in basic Haskell tutorials.
However Haskell combines this laziness with immutability, which is an easier combination to reason about than laziness with mutable side effects.
Compiler technology for lazy languages is definitely impressive, but it's still the case that if you want to squeeze the last bit of performance out of Haskell code it does usually require manually forcing eager evaluation.
See: "Special Forms in Lisp" by Pitman https://dl.acm.org/doi/pdf/10.1145/800087.802804
Shutt's thesis does a good job talking about the history.
At the moment you can't do comptime allocations, so everything has to exist on the stack, but you can, for example, embed a file like a config or dataset, and run it thru a parser to turn it into some data structure, which is something you might otherwise work into the build system, running python/perl scripts to generate temporary code files, but now you can cut out the extra dependency by having that code generation in the same language.
Could it be because some of it, say, places a delegate, bound to a closure, into an immutable struct within a child thread's run function, and passes that to another thread via a send to be called?
This means, if a C library is available, D can use it.
(There are some exceptions. While ImportC will handle C preprocessor metaprogramming just fine, those metaprogramming macros won't be available to the D code, other than simple #define manifest constant declarations.)
Can't agree more. Such a powerful language but with an absolute meager ecosystem! No dogfooding.
GC and crashes are pretty common on long running high performance apps. They should just build things from the ground up using betterC.
int i = f(3); // evaluate at run time
enum i = f(3); // evaluate at compile time
Note that it's not the function that is specified as being run at compile time, it is the use of the function that determines it. There is no difference between compile time functions and runtime functions. The use that triggers a function to be evaluated at compile time is when the function call is in a const-expression. enum res = toggle_gpio(FRONT_LED_2);
Obviously, there is no difference between run-time functions that are qualified for compile-time execution, and compile-time functions.Then, suppose we have all these requirements
- We want a separate compilation model where the code which is calling f(3) at either compile or run time knows nothing about how f is defined, only how it's declared.
- We want not to include the compiled image of f in the target program, if if is only called at compile time, only if at least one call to it somewhere in the program is staged to run-time.
- We want to make sure that if f is called at compile time, nobody can change its definition to accidentally call for run-time semantics like toggle_gpio(FRONT_LED_2).
and that's how we probably arrive at a declarative mechanism like constexpr that goes on the function, rather than trying to orchestrate thing remotely/indirectly by placing a call to the function into a compile-time context.
Not including functions never called for runtime execution in the runtime is a normal compiler optimization. No user input is necessary.
If the function's semantics change so it is no longer runnable at compile time, the compiler will let you know when you try it. If you want to force the detection, just call it at compile time with dummy declaration.
> that's how we probably arrive at a declarative mechanism like constexpr that goes on the function, rather than trying to orchestrate thing remotely/indirectly by placing a call to the function into a compile-time context.
This issue is raised now and then, and in 16 years of CTFE in D it has never ever been reported as causing an actual problem.
Great we have many good languages to choose from
So when I see comments like this that say "D also has X feature", I'm curious about why D didn't take off but Zig (likely) will.
In Zig's near category, there are other languages like Odin, Jai, Vlang, Rust, etc... For example, by the time Zig reaches 1.0, Jai (which is in Beta) might be publicly released, others languages might have added some "killer" features, and/or others in development might have also reached production quality.
Zig has quite a long way to go, and if anything may never achieve the popularity or widespread usage of D. Examples of the difficulty of programming languages rising through the ranks, is Nim and Crystal. Both Nim (2008) after 14 years and Crystal (2014), have not even made it into the top 50.
And no Vlang is not a programming language, it's a scam and should be treated as one.
Stop deluding yourself. If we take out TIOBE, and use the IEEE's Top Programming Languages of 2022 (https://spectrum.ieee.org/top-programming-languages-2022), D is still recognized (even higher than the 30s) while Zig, Nim, or Crystal do not even make their chart. That's the reality. The point is, it's hard to climb the charts, and not to underestimate the positions of established languages or that they will suddenly be abandoned.
> Vlang...
To begin with, you are a known troll (with possibly multiple troll accounts at HN) that has spent over an year engaging in slander, lies, and flames about the language almost any time it's mentioned.
Looks like you are the creator or involved with some other language that can't get as much support and popularity, and have taken to being underhanded. It would be advisable for you to stop being obsessed with Vlang. How many years of your life will you waste on such childish antics? You would be better off focusing on making your programming language better, if it's not already too late and its a failure.
As for pushing this bold face lie that the language is a "scam", that's both ludicrous and easily proven false. Vlang has many hundreds of code examples and projects. Easily found at:
1) Vlang examples on GitHub (https://github.com/vlang/V/tree/master/examples).
2) Vlang at Rosetta Code (https://rosettacode.org/wiki/Category:Vlang).
3) Awesome Vlang at GitHub (https://github.com/vlang/awesome-v).
The "scam" is you tricking yourself into thinking that your troll tactics are working. Instead, your continual and unnecessary slander is making what you are even more obvious.
So fopen, print and all IO or OS functions should be avoided, or even warned. It might be useful in certain edge-cases, but in my 20 years of compile-time programming, it's almost ever a bug, not a feature.
Example: the BEGIN block is executed during the compile phase as the parser encounters it. The code injected in BEGIN blocks can affect how the rest of the file is parsed.
$ perl -E 'exit; BEGIN { say "Hello" }'
Hello
"perl -c" stops the program after compile, but before execution. In the following code the BEGIN block runs, but not "exit 1". $ perl -c -E 'exit 1; BEGIN { say "Hello" }'; echo $?
Hello
-e syntax OK
0
More info:* https://perldoc.perl.org/perlmod#BEGIN,-UNITCHECK,-CHECK,-IN...
entirely at compile-time, I was able to generate lookup tables for "given this cell index, give me the indexes of the cells in its row, column, and box". and I simply looped over that for each board size I wanted to support, from the standard 9x9 all the way up to 64x64.
another useful feature was support for arbitrary-sized ints [0] and packed structs [1]. for a 9x9 board, a cell can be represented with a 9-bit bitfield for possible values; a uint4 for the actual value; and a single bit indicating whether the value is known. Zig will happily pack that into a struct that occupies only 16 bits, and handle all the bit shifting & masking for you when you access the struct fields.
this meant I was able to represent the state of a 9x9 board in just 162 bytes - a flat array of those packed structs. and the exact same code could operate on the ~28kb of state needed for 64x64.
if you want control over the memory layout you need either a packed struct or an extern struct [0].
yes, normal structs would have worked fine - like I said, I was learning Zig in the process, so I was going out of my way to experiment with its unique features.
So thank you for this actual concrete example of it being used. But why is that useful? Why regenerate a database every time you compile? Now that you have compiled the program once, can you not just delete that code and reduce your compile time to a fraction of what it was?
Adding a compile-time parameter to a function is considerably more convenient than writing a code generator. Zig's compile-time performance might be a problem someday, but there is likely not enough Zig code yet for this to matter. Perhaps they'll add caching if it ever turns out to be an issue.
I have had comptime lookup table generation and similar stuff crash the compiler or take a long time to run. I fell back to external code generators in these cases.
In Lisps, for example, that phase is called the macro-expansion phase (of which there can be layers of) and generally acts as a source-to-source transformation from s-exp to s-exp (possibly with side-effects). Here's an example in a Gambit Scheme REPL:
> when
*** ERROR IN (stdin)@8.1 -- Macro name can't be used as a variable: when
> (when #t #f)
#f
> (pp (lambda () (when #t #f)))
(lambda () (if #t #f))
So you can see the 'when' macro expands to an 'if', and that has to happen _before_ evaluation. So the REPL is in fact a REEPL (Read Expand Eval Print Loop).Very useful.
A more natural form of duffs device. https://en.wikipedia.org/wiki/Duff%27s_device
The Go community has a rule of committing both the code generator (for later regeneration and maintenance) and the generated code (to avoid rebuilding it every time).
64x64 seems to need 64 bits for values, ie 8 bytes before we consider the known case, but there are 4096 cells. So that's surely 32kB of state immediately ?
It seems to me that if we don't care to distinguish whether we "know" a value for which there is only one possibility, we can save encoding this, so the 9x9 board only needs 9 bits per cell, the 64x64 board only needs 64-bits.
In this case we can answer "known?" as is_power_of_two() and we can find the known value using e.g. trailing_zeros() which is cute.
On the topic of comptime lookup tables, transcendental discretizations are popular in graphics programming (rescale sin/cos to fit appropriately into some integer type, make an array of them to turn transcendental approximations into array lookups). Those are easy to make flawless and fast in Zig.
Another thing that enables without too much song and dance is compile-time duck typing. You can write a function that easily handles a call like randint([][4]@Vector(12, u8), .{Min(0), Max(13), my_allocator, Shape(.{12345})}) which would allocate that type with an appropriate size and fill it with random integers within the prescribed bounds. It's perhaps a bit more verbose than a truly dynamic language, but comptime type introspection enables that sort of powerful control flow without especially burdensome implementations or any runtime performance cost (ignoring cache effects and whatnot), and the whole thing is statically guaranteed to work (within the bounds of what Zig actually promises).
- ability to import and use C libraries, no FFI or custom bindings required
- clear & logical distinctions between different pointer types and arrays
- allocators as a first class concept. this makes zig really safe because you can pass in test allocators that will report leaks etc.
If you enjoy hot takes and ancient memes, I made a talk about zig a couple of months ago where I lay out why I think it's The Chosen One to succeed C.
Not knocking Zig, I think it's swell, but it wasn't near the first language with this feature. D comes to mind, and C++ has it now with "constexpr" and "consteval".
Yet Smalltalk was not the first language supporting image based development. I would pick Julia to talk about multiple dispatch. Likewise Zig is picked not because it is the first doing comptime but because that is a central feature of Zig and it is a language which really puts emphasis on it.
Really? I can't believe that! Running code at compile time is as old as Lisp! And it is present in some form or other in some other popular programming languages too. Like constexpr and templates in C++.
> experts told Andrew Kelley, the creator of the Zig programming language, that having code which could run at compile time was a really dumb idea
> Running code at compile time is as old as Lisp
Old ideas are not immune to be considered dumb by some people.
Also I find Zig's choice to use abbreviated keywords rather cryptic, for example using 'fn' instead of 'function' only hurts readability I think.
And if you go digging, I think you can still find solutions for last year’s advent of code.
I am not super familiar with zig, but I would say I am not a fan of using the entire word "function"
I believe "fn" is used in rust and I think its readable.
Maybe something like Go's "func" is better, but I really feel this is all arbitrary.
Again its personal preference, but I'm curious if there is a language you use often that makes you like that specifically?
Java is a bit like that, but has the C++ data types as its function key word basically. I'm trying to think if I am forgetting one.
Larry Wall, its designer, explicitely choose short words for common keywords.
I don't get the problem with `fn`, though. Pretty much every language uses abbreviations like that and I've never heard anyone complain about it: enum, char, int, uint, def, ...you get used to it quickly, you need to build a mental model for them either way.
If that's not the case (or maybe plans changed?) I stand corrected. It's definitely not my intention to spread BS.
> There are a lot of people in this thread saying that I said stuff I didn't say. It's maddening.
https://news.ycombinator.com/item?id=32752383#32752885
I bought into the focus on the build toolchain, until I found out my old Mac wasn't supported.
Really the only thing I don't think I've ever seen before is how zig passes around memory allocators. And that's probably because I'm not a systems programmer by trade, so I'm less familiar with that sort of thing.
How does Zig handle the monomorphization problem for generics in library APIs? In other words, give the `maximum` function from the blog post, if I want to distribute a binary but make that function available to clients of my binary to call with different types, what does Zig do?
Haskell templates are in haskell, the "compile time and run time language are the same" feature is not unique either.
If anyone reading this is interested in compile-time computation, you owe it to yourself to read Paul Graham (of HN)'s own work on compile time programming, "On Lisp"[0], or to take compile-time ideas to the next level, Doug Hoyte's "Let Over Lambda"[1] (LOL). Lisp languages have a long and interesting history of compile-time computation via their various macro systems, and given the first-class inclusion in most Lisps, a greater variety of ideas have been explored regarding compile-time computation.
A few interesting examples:
LOL's "Pandoric macro"[2] lets you monkey-patch closure values. It's absolutely bonkers and would almost certainly be pathological to introduce to a codebase, but it's an example of pushing the boundaries of what's possible.
Common Lisp's object system, implemented via macro[3]. To be honest, the entire Common Lisp language is a masterclass when it comes to learning about macros (I can't avoid mentioning Scheme's hygeinic macro system and Lisp-1/Lisp-2.)
A special non-Lisp shout-out goes to Rust's Diesel library for embedding SQL DDL queries into macros[4] which is not something I've personally seen before.
Clojure has a few interesting (and practical) macro libs, particularly core.async, which is an implementation of CSP (similar to Golang's channels AFAIK), it embeds perfectly into the existing language and extends its capabilities even though it's merely a library. Another interesting lib which comes to mind is Meander[5], which uses term-rewriting to provide transparent data transformations. Think declaratively interacting with data structures at compile time, and the library figuring out the best way of turning it into imperative value manipulation code.
[0] http://www.paulgraham.com/onlisp.html [1] https://letoverlambda.com/ [2] https://letoverlambda.com/index.cl/guest/chap6.html#sec_7 [3] https://mitpress.mit.edu/9780262610742/the-art-of-the-metaob... [4] https://diesel.rs/ [5] https://github.com/noprompt/meander
Blatant, arbitrary compile-time evaluation (other than code transformation) is not that common in Lisp programs; which could be why such a macro wouldn't be used a lot.
For complicated, calculated constants, the Common Lisp defconstant has certain semantics that work in that space, where responsibilities are foisted onto the programmer: [A]n implementation may choose to evaluate the value-form [of defconstant] at compile time, load time, or both. Therefore, users must ensure that the initial-value can be evaluated at compile time (regardless of whether or not references to name appear in the file) and that it always evaluates to the same value.
In Common Lisp, arguably, defconstant is the mechanism of choice for blatant compile-time evaluation for actually obtaining a value from a complex expression which is thereafter treated as a constant. And its semantics isn't precise enough for the task of retrieving something from a specific environment. Using defconstant to access a file in the compilation environment won't work if the evaluation is done at load time.
In any case, defconstant looks very similar to these constant expression mechanisms that are cropping up.