Hacker News new | past | comments | ask | show | jobs | submit login
A Review of the Odin Programming Language (graphitemaster.github.io)
145 points by gingerBill on Sept 11, 2022 | hide | past | favorite | 140 comments



One thing I like about Odin is that you can use any allocator with any existing function, without wiring that function to specifically do that. It just comes for free. In a way, it completely decouples the code from the allocator choice. It's especially nice because we can use any allocator with any third party code.

I think this kind of decoupling is a big step in the right direction. The more concerns we can decouple from each other, the simpler and more flexible our codebases become and the less time we have to spend infectiously refactoring.


> It just comes for free

To be fair, specifically the price is a hidden context passed to every function. Hiding it means the programmer needn't expend effort thinking about it as they would if you did this by hand, but in performance terms this isn't really free.


This can be implemented many different ways without losing a register (/ other parameter storage). It’s called dynamic scoping or thread local variables.


Thread-local variables are implemented using a reserved register. Essentially thread-locals in C++ are the hidden context.


To be fair, they're implemented with one of the segment registers, at least on x86-64. So there isn't much else that programs could have used it for, since it takes a syscall just to set the value.


That doesn't "lose" a register because the program already wasn't using it.


I remember first seeing this idea from Jai (https://youtu.be/ciGQCP6HgqI), and I'm happy it's landing in other languages.

Zig went another direction and requires you to specify the allocator explicitly via a function parameter, which is understandable given the language's goal to make everything explicit.


Odin pulls a lot from and is influenced by Jai. Regardless of how people can feel about that, Odin is open to the public, to be used and tested by anyone.


> I don’t buy into the rhetoric that memory safety solves everything

I haven’t heard that rhetoric, only that memory safety is a really good thing. If I can pay up front with some static analysis and eliminate entire classes of bugs, that’s just such a great thing, even if it doesn’t solve everything.

I want to like and use Odin, because the guy behind it seems really cool, and it’s pretty indie, like zig. But the strong guarantees of rust are just so compelling.


I'd probably agree with that characterization. It's not that people are out there specifically claiming memory safety solves everything, but rather that the 'public consciousness' seems to have utterly forgotten that any other kind of safety exists.

As a result, you get languages like (and mainly) Rust being held up as the paragons of program correctness and safety in general. Any discussions of program safety end up being entirely about memory safety.

It's just an endless hoard of comments/blogs/posts/etc of people conflating memory safety with correctness and not even acknowledging - or possibly even knowing - there are other kinds of safety out there.

So the rhetoric being referenced there is largely implicit. Anybody who actually mentions memory safety in specific terms is already ahead of the curve.


Memory safety is a bit like seatbelts (pardon the analogy). It's possible to drive a car and never need it and seatbelts can be uncomfortable.

But I would never want to get in a car without one and it makes me question the sanity of people with saying stuff like "You most likely won't need it".


There are plenty of programs written in C that will not kill somebody if there is a memory safety issue. It is not "insane" to write a game in C. Seatbelts are about physical safety which is in a completely different category.


I write art/music apps where nobody dies if it crashes in the field. But my perspective is this: because I'm an indie developer, I will get more hate mail and nasty reviews from people when they lose their work because my app crashed (or even if they don't lose their work!). I'd like to avoid said hate mail, and the nasty reviews hurt my business. I think developers at companies are more insulated from the customer and don't think about this as much.


> There are plenty of programs written in C that will not kill

No, of course not (see analogy) - It will just make me wish for sweet, sweet release of death (see hyperbole).

Point is, you can eliminate a class of nasty bugs. And instead you double down on it.

So, I'll use a more programming example. C has more foot guns than just memory issues e.g. `if (a=4)`.

For all its faults, Java only allowing boolean expression in if statements was a move in right direction.

Now imagine after Java some C inheritor (Rust, Go, Carbon, Zig, Nim...) looked at `if (a=4)` and said. Yes. We need that in our lang.

Can't you see we regressed in this hypothetical example? We traded correctness for expressivity.

Same with memory safety. Rather than using a GC or some borrow checking system, we shrug and say, well what can ya do?


> Rather than using a GC or some borrow checking system, we shrug and say, well what can ya do?

Try different paths until someone stumbles on the One True Way™.

Plus, `if (a=4)` will emit a compiler warning of ‘statement always evaluates to true’ or somesuch and is useful in some cases like ‘if (!(a=foo())) goto error;’


> Try different paths until someone stumbles on the One True Way™.

I'm aware there isn't a perfect solution (because of Rice Theorem). But we don't need perfect, we need better than status quo (C).

> Plus, `if (a=4)` will emit a compiler warning

It's still a step down. What is better than a compiler warning that people can ignore? Prohibiting the syntax altogether.

> goto error;

This is another example of a famous chain-footgun. Most langs eschew use of goto even if it allows more flows than plain old for/while loops.


> But the strong guarantees of rust are just so compelling.

Rust's safety comes at a cost. I think its not for the greater good to pretend to ignore such, and fall too deeply into hype, that can be partially generated by the companies behind it.

With all things, there are positives and negatives. If you are into RAD or programs that you are creating are not mission-critical like that, then a person might want to avoid things like: longer compile times, low readability, difficult syntax, longer development times, etc...

There is plenty of room for newer languages like Odin, Go, Vlang, etc... Where the focus has a wider scope for more convenient general-purpose programming.


The way the article reads feels very authentic and the transparency in conflict of the review is refreshing.


> This means valgrind cannot really detect memory issues as well since Odin has it’s own internal allocators. helgrind cannot work because Odin doesn’t use pthread primitives. ltrace cannot work because that just provides wrappers over every libc function, etc

Unfortunately, I've gone through too many bugs that had to be fixed through valgrind debugging that I'm unwilling to part with it


The Odin team are working to put valgrind, helgrind, callgrind, memcheck, etc in to the Odin core library to provide better debugging tools! Along with many other debugging tools.

https://github.com/odin-lang/Odin/tree/master/core/sys/valgr...


Related:

Odin Programming Language - https://news.ycombinator.com/item?id=30394000 - Feb 2022 (42 comments)

The Odin Programming Language - https://news.ycombinator.com/item?id=22199942 - Jan 2020 (141 comments)

The Odin Programming Language - https://news.ycombinator.com/item?id=20075638 - June 2019 (3 comments)


> defer: Declarative control flow that obviates the need for RAII.

I feel like RAII is a term that has expanded to mean more than it means on its face. Maybe someone more familiar with the larger meaning of the term could help me understand? How does a defer capability have anything to do with initializing a resource upon allocation?


In C you have a system of memory allocation tied to scoping. A statement like "int x[10];" allocates memory such that when x goes out of scope, the memory is deallocated. RAII generalizes the concept so that when the variable comes in scope, some initializer function is called, and when it goes out of scope some destructor is called. This allows us to use it for dynamic arrays (unlike VLAs in C), hash tables, etc. We can replicate the system by simply calling the initializer when the var comes in scope, and the destructor when it goes out of scope. However, this creates the problem that the two statements are far apart from one another, and thus there is a chance that they might grow out of sync as the code changes. Defer allows us to cause some action to occur when the variable goes out of scope, but declare this at the site where we perform initialization, thus grouping the two together and making it easier to modify them in tandem.


