Chris X Edwards

While no means no, sometimes nay means yes, like when speaking Greek. Rejecting near universally honored PIE heritage Ναι means Yes.
2026-08-19 17:44
Is pointer arithmetic really more complicated than picking through DOM elements? At least the CPU agrees with me that it's not.
2026-08-11 14:50
How seriously should we take AI hype? My score card is still 100 spam emails a day and zero compelling in game NPCs. Any day now!
2026-08-05 07:18
Some insights produce more questions than answers. E.g. Tom and Jerry are both nicknames for WW2 belligerents, odd for the 1940 US cartoon.
2026-06-10 06:56
Unimpressed with artists fussing over AI because of exploitation but still keeping Adobe in business.
2026-05-21 06:46
Blah Blah
--------------------------

SnowC - A Demonstration

2026-07-31 20:10

In previous posts I introduced SnowC and explained how it works and what challenges had to be overcome. In this post let’s look at SnowC in action and see what it’s capable of.

Recall that the most challenging project goal was to be able to convert wild C source code into sensible SnowC. That was phase one. Phase two was relatively easy, just following the SnowC rules to convert SnowC back to C. Let’s have a closer look at phase three which was to get both of these programs working well enough that the SnowC conversion programs' source code themselves could be both converted.

We can start by downloading the SnowC project from…

Here is a good way to get to a directory with the SnowC source code.

$ cd /tmp/
$ wget -qO- https://xed.ch/project/snowc/snowc.tgz | tar -xzf -
$ cd snowc-*

To test the round trip conversions, I’ve created a script. The script is pretty illustrative and kind of interesting in a mind bending kind of way, but you can also ignore its details and skip ahead to just focus on the big picture.

#!/bin/bash
# Enter number of rounds as an argument or default to 3.
MAX_ROUNDS=${1:-3}

# === Initialization (Round 0) ===
echo "=== Round 0: Initial Compilation ==="
gcc -Wall -o c2snowc c2snowc.c
gcc -Wall -o snowc2c snowc2c.c
# Generate initial SnowC files (.cno) from original C.
./c2snowc c2snowc.c > c2snowc.cno
./c2snowc snowc2c.c > snowc2c.cno
# Convert initial SnowC back to C (Round 1 output).
./snowc2c c2snowc.cno > C2SNOWC-01.c
./snowc2c snowc2c.cno > SNOWC2C-01.c
# Compile Round 1 C files.
gcc -Wall -o C2SNOWC-01 C2SNOWC-01.c
gcc -Wall -o SNOWC2C-01 SNOWC2C-01.c
# Initial check (Round 1 verification).
echo "=== Round 1 Verification ==="
diff <(./c2snowc C2SNOWC-01.c) <(./C2SNOWC-01 C2SNOWC-01.c)
md5sum <(./c2snowc C2SNOWC-01.c) <(./C2SNOWC-01 C2SNOWC-01.c) | cut -b-32

