Repository navigation
EPIC: t27c's measured weak points -- 51 of 1155 generated Verilog modules synthesise to any logic #5905
Description
On 2026-10-04 we measured t27c against Rust, C/C++, Zig, Go, Swift and Lean, and against the HDL generators (Chisel/CIRCT, Clash, BSC, Calyx, Filament, Hardcaml, Verilator). Translation is not where t27c is behind:
| N = 5000 functions | microseconds per function |
|---|---|
clang -fsyntax-only |
24 |
| t27c typecheck + gen | 30 |
go tool compile |
95 |
rustc check |
154 |
swiftc -typecheck |
2085 |
That is about 200k lines/s on the real corpus (compile-all, 1178 specs, one thread, 1.1-1.5 s per backend). This epic is the other column: the measured weak points. Each one has a number, a way to reproduce it, and a child issue or an existing issue. It closes when every row has a closed issue or a written, documented exception.
This issue is a GOAL, not a bee task. It has no Boundary on purpose, so the Queen does not dispatch it. The work is filed as child issues, each with its own Boundary and acceptance criteria.
The weak points
| # | weak point | measured | issues |
|---|---|---|---|
| 1 | gen-verilog emits simulation-shaped Verilog | 51 of 1155 generated modules synthesise to any logic | #5904 (first step), #4549, #2869, #2241, #1258 |
| 2 | the generated corpus does not compile file by file | zig ast-check 759/1178, clang -fsyntax-only 385/1178, iverilog 381/1171 |
#5706, #5404 |
| 3 | a t27 function named like a Zig primitive (f16, f80, u8) generates Zig that does not compile (name shadows primitive) |
zig_ident does not escape primitive type names |
#2631 |
| 4 | the whole path runs at the speed of somebody else's backend | N=1000: t27c gen 35-46 ms, then zig test 1829 ms, rustc -O0 374 ms, clang -O0 188 ms |
own-backend MVP t27b (to be filed) |
| 5 | t27c itself builds slowly | clean release build 142.7 s; touching compiler.rs (44.6k lines) costs ~30 s of release rebuild |
#5899 / #5900, #5912, #5919, #5928, #5934, plus the PRs listed under 5 below |
1. Verilog: simulation-shaped, not synthesisable
COMPETITORS.md section 4.2 already says it: the module carries a fixed clk / rst_n / en / ready interface, drives assign ready = 1'b1;, and emits the spec's arithmetic as Verilog functions that nothing instantiates, so synthesis removes all of it. The README's gate list is parse, icarus-lowerable, icarus-simulate, icarus-cocotb, seal --save; synthesis is not a gate and never was. CIRCT and Chisel emit RTL that synthesises; this is where we are measurably behind them, not on speed.
Measured on the whole tree. t27c from master c8afc32b8 (the binary had #5900 applied, which changes only Cargo.toml), yosys 0.67, read_verilog -sv; synth -top <module> -flatten; stat, 180 s per spec:
| specs | |
|---|---|
specs/**/*.t27 |
1174 |
t27c gen-verilog produces a module |
1155 |
... of which carry // NO DATA PORTS |
1061 (91.9%) |
| yosys accepts the module | 833 |
| ... and synthesises it to 0 cells | 782 |
| ... and to more than 0 cells | 51 |
| yosys rejects the module | 299 |
yosys times out (all specs/ternary/gft_* and bitnet_neuron, which do have ports) |
23 |
The 299 rejections, by first yosys error: 121 unexpected TOK_REAL (a float lowered to Verilog real, which does not synthesise), 33 unexpected '@', 14 unexpected TOK_ID, 11 unexpected '.', the rest in groups of 8 or fewer.
By tree: specs/fpga/ 69 specs, 3 with logic; specs/igla/race/ 26, 8 with logic; the Verilog ports under specs/port/**/fpga/ 35, 4 with logic. Stage 7 (#4549) asks for hand-written Verilog to be re-authored as .t27 and regenerated (port tasks like #5877). Their acceptance criteria check that a module of the right name is generated, not that it synthesises, so a port can pass and produce no hardware.
The MAC case. The ternary MAC that works and is in the bitstream is hand-written (fpga/verilog/ternary_mac_synth.v); specs/igla/race/ternary_mac.t27 generates 0 cells. Adding the documented remedy, fn on_comb(acc: i32, a: i8, w: TernaryWeight) -> i32, still gives 0 cells: the entry point is refused because TernaryWeight is a struct, although the same file's functions lower it as [7:0]. Over scalars the same entry gives 509 cells. That is #5904, the first step.
The order the work goes in:
- gen-verilog refuses an on_comb over a struct or u1 although its own functions lower both -- ternary_mac stays at 0 cells #5904 / fix(gen-verilog): size struct and uN entry ports by one rule (#5904) #5955 -- struct and
uNentry-point ports, one definition of "sized" shared by backend and census (PR open: the MAC entry withw: TernaryWeightgoes from NO DATA PORTS toinput wire [7:0] w, 222 yosys cells) - floats: 121 specs fail on
real. A float port needs an encoding (raw IEEE bits, GF16, fixed point) and the code is right that this is a decision, not a default. Decide it once, write it down, then lower to it. - the 22 FORCED_SCALAR specs (
t27c entry-points): exactly one function could be the entry point and all its types are sized, so adding a forwardingon_combinvents nothing. A spec edit each, each reviewed. - synthesis becomes a gate: for every spec that declares
on_comboron_clock,yosys synthmust report more than 0 cells, as asuitecolumn next toicarus-simulate. - the 23 yosys timeouts: find out whether it is design size or something the emitter does (for example function inlining) before calling it a size problem.
- Stage 7 port tasks gain a synthesis criterion once the gate exists.
2. The generated corpus does not compile file by file
Each generated file checked alone, no cross-module imports, iverilog without -g2012, so these are lower bounds: zig ast-check 759/1178, clang -fsyntax-only 385/1178, iverilog 381/1171. Real syntax errors are among the failures: expected ',' after field in Zig, and array declarations C does not accept. Related: #5706 (ast-check VALID does not mean it compiles), #5404.
3. Zig primitive shadowing
A t27 function or parameter named f16, f80, u8 and similar becomes a Zig identifier that shadows a primitive, and Zig refuses it. zig_ident deliberately does not escape primitive type names. Tracked in #2631.
4. No backend of our own
Every number after t27c belongs to someone else's compiler. Full path at N=1000 functions (median ms):
| generated code -> backend | N=1000 | N=5000 |
|---|---|---|
| t27c gen | 35-46 | 168-208 |
.c -> clang -O0 |
188 | 900 |
.rs -> rustc -O0 |
374 | 2108 |
.zig -> zig test |
1829 | 3370 |
.v -> iverilog / yosys / verilator lint |
103 / 172 / 286 | 480 / 819 / 1281 |
A minimal native backend (t27b: AST -> IR -> AArch64 -> run, differential-tested against the Zig path) is being prototyped; it gets its own issue when it has numbers.
5. t27c build time
Clean release build 142.7 s. A large share of it is crates the compiler itself does not use:
- chore(deps): drop unused candle-core/candle-nn from t27c (clean build 142.7 s -> 97.9 s) #5900 (closes t27c: unused candle-core/candle-nn are back in bootstrap/Cargo.toml (#980 regression), ~30% of clean build #5899): drop the unused
candle-core/candle-nn-- clean build -31% - t27c: build the compiler without its web stack -- service crates behind cargo features #5912: split the HTTP client, server and TLS crates behind cargo features, so
cargo build -p t27cbuilds the compiler only (212 -> 49 crates) - t27c: reqwest on native-tls instead of rustls + aws-lc (drop the aws-lc-sys cmake build) #5919:
reqweston native-tls instead of aws-lc (noaws-lc-syscmake build;--features netbinary 15.6 -> 12.6 MB) - split
bootstrap/src/compiler.rs(44.6k lines; the release rebuild of that one module is the ~30 s) - CONTRIBUTING: document the fast t27c inner loop (check / debug / incremental release) #5934 / docs(contributing): document the fast t27c inner loop #5945: document the fast inner loop:
cargo check/ debug (~3 s), orCARGO_PROFILE_RELEASE_INCREMENTAL=true - build.rs FROZEN_HASH panic tells you to run a command that hits the same panic #5928: the
FROZEN_HASHpanic printscargo run --release -- frozen-digestas the remedy, and that command hits the same panic after any edit tocompiler.rs
Reproduce section 1
for s in $(find specs -name '*.t27' | sort); do
t27c gen-verilog "$s" > /tmp/x.v || continue
top=$(grep -m1 -oE '^ *module +[A-Za-z_0-9]+' /tmp/x.v | awk '{print $2}')
printf '%s ' "$s"
yosys -p "read_verilog -sv /tmp/x.v; synth -top $top -flatten; stat" 2>&1 \
| grep -E '^ +[0-9]+ +cells$' || echo "0 cells or error"
done
In yosys 0.67's stat, a design with no cells prints no cells line at all.
Activity
fix(gen-verilog): name a refused on_clock entry point in the .v file (g…
fix(gen): signed and float % lowers to @Rem (Closes #5973)
fix(t27c): refuse a Rust match block the parser kept as text; rewrite…
fix(t27c): refuse macro calls and typeless fields; rewrite eight Mark…
fix(t27c): read a dotted module name and use path whole
seals: regenerate for the physics move
specs: move the three real-valued models from fpga/ to physics/ (#9233)