It's had those extra connotations from the start- the whole reason to tie allocation to initialization is so that you can also tie deallocation to destruction, making it automatic and thus less error-prone. "RAII" is mostly just a confusing name for "destructors."


As with most C++ terms, it means something completely perpendicular to what it implies. In general it means that deterministic destructors release a resource at the moment the value representing it goes out of scope, e.g. deallocating memory or closing a file handle.

IMO Odin is very wrong on this point: the important thing about destructors is that you know where to find them, so e.g. you can have closures that know how to free their captures when they are freed. There's nothing like that for Odin.


> the important thing about destructors is that you know where to find them

The problem isn't that programmers have trouble finding a destructor. The problem is it's not apparent looking at any snippet of C++ code which objects have destructors running additional cleanup logic or other extra code without looking up each class declaration and verifying.

I believe Odin is trying to solve this by forcing the programmer to make this code explicit, but reduce the number of footguns by allowing the programmer to keep the initialization and free close to each other.


I didn't say programmers have trouble finding it. My example, closures automatically calling it, is about the compiler knowing where to find it. It does not reduce the number of footguns when you have to manually free every resource, no.


But this just shifts the problem. Now it's not apparent looking at any snippet of Odin code which objects should have cleanup logic but don't, without looking up each type and reading its documentation.


You can apply that exact same logic to languages with RAII, e.g. C++. "It's not apparently looking at any snippet of C++ code which objects have constructors and destructors, without looking up each type and reading its documentation."

