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.
That being said, there's always space for exploring other design choices! I haven't tried Odin yet, but it looks very interesting.
https://twitter.com/TheGingerBill/status/1495004577531367425
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.
"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.
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.
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.
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)
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!".
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.
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.
Yes they do, if they detect UB they consider the result poisoned and delete any code that depends on it.
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.
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.
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.
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.
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.
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.
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.
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.
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)
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.
I totally agree.
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.
And the latest version of C now requires two's complement too!
> 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
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.
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)`.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...
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.
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
How is that a pro? That's a net negative.
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.
Well, that's something completely different then.
Perhaps that's the GP is referring to?
Unwanted type coercions are an infamous problem in C and C++.
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.
From my understanding, Jai is still years away from a general public release.
you can always try asking, worked for me
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.
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.
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.
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
Kitchen sink languages require you to build the kitchen before you can cook.
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.
From my understanding, rust has a little bit of both.
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.