TuneC: a written tune becomes the bytes the player reads

The compiler, and the last thing the ladder was waiting for. A tune names
its instruments, writes sequences of notes and durations, and gives each
voice an order list of sequence names - which is where repetition comes
from, since a phrase played four times is written once and named four
times.

  #Tick 0d125000
  #Patch Oboe oboe.patch
  #Voice 0d0 Oboe
  #Sequence Verse
    0d64 0d4  0d67 0d4  0d72 0d8
  #Order 0d0
    Verse Verse Ending

"#" is a directive and ";" is a comment, exactly as in SplitBit assembly
and in the shell's scripts, and numbers are written the way the assembler
writes them. One rule across the machine rather than a third dialect -
and the rule earned itself immediately: the first tune I wrote said
"#Voice 0" and was refused, correctly, for a bare number.

WHAT IT REFUSES IS EVERYTHING THE PLAYER CANNOT NOTICE. The machine has
no names, so it cannot say a sequence does not exist. It has no lengths,
so it cannot say the voices will come apart four bars after the mistake.
A duration of nought is counted down to 255 and held, which sounds like a
hang rather than an error. And by the time a tune is loaded, "no starting
instrument" and "instrument nought" are the same byte - so the user's
ruling, that a voice with a part and no instrument is an error, can only
be kept here.

SoundPatch gains --blob, writing the same table as raw bytes. It stays
the only thing that reads soundThing's JSON: a second program parsing
that format is a second opinion about what a patch means, and the seam
between two opinions is where the LFO bug lived for a fortnight. Patches
are found beside the tune and then on a -I path, the way an include is.

THE TEST IS THAT TWO IMPLEMENTATIONS AGREE. maketune.py lays the fixture
out by hand and TuneC compiles a written source, and the suite checks
they match byte for byte - the discipline SplitDisk and sbfs.asm are held
to, for the same reason: either alone is only self-consistent. The
fixture predates the compiler, so this is also TuneC checked against
something written before it existed. Four more checks cover the four
refusals.

Also: the SoundPatch binary was tracked, alone among the six tools, and
.gitignore lists every other one. Untracked, and TuneC added beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-05 21:11:36 -04:00
co-authored by Claude Opus 5
parent bfc46d982e
commit 2808688fa1
10 changed files with 713 additions and 12 deletions
+4 -4
View File
@@ -37,7 +37,7 @@ Everything in between is somewhere on that line.
make test
```
Builds the five tools - and Voyager, where Raylib is installed - checks they compile under
Builds the six tools - and Voyager, where Raylib is installed - checks they compile under
strict ISO C, and runs the scripts in order. Takes a few seconds. Everything must pass; there are no expected failures at the
level of the suite, only tests that record an expected failure of the assembler.
@@ -45,7 +45,7 @@ level of the suite, only tests that record an expected failure of the assembler.
make sanitize
```
The same suite with the five tools rebuilt under AddressSanitizer and
The same suite with the six tools rebuilt under AddressSanitizer and
UndefinedBehaviorSanitizer. See [The Sanitizer Run](#the-sanitizer-run).
Individual scripts can be run on their own, from anywhere:
@@ -540,7 +540,7 @@ which stops the markers outliving the code they were about.
make sanitize
```
Rebuilds all five tools with `-fsanitize=address,undefined` and runs **the whole suite**
Rebuilds all six tools with `-fsanitize=address,undefined` and runs **the whole suite**
under them. What it reliably catches is invalid access: reads and writes off the end of an
array, use after free, leaks, and arithmetic the standard does not define.
@@ -552,7 +552,7 @@ deliberate `calloc`. The machine's Program and Data memories are static arrays,
sanitizers neither fill nor bound-check - which is the same fact, seen from a
different side, as the overrun blind spot below.
It runs everything because it used to not. It built all five tools sanitized and then ran
It runs everything because it used to not. It built all six tools sanitized and then ran
only `run.sh` and `terminal.sh`, so SplitDisk was compiled with the sanitizers and never
exercised, and `native.sh` - which drives the assembler and the emulator harder than
anything else here - was skipped entirely. Those are exactly where block arithmetic on disk