:date: 2026-07-31 20:10 :tags:
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.
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!]