The very first C compilers ran on a PDP-11 in just a few dozen kilobytes of memory. The entire emphasis was on minimalism, and that meant that things like type enforcement was left to the human.
One of the things the earliest language was missing was the "void" type. If you didn't give a return type to a function, it just defaulted to returning "int". Therefore it was totally normal to have a function fail to return anything.
Since there was no distinction between "function returning int" and "function returning nothing", there was nothing stopping your program from using the value returned... it just meant you got whatever happened to be in the right register.
What does it mean for us today? Basically nothing. "void" was added 30+ years ago when ANSI C appeared. The language couldn't just break the old behavior but in any rational environment you enable enough compiler warnings to avoid these ancient quirks entirely:
$ gcc -Wall -c a.c
a.c:1:1: warning: return type defaults to ‘int’ [-Wimplicit-int]
1 | foo() {}
| ^~~
a.c: In function ‘foo’:
a.c:1:8: warning: control reaches end of non-void function [-Wreturn-type]
1 | foo() {}
| ^This particular quirk is a few years older than C itself, actually! C was based on a similar language called B. Now B was originally written for PDP-7 and similar machines, which could only address words in memory, not individual bytes. So it was designed "typeless" - the only and always-implicit type in that language is the machine word, so e.g. (a+b) always adds two ints, and (*a) always dereferences a pointer. Otherwise, the syntax was very similar to C:
max(a, b) {
if (a > b)
return a;
else
return b;
}
swap(a, b) {
auto c = *a;
*a = *b;
*b = c;
}
main() {
auto a, b;
swap(&a, &b);
}
Since all functions took zero or more words as arguments, and returned a word, there was no need for function prototypes, either, or for function pointer types - if you used () on something, it was treated as a function / function pointer with the corresponding number of parameters. Even labels / goto worked like that.When they got a PDP-11, which had byte-addressed memory, this simplistic approach no longer worked - they had to distinguish char/int and char*/int*. So C added types and pointers - but kept int as the default type, as well as the keyword "auto" (which is completely redundant in C if you specify the type), so that existing B code could be easily ported. For the same reason, they allowed calling functions without declaring them, assuming int as return type.
This is also presumably why pre-ANSI K&R C had that weird syntax for parameter types:
max(a, b)
int a;
int b;
{ ... }
If you omit the types, they all default to ints, and it becomes identical to the B declaration!That implicit int rule made it into the ANSI C89 / ISO C90 standard, since it was still common enough in K&R C code floating around at the time, and they were trying to not break things too much. It eventually got removed in C99, except that "short" and "long" still allow to omit the following "int". The useless "auto" is still around, though, although it might eventually get repurposed the same way it was in C++.
"C is quirky, flawed, and an enormous success."
But sure, I wouldn't recommend anyone choose C as a language for new software unless they have an extremely good reason, or if you really love stuff like implementing every data structure you use from scratch and using void* as a kind of wildly unsafe generic.
Unless you are doing firmware where all those constraints are still around, and you have to deal with whatever compiler the vendor supplies for their microcontroller, which will almost certainly be some variant of C.
But yeah doing so is being a special snowflake, still they are in business to this day.
As such, it produces code based on what you tell it, no more, no less. If you don't tell it what value to return from a subroutine, it does not generate the code to. It is quite common to have a function that does not return a value.
The case shown here is not a case of the compiler doing 'what it likes', but the compiler not emitting any code for it. The fact that the return value from the function called last is still in the register used to return values when the function returns is simply a result of no code that touches that register in between.
As someone else pointed out, if you look at the code generated for a PDP-11 all the quirky things like, pointers with pre or post increment operators make much more sense as they emit instructions that do just that.
Does the AX register exist in the caller? Yes.
Does it have a value? Yes.
Is its 'value' undefined. Yes.
Is there any undefined 'behaviour' of anything you told it to do? Not to me. Subtle maybe.