Hacker Newsnew | past | comments | ask | show | jobs | submit | cyber1's commentslogin

To me, this whole effort of rewriting Bun from Zig to Rust looks like a big marketing move. The question is: if Anthropic AI is really that powerful, why not just fix the bugs and give it the more ambitious task of redesigning the existing Bun Zig codebase in a way that eliminates not only the current bugs but also prevents similar ones from happening in the future?


The sole reason for that rewrite was Zig creator announcing he won't be accepting AI contributions. It hurt Anthropic's feelings.


Conversely we have actual evidence of Bun rewriting in Rust because Andrew wouldn't accept AI contributions, actually hurt Andrew's feelings.


Not really. It seems like the Rust rewrite blog post triggered this. Which, even though I don’t agree with the tone, seems to be valid criticism.


[citation needed]


Here[0] are other reasons besides AI

- [0] https://ziggit.dev/t/bun-s-zig-fork-got-4x-faster-compilatio...


I think that post highlights a difference in philosophical approach to software development. Bun is focused on moving fast and will deal with the consequences as they come. Zig takes a more planful approach.

I've experienced both in my career and I fall solidly on the planful side. It's why stopped using homebrew and I've avoided huggingface packages as much as possible.


What are you using instead of Homebrew?


MacPorts


Citation that this was the reason or citation that he said that?

For the latter: https://youtu.be/iqddnwKF8HQ?si=cvU8Fh3ah7ZCxg3M

From 26:38


But Rust is exactly the tooling that gives humans and LLMs a lot of those checks for free, and things like RAII.


If you use Rust the way it was designed to be used, rather than relying on countless "unsafe" blocks, you need to redesign the entire codebase architecture to make it compatible with the borrow checker rules.


Even if you “rely on countless unsafe blocks”, unsafe is additive, it gives access to additional APIs which are not checked. It does not disable affine types, the borrow checker, or send/sync traits. Unless the entire codebase is unsafe (e.g. fresh out of c2rust) it’s very hard to not have more guarantees.

And because unsafe is generally highly local or localizable reasoning (conventionally backed by safety justifications) it really is quite reasonable to go plugging at it, or task an AI within that.


For 99.99% of cases, you're reading and writing this under an operating system whose kernel is written in a language without send/sync, and inside a browser that also largely written in languages without send/sync, because those systems are fundamentally well designed. So instead of fixing the bugs and rethinking the architecture, the author of Bun decided to transpile almost the entire codebase from Zig to Rust without a deep architectural review. Okay...


Those systems you're alluding to received ungodly amounts of work and resources, vastly more than most projects can ever hope for, and yet they're still full of holes and security exploits. You're unwittingly making a great argument against using C, C++ – or Zig.


Those systems are primarily written in C or C++-but-in-C-style, right? Without exploiting RAII among other features, as Jarred mentioned that he liked in Rust. While Rust took RAII from C++.


What I mentioned is only a tiny part of the entire software, which has been successfully written in C, C++, and now in Zig as well, and is used daily by people around the world.


The borrow checker really isn't that bad. It isn't like they were porting from something with GC. They were already having to think about these things anyway. Even then opus seems to have no difficulty going between c# and rust while respecting the idioms of both. No unsafe needed. Zig should be even easier except the lack of a training corpus for whatever frankenversion of zig that bun was using.


All that unsafe does in these cases is enable the "unsafe super-powers" which the compiler can't check, thus shifting the responsibility onto the author. But for example if you've got some code which doesn't borrow check, and you just sprinkle unsafe keywords on it, now you've got code which still doesn't borrow check and diagnostics telling that this unsafe keyword was futile and you should remove that.

I haven't reviewed this code, but the percentages described don't sound like they'd need a huge architectural overhaul to use much less unsafe, it might take more actual human effort than they want though.


It seems they’ve already done analysis over what unsafe usage they can get rid of after the port.

https://bun.com/bun-unsafe-audit


If you are writing in C-like codebase and aren't tracking lifetimes and ownership at least as well as a borrow checker you're opening yourself to CVEs.


The concept of lifetimes was invented long before Rust borrow checker was even "scratched on paper." Of course, people who understand what they are they doing have known about these concepts for decades and have built their architectures around them. However, Rust borrow checker is much stricter than that. You have to build your architecture around the fact that you can't have more than one mutable reference at a time, and your data structures have to be represented as directed, acyclic graphs, etc. If you have to break all these rules by using a lot of unsafe because it's too difficult to represent your system within Rust safe subset using only value semantics and borrow-checked references, etc, then why use Rust instead of C++, C, or Zig?


Where did I say Rust's borrow checker predates lifetimes?

I'm saying you have to be as meticulous as a borrow checker. Not as strict, of course, but you have to analyze each pointer, how and when it's used, and then cross-reference with the documented lifetime.

Just like thread safety existed before Rust made it explicit. Now you don't need to investigate if data is thread safe if it's `Send + Sync`.

This is a job humans suck at and compilers excel at. With some caveats. There will be edge cases that will need to be manually proven.


I do wonder to what degree this weird play originated from Anthropic, versus from an overeager founder selling past the close.

I can imagine Anthropic wanting to acquire Bun without the gimmicks.


IMHO 'marketing' as a supposed incentive is too easily thrown around by people who probably don't know what marketing really is.


It is worth noting that before rewriting in Rust, Bun maintained a fork used to accelerate the compilation and informed those who asked that this fork could never be merged due to Zig's zero-LLM policy.

A few weeks later, Bun began the Rust rewrite. Although not explicitly stated, I suspect these two events may be related.


As was mentioned countless times here on HN, the fork didn't get merged not because of any no-AI policy but because the Zig maintainers considered it to be of poor quality and were already working on more comprehensive improvements to the compiler which would bring the same (and more) benefits, but without the downsides and quality issues of the fork.


zig doesn't accept ai written code


How is Bun codebase connected to Zig codebase?


Rumor has it there is a HN submission on the frontpage right now about that very thing:

https://news.ycombinator.com/item?id=48843352


You can't do "redesigning the existing Zig code in a way that eliminates not only the current bugs but also prevents similar ones from happening in the future" without actually changing Zig itself.


Why?


"effort" is a big word to describe typing out a few prompts to create something with 5k+ open issues.


"Typing out a few prompts" does not match the process described in Jarred's article. https://bun.com/blog/bun-in-rust


Static guarantees are better than stochastic parrots. A static linger beats telling Claude "check this idiom". Etc etc.


Well either Claude cannot fix those bugs or it can but the Bun team decided to not fix those bugs on purpose.

Not sure which is worse.


2664 "unsafe {", 1835 "unsafe fn". This is completely unsafe. It doesn't look like a rewrite that understands what's actually going on or how the architecture should be redesigned to take advantage of Rust strengths. Instead, it looks like an AI generated transpilation with extensive use of raw pointers.


Note that most of the unsafes are confined to the parser which was generated by running c2rust over the Postgres parser. The Postgres parser is itself generated from yacc/bison, so I decided to port it over mechanically rather than idiomatically.

If there's particular unsafes that you think are egregious, let me know.


Just wanted to say: I'm thoroughly impressed with how far in the weeds you're replying in this comment section. I'm learning a lot from the threads.


Same. Sorry to see so much hate here.


Thank you!


I don't know if converting the code could be an issue with copyright, but might be contrived as plagiarism


oh no.. will they get grounded?


Counterpoint: All of the current Postgres codebase is already wrapped in an invisible unsafe{}.

The difference with a Rust codebase like this is that all of the unsafe code has been neatly isolated and clearly marked. The outside code is safe — at least according to the definition of what Rust considers safe, which is a high bar indeed and objectively superior to the unsafe mess that is C — and the unsafe code is naturally fenced in, which means that it can be seen by developers and tackled by incrementally.

In some cases unsafe is unavoidable, but it is possible for a human to verify that it is, in fact, acceptably safe even if inside an unsafe block.


Valid point, but unsafe in Rust is more dangerous than unsafe in C, bcs of aliasing. For example PG is compiled with -fno-strict-aliasing.


