digitalmars.com                        
Last update Thu Jan 9 18:31:36 2025

Obvious Things C Should Do

January 8, 2025

written by Walter Bright

Standard C undergoes regular improvements, now at C23. But there are baffling things that have not been fixed at all. The Dlang community embedded a C compiler in the D programming language compiler so it could compile C. This C compiler (aka ImportC) was built from scratch. It provided the opportunity to use modern compiler technology to fix those shortcomings. Why doesn’t Standard C fix them?

Evaluating constant-expression

Consider the following C code:

int sum(int a, int b) { return a + b; }

enum E { A = 3, B = 4, C = sum(5, 6) };

When compiled with gcc:

gcc -c test.c
test.c:3:20: error: enumerator value for C is not an integer constant
 enum E { A = 3, B, C = sum(5, 6) };
                    ^
In other words, while C can compute at compile time a simple expression by constant folding, it cannot execute a function at compile time. But ImportC can.

Everywhere a C constant-expression appears in the C grammar the compiler should be able to execute functions at compile time, too, as long as the functions do not do things like I/O, access mutable global variables, make system calls, etc.

Compile Time Unit Testing

Once the C compiler can do compile time function evaluation (CTFE), suddenly other things become possible.

For example, ever notice that seeing unit tests in C code is (unfortunately) rather rare? The reason is simple - unittests require a separate target in the build system, and must be built and run as a separate executable. Being a bit of a nuisance means it just does not happen. (Maybe you are one of those people who get up and jog a mile every morning, and you probably also carefully write unit test setups! I know you exist out there - somewhere!)

int sum(int a, int b) { return a + b; }

_Static_assert(sum(3, 4) == 7, "test #1");
gcc -c test.c
test.c:3:16: error: expression in static assertion is not constant
_Static_assert(sum(3, 4) == 7, "test #1");
               ^

ImportC can compile it.

This enables unit tests of functions that can be run at compile time. No separate build is required. No extra work is required. The unit tests run every time the code is compiled. I use this extensively in the test suite for ImportC.

Forward Referencing of Declarations

More code:

int floo(int a, char *s) { return dex(s, a); }

char dex(char *s, int i) { return s[i]; }
gcc -c test.c
test.c:4:6: error: conflicting types for dex
 char dex(char *s, int i) { return s[i]; }
      ^
test.c:2:35: note: previous implicit declaration of dex was here
 int floo(int a, char *s) { return dex(s, a); }

If the order of floo and dex are reversed, it compiles fine. I.e. the compiler only knows about what lexically precedes it. Forward references are not allowed. Isn’t this stone age compiler design? Modern languages don’t have this problem, why does it persist in C and C++? ImportC is not a modern language, but it is a modern compiler and accepts arbitrary orders of the global declarations.

Why does this matter? It usually means that every forward definition needs an extra declaration:

char dex(char *s, int i); // declaration

int floo(int a, char *s) { return dex(s, a); }

char dex(char *s, int i) { return s[i]; } // definition

It’s just purposeless busywork to do that. Not only is it a nuisance, it drives programmers to lay out the declarations backwards. The leaf functions come first, and the global interface functions are last. It’s like reading a newspaper article from the bottom up. It makes no sense.

ImportC can compile the declarations in any order.

Importing Declarations

Given three files, floo.d, dex.h, dex.c:

// floo.c
#include "dex.h"
int floo(int a, char *s) { return dex(s, a); }
// dex.h
char dex(char *s, int i);
// dex.c
#include "dex.h"
char dex(char *s, int i) { return s[i]; }

Having to craft a .h file for each external module is a lot of busy work, right? Even worse, if the .h file turns out to not exactly match the .c file, you are in for a lot of time trying to figure out what went wrong.

What’s the answer? Importing dex.c!

// floo.c
__import dex;
int floo(int a, char *s) { return dexx(s, a); }
// dex.c
char dexx(char *s, int i) { return s[i]; }

No need to even write a .h file at all. Of course, this also works with ImportC.

References

Home | Runtime Library | IDDE Reference | STL | Search | Download | Forums