News for hackers. Stuff that matters.
Re:The Problem with Friendly C by zAy0LfpBZLC8mAC on Thursday December 24, 2015 @08:53PM (#10790164)
> You want the compiler to error out until you tell it whether or not you want a check. Or rather, whether you want checked-division semantics—which will be free on some architectures and costly on others—or unchecked-division semantics [...]

I think I disagree. I don't want that to be a feature of the compiler, but of the language, in which case there is no way to "error out". That is to say: there should be operators for one kind of semantics and operators for another kind (it's a bad idea to overload the same operator on the same operands with different semantics). And if the language doesn't have that feature, the compiler shouldn't error out--unless it can prove that your code will unavoidably run into undefined behaviour, in which case, simply not producing a binary at all is perfectly within the range of acceptable results, and probably a good idea.

> [...] you then have to code for both the case where your function can now trap/throw an exception, and the case where control continues through your function using the invalid calculated intermediate as input for further operations. You've probably coded to handle one or the other—but it's very unlikely you've coded to handle both. And why would you want to have to?

If you think in terms of handling the results of undefined behavior, you are kindof doing it backwards. The language specifies certain preconditions that you have to meet in order for some operation to be defined. It's your job as a programmer to make sure that those preconditions are met, in which case it doesn't matter what some compiler does when they are not met.

> The compiler should ensure that your code has the semantics you want everywhere—whether those be "always do X" or "do whatever the target arch's closest approximation to X is."

Well, yes and no. Giving you the same semantics certainly is a good idea, where possible. But to "do whatever the target arch's closest approximation to X is." is a useless idea, as you can not write any reliable code on that basis. A language always has to specify certain properties that an operation needs to have, and every compiler needs to choose an implementation that fulfill those requirements, not just some vague "as close as possible"--that way, if you write code that only relies on those guaranteed properties, it will compile and run correctly on all targets. The question is only how many details are guaranteed properties and which parts are up to the implementation, aka "undefined behaviour", aka "stuff you must not rely on in your code". It's kindof impossible to really guarantee "the same semantics"--however, there are some guarantees that would be useful for a lot of code and that C, unfortunately, does not provide.

> What we call "undefined behavior" in C is actually implemented by C compilers as a very specific set of assumptions about what semantics developers prefer—and those assumptions may or may not be right for most code, but they can't be right for 100% of code.

That's actually not true. What we call "undefined behavior" in C is actually implemented by C compilers as "it doesn't matter what happens in this case". Sometimes, compiler writers will actually define some of the things that are undefined behavior in the standard, but more often than not, the compiler doesn't actually contain any explicit definition for what is supposed to happen in those cases. The compiler only worries about transforming your source code into machine code that does what the standard specifies where the standard does specify what is supposed to happen--where the standard doesn't specify anything, any resulting behavior is just a coincident byproduct of how the compiler works, which can change with any new release, and even depending on optimization options.

> So why should compilers allow programmers to leave their desired semantics implicit?

They actually don't. If the programmer wants to express "add x to y (two signed integers) with wrap around" and writes in a C program "x+y", the programmer has simply made a mistake. It may be that in some cases, if you write "x+y", your compiler will behave with those intended semantics. But that is a coincidence. There is nothing implicit there, "x+y" in C simply means something different than the programmer intended, and just by coincidence sometimes produces the desired result. It's not any more correct than writing "exit(0);" when you want to print "hello world"--the only difference is that the compiler is free to produce the intended result even though it's not required to, which confuses many people. But just because the compiler sometimes produces the intended result does not mean that what you wrote actually means what you intended.

Re:The Problem with Friendly C by derefr on Thursday December 24, 2015 @09:36PM (#10790232)
I agree with pretty much everything you wrote here. I think my main disagreement is that you can fix this at the language level. Languages are static entities; some are "living" in the sense that they get new major revisions (C++0x, Python 3, etc.) but on a day-to-day basis, you have to deal with the language you've got, and most languages don't have any way to "annotate in" these sort of semantics on a per-module basis. (C is one of the only places this has been given even bare thought, actually, with compiler #pragmas.)

IMHO the goal of a compiler is this: to take as input 1. code represented in some standard grammar, and 2. configuration represented in a compiler-specific format; and to combine these two things to generate a build artifact.

I assume that you want the build artifact to have the same deterministic behavioral semantics on all target platforms, regardless of whether your language allows you to encode those semantics. Insofar as compilers have the two inputs stated above, then each semantic property that can't be made explicit by the language grammar, must then fall to the compiler's configuration.

In other words, it's the compiler's job (through you) to fix the language's mistakes, such that vague code becomes non-vague binaries.

Imagine for a moment that, instead of global command-line switches passed directly to the compiler, the compiler's configuration existed in the form of a "hints file" alongside each source file—effectively, a proprietary system of markup/annotations for the language, but separated into its own file instead of being intermingled in the source like Java's @annotations.

In such a model, you'd have a 1:1 correspondence between input #1 and input #2: each file of C code would have an equivalent file of hints, telling the compiler everything it needs to know that isn't in the C code (and which, moreover, can't actually be expressed at all in C.)

With our current backwards batch-programming methodologies, you'd probably have to write the "hint" files yourself, or have your IDE generate them by twiddling knobs. My real expectation, though, is that your compiler would actually run through your source files interactively, asking you questions where it thinks things are vague and recording the answers in its hint files, only actually doing a compilation pass once all its questions have been answered. Your compiler would actually work with you like a copy-editor, taking your muddled prose and putting in little query-marks to show its confusion.

Which is all to say, we're never going to get a revision of the C language standard that will actually give "x + y" platform-independent overflow semantics. But we could definitely create compilers that would annotate each "x + y" in a C codebase with a particular overflow semantics, without doing anything to the code itself that breaks the C language grammar. This would let us remove the vast majority of what is currently "undefined behavior" from our code, without fighting a losing battle for more semantically-explicit languages.

Re:The Problem with Friendly C by zAy0LfpBZLC8mAC on Thursday December 24, 2015 @10:10PM (#10790285)
> Anyway, that's all to say, IMHO the goal of a compiler is to take as input 1. code represented in some standard grammar, and 2. configuration represented in a compiler-specific format, and combine these to generate a build artifact.

Then, what you really are doing is defining a new language. And one with a horrible syntax at that. If the "code" alone does not actually describe what the program is supposed to do, that "configuration" becomes part of the program (if the binary's behaviour changes depending on whether you compile it with or without the "configuration", it is obviously part of the code), and having parts of your program kindof "out of band" is about the worst idea ever for auditing and maintenance, and for general readability. Also, given that that part of your new language is not part of the C standard, say, you cannot use any C compiler to compile it. If you are lucky, you find another compiler that implements another new language based on C with a different syntax for the "configuration" that provides the same semantics, then you can start porting from your own new language to that compiler's new language, and then maintain two implementations of that part of your code (erm, "configuration").

If you want to create a new language with better semantics, better invent one with sane, in-band, syntax. If you want to write C ... well, then write C, C is turing complete, so you can express anything you need in it, and every (non-buggy) C compiler will understand it.

> My real expectation, though, is that your compiler would actually run through your source files interactively, asking you questions where it thinks things are vague and recording the answers in its hint files, only actually doing a compilation pass once all its questions have been answered.

I think that that can't work. There are just so many places where it's somewhere between extremely hard and impossible for a C compiler to prove that undefined behaviour cannot happen that you probably would need a dozen annotations for every line of your code. Plus, keeping track of how the recorded annotations map to the code after it has been modified is extremely fragile, so if you wanted to be sure, you'd have to answer all the questions for a source file again after any semantic change you make.