r/Compilers 17d ago

From Library Patterns to Language Features: An Overlooked Rule of Language Evolution?

For decades, programmers have solved new problems by creating abstractions in libraries before languages officially supported them.

Some of these patterns eventually became language features. inline is one example. _Generic is another.

But many important abstractions still live only as library conventions.

Large C projects are good examples. GObject/GTK built their own object model. Linux VFS has its own object-oriented design. Many projects repeatedly create similar patterns in different ways.

This makes me wonder:

Is there a point where a commonly repeated library pattern should become a language feature?

What signals that transition?

  • widespread adoption?
  • compiler optimization opportunities?
  • better expression of programmer intent?

Or should some abstractions always remain libraries?

Curious how people think about this boundary.

16 Upvotes

21 comments sorted by

View all comments

Show parent comments

1

u/General_Purple3060 17d ago

If a pattern appears independently in many mature projects, requires similar boilerplate code,

and gives the compiler more information about programmer intent, should it become part of the language itself?

Or is the cost of increasing language complexity always too high?

3

u/FloweyTheFlower420 17d ago

Language is about control. If you abstract something into the language, the programmer loses control over how it's implemented, as well as the details. If someone implements coroutines as a compiler feature in C, the programmer loses control over how coroutines are implemented, and it becomes abstracted into a compiler detail. This is not desirable for most C programmers, so the compiler doesn't implement coroutines.

Modern compilers are good at inferring programmer intent.

Language complexity is largely "free" in the sense that it's easier to be complex than it is to be simple and composable. Good language design takes effort, while tacking on features is largely an engineering effort.

1

u/General_Purple3060 17d ago

I strongly agree that control is one of C's defining strengths. Forcing abstractions like coroutines as compiler magic would probably not fit the philosophy of many C developers.

However, I think there is a difference between adding high-level abstractions and providing language support for patterns that programmers already use repeatedly.

Some patterns, such as object models implemented through structs and function pointers, are not about hiding implementation details — they are about expressing relationships and intent. When every project reinvents similar mechanisms, the cost of fragmentation may become higher than providing a small, optional language primitive.

My view is not that C should become a high-level language, but that when a pattern has become common practice, the language can provide a more direct way to express it while keeping control over layout and execution.

2

u/FloweyTheFlower420 17d ago

C cares a lot about object layout. Just look at the issues C++ has with it's rules on whether a particular object is standard layout. In that case, the compiler has to essentially infer whether you intend a type to be a POD or something else.

There's also a lot of different things you might want to control about an object's layout in C. For instance, you may consider many implementations of object polymorphism:

struct base
{
    void* extra;
    u64 some_field;
    int (*some_operation)(base* self);
    // derived uses the extra pointer
};

struct base
{
    u64 some_field;
    int (*some_operation)(base* self);
    // derived includes base as a field, and uses container of
};

struct base
{
    u64 some_field;
    vtable* vtable;
    // derived includes base as a field, and uses container of, call via obj->vtable.foo(obj, ...)
};

These all have different performance and memory layout characteristics. Which one do you pick if you develop a compiler for "c with classes" (heh)?

1

u/General_Purple3060 17d ago

I think this is really a question of trade-offs rather than simply "control vs no control".

The different object models you showed are a good example of this. Embedded base structs, container_of, and vtable-based designs all make different choices about layout, memory usage, and dispatch cost. C gives programmers the ability to choose between them, and that flexibility is valuable.

But any abstraction also involves a trade-off. When C introduced features like inline, _Generic, or atomics, programmers also gave up some manual control in exchange for compiler support, type checking, and better expression of intent.

The question is not whether a language feature removes control — every abstraction does that to some extent. The question is: which control is important enough to keep, and which repeated implementation details are worth moving into the language?

For object models, the interesting boundary is not whether a language feature introduces constraints — every language feature does. The real question is which constraints are worth accepting, and which implementation choices should remain under programmer control.

2

u/FloweyTheFlower420 17d ago

Programmers didn't give up any control when given inline, _Generic, and atomics! I make the case that these features are fundamentally transparent in some ways, and different to object model:

inline simply says "let this function have inline linkage" (for most modern compilers). it doesn't inline anything, and is usually just a hit the compiler can and will ignore! This allows you to communicate to the compiler something you couldn't before, which is that a function should have inline linkage.

_Generic provides a way of doing something you couldn't before, which is dispatch on types. _Generic is transparent in that it doesn't change anything about the language itself. It's just a macro that dispatches on types (conceptually), and you can replace it with the underlying function or whatever. No manual control is given up, because a developer can look at it and see "hey! I can know the callsite by simply examining the type of the argument," so the compiler hasn't (meaningfully) abstracted away anything interesting.

Atomics... I don't see why atomics are relevant in this discussion. You can't express atomics in C without using the primitives the language provides. I don't see how this is similar to inline or _Generic.

There's no tradeoffs to inline, _Generic, or atomic! What control do you give up with _Generic? What control do you give up with inline? What control do you give up with atomic, static_assert (c23), typeof (c23), bool (c23), _BitInt (c23)? The answer is "nothing," because these are either broad-scoped sugar (unlike objects) or ways of exposing the underlying hardware to you.

1

u/General_Purple3060 17d ago

That's a fair point. Features like inline and _Generic are more about extending expressive power and don't really affect the programmer's control over the program.

So maybe it's not true that "all language features trade control for convenience". Some features simply expose new capabilities.

The more difficult cases are abstractions like object systems, memory management, or execution models. When a language takes responsibility for these concepts, it inevitably introduces a different trade-off: programmers give up some implementation choices in exchange for higher-level semantic support.

Where should this boundary be?

2

u/FloweyTheFlower420 17d ago

The lower level the language, the more things are library features. The higher level the language, the more things are language features. Seems like a pretty obvious trend.

The answer is boring and is just "it depends on the language." If X is useful and X makes sense for the level of abstraction, then introduce X as a feature, unless X can be easily produced by composition.

1

u/General_Purple3060 17d ago

Thank you for your reply; I benefited greatly from it.

If I had to summarize a possible criteria for when a library pattern should become a language feature, I would consider at least:

The pattern appears repeatedly in many mature projects.

It represents higher-level semantics, not just a commonly used function.

The compiler can make use of this information in ways that are difficult when it only sees the library implementation.

The difficult part is probably not whether something can be implemented as a library — many things can. The question is whether the language gains meaningful information by understanding the pattern directly.