Stack traces, however, are still line based and thus if an exception occurs (like null reference) you only get the line, not the statement.
Honestly, even if you had to hit an 'expand this statement' button in your debugger to see `if x() && y()` spread into:
x_val = x()
if x_val
y_val = y()
if y_val
...
it would completely remove the necessity to write strange things to get around this limitation. Why do we have compilers and a huge variety of languages if not to stop writing strange things unnecessarily?On the other hand, an optimizing compiler already makes it pretty hard to single-line-step through a program (what with reordering, CSE, and more sophisticated transforms). Single-expression-stepping would be an even more difficult "debugging illusion" to provide.
On the consumer side, knowing where you are is actually the least hard problem in optimized debugging, compared to things like tracking variables that got split up into multiple disjoint registers, or part in register/part in memory, etc
As for where you are, the line table already will tell you that the column number changed on pc address advance, but line number did not. Thus, you know that you moved an expression, but not a line.
GDB doesn't happen to support this, and simply looks steps until line number change.
But it's not fundamentally hard from the debugger perspective.
On the producer side: When it comes to knowing where you are, you know you can't produce a 1-1 mapping, so you don't try. You can of course, properly present inlined functions as if they were function calls, and gdb will even do this. But there are times when lines or expressions were merged, and there simply is no right answer.
It's not hacky, it's actually completely well structured. Even debugging info for C/C++ macros is there and done properly.
None of this is new. DWARF2 (or 3 or 4), the standard debugging format for all non-MS compilers, has been able to do this well since 1993.
In Clang/LLVM, for example, nodes are annotated with debug information using LLVM's metadata system. However, it is strictly optional for any given transformation to preserve the metadata on IR nodes. So what makes it through after optimizations is kind of a "best effort" affair.
Not all compilers even really degrade. GCC does post-hoc variable tracking (and thus is mostly unaffected by optimization except when values are optiized out completely). For declarations and statements, the info is always on the declaration, so it won't be lost.
Both compiler guarantee that at -O0, all debug info will be kept.
Here's an example of gcc versus clang (a "frontend" for LLVM):
$ gcc-4.2 -fsyntax-only -Wformat format-strings.c
format-strings.c:91: warning: too few arguments for format
$ clang -fsyntax-only format-strings.c
format-strings.c:91:13: warning: '.*' specified field precision is missing a matching 'int' argument
printf("%.*d");
^
LLVM is does some cool stuff :). Some other nice examples are at http://clang.llvm.org/diagnostics.html.Anyway, those are compiler warnings rather than after-the-fact debugging. I don't think LLVM's debugger, lldb, does any better in this case.
All of the debuggers I've seen are line based with the option of single stepping through assembly.
Hopefully the LLDB people will be able to modernize debugging to the degree that the clang guys have modernized error messages, but that still remains to be seen.
That said, a huge cascade of them is something I try to avoid. Two or three, okay. Nine or ten, break it up and make it clear and readily debuggable.
Ignoring the "no space after if/while/for" issue, I'd suspect that code to not have been the author's intent if I just came across it.