YANC emblem: three chevrons pointing forward
The compilers of SAPHO

YANC

Yet Another Compiler

Write a program in C± or C++, and YANC carries it down, stage by stage, to a processor: the assembly, the synthesizable Verilog core, the memory images, the testbench. AURORA drives it with buttons. A shell script drives it just as well.

source » assembly » silicon »
Active development v5.2 C · Flex · Bison MIT  Windows SAPHO AURORA YANC
» 01 The name

The name is a joke from the compiler tradition. The job is not.

In 1975, Bell Labs named its parser generator yacc: Yet Another Compiler-Compiler. Naming a serious tool as if it were one more of the same is an old habit of people who build compilers, and YANC, Yet Another Compiler, stands in that lineage on purpose. What it does is less usual. Most compilers translate a program into instructions for a processor that already exists. YANC reads your program and emits the processor itself: a SAPHO core in synthesizable Verilog, shaped around that one algorithm, with its program and data memories and a testbench ready to run.

The suite is five small binaries written in C, with lexers generated by Flex and parsers by Bison, built by GCC on Windows, plus two helpers that format waveforms for viewing. Its error messages come in two languages, Portuguese first and English on a flag, because the lab that wrote it teaches in Portuguese and publishes in English. Every one of the hundred-plus test programs in the repository ships with a frozen golden.asm and golden simulation output, so a change to any compiler has to reproduce the whole suite, byte for byte, before it lands.

» 02 Two languages, one core

Start from or from C++. Arrive at the same processor.

C± (read it C plus minus) is the house language: C at the base, extended for signal processing and physics, and cut of what a processor cannot honour. Its first lines are hardware, not code. #NUBITS picks the width of the datapath, #NBMANT and #NBEXPO define the floating-point format, and from there down you write ordinary loops and functions, with a native comp complex type, literals like 0.7071 + 0.7071i, and built-ins from sqrt to an FFT whose bit-reversed read has its own syntax, data[j).

C++ is the second front-end, and it is not a toy. cppcomp compiles a real subset, structs by value, destructors on early return, function pointers, std::array, lambdas as global initializers, through the same road: a #pragma yanc names the processor, and the output is the same assembly the C± compiler emits. Both languages meet in one backend and one core. Processor creation in C++ is now rolling into the platform as a first-class flow.

Sqrt.cmm
#PRNAME Sqrt
#NUBITS 32
#NBMANT 23
#NBEXPO 8
#NDSTAC 5
#SDEPTH 5

float my_sqrt(float num)
{
    if (num == 0.0) return 0.0;

    int v = (((num << 1) >>> 24) + 22) >>> 1;  // get the exponent
        v = ((((v-22) << 23) + (1 << 22)) << 1) >> 1;

    float x; copy(v,x);

    x = 0.5 * (x+num/x);   // four Newton steps
    x = 0.5 * (x+num/x);
    x = 0.5 * (x+num/x);
    x = 0.5 * (x+num/x);

    return x;
}

From the test suite, trimmed. The header is the hardware; the body is a square root by Newton's method, seeded by a bit-trick on the float's exponent.

test61.cpp C++
#pragma yanc prname test61
#include <array>

constexpr int M_PULSE = 15;
constexpr int C_PEAK  = 5;

// a lambda builds the pulse shape at compile time
const std::array<float, M_PULSE> TRIANGLE_INIT = []() {
    std::array<float, M_PULSE> h = {};
    constexpr float base[5] = { 0.25f, 0.5f, 1.0f, 0.5f, 0.25f };
    for (int k = 0; k < 5; ++k) {
        int idx = C_PEAK + (k - 2);
        if (idx >= 0 && idx < M_PULSE) h[idx] = base[k];
    }
    return h;
}();

void main(void) {
    for (int k = 0; k < M_PULSE; k = k + 1)
        out(0, (int)(TRIANGLE_INIT[k] * 1000.0f));
}

Also from the test suite, trimmed. Modern C++ shaping a triangular pulse, the kind of waveform a calorimeter front-end sees, compiled to the same SAPHO assembly.

one assembly · one backend · one processor
» 03 The pipeline

Five binaries, one descent

Each stage is a separate program with one job, in the old Unix way. The front-ends translate your language into SAPHO assembly. appcomp makes a first pass over that assembly and resolves every variable and label to an address. asmcomp then writes the hardware: the processor, its memories, its testbench. From there it is simulation and reading waves.

