Repository navigation
gen-verilog cannot instantiate device primitives (STARTUPE2, BUFG, BSCANE2, LUTs), so on-die designs need a hand-written wrapper #8660
Description
Defect under the self-host epic (see #5980). Found while bringing up die D (see #8635, #8655).
Problem
t27c gen-verilog has no way to instantiate a device primitive: STARTUPE2, BUFG, BSCANE2, DNA_PORT, LUT1 or LUT2, with parameters, port connections and attributes such as keep and dont_touch. It also has no way to instantiate another generated module, or many copies of one. So every on-die design needs a hand-written Verilog wrapper. Hand-written foreign code may only shrink (see #8236), so such a wrapper cannot be committed, and the build cannot be reproduced from the tree.
HirInstance and emit_instance exist in bootstrap/src/compiler.rs, but no lowering from the AST ever builds one; only a unit test does.
Minimal repro (master 1f6fc41, t27c lab)
prim_repro.t27: the smallest design that needs a primitive. Its generated header is only clk, rst_n, en, ready, beat, and nothing can say that clk is STARTUPE2's CFGMCLK.
// SPDX-License-Identifier: Apache-2.0
module PrimRepro;
// The smallest design that needs a primitive: a heartbeat clocked by STARTUPE2's CFGMCLK.
// There is no way to say "this module's clk is STARTUPE2.CFGMCLK", nor to place a BSCANE2
// or a LUT1, so a hand-written wrapper has to instantiate them.
var beat : u32 = 0
fn on_clock() { beat = beat + 1; }
test beat_counts { beat = 0; on_clock(); assert_eq(beat, 1); }
prim_try.t27: calling the primitive by name. gen-verilog exits 0 and emits mclk <= STARTUPE2(1'b0);, a call to an undeclared Verilog function that yosys rejects. Meanwhile t27c test-report says BLOCKED ... use of undeclared identifier 'STARTUPE2'. So the Verilog backend also accepts an undeclared callee silently.
// SPDX-License-Identifier: Apache-2.0
module PrimTry;
var beat : u32 = 0
var mclk : bool = false
fn on_clock() {
mclk = STARTUPE2(false);
beat = beat + 1;
}
test beat_counts { beat = 0; on_clock(); assert_eq(beat, 1); }
Acceptance
gen-verilog can emit this wrapper for specs/fpga/die_probe.t27 from .t27. It is the 27-line scratch file that ran on die D, kept out of the tree:
`default_nettype none
// Scratch wrapper for specs/fpga/die_probe.t27 (issue #8635): primitives and wiring only, no logic.
module die_probe_top #(parameter integer JTAG_CHAIN_N = 1);
wire mclk, clk, run, tdo, tck, sel, cap, shf, tdi;
wire [31:0] da, db;
STARTUPE2 #(.PROG_USR("FALSE"), .SIM_CCLK_FREQ(10.0)) startup (.CFGCLK(), .CFGMCLK(mclk), .EOS(), .PREQ(),
.CLK(1'b0), .GSR(1'b0), .GTS(1'b0), .KEYCLEARB(1'b0), .PACK(1'b0), .USRCCLKO(1'b0), .USRCCLKTS(1'b0),
.USRDONEO(1'b1), .USRDONETS(1'b1));
BUFG gclk (.I(mclk), .O(clk));
BSCANE2 #(.JTAG_CHAIN(JTAG_CHAIN_N)) bscan (.CAPTURE(cap), .DRCK(), .RESET(), .RUNTEST(), .SEL(sel), .SHIFT(shf),
.TCK(tck), .TDI(tdi), .TMS(), .UPDATE(), .TDO(tdo));
genvar k;
generate for (k = 0; k < 64; k = k + 1) begin : ro
(* keep, dont_touch *) wire [4:0] n;
(* keep, dont_touch *) LUT2 #(.INIT(4'h7)) g (.I0(run), .I1(n[4]), .O(n[0]));
(* keep, dont_touch *) LUT1 #(.INIT(2'h1)) i1 (.I0(n[0]), .O(n[1]));
(* keep, dont_touch *) LUT1 #(.INIT(2'h1)) i2 (.I0(n[1]), .O(n[2]));
(* keep, dont_touch *) LUT1 #(.INIT(2'h1)) i3 (.I0(n[2]), .O(n[3]));
(* keep, dont_touch *) LUT1 #(.INIT(2'h1)) i4 (.I0(n[3]), .O(n[4]));
wire d;
RoRace r (.clk(n[4]), .rst_n(1'b1), .en(1'b1), .ready(), .count(), .done(d));
if (k % 2 == 0) begin : a assign da[k / 2] = d; end else begin : b assign db[k / 2] = d; end
end endgenerate
DieProbe p (.clk(clk), .rst_n(1'b1), .en(1'b1), .tck(tck), .sel(sel), .capture(cap), .shift(shf), .tdi(tdi),
.done_a(da), .done_b(db), .ready(), .run(run), .tdo(tdo));
endmodule
`default_nettype wire
gen-verilog must also refuse a call to an undeclared function, as the Zig path does.
User Scenarios
- Given a
.t27spec that declares a module needing device primitives (STARTUPE2, BUFG, BSCANE2, LUT1, LUT2) with parameters, port connections, andkeep/dont_touchattributes, Whent27c gen-verilogruns on that spec, Then the emitted Verilog contains correct primitive instantiations matching the acceptance wrapper forspecs/fpga/die_probe.t27. - Given a
.t27spec that calls an undeclared identifier as if it were a function (e.g.,STARTUPE2(false)), Whent27c gen-verilogruns, Then the compiler rejects the spec with an error (exit non-zero) instead of emitting a call to an undeclared Verilog function.
Requirements
- FR-001: The compiler MUST lower primitive instantiation syntax from the AST into
HirInstancenodes with parameters, port mappings, and attributes (keep,dont_touch). - FR-002: The Verilog backend MUST emit device primitives (STARTUPE2, BUFG, BSCANE2, DNA_PORT, LUT1, LUT2) with full parameterization, port connections, and attributes exactly as shown in the acceptance wrapper.
- FR-003: The Verilog backend MUST support
generate/forloops withgenvarand indexed instantiation to replicate the 64-instance ring-oscillator array. - FR-004: The Verilog backend MUST support instantiating generated modules (e.g.,
DieProbe,RoRace) with positional and named port connections. - FR-005: The compiler MUST reject calls to undeclared identifiers (such as
STARTUPE2used as a function) at the HIR lowering or type-checking stage, causingt27c gen-verilogto exit non-zero. - FR-006: The generated Verilog for
specs/fpga/die_probe.t27MUST be functionally equivalent to the 27-line acceptance wrapper (modulo formatting anddefault_nettypedirectives).
Boundary
- specs/fpga/die_probe.t27
- bootstrap/src/compiler.rs
- proofs/lean4/Trinity/IcarusLowerable/Verilog.lean
Activity
gHashTag commentedon Oct 10, 2026
A second design blocked by this: the actor engine. It comes from the FPGA lane of gHashTag/trios#1763.
specs/queen/actors_hw.t27(QueenActorsHw, #8353) has yosys numbers only. The lane's next steps need a top that gen-verilog cannot emit today: a place-and-route with a timing report onxc7a200tfbg484-2, then a run on the AX7203 throught27c bench-agent.Still reproduces on t27c 0.5.2 (master c31c059):
prim_try.t27from the body exits 0 and emitsmclk <= STARTUPE2(1'b0);.HirInstanceis still built only in#[test] fn test_emit_instance.Why the engine needs a top before it can even be placed. Every
varofQueenActorsHwbecomes an output port, so the generated module has 254 input bits and 258 output bits, 512 in all. Counted from prjxray-dbpackage_pins.csv, the XC7A200T packages have these user IOs:None can hold 512. nextpnr-xilinx's IO placer gives every unconstrained port a pin and stops with "IO placer ran out of available IOs". So the engine reaches a die, and gets a timing report, only inside a top that keeps those ports internal.
What that top holds, beyond the die_probe case above:
opand the operands from its vectors and checks the four answer registers, andQueenActorsHw(FR-004);The self-test could call the
hw_decisions directly instead of instantiating the engine. But gen-verilog does not lower a function brought in byuseeither, and still exits 0; that is #8769.Until one of these lands, the actor engine stops at synthesis. There is no generated JTAG top, no place-and-route, and no bench job.
🤖 Generated with Claude Code
t27b: verilog -- a Verilog path decided in .t27 plans: extern modules…
gHashTag commentedon Oct 11, 2026
Closing: the need is met in
.t27on master, not inbootstrap/src/compiler.rs.specs/fpga/xc7_primitives.t27(see actors_hw on the die: a self-test and a JTAG top, generated by t27b verilog from .t27 #8826).t27b verilogwrites their instances (see t27b: verilog -- a Verilog path decided in .t27 plans: extern modules, instances, declared ports, the use closure (Closes #8796) #8820), and actors_hw got its on-die top this way.specs/fpga/xc7_primitives.t27only.Why close rather than keep it open: this issue's Boundary is
bootstrap/src/compiler.rs. The Queen holds that file whole while this issue is in flight, and per the #9021 replay it alone shut out 122 other candidates. The fix it asked for would also grow hand-written Rust in a file that may only shrink (see #8236).spec(queen): a dispatch holds the files of its issue as it stands now…