A bunch of stuff I used to not know about K&R C

birdculture1 pts0 comments

a bunch of stuff i used to not know about K&R C

a bunch of stuff i used to not know about K&R C

2026-07-12

ok so i've been reading the "specification" from "the c programming language" (1st edition). alongside that, i've also been reading the c89 rationale document, and the source code for the 7th edition unix c compiler. i've also been reading some other related stuff.

and i didn't really know anything about pre-ansi c until i did this research, but i found a bunch of stuff, so i'm documenting my findings here :3

wait hang on real quick before i get started here's a thing i thought about

so, c's grammar is context-sensitive and ambiguous and Bad; we already know this. but in the context of c's development, it kinda makes sense actually. well, ok, it doesn't make "sense", but it's more justifiable.

the original c compiler was a single-pass compiler. that was one of c's things actually, that it was even possible to write a single-pass compiler for it -- that was a necessity for a heavily memory-constrainted system like the ones they were working with. and if you're writing a single-pass compiler, then, by necessity, you're already keeping track of scopes and identifiers and stuff while parsing. so the fact that the grammar changes depending on whether or not an identifier refers to a typedef really doesn't matter, since it's not much any extra work to just check; you'll need to check at some point anyway.

in modern c, this is really hard to handle correctly because of its fucked up scoping rules, particularly that you can have a discontinuous scope if you have a nested function declarator in the prototype of a function definition. but in k&r c, this is a non-issue, since function prototype scope isn't a thing at all!

so, like, don't get me wrong, c's grammar is awful and bad and not good. and the context-sensitivity was a mistake. but, i can understand where it came from, and how it did the job well in a historical context.

anyway here's the actually interesting stuff:

void is an ansi invention, apparently

maybe this is common knowledge, but it surprised me. i guess there wasn't really a need for it back then: functions implicitly returned int, but if you didn't want to return anything meaningful, you could just not use a return statement, or use return without an expression. and all pointers were assignable to all other pointers, so char * could be used for "generic" pointers

different floating point types

there was no long double, however, there was long float, which was a synonym for double. i'm really curious what the history/rationale of this was. long double makes sense because it avoids adding a new keyword, and long float would make sense if double wasn't a keyword, but the fact that both can be used interchangeably is weird to me

some more interesting stuff with type specifiers

there was no signed specifier; only unsigned. furthermore, there were much fewer valid combinations of type specifiers: unsigned, short, and long acted as "adjectives" to be used alongside int (or float in the case of long float).

so, long wasn't valid, but long int was. the order didn't matter, so int long was also valid.

likewise, unsigned int was a thing, but unsigned wasn't, and neither was unsigned char.

there could also be only one adjective, so, for example, there was no unsigned short int (some later compilers implemented other unsigned types as extensions; more on that in a bit).

so like, the idea was that you'd almost always use signed integers, except that char was either signed or unsigned depending on what was most efficient for the target, and int could also be made unsigned. this surprised me.

different behavior for integer promotion

speaking of unsigned: so, the dialect of c documented in "the c programming language" only allows the unsigned specifier alongside int. but later compilers allowed unsigned short int and unsigned long int (and possibly unsigned char?) as extensions.

but the thing is, since there wasn't a standard, no one could agree on how integer promotion should work with unsigned integers. this wasn't documented in k&r (since it wasn't relevant), so two diverging practices emerged: "unsigned preserving" and "value preserving".

so like, assume sizeof(short int) . now, what's the result of the expression -1 ?

"unsigned preserving" implementations would say the result is 0 (false), since the operands are promoted to unsigned int. "value preserving" implementations would say the result is 1, since all possible unsigned short values fit within (signed) int, so it's made signed.

honestly, i'm surprised (but glad) that the standard ended up taking a hard stance here, rather than just making the behavior implementation defined. the standardized behavior was the "value preserving" behavior. the rationale has this to say:

The unsigned preserving rules greatly increase the number of situations where unsigned int confronts signed int to yield a questionably signed result,...

unsigned long stuff wasn signed since

Related Articles