# === Main Loop (Previous Round = 1 to MAX_ROUNDS - 1) ===
for PR in $(seq -f'%02.0f' $(( ${MAX_ROUNDS} - 1 )) ); do
    R=$(printf "%02d" $((10#$PR + 1))) # This round = PR++.
    echo "=== Round $R ==="
    # 1. Create (R-1) SnowC from (R-1) C.
    # Using the executables from the previous round (PR).
    ./C2SNOWC-$PR C2SNOWC-$PR.c > C2SNOWC-$PR.cno
    ./C2SNOWC-$PR SNOWC2C-$PR.c > SNOWC2C-$PR.cno
    # 2. Create (R) C from (R-1) SnowC.
    # Using the SNOWC2C executable from the previous round.
    ./SNOWC2C-$PR C2SNOWC-$PR.cno > C2SNOWC-$R.c
    ./SNOWC2C-$PR SNOWC2C-$PR.cno > SNOWC2C-$R.c
    # 3. Compile (R) C to (R) executables.
    gcc -Wall -o C2SNOWC-$R C2SNOWC-$R.c
    gcc -Wall -o SNOWC2C-$R SNOWC2C-$R.c
    # 4. Verification: Compare (R) executable output vs (R-1) C input.
    echo "Verifying Round $R..."
    diff <(./C2SNOWC-$R C2SNOWC-$PR.c) <(./C2SNOWC-$R C2SNOWC-$R.c)
    md5sum <(./C2SNOWC-$R C2SNOWC-$PR.c) <(./C2SNOWC-$R C2SNOWC-$R.c) | cut -b-32
done

Basically this script compiles the SnowC conversion programs from the original C source code (the code I originally wrote) and then uses those executables to convert the same source code to SnowC and then back to C. After a new set of C source code files has been generated, you can start the process again with the new files. You can specify how many times you want to make this round trip.

If I run this with a single round trip, it works. The outputs of the new version and the original version match.

$ ./roundtrip 1
=== Round 0: Initial Compilation ===
=== Round 1 Verification ===
13133dd985944b14d2482971baa3c9a1
13133dd985944b14d2482971baa3c9a1

This means the newly generated C source code behaves exactly like the original source code. And if I run the script 99 times and check the 99th executable, it also functions identically to the first.

$ md5sum <(./c2snowc c2snowc.c) <(./C2SNOWC-99 c2snowc.c) | cut -b-32
ba07eb908e548a6e6ceb36b70cd087b3
ba07eb908e548a6e6ceb36b70cd087b3

Hmm, shouldn’t those md5 summaries be the same? Probably, it would be nice for sure. When the script is run with a high number of cycles you can easily see the problem. Those diff checks show that trailing comments are somehow getting pushed to the right by one space every iteration.

drift.gif

It looks like when braces are removed to make the SnowC, all the spaces around them stay, and then when a brace is added back to make the C again, it gets a polite space to format/separate it from the previous characters but all the spaces from the last round are still there too. So the line grows. An interesting bug for sure but minor enough to really be a serious issue only if you’re doing hundreds of round trip conversions!

I’ll fix this bug but it does not affect the critical goal of phase three. With the code supporting a functional round trip, it is now reasonable from here on to be able to continue development in SnowC. Hopefully we’ll see how that turns out sometime in the future.

Getting to phase three where a round trip is possible was the main original goal and now that I have done that, I’ll probably take a break from this project. Besides the issue I just mentioned, I have some known bugs I’d like to fix and I would like to try working directly in SnowC to see what that’s actually like. But what is next? What is phase four?

I think the next major milestone is improving and proving the robustness of the system by having it try to do round trips with C code in the wild. My main target is the Linux kernel.

Ignoring header files (for now) the 7.1.5 Linux kernel is distributed with 36684 C source code files. I used c2snowc to convert all but 11 of them. In just a quick inspection of those, I discovered a nasty configuration of nugatory else matching (see yesterday’s post). Since those 11 all hung (and not, say, a seg fault) I’m hoping they might all have the same problem. That’s only the first half of the mission. I also converted the 36673 Linux kernel SnowC files back into C. No trouble there. But Linux isn’t just some test code. Does that generated C compile? No. Even make tinyconfig dies pretty quickly into the process. But it’s a superb platform for catching C code that is difficult to translate. So I’ll probably work on whatever deficiencies that strategy reveals.

One thing converting 36673 files did answer is how fast is SnowC? Converting the C to SnowC took 2min 55sec or 4.8ms per file. With its simple algorithm, converting those SnowC files back to C was 25% quicker at 2min 12sec or 3.6ms per file. For reference, on my computer just a simple ls or cal take about 12ms. Just showing me the date takes about twice as long as converting SnowC code. So I’m pretty happy with the performance. If you come up with a much quicker way to pull the inherent redundancy out of well written C code, I’d love to see it!

[If you’ve made it this far, thank you for reading all this! Or any of it! I hope you learned something, even if only about my ability to get a little too obsessive about my projects!]

SnowC - Challenges - Some Final Pointers

2026-07-30 16:33

There is actually a point to this series on challenges I faced when creating a decent implementation of SnowC. I think the SnowC idea is very sensible and it is worth pursuing but while I’m proud of what I’ve accomplished, I’m not claiming to be our species' best candidate to implement the idea. And one day in the future — but not today! — maybe our robot friends can even improve on what I’ve done. So these posts serve as a guide for what someone will need to think about if they want to create a SnowC conversion system of their own. This post will round out some of the other random challenges I ran into. Some were expected, some not so much.

Literals

Let’s start with the first thing you need to start with. It may come as a slight surprise that the first thing to deal with is character and string literals. I take a special interest in specifying text in programming languages so it came as no surprise to me to discover that strings must be isolated and dealt with first. It’s pretty easy to see why if we imagine a string talking about the syntax of specifying strings.

char *help = "Use \" to specify strings in C: \"a string\"";

Obviously that’s annoying but really SnowC doesn’t care what your string says, it just needs your strings to not confuse things. That could happen in an example like this.

char *a = "char /*{ick}*/ *b = \"b\"; int c = 0; "; int c = 0;

You can use the c2snowc map diagnostic option (-m) to show how the characters of this line get classified, or mapped into general categories.

char *a = "char /*{ick}*/ *b = \"b\"; int c = 0; "; int c = 0;
CCCC_CC_C_$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$;_CCC_C_C_C;+

With the characters of the string properly identified it’s much easier to see that there are only two proper semicolons here to worry about. The same idea applies to this line involving character literals.

char c = ';' == '\'' + ';'; c++;
CCCC_C_C_'''_CC_''''_C_''';_CCC;+

It may seem odd, but since strings can say any crazy thing, they must be neutralized before anything else is touched. And although the fail cases are subtle, character literals should be immediately neutralized next.

Comments

The next thing to isolate and neutralize must be modern double slash comments. This was originally a C++ thing and looks like this.

char *comments = "can get messy"; // So true!

This (everything after //) is the primary comment style that takes precedence over a secondary comment style. Old school K&R comments (/* which look like this */) are very clever. Too clever. Probably the biggest source of real problems is C’s classic comment style. It’s bad enough looking for the last meaningful character of a line knowing that any random junk can follow in the form of a comment. For example, the last meaningful character here is the first semicolon.

x /= 3; //= 3; ///= 3; /* This is totally valid C code because FML. */

But the problems get infinitely worse when you consider that classic C commenting can be mischievously injected anywhere. Looking for a thing that needs to follow a thing? Well, you better count on it following a near random collection of obfuscating garbage in the form of a comment.

So identify all the characters that are double slash comments and then identify all the other comment characters. Then don’t forget about them, because they could possibly be causing mischief damn near anywhere!

Just remember that you can comment out preprocessor syntax, so deal with comments first. Which brings us to preprocessor problems.

Preprocessor

Comments are hard because they can lurk anywhere muddling important semantic details. But preprocessor directives have a different problem. They are often used to swap in chunks of real C code at the last minute. I actually did not do a super deep dive into insane preprocessor abuse. I am pretty tame with my usage personally and, well, it was a lower priority for me. Sorry.

Basically if the first significant character of a line is a # I’m calling it a preprocessor directive and it gets ignored much like a comment. If there is some kind of #if, #ifdef, #elif, #else or similar construction with some ugly confusing code hiding inside, well, you’ll probably need to take special care.

One conceptual problem is that the SnowC to C conversion injects the missing C syntax (braces, semicolons) back into the code. If a preprocessor range isn’t anticipating this, there can be problems. Here’s a simplified example equivalent to something I found in the Linux kernel starting with the original C.

void fn() {
  for (q1) {
    if (q2) {
      a();
      #if (FANCY_FEATURE == ENABLED)
      fancy_feature();
      #endif
    }
  }
}

The SnowC is fine.

void fn()
  for (q1)
    if (q2)
      a()
      #if (FANCY_FEATURE == ENABLED)
      fancy_feature()
      #endif

But look what happens when it goes back to C.

void fn() {
  for (q1) {
    if (q2) {
      a();
      #if (FANCY_FEATURE == ENABLED)
      fancy_feature();
    }
  }
}
      #endif

Leaving out all the closing braces when the fancy feature is not enabled will be a serious problem. Comments also can be moved to undesired locations in this way but that’s less problematic.

I could have all closing brace generation wait until after any preprocessor lines (and comments) but this is sometimes not the right choice either. I suspect it is the more common choice and I may switch the default to it. I’m starting to think that the correct solution is for SnowC to insist you place your preprocessor statement’s level intentions the same way as everything else — with indent level. Consider it on the to do list.

Labels

C has a quirky syntax for "labels" which are targets for the goto statement. Using goto in the first place is not really encouraged so labels aren’t even all that common. But they’re common enough that they need to be dealt with.

In C, a label is generally a word followed by a colon. Like this.

found:

Easy, right? Well, not so fast! Have a look at some of my test code where I try to anticipate label problems.

label1:
    label_with_leading_spcs:
label_with_trailing_spcs:
label_with_intercolon_spcs   :
/*precomment*/label_with_precomment:
label_with_post_comment:// Post comment.
label_with_post_comment_trailings:/* Post comment w trailing. */
label_with_intercomment/* Inter comment.*/:
_yes_a_label:
_yes_2_label:
3_not_a_label:  // Can't start with a number.
not%a_label:
not^a_label:
default_plus_is_label:
default_ : // Is a label.
default:   // Technically not a label despite appearances.

C has a way of making you think it’s simple and obvious. And then you dig into it and find out, yikes, a lot can be taken for granted by the programmer.

So why exactly do we need to know what text is a label? Because SnowC needs to mostly ignore them sometimes. For example when they are between a singleton control word and the resolving statement.

Unfortunately C has another use for colons. A couple in fact. Look at this syntax — which I checked does actually compile.

 int not_a_label = 0;            | int not_a_label = 0
 x2 = x?                         | x2 = x?\
 not_a_label:                    | not_a_label:
 99;                             | 99

Fortunately, despite failing to realize that not_a_label: is not a label, the SnowC conversion just happens to work because of, well, luck. I am walking away from this one.

Nugatory

If you don’t know what the word "nugatory" means, I suggest you look it up (click here!) and start using it. It’s a great word!

You can probably figure it out from my test file called nugatory.c which gave me a surprising amount of grief. Here it is with the generated SnowC on the right.

 //TEST: Blank statements and blocks.           | //TEST: Blank statements and blocks.
 if (q0) {} // Empty block.                     | if (q0) {}\ // Empty block.
 if (q1) ; // Blank statement.                  | if (q1)  // Blank statement.
 else if (q2) ;                                 | else if (q2)
 else if (q3) {                                 | else if (q3) {\
 }else if (q4) {      // Some spaces.           | }else if (q4) {\      // Some spaces.
 } else if (q5)  // Ick.                        | }else if (q5)\  // Ick.
 { }                                            | {}\
 else if (q6) {                                 | else if (q6) {\
     /* Pass. */                                | /* Pass. */
 } else {// Uncomment for diagnostic.           | }else {\// Uncomment for diagnostic.
     //printf("Error:\n"); /* ERROR */          | //printf("Error:\n"); /* ERROR */
 }                                              | }\
 x++;                                           | x++
                                                |

Dang that’s gnarly! But if you follow the simple SnowC rules to convert that back to C, it will produce semantically equivalent C code.

Note the final line is blank. I also have several tests for blank lines at the beginning and end and other inconvenient places. It’s little things like that which can be a real pain in the ass.

I’m sure there are other challenges I could highlight but that concludes my tour of the main problems that one must deal with when trying to pull the redundancy out of C code. It may not be easy, but for practical use, I have demonstrated that it is possible.

SnowC - Challenges - Else

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                             |
 }                                                                      |

SnowC - Challenges - Levels

2026-07-28 19:53

Back in 2015 when I conceived of the idea for SnowC, I first thought it would be so easy to convert from SnowC to C that I could do it by storing the state of the indentation level in the call stack using recursion. That turned out to be too ambitious but it was close.

However what was really delusional and naive was thinking that converting from C might be possible using recursion too. While I got over that quickly, even starting on it fresh this year, I thought, surely this conversion can not be that difficult. Wow, was I profoundly wrong! Over two months later I now know an unhealthy amount about pathological C code and I have been humbled. When it comes to the entire possibility space of C source code, nothing is simple.

Which brings us to indent levels. Easy, right? Well let’s just say that it’s easy when it’s easy. Like this example (C left, SnwoC right).

 // Level 0                                              | // Level 0
 int level1(int x) { // Level 0 -> 1                     | int level1(int x)  // Level 0 -> 1
     if (x) { // Level 1 -> 2                            |     if (x)           // Level 1 -> 2
         if (x % 2) { // Level 2 -> 3                    |         if (x % 2)     // Level 2 -> 3
             x++;                                        |             x++
             for (int y=x; y<99; y++) { // Level 3 -> 4  |             for (int y=x; y<99; y++)  // Level 3 -> 4
                 if (x + y == 99) { // Level 4 -> 5      |                 if (x + y == 99)      // Level 4 -> 5
                     return y;                           |                     return y
                 } // Level 5 -> 4                       |                 // Level 5 -> 4
             } // Level 4 -> 3                           |             // Level 4 -> 3
         } // Level 3 -> 2                               |         // Level 3 -> 2
     } //Level 2 -> 1                                    |     //Level 2 -> 1
 } //Level 1 -> 0                                        | //Level 1 -> 0

And without the comments, the SnowC is pretty legible.

int level1(int x)
    if (x)
        if (x % 2)
            x++
            for (int y=x; y<99; y++)
                if (x + y == 99)
                    return y

Those comments are there to show how the core concept works. You basically need a simple integer variable that stores the indent level. When you find an opening brace ({), increment the indent level, and when you find a closing brace (}), decrement the indent level. Easy, right? Right? Ohhhhh no.

hiding_creeper.jpg

It gets pretty bad. Let’s start with relatively simple singleton hassles. In a recent post we just covered how the C creators loved to do level nesting with no braces at all. I call those cases singletons. Ok, fine, so that is a thing. Sure, whatever. Surely there’s a solution, I thought.

Usually SnowC can happily not really care one bit what your C code is actually doing. But because of this issue, I realized there needed to be a complex parsing effort so all the singleton (braceless) control syntax could correctly contribute to the indent levels being adjusted properly. Not easy but doable. So I dug in and got to it.

Single line singletons are easily handled by just ignoring them. They are resolved before they can even make an impact on the code’s indentation structure.

Multi-line singletons are a different story, so let’s start with them.

An interesting property of SnowC is that a round trip will completely remove multi-line singletons! For example, consider this authentic K&R example (from page 92).

for (i = 1; yearday > daytab[leap][i]; i++)
    yearday -= daytab[leap][i];

In SnowC it becomes.

for (i = 1; yearday > daytab[leap][i]; i++)
    yearday -= daytab[leap][i]

Note how close this actually is to what K&R imagined! But there is no way to hint that you do not want the more ordinary and predictable C you will get on the return conversion. Here is what converting back to C produces.

for (i = 1; yearday > daytab[leap][i]; i++) {
    yearday -= daytab[leap][i];
}

Note that this C produces the exact same SnowC shown above.

What if the entire singleton resolves on one line? And importantly what of subsequent additional elements like b() in this line of C code?

if (q) a(); b();

In theory this could be done by leaving it mostly as is with the non-trailing semicolons intact.

if (q) a(); b()

That is actually valid SnowC and will convert to the previous C one liner. However, when that C one liner converts to SnowC, it will get broken up.

if (q) a()
b()

The reason for this choice can be seen when there are line breaks which can cause trouble. Consider this SnowC.

if (q)
  a(); b()

If you are a real C programmer, hopefully that did not cause some life threatening allergic reaction. Which of these two possibilities is correct when it converts to C?

if (q) {
  a();
}
b();

Or…

if (q) {
  a(); b();
}

Both are reasonable interpretations.

While the first looks good at a glance, the second follows the simple SnowC rules. And for a few good reasons. What we can be sure about is that the following C code won’t ever be possible to generate by converting from SnowC.

if (q)
  a(); b();

Currently converting from C to SnowC, the rule is that singletons get split up if they resolve on the same line as another statement that would not share its level.

To see why this must be consider the following round trip sequences of conversions. Following the conversions vertically, on the left is C → SnowC → C and on the right is SnowC → C → SnowC.

C:                     SnowC:
    if (q)                if (q)
      a; b;                 a; b

   |                     |
   V                     V

SnowC:                 C:
    if (q)                if (q){
      a                     a; b;
    b                     }

   |                     |
   V                     V

C:                     SnowC:
    if(q) {               if (q)
      a;                    a; b
    }
    b;

Note how the SnowC can mostly preserve its form into C and back. But the C structured like this is confusing to begin with, and must get sorted out in SnowC for the braces to actually work when converted back. Because the C style in the upper left is confusing and discouraged (heck, K&R don’t even do it!) it is better to just cut up singletons packed on a line which will prepare them for how SnowC needs them to be. If you want SnowC as found on the right, use braces in the C like a normal person i.e. like the C code in the middle on the right.

If you want to see some serious SnowC puzzles check out my test file called spaceless.c and its corresponding SnowC.

$ echo "=== $F";pr -w$(tput cols) -o1 -s'| ' -tm $D/$F <(./c2snowc -i4 $D/$F)
=== spaceless.c
 void f(int q,int a){                        | void f(int q,int a)
 if(q)a++;                                   |     if(q)a++
 if(q)a++;b++;                               |     if(q)a++
 if(q)a++;else--a;                           |     b++
 if(q)if(q)a++;else--a;                      |     if(q)a++
 if(q)a++;else/**/if(q)a++;else--a;          |     else--a
 }                                           |     if(q)if(q)a++
                                             |     else--a
 if(q)                                       |     if(q)a++
 a();b();                                    |     else/**/if(q)a++
                                             |     else--a
 if(q)a+                                     |
 a;b();                                      | if(q)
                                             |     a()
 if(q){                                      | b()
 a();b();}                                   |
                                             | if(q)a+\
 if(q){                                      | a
 a();b();                                    | b()
 c();}                                       |
                                             | if(q)
                                             |     a();b()
                                             |
                                             | if(q)
                                             |     a();b()
                                             |     c()

Like many of my test files, it is a reminder that polite indenting is a choice in C; in SnowC it is a promise. It’s also an unusual example of a conversion to SnowC that results in more lines.

But those single singletons are relatively easy. Let’s take a look at more serious level hassles.

Check out this normal example from K&R p56, where the braces of the do control keyword control the indent. Also note that the indent is properly controlled for the two if statements too even though there are no braces. (C left, SnowC right)

 /* itoa: convert n to characters in s */                               | /* itoa: convert n to characters in s */
 void itoa(int n, char s[])                                             | void itoa(int n, char s[])
 {                                                                      |     int i, sign
     int i, sign;                                                       |     if ((sign = n) < 0) /* record sign */
     if ((sign = n) < 0) /* record sign */                              |         n = -n /* make n positive */
         n = -n; /* make n positive */                                  |     i = 0
     i = 0;                                                             |     do  /* generate digits in reverse order */
     do { /* generate digits in reverse order */                        |         s[i++] = n % 10 + '0' /* get next digit */
         s[i++] = n % 10 + '0'; /* get next digit */                    |     while ((n /= 10) > 0) /* delete it */
     } while ((n /= 10) > 0); /* delete it */                           |     if (sign < 0)
     if (sign < 0)                                                      |         s[i++] = '-'
         s[i++] = '-';                                                  |     s[i] = '\0'
     s[i] = '\0';                                                       |     reverse(s)
     reverse(s);                                                        |
 }                                                                      |

To pull this off requires parsing the code deeply enough to recognize all 6 of the possible control words that can trigger a singleton (if, while, for, switch, do, else) and recognize when they are not going to be using braces. Just identifying those is a big job. When I finally got that working I was pretty relieved. Mostly that it was even possible.

But it gets worse. The C language allows an unhealthy mix of brace levels and singleton levels. While experimenting with demented code that nobody would ever use (I hope!) I discovered something very unnerving. I slowly started to realize that simply tracking the brace depth and the singleton level depth was not going to do the job. (Theoretically; in practice it mostly would.)

Check out this sample program where I’ve labeled the conditions to reflect the brace depth (e.g. B1) and also the singleton depth (e.g. S1). (mixed_lvls.c, C left, SnowC right)

 void main (int argc, char **argv) {                                    | void main (int argc, char **argv)
  if (B1) {                                                             |   if (B1)
    while (B1S1)                                                        |     while (B1S1)
      for (B1S2)                                                        |       for (B1S2)
        if (B2) {                                                       |         if (B2)
          if (B2S1)                                                     |           if (B2S1)
            if (B2S2)                                                   |             if (B2S2)
              B2S2Resolve();                                            |               B2S2Resolve()
          if (B2S1)                                                     |           if (B2S1)
            if (B2S2)                                                   |             if (B2S2)
              B2S2Resolve();                                            |               B2S2Resolve()
            else //B2S2                                                 |             else //B2S2
              B2S2ElseResolve();                                        |               B2S2ElseResolve()
        }                                                               |   else if (B1)
  } else if (B1) {                                                      |     while (B1S1)
    while (B1S1)                                                        |       for (B1S2)
      for (B1S2)                                                        |         if (B1S3)
        if (B1S3)                                                       |           B1S3Resolve()
           B1S3Resolve();                                               |   else // B1
  } else // B1                                                          |     while (B1S1)
    while (B1S1)                                                        |       for (B1S2)
      for (B1S2)                                                        |         if (B1S3)
        if (B1S3)                                                       |           B1S3Resolve()
          B1S3Resolve();                                                | // End main()
 } // End main()                                                        |

What’s important to note here is that although chains of braceless singletons like if (a) if (b) if (c) do_this(); do increase indent depth, when the do_this() finally runs you can’t just set the singleton depth to zero and call it a day. That whole thing may be nested in one or more sets of braces that each have their own chain of singletons to unwind!

When I realized this, I had to scrap a lot of work and rethink the whole deal. And for what? A crazy obscure problem that in practice would hardly ever come up. But damn it, I was committed to doing it as well as I could.

The solution I came up with is that I create a heap array where I can store the singleton depth for each brace level. This is the only way an accurate unraveling of the most complex structures can be done. The initial allocation allows for ten brace levels deep. Is that the nesting depth limit then? No. If you go beyond ten, it will allocate twenty; if you fill that up, forty more, etc. until you run out of RAM.

This is my test of the dynamic array provision storing a long chain of singletons in a clause 11 brace levels deep.

 int main(int c, char **v) {                                 | int main(int c, char **v)
 if (q) {                                                    |   if (q)
 if (q) {                                                    |     if (q)
 if (q) {                                                    |       if (q)
 if (q) {                                                    |         if (q)
 if (q) {                                                    |           if (q)
 if (q1)   {                                                 |             if (q1)
 if (q2){                                                    |               if (q2)
 if (q3) {                                                   |                 if (q3)
 if (q) {                                                    |                   if (q)
 if (q) {                                                    |                     if (q)
 for (qS) a();                                               |                       for (qS) a()
 if (qS)                                                     |                       if (qS)
 if (qS)                                                     |                         if (qS)
 if (qS)                                                     |                           if (qS)
 if (qS)                                                     |                             if (qS)
 if (qS)                                                     |                               if (qS)
 if (qS)                                                     |                                 if (qS)
 if (qS)                                                     |                                   if (qS)
 if (qS)                                                     |                                     if (qS)
 if (qS)                                                     |                                       if (qS)
 if (qS)                                                     |                                         if (qS)
 if (qS)                                                     |                                           if (qS)
 if (qS)                                                     |                                             if (qS)
 if (qS)                                                     |                                               if (qS)
 if (qS)                                                     |                                                 if (qS)
 a++;                                                        |                                                   a++
   } } } } } } } } } }                                       | // Final line comment immediately after action.
 }                                                           |
 // Final line comment immediately after action.             |

If you understand how ugly that is, buckle up, it gets quite a bit worse! I’ll save that for the next post.

SnowC - Challenges - Aggregate Types

2026-07-27 19:56

Today is the first in a series on challenges I ran into while designing and working on SnowC. My hope is that these can offer insight into how the system really works and what the thinking was behind my design decisions. These posts also highlight how challenging this project actually is — if you’re keen to improve on my system — and what I did to overcome those challenges.

Instead of starting at the beginning let’s look at the last major challenge I tackled: aggregate types.

When I had the realization that braces are redundant in good code, I was naively thinking that the function of braces was to control nesting levels. And that is something they do. Usually.

It turns out that — in C specifically — braces are also used for Other Things. After carefully studying those my personal opinion is that this is a questionable stylistic language design choice. I get it — space is tight with a limited number of characters and a lot of functions to perform. But overloading braces with some bonus meanings is none too cool. Fortunately SnowC improves the situation quite a bit!

Which brings us to a feature of C that I procrastinated dealing with until the last possible moment. While writing c2snowc.c I actually had no idea how I was going to deal with aggregate types until everything else was finished! These are (possibly) complex definitions and declarations of struct, union, and enum types that often use curly braces in a quirky way. Programmers generally do not have strong feelings about how those braces correspond to indentation if they have any at all.

Let’s carefully unravel the solution because it also helps explain effective SnowC usage generally. Remember that SnowC is basically stripped down C that easily reconstitutes back to C if you follow some very simple rules.

Consider an analogy — if I ask some kind of robot chef to convert some dehydrated fruit into regular fruit and then I mischievously give it some fresh fruit, I’m likely to get back well-soaked regular fruit. And maybe that’s a good trick if what you really wanted was washed fruit. If you know the simple rules you can use them to your advantage.

SnowC is like that with braces. Generally the point of SnowC is to allow you to not fuss with braces. If you really want them however that’s fine but the conversion isn’t going to think too hard about what you really are trying to do. It will just follow the simple rules for converting back to normal C. Making sure the C is correct and makes sense is on you.

Consider this SnowC on the left and it’s corresponding automatically generated C on the right.

 void f(int x)                           | void f(int x) {
    for (int i=0; i<x; i++) x=-x         |    for (int i=0; i<x; i++) x=-x;
                                         | }

It follows the simple rules. Braces are added because of the indent, and a semicolon is tacked on because the conversion sees no reason not to. The for line isn’t scrutinized to see if you’re an intelligent C programmer. Look what happens if you add some clumsy braces to the SnowC.

 void f(int x)                           | void f(int x) {
    for (int i=0; i<x; i++) { x=-x }     |    for (int i=0; i<x; i++) { x=-x };
                                         | }

The same simple rules are correctly applied but now the C is invalid. Getting that part right is up to you!

If you understand and anticipate the conversion rules, you can try adventurous tricks to produce valid C.

 void f(int x)                                | void f(int x) {
    for (int i=0; i<x; i++) { x=-x; } x++     |    for (int i=0; i<x; i++) { x=-x; } x++;
                                              | }

This is now correct C. The exact same simple rules were used — the programmer just did a better job of playing the game.

However note that when you convert this generated C back to SnowC it will get looked at more carefully and cleaned up. When starting with wild C code, those rules are not so simple! (Now input C on the left and generated SnowC on the right.)

 void f(int x) {                              | void f(int x)
    for (int i=0; i<x; i++) { x=-x; } x++;    |   for (int i=0; i<x; i++)
 }                                            |     x=-x
                                              |   x++

If you suspect that a conversion to SnowC and then back to C can help clean up badly structured C code, you’re right! Even if you never use or look at SnowC code, the conversion programs can highlight and unravel badly structured C code.

Now that we’ve seen how braces can be dropped into SnowC code and mostly ignored, it’s easier to imagine how this could be useful for aggregate types. Let’s look at some examples. Consider the conversion of this complex badly formatted C struct (left) to SnowC (right).

 struct Point {                           | struct Point
  int x;                                  |     int x
  int y;                                  |     int y
  struct { int z;                         |     struct
   struct {int w;} nested;                |         int z
  } inner;                                |         struct
 };                                       |             int w
                                          |         nested
                                          |     inner
                                          | ;

The conversion pulls out semicolons and braces that the simple rules of SnowC to C conversion can easily replace. The random styling and indentation is not so random in SnowC. Compare old original C (left above) to the new C (below on the right) after a round trip conversion.

 struct Point                             | struct Point {
     int x                                |     int x;
     int y                                |     int y;
     struct                               |     struct {
         int z                            |         int z;
         struct                           |         struct {
             int w                        |             int w;
         nested                           |         }
     inner                                |         nested;
 ;                                        |     }
                                          |     inner;
                                          | }
                                          | ;
                                          |

If you’re not a C programmer and have just been following along roughly looking at the structure (bravo BTW!) then this code may not seem remarkable. SnowC conversions are just doing what they’ve been doing in all code we’ve seen. Well, with one tiny exception.

We have not seen a C to SnowC conversion leave an isolated semicolon when it is the last effective character of a line. Normally, the conversion to SnowC drops ending semicolons. This one represents a genuine special case rule that was created for aggregate types. The rule is: if a removable closing brace effectively (ignoring comments and whitespace) immediately precedes a semicolon which is the last effective character, then the semicolon stays.

If you think about it, a closing brace followed immediately by a semicolon is kind of a weird construction. As far as I can tell, it actually only ever shows up when defining types in situations like this.

The nice thing about the SnowC to C conversion is that it remains simple and follows the rules. If the last effective character is a semicolon in SnowC clearly that’s weird and it must be there for a reason, so it is left alone (and a second semicolon is not added to it).

Thank goodness we’ve solved the puzzle of aggregate types! Whew! What a relief.

Wait, what? We still haven’t? Ugh. It was about here that I (like anyone reading this) started to despair that maybe there were an infinite number of awful edge cases. I worried that maybe the whole concept was intractable. Luckily that wasn’t true! Let’s take a look at the last tricky problem involving aggregate types.

So far we have seen a pretty normal struct definition but there’s something else that can happen in type related syntax that involves braces and that is actually filling in values. It doesn’t even have to be fancy aggregate types. Here’s a simple array getting its values preloaded at definition time.

int arr[] = {1, 2, 3};

Gah! Who invited those braces?! And here are some typical aggregate types with similar syntax.

enum bool { FALSE, TRUE };
struct point p = { y: yvalue, x: xvalue };

Clearly if those braces are converted to indents, that’s going to be a mess. This is where being able to leave braces in SnowC becomes critical. Let’s fast forward and show what c2snowc actually does with those lines. Here are the SnowC conversions.

int arr[] = {1, 2, 3}
enum bool { FALSE, TRUE }
struct point p = { y: yvalue, x: xvalue }

That’s right, it just pulls the trailing semicolons off and says have a nice day! This means of course that SnowC can follow it’s reliable rules back to C and just add the semicolon back. But how is this possible?

What I realized is that this kind of thing only ever happens when the contents of the braces do not contain semicolons. Contrast with the earlier struct example. I created a function that scans brace pairs and if they contain no semicolons or other braces between the last characters of other C code (if any) and the closing brace, the braces are remapped as normal non-brace miscellaneous C code. I call this kind of brace, weak braces. Once their special meaning as a true brace is taken off the map of what’s going on, everything behaves very nicely. It’s like internally pretending to switch those braces out for some other kind of syntax while still in fact using the braces characters.

Unfortunately that still leaves nasty situations like this.

struct Point origin = {0, 0, {0, {0}}};

I realized that only the innermost {0} would get converted to weak braces. But! Once that conversion was done, if I ran the conversion again the next pair would get converted. I realized that if I returned how many conversions took place, I could elegantly solve the entire problem with this single line.

while (map_C_weak_braces(...));

That basically says, keep doing this conversion over and over until there is nothing left to convert. And once that is done, everything works very nicely. This may be the one place where I traded execution time for code elegance and my own sanity.

--------------------------

For older posts and RSS feed see the blog archives.
Chris X Edwards © 1999-2026