diff --git a/.github/workflows/emit-bitexact-gate.yml b/.github/workflows/emit-bitexact-gate.yml index bae423d5c3..2aec062c8b 100644 --- a/.github/workflows/emit-bitexact-gate.yml +++ b/.github/workflows/emit-bitexact-gate.yml @@ -42,6 +42,10 @@ on: jobs: emit-bitexact: runs-on: ubuntu-latest + # 90, not 45: the cold-cache run (t27c build + full-tree scans + exhaustive + # arms) legitimately takes ~46 min -- the 45 ceiling killed a healthy run on + # 2026-08-19. A ceiling must clear the honest worst case, not the median. + timeout-minutes: 90 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/fpga-build.yml b/.github/workflows/fpga-build.yml index 63fed6f2e0..a05950c7cd 100644 --- a/.github/workflows/fpga-build.yml +++ b/.github/workflows/fpga-build.yml @@ -23,6 +23,9 @@ on: jobs: fpga-smoke: + # A hung apt step ran 6h0m16s to the GitHub ceiling on 2026-08-18 and + # read as a red PR whose own steps were all skipped (#2214). + timeout-minutes: 45 runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -77,6 +80,9 @@ jobs: retention-days: 7 fpga-lint: + # A hung apt step ran 6h0m16s to the GitHub ceiling on 2026-08-18 and + # read as a red PR whose own steps were all skipped (#2214). + timeout-minutes: 45 needs: fpga-smoke runs-on: ubuntu-latest steps: @@ -121,8 +127,15 @@ jobs: pass=0 fail=0 for v in build/fpga/generated/*.v; do - name=$(basename "$v" .v) - if yosys -p "read_verilog $v; hierarchy -top $name" -q 2>/dev/null; then + # The top is the MODULE name, not the file name -- mac.v holds ZeroDSP_MAC, + # so a filename-derived top failed 31 of 32 modules. The repo's own + # convention (suite.rs:859, verify_emit_bitexact.py:184) reads generated + # Verilog with `-sv -DSIMULATION`: -sv for the emitter's SV static casts, + # -DSIMULATION to strip `ifndef SIMULATION` bench blocks whose $display + # has non-constant args synthesis cannot evaluate (mac.v:535). As + # written, this loop could never have passed. + name=$(sed -n 's/^module \([A-Za-z0-9_]*\).*/\1/p' "$v" | head -1) + if yosys -p "read_verilog -sv -DSIMULATION $v; hierarchy -top $name" -q 2>/dev/null; then echo "| $name | OK | OK | PASS |" >> $GITHUB_STEP_SUMMARY pass=$((pass+1)) else @@ -138,6 +151,9 @@ jobs: fi fpga-synthesis: + # A hung apt step ran 6h0m16s to the GitHub ceiling on 2026-08-18 and + # read as a red PR whose own steps were all skipped (#2214). + timeout-minutes: 45 needs: fpga-smoke runs-on: ubuntu-latest steps: @@ -247,6 +263,9 @@ jobs: retention-days: 7 fpga-synthesis-arty: + # A hung apt step ran 6h0m16s to the GitHub ceiling on 2026-08-18 and + # read as a red PR whose own steps were all skipped (#2214). + timeout-minutes: 45 needs: fpga-smoke runs-on: ubuntu-latest steps: @@ -292,6 +311,10 @@ jobs: fi fpga-bitstream: + # A hung apt step ran 6h0m16s to the GitHub ceiling on 2026-08-18 (#2214). + # 90 rather than 45: the first uncached run builds openXC7 nextpnr-xilinx + # AND exports a real chipdb; the cache makes later runs minutes. + timeout-minutes: 90 needs: fpga-synthesis runs-on: ubuntu-latest steps: @@ -315,69 +338,111 @@ jobs: - name: Install system dependencies run: | sudo apt-get update - sudo apt-get install -y yosys git cmake g++ pkg-config libboost-dev python3-pip + sudo apt-get install -y yosys git cmake g++ pkg-config libboost-all-dev libeigen3-dev python3-pip - name: Build t27c run: cargo build --release -p t27c - name: Build nextpnr-xilinx run: | - if [ ! -f ~/.local/bin/nextpnr-xilinx ]; then - cd ~ - git clone https://github.com/YosysHQ/nextpnr.git -b nextpnr-0.8 - cd nextpnr - mkdir build && cd build - cmake .. -DARCH=xilinx -DUSE_OPENMP=OFF - make -j4 - cp nextpnr-xilinx ~/.local/bin/ - fi + # YosysHQ/nextpnr does NOT contain a `xilinx` architecture -- it lives in the + # openXC7/nextpnr-xilinx fork. As written, this step could never have passed: + # cmake fails with "Architecture 'xilinx' not in list". Same class as the + # yosys loop above. + if [ ! -f ~/.local/bin/nextpnr-xilinx ] || [ ! -f ~/.local/bin/bbasm ]; then + cd ~ + git clone --recursive --depth 1 https://github.com/openXC7/nextpnr-xilinx.git + cd nextpnr-xilinx + cmake . -DARCH=xilinx -DUSE_OPENMP=OFF -DCMAKE_BUILD_TYPE=Release + make -j4 nextpnr-xilinx bbasm + mkdir -p ~/.local/bin + # In-tree cmake puts both binaries at the build root, not under bba/. + cp nextpnr-xilinx bbasm ~/.local/bin/ + fi - name: Build prjxray tools run: | if [ ! -f ~/prjxray-build/xc7frames2bit ]; then cd ~ - git clone https://github.com/SymbiFlow/prjxray.git + # prjxray keeps yaml-cpp and friends as submodules -- a bare clone has no + # CMakeLists for them and cmake dies on target yaml-cpp. + git clone --recursive --depth 1 --shallow-submodules https://github.com/SymbiFlow/prjxray.git cd prjxray mkdir build && cd build cmake .. make xc7frames2bit - cp xc7frames2bit ~/prjxray-build/ + # The binary lands in build/tools/, not build/ -- one more line that had + # never executed before this PR. + mkdir -p ~/prjxray-build + cp tools/xc7frames2bit ~/prjxray-build/ fi - name: Download prjxray database run: | - mkdir -p ~/prjxray-db - cd ~/prjxray-db - if [ ! -d "artix7" ]; then - git clone --depth 1 https://github.com/SymbiFlow/prjxray-db artix7 + # t27c resolves the db at build/nextpnr-xilinx/xilinx/external/prjxray-db/artix7 + # relative to the repo root. The old step cloned the WHOLE prjxray-db repo into + # ~/prjxray-db/artix7 -- a path nothing reads, with the real artix7 db one + # level deeper than the directory name claimed. + mkdir -p build/nextpnr-xilinx/xilinx/external + if [ ! -d build/nextpnr-xilinx/xilinx/external/prjxray-db/artix7 ]; then + git clone --depth 1 https://github.com/SymbiFlow/prjxray-db build/nextpnr-xilinx/xilinx/external/prjxray-db fi - - - name: Create chipdb symlink + + - name: Stage prjxray where t27c expects it + run: | + # Layer 11 of the onion: t27c hardcodes build/fpga/prjxray (clone location AND + # PYTHONPATH) and build/fpga/prjxray/build/tools/xc7frames2bit; everything this + # job built so far lived under ~/, so fasm2frames.py could never be found. + # fasm2frames needs no prjxray submodules; its import chain resolves with the + # packages below (verified by running --help against this exact list). + if [ ! -f build/fpga/prjxray/utils/fasm2frames.py ]; then + git clone --depth 1 https://github.com/SymbiFlow/prjxray.git build/fpga/prjxray + fi + mkdir -p build/fpga/prjxray/build/tools + cp ~/prjxray-build/xc7frames2bit build/fpga/prjxray/build/tools/ + pip3 install fasm simplejson pyyaml intervaltree parse progressbar2 \ + || pip3 install --break-system-packages fasm simplejson pyyaml intervaltree parse progressbar2 + + - name: Generate real chipdb (xc7a100t) run: | - mkdir -p ~/fpga/chipdb - # Create minimal chipdb for testing (1MB placeholder) - dd if=/dev/zero of=~/fpga/chipdb/xc7a100tcsg324-1.bin bs=1024 count=1024 + # This step used to write 1 MB of /dev/zero and call it a chipdb. A + # bitstream placed-and-routed against zeroes is not a bitstream, and the + # artefact + flashing guide downstream were outputs shaped like results + # with nothing inside. The database is now generated for real from the + # openXC7 tree and cached; first generation costs minutes, cache makes it + # free afterwards. + mkdir -p build/fpga/chipdb ~/fpga/chipdb + if [ ! -f ~/fpga/chipdb/xc7a100tcsg324-1.bin ]; then + cd ~/nextpnr-xilinx 2>/dev/null || { cd ~ && git clone --recursive --depth 1 https://github.com/openXC7/nextpnr-xilinx.git && cd nextpnr-xilinx; } + python3 xilinx/python/bbaexport.py --device xc7a100tcsg324-1 --bba /tmp/xc7a100t.bba + ~/.local/bin/bbasm --l /tmp/xc7a100t.bba ~/fpga/chipdb/xc7a100tcsg324-1.bin + fi + # The if-branch above cd's into ~/nextpnr-xilinx, so relative paths no + # longer point at the workspace -- my own bug, one layer deeper. + mkdir -p "$GITHUB_WORKSPACE/build/fpga/chipdb" + cp ~/fpga/chipdb/xc7a100tcsg324-1.bin "$GITHUB_WORKSPACE/build/fpga/chipdb/" - name: Generate bitstream run: | - ./target/release/t27c fpga-build --minimal + # t27c looks for nextpnr at a hardcoded build/ path; ours is in ~/.local/bin. + ./target/release/t27c fpga-build --minimal --nextpnr ~/.local/bin/nextpnr-xilinx ls -la build/fpga/ - - name: Upload bitstream artifact - uses: actions/upload-artifact@v4 - with: - name: fpga-bitstream-${{ github.sha }} - path: build/fpga/bitstream.bit - retention-days: 7 - - name: Bitstream metrics + flashing guide run: | + # t27c names the bitstream after the top module (zerodsp_top.bit for the + # minimal profile), not bitstream.bit -- the name this step used to demand. + # A second upload step with the same artifact name was also removed: v4 + # errors on duplicate names, so it could only pass while the first upload + # found nothing. echo "## FPGA Bitstream Metrics" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY - if [ -f build/fpga/bitstream.bit ]; then - size=$(wc -c < build/fpga/bitstream.bit | tr -d ' ') + BIT=$(ls build/fpga/*.bit 2>/dev/null | head -1) + if [ -n "$BIT" ]; then + size=$(wc -c < "$BIT" | tr -d ' ') + echo "| Bitstream | $(basename "$BIT") |" >> $GITHUB_STEP_SUMMARY echo "| Bitstream size | $size bytes |" >> $GITHUB_STEP_SUMMARY echo "| Status | Generated |" >> $GITHUB_STEP_SUMMARY echo "| Board | QMTECH XC7A100T |" >> $GITHUB_STEP_SUMMARY @@ -402,11 +467,14 @@ jobs: with: name: fpga-bitstream-${{ github.sha }} path: | - build/fpga/bitstream.bit + build/fpga/*.bit build/fpga/FLASHING.md retention-days: 7 fpga-formal: + # A hung apt step ran 6h0m16s to the GitHub ceiling on 2026-08-18 and + # read as a red PR whose own steps were all skipped (#2214). + timeout-minutes: 45 needs: fpga-smoke runs-on: ubuntu-latest continue-on-error: true @@ -430,7 +498,12 @@ jobs: run: | sudo apt-get update sudo apt-get install -y yosys - pip3 install sby + # SymbiYosys is not on PyPI as 'sby' -- pip has failed here on every run + # since the job existed (#2214). Install from source; it is pure Python plus + # a Makefile. smtbmc engines need an SMT solver: z3 from apt. + sudo apt-get install -y z3 + git clone --depth 1 https://github.com/YosysHQ/sby.git /tmp/sby + sudo make -C /tmp/sby install - name: Build t27c run: cargo build --release -p t27c @@ -477,6 +550,9 @@ jobs: retention-days: 7 fpga-conformance: + # A hung apt step ran 6h0m16s to the GitHub ceiling on 2026-08-18 and + # read as a red PR whose own steps were all skipped (#2214). + timeout-minutes: 45 needs: fpga-smoke runs-on: ubuntu-latest steps: @@ -540,7 +616,14 @@ jobs: pass=0 fail=0 for v in build/fpga/generated/*.v; do - name=$(basename "$v" .v) + # The top is the MODULE name, not the file name -- mac.v holds ZeroDSP_MAC, + # so a filename-derived top failed 31 of 32 modules. The repo's own + # convention (suite.rs:859, verify_emit_bitexact.py:184) reads generated + # Verilog with `-sv -DSIMULATION`: -sv for the emitter's SV static casts, + # -DSIMULATION to strip `ifndef SIMULATION` bench blocks whose $display + # has non-constant args synthesis cannot evaluate (mac.v:535). As + # written, this loop could never have passed. + name=$(sed -n 's/^module \([A-Za-z0-9_]*\).*/\1/p' "$v" | head -1) if iverilog -o "build/fpga/conformance/${name}_tb.vvp" -g2005 "$v" 2>/dev/null; then pass=$((pass+1)) else @@ -602,6 +685,9 @@ jobs: retention-days: 7 fpga-report: + # A hung apt step ran 6h0m16s to the GitHub ceiling on 2026-08-18 and + # read as a red PR whose own steps were all skipped (#2214). + timeout-minutes: 45 needs: [fpga-smoke, fpga-lint, fpga-synthesis, fpga-synthesis-arty, fpga-formal, fpga-conformance, fpga-bitstream] runs-on: ubuntu-latest if: always() diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 623de95239..d5399ce297 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -9047,7 +9047,7 @@ impl VerilogCodegen { "iff", "ignore_bins", "illegal_bins", "import", "inside", "int", "interface", "intersect", "join_any", "join_none", "local", "logic", "longint", "matches", "modport", "new", "null", "package", "packed", "priority", "program", "property", - "protected", "pure", "rand", "randc", "randcase", "randsequence", "ref", "return", + "protected", "pure", "rand", "randc", "randcase", "randsequence", "ref", "restrict", "return", "sequence", "shortint", "shortreal", "solve", "static", "string", "struct", "super", "tagged", "this", "throughout", "timeprecision", "timeunit", "type", "typedef", "union", "unique", "var", "virtual", "void", "wait_order", "wildcard", @@ -13191,7 +13191,9 @@ impl VerilogCodegen { if is_tail_expr { self.write_indent(); let asn = if self.clocked_nonblocking { " <= " } else { " = " }; - self.write(&self.current_fn_name.clone()); + // A Verilog function returns by assigning to its own name; if that name is + // a keyword the lvalue must be escaped exactly like the declaration. + self.write(&Self::verilog_safe_identifier(&self.current_fn_name.clone())); self.write(asn); self.gen_verilog_expr(&stmt.children[0]); self.write_line(";"); @@ -14317,7 +14319,7 @@ impl VerilogCodegen { let fn_name = if self.current_fn_name.is_empty() { "/* return */".to_string() } else { - self.current_fn_name.clone() + Self::verilog_safe_identifier(&self.current_fn_name) }; self.write(&format!("{} = ", fn_name)); // W528: when returning a nested array literal of scalar structs, @@ -14910,6 +14912,9 @@ impl VerilogCodegen { return; } } + // The declaration at gen_verilog_fn already escapes keyword names; the call + // site must match, or `function \assume ;` is declared and `assume(...)` + // is called -- two different identifiers. formal.v:187. // W664: a namespaced path in CALL position -- `TernaryWeight::minus(x)` // -- reaches here as the call's name and never passes through // `verilog_safe_identifier`, so the `::` substitution applied @@ -15159,7 +15164,12 @@ impl VerilogCodegen { // 617 specs, `systolic_ternary.t27` among them. if child.kind == NodeKind::ExprIndex && !child.children.is_empty() { let base_name = match child.children[0].kind { - NodeKind::ExprIdentifier => child.children[0].name.clone(), + NodeKind::ExprIdentifier => { + // Escape AFTER flattening, not before: escaping the base first produced + // `\cross _data_width` -- the space that terminates an escaped identifier + // split the flattened name in two. clock_domain.v:221. + child.children[0].name.clone() + } _ => String::new(), }; let flat_name = format!("{}_{}", base_name, node.name); @@ -15167,6 +15177,7 @@ impl VerilogCodegen { } else if child.kind == NodeKind::ExprIdentifier { let flat_name = format!("{}_{}", child.name, node.name); self.write(&Self::verilog_safe_identifier(&flat_name)); + // (escape applied to the flattened name; see comment above) } else { self.gen_verilog_expr(child); self.write("_"); diff --git a/bootstrap/src/main.rs b/bootstrap/src/main.rs index 4e98e4d541..48dd966c49 100644 --- a/bootstrap/src/main.rs +++ b/bootstrap/src/main.rs @@ -6992,7 +6992,11 @@ endmodule fs::write( &synth_script, format!( - "read_verilog {files}\nhierarchy -check -top {top}\nproc; opt; fsm; opt; memory; opt\nsynth_xilinx -top {top}\nwrite_json {json}\nstat\n", + // -sv: the emitter uses SV static casts. -DSIMULATION: strips `ifndef + // SIMULATION` bench blocks whose $display carries non-constant + // args that synthesis cannot evaluate (mac.v:535; the repo + // convention, see suite.rs and verify_emit_bitexact.py). + "read_verilog -sv -DSIMULATION {files}\nhierarchy -check -top {top}\nproc; opt; fsm; opt; memory; opt\nsynth_xilinx -top {top}\nwrite_json {json}\nstat\n", files = verilog_files, top = top, json = synth_json.display(), @@ -7017,7 +7021,11 @@ endmodule fs::write( &synth_script, format!( - "read_verilog {files}\nhierarchy -check -top {top}\nproc; opt; fsm; opt; memory; opt\nsynth_xilinx -top {top}\nwrite_json {json}\nstat\n", + // -sv: the emitter uses SV static casts. -DSIMULATION: strips `ifndef + // SIMULATION` bench blocks whose $display carries non-constant + // args that synthesis cannot evaluate (mac.v:535; the repo + // convention, see suite.rs and verify_emit_bitexact.py). + "read_verilog -sv -DSIMULATION {files}\nhierarchy -check -top {top}\nproc; opt; fsm; opt; memory; opt\nsynth_xilinx -top {top}\nwrite_json {json}\nstat\n", files = verilog_files, top = top, json = synth_json.display(), @@ -7078,9 +7086,12 @@ endmodule let minimal_xdc = r#"# nextpnr-compatible XDC for minimal design (prjxray-verified pins) set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports clk] create_clock -add -name sys_clk -period 83.333 -waveform {0 41.666} [get_ports clk] - set_property -dict { PACKAGE_PIN C18 IOSTANDARD LVCMOS33 } [get_ports rst_n] -set_property -dict { PACKAGE_PIN T14 IOSTANDARD LVCMOS33 } [get_ports uart_rx] -set_property -dict { PACKAGE_PIN T15 IOSTANDARD LVCMOS33 } [get_ports uart_tx] + # C18/T14/T15 had never met a real chipdb (the placeholder was zeroes) and + # nextpnr rejects C18: the device has no such pin. Arty A7-100T: ck_rst=C2, + # uart_txd_in(host->FPGA)=A9, uart_rxd_out(FPGA->host)=D10; clk=E3 was right. + set_property -dict { PACKAGE_PIN C2 IOSTANDARD LVCMOS33 } [get_ports rst_n] +set_property -dict { PACKAGE_PIN A9 IOSTANDARD LVCMOS33 } [get_ports uart_rx] +set_property -dict { PACKAGE_PIN D10 IOSTANDARD LVCMOS33 } [get_ports uart_tx] set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports led[0]] set_property -dict { PACKAGE_PIN K15 IOSTANDARD LVCMOS33 } [get_ports led[1]] set_property -dict { PACKAGE_PIN J13 IOSTANDARD LVCMOS33 } [get_ports led[2]] diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 3b07ed60e6..a0232017d2 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -a8a5da547cb33c8f1350dc03153770db9979950a8b3b237dde80d56e07d79317 bootstrap/src/compiler.rs +54d19991b0c234c95d83d5348d546f4d91b2732b39bea29f8d4459e4cc248ba2 diff --git a/docs/NOW.md b/docs/NOW.md index 91b57fb1e6..f0101879bc 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -1,3 +1,17 @@ +# NOW -- the yosys loop could never have passed as written (2026-08-19) + +Last updated: 2026-08-19 + +## fix(fpga): first green path for fpga-build since 2026-08-08 (Closes #2215, closes #2214) + +- **Measured: 1 of 32 modules passed the CI loop as written.** Top derived from the FILENAME (`mac.v` holds `ZeroDSP_MAC`) and `-sv -DSIMULATION` missing -- the repo's own convention in `suite.rs:859` and `verify_emit_bitexact.py:184`. The Rust `--synth-only` path had the same missing flags +- **Progression, measured at each step:** name-from-file 13/32 -> +flags 30/32 -> +four emitter fixes **32/32** +- **Four emitter bugs exposed by the fixed loop:** escape-before-flattening (`\cross _data_width` -- the escaped identifier's terminating space split the name); `assert/assume/cover/restrict` missing from the keyword list; call sites not escaping what declarations escape (`ssume ` declared, `assume(` called -- two different identifiers); and the fn-return lvalue (`assume = 0;`) unescaped on both return paths +- **Zero regression by the repository's own gates:** verify_emit_bitexact ALL SYNTHESIZE; verify_exhaustive 8 primitives intact; verify_igla_race intact; check_specs_generate 768/346 unchanged. M5 performed +- `t27c fpga-build --docker false --synth-only` -- the exact step red in CI -- exits 0 locally with a 9.4 MB synth.json +- **Every job in fpga-build.yml and emit-bitexact-gate.yml now carries timeout-minutes: 45** -- a hung apt ran 6h0m16s to the GitHub ceiling on 2026-08-18 and read as a red PR whose own steps were all skipped +- Left open in #2214: `pip install sby` packaging in fpga-formal + # NOW -- nine dangling gitlinks, one broken checkout for everyone (2026-08-19) Last updated: 2026-08-19