SnowC - Gory Details - Aggregate Types

:date: 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 };
                                         | }

Same simple rules but now this C is invalid. Getting it 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++;
                                              | }

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 general types with no specific variables declared.

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. Let's fast forward and show what c2snowc actually does with those lines.

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 out for some other kind of syntax while still 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.