8
$\begingroup$

C89 had a requirement that all declarations must appear at the top of the scope before any statements:

int func(void) {
    int x = 10;
    f();
    int y = 20; // Invalid
    // ...
}

However this requirement was easy to get around most of the time, either by simply moving declarations to the top, and initializing them out of line if their initializer expressions were order-sensitive to the code in the statements, or one could just put a scope around them:

int func(void) {
    int x = 10;
    f();
    {
        int y = 20; // Valid
        // ...
    }
}

Given that the C99 requirement to allow mixed declarations did not cause any problems (that I am aware of, as in this could not have possibly broken legacy code, etc.) and that identical semantics could be easily achieved by using the {} hack above, what prevented old C89 compilers from handling mixed declarations?

$\endgroup$

4 Answers 4

16
$\begingroup$

One pass compilation

C was originally (and, as far as I know, still is) designed to allow for one pass compilation. This means you tokenize, parse, and output, all in one pass, never building an intermediate structure like an ΑST. As you can imagine, this is quite good on memory usage, although a fair bit more complex than building an AST. This was a reasonable compromise back when memory was expensive.

Therefore, if one restricts all variable declarations to the top of the scope, one knows how big the stack should be for the whole scope. You never have to re-adjust the stack in the middle of a scope [1]; you only extend it at the start of the scope, and unextend it at the end of the scope. Of course, one can still allow variable decls in the middle of a scope while maintaining one-pass compilation support - it's just more complex. By forcing you to declare all your variables at the start of a scope, it makes it a fair bit easier to figure out those stack transforms.

[1]: Remembering that if you're doing one pass compilation, you can't use lookahead or AST traversal to figure out all the variables in a scope and transform the stack at the start of the scope accounting for all the variables.

$\endgroup$
9
  • 1
    $\begingroup$ you could use back patching (like you would need to do for some structured control flow) to adjust the stack size though $\endgroup$ Nov 7, 2023 at 11:43
  • 1
    $\begingroup$ You could just treat a declaration in the middle of a block as if it started a nested block that extends to the end of the original block. $\endgroup$
    – Barmar
    Nov 7, 2023 at 16:25
  • 1
    $\begingroup$ @Barmar: That would yield semantics inconsistent with the C Standard if both a goto label and a forward goto statement appear above the declaration, and if code which branches to the first label relies upon the declared object's value being indisturbed after execution reaches the target of the forward goto. $\endgroup$
    – supercat
    Nov 7, 2023 at 18:40
  • 1
    $\begingroup$ @ratchetfreak: I would think even something like an 8080-based Altair with a paper tape reader and punch could probably have supported a very practical development system using a purpose-designed high-level language that would allow programs to be processed with one pass through the source tape, yielding a destination tape that could be loaded directly, applying back patches at load time. I wonder if any dev tools for such systems were designed that way. $\endgroup$
    – supercat
    Nov 7, 2023 at 18:43
  • 1
    $\begingroup$ Except it doesn't work. You can have a function call initializer. $\endgroup$
    – Joshua
    Nov 7, 2023 at 22:36
3
$\begingroup$

In languages which require that all variable declarations within a code block start with a recognizable reserved word, and requires that initialization expressions be compile-time constants, a compiler could generate code for a function which sets up a stack frame and initializes any objects with constant values before generating any other code for that funcion. Such code would thus be usable even on single-pass build systems that need to produce a code image directly. If a compiler is targeting a three-pass assembler, it might be able to do something like:

_Foo:
  push  bp
  mov   bp,sp
  sub   sp,ssize_Foo

and define the size later on in the code, and such a design might work on a two-pass assembler if the target platform would only have one operand size mode for the SUB or if the compiler always used the largest one, but letting the compiler choose among subtract-byte, subtract-word, or don't-subtract-anything operations before outputting code would make things more efficient.

The C Standard ended up mandating most of the complexity necessary to accommodate mid-block declarations by allowing initialization expressions to contain executable code, and one might reasonably argue that once initialization expressions could contain code there was no longer much advantage to forbidding mid-block declarations. Requiring that declarations precede the first branch target within a function still had value, however, which got lost in C99. Given something like:

int test(int x)
{
   int arr[x];
   label1:
     ... do some stuff
     goto label3:
   label2:
     ...

a compiler would have no way of knowing whether the lifetime of anything which is declared after label2 might extend into code between label1 and label2 until it finished parsing the entire function, and would thus not be able to determine the offset of arr until then.

IMHO, many of the purposes that would be served by mid-block declarations could be better served by a "temporary declaration" syntax, which would define things until the next "end temporary scope" directive which would behave independently from normal block scope. If one wants to compute e.g. (x2-x1)*(x2-x1)+(y2-y1)*(y2-y1), being able to declare temporary objects to hold x2-x1 and y2-y1 whose lifetime ends before whatever was being initialized with that expression would be better than having such declarations always extend through the end of the block.

$\endgroup$
0
1
$\begingroup$

It makes stack management easier, because you do stack layout and then you do other things in the body, but you do not have to deal with changing the stack layout in the middle of a function. This sounds extremely stupid, but it makes code really easy. It's kind of hard to understand these days if you build a language yourself, because you just dump everything into LLVM and can expect it to generate good code for you. If you have to do everything yourself and if writing code is a lot more time consuming, because there weren't exactly any good tools available back then, strange simplifications like this are actually plausible.

$\endgroup$
3
  • 2
    $\begingroup$ char * a = malloc(4096); size_t b = myfunction(a); It's like there's a ghost rule that says can't call functions in initializers. $\endgroup$
    – Joshua
    Nov 7, 2023 at 22:38
  • $\begingroup$ You can execute any code in a declaration. $\endgroup$
    – gnasher729
    Nov 8, 2023 at 16:55
  • 1
    $\begingroup$ @gnasher729: Some early compilers didn't allow that, and such non-allowance simplified compiler design. Allowing executable code in initialization expressions but not allowing other statements required that compiler writers do 90% of the work necessary to support more general placements, while offering programmers less than 90% of the benefit. $\endgroup$
    – supercat
    Nov 8, 2023 at 18:32
0
$\begingroup$

There is a syntax for declaration, and a syntax for statement. In old C versions the contents of a block was zero or more declarations, followed by zero or more statements. In newer C versions it’s zero or more declarations or statements. That’s the whole difference.

Forcing declarations first doesn’t make anything easier. Consider that in T var = expression the expression can be a comma expression made of 100 subexpressions, can contain function calls, and be arbitrary complicated. With the ?: operator and recursive calls I wouldn’t be surprised if you can replace any statements other than perhaps goto with declarations. So a compiler for old C must handle anything a compiler for new C can handle.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .