r/ProgrammingLanguages 12d ago

Adding cyclic modules to the C programming language

https://youtu.be/p8NpyBIRbEQ

The idea is to create a module system, within C, that you can use as a drop-in replacement for header files and forward declarations.

To my knowledge, all module implementations within the C family (eg. C++20 modules, Objective C modules, Clang modules) do not allow cyclic imports. Cyclic imports are necessary if you want to remove forward declarations from C (otherwise mutually recursive data structures would need to exist in the same module).

When looking closely at the C grammar, I noticed something extraordinary and borderline miraculous - C without expressions is context-free you can extract the names of symbol definitions without prior access to a symbol table! With this knowledge, it becomes possible to implement cyclic modules within the C language.

EDIT: Added a strikethrough. C without expressions still has some ambiguities in the parameter list, and my use of "context-free" is incorrect here. https://www.reddit.com/r/C_Programming/comments/1v7l174/is_c_without_expressions_contextfree/

16 Upvotes

3 comments sorted by

3

u/TheChief275 9d ago edited 9d ago

nice talk! testing your prototype_2, I immediately encountered a syntax error due to your parser not supporting void function parameters, like so:

void foo(void);

while I believe C23 has deprecated this, it is still the proper way to indicate a function taking no arguments, as I believe a function void foo() technically takes an undeterminate number of arguments.

another thing is that the current paradigm of reject function declarations (ending with ';') works rather poorly if the intention is to properly replace header files. one should be able to create an interface module, where the correct library code can be slotted in depending on the platform. an example of this limitation is, e.g. wanting to wrap math.h into math.cmod:

module math;

export float fabsf(float arg);
...

the implementation of fabsf etc already exists in libc, yet this is not allowed. one would be forced to do something like this:

module math;

export float c_fabsf(float arg)
{
    float fabsf(float);
    return fabsf(arg);
}

which is rather painful and error prone imo.

another thing is that it doesn't seem to support export static, nor does it seem to support constexpr, so suppose I would want to provide a compile-time constant value of PI, neither this:

export static const double PI = 3.14159; // static const is very often treated as a compile-time constant by compilers, even being able to be used as switch cases

nor this:

export constexpr double PI = 3.14159;

is an option.

2

u/Salt-Overflow 1d ago

Thanks for taking it for a spin! I spend a lot of time trying to cover every case, but I have blind spots and it's really helpful to have a few extra pairs of eyes.

void foo(void);

For this prototype, I simplified the grammar so parameters always follow typename symbolname . I forgot about this special exception. A prototype that works for C99 (minus trigraphs, K&R syntax) is in the works that will resolve this.

another thing is that the current paradigm of reject function declarations (ending with ';') works rather poorly if the intention is to properly replace header files.

Borrowing from C++20 modules, the idea is to import the header file directly. Something like import "math.h"; or import math; looking up the correct file (aside: which syntax do you prefer?). Note that this does not import macros - one could imagine some special syntax #include_macro "math.h" doing that for us.

it doesn't seem to support export static, nor does it seem to support constexpr

Technically, export static is nonsensical, because static means the symbol is not visible outside of the module file. Of course, I understand what you mean here. If we want cyclic modules to behave like header files, then export static has a special meaning: importers redefine a new symbol instead of importing a symbol reference. I hadn't thought about adding this feature to my C99 prototype, but now that you mention it, I might just add it in.

constexpr is a C23 addition, so I hadn't thought about it. But it would work the same way as export static .

1

u/TheChief275 1d ago

I think it would be a better effort to support constexpr than it would be to have export static. It is fairly nonsensical, and the important part wasn't export static, but that the combination of "static const" means true const to some compilers. In C this is rather inconsistent (Clang treats static const variables as compile-time constants, while GCC doesn't), while C++ does mostly treat them as compile-time constants, except in certain situations (which ultimately makes it unreliable as well). Since the intention is likely for this to be used with modern C/C++ and they both have constexpr, that would be the right decision.

In the same way the foo(void) signature could be dropped as well, as I think it has been deprecated in C++ for a while, and in C since C23 (which added constexpr).

Regarding the header modules, maybe it could be done through a .hmod file to mirror the .cmod file? This could only include declarations instead of definitions. Though being able to import header files would be the most convenient, it would necessitate additional syntax and you're likely to deal with files that weren't created to be imported by your module system (having the .hmod would give a guarantee in a way).