Note the difference here is between being implicit vs explicit, and obvious vs hidden. RAII is implicit and hidden, which may not be a good thing in certain projects, especially when copy constructors are being called all over the place. (And move semantics don't replace copy constructors in different places, only help reduce the use of them in many cases)


Yes, that is what I replied to... my point was that neither approach is a pure improvement over the other.


Odin doesn't even have closures, or any feature that requires this kind of RAII. The whole point here is that the language doesn't deallocate for you.


I know. You say that like it's a good thing.


There's value in having a straightforward language where you know what's going to happen at any given moment. It's not good for everyone (and -thing), but languages like these have to exist, and they're needed for good reasons.


If I call a closure, I know what's going to happen - the closure is going to get called. Unless you are talking about the virtual jump, in which case that ship has sailed because Odin has function pointers. This is an allergic reaction to anything more useful than C, not a real complaint.


Odin and Zig are the two up-and-coming languages that I keep my eye on. I like how they are trying to find a sweet spot where they offer more than plain old C, but without becoming overbearing like Ada, C++, or Rust.


Odin has renewed my joy of programming.

Built in bounds checking, slices, distinct typing, no undefined behavior, consistent semantics between optimization modes, minimal implicit type conversions, context system and the standard library tracking allocator combine together to eliminate the majority of memory bugs I found use for sanitizers in C/C++. Now I'm back to logic bugs, which neither Rust nor sanitizers can help you directly with anyway because they rely on program and not language semantics. Obviously these features together do not eliminate those classes of bugs, like Rust, but Odin chooses a different point on the efficient frontier to target, and does so masterfully.

To put the cherry on top, the sane package system, buttery smooth syntax, sensible preprocessor (the 'constant system'), generics, LLVM backend for debug info / optimization, open source standard library, simply build system, engaging and well intended community make day to day programming a pleasant experience.


My experience of Rust is that affine types go a long way towards eliminating some classes of logic bugs.

That being said, there's always space for exploring other design choices! I haven't tried Odin yet, but it looks very interesting.


Odin's stance towards undefined behavior was probably the decisive element that made me prefer it over the other languages.

https://twitter.com/TheGingerBill/status/1495004577531367425


How does this work in practice? Is the behavior of a use-after-free defined? Data races? The latter even more so for objects that don't fit in one machine word, such as slices.

While avoiding undefined behavior is a noble goal, my personal feeling is that actually achieving that will be much harder than it might first seem, and will probably end up precluding a good deal of optimization.

Of course, C has an entire class of UB that is much more excessive than useful, for example left shift of a negative integer. It's clearly and obviously possible to do much better than C. I'm just skeptical that "no UB at all" is in reach for a low-level, systems programming language that is also portable and can be compiled with optimization comparable to C.


I think the twitter thread I linked answers your questions?

"use after free" is an instance of a memory access pattern that is considered invalid from the program's point of view.

What happens depends on the situation:

Was the memory returned to the operating system? If so it will probably result in a page fault and if you don't have a thing to handle the signal then the OS will crash your program.

Was the memory part of an arena managed by the custom allocator that still owns it? If so it will return whatever value is contained in the address being dereferenced.


This is fundamentally the same thing as undefined behavior, regardless of whether Odin insists on calling it by a different name. If you don't want behavior to be undefined, you have to define it, and every part of the compiler has to respect that definition. If a use-after-free is not undefined behavior in Odin, what behavior is it defined to have?

As a basic example, if the compiler guarantees that the write will result in a deterministic segmentation fault, then that address must never be reused by future allocations (including stack allocations!), and the compiler is not allowed to perform basic optimizations like dead store elimination and register promotion for accesses to that address, because those can prevent the segfault from occurring.

If the compiler guarantees that the write will result in either a segfault or a valid write to that memory location, depending on the current state of the allocator, what guarantees does the compiler make about those writes? If some other piece of code is also performing reads and writes at that location, is the write guaranteed to be visible to that code? This essentially rules out dead store elimination, register promotion, constant folding, etc. for both pieces of code, because those optimizations can prevent one piece of code from observing the other's writes. Worse, what if the two pieces of code are on different threads? And so on.

If the compiler doesn't guarantee a deterministic crash, and it doesn't guarantee whether or not the write is visible to other code using the same region of memory, and it doesn't provide any ordering or atomicity guarantees for the write if it does end up being visible to other code, and then it performs a bunch of optimizations that can affect all of those things in surprising ways: congratulations, your language has undefined behavior. You can insist on calling it something else, but you haven't changed the fundamental situation.


You language has behavior not defined within the language, sure. What it does not now have is permission for the compiler to presume that the code never executes with input that would cause the behavior not defined to occur.


The compiler is already doing that when it performs any of the optimizations I mentioned above. When the compiler takes a stack-allocated variable (whose address is never directly taken) and promotes it to a register, removes dead stores to it, or constant-folds it out of existence, it does so under the assumption that the program is not performing aliasing loads and stores to that location on the stack. In other words, it is leaving the behavior of a program that performs such loads and stores undefined, and in doing so it is directly enabling some of the most basic, pervasive optimizations that we expect a compiler to perform.

In a language with raw pointers, essentially all optimizations rely on this type of assumption. Forbidding the compiler from making the assumption that undefined behavior will not occur essentially amounts to forbidding the compiler from optimizing at all. If that is indeed what you want, then what you want is something closer to a macro assembler than a high-level language with an optimizing compiler like C. It's a valid thing to want, but you can't have your cake and eat it too.


When you put it like that, it's actually interesting. If they went ahead and said, "This is a language which by design can't have an optimizing compiler, it's strictly up to the programmer - or the code generator, if used as an intermediate language - to optimize" then it would at least be novel.

But as they don't, I see it more as an attempt to annoy the people who have studied these sort of things (I guess you are the people who "suck the joy out of programming" in their eyes)


No, the compiler is not "already" doing that. Odin uses the llvm as a backend (for now) and it turns off some of those UB-driven optimimzations (as mentioned in the OP).

Some things are defined by the language, some things are defined by the operating system, some by the hardware.

It would be silly for Odin to say "you can't access a freed pointer" because it would have to presume to know ahead of time how you utilize memory. It does not. In Odin, you are free to create an allocator where the `free` call is a no-op, or it just logs the information somewhere without actually reclaiming the 'freed' memory.

I can't speak for gingerBill but I think one of the reasons to create the language is to break free from the bullying of spec laywers who get in the way of systems programming and suck all the joy out of it.

> it does so under the assumption that the program is not performing aliasing loads and stores to that location on the stack

If you write code that tries to get a pointer to the first variable in the stack, and guess the stack size and read everything in it, Odin does not prevent that, it also (AFAIK) does not prevent the compiler from promoting local variables to registers.

Again, go back to the twitter thread. An explicit example is mentioned:

https://twitter.com/TheGingerBill/status/1496154788194668546

If you reference a variable, the langauge spec guarantees that it wil have an address that you can take, so there's that. But if you use that address to try to get other stack variables indirectly, then the language does not define what happens in a strict sense, but it's not 'undefined' behavior. It's a memory access to a specific address. The behavior depends on how the OS and the Hardware handle that.

The compiler does not get to look at that and say "well this looks like undefined behavior, let me get rid of this line!".


> If you write code that tries to get a pointer to the first variable in the stack, and guess the stack size and read everything in it, Odin does not prevent that, it also (AFAIK) does not prevent the compiler from promoting local variables to registers.

This is exactly what I described above. Odin does not define the behavior of a program which indirectly pokes at stack memory, and it is thus able to perform optimizations which exploit the fact that that behavior is left undefined.

> The compiler does not get to look at that and say "well this looks like undefined behavior, let me get rid of this line!".

This is a misleading caricature of the relationship between optimizations and undefined behavior. C compilers do not hunt for possible occurrences of undefined behavior so they can gleefully get rid of lines of code. They perform optimizing transformations which are guaranteed to preserve the behavior of valid programs. Some programs are considered invalid (those which execute invalid operations like out-of-bounds array accesses at runtime), and those same optimizing transformations are simply not required to preserve the behavior of such programs. Odin does not work fundamentally differently in this regard.

If you want to get rid of a particular source of undefined behavior entirely, you either have to catch and reject all programs which contain that behavior at compile time, or you have to actually define the behavior (possibly at some runtime cost) so that compiler optimizations can preserve it. The way Odin defines the results of integer overflow and bit shifts larger than the width of the operand is a good example of the latter.

C does have a particularly broad and programmer-hostile set of UB-producing operations, and I applaud Odin both for entirely removing particular sources of UB (integer overflow, bit shifts) and for making it easier to avoid it in general (bounds-checked slices, an optional type). These are absolutely good things. However, I consider it misleading and false to claim that Odin has no UB whatsoever; you can insist on calling it something else, but that doesn't change the practical implications.


> They perform optimizing transformations which are guaranteed to preserve the behavior of valid programs. Some programs are considered invalid (those which execute invalid operations like out-of-bounds array accesses at runtime), and those same optimizing transformations are simply not required to preserve the behavior of such programs.

I think this is the core of the problem and it's why people don't like these optimizations and turn them off.

Again I'm not the odin designer nor a core maintainer, so I can't speak on behalf of the language, but from what I understand, Odin's stance is that the compiler may not make assumptions about what kind of code is invalid and whose behavior therefore need not be preserved by the transformations it makes.


> C compilers do not hunt for possible occurrences of undefined behavior so they can gleefully get rid of lines of code.

Yes they do, if they detect UB they consider the result poisoned and delete any code that depends on it.


> The compiler does not get to look at that and say "well this looks like undefined behavior, let me get rid of this line!".

No production compiler does that (directly). This is silly. We want to help programmers. They sometimes keep it even if it is known to be UB just because removing it is unlikely to help optimizations.

But if you are optimizing assuming something does not happen, then you have undefined behavior. And you are always assuming something does not happen when optimizing.


> The compiler is already doing that when it performs any of the optimizations I mentioned above. When the compiler takes a stack-allocated variable (whose address is never directly taken) and promotes it to a register, removes dead stores to it, or constant-folds it out of existence, it does so under the assumption that the program is not performing aliasing loads and stores to that location on the stack. In other words, it is leaving the behavior of a program that performs such loads and stores undefined, and in doing so it is directly enabling some of the most basic, pervasive optimizations that we expect a compiler to perform.

No, that's C-think. Yes, when you take a stack-allocated variable and do those transformations, you must assume away the possibility that it's there are aliasing accesses to its location on the stack. Thus, those are not safe optimizations for the compiler to perform on a stack-allocated variable.

It's not something you have to do. The model of treating each variable as stack-allocated until proven (potentially fallaciously) otherwise is distinctly C brain damage.

> If that is indeed what you want, then what you want is something closer to a macro assembler than a high-level language with an optimizing compiler like C. It's a valid thing to want, but you can't have your cake and eat it too.

This is a false dichotomy advanced to discredit compilers outside the nothing-must-be-faster-than-C paradigm, and frankly a pretty absurd claim. There are plenty of "high-level" but transparent language constructs that can be implemented without substantially assuming non-aliasing. It's totally possible to lexically isolate raw pointer accesses and optimize around them. There is a history of computing before C! Heck, there are C compilers with "optimization" sets that don't behave as pathologically awfully as mainstream modern compilers do when you turn the "optimizations" off; you have to set a pretty odd bar for "optimizing compiler" to make that look closer to a macro assembler.

It's okay if your compiler can't generate numerical code faster than Fortran. That's not supposed to be the minimum bar for an "optimizing" compiler.


We are talking about Odin, a language aiming to be 'better C' the way Zig is. The literal only reason anyone uses C is to write code that runs as fast as possible, whether for resource-constrained environments or CPU-bound hot-paths. Odin has many features that one would consider warts if you weren't in an environment where you'd otherwise turn to C, such as manual memory freeing. If I were pre-committing to a language that runs five times slower than C, I have no reason to select Odin over C#, a language that runs only ~2.4 times slower than C.


> The model of treating each variable as stack-allocated until proven (potentially fallaciously) otherwise is distinctly C brain damage.

OK, let's consider block-local variables to have indeterminate storage location unless their address is taken. It doesn't substantively change the situation. Sometimes the compiler will store that variable in a register, sometimes it won't store it anywhere at all (if it gets constant-folded away), and sometimes it will store it on the stack. In the last case, it will generate and optimize code under the assumption that no aliasing loads or stores are being performed at that location on the stack, so we're back where we started.


Borrowing gingerbill's terminology, your "mini spec" is still incomplete. And it is unclear how an implicit or incomplete mini spec is different from UB, except for the fact that the compiler can't take advantage of that. From the user's perspective a gap in the mini spec is still a gap that needs to be memorized, considered and avoided much like UB. If you do somehow manage to define every "mini spec", this poses another problem that your specification limits what you can do in the future---for example you would be unable to switch the memory allocator.


The difference is that in the case of UB in C the compiler will say "aha! I will just assume this can never happen", where as with platform-dependent behavior, the compiler can never make such assumption, so has to be conservative about its assumptions.


You can already order a C compiler not to make an assumption based on UB, e.g. -fno-strict-aliasing, -ftrapv and others. Did that make C users happy? Not sure.


The point of the C11 memory model is that it gives formal bounds on what optimizations the compiler is and is not able to do. In particular, it is free to reorder memory operations as if the program was single-threaded, unless the memory operations are explicitly marked as atomic. My assertion is that if you do these optimizations and then have a data race, it's functionally equivalent to undefined behavior, even if you call it something different and loudly proclaim that your language doesn't have UB.


Obviously Odin does not use C's memory model. And in instances where LLVM optimizes Odin for UB, it is a bug, and not a feature. Odin explicitly opts out of all optimization passes that depend or leverage UB, but that's a moving target.

For example, as mentioned in the article, Odin does not leverage LLVM's poison value optimizations, which are derived from optimizations exploiting undefined behavior.

Sure, some code is slowed. But can you point to a well known and well used algorithm whose runtime characteristics depend upon exploiting UB? If you code goes fast because it's doing undefined things the compiler strips away, that's a structural bug in the application, in my view.


I'll give a concrete example. It's not the most compelling optimization in the world, but I think illustrates the tradeoffs clearly. The following is pseudocode, not any particular language, but hopefully will be clear.

    let i = mystruct.i;
    if (i < array.length) {
        let x = expensive_computation(i);
        array[i] = x;
    }
For the purpose of this example, let's say that the expensive computation is register-intensive but doesn't write any memory (so we don't need to get into alias analysis issues). Because it is register-intensive, our optimizer would like to free up the register occupied by i, replacing the second use of i by another load from mystruct.i. In C or unsafe Rust, this would be a perfectly fine optimization.

If another thread writes struct.i concurrently, we have a time of check to time of use (TOCTOU) error. In C or unsafe Rust, that's accounted for by the fact that a data race is undefined behavior. One of the behaviors that's allowed (because basically all behavior are allowed) is for the two uses of i to differ, invalidating the bounds check.

Different languages deal with this in different ways. Java succeeds in its goal of avoiding UB, disallowing this optimization; the mechanism for doing so in LLVM is to consider most memory accesses to have "unordered" semantics. However, this comes with its own steep tradeoff. To avoid tearing, all pointers must be "thin," specifically disallowing slices. Go, by contrast, has slices among its fat pointer types, so incurs UB when there's a data race. It's otherwise a fairly safe language, but this is one of the gaps in that promise.

Basically, my argument is this. If you're really rigorous about avoiding UB, you essentially have to define a memory model, then make sure your use of LLVM (or whatever code generation technique) is actually consistent with that memory model. That's potentially an enormous amount of work, very easy to get subtly wrong, and at the end of the day gives you fewer optimization opportunities than C or unsafe Rust. Thus, it's certainly not a tradeoff I personally would make.


Thanks. Currently Odin would cache i on the stack for retrieval later, granting LLVM the ability to load it into a register if profitable with knowledge that after the read, `i` is constant, which bypasses the RAW hazard after the initial read.

My view is that undefined behavior is a trash fire and serious effort should be undertaken to fix the situation before it gets even more out of hand.


> For the purpose of this example, let's say that the expensive computation is register-intensive but doesn't write any memory (so we don't need to get into alias analysis issues). Because it is register-intensive, our optimizer would like to free up the register occupied by i, replacing the second use of i by another load from mystruct.i. In C or unsafe Rust, this would be a perfectly fine optimization.

WAT?

This is obviously not "fine".

This kind of bullshit is why I said Odin's stance on UB is what swayed me to prefer it.


> If you code goes fast because it's doing undefined things the compiler strips away, that's a structural bug in the application, in my view.

I think that's the inverse of what UB-based optimizations do. Those optimizations rely on you _never_ doing what is considered UB. That's what makes the optimization correct. The problem is that, in practice, what's usually considered UB is not something that the language can always statically ensure you're not doing, and so when you do make a mistake, you land in a situation where the optimization seems to make things worse, instead of protecting you.


> Obviously Odin does not use C's memory model.

How is that obvious? And which memory model does Odin use? Just to be clear: a "memory model" is not about memory management, it is about the behaviour of multithreaded programs: https://en.m.wikipedia.org/wiki/Memory_model_(programming)


Thanks. I was thinking of strict-aliasing and the associated undefined behavior, which Odin forbids. Odin's atomic intrinsics model after C11 closely (and require the programmer to emit memory barriers when cache behavior must be controlled by instructions). I believe the final memory model will be platform defined. There is no built-in atomic data type in Odin. Only atomic operations are available, and most sync primitives are implemented in the standard library wrapping OS capabilities (WaitOnAddress), etc.

The parent comment said: >then have a data race, it's functionally equivalent to undefined behavior

This is a matter of interpretation but there is a categorical difference between "this read is undefined so the compiler will silently omit it" and "this read will read whatever value is in the CPU cache at this address, even if the cache is stale". The difference is a matter of undefined behavior from the language versus the application being in an undefined state.


> This is a matter of interpretation but there is a categorical difference between "this read is undefined so the compiler will silently omit it" and "this read will read whatever value is in the CPU cache at this address, even if the cache is stale". The difference is a matter of undefined behavior from the language versus the application being in an undefined state.

I totally agree.


How is that different from specifying the behavior of addition only when it won’t overflow? The C spec might as well say that it is invalid to overflow, and you are responsible for checking for that (and that’s kind of what they do), but that’s what UB is afaik.


> for example left shift of a negative integer. It's clearly and obviously possible to do much better than C.

If you want to allow machine architectures using other than two's complement while simultaneously striving for efficient translations, that isn't all that obvious to me.


Name a machine that people still program for that is NOT two's complement.

And the latest version of C now requires two's complement too!


Left shift with a negative shift count still has UB issues unless you generate inefficient code. The way the shift count is read is different on ARM, x86 scalar, and x86 SIMD… so you can't autovectorize it without UB.


Odin does not allow for negative shifts to begin with. To copy from the Overview[1]:

> The right operand in a shift expression must have an unsigned integer type or be an untyped constant representable by a typed unsigned integer.

and [2]:

> The shift operators shift the left operand by the shift count specified by the right operand, which must be non-negative. The shift operators implement arithmetic shifts if the left operand a signed integer and logical shifts if the it is an unsigned integer. There is not an upper limit on the shift count. Shifts behave as if the left operand is shifted `n` times by `1` for a shift count of `n`. Therefore, `x<<1` is the same as `x*2` and `x>>1` is the same as `x/2` but truncated towards negative infinity.

[1] https://odin-lang.org/docs/overview/#arithmetic-operators [2] https://odin-lang.org/docs/overview/#integer-operators


Requiring the shift amount to be unsigned is very sensible.

There are two other issues. One is when the shift amount is greater than or equal to the machine word size, for example 1u32 << 32. Java and Rust (release mode; debug is allowed to panic similarly to other forms of arithmetic overflow) take the position that the shift amount is masked to be modulo the word size, which happens to match Intel and aarch64 assembly instructions but not 32-bit ARM, so (unless the optimizer can prove the range) requires an additional mask instruction. This is the motivation for not explicitly specifying the behavior in C, though I believe a very strong case can be made that it should be implementation defined rather than undefined. In any case, it sounds like you are making the opposite decision as Java, so there's extra logic needed to correctly implement it on Intel and aarch64 but it should work efficiently on 32-bit ARM.

But I was specifically referring to the fact that -1 << 1 is UB in C. It was fixed in C++20 (so the value is specified to be -2, as arithmetic is now defined to be twos complement), but that fix is not in the C23 draft. I consider this perhaps the canonical example of programmer-hostile UB.


My second quote above actually cover your cases in the case of "overshift". In the case of overshift, the value is zeroed. Empirically I have found that the vast majority of shift factors can be known at compile time (or at least the valid range of integer), meaning it will compile to 1 instruction rather 2/3.

In naïve C, the shift would look like this:

    (y < 8*sizeof(x)) > (x << y) : 0

In practice, the "select" instruction is never needed. This approach to shifting also places never in that it feels more like 2's complement in that `x << y` is now equivalent to `x * (2*y)`.


That's a reasonable choice and I do agree it has nice properties, for example (a << b) << c is equal to a << (b + c) (unless that latter addition overflows), but it also does put you pretty firmly in the category of "not squeezing out every last cycle like you can in C." Usually that's one of the the things people expect in a tradeoff involving safety.

Regarding "never," I am an adventurous programmer[1], so am fully willing to accept that I am the exception that proves the rule.

On the more general point of UB for arithmetic overflow, I think we're on the same side: this is a pretty broken story in the way C has ended up, and one of the clear motivations for a cleaned up better-than-C language. I'm more interested in your thoughts on data races, where I suspect we have a substantive difference in perspective.

[1]: https://github.com/linebender/kurbo/blob/de52170e084cf658a2a...


Odin forbids negative shifts.


It's not a genuine negative shift, because the CPU doesn't implement negative shifts either.

It's a trick to simplify eg 'x << (32 - y)' to 'x << (-y)' and save an instruction, seen in ffmpeg, because the compilers don't always do it themselves. This works because of the shift masking behavior.


Odin's shift behaviour is different to that of C's undefined behaviour.

To copy from the Overview[1]:

> The right operand in a shift expression must have an unsigned integer type or be an untyped constant representable by a typed unsigned integer.

and [2]:

> The shift operators shift the left operand by the shift count specified by the right operand, which must be non-negative. The shift operators implement arithmetic shifts if the left operand a signed integer and logical shifts if the it is an unsigned integer. There is not an upper limit on the shift count. Shifts behave as if the left operand is shifted `n` times by `1` for a shift count of `n`. Therefore, `x<<1` is the same as `x2` and `x>>1` is the same as `x/2` but truncated towards negative infinity.

In the case of overshift, the value is zeroed. Empirically I have found that the vast majority of shift factors can be known at compile time (or at least the valid range of integer), meaning it will compile to 1 instruction rather 2/3. In naïve C, the shift would look like this:

    (y < 8*sizeof(x)) > (x << y) : 0
In practice, the "select" instruction is never needed. This approach to shifting also places never in that it feels more like 2's complement in that `x << y` is now equivalent to `x (2*y)`.

This means that `(a << b) << c` is equivalent to `a << (b + c)`. In your example of `x << (32 - y)`, that logic works as what people intuitively intent it to mean, unlike in C where when `y` is 0, the shift behaviour is platform defined (nothing on x86 (5 bit shift) but zeros on powerpc (6 bit shift)).

[1] https://odin-lang.org/docs/overview/#arithmetic-operators

[2] https://odin-lang.org/docs/overview/#integer-operators


Those machines can stay using whatever compiler/language already works for them today. Enough of holding back the rest of the world because of these fabled non-standard architectures.


> minimal type inference

How is that a pro? That's a net negative.


I should clarify. I may have mistyped.

Odin allows type inference at the declaration level `foo := 1` for example, and a few other places, largely from the constant system. 1 can be an integer, float, or even a matrix, given the larger context.

What I meant was implicit type conversion. Integers do not automatically cast as booleans in Odin, as an example.


> What I meant was implicit type conversion.

Well, that's something completely different then.


That depends on your goal. Want to move fast and break things? Then type inference is great. Want to build things that are solid and reliable? Type inference can be a very bad thing in the wrong hands and most hands are the wrong hands.


How is type inference for "moving fast and breaking things" but not for building solid and reliable things? I'm not quite sure we're talking about the same concept here.


Having been a professional OCaml developer, a long time ago, I found out that too much type inference gets into the way of proper documentation and, to some extent, proper naming conventions. Once code stabilized, we started annotating everything, as in languages with much more limited support for type inference, because it made finding type-level (and sometimes runtime) issues easier.

Perhaps that's the GP is referring to?


I'm comfortable with Rust's choice to infer types only within a function, OCaml does sound like it has too much inference. But I think the GP was just confused about vocabulary and what they're really talking about is coercion and they originally wrote "minimal type inference" instead of "minimal type coercion". I think they subsequently corrected to "minimal implicit type conversions" which, is basically just more words for the same thing.

Unwanted type coercions are an infamous problem in C and C++.


You might want to also pay attention to Jai (or whatever Jonathan Blow ends up naming it)

Like the author of Odin, Blow has significant experience writing software in a specific domain (video games), has strong opinions about what's wrong with existing languages, and decided he could do better.

You can't actually use Jai yourself yet, it is as yet a closed beta (though you might know somebody who can get you in), but you can already get a flavour of it and I think it's probably in the sphere of things you'd be interested in judging from your comment.

Personally I think we need to stop treating safety as optional, as a C programmer for about 30 years, about 15-20 years of that for pay, I found Rust very pleasant and would now always choose it over C or these C replacements - although currently I get paid to write Python and C# in my day job.

But I'm clearly in the minority, for now at least, so I expect at least one of these C replacements like Odin or Zig to get significant adoption. Probably pays to know several of them, as it's far from clear which will succeed and I doubt there's room for all of them over the long term.


But, what are people going to do with Jai, that they can't already do with Odin? As Odin was strongly influenced by Jai, it could be argued that most of whatever was innovative, is already incorporated in Odin and available today. Even more, since Odin is publicly available, its arguably being "battle tested" to a higher degree to make it a more polished product.

From my understanding, Jai is still years away from a general public release.


> You can't actually use Jai yourself yet, it is as yet a closed beta (though you might know somebody who can get you in)

you can always try asking, worked for me


I’m really surprised to see those language emerge after having read so many praise about rust being a fantastic system language.

Although, from a personal standpoint, anytime i see rust code or read about horror stories fighting the compiler i wonder how that language gained so much popularity.


Odin emerged partly out of the "Handmade Network" which is a group of people interested in a style of programming that is very different from what is usually accepted by the rest of the industry as best practice.

See: https://www.youtube.com/watch?v=f4ioc8-lDc0&t=4407s


What I genuinely don’t understand is why do we focus so much on the low-level/system front? It is (or very much should be) a small niche, and most business applications are better served by managed languages that won’t get a huge list of vulnerabilities from memory corruption alone.


Low-level and system programming are the areas most hurting for better languages.

High level has several modern scripting languages (Python, Ruby, Javascript), and typed languages (Java, C#, Kotlin). Low-level programming has had C and C++ for 30+ years, and both have major problems that need fixing. Plus, there's lots of interest in having programs run much much faster, especially as people realize that every line of Python they write could be 10x faster in a lower level language, with minimal extra work. This is why Rust, and now Odin, get so much attention.

Of course, there's also a renaissance brewing for mid-level languages, including Go, nim, and crystal.


Systems-level programming is a frontier for language design precisely because higher-level domains are already so well-served by established platforms. If your problem can be solved in Python or JavaScript, you're potentially creating a lot of work for yourself by using a language that's sort of like either of these, but not actually compatible with their libraries. The internet is littered with upstart languages that were made this way and withered on the vine. On the other hand, if you're working in a problem space with tighter performance constraints, and you already can't touch these languages, and you can't even count on libraries written in C or C++, then you suddenly, paradoxically, have a lot more options.


Personally, I would use Rust even if it was managed/not as low level. Advanced ML type system + best-in-class developer UX/tooling is the biggest selling point for me.


It's popular because the compiler is difficult - people would rather suffer the pain at compile time instead of runtime for particular projects, so it is very well suited to those.


I still feel that we're overdue for some paradigm shift in programming languages. There's a nebulous feeling, probably informed by my amateurishness, that some tasks just shouldn't be this hard. Seeing the whole buzz around GitHub Copilot, which seems to confirm that 90% of the typing we do is useless, makes me think that there's another level of "semantic abstraction" (?) we're missing.


I think an interesting distinction is this: it's not that 90% of the typing you do is necessarily useless, it's that it's been done before. Copilot is, in a sense, drawing from a corpus of prior work. In that way you are kind of using it as if you found a published library for your more specific use cases. So it's allowing you to draw on prior work without necessarily having external packages split up with the granularity of many variations of functions (which, ideally, would allow you to pull in exactly what you need from prior work, but would certainly be onerous in practice).

That being said, I've never used Copilot myself, so I can't speak very confidently about it. But from what I've seen, it kind of allows you to incorporate every library that's ever been made open source into your project, but in a more granular fashion. Which naturally would save you some typing :)

P.S. I realize Copilot isn't necessarily copying other code verbatim, though I assume pretty often it basically ends up doing that, at least in pieces.


This is an interesting take. One does need to draw a distinction between “what everybody writes” (copilot generates) and “what has to be written” (this code is the ‘useless’ stuff bemoaned by the parent comment). Obviously everybody writes what has to be written, but you're right that there is a distinction.

One gets a vision of the copilot autocomplete, as kind of missing what would be an essential highlight, in the templates it provides. “These parts highlighted in red, do not change those, for some reason everybody does it that way. But these parts highlighted in yellow, I've seen a bunch of different takes on those; this is where some variation occurs and where you might want to customize it yourself.”

On this take, copilot is a poor way of providing syntactic templates—macros—and indeed those macros take the form of template substitution, which means they form in theory a lambda calculus for that metalanguage.

That is itself interesting because I only know of one programming language which says “we are going to live out here in la-la-functional-programming-math-land, but we are going to describe values which are actual fragments of computer programs,” and that language is Haskell—though never used to write at this scale!. Interesting to think that the Ur-goal of Copilot is to provide what you were missing because you didn't wrap your language in Haskell in the first place!

With that said, a better way to start with metalanguage design if this problem irks you is probably not Haskell itself (it doesn't have an easy way to swap out its compile target language from C-- to some other target, I don't think) but something like Ometa2: http://wiki.squeak.org/squeak/3930


I feel like some languages make it much easier (or at least require less code) to accomplish certain tasks (Perl, for text processing, and Erlang for concurrency/distributed systems), while other languages make it more practical to do anything at all, but the tradeoff is that they’re much more verbose.

Kitchen sink languages require you to build the kitchen before you can cook.


Very interesting. I don't have experience with copilot, but with my own programming I tend to abstract heavily until repetition is removed and there's little to repeat (that said there are places where the language, C# in my case, could support my style of programming better). I'll check out some vids.


An interesting point. I wonder if the knowledge contained in github copilot could somehow be extracted to come up with suitable semantic abstractions.


a programming language (together with its standard library) should guide you toward safe code, while keeping an enjoyable experience.

Saying "this thing is hard to get right, so the PL should make you feel the pain" is only marginally better than a language that pretends the problem isn't there at all.


there's good difficult, the one that forces you to clarify your point, and there's bad one: the one that makes simple constructs hard or impossible without going through hoops.

From my understanding, rust has a little bit of both.


It combines two things that appeal two large camps of developers, ML style and performance. I even think that its memory safety isn't that the main attractive point. It's like a language that's aim to sell both ML family and C guys, and that's over 50% of volume of devs' voice.


> ...i wonder how that language gained so much popularity.

Well, we should not underestimate the power of corporate backers pushing and hyping their languages. And once the hype picks up momentum, it takes a lot to slow it down.


> Odin has two idiomatic ways to allocate memory. The make and new procedures. When destroying something created with make you call delete. When destroying something created with new you call free. This is a bit confusing if you come from C++ where new is paired with delete.

What's the difference between the make and new then?

> There’s no need for a build system, nor to explicitly call the linker. The compiler does it all.

Umm, so there is a build system, but it's just integrated into the compiler? What is the benefit here? Rust has an excellent build system out of the box (cargo), but it's still separate from the compiler itself.


The distinction between make and new is similar to Go.

new allocates memory for the given type.

make allocates memory referenced by the type.

For example, a slice is defined as:

    slice :: struct {
        data: rawptr,
        length: int
    }
When you make a slice, you don't allocate space for the struct. You allocate space for the data to be referenced by the struct.

> Umm, so there is a build system, but it's just integrated into the compiler? What is the benefit here? Rust has an excellent build system out of the box (cargo), but it's still separate from the compiler itself.

What's the benefit of not having the build system integrated into the compiler?

Ideally if you are not linking to external libraries, and all the code is in the language, you don't want to go through the typical stages of the C compiler where it first produces object files and then links them. You want to just produce the final exe directly. I don't think Odin does this - at least at this point - but anyway messing around with object files should be considered obsolete.


> What's the benefit of not having the build system integrated into the compiler?

Being able to write new/integrate with existing build systems (i.e. Bazel).

> Ideally if you are not linking to external libraries

I'm afraid that's rarely the case.

> messing around with object files should be considered obsolete.

Messing with them yourself, as you need to do when you use `make` in C/C++ (without cmake or anything more fancy) - I agree, it should be considered obsolete. But what if I want to mess with them because, for example, I want to add support for Odin to Bazel?


I'm confused.

What sort of advantage do you imagine a build system like Bazel would provide for you?

Build systems for C are bandaids. You would not ideally want them. They are extra baggage that you just have to carry around because of legacy reasons. It's not something to want.


Does Odin's build system solve all the problems Bazel does, e.g. scalable and nearly perfect caching? And why were you under the impression it is only useful for C, given that those problems need to be solved in a wide variety of languages? Cargo is best-in-class, but the build times for our monorepo convinced us to switch to building Rust with Bazel just for the CI experience.

This is, in fact, the primary problem I have with Odin: it builds on almost nothing that came before it, assuming that everything is C/complex/hidden/just generally NIH, and throws away the good with the bad.


I suppose for me it's the opposite: that's one of the attractive things about it.

It doesn't just blindly accept the way things have always been done.

> Cargo is best-in-class, but the build times for our monorepo convinced us to switch to building Rust with Bazel just for the CI experience.

I'm a bit surprised (not really) you don't see the contradiction in your statement. Something is really slow that you decided to throw it away, but it's also "best in class". I wonder if that's what the "best in class" is, what that says about the entire class? Maybe it's really all garbage?


What best in class means is that it solves large numbers of problems, like dependency hell, or opt-in library features, that no other system of its style solves (including Odin). The problem that it doesn't solve, i.e. scalable perfect caching, is also a problem that Odin doesn't solve, and no other system solves except Bazel, but you would not call Bazel the best because it has many developer-experience and learning-curve related downsides in addition to being out-of-ecosystem. For repos big enough where build times are a serious problem, you want to trade out a better system for Bazel, because what you lose in DX you gain eightfold in performance, a tradeoff that is only good if performance is significantly negatively impacting DX.

So, to recap, Odin is incapable of solving a problem many people have due to an aspect of its design that everyone knows you shouldn't do, but Odin doesn't bother with what everyone knows you should do. The thought process in Odinspace is that it is Old and Odin is New, and therefore while nobody has bothered checking what sort of problem this system solves, it can't be important because it's Old.

You praise it for not blindly accepting what other languages do, but what it instead does is blindly reject what other languages do. Blindness is the problem, not acceptance.


> What sort of advantage do you imagine a build system like Bazel would provide for you?

Build my C (or Rust, or Odin) Python extension, package the Python app with the extension and whatever other dependencies are required (e.g., generated protocol buffer libraries) into a Docker image, push the image to a docker repo, and apply changes to a Kubernetes deployment with a single command, and without doing any unnecessary work like rebuilding those native extensions if nothing has changed.


While that sounds abhorant to me, I can't think of a reason why you can't fit the odin compiler in there. Just have a step where the odin compiler is executed, and take its output and pipe it through the rest of the system.


It's primarily an advantage if you're trying to perform builds across languages. Bazel lets you build a C library that gets linked into a Rust application from a single build definition.


There shouldn't be a buffet of build systems. How much time is wasted bikeshedding on different build systems, not to mention writing in their proprietary language/api and having to maintain a separate program?

There doesn't need to be a variety of build systems because all they do is put out an executable. Its a simple thing that doesnt benefit from having competing products


I was wondering why the author would think me, the reader, might get very angry about something he wrote, but then I saw this from the creator of Zig:

I see a lot of toxic Rust vs Zig discourse on Twitter right now...

https://twitter.com/andy_kelley/status/1568679389113757698

There seem to be a lot of heated passions about C replacement languages right now. The Zig v Rust issue seems to boil down to the Rust community putting a lot of effort into making memory safety a priority for everyone in the industry and don't think new languages should backslide into C's "unsaftey"


I suspect a lot of the toxic discourse originally comes from folks not understanding each other's use cases and priorities.

For some, memory safety is a means to an end: delivering value. In this perspective, one must weigh the benefits of improving memory safety against the complexity costs of proving memory safety. In some situations, the improvement is too small and the added complexity is too much. Some apps and some embedded situations come to mind. Languages like Odin and Zig can be stellar in these situations.

For others, memory safety is a responsibility, and upholding it is a basic requirement of modern software no matter what costs we need to pay for it, and if the world would just accept that, then we as a society could move past the days of rampant vulnerabilities and odd memory bugs.

Both sides are equally compelling, to me at least. What I hope people can learn is that it really depends on the situation. There is a place and time for both approaches. Once we can accept that, I think the toxicity will dissipate.


This is exactly the way I see it. I view the bar for developer responsibility to be much higher than some others do. I suspect it's because I come from the world of information security where we deal very directly with the consequences of not having a higher bar.

To me, the idea that a new language would be anything other than entirely memory safe, unless it is very domain specific to areas where that's not important, is just another example of developers lowering the bar. And I'm fine calling that out and being called a zealot or whatever.


You didn't explicitly say this, but for anyone who might misinterpret your comment to imply that any memory-unsafe should be specifically targeted at certain small domains:

A vast swath of the programming world doesn't need the level of memory safety that Rust has: apps and webapps are generally sandboxed and only talk to a trusted first-party domain, and games don't need memory safety if they're single player, or even multiplayer co-op against AI. There are a lot of aspects of the industry like this.

We do need memory safety in any case that receives untrusted input (for security reasons), cases that handle multiple users' data (for privacy reasons), and safety critical software. There are plenty more cases like these too. Languages like Rust (or even more memory-safe languages) are a stellar choice for this side of the programming world, but not necessarily the other side.

Creating simple languages to serve the purposes best served by simple languages is a good thing, and I celebrate and applaud Odin and Zig for that. It's up to the individual developer to make a responsible choice based on their situation, and any developer that uses the wrong language for the wrong situation is indeed guilty of lowering the bar.


Yeah, to be clear, some domains are different. Gaming is one example of a domain where "best effort" memory safety is perfectly acceptable. Would I prefer full memory safety? Sure, but we don't see games as an attack vector in the wild very often and there are good reasons for that.

If someone wants to build a game in a language that's best-effort memory safety go for it. Similarly, CLI applications are often never called from an untrusted context and often provide semantic power that is equivalent to code execution anyways, so again I'd love to see memory safety, but I'm not going to care that much.

But these are exceptions - you'd still want to way whatever you're getting from memory-unsafe-language-X against just using a memory safe language. The default should be memory safety. Given that we have Go, Rust, Java, and more, there are few situations where memory safety isn't an easy option. Not zero, just few.


Even without it being an attack vector, memory unsafety in games can suck as a user. I play a game that’s high stakes, when you die you lose your gear. I’ve lost two full sets of endgame gear recently because of a crash due to an EXCEPTION_ACESS_VIOLATION. I found it kinda funny in a dark way. Anyway, cheats can also happen due to these sorts of issues. And let’s not even start about anticheat; if Battleye or god forbid Vanguard has a memory unsafety issue that’s exploitable, a lot of people are going to be in a very bad place.

Single player games maybe but they can still cause problems the end user sees.


EXCEPTION_ACCESS_VIOLATION is well-defined & memory safe. That's just a crash, same thing that happens when Helix crashes because it called .unwrap() on an error. It's correctness that affects the user experience, not memory safety.

From a cheating perspective, memory safety is entirely irrelevant when the game code is running on the users' computers, where they have full control over the machine code of the client, their own RAM, and the bytes being sent to the network.

There are plenty of important use cases to care about memory safety, but video games executing on players' computers ain't it.


In addition to what my sibling comment said,

> where they have full control over the machine code of the client,

Part of the state of modern anti-cheat is developers taking extreme measures to fight exactly this. Developers don’t just throw their hands up in the air and say it’s unfixable. The softwares I mention run as kernel modules, hook into all sorts of parts of your system, and sometimes even stream new mitigations to be executed as the game is running. That’s why an RCE in this software would be extremely bad.


EXCEPTION_ACCESS_VIOLATION itself is well-defined and memory safe, but the program constructs that cause it are generally not. A use-after-free or null pointer dereference may cause an access violation in one run but corrupt memory and keep going in another. This is a very typical approach to building an exploit- find something like this and then tweak the input to turn it into something worse.

This also plays into undefined behavior- unless the source language itself guarantees that a particular program will generate an access violation, chances are the access violation only happens by luck. For example, null pointer dereference is typically UB rather than specified as a crash, because while accessing the actual address 0 will reliably cause an access violation (assuming you are in such an environment), field projection and array indexing can move that address up out of the 0 page and lead to arbitrary memory access.


> EXCEPTION_ACCESS_VIOLATION is well-defined & memory safe. That's just a crash, same thing that happens when Helix crashes because it called .unwrap() on an error.

Just because both of those things cause a crash doesn't mean they are the same.

Crashing due to an incorrect .unwrap() is a bug in the business logic of the application, the developer made a wrong assumption.

EXCEPTION_ACCESS_VIOLATION is a crash due to a memory issue that a memory safe language would have prevented.

> It's correctness that affects the user experience, not memory safety.

Memory safety is part of correctness, your program can't be correct if it crashes with EXCEPTION_ACCESS_VIOLATION.


Sure, I think all developers, virtually regardless of domain, should strive for higher quality software. The bar is really low. But the fight I choose is memory safety in cases where end user security will be harmed without it - to me, that's such a pathetically low bar already, surely people can't argue against it (they can and they do lol I hate this industry tbh).


I'm not sure I'd characterize Rust as an easy enough option for most people/cases, or that there's just a "few" situations, but apart from that I like your perspective, well said.


> games don't need memory safety if they're single player, or even multiplayer co-op against AI.

I've seen a bug in handle convert function* in Warcraft 3 that resulted in bunch of maps, some singleplayer, some coop, some MP that lead to crashes (E can't find much material about it).

So no, memory security matters even in SP games with user made content and bug. Attacker can spoof or take a user mod and exploit it for their own gains

* Warcraft 3 had a really rudimentary language called JASS, however due to a way some handle convert function could leak address of objects, an entire new language stack (vJASS) was formed around it. Many maps embraced vJass. And several years later it turned out that function could be exploited.


> I've seen a bug in handle convert function* in Warcraft 3 that resulted in bunch of maps, some singleplayer, some coop, some MP that lead to crashes (E can't find much material about it).

Crashes that didn't cause a lot of damage. Using a language like Rust could add way more overhead than it's worth. The GP's point was that some applications just don't require complete safety, a crash or two won't kill anyone (which could happen literally in a hospital computer for example).


> Crashes that didn't cause a lot of damage.

Update. Crashes and RCE in fanmade maps.

Looking at https://wc3we.fandom.com/wiki/Jass_Coding I found a reference to the bug being fixed in 1.24.

And tracking that down lead to: https://gaming-tools.com/warcraft-3/patch-1-24/

One of the errors fixed:

     > Bug Fix: Fixed an __exploit that allowed users to execute arbitrary code__ in Warcraft 3 Funmaps.
     > Bug Fix: Fixed several World Editor crashes.
     > New JASS has table functions have been added to the Warcraft 3 World Editor, to fix unsafe type casting.(Save Item Handle, Unit Handle, Load Item Handle, Load Unit Handle, Get Handle ID).
EDIT: I was right originally, game error in modded games can lead to horrible exploits.


I think there is some similarity between information security and Rust community; there had been the elephant in the room, and they were the first to actively acknowledge and get rid of it. Since it was long neglected by others, the elimination of the elephant---vulnerability or memory bugs---can be seen as the utmost goal to some (but not all) of them. Others will complain, but as those others were mostly who neglected the elephant from the beginning, their complaints are mixed, some reasonable, some not. It's clear that this situation is far from being ideal, both for who are aware of the elephant and not, but I'm not sure how to mend this gap in understanding.


> the Rust community putting a lot of effort into making memory safety a priority for everyone in the industry

It would help if they did that in a manner that resembled harrassment a bit less. I realize most of the community means well but by now it should really be clear that they picked the wrong way.


Never seen this.


Rust community harassment? Must have missed when rust developers harassed the maintainer of actix so bad he quit and temporarily deleted the project repo.

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


An unfortunate incident, but was that repeated? To my knowledge the community has significantly calmed down after that incident, exactly because it was an unprecedented drama and many agreed that the drama is bad.


Nope, didn't miss that, have been a part of rust since 2014. I don't consider any of that harassment at all, let alone harassment with regards to other languages. I'm not going to reshash the discussion as this happened years ago, geofft summarizes things pretty well in that thread, but this has all been discussed to death anyways. And, again, that's still not an example of Rust users "harassing" anyone for using other languages.


It wasn't helped by Ziglang's VP of Community Loris Cro using the term 'full-time safety coomer'.



I think the problem was after he apologized, he should have just stayed quiet online for a few days (for Twitter to let some steam off) but failed to do so.

Maybe it's a bit hard for someone who has a very "online" job to be offline for some time. (Vice President... of Community? Is this an official job title related to the Zig Software Foundation?)


Come on, that was funny.


I decided that if I ever get a dog, I will name him Odin. That way, when he gets lost, I can walk around and yell "Ooooodddddiiiiinnnnnn!!"




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

Search: