Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/emit-bitexact-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
158 changes: 122 additions & 36 deletions .github/workflows/fpga-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
19 changes: 15 additions & 4 deletions bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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(";");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -15159,14 +15164,20 @@ 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);
self.write(&Self::verilog_safe_identifier(&flat_name));
} 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("_");
Expand Down
Loading
Loading