Rendered at 23:21:51 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
omoikane 21 hours ago [-]
GCC still not implementing the standard pragmas reminds me of the old GCC pragma easter egg. This is in cccp.c from gcc 0.9:
/*
* the behavior of the #pragma directive is implementation defined.
* this implementation defines it as follows.
*/
do_pragma ()
{
close (0);
if (open ("/dev/tty", O_RDONLY) != 0)
goto nope;
close (1);
if (open ("/dev/tty", O_WRONLY) != 1)
goto nope;
execl ("/usr/games/hack", "#pragma", 0);
execl ("/usr/games/rogue", "#pragma", 0);
execl ("/usr/new/emacs", "-f", "hanoi", "9", "-kill", 0);
execl ("/usr/local/emacs", "-f", "hanoi", "9", "-kill", 0);
nope:
fatal ("You are in a maze of twisty compiler features, all different");
}
It's gone by gcc 1.21:
#if 0
/* This was a fun hack, but #pragma seems to start to be useful.
By failing to recognize it, we pass it through unchanged to cc1. */
...
#endif
im3w1l 12 hours ago [-]
What is the meaning of sending the #pragma as a commandline argument to those games?
mitxela 11 hours ago [-]
It's the name of the process.
im3w1l 10 hours ago [-]
Okay but that just makes it all more mysterious. Why would you set it to #pragma. Does it affect the program behavior somehow? Also why is -f sent as the name of process to emacs instead of as the first argument?
sethev 10 hours ago [-]
It was a joke and apparently the person who wrote the code thought that would be funny. execl actually replaces the current running process (the compiler) with the new one, so this very much does not doing anything useful.
AnimalMuppet 10 hours ago [-]
I think it doesn't change the behavior at all. I think it means that if you run "ps", it will show up as "#pragma" instead of as "hack" or "rogue".
MisterTea 2 days ago [-]
As someone who loves and enjoys writing Plan 9 c, all I can say is "standard c", whatever that means these days, is a mess. Too many things to name ranging from the only two oss compilers being bloated monsters, post c99 additions being pointless, in-line asm ruining portability*, and POSIX can die in a fire (along with typewriter terminals.)
I have a friend who is currently on the C committee and is likely going to leave because of all the bad ideas which have both been implemented and going to be implemented.
* No, spamming a million ugly #ifdefs all over the source files is gross and hard to read. Split up the code dammit.
WalterBright 19 hours ago [-]
It would be nice for the C Standard to say that `char` is unsigned. The damage done by making its signedness implementation-defined is incalculable.
(D sez `char` is unsigned. No more nonsense!)
C also needs to implement proper arrays. D does it, and that one feature is worth its weight in gold:
It's sooo nice to not have array overflows anymore.
avadodin 14 hours ago [-]
The right choice would probably be to make it signed because everything is an int in trad C and all other trad types are signed unless explicitly specified as unsigned.
Not that I disagree with making basic types unsigned by default, just not in C.
WalterBright 7 hours ago [-]
It makes no sense for a character type to be signed. This is why D has:
char is used for utf-8, byte and ubyte are signed and unsigned small integers. This seems like small thing, but it makes D code much more readable than C.
JdeBP 11 hours ago [-]
That would be the self-consistent choice. But it is debatable that it is the right choice when things like the <ctype.h> functions sometimes fall over, either accidentally or deliberately, if erroneously given a signed char, and sign extension can sometimes accidentally yield an EOF value.
WalterBright 7 hours ago [-]
I haven't run across a C compiler that defaults to signed char in a looong time. It's time to discard signed char so people can write portable V by default. Use signed char if and when it is needed.
I bet a lot of existing C code will break if one sets char to signed with a compiler switch.
Anybody reviewing C code and sees just char will have to waste time wondering if the code handles the implementation-defined signed-ness correctly.
And yes, I know about stdint.h, a poor attempt to deal with this problem.
dzaima 2 hours ago [-]
...I guess you don't use any C compilers targeting x86-64 then? Cause on all the main configurations in my testing (gcc+linux, clang+linux, clang+windows, msvc+windows) x86-64 uses signed char. As such more programs probably break with forced unsigned char than signed char.
pjmlp 12 hours ago [-]
Ah so Java did the right decision with signed bytes! /s
peterfirefly 13 hours ago [-]
But the VAX really, really wants to make it signed.
WalterBright 7 hours ago [-]
The VAX is a relic.
glouwbug 22 hours ago [-]
Plan 9 inheritance is really nice. You can still use it with `gcc -fplan9-extensions`. It builds some nice polymorphic workflow an array of tagged enum void pointers
jcranmer 21 hours ago [-]
There's a lot of post-C99 that has somewhat questionable value (_Generic is very much a we-have-templates-at-home kind of feature), but there are some post-C99 features that I'm rather happy to have:
* thread_local and _Atomic support from C11
* fopen("foo", "x") from C11
* aligned_alloc from C11
* CMPLX macro from C11
* C++ attribute syntax ([[foo]] flows better to me than __attribute__((foo)) does)
* Some of the new operations like ckd_add or stdc_rotate_left. I do dislike the names, but meh, at least C is starting to gain a competent set of fundamental operations.
Joker_vD 20 hours ago [-]
Also, C23 (and C2y) is moving ever closer to actually having defined semantics. As in, "just because one branch of your code, for this particular input, has read a value out of an array's bounds, your whole program's execution doesn't suddenly rewinds back to the start (with I/O included) and NOPs/loops infinitely/executes random bytes after main()".
Also, I'm personally still waiting for a "shift left" operator having cleaner wording in the standard:
If E1 has an unsigned type, the value of the result is E1 × 2**E2, wrapped
around. If E1 has a signed type and nonnegative value, and E1 × 2**E2 is
representable in the result type, then that is the resulting value; otherwise,
the behavior is undefined.
What does "otherwise" means here? It can't mean "if E1 has an unsigned type, or negative value, or E1 × 2**E2 is unrepresentable, then the behavior is undefined" because the part "if E1 has an unsigned type" has already been covered by the previous clause. Does it mean "If E1 has negative value, or E1 × 2**E2 is unrepresentable, then the behavior is undefined"? Because that's a ridiculous ordinance: (-1) << 1 is UB?!
jcranmer 19 hours ago [-]
> Does it mean "If E1 has negative value, or E1 × 2*E2 is unrepresentable, then the behavior is undefined"? Because that's a ridiculous ordinance.
That's exactly what it means, see the change mentioned in https://www.open-std.org/jtc1/sc22/wg14/www/docs/n723.htm. (Previously, in C89, left shift on signed integers was completely undefined, only unsigned integers had a defined left-shift operation.) My guess is that it has to do with signed-magnitude representation causing left shifts to be absolute garbage with negative numbers. You also have to remember that the standard having undefined behavior doesn't preclude implementations from defining that behavior, and this is especially true with the freewheeling nature of a lot of the original undefined behavior introduced in C89 and C99.
> Also, C23 (and C2y) is moving ever closer to actually having defined semantics.
Eh, having seen how some of the sausage gets made, there's definitely been some cases where the overeager urge to rip out undefined behavior from the standard has worsened the quality.
derf_ 10 hours ago [-]
> Previously, in C89, left shift on signed integers was completely undefined...
No, it was implementation defined, which is very different. Every implementation you were ever likely to encounter defined it reasonably. Making some of it undefined now has created a bunch of code that uses macros to cast back and forth between unsigned to get the expected behavior (or, for cases where it is provable that the result is representable, to replace the left shift with a multiplication and hope the optimizing compiler translates it back, because compilers now warn on any left shift of a negative value, despite the fact that the semantics are supposed to be the same as the corresponding multiplication now).
In other words, it created a mess for basically no reason.
jcranmer 10 hours ago [-]
This is the complete definition of the behavior of left shift operators in C89:
> The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 multiplied by the quantity, 2 raised to the power E2, reduced modulo ULONG_MAX+l if E1 has type unsigned long, UINT_MAX+1 otherwise. (The constants ULONG_MAX and UINT_MAX are defined in the header <limits.h>.)
You notice what's not mentioned there? What happens if E1 is signed. The standard says nothing. And when the standard says nothing, it's undefined behavior.
derf_ 4 hours ago [-]
From the beginning of Section 3.3 (several pages before the text you quoted in Section 3.3.7):
Some operators (the unary operator ~ , and the binary operators << ,
>> , & , ^ , and | , collectively described as bitwise operators)
shall have operands that have integral type. These operators return
values that depend on the internal representations of integers, and
thus have implementation-defined aspects for signed types.
MobiusHorizons 8 hours ago [-]
The behavior at the bit level seems very well defined based on that excerpt. You would need to know how signed integers are defined to know what the numeric value of the result would be. That sounds like it would be implementation dependent, but also completely predictable. Am I missing something?
MobiusHorizons 8 hours ago [-]
I would be very curious to know why such shifts would be UB as opposed to merely implementation specific.
I guess I can understand that signed shifts bump up against how the hardware represents signed numbers at the bit level, but it’s not like the assembly is unclear on any given platform.
Joker_vD 3 hours ago [-]
Some ancient machines trapped on signed overflow, including the ones caused by arithmetic shift left (as opposed to logical shift left), and apparently you can't define your implementation-defined behaviour as "oh, it traps in this case" — although still I don't believe the last part.
But if it is true, then yeah, that'd make (some) sense. I say "some" because surely in this case the implementations could simply emit the "logical shift left" instruction for the shift operation both for ints and unsigned ints? I don't think there ever was a machine that actually tracked the signedness/unsignedness of registers.
Joker_vD 18 hours ago [-]
> My guess is that it has to do with signed-magnitude representation causing left shifts to be absolute garbage with negative numbers.
First of all, why not make it implementation-defined? That seems rather more useful for the programmer. Second, I'm personally aware of only a single machine with signed-magnitude representation, IBM 7090, and its ALS (Accumulator Left Shift) instruction treated the sign bit as sticky; amusingly, LGL (Logical Shift Left) did as well, so you had to put the value being shifted into the MQ register, not the accumulator, when you wanted an unsigned shift. So... no, that can't be it?
jcranmer 18 hours ago [-]
One of the tricky things is that undefined behavior is commonly understood as having an unlimited "blast radius" and implementation-defined behavior is commonly understood as not permitting that kind of "blast radius." So any extension point that needed that unlimited "blast radius" had to be undefined behavior. (This has changed somewhat in modern times, but it does hold a little bit today--I'd point you to the discussion of N3863 at the most recent meeting, but I don't have any minutes that I feel really captured that discussion well enough.)
And one of those kinds of "blast radius" is traps, and I suspect the original motivation for making signed integer overflow undefined behavior was precisely to preserve the ability to trap on signed integer overflow, which couldn't be implementation-defined. But the records of the discussions leading up to C89 and C99 are just too fragmentary for me to be certain.
userbinator 17 hours ago [-]
The problem with UB, as evidenced over the years, is that it causes compiler writers to drift much too far from reality, despite the standard itself suggesting "behaving [...] in a documented manner characteristic of the environment" as one of the (better) choices.
Joker_vD 17 hours ago [-]
> And one of those kinds of "blast radius" is traps,
Wait. An implementation-defined behaviour isn't allowed to be trap/termination of execution? Okay, make it unspecified then, with the options of trapping or calculating some unspecified (not indeterminate) integer value.
> N3863
Hm?
Because implementation-defined behavior relies on the definition of unspecified behavior, it
means that implementation-defined behavior requires two or more possibilities to be defined in
the standard. However, a brief look at its uses in the standard clearly shows that there isn’t
always a list of potential behaviors to pick from.
Eh, I'd argue that omission of such a list is merely an implicit infinite ("two or more", you know) list of possibilities, its construction being self-evident.
N3783 5.2.1.2p1.1: Physical source file multibyte characters are mapped, in an implementation-
defined manner, to the source character set (introducing new-line characters for end-of-line
indicators) if necessary.
There is an infinite list of possible ways to map multibyte characters to the source character set — an implementation has to pick one and document it.
N3783 5.2.1.3p1: A conforming implementation shall produce at least one diagnostic message
(identified in an implementation-defined manner)
There is an infinite list of possible diagnostic messages...
N3783 5.2.2.2p1-2: In a freestanding environment (in which C program execution can take
place without any benefit of an operating system), the name and type of the function called at
program startup are implementation-defined.
There is an infinite list of possible C identifiers that an implementation may chose as the name of the function to call at program startup.
jcranmer 17 hours ago [-]
N3863 is one of those papers where what the paper proposed and what was actually discussed in the meeting ended up being rather different things, and it's the meeting discussion that I'm pointing to, not the paper itself.
> Wait. An implementation-defined behaviour isn't allowed to be trap/termination of execution? Okay, make it unspecified then, with the options of trapping or calculating some unspecified (not indeterminate) integer value.
Unspecified behavior also isn't allowed to be a trap. The difference between unspecified and implementation-defined is whether or not the compiler has to document its choice; the difference between unspecified and undefined is whether or not the choice is limited.
leni536 14 hours ago [-]
There is already a precedent for a signed integer operation to produce an implementation-defined value or to trap. It's for integer conversion:
> Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.
There is no reason that this wording strategy could not be reused for bit shifts.
In general there is no constraint that unspecified or implementation-defined behavior can't be a trap. Without further qualification "unspecified behavior" or "implementation-defined behavior" is still open-ended, and it's up to the surrounding standard text to restrict the behavior further.
> behavior, that results from the use of an unspecified value, or other behavior upon which this document provides two or more possibilities and imposes no further requirements on which is chosen in any instance
"Possibilities" include trap
uecker 18 hours ago [-]
Well, you could start to help? (instead of just being there to make sure special interests are preserved in voting)
I also do not see where the quality suffered. There were some rather minor issues we created and need to cleaned up.
jcranmer 18 hours ago [-]
> (instead of just being there to make sure special interests are preserved in voting)
This kind of attitude tends to make me suspect that you are disinclined to actually listen to my feedback.
uecker 18 hours ago [-]
Prove it. Aaron told me explicitly that he was inviting people because he was not happy that there are too many with a user view point. And then a couple people show up that never contribute papers but consistently vote for some special interest.
jcranmer 17 hours ago [-]
I joined WG14 and WG21 because I was worried that there were not enough people on either committees who had the perspective of someone who works in middle-end optimization rather than the frontend. I haven't written any papers for WG14 because so far all of the feedback I need to give can easily be given merely by discussion on the reflector or during the paper discussions (this is not the case for WG21, and I have consequently written papers for them), and--as a middle-end person--there is actually relatively little feature development for me to specifically propose. Most of the big features I'd want to see included already have active proposals (e.g., _Vec) and there's little reason for me to write a different proposal. Maybe you'll eventually see a paper from me if I take up some of the proposal work for CFP, or if I get around to writing a version of restrict that's based off of the Tree Borrows model. Or maybe if I turn some of my thoughts on multiple return values into a paper, though I don't actually have a proper frontend proposal for that that I like, so it's more likely to remain a reflector message.
uecker 9 hours ago [-]
Yes, thanks. Sorry I am not trying to be hostile, I am just a little bit annoyed about the lack of progress we have in some areas.
uecker 18 hours ago [-]
Just as a recent example. There is a case where the standard obviously is broken . With a lot of support by most of the committee and actual work by a couple of people there is a paper that aims to fix it. The only people who never constructively helped but still vote against this important repair is your block of other non-contributing people you are part of: https://github.com/c-lang-org/polls/blob/main/n3909.md
(Also a counter proposal would be constructive.)
uecker 17 hours ago [-]
But I apologize, I should not be so negative. But I hope you see where the frustration comes from. The cleanup efforts are driven by a lot of volunteer work. We could use any help and instead of people just voting against such works.
jcranmer 7 hours ago [-]
The complaint that has been brought up there--repeatedly--is that the term 'discarded' creates confusion with the similar 'discarded-value expression' in C++, which is completely different semantically despite the similar name.
I think it means what you think is ridiculous. Shifting the sign bit out is UB. Unrepresentable results are UB.
Joker_vD 19 hours ago [-]
> Shifting the sign bit out is UB
Shifting into the sign bit is also UB. Can I please have a language that actually has bitwise operators with defined semantics? No CPUs had differentiated between SAL/SHL for more than 30 years, if that was the original justification.
I'm fine with left shift trapping on overflow. I'm fine with it just wrapping around. I'm even fine with bitwise shifts simply unapplicable to ints ("cast to unsigned first, then cast back to int"). I am not fine with an operator that invokes UB for literally half of the values of the lhs's domains.
> The << operator performs a smart shift, which defines a result for a shift of any value.
> - Using a negative value for rhs performs a right shift using abs(rhs).
> - Using a value for rhs that is greater than or equal to the bit width of lhs is an overshift, resulting in zero.
> - Using any other value for rhs performs a left shift on lhs by that amount.
And while the docs don’t say it, `Int.max << 1 == -2`. And in fact, `T.max << 1 == -2` for all `T: BinaryInteger`.
uecker 18 hours ago [-]
I was thinking about eliminating this shift-into-left bit UB. There was a previous attempt though that failed to get consensus. (but long enough ago that one could try again).
But all this signed overflow UB can be turned into traps using sanitizers.
In practice, generally mandating a trap is difficult though in ISO C. My plan is to mandate this as part of a special mode, in which case I would just leave this UB in the main standard.
(One problem we also have that some special interests groups in WG14 are rather hostile to our work and the rest of the C community - so I encourage everybody who is actually using C and caring about it to join).
Joker_vD 17 hours ago [-]
> I was thinking about eliminating this shift-into-left bit UB.
At least make it "change of the sign bit is UB" — if 0 shifts into the sign-bit of a positive number, or 1 shifts into the sign-bit of a negative number, that shouldn't be UB. This would make left-shifts of negative numbers well-defined, their behaviour being that of two's complement numbers. And C23 already demands two's complement from the implementations, doesn't?
> signed overflow UB can be turned into traps using sanitizers.
Well, yeah, sure, but shifting -1 one place to the left doesn't overflow, it produces -2; only shifting INT_MIN or something up to and including INT_MIN + (INT_MAX/2) would overflow.
uecker 8 hours ago [-]
I agree. Thanks!
leni536 14 hours ago [-]
> Can I please have a language that actually has bitwise operators with defined semantics?
C++? It has defined semantics since C++20.
Joker_vD 2 hours ago [-]
No, not really? From C++23:
4) The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits
are filled with zeros. If E1 has an unsigned type, the value of the result
is E1 × 2**E2, wrapped around. If E1 has a signed type and nonnegative value,
and E1 × 2**E2 is representable in the result type, then that is the resulting
value; otherwise, the behavior is undefined.
5) The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an
unsigned type or if E1 has a signed type and a nonnegative value, the value of
the result is the integral part of the quotient of E1 / 2**E2. If E1 has a
signed type and a negative value, the resulting value is implementation-defined.
It's marginally more well-defined, sure, but not fully. The right shifts of negative numbers are no longer UB but are still an unportable mystery; and the left shifts of negative values are still outright UB.
pjmlp 12 hours ago [-]
Ada?
Operations are well defined, and to do stuff like in C, you must do Unchecked_Something calls.
mitxela 11 hours ago [-]
It's exactly understandable if you think like a human instead of like a stupid logic machine. It would make no sense for the same thing to be both defined and undefined, and they defined it in the previous sentence, so obviously they did not mean to also undefine it.
I always took those to mean "we're sure a bunch of you are already using the keyword we wanted to use as typedef in your codebase, so here's something that's in the standard's namespace, and you can typedef it to something sane for your codebase".
uecker 18 hours ago [-]
Indeed. And those typedefs were even provided by including stdbool.h
C23 broke a lot of code by making this proper lower case keywords, we probably should have waited longer.
pjmlp 17 hours ago [-]
It doesn't get rid of _Really_Hate_This, only for a few that already have enough years behind them, the process for new keywords remains the same.
Bounded checked slices like proposed by Dennis Ritchie, or Walter Bright, remains a pipe dream.
foldr 12 hours ago [-]
_Generic is very misunderstood. It's just defined so that there's a standards-compliant way of implementing the macros in tgmath.h (which previously required compiler-specific hacks). I don't think it was ever intended as a general-purpose feature of any great value.
NooneAtAll3 14 hours ago [-]
> (_Generic is very much a we-have-templates-at-home kind of feature)
_Generic isn't generics/templates
it is function overload
201984 22 hours ago [-]
>post c99 additions being pointless
Atomics are necessary nowadays though.
3836293648 21 hours ago [-]
As a part of the language? Are they really? You can do it all with library functions that just take regular int* (and so on).
Not that I'd want it, I like complex type systems, but I see it as philosophically consistent with C.
aw1621107 21 hours ago [-]
> You can do it all with library functions that just take regular int* (and so on).
It's not just about atomics, but you might still find Hans Boehm's 2005 paper "Threads Cannot Be Implemented As a Library" [0] interesting. It describes (among other things) correctness and performance issues that can arise if the compiler and/or language don't know about thread/atomic semantics.
Yeah, it's all fun and games until you realize that you compiler may invent writes in order to eliminate a branch. Which is rather nonsensical, when you stop to think about it — surely an additional store to global can't be faster than a mispredicted branch?
homosapien97 18 hours ago [-]
Branch mispredictions on deep machines can be devastating, whereas a store can hit in your L1. Storing can definitely be faster than a mispredicted branch if you think you'll hit in your L1/L2
adgjlsfhk1 18 hours ago [-]
store to a global is typically 0 cycles (although subsequent reads may be slowed down)
dokyun 15 hours ago [-]
It seems like the guy analyzes how Pthreads are formally defined and declares that "Threads cannot be implemented as a library". Maybe if the title was changed to "Pthreads cannot be implemented as a library" it'd be true. Pthreads are fucking abysmal.
> We explicitly do not address the less frequently used environments in which multiple concurrent threads communicate communicate primarily through message passing, such as C programs communicating only through the MPI[34] library, or Erlang[11] or Concurrent ML[28] programs.
Heh, there's your answer. CSP FTW!
gpderetta 13 hours ago [-]
Nothing to do with pthreads. As long as you want to use shared memory to communicate with concurrent processes, you need a memory model with compiler support. Even if you exclusively use mutexes.
jcranmer 21 hours ago [-]
Compilers have, for several decades now, stopped viewing optimization in terms of optimizing the instructions of a function and instead viewed it as preserving the semantics under some machine model. Consequently, it's rather important to actually nail down some of the object model semantics to be able to do things like understand which loads and stores actually need to be preserved, and you can't really do that with just library functions.
And, FWIW, in a lot of high-performance code, the overhead of a function call may be too high for an atomic operation.
publicmail 21 hours ago [-]
A library function written in what? Atomics also come along with memory ordering which isn’t otherwise expressible in the language, as in, there was no standard way to do a “load acquire” before, for example.
17 hours ago [-]
dokyun 17 hours ago [-]
Funny this is getting downvoted cause it's literally what Plan 9 C does.
I don't think that man page on its own is sufficient to draw that particular conclusion. You need compiler/language support to ensure atomics are handled properly, so you'd need to know how Plan 9's compiler/language specifies atomics.
dokyun 15 hours ago [-]
> You need compiler/language support to ensure atomics are handled properly
That's an assumption that you're treating as though it somehow informs reality. I gave you an example of atomics being handled by a library, and the man page points to where in the source code it's defined.
> so you'd need to know how Plan 9's compiler/language specifies atomics.
It doesn't, as far as I can tell. Kencc is a very "dumb" compiler that does little magic nor does it extend the language beyond the standard C that the Bell Labs guys were used to. For whatever reasons, they found it expedient to write new libraries instead of changing the language itself. They did occasionally write compilers for new languages when they had wacky ideas that would be difficult to accomodate, like Newsqueak and Alef. In Alef's case they actually did accomodate some of it, which is how Plan 9 got libthread.
aw1621107 13 hours ago [-]
> That's an assumption that you're treating as though it somehow informs reality.
At least to me, section 4 seems to imply that the "assumption" is a bit more than just that. For example:
> We know of at least four optimizing compilers (three of them production compilers) that performed this transformation at some point during their lifetime; sometimes at least partially reversing the decision when the implications on multi-threaded code became known.
i.e., there are real-life instances of the lack of compiler/language support leading to miscompilations. Yes, that example is talking about threads, but I think a similar argument should apply to atomics.
> I gave you an example of atomics being handled by a library
The thing is that nothing in that man page indicated to me that that library wouldn't be subject to problems similar to that described in Boehm's paper; e.g., for all I know that library is the Plan 9 moral equivalent of pthreads and is subject to similar problems pthreads is when a non-thread-aware optimizing compiler is used.
> Kencc is a very "dumb" compiler that does little magic
This could (partially?) address things though. If Plan 9 always uses that compiler and if there's no interest in implementing the optimizations Boehm describes then that would obviate 2 of the 3 correctness issues Boehm describes (though that would also imply that Plan 9 uses its own dialect of C with its own set(s) of guarantees, both explicit and implied). The only thing I'm uncertain about is to what extent hardware reordering would be relevant; I thought I remember reading about that making things interesting, but I don't remember off the top of my head to what extent.
pjmlp 11 hours ago [-]
Plan 9 did got libthread, because Alef wasn't designed to be garbage collected, and it was proven as failure to do so.
To quote Rob Pike,
"Early 1990s: Alef (Phil Winterbottom) grafted the concurrency
and communications model of Newsqueak onto a more
traditional compiled C-like language.
Problem: with C's memory model in a concurrent world, hard
to know when to free items.
All the other languages in this talk are garbage-collected,
which is essential to easy concurrent programming."
which is precisely why they can get away with a library seemingly without having to specify or coordinate anything with the compiler.
dzaima 13 hours ago [-]
That a library exists that claims to do atomics doesn't mean that it actually has working well-defined semantics, and that no standards-compliant C99 compiler can do optimizing transforms that'd break it.
Of course no doubt that (hopefully) C compilers used for compiling plan 9 things won't break it, but...that's then talking about plan9-C, not C99, at which point you're necessarily already in the bucket of "this library relies on extra guarantees that plan9 C makes, instead of it just being a regular C99 library".
> > so you'd need to know how Plan 9's compiler/language specifies atomics.
> It doesn't, as far as I can tell. Kencc is a very "dumb" compiler
...So it effectively quietly makes a bunch of quiet guarantees that C99 doesn't, on account of not making certain valid optimizations. Even if not explicit, that's still semantics outside of those C99 specifies, that code may be relying on.
Fact of the matter is, you cannot make a full atomics library that will necessarily work correctly on C99. As a simple example I came up with on the spot:
static int foo;
int main() {
some_preparation();
foo = 2;
fence_probably();
do_thing(&foo);
}
A fully-proper standards-compliant C99 compiler could notice that foo isn't used until the `do_thing` call, and as such reorder the `foo = 2;` to after the `fence_probably` call (yay for putting all operations on foo in the same place!).
But, if atomics exist, `do_thing` could do a relaxed store of its given foo address to a global, and another thread could then do a relaxed load of that and dereference that to get foo's value. And if the compiler decides to reorder the store to `foo` to after the fence (as a C99 compiler can), the program now has a data race that it otherwise would not.
(hell, a C99 compiler could even insert `int tmp = something; something = 123; something = tmp;` for any variable or global or field `int something`, in any place in the code (as long as `something` is known to be mutable), which'd break basically every multithreaded thing; this effectively one of the things the "Threads Cannot Be Implemented As a Library" paper mentions)
pjmlp 18 hours ago [-]
The problem starts already with being Plan 9, as it was the evolution of UNIX, the authors even played with Alef as C's successor.
In what concerns C itself, as the language authors, they did not care that much about the standard, hence why it is mostly C89 with Rob Pike ideas on how header files should be organised, and other Plan 9 specific idioms.
iberator 11 hours ago [-]
You love it? Then how come there are almost no applications for Plan9 ? Literally like only 2 editors for example .
What software do you write?
hollowonepl 18 hours ago [-]
As much as I’m sentimental about C/C++ myself, changing language spec every 3 years is not what I call standards
childintime 7 hours ago [-]
The C they have been making is full of UB foot guns and is ever more painful to work with. That only really makes sense if you actually want to frustrate devs and alienate them. I actually have nothing against sunsetting the language, it would be a noble goal to minimize changes, so a new language (like Zig does) can integrate a C compiler without having to spend time on updating it.
uecker 18 hours ago [-]
It does not change every 3 years for C: C99, C11, C17 (minor cleanup), C23.
pjmlp 18 hours ago [-]
Plenty of industry standards change every few years, that is why a date is part of their identification number.
hollowonepl 10 hours ago [-]
electricity plugs haven't changed for hundred of years, that's a standard. if you change things inside a package every couple of years then it's a product line not a standard.. like Jeep Wrangler that looked different years ago and now.. as an analogy.
I'm not critical about languages evolving.. just the tech changes so quickly that perhaps standardising it doesn't make any sense any more.
I wonder if languages like RUST or GO will ever become ISO standards.. I do believe all these new platforms won't even get close to that need.
jcranmer 9 hours ago [-]
As far as I can tell, the US didn't start standardizing the power plug until the 1940s, and the current three-prong system doesn't come about until the 1960s. The actual receptacles themselves have had improvements to them over the decades, the most recent one coming about apparently in 2016.
> I wonder if languages like RUST or GO will ever become ISO standards..
I doubt it, but mostly because everyone I know on the ISO standardization committees have expressed frustration with ISO (the organization) in ways that I haven't seen for other standards bodies like Ecma.
pjmlp 9 hours ago [-]
Actually they have, depending on the country.
Haven't travelled much around Europe in the last decades, or had to deal with electricity projects, I gather.
Nowadays, due to the ways of ISO most languages end up going with ECMA, as ISO processes are considered somehow Jurassic.
However a resurgence in cold war like state of the world might indeed, bring us back to ISO as main driver for standardising languages instead of ECMA or open source, due to the way it ensures specifications and implementations aren't tainted by foreign politics.
BigTTYGothGF 10 hours ago [-]
> electricity plugs haven't changed for hundred of years, that's a standard
Spoken like someone who's never felt the frustration of having a grounded plug and the only available sockets being two-prong.
yuye 9 hours ago [-]
Here in Taiwan, many people just rip the ground prong out of the plug.
10 hours ago [-]
sylware 12 hours ago [-]
linux "C"... Linus T. is holding the line on syscall ABI stabitily but failed at keeping plain and simple C with assembly source files to uncouple linux code from the few compilers able to build it and unlock smaller alternatives (for instance cproc/qbe).
I don't really mind though as it seems the far away real future is something like everything RISC-V assembly, "C" would only be the binary specification.
And some python/javascript/etc interpreters written in RISC-V assembly themselves, because sometimes my code can be slow and non-rigorous.
dzaima 22 hours ago [-]
> gcc, however, always defines FLT_ROUNDS to 1
Well, considering that gcc doesn't support the FENV_ACCESS pragma, and as such all fesetround calls are always UB, it is valid for it to act as if the rounding mode never changes from the default :)
uecker 18 hours ago [-]
Well, it is not ok to not support FENV_ACCESS though.
amszmidt 17 hours ago [-]
I skimmed the standard and POSIX, but couldn’t find anything that an implementation is required to support FENV_ACCESS.
“ISO C language standard requires that C compilers support the following three pragmas, and some C++ compiler vendors support them, to varying degrees, in their C++ frontends:
At least some GCC devs claimed that the standard does not impose any requirements on these macros if a compiler does not claim strict ISO/IEC 6055 (aka IEE 754) conformance (which is still optional, and gcc and clang do not claim it).
Other devs disagree.
Bratmon 2 hours ago [-]
I admit "We don't have to comply with the standard if we don't claim to comply with the standard" is tautologically true.
But it does kind of support the whole "GCC C is not standard C" point.
Joker_vD 15 hours ago [-]
4 Conformance
A strictly conforming program shall use only those features of the language and
library specified in this document.
[...]
The two forms of conforming implementation are hosted and freestanding. A
conforming hosted implementation shall accept any strictly conforming program.
And it doesn't say anywhere in the standard that it's optional to support FENV_ACCESS pragma — it's a feature in the standard, so if you want to be a conforming implementation, you have to implement it. Of course, you can always be a non-conforming implementation, that also is allowed. Who's to stop you?
dzaima 14 hours ago [-]
Yeah, that's still an issue of course. Root cause is still just that gcc utterly doesn't support any of fenv anything.
That said, I was surprised that it's mandatory by the standard (and wasn't made optional later like VLAs); feels rather unlike C to require such precise control over something it doesn't even specify any behavior of - imagine if we had mandatory macros for integer overflow behavior and whatnot!
...That "that said" said, C11 does also contain:
> Each of the macros FE_DOWNWARD FE_TONEAREST FE_TOWARDZERO FE_UPWARD is defined if and only if the implementation supports getting and setting the represented rounding direction by means of the fegetround and fesetround functions.
so presumably one could just not define those and get away with not implementing (at least the rounding mode part of) fenv.h, properly allowing the `#define FLT_ROUNDS 1` hack? gcc+glibc does define those, though.
fooker 8 hours ago [-]
The purpose of a language standard is circular, it’s that it exists and is free from inconsistencies.
Whether a compiler implements the standard is up to the compiler developers and maybe its users.
In an era where code portability was of utmost importance, maybe this made a lot of sense.
But now we can pretty much treat a compiler and its libraries the standard for the language, like Rust does.
I have a friend who is currently on the C committee and is likely going to leave because of all the bad ideas which have both been implemented and going to be implemented.
* No, spamming a million ugly #ifdefs all over the source files is gross and hard to read. Split up the code dammit.
(D sez `char` is unsigned. No more nonsense!)
C also needs to implement proper arrays. D does it, and that one feature is worth its weight in gold:
https://www.digitalmars.com/articles/C-biggest-mistake.html
It's sooo nice to not have array overflows anymore.
Not that I disagree with making basic types unsigned by default, just not in C.
I bet a lot of existing C code will break if one sets char to signed with a compiler switch.
Anybody reviewing C code and sees just char will have to waste time wondering if the code handles the implementation-defined signed-ness correctly.
And yes, I know about stdint.h, a poor attempt to deal with this problem.
* thread_local and _Atomic support from C11
* fopen("foo", "x") from C11
* aligned_alloc from C11
* CMPLX macro from C11
* C++ attribute syntax ([[foo]] flows better to me than __attribute__((foo)) does)
* C23 finally gets rid of _Who _The _Hell _Thinks _Users _Like _This-style _Keywords.
* Multi-level break/continue in C2y
* Variable declarations in if conditions.
* Some of the new operations like ckd_add or stdc_rotate_left. I do dislike the names, but meh, at least C is starting to gain a competent set of fundamental operations.
Also, I'm personally still waiting for a "shift left" operator having cleaner wording in the standard:
What does "otherwise" means here? It can't mean "if E1 has an unsigned type, or negative value, or E1 × 2**E2 is unrepresentable, then the behavior is undefined" because the part "if E1 has an unsigned type" has already been covered by the previous clause. Does it mean "If E1 has negative value, or E1 × 2**E2 is unrepresentable, then the behavior is undefined"? Because that's a ridiculous ordinance: (-1) << 1 is UB?!That's exactly what it means, see the change mentioned in https://www.open-std.org/jtc1/sc22/wg14/www/docs/n723.htm. (Previously, in C89, left shift on signed integers was completely undefined, only unsigned integers had a defined left-shift operation.) My guess is that it has to do with signed-magnitude representation causing left shifts to be absolute garbage with negative numbers. You also have to remember that the standard having undefined behavior doesn't preclude implementations from defining that behavior, and this is especially true with the freewheeling nature of a lot of the original undefined behavior introduced in C89 and C99.
> Also, C23 (and C2y) is moving ever closer to actually having defined semantics.
Eh, having seen how some of the sausage gets made, there's definitely been some cases where the overeager urge to rip out undefined behavior from the standard has worsened the quality.
No, it was implementation defined, which is very different. Every implementation you were ever likely to encounter defined it reasonably. Making some of it undefined now has created a bunch of code that uses macros to cast back and forth between unsigned to get the expected behavior (or, for cases where it is provable that the result is representable, to replace the left shift with a multiplication and hope the optimizing compiler translates it back, because compilers now warn on any left shift of a negative value, despite the fact that the semantics are supposed to be the same as the corresponding multiplication now).
In other words, it created a mess for basically no reason.
> The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 multiplied by the quantity, 2 raised to the power E2, reduced modulo ULONG_MAX+l if E1 has type unsigned long, UINT_MAX+1 otherwise. (The constants ULONG_MAX and UINT_MAX are defined in the header <limits.h>.)
You notice what's not mentioned there? What happens if E1 is signed. The standard says nothing. And when the standard says nothing, it's undefined behavior.
I guess I can understand that signed shifts bump up against how the hardware represents signed numbers at the bit level, but it’s not like the assembly is unclear on any given platform.
But if it is true, then yeah, that'd make (some) sense. I say "some" because surely in this case the implementations could simply emit the "logical shift left" instruction for the shift operation both for ints and unsigned ints? I don't think there ever was a machine that actually tracked the signedness/unsignedness of registers.
First of all, why not make it implementation-defined? That seems rather more useful for the programmer. Second, I'm personally aware of only a single machine with signed-magnitude representation, IBM 7090, and its ALS (Accumulator Left Shift) instruction treated the sign bit as sticky; amusingly, LGL (Logical Shift Left) did as well, so you had to put the value being shifted into the MQ register, not the accumulator, when you wanted an unsigned shift. So... no, that can't be it?
And one of those kinds of "blast radius" is traps, and I suspect the original motivation for making signed integer overflow undefined behavior was precisely to preserve the ability to trap on signed integer overflow, which couldn't be implementation-defined. But the records of the discussions leading up to C89 and C99 are just too fragmentary for me to be certain.
Wait. An implementation-defined behaviour isn't allowed to be trap/termination of execution? Okay, make it unspecified then, with the options of trapping or calculating some unspecified (not indeterminate) integer value.
> N3863
Hm?
Eh, I'd argue that omission of such a list is merely an implicit infinite ("two or more", you know) list of possibilities, its construction being self-evident. There is an infinite list of possible ways to map multibyte characters to the source character set — an implementation has to pick one and document it. There is an infinite list of possible diagnostic messages... There is an infinite list of possible C identifiers that an implementation may chose as the name of the function to call at program startup.> Wait. An implementation-defined behaviour isn't allowed to be trap/termination of execution? Okay, make it unspecified then, with the options of trapping or calculating some unspecified (not indeterminate) integer value.
Unspecified behavior also isn't allowed to be a trap. The difference between unspecified and implementation-defined is whether or not the compiler has to document its choice; the difference between unspecified and undefined is whether or not the choice is limited.
https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf#subsu...
> Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.
There is no reason that this wording strategy could not be reused for bit shifts.
In general there is no constraint that unspecified or implementation-defined behavior can't be a trap. Without further qualification "unspecified behavior" or "implementation-defined behavior" is still open-ended, and it's up to the surrounding standard text to restrict the behavior further.
https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf#subse...
> behavior, that results from the use of an unspecified value, or other behavior upon which this document provides two or more possibilities and imposes no further requirements on which is chosen in any instance
"Possibilities" include trap
I also do not see where the quality suffered. There were some rather minor issues we created and need to cleaned up.
This kind of attitude tends to make me suspect that you are disinclined to actually listen to my feedback.
(Also a counter proposal would be constructive.)
Very informative, thanks
Shifting into the sign bit is also UB. Can I please have a language that actually has bitwise operators with defined semantics? No CPUs had differentiated between SAL/SHL for more than 30 years, if that was the original justification.
I'm fine with left shift trapping on overflow. I'm fine with it just wrapping around. I'm even fine with bitwise shifts simply unapplicable to ints ("cast to unsigned first, then cast back to int"). I am not fine with an operator that invokes UB for literally half of the values of the lhs's domains.
> The << operator performs a smart shift, which defines a result for a shift of any value.
> - Using a negative value for rhs performs a right shift using abs(rhs).
> - Using a value for rhs that is greater than or equal to the bit width of lhs is an overshift, resulting in zero.
> - Using any other value for rhs performs a left shift on lhs by that amount.
And while the docs don’t say it, `Int.max << 1 == -2`. And in fact, `T.max << 1 == -2` for all `T: BinaryInteger`.
But all this signed overflow UB can be turned into traps using sanitizers. In practice, generally mandating a trap is difficult though in ISO C. My plan is to mandate this as part of a special mode, in which case I would just leave this UB in the main standard.
(One problem we also have that some special interests groups in WG14 are rather hostile to our work and the rest of the C community - so I encourage everybody who is actually using C and caring about it to join).
At least make it "change of the sign bit is UB" — if 0 shifts into the sign-bit of a positive number, or 1 shifts into the sign-bit of a negative number, that shouldn't be UB. This would make left-shifts of negative numbers well-defined, their behaviour being that of two's complement numbers. And C23 already demands two's complement from the implementations, doesn't?
> signed overflow UB can be turned into traps using sanitizers.
Well, yeah, sure, but shifting -1 one place to the left doesn't overflow, it produces -2; only shifting INT_MIN or something up to and including INT_MIN + (INT_MAX/2) would overflow.
C++? It has defined semantics since C++20.
Operations are well defined, and to do stuff like in C, you must do Unchecked_Something calls.
I always took those to mean "we're sure a bunch of you are already using the keyword we wanted to use as typedef in your codebase, so here's something that's in the standard's namespace, and you can typedef it to something sane for your codebase".
C23 broke a lot of code by making this proper lower case keywords, we probably should have waited longer.
Bounded checked slices like proposed by Dennis Ritchie, or Walter Bright, remains a pipe dream.
_Generic isn't generics/templates
it is function overload
Atomics are necessary nowadays though.
Not that I'd want it, I like complex type systems, but I see it as philosophically consistent with C.
It's not just about atomics, but you might still find Hans Boehm's 2005 paper "Threads Cannot Be Implemented As a Library" [0] interesting. It describes (among other things) correctness and performance issues that can arise if the compiler and/or language don't know about thread/atomic semantics.
[0]: https://web.stanford.edu/class/archive/cs/cs240/cs240.1236/o...
> We explicitly do not address the less frequently used environments in which multiple concurrent threads communicate communicate primarily through message passing, such as C programs communicating only through the MPI[34] library, or Erlang[11] or Concurrent ML[28] programs.
Heh, there's your answer. CSP FTW!
And, FWIW, in a lot of high-performance code, the overhead of a function call may be too high for an atomic operation.
https://man.cat-v.org/plan_9/2/atom
That's an assumption that you're treating as though it somehow informs reality. I gave you an example of atomics being handled by a library, and the man page points to where in the source code it's defined.
> so you'd need to know how Plan 9's compiler/language specifies atomics.
It doesn't, as far as I can tell. Kencc is a very "dumb" compiler that does little magic nor does it extend the language beyond the standard C that the Bell Labs guys were used to. For whatever reasons, they found it expedient to write new libraries instead of changing the language itself. They did occasionally write compilers for new languages when they had wacky ideas that would be difficult to accomodate, like Newsqueak and Alef. In Alef's case they actually did accomodate some of it, which is how Plan 9 got libthread.
At least to me, section 4 seems to imply that the "assumption" is a bit more than just that. For example:
> We know of at least four optimizing compilers (three of them production compilers) that performed this transformation at some point during their lifetime; sometimes at least partially reversing the decision when the implications on multi-threaded code became known.
i.e., there are real-life instances of the lack of compiler/language support leading to miscompilations. Yes, that example is talking about threads, but I think a similar argument should apply to atomics.
> I gave you an example of atomics being handled by a library
The thing is that nothing in that man page indicated to me that that library wouldn't be subject to problems similar to that described in Boehm's paper; e.g., for all I know that library is the Plan 9 moral equivalent of pthreads and is subject to similar problems pthreads is when a non-thread-aware optimizing compiler is used.
> Kencc is a very "dumb" compiler that does little magic
This could (partially?) address things though. If Plan 9 always uses that compiler and if there's no interest in implementing the optimizations Boehm describes then that would obviate 2 of the 3 correctness issues Boehm describes (though that would also imply that Plan 9 uses its own dialect of C with its own set(s) of guarantees, both explicit and implied). The only thing I'm uncertain about is to what extent hardware reordering would be relevant; I thought I remember reading about that making things interesting, but I don't remember off the top of my head to what extent.
To quote Rob Pike,
"Early 1990s: Alef (Phil Winterbottom) grafted the concurrency and communications model of Newsqueak onto a more traditional compiled C-like language.
Problem: with C's memory model in a concurrent world, hard to know when to free items.
All the other languages in this talk are garbage-collected, which is essential to easy concurrent programming."
http://go-lang.cat-v.org/talks/slides/emerging-languages-cam...
which is precisely why they can get away with a library seemingly without having to specify or coordinate anything with the compiler.
Of course no doubt that (hopefully) C compilers used for compiling plan 9 things won't break it, but...that's then talking about plan9-C, not C99, at which point you're necessarily already in the bucket of "this library relies on extra guarantees that plan9 C makes, instead of it just being a regular C99 library".
> > so you'd need to know how Plan 9's compiler/language specifies atomics.
> It doesn't, as far as I can tell. Kencc is a very "dumb" compiler
...So it effectively quietly makes a bunch of quiet guarantees that C99 doesn't, on account of not making certain valid optimizations. Even if not explicit, that's still semantics outside of those C99 specifies, that code may be relying on.
Fact of the matter is, you cannot make a full atomics library that will necessarily work correctly on C99. As a simple example I came up with on the spot:
A fully-proper standards-compliant C99 compiler could notice that foo isn't used until the `do_thing` call, and as such reorder the `foo = 2;` to after the `fence_probably` call (yay for putting all operations on foo in the same place!).But, if atomics exist, `do_thing` could do a relaxed store of its given foo address to a global, and another thread could then do a relaxed load of that and dereference that to get foo's value. And if the compiler decides to reorder the store to `foo` to after the fence (as a C99 compiler can), the program now has a data race that it otherwise would not.
(hell, a C99 compiler could even insert `int tmp = something; something = 123; something = tmp;` for any variable or global or field `int something`, in any place in the code (as long as `something` is known to be mutable), which'd break basically every multithreaded thing; this effectively one of the things the "Threads Cannot Be Implemented As a Library" paper mentions)
In what concerns C itself, as the language authors, they did not care that much about the standard, hence why it is mostly C89 with Rob Pike ideas on how header files should be organised, and other Plan 9 specific idioms.
What software do you write?
I'm not critical about languages evolving.. just the tech changes so quickly that perhaps standardising it doesn't make any sense any more.
I wonder if languages like RUST or GO will ever become ISO standards.. I do believe all these new platforms won't even get close to that need.
> I wonder if languages like RUST or GO will ever become ISO standards..
I doubt it, but mostly because everyone I know on the ISO standardization committees have expressed frustration with ISO (the organization) in ways that I haven't seen for other standards bodies like Ecma.
Haven't travelled much around Europe in the last decades, or had to deal with electricity projects, I gather.
Nowadays, due to the ways of ISO most languages end up going with ECMA, as ISO processes are considered somehow Jurassic.
However a resurgence in cold war like state of the world might indeed, bring us back to ISO as main driver for standardising languages instead of ECMA or open source, due to the way it ensures specifications and implementations aren't tainted by foreign politics.
Spoken like someone who's never felt the frustration of having a grounded plug and the only available sockets being two-prong.
I don't really mind though as it seems the far away real future is something like everything RISC-V assembly, "C" would only be the binary specification.
And some python/javascript/etc interpreters written in RISC-V assembly themselves, because sometimes my code can be slow and non-rigorous.
Well, considering that gcc doesn't support the FENV_ACCESS pragma, and as such all fesetround calls are always UB, it is valid for it to act as if the rounding mode never changes from the default :)
“ISO C language standard requires that C compilers support the following three pragmas, and some C++ compiler vendors support them, to varying degrees, in their C++ frontends:
where arg is either ON, OFF, or DEFAULT.”⇒ it seems C compilers must support this (but C++ compilers need not)
At least some GCC devs claimed that the standard does not impose any requirements on these macros if a compiler does not claim strict ISO/IEC 6055 (aka IEE 754) conformance (which is still optional, and gcc and clang do not claim it).
Other devs disagree.
But it does kind of support the whole "GCC C is not standard C" point.
That said, I was surprised that it's mandatory by the standard (and wasn't made optional later like VLAs); feels rather unlike C to require such precise control over something it doesn't even specify any behavior of - imagine if we had mandatory macros for integer overflow behavior and whatnot!
...That "that said" said, C11 does also contain:
> Each of the macros FE_DOWNWARD FE_TONEAREST FE_TOWARDZERO FE_UPWARD is defined if and only if the implementation supports getting and setting the represented rounding direction by means of the fegetround and fesetround functions.
so presumably one could just not define those and get away with not implementing (at least the rounding mode part of) fenv.h, properly allowing the `#define FLT_ROUNDS 1` hack? gcc+glibc does define those, though.
Whether a compiler implements the standard is up to the compiler developers and maybe its users.
In an era where code portability was of utmost importance, maybe this made a lot of sense.
But now we can pretty much treat a compiler and its libraries the standard for the language, like Rust does.