K&R C Default Argument Promotion Rules

In early K&R C implementations, before the introduction of function prototypes by the ANSI C standard, the compiler could not verify whether the arguments supplied at a function call matched the parameters expected by the function definition. To prevent mismatched calling conventions and simplify code generation, the compiler applied "default argument promotions" to every argument passed to a function. This process automatically widened smaller integer types to int and single-precision floating-point types to double, establishing a predictable standard for how data traveled across the call stack.

The Absence of Function Prototypes

In the original definition of C described by Brian Kernighan and Dennis Ritchie (K&R C), function declarations only specified the return type, omitting any parameter list:

int calculate(); /* Declaration specifies return type, but no parameters */

Because separate compilation units could call calculate without knowing the exact types of its formal parameters, the compiler needed a universal agreement between the caller and the callee. Without such an agreement, pushing a 1-byte char where a 2-byte or 4-byte int was expected could corrupt the stack or misalign CPU registers.

The Standard Promotion Rules

Whenever a function call occurred, the caller applied two primary promotion rules to the arguments:

  1. Integral Promotions: Any integral argument smaller than an int—specifically char and short, whether signed or unsigned—was automatically widened to an int. If an unsigned short could not fit within a signed int on architectures where they were the same size, it was promoted to unsigned int.
  2. Floating-Point Promotions: Any argument of type float was automatically promoted to double.

Additionally, array arguments decayed into pointers to their first elements, and function names decayed into function pointers.

Handling Arguments on the Callee Side

In K&R function definitions, parameters were declared using a two-step syntax: the parameter names appeared in the parenthetical list, followed by their type declarations before the opening brace:

int process_data(c, f)
char c;
float f;
{
    /* Function body */
}

Because of default argument promotions, the caller always passed an int for c and a double for f.

To accommodate this, the compiler adjusted the function prologue. Upon entry, the callee retrieved the widened values (int and double) from the stack or registers and implicitly cast them down to the declared local types (char and float) for use inside the function body.

Architectural Rationale

These promotion rules were chosen to reflect early hardware constraints and optimize compiler simplicity:

  • Word Alignment: Early minicomputers, such as the PDP-11 on which C was developed, handled operations on machine-word boundaries far more efficiently than sub-word operations. Pushing full machine words (int) avoided complex masking and shift operations during stack manipulation.
  • Floating-Point Hardware: Many early floating-point units executed computations using double-precision registers by default, making single-precision argument passing an unnecessary source of conversion overhead at the call site.
  • Variadic Functions: Functions that accept variable numbers of arguments, such as printf, relied entirely on these fixed promotions to parse trailing arguments safely using macros like va_arg.

Legacy in Modern C

When ANSI C (C89) introduced function prototypes, default argument promotions were made conditional. For prototyped functions, arguments are converted directly to the types declared by the parameter list. However, default argument promotions remain active in standard C today for two specific use cases: when calling functions that lack a prototype, and when passing arguments to the variable-argument portion (...) of a variadic function.