trace metadata: cmm_log.txt · pc_*_mem.txt .cmm .cpp cmmcomp C± → assembly cpppp preprocess cppcomp C++ → assembly proc.asm appcomp resolves every address asmcomp writes the hardware proc.v *.mif proc_tb.v AFTER YANC simulate Icarus · Verilator vcd watch Surfer · GTKWave C++ assembly onward trace metadata
The whole descent, drawn as it runs. The dotted rail is the part most toolchains do not have: the front-ends write cmm_log.txt and pc_*_mem.txt alongside the assembly, and the waveform viewer reads them to show your source code executing, line by line.

After YANC: simulate and watch

Icarus Verilogsimulation
Verilatorfast simulation
Surferwaveforms
GTKWavewaveforms
» 04 Native physics

A language that writes ⟨bra|ket⟩

C± reserves the # operator for Dirac assignment. Matrices and vectors move in the bra-ket notation physics already writes on paper, after Dirac's 1958 formulation, and the compiler turns each line into the exact sequence of multiply-accumulates it means. These lines are from the test suite, verbatim:

A # 1.0|I|;
a # |0⟩;
a # |B|a⟩;
a # 2.0|b⟩;
A # |a⟩⟨b|;
a # 0.001|in(0)⟩;
A # 1.0|I|initialize A as the identity matrix
a # |0⟩zero the vector
a # |B|a⟩matrix times vector: a = B·a
a # 2.0|b⟩scale a vector: a = 2b
A # |a⟩⟨b|outer product: A = a·bᵀ
a # 0.001|in(0)⟩read an input port into a vector, scaled

Not a demo syntax. The declaration forms carry a real recursive-least-squares filter in the proc_rls test, the kind of adaptive filter the lab puts on FPGAs.

» 05 Watch it run

The waveform knows your source code

Because the compilers emit a table mapping every program-counter value back to the C± line that produced it, and every variable to its data-memory address, the waveform is not a wall of buses. Alongside the clock you see the source line currently executing, the assembly opcode under it, and each of your variables as a proper signal, updating as the processor writes it. Debugging feels like stepping through code, except the thing stepping is hardware.

All of that visibility lives behind a simulation-only flag, YANC_SIM_VIS. Turn it off, or synthesize for real, and it costs zero gates.

AURORA opens these traces in Surfer or in GTKWave; the lab maintains a fork of each, and today the two are equals in ease of use. The captures on this page come from Surfer.

Surfer showing a SAPHO FFT processor running: clock, I/O outputs, the assembly opcode track and the exact C± line executing
screenshot: source-level trace in Surfer
The FFT test in Surfer. Under the clock and the outputs, the Instructions group carries the two tracks the metadata makes possible: the opcode (LOD, F_MLT, F2I, OUT) and the C± line executing, here fout(1,1000.0*imag(data[1)));
» 06 Hardware that fits

Nothing you don't use gets built

The SAPHO instruction set is parameterized by the compiler. If your program never divides, the divider is never synthesized; if it never touches floating point, there is no floating-point unit. Word width is yours to choose, and shaving three bits off a mantissa is a real decision with a real payoff in logic. This is the compiler's half of SAPHO's thesis, and it has numbers behind it, measured on the ATLAS Tile Calorimeter work that YANC was born to serve.

140 → 26 processors needed for online energy reconstruction, when the multi-core array scales from 320 MHz toward modern clock rates. Fewer cores, same physics.
−65% logic elements at the scaled operating point, for the same reconstruction task.
32 → 29 bits one word-width decision in the same study: three bits off the datapath, 6% of the logic back, no loss in the result.
+2 instructions the @ (PSET) and /> (NORM) operators entered the ISA from that paper, cutting branches and simplifying fixed-gain division. The language grows where the physics pushes it.

Get YANC

If you use AURORA, you already have it: the IDE downloads the compilers on first launch and keeps them current. Standalone, grab the binaries from the latest release, a Windows zip, and drive the whole descent from one script.

shell
# once: fetch toolchain deps
$ Scripts/setup.sh

# the whole descent, one command (bundled example: an order-8 FFT)
$ Scripts/single_proc.sh
$ Scripts/single_proc.sh --sim verilator

# the C++ flavor
$ Scripts/single_proc_cpp.sh