:date: 2026-07-29 05:59
In the last post we looked at how challenging it was to just track level depth. To understand the problem better let's review how singletons can cause mischief. The classic singleton is something like this conceptual code.
SINGLETON_KEYWORD (optional_conditions) resolving_statement;
The SINGLETON_KEYWORD can be if, for, while, switch, do, or
else. When the resolving statement completes, the singleton is
complete. But what if the resolving_statement is also another
SINGLETON_KEYWORD?
SINGLETON_KEYWORD
SINGLETON_KEYWORD
SINGLETON_KEYWORD
SINGLETON_KEYWORD
resolving_statement;
The resolving statement closes out the nearest singleton which then closes out the next and the whole chain unwinds. Now that I've explained it to you, you're ready to believe that's how it truly works, right? If you don't know anything about C you are certainly ready to believe this. Even if you are a professional C programmer, you might still be fooled because no sane person ever writes code like this!
The resolving statement unraveling all the nested singletons is a nice neat concept, but unfortunately, it is not C's nice neat concept. Let's see how C really behaves. Check out this singleton puzzle paying attention to the indent levels.
if (L1)
for (L2;;)
if (L3)
resolve(3,2,1);
if (L1) // Previous resolve fully resets level.
resolve(1);
That does look like the neat algorithm I described. The level of the
first if has been fully reset because the entire chain of singletons
has been resolved. Seems reasonable, right? C then puts you in the
uncomfortable position of thinking the following code is also
reasonable even though there is a kind of inconsistency to it.
if (L1)
for (L2;0;)
if (L3)
resolve(only3);
else // Previous resolves only one level!
resolve(3,2,1); // Finally resolves.
The else resolves with a single statement clearly making it a
singleton keyword. But there is more to it. It turns out that else
is a special singleton keyword that has its own ideas about indent
level.
This inconsistent feature of C's control keywords introduces the need
to track if the last singleton keyword was specifically an if and
then on closing out that if, the levels get adjusted differently if a
look ahead can find an else nearby. Good times.
If you think all that sounds challenging, I've got some bad news for
you because it turns out to be a lot worse than that. I realized that
any else must align with any inner if regardless of either
executing a braced clause or not. This actually requires a
FILO stack
to keep track of any pending if that might possibly go with an
else at every level of brace depth. And these must be politely
dropped once there is no possibility for an else at that brace
level. Don't worry if that takes you several days to get your head
around; been there.
At this point I was seriously starting to question my judgment at taking on this project. But I pressed on and figured out a way. The need for this quirky syntax tracking should almost never arise in any sane modern production code. For that reason each tracked level on the stack gets a leisurely heap allocation and I'm not even going to apologize.
When I finally implemented that, my test setup caught another error in the K&R sample from page 132! There already was a missing semicolon in that code but also I realized line 20 is incorrectly indented! Fortunately SnowC caught it nicely. And of course it must be said that in C, this is not technically an error — it does compile fine. But it sure is a faux pas!
(KR132-cat.c)
#include <stdio.h> | #include <stdio.h>
/* cat: concatenate files, version 1 */ | /* cat: concatenate files, version 1 */
main(int argc, char *argv[]) | main(int argc, char *argv[])
{ | FILE *fp
FILE *fp; | void filecopy(FILE *, FILE *)\ //[Original missing ;]
void filecopy(FILE *, FILE *) //[Original missing ;] | if (argc == 1) /* no args; copy standard input */
if (argc == 1) /* no args; copy standard input */ | filecopy(stdin, stdout)
filecopy(stdin, stdout); | else
else | while(--argc > 0)
while(--argc > 0) | if ((fp = fopen(*++argv, "r")) == NULL)
if ((fp = fopen(*++argv, "r")) == NULL) { | printf("cat: can't open %s\n", *argv)
printf("cat: can't open %s\n", *argv); | return 1
return 1; | else
} else { | filecopy(fp, stdout)
filecopy(fp, stdout); | fclose(fp)
fclose(fp); | return 0 //[Original indent wrong!]
} | /* filecopy: copy file ifp to file ofp */
return 0; //[Original indent wrong!] | void filecopy(FILE *ifp, FILE *ofp)
} | int c
/* filecopy: copy file ifp to file ofp */ | while ((c = getc(ifp)) != EOF)
void filecopy(FILE *ifp, FILE *ofp) | putc(c, ofp)
{ |
int c; |
while ((c = getc(ifp)) != EOF) |
putc(c, ofp); |
} |
I already noted their page 51 example which highlights this exact problem deliberately. The SnowC on the right is automatically corrected.
if (n > 0) | if (n > 0)
for (i = 0; i < n; i++) | for (i = 0; i < n; i++)
if (s[i] > 0) { | if (s[i] > 0)
printf("..."); | printf("...")
return i; | return i
} | else /* WRONG */
else /* WRONG */ | printf("error -- n is negative\n")
printf("error -- n is negative\n"); |
Hopefully you now have a better understanding of just how wrong I was when I originally thought of the SnowC concept and I thought it surely couldn't be all that hard. It was a hell of a challenge but I'm glad I stuck with it and I am very happy with the results.
I'll leave you with a couple of extra awful conversion tests that I used during development, starting with an example of singletons, braces, and a mix. Remember, SnowC is ignoring all the (possibly spurious) indentation of the original and recreating it with structural rigor.
(nested_if_else.c)
int main(int argc, char** argv) { | int main(int argc, char** argv)
// All singletons. | // All singletons.
if (q1) | if (q1)
if (q2) | if (q2)
if (q3) | if (q3)
if (q4) | if (q4)
a4(); | a4()
else | else
e4(); | e4()
else | else
e3(); | e3()
else | else
e2(); | e2()
else | else
e1(); | e1()
// All brace blocks. | // All brace blocks.
if (q1) { | if (q1)
if (q2) { | if (q2)
if (q3) { | if (q3)
if (q4) { | if (q4)
a4(); | a4()
} | else
else { | e4()
e4(); | else
} | e3()
} | else
else { | e2()
e3(); | else
} | e1()
} | // Mixed.
else { | if (q1)
e2(); | if (q2)
} | if (q3)
} | if (q4)
else { | a4()
e1(); | else
} | e4()
// Mixed. | else
if (q1) { | e3()
if (q2) | else e2()
if (q3) { | else
if (q4) { | e1()
a4(); |
} |
else |
e4(); |
} |
else { |
e3(); |
} |
else e2(); |
} |
else { |
e1(); |
} |
} |
Here is another challenging test for the if/else matching. The B is brace level and the S is singleton level.
(badelse.c)
void main() { | void main()
// All singletons. | // All singletons.
for (q1) | for (q1)
for (q2) | for (q2)
if (q3) | if (q3)
while (q4) | while (q4)
for (q5) | for (q5)
a(); | a()
else | else
if | if
while | while
for | for
b(); | b()
else | else
c(); | c()
ok1(); | ok1()
// With some braces. | // With some braces.
for (q1) | for (q1)
for (q2) | for (q2)
if (q3) { | if (q3)
while (q4) | while (q4)
for (q5) | for (q5)
a(); | a()
} | else
else { | if
if | while
while | for
for | b()
b(); | else
else | c()
c(); | ok2()
} | // With challenging else requirements.
ok2(); | if (d) // B1 S0
// With challenging else requirements. | while (w) // B1 S1
if (d) // B1 S0 | if (c) // B1 S2 --------+
while (w) // B1 S1 | if (b) // B2 S0 -----+ |
if (c) { // B1 S2 --------+ | for (f) // B2 S1 | |
if (b) // B2 S0 -----+ | | if (a) // B2 S2 ---+ | |
for (f) // B2 S1 | | | a++ // B2 S3 | | |
if (a) // B2 S2 ---+ | | | else // B2 S2 ---+ | |
a++; // B2 S3 | | | | a() // B2 S3 | |
else // B2 S2 ---+ | | | else // B2 S0 -----+ |
a(); // B2 S3 | | | b() // B2 S1 |
else // B2 S0 -----+ | | // B2 S1 |
b(); // B2 S1 | | else // B1 S2 --------+
} // B2 S1 | | c() // B1 S3
else // B1 S2 --------+ | else // B1 S0
c(); // B1 S3 | d() // B1 s1
else // B1 S0 | return // B1 S0
d(); // B1 s1 |
return; // B1 S0 |
} |