Patch notes

34 updateslatest: #35source

1.2 (in progress, from 24 September 2026)

Every update below comes with the tests that prove it: what each test checks, and the numbers before and after.

#1 · The CLI and the guide say the same thing · 24 September

dewfpga uninstall says what it removes, and removes only its own. It used to print removing: ~/fpga ~/.dewfpga <link> and then delete five entries inside ~/fpga, not the folder. It also deleted Homebrew's dewfpga link even when that link pointed to another copy of the CLI. Now it lists every path it deletes, touches the link only when the link points to itself, leaves anything else in ~/fpga alone, and says nothing to remove when there is nothing. Tried on a fake install with a student project inside ~/fpga: the project file survived, the real Homebrew link survived, a second run printed nothing to remove, and an empty folder was removed.

Version 1.2.0. The CLI had said 0.1.0 since 19 September. The patch number and dewfpga --version are now the same number.

The guide stopped contradicting itself.

Tried: writing the right count into section 7. Dropped it, because the list grows every time a new student repo turns up a new case. The text now says what each case does instead of how many there are. The HTML page and the PDF (12 pages) were rebuilt from the same source.

README.tr described rules that stopped being true on 21 September. It said the top module is chosen by its .xdc file, sim needs <top>_tb.sv, two .sv files in one folder is an error, and dewfpga check has six rows. The top now comes from the module hierarchy, the testbench is recognized by its content, file names are free, and check prints nine rows. README.tr is now a translation of the English README.

Disk space, measured.

Tests. test/run.sh: 51 passed, 0 failed before the change (81 s) and 51 passed, 0 failed after. One test was renamed: "top found from xdc" is now "top found from the hierarchy", which is what it has checked since 21 September.

Not done here. The README still says "46 checks"; update #6 locks that number to the test suite.

#2 · 132 SystemVerilog probes run through the product, every stage checked · 25–26 September

In numbers. Before #2, test/run.sh passed 51 checks, and the 43 probes of the 2509 measurement lived outside the repo, run by a script that called yosys directly (33 of 43 synthesized). After: test/run.sh passes 178, fails 0, and counts 87 known gaps apart; its probe section runs 132 probes through the CLI itself. Synthesis 69/132, all four stages 30/132, and 29 of the 111 probes Vivado supports pass all four: the other 82 are our gap. 17 probes build a bitstream whose netlist fails: 15 do something else (a silent wrong), and 2 (35, an MMCM, and 97, a block RAM) have no simulation model to check them against. The product did not change in #2 (bin/ and templates/ are the same as before); #3 and #4 are scored against these numbers. Seven break rounds sent 195 reports; every one was reproduced (one of round 7's in part), seven side claims inside them did not hold (below), and what was not fixed here is listed with the update that owns it. Every probe, with its construct, Vivado's status, the source and the stage that fails, is listed under "Every probe" below.

What a student may write is now part of the test suite. test/sv/ holds 132 probes, one construct each: design.sv (one probe has design.v) with module top, tb.sv with module tb that prints PASS, sometimes a .svh, a .mem, a package, interface, submodule or netlist-form module in its own .sv, or the lab's own Basys3_Master.xdc with only some pins uncommented (96b; 93's one design file is counter.sv, as its lab has it). 43 came from the 2509 measurement (svprobe); the comments that named a student and a student's file are gone. 8 were added in the first break round, 18 in the second, 23 in the third, 8 in the fourth, 15 in the fifth, 8 in the sixth and 9 in the seventh (below), for cases the 43 did not cover. Every probe goes through four stages, all of them through bin/dewfpga and templates/Makefile, in a temp folder that holds only the probe's sources:

test/sv/expect.tsv has one row per probe: the construct, whether Vivado accepts it, the source for that, what each stage does today, and what the student sees. test/sv/run.sh fails on any difference, in both directions: a stage that stops passing, and a stage that starts passing without the table saying so. It also fails on a row without a source, on a file:line in the today column that no stage printed, and on a message the today column quotes ('...' or "...") that no stage printed word for word (round 4). It prints four numbers: synthesis, all four stages, Vivado-supported probes that pass all four, and known gaps. A known gap does what the table says, but either Vivado supports it and a stage fails, or its bitstream builds while its netlist fails (a silent wrong).