Thank you. I write quite a lot of unsafe code myself and while safe Rust is much easier to get right than C, I'd say unsafe Rust is at least 10x harder to do correctly. Rustc's aliasing rules don't vanish when you use unsafe, you'll have to uphold them yourself!


I set all my Rust LLM written projects to 'unsafe=deny'. Not sure why not everyone is anticipating your comment.


Let me just copy this review comment into my prompt.

a few hours later

Fixed!


Why even use rust...


Because in C everything is unsafe, by definition.


what a nonsense


Care to explain?


I also hope that WG14 eventually adopts successful features from Zig, such as comptime, and reduces the use of ugly macros and _Generic.


The biggest advantages of Zig for me are that everything is explicit (no hidden features like overloads or implicit conversions) and that its metaprogramming is powerful, easy to use, and easy to understand.


Just look at this: https://pvs-studio.com/en/blog/posts/cpp/1129/ - 11 parts about C++ undefined behavior from people who specialize in finding this stuff. And that’s only the tip of the iceberg.

I use C++ daily, and it’s an overcomplicated language. The really good thing about Rust or Zig is that (mostly) everything is explicit, and that’s a big win in my opinion.

In defense of C++, I can only say that lots of interesting projects in the world are written in it.


No, it does. "The only two features in the language that do not follow the zero-overhead principle are runtime type identification and exceptions, and are why most compilers include a switch to turn them off." - https://en.cppreference.com/w/cpp/language/Zero-overhead_pri...


So saying 'it has a runtime' doesn't really make sense, it has a runtime if you want for two features that aren't necessary.


When most people refer to Java, they're referring to the "Java SE Platform" (the one that ships with all the stdlib stuff you'd expect, requiring a ton of process-boot-time logic and runtime support threads); not the "Java SE Embedded Platform" (the one you'd use to write e.g. Java Card software for smartcards, that doesn't have all of that stuff.) "Java for the Java SE Platform" and "Java for the Java SE Embedded Platform" are essentially two different languages, with the latter being a strict subset of the former.

When most people refer to C++, they're referring to the platform you get by linking the C++ STL. There's also what you might informally call "RTOS kernel C++" — C++ with all its syntax features (exceptions and destructors and RTTI) but no STL — that you get by linking only to a core C++ runtime support libraries (libsupc++/libc++abi/etc); and there's also "ultra-low-power embedded C++", where you don't even link to these, and don't get to use those features, so you instead have to use placement `new` and manual destructor calls, do your own error-handling with sentinel values or tagged unions or what-have-you, etc. These platforms are, again, essentially their own languages, each a strict subset of the one before.

Just like you can't hire a random Java programmer and expect them to be immediately productive on a Java Card codebase, C++ programmers are not "ultra-low-power embedded C++" programmers, any more than C# or Objective-C programmers are C programmers, or any more than C programmers are assembly-language programmers.

---

You might rebut with "C++ has a lot of subsets; it's a language with a lot of features that people can take or leave; people still call all of these subsets C++."

Yes, people do get used to thinking of C++ as "a language that has a bunch of different features that you can choose to use or not", and that mental-schema inertia carries over into this case — but it doesn't / shouldn't actually be applied here, and people are wrong to do so.

In the case of other C++ features, if you're not using a feature, it's because you just don't need what it does. When you choose to constrain your use of most C++ features, this results in the codebase being easier to understand. For most C++ features, avoiding use of the feature reduces the experience barrier required to begin contributing to the codebase. Which is why the subsets of C++ that don't use these features, are still just "C++": anyone who is a "C++ programmer" can instantly and intuitively maintain a codebase that is written in one of these subsets of C++.

But in the case of the syntax features requiring C++ runtime support, if you're strictly adhering to not using those... then you're having to do far more onerous and esoteric stuff (placement new + manual destructing; C-like error handling; BYO type metadata with intermediate void-ptr casting) instead. And that's stuff they don't even teach you how to do in a C++ course, or even a very thick C++ textbook.

As a "C++ programmer" cannot be expected to code for this "ultra-low-power embedded C++", you may as well consider it a separate language.

---

And if you agree with that, then you should agree that it makes sense to claim that C++ has a runtime. It is only the separate language, "ultra-low-power embedded C++", that doesn't.


It's easy to claim generalities and from there claim things that must be true, but when it comes down to it, C++ enables things that just can't be done without changing. Turning off a feature is trivial, adding destructors, move semantics and being able to wrap generic data structures up so they have value semantics is just not something you can do with C.

Saying "people generally mean this" is not only not true it isn't any sort of technical argument.


At first glance, when I looked through the Zig reference, I didn’t like a lot about its syntax (though syntax isn’t the most important thing for me). But when I tried writing in it, I changed my mind - it’s a concise and convenient language with a very low entry barrier. It feels like Go, and with some C experience, you can quickly start writing functional stuff.


"func" is fine; "function" is too long. "fn" is also good, but for example, Go was designed with "func," and it's one of the most successful, readable languages in the world, so why not?


Many C programmers need proper generic programming mechanisms (perhaps something like Zig's comptime) in C, but macros are the worst possible approach, and they don't want to switch to a different language like C++. As a result, they struggle with these issues. This is what I think the standardization committee should focus on, but instead, they introduced _Generic.


The biggest issue is the ABI for C - it's the lingua-franca of language interoperability and can't really be changed - so whatever approach is taken it needs to be fully compatible with the existing ABI. `_Generic` is certainly flawed but doesn't cause any breaking ABI changes.

That's also a major reason why you'd use C rather than C++. The C++ ABI is terrible for language interoperability. It's common for C++ libraries to wrap their API in C so that it can be used from other language's FFIs.

Aside from that another reason we prefer C to C++ is because we don't want vtables. I think there's room for a `C+` language, by which I mean C+templates and not C+classes - perhaps with an ABI which is a subset of the C++ ABI but superset of the C ABI.


> we don't want vtables

Then don't use virtual functions. Then there will be no vtables.

You might have known that already, but in general I'm surprised how many engineers think that all C++ classes have vtables. No, most in fact do not. C++ classes generally have the same memory layout as a C struct as long as you don't use virtual functions.


> I think there's room for a `C+` language, by which I mean C+templates and not C+classes - perhaps with an ABI which is a subset of the C++ ABI but superset of the C ABI.

indeed, i have spoken to a lot of my colleagues about just that. if overloading is not allowed, perhaps there is still some hope for a backwards compatible abi ?


I don't think we can get away with just using the C ABI - or even if we did, we would need a standardized name-mangling scheme, and then any language which consumes the ABI would need to be aware of this name-mangling scheme, so it would effectively be a new ABI.

We might be able to make this ABI compatible with C if no templates are used, which wouldn't cause breaking changes - but for other compilers to be able to use templates they would need to opt-in to the new scheme. For that we'd probably want to augment libffi to include completely new functions for dealing with templates. Eg, we'd have an ffi_template_type, and an ffi_prep_template for which we supply its type arguments - then an ffi_prep_templated_cif for calls which use templates, and so forth. It would basically be a new API - but probably still more practical than trying to support the C++ ABI.

Another issue is that if we compile some library with templates and expose them in the ABI, we need some way to instantiate the template with new types which were not present when the library was compiled. There's no trivial solution to this. We'd really need to JIT-compile the templates.


> ... we would need a standardized name-mangling scheme, ...

may you please elaborate on _why_ you think this is needed ?


If the templates are monomorphized, each instantiation of a templated function will have a different address. To acquire the address of any given instantiation we need a symbol in the object file.


What isn't clear to me why one would ever want monomorphization in the first place.


Not ever wanting monomorphization seems like a bit of a strong claim to me. Why do you take that position?


I was asking the question why one would ever want it.


Right. I had interpreted you asking that question as you having taken that position and soliciting responses for a discussion. Seems that was an improper reading.


exactly !


How can you have templates without name mangling and overloads?


This is true. I agree with this statement. It's the holy cow of C. However, the problem with generic programming and metaprogramming isn't going away, and many people continue to struggle with it. Introducing something like compile-time reflection might be a solution...


They showed something they think it’s neat. You start a topic with the assumption that they struggle, not sure how you get that information from the original post or you just want to state that claim anyway?


The most insulting thing about _Generic is the name. Really? _Generic? For a type-based switch with horrific syntax? What were they thinking...

That said, generic programming in C isn't that bad, just very annoying.

To me the best approach is to write the code for a concrete type (like Vec_int), make sure everything is working, and then do the following:

A macro Vec(T) sets up the struct. It can then be wrapped in a typedef like typedef Vec(int) Vec_i;

For each function, like vec_append(...), copy the body into a macro VEC_APPEND(...).

Then for each relevant type T: copy paste all the function declarations, then do a manual find/replace to give them some suffix and fill in the body with a call to the macro (to avoid any issues with expressions being executed multiple times in a macro body).

Is it annoying? Definitely. Is it unmanageable? Not really. Some people don't even bother with this last bit and just use the macros to inline the code everywhere.

Some macros can delegate to void*-based helpers to minimize the bloating.

EDIT: I almost dread to suggest this but CMake's configure_file command works great to implement generic files...


There are less annoying ways to implement this in C. There are at least two different common approaches which avoid having macro code for the generic functions:

The first is to put this into an include file

  #define type_argument int
  #include <vector.h>
Then inside vector.h the code looks like regular C code, except where you insert the argument.

  foo_ ## type_argument ( ... )
The other is to write generic code using void pointers or container_of as regular functions, and only have one-line macros as type safe wrappers around it. The optimizer will be able to specialize it, and it avoids compile-time explosion of code during monomorphization,

I do not think that templates are less annoying in practice. My experience with templates is rather poor.


An idea I had was to implement a FUSE filesystem for includes, so instead of the separate `#define type_argument` (and `#undef type_argument` that would need to follow the #include), we could stick the type argument in the included filename.

   #include <vector.h(int32_t)>
   #include <vector.h(int64_t)>
The written `vector.h(type_argument)` file could just be a regular C header or an m4 file which has `type_argument` in its template. When requesting `vector.h(int32_t)` the FUSE filesystem would effectively give the output of calling `gcc -E` or `m4` on the template file as the content of the file being requested.

Eg, if `vector.h(type_argument)` was an m4 file containing:

    `#ifndef VECTOR_'type_argument`_INCLUDED'
    `#define VECTOR_'type_argument`_INCLUDED'

    typedef struct `vector_'type_argument {
        size_t length;
        type_argument values[];
    } `vector_'type_argument;
 
    ...
    #endif
Then `m4 -D type_argument=int32_t vector.h(type_argument)` gives the output:

    #ifndef VECTOR_int32_t_INCLUDED
    #define VECTOR_int32_t_INCLUDED
    
    typedef struct vector_int32_t {
        size_t length;
        int32_t values[];
    } vector_int32_t;
    
    ...
    #endif
But the idea is to make it transparent so that existing tools just see the pre-processed file and don't need to call `m4` manually. We would need to mount each include directory that uses this approach using said filesystem. This shouldn't require changing a project's structure as we could use the existing `include/` or `src/` directory as input when mounting, and just pick some new directory name such as `cfuse/include` or `cfuse/src`, and mount a new directory `cfuse` in the project's root directory. The change we'd need to make is in any Makefiles or other parts of the build, where instead of `gcc -Iinclude` we'd have `gcc -Icfuse/include`. Any non-templated headers in `include/` would just appear as live copies in cfuse/include/, so in theory this could work without causing anything to break.


That's the craziest idea on this topic I've seen so far! I'm not sure that's a good or a bad thing, but it sure is a thing!


Those techniques being less annoying is highly debatable ;). Working with void* is annoying, header includes look quite ugly with the ## concatenation everywhere or even a wrapper macro. It also gets much worse when you need to customize the suffix (because type_argument is char* or whatever).

Sometimes the best option is an external script to instantiate a template file.


It may be debatable, but I would say C++'s template syntax is not nicer. I do not think working with void pointers is annoying, but I also prefer the container_of approach. The ## certainly has the limitation that you need to name things first, but I do not think this much of a downside.

BTW, here is some generic code in C using a variadic type. I think this quite nice. https://godbolt.org/z/jxz6Y6f9x

Running a program for meta programming are always a possibility, and I would agree that sometimes the best solution.


I don't think

    T ## _foo (T foo, ...)
is that much different from

    <T>::foo (T foo, ...)
Same for:

    foo (Object * a)
vs:

    foo (void * a)


Hey, I understand you and know this stuff well, having worked with it for many years as a C dev. To be honest, this isn't how things should generally be done. Macros were invented for very simple problems. Yes, we can abuse them as much as possible (for example, in C++, we discovered SFINAE, which is an ugly, unreadable technique that wasn't part of the programming language designer's intent but rather like a joke that people started abusing), but is it worth it?


The name has to be ugly, new names in C are always taken from the set of reserved identifiers: those starting with an underscore & a capital letter, or with two underscores. Since they didn't reserve any "normal" names, all new keywords will be stuff like `_Keyword` or `__keyword`, unless they break backwards compatibility. And they really hate breaking backwards compatibility, so that's quite unlikely.


The problem is not the _G, the problem is the "generic". It is a completely wrong name for what it does.


This is an old tradition in ISO C. unsigned actually means modulo and const actually means immutable.


username checks out


I don't struggle, I switch from C++ to C and find this much nicer.


I'm currently at a crossroads: C++ or Zig. One is very popular with a large community, amazing projects, but has lots of ugly design decisions and myriad rules you must know (this is a big pain, it seems like even Stroustrup can't handle all of them). The other is very close to what I want from C, but it's not stable and not popular.


Why not C?

Its only real issue is that people will constantly tell you how bad it is and how their language of choice is so much better. But if you look at how things work out in practice, you can usually do things very nicely in C.


My choice in this situation is indeed C, but every once in a while I hit a problem that makes me yearn for better metaprogramming.

Perfect hashing that you’d ideally use two different approaches for depending on whether the platform has a cheap popcount (hi AArch32), but to avoid complicating the build you give up and emulate popcount instead. Hundreds of thousands of lines of asynchronous I/O code written in a manual continuation-passing style, with random, occasionally problematic blocking synchronization sprinkled all over because the programmer simply could not be bothered anymore to untangle this nested loop, and with a dynamic allocation for each async frame because that’s the path of least resistance. The intense awkwardness of the state-machine / regular-expression code generators, well-developed as they are. Hoping the compiler will merge the `int` and `long` code paths when their machine representations are identical, but not seeing it happen because functions must have unique addresses. Resorting to .init_array—and slowing down startup—because the linker is too rigid to compute this one known-constant value. And yes, polymorphic datastructures.

I don’t really see anybody do noticeably better than C; I think only Zig and Odin (perhaps also Hare and Virgil?) are even competing in the same category. But I can’t help feeling that things could be much better. Then I look at the graveyard of attempted extensions both special-purpose (CPC[1]) and general (Xoc[2]) and despair.

[1] https://github.com/kerneis/cpc

[2] https://pdos.csail.mit.edu/archive/xoc/


It would be interesting to understand better where language feature are actually needed or helpful, and where the code should be organized differently. I also observe that often cure if worse than the disease.

Many example I see where people argue for metaprogramming features are not all convincing to me. For example, there was recently a discussion about Zig comp-time. https://news.ycombinator.com/item?id=44208060 This is the Zig example: https://godbolt.org/z/1dacacfzc Here is the C code: https://godbolt.org/z/Wxo4vaohb

Or there was a recent example where someone wanted to give an example for C++ coroutines and showed pre-order tree traversal (which I can't find at the moment), but the C code using vec(node) IMHO was better: https://godbolt.org/z/sjbT453dM compared to the C++ coroutine version: https://godbolt.org/z/fnGzszf3j (from https://news.ycombinator.com/item?id=43831628 here). Edited to add source.


Macros are the best possibly approach, compared to C++ templates or _Generic


Glory to Ukraine! Slava Ukraini!


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: