[HN Gopher] Low-Level Optimization with Zig
       ___________________________________________________________________
        
       Low-Level Optimization with Zig
        
       Author : Retro_Dev
       Score  : 287 points
       Date   : 2025-06-07 07:26 UTC (1 days ago)
        
 (HTM) web link (alloc.dev)
 (TXT) w3m dump (alloc.dev)
        
       | flohofwoe wrote:
       | > I love Zig for it's verbosity.
       | 
       | I love Zig too, but this just sounds wrong :)
       | 
       | For instance, C is clearly too sloppy in many corners, but Zig
       | might (currently) swing the pendulum a bit too far into the
       | opposite direction and require too much 'annotation noise',
       | especially when it comes to explicit integer casting in math
       | expressions (I wrote about that a bit here:
       | https://floooh.github.io/2024/08/24/zig-and-emulators.html).
       | 
       | When it comes to performance: IME when Zig code is faster than
       | similar C code then it is usually because of Zig's more
       | aggressive LLVM optimization settings (e.g. Zig compiles with
       | -march=native and does whole-program-optimization by default,
       | since all Zig code in a project is compiled as a single
       | compilation unit). Pretty much all 'tricks' like using
       | unreachable as optimization hints are also possible in C,
       | although sometimes only via non-standard language extensions.
       | 
       | C compilers (especially Clang) are also very aggressive about
       | constant folding, and can reduce large swaths of constant-
       | foldable code even with deep callstacks, so that in the end there
       | often isn't much of a difference to Zig's comptime when it comes
       | to codegen (the good thing about comptime is of course that it
       | will not silently fall back to runtime code - and non-comptime
       | code is still of course subject to the same constant-folding
       | optimizations as in C - e.g. if a "pure" non-comptime function is
       | called with constant args, the compiler will still replace the
       | function call with its result).
       | 
       | TL;DR: if your C code runs slower than your Zig code, check your
       | C compiler settings. After all, the optimization heavylifting all
       | happens down in LLVM :)
        
         | Retro_Dev wrote:
         | Ahh perhaps I need to clarify:
         | 
         | I don't love the noise of Zig, but I love the ability to
         | clearly express my intent and the detail of my code in Zig. As
         | for arithmetic, I agree that it is a bit _too_ verbose at the
         | moment. Hopefully some variant of
         | https://github.com/ziglang/zig/issues/3806 will fix this.
         | 
         | I fully agree with your TL;DR there, but would emphasize that
         | gaining the same optimizations is easier in Zig due to how
         | builtins and unreachable are built into the language, rather
         | than needing gcc and llvm intrinsics like
         | __builtin_unreachable() -
         | https://gcc.gnu.org/onlinedocs/gcc-4.5.0/gcc/Other-Builtins....
         | 
         | It's my dream that LLVM will improve to the point that we don't
         | need further annotation to enable positive optimization
         | transformations. At that point though, is there really a
         | purpose to using a low level language?
        
           | flohofwoe wrote:
           | Yeah indeed. Having access to all those 'low-level tweaks'
           | without having to deal with non-standard language extensions
           | which are different in each C compiler (if supported at all)
           | is definitely a good reason to use Zig.
           | 
           | One thing I was wondering, since most of Zig's builtins seem
           | to map directly to LLVM features, if and how this will affect
           | the future 'LLVM divorce'.
        
             | Retro_Dev wrote:
             | Good question! The TL;DR as I understand it is that it
             | won't matter too much. For example, the self-hosted x86_64
             | backend (which is coincidentally becoming default for
             | debugging on linux right now -
             | https://github.com/ziglang/zig/pull/24072) has full support
             | for most (all?) builtins. I don't think that we need to
             | worry about that.
             | 
             | It's an interesting question about how Zig will handle
             | additional builtins and data representations. The current
             | way I understand it is that there's an additional opt-in
             | translation layer that converts unsupported/complicated IR
             | to IR which the backend can handle. This is referred to as
             | the compiler's "Legalize" stage. It should help to reduce
             | this issue, and perhaps even make backends like
             | https://github.com/xoreaxeaxeax/movfuscator possible :)
        
           | matu3ba wrote:
           | > LLVM will improve to the point that we don't need further
           | annotation to enable positive optimization transformations
           | 
           | That is quite a long way to go, since the following formal
           | specs/models are missing to make LLVM + user config possible:
           | 
           | - hardware semantics, specifically around timing behavior and
           | (if used) weak memory
           | 
           | - memory synchronization semantics for weak memory systems
           | with ideas from "Relaxed Memory Concurrency Re-executed" and
           | suggested model looking promising
           | 
           | - SIMD with specifically floating point NaN propagation
           | 
           | - pointer semantics, specifically in object code
           | (initialization), se- and deserialization, construction,
           | optimizations on pointers with arithmetic, tagging
           | 
           | - constant time code semantics, for example how to ensure
           | data stays in L1, L2 cache and operations have constant time
           | 
           | - ABI semantics, since specifications are not formal
           | 
           | LLVM is also still struggling with full restrict support due
           | to architecture decisions and C++ (now worked on since more
           | than 5 years).
           | 
           | > At that point though, is there really a purpose to using a
           | low level language?
           | 
           | Languages simplify/encode formal semantics of the (software)
           | system (and system interaction), so the question is if the
           | standalone language with tooling is better than state of art
           | and for what use cases. On the tooling part with incremental
           | compilation I definitely would say yes, because it provides a
           | lot of vertical integration to simplify development.
           | 
           | The other long-term/research question is if and what code
           | synthesis and formal method interaction for verification,
           | debugging etc would look like for (what class of)
           | hardware+software systems in the future.
        
             | eptcyka wrote:
             | For constant time code, it doesn't matter too much if data
             | spills out of a cache, constant time issues arise from
             | compilers introducing early exits which leaves crypto open
             | to timing attacks.
        
               | matu3ba wrote:
               | Thanks for the info. Do you have a good overview on what
               | other hardware properties or issues are relevant?
        
         | skywal_l wrote:
         | Maybe with the new x86 backend we might see some performance
         | differences between C and Zig that could definitely be
         | attributed solely to the Zig project.
        
           | saagarjha wrote:
           | I would be (pleasantly) surprised if Zig could beat LLVM's
           | codegen.
        
             | Zambyte wrote:
             | So would the Zig team. AFAIK, they don't plan to (and have
             | said this in interviews). The plan is for super fast
             | compilation and incremental compilation. I think the
             | homegrown backend is mainly for debug builds.
        
               | Cloudef wrote:
               | The backends do already have some simple optimizations.
               | Of course focus is debug builds and speed, but long term
               | goal is for them to be competitive as well.
        
         | messe wrote:
         | With regard to the casting example, you could always wrap the
         | cast in a function:                   fn
         | signExtendCast(comptime T: type, x: anytype) T {
         | const ST = std.meta.Int(.signed, @bitSizeOf(T));
         | const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x)));
         | return @bitCast(@as(ST, @as(SX, @bitCast(x))));         }
         | export fn addi8(addr: u16, offset: u8) u16 {             return
         | addr +% signExtendCast(u16, offset);         }
         | 
         | This compiles to the same assembly, is reusable, and makes the
         | intent clear.
        
           | flohofwoe wrote:
           | Yes, that's a good solution for this 'extreme' example. But
           | in other cases I think the compiler should make better use of
           | the available information to reduce 'redundant casting' when
           | narrowing (like the fact that the result of `a & 15` is
           | guaranteed to fit into an u4 etc...). But I know that the Zig
           | team is aware of those issues, so I'm hopeful that this stuff
           | will improve :)
        
             | hansvm wrote:
             | This is something I used to agree with, but implicit
             | narrowing is dangerous, enough so that I'd rather be more
             | explicit most of the time nowadays.
             | 
             | The core problem is that you're changing the semantics of
             | that integer as you change types, and if that happens
             | automatically then the compiler can't protect you from
             | typos, vibe-coded defects, or any of the other ways kids
             | are generating almost-correct code nowadays. You can
             | mitigate that with other coding patterns (like requiring
             | type parameters in any potentially unsafe arithmetic helper
             | functions and banning builtins which aren't wrapped that
             | way), but under the swiss cheese model of error handling it
             | still massively increases your risky surface area.
             | 
             | The issue is more obvious on the input side of that
             | expression and with a different mask. E.g.:
             | const a: u64 = 42314;       const even_mask: u4 = 0b0101;
             | a & even_mask;
             | 
             | Should `a` be lowered to a u4 for the computation, or
             | `even_mask` promoted, or however we handle the internals
             | have the result lowered sometimes to a u4? Arguably not.
             | The mask is designed to extract even bit indices, but we're
             | definitely going to only extract the low bits. The only
             | safe instance of implicit conversion in this pattern is
             | when you intend to only extract the low bits for some
             | purpose.
             | 
             | What if `even_mask` is instead a comptime_int? You still
             | have the same issue. That was a poor use of comptime ints
             | since now that implicit conversion will always happen, and
             | you lost your compiler errors when you misuse that
             | constant.
             | 
             | Back to your proposal of something that should always be
             | safe: implicitly lowering `a & 15` to a u4. The danger is
             | in using it outside its intended context, and given that
             | we're working with primitive integers you'll likely have a
             | lot of functions floating around capable of handling the
             | result incorrectly, so you really want to at least use the
             | _right_ integer type to have a little type safety for the
             | problem.
             | 
             | For a concrete example, code like that (able to be
             | implicitly lowered because of information obvious to the
             | compiler) is often used in fixed-point libraries. The
             | fixed-point library though does those sorts of operations
             | with the express purpose of having zeroed bits in a wide
             | type to be able to execute operations without loss of
             | precision (the choice of what to do for the final
             | coalescing of those operations when precision is lost being
             | a meaningful design choice, but it's irrelevant right this
             | second). If you're about to do any nontrivial arithmetic on
             | the result of that masking, you don't want to accidentally
             | put it in a helper function with a u4 argument, but with
             | implicit lowering that's something that has no guardrails.
             | It requires the programmer to make zero mistakes.
             | 
             | That example might seem a little contrived, and this isn't
             | something you'll run into every day, but every nontrivial
             | project I've worked on has had _something_ like that, where
             | implicit narrowing is extremely dangerous and also
             | extremely easy to accidentally do.
             | 
             | What about the verbosity? IMO the point of verbosity is to
             | draw your attention to code that you should be paying
             | attention to. If you're in a module where implicit casting
             | would be totally fine, then make a local helper function
             | with a short name to do the thing you want. Having an
             | unsafe thing be noisy by default feels about right though.
        
               | throwawaymaths wrote:
               | you could give the wrapper function a funny name like
               | @"sign-cast" to force the eye to be drawn to it.
        
           | johnisgood wrote:
           | Yeah but what is up with all that "." and "@"? Yes, I know
           | what they are used for, but it is noise for me (i.e.
           | "annotation noise"). This is why I do not use Zig. Zig is
           | more like a lighter C++, not a C replacement, IMO.
           | 
           | I agree with everything flohofwoe said, especially this: "C
           | is clearly too sloppy in many corners, but Zig might
           | (currently) swing the pendulum a bit too far into the
           | opposite direction and require too much 'annotation noise',
           | especially when it comes to explicit integer casting in math
           | expressions ".
           | 
           | Seems like I will keep using Odin and give C3 a try (still
           | have yet to!).
           | 
           | Edit: I quite dislike that the downvote is used for "I
           | disagree, I love Zig". _sighs_. Look at any Zig projects, it
           | is full of annotation noise. I would not want to work with a
           | language like that. You might, that is cool. Good for you.
        
             | codethief wrote:
             | > Yeah but what is up with all that "." and "@"
             | 
             | "." = the "namespace" (in this case an enum) is implied,
             | i.e. the compiler can derive it from the function signature
             | / type.
             | 
             | "@" = a language built-in.
        
               | johnisgood wrote:
               | I know what these are, but they are noise to me.
        
               | pyrolistical wrote:
               | It is waaaaaaay less noisy than c++
               | 
               | C syntax may look simpler but reading zig is more comfy
               | bc there is less to think about than c due to explicit
               | allocator.
               | 
               | There is no hidden magic with zig. Only ugly parts. With
               | c/c++ you can hide so much complexity in a dangerous way
        
               | johnisgood wrote:
               | FWIW: I hate C++, too.
        
               | Simran-B wrote:
               | It's not annotation noise however, it's syntax noise.
        
               | johnisgood wrote:
               | Thanks for the correction. Is it really not "annotation"?
               | What makes the difference?
        
               | Simran-B wrote:
               | You're not providing extra information to the compiler,
               | clarifying the intent, but merely follow the requirements
               | of the language when writing . to infer the type or @ to
               | use a built-in function.
        
               | johnisgood wrote:
               | Thank you. My previous comment got down-voted despite it
               | being a legitimate question, weird times.
        
               | kprotty wrote:
               | C++'s `::` vs Zig's `.`
               | 
               | C++'s `__builtin_` (or arguably `_`/`__`) vs Zig's `@`
        
               | johnisgood wrote:
               | I hate C++, too.
        
             | pjmlp wrote:
             | Despite all bashes that I do at C, I would be happy if
             | during the last 40 years we had gotten at least fat
             | pointers, official string and array vocabulary types
             | (instead of everyone getting their own like SDS and glib),
             | namespaces instead of mylib_something, proper enums (like
             | enum class in C++, enums in C# and so forth), fixing the
             | pointer decay from array to &array[0], less UB.
             | 
             | While Zig fixes some of these issues, the amount of @ feels
             | like being back in Objective-C land and yeah too many uses
             | of dot and starts.
             | 
             | Then again, I am one of those that actually enjoys using
             | C++, despite all its warts and the ways of WG21 nowadays.
             | 
             | I also dislike the approach with source code only libraries
             | and how importing them feels like being back in JavaScript
             | CommonJS land.
             | 
             | Odin and C3 look interesting, the issue is always what is
             | going to be the killer project, that makes reaching for
             | those alternatives unavoidable.
             | 
             | I might not be a language XYZ cheerleeder, but occasionally
             | do have to just get my hands dirty and do the needfull for
             | an happy customer, regardlees of my point of view on XYZ.
        
             | throwawaymaths wrote:
             | the line noise is really ~only there for dangerous stuff,
             | where slowing down a little bit (both reading and writing)
             | is probably a good idea.
             | 
             | as for the dots, if you use zig quite a bit you'll see that
             | dot usage is incredibly consistent, and _not_ having the
             | dots will feel wrong, not just in an  "I'm used to it
             | sense/stockholm syndrome" but you will feel for example
             | that C is _wrong_ for not having them.
             | 
             | for example, the use of dot to signify "anonymous" for a
             | struct literal. why _doesn 't_ C have this? the compiler
             | must make a "contentious" choice if something is a block or
             | a literal. by contentious i mean the compiler knows what
             | its doing but a quick edit might easily make you do
             | something unexpected
        
             | elcritch wrote:
             | You might try out Nim. It has a low annotation noise level.
             | The python like syntax feels odd for a systems language at
             | first but since it's statically typed it works well. The
             | simpler syntax seems to work very well with LLMs too.
             | 
             | Basically it's a better C/C++ for me and sits between C and
             | C++ in complexity.
             | 
             | I tried Zig for a while years ago but found the casts and
             | other annotations frustrating. And at the time the language
             | was pretty unstable in how it applied those rules. Plus
             | I've never found a use for custom allocators, even on
             | embedded.
        
         | knighthack wrote:
         | I'm not sure why allowances are made for Zig's verbosity, but
         | not Go's.
         | 
         | What's good for the goose should be good for the gander.
        
           | nurbl wrote:
           | I think a better word may be "explicitness". Zig is sometimes
           | verbose because you have to spell things out. Can't say much
           | about Go, but it seems it has more going on under the hood.
        
           | ummonk wrote:
           | Zig's verbosity goes hand in hand with a strong type system
           | and a closeness to the hardware. You don't get any such
           | benefits from Go's verbosity.
        
           | Zambyte wrote:
           | FWIW Zig has error handling that is nearly semantically
           | identical to Go (errors as return values, the big semantic
           | difference being tagged unions instead of multiple return
           | values for errors), but wraps the `if err != nil { return
           | err}` pattern in a single `try` keyword. That's the verbosity
           | that I see people usually complaining about in Go, and Zig
           | addresses it.
        
             | kbolino wrote:
             | The way Zig addresses it also discards all of the runtime
             | variability too. In Go, an error can say something like
             | unmarshaling struct type Foo: in field Bar int: failed to
             | parse value "abc" as integer
             | 
             | Whereas in Zig, an error can only say something that's
             | known at compile time, like IntParse, and you will have to
             | use another mechanism (e.g. logging) to actually trace the
             | error.
        
               | metaltyphoon wrote:
               | Yep. Errors carry no context whatsoever and you have no
               | idea where they came from.
        
               | Zambyte wrote:
               | You can trace error returns with the builtin
               | @errorReturnTrace function.
               | 
               | https://ziglang.org/documentation/0.14.1/#errorReturnTrac
               | e
               | 
               | https://ziglang.org/documentation/0.14.1/#Error-Return-
               | Trace...
        
         | Zambyte wrote:
         | Regarding the explicit integer casting, it seems like there is
         | some cleanup that will be coming soon:
         | https://ziggit.dev/t/short-math-notation-casting-clarity-of-...
        
         | titzer wrote:
         | Zig has some interesting ideas, and I thought the article was
         | going to be more on the low-level optimizations, but it turned
         | out to be "comptime and whole program compilation are great".
         | And I agree. Virgil has had the full language available at
         | compile time, plus whole program compilation since 2006. But
         | Virgil doesn't target LLVM, so speed comparisons end up being a
         | comparison between two compiler backends.
         | 
         | Virgil leans heavily into the reachability and specialization
         | optimizations that are made possible by the compilation model.
         | For example it will aggressively devirtualize method calls,
         | remove unreachable fields/objects, constant-promote through
         | fields and heap objects, and completely monomorphize
         | polymorphic code.
        
         | int_19h wrote:
         | I rather suspect that the pendulum will swing rather strongly
         | towards more verbose and explicit languages in general in the
         | upcoming years solely because it makes things easier for AI.
         | 
         | (Note that this is orthogonal to whether and to what extent use
         | of AI for coding is a good idea. Even if you believe that it's
         | not, the fact is that many devs believe otherwise, and so
         | languages will strive to accommodate them.)
        
       | KingOfCoders wrote:
       | I do love the allocator model of Zig, I would wish I could use
       | something like an request allocator in Go instead of GC.
        
         | usrnm wrote:
         | Custom allocators and arenas are possible in go and even do
         | exist, but they ara just very unergonomic and hard to use
         | properly. The language itself lacks any way to express and
         | enforce ownership rules, you just end up writing C with a
         | slightly different syntax and hoping for the best. Even C++ is
         | much safer than go without GC
        
           | KingOfCoders wrote:
           | They are not integrated in all libraries, so for me they
           | don't exist.
        
       | saagarjha wrote:
       | > As an example, consider the following JavaScript code...The
       | generated bytecode for this JavaScript (under V8) is pretty
       | bloated.
       | 
       | I don't think this is a good comparison. You're telling the
       | compiler for Zig and Rust to pick something very modern to
       | target, while I don't think V8 does the same. Optimizing JITs do
       | actually know how to vectorize if the circumstances permit it.
       | 
       | Also, fwiw, most modern languages will do the same optimization
       | you do with strings. Here's C++ for example:
       | https://godbolt.org/z/TM5qdbTqh
        
         | Retro_Dev wrote:
         | You can change the `target` in those two linked godbolt
         | examples for Rust and Zig to an older CPU. I'm sorry I didn't
         | think about the limitations of the JS target for that example.
         | As for your link, It's a good example of what clang can do for
         | C++ - although I think that the generated assembly may be sub-
         | par, even if you factor in zig compiling for a specific CPU
         | here. I would be very interested to see a C++ port of
         | https://github.com/RetroDev256/comptime_suffix_automaton
         | though. It is a use of comptime that can't be cleanly guessed
         | by a C++ compiler.
        
           | saagarjha wrote:
           | I just skimmed your code but I think C++ can probably
           | constexpr its way through. I understand that's a little
           | unfair though because C++ is one of the only other languages
           | with a serious focus on compile-time evaluation.
        
         | vanderZwan wrote:
         | In general it's a bit of an apples to fruit salad comparison,
         | albeit one that is appropriate to highlight the different use-
         | cases of JS and Zig. The Zig example uses an array with a known
         | type of fixed size, the JS code is "generic" at run time (x and
         | y can be any object). Which, fair enough, is something you'd
         | have to pay the cost for in JS. Ironically though in this
         | particular example one actually would be able to do much better
         | when it comes to communicating type information to the JIT:
         | ensure that you _always_ call this function with Float64Arrays
         | of equal size, and the JIT will know this and produce a faster
         | loop (not vectorized, but still a lot better).
         | 
         | Now, one rarely uses typed arrays in practice because they're
         | pretty heavy to initialize so only worth it if one allocates a
         | large typed array one once and reuses them a lot aster that, so
         | again, fair enough! One other detail does annoy me a little
         | bit: the article says the example JS code is pretty bloated,
         | but I bet that a big part of that is that the JS JIT can't
         | guarantee that 65536 equals the length of the two arrays so
         | will likely insert a guard. But nobody would write a for loop
         | that way anyway, they'd write it as _i < x.length_, for which
         | the JIT _does_ optimize at least one array check away. I admit
         | that this is nitpicking though.
        
       | uecker wrote:
       | You don't really need comptime to be able to inline and unroll a
       | string comparison. This also works in C:
       | https://godbolt.org/z/6edWbqnfT (edit: fixed typo)
        
         | Retro_Dev wrote:
         | Yep, you are correct! The first example was a bit too
         | simplistic. A better one would be
         | https://github.com/RetroDev256/comptime_suffix_automaton
         | 
         | Do note that your linked godbolt code actually demonstrates one
         | of the two sub-par examples though.
        
           | uecker wrote:
           | I haven't looked at the more complex example, but the second
           | issue is not too difficult to fix:
           | https://godbolt.org/z/48T44PvzK
           | 
           | For complicated things, I haven't really understood the
           | advantage compared to simply running a program at build time.
        
             | Cloudef wrote:
             | To be honest your snippet isn't really C anymore by using a
             | compiler builtin. I'm also annoyed by things like `foo(int
             | N, const char x[N])` which compilation vary wildly between
             | compilers (most ignore them, gcc will actually try to check
             | if the invariants if they are compile time known)
             | 
             | > I haven't really understood the advantage compared to
             | simply running a program at build time.
             | 
             | Since both comptime and runtime code can be mixed, this
             | gives you a lot of safety and control. The comptime in zig
             | emulates the target architecture, this makes things like
             | cross-compilation simply work. For program that generates
             | code, you have to run that generator on the system that's
             | compiling and the generator program itself has to be aware
             | the target it's generating code for.
        
               | uecker wrote:
               | It also works with memcpy from the library:
               | https://godbolt.org/z/Mc6M9dK4M I just didn't feel like
               | burdening godbolt with an inlclude.
               | 
               | I do not understand your criticism of [N]. This gives
               | compiler more information and catches errors. This is a
               | good thing! Who could be annoyed by this:
               | https://godbolt.org/z/EeadKhrE8 (of course, nowadays you
               | could also define a descent span type in C)
               | 
               | The cross-compilation argument has some merit, but not
               | enough to warrant the additional complexity IMHO.
               | Compile-time computation will also have annoying
               | limitations and makes programs more difficult to
               | understand. I feel sorry for everybody who needs to
               | maintain complex compile time code generation. Zig
               | certainly does it better than C++ but still..
        
               | Cloudef wrote:
               | > I do not understand your criticism of [N]. This gives
               | compiler more information and catches errors. This is a
               | good thing!
               | 
               | It only does sane thing in GCC, in other compilers it
               | does nothing and since it's very underspec'd it's rarely
               | used in any C projects. It's shame Dennis's fat pointers
               | / slices proposal was not accepted.
               | 
               | > warrant the additional complexity IMHO
               | 
               | In zig case the comptime reduces complexity, because it
               | is simply zig. It's used to implement generics, you can
               | call zig code compile time, create and return types.
               | 
               | This old talk from andrew really hammers in how zig is
               | evolution of C:
               | https://www.youtube.com/watch?v=Gv2I7qTux7g
        
               | uecker wrote:
               | Then the right thing would be to complain about those
               | other compilers. I agree that Dennis' fat pointer
               | proposal was good.
               | 
               | Also in Zig it does not reduce complexity but adds to it
               | by creating an distinction between compile time and run-
               | time. It is only lower complexity by comparing to other
               | implementations of generic which are even worse.
        
               | Cloudef wrote:
               | Sure there's tradeoffs for everything, but if I had to
               | choose between macros, templates, or zig's comptime, I'd
               | take the comptime any time.
        
               | uecker wrote:
               | To each their own, I guess. I still find C to be so much
               | cleaner than all the languages that attempt to replace
               | it, I can not possibly see any of them as a future
               | language for me. And it turns out that it is possible to
               | fix issues in C if one is patient enough. Nowadays I
               | would write this with a span type:
               | https://godbolt.org/z/nvqf6eoK7 which is safe and gives
               | good code.
               | 
               | update: clang is even a bit nicer
               | https://godbolt.org/z/b99s1rMzh although both compile it
               | to a constant if the other argument is known at compile
               | time. In light of this, the Zig solution does not impress
               | me much: https://godbolt.org/z/1dacacfzc
        
               | pjmlp wrote:
               | Not only it was a good proposal, since 1990 that WG14 has
               | not done anything else into that sense, and doesn't look
               | like it ever will.
        
               | uecker wrote:
               | Let's see. We have a relatively concrete plan to add
               | dependent structure types to C2Y: struct foo { size_t n;
               | char ( _buf)[.n]; };
               | 
               | Once we have this, the wide pointer could just be
               | introduced as syntactic sugar for this. char (_buf)[:] =
               | ..
               | 
               | Personally, I would want the dependent structure type
               | first as it is more powerful and low-level with no need
               | to decide on a new ABI.
        
               | int_19h wrote:
               | This feels like such a massive overkill complexity-wise
               | for something so basic.
        
               | uecker wrote:
               | Why do you think so? The wide pointers are syntactic
               | sugar on top of it, so from an implementation point of
               | view not really simpler.
        
               | pjmlp wrote:
               | Thanks, interesting to see how it will turn out.
        
               | pron wrote:
               | C also creates a distinction between compile-time and
               | run-time, which is more arcane and complicated than that
               | of Zig's, and your code uses it, too: macros (and other
               | pre-processor programming). And there are other
               | distinctions that are more subtle, such as whether the
               | source of a target function is available to the caller's
               | compilation unit or not, static or not etc..
               | 
               | C only seems cleaner and simpler _if you already know it
               | well_.
        
               | uecker wrote:
               | My point is not about whether compile-time programming is
               | simpler in C or in Zig, but that is in most cases the
               | wrong solution. My example is also not about compile time
               | programming (and does not use macro:
               | https://godbolt.org/z/Mc6M9dK4M), but about letting the
               | optimizer do its job. The end result is then leaner than
               | attempting to write a complicated compile time solution -
               | I would argue.
        
               | pyrolistical wrote:
               | Right tool for the job. There was no comptime problem
               | shown in the blog.
               | 
               | But if there were zig would prob be simpler since it uses
               | one language that seamlessly weaves comptime and runtime
               | together
        
               | uecker wrote:
               | I don't know, to me it seems the blog tries to make the
               | case that comptime is useful for low-level optimization:
               | "Is this not amazing? We just used comptime to make a
               | function which compares a string against "Hello!\n", and
               | the assembly will run much faster than the naive
               | comparison function. It's unfortunately still not
               | perfect." But it turns out that a C compiler will give
               | you the "perfect" code directly while the comptime Zig
               | version is fairly complicated. You can argue that this
               | was just a bad example and that there are other examples
               | where comptime makes more sense. The thing is, about two
               | decades ago I was similarly excited about expression-
               | template libraries for very similar reasons. So I can
               | fully understand how the idea of "seamlessly weaves
               | comptime and runtime together" can appear cool. I just
               | realized at some point that it isn't actually all that
               | useful.
        
               | pron wrote:
               | > But it turns out that a C compiler will give you the
               | "perfect" code directly while the comptime Zig version is
               | fairly complicated.
               | 
               | In this case _both_ would (or could) give the  "perfect"
               | code without any explicit comptime programming.
               | 
               | > I just realized at some point that it isn't actually
               | all that useful.
               | 
               | Except, again, C code often uses macros, which is a more
               | cumbersome mechanism than comptime (and possibly less
               | powerful; see, e.g. how Zig implements printf).
               | 
               | I agree that comptime isn't necessarily very useful for
               | _micro_ optimisation, but that 's not what it's for.
               | Being able to shift computations in time is usedful for
               | more "algorithmic" _macro_ optimisations, e.g. parsing
               | things at compile time or generating de /serialization
               | code.
        
               | uecker wrote:
               | Of course, a compiler could possibly also optimize the
               | Zig code perfectly. The point is that the blogger did not
               | understand it and instead created an overly complex
               | solution which is not actually needed. Most C code I
               | write or review does not use a lot of macros, and where
               | they are used it seems perfectly fine to me.
        
               | quibono wrote:
               | Possibly a stupid question... what's a descent span type?
        
               | uecker wrote:
               | Something like this: https://godbolt.org/z/er9n6ToGP It
               | encapsulates a pointer to an array and a length. It is
               | not perfect because of some language limitation (which I
               | hope we can remove), but also not to bad. One limitation
               | is that you need to pass it a typedef name instead of any
               | type, i.e. you may need a typedef first. But this is not
               | terrible.
        
               | quibono wrote:
               | Thanks, this is great! I've been having a look at your
               | noplate repo, I really like what you're doing there
               | (though I need a minute trying to figure out the more
               | arcane macros!)
        
               | uecker wrote:
               | In this case, the generic span type is just #define
               | span(T) struct CONCAT(span_, T) { ssize_t N; T* data; }
               | And the array to span macro would just create such an
               | object form an array by storing the length of the array
               | and the address of the first element. #define
               | array2span(T, x) ({ auto __y = &(x); (span(T)){
               | array_lengthof( ___y), &(___y)[0] }; })
        
       | justmarc wrote:
       | Optimization matters, in a huge way. Its effects are compounded
       | by time.
        
         | sgt wrote:
         | Only if the software ends up being used.
        
       | el_pollo_diablo wrote:
       | > In fact, even state-of-art compilers will break language
       | specifications (Clang assumes that all loops without side effects
       | will terminate).
       | 
       | I don't doubt that compilers occasionally break language specs,
       | but in that case Clang is correct, at least for C11 and later.
       | From C11:
       | 
       | > An iteration statement whose controlling expression is not a
       | constant expression, that performs no input/output operations,
       | does not access volatile objects, and performs no synchronization
       | or atomic operations in its body, controlling expression, or (in
       | the case of a for statement) its expression-3, may be assumed by
       | the implementation to terminate.
        
         | tialaramex wrote:
         | C++ says (until the future C++ 26 is published) all loops, but
         | as you noted C itself does not do this, only those "whose
         | controlling expression is not a constant expression".
         | 
         | Thus in C the trivial infinite loop for (;;); is supposed to
         | actually compile to an infinite loop, as it should with Rust's
         | less opaque loop {} -- however LLVM is built by people who
         | don't always remember they're not writing a C++ compiler, so
         | Rust ran into places where they're like "infinite loop please"
         | and LLVM says "Aha, C++ says those never happen, optimising
         | accordingly" but er... that's the wrong language.
        
           | el_pollo_diablo wrote:
           | Sure, that sort of language-specific idiosyncrasy must be
           | dealt with in the compiler's front-end. In TFA's C example,
           | consider that their loop                 while (i <= x) {
           | // ...       }
           | 
           | just needs a slight transformation to                 while
           | (1) {           if (i > x)               break;           //
           | ...       }
           | 
           | and C11's special permission does not apply any more since
           | the controlling expression has become constant.
           | 
           | Analyzes and optimizations in compiler backends often
           | normalize those two loops to a common representation (e.g.
           | control-flow graph) at some point, so whatever treatment that
           | sees them differently must happen early on.
        
             | pjmlp wrote:
             | In theory, in practice it depends on the compiler.
             | 
             | It is no accident that there is ongoing discussion that
             | clang should get its own IR, just like it happens with the
             | other frontends, instead of spewing LLVM IR directly into
             | the next phase.
        
           | kibwen wrote:
           | _> Rust ran into places where they 're like "infinite loop
           | please" and LLVM says "Aha, C++ says those never happen,
           | optimising accordingly" but er... that's the wrong language_
           | 
           | Worth mentioning that LLVM 12 added first-class support for
           | infinite loops without guaranteed forward progress, allowing
           | this to be fixed: https://github.com/rust-
           | lang/rust/issues/28728
        
             | loeg wrote:
             | For some context, 12 was released in April 2021. LLVM is
             | now on 20 -- the versions have really accelerated in recent
             | years.
        
               | username223 wrote:
               | At least it's not just clownish version acceleration.
               | They decided they wanted versions to increase faster
               | somewhere around 2017-2018 (4.xx), and the version
               | increase is more or less linear before and after that
               | time, just at different slopes.
        
               | 3836293648 wrote:
               | And it's now a yearly major release, is it not?
               | 
               | Same as with GCC
        
               | username223 wrote:
               | I can't say I'm happy with "yearly broken backward
               | compatibility," but at least it's predictable.
        
               | loeg wrote:
               | Looks like two major versions per year for LLVM.
        
       | dustbunny wrote:
       | What interests me most by zig is the ease of the build system,
       | cross compilation, and the goal of high iteration speed. I'm a
       | gamedev, so I have performance requirements but I think most
       | languages have sufficient performance for most of my requirements
       | so it's not the #1 consideration for language choice for me.
       | 
       | I feel like I can write powerful code in any language, but the
       | goal is to write code for a framework that is most future proof,
       | so that you can maintain modular stuff for decades.
       | 
       | C/C++ has been the default answer for its omnipresent support. It
       | feels like zig will be able to match that.
        
         | FlyingSnake wrote:
         | I recently, for fun, tried running zig on an ancient kindle
         | device running stripped down Linux 4.1.15.
         | 
         | It was an interesting experience and I was pleasantly surprised
         | by the maturity of Zig. Many things worked out of the box and I
         | could even debug a strange bug using ancient GDB. Like you, I'm
         | sold on Zig too.
         | 
         | I wrote about it here:
         | https://news.ycombinator.com/item?id=44211041
        
         | osigurdson wrote:
         | I've dabbled in Rust, liked it, heard it was bad so kind of
         | paused. Now trying it again and still like it. I don't really
         | get why people hate it so much. Ugly generics - same thing in
         | C# and Typescript. Borrow checker - makes sense if you have
         | done low level stuff before.
        
           | int_19h wrote:
           | If you don't happen to come across some task that implies a
           | data model that Rust is actively hostile towards (e.g. trees
           | with backlinks, or more generally any kind of graph with
           | cycles in it), borrow checker is not much of a hassle. But
           | the moment you hit something like that, it becomes a massive
           | pain, and requires either "unsafe" (which is strictly more
           | dangerous than even C, never mind Zig) or patterns like using
           | indices instead of pointers which are counter to high
           | performance and effectively only serve to work around the
           | borrow checker to shut it up.
        
             | carlmr wrote:
             | >requires either "unsafe" (which is strictly more dangerous
             | than even C, never mind Zig)
             | 
             | Um, what? Unsafe Rust code still has a lot more safety
             | checks applied than C.
             | 
             | >It's important to understand that unsafe doesn't turn off
             | the borrow checker or disable any of Rust's other safety
             | checks: if you use a reference in unsafe code, it will
             | still be checked. The unsafe keyword only gives you access
             | to these five features that are then not checked by the
             | compiler for memory safety. You'll still get some degree of
             | safety inside of an unsafe block.
             | 
             | https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html
        
             | dwattttt wrote:
             | > the moment you hit something like that, it becomes a
             | massive pain, and requires either "unsafe" (which is
             | strictly more dangerous than even C, never mind Zig) or
             | patterns like using indices instead of pointers
             | 
             | If you need to make everything in-house this is the
             | experience. For the majority though, the moment you require
             | those things you reach for a crate that solves those
             | problems.
        
             | Ar-Curunir wrote:
             | I'm sorry, but your comment is a whole lot of horseshit.
             | 
             | Unsafe rust still has a bunch of checks C doesn't have, and
             | using indices into vectors is common code in high
             | performance code (including Zig!)
        
             | creata wrote:
             | > patterns like using indices instead of pointers which are
             | counter to high performance
             | 
             | Using indices isn't _bad_ for performance. At the very
             | least, it can massively cut down on memory usage (which is
             | in turn good for performance) if you can use 16-bit or
             | 32-bit indices instead of full 64-bit pointers.
             | 
             | > "unsafe" (which is strictly more dangerous than even C,
             | never mind Zig)
             | 
             | Unsafe Rust is much safer than C.
             | 
             | The only way I can imagine unsafe Rust being more dangerous
             | than C is that you need to keep exception safety in mind in
             | Rust, but not in C.
        
               | whytevuhuni wrote:
               | Not quite, you also need to keep pointer non-nullness,
               | alignment and aliasing safety in Rust, which is very
               | pervasive in Rust (all shared/mutable references) but
               | very rare in C (the 'restricted' keyword).
               | 
               | In Rust, it's not just using an invalid reference that
               | causes UB, but their very creation, even if temporary.
               | For example, since references have to always be aligned,
               | the compiler can assume the pointer they were created
               | from was also aligned, and so suddenly some ending bits
               | from the pointer are ignored (since they _must 've_ been
               | zero).
               | 
               | And usually the point of unsafe is to make safe wrappers,
               | so unafe Rust makes or interacts with safe shared/mutable
               | references pretty often.
        
               | creata wrote:
               | It's just hard for me to imagine someone accidentally
               | messing up nonnullness or aliasing, because it's really
               | in-your-face that you need to be careful when
               | constructing a reference unsafely. There are even
               | idiomatic methods like ptr::as_ref to avoid accidentally
               | creating null references.
        
             | jplusequalt wrote:
             | >which is strictly more dangerous than even C, never mind
             | Zig
             | 
             | No it's not? The Rust burrow checker, the backbone of
             | Rust's memory safety model, doesn't stop working when you
             | drop into an unsafe block. From the Rust Book:
             | 
             | >To switch to unsafe Rust, use the unsafe keyword and then
             | start a new block that holds the unsafe code. You can take
             | five actions in unsafe Rust that you can't in safe Rust,
             | which we call unsafe superpowers. Those superpowers include
             | the ability to:                   Dereference a raw pointer
             | Call an unsafe function or method         Access or modify
             | a mutable static variable         Implement an unsafe trait
             | Access fields of a union
             | 
             | It's important to understand that unsafe doesn't turn off
             | the borrow checker or disable any of Rust's other safety
             | checks: if you use a reference in unsafe code, it will
             | still be checked. The unsafe keyword only gives you access
             | to these five features that are then not checked by the
             | compiler for memory safety. You'll still get some degree of
             | safety inside of an unsafe block.
        
             | jplusequalt wrote:
             | >using indices instead of pointers which are counter to
             | high performance
             | 
             | Laughs in graphics programmer. You end up using indices to
             | track data in buffers all the time when working with
             | graphics APIs.
        
             | cornstalks wrote:
             | (This is a reply to multiple sibling comments, not the
             | parent)
             | 
             | For those saying unsafe Rust is strictly safer than C,
             | you're overlooking Rust's extremely strict invariants that
             | users must uphold. These are much stricter than C, and
             | they're extremely easy to accidentally break in unsafe
             | Rust. Breaking them in unsafe Rust is instant UB, even
             | before leaving the unsafe context.
             | 
             | This article has a decent summary in this particular
             | section: https://zackoverflow.dev/writing/unsafe-rust-vs-
             | zig/#unsafe-...
        
               | creata wrote:
               | The author seems to mostly be talking about the aliasing
               | rules, but if you don't want to deal with those, can't
               | you use UnsafeCell?
               | 
               | Imo, the more annoying part is dealing with exception
               | safety. You need to ensure that your data structures are
               | all in a valid state if any of your code ( _especially_
               | code in an unsafe block) panics, and it 's easy to forget
               | to ensure that.
        
           | sapiogram wrote:
           | Haters gonna hate. If you're working on a project that needs
           | performance and correctness, nothing can get the job done
           | like Rust.
        
             | LAC-Tech wrote:
             | unless you have to do anything that relies on a C API (such
             | as provided by an OS) with no concept of ownership, then
             | it's a massive task to get that working well with idiomatic
             | rust. You need big glue layers to really make it work.
             | 
             | Rust is a general purpose language that can do systems
             | programming. Zig is a systems programming language.
             | 
             | (Safety Coomers please don't downvote)
        
               | saghm wrote:
               | What does it even mean to be able to "do systems
               | programming" but not actually be a "systems programming
               | language"? I would directly disagree with you, but what
               | you're arguing is so vague that I don't even know what
               | you're trying to claim. The only way I can make sense of
               | this is if you literally define a "systems programming
               | language" as C and only other things that are tightly
               | tied to it, which I guess is fine if you like tautologies
               | but kind of makes even having a concept of " systems
               | programming language" pretty useless.
        
               | bbkane wrote:
               | And yet Rust in the one in the Linux and Windows kernels,
               | so people must think it's worth the effort. https://threa
               | dreaderapp.com/thread/1577667445719912450.html is
               | certainly a glowing recommendation
        
               | LAC-Tech wrote:
               | Kernels are big pieces of software. Rust is used for
               | device drivers mainly, right? So in that case you write
               | an idiomatic rust lib and wrap it in a C interface and
               | load it in.
               | 
               | Actually interfacing with idiomatic C APIs provided by an
               | OS is something else entirely. You can see this is when
               | you compare the Rust ecosystem to Zig; ie Zig has a
               | fantastic io-uring library in the std lib, where as rust
               | has a few scattered crates none of which come close the
               | Zig's ease of use and integration.
               | 
               | One thing I'd like to see is an OS built with rust that
               | could provide its own rusty interface to kernel stuff.
        
               | ArtixFox wrote:
               | Hello can you point me to more information about zig's
               | and rust's io-uring implementations
        
               | LAC-Tech wrote:
               | Hey Artix!
               | 
               | Zig's is in the standard library. From the commits it was
               | started by Joran from Tigerbeetle, and now maintained by
               | mlugg who is a very talented zig programmer.
               | 
               | https://ziglang.org/documentation/master/std/#std.os.linu
               | x.I...
               | 
               | The popular Rust one is tokio's io-uring crate which 1)
               | relies on libc; the zig one just uses their own stdlib
               | which wraps syscalls 2) Requires all sorts of glue
               | between safe and unsafe rust.
               | 
               | github.com/tokio-rs/io-uring
        
               | WD-42 wrote:
               | The OS is called Redox.
        
               | LAC-Tech wrote:
               | It actually provides rust APIs to dev systems software
               | against that run on it?
               | 
               | I know it's written in rust, but I am talking more
               | specifically than that.
        
           | dgb23 wrote:
           | Both are great languages. To me there's a philosophical
           | difference, which can impact one to prefer one over the
           | other:
           | 
           | Rust makes doing the wrong thing hard, Zig makes doing the
           | right thing easy.
        
         | raincole wrote:
         | I wonder how zig works on consoles. Usually consoles hate
         | anything that's not C/C++. But since zig can be transpiled to
         | C, perhaps it's not completely ruled out?
        
           | jeroenhd wrote:
           | Consoles will run anything you compile for them. There are
           | stable compilers for most languages for just about any
           | console I know of, because modern consoles are pretty much
           | either amd64 or aarch64 like phones and computers are.
           | 
           | Language limitations are more on the SDK side of things. SDKs
           | are available under NDAs and even publicly available APIs are
           | often proprietary. "Real" test hardware (as in developer
           | kits) is expensive and subject to NDAs too.
           | 
           | If you don't pick the language the native SDK comes with
           | (which is often C(++)), you'll have to write the language
           | wrappers yourself, because practically no free, open,
           | upstream project _can_ maintain those bindings for you.
           | Alternatively, you can pay a company that specializes in the
           | process, like the developers behind Godot will tell you to
           | do: https://docs.godotengine.org/en/stable/tutorials/platform
           | /co...
           | 
           | I think Zig's easy C interop will make integration for Zig
           | into gamedev quite attractive, but as the compiler still has
           | bugs and the language itself is ever changing, I don't think
           | any big companies will start developing games in Zig until
           | the language stabilizes. Maybe some indie devs will use it,
           | but it's still a risk to take.
        
         | haberman wrote:
         | > I feel like I can write powerful code in any language, but
         | the goal is to write code for a framework that is most future
         | proof, so that you can maintain modular stuff for decades.
         | 
         | I like Zig a lot, but long-term maintainability and modularity
         | is one of its weakest points IMHO.
         | 
         | Zig is hostile to encapsulation. You cannot make struct members
         | private:
         | https://github.com/ziglang/zig/issues/9909#issuecomment-9426...
         | 
         | Key quote:
         | 
         | > The idea of private fields and getter/setter methods was
         | popularized by Java, but it is an anti-pattern. Fields are
         | there; they exist. They are the data that underpins any
         | abstraction. My recommendation is to name fields carefully and
         | leave them as part of the public API, carefully documenting
         | what they do.
         | 
         | You cannot reasonably form API contracts (which are the
         | foundation of software modularity) unless you can hide the
         | internal representation. You need to be able to change the
         | internal representation without breaking users.
         | 
         | Zig's position is that there should be no such thing as
         | internal representation; you should publicly expose, document,
         | and guarantee the behavior of your representation to all users.
         | 
         | I hope Zig reverses this decision someday and supports private
         | fields.
        
           | eddd-ddde wrote:
           | Just prefix internal fields with underscore and be a big boy
           | and don't access them from the outside.
           | 
           | If you really need to you can always use opaque pointers for
           | the REALLY critical public APIs.
        
             | haberman wrote:
             | I am not the only user of my API, and I cannot control what
             | users do.
             | 
             | My experience is that users who are trying to get work done
             | will bypass every speed bump you put in the way and just
             | access your internals directly.
             | 
             | If you "just" rely on them not to do that, then your
             | internals will effectively be frozen forever.
        
               | nicoburns wrote:
               | > If you "just" rely on them not to do that, then your
               | internals will effectively be frozen forever.
               | 
               | Or they will be broken when you change them and they
               | upgrade. The JavaScript ecosystem uses this convention
               | and generally if a field is prefixed by an underscore
               | and/or documented as being non-public then you can expect
               | to break in future versions (and this happens frequently
               | in practice).
               | 
               | Not necessarily saying that's better, but it is another
               | choice that's available.
        
               | lll-o-lll wrote:
               | Or you change it and respond with "You were warned".
               | 
               | I seriously do not get this take. People use reflection
               | and all kinds of hacks to get at internals, this should
               | not stop you from changing said internals.
               | 
               | There will always be users who do the wrong thing.
        
               | jjmarr wrote:
               | Let's say I'm in a large company. Someone on some other
               | team decided to rely on my implementation internals for a
               | key revenue driver, and snuck it through code review.
               | 
               | I can't break their app without them complaining to my
               | boss's boss's boss who will take their side because their
               | app creates money for the company.
               | 
               | Having actual private fields doesn't 100% prevent this
               | scenario, but it makes it less likely to sneak through
               | code review before it becomes business-critical.
        
               | lll-o-lll wrote:
               | You can still create modules in zig, just use the
               | standard handle pattern as you might in c/c++. I think
               | that many of us have worked in "large company", and the
               | issue you describe is not resolved with the "private"
               | keyword. You need to make your "component/module" with a
               | well defined boundary (normally dll/library), a "public
               | interface" and the internals not visible as symbols.
               | 
               | That doesn't save you in languages that support
               | reflection, but it will with zig. Inside a module, all
               | private does is declare intent.
               | 
               | In languages with code inheritance, I think inheritance
               | across module boundaries is now widely viewed as the
               | anti-pattern that it is.
        
             | 9d wrote:
             | [flagged]
        
               | lll-o-lll wrote:
               | Not everyone has to follow the MS approach of not
               | breaking clients that rely on "undocumented" behavior.
               | Document what will not be broken in future, change the
               | rest and ignore the wailing.
               | 
               | It's antithetical to what Zig is all about to hide the
               | implementation. The whole idea is you can read the entire
               | program without having to jump through abstractions 10
               | layers deep.
        
           | dustbunny wrote:
           | I don't care about public/private.
        
           | 9d wrote:
           | Andrew has so many wrong takes. Unused variables is another.
           | 
           | Such a smart guy though, so I'm hesitant to say he's wrong.
           | And maybe in the embedded space he's not, and if that's all
           | Zig is for then fine. But internal code is a _necessity_ of
           | abstraction. I 'm not saying it has to be C++ levels of
           | abstraction. But there is a line between interface and
           | implementation that ought to be kept. C headers are nearly
           | perfect for this, letting you hide and rename and recast
           | stuff differently than your .c file has, allowing you to
           | change how stuff works internally.
           | 
           | Imagine if the Lua team wasn't free to make it significantly
           | faster in recent 5.4 releases because they were tied to
           | _every_ internal field. We all benefited from their freedom
           | to change how stuff works inside. Sorry Andrew but you 're
           | wrong here. Or at least you were 4 years ago. Hopefully
           | you've changed your mind since.
        
             | haberman wrote:
             | I agree with almost all of this, including the point about
             | c header files, except that code has to be in headers to be
             | inlined (unless you use LTO), which in practice forces code
             | into headers even if you'd prefer to keep it private.
        
               | keldaris wrote:
               | There's nothing wrong with using LTO, but I prefer simply
               | compiling everything as a single translation unit ("unity
               | builds"), which gets you all of the LTO benefits for free
               | (in the sense that you still get fast compile times too).
        
             | philwelch wrote:
             | > I'm not saying it has to be C++ levels of abstraction.
             | But there is a line between interface and implementation
             | that ought to be kept. C headers are nearly perfect for
             | this, letting you hide and rename and recast stuff
             | differently than your .c file has, allowing you to change
             | how stuff works internally.
             | 
             | Can't you do this in Zig with modules? I thought that's
             | what the 'pub' keyword was for.
             | 
             | You can't have private fields in a struct that's publicly
             | available but the same is sort of true in C too. OO style
             | encapsulation isn't the only way to skin a cat, or to send
             | the cat a message to skin itself as the case may be.
        
               | 9d wrote:
               | I don't know Zig so I dunno maybe
        
             | girvo wrote:
             | > But internal code is a necessity of abstraction
             | 
             | I just fundamentally disagree with this. Not having
             | "proper" private methods/members has not once become a
             | problem for me, but overuse of them absolutely _has_.
        
           | unclad5968 wrote:
           | I disagree with plenty of Andrew's takes as well but I'm with
           | him on private fields. I've never once in 10 years had an
           | issue with a public field that should have been private,
           | however I have had to hack/reimplement entire data structures
           | because some library author thought that no user should touch
           | some private field.
           | 
           | > You cannot reasonably form API contracts (which are the
           | foundation of software modularity) unless you can hide the
           | internal representation. You need to be able to change the
           | internal representation without breaking users.
           | 
           | You never need to hide internal representations to form an
           | "API contract". That doesn't even make sense. If you need to
           | be able to change the internal representation without
           | breaking user code, you're looking for opaque pointers, which
           | have been the solution to this problem since at least C89, I
           | assume earlier.
           | 
           | If you change your data structures or the procedures that
           | operate on them, you're almost certain to break someone's
           | code somewhere, regardless of whether or not you hide the
           | implementation.
        
             | dgb23 wrote:
             | > I've never once in 10 years had an issue with a public
             | field that should have been private, however I have had to
             | hack/reimplement entire data structures because some
             | library author thought that no user should touch some
             | private field.
             | 
             | Very similar experience here. Also just recently I really
             | _had_ to use and extend the "internal" part of a legacy
             | library. So potentially days or more than a week of work
             | turned into a couple of hours.
        
             | haberman wrote:
             | Most data structures have invariants that must hold for the
             | data structure to behave correctly. If users can directly
             | read and write members, there's no way for the public APIs
             | to guarantee that they will uphold their documented API
             | behaviors.
             | 
             | Take something as simple as a vector (eg. std::vector in
             | C++). If a user directly sets the size or capacity, the
             | calls to methods like push_back() will behave incorrectly,
             | or may even crash.
             | 
             | Opaque pointers are one way of hiding representation, but
             | they also eliminate the possibility of inlining, unless LTO
             | is in use. If you have members that need to be accessible
             | in inline functions, it's impossible to use opaque
             | pointers.
             | 
             | There is certainly a risk of "implicit interfaces" (Hyrum's
             | Law), where users break even when you're changing the
             | internals, but we can lessen the risk by encapsulating data
             | structures as much as possible. There are other strategies
             | for lessening this risk, like randomizing unspecified
             | behaviors, so that people cannot take dependencies on
             | behaviors that are not guaranteed.
        
               | xboxnolifes wrote:
               | > Most data structures have invariants that must hold for
               | the data structure to behave correctly. If users can
               | directly read and write members, there's no way for the
               | public APIs to guarantee that they will uphold their
               | documented API behaviors.
               | 
               | You can, just not in the "strictly technical" sense. You
               | add a "warranty void if these fields are touched"
               | documentation string.
        
               | Ygg2 wrote:
               | That's honestly horrible. It's like finding your job is
               | guaranteed by a pinkie promise, or the equivalent.
        
               | pjmlp wrote:
               | I prefer liability when devs misuse software with
               | consequences for society infrastructure.
        
             | the__alchemist wrote:
             | Like unclad, I disagree that not having private fields is a
             | problem. I think this comes down to programming style. For
             | an OOP style (Just one example), I can see how that would
             | be irritating. Here's my anecdote:
             | 
             | I write a lot of rust. By default, fields are private. It's
             | rare to see a field in my code that omits the `pub` prefix.
             | I sometimes start with private because I forget `pub`, but
             | inevitably I need to make it public!
             | 
             | I like in principle they're there, but in practice, `pub`
             | feels like syntactic clutter, because it's on all my
             | fields! I think this is because I use structs as abstract
             | bags of data, vice patterns with getters/setters.
             | 
             | When using libraries that rely on private fields, I
             | sometimes have to fork them so I can get at the data. If
             | they do provide a way, it makes the (auto-generated) docs
             | less usable than if the fields were public.
             | 
             | I suspect this might come down to the perspective of
             | application/firmware development vice lib development. The
             | few times I do use private fields have been in libs. E.g.
             | if you have matrix you generate from pub fields and
             | similar.
        
               | pjmlp wrote:
               | One the key principles for modular software is
               | encapsulation, it predates OOP by decades, and at least
               | even C got that correct.
        
           | mwkaufma wrote:
           | > You need to be able to change the internal representation
           | without breaking users.
           | 
           | Unless the user only links an opaque pointer, then just
           | changing the sizeof() is breaking, even if the fields in
           | question are hidden. A simple doc comment indicating that
           | "fields starting with _ are not guaranteed to be minor-
           | version-stable" or somesuch is a perfectly "reasonable" API.
        
             | Dylan16807 wrote:
             | The chance of someone relying on the size at an API level
             | is extremely small. That's far less risky than exposing
             | every field.
        
             | nevi-me wrote:
             | I'd imagine semantic versioning to be more subjective with
             | a language that relies on a social contract, because if a
             | user chooses to use those private fields, a minor update or
             | patch could break their code.
             | 
             | It does feel regressive to me. I've seen people easily
             | reach for underscored fields in Python. We can discourage
             | them if the code is reviewed, but then again there's also
             | people who write everything without underscores.
        
           | dgb23 wrote:
           | Some years ago I started to just not care about setting
           | things to "private" (in any language). And I care _a lot_
           | about long term maintainability and breakage. I haven't
           | regretted it since.
           | 
           | > You cannot reasonably form API contracts (...) unless you
           | can hide the internal representation.
           | 
           | Yes you can, by communicating the intended use can be made
           | with comments/docstrings, examples etc.
           | 
           | One thing I learned from the Clojure world, is to have a
           | separate namespace/package or just section of code, that
           | represents an API that is well documented, nice to use and
           | more importantly stable. That's really all that is needed.
           | 
           | (Also, there are cases where you actually need to use a thing
           | in a way that was not intended. That obviously comes with
           | risk, but when you need it, you're _extremely_ glad that you
           | can.)
        
             | haberman wrote:
             | I have the opposite experience. Several years ago I didn't
             | worry too much about people using private variables.
             | 
             | Then I noticed people were using them, preventing me from
             | making important changes. So I created a pseudo-"private"
             | facility using macros, where people had to write
             | FOOLIB_PRIVATE(var) to get at the internal var.
             | 
             | Then I noticed (I kid you not) people started writing
             | FOOLIB_PRIVATE(var) in their own code. Completely
             | circumventing my attempt to hide these internal members.
             | And I can't entirely blame them, they were trying to get
             | something done, and they felt it was the fastest way to do
             | it.
             | 
             | After this experience, I consider it an absolute
             | requirement to have a real "private" struct member facility
             | in a language.
             | 
             | I respect Andrew and I think he's done a hell of a job with
             | Zig. I also understand the concern with the Java precedent
             | and lots of wordy getters/setters around trivial variables.
             | But I feel like Rust (and even C++) is a great
             | counterexample that private struct variables can be done in
             | a reasonable way. Most of the time there's no need to have
             | getters/setters for every individual struct member.
        
               | raincole wrote:
               | > And I can't entirely blame them
               | 
               | You can't blame them, but they can't blame you if you
               | break their code.
        
               | tayo42 wrote:
               | That's pretty much why I never bother with the underscore
               | prefix convention when using python. If someone wants to
               | use it they'll do it anyway.
        
               | geysersam wrote:
               | It's about the contract with the users. I don't think you
               | should worry about breaking someone using the private
               | fields of your classes. Making a field private, for
               | example by prefixing an underscore in Python, tells the
               | users "for future maintainability of the software I allow
               | myself the right to change this field without warning,
               | use at your own peril".
               | 
               | If you hesitate changing it because you worry about users
               | using it anyway you are hurting the fraction of your
               | users who are not using it.
        
               | haberman wrote:
               | This is company code in a monorepo. If a change breaks
               | users, it will simply be rolled back.
               | 
               | Everyone is brainstorming ways to work around Zig's lack
               | of "private". But nobody has a good answer for why Zig
               | can't just add "private" to the language. If we agree
               | that users shouldn't touch the private variables, why not
               | just have the language enforce it?
        
               | SpaghettiCthulu wrote:
               | Because sometimes the user really wants to access those
               | fields, and if the language enforces them being private,
               | the user will either copy-paste your code into their
               | project, or fork your project and make the fields public
               | there. And now they have a lot of extra work to stay up-
               | to-date when compared to just making the necessary
               | changes if those fields ever change had they been public.
        
               | haberman wrote:
               | I would be satisfied if the language supported this use
               | case by offering a "void my warranty" annotation that let
               | a given source file access the privates of a given
               | import.
               | 
               | Companies with monorepos could easily just ban the
               | annotation. OSS projects could easily close any user
               | complaint if the repro requires the annotation.
               | 
               | This seems like a great compromise to me. It would let
               | you unambiguously mark which parts of the api are
               | private, in a machine checkable way, which is undoubtedly
               | better than putting it into comments. But it would offer
               | an escape hatch for people who don't mind voiding their
               | warranty.
        
               | pjmlp wrote:
               | That is the beauty of binary libraries, they enforce
               | encapsulation.
        
               | magicalhippo wrote:
               | I started using Boost's approach, that is keep those
               | things public but in their own clearly-named internal
               | namespace (be it an actual namespace or otherwise).
               | 
               | This way users can get to them if they really need to,
               | say for a crucial bug fix, but they're also clearly an
               | implementation detail so you're free to change it without
               | users getting surprised when things break etc.
        
               | pjmlp wrote:
               | C++ precedent though, getters and setters were widely
               | adopted in C++ frameworks before Java was even an idea.
        
               | josephg wrote:
               | > Then I noticed (I kid you not) people started writing
               | FOOLIB_PRIVATE(var) in their own code.
               | 
               | If it's in an internal monorepo, this should be super
               | easy to fix using grep.
               | 
               | Honestly it sounds like a great opportunity to improve
               | your API. If people are going out of their way to access
               | something that you consider private, it's probably
               | because your public APIs aren't covering some use case
               | that people care about. That or you need better
               | documentation. Sometimes even a comment helps:
               | int _foo; // private. See getFoo() to read.
               | 
               | I get that it's annoying, but finding and fixing internal
               | code like this should be a 15 minute job.
        
           | ants_everywhere wrote:
           | You're getting a lot of responses with very strong opinions
           | from people who talk as if they've never had to care about
           | customers relying on their APIs.
        
             | josephg wrote:
             | It's a trust thing.
             | 
             | If you can trust that downstream users of your api won't
             | misuse private-by-convention fields (or won't punish you
             | for doing so), it's not a problem. That works a lot of the
             | time: You can trust yourself. You can usually your team. In
             | the opensource world, you can just break compatibility with
             | no repercussions.
             | 
             | But yes, sometimes that trust isn't there. Sometimes you
             | have customers who will misuse your code and blame you for
             | it. But that isn't the case for all code. Or even most
             | code.
        
           | LAC-Tech wrote:
           | The solution to this is to simply put an underscore before
           | the variables you don't think others should rely on, then
           | move on with your life.
        
           | jenadine wrote:
           | From my understanding, making stable API is impossible in Zig
           | anyway, since Zig itself is still making breaking changes at
           | the language level
        
           | flohofwoe wrote:
           | > Zig is hostile to encapsulation. You cannot make struct
           | members private
           | 
           | In Zig (and plenty of other non-OOP languages) modules are
           | the mechanism for encapsulation, not structs. E.g. don't make
           | the public/private boundary _inside_ a struct, that 's a
           | silly thing anyway if you think about it - why would one ever
           | hand out data to a module user which is not public - just to
           | tunnel it back into that same module later?
           | 
           | Instead keep your private data and code inside a module by
           | not declaring it public, or alternatively: don't try to carry
           | over bad ideas from C++/Java, sometimes it's better to
           | unlearn things ;)
        
             | the__alchemist wrote:
             | Concur. Or, the in-between: Set the structs to be private
             | if you need. I make heavy use of private structs and
             | modules, but rarely private fields.
        
             | jandrewrogers wrote:
             | I think the bigger issue with "public" and "private" is
             | that is insufficiently granular, being essentially all or
             | nothing. The use of those APIs in various parts of the code
             | base is not self-documenting. Hyrum's Law is undefeated.
             | 
             | C++ has the PassKey idiom that allows you to whitelist what
             | objects are allowed to access each part of the public API
             | at compile-time. This is a significant improvement but a
             | pain to manage for complex whitelists because the language
             | wasn't designed with this in mind. C++26 has added language
             | features specifically to make this idiom scale more
             | naturally.
             | 
             | I'd love to see more explicit ACLs on APIs as a general
             | programming language feature.
        
               | flohofwoe wrote:
               | > I'd love to see more explicit ACLs on APIs as a general
               | programming language feature.
               | 
               | In that I agree, but per-member public/private/protected
               | is a dead end.
               | 
               | I'd like a high level language which explores organizing
               | all application data in a single, globally accessible
               | nested struct and filesystem-like access rights into
               | 'paths' of this global struct (read-only, read-write or
               | opaque) for specific parts of the code.
               | 
               | Probably a bit too radical to ever become mainstream
               | (because there's still this "global state == bad" meme -
               | it doesn't have to be evil with proper access control -
               | and it would radically simplify a lot of programs because
               | you don't need to control access by passing 'secret
               | pointers' around).
        
             | cobbal wrote:
             | Why would you hand out data that gets tunneled back in?
             | 
             | There are lots of use cases for this exact pattern. An
             | acceleration structure to speed up searching complex
             | geometry. The internal state of a streaming parser. A lazy
             | cache of an expensive property that has a convenient
             | accessor. An unsafe pointer that the struct provides
             | consistent, threadsafe access patterns for. I've used this
             | pattern for all these things, and there are many more uses
             | for encapsulation. It's not just an OO concern.
        
           | pdpi wrote:
           | > The idea of private fields and getter/setter methods was
           | popularized by Java, but it is an anti-pattern.
           | 
           | I agree with this part with no reservations. The idea that
           | getters/setters provide any sort of abstraction or
           | encapsulation at all is sheer nonsense, and is at the root of
           | many of the absurdities you see in Java.
           | 
           | The issue, of course, is that Zig throws out the baby with
           | the bath water. If I want, say, my linked list to have an
           | O(1) length operation, i need to maintain a length field, but
           | the invariant that list.length actually lines up with the
           | length of the list is something that all of the other
           | operations need to maintain. Having that field be writable
           | from the outside is just begging for mistakes. All it takes
           | is list.length = 0 instead of list.length == 0 to screw
           | things up badly.
        
             | ArtixFox wrote:
             | You can have a debug time check.
        
           | sramsay64 wrote:
           | I think I mostly agree, but I do have one war story of using
           | a C++ library (Apache Avro) that parsed data and exposed a
           | "get next std::string" method. When parsing a file, all the
           | data was set to the last string in the file. I could see each
           | string being returned correctly in a debugger, but once the
           | next call to that method was made, all previous local
           | variables were now set to the new string. Never looked too
           | far into it but it seemed pretty clear that there was a bug
           | in that library that was messing with the internals of
           | std::string, (which if I understand is just a pointer to
           | data). It was likely re-using the same data buffer to store
           | the data for different std::string objects which shouldn't be
           | possible (under the std::string "API contract"). It was a
           | pain to debug because of how "private" std::string's
           | internals are.
           | 
           | In other words, we can at best form API contracts in C++ that
           | work 99% of the time.
        
             | jandrewrogers wrote:
             | FWIW, the std::string buffer is directly accessible for
             | (re-)writing via the public API. You don't need to use any
             | private access to do this.
        
           | pif wrote:
           | You are right. Don't listen to the idiots!
        
           | voidfunc wrote:
           | How is this any different than Python or Ruby? You can access
           | internals easily and people don't have a problem writing
           | maintainable modular software in those languages.
           | 
           | Not to mention just about every language offers runtime
           | reflection that let's you do bad stuff.
           | 
           | IMO, the Python adage of "We are all consenting adults here"
           | applies.
        
           | Galanwe wrote:
           | > You cannot reasonably form API contracts (which are the
           | foundation of software modularity) unless you can hide the
           | internal representation
           | 
           | Python is a good counter example IMHO, the simple convention
           | of having private fields prefixed with _/__ is enough of a
           | deterrent, you don't need language support.
        
         | wg0 wrote:
         | Zig seems to be simpler Rust and better Go.
         | 
         | Off topic - One tool built on top of Zig that I really really
         | admire is bun.
         | 
         | I cannot tell how much simpler my life is after using bun.
         | 
         | Similar things can be said for uv which is built in Rust.
        
           | FlyingSnake wrote:
           | Zig is nothing like Go. Go uses GC and a runtime while Zig
           | has none. While Zig's functions aren't coloured, it lacked
           | the CSP style primitives like goroutines and channels.
        
           | 9d wrote:
           | Zig is like a _highly opinionated_ modern C
           | 
           | Rust is like a _highly opinionated_ modern C++
           | 
           | Go is like a _highly opinionated_ pre-modern C with GC
        
             | cgh wrote:
             | In a previous comment, you remarked you don't even know
             | Zig.
        
               | 9d wrote:
               | I don't.
        
               | LexiMax wrote:
               | I do. I find his osmosis-based summation accurate.
        
               | 9d wrote:
               | _Yay!_
        
         | 9d wrote:
         | > C/C++ has been the default
         | 
         | You're not really going to make something better than C. If you
         | try, it will most likely become C++ anyway. But do try anyway.
         | Rust and Zig are evidence that we still dream that we can do
         | better than C and C++.
         | 
         | Anyway I'm gonna go learn C++.
        
           | flohofwoe wrote:
           | C++ has been piling more new problems on top of C than it
           | inherited from C in the first place (and C++ is now caught in
           | a cycle of trying to fix problems it introduced a couple of
           | versions ago).
           | 
           | Creating a better C successor than C++ is really not a high
           | bar.
        
       | timewizard wrote:
       | That for loop syntax is horrendous.
       | 
       | So I have two lists, side by side, and the position of items in
       | one list matches positions of items in the other? That just makes
       | my eyes hurt.
       | 
       | I think modern languages took a wrong turn by adding all this
       | "magic" in the parser and all these little sigils dotted all
       | around the code. This is not something I would want to look at
       | for hours at a time.
        
         | int_19h wrote:
         | Such arrays are an extremely common pattern in low-level code
         | regardless of language, and so is iterating them in parallel,
         | so it's natural for Zig to provide a convenient syntax to do
         | exactly that in a way that makes it clear what's going on
         | (which IMO it does very well). Why does it make your eyes hurt?
        
           | timewizard wrote:
           | It looks to me like:                  for (one, two, three)
           | |uno, dos, tres| { ... }
           | 
           | My eyes have to bounce back and forth between the two lists.
           | When the identifiers are longer than this example it
           | increases eye strain. Maybe it's better when you wrote it and
           | understand it, but trying to grok someone else's code, it
           | feels like an obstacle to me.
        
       | csjh wrote:
       | > High level languages lack something that low level languages
       | have in great adundance - intent.
       | 
       | Is this line really true? I feel like expressing intent isn't
       | really a factor in the high level / low level spectrum. If
       | anything, more ways of expressing intent in more detail should
       | contribute towards them being higher level.
        
         | wk_end wrote:
         | I agree with you and would go further: the _fundamental_
         | difference between high-level and low-level languages is that
         | in high-level languages you express intent whereas in low-level
         | languages you are stuck resorting to expressing underlying
         | mechanisms.
        
         | jeroenhd wrote:
         | I think this isn't referring to intent as in "calculate the tax
         | rate for this purchase" but rather "shift this byte three
         | positions to the left". Less about what you're trying to
         | accomplish, and more about what you're trying to make the
         | machine do.
         | 
         | Something like purchase.calculate_tax().await.map_err(|e|
         | TaxCalculationError { source: e })?; is full of intent, but you
         | have no idea what kind of machine code you're going to end up
         | with.
        
           | csjh wrote:
           | Maybe, but from the author's description, it seems like the
           | interpretation of intent that they want is to generally give
           | the most information possible to the compiler, so it can do
           | its thing. I don't see why the right high level language
           | couldn't give the compiler plenty of leeway to optimize.
        
           | raincole wrote:
           | In other words, high-level languages express high-level
           | intents, while low-level languages express low-level intents.
           | 
           | In yet other words, tautology.
        
       | 9d wrote:
       | > People will still mistakenly say "C is faster than Python",
       | when the language isn't what they are benchmarking.
       | 
       | Yeah but some language features are disproportionately more
       | difficult to optimize. It can be done, but with the right
       | language, the right concept is expressed very quickly and
       | elegantly, both by the programmer and the compiler.
        
       | WalterBright wrote:
       | > Rust's memory model allows the compiler to always assume that
       | function arguments never alias. You must manually specify this in
       | Zig.
       | 
       | I've avoided such manual specification of aliasing because:
       | 
       | 1. few people understand it
       | 
       | 2. using it erroneously can result in baffling bugs in your code
        
       | WalterBright wrote:
       | > The flexibility of Zig's comptime has resulted in some rather
       | nice improvements in other programming languages.
       | 
       | Compile time function execution and functions with constant
       | arguments were introduced in D in 2007, and resulted in many
       | other languages adopting something similar.
       | 
       | https://dlang.org/spec/function.html#interpretation
        
       | kamma4434 wrote:
       | I know nothing of Zig, but I worked long enough in lisp to know
       | that the best macros are the ones you don't write. They are
       | wonderful but they have just as many drawbacks, and don't compose
       | nicely.
        
       ___________________________________________________________________
       (page generated 2025-06-08 23:01 UTC)