test/run.sh runs it and prints one line per probe: PASS, GAP (counted on its own, not as a pass; it does not fail the run) or FAIL. A probe that fails where Vivado refuses the code, or where what Vivado does is unverified, is a PASS, unless its bitstream builds from a netlist that fails: that silent wrong is a GAP whatever Vivado does (03, 03b, 05 and 39 are unverified and gaps; this said PASS until round 5, and so did test/run.sh's header). CI runs the same and keeps the rows as the sv-results artifact. With the measured yosys and iverilog, when #3 or #4 closes a gap the run fails until expect.tsv records the new result, so every change in these numbers is an edit of the table. With another yosys (CI installs 0.68+post) a stage that starts passing is only a note, so the table is updated from a local run.

Found on the way: the CLI's top finder misses an array of instances. inv u_inv [3:0] (...) is in UG901's list of supported Verilog, and it compiles through the CLI. What fails is the automatic top: the scanner does not read inv u_inv [3:0] ( as an instance, so inv looks like a second top, and dewfpga bit prints 2 modules could be the top (nothing instantiates them): inv, top. Name it: dewfpga bit inv, which names the wrong one. dewfpga bit top builds it (4 LUT), and so does plain dewfpga bit when the file is named top.sv. The old runner called yosys directly and never saw this, so synthesis through the product is 32/43, not the 33/43 the plan started from. The fix is #4's.

The Vivado column, checked row by row. Sources: UG901 v2023.2 (chapter, table, page), UG953 v2022.2 (the 7-series primitive guide), and Vivado logs from public CS223 lab repos. A quoted log line keeps the message; the student's module and signal names are replaced by a description. A forum is marked as a weak source. The 132: 111 supported, 3 not supported, 18 unverified (the 123 of round 6: 105, 3, 15; the 115 of round 5: 99, 3, 13; the 100 of round 4: 84, 3, 13). Changes from the 2509 measurement:

Tried, and why it changed.

Break round 1. Three independent breakers (a student who writes what Vivado accepts, a mutation tester, a regression reader) ran against #2 and sent 38 reports; some are the same break seen by two or three of them. Every one was reproduced in a scratch folder. Two side claims inside them did not hold as written: one breaker counted 20 probes hidden behind passed 94, and the table has 18 (43 minus the 25 that passed all four; 13 of them Vivado-supported, as the breaker said); another's aside, that CI's golden check would also miss every LUT INIT zeroed before nextpnr, came from reading test/run.sh, and it was not run under CI's yosys 0.68 (not verified). What changed:

Tried, and dropped:

Found and left to later updates (reproduced, not fixed here):

Break round 2. The same three kinds of breaker ran against the tree after round 1 and sent 30 reports; two of them are the same break (13b). Each was run in a scratch folder: all 30 reproduced. Fixed here:

Tried, and dropped:

Found and left to later updates (reproduced, not fixed here):

Break round 3. The same three kinds of breaker ran against the tree after round 2 and sent 25 reports. Each was run in a scratch folder: all 25 reproduced. Fixed here:

Tried, and dropped:

Found and left to later updates (reproduced, not fixed here):

Break round 4. The same three kinds of breaker ran against the tree after round 3 and sent 23 reports; two of them are the same break (a netlist that lost its reset), so 22 breaks. Each was run in a scratch folder: all 22 reproduced. (This said "sent 22 reports" until the final count.) A first attempt at this round stopped halfway (a network outage); its eqv.py rewrite and eight probe drafts were checked here, one draft was rewritten (81) and the rest were finished. Fixed here:

Tried, and dropped:

Found and left to later updates (reproduced, not fixed here):

Break round 5. The same three kinds of breaker ran against the tree after round 4 and sent 22 reports. Each was run in a scratch folder: all 22 reproduced. Two side claims inside them did not: that a course project hits the same eqv.py bug through a wire named message (grep -rlE 'message' over the corpus .sv and .v files, also without case: 0 files; the Lab 4 shape the fix covers was found in one repo's logs and is the case regport), and that the Vivado 2021.2 lab writes its range-only port in an always_ff (it is an always_comb, as 08c's source says). A first attempt at this round stopped before it changed anything (a network outage). The eight probe folders a round-4 attempt had left without rows (13c, 18c, 21b, 22c, 22d, 22e, 64b, 81) already had their rows when this round started: 100 folders, 100 rows, and the runner's folder check agreed. Fixed here:

Tried, and dropped:

Found and left to later updates (reproduced, not fixed here):

Break round 6. The same three kinds of breaker ran against the tree after round 5 and sent 36 reports. Each was run in a scratch folder: all 36 reproduced (a mutation report by its mutation and the checks that could catch it, not by a full suite run each; one had a moved line: the lenient-verdict mutation is on test/run.sh line 130, not 126). A first attempt at this round stopped before it changed anything (a network outage). The eight probe folders the task named as lacking rows (13c, 18c, 21b, 22c, 22d, 22e, 64b, 81) have had their rows since round 4: 115 folders, 115 rows. Fixed here:

Tried, and dropped:

Found and left to later updates (reproduced, not fixed here):

Break round 7. The same three kinds of breaker ran against the tree after round 6 and sent 21 reports. Each was run in a scratch folder: 20 reproduced as written and one in part (the port-line comment, below); a mutation report by its mutation and the cases or checks that could catch it, not by a full suite run each. Three side claims did not hold (below). A first attempt at this round stopped before it changed anything (a network outage). The eight probe folders the task named as lacking rows (13c, 18c, 21b, 22c, 22d, 22e, 64b, 81) have had their rows since round 4, as round 6 found too: 123 folders and 123 rows before this round, 132 and 132 after. Fixed here:

Side claims that did not hold:

Tried, and dropped:

Found and left to later updates (reproduced, not fixed here):

What the breakers tried that held. Every round, the breakers also reported what they tried and could not break. Those runs are part of the evidence, so they are listed here, round by round.

Tests. test/run.sh: 51 passed, 0 failed before #2 (78 s); passed 94, failed 0 after #2's first draft (228 s). After break round 1: passed 86, failed 0, known gaps 23 (356 s). After break round 2: passed 99, failed 0, known gaps 41 (305 s). After break round 3: passed 119, failed 0, known gaps 56 (409 s). After break round 4: passed 138, failed 0, known gaps 63 (556 s). After break round 5: passed 148, failed 0, known gaps 77 (474 s). After break round 6: passed 166, failed 0, known gaps 82 (533 s; the final run of that round, 528 s, the same). After break round 7: passed 178, failed 0, known gaps 87 (583 s). The 178 are the 51 from before, 4 hygiene checks, 24 checks of the probe runner itself, 54 checks of eqv.py and eqv.sh, and the 45 probes that are not known gaps (round 4: 63 stopped being a gap and the 8 new probes were all gaps; round 5: of the 15 new probes, 14 are gaps and 84 passes; round 6: of the 8 new probes, 5 are gaps, and 63b, 91 and 92 pass; round 7: of the 9 new probes, 5 are gaps, and 31b, 95, 98 and 99 pass). The final run on 26 September, after these notes were written: passed 178, failed 0, known gaps 87 (582 s), the same (evidence below).

The four stages today, before #3 and #4 change anything:

rtl synth netlist bit
pass, the 43 from 2509 35 32 27 32
pass, the 51 after round 1 37 35 28 35
pass, all 69 49 38 30 37
fail, all 69 20 31 8 32
not run (no netlist), all 69 31
pass, all 92 60 47 37 44
fail, all 92 32 45 10 48
not run (no netlist), all 92 45
pass, all 100 62 52 39 49
fail, all 100 38 48 13 51
not run (no netlist), all 100 48
pass, all 115 68 57 42 53
fail, all 115 47 58 15 62
not run (no netlist), all 115 58
pass, all 123 75 62 45 57
fail, all 123 48 61 17 66
not run (no netlist), all 123 61
pass, all 132 84 69 49 60
fail, all 132 48 63 20 72
not run (no netlist), all 132 63

Every probe. The 132 folders of test/sv, from the final run's rows (they equal expect.tsv). Per probe: the id, Vivado's status (supported, not supported, unverified), the verdict (ok: does what the table says and is not a known gap; gap: a known gap), the stages that fail, the runner's note if any, the construct, and where the Vivado status comes from. "Vivado 2020.1 log" is a Vivado log in a public CS223 (or CS-224) lab repo or in the course's example project, quoted in expect.tsv without the student's names; UG901 is the Vivado synthesis guide, UG953 the 7-series primitive guide. expect.tsv holds the full quote for every source and what the student sees today. Summary: 45 ok, 87 gap; 111 supported, 3 not supported, 18 unverified.

01_latch_comb                supported     gap  fails: synth, bit (netlist not run)
    always_comb with an if and no else (a latch is inferred)
    source: Vivado 2020.1 log; UG901 v2023.2 p.84
02_unnamed_inst              supported     gap  fails: rtl, synth, bit (netlist not run)
    module instance with no instance name: inv(sw[0], led[0]);
    source: Vivado 2020.1 log
03_async_reset_nonconst      unverified    gap  fails: netlist
    always_ff @(posedge clk or posedge btnC) if (btnC) q <= sw[3:0]; (async load from a signal)
    source: UG901 v2023.2 p.81-82
03b_async_reset_selfref      unverified    gap  fails: netlist
    a button used as an async 'reset' that loads cnt+1 (the button-as-clock counter)
    source: UG901 v2023.2 p.81-82
04_dual_edge                 unverified    ok   fails: synth, bit (netlist not run)
    always @(posedge clk or negedge clk) cnt <= cnt + 1;
    source: UG901 v2023.2 p.81; forum posts (weak source)
05_two_block_driver          unverified    gap  fails: netlist
    one reg written from two always blocks on different edges (posedge btnU / posedge btnC)
    source: forum posts (weak source)
06_missing_end_default       not supported ok   fails: rtl, synth, bit (netlist not run)
    a case item's begin (S2: begin) never closed before default: (invalid code; a typo seen in a
    CS223 lab)
    source: IEEE 1800-2017 A.6.7
07_enum_packed_array         supported     ok   passes all four
    packed array of enums: statetype [1:0] state, nextstate;
    source: Vivado 2020.1 log; UG901 v2023.2 p.285
08_port_dir_inherit          supported     ok   passes all four
    ANSI port inheriting its direction: output [6:0] seg, logic dp, (the course's seven-segment
    file)
    source: Vivado 2020.1 log
08b_port_dir_comment         supported     gap  fails: synth, bit (netlist not run)
    the course's port line (output [6:0] seg, logic dp) with comments in the port list: one after
    seg, one with a ';' in it
    source: Vivado 2020.1 log; IEEE 1800-2017 5.4
08c_port_dir_inherit_proc    supported     gap  fails: rtl
    the course's port line shape with a range only, written in an always block: output logic [6:0]
    seg, [3:0] an; with an set in an always_comb
    source: Vivado 2021.2 log; IEEE 1800-2017 23.2.2.3; slang
09_always_procs              supported     ok   passes all four
    always_ff / always_comb / always_latch
    source: UG901 v2023.2 p.286
10_enum_fsm                  supported     ok   passes all four
    typedef enum FSM with an explicit base type and with the default (int), plus a light_t'(l+1)
    cast
    source: UG901 v2023.2 p.285
11_enum_methods              supported     gap  fails: synth, bit (netlist not run)
    enum methods s.first() / s.next() / s.last()
    source: UG901 v2023.2 p.285
12_struct_packed             supported     ok   passes all four
    typedef struct packed, nested, member read and write
    source: UG901 v2023.2 p.285
12b_struct_assign_pattern    supported     gap  fails: rtl, synth, bit (netlist not run)
    named assignment pattern '{hi: x, lo: y} and '{default: 0}
    source: UG901 v2023.2 p.286
12c_struct_array_var_index   supported     gap  fails: synth, bit (netlist not run)
    a packed array of structs indexed by a signal, then a member select: cars[sw[15]].x
    source: UG901 v2023.2 p.285, p.287
13_arrays_multidim           supported     ok   passes all four
    packed 2-D, unpacked, unpacked 2-D arrays, for (int i..) in always_comb
    source: UG901 v2023.2 p.285
13b_unpacked_array_port      supported     gap  fails: synth, bit (netlist not run)
    an unpacked array as a module port: input logic [3:0] v [0:3]
    source: UG901 v2023.2 p.161, p.162, p.164; UG901 v2022.2 p.160-162; IEEE 1800-2017 A.1.3
13c_unpacked_array_concat    supported     gap  fails: rtl, synth, bit (netlist not run)
    an unpacked array concatenation: assign arr = {sw[3:0], sw[7:4]}; to logic [3:0] arr [0:1]
    source: UG901 v2023.2 p.286
13d_packed_2d_var_index      supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    a packed 2-D array indexed by a variable, then a bit or part select: grid[i][j] <= ... in a
    loop, grid[sw[13:12]][3:1]
    source: UG901 v2023.2 p.285; Vivado 2019.1 log
14_params                    supported     ok   passes all four
    typed parameters, one depending on another, localparam, #(.W(8),.K(3)) and positional #(4)
    source: UG901 v2023.2 p.288
15_generate                  supported     ok   passes all four
    generate for with genvar (named block), for (genvar j..) with if-generate, case-generate
    source: UG901 v2023.2 p.288
16_func_task                 supported     ok   passes all four
    function automatic with a loop, plain function, task automatic called in always_comb
    source: UG901 v2023.2 p.287
16b_func_return              supported     gap  fails: synth, bit (netlist not run)
    'return expr;' inside a function
    source: UG901 v2023.2 p.287
16c_while_loop               supported     gap  fails: synth, bit (netlist not run)
    while loop in an always block (UG901 example)
    source: UG901 v2023.2 p.257, p.258
17_clog2_bits_cast           supported     ok   passes all four
    $clog2 in a localparam and a range, $bits, size cast 4'(expr)
    source: UG901 v2023.2 p.240, p.285
18_unique_priority           supported     ok   passes all four
    unique case / priority casez
    source: UG901 v2023.2 p.287, p.279; UG901 v2022.2 p.290
18b_unique_priority_if       supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    unique if / priority if
    source: UG901 v2023.2 p.287
18c_unique0_if               supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    unique0 if: an if/else if chain where no branch may match
    source: UG901 v2023.2 p.287
19_casez_casex               supported     ok   passes all four
    casez with ? wildcards, casex
    source: UG901 v2023.2 p.279
20_inside                    not supported ok   fails: rtl, synth, bit (netlist not run)
    set membership: x inside {1,3,[8:10]} and case (x) inside
    source: UG901 v2023.2 p.287; UG901 v2022.2 Table 8-1
21_incdec_assignops          supported     ok   passes all four
    x++ and x += in always_comb, k++ inside always_ff, for (int i..;i++) with c++
    source: UG901 v2023.2 p.286
21b_assign_in_expr           supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    an assignment within an expression: a = (b = sw[3:0]) + 4'd1; in always_comb
    source: UG901 v2023.2 p.286
22_interface_modport         supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    interface with two modports, modport-typed module ports (sum_if.prod bus)
    source: UG901 v2023.2 p.288, p.283-284
22b_interface_own_file       supported     gap  fails: rtl, synth, bit (netlist not run)
    an interface declared in its own file (sum_if.sv), generic interface ports
    source: UG901 v2023.2 p.288
22c_interface_ports          supported     gap  fails: netlist
    an interface with ports, interface cnt_if(input logic clk, input logic en), holding a counter;
    top reads c.q
    source: UG901 v2023.2 p.288
22d_interface_array          supported     gap  fails: netlist
    an array of interfaces: pair_if p [0:1] (); p[0].d and p[1].d driven and read in top
    source: UG901 v2023.2 p.288
22e_modport_expression       supported     gap  fails: rtl, synth, bit (netlist not run)
    a modport expression: modport lo (input .nib(data[3:0])); a submodule reads b.nib
    source: UG901 v2023.2 p.288; IEEE 1800-2017 25.5.4; slang
22f_interface_member_access  supported     gap  fails: netlist
    an interface without ports whose members the module that instantiates it drives and reads:
    add_if bus (); assign bus.a = sw[7:0]; ... led = bus.s
    source: UG901 v2023.2 p.288, p.282
23_package_import            supported     gap  fails: synth, bit (netlist not run)
    package (enum, parameter, localparam, function) + import pkg::*; inside the module + pkg::NAME
    source: UG901 v2023.2 p.288, p.284
23b_package_file_import      supported     gap  fails: synth, bit (netlist not run)
    the same package with import pkg::*; at file level, using an imported typedef
    source: UG901 v2023.2 p.288, p.284
23c_package_own_file         supported     gap  fails: rtl, netlist
    a package in its own file (pkg.sv), referenced as cfg_pkg::NAME
    source: UG901 v2023.2 p.288
23d_package_header_import    supported     gap  fails: rtl, synth, bit (netlist not run)
    import in the module header: module top import pkg::*; (...)
    source: UG901 v2023.2 p.288
23e_package_export           supported     gap  fails: synth, bit (netlist not run)
    a package that exports a name it imported: package p2; import p1::K; export p1::K; endpackage,
    and import p2::* in the module
    source: UG901 v2023.2 p.288
24_preproc_include           supported     ok   passes all four
    `include of a .svh, `define with arguments, `ifdef/`ifndef/`else, +: part select
    source: UG901 v2023.2 p.239
25_port_conn_dotstar         supported     ok   passes all four
    .* and .name implicit port connections
    source: UG901 v2023.2 p.281-282, p.287
26_inst_array                supported     gap  fails: synth, bit (netlist not run)
    array of instances: inv u_inv [3:0] (.a(sw[3:0]), .y(led[3:0]));
    source: UG901 v2023.2 p.239
27_readmemh_rom              supported     ok   passes all four
    ROM from initial $readmemh("rom.mem") with async and sync read
    source: UG901 v2023.2 p.240, p.155, p.177
28_dist_ram                  supported     ok   passes all four
    RAM: sync write, async read (distributed RAM, -nobram)
    source: UG901 v2023.2 p.114
29_shift_reg_srl             supported     ok   passes all four
    32-deep shift register (SRL) + dynamic tap sr[sw[8:4]]
    source: UG901 v2023.2 p.88, p.91
30_mult_signed               supported     ok   passes all four
    signed 8x8 multiply, >>> arithmetic shift, signed compare, $signed
    source: UG901 v2023.2 p.94, p.286, p.240
31_reset_styles              supported     ok   passes all four
    sync active-high reset, async active-low reset (negedge rst_n), async set to a nonzero constant
    source: UG901 v2023.2 p.81, p.82
31b_negedge_set              supported     ok   passes all four
    registers on the falling edge with no start value: a counter with a synchronous set (FDSE_1) and
    a bit with an asynchronous preset (FDPE_1), both reset from two switches
    source: UG901 v2023.2 p.81
32_clkdiv_7seg               supported     ok   passes all four
    clock divider making a derived clock (always_ff @(posedge slow)) + 4-digit 7-seg multiplexing +
    hex ROM via case
    source: Vivado 2020.1 log; UG901 v2023.2 p.81
33_tristate_pmod             supported     gap  fails: netlist
    inout JA: JA[0] = oe ? d : 1'bz and read back; JA[7:1] = 'z, JA[1] read as input
    source: UG901 v2023.2 p.86; UG953 v2022.2 p.386
33b_inout_read_only          supported     ok   passes all four
    inout pin only read (never assigned 'z) + one bidirectional pin
    source: UG901 v2023.2 p.86; UG953 v2022.2 p.386
33c_tristate_submodule_port  supported     gap  fails: netlist, bit
    the textbook's tristate buffer module (assign y = en ? a : 4'bz) instantiated four times, each
    driving one nibble of an output tri [15:0] port
    source: UG901 v2023.2 p.86
34_bufg                      supported     gap  fails: rtl
    UNISIM primitive instantiation: BUFG
    source: UG901 v2023.2 p.231-232; UG953 v2022.2 p.266
35_mmcm                      supported     gap  fails: rtl, netlist
    UNISIM MMCME2_BASE (100 to 50 MHz) + BUFG
    source: UG953 v2022.2 p.459; UG901 v2023.2 p.232
36_keep_attr                 supported     ok   passes all four
    (* keep = "true" *) and (* dont_touch = "true" *) on internal nets
    source: UG901 v2023.2 p.51, p.59
37_var_init                  unverified    ok   passes all four
    register power-up value: logic [3:0] cnt = 5; and initial c2 = 9;
    source: UG901 v2023.2 p.244, p.251, p.155
37b_decl_init_expr           unverified    gap  fails: netlist
    a variable declared with an initializer that reads inputs: logic [3:0] sum = sw[3:0] + sw[7:4];
    (meant as an adder)
    source: UG901 v2023.2 p.286, p.244; IEEE 1800-2017 6.8
38_gate_primitives           supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    gate primitives (not, and, or, xor) driving logic variables and an output logic port
    source: Vivado 2015.2 log; UG901 v2023.2 p.242
39_undeclared_name           unverified    gap  fails: rtl, netlist
    a typo: an undeclared name on the right of an assign (summ for sum)
    source: IEEE 1800-2017 6.10
40_display_module_name       supported     gap  fails: rtl, synth, bit (netlist not run)
    a module named display next to a $display call in a design module
    source: IEEE 1800-2017 A.1.2; UG901 v2023.2 p.239
40b_module_name_in_string    supported     gap  fails: rtl, synth, bit (netlist not run)
    a module name and ( inside a $display string in a design module: $display("counter (u_cnt)
    wrapped") next to module counter
    source: UG901 v2023.2 p.239; IEEE 1800-2017 5.9
41_wildcard_equality         supported     gap  fails: synth, bit (netlist not run)
    wildcard equality: sw[7:0] ==? 8'b1??0_??01 and !=?
    source: UG901 v2023.2 p.286
42_streaming_concat          not supported ok   fails: rtl, synth, bit (netlist not run)
    streaming concatenation: {<<{sw[7:0]}} (bit reverse) and {<<4{sw[15:8]}}
    source: UG901 v2023.2 p.287, p.286; UG901 v2022.2 p.289
43_foreach_loop              supported     gap  fails: synth, bit (netlist not run)
    foreach over an unpacked array in always_comb
    source: UG901 v2023.2 p.287, p.280
44_do_while_loop             supported     gap  fails: synth, bit (netlist not run)
    do ... while in always_comb, twice: a popcount, and a loop whose condition is false on entry
    (its body still runs once, which tells do-while from while)
    source: UG901 v2023.2 p.287, p.280
45_break                     supported     gap  fails: synth, bit (netlist not run)
    break out of a for loop in always_comb (lowest set bit)
    source: UG901 v2023.2 p.280, p.287
46_continue                  unverified    ok   fails: synth, bit (netlist not run)
    continue in a for loop in always_comb (count of set bits)
    source: UG901 v2023.2 p.287
47_type_param                supported     gap  fails: synth, bit (netlist not run)
    type parameter: module inv_t #(parameter type T = logic [3:0]), overridden with #(.T(logic
    [7:0]))
    source: UG901 v2023.2 p.285
48_iff_event                 unverified    ok   fails: rtl, synth, bit (netlist not run)
    iff event qualifier: always_ff @(posedge clk iff sw[15])
    source: UG901 v2023.2 p.286; IEEE 1800-2017 9.4.2.3; slang; UG901 v2022.2 p.288
49_default_arg               supported     gap  fails: synth, bit (netlist not run)
    function argument with a default value, called without it
    source: UG901 v2023.2 p.287
50_arg_by_name               supported     gap  fails: synth, bit (netlist not run)
    function arguments bound by name, in another order: diff(.b(x), .a(y))
    source: UG901 v2023.2 p.287
51_unpacked_param_array      supported     gap  fails: rtl, synth, bit (netlist not run)
    an unpacked array localparam set with an assignment pattern: localparam logic [6:0] SEGS [0:3] =
    '{...}
    source: UG901 v2023.2 p.285, p.286
51b_packed_param_array       supported     gap  fails: rtl, synth, bit (netlist not run)
    a packed 2-D localparam as a lookup table: localparam logic [3:0][6:0] SEG = {...}; seg =
    SEG[sw[1:0]];
    source: UG901 v2023.2 p.285
51c_packed_param_typedef     supported     gap  fails: rtl, netlist
    the same lookup table with its type named first: typedef logic [3:0][6:0] tab_t; localparam
    tab_t SEG = {...};
    source: UG901 v2023.2 p.285
52_internal_tristate         supported     gap  fails: netlist, bit
    an internal tri bus with two tristate drivers taking turns (the textbook's mux from tristate
    buffers)
    source: UG901 v2023.2 p.86
53_async_reset_sync_clear    supported     gap  fails: synth, bit (netlist not run)
    async reset ORed with a synchronous clear: always_ff @(posedge clk, posedge btnC) if (btnC ||
    btnU) q <= 0;
    source: Vivado 2021.2 log
54_use_before_decl           supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    a name used before its declaration: assign led[0] = stop; logic stop;
    source: Vivado 2017.3 log
55_enum_conditional          unverified    ok   fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    a conditional between two values of one enum: next = sw[0] ? RUN : IDLE;
    source: UG901 v2023.2 p.285; slang
56_unit_scope_enum           supported     gap  fails: synth, bit (netlist not run)
    typedef enum {S0, S1, S2} outside the module (compilation-unit scope), with the textbook's state
    names
    source: UG901 v2023.2 p.270, p.285
56b_unit_typedef_file        supported     gap  fails: rtl, synth, bit (netlist not run)
    a typedef at file scope in a file of its own (types.sv holds only typedef enum ... state_t;),
    used by the design's module
    source: UG901 v2023.2 p.270
57_ready_module_netlist      supported     gap  fails: rtl, synth, bit (netlist not run)
    a course 'ready module' in netlist form in its own file ((* keep_hierarchy *), \<const0>, GND,
    LUT2), instantiated by the design
    source: Vivado 2017.3 log
58_hier_name                 supported     gap  fails: netlist
    a hierarchical name that reads a submodule's signal: assign led[1:0] = u_fsm.state;
    source: UG901 v2023.2 p.239, p.288
59_isunknown                 supported     gap  fails: netlist
    $isunknown(sw) in a design (a real signal is never x, so it is 0)
    source: UG901 v2023.2 p.240
60_pass_by_ref               supported     gap  fails: rtl, synth, bit (netlist not run)
    a function with a ref argument: function automatic void inc(ref logic [3:0] x)
    source: UG901 v2023.2 p.287
61_enum_range                supported     gap  fails: synth, bit (netlist not run)
    enum ranges: typedef enum {S[3]} (S0, S1, S2) and {T[5:7]} (T5, T6, T7)
    source: UG901 v2023.2 p.285
62_type_operator             supported     gap  fails: rtl, synth, bit (netlist not run)
    the type operator: var type(a) b;
    source: UG901 v2023.2 p.285
63_real_cast                 unverified    ok   fails: synth, bit (netlist not run)
    a real localparam cast to int: localparam int INC = int'(STEP); (2.6 rounds to 3)
    source: UG901 v2023.2 p.285, p.273, p.286
63b_real_localparam_expr     unverified    ok   fails: synth, bit (netlist not run)
    a real literal in a localparam used in an expression: localparam MAX = 1e1; ... count == MAX - 1
    (a lab writes 50e6)
    source: UG901 v2023.2 p.285, p.273, p.286
64_unpacked_struct           supported     gap  fails: rtl, synth, bit (netlist not run)
    an unpacked struct: typedef struct { logic [3:0] a; logic [3:0] b; } (no packed)
    source: UG901 v2023.2 p.285
64b_unpacked_union           supported     gap  fails: rtl, synth, bit (netlist not run)
    an unpacked union: typedef union { logic [7:0] byte_v; logic [3:0] nib; } (no packed), one
    member written and read
    source: UG901 v2023.2 p.286
65_stream_unpack             supported     gap  fails: rtl, synth, bit (netlist not run)
    streaming concatenation as the target of an assignment: {>>{a, b}} = sw[7:0];
    source: UG901 v2023.2 p.287
66_alias                     unverified    ok   fails: rtl, synth, bit (netlist not run)
    alias a = b; (two nets made one)
    source: UG901 v2023.2 p.286; UG901 v2022.2 p.288-289
67_array_compare             supported     gap  fails: rtl, synth, bit (netlist not run)
    equality of two whole unpacked arrays: a == b, a != b
    source: UG901 v2023.2 p.285, p.286
68_array_slice               supported     gap  fails: rtl, synth, bit (netlist not run)
    a slice of an unpacked array: b = a[2:3];
    source: UG901 v2023.2 p.285
69_disable                   supported     gap  fails: synth, bit (netlist not run)
    disable of a named block to leave a loop (the lowest switch that is up)
    source: UG901 v2023.2 p.238
70_wor_wand                  supported     gap  fails: bit
    a wor and a wand net, two drivers each
    source: UG901 v2023.2 p.238
71_array_copy                supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    one whole unpacked array assigned to another in always_ff: b <= a;
    source: UG901 v2023.2 p.285
72_void_func_outputs         supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    a void function that returns through output arguments
    source: UG901 v2023.2 p.287
73_array_arg                 supported     gap  fails: rtl
    note: netlist: testbench only, iverilog cannot compile the RTL
    an unpacked array as a function argument
    source: UG901 v2023.2 p.285
74_verilog2005_file          supported     gap  fails: rtl, synth, bit (netlist not run)
    a .v design file with bit and final as names (keywords in SystemVerilog, names in Verilog 2005)
    source: UG901 v2023.2 p.269
75_timing_not_met            supported     gap  fails: bit
    a design that misses 100 MHz: a 14-bit value turned into four registered decimal digits (a long
    divide chain)
    source: Vivado 2015.2 log
76_implicit_net              supported     ok   passes all four
    names the standard declares implicitly: a net from a port connection (.y(w)) and one from the
    left of an assign (assign eq = ...)
    source: Vivado 2021.2 log; IEEE 1800-2017 6.10
77_initial_from_signal       unverified    ok   fails: synth, bit (netlist not run)
    an initial block that reads a signal: initial num = sw; (seen in a CS223 lab)
    source: UG901 v2023.2 p.251, p.244
78_keep_hierarchy            supported     gap  fails: rtl, synth, bit (netlist not run)
    (* keep_hierarchy = "yes" *) on a module, spaced as in UG901
    source: UG901 v2023.2 p.60
79_keep_hierarchy_nospace    supported     ok   passes all four
    the same attribute written without spaces: (*keep_hierarchy="yes"*) (yosys keeps the submodule)
    source: UG901 v2023.2 p.60
80_async_reset_trailing      unverified    ok   fails: synth, bit (netlist not run)
    an always_ff with an async reset and a statement after its if/else (d <= q; runs on the reset
    edge too)
    source: UG901 v2023.2 p.81-82
81_finish_in_design          supported     gap  fails: rtl, netlist
    note: the CLI took counter for the top
    a design module that calls $finish in a simulation check (always @(posedge clk) if (...)
    $finish;), its submodule's ports named as the XDC's (counter.sv)
    source: UG901 v2023.2 p.239, p.240
82_file_tasks_in_design      supported     gap  fails: synth, bit (netlist not run)
    file tasks left in a design module: $fopen, $fdisplay and $fwrite (a debug trace)
    source: UG901 v2023.2 p.239
83_realtobits                supported     gap  fails: rtl, synth, bit (netlist not run)
    $realtobits and $bitstoreal in localparams (a real turned into its IEEE 754 bits and back)
    source: UG901 v2023.2 p.240
84_repeat_const              supported     ok   passes all four
    a repeat loop with a constant count in always_comb (count the switches that are up)
    source: UG901 v2023.2 p.238
85_bitstream_cast            supported     gap  fails: rtl, synth, bit (netlist not run)
    a bitstream cast to an unpacked array: q = quad_t'(sw[7:0]); with typedef logic [3:0] quad_t
    [0:1]
    source: UG901 v2023.2 p.285
86_extern_module             supported     gap  fails: rtl, synth, bit (netlist not run)
    an extern module declaration ahead of the module: extern module inv4(input logic [3:0] a, output
    logic [3:0] y);
    source: UG901 v2023.2 p.287
87_config                    supported     gap  fails: synth, bit (netlist not run)
    a config declaration next to the design: config cfg; design work.top; default liblist work;
    endconfig
    source: UG901 v2023.2 p.239, p.288
88_class_static              supported     gap  fails: rtl, synth, bit (netlist not run)
    a class used for its static function, called as util::rev(sw[3:0]) (no object is made)
    source: UG901 v2023.2 p.288
89_clkdiv_slow_logic         supported     gap  fails: bit
    a clock divider's output clocks more logic than 100 MHz allows (four decimal digits of a 14-bit
    value on the slow clock); the XDC has no create_clock, as the course's master XDC arrives
    source: Vivado 2015.2 to 2021.2 routed timing reports (11 lab runs)
90_elab_warning_initial      supported     gap  fails: synth, bit (netlist not run)
    a parameter check in an initial block: initial if (N < 8) $warning(...)
    source: UG901 v2023.2 p.286, p.241
91_submodule_own_file        supported     ok   passes all four
    a design split over two files: top in design.sv, its submodule counter in counter.sv (the usual
    lab layout)
    source: UG901 v2023.2 p.270
92_display_in_design         supported     ok   passes all four
    a $display debug print left in a clocked design module: if (q == 4'd15) $display("wrapped");
    source: UG901 v2023.2 p.239
93_two_roots_file_name       supported     gap  fails: netlist
    note: the CLI took counter for the top
    two modules nothing instantiates, in one file named after one of them (counter.sv: the plain
    counter, and the board version the project names as its top)
    source: Vivado 2015.2 logs of two CS223 Lab 4 projects (.xpr and runme.log)
94_power_var_base            supported     gap  fails: bit
    a variable raised to a constant power: assign led = sw[7:0] ** 2;
    source: UG901 v2023.2 p.278, p.249
95_empty_submodule           unverified    ok   fails: netlist, bit
    a submodule declared with its ports and no body (a lab part not written yet), its output used
    source: none (no Vivado log; UG901 is silent)
96_port_one_bit_vector       supported     gap  fails: bit
    a port declared [N-1:0] with N = 1: a vector of one bit, numbered 0
    source: UG901 v2023.2 p.285; IEEE 1800-2017 7.4
96b_port_offset_range        supported     gap  fails: bit
    ports numbered from 1: input logic [4:1] sw, with the lab's own XDC (sw[1] to sw[4] only)
    source: UG901 v2023.2 p.285; IEEE 1800-2017 7.4
97_ram_style_block           supported     gap  fails: netlist
    UG901's RAM_STYLE attribute: (* ram_style = "block" *) on a 256-byte memory
    source: UG901 v2023.2 p.64
98_module_named_primitive    unverified    ok   fails: synth, bit (netlist not run)
    a student's module named like a Xilinx primitive: module INV (GND, VCC and MUXF7 too)
    source: UG901 v2023.2 p.232 (the guide is silent on the name)
99_string_parameter          unverified    ok   fails: synth, bit (netlist not run)
    a parameter of type string: parameter string INIT_FILE = "digits.mem" (a ROM's $readmemh file)
    source: UG901 v2023.2 p.285, p.238, p.247, p.232

Does the suite catch breakage? Each mutation below was made in a scratch copy and reverted.

Evidence.

$ test/sv/run.sh
test/sv: 51 probes, yosys 0.69+post, iverilog 13.0
  gap  01_latch_comb                rtl pass  synth fail  netlist -     bit fail
  gap  03_async_reset_nonconst      rtl pass  synth pass  netlist fail  bit pass
  ok   07_enum_packed_array         rtl pass  synth pass  netlist pass  bit pass
  gap  18b_unique_priority_if       rtl fail  synth pass  netlist pass  bit pass   note: netlist: testbench only, iverilog cannot compile the RTL
  ...
synthesis 35/51, all four stages 24/51, Vivado-supported probes passing all four stages 23/42, known gaps 23
matches expect.tsv: 51/51                                  (226 s)

$ PATH=<yowasp yosys 0.68 shim>:$PATH test/sv/run.sh
test/sv: 51 probes, yosys 0.68, iverilog 13.0  (expect.tsv was measured with yosys 0.69+post, iverilog 13.0: only regressions and new silent wrongs fail)
synthesis 35/51, all four stages 24/51, Vivado-supported probes passing all four stages 23/42, known gaps 23
matches expect.tsv: 51/51

$ SV_OUT=rows.tsv test/run.sh          (from a scratch folder; rows.tsv is there afterwards, 2279 bytes)
== probe runner (test/sv/run.sh on a copy, one probe each)
  PASS a build output left in a probe folder is not reused
  PASS a new silent wrong fails with any yosys
  PASS an unexpected pass fails with the measured yosys
  PASS a row without a Vivado source is refused
  PASS a wrong line in the latch error is caught
== SystemVerilog probes (test/sv)
  GAP  sv 01_latch_comb: rtl pass, synth fail, netlist -, bit fail
  ...
  PASS sv 07_enum_packed_array: rtl pass, synth pass, netlist pass, bit pass
  ...
passed 86, failed 0, known gaps 23

The same new checks run against the tree before this round: the old personal-path grep misses the
planted line, student names FAIL, stale top_sim FAIL (the run said "rtl pass"), new silent wrong FAIL (the
run said "note: ..." and exit 0), empty source FAIL, latch line FAIL. The unexpected-pass check passes
there too: it guards the runner's exact mode, which was already right.

The breaker's LUT flip in 30's netlist (32'd2720070586 -> 32'd2686516154); the old testbench printed PASS:
tb on the netlist: FAIL: >>>, $signed or signed < (sw=1410 seg=0000010, want sh=1 a<b=1)
EQV FAIL: 128 of 1507328 compared output bits differ between the RTL and the netlist

Round 2:

$ test/run.sh                                     (from a scratch folder, SV_OUT=rows-final.tsv)
== probe runner (test/sv/run.sh on a copy, one probe each)
  PASS a testbench that prints FAIL is an rtl fail
  PASS a probe folder without a row is refused
  PASS a bit run without top.bit is a bit fail
  PASS a vivado column its source does not back is refused
  PASS a probe that stops matching expect.tsv fails this suite
  PASS the verdict of each row is checked, not trusted
  PASS eqv.py: an equal netlist passes
  PASS eqv.py: a fault only on led[15] is caught
  PASS eqv.py: a fault only at sw=0000 is caught
  PASS eqv.py: a clocked fault only at sw=ffff is caught
  PASS eqv.py: a clocked fault only at sw=7fff is caught
  PASS eqv.py: a fault after 200 cycles is caught
== SystemVerilog probes (test/sv)
  GAP  sv 13b_unpacked_array_port: rtl pass, synth fail, netlist -, bit fail
  GAP  sv 52_internal_tristate: rtl pass, synth pass, netlist fail, bit fail
  PASS sv 55_enum_conditional: rtl fail, synth pass, netlist pass, bit pass (note: netlist: testbench only, iverilog cannot compile the RTL)
  GAP  sv 57_ready_module_netlist: rtl fail, synth fail, netlist -, bit fail
  ...
  synthesis 38/69, all four stages 24/69, Vivado-supported probes passing all four stages 23/60, known gaps 41
passed 99, failed 0, known gaps 41                          (305 s)

$ PATH=<yowasp yosys 0.68 shim>:$PATH test/sv/run.sh
test/sv: 69 probes, yosys 0.68, iverilog 13.0  (expect.tsv was measured with yosys 0.69+post, ...)
synthesis 38/69, all four stages 24/69, Vivado-supported probes passing all four stages 23/60, known gaps 41
matches expect.tsv: 69/69                                   (the row check on these rows: 28 passed, 0 failed, 41 gaps)

$ test/sv/eqv_check.sh <case>, with round 1's eqv.py and with mutations of the new one
round 1's eqv.py   equal ok   msb ok    zero RED  ones RED  dense RED  deep ok
compare bit 0      equal ok   msb RED   zero ok   ones ok   dense ok   deep ok
16 cycles          equal ok   msb ok    zero ok   ones ok   dense RED  deep RED
no corner values   equal ok   msb ok    zero ok   ones RED  dense RED  deep ok

The breakers' flips, round 1's eqv.py then the new one:
  sw=0000 only:  EQV PASS: 1048576 output bits compared   ->  EQV MISMATCH ... led[0] rtl 1, netlist 0; inputs sw=0000
  21 at ffff:    EQV PASS: 262024 output bits compared    ->  EQV MISMATCH ... led[11] rtl 0, netlist 1; inputs btnC=0 sw=ffff
  21 at 7fff:    (passed 4096 cycles)                     ->  EQV MISMATCH ... led[11] rtl 1, netlist 0; inputs btnC=0 sw=7fff

Mutations, each on a copy of the fixed tree, and the check that goes red:
  runner writes ok for bad          a probe that stops matching expect.tsv fails this suite
  row check files bad as gap        the verdict of each row is checked, not trusted
  row check drops the stage compare the verdict of each row is checked, not trusted
  row check ignores the exit code   the verdict of each row is checked, not trusted
  rtl without grep PASS             a testbench that prints FAIL is an rtl fail
  silent wrong not a gap            sv 05_two_block_driver: the runner says ok, but ... make it gap
  folder/row check removed          a probe folder without a row is refused
  synth read from the exit code     sv 52_internal_tristate: synth fail, expected pass
  bit without top.bit               a bit run without top.bit is a bit fail
  vivado column check removed       a vivado column its source does not back is refused
  student-name regex gutted         student-name check sees a quoted name
  eqv: bit 0 only / 16 cycles / walk from 1 / no corner values    eqv.py: msb / deep, dense / zero / ones, dense
The fixed tree without a mutation: all 13 of those checks pass.

Before and after, on the tree the breakers had:
  13b                                    PASS sv 13b_unpacked_array_port  ->  GAP  sv 13b_unpacked_array_port
  runner ok-for-bad + 14 broken          PASS sv 14_params, exit 0        ->  FAIL sv 14_params: the runner says ok, but its stages ... differ from expect.tsv
  silent wrong not a gap                 PASS sv 03_async_reset_nonconst  ->  FAIL sv 03_async_reset_nonconst: the runner says ok, but ... make it gap

Round 3:

$ GITHUB_STEP_SUMMARY=summary.md SV_OUT=rows-final.tsv test/run.sh      (from a scratch folder)
== probe runner (test/sv/run.sh on a copy, one probe each)
  PASS a netlist that differs from its RTL fails, with any yosys
  PASS a testbench FAIL on the netlist fails, with any yosys
  PASS the runner checks leave CI's step summary alone
  ...
  PASS eqv.py: a fault only at sw=1234 is caught
  PASS eqv.py: a clocked fault only at sw=0100 is caught
  PASS eqv.py: a fault only while btnC is pressed is caught
  PASS eqv.py: an undriven (z) output is caught
  PASS eqv.py: an x output is caught
  PASS eqv.py: a register with INIT x starts at 0, as on the board
  PASS eqv.py: a start-value difference before the reset is apart
  PASS eqv.py: a kept submodule named as the RTL's passes
  PASS eqv.py: a fault inside a kept submodule is caught
== SystemVerilog probes (test/sv)
  GAP  sv 58_hier_name: rtl pass, synth pass, netlist fail, bit pass
  GAP  sv 59_isunknown: rtl pass, synth pass, netlist fail, bit pass
  GAP  sv 75_timing_not_met: rtl pass, synth pass, netlist pass, bit fail
  PASS sv 79_keep_hierarchy_nospace: rtl pass, synth pass, netlist pass, bit pass
  ...
  synthesis 47/92, all four stages 26/92, Vivado-supported probes passing all four stages 25/77, known gaps 56
passed 119, failed 0, known gaps 56                          (409 s)
$ cat summary.md                                              (one line; 8 before)
SystemVerilog probes (yosys 0.69+post, iverilog 13.0): synthesis 47/92, all four stages 26/92, ...

$ PATH=<yowasp yosys 0.68 shim>:$PATH test/sv/run.sh       (CI's yosys; brew untouched)
test/sv: 92 probes, yosys 0.68, iverilog 13.0  (expect.tsv was measured with yosys 0.69+post, ...)
synthesis 47/92, all four stages 26/92, Vivado-supported probes passing all four stages 25/77, known gaps 56
matches expect.tsv: 92/92                                    (the only notes: 8 "testbench only")
$ PATH=<the same shim>:$PATH test/sv/eqv_check.sh <each of the 15 cases>: 15 of 15 as expected

eqv.py mutations, each on a copy, and the eqv_check.sh cases that go red:
  exhaustive walk off (EXHAUSTIVE_BITS = 0)    walk
  no mostly-zeros values                        sparse
  no mostly-ones values                         dense
  buttons never pressed                         button
  a z counted as unknown                        zout
  an x counted as unknown                       xout
  INITs left x (no board start)                 init, xstart
  no start-value excuse                         xstart
  every 0/1 difference excused                  msb, zero, walk, ones, dense, sparse, deep, button, hierfault
  the excuse without its 0/1 test               xout
  only top renamed                              hier, hierfault
  cell types not renamed                        hierfault
  compare bit 0 only                            msb, zout, xout
  16 cycles                                     dense, sparse, deep
  walk from 1                                   zero
  no corner values                              ones, dense, sparse
The fixed eqv.py: all 15 cases pass.

The runner, and the checks that go red:
  netlist-vs-RTL comparison deleted (CI mode)   FAIL a netlist that differs from its RTL fails, with any yosys
  testbench PASS dropped from the netlist stage FAIL a testbench FAIL on the netlist fails, with any yosys
  the summary check on the round-2 tree         FAIL the runner checks leave CI's step summary alone (PASS now)
  the port-line note one line down              ok 08 on the round-2 tree -> BAD 08_port_dir_inherit ... expect.tsv says design.sv:2, no stage printed it

Before and after:
  79 on the round-2 tree   BAD 79_keep_hierarchy_nospace ... net_eqv.v:3: error: 'inv' has already been declared in this scope.  ->  ok
  the undriven led[4]      EQV PASS: 983040 output bits compared, all equal (65536 more were x in the netlist only)  ->  EQV MISMATCH at 3000 (gray): led[4] rtl 0, netlist x; inputs sw=0000
  55's 48 LUT flips        the old testbench passes 23 of the 36 that change the netlist  ->  the new one passes 0

Round 4:

$ GITHUB_STEP_SUMMARY=<absolute path> SV_OUT=rows-final.tsv test/run.sh      (from a scratch folder)
== static
  PASS student-name check sees a quoted name
== probe runner (test/sv/run.sh on a copy, one probe each)
  PASS the score line is counted again from the rows
  PASS a message the today column quotes is checked
  PASS a file:line of a .v file is checked
  ...
  PASS eqv.py: a packed array of enums with no start value is started
  PASS eqv.py: a 1-bit enum with no start value is started
  PASS eqv.py: a netlist that lost its reset is caught
  PASS eqv.py: a re-encoded state machine is compared after one reset
  PASS eqv.py: ports named x, mode, seed, r, n, v, k do not clash with the bench
  ...
== SystemVerilog probes (test/sv)
  PASS sv 07_enum_packed_array: rtl pass, synth pass, netlist pass, bit pass
  GAP  sv 13c_unpacked_array_concat: rtl fail, synth fail, netlist -, bit fail
  GAP  sv 18c_unique0_if: rtl fail, synth pass, netlist pass, bit pass (note: netlist: testbench only, iverilog cannot compile the RTL)
  GAP  sv 22c_interface_ports: rtl pass, synth pass, netlist fail, bit pass
  GAP  sv 22d_interface_array: rtl pass, synth pass, netlist fail, bit pass
  PASS sv 63_real_cast: rtl pass, synth fail, netlist -, bit fail
  GAP  sv 81_finish_in_design: rtl fail, synth pass, netlist fail, bit pass (note: the CLI took counter for the top)
  ...
  synthesis 52/100, all four stages 26/100, Vivado-supported probes passing all four stages 25/84, known gaps 63
passed 138, failed 0, known gaps 63                          (556 s)
$ cat summary.md                                              (GITHUB_STEP_SUMMARY=<absolute path>; one line)
SystemVerilog probes (yosys 0.69+post, iverilog 13.0): synthesis 52/100, all four stages 26/100, Vivado-supported probes passing all four stages 25/84, known gaps 63
$ PATH=<yowasp yosys 0.68 shim>:$PATH test/sv/run.sh <the 100, in four runs>          (CI's yosys; brew untouched)
test/sv: 25 probes, yosys 0.68, iverilog 13.0  (expect.tsv was measured with yosys 0.69+post, ...)
matches expect.tsv: 25/25, 25/25, 25/25, 24/25    BAD 75_timing_not_met ... expect.tsv quotes 'ERROR: timing not met: 33.61 MHz (FAIL at 100.00 MHz)', no stage printed it
after the rewording, 75 under both:  gap  75_timing_not_met  rtl pass  synth pass  netlist pass  bit fail
$ PATH=<the same shim>:$PATH test/sv/eqv_check.sh <each of the 30 cases>: 30 of 30 as expected
$ GITHUB_STEP_SUMMARY=<absolute path> test/sv/run.sh 07_enum_packed_array 81_finish_in_design; cat <that file>
SystemVerilog probes (yosys 0.69+post, iverilog 13.0): synthesis 2/2, all four stages 1/2, Vivado-supported probes passing all four stages 1/2, known gaps 1

$ test/sv/eqv_check.sh <case>, round 3's eqv.py (its argument count only widened) and today's
                 round 3                                                        today
names            eqv.sv:12: error: 'r' has already been declared in this scope.  EQV PASS: 98304 output bits compared, all equal
lostreset        EQV PASS: 262136 ... (31956 more differed only while a register had no start value)   EQV FAIL: 33108 of 262144 ...
lostinit         EQV PASS: 196608 ... (35328 more differed only while ...)       EQV FAIL: 35328 of 196608 ...
(the other 27 cases give the same verdict with both; they pin what round 3 left unpinned, below)

eqv.py mutations, each on a copy, and the eqv_check.sh cases that go red:
  no reset press before the first compare      recode
  every start value 0                          fillset fillaset
  no start values in the RTL                   xstart pkenum enum1
  LDPE starts at 1                             ldpe ldfill
  FDSE starts at 0                             fdse fillset
  FDPE starts at 0                             fillaset
  a latch's start taken from its preset        ldfill
  buttons out of the exhaustive walk           cbutton
  inout pins never driven from outside         ioread iofloat
  inout pins not compared                      iofloat
  512 cycles / 3999 cycles                     deep / deep
  buttons never pressed after the start        button
  netlist INITs left x                         init fdse ldpe ldfill xstart fillset fillaset pkenum enum1
  only top renamed                             hierfault
  every register set bit by bit (07's crash)   pkenum enum1
  a 1-bit enum not started                     enum1
Today's eqv.py: all 30 cases as expected.

The product's netlists with every reset/set pin that is not a constant tied to 0 (tb.sv on it; round 3's bench; today's):
  09  tb FAIL  EQV PASS: 262132 output bits compared, all equal (31956 more ...)   EQV MISMATCH at 28000 (data): led[0] rtl 0, netlist 1
  10  tb FAIL  EQV PASS: 262140 ... (20140 more ...)                               EQV MISMATCH at 28000 (data): led[4] rtl 0, netlist 1
  21  tb FAIL  EQV PASS: 262120 ... (96886 more ...)                               EQV MISMATCH at 28000 (data): led[0] rtl 0, netlist 1
  31  tb FAIL  EQV PASS: 262092 ... (96814 more ...)                               EQV MISMATCH at 28000 (data): led[0] rtl 0, netlist 1
  32  tb FAIL  EQV PASS: 196608 ... (46705 more ...)                               EQV MISMATCH at 36000 (posedg): an[0] rtl 0, netlist 1
  34  tb FAIL  EQV PASS: 196604 ... (24440 more ...)                               EQV MISMATCH at 14000 (btns): led[0] rtl 0, netlist 1

The runner and test/run.sh's row check over 07, 01, 52, 70, each with a mutated runner:
  round 3's tree, supported probes all counted as passing   passed 1, failed 0 (the line says 4/4)
  today, the same                                           FAIL sv: test/sv/run.sh says '... passing all four stages 4/4, known gaps 3', its rows make it '... 1/4, known gaps 3'
  today, synthesis counted from the bit stage               FAIL sv: test/sv/run.sh says 'synthesis 1/4, ...', its rows make it 'synthesis 3/4, ...'
  today, no mutation                                        passed 1, failed 0, known gaps 3

The latch advice replaced with 'something went wrong.':
  round 3's runner   gap  01_latch_comb   rtl pass  synth fail  netlist -  bit fail          matches expect.tsv: 1/1
  today's runner     BAD  01_latch_comb   ... expect.tsv quotes 'Give q a default value at the top of the block.', no stage printed it
Each mutation on a copy, the new check that goes red (all four green on round 3's suite):
  the backquote dropped from the student-name pattern   FAIL student-name check sees a quoted name
  .v dropped from the runner's file:line pattern        FAIL a file:line of a .v file is checked
  the runner's quote check removed                      FAIL a message the today column quotes is checked
  the runner counts every supported probe               FAIL sv: test/sv/run.sh says ... (the full run's row check, above)

81 with a runner that reads only top.json (round 3's), and today's:
  BAD  81_finish_in_design   rtl fail  synth fail  netlist -     bit fail   synth fail, expected pass; netlist -, expected fail; bit fail, expected pass
  gap  81_finish_in_design   rtl fail  synth pass  netlist fail  bit pass   note: the CLI took counter for the top

Round 5:

$ GITHUB_STEP_SUMMARY=<absolute path> SV_OUT=rows-final.tsv test/run.sh      (from a scratch folder)
== static
  PASS shellcheck                                                (now also test/sv/eqv.sh)
== error paths
  PASS unnamed instance -> line and fix                          (now also 'Write:  sub u_sub(')
== probe runner (test/sv/run.sh on a copy, one probe each)
  PASS a vivado value other than supported, not supported, unverified is refused
  PASS a message the today column quotes in double quotes is checked
  PASS eqv.py: an output lost only between a button change and the clock edge is caught
  PASS eqv.py: a register with an asynchronous reset starts in the RTL as on the board
  PASS eqv.py: a latch with no start value starts at 0 in the RTL, as on the board
  PASS eqv.py: a reset value of 0011 starts each bit as its FDSE or FDRE does
  PASS eqv.sh: a submodule's register with no start value is started in the RTL
  PASS eqv.sh: a fault in a submodule's register with no start value is caught
  PASS eqv.py: a register that feeds a submodule's port is started by its own name
== SystemVerilog probes (test/sv)
  GAP  sv 08c_port_dir_inherit_proc: rtl fail, synth pass, netlist pass, bit pass
  GAP  sv 51c_packed_param_typedef: rtl fail, synth pass, netlist fail, bit pass
  PASS sv 84_repeat_const: rtl pass, synth pass, netlist pass, bit pass
  ...
  synthesis 57/115, all four stages 27/115, Vivado-supported probes passing all four stages 26/99, known gaps 77
passed 148, failed 0, known gaps 77                          (474 s)
$ cat summary.md                                              (one line)
SystemVerilog probes (yosys 0.69+post, iverilog 13.0): synthesis 57/115, all four stages 27/115, Vivado-supported probes passing all four stages 26/99, known gaps 77
$ PATH=<yowasp yosys 0.68 shim>:$PATH test/sv/run.sh <the 15 new probes>     (CI's yosys; brew untouched)
test/sv: 15 probes, yosys 0.68, iverilog 13.0  (expect.tsv was measured with yosys 0.69+post, ...)
matches expect.tsv: 15/15                                     (52 s)
$ PATH=<the same shim>:$PATH test/sv/eqv_check.sh <each of the 37 cases>: 37 of 37 as expected (111 s)
$ test/sv/run.sh <the 115, in four runs of 29, 29, 29 and 28>
matches expect.tsv: 29/29, 29/29, 29/29, 28/28          (124 s, 105 s, 51 s, 55 s)

The breaker's probe 90_reg_to_port (register n feeds a submodule's port D; top also has a register D):
  round 4's runner   BAD  90_reg_to_port   rtl pass  synth pass  netlist fail  bit pass   netlist fail, expected pass
                              net:    eqv.sv:20: error: 'eqv_rtl.A1.D' is not a valid l-value for a procedural assignment.
  today's runner     ok   90_reg_to_port   rtl pass  synth pass  netlist pass  bit pass

The breaker's divider in a submodule, the product's netlist of a copy that counts down:
  round 4's runner, its eqv() as it is        EQV FAIL: 26112 of 196608 compared output bits differ between the RTL and the netlist
  round 4's runner, its eqv() without flatten EQV PASS: 147456 output bits compared, all equal
  today's eqv.sh                              EQV FAIL: 26112 of 196608 compared output bits differ between the RTL and the netlist

eqv mutations, each on a copy; the cases that go red. Round 4's tree ran its 30 cases plus the breakers' 4
(mealyb, xstarta, xstartl, fillasym added to it); today's tree runs its 37:
                                                    round 4's tree              today
  no compare after the buttons change (eqv.py)      mealyb (the 30: none)       mealyb
  async-reset flip-flops not started (eqv.py)       xstarta (the 30: none)      xstarta
  latches not started (eqv.py)                      xstartl (the 30: none)      xstartl
  reset value bits in the other order (eqv.py)      fillasym (the 30: none)     fillasym
  no flatten in the RTL register listing            none (it was in run.sh)     subfault (it is in eqv.sh)
  no proc in the RTL register listing (eqv.sh)                                  25 cases, subreg subfault regport among them
  the always block's module not looked at first                                 subreg subfault regport
  eqv.py --netlist skipped (INITs left x, modules not renamed)                   17 cases
Today's tree without a mutation: all 37 cases as expected (21 s).

The runner checks on a copy of today's tree with the breakers' runner mutations:
  "..." quotes not read from the today column   FAIL st_fixtext    (the run: gap 02_unnamed_inst ... matches expect.tsv: 1/1)
  the vivado value check removed                FAIL st_vivvalue   (the run: ok 01_latch_comb ... matches expect.tsv: 1/1)
On round 4's tree the same two mutations left the suite green: 02 'matches expect.tsv: 1/1' with the fix
text gone from the CLI, 01 'ok' with its column written 'Supported'.

The new probes' testbenches, each on a correct design and on one with its construct broken:
  08c  the line bit rewrites, iverilog   PASS    an = 4'b1110 for two digits          FAIL: sw=0100 an=1110 seg=1111111
  12c  iverilog                          PASS    .x and .y swapped                     FAIL: sw=0001 led=0001
  13d  yosys-slang netlist               PASS    sw[4*j + i] in the loop               FAIL: sw=0101 led=5005 want=1101
  13d  yosys-slang netlist               PASS    the two read indices swapped          FAIL: sw=1010 led=0010 want=1010
  23e  iverilog / yosys-slang            PASS    K = 6                                 FAIL: sw=0000 led=0006 (both)
  33c  iverilog                          PASS    4'b0 instead of 4'bz                  FAIL: sw=0000 led=0000
  40b  iverilog                          PASS    the counter steps by 2                FAIL: after 1 edges led=a5a2
  51b  yosys-slang netlist               PASS    two table entries swapped             FAIL: sw=0002 seg=0110000
  51c  yosys-slang netlist               PASS    two table entries swapped             FAIL: sw=0002 seg=0110000
  82   iverilog                          PASS    q + sw[4:1]                           FAIL: cycle 0 led=0002 want=4
  83   yosys-slang netlist               PASS    $realtobits(3.5)                      FAIL: sw=0000 led=400a
  83   yosys-slang netlist               PASS    $bitstoreal of 1.0                    FAIL: sw=0000 led=4000
  84   iverilog                          PASS    repeat (7)                            FAIL: sw=da80 led=da00
  85   yosys-slang netlist               PASS    quad_t [1:0] instead of [0:1]         FAIL: sw=c201 led=c201
  86   yosys-slang netlist               PASS    y = a                                 FAIL: sw=0000 led=0000
  87   iverilog                          PASS    the byte swap undone                  FAIL: sw=3c00 led=3c00
  88   yosys-slang netlist               PASS    rev returns a                         FAIL: sw=0101 led=0101

51c, the product's netlist and yosys-slang's, sw = 0 to 3 (the table: 1000000 1111001 0100100 0110000):
  dewfpga bit    pnr ok: 0 LUT, 0 FF ...    seg 0000000 0000000 0000000 0000000
  yosys-slang                               seg 1000000 1111001 0100100 0110000

Round 6:

$ GITHUB_STEP_SUMMARY=<absolute path> SV_OUT=rows-final.tsv test/run.sh      (from a scratch folder)
== static
  PASS student-name check sees a quoted name                     (now a quoted name after each of 10 words)
== probe runner (test/sv/run.sh on a copy, one probe each)
  PASS an unexpected pass in a row is not trusted with the measured yosys
  PASS a message the today column quotes second is checked
  PASS a file:line the today column names second is checked
  PASS a comparison that fails with exit 1 is a netlist fail
  PASS eqv.py: a register whose slice a wire names is started by its own name
  PASS eqv.py: a register with an alias wire that sorts first is started by its own name
  PASS eqv.py: 1-bit enums of a submodule and of the file are started
  PASS eqv.py: a 2-bit enum the netlist starts in a state the RTL never has is caught
  PASS eqv.py: a LUT RAM that powers up with the wrong contents is caught
  PASS eqv.py: an output lost only between a switch change and the clock edge is caught
  PASS eqv.sh: a bench that does not compile while the RTL does is a FAIL
  PASS eqv.sh: a package in its own file that sorts after the design is compiled first
  PASS eqv.py: a bench that compares nothing is a FAIL
  PASS eqv.py: a netlist one clock off at the start, as dewfpga sim shows it, passes
  PASS eqv.py: a netlist that mixes two starts in one compare is caught
== SystemVerilog probes (test/sv)
  GAP  sv 22f_interface_member_access: rtl pass, synth pass, netlist fail, bit pass
  GAP  sv 37b_decl_init_expr: rtl pass, synth pass, netlist fail, bit pass
  GAP  sv 44_do_while_loop: rtl pass, synth fail, netlist -, bit fail
  GAP  sv 56b_unit_typedef_file: rtl fail, synth fail, netlist -, bit fail
  PASS sv 63b_real_localparam_expr: rtl pass, synth fail, netlist -, bit fail
  GAP  sv 89_clkdiv_slow_logic: rtl pass, synth pass, netlist pass, bit fail
  GAP  sv 90_elab_warning_initial: rtl pass, synth fail, netlist -, bit fail
  PASS sv 91_submodule_own_file: rtl pass, synth pass, netlist pass, bit pass
  PASS sv 92_display_in_design: rtl pass, synth pass, netlist pass, bit pass
  ...
  synthesis 62/123, all four stages 29/123, Vivado-supported probes passing all four stages 28/105, known gaps 82
passed 166, failed 0, known gaps 82                          (533 s; rows-final.tsv has 123 rows)
$ cat summary.md                                              (one line)
SystemVerilog probes (yosys 0.69+post, iverilog 13.0): synthesis 62/123, all four stages 29/123, Vivado-supported probes passing all four stages 28/105, known gaps 82
$ test/sv/run.sh <the 123, in four runs of 31, 31, 31 and 30>
matches expect.tsv: 31/31, 31/31, 31/31, 30/30          (136 s, 114 s, 39 s, 73 s)
synthesis 19+23+8+12 = 62/123, all four stages 12+12+0+5 = 29/123, supported and all four 28/105, known gaps 82
$ PATH=<yowasp yosys 0.68 shim>:$PATH test/sv/run.sh <the 8 new probes and the 6 changed rows>     (CI's yosys)
test/sv: 14 probes, yosys 0.68, iverilog 13.0  (expect.tsv was measured with yosys 0.69+post, ...)
matches expect.tsv: 14/14                                     (63b's crash quote removed first: 13/14 with it)
$ PATH=<the same shim>:$PATH test/sv/eqv_check.sh <each of the 48 cases>: 48 of 48 as expected (152 s)

The breakers' netlists, round 5's eqv.sh, then today's:
  Gray counter (6 FDRE)   EQV FAIL: 9 of 36864 compared output bits differ between the RTL and the netlist
                          EQV PASS: 36864 output bits compared, all equal (at 9 compares to the RTL as dewfpga sim starts it, ...)
  enum in a submodule     EQV FAIL: the RTL compiles but the equivalence bench does not   (Unable to bind `eqv_rtl.OFF')
                          EQV PASS: 196608 output bits compared, all equal
  wire low = led[3:0]     EQV FAIL: the RTL compiles but the equivalence bench does not   ('eqv_rtl.low' is not a valid l-value)
                          EQV PASS: 262144 output bits compared, all equal
The breakers' netlists that have to fail, today's bench (the excuse passes none of them):
  enum state 11 at power-up   EQV FAIL: 2 of 196608 compared output bits differ between the RTL and the netlist
  28's RAM all ones           EQV FAIL: 4096 of 262144 compared output bits differ between the RTL and the netlist
  led[0] a constant 1         EQV FAIL: 3609 of 196608 compared output bits differ between the RTL and the netlist

The new eqv_check cases on round 5's eqv.py and eqv.sh (red = the case does not give its verdict):
  regslice alias enum1sub romstart                          red (the first three: the bench does not compile;
                                                            romstart: EQV FAIL: 21501 of 196608)
  enumstart memstart datasw nobench pkgfile nothing mixstart as expected (they pin what round 5 already did)

eqv mutations on a copy of today's tree, and the cases that go red (48 cases; none without a mutation):
  12 the multi-bit enum start deleted                  enumstart
  13 memories not started                              memstart
  14 no data compare with a clock                      enumstart datasw
  15 eqv.sh's bench guard removed                      nobench
  18 the assigned_in fallback removed                  regslice alias enum1sub
  22 the packages-first order removed                  pkgfile
  23 "nothing compared" no longer a FAIL               nothing
  the sim-start excuse removed                         romstart
  the excuse per bit, not per compare                  mixstart
  an x in the sim-start copy excuses                   subfault enumstart memstart
  the 1-bit enum item named in top's scope             enum1sub
  assigned_in reads whole lines, not columns           alias enum1sub
On round 5's tree the breakers' seven (12 to 23) each left all 37 cases green.

Runner mutations on a copy of today's tree, the old checks that could see them, and the new one:
  only a row's first quote is checked        PASS st_latchtext, PASS st_fixtext, FAIL st_quote2
  only a row's first file:line is checked    PASS st_latchline, PASS st_vfile, FAIL st_line2
  any eqv.sh failure is a skip (-ne 0)       PASS st_eqvpin, FAIL st_eqvexit
  sv_rows always lenient (exact=0)           PASS st_verdicts, PASS st_score, FAIL st_exact
  the name check's words cut to module       FAIL st_names

Product and probe mutations, the probe that goes BAD:
  sim compiles only the first design file    BAD 91_submodule_own_file  rtl fail, expected pass   (tb.sv:7: error: Unknown module type: top)
  delete t:$print dropped                    BAD 92_display_in_design   bit fail, expected pass   (no Bels remaining of type '$print')
  44's two do-while loops written as while   BAD 44_do_while_loop       rtl fail, expected pass
Before, no probe could see the first two (only 57 has two design files, and the CLI stops it; 40, 40b
and 81 never hand their $display to yosys); the breakers' full runs with them: passed 144, failed 0.
44 with its one loop written as while matched expect.tsv (gap, 1/1).

-noautowire, every probe read with and without it (hierarchy, proc): 22, 22b, 22c, 22d, 22f, 23c, 39, 58, 76
change; 63b crashes either way.

The new probes' testbenches, on the RTL (iverilog) and with one edit:
  89   PASS    d1 without % 10                       FAIL: value 9999: led=9979, want 9999
  89   PASS    the slow clock never toggles          FAIL: value 9999: led=0000, want 9999
  90   PASS    N = 5                                 FAIL: sw=0010 led=0010, want 0000
  63b  PASS    MAX = 1.2e1                           FAIL: after 10 edges led=0000
  37b  PASS    wire instead of logic (continuous)    FAIL: led follows sw (sw=0101 led=0001, was 0000): an initializer runs once
  22f  PASS    s = a - b                             FAIL: sw=0100 led=00ff
  91   PASS    the counter steps by 2                FAIL: after 1 edges sw=5e81 led=5e83
  92   PASS    the counter steps by 2                FAIL: after 1 edges sw=5e81 led=5e82
  56b  PASS    RUN goes back to IDLE                 FAIL: after 2 edges led=0000, want 2
  44   PASS    the second loop written as while      FAIL: do-while popcount, and a body that runs once (sw=8000 led=0001 want 1, 1)

Round 6's final run, 26 September (the tree after round 6):

$ GITHUB_STEP_SUMMARY=<absolute path> SV_OUT=rows-final.tsv test/run.sh      (from a scratch folder)
== static
  PASS shellcheck
  PASS no personal paths
  PASS personal-path check sees test/sv/run.sh
  PASS no names from student code in expect.tsv
  PASS student-name check sees a quoted name
  ...
== probe runner (test/sv/run.sh on a copy, one probe each)          71 PASS: 23 runner checks, 48 eqv checks
  ...
== SystemVerilog probes (test/sv)                                    41 PASS, 82 GAP, 0 FAIL
  GAP  sv 01_latch_comb: rtl pass, synth fail, netlist -, bit fail
  ...
  PASS sv 92_display_in_design: rtl pass, synth pass, netlist pass, bit pass
  synthesis 62/123, all four stages 29/123, Vivado-supported probes passing all four stages 28/105, known gaps 82

passed 166, failed 0, known gaps 82
real 527,76                                                           (exit 0; rows-final.tsv has 123 rows)
$ cat summary.md                                                      (one line)
SystemVerilog probes (yosys 0.69+post, iverilog 13.0): synthesis 62/123, all four stages 29/123, Vivado-supported probes passing all four stages 28/105, known gaps 82

$ PATH=<yowasp yosys 0.68 shim>:$PATH SV_OUT=<chunk>.tsv test/sv/run.sh <25 ids>     (CI's yosys; brew untouched; 5 runs)
test/sv: 25 probes, yosys 0.68, iverilog 13.0  (expect.tsv was measured with yosys 0.69+post, iverilog 13.0: only regressions and new silent wrongs fail)
matches expect.tsv: 25/25, 25/25, 25/25, 25/25, 23/23        (175 s, 149 s, 129 s, 61 s, 131 s; 0 BAD)
synthesis 14+16+14+6+12 = 62/123, all four stages 9+8+7+0+5 = 29/123,
Vivado-supported all four 9+8+6+0+5 = 28 of 20+24+19+21+21 = 105, known gaps 14+16+15+21+16 = 82
notes: 11 "netlist: testbench only, iverilog cannot compile the RTL", 1 "the CLI took counter for the top"
$ diff <the native rows, id to bit> <the 0.68 rows, id to bit>                          (identical, 123 rows)
$ PATH=<the same shim>:$PATH test/sv/eqv_check.sh <each of the 48 cases>
as expected: 48 of 48; red: none                              (151 s)
$ readlink /opt/homebrew/bin/dewfpga; yosys -V
<the repo>/bin/dewfpga
Yosys 0.69+post (git sha1 143eb14f9cc55d6f8927e68523b0c9d2166ed02c, ...)

Round 7:

$ GITHUB_STEP_SUMMARY=<absolute path> SV_OUT=rows-final.tsv test/run.sh      (from a scratch folder)
== static
  PASS personal-path check sees test/run.sh                         (new)
== probe runner (test/sv/run.sh on a copy, one probe each)
  PASS a testbench that prints PASS while dewfpga sim exits 1 is an rtl fail      (new)
  PASS the score line is counted again from the rows                (now also: no score line, 'Synthesis ...')
  PASS eqv.py: a wrong netlist that matches sim's x-optimistic copy at some compares is caught
  PASS eqv.py: a netlist that shows only what sim shows while its register is x is caught
  PASS eqv.py: an x in sim's copy excuses no x in the netlist
  PASS eqv.py: a reset on a switch is pressed first, like a button
  PASS eqv.sh: a register that shares its variable with an assign is started
  PASS eqv.py: an FDSE_1 and an FDPE_1 with INIT x start at 1, as on the board
== SystemVerilog probes (test/sv)
  PASS sv 31b_negedge_set: rtl pass, synth pass, netlist pass, bit pass
  GAP  sv 93_two_roots_file_name: rtl pass, synth pass, netlist fail, bit pass (note: the CLI took counter for the top)
  GAP  sv 94_power_var_base: rtl pass, synth pass, netlist pass, bit fail
  PASS sv 95_empty_submodule: rtl pass, synth pass, netlist fail, bit fail
  GAP  sv 96_port_one_bit_vector: rtl pass, synth pass, netlist pass, bit fail
  GAP  sv 96b_port_offset_range: rtl pass, synth pass, netlist pass, bit fail
  GAP  sv 97_ram_style_block: rtl pass, synth pass, netlist fail, bit pass
  PASS sv 98_module_named_primitive: rtl pass, synth fail, netlist -, bit fail
  PASS sv 99_string_parameter: rtl pass, synth fail, netlist -, bit fail
  ...
  synthesis 69/132, all four stages 30/132, Vivado-supported probes passing all four stages 29/111, known gaps 87
passed 178, failed 0, known gaps 87                          (583 s; exit 0; rows-final.tsv has 132 rows, 178 PASS, 87 GAP, 0 FAIL)
  final run, after every file but these notes was written: the same, 582 s, the same 132 rows
$ cat summary.md                                              (one line)
SystemVerilog probes (yosys 0.69+post, iverilog 13.0): synthesis 69/132, all four stages 30/132, Vivado-supported probes passing all four stages 29/111, known gaps 87
$ SV_OUT=<chunk>.tsv test/sv/run.sh <the 132, in five runs of 27, 27, 27, 27 and 24>
matches expect.tsv: 27/27, 27/27, 27/27, 27/27, 24/24                (the first 118 s)
synthesis 15+19+13+10+12 = 69/132, all four stages 10+11+4+1+4 = 30/132,
Vivado-supported all four 10+11+3+1+4 = 29 of 22+26+21+23+19 = 111, known gaps 15+15+20+22+15 = 87
(the rows, id to bit, equal those of the full run)
$ PATH=<yowasp yosys 0.68 shim>:$PATH test/sv/run.sh <the 9 new probes and 13d>     (CI's yosys; brew untouched)
test/sv: 10 probes, yosys 0.68, iverilog 13.0  (expect.tsv was measured with yosys 0.69+post, ...)
matches expect.tsv: 10/10
$ PATH=<the same shim>:$PATH SV_OUT=<chunk>.tsv test/sv/run.sh <the 132, in the same five runs>
matches expect.tsv: 27/27, 27/27, 27/27, 27/27, 24/24        (193 s, 186 s, 115 s, 113 s, 117 s; 0 BAD)
synthesis 69/132, all four stages 30/132, Vivado-supported all four 29/111, known gaps 87 (the same sums)
notes: 11 "netlist: testbench only, iverilog cannot compile the RTL", 2 "the CLI took counter for the top"
$ diff <the native rows, id to bit> <the 0.68 rows, id to bit>                          (identical, 132 rows)
$ PATH=<the same shim>:$PATH test/sv/eqv_check.sh <each of the 54 cases>
as expected: 54 of 54; red: none                              (172 s)

The breakers' netlists, round 6's eqv.sh, then today's:
  digit counter, one LUT bit flipped   EQV PASS: 196608 output bits compared, all equal (at 3072 compares to the RTL as dewfpga sim starts it, ...)
                                       EQV FAIL: 3072 of 196608 compared output bits differ between the RTL and the netlist
  digit counter lost (led = 1111)      EQV PASS: 196608 output bits compared, all equal (at 9216 compares to the RTL as dewfpga sim starts it, ...)
                                       EQV FAIL: 9216 of 196608 compared output bits differ between the RTL and the netlist
  one-hot FSM, reset on sw[15]         EQV FAIL: 10 of 196608 compared output bits differ between the RTL and the netlist
                                       EQV PASS: 196608 output bits compared, all equal
  the same, reset on btnC              EQV PASS: 262144 output bits compared, all equal          (both)
  counter on led[3:0], assign the rest EQV FAIL: the RTL compiles but the equivalence bench does not
                                       EQV PASS: 262144 output bits compared, all equal
  the corpus Gray counter (6 FDRE)     EQV PASS: 36864 output bits compared, all equal (at 9 compares to the RTL as dewfpga sim starts it, ...)
                                       EQV PASS: 36864 output bits compared, all equal
  counter with no start, led[3:0] x    EQV FAIL: 49152 of 196608 compared output bits differ between the RTL and the netlist   (both)
  negedge set counter (FDSE_1, INIT x) EQV PASS: 196608 output bits compared, all equal          (both)

The new eqv_check cases on round 6's eqv.py and eqv.sh:
  simflip   red   EQV PASS: 196608 output bits compared, all equal (at 3072 compares ...)
  simlost   red   EQV PASS: 196608 output bits compared, all equal (at 9216 compares ...)
  swreset   red   EQV FAIL: 10 of 196608 compared output bits differ between the RTL and the netlist
  partassign red  EQV FAIL: the RTL compiles but the equivalence bench does not
  xhold negset    as expected (they pin what round 6 already did)
All 48 old cases on today's eqv.py and eqv.sh before the five were changed: 48 of 48 as expected (40 s).

eqv mutations on a copy of today's tree, and the cases that go red (54 cases; none without a mutation):
  the x test dropped from the sim copy's rule                   xhold
  STARTS_AT_1 without FDSE_1 and FDPE_1                         negset
  reset values ignored (every register started at 0)           fillset fillaset fillasym
      (the same with their resets still on sw[15]: all five green, the press hides the start)
  the sim copy counted as resolved from the start               simlost romstart
  the excuse per compare again                                  simflip simlost
  the excuse removed                                            romstart
  switch resets not pressed                                     swreset
  no bit-by-bit start (--bits ignored)                          partassign

Runner and test/run.sh mutations, the check that sees each:
  the rtl stage's exit code not required        st_rtlexit: PASS on the runner, FAIL on the mutant
                                                (the runner on 07 with the planted $error: BAD 07_enum_packed_array rtl fail)
  the runner prints no score line               sv_rows: 'test/sv/run.sh printed no score line, its rows make it
                                                'synthesis 1/2, ...'', fail=1 (round 6's sv_rows: fail=0)
  a home-folder path appended to test/run.sh    round 6's personal_paths: 0 lines; today's: 1 (test/run.sh:469)

13d's testbench, all 432 single-bit LUT INIT flips of the product's netlist, in one simulation (408 s):
  caught 414 of 432 (the breaker's count with the old testbench: 308)
  the 18 left: LUT _32_ (it drives led[12]) bits 6 7 12 13 14 15 18 19 22 23 24 25 26 27 28 29 30 31
  input combinations of _32_ that never occur, every grid value x every row and column (1048576 cases, 7 s):
                   6 7 12 13 14 15 18 19 22 23 24 25 26 27 28 29 30 31
  the breaker's flip (LUT 3, bit 27): old testbench PASS, new FAIL: grid=84f5 sw=c45a led=54f5 want=44f5

The new probes' testbenches, on the RTL (iverilog) and with one edit:
  93   PASS    the plain counter as top                FAIL: after 1 clocks led=1, want 0
  94   PASS    sw[7:0] ** 3                            FAIL: sw=0002 led=8 want 4
  94   PASS    sw[7:0] * 2                             FAIL: sw=0001 led=2 want 1
  95   PASS    led = {sw[14:0], b}                     FAIL: sw=0001 led=000X
  96   PASS    assign led = sw                         FAIL: 2 of 2
  96b  PASS    assign led = sw                         FAIL: 16 of 16
  97   PASS    writes the address, not the data        FAIL: address 0 led=0000
  97   PASS    reads the next address                  FAIL: address 0 led=000a
  98   PASS    INV passes a through                    FAIL: sw=0000 led=0000
  99   PASS    the ROM read at ~a                      FAIL: address 0 led=00a6
  31b  PASS    posedge instead of negedge              FAIL: the rising edge moved q: led=0000
  31b  PASS    the set loads 0                         FAIL: the set on the falling edge: led=0000
  31b  PASS    the preset loads 0                      FAIL: the preset, before any edge: led=0003

Measured apart for the new rows:
  93 as lab4.sv        ERROR: 2 modules could be the top (nothing instantiates them): counter, top. Name it:  dewfpga bit counter
  corpus 061 (its own XDC)   pnr ok: 6 LUT, 6 FF, 830.56 MHz (PASS at 100.00 MHz); the plain counter's .bit, exit 0, no 'top module:' line
  94 with 16'd3 ** sw[2:0]   the same 'no Bels remaining of type '$pow'' and 'does not fit'
  94 with 2 ** sw[3:0]       pnr ok: 16 LUT, 0 FF;   with sw[7:0] * sw[7:0]: pnr ok: 0 LUT, 0 FF (a DSP48E1)
  95 with b unused           pnr ok: 0 LUT, 0 FF, no clocked paths, timing not applicable
  96, nextpnr-xilinx on the product's top.json: Constraining 'led' to site 'IOB_X0Y3', 'sw' to 'IOB_X0Y11'
      (the sites of led[0] and sw[0] in a 16-bit design)
  96b, the same with its own XDC: exit 0, sw[1] to sw[4] on IOB_X0Y12, Y10, Y9, Y7 (the sites of sw[1] to sw[4])
  97 with rom_style = "block" on an initialized ROM: RAMB18E1: 1/150, pnr ok: 0 LUT, 0 FF, no clocked paths
  98 as GND, VCC, MUXF7: cells_sim.v:28, :24, :342: ERROR: Re-definition of module ...;  as BUF: pnr ok: 1 LUT
  99 untyped: pnr ok: 7 LUT, 0 FF; its netlist: the testbench PASS, EQV PASS: 1048576 output bits compared, all equal

The port-line fix (check_xdc.py --fix-ports) on eight placements of a comment, 1 = the line was fixed:
  // a comment after seg,  at the end of the seg, line                1   (the comment itself ends in a comma)
  // the segments          at the end of the seg, line                0
  // ...                   on a line of its own between the two       0
  /* ... */                on a line of its own between the two       0
  /* ... */                between seg, and logic dp on one line      0
  /* ; ... */              after seg,                                 0
  // ...                   after logic dp,                            1
  /* ... */                before the comma after seg                 1

Not verified.

Not done here.

#3 · Silent wrongs stop the build · 26–27 September

In numbers. A silent wrong is a design that builds a bitstream and does not do what the simulation showed: expect.tsv rows with bit=pass and netlist=fail. Before #3: 17. After: 2 (35, an MMCM, and 97, a block RAM: their netlists have no simulation model, the circuits are right). Of the 15 others, one became right (33, a PMOD pin tied to 'z and read back) and 14 stop with an ERROR that names the student's file and line and says how to fix it; no .bit is written. Three probes that used to fail now pass all four stages (26 an array of instances, 33, 56 a file-scope enum), so the Vivado-supported probes passing all four stages went 29/111 → 32/111 and all four stages 30/132 → 33/133 (one probe added, 95b). Synthesis 69/132 → 57/133: the silent wrongs now stop there. Known gaps 87 → 79. test/run.sh: 178 passed before, 187 passed, 0 failed, 79 known gaps after (9 min 10 s). dewfpga bit on the blink example: 4.6 s before, 4.1 s after (three yosys runs instead of one: a preprocessor dump, elaboration, synthesis; measured once each, warm).

What the student sees. test/sv/39_undeclared_name (a typo, summ for sum), dewfpga bit, before:

design.sv:4: Warning: Identifier `\summ' is implicitly declared.
Warning: Wire top.\summ is used but has no driver.
pnr ok: 0 LUT, 0 FF, no clocked paths, timing not applicable   (full log: top.log)
top.bit  2.2 MB
exit 0

after:

design.sv:4: Warning: Identifier `\summ' is implicitly declared.
ERROR: design.sv:4: summ is not declared anywhere and nothing drives it: yosys made it a 1-bit wire stuck at x and built the design without it (dewfpga sim refuses this code). Declare it, or check the spelling (a typo of a declared name).
exit 2

05_two_block_driver (one register written from two always blocks) printed 22 lines of yosys warnings and top.bit 2.2 MB, exit 0; now, after the first warning:

ERROR: design.sv:3 and design.sv:4: cnt is written from two always blocks (design.sv:3 and design.sv:4). Drive each signal from one always block: yosys resolved the conflict to a constant, so the board would not do what the simulation showed.

and no bitstream. 93_two_roots_file_name (the plain counter and the board's top in one file named counter.sv) built the plain counter, counter.bit 2.2 MB, exit 0, without a word; now:

ERROR: 2 modules could be the top (nothing instantiates them): counter, top. Name it:  dewfpga bit counter
exit 1

What changed, one mechanism per class. Product files only: templates/Makefile (111 → 252 lines), templates/check_xdc.py (192 → 318), bin/dewfpga (162 → 164).

Tried and did not work. -noautowire (above). yosys check -assert for the two-block case: it also counts the "no driver" warnings legal designs print. connect -unset for the 'z pin: it rewires the read too and the LED reads x. A first .mem grep pattern with a literal ( broke make 3.81 ("unterminated call to function sort"). Parsing top.il in the awk's BEGIN block read an empty file (the pipe starts awk before yosys writes it); it is read lazily now. hierarchy -check in the elaboration run refused the UNISIM primitives (34 BUFG, 35 MMCM: "not part of the design"); the check is the synthesis run's, which has the cell library. The first two-block rule fired on 52's tri bus with "(line unknown)"; a conflict without an always-block line is not this error.

Break rounds. Two, each a student (up to 10 new designs against the claim) and a regression reader (the diff, the 31 protect probes, the legal neighbour of every new rule), then a fix agent per task and an integration; then a judge was added to the script for the next updates (this run was made before it). Round 1: 9 reports, 8 in scope (constant start value from localparam int refused; $isunknown in a block comment and under `ifndef SYNTHESIS; an interface port on its own line refused; the typedef scan firing on a comment; import pkg::* reported as a typo; always_ff + assign printing "(line unknown)"; sim's two-roots message saying dewfpga bit). Round 2: 5 reports, 5 in scope (the typedef scan missing an `included .svh; $finish under `ifndef SYNTHESIS; a package file sorting after its user; a struct member taken for a type name; a second "(line unknown)" line in a submodule). All reproduced and fixed, each with the legal neighbour re-run. Held in both rounds: undeclared names in a .v submodule, hierarchical names two levels deep and into a generate block, two always_ff on the same edge in a parameterised submodule, an active-low async load in a .v file, $isunknown in an included function, start-value spellings in reg, typedef'd and parameterised forms, two roots across .v and .sv, two always_ff writing different bits of one vector (builds), a reset branch loading a parameter (builds), dotted names of a packed struct (builds).

Tests. test/sv/run.sh on the 16 target rows: each row's stages and its today quotes were rewritten to what the product prints (the runner fails a quote no stage printed). Two runner checks changed because the product changed: "a new silent wrong fails with any yosys" plants its wrong row on 97 (it used 05, which the product now refuses), and the personal-path scan skips the gitignored .claude/ folder (the workflow's worktrees live there). The full suite ran three times in the main session: the first run found the 34/35/52 regressions above and the 26/56 gains; the second failed only on 22b's stale quote (its message changed: the interface scan now reads every .sv in the folder, not only the module files, so an interface in its own file is classed as an interface, and the row quotes what yosys then prints); the third is the line quoted in the numbers.

Found and left to later updates.

CI. The first CI run of #3 was red on 95b: the message named the two files in the other order (design_old.sv:2 and design.sv:1). The CLI's file list came from the shell glob, whose order follows the locale (on the CI runner _ sorts before .), so the tools read the files in a different order on different machines and messages named them differently. The list is sorted with LC_ALL=C now, and the twice-defined message sorts its pair; the second CI run is the one that counts.

Orchestration, for the record. One workflow: 3 build agents in their own worktrees, one integrator, 2 breakers × 2 rounds, up to 3 fix agents per round, 14 agents in 3 h 0 min, then the harness killed the last integrator (a 20-probe run is silent for more than 3 minutes; it had already applied both fixes). Lessons written into the script: at most 5 probes per call, fix agents copy the integrated product into their worktree first (each spent 10 minutes discovering that their worktree was at HEAD), one scratch folder per agent (two overwrote each other's repro), a judge with a reproduced criticism before the close.

#4 · What Vivado accepts compiles: yosys-slang as the second reader · 27 September

In numbers. Vivado-supported probes passing all four stages 32/111 → 59/111; all four stages 33/133 → 64/133; synthesis 57/133 → 108/133; known gaps 79 → 52; silent wrongs 2 → 2 (35, 97, no simulation model). test/run.sh: 187 passed before, 213 passed, 0 failed after (13 min 48 s; the probe stage is longer, every refused design now runs through two readers). dewfpga bit on blink: 4.1 s → 3.8 s (the first reader builds it; a design that falls to the second reader pays one more yosys run). dewfpga check has a tenth row, yosys-slang, and the install grew by one step. CI on the commit: 212 passed, 0 failed, 26 min 59 s (15 min 32 s for #3: the slang build on the runner is the difference).

The reader decision, measured first (4a). One agent ran the synthesis stage of all 133 probes under four front ends: (A) yosys' read_verilog -sv, today's; (B) read_slang (the yosys-slang plugin, commit 9676786, 23 September 2026); (C) slang first, read_verilog when slang refuses; (D) read_verilog first, slang when it refuses. Counted: Vivado-supported probes whose netlist is right (the testbench passes on the netlist and eqv.sh finds it equal to the RTL). A 50, B 80, C 87, D 88. B alone loses 8 that A builds right (21, 31, 31b, 32, 33, 34, 54, 84): slang refuses a blocking assignment after a non-blocking one and repeat in always_comb, emits $aldff for a constant async reset (which #3's guard read as a load from a signal), and lowers assign JA[7:1] = 'z to a $buf of x, so 33 reads x from the pin. D has none of these (they take the first path) and gains 38 the first reader refuses. The rule was fixed before the run: most right netlists with zero regressions among the 33 all-pass probes; D. Its holes, named in the same report and closed here: 60 (ref arguments, which slang drops: a silent wrong), the two-driver and async-load guards on slang's output, 01 (slang builds an always_comb latch as a LUT loop, not a latch). Install cost: one more cmake build in install.sh, 2 min 59 s for the whole install.sh re-run on this machine (every other step stamped, so that is the clone and the build), 8.9 MB of slang.so.

What the student sees. 16b_func_return (return inside a function), dewfpga bit, before:

design.sv:3: ERROR: syntax error, unexpected TOK_ID
exit 2

after:

note: top read with yosys-slang (yosys' own reader refused this code)
pnr ok: 7 LUT, 0 FF, no clocked paths, timing not applicable   (full log: top.log)
top.bit  2.2 MB
exit 0

58_hier_name (assign led[1:0] = u_fsm.state;) was #3's error; now pnr ok: 3 LUT, 2 FF, 420.88 MHz, top.bit 2.2 MB, and the netlist equals the RTL. 01_latch_comb (an if without else in always_comb) stopped with "Give q a default value"; now:

warning: design.sv:3: latch inferred for q (an if without else in always_comb); Vivado builds it too, with the same warning; if you meant a register use always_ff, if a latch use always_latch
pnr ok: 0 LUT, 1 FF ...   top.bit  2.2 MB

with one LDCE in the netlist, as Vivado builds it (UG901 Ch.5, Latches). 02_unnamed_inst (inv(sw[0], led[0]);) was an error; now the CLI writes the name into the file, as it already did for the course's seven-segment port line, and says so:

note: design.sv:5: named the instance:  inv u_inv(sw[0], led[0]);   (the standard and Yosys need it; Vivado accepts both)

11_enum_methods (s.next()) is still refused, now by both readers, and the student gets slang's line:

yosys' reader:  design.sv:5: ERROR: Can't resolve function name `\s.first'.
ERROR: design.sv:5: s.next() is an enum method that steps through the values (next/prev). yosys does not build enum methods, and yosys-slang builds first()/last() only. Write the step out as a case on s ...

What changed.

Tried and did not work. tribuf -logic before synth_xilinx for the internal tri bus (52, 33c): nextpnr still finds $_TBUF_ cells (a buffer that reaches a port is kept); 70's $and/$or from wor/wand and 94's $pow were not reached (all three stay #5's "does not fit" message). Rewriting only the "Latch inferred" line as a warning: yosys raises it as an error for always_comb, so the design fell to slang and built the LUT loop. The first latch rule matched $mux only; a case without default is a $pmux (round 2). The first ref scan and the $isunknown scan fired inside strings and block comments (fixed: the $isunknown scan reads yosys' preprocessor dump; the ref scan is still text-based and a $display string holding , ref x stops the build: open, #5). 23c on the slang path was not measured raw: the package scan stops it before either reader, since the CLI hands both readers the module files only.

Break rounds and the judge. Round 1: the student breaker died after three tool calls (the disk filled: see below); the regression reader sent 3 in-scope reports (the slang-path latch as a LUT loop, high; the function-named-like-a-module rewrite, medium; ref in a string, low), all reproduced, the first two fixed. Round 2: 2 + 3 reports (a case latch on the slang path, high; a .v next to a .sv that needs slang read as SystemVerilog, medium; the plugin dormant as installed, medium; the wording "an if without else" for a case; a task call named as an instance, low); the first three fixed, the case wording fixed with the $pmux rule. The judge (an agent that did not build any of it) returned fix with 10 criticisms: the $pmux latch not yet in the main tree (it was in a worktree the disk-full integrator could not apply), the metric blind to a wrong slang netlist (below), the installer deleting a working plugin first, 11's row stale, a slang.so that exists but does not load, and the guide quoting a 217 s install next to a step that adds 4 minutes. Its process criticisms are in the orchestration note below. What held, in the breakers' words: a latch in a submodule in its own file on the slang path (ERROR with hold.sv:3), always_latch on the slang path (builds, 1 FF), four unnamed instances in one file with #(.N(4)) and a generate, an unnamed instance in a .v with the course port line, two always blocks and an async load on the slang path (both ERRORs with lines), $isunknown on a design only slang reads, nested and generated latches, an interface design with a constant async reset (4 FDCE, no false $aldff error), the 47 protect probes and the 6 canaries unchanged.

The metric was blind, and the suite changed for it. test/sv/eqv.sh listed the RTL's registers with read_verilog -sv alone, so for every probe only slang can read it printed EQV FAIL: yosys could not list the RTL's registers after the testbench had passed on the netlist, and 33 slang-built rows were recorded as bit=pass, netlist=fail: the suite's silent-wrong count went from 2 to 35 while the netlists were right (a scratch copy of eqv.sh with read_slang as the fallback: 16 EQV PASS, 16 EQV SKIP where iverilog cannot compile the RTL, 1 still unreadable, 74). The judge called it: for those 33 a wrong slang netlist would pass the suite as the recorded state. eqv.sh now lists the registers with the same two readers as the product, so the netlist stage sees slang-built designs. 45 rows of expect.tsv were then rewritten from the logs of a run with the fixed eqv.sh: the stage columns and a today column quoting what the product prints (the note: top read with yosys-slang line, the pnr ok line, the EQV PASS line, or the ERROR that stops it).

Tests. The full suite ran three times in the main session after the integration: the first run found 45 rows to rewrite and 7 CLI checks the product change had turned (an unnamed instance is a note now, not an error; a .v with SystemVerilog inside stops, as in Vivado; the runner's self-checks that planted their mutation on 01's and 02's old messages plant it on the new ones, st_score plants its gap on 11); the second found 5 rows whose quotes the runner's parser cut at a backtick, a double quote or a home path; the third passed 213 and failed 1, the self-check st_fixtext, whose mutation string had a colon the source does not have: fixed and reproduced by hand (the mutated copy prints the expected no stage printed it line); the full suite on the committed tree is CI's run. test/sv/eqv.sh lists the RTL's registers with both readers. .claude/rows-from-logs.py rewrites a probe's row from the logs of a KEEP=1 run (used for the 45 rows; it cuts a quote at an apostrophe, a backtick, a double quote and /Users/, which the runner's quote parser and the personal-path check would trip on).

Found and left to later updates.

Orchestration, for the record. 19 agents, 1 h 58 min: 3 build (45, 30, 30 minutes), 1 integrator, 2 break rounds, 3 + 2 fix agents, a judge, 3 fix agents on the judge's findings. The machine's data volume hit 100% during break round 1: 40 GB of .vcd files that #2's breakers had left in a scratch folder on 25 September, 26 GB of KEEP=1 work folders under $TMPDIR, 5 GB of the 4a measurement's probe copies; the round-1 student, the round-2 and round-3 integrators and the judge ran without a shell (ENOSPC on the harness's own output file). The round-2 and round-3 fixes were applied by hand from their worktrees in the main session after the disk was cleared. The judge's process notes, applied to the next update: the integrator must run the demo probes itself and quote them (11's row went out stale); a probe belongs to one task (08b was on two lists); a fix agent commits to a branch so the integrator merges commits, not worktree files; the guard must be measurable by the suite before the run (the eqv.sh reader was not); a 4 GB free-disk check before the run, and KEEP=1 work folders deleted by the agent that made them.

#5 · Every message in one shape, with its code and its page · 27 September

In numbers. Over the 35 probes whose messages this update owns (a KEEP=1 run of each, before and after, rtl.log and bit.log): lines the product prints as an ERROR, a warning or a note, 68 before, 70 after; of them, carrying a code in brackets, 0 → 59, and 36 of the 59 start with the student's file:line (the rest are the toolchain, the XDC as a whole, timing, or the two notes read-with-slang and no-create-clock). The 11 uncoded lines after are the tools' own lines, kept above the coded one (yosys' Multiple edge sensitive events, nextpnr's Unable to place cell, yosys' Failed to get a constant init value). The catalog: 10 pages → 78, one per code, generated from docs/errors.md. The four stage columns of expect.tsv are byte-identical before and after (md5 of the columns, both trees): this update changed words, not verdicts; Vivado-supported probes passing all four stages 59/111 → 59/111. test/run.sh: 213 passed before, 213 passed, 0 failed after (14 min 41 s; two rows were requoted after the run and re-run alone, CI runs the whole suite on the commit).

The shape. One line per problem: the student's file and line, the kind, the code, what is wrong, the fix in one sentence, the page.

design.sv:4: ERROR [undeclared-name]: summ is not declared anywhere and nothing drives it: yosys made it a 1-bit wire stuck at x and built the design without it (dewfpga sim refuses this code). Fix: declare it, or check the spelling (a typo of a declared name). https://nosey-dewdrop.github.io/dewfpga/errors/undeclared-name/

A problem with no place in the student's code starts with the kind (ERROR [board-not-found]: ...). yosys', iverilog's, nextpnr's and yosys-slang's own lines stay as they are, above the product's line; when the product already made a coded ERROR, slang's diagnostic is shown as a plain yosys-slang: ... line, not a second ERROR. Warnings and notes carry a code too (warning [latch], note [unnamed-instance]). The code is the page's folder under /errors/; the ten pages that existed keep their slugs.

What the student sees. 77_initial_from_signal (initial num = sw;), dewfpga bit, before:

yosys' reader:
  ERROR: Failed to get a constant init value for \num: \sw
ERROR: design.sv:3:17: reading net state during design initialization unsupported (yosys-slang)

after:

ERROR: Failed to get a constant init value for \num: \sw
design.sv:3: ERROR [initial-from-signal]: `num = sw` here is a start value, not a wire: it reads sw once, at time 0 (IEEE 1800-2017 6.8), and the chip has no power-up value that follows a signal, so yosys stops. Fix: for a wire write  assign num = sw;  for a register give num a constant start value (logic [15:0] num = '0;) and load sw with the clock (if (btnC) num <= sw;). https://nosey-dewdrop.github.io/dewfpga/errors/initial-from-signal/

31b_negedge_set's pnr line said 337:slice$1535.C': 358.68 MHz; it says clk 358.68 MHz. 04_dual_edge got "a signal is written from two always blocks with different edges"; it gets design.sv:3: ERROR [dual-edge]: this always block is sensitive to both edges of clk (@(posedge clk or negedge clk)), so cnt would change on every edge: no flip-flop on the chip does that ... Fix: clock the block on one edge. 33c (a tri-state buffer on an output port) got nextpnr's no Bels remaining of type '$_TBUF_' and "the design does not fit"; it gets design.sv:4: ERROR [internal-tristate] naming the z lines and saying that Vivado converts such a driver to logic and this chain cannot yet.

What changed.

Break rounds and the judge. Round 1, the student: 5 findings, all reproduced (an empty submodule with a parameter built silently through slang, its outputs x, high; the textbook tri-state mux written inline on a tri net reported as two always blocks, high; a dual-edge list spanning two lines reported as an async reset with a clear, medium; 3 ** sw[2:0] reported as a signal base, medium; the .v note after a coded error on plain Verilog, low). Round 2: 5 + 2 (two z drivers in a generate loop and always @* plus assign on one signal both reported as two always blocks, high; an undeclared name inside a generate sent to the unsupported-construct page, medium; the latch warning saying "an if without else" for a case, low; a ** message quoting an expression not in the file, low; two inline z drivers under yosys' own reader named by a yosys-internal net, medium; 33c's message claiming Vivado converts a top output's driver to logic where UG901 says OBUFT, low). The judge (an agent that built none of it) returned fix with 6 criticisms, 5 reproduced and in scope: an async reset with the wrong polarity (posedge btnC, if (!btnC)) reported as "loaded from a signal" with a fix naming rst, load, sw that are not in the design (high); the same message hardcoding (posedge clk or posedge btn) after 16 raw Emulating async set + reset lines (medium); timing-not-met advising create_clock -period 20.000 (50 MHz) to a design that reaches 33.61 MHz (medium); a case latch at top level called an if (medium); a driver conflict in a submodule printed twice, q and u_cnt.q (low). Three fix rounds closed all of them: the driver conflicts are named by their source lines and their real kind (always-and-assign, two-assign-drivers, tristate-to-logic), async-reset-polarity is a code of its own, the END-branch message uses the student's names and edges, the Emulating lines are folded, [latch] tells case from if, and the timing fix proposes a period the measured frequency allows. Held: the 35 targets and the 6 canaries with every stage unchanged, the CLI checks that grep the messages (rewritten by the integrator), two-file designs after round 3.

Tried and did not work. A bare # inside a grep pattern in a make variable starts a make comment and cut the awk program on every probe (\#). The first clock resolver read every module in the json. The first 5C link check flagged /dewfpga/sim/ and /dewfpga/templates/*, which CI copies next to site/ (the check resolves against site/ then the repo root), and the query string of ?view=tb. The .v note printed after a coded error that already named the cause (dropped there).

Tests. The suite's message greps (the CLI checks that look for a message, and the runner's self-checks that plant a mutation in a message) were rewritten by the integrator to the new lines; 44 rows of expect.tsv were requoted from logs after the build, 2 more after the judge's fixes (03b, 22b), all with .claude/rows-from-logs.py, which now recognizes the file-first shape. The full suite: 213 passed, 2 failed on the two stale rows, then those two re-run alone: matches expect.tsv: 2/2.

Found and left to later updates.

Orchestration, for the record. 15 agents, 2 h 22 min, no agent died: 3 build (45, 35, 35 minutes), 1 integrator, 2 break rounds (student + regression reader), 3 fix agents, the judge, 3 integrations. The integrators committed in the main tree (four commits, squashed into this one). The judge's process notes, applied to the script: the diff is read against the update's base commit, not HEAD (the update may already be committed); the metric must reach the judge as a number, not as rows (the coded-line count over all 35 targets was lost between the integrator and the judge and was re-counted here); a message with a formula in it (create_clock -period) is checked against the number printed next to it.

#6 · The CLI's untested paths get checks, one number for the count, a test that reads the bitstream, the board test ready for the board · 27 September

In numbers. test/run.sh: 213 passed before, 237 passed, 0 failed after (15 min 7 s; the 24 new checks add about 35 s), plus one SKIP: flash on the board: no board: plug the Basys3 in and run test/run.sh again. test/run.sh --list prints every check's name, 133 probes and 293 checks (the two clean-install checks and the board check are counted on every machine, so no single plain run reaches 293: the header says which ones a run skips). README, README.tr and the cli page quote 293 and 133 from --list, and a check compares them. Vivado-supported probes passing all four stages 59/111 → 59/111 (this update changes tests, not verdicts); the stage columns of expect.tsv unchanged. test/bit-check.sh reads the frames back out of blink.bit and compares them with the .fasm: bit ok: blink.bit holds the 5408 frames of blink.fasm (105 non-zero), read back from 1 FDRI write(s); another design's fasm, a .bit cut in half and a .bit with one bit flipped in a used frame are refused. The board checks are written and skip without a board; their first real run is Damla's, with the Basys3 on USB.

6A · The CLI's untested paths get checks; test/run.sh --list prints the count the docs quote.

test/run.sh --list prints every check's name, then 133 probes and 289 checks, and exits 0 without running anything; ONLY=regex test/run.sh runs the checks whose name matches (the probes are skipped). README, README.tr and site/cli quote 289 checks / 289 kontrol and 133 SystemVerilog probes, and one check greps the three files for the two numbers --list printed, so a stale number fails the suite.

Paths that had no check before (15 in == error paths, 3 in == rebuild rules): bit src/x, bit --foo, an FPGA_HOME with a space, a UTF-8 BOM, a testbench without $finish (SIM_TIMEOUT=2), a testbench ending with $stop, a _tb.v testbench (Verilog-2005), a missing IOSTANDARD, a failed build removing the old .bit, uninstall removing what install built and keeping the rest, uninstall with nothing installed, a design only yosys-slang reads, the note naming dewfpga install when slang.so is absent, two clocks named on the pnr line; a changed .svh include rebuilds and the .bit and .frames differ byte for byte, a changed .mem ROM rebuilds and the .bit differs, a renamed testbench recompiles (sleep 1.1 first: make compares whole seconds). personal_paths adds --exclude=.git (in a worktree .git is a file holding a home path).

Integration: 6A and 6B both edited test/run.sh and clashed in three hunks (the shellcheck line, the old flash checks, the section after the rebuild rules); resolved by hand keeping both. 6A's count was 284 before 6B's block (+5 checks without a board); the number on the pages is what --list prints after both: 289. Run alone: ONLY='new \+ dewfpga bit|unnamed instance|blink\.bit|cut in half|flipped|flash|quote the numbers|shellcheck' test/run.sh -> passed 11, failed 0, known gaps 0, with SKIP flash on the board: no board: plug the Basys3 in and run test/run.sh again. Untested tonight: the flash check with a board (no board plugged in), the full suite.

6B · A test looks inside the bitstream; the board test is ready for the board.

test/bit-check.sh <top.bit> <top.fasm|top.frames> reads the bitstream back. No test had ever opened a .bit: bit checked the file's size and that the .frames file was the same twice. Now the .bit is parsed as UG470 chapter 5 lays it out: the header up to the sync word AA995566, then the type 1 / type 2 configuration packets; the FDRI write is cut into 101-word frames (UG470 table 5-18, 7-series) and each frame gets its address by walking part.yaml's columns the way xc7frames2bit does (block type, top/bottom, row, column, minor; two zero frames between rows and block types). Every one of the part's 5408 frames is compared with what fasm2frames.py makes of the .fasm (a .frames file is taken as is; frames it does not name are zero). The one thing not compared is the 13-bit ECC field of word 50, which xc7frames2bit computes and the .frames does not hold. prjxray's bitread is not built in ~/fpga (only xc7frames2bit is), so the reader is 60 lines of Python in the script, on the venv's python and yaml.

What it printed (paths shortened):

test/run.sh: the block == inside the bitstream, and the board (one delimited block after the project template section, marked #6B), seven checks without a board, run alone against a prebuilt blink and probe 07's fasm: passed 7, failed 0. The five bitstream checks are the five lines above, on the blink dewfpga new built ($T/p) and the sw -> led design of the unnamed-instance check ($T/ui). The two flash checks moved here from the build section: dewfpga flash without a board has to exit 1 with ERROR [board-not-found]: board not found and no make: *** trailer, and the run then prints one line: SKIP flash on the board: no board: plug the Basys3 in and run test/run.sh again. With a Basys3 on USB (system_profiler lists Digilent) the block instead runs dewfpga flash on that blink and greps openFPGALoader's own success line, Done (the string in /opt/homebrew/bin/openFPGALoader; SRAM only, gone at power off). The old DEWFPGA_TEST_BOARD=1 gate is gone: a plugged-in board is programmed with blink. Not run with a board tonight: no board was plugged in. The board branch is written from openFPGALoader's strings, not from a run; the first run with a board is the check of that line. shellcheck -S style covers test/bit-check.sh now.

Test count: two flash checks before, seven checks in the block after: +5 without a board (+4 with one). test/run.sh was not run whole here (9 minutes); the block was run on its own.

Fix round 1 (6B-fix), two findings of the judges:

  1. --list printed 288 with a board, 289 without. The board branch held one check, the no-board branch two, and --list walks whichever branch the machine is in, so the number the three pages quote (289) was wrong on the machine the board is plugged into, and the docs check failed there. Reproduced with a fake system_profiler first on PATH (test/run.sh calls it unqualified) that prints Digilent USB Device: test/run.sh --list | tail -1 printed 288 checks and ONLY='quote the numbers' test/run.sh printed FAIL README, README.tr and site/cli quote the numbers --list prints (checks, probes). Now either branch is two checks: with a board, flash on the board: openFPGALoader programs it (its Done line) and flash on the board: exit 0, no ERROR line (dewfpga flash >out 2>&1 && ! grep -q 'ERROR \[' out). After: 289 checks with the fake board and without, and the docs check PASS in both. The board branch is still not run against a board (none plugged in tonight).
  2. bit-check.sh blamed the .fasm when the tools were not where it looked, and it looked only at FPGA_HOME/~/fpga, not at .fpga_home as bin/dewfpga does. Reproduced: FPGA_HOME=/nowhere test/bit-check.sh top.bit top.fasm printed line 25: /nowhere/venv/bin/python: No such file or directory and ERROR [bit-check]: fasm2frames could not read top.fasm. Now the script resolves the folder the way bin/dewfpga line 18 does ($FPGA_HOME, else the repo's .fpga_home, else ~/fpga), checks that the venv python, fasm2frames.py and the part's part.yaml exist before anything else, and names the missing one and where the folder came from: ERROR [bit-check]: /nowhere/venv/bin/python is not there (FPGA_HOME=/nowhere). Fix: run install.sh, or set FPGA_HOME to the folder that holds venv/, prjxray/ and nextpnr-xilinx/. With .fpga_home saying /elsewhere (a copy of the script under a scratch root): ERROR [bit-check]: /elsewhere/venv/bin/python is not there (<root>/.fpga_home says /elsewhere). Fix: .... A real fasm2frames failure now carries its last stderr line instead of "could not read": on a file holding garbage line !!: ERROR [bit-check]: fasm2frames (~/fpga/prjxray/utils/fasm2frames.py) failed on bad.fasm: textx.exceptions.TextXSyntaxError: .../bad.fasm:1:14: Expected S or '=' or '{' or '#' or Identifier or Newline or EOF => 'bage line *!! '. Fix: the .fasm has to be nextpnr-xilinx's own output for xc7a35tcpg236-1; rebuild it with dewfpga bit. The sane run is unchanged: bit ok: top.bit holds the 5408 frames of top.fasm (192 non-zero), read back from 1 FDRI write(s). shellcheck -S style on both scripts: clean.

Fix round 1 (6A-fix + integration), the other findings:

  1. --list printed a different number under FULL=1 and on a board machine. Now every branch is listed whatever the machine: the two board checks, the two no-board checks and the two clean-install checks all count, so test/run.sh --list | tail -1 prints 293 checks plain, under FULL=1, with a fake system_profiler that prints Digilent USB Device, and with both. The run prints fewer PASS lines than that, never more: the branch the machine cannot run prints one SKIP line (SKIP flash on the board: no board: plug the Basys3 in and run test/run.sh again, or SKIP flash w/o board (2 checks): a Basys3 is on USB). The docs check reads FULL='' test/run.sh --list and finds the N probes line by pattern, not by position. README, README.tr and site/cli say 293 and why. Merged with finding 1 above: the board branch keeps 6B-fix's two checks (Done line; exit 0 and no ERROR line), so 289 + 2 board + 2 install = 293. With the fake board: the docs check PASS, the two board checks FAIL (no board is plugged in), as they must.
  2. The bitstream and board checks could not run alone under ONLY=regex: they used $T/p and $T/ui, built by checks the regex had skipped. need_p and need_ui build them when missing. ONLY='blink.bit|\.bit |flash' test/run.sh: the five bitstream checks and the two no-board checks PASS, passed 10, failed 0 (three rebuild/probe-runner checks match the regex too). The == build and == multi-file design checks still need the section's first check in the regex; the header of test/run.sh says so.
  3. Integration note. The tree before this update has no --list: test/run.sh --list there ignores the argument and starts the whole nine-minute suite (the before/after demo killed it after 25 s, exit 142). Its pages said 46 checks. bin/dewfpga, templates/*, install.sh and test/sv/expect.tsv are untouched.

Fix round 3 (6B-fix), one finding of the judges:

  1. A board check that FAILs showed nothing a student could act on. The first on-board check piped dewfpga flash into grep -q 'Done' and the others wrote to their own out file, so $T/out (what bad() prints five lines of) was empty; the same for the seven bitstream checks. Reproduced with a fake system_profiler on PATH printing Digilent USB Device, ONLY='flash on the board' test/run.sh: FAIL flash on the board: openFPGALoader programs it (its Done line) and FAIL flash on the board: exit 0, no ERROR line with nothing under either, passed 0, failed 2; and in a scratch copy of the product with board-not-found renamed, ONLY='flash w/o board': FAIL flash w/o board: board-not-found line, exit 1, bare. Now test/run.sh has say <cmd...>: it runs the command with its output kept in $T/say for the check's grep and printed too, so it lands in the check's output and under a FAIL; the nine checks of the block run the product through it. check() also prints any ERROR [ line past the first five (shown), since the product's ERROR line comes last, after openFPGALoader's own lines. After, the same fake board: FAIL flash on the board: openFPGALoader programs it (its Done line) followed by unable to open ftdi device: -3 (device not found) / JTAG init failed with: unable to open ftdi device / ERROR [board-not-found]: board not found: the Mac sees no Basys3 on USB (openFPGALoader: unable to open ftdi device). Fix: plug the USB into the PROG port, switch the power ON, and try another cable or port. https://nosey-dewdrop.github.io/dewfpga/errors/board-not-found/, the same three lines under the second FAIL; the renamed copy shows its ERROR [board-missing]: ... line under its FAIL; a bit-check refusal made to fail (its "frames differ" wording renamed in a copy) shows ERROR [bit-check]: .../p/blink.bit does not hold the frames of .../ui/ui.fasm: 111 of 5408 frames disagree, first 3: 0x00000a00 word 24: .... Unchanged where it should be: ONLY='blink.bit|\.bit |flash' test/run.sh on the real product, no board: the five bitstream checks and the two no-board checks PASS, one SKIP line; test/run.sh --list | tail -1 still 293 checks; shellcheck -S style test/run.sh clean. Still not run with a board (none plugged in).

Tried and dropped:

Also in this commit. The check uninstall refuses FPGA_HOME=HOME ran the real CLI's uninstall against the real home (the judge read it: the day the guard breaks, the test deletes ~/venv); it runs against a scratch home now, with a scratch fpga and venv that must survive.

Found and left to later updates.

Orchestration, for the record. 15 agents, 1 h 27 min, none died: 2 build (40, 30 minutes), 1 integrator, 2 break rounds, 3 fix agents, the judge, 3 integrations; the integrators committed in the main tree (the first commit went out with the #5 CI fix; the three fix commits are squashed into this one). The judge's process notes: the metric "N passed before → M passed after" needed two full runs no agent was allowed to make (the main session made them: 213 → 237); two agents in one file (test/run.sh) cost a hand merge in three hunks; a before-demo command must be vetted against the before tree (--list did not exist there and started the whole suite); "the same number on every machine" shipped twice without a run on a second kind of machine (env -i would have caught yosys: command not found at line 83 and the board and FULL modes); a check that hides the product's output prints a bare FAIL (fixed in round 3 with say/shown); "each new check broken once" was promised and not evidenced in the integrator's report (the judge did three).

#7 · Safer installation and reproducible simulator distribution · 3 October

The installer checks the destination before replacing it. HOME, its parents and aliases, the filesystem root, Git checkouts, unreadable directories and unrelated nonempty folders are refused. An update reserves a private backup beside the destination and restores the previous installation if moving the new copy fails. If restoration or cleanup is blocked, the error names the retained backup. A failed first installation removes its partial copy when possible.

Uninstall checks and removes the same physical directory. A path containing a symbolic link followed by .. previously passed a check of one directory and removed entries in another. The reproduced scratch-HOME case now exits with ERROR [fpga-home-unsafe] and keeps both the home files and the decoy directory. CDPATH no longer redirects a relative installation path.

Only tracked files enter the release package and static site. scripts/package.py stages tracked package inputs and rejects symbolic links. CI and the mirror copy tracked site and project-template files. The mirror refuses uncommitted tracked changes and untracked files in Vite's public directory, including ignored dotfiles. Its temporary Vercel state is removed on failure as well as success. Fixtures proved an untracked template file entered the old npm package and is excluded by the new packaging command.

The simulator ships its license files. Its footer links to the actual MIT license, Icarus GPL text and generated third-party notices in the built distribution. The inventory identifies JointJS as MPL-2.0 and distinguishes installed dependencies from confirmed bundled code. The Icarus recipe pins a full source commit and applies the required JavaScript compatibility edit automatically. A clean rebuild with Emscripten 6.0.2-git reproduced all four shipped files byte-for-byte; node sim/src/tb/wasm/build/smoke.mjs checks compilation, includes, counter output, plusargs, VCD output and rejection of invalid source. Hashes and commands are in the build README.

Validation and limits. The first full local run passed 258 checks, failed 0, with 52 known compiler gaps. After the additional security fixes, the second full run passed 262 and failed 1: the personal-path scan found a local Python bytecode cache. All 133 probe outcomes were unchanged (108 synthesize, 64 pass all four stages, 59/111 Vivado-supported probes pass all four). After removing that cache and isolating two new uninstall tests from the real Homebrew link, the affected security, static, count and privacy checks passed 35, failed 0. The verification intentionally pointed the real CLI link at the tested checkout and confirmed it survived. --list reports 319 available checks; that is not the number executed on this machine. A builder's initial mutation claim was rejected because its fixture setup was absent; the independent corrected run passed 15 on the original and failed the intended check on the mutant. A physical Basys3 test, this commit's remote CI and deployment are not claimed by these local results.

#8 · Browser tests and checks before publishing · 3 October

cd sim && npm test now builds the simulator, serves it on a free local port and runs 31 real browser checks. It uses Playwright's installed browser by default, closes the preview, and returns failure for failed assertions or a timeout. On macOS/Linux, timeout cleanup also terminates browser descendants still attached to the test process. The old machine-specific executable path is gone. Both a cached-browser run and an independent default-browser run passed 31, failed 0; the latter used Chromium 145.0.7632.6. A forced timeout returned 124 and left no new Chromium process. Processes already reparented outside the test tree are a known limit of this cleanup; it is not a general process supervisor.

The Ubuntu CI job installs the locked dependencies and Chromium, then runs those browser tests. Pages publication requires that job and the macOS CLI job. The assembled Pages artifact and the rewritten mirror bundle must pass test/site-check.py before publication. Checks cover links and anchors, navigation presence and consistency, canonical metadata, sitemap membership, duplicate IDs, question punctuation and unexpected package files. The question rule is a language heuristic. Ten deliberately broken site fixtures now run in CI, including deleted navigation, missing local anchors and questions outside h1–h6; an earlier review showed those cases could pass silently.

The source check exposed 69 missing error-page sitemap entries. docs/sitemap-build.py now regenerates the sitemap from tracked indexable pages and is called by the documentation builders. Local validation after rebuilding both artifacts: 86 source pages, 0 failures; 88 Pages bundle pages, 0 failures; 88 mirror pages, 0 failures. The same-page anchor fix and the sitemap fix were checked together on rebuilt artifacts. Remote CI is pending for this commit; local browser results are not represented as Ubuntu results. Commands are in sim/test/README.md.

#9 · One navigation source, unchanged desktop layout · 3 October

docs/nav.py now generates navigation from one chip table, and docs/nav.css supplies the shared navigation CSS. The guide and error-page builders use the same source. Generated copies are checked by test/site-check.py; changes to an active-page marker, Turkish language label, chip link or generated CSS are detected. A second generator run is byte-identical across the 89 checked copies. Eleven unreferenced simulator debug/screenshot scripts were removed; the browser test runner and engine checks remain.

Measured at 1366px: all nine navigation crops have 0 differing pixels, and all eight static full-page captures have 0 differing pixels. The simulator's animated area differs below the navigation strip and is not claimed identical. At phone width, the CLI's nine differing pixels also occur between two unchanged baseline captures; simulator captures remain dynamic. An independent comparison of computed styles at seven widths found no non-custom-property change. Both documentation generators, site checks, ten fault-injection fixtures and the Vite build passed. Rebuilt Pages and mirror artifacts each contain 88 clean pages. Physical GUI and this commit's remote CI are not claimed by these local measurements.

#10 · Patch notes on the site and working navigation hit areas · 3 October

The seventh navigation chip opens /patch-notes/, generated from this file by python3 docs/patch-notes-build.py. The index lists updates newest first and #latest links to the highest numbered entry. The Turkish navigation calls it “yama notları”. The generator disables dollar-delimited mathematics: an independent review reproduced Pandoc treating a paragraph containing $finish as mathematics and corrupting its prose. Raw HTML is disabled too; code and prose remain text. The page's modification date comes from this file's last Git commit, not from an invented release date.

The review also found that the existing transparent desktop link boxes used percentage units for their vertical positions. At widths 900, 1024 and 1366, the visible chips' centres hit the navigation bar instead of their links; the home link was affected too. Container width units now place all eight link boxes over the artwork. Independent hit tests and real mouse clicks passed at 900, 1024, 1366, 1920, 390 and 320 pixels on five pages. At desktop widths, every changed pixel on the checked static pages lies inside the added chip; the six existing chips keep their artwork and positions. Mobile chips remain 44 pixels high. The simulator's animated content is not claimed pixel-identical.

Two generator runs are byte-identical. The final source check passed on 87 pages; rebuilt Pages and mirror artifacts each passed on 89 pages, 0 failures. The clean baseline and ten deliberately broken site fixtures passed, as did the Vite build. A root rerun verified 210 chip clicks across 30 page/viewport combinations, with no failed hit or click. A 320-pixel CLI table overflow was reproduced in the previous page too and is corrected in #11. Chromium is the measured browser; Safari, Firefox, physical phones and older browsers without container width units remain unverified. The page is long, mostly because of the historical #2 report; its newest-first index and latest link provide direct access. Remote CI and publication are separate checks.

CI evidence now available for #8: clean macOS installation and suite 261 passed, 0 failed, 52 known gaps; Ubuntu browser suite 31 passed, 0 failed; assembled Pages artifact 88 clean pages, 0 failures. Run: 37107944441. These results apply to that commit and do not claim that the later Vivado or VS Code changes passed.

#11 · Five CLI steps and accurate Homebrew diagnosis · 3 October

The CLI page now explains the path from Homebrew to a blinking Basys3 in five steps. The guide relies on Homebrew's normal Command Line Tools installation and retains the explicit xcode-select recovery command for a missing developer-tools check. Its top-module explanation now matches the CLI: hierarchy and a uniquely matching XDC can select a design; a file name alone does not silently select it. A design containing $finish is an error, not automatically a testbench. The existing standalone-package limitation is stated.

Both the downloadable installer and install.sh distinguish a missing Homebrew installation, an Apple Silicon installation missing from PATH and an Intel Homebrew installation. They print the relevant next step before downloading the toolchain. An additional root check found that an Intel brew already on PATH bypassed the first implementation: that case now stops before executing it and explains the Apple Silicon path. Tests use executable sentinels to prove that none of those diagnostic branches invokes the discovered Homebrew binary.

The final affected installer, security, static, privacy and documentation-count checks passed 24, failed 0. test/run.sh --list now reports 321 available checks; it is not a count of checks executed by that focused run. Independent review approved the original installer patch; the root's extra Intel-on-PATH cases passed for both entry points.

At 320 pixels, the toolchain table previously widened the page to 384 pixels. It now scrolls inside its own wrapper, and the page stays 320 pixels wide. Chromium checks at 320, 390 and 1366 pixels find all five steps with no page overflow. Wrapping the unchanged desktop table produces 0 pixel differences and identical table geometry in a controlled before/after capture. These are browser measurements, not physical-phone or board tests. The final commit's full CI remains a separate acceptance check.

#12 · Vivado projects and editor tasks that preserve user files · 3 October

Commands now discover a Vivado .xpr or .srcs layout and read the enabled design, constraint and simulation sets. Running from a nested source directory writes build outputs in the project root. The project's selected top is respected unless explicitly named. dewfpga tops lists candidates as module/file pairs without writing files. Missing design files and ambiguous active sets produce actionable errors; a problem confined to the simulation set stops sim, not bit, tops or clean.

Local include directories reach both readers. .v files retain Verilog-2005 keywords in Icarus and the slang fallback. The first implementation deleted a pre-existing top.kw directory and mapped a/b.v and a_b.v to the same temporary filename. Root counterexamples rejected it. Wrappers now use a privately reserved directory, numbered filenames and scoped cleanup. clean leaves other recipes' temporary folders alone. Probe 74's RTL stage now passes; its netlist stage remains a known gap.

dewfpga vscode builds a deterministic companion VSIX and installs user tasks only for the selected supported editor and named profiles with the companion available. JSONC comments and unrelated tasks survive. Removal checks recorded content hashes, keeps user edits, and rolls back task writes if the ownership record cannot be saved. Symlinked setup records are refused. Automatic install/uninstall hooks can be skipped with DEWFPGA_SKIP_VSCODE=1; explicit editor setup still works. Native tests use that flag and isolated fake editors so they cannot modify a developer's real profile.

Independent review reproduced Cursor's code being mistaken for VS Code and a failed profile retry losing its earlier ownership record. A further root case showed that a code symlink into Insiders must not be selected for a Code removal. These cases have specific regression checks. Profile metadata was checked against Microsoft's source; that is distinct from testing the real editor GUI.

Focused checks before final acceptance: task writer 69/0, companion 35/0, Vivado 82/0, setup 89/0. An additional eight editor-boundary checks passed in a corrected isolated copy. The first full run caught two flat-folder compile-line regressions despite those focused results. They are retained in the test history; the corrected full run passed 269, failed 0, with 52 known gaps, in 24 minutes 11 seconds, with the tested files unchanged. The command list contains 325 available checks, not 325 executed successes. Real GUI and Basys3 acceptance remain unverified. Remote CI is pending for this integration.

Release review then found the tracked-file package builder omitted the companion sources. The old archive now fails validation for all four missing files. The fixed archive builds a VSIX byte-identical to the source tree and runs vscode --print. Both assembled site variants pass: 101 pages, 0 failures each. CI now builds the companion from the unpacked release, so source-only success cannot hide this omission.

#13 · Accurate lint severity and fresh waveforms · 3 October

The companion offers an explicit lint-shim setup command. Icarus's specific constant-select sorry diagnostic becomes a warning; syntax errors, other diagnostics and exit codes remain intact. The prior global setting is restored only when dewfpga owns the current value; workspace overrides and user changes are preserved. Symlink, hard-link and copied-shim recursion cases are covered.

A successful simulation task opens a new or changed VCD directly in its working project root. Before/after file identity, nanosecond timestamps and small-file hashes prevent unchanged old waveforms from opening, including same-second cases. The installed Verilog extension declares a default VCD viewer; actual GUI opening remains unverified. Nested or absolute dumpfile destinations are not discovered automatically.

The top chooser uses the real Quick Pick lifecycle, remembers choices per project and cancels the task on Escape. The companion suite passed 35, failed 0, skipped 0 in the integrated tree. These extension features share the #12 installation and ownership boundary and are committed together. Full CLI acceptance passed 269/0 with 52 known gaps. Remote CI passed for the combined integration.

#14 · Machine-readable CLI results · 3 October

check, sim, bit and flash now accept --json. One result object carries a stable schema tag, process and child exit codes, bounded diagnostics and logs, source locations and artifact provenance. Text commands keep their original output. The timeout option bounds the child process group; parent SIGINT and SIGTERM also stop that group. A deliberately daemonized process outside the group is not covered.

Independent review rejected false success after reader failures or open orphan pipes, warning floods that hid errors, stale artifacts attributed to the wrong top, and relative artifact scans from a nested Vivado directory. Root follow-ups fixed similarly named old bitstreams and multi-driver diagnostics containing several source locations. Unknown top/artifact provenance remains unknown instead of guessed.

The integrated tree passed 30 tests, 0 failures, 0 skips, including real sim and bit commands, text-mode parity and interruption cleanup. Separate real XPR and XPR-less projects passed 10 checks for nested roots, caching, unrelated artifacts and source paths containing spaces. The preceding #12–#13 full CLI run passed 269/0 with 52 known gaps; it is not a full run of this newer commit. Remote CI for this update passed. No real board programming is claimed.

#15 · MCP tools backed by the checked CLI · 3 October

dewfpga mcp exposes check, new, sim, bit, flash and explain_error over stdio. Build tools return the CLI's structured result; protocol stdout stays clean. The offline catalog ships in the release. Installation creates a pinned, optional SDK environment with staged replacement and ownership-aware removal. No client configuration is registered automatically.

Independent review reproduced unbounded output buffering, contradictory JSON accepted as success, cancellation leaving another process session alive, a missing ownership marker during environment creation, and uninstall racing an active install. The corrected server bounds capture, validates schema/type/status agreement and tracks observed descendant groups. Root integration also refuses the entire CLI uninstall before removing toolchain files while the SDK lock is held.

The root's SDK-backed suite passed 24 tests, 0 failures, 0 skips, including the real JSON CLI, protocol discovery, cancellation and the uninstall lock. The combined installer/uninstaller, JSON, MCP and static checks passed 40/0 before the extra CLI lock case was added; that case separately passed. Independent review also built and removed the real pinned SDK in a temporary FPGA_HOME, with foreign files retained. The unpacked release also served real SDK calls for new, sim and offline error explanation; check correctly reported its temporary home had no toolchain. Both site bundles passed 104 pages, 0 failures. Remote CI requires the SDK instead of accepting skipped protocol tests. The first CI failed in the installer; the correction below passed remote CI. No physical board was programmed.

Process-tree cleanup depends on ps. A daemon that reparents before observation and PID reuse remain limitations. Tool annotations describe effects; they are not an authorization enforcement mechanism. Clients must obtain user authorization for flash.

#15 integration correction · 3 October

The first remote CI run failed before verification: the optional MCP installation used an undefined HERE variable. Installation now resolves its repository path once before using it. The new offline whole-installer tests reproduce that failure with MCP enabled and check success, optional helper failure, and explicit skipping (3/3 passed after correction, 2/3 failed before). The repeated-install test also checks exit status, and ShellCheck checks unassigned uppercase variables.

The combined #14–#16 local run exposed a second regression: without Python on PATH, the new MCP removal hook prevented removal of a CLI-only installation. That path works again; existing SDK or lock entries cause an explicit refusal that preserves all files when ownership cannot be checked. Tests cover both an SDK directory and a dangling lock symlink.

The initial full run was 272 passed, 1 failed, 52 known gaps, with unchanged tested sources. After both corrections, the targeted static, installer, SDK-backed MCP and editor suites passed 6 top-level checks, 0 failures. The corrected combined local tree (also containing pending #16 guidance) passed 273/0 with 52 known gaps in 18 minutes; tested source hashes were unchanged. Remote CI for the correction passed, including clean macOS installation, the full suite and Ubuntu browser tests. The earlier failed runs remain recorded as failures.

#16 · Project instructions and a measured repair experiment · 3 October

dewfpga new now writes AGENTS.md and a one-line CLAUDE.md import into the project. The instructions name actual CLI commands, structured result fields and exit codes, preserve ports and constraints, and reserve board programming for an explicit user request. The generated llms.txt links the guide and error catalog; regeneration and all referenced error pages are checked. The listed suite grows from 327 to 329 checks.

The replacement experiment ran 16 fresh Fable 5.1 sessions, one attempt for each of eight fixtures in each arm. Both arms passed 8/8 repairs and all 130 acceptance checks, including fresh builds, testbench mutations, port interfaces and netlist equivalence. Plain/guided observations were 50/41 tool calls, 516.87/550.92 process seconds, and 0/13 Bash calls containing --json. These numbers do not demonstrate a guidance benefit: the sample is small, two fixtures are CLI auto-repair controls, and most reference testbenches were visible in the input.

Root invalidated the previous batch after finding guidance in a shared ancestor and an explicit read by a plain agent. Its results remain in the repository as invalid. The replacement checked both lexical and resolved ancestors before every call; root found no plain-arm guide or reference access in the transcripts. One guided agent used an outside-project temporary backup of its own source; this protocol deviation is disclosed. The protocol is not a filesystem sandbox. See the full results and limits.

The combined local #15 correction and #16 product sources passed 273/0 with 52 known gaps. The only previously tested files changed afterward are the experiment README and manifest; product source hashes match that run. The experiment's 63 frozen harness files remained unchanged throughout the batch. Remote CI for a80556d passed: clean macOS installation, the full CLI suite, packaged commands, assembled site and Ubuntu browser tests. No physical board acceptance is claimed.

#17 · Browser compilation checks the design and clears failed runs · 3 October

Yosys errors now open their source file at the reported line. Missing module errors point to the instance. Synthesizable $display calls are removed with an explanation; a failed compile clears old outputs and DONE while POWER stays on. Edits survive a reload even when Run was never pressed. All five original regressions passed in Chromium and WebKit (5/0 each).

The worker checks the hierarchy, uses the Yosys reader with a Slang fallback and checks hardware without SIM before accepting the simulation netlist. Guards for undefined names, multiple drivers, non-constant asynchronous loads and unsupported source constructs remain active; latches produce a warning. Source file extensions retain Verilog-2005 versus SystemVerilog semantics. Package/interface dependencies, hierarchical references, temporary-name collisions and Xilinx library read state were corrected during review.

Root found a wrong interface output (00Zz) that the earlier direct-Yosys test did not cover. Flattening the actual worker netlist without scope-info cells fixed the DigitalJS boundary. Three actual worker netlists now pass their original native testbenches and 576 DigitalJS output comparisons. The additional engine-isolation suite passed 16 checks, including file collisions, a unit-scope enum, a user module named INV and four guard controls.

The last compilation-decision audit covers 133 cases: 124 matching decisions, 7 native-accept/browser-reject cases, 2 reverse differences and 0 infrastructure errors. The remaining rejects are unnamed instances, inherited procedural port direction, BUFG, MMCM, internal tristate, a netlist-form module and a bitstream cast. The two reverse differences are separate interface/package source files that the native CLI source selector omits; their browser behavior was tested above. These counts do not establish corpus-wide behavioral equivalence or hardware timing.

#18 · A workspace that keeps names, choices and edits · 3 October

A directory tree and freely named sources, headers, memory data and multiple XDC files replace the fixed-name workspace. Top and testbench selection use source content; ambiguous choices are shown instead of guessed. Active XPR filesets exclude disabled/inactive files. A selected constraints file follows renames, including case-only renames, and a deleted selection does not transfer to a new file with the same name.

Autosave preserves original BOM/CRLF bytes until a file is edited. Share links keep top/testbench/XDC choices, file roles, active file and bytes in a fresh browser profile; old links still load. A failed link offers ZIP export. Workspace unit checks passed 69/0. Review found and fixed choice loss during rename and stale choice inheritance after deletion.

#19 · Bring a lab folder and take it back to the CLI · 3 October

Open-folder and drop import retain relative source/header/memory paths and reject invalid imports atomically. ZIP export includes the selected project manifest; a real nested ROM project exported from the browser passed CLI sim and bit checks. Missing or external active XPR references are reported rather than silently omitted.

WebKit exposed an unreadable FileSystemEntry for an otherwise readable in-memory File. Loose files now use the direct File; directories retain entry recursion. Root also found that stripping a shared ../ prefix could turn an unsafe path into a safe-looking name. Such roots remain invalid. Independent review proved that the old drop-rejection test could pass on a read error without reaching validation; it now requires the invalid path in the diagnostic.

Physical directory-entry drag/drop remains unverified: browser automation did not expose usable directory entries. Recursion is covered by unit tests, and real browser folder-input and direct-file drop flows are covered separately.

#20 · Located diagnostics while typing · 3 October

Live checks mark source lines and list problems. Actual errors disable Run; warnings remain runnable. Each diagnostic has a non-clickable hint showing coming soon!. Header paths, missing includes and repeated messages now produce useful locations.

Independent review reproduced a valid design timing out behind 50 pending checks after 60 edits. Checks are now coalesced: the same probe posted 7 checks with at most 1 pending, and Run completed in 16 seconds. A check timeout now says the sources are unchecked, replaces the worker and leaves Run available to compile again. The old board path blocked valid Run; the old testbench path falsely said there were no problems. Both behaviors were corrected.

The reviewer's final source checks passed 24/0 and its UI suite 126/0. Root tightened an older check that accepted not running as valid recovery. Remaining limits: queue wait counts against the request timeout; a check timeout can interrupt a concurrent run; two identical missing-include names can locate the first include site. No network cold-start timing is claimed.

CI follow-up (3 October). A WebKit run completed the memory design but exceeded the old 45-second aggregate check at 46.2 seconds. That check mixed worker speed with queue depth. The regression now records actual worker requests and replies at the Run click, requires at most one preceding check and no new lint while Run is active, and keeps bounded completion plus a working circuit. Four injected small jobs demonstrate that the same count rejects a backlog. The control uses a small circuit, avoiding an unnecessary second large memory simulation. Derived timing assertions were rejected during independent review: the worker's compile counter excludes hardware validation, and inferred durations did not provide independent evidence. Product scheduling remains unchanged.

#21 · Board controls retain the state the student is holding · 3 October

Switches and momentary buttons support pointer and keyboard input. Releasing one input method no longer releases a button still held by the other; focus loss releases held controls. Source and board hover links use the bound port/pin, without mistaking a helper-module port for the top-level output. Mobile tooltips stay inside the viewport. Failed runs retain POWER and clear DONE and outputs.

Independent browser checks passed 72/0. The expanded WebKit gate initially failed because its touch-hold test called Chromium-only CDP. Chromium still tests a trusted touch hold; WebKit tests the same handlers with explicitly labelled synthetic touch pointer events. This is not physical Safari/phone acceptance. Board pitch still limits mobile target width; LED changes are not announced by a screen reader.

#22 · Explain slow counters and keep Max responsive · 3 October

Detected divider bits and threshold comparisons explain why a 100 MHz design looks still at browser speed and suggest an explicit SIM divider. Closing the advice panel persists across reruns until the advice changes. Threshold comparisons, including strict versus inclusive bounds and mirrored operands, were corrected. Finite and Max clock modes both use a bounded per-frame work budget.

The repeated two-counter experiment measured roughly 5.8–5.9 thousand UI-driven DigitalJS cycles/s. Its isolated DigitalJS kernel reached 12–13 thousand/s; a CXXRTL kernel compiled locally to WASM ran in Chromium at 198–206 million/s, with 14 output comparisons. This measures a precompiled browser kernel, not a dynamic browser C++ compiler or a general equivalence result. Divider advice was selected; down-counter hints remain absent and Max does not promise a physical 100 MHz rate.

#23 · Prepare simulator discovery and enforce its browser checks · 3 October

The simulator's noindex directive is removed, its source metadata enters sitemap validation, and the home page distinguishes the available simulator/testbench/CLI from the future browser bitstream compiler. Existing source checks plus deliberately broken metadata catch a restored noindex, missing sitemap entry and bad canonical.

CI now runs the UI, five original regressions, board and hardware worker suites on both Chromium and WebKit. macOS additionally checks actual worker netlists and engine isolation with native tools. The original expanded WebKit gate failed before the CDP fixture repair; that failure is retained. Final combined acceptance is recorded below. The main-only deployment guard remains; this development update does not publish a live site or establish physical-board or GUI acceptance.

Combined #17–#23 root acceptance passed in both Chromium and WebKit: source 24/0, workspace 69/0, UI 129/0, original regressions 5/0, board 72/0 and worker 17/0, with no page errors. The assembled Pages and mirror packages each passed 104 pages, 0 failures, 1909 links. Their CLI archive is byte-identical to the #16 archive already tested with real sim and bit commands. The required SDK-backed full CLI rerun passed 273/0 with 52 known gaps in 20 minutes 26 seconds; all product and test hashes stayed unchanged (only these release notes and their HTML changed). A first local attempt omitted the SDK Python setting, failed the required MCP check and was stopped; that attempt is preserved separately. Remote CI run 37129232619 then passed on the exact db5b855 head: both web jobs and macOS succeeded. CI CLI counted 271/0 with 52 known gaps; two version-dependent checks were skipped with the CI Yosys build. The Pages deployment job was skipped.

#24 · Newsletter consent and working management links · 3 October

The optional newsletter page (not open yet) records a versioned consent request and queues a confirmation message. No account is required. Patch notes become eligible only after confirmation. Confirmation and newsletter messages carry the actual unsubscribe token in their body and List-Unsubscribe header. Public configuration is empty by default, so collection stays disabled until the operator configures the existing project and completes the notice.

A new consent generation now rotates its management token and clears the old account binding. Previously, an old mail link and linked account could continue to control fresh consent. A late completion for confirmation T1 can no longer mark a newer T2 request as mailed: the service RPC checks the token it actually sent. Cleanup also preserves a newly mailed link's 48-hour validity instead of removing it when an older pending request reaches seven days. Removal requires an operated cleanup; these thresholds are not an automatic deletion promise.

The data and privacy draft (not open yet) separates application records from provider logs and identifies the controller, contact, processing and retention details still missing. It does not claim KVKK/GDPR compliance.

#25 · Mail attempts retain uncertainty and campaigns require an owner test · 3 October

The operator's sender now binds each idempotency key to its exact payload and logical mail. A lost response, ambiguous server failure or concurrent-key reply retains the same key within a bounded window. A later refusal cannot erase an earlier uncertain attempt, including within the same claim. Claims carry an ownership token so a delayed refusal cannot release a newer sender's claim. Quota accounting includes delayed acceptance and outstanding uncertainty. These controls do not claim exactly-once delivery.

The ledger limits dewfpga's own allocation. A live sender observes the existing Resend account's usage before each ledger claim; unavailable usage or insufficient operator-chosen headroom defers work without creating a claim. External senders can still consume capacity between that observation and a request. Provider quota refusal stops the run; no guaranteed Auth reservation is claimed.

An owner test requires the owner's explicit confirmed subscription, a usable unsubscribe link and a receipt note with measured or explicitly attested unsubscribe evidence before the matching campaign can run. Acceptance by the provider is not inbox receipt. The campaign rechecks subscriber eligibility before each dispatch. If fresh consent changes a previous campaign payload, that recipient is reported as conflicted without changing the old intent or sending again; unrelated recipients continue. The operator guide is mail/README.md.

Before/after evidence. Earlier immutable candidates reproduced duplicate-key retry, stale-claim, accounting and token-lifecycle failures. The final integrated local gates use the shipped migrations rather than substitute SQL: SQL 173/0, mail flow 60/0, sender fault scenarios 27 passed, lifecycle 112/0, account-usage unit tests 10/0, and usage/sender integration 8/0. Counts are checks within their suites, not distinct defects. Provider replies are modeled; no real mail was sent. An intermediate same-claim snapshot failed two root expectations; the final sender retains uncertainty and the integration regression passes. A worker mutation removing that rule made the mail test fail as intended.

Before activation (4 October). A review against Resend's and Supabase's own documentation found gaps that local tests could not show. A sending-only Resend key is refused by the usage endpoint (401 restricted_api_key): the worker used to defer every run with "account usage unavailable" and no cause; it now names the status and error and says a full-access key is needed, without echoing the key or the response text. After a 429 rate-limit refusal the sender now waits before its next derived key (the provider's retry-after when sane, else 1 s then 3 s, at most 60 s); before, all three keys were spent within milliseconds and that recipient missed the issue. The limit is per team and shared with dewsletter. Re-applying 002 no longer deletes a consumer row the operator configured, and the ledger's id sequence is revoked from anon and authenticated, which existing Supabase projects grant on every new public sequence.

Two operator scripts prepare the first live apply. mail/ops/preflight_readonly.sql reports the shared project in one read-only transaction: dewfpga objects and grants, other applications' tables tied to auth.users, triggers, default privileges, foreign callers and which migration parts already exist. mail/ops/rollback_dewfpga.sql removes only dewfpga's objects and drops nothing when another application uses them. Both live outside mail/sql: in the first draft a loop over that folder ran the rollback right after the migrations, which a sender test caught. Local gates on the integrated head: activation scripts 142/0, SQL 187/0, mail flow 60/0, every sender scenario including the 429 wait, lifecycle 112/0, usage 13 tests OK, pages 137/0, Chromium and WebKit 158/0. Provider and Auth replies are modeled; nothing was applied to the live project and no mail was sent.

#26 · Optional accounts with local session handling and scoped deletion · 3 October

The account page (not open yet) uses Supabase magic-link signup/sign-in and explicit dewfpga enrolment. Premium is marked coming soon; existing free features remain available without membership. Export and deletion apply to the dewfpga profile and token-linked subscriptions. They preserve the shared Auth identity and other applications' records. Anonymous subscriptions remain managed by their own links. The application installs no shared Auth signup trigger.

Forms ship disabled until their handlers and configuration are ready. Malformed Auth fragments are removed without breaking the page; blocked storage and expired sessions produce explicit feedback. Temporary service failures preserve the session for retry. Logout requests use local scope. A failed logout distinguishes local clearing from unconfirmed server revocation. A lost response to a write says its result is unknown, rather than falsely claiming no change occurred.

Validation. On the integrated schema, real Chromium and WebKit ran 79 checks each, 158/0 total, against private PostgreSQL, a PostgREST-shaped test adapter and fake Auth. This includes disabled/no-JS forms, malformed links, consent, account controls, expired sessions and a real SQL write followed by a dropped HTTP response. Source/helper checks passed 137/0. The private database now shuts down cleanly after browser tests. CI adds a separate offline-mail gate and the Pages job depends on it; no CI step enables live sending.

Activation and closure are still open. Supabase configuration, sender domain, SMTP delivery, real owner receipt/unsubscribe, final notice details and a live announcement have not been verified or performed. This combined candidate's SDK-backed full CLI run passed 273/0 with 52 known gaps in 17 minutes 35 seconds; tested source hashes stayed unchanged. Pages and mirror bundles each passed 109 pages, 2006 links, 0 failures; the CLI archive remains byte-identical to the previously accepted release. A separate Opus review exercised the consent-conflict path and found no demonstrated safety fault within its stated inspection scope. It exposed the conservative limitation that even a proven refusal is not automatically reissued for fresh consent; that policy and per-run log are now explicit, with all four prior outcomes covered. Exact-head remote CI remains a required PR gate. No live publication, main-branch merge or physical-board/GUI acceptance is implied.

#28 · dewfpga sim simulates what the build reads when iverilog refuses the design · 4 October

The simulation no longer stops where the bitstream goes through. Of the 111 probes Vivado supports, 52 did not pass all four stages, and 28 of those failed only in dewfpga sim: iverilog does not read unique if, interfaces and modports, unpacked structs and unions, streaming, whole-array compare/copy/slice, a class used for its static function, the type operator and more, while the build read the same file and made a working bitstream. A student got a .bit from dewfpga bit and design.sv:7: ERROR [iverilog-refused]: iverilog cannot compile this code from dewfpga sim.

Now, when iverilog refuses a design file (not the testbench), sim runs the build's own front half, the same readers and the same checks as dewfpga bit, writes that design back as Verilog (top_sim.elab) and simulates it with the testbench and yosys' models of the Xilinx cells: design.sv:7: note [sim-from-build]: iverilog cannot compile this design (its lines are above), so the simulation runs on the design as the build reads it ..., then the testbench's own PASS. A design the build refuses is refused by sim too, with the build's line: a function with a ref argument, which yosys-slang silently drops, still stops instead of passing (measured: without that rule the second path printed the testbench's FAIL). A testbench iverilog cannot read gets the old error. What differs from simulating the source (delays inside the design are ignored, a testbench reference to an internal signal the build renamed can break) is on the new error page.

An independent review found one case this path got wrong: a testbench that sets the top module's parameters (top #(.W(2)) dut). The build keeps the defaults, so the elaborated design had no parameter left and iverilog blamed the student's testbench. Vivado's simulator applies those values; this path cannot, so it now stops at the testbench's line with [sim-from-build-parameters] and says why, instead of testing another design.

Numbers. Vivado-supported probes passing all four stages: 59/111 → 86/111; all four stages 64/133 → 95/133; synthesis 108/133 → 108/133. Known gaps: 52 → 25. Silent wrongs (bitstream built, netlist check fails): 3 → 3, the same rows (35, 74, 97). Closed in simulation: 31 probes (27 of them Vivado-supported). Not closed: 35 (no simulation model of MMCME2_BASE in yosys' cell library), 51c (the 2-D typedef scan stops before iverilog), 60 (a ref argument, refused on purpose). Blink's simulation is byte-identical and as fast as before; a design on the second path takes about 3 s instead of failing.

Tests. 6 new checks; each turns red when its rule is removed: the second path passes an interface design; a ref design is refused, not passed; a testbench iverilog cannot read keeps the old error; a design both readers refuse prints both; a testbench that sets parameters gets the new error at its line; blink's output is unchanged.

#29 · A failed build leaves no old bitstream, wrong pins are named, a quick save rebuilds · 4 October

A wrong pin name stops at its own XDC line. With PACKAGE_PIN ZZ99 on the clock line, the pin check printed xdc ok: 33 ports, all mapped., then nextpnr stopped with its raw ERROR: Unable to constrain IO 'clk', device does not have a pin named 'ZZ99', no file, no line, no fix. Now the check reads the chip's own pin list (the prjxray database the bitstream is built from) and stops first: blink.xdc:5: ERROR [no-such-pin]: ZZ99 is not an I/O pin of the Basys3's chip ... Fix: .... A pin written in small letters (w5) and two ports on one pin ([pin-used-twice], both lines named) are caught the same way. If nextpnr still refuses a pin, its line gets the same code and XDC line.

A failed build leaves no old bitstream. After that failure the previous blink.bit was still in the folder, byte for byte the old design, with its .fasm, .frames and routed netlist. Anything that loads the file by name would load yesterday's design. The pin-and-route stage now removes its outputs before it starts, as synthesis already did; dewfpga flash after a failed build stops at the same error line.

A save in the same second rebuilds. macOS's make (3.81) compares whole seconds. A testbench edited within the second of the last compile was not newer to it: dewfpga sim ran the old simulation and printed its old PASS (measured: an edit 0.4 s after the build, 0 of 5 rebuilt). VS Code's save and build in one keystroke hits exactly this. The stage records a checksum of every source next to its file list; when the content differs the stage builds again. With the planted $error and the same timestamp, sim now compiles, prints the error and exits 2. Blink's simulation time did not change (0.14-0.20 s).

On a real Basys3 (6 October). The board test ran with a board for the first time: dewfpga flash programmed blink (openFPGALoader's Done line, no ERROR). With PACKAGE_PIN ZZ99, dewfpga flash stopped at blink.xdc:5: ERROR [no-such-pin], never started the programmer, and left no .bit.

Tests. 8 new checks (listed checks 329 → 337; with #28's six, 343), each with a mutation: removing the old-output cleanup, the pin list, the duplicate-pin check, nextpnr's coded lines or the checksum rebuild turns its check red. The same-second checks ran three times in a row and passed each time. Not verified: nextpnr's behaviour on the chip's 14 transceiver pads, which the pin list accepts.

Closing run (6 October, board unplugged). test/run.sh: passed 314, failed 0, known gaps 25 in 21 min 7 s, the board check skipped (SKIP flash on the board: no board; no FTDI device on the USB bus at that hour), the probe table byte-identical to the two runs before it. Two checks were red in the run before it, neither in the product: shellcheck read the Verilog $error in a new test function as a shell variable (the directive the sibling functions carry was missing), and the MCP check's SDK venv lived in $TMPDIR, which macOS empties of files untouched for three days (the venv was built on 3 October; on the 6th it had 420 directories and 0 .py files, so dewfpga mcp reported mcp-not-installed; python3.14 -m venv and pip install -r templates/mcp-requirements.txt rebuilt it and the check passed alone and in the full run). The mail suites: 9 suites, each exit 0 (activation SQL 142, SQL 187 with 154 privilege checks, mail flow 60, sender idempotency, lifecycle 112, usage 13, integration 8, pages 139, Chromium and WebKit 158; 0 failures). Site check: 112 pages, 2053 links, 0 failures.

#30 · The install keeps only the chain that runs: about 0.25 GB on disk instead of 1.5 · 7 October

What a clean install left behind. The install line said # ~4 min, 1.4 GB, once, and a clean install into an empty folder measured on 7 October was 1543 MB with 25 .git folders in it: nextpnr-xilinx 1086 MB (its full prjxray-db submodule holds five FPGA families, 853 MB, the Basys3 reads one), prjxray 217 MB (ten submodules, four never built), yosys-slang 101 MB of sources around a 9 MB plugin, and every build tree. The chain that runs, the binary, the artix7 database, the chip database, the venv, fasm2frames, xc7frames2bit and slang.so, is 237 MB. The installer asked for 4 GB free: the install needs 1.4 GB plus build scratch, at least 4 GB.

Now the install fetches less and removes what it built from. nextpnr-xilinx comes with the two submodules its chip-database exporter reads, prjxray-db as a partial clone of artix7 alone; prjxray with the six submodules its cmake uses; the four it never builds (fasm, yosys, python-sdf-timing, display_port, 100 MB) are not fetched. After each build the sources and the build tree go, and the installer says what stays: ✓ prjxray pruned to xc7frames2bit, utils/ and the python package (2 MB), ✓ nextpnr-xilinx pruned to the binary and the artix7 database (86 MB), ✓ slang.so built, loads in Yosys 0.69+post (read_slang); sources removed (9 MB kept). Every step header carries the size so far ([6] yosys-slang (SystemVerilog reader plugin, from source, ~3 min) (+218s, 246 MB)), the last line is Total: 415 s, ~/fpga: 255 MB., and the free-space check now asks for 2 GB: the build peaks at about 850 MB (the install that stays is ~237 MB), so at least 2 GB are needed. (the peak printed by the three clean installs of 7 October: 811, 833 and 834 MB, at the chip database step, when the sources, the build trees and the 268 MB .bba are all there at once). A second run skips every step by its stamp and the files it left, no git involved: ↷ nextpnr-xilinx @ 3fd7878 (binary + artix7 database) (already there, skipped), `↷ prjxray @ c9f02d8 (xc7frames2bit

Numbers. Clean install into an empty folder: 1543 MB → 237 MB in the closing run's second full run (the suite's du right after the install, log included; the first full run's du said 253 MB, and the integrator's install of the same morning measures 237 MB), .git folders 25 → 0, 415 s for the install, 3 s for the second run. The Homebrew side, which the pages' number never included, measured on this Mac: the eight packages the installer asks for are 237 MB (python@3.14 87, cmake 71, yosys 50, eigen 11, openFPGALoader 10, Icarus Verilog 7, ninja and pkg-config under 1), and the 16 packages they pull in another 562 MB (boost 377, icu4c 84, tcl-tk 43, openssl 39). So a student's Mac gets about 0.25 GB in ~/fpga and about 0.8 GB under Homebrew, plus the 51 MB agent SDK if they ask for it. The compiler did not change: the probe table of the closing run is byte-identical to #28-#29's (synthesis 108/133, all four stages 95/133, Vivado-supported probes passing all four stages 86/111, known gaps 25), and the fresh chain's blink .fasm equals the golden one.

Tests. Four new checks in the FULL=1 section (listed checks 343 → 347): the clean install is under 1 GB (its MB printed under the PASS line), it leaves no .git at any depth, a second run skips every step in under 10 s with no built line, and uninstall removes the fresh folder whole. The size checks were run by the update's builder against the installer from before this update and fail there (1543 MB, 25 clones); against the new one they pass. The suite now also puts the tree's .fpga_home back after the clean install, which the install's step 7 overwrites. shellcheck -S style and bash -n on the new install.sh are clean. Not exercised end to end: the rebuild after a Yosys upgrade (same code path as the clean build, with the pruning after it) and a venv rebuilt after a Homebrew Python upgrade (prjxray keeps setup.py and README.md for that pip install -e).

Closing run (7 October, board unplugged). The first run: passed 319, failed 1, known gaps 25, and the one FAIL was not the product: no personal paths found templates/__pycache__/mcp_setup.cpython-314.pyc, left at 04:41 by a run of the MCP test without -B (its rollback test imports mcp_setup.py), carrying this Mac's home path, the same class of leftover #7's closing run hit. The test now sets sys.dont_write_bytecode like its two sibling suites, so a run without -B leaves nothing. The second run: passed 320, failed 0, known gaps 25, one PASS more than the first (the fixed check), its probe table byte-identical to the first run's and to #28-#29's. The clean install took 415 s in the first run (peak 811 MB) and printed a peak of 810 MB in the second; its second run took 3 s both times, and find -name .git over the live fresh folder the moment its last build ended found nothing (89 MB chipdb, 86 nextpnr-xilinx, 52 venv, 9 yosys-slang, 2 prjxray, 1 install.log: 237 MB). The clean install's du is the number above. The board check: SKIP flash on the board: no board (no FTDI device on the USB bus during either run). Mail suites: 9 suites, each exit 0 (activation SQL 142, SQL 187, mail flow 60, sender idempotency, lifecycle 112, usage 13, integration 8, pages 139, Chromium and WebKit 158; 0 failures). Site check: 112 pages, 2055 links, 0 failures.

Open.

#31 · A random-design generator checks the SystemVerilog DDCA teaches against its own simulation · 7 October

The product did not change in this update. It adds a test tool, test/fuzz, separate from test/run.sh: gen.py <seed> <folder> writes a random design in the SystemVerilog the course book (DDCA, chapters 4, 5 and 7) and CS223 teach, on the Basys3's own ports, with its XDC and a testbench of 300 cycles of random switches and buttons that prints every output every cycle and expects nothing. Forty language features are switches (always_comb, case without default, casez, unique case, async reset, a start value in the declaration, always_latch, an enum state machine, parameters and #(.N(..)) overrides, generate for/if, packed and unpacked arrays, a memory, packed structs, typedefs, functions with break, a for loop in always_comb, named, positional and .* connections, a package in its own file, submodules in their own files, signed arithmetic, shifts, reductions, concatenation, $clog2, mixed widths, the seven-segment ports, three or four levels of modules, the same submodule twice); a seed draws them, --on/--off force them, so a failing design narrows to the feature that causes it. run.sh <first seed> <count> runs each design through dewfpga sim (the RTL in iverilog) and dewfpga bit, then simulates the netlist the build made (read_json, write_verilog, iverilog with yosys' Xilinx cell models, the same testbench, the way test/sv does) and compares the two traces bit by bit: a bit the RTL knows (0 or 1) must be the same in the netlist, an x in the RTL matches anything. Six outcomes per seed: ok, silent-wrong (the bitstream built, the netlist does not do what the simulation showed), refused-coded, refused-uncoded, crash, sim-refused (no independent reference: dewfpga sim failed or ran on the build's view). The last line is the metric, fuzz: ok 123 of 144; silent-wrong 10; refused-coded 6; refused-uncoded 0; crash 0; sim-refused 5, and a failed seed's folder stays.

What 194 designs found. Seeds 1-12, 100-243 and 500-537 (two batches stopped at the 90-minute mark; a seed costs about 8 s, of which dewfpga bit about 4.5 s): 168 ok, 14 silent-wrong, 6 refused with a code, 0 refused without one, 0 crashes, 6 sim-refused. The failures fall into six families, each with the update that owns it:

Numbers. From the closing run of this tree: Vivado-supported probes passing all four stages 86/111 → 86/111, all four stages 95/133 → 95/133, synthesis 108/133 → 108/133, known gaps 25 → 25, the probe table byte-identical to #30's closing run (the compiler did not change). test/run.sh: passed 314, failed 0, known gaps 25 in 20 min 52 s, the board check skipped (SKIP flash on the board: no board; no FTDI device on the USB bus at that hour), no check changed since #30 (the 320 of #30's run included its FULL=1 clean install, not run here). The listed count stays 347: test/fuzz is not in the suite and is not in CI.

Tests. The tool is the test; what it checks on the product is the comparison above. The four shrunk reproductions were run through run.sh's steps by hand: the unique casez and the always_latch ones MISMATCH, the enum state machine and an 8x8 multiplier EQUAL: 4 cycles. compare.py on the kept folders gives the lines quoted above. Mail suites: 9 suites, each exit 0 (activation SQL 142, SQL 187, mail flow 60, sender idempotency, lifecycle 112, usage, integration, pages 139, Chromium and WebKit 158; 0 failures). Site check: 112 pages, 2056 links, 0 failures.

Open.

#32 · A latch whose gate and data change together, and a unique casez whose items overlap: the chain names both before the board differs from the simulation · 7 October

The two silent wrongs #31's generator found and left open are now named by the product. Before, a student with an eight-line unique casez whose items 4'b10?? and 4'b??11 both match 1011 typed dewfpga sim and saw led = 1 for that input, then dewfpga bit and saw the build go through as if nothing were wrong:

xdc ok: 32 ports, all mapped, 73 unused pins in the XDC ignored.
pnr ok: 2 LUT, 0 FF, no clocked paths, timing not applicable   (full log: top.log)
top.bit  2.2 MB

and the board showed led = 3: iverilog ignores unique and takes the first item; yosys, yosys-slang and Vivado (UG901 v2023.2 Ch.10 p.279: a unique case is parallel_case and full_case) build the items as parallel logic and OR their values when two match. Now the source scan that runs before the tools stops both dewfpga sim and dewfpga bit the same way, with the two items, the value they share and the fix on the line:

design.sv:5: ERROR [unique-case-overlap]: the items 4'b10?? (design.sv:4) and 4'b??11 (design.sv:5) of this unique casez both match 4'b1011, and unique promises that only one item can match: the simulation takes the first item (iverilog ignores unique), and the hardware, built as parallel logic the way Vivado builds a unique case (UG901 Ch.10, parallel_case), ORs the two items' values, so the board would not do what the simulation showed. Fix: make the items disjoint (4'b10?? and 4'b0?11), write priority casez when the first match should win, or drop unique. https://nosey-dewdrop.github.io/dewfpga/errors/unique-case-overlap/

The rule: a unique or unique0 case, casez or casex whose every label is an integer literal, or a localparam of the module whose value is one, is checked pairwise for a value two items both match (a ? or z, and x in a casex, is a wildcard); a statement with an enum name, a parameter (an instance may override it), an expression, a range or inside among its labels is skipped, no guess. Two labels of one item (4'b10??, 4'b??11: led = 1;) are one item with one value and may overlap. A localparam item is named with its value (HI = 4'b10zz) so the overlapping bits are on the line. The same unique case with disjoint items, as the course's 18_unique_priority writes it, builds as before.

The second one, an always_latch whose gate and data come from the same switches (seed 7 of #31, cut to six lines: always_latch if (s < sw) led = {15'b0, (sw < 16'd1791)}; with s a function of sw), built before with pnr ok: 22 LUT, 1 FF, no clocked paths, timing not applicable and not a word, and the simulation kept led = 0 where the netlist, and the board, take led = 1: a latch has no clock edge to order its inputs, the gate and the data arrive through LUTs of different delay. The latch is what the student asked for and both readers build it right (one LDCE, as Vivado does, UG901 Ch.5), so the build goes on, and now warns once per always_latch, with its gate:

design.sv:4: warning [latch-hazard]: always_latch builds a latch for led (an LDCE cell, as Vivado builds it, UG901 Ch.5 Latches), and a latch has no clock: when its gate (s < sw) closes in the same instant its data changes, the board keeps the old value or takes the new one depending on which wire is faster, where the simulation always keeps the old one; dewfpga sim cannot show that race. Fix: if a register was meant, write always_ff @(posedge clk) with the gate as its enable:  if (s < sw) led <= ...;  keep always_latch only when the gate is held steady while the data changes. https://nosey-dewdrop.github.io/dewfpga/errors/latch-hazard/

The warning reads the $dlatch cells after proc (yosys' own Latch inferred line is hidden for an always_latch, yosys-slang prints none) and names the first if ( of the block; comments are blanked before any rule reads a source line, so always_latch // if (reset) was here once names the if on the next line, and an always_comb whose comment says always_latch still gets [latch], not [latch-hazard]. Both messages have their page in the catalog (errors/unique-case-overlap/, errors/latch-hazard/), with the design that printed them and why.

test/fuzz/run.sh learns the difference: a seed whose netlist differs from its RTL in a known bit (0 or 1 on both sides, not an x) while the build printed warning [latch-hazard] is warned, not silent-wrong: the product named, with the line, the construct whose board behaviour differs from the simulation. A netlist x where the RTL knows the bit is never excused by a latch warning. A coded refusal at the sim step is now refused-coded, not sim-refused: the scan stops sim and bit the same way, so it is the product's refusal, not a missing reference. And the line before the metric splits the refusals by code (the line quoted under Numbers), so a check that moves designs out of ok is weighed by name.

Numbers. From the closing run of this tree: Vivado-supported probes passing all four stages 86/111 → 88/113, all four stages 95/133 → 97/137, synthesis 108/133 → 111/137, known gaps 25 → 26; the 133 rows of #31's table are byte-identical, the four new rows are the four probes below. The new gap is 101c_always_latch_race, a documented silent wrong: the bitstream builds with the warning, the netlist's LDCE takes the new data as the gate closes (FAIL: step 1: sw=0000 led=0001, expected 0000) where the RTL keeps the old. test/run.sh: passed 317, failed 0, known gaps 26 in 18 min 46 s (the second run; its probe table equals the first run's byte for byte). The listed count goes 347 → 351 (the four probes); the first closing run was passed 316, failed 1, the one failure the count lock (README, README.tr and site/cli still quoted 347 checks and 133 probes), fixed in the close. The board check was skipped (SKIP flash on the board: no board; no FTDI device on the USB bus).

test/fuzz/run.sh 1 500, the same 500 seeds as #31's second measurement, before this update: fuzz: ok 438 of 500; silent-wrong 27; refused-coded 21; refused-uncoded 0; crash 0; sim-refused 14. After, in the close:

fuzz: refused-coded by code: unique-case-overlap 24; slang-refused 20; package-file-not-given 14
fuzz: ok 416 of 500; silent-wrong 19; warned 7; refused-coded 58; refused-uncoded 0; crash 0; sim-refused 0

(JOBS=3, 17 min 53 s.) Seed by seed against the before run: 416 stay ok; 22 go ok → refused-coded (27, 83, 128, 170, 174, 182, 197, 275, 289, 290, 297, 305, 322, 336, 360, 362, 363, 406, 443, 485, 493, 497: a unique casez with overlapping items whose netlist nonetheless equalled the RTL, see Open); 1 silent-wrong → refused (seed 6, the overlap that built wrong); 7 silent-wrong → warned (7, 88, 125, 159, 327, 384, 412: each has an always_latch, the build printed [latch-hazard], the first difference is a known bit); 19 silent-wrong stay silent-wrong (the netlist-x-at-cycle-0 family and the unexplained one, 121 and 214 among them); 14 sim-refused → refused-coded by the counting rule alone (the package-file-not-given family, the same refusal now counted as the product's); the 21 refused stay refused. No seed went from ok to silent-wrong, no crash. Read plainly: the two families this update names are 8 of the 27 silent wrongs (7 warned, 1 refused), 19 remain unnamed, and ok drops by 22 designs that were right by accident.

Tests. Four probes with their expect.tsv rows, each quoted message checked against the run's output by the probe runner (a probe that stops matching expect.tsv fails the suite): 100_unique_casez_overlap (rtl fail, synth fail, bit fail: no top.json, no top.bit, the error from sim and from bit), 101_always_latch (a latch on purpose, read by yosys' own reader: builds, one warning at the always_latch line, 0 LUT, 1 FF), 101b_always_latch_slang (the same latch next to a foreach, so yosys-slang reads it: the same warning, note [read-with-slang]), 101c_always_latch_race (the gap above). The eleven neighbours of the two rules (01_latch_comb, 18_unique_priority, 18b, 18c, 19_casez_casex, 10_enum_fsm, 43_foreach_loop and the four) matches expect.tsv: 11/11; 16 of the 95 probes the integrator listed as the rules' blast radius ran through test/sv/run.sh after the change, and the old and the new scanner were run with --scan on all 134 probe folders for bit and for sim (268 scans): byte-identical output everywhere but 100_unique_casez_overlap. The judge's own run of 100, 101 and 101c: matches expect.tsv: 3/3. No new check went into test/run.sh beyond the probes: the fuzz runner's warned rule and its by-code line have no unit test. Mail suites: 9 suites, each exit 0 (activation SQL 142, SQL 187, mail flow 60, sender idempotency, lifecycle 112, usage, integration, pages 139, Chromium and WebKit 158; 0 failures). Site check: 114 pages, 2092 links, 0 failures.

Open.

#33 · The chain reads every design file, not only the ones that hold a module, and a .* connection whose port and signal widths differ is named with the fix before either reader refuses it · 7 October

Two families #31's generator left to this update. The first is the oldest gap in the probe table, 23c_package_own_file (since #4): a lab folder with pkg.sv (package cfg_pkg; localparam logic [3:0] MAGIC = 4'hA; ...) next to a design.sv that writes cfg_pkg::MAGIC. Before, the CLI handed the tools only the files that hold a module, so pkg.sv was never read, and dewfpga sim and dewfpga bit both stopped before the tools with

design.sv:2: ERROR [package-file-not-given]: cfg_pkg::MAGIC names the package cfg_pkg, which is declared in pkg.sv, and the tools are not given that file (dewfpga hands them the files that hold a module: design.sv): iverilog stops at a syntax error there; yosys would drop the import and build every name of cfg_pkg as an undriven 1-bit wire: x on the board. Fix: add   `include "pkg.sv"   as the first line of design.sv; or move the package into design.sv, above the module. https://nosey-dewdrop.github.io/dewfpga/errors/package-file-not-given/

and the README said "Standalone package/interface/typedef-only files remain a CLI limitation". Now the same folder simulates and builds without a line added to it:

top module: top
iverilog -g2012 -o top_sim pkg.sv design.sv tb.sv
PASS
tb.sv:11: $finish called at 256 (1s)
xdc ok: 32 ports, all mapped, 73 unused pins in the XDC ignored.
pnr ok: 2 LUT, 0 FF, no clocked paths, timing not applicable   (full log: top.log)
top.bit  2.2 MB

The rule: every .sv and .v the CLI collected goes to the tools, the files that hold no module first (a package, an interface, typedefs at file scope: Vivado lists such files among a project's sources, UG901 Ch.10 Compilation Units and Packages), each after the files that declare a name it uses: a package before the files that name it (z_pkg.sv before a_pkg.sv when a_pkg imports z_pkg, whatever the alphabet says), a typedef file before the interface that uses the type; among the ready files a package file comes first, then the order given; the module files follow. A file a design source includes is left to the include (handing it too declares the package twice, and iverilog stops at its first line). A file the testbench `includes is not taken out of the build's list, since the build never reads the testbench; the simulation, which reads both, refuses the second copy with the line to delete:

tb.sv:1: ERROR [included-twice]: tb.sv `includes pkg.sv, and the simulator reads pkg.sv with the design already (dewfpga hands every .sv and .v of the folder to the simulator, a package file first: pkg.sv design.sv): read a second time, package cfg_pkg is declared twice and iverilog stops at its first line ('syntax error', 'I give up'). Fix: delete the `include "pkg.sv" line from tb.sv; cfg_pkg reaches the testbench through the design, which iverilog reads first. https://nosey-dewdrop.github.io/dewfpga/errors/included-twice/

yosys-slang, the second reader, now reads the files as one compilation unit (--single-unit), as yosys' own reader and Vivado (UG901 Ch.10 Compilation Units, p.269-271) do: a typedef at file scope in types.sv is visible in iface.sv. When slang still reports a name that some file declares as a typedef, [undeclared-name] names that file and whether it is read after the user or never (a .svh no file includes), with the include fix. The [package-file-not-given] line stays for the case that remains: a package in a .svh or .vh no design file includes, or in a file outside the folder or the project; it now says which files the tools are read ("every .sv and .v of the folder or of the project: design.sv; a .svh or .vh reaches them only through an include"). With this, 22b_interface_own_file (an interface in sum_if.sv) and 23c build and run on the board's netlist, and 56b_unit_typedef_file (a typedef-only types.sv) simulates and builds (see Open for its netlist half).

The second family: a .* connection whose port and signal widths differ (six seeds of #31, probe 102_dotstar_param_width: sub #(.N(8)) u (.*); with logic [3:0] sub_in, sub_out;). Before, dewfpga sim printed iverilog's two warnings (design.sv:7: warning: Port 1 (sub_in) of module sub expects 8 bit(s), given 4., Padding 4 high bits of the port.) and PASS, and dewfpga bit stopped with yosys' own line and two [slang-refused] lines with a caret under .*, the message of a reader, not a verdict. Now both commands print one line of the product's, the same at sim and at bit, with the instance, the port, the two widths, what the simulation did with the difference, and the fix:

design.sv:7: ERROR [implicit-port-width]: the .* connection of u (module sub) joins the port sub_out, 8 bits wide with N = 8, to the signal sub_out declared 4 bits wide: an implicit connection (.* or .sub_out) takes the port and the signal exactly as declared, with no padding or cutting (IEEE 1800-2017 23.3.2.3), so neither reader builds it, where dewfpga sim (iverilog) widened the signal to the port, so the port's 4 high bits reach nothing (its line: Padding 4 high bits of the port) and ran. Fix: declare sub_out with the port's width (logic [7:0] sub_out;), or connect it by name, .sub_out(sub_out), which pads or cuts to the port's width as the simulation did. https://nosey-dewdrop.github.io/dewfpga/errors/implicit-port-width/

The simulation finds the case in iverilog's own padding warning and refuses with the build's line, so a student never sees a simulation pass that no bitstream can be built from. What the simulation did is said per port direction, the way iverilog's second line says it (an input port cuts a wider signal or pads a narrower one; an output port wider than the signal widens the signal, so the port's high bits reach nothing). When the mismatched signal is a port of the parent module itself (.* to an 8-bit led on sub inside top(... output logic [15:0] led)), redeclaring it is no fix (its width is the board's wiring), and the message gives the named form instead, .led(led[7:0]). The check reads the design's files only: the same mismatch on the testbench's own top dut (.*) is not this error (no reader builds the testbench, the simulation runs). A named connection with the same widths (102c_named_conn_width) is an assignment, padded or cut by all three tools, and builds; the same .* with the signals declared the port's width (102b_dotstar_param_equal) builds in yosys' own reader. A port whose type is not a plain vector (input int a to a logic [31:0] a) is still [slang-refused], with slang's own words.

Numbers. From the closing run of this tree: Vivado-supported probes passing all four stages 88/113 → 95/118, all four stages 97/137 → 104/143, synthesis 111/137 → 119/143, known gaps 26 → 24. Against #32's closing table the 137 old rows are byte-identical but three: 22b_interface_own_file and 23c_package_own_file go from fail fail - fail to pass pass pass pass (the two gaps closed), 56b_unit_typedef_file from fail fail - fail to pass pass fail pass (still a gap, its netlist half below); the six new rows are the six probes under Tests. 102_dotstar_param_width is not a gap: its row expects the refusal. test/run.sh: passed 325, failed 0, known gaps 24 in 30 min 41 s (the board check was skipped, SKIP flash on the board: no board: no FTDI device on the USB bus). The listed count goes 351 → 357 (the six probes). test/fuzz/run.sh 1 500, the same 500 seeds, before this update (#32's closing run of the released tree):

fuzz: refused-coded by code: unique-case-overlap 24; slang-refused 20; package-file-not-given 14
fuzz: ok 416 of 500; silent-wrong 19; warned 7; refused-coded 58; refused-uncoded 0; crash 0; sim-refused 0

After, in this close (JOBS=3, 19 min 27 s):

fuzz: refused-coded by code: unique-case-overlap 24; implicit-port-width 22
fuzz: ok 428 of 500; silent-wrong 19; warned 7; refused-coded 46; refused-uncoded 0; crash 0; sim-refused 0

Seed by seed: 466 seeds keep their result; 12 go package-file-not-given → ok (9, 72, 78, 168, 185, 186, 279, 307, 316, 329, 374, 472: a package in its own file, now read); 2 go package-file-not-given → implicit-port-width (192, 200: once the package file was read, the .* width mismatch behind it showed); 20 go slang-refused → implicit-port-width (16, 35, 79, 87, 106, 129, 163, 179, 189, 196, 304, 315, 323, 349, 355, 365, 433, 434, 444, 454: the same refusal, now the product's line with the fix). No ok went anywhere, no seed went to silent-wrong, the 19 silent wrongs and the 7 warned are the same seeds as before. The update's claim text said ok 430 and implicit-port-width 20; the measured numbers are 428 and 22, the integrator's two full runs before its last commit said the same 428 and 22.

Tests. Six probes with their expect.tsv rows, each quoted message checked against the run's output by the probe runner: 102_dotstar_param_width (rtl fail, synth fail, bit fail: the [implicit-port-width] line from sim and from bit, no top.bit), 102b_dotstar_param_equal, 102c_named_conn_width (both build, 8 LUT and a named connection the readers resize), 103_package_file_two_users (a package file imported by two module files, no include anywhere: EQV PASS: 1048576 output bits compared, all equal), 103b_two_packages_order (a_pkgimportsz_pkg: the product reads z_pkg.sv a_pkg.sv design.sv), 103c_package_file_included(23c's old fix,include on line 1: the file is left to the include). Three rows rewritten: 22b, 23c(both now pass all four stages) and56b(rtl, synth and bit pass; its netlist half is the harness's, see Open). The integrator ran the 97 probes it listed as the blast radius of the file-order change throughtest/sv/run.shafter the change:matches expect.tsv: 97/97, 0 changed rows. The judge's own angles passed: a parent's own narrower port via .*, a wide signal cut, an interface file that imports a package file, a file holding both a package and a module. The judge's one medium finding in the repro (a package file included only by the testbench was dropped from the design list, and the fix led to iverilog's "I give up") is closed by [included-twice], measured in this close on 23c with the include moved into tb.sv. No new check went into test/run.sh` beyond the probes. Mail suites: 9 suites, each exit 0 (activation SQL 142, SQL 187, mail flow 60, sender idempotency, lifecycle 112, usage, integration, pages 139, Chromium and WebKit 158; 0 failures). Site check: 116 pages, 2128 links, 0 failures.

Open.

#34 · The netlist the build writes starts every register as the bitstream does: a register without a start value at 0, a set or preset register at 1 · 7 October

The first of the two unexplained silent-wrong families #31's generator left: the netlist prints x before the first clock edge where the simulation knows a 0 or a 1. A lab design declares its state register without a start value and reads it before the reset button is ever pressed:

typedef enum logic [1:0] {IDLE, RUN, DONE} state_t;
state_t state, next;
always_ff @(posedge clk) if (btnC) state <= IDLE; else state <= next;
always_comb if (state == DONE) led = 16'hFFFF; else led = '0;

dewfpga bit prints the same lines before and after this update (pnr ok: 3 LUT, 3 FF, clk 830.56 MHz (PASS at 100.00 MHz), top.bit 2.2 MB); what changes is a file it writes into the lab folder, top.json, the netlist nextpnr-xilinx places and routes into top.bit, and the one the probe runner and the generator simulate against the student's own testbench as their third stage. Before, yosys left every flip-flop whose register had no start value at INIT = 1'hx (in the netlist written as Verilog: INIT(1'hx) on each of the three flip-flops of this state register), and the netlist simulation started them at x, so the case and the if on state drove every LED to x. The probe's testbench on that netlist:

FAIL: before the first edge led=xxxxxxxxxxxxxxxx, expected all off

The board never shows an x: nextpnr-xilinx programs a flip-flop without a start value at 0 when it is an FDRE, FDCE, LDCE or LDPE and at 1 when it is an FDSE or FDPE (a register with a synchronous set or an asynchronous preset), and the same for their falling-edge _1 forms; that is its def_init in xilinx/fasm.cc, and it was checked on the product's own FASM (a design with 4 FDRE, 1 FDSE and 1 FDPE: the ZINI bit on exactly the 4 FDRE). So the generator's third stage reported a difference that was the netlist simulation's, not the board's, and could not see past it to what the board does. Now the build runs yosys' setundef on the INIT parameters before write_json: -zero on FDRE, FDCE, LDCE, LDPE and their _1 forms and on every LUT, RAM and SRL cell, -one on FDSE, FDPE and their _1 forms. The same netlist now carries INIT(1'h0) on the three flip-flops, and the same testbench prints

PASS

Only x bits are replaced: a start value the student declared (logic [3:0] cnt = 5;) stays as written on an FDRE and on an FDSE alike (the judge checked a declared 0 on an FDSE survives -one), and every LUT, RAM and SRL INIT is already known and stays. The student's own fix for a design that must not depend on this is still a start value in the declaration (UG901 v2022.2 Ch.4 p.79: "To initialize the content of a Register at circuit power-up, specify a default value for the signal during declaration"; p.244: the value is "carried as an INIT attribute"). What Vivado programs for a register with no start value is not in the guide; the values above are nextpnr-xilinx's, measured, not Vivado's.

For the generator, seed 53 is the picture. Before:

  s53   silent-wrong    MISMATCH: cycle 0  rtl: 00000000xxxxxxxx xxxxxxx 0011 0  net: 00000000xxxxxxxx 00xxxx1 00x1 0

After:

  s53   ok

Numbers. From the closing run of this tree: Vivado-supported probes passing all four stages 95/118 → 97/120, all four stages 104/143 → 106/145, synthesis 119/143 → 121/145, known gaps 24 → 24. Against #33's closing table the 143 old rows are byte-identical; the two new rows are the two probes under Tests. test/run.sh: passed 327, failed 0, known gaps 24 in 26 min 6 s, the second of two runs (the first ended passed 326, failed 1: the three pages still quoted 357 checks and 143 probes where --list prints 359 and 145, the two new probes uncounted, fixed in this close; the probe table of both runs is identical). The board check was skipped, SKIP flash on the board: no board: no FTDI device on the USB bus. The listed count goes 357 → 359 (the two probes). test/fuzz/run.sh 1 500, the same 500 seeds, before this update (#33's closing run of the released tree):

fuzz: refused-coded by code: unique-case-overlap 24; implicit-port-width 22
fuzz: ok 428 of 500; silent-wrong 19; warned 7; refused-coded 46; refused-uncoded 0; crash 0; sim-refused 0

After, the integrator's run at the final tree (JOBS=3, finished after its report; this close did not run the 500 again, the product files of the tree are the same, this close changed documentation and counts only):

fuzz: refused-coded by code: unique-case-overlap 24; implicit-port-width 22
fuzz: ok 436 of 500; silent-wrong 11; warned 7; refused-coded 46; refused-uncoded 0; crash 0; sim-refused 0

Seed by seed: 8 seeds go silent-wrong → ok (53, 151, 157, 195, 281, 393, 479, 487), the 11 that stay silent-wrong (21, 62, 65, 121, 134, 161, 162, 214, 263, 302, 494) were silent-wrong before, no seed moved the other way, the 7 warned and the 46 refused are the same seeds. The judge's own partial run of the same command (205 seeds, stopped at its budget) agreed seed for seed up to 205. The update's claim text said 436 and 11; the measured line says the same.

Tests. Two probes with their expect.tsv rows: 104_fsm_before_first_reset (the 3-state enum FSM above, case and if on a register with no start value, led checked before any clock edge, then through a reset, IDLE → RUN → DONE → IDLE) and 104b_sync_set_start_one (a 1-bit armed register set by the reset button and cleared by a switch, an FDSE, beside a 16-bit counter on FDRE; led must be all on before the first edge: the FDSE starts at 1, the FDRE at 0). Both were pass pass fail pass on the released tree (a silent wrong: the simulation, the synthesis and the bitstream pass, the netlist stage fails) and are pass pass pass pass now, with EQV PASS: 262144 output bits compared, all equal on 104. The integrator's guard after the change: the blink template's FASM equals test/golden/blink.fasm (the bitstream itself does not change: nextpnr already read an x INIT as its default, the netlist now says so too), and the eight probes with a register, RAM or SRL of every kind (37_var_init, 31_reset_styles, 31b_negedge_set, 29_shift_reg_srl, 28_dist_ram, 27_readmemh_rom, 10_enum_fsm, 101c_always_latch_race) matches expect.tsv: 8/8; then the 104 probes that passed all four stages, matches expect.tsv: 104/104, 0 changed rows. The whole table ran once more in this close, see Numbers. A probe draft written as next = sw[0] ? RUN : IDLE hit iverilog's "This assignment requires an explicit cast" (#31's note), so 104 writes the transitions as if/else inside the case. No new check went into test/run.sh beyond the probes. Mail suites: 9 suites, each exit 0 (activation SQL 142, SQL 187, mail flow 60, sender idempotency, lifecycle 112, usage, integration, pages 139, Chromium and WebKit 158; 0 failures). Site check: 116 pages, 2130 links, 0 failures.

Open.

#35 · A register read before its first reset: dewfpga sim starts it where the board starts it, and the netlist keeps the state codes the student wrote · 8 October

The open item #34 left, closed from both sides. The lab design is the same state machine, declared without a start value and read before the reset button is ever pressed:

typedef enum logic [1:0] {IDLE, RUN, DONE} state_t;
state_t state, next;
always_ff @(posedge clk) if (btnC) state <= IDLE; else state <= next;
always_comb begin next = state; case (state) IDLE: next = RUN; RUN: next = DONE; DONE: if (sw[0]) next = IDLE; default: next = IDLE; endcase end
assign led = (state == RUN) ? 16'hFFFF : '0;

Before, dewfpga sim started state at x, as iverilog starts every variable, and a testbench that looks before the first clock edge saw it (measured on the released tree in this close):

iverilog -g2012 -o top_sim design.sv tb.sv
FAIL: before the first edge led=xxxxxxxxxxxxxxxx, expected all off (IDLE, the reset value the board starts in)

and dewfpga bit printed pnr ok: 3 LUT, 3 FF, clk 645.58 MHz (PASS at 100.00 MHz): three flip-flops for a two-bit register, because yosys' fsm pass re-encoded it one-hot, and the three started at 0 (#34), which is none of the three states. The netlist under the same testbench, with no reset pressed: FAIL: after the first edge, no reset pressed, led=0000000000000000, expected all on (RUN: the board starts in IDLE and moves on). Two different wrongs: the simulation did not know where the board starts, and the board did not start in a state at all. Now dewfpga sim prints

iverilog -g2012 -o top_sim design.sv tb.sv
note [sim-starts-as-board]: 1 register without a start value starts as the board does, not at x: state at 0 (its reset value). Vivado's simulator shows x there until the first reset; a start value in the declaration (state_t state = IDLE;) is honoured by every tool (UG901 v2022.2 Ch.4 p.79). https://nosey-dewdrop.github.io/dewfpga/errors/sim-starts-as-board/
PASS

and dewfpga bit prints pnr ok: 3 LUT, 2 FF, clk 624.22 MHz (PASS at 100.00 MHz): the register keeps the student's two-bit codes, 00 is IDLE, and the netlist under the same testbench prints PASS, moving to RUN at the first edge as the simulation does (EQV PASS: 262144 output bits compared, all equal).

How the simulation knows: before iverilog runs, the design's registers and memories are listed with yosys (read, proc, flatten, opt_clean, memory_collect, opt_dff; yosys-slang when yosys' own reader refuses the code), and a new product file, templates/sim_starts.py, writes one more module that is compiled with the testbench and at time 0, after the design's own declaration values and the testbench's time-0 statements ran, sets every register bit that is still x to the board's value: its reset value when it has a reset (a sync reset to 5 starts at 4'b0101, an async reset to 4'b1010 at 4'b1010: the flip-flops yosys maps to FDSE or FDPE power up at 1, the FDRE and FDCE at 0, #34's values), 0 without a reset, 0 for every word of a LUT RAM nobody wrote. A declared start value is never x and stays. The module is removed once iverilog has compiled it (a file left behind would reach dewfpga bit). Nothing to start, as in blink, whose two registers have declared values: no note, no file; the design iverilog refuses (the sim-from-build path) gets the same module from the build's own view of the design. Measured on probe 105_reset_value_start (four registers: sync reset to 5, async reset to 4'b1010, no reset, declared 9): before, FAIL: before the first edge a=xxxx b=xxxx c=xxxx d=1001, expected 0101 1010 0000 1001; after, note [sim-starts-as-board]: 3 registers without a start value start as the board does, not at x: a at 4'b0101 and b at 4'b1010 (their reset values), c at 0 (no reset). and PASS, with pnr ok: 27 LUT, 16 FF and EQV PASS: 262144 output bits compared, all equal on the netlist. On 105c_memory_before_write (a 4-word LUT RAM read before any write): before FAIL: before any write mem[0] reads xxxxxxxx, expected 0 (the board's LUT RAM starts at 0), after note [sim-starts-as-board]: 1 memory without a start value starts as the board does, not at x: mem at 0 (a LUT RAM). and PASS. A table that $readmemh fills only in part is not started: yosys folds its unloaded words as don't-care and the netlist reads some value there (measured by the integrator: CC where the simulation shows x), so those words stay x and the note says so, with the fix (load every word, or fill the table first).

How the netlist keeps the codes: before synth_xilinx reads the design, every named wire gets fsm_encoding = "none", which yosys' fsm_detect honours, so no register is re-encoded; the attribute is unset again after synthesis (as a net attribute it moved nextpnr's placement: blink's FASM differed in 5 attribute lines and 254.78 MHz for 278.71, measured by the integrator), and blink's FASM is byte for byte test/golden/blink.fasm (0 differing lines, measured in this close after the fix below). A student's own attribute is replaced too, with a note at its line, because left in place it kept the recoding. Measured on a ctl module with (* fsm_encoding = "one_hot" *) logic [1:0] state;: before, pnr ok: 3 LUT, 3 FF, 3 FDRE all INIT(1'h0), the netlist's testbench FAIL: after first edge no reset led=0000000000000000 expected all on (RUN); after,

design.sv:3: note [fsm-encoding-kept]: (* fsm_encoding = "one_hot" *) on state is not applied: the netlist keeps the 2-bit codes the declaration gives state, so its power-up (every flip-flop at 0, as the board starts a register without a start value) is the state with code 0, the one dewfpga sim starts in; re-encoded, the flip-flops would start in no state until the first reset (the netlist and the board would sit dark where the simulation moved on). Vivado applies the attribute (UG901 FSM_ENCODING), and the guide does not say what its re-encoded register holds before the first reset. Fix: none needed for this board, and the attribute may stay for Vivado; to start state in a state other than the one with code 0, give it a start value in its declaration (state_t state = RUN;), which every tool honours. https://nosey-dewdrop.github.io/dewfpga/errors/fsm-encoding-kept/
pnr ok: 3 LUT, 2 FF, clk 624.22 MHz (PASS at 100.00 MHz)   (full log: top.log)

2 FDRE, and the netlist's testbench PASS. The note is for a register only: state_t state, next; puts the attribute on both, and next, written in always_comb, gets the none silently (the integrator's fix round 2, after the note fired twice). Until this close that link led nowhere: the note was the one code the Makefile prints with no page in the catalog, so the page was written from the measurement above and a check now reads every code the Makefile and the CLI print against docs/errors.md.

What else the build does before synth_xilinx now, from the integrator's fix round 3 after the judge's repro: a register with no reset and no start value that never changes (its D is its own Q, fuzz seed 29) was folded by yosys to its init, x, and every LED behind it was an OBUF of 1'hx with pnr ok: 0 LUT, 0 FF and no word about it, where dewfpga sim now showed 0011001100110011; such a register gets init 0 first, where the simulation and a surviving FDRE start it. A memory with no start value gets INIT 0 as yosys' memory cell, where the simulation starts its words; without it yosys folded a memory every write of which stored the same constant (seed 33) into that constant, and the netlist showed it at cycle 0 where the simulation showed 0.

This close found two regressions of that round in the full run and fixed both: the new passes ran before yosys' check, whose warnings two refusals read. 39_undeclared_name (a typo, summ for sum) built a bitstream, pnr ok: 0 LUT, 0 FF, with Warning: Wire top.\led [0] is used but has no driver.: opt_clean had removed the undriven implicit wire, so the warning named led [0] instead of summ, and the awk found no implicit wire of that name. 05_two_block_driver (a register written from two always blocks) stopped on a raw ERROR: Conflicting init values for signal 1'0 (\led [1] = 1'x != 1'0). instead of the coded two-always-drivers line: opt_dff had folded the two conflicting flip-flops before any check saw them. Now check runs right after proc, where synth_xilinx's own first check ran before (its prepare stage is proc; check; ...; opt_clean; check), and both probes refuse as the released tree did: design.sv:4: ERROR [undeclared-name]: summ is not declared anywhere and nothing drives it ..., exit 2, no bitstream, and design.sv:3 and design.sv:4: ERROR [two-always-drivers]: cnt is written from two always blocks ... with yosys' multiple conflicting drivers for top.\cnt [3] lines around it and the raw init line dropped once the coded one is out.

Numbers. test/run.sh on this tree: the first full run passed 328, failed 2, known gaps 24 (the two regressions above, 29 min; probe table synthesis 125/148, all four stages 109/148, Vivado-supported probes passing all four stages 99/122, known gaps 24, the 125 counting the typo that built); after the fix passed 331, failed 0, known gaps 24 in 23 min 37 s, with the probe table synthesis 124/148, all four stages 109/148, Vivado-supported probes passing all four stages 99/122, known gaps 24; between the two runs only the 05 and 39 rows changed, from bad to ok. Against #34's closing table: Vivado-supported probes passing all four stages 97/120 → 99/122, all four stages 106/145 → 109/148, synthesis 121/145 → 124/148, known gaps 24 → 24; the three new rows are the three probes under Tests (105c is unverified, so it adds to the stages and not to the Vivado-supported count), and the 145 old rows are byte for byte #34's closing table. The listed count goes 359 → 363 (three probes, one check). The board check was skipped, SKIP flash on the board: no board. test/fuzz/run.sh 1 500, the same 500 seeds, JOBS=3. Before, the line #34's note published for the released tree:

fuzz: ok 436 of 500; silent-wrong 11; warned 7; refused-coded 46; refused-uncoded 0; crash 0; sim-refused 0

After, this close's own run of the final tree (25 min):

fuzz: refused-coded by code: unique-case-overlap 24; implicit-port-width 22
fuzz: ok 446 of 500; silent-wrong 2; warned 6; refused-coded 46; refused-uncoded 0; crash 0; sim-refused 0

Seed by seed against the released tree's 500 (the integrator's run of the base commit, the same results the #34 note published): 9 seeds go silent-wrong → ok (21, 62, 65, 121, 134, 161, 162, 263, 494), one goes warned → ok (327: the one-LED difference at cycle 29 that its latch warning forgave is gone; not analysed further), the 2 that stay silent-wrong (214, 302) were silent-wrong before, no seed moved the other way, the 46 refused are the same seeds with the same codes. The integrator's run of its tree, before this close's check fix, gave the same line.

Tests. Three probes with their expect.tsv rows: 105_reset_value_start (the four registers above), 105b_fsm_first_edge (the state machine above, read at the first edge before any reset) and 105c_memory_before_write (the LUT RAM above). On the released tree, measured in this close with the base CLI: 105 FAIL ... a=xxxx b=xxxx c=xxxx d=1001 in the simulation while its netlist passed (a silent wrong), 105b failed in the simulation (x) and on the netlist (dark after the first edge, 3 FDRE), 105c failed in the simulation while its netlist passed. On this tree all three are pass pass pass pass. 104_fsm_before_first_reset's row is reworded: the register is 2 FDRE now, its all-zero start is IDLE, and the netlist leaves it at the first edge as the RTL does. The six probes this close touched or doubted (05, 39, 104, 105, 105b, 105c) through the probe runner after the fix: matches expect.tsv: 6/6; the whole table ran in the full run, see Numbers. New check in test/run.sh: every code the Makefile and the CLI print has an entry in docs/errors.md (57 codes read from the Makefile's ERROR [..], warning [..], note [..] strings and its fmt("note", "..") calls and from the CLI); its mutation, the catalog without the new entry, prints no entry in docs/errors.md: fsm-encoding-kept and fails. The integrator's own guards before this close: the 10 student designs of its breaker (a submodule's register, a non-zero reset value, two machines, a generate loop, explicit codes, a packed struct, the one-hot attribute, a localparam machine, a testbench whose clock starts high) all PASS in the simulation and on the netlist with the note, and the 19 probes with a register, RAM, SRL or enum of every kind matches expect.tsv: 19/19. Mail suites: 9 suites, each exit 0 (activation SQL 142, SQL 187, mail flow 60, sender idempotency, lifecycle 112, usage, integration, pages 139, Chromium and WebKit 158; 0 failures). Site check: 118 pages, 2165 links, 0 failures.

Open.

Unreleased: preserve vector indices in pin validation

A parameterized port of width one ([0:0]) was mistaken for a scalar, and ports such as [4:1] were renumbered from zero. Both were rejected before place-and-route even with correct XDC assignments. The checker now uses Yosys vector metadata and declared offsets, including ascending ranges. The student's HDL and pin assignments do not need to change.

Validation: 35 focused checks over seven real Yosys port shapes, including missing/commented pins, missing IOSTANDARD and duplicate pin assignments. The old checker fails this regression. Probes 96 and 96b pass pin validation and place-and-route; RTL simulation and netlist equivalence are checked, and their FASM is converted to bitstreams. The restricted execution environment blocks CLI simulation's /dev/fd redirection and the frame converter's default /dev/stdout; validation uses the same Makefile simulation target and an explicit frame output filename. Full CLI success, VS Code GUI and physical board behavior are not claimed for this candidate. No full suite or deployment was run.

Unreleased: editor tasks find the installed Homebrew tools

VS Code tasks and the top-module picker now append /opt/homebrew/bin to the inherited executable search path. A GUI environment without shell setup could find the dewfpga executable but report the installed synthesis, simulation and programming tools as missing. Existing tool overrides keep precedence. Both generated user tasks and new-project tasks receive the fallback.

Validation: task writer 71 checks and extension 36 checks passed. The actual CLI check succeeds with the generated task environment starting from system-only PATH; an absent toolchain is still refused. The real CLI top picker also resolves the nested mixed-language project from that environment. These are process and API tests, not a claim of physical board or GUI acceptance. Existing installations need the updated companion/tasks via dewfpga vscode; existing project-local tasks are not silently rewritten. Not deployed.

Unreleased: build without device-file output redirection

CLI simulation keeps stdout live and filters stderr from a temporary file after make returns, preserving its exit status. Editor diagnostics use a merged pipe for absolute paths. Frame conversion writes its normal output filename directly. This removes the /dev/fd and /dev/stdout dependency that prevented validation in restricted execution environments; no permissions were changed. Simulation stderr is now delivered when make completes, rather than streamed during execution.

The installed editor task's command and environment were invoked on the nested multi-file Working Lab fixture. Simulation passed; build produced a 2.2 MB bitstream and passed timing. Programming reached openFPGALoader and returned board-not-found. This validates the command path, not a GUI keystroke or physical board behavior. Failing-testbench and simulation-timeout regressions both passed.

Follow-up: probes 96 and 96b now pass all four stages through the unmodified probe runner and the actual CLI, without manually invoking intermediate stages. This closes their earlier restricted-environment validation gap, not physical board acceptance.

Unreleased: distinguish programmer access from macOS USB visibility

A failed FTDI open no longer asserts that macOS cannot see the board. The message and guide distinguish the programmer's access from physical presence. The existing board-not-found code and nonzero exit behavior are preserved. Four controlled programmer outcomes pass without synthesis or physical USB access; a failure does not erase or modify the existing bitstream.

Local candidate · Your own project, from editor to board · 8 October

The short CLI guide now starts with the user's own design and pin file, followed by dewfpga flash, which builds and loads the board. VS Code's task setup and run action are explicit. Local testbench simulation and browser simulation have their own paths; neither is presented as a prerequisite for programming the board. The English and Turkish README introductions follow the same route. Desktop and mobile layout checks passed, and the source-site check found no broken links across 118 pages. These are local changes, not a published installation test.

The browser simulator previously discarded the source numbering of ports. Correct pin files for ascending, nonzero and negative ranges could be rejected or connected to the wrong internal bit. The candidate keeps source indices through pin binding, board labels and code-to-board hover. Its focused regression uses a new circuit with renamed ports, a separately computed truth table, actual switch clicks and LED rendering. Both Chromium and WebKit passed the focused checks; changing XOR to AND changes the displayed result, and missing or conflicting pins stop the run. The existing Chromium board checks also passed. The native reader's single_bit_vector net attribute is now preserved too: a known [0:0] port requires [0], and the bare-name negative control passes. The slang reader omits that attribute; unmarked width-one ports retain the browser's existing bare-name / [0] compatibility, so this is not a claim of universal pin-alias parity. Full-suite closure is recorded below; physical LED observation remains pending. A separate own-project SRAM upload completed successfully.

Separately, three small projects outside the lab corpus express the same function with a for loop, a statically bounded while loop and a reduction expression. Each passed CLI simulation, bitstream generation and the generated netlist's independent 16-row truth table; their source files were unchanged. This proves these examples, not all SystemVerilog. Two previously recorded random designs reproduced behavior mismatches even though the measurement runner exited successfully. Focused fixes now pass: repeated products retain both terms, and an unwritten register is initialized before optimization can erase it. Six arithmetic circuits and four startup circuits match independently computed expectations in RTL simulation and synthesized netlists. The original failing designs also match after the fixes. The earlier #35 assessment that both remaining cases were unrelated to startup was incorrect for seed 214. DSP multipliers remain; surrounding adders and registers use more LUT/FF resources. These checks do not constitute physical arithmetic measurements or a new full random-design campaign.

The CLI now compiles compatibility-normalized copies instead of rewriting the user's saved source. Inherited port directions and unnamed instances continue to build; successful and failing builds preserve source bytes, diagnostics retain the original file and line, and editing a source or included header changes the next result. The direct Makefile path is covered by an actual forced compile as well.

An updated build recipe or checker now invalidates cached simulation and synthesis outputs. Previously, the fixed chain could call a known-wrong old bitstream “up to date.” The regression preserves file timestamps while changing tool content, then checks both the necessary rebuild and subsequent cache reuse.

A module header may import packages before its port list. Those import semicolons previously made the scanner classify a real design as a testbench and select its child as top. The CLI now finds the intended design and preserves original line locations through normalization. The browser's own-project acceptance uses this form rather than changing the user's code into an easier example.

The complete Chromium suite passed in 359.7 seconds with unchanged source hashes, including the new own-project journey and one-bit-vector negative check. Import exclusion notices now remain visible after the automatic compile. The full WebKit attempt reached a new test assumption: WebKit does not deliver hidden .Xil files to the application. The test now checks the actual FileList; the corrected journey and the remaining hardware-validation stage passed separately. Earlier passing stages were not unnecessarily repeated.

The complete CLI run recorded 332 passes, eight failures and 21 known gaps. The original failure record is retained. Targeted follow-ups closed all eight failures: old PATH/normalizer test doubles, a missing test SDK, an equivalence reader that needed the same lexical source preparation and language mode as the product, and outdated probe expectations. The equivalence check still compares RTL with a separately synthesized netlist; all 54 controls passed, including 27 deliberately wrong netlists that were still rejected. The full 148-row record plus six targeted replacement rows has 129 ordinary outcomes and 19 known gaps, with 114 passing all four stages. This is a reconciled record, not a second full run. Probe 98 still has only its netlist testbench check because its independent equivalence bench cannot compile. Missing MMCM/BRAM models and a warned latch race remain explicit limits.

The final package was installed in an isolated directory and built the actual browser-downloaded project. Real Basys3 SRAM programming passed, including restoring the own-project parity circuit after the suite's blink checks. Physical LED observation and an actual VS Code GUI run remain unverified; an isolated extension and task installation is not substituted for that GUI observation. Nothing here claims a published deployment or universal SystemVerilog support.

Local candidate · An empty folder in VS Code, and a misspelled endmodule · 14 October

An empty folder now writes its own design file. A student opened an empty lab1 folder in VS Code. Nothing in the window pointed at dewfpga, and the coding agent they asked did not know it, so the student wrote a 10-line .xdc and a 22-line Makefile by hand next to the design. The same design, alone in a folder, builds to a bitstream in 4.6 seconds with no .xdc and no Makefile. Now an empty folder asks one question, Create top.sv. The button writes top.sv (16 switches to 16 LEDs, with the port names that need no pin file listed at the top), opens it under the Run button, and writes the agent notes beside it. A folder with files, a window with two folders, or a "Never" answer gets no question; an existing top.sv is never overwritten. The same action is the command dewfpga: new Basys3 design in this folder (top.sv).

The agent notes start with the one-file path. AGENTS.md used to describe only the folder commands. Its first section now says one .sv is enough, names the automatic ports, and tells the agent never to ask for a Makefile, an .xdc or a build script, and never to install other FPGA tools. The file stays within its 60-line limit.

A misspelled endmodule is named with its line. lab1_top.sv ended in endmodulek. The source scan pairs each module with the next endmodule, so lab1_top dropped out, the two modules it instantiates looked unused, and the CLI said two-tops and suggested full_adder as the top. Now:

lab1_top.sv:27: ERROR [module-without-endmodule]: module lab1_top (lab1_top.sv:21) has no endmodule: line 27 reads endmodulek. Fix: write endmodule on line 27.

With no word like endmodule in the module, the message names the line where the next module starts. Single-file Run now prints the scan's message as it is, and its two-tops fix names dewfpga run <file> --top NAME instead of the folder command.

Tests: three new single-file checks (misspelled, missing, a real two-top file) fail on the installed 1.2.0 and pass now. The scan raised no alarm on any of the 364 .sv/.v files in the repository. The VS Code extension suite passed 41/41; the new checks build the written top.sv with the real chain (32 ports, all pinned automatically). The full suite ran once: 340 passed, 3 failed, 19 known gaps unchanged. The three were closed and rerun: AGENTS.md had grown to 74 lines (limit 60), site/llms.txt lacked the new code, and the Python bytecode the tests left in templates/__pycache__ held this machine's home path (the suite now writes no bytecode). The SystemVerilog probes are as on 8 October: 114 of 148 pass all four stages. Not tried: the button in a real VS Code window and programming the board with this change.

Local candidate · Run shows its error, reads the lab folder, and prints only what a student acts on · 15 October

A failed Run says why. The play button used to start dewfpga run as the terminal's own process, so a failure reached the student as VS Code's terminal process terminated with exit code: 1 and nothing else. That is what happened on a file that held a pasted terminal log instead of a design. Now the extension starts the command itself and keeps its output in a terminal that stays open. The first error is a notification (lab1_top.sv:27: module lab1_top ... has no endmodule: line 27 reads endmodulek), every file:line message is underlined in its file and listed in Problems, and a success says the file is on the Basys3.

Run reads the lab folder. A lab's top usually instantiates modules from the files beside it. Run used to read only the file it was started on and stopped at unknown module 'full_adder'. Now it reads the modules the file uses, transitively, from the other .sv/.v files of its folder, plus the package and interface files they name, and prints Also reads: full_adder.sv. Run on a submodule's file (full_adder.sv) builds the one top that uses it (top: top, in top.sv). A folder file the design does not use (an old try that does not compile, a testbench) is never read.

The output lost three lines nobody could act on. A design with no clock printed a no-create-clock note about pins.xdc, a file the student never wrote, and nextpnr's Warning: No clocks found in design. The note now prints only when the design has a clocked cell, and the warning is dropped like the [current_design] lines. Run's full log: path named a file in a deleted temporary folder; it now names the kept copy, also after a failed build. A scan message named the staged copy (no design module in source.sv.); it now names the student's file. Lab 1's Run output went from 8 lines to 6.

Tests: the VS Code suite passed 42/42; its new check runs the real CLI on a misspelled endmodule and reads the notification, the underlined line (5) and the terminal text. Two new folder checks (a package, a submodule file, the top file, a testbench and a broken old try in one folder) build from either file, and the netlist gives the full adder's 8 rows right; a used sibling's error is named with its own file and line. Both fail on the installed 1.2.0. The pin checks (23) and the golden bitstream are unchanged. The full suite ran once on this candidate: 342 passed, 2 failed. One was this change: probe 97's block RAM is a clocked cell, and there nextpnr's No clocks found is the gap's own evidence, so the warning is now dropped only when the design has no clocked cell; the probe matches expect.tsv again (known gaps 19). The other was the personal-path check reading the __pycache__ a direct test run leaves; that folder is never committed and is no longer scanned. The SystemVerilog probes are unchanged: 114 of 148 pass all four stages.

Local candidate · Run builds every lab folder the folder command builds · 15 October

Measured on the students' own folders. 89 lab folders from 94 public CS223 repositories, each on a fresh copy: the folder command (dewfpga bit) against Run on the file a student opens (dewfpga run <file> --build-only, the play button). Before this update the folder command built 13 and Run 2, and every one of the 13 failed on Run. Now Run builds 17: all 13, and 4 the folder command refuses for a reason that is not that design's.

Run reads the folder's pin file. Run used to take only <file>.xdc or the automatic pins, so a lab whose pins are in constraints.xdc or Lab4Const.xdc, for ports named swIns, data or reset, stopped at No default Basys3 pin. Now, as the folder command does, Run takes the one .xdc beside the top that pins the design's ports; one that pins none of them (another lab's) is passed over, and the automatic pins stay for a folder with no such file.

Run on a Vivado project's file builds the project. A file under <name>.srcs/sources_1/new has its pins in constrs_1 and its file list in the .xpr. Run now builds it as the folder command reads the project: its enabled files, its top, its .xdc, and the outputs next to the .xpr. The project command names files from the project folder; Run writes each file:line and the log path out in full, so the editor underlines the right file.

Of the 4 folders only Run builds, 2 hold a file the design does not use that the folder command stops on (a module defined in both a .sv and a .v; an old file that does not compile), and 2 have no .xdc and only board port names.

Tests: two new checks. A folder with constraints.xdc for ports a b y c builds with that file, also beside another lab's .xdc, and a design with board port names and only another lab's .xdc gets the automatic pins. A copy of the Vivado project fixture builds from its source file, the .bit lands next to the .xpr, and an error is named with the source's full path and line. The full suite passed 343 of 343 with 19 known gaps; the SystemVerilog probes are unchanged (114 of 148 pass all four stages).

Local candidate · The course's ready netlist modules build · 15 October

SevSeg_4digit.sv stopped every design that used it. The course hands out its four-digit seven-segment driver as a Vivado netlist. Its port list ends in .dp(\<const0> ), an explicit port: the port dp, whose inside is the net \<const0> (Vivado writes a constant output that way), declared as output \<const0> ;. Vivado builds it; yosys' reader stopped at the . (syntax error, unexpected TOK_ID, expecting '.'), and yosys-slang, the second reader, does not know Vivado's cells (GND, LUT4). In the corpus, 5 lab folders stopped there. The copy the tools read now has dp in the port list and output dp; assign dp = \<const0> ; on the declaration's line, with a note for each line (explicit-port); the student's file stays as it is.

Checked against an independent simulation: the original module, as Vivado means it, under iverilog with the Xilinx cell models, against the netlist the build writes, both driving the same inputs (changed halfway): 600,000 clock cycles, 0 differences, no x, the digit select switched 10 times, dp 0. A netlist with one LUT bit flipped differs within 400,000 cycles, so the comparison is not blind.

Tests: a new check builds a small netlist-form module of the same shape (an explicit constant port, a GND, a LUT2), reads the two notes, keeps the file's bytes, and simulates the netlist: the AND is right on all 4 inputs and dp is 0. It fails on the installed 1.2.0.

An implicit net between two cells was refused as undeclared. Vivado's netlists connect cells with nets they never declare (.Q(direction_motor) on a flip-flop, .I4(direction_motor) on a LUT): an implicit wire, as Verilog allows. The build's first check ran before yosys knew which pin of FDRE is an output, reported the wire as used but has no driver, and the CLI turned that into undeclared-name ... nothing drives it. The course's ready stepper-motor module stopped there. That check now runs on its own, before the synthesis, with the Xilinx cell library read as black boxes (read_verilog -lib -nooverwrite), so a cell's output counts as a driver; a design module named like a cell (INV) stays the student's. The synthesis itself reads the design as before. A first try loaded the library into the synthesis run: the suite caught it, because probe 75's longest path went from 33.61 MHz to 23.02 MHz (the library read without its timing data took that data from the LUT mapping). With the check on its own, probe 75 is at 33.61 MHz again.

Checked the same way: a lab project that uses both ready modules (keypad, stepper motor, seven-segment), its sources under iverilog with the Xilinx cell models against the built netlist, reset then key and start presses: 399,969 compared cycles, 0 differences, no x in the reference (its outputs changed 9 times in that window, so this is a narrow check). The seven-segment comparison was repeated with this change: 0 differences in 600,000 cycles. Two new checks: an implicit net from a flip-flop to a LUT builds (it fails on the installed 1.2.0), and a design module called INV is read by yosys' own reader with one LUT. On the 89 corpus folders, Run now builds 24 (17 before) and the folder command 20 (13 before); every folder the folder command builds, Run builds. The 7 new ones are the course's example projects and a student game that use these ready modules.

Several pin files: the one that pins the top's ports. A Vivado constraint set often holds one .xdc per lab part (cons_decoder2to4.xdc, cons_mux8to1_function.xdc), and Vivado applies them all; a port no design has is skipped with a warning. The folder command used to stop at two-xdc-files unless one was named after the top. Now, when exactly one of them pins any of the top's ports, it uses that one and says so (pins: ... (the one .xdc here that pins mux8to1_function's ports)). When two of them pin the same port (out[3] in one file, out in the other), which pin Vivado keeps is not guessed: the stop stays. Run picks its folder's pin file by the same code. Two new checks: a flat folder with two files (one for another part) builds, the same folder with both pinning y stops, and a Vivado-layout constraint set builds from both the folder command and Run. Both fail on the installed 1.2.0. On the corpus this adds the 2 folders of one student's Lab 3 project: Run builds 26 of the 89 folders, the folder command 22. The full suite passed every product check (342, 19 known gaps; the SystemVerilog probes unchanged, 114 of 148). The one failure was a test's: the SIGINT case of test-json.py waited 15 s for a wrapper that never got the signal, because the suite had been started with run.sh &, and a shell without job control starts a background job with SIGINT ignored, which the wrapper rightly keeps. The test now starts the wrapper with SIGINT at its default; it passes under both launches, and the old test fails the same way when its parent ignores SIGINT.

Local candidate · The folder command needs no pin file for the board's port names · 15 October

⌘⇧B and dewfpga flash stopped where Run did not. A folder with only lab1_top.sv (ports sw, led) built with Run, but the folder command, and with it VS Code's build task, stopped at no-xdc-file: copy Basys3_Master.xdc here and uncomment the pins you use. That is the file a student had to write by hand for lab 1. Now, with no .xdc in the folder and every port of the top named like the board's, the folder command writes <top>.autopins from the synthesized ports with the same code as Run, says pins: automatic Basys3 mapping (no .xdc here, and lab1_top's ports have the board's names), and builds. The pins are byte for byte the ones Run writes for the same file. They follow the design: a new an port gets its pins on the next build, a second build with nothing changed is up to date, and dewfpga clean removes the file. A port without a board name still stops the build, and the message now names those ports (these ports of adder have no board name: a b y). The READMEs say what Run and the folder command read since last night.

Tests: the two checks that expected the stop for board names now expect the build, its pins, up to date and clean; a new check keeps the stop and the named ports for a b y. On the corpus, the folder command now builds 24 of the 89 folders (22 before): the 2 new ones have no .xdc and only board port names. SUITE_C6


Stuck on an error? Every error from this setup, filed under the exact line it printed.