Zig self hosted compiler is now capable of building itself
github.com
github.com
The Zig self-hosted compiler codebase consists of 197,549 lines of code.
There are several different backends, each at varying levels of completion. Here is how many behavior tests are passing:
LLVM: 1101/1138 (97%)
WASM: 919/1138 (81%)
C: 740/1138 (65%)
x86_64: 725/1138 (64%)
arm: 490/1138 (43%)
aarch64: 411/1138 (36%)
As you might guess, the one that this milestone is celebrating is the LLVM backend, which is now able to compile the compiler itself, despite 3% of the behavior tests not yet passing.The new compiler codebase, which is written in Zig instead of C++, uses significantly less memory, and represents a modest performance improvement. There are 5 more upcoming compiler milestones which will have a more significant impact on compilation speed. I talked about this in detail last weekend at the Zig meetup in Milan, Italy[1].
There are 3 main things needed before we can ship this new compiler to everyone.
1. bug fixes
2. improved compile errors
3. implement the remaining runtime safety checks
If you're looking forward to giving it a spin, subscribe to [this issue](https://github.com/ziglang/zig/issues/89) to be notified when it is landed in master branch.
Edit: The talk recording about upcoming compiler milestones is uploaded now [1]
The reduced memory has significant value. Being able to do the same build on less expensive hardware or do more with the same hardware is a significant financial performance improvement
Does using Zig over C++ lead to "less memory, and represents a modest performance"? Or was the C++ implementation a bit sloppy? (lacking data oriented design for instance)
Also, what specifically are you most excited using Zig for?
The new Zig implementation is certainly more well designed than the C++ implementation, for several reasons:
* It's the second implementation of the language
* It did not have to survive as much evolution and language churn
* I leveled up as a programmer over the last 7 years
* The Zig language safety and debugging features make it possible to do things I would never dream of attempting in C++ for fear of footguns. A lot of the data-oriented design stuff, for example, makes use of untagged unions, which are nightmarish to debug in C++ but trivial in Zig. In C++, accessing the wrong union field means you later end up trying to figure out where your memory became corrupted; in Zig it immediately crashes with a stack trace. This is just one example.
* Zig makes certain data structures comfortable and ergonomic such as MultiArrayList. In C++ it's too hard, you end up making performance compromises to keep your sanity.
Generally, I would say that C++ and Zig are in the same performance ballbark, but my (obviously biased) position is that the Zig language guides you away from bad habits where as C++ encourages them (such as smart pointers and reference counting).
As for less memory, I think this is simply a clear win for Zig. No other modern languages compete with the tiny memory footprints of Zig software.
Some of the projects I am exited to use Zig for:
* rewriting Groove Basin (a music player server) in zig and adding more features
* a local multiplayer arcade game that runs bare metal on a raspberry pi
* a Digital Audio Workstation
If I understood it correctly, you think smart pointers and reference counting are bad habits. Why? Especially the smart pointers bit.
Why does Zig use less memory than other languages? Is it inherent to Zig, or can it be reproduced in other languages?
These are things that a "systems language" programmer should put in the work to do correctly/near-optimally, and not ask the compiler to just do something "good enough", like Python would.
I don't really feel this is the case in Rust.
You can easily add a manual “drop” call in Rust at any point if you want to force an allocation to be freed sooner, but I speak from years of experience using Rust for work when I say that Rust’s RAII model is not problematic in practice. I’m not simply speculating or theorizing, and I have professional experience with a variety of languages at all levels of the stack. I personally don’t mind garbage collectors most of the time, but Rust is great when you need more control.
In C++, RAII can absolutely be problematic because you are able to easily do things that cause undefined behavior by accident, which is arguably worse than either leaking by default or the mere act of holding onto memory for the duration of a scope.
If you can propose a system which cannot be contrived to have any downside, that would be fantastic! In the real world, Rust’s approach to memory management is extremely pragmatic and beneficial. I’m sure someone will eventually improve on Rust’s approach, but “leak by default” isn’t it.
I honestly do enjoy following Zig… it is a fascinating language taking a really interesting approach to solving many problems, but its memory safety story is not where I want it to be yet. Leaking memory by default is technically safe, but it's not exactly endearing.
Don't get me wrong. I do think that rust and c++ RAII is much more convenient and safe than the C or Zig way.
(I'd even prefer if you could annotate given struct in rust so the compiler could drop them as soon as it's no longer used, but that s not that simple)
It would probably be a breaking change to automatically call an explicit Drop implementation anywhere other than the end of the current scope, so I think that would have to be left as-is. String doesn't implement Drop, so it could easily be dropped eagerly within the scope as soon as it won't be referenced again. Such a change would be roughly equivalent to any of the compiler optimizations that reorder statements in ways that should be unobservable.
fn foo(xx : &Mutex<String>) {
let lock = xx.lock();
let ptr = &mut *lock as *mut String;
unsafe { use_from_c(ptr) }
}
You get the idea: you don't want the lock or the string to be dropped before the unsafe code, even if the actual string is no longer used. That's the breaking change. It's hard to detect automatically, so hard to justify even in an edition.Outside of uses of unsafe, are there any other serious problems with eagerly dropping values that manually implement Drop? Maybe this fallback mechanism would also have to be invoked simply for casting to a raw pointer anywhere in the function. Either way, a list of exceptions to eager drop would arguably be better than not having eager drop, as long as the list was sound.
It would still be a breaking change, and would definitely require at least being restricted to a new edition. Some people currently use Drop as a kind of scoped “defer”, especially for things like instrumentation, so maybe it would be time for the language to introduce a proper “defer” statement that exists for that purpose instead of making Drop so lazy for everyone.
I explained in my previous comment that you can explicitly "drop" any value at any time in Rust, if you choose.[0] But if you don't, it will still be dropped at the end of the scope. The developer has control, but the language will watch your back.
I don't even think most mainstream uses of PHP have done the process-per-request model for decades, but I could be wrong.
Here is a series of articles that describes it in detail
https://www.php.net/manual/en/features.gc.php
Especially look at this example where the same script is executed with and without GC
https://www.php.net/manual/en/features.gc.performance-consid...
I’m assuming by string you mean a stack allocated array of char and not a std::string.
For locks it is seriously the best way to manage them.
pub enum List { Empty, Elem(i32, Box<List>), }
instead of :
pub struct List { head: Link, }
enum Link { Empty, Some(Box<Node>), }
struct Node { elem: i32, next: Link, }
I thought Aria's "Entirely Too Many Lists" tutorial actually tries to build this, but it actually doesn't, she draws you the resulting "list" and then is like, OK, that's clearly not what we want, let's build an actual (bad, slow, wasteful) linked list as our first attempt.
The difference really lies in the fact that i now have some data stored on the stack (The element) and some data stored on the heap because it's recursive. Was just a random example of where it's poor practice. As others have noted, linkedlists are a terrible data structure to begin with.
There is a scalable solution for UAF that doesn't involve introducing a lifetime/region system: garbage collection. Of course, that comes with its own set of tradeoffs.
There are many cool possibilities that C++ has never explored and frankly I find your argument unimaginative.
From the man page (which I believe quotes ISO C): "The free() function shall cause the space pointed to by ptr to be deallocated; that is, made available for further allocation." If free doesn't do that because another allocation is pointing into that page, that's a violation of the contract. Standard quarantine doesn't violate the spec because the space will eventually become available no matter what, but perpetual quarantine does.
One way to fix the problem would be to round up all allocations to 4kB, but that's very wasteful and slow due to cache and page table traffic; you'd be better off from a performance point of view with a garbage collector. More promising is ARM MTE, though the tag is currently too small to really be called a solution to UAF as opposed to a mitigation. A 64-bit tag would be enough, but I'm not sure what the performance costs of that would be--I wouldn't be surprised if the increased memory traffic makes it slower than a GC.
Quoting and bolding. I think this is the thing that I need to change most about my own style of programming.
So how do you solve SIMD with C, without language extensions?
C is not a special snowflake.
Writing generic SIMD code is more portable. C has libraries, and Zig also has vector primitives available through built-in functions.
In fact, a few GC enabled languages have explicit SIMD support.
For certain problems you would want a TLA+ specification for safety and especially liveness either way. It's not like Rust absolutely guarantees correctness in all cases.
Rust sits in a sweet spot between C/Zig and languages like Java, but it's not an appropriate replacement for either of them.
I believe that Rust can be as low level as you want. If you have to fight the language to accomplish something, it's because you're probably trying to do something unsafe, and Rust will let you do that if you pay the price.
I was looking for examples of real cases where Zig is a better option than Rust. And while Zig is a lovely language, its main points against Rust are that it lets you do unsafe things more easily, and it's easier to learn.
I enjoy synthesizers (including Eurorack) and looking forward playing with a Zig DAW!
But in terms of the implementation, this means working with untagged unions, because the tags are maintained externally.
Woah.
A little bit ago I went to write a little tool that mucked with the FL Studio FLP format. It was easy enough to guess out the bits that I cared about, so I pretty much just did that using a couple quick projects with specific things in them. However, I did check to see if anyone else had mucked around with the FLP format, and couldn’t help but notice your name. Was pretty surprised as a very curious onlooker to the Zig programming language. You certainly seem to get around :)
That’s a little tangential, but I guess I mention it because I was actually wondering if this was ever something you planned on doing given the fact that it was clear you had dabbled with DAW stuff (forgive me for not knowing if you have a more rich connection to music production than just that; I never bothered to check.)
A DAW in Zig sounds like a kick-ass idea. I tried to write a sort-of DAW toy with friends in Rust and it was a lot of fun even if it mostly made me realize how challenging it could be. (And also how bad at math I am. It took me so much effort to feel like I could understand FFTs enough to actually implement them.) It makes me wonder if your Zig DAW would be open source? It would be a fun project to attempt to contribute to, if or when it ever came to fruition.
Exciting stuff. Congrats on the Zig milestone.
Do you understand how little DSP is involved in writing a DAW? It has almost nothing to do with DSP and everything to do with application architecture, data management, threading and more.
That said, Rust seemed pretty promising for writing the audio engine of a DAW due to the memory ownership model. It was relatively easy to come up with a way to architect a very basic lock-free audio thread and feel sure that it was at least memory correct. I have no idea how a real DAW avoids certain pitfalls in the audio thread; lock-free isn’t too hard, but avoiding allocations in all circumstances seems tricky.
Take a listen to my interview with Justin Frankel of Reaper - somehow in our 3hr+ epic chat, he acknowledges that they don't avoid locks entirely. And Ardour, despite trying, also fails to do so. Anecdotally, from my conversations with other DAW developers, they also do no manage it 100%. As Justin put it, it's more about contention avoidance than lock avoidance.
Avoiding actual stack-based allocation is pretty easy in C++. You just have to want to do it, and remember to do it.
Memory correctness is really easy in the RT threads of a DAW precisely because they do (should) not allocate. You're dealing with pre-allocated memory blocks that do not change over time. It's almost the simplest case of memory mgmt that I know of within a DAW. I have seen several new-to-DAW developers adopting ill-advised schemes for memory mgmt in an RT context. The "list of blocks" is one pattern that I consider something to avoid (and unnecessady). Single-reader/single-writer lock-free (circular) FIFOs get you almost all the way there, for everything.
The main reason I felt Rust was nice was simply the re-assurance that I could not, even if I wanted to, accidentally cause a data race within the confines of safe Rust. It’s not that the actual code was hard, but it did force me to ensure that what I was doing with the data model was actually safe. It wound up guiding how data flow worked in the playback engine.
Anyway, thanks for the pointer to the interview. I don’t expect myself to be writing the next Reaper or Ardour so it is probably OK if I don’t quite grok what you’re getting at. But, I do find this stuff rather interesting, so I would love to have a listen when I get a chance.
> As for less memory, I think this is simply a clear win for Zig. No other modern languages compete with the tiny memory footprints of Zig software.
Is not true at all. There are several other modern languages that compete with Zig for small memory footprints.
However, I could believe Zig's stdlib and culture encourages low memory footprints, but there isn't anything novel in the language that makes it inherently lower memory footprint. Though to name a few languages I'd say that can be directly comparable are Rust, D (esp. BetterC mode), and Nim. Even Julia & Go in the right context. Though honestly I often prefer wasting a few hundred bytes of RAM here and there, even on a microcontroller, for pure convenience.
edit: forgot Odin. Another comment mentioned it, though I've never used it. It looks like it's used in production.
* MultiArrayList
* arbitrary size-integers that make it easy to pack data very tightly
* easy access to several special purpose allocators which reduce the need for strategies like reference counting
* MultiArrayList is convenient and easy to do in Rust, D, Nim. Odin seems built around it. * Arbitrary bit-size integers seem more gimmicky than anything and similar to struct bitfields in other languages (except Rust oddly). Most compilers don't even pack uint8/uint16's for performance reasons. * Special purpose allocators are a bit more interesting. Still they don't provide actual memory safety AFAICT and can have fragmentation and performance penalties.
It'll be interesting to see larger Zig code bases emerge in different fields and see how the memory footprint compares in practice.
http://www.catb.org/esr/structure-packing/
https://github.com/Twon/Alignment/blob/master/docs/alignment...
Also ARM for example doesn't have 8/16 bit registers so int8 or int16 will use a 32bit register:
https://stackoverflow.com/a/23716920
Curiosity got to me, perhaps Zig had improved significantly. So I compared the first benchmark I found (kostya/benchmarks/bf) with Zig with Nim. For the smaller input (bench.b) Zig did run with ~22% less RAM (about 20kB less).
However, for the larger input (mandel.b) Nim+ARC used ~33% less RAM in safe mode: Nim 2.163mb -d:release; Zig 2.884mb -O ReleaseSafe; Zig 2.687mb -O ReleaseFast. The Nim requires 0.5mb less ram and the code is ~40% shorter. I don't have time to try out the Rust or Go versions though.
edit: grammar
Note I ran the benchmarks locally (MacBook Air M1) because the reported benchmark uses the older (default) Nim GC while I only use Nim+ARC. I also had to fix the Zig code and it took a few tries to get the signed/unsigned int conversions working. I tried tweaking flags for both a bit as well to see how stable they were. Zig's memory usage was pretty constant. Nim had a small speed vs memory tradeoff that could be tweak-able, but the defaults used the least memory.
Overall I'd expect exact memory usage by language(s) to vary some by benchmark and one random benchmark isn't conclusive. Still I didn't find anything to indicate Zig is clearly better than other new generation languages. Manual memory management might actually be worse than letting a compiler manage it in some cases.
1: https://github.com/kostya/benchmarks/tree/master/brainfuck
ORLY?
Maybe you can put together a better, faster team (like Presonus managed to do for Studio One), but most current DAWs have been in existence for 20 years or more. Catching up with that is a challenge, and if you don't catch up then it's an interesting toy or half-a-DAW.
So why?
ps. obviously I am biased.
That's particularly interesting considering the rust compiler in rust has never been as fast as the original OCaml one
I was there and had to suffer through this more than virtually anyone else :)
> 7 femto-bunnies - rustboot building Rust prior to being retired
> 49 kilo-hamsters - rustc building Rust immediately after rustboot's retirement
> 188 giga-sloths - rustc building Rust in 2020
The simple example in the README.md seems straightforward enough, but I wonder if there are any pathological explosions in practice.
For a pathological explosion, you mean something like a Zip Bomb? Wasm and C are pretty close together in their semantics, Wasm hides the stack and prevents jumping into the middle of a function (CFI, Control Flow Integrity). I think the code bloat should be on the order of some multiple of the smallest interpreter.
I just did a quick scan of Wasm interpreters (3 in Rust, 1 in C)
yblein/rust-wasm ~4kloc
rhysd/wain ~16kloc
paritytech/wasmi ~25kloc
wasm3/wasm3 ~22kloc (C)
My hunch is that the expanded code would be approximately (2x-5x interpreter + bin.wasm). I just did a spot check with doom.wasm, I am wrong. The resulting expanded C code when compiled to Arm is 2x the wasm binary size. 4.1M wasidoom.o
1.8M wasidoom.wasm
https://en.wikipedia.org/wiki/Zip_bomb* Is that a list of compilation targets?
* If not all behavior tests pass, does that not mean that the compiler fails to compile programs correctly?
Please indulge those of us who are not familiar with self-hosting compiler engineering.
Snippets of zig code that use language features and then make sure those features did the right thing. You can find them here: https://github.com/ziglang/zig/tree/master/test/behavior
> Is that a list of compilation targets?
Mostly. Pedantically, it's a list of code generation backends, each of which may have multiple compilation targets. So for example the LLVM backend can target many architectures. The ones that are architecture specific are currently debug-only and cannot do optimization.
> If not all behavior tests pass, does that not mean that the compiler fails to compile programs correctly?
Some tests are not passing because they cause an incorrect compile error, others compile but have incorrect behavior (miscompilation). Don't use Zig in production yet ;)
(edit: fix formatting)
I also wish we could rewrite everything in a modern language, but the reality is that we can’t and that if we could, it would take a LONG time. The ability to start new projects, or extend existing ones, with a modern and more ergonomic language—Zig—and be able to seamlessly work with C is incredible.
I look forward to the self-hosted compiler being complete, and hopefully a package manager in the future. I’d really like to start using Zig for more projects.
What zig offers is even better - because zig included a CC, you can actually reduce complexity with zig by getting a single compiler version for all platforms, rather than a fixed zig + each platform’s cc. And with it, trivially easy cross-platform builds - even with existing C code. That’s cool! Go has excellent cross-compilation, but Go with C, not so much.
Rust is a powerful tool, but it’s a complex ecosystem unto itself - both conceptually and practically. It has great interoperability frameworks, but the whole set of systems comes at a substantial learning cost. Plus, porting an existing software design to Rust can be a challenge. It’s more like the “scala of C” if we’re trying to stretch the analogy past the breaking point.
On the JVM world not really.
Or is it both at once?
Kotlin is to Java (on Android) as Swift is to Objective-C (on Apple) - the successor primary platform language.
Feels like an environment that moves so quickly (to someone like me anyway). Can barely keep up.
https://talkingkotlin.com/the-first-kotlin-commit-in-android...
Kotlin and Java are the main languages supported on Android.
They are still using Android Java on the system layers, because they aren't rewriting the whole stack.
Even the update for Java 11 LTS subset on Android 13 is mostly likely caused by the Java ecosystem moving forward, than the willingness of Android team to support anything other than what can be desugared into Java 8 / DEX somehow.
You are right in relation to JetBrains, they even acknowledge it on their blog post introducing Kotlin.
https://blog.jetbrains.com/kotlin/2011/08/why-jetbrains-need...
"And while the development tools for Kotlin itself are going to be free and open-source, the support for the enterprise development frameworks and tools will remain part of IntelliJ IDEA Ultimate, the commercial version of the IDE. And of course the framework support will be fully integrated with Kotlin."
Also, Oracle's lawsuit against Google for copying Java APIs.
0 of our recent or current projects still use java.
Google is either moving/extending libs to natively integrate with kotlin (numerous -ktx libs) or they are kotlin-only (Compose) anyway.
I don't really see the Jetbrains lock-in thing, because: Android Studio is free, you can use any other IDE with syntax highlighting and the terminal to run tests & to compile.
If you want to blame someone for locking in android devs into Android Studio, it would be Google, because they build the previews into Android Studio afaik. But you would have the same criticism at Apple/XCode. Supporting one IDE is already tough I guess.
> If only [Oracle] hadn't sued Google, Java would still have been the pre-eminent language for Android development. Sadly Android is stuck at legacy Java 8 permanently now. So, modern Java is stuck as a server-side language with dozens of competitors.
A reply argued that Android is on Java 11 now, and then you noted (hi!) that it's "a subset". Huh.
I'm trying to get a handle on understanding the ramifications of the legal/licensing situation, and the actual concrete impact on Java's use in Android. The subject seems somewhat murky and opaque. Is there possibly a high-level disambiguation about what's going on published anywhere?
You can check the Javadoc for the standard library, and the JVM specification. Then compare with DEX/ART and the Android documentation for Java APIs.
The lawsuit was about Sun’s license that explicitly demanded a purchase for use on mobile devices for their Java programming language (as that was the area they wanted to get money from). Google instead copied most of the APIs and called it a day, and Oracle bought Sun and went after the lawsuit.
But since the license changed in the meantime so that OpenJDK is completely open-source and has the same license as the Linux kernel, it was all about an older state of things.
That's the Maintain it With Zig approach :^)
Tracking issue for overall progress on the self-hosted compiler: https://github.com/ziglang/zig/issues/89
Zig's New Relationship with LLVM: https://kristoff.it/blog/zig-new-relationship-llvm/
But you're right that C is not a good example.
The Guix project likes bootstrappility very much. They basically host a tiny assembly C-compiler (only for a subset of C) which can compile a C compiler written in C for the whole subset that can bootstrap the whole ecosystem.
It's a pretty common state of affairs, actually. Often arises out of the second or third implementation of the compiler being much better than the first attempts, probably coupled with the momentum of people using the language who can contribute to the tooling because it's in the same language.
https://elephly.net/posts/2017-01-09-bootstrapping-haskell-p...
Is it worth learning Rust or Zig and dealing with the borrow checker or manual memory management in general, or are GC languages like Nim or Crystal good enough? I'm not doing any embedded programming by the way, just interested in command line apps and their speed as compared to, say, TypeScript, which is what I usually write command line apps in.
My question could be further constrained then to be, is learning Rust or Zig despite its manual memory management worth it for applications that are normally already garbage collected in their current implementations? Or are languages like Nim and Crystal enough? Does Rust and Zig have other benefits despite manual memory management?
But most software are throughput-oriented.
Nim’s ARC system also doesn’t use atomic or locks which means it’s runtime overhead is very low. I use it successfully on embedded devices for microsecond timed events with no large tail latencies.
I wouldn't buy into much Apple marketing regarding its performance though,
https://github.com/ixy-languages/ixy-languages
It makes sense in the context of tracing GC having been a failure in Objective-C due to its C semantics, while automating Cocoa's retain/release calls was much safer approach. Swift naturally built on top of that due to interoperability with Objective-C frameworks.
Nim has taken other optimizations into consideration, however only in the new ORC implementation.
Still, all of them are much better than managing memory manually.
I wouldn’t make claims on Swifts overall performance, but just it’s memory usage (really Obj-Cs) and particularly for GUI development. Java’s GCs have always been very memory hungry, usually to the tune of 2x. Same with .Net. Though to be fair Go’s and Erlang’s GCs have much better memory footprints. Erlang’s actor model benefits it there.
Agreed, they’re all better than manual memory management.
Zig has no garbage collection btw, but makes it easier than C to handle that. Another language without garbage collection that helps a lot to avoid memory issues is Ada (Looks a bit like Pascal). So there are alternatives to Rust.
> Is it worth learning
Languages are easy to pick up once you understand fundamentals. The borrow checker is intuitive if you have an understanding of stack frames, heap/data segment, references, moved types, shared memory.
You then should be asking "Is it worth using?", then evaluate use cases.. pros/cons.. etc.
For CLI, Rust is likely the easiest given it's macros, but if you struggle with the borrow checker then it won't be. You will be fighting the compiler instead of developing something.
Depending what your CLI program is doing, you might want to evaluate what libraries are available, how they handle I/O, and parallelism.
JavaScript has incredibly easy and fast concurrent I/O thanks to libuv and v8.
> Languages are easy to pick up once you understand fundamentals. The borrow checker is intuitive if you have an understanding of stack frames, heap/data segment, references, moved types, shared memory.
I see this sentiment often. In the last 10 years, I have come up a level in raw C, learned Kotlin, Swift, Python, Elixir/Erlang, and a smattering of JavaScript, all coming from a background that included Fortran and Smalltalk.
My problem with the dialogue is what is meant by “learn.” I have architected, implemented, and maintain different components of our products in all these languages currently. I think that demonstrates I have “learned” these languages, at least at this level of “picked up.” But I can’t write Python the way Brett Canon does. Or Elixir the way Jose Valium does. Or any of their peers. And in that regard I still very much feel I have not “learned”.
I spent a couple days playing with Zig a month or so ago. I became familiar with the way it worked. I could spend another month or so in that phase, and then could probably comfortably accomplish things with it. But I don’t think I’d feel like I’d “learned” it.
It reminds me of my experience learning Norwegian. I lived in Norway for 2 years and did my best to speak as much Norwegian as I could. At six months I could definitely get by. At 13 months, as I embraced the northern dialect, I was beginning to surprise Norwegians that I was from the states. I started dreaming in Norwegian at some point after that. But even at 24 months, able to carry on a fluid conversation, I realized I still could “learn” the language better than I currently knew it.
So I guess, it always seems there needs to be more context, from both the asker and the answerer, when this “should I learn X” discussion is had. Learning is not a merit badge.
When I tried to work with Rust a few years ago I found it utterly impenetrable. I just had no idea what the borrow checker was doing, did not understand what the error messages meant, and honestly couldn't even understand the documentation or the tutorials on the subject. Understanding what is happening in C or Zig is pretty easy; in Rust it's always been a nightmare for me. I just really don't grok the "lifetime" concept at all, it feels like I'm trying to learn academic computer science instead of a programming language.
Rust feels to me like a powerful, expressive language for professional programmers at the top of their game. That's a complement for any language. But it comes at the cost of mind-numbing complexity for anyone who's not an expert.
The official Rust book is targeted at novices with some programming experience. There's also Rustlings https://github.com/rust-lang/rustlings for a more practical approach.
> When I tried to work with Rust a few years ago I found it utterly impenetrable. I just had no idea what the borrow checker was doing, did not understand what the error messages meant, and honestly couldn't even understand the documentation or the tutorials on the subject
The compiler diagnostics have improved a lot over time. It's quite possible that some of the examples you have in mind return better error messages.
> in Rust it's always been a nightmare for me. I just really don't grok the "lifetime" concept at all, it feels like I'm trying to learn academic computer science instead of a programming language.
Academic computer science calls lifetimes "regions", which is perhaps a clearer term. It's a fairly clean extension of the notion of scope that you'd also find in languages like C or Zig. It's really not that complex, even though the Rust community sometimes finds it difficult to convey the right intuitions.
The language team has done a great job rounding rough edges, and this next roadmap is slated for even more polishing. They heavily prioritize dev experience which is why i think people like myself (a GC'd language person historically) use and love Rust so much.
For a language that is around 8 years old, this may be a serious problem, since the surrounding ecosystem has been probably written without parallelism in mind, and it may take a very long time to be updated (if ever).
They do, but it is hidden inside a compiler flag, if you compile your prject with `Dpreview_mt` then it will come with multi-threaded support. This has been an experimental feature for a few year though, and there is not much improvement since it first got introduced.
Personally I don't use crystal for this kind of feature, and it runs stable enough when I use it for some cpu intensive tasks when I rarely need it.
Crystal really shines when you need something that you usually write a python/ruby script to do, especially for tasks that run for hours. Converting some script from ruby to crystal and run it in production mode typically reduce the time consumed to 1/5 or even 1/10 of the original depends on the job. As someone who have to read gigabytes of text files regularly, Crystal is currently the best one for the task.
The compilation time for released binary is something need much improvement though. And I'm not sure if they can even achieve incremental compilation.
For the cases mentioned, I think crystal is immensely helpful: - Reading/writing files are easy, usually a single method will give you the result you want. - Working with directories are nice, things like `Dir.mkdir_p`, `Dir.each_child`, `File.exists?`... all existed to make your life easier. - Like ruby, you can invoke shell command easily using backticks - There are some useful libraries to for console app, like `colorize` or `option_parser`. Crystal is a battery included language, so the standard library is filled with useful libraries. - Working with lists and hashmaps is a breeze, since the Enumerable and Iterable modules are filled with useful methods, mostly inspired from ruby land. - Concurrent is built in, so you can trivially write performant IO-bounded tasks like web crawlers.
For a project that made by a handful of people, I just can't praise the dev team enough for making a language this practical.
It depends on the domain. From a production perspective, an flag-gated functionality that has been experimental for two or more years, is not "built-in". Plus, as explained, the ecosystem (I think I've read even the stdlib) doesn't give guarantees about thread safety
For small-scall scripting, then sure, it could be useful - but anything will do. I've evaluated for use at my company, and discarded it, because of the lack of libraries. Sadly, this is a chicken-and-egg situation. I've also evaluated contributing to it, but I won't until multithreading is stable.
Well this might be the problem. In corporate environment you can't afford to be too adventurous.
Personally I solve the "lack of libraries" problem by using more than one language, then connect them via child process call or some persistent storage like database or plain text files.
But it's entirely a different matter when the code need to be used by a lot of people.
Obviously i'm biased, but i quite enjoy it. I find i am more efficient now than before, because it manages to give me the ease of the "easier" languages quite often with a rich set of tooling when i need to go deeper.
Personally i feel the concern over the borrow checker is way overblown. 95% of the time the borrow checker is very simple. The common worst case i experience is "oh, i'm borrowing all of `foo` when i really want `foo.bar` - which is quite easily solved by borrowing just the `.bar` in a higher scope.
The lifetime jazz is rarely even worth using when compared to "easier" languages. Throw a reference count around the data and you get similar behavior to that of them and you never had to worry about a single lifetime. Same goes for clones/etc.
I often advocate this. For beginners to use the language like it was a GC'd language. A clone or reference count is often not a big deal and can significantly simplify your life. Then, when you're deeper into the language you can test the waters of lifetimes.
So yea. Your mileage will vary i'm sure. But i find Rust to be closer to GC'd languages than actual manual languages, in UX at least. You won't screw up and leak or introduce undefined behavior, which is quite a big UX concern imo.
I know dealing with the borrow checker can feel like a dead-end sometimes, but dealing with it is something that you can learn. Things that it can't handle fall into a handful of common patterns (like self-referential structs). Once you learn what not to do, you can recognize and avoid the issues even before you write the code.
I have been using Rust professionally as well and had a different experience. For anything singlethreaded I agree with you. For any asynchronous behavior, whether it's threads or async or callbacks, the borrow checker gets very stubborn. As a response, people just Arc-Mutex everything, at which point you might as well have used a GC language and saved yourself the headache.
However, enums with pattern matching and the trait system is still unbeatable and I miss it in other languages.
- The borrow checker is one of the easier parts of Rust to grok, it's just as you say, not that complicated in the end.
- Traits are more annoying to understand and find in source code when they can get added from anywhere, and suddenly you code gets extra functionality, or it's missing the right one unless you import the right crate but there is no "back-reference" so you're not clear what crate the code actually comes from.
- Crates/libraries are harder to grok with their mod.rs/lib.rs files and whatnot, in order to structure your application over many files.
- Macros are truly horrible in Rust, both to write and debug, but then my only experience with macros are with Clojure, where writing macros is basically just writing normal code and works basically the same way
- Compilation times when you start using it for larger projects tend to be kind of awful. Some crates makes this even worse (like tauri, bevy) and you need to be careful on what you add as some can have a dramatic impact on compilation speed
- The async ecosystem is not as mature as it seems on first look. I'm really looking forward to it all stabilizing in the future. Some libraries support only sync code, others only async but only via tokio, others using other async libraries. Read somewhere that eventually it'll be possible to interop between them, time will tell.
- Control flow with Option/Result and everything that comes with it is surprisingly nice and something I'm replicating when using other languages (mainly a Clojure developer by day) now.
My development journey was PHP -> JavaScript -> Ruby -> Golang -> Clojure with doing them all the capacity of backend/frontend/infrastructure/everything in-between, freelancing/consulting/working full-time at long term companies, so take my perspective with that in mind. Rust would be my first "low-level" language that I've used in larger capacity.
Any programming language is good enough for their own use cases :) It's a matter of understanding which the use cases are.
I'm a big Rust fan, but I nonetheless believe that the use cases for programming languages with manual memory management are comparatively small, in particular, since GC has been improved a lot in the last decade.
For undecided people, I conventionally suggest Golang. Those who at some point need deep control, will recognize it and change accordingly.
However, since you're just interested in writing a fast command-line app, what about the JVM or .NET? Those have a startup time issue, but once they're running they're very fast, less than an order of magnitude slower than C/Rust/Zig/etc.
When using C, if you want something from a higher-level language, usually you'd just do what that language's runtime does internally. So for a vector of heterogeneous objects, you could do a linked-list of pointers to the data objects. Something like:
struct VecItm { struct VecItm next; void *data; size_t data_len; char objTypeId; };
Is it more work? Yes. But at this point I would stop and consider if I really need a heterogeneous collection. Do we really need to store a list of objects with arbitrary sizes? What if we have 3 types of objects, in 3 arrays, and we store index into those arrays? struct Foo { unsigned byte x; };
struct Bar { unsigned word x; };
struct Baz { unsigned long x; };
struct Foo foos[256];
struct Bar bars[256];
struct Baz bazzes[256];
// objTypeId: 0 = foo, 1 = bar, 2 = baz
struct VecItm { struct VecItm next; unsigned int idx; char objTypeId; };
Or what if we can do something even better? What if Foo, Bar, and Baz all have a max size that isn't too wildly different? Can we store them all in an array directly? // objTypeId: 0 = foo (x contains 8 bits), 1 = bar (x contains 16 bits), 2 = baz (x contains 32 bits)
struct VecItm { unsigned long x; char objTypeId; };
struct VecItm vecItms[256];
> Its “macro” system is just a disgusting hackIt's really not that bad. It can result in spaghetti-code where your macros are using other macros and they're spread all over a header file. But if you use them surgically, only when needed, they don't cause much trouble.
> I could just as well write a sed script
The benefit of C macros over sed is if you use some language-aware tooling like an IDE (such as CLion), it will syntax-check and type-check your macros.
Your second example misses the auto-growing part. Sure, I want an array, but if that becomes too small, I automatically want to realloc the underlying data to a larger array. And I want to use it for Foo, for int and for any sized objects, generally.
C++ can create the proper zero-overhead abstraction over it by generating code for each type I use it for, C to my knowledge can’t do so without copy-pasting code or doing things with runtime overhead, resulting in (often) much slower code.
Nim also can double as a web language by transpiling to JS.
Then you can automate code generation with the sublime macros, which are just standard Nim code to create Nim code. No new syntax or special rules required - any Nim code can be run at compile time or run time, so you can use standard/3rd party libraries at compile time to write macros and give the user a slick syntax whilst removing boilerplate.
I really miss languages without straight forward metaprogramming after using Nim. It's something that multiplies the power of a language, rather than just adds to it.
In addition, such detractors were acting like a brand new programming language would be a finished polished product from day 1, when that was not the case for their far older languages. For instance, Odin and Crystal are older than Vlang, but has been surpassed by it in various respects.
And don't get me wrong, I like Odin. That's because Odin is among the newer breed of languages that have continued the trend in which Go started of non class-based OOP and more generalized OO, that are contenders to be alternatives to C/C++ (like Zig).
In the case of Vlang, it has clearly been developing rapidly and consistently, and continually gaining in popularity. Simply looking at their releases (and release schedule) along with their documentation (which various newer contenders are lacking in even that), will show a lot of the controversy is without merit or distortions of language development reality.
https://github.com/vlang/v/releases https://github.com/vlang/v/blob/master/doc/docs.md https://modules.vlang.io/
If a person does their research, then they would have clarity on the subjects. This is partly why I think it is better to refer people to the website or advise them to do their own evaluation, as oppose to reference critics or 3rd party websites who may have hidden motives and are advocates for a competing language. I think the competition between the newer languages has come to that point.
Vlang has both the options of Autofree and GC, or you can do it manually. As the language is still developing (like other languages such as Crystal, Odin, and Zig), users should know these memory management options are experimental and still being refined.
Are you sure about this? Let's look at their first feature, "No null". Ironically every vlang project including compiler itself is full of `isnil`[0] check.
This is just trailer for their lies; once start digging you'd find misrepresentation in every major features they advertise.
There's very little allocation since it supports returning VLAs (like strings) from functions via a secondary stack. Its Alire tool does the toolchain install and provides package management, so trying the language out is super easy. I've done a few bindings to things in C with it, which is ridiculously easy.
https://coalton-lang.github.io/reference/
I don’t think it has sum types, but it offers a rather sophisticated and very functionally oriented type system that compiles back to SBCL. It looks neat, haven’t found time to play yet…
There are no fast languages, only fast language implementations.
NodeJS/V8 is pretty fast (even faster than you probably think)—particularly if you're already doing things like making the sort of compromises where you limit yourself to writing only programs that can be expressed under the TypeScript regime. It's usually the case that it's not the NodeJS runtime that is the problem but rather the NodeJS programming style that is the source of the discomfort with "speed" that you will have experienced.
i.e how can someone look at a self hosted zig compiler and build it themself from source, never needing to download blobs from the internet?
Otherwise you lose the ability to trust anything you build.
Unless you’re recording memory contents and executing instructions by hand, you’ve just discovered the Ken Thompson hack. At some point the pragmatic thing is to trust some bits from a trusted source (e.g. downloaded from an official repo w/ a known cert, etc.).
C17 -> assembly of your choice, written in assembly
Zig -> C17 “transpiler”, written in C17
No, the only way to solve this problem is to start with a computer entirely devoid of code and bit-bang your assembler into the machine with switches, the way the first users of the MITS Altair did it.
But, yeah if you don't have a bunch of grad students at the ready, an assembler hand-written in machine code is the only option if you want to trust the entire stack. Though I'm not sure what that would get you. I don't know of any higher language compilers that are written directly in assembly these days, so you'd never be able to compile your C/C++ compiler.
So yeah, if you wanted to, you could bit-bang an assembler capable of assembling a simple C compiler, then in your simple subset of C you could implement a full C compiler, and from there you can do anything you want!
The grad student approach also sounds interesting. A basic Lisp interpreter can easily fit on a single letter-sized sheet of paper. A single person could hand-compile that, it would just take longer. But, if you're living alone in a cabin in the woods with your own hand-built computer and a personal library of computer books in hard copy, that would be a totally feasible project.
That's cool, thanks for showing me.
Or the Verilog/VHDL for the logic gates used by the CPU, for that matter?
Ideally, yes. But you still will be more secure than having multiple security holes.
Tons of compilers emit machine code.
You said 'that's no longer the case'. Isn't that the same as saying they don't?
I’m assuming these will primarily be for super fast debug builds (at least to start), and LLVM (and maybe C backend too) will still be the favored backend(s) for release builds.
How suitable is zig for such a task compared to say rust?
I know that higher level languages will have primitives that aim to represent strings, but if you need to get into the weeds with Unicode then you'll be leaning on a library regardless
Yes would agree and see them as platform related. It's just too large a task to create from scratch. Like say on JVM you can compile to bytecode and have strings already built into platform, and java.time, and ability to access an ecosystem of libraries.
With zig could one could use c or rust libraries?
Just parsing C++ is a rather large undertaking (my understanding is that it's syntax is turing complete).
edit: all your base are belong to us
Fast compilation speeds are a nice to have but basically irrelevant for normal developing of software in my experience.
Your day job project takes half an hour to compile and you don't see the point in speeding that up?
If compile times are longer you just compile less often and write larger portions of code at a time and you generally keep writing code while it compiles.
Somehow like having live orthographic checker always on while writing some prose: it can't easily distract the mind from focusing on the expression of ideas. And even if you try to ignore all these hints of early micro fixes, the brain is at very least spending resources at filtering avoidable noise.
Thus said, as long as this is an option that can easily switched, the possibility to limit notifications of these issues can be isolated into dedicated sessions.
I guess it might also greatly depend on each individual, how they appreciate to work, and how it impacts their workflow.