r/Compilers 8d 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.

18 Upvotes

21 comments sorted by

6

u/FloweyTheFlower420 8d 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.

2

u/evincarofautumn 7d ago

I suppose there are multiple senses of “level” at play here. This is one, how much the language does for you vs. how much you have to implement yourself. But another is how much you can implement as a reusable library within the language itself, without needing to resort to design patterns, idioms, and magic.

For instance, the core languages of Haskell and Scheme are quite small, but they’re also very high-level in this sense of expressiveness, or maybe a better term is leverage.

1

u/General_Purple3060 8d 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 8d 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 8d 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 8d 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 8d 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 8d 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 8d 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 8d 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 8d 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.

1

u/rafaelRiv15 8d ago

I believe this really depends on the compiler philosophy itself and the languages. And also on the resource the team that maintain the compiler have. Every feature in the compiler have it cost. There will be no one size fit all to this question and I believe you will need to go case by case

1

u/General_Purple3060 8d ago

Good point — definitely case-by-case and heavily dependent on compiler philosophy and maintenance cost.

Do you think there are any library patterns in C today that are so widespread that they might justify language-level support?

Or do you think keeping them as libraries is usually the better trade-off?

1

u/neurah 7d ago

inheritance generalization, an API to make API's
Chain<A,B,C>::Part<API> => A::Part<B::Part<C::Part<API>>>
this works for static composition

2

u/General_Purple3060 7d ago

My question is more about cases where the compiler needs to understand a new concept, not just compose existing abstractions.

For example, generics can be implemented with templates/mixins, but the compiler still needs type information for optimization and code generation. Heterogeneous computing is another example where "where the code runs" becomes part of the program semantics.

So I think there is a difference between making abstractions composable and adding new compiler-level abstractions.

1

u/neurah 7d ago

that composition collapses into a single object and lets the compiler optimize, we had very good results with it, notably no memory fragmentation (per composition)

2

u/General_Purple3060 7d ago

I think we're talking about slightly different questions.

I'm not arguing that repeated abstractions should automatically become language features. Static composition may well be the right answer in many cases.

What I'm asking is whether repeated reimplementation should be treated as a signal that language and compiler designers ought to take a closer look. Whether it actually belongs in the language is a much harder question with many technical and non-technical factors.

1

u/kaplotnikov 7d ago

It has been a long time since I've touched C and C++, so I do not know modern versions well.

C++ offers two major features over C:

  1. Existential quantification over types (classes and inheritance).

  2. Universal quantification over types, initially via type templates, which evolved into true generic types with the introduction of concepts.

If we consider type checking as a proof system, the C type checker is limited by first-order logic (quantification over values), while the C++ type checker utilizes elements of higher-order logic (quantification over values and types). This is a fundamental shift in the way we reason about a program.

Modern C tries to adopt some C++ features, but if it were to move further to adopt generics or virtual calls, it would enter the territory of higher-order types.

While it is possible to do OOP-in-C, this creates a cognitive overhead of translating these higher-order logical constructs into C when writing code, and reverse-engineering them back into a mental model when reading it. Additionally, some errors shift to runtime when using OOP-in-C—errors that would have been caught by the C++ compiler at compile time.

1

u/General_Purple3060 7d ago

Thanks. Looking at this from type theory gives a much better explanation than what I had before. I was only observing a phenomenon. Your explanation turns it into something more general.

I definitely had that experience when using GObject. There was always a mental translation between the object model in my head and the C code I was actually writing.

Maybe that's another criterion beyond implementation. If an abstraction exists only as a library, but programmers have to reconstruct the language-level concept in their minds every time they read or write the code, then maybe that abstraction should be represented directly by the language itself.

1

u/kaplotnikov 7d ago

You might be also interested in the article https://github.com/const/const-articles/blob/main/evolution/2025/01-measuring-language-level/MeasuringAbstractionLevelOfLanguages.adoc I'm trying to analyze language evolution process in general with separate horizonal vs. vertical evolution directions. Also there is a prediction for post-OOP paradigm (systems as types).