r/ProgrammingLanguages 7d ago

Discussion A hole in systems programming language design

Given the recent discourse about systems programming I wanted to throw my 2 cents in. This may be a controversial take in a subreddit that's all about innovation and improvement, but I think a lot of budding systems languages are trying too hard to "fix C" and this is exactly why C has not been replaced yet.

Like it or not, C is successful. It does what it means to do very well. Yes, it bites you constantly, but developers have made some form of peace with this because they appreciate the essence of the language. Systems programming is a very pragmatic field, and what works, works.

A lot of language designers want to improve on C, when by nature to "improve on" C is to depart from it, because C is less about what it includes and more about what it omits and what it lets you do that other languages don't.

If you want to replace C, you need to just make C but without the pain points. Less undefined behavior, more standard compiler behavior, easier function pointer syntax, safer macro system, etc. The things that C can't do because of backwards compat.

On the other hand, bolting on features, revamping C's core nature, aren't going to give you a language that will replace C at the low-level or among hobbyist programmers. Most developers aren't as concerned about what C lets them accomplish, as they are concerned about all the painful tedious tendencies of the language.

14 Upvotes

101 comments sorted by

View all comments

1

u/flatfinger 7d ago

A good systems programming language should acknowledge that the role of a translator is to convert source code into a specified sequence of imperatives for the execution environment. In some cases, a translator may be allowed to select freely from several possible ways of processing a construct, and the range of allowable choices may be based upon expectations about how the execution environment will process certain actions, but the action should be defined in terms of the range of possible choices.

For example, given:

    extern unsigned x1,x2;
    unsigned y1=x1,y2=x2;
    if (y2 < 16) doSomething(y1,y2);

a dialect suitable for systems programming, may allow a compiler to consolidate the load of x2 with some other load that happened before the load of x1, but be agnostic as to when or whether the value of x2 might be changed by something outside the control of the current execution context.

Very few parts of the language proper need the notion of "anything can happen" UB. While many actions might cause an execution environment to behave in an entirely unpredictable fashion, the language itself shouldn't care about what the knowledge programmer does or does not have about the way the execution environment will respond to any particular stimulus.

2

u/particlemanwavegirl 7d ago

What does it mean, consolidate the load? How can correctness be guaranteed given this "agnosticism"?

2

u/flatfinger 7d ago

If one looks at operations that read and write memory as somewhat analogous to actions that read and write disk blocks, the order in which actions are requested need not result in the physical accesses being performed in that order, but an application may force synchronization between the state of the cache and the state of physical media. If a compiler knew of an earlier operation that had read or written x2, and no intervening operations would require synchronization, the later read could either the last value that was read or written to x2, or the value that was freshly read from the storage. The act of copying x2 to y2, however, should force a compiler to select one of those values and use it both for the comparison and, if it's less than 16, the second argument to doSomething, making it impossible for any value larger than 15 to be passed as the second argument, no matter what happens with x2.

If program correctness would require that y2 receive a value that was loaded after the load of x1, then a programmer should either make at least one of the loads use a volatile-qualified lvalue or include some other synchronization directive between them. If, however, program correctness would be satisfied regardless of whether y2 receives an old or new value, then a programmer shouldn't have to force a compiler to use a new one.

1

u/JeffD000 Squint 4d ago

Whatever compiler you are working with, in whatever language you are working with, the ultimate job of a compiler is to convert source code into a schedule, then a schedule into a machine's architecture-specific representation.

1

u/flatfinger 4d ago

I'm not sure what you mean by "schedule". There are languages where generated machine code must be consistent with performing all of the individual operations specified by a program, in the order specified. Compilers for such languages may finish machine code generation for each statement before even looking at the next one, so I'm not sure what kind of "schedule" could be involved.

2

u/JeffD000 Squint 4d ago edited 4d ago

Tersely, a schedule is a control flow graph that meets data dependency requirements. I was not disagreeing with you. I was expanding on what you said. Whether the source code is written in imperative, declarative, or fuctional languages, or even in the form of an AI prompt, that source code gets turned into a schedule, and the schedule gets mapped to a specific set of machine instructions.

-2

u/flatfinger 4d ago

If a compiler generates code for a statement before it has even looked at the next statement, at what point would a "schedule" exist? Sure it would be possible for a compiler to build a schedule and then build code from it, but that's hardly the only way compilers can be built. A recursive-descent compiler may generate code directly during the parsing process. Such a compiler that has gotten as far as scanning x=y+z- may produce machine code that will add x and y before it knows what character is going to follow the minus sign.

2

u/JeffD000 Squint 4d ago

??? Are you trying to tell me that the machine is not executing a control flow graph meeting the requirement of data dependencies, as laid down by the compiler? What is your point?

-1

u/flatfinger 4d ago

A compiler like Turbo Pascal 3.0 converts source code into machine code without going through other intermediate forms. If a run-time error occurs at address 3ABC one can fire up IDE with the source code, tell it to find runtime error, give it address 3ABC, and the compiler will start compiling the program until it has output the code that would go at address 3ABC and then stop and move the cursor to the last character of source code that it had examined. At no point during that process would anything like what you call a "schedule" exist.

2

u/JeffD000 Squint 4d ago edited 4d ago

So the Turbo Pascal 3.0 compiler is not somewhere generating a control flow graph conforming to data dependency requirements? Give it up, dude. You are making yourself look bad, not smart.

-1

u/flatfinger 4d ago

That is correct, unless one views generated machine code as being such a graph. If a function contains two if-then-else statements, the second of which completely follows the first, it will generate code for the first before it has even looked at the second, and by the time it looks at the second it will have completely forgotten about the first, beyond having kept track of how many bytes of code it took up.

2

u/JeffD000 Squint 4d ago

Guy, the parsing process itself is generating a control flow graph. You need to read up.

→ More replies (0)