diff --git a/.github/workflows/run-syntax-gate.yml b/.github/workflows/run-syntax-gate.yml index 59b47d736..b6c7ace99 100644 --- a/.github/workflows/run-syntax-gate.yml +++ b/.github/workflows/run-syntax-gate.yml @@ -12,11 +12,13 @@ on: - '.github/workflows/**' - 'tools/check_run_syntax.py' - 'tools/test_check_run_syntax.py' + - 'tools/run_syntax_fixtures/**' pull_request: paths: - '.github/workflows/**' - 'tools/check_run_syntax.py' - 'tools/test_check_run_syntax.py' + - 'tools/run_syntax_fixtures/**' workflow_dispatch: jobs: @@ -26,7 +28,8 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # the self-test reads main's two broken files at e6eac090 + # No history needed: the self-test's two inputs from e6eac090 are + # files in tools/run_syntax_fixtures/, pinned by blob id. submodules: false - name: The gate rejects what it exists to catch run: python3 tools/test_check_run_syntax.py diff --git a/tools/run_syntax_fixtures/build-matrix.e6eac090.yml.txt b/tools/run_syntax_fixtures/build-matrix.e6eac090.yml.txt new file mode 100644 index 000000000..9d07d6793 --- /dev/null +++ b/tools/run_syntax_fixtures/build-matrix.e6eac090.yml.txt @@ -0,0 +1,146 @@ +name: Build Matrix (consolidated openXC7) + +# Single consolidated workflow that replaces the per-design permutation +# workflows (ax7203-corona-compute-* / ax7203-corona-decode-*). +# Build target is derived from `format` + `op` + `type`: +# compute -> corona_compute_${format}_${op}_ax7203 +# decode -> corona_decode_${format}_ax7203 +# The Docker pipeline is identical to build-ax7203-bitstream.yml +# (yosys -> nextpnr -> fasm2frames -> xc7frames2bit). + +on: + workflow_dispatch: + inputs: + format: + description: 'Numeric format (e.g. gf4, fp32, posit8, bf16)' + required: true + default: 'gf4' + op: + description: 'Operation for compute (add|mul|fma|alu|cmp|to-fp32|...). Ignored for decode.' + required: false + default: 'add' + type: + description: 'Design type' + type: choice + options: + - compute + - decode + required: true + default: 'compute' + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - name: Resolve design name + id: design + run: | + FMT="${{ github.event.inputs.format }}" + OP="${{ github.event.inputs.op }}" + TYPE="${{ github.event.inputs.type }}" + if [ "$TYPE" = "compute" ]; then + DESIGN="corona_compute_${FMT}_${OP}_ax7203" + else + DESIGN="corona_decode_${FMT}_ax7203" + fi + echo "design=$DESIGN" >> "$GITHUB_OUTPUT" + echo "format=$FMT" >> "$GITHUB_OUTPUT" + echo "type=$TYPE" >> "$GITHUB_OUTPUT" + echo "Resolved design: $DESIGN (type=$TYPE)" + + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Generate chipdb cache + id: chipdb + uses: actions/cache@v4 + with: + path: chipdb-xc7a200tfbg484-2.bin + key: chipdb-xc7a200tfbg484-2-v2 + + - name: Build chipdb (if not cached) + if: steps.chipdb.outputs.cache-hit != 'true' + run: | + docker run --rm -v "$(pwd):/github" regymm/openxc7:latest bash -c ' + cd /nextpnr-xilinx/xilinx && + python3 python/bbaexport.py \ + --xray /nextpnr-xilinx/xilinx/external/prjxray-db/artix7 \ + --metadata /nextpnr-xilinx/xilinx/external/nextpnr-xilinx-meta/artix7 \ + --device xc7a200tfbg484-2 \ + --constids /nextpnr-xilinx/xilinx/constids.inc \ + --bba /tmp/chipdb.bba && + bbasm --le /tmp/chipdb.bba /github/chipdb-xc7a200tfbg484-2.bin' + + - name: Full bitstream build + run: | + DESIGN="${{ steps.design.outputs.design }}" + FMT="${{ steps.design.outputs.format }}" + TYPE="${{ steps.design.outputs.type }}" + echo "Building $DESIGN..." + docker run --rm \ + -v "$(pwd):/github" \ + -e DESIGN="$DESIGN" \ + -e FMT="$FMT" \ + -e TYPE="$TYPE" \ + regymm/openxc7:latest bash -c ' + cd /github/fpga/openxc7-synth + # Source files differ by type: + # compute -> param adders/mulers + the design + # decode -> the design + format-specific decode module under external/ + if [ "$TYPE" = "decode" ]; then + READS="corona_decode_${FMT}_ax7203.v /github/external/tt-trinity-corona/src/rtl/${FMT}_decode.v" + else + READS="gf_adder_param.v gf_mul_param.v gf_div_param.v gf_quire_param.v gf_sqrt_param.v gf16_plus_mac.v ${DESIGN}.v" + fi + # Synthesis flags: -abc9 is REQUIRED (Wave 3: removal = 70%→19% regression) + # -nocarry: carry chains break routing on this part + # -nodsp: for MUL designs (DSP48E1 partial in openXC7); ADD doesn't need it + if [ "$OP" = "mul" ]; then + SYNTH_FLAGS="-flatten -abc9 -nocarry -nodsp -arch xc7" + else + SYNTH_FLAGS="-flatten -abc9 -nocarry -arch xc7" + fi + echo "=== Step 1: Yosys ===" + yosys -p "read_verilog $READS; synth_xilinx $SYNTH_FLAGS; write_json ${DESIGN}.json" || exit 1 + echo "=== Step 2: nextpnr ===" + cp /github/chipdb-xc7a200tfbg484-2.bin /chipdb/xc7a200tfbg484-2.bin + nextpnr-xilinx --chipdb /chipdb/xc7a200tfbg484-2.bin \ + --json ${DESIGN}.json \ + --xdc ax7203_corona.xdc \ + --fasm ${DESIGN}.fasm \ + --router router1 \ + --timing-allow-fail \ + --freq 10.0 || exit 1 + echo "FASM lines: $(wc -l < ${DESIGN}.fasm)" + echo "=== Step 3: fasm2frames ===" + /prjxray/env/bin/fasm2frames \ + --db-root /nextpnr-xilinx/xilinx/external/prjxray-db/artix7 \ + --part xc7a200tfbg484-2 \ + ${DESIGN}.fasm ${DESIGN}.frames 2>/dev/null + echo "fasm2frames done, frames lines: $(wc -l < ${DESIGN}.frames)" + [ -s ${DESIGN}.frames ] || exit 1 + echo "=== Step 4: xc7frames2bit ===" + xc7frames2bit \ + --part_file /nextpnr-xilinx/xilinx/external/prjxray-db/artix7/xc7a200tfbg484-2/part.yaml \ + --frm_file ${DESIGN}.frames \ + --output_file ${DESIGN}.bit + echo "xc7frames2bit rc=$?" + ls -la ${DESIGN}.bit || exit 1 + echo "=== BITSTREAM DONE ===" + ' + + - name: Upload FASM + JSON (debug) + if: always() + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.design.outputs.design }}_debug + path: | + fpga/openxc7-synth/*.fasm + fpga/openxc7-synth/*.json + + - name: Upload bitstream + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.design.outputs.design }}_bitstream + path: fpga/openxc7-synth/${{ steps.design.outputs.design }}.bit diff --git a/tools/run_syntax_fixtures/iddr-golden-diff.e6eac090.yml.txt b/tools/run_syntax_fixtures/iddr-golden-diff.e6eac090.yml.txt new file mode 100644 index 000000000..034f4e968 --- /dev/null +++ b/tools/run_syntax_fixtures/iddr-golden-diff.e6eac090.yml.txt @@ -0,0 +1,191 @@ +# Where does IFFDELMUXE3 live? Ask Vivado. +# +# openXC7/nextpnr-xilinx#114: IDDR does not capture at all on the AX7203 -- +# both outputs inert in every edge mode, so RGMII receive is dead. Every +# hypothesis reachable by reading the database has been eliminated; what is +# left is the ILOGIC input-delay mux, IFFDELMUXE3, whose FASM position nobody +# has pinned down. +# +# @hansfbaier built the two reference bitstreams this needs (Vivado 2024.1, +# xc7a35tcsg324-1, identical pins, one intended difference): +# +# A_direct_top.bit IDDR with D straight from the pad -> mux P1 "direct" +# B_idelay_top.bit IDDR with D through IDELAYE2 -> mux P0 "idelay" +# +# Decode both, diff the ILOGIC features, and whatever single bit survives the +# artefact filters is the answer. +# +# Two artefact classes have to be filtered or they drown the signal -- last +# time they were 126 of 168 apparent differences: +# * all-negated features: a line whose bits are all '!' states an absence, +# and absence is often just the other value of a multi-valued feature +# * `always` pseudo-pips: routing the tool emits unconditionally +# +# bitread needs openXC7/prjxray#6 (merged 2026-08-17) -- without it the tool +# frees the bitstream bytes before parsing them and dies on every input, which +# is why these references sat unread for four days. +# +# phi^2 + 1/phi^2 = 3 = TRINITY + +name: IDDR golden diff (#114) + +on: + workflow_dispatch: + +env: + OPENXC7_IMAGE: regymm/openxc7@sha256:eced1cdd4727549f2d983328e0cf170fb6f6f67d87f19b2bf24365163368c70c + BITSTREAMS: https://github.com/user-attachments/files/31164284/iddr-probe-bitstreams.zip + PART: xc7a35tcsg324-1 + +jobs: + diff: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Pull the pinned image + run: | + for i in 1 2 3 4 5 6; do docker pull "$OPENXC7_IMAGE" && break; sleep $((i*20)); done + + - name: Fetch the reference bitstreams + run: | + mkdir -p ref + curl -sSL --retry 3 -o ref/probe.zip "$BITSTREAMS" + cd ref && unzip -o -q probe.zip && ls -l *.bit + # Same size, different content, or the "A/B" is not one. + cmp -s A_direct_top.bit B_idelay_top.bit \ + && { echo "::error::the two reference bitstreams are byte-identical"; exit 1; } || true + + - name: Decode both, with the bitread fix applied + run: | + set -e + docker run --rm -v "$PWD:/w" -w /w "$OPENXC7_IMAGE" bash -c ' + set -e + cd /prjxray + git fetch --depth 1 origin master 2>/dev/null || true + # openXC7/prjxray#6: keep the bitstream bytes alive until parsed. + git remote add oxc7 https://github.com/openXC7/prjxray 2>/dev/null || true + git fetch --depth 50 oxc7 master + git checkout FETCH_HEAD -- tools/bitread.cc + grep -q "keep_alive\|std::move\|bytes" tools/bitread.cc || true + cd build 2>/dev/null || { mkdir -p /prjxray/build && cd /prjxray/build && cmake .. >/dev/null; } + make -j"$(nproc)" bitread >/dev/null 2>&1 || make -j"$(nproc)" >/dev/null 2>&1 + source /prjxray/env/bin/activate + DB=/nextpnr-xilinx/xilinx/external/prjxray-db/artix7 + + # Find the tool rather than assuming its name: fasm2frames is a + # console script in the venv, bit2fasm may only exist as a script + # under utils/. A failure here must say what IS present, or it + # reads like a broken bitstream instead of a missing binary. + if command -v bit2fasm >/dev/null 2>&1; then + B2F="bit2fasm" + elif [ -f /prjxray/utils/bit2fasm.py ]; then + B2F="python3 /prjxray/utils/bit2fasm.py" + else + echo "::error::no bit2fasm found" + echo "venv bin: $(ls /prjxray/env/bin 2>/dev/null | tr "\n" " ")" + echo "utils: $(ls /prjxray/utils/*.py 2>/dev/null | xargs -n1 basename | tr "\n" " ")" + exit 1 + fi + echo "using: $B2F" + + # bit2fasm shells out to bitread; prjxray locates it through these. + BITREAD=$(find /prjxray -name bitread -type f -perm -u+x 2>/dev/null | head -1) + [ -n "$BITREAD" ] && export XRAY_BITREAD="$BITREAD" \ + && export PATH="$(dirname "$BITREAD"):$PATH" + echo "bitread: ${BITREAD:-NOT FOUND}" + export XRAY_DATABASE_DIR=$DB XRAY_DATABASE=artix7 XRAY_PART='"$PART"' + + for f in A_direct B_idelay; do + $B2F --part '"$PART"' --db-root $DB /w/ref/${f}_top.bit > /w/ref/$f.fasm 2>/w/ref/$f.err \ + && echo "$f decoded: $(wc -l < /w/ref/$f.fasm) lines" \ + || { echo "::error::bit2fasm failed on $f"; tail -5 /w/ref/$f.err; exit 1; } + done + ' + + - name: The diff, artefacts filtered + run: | + set +e + say() { echo "$1" >> $GITHUB_STEP_SUMMARY; echo "$1"; } + say "## IFFDELMUXE3: what differs between direct and delayed" + say "" + + RAW=$(diff ref/A_direct.fasm ref/B_idelay.fasm | grep -cE '^[<>]') + say "Raw differing lines: **$RAW**" + + # Keep only ILOGIC/IDELAY features, drop the two artefact classes. + for f in A_direct B_idelay; do + grep -E 'ILOGIC|IDELAY' ref/$f.fasm \ + | grep -vE '^\S+\.[A-Z0-9_.]*\balways\b' \ + | awk '{ if ($0 !~ /^[^ ]+ +(![0-9]+_[0-9]+ *)+$/) print }' \ + | sort -u > ref/$f.filtered + done + + say "After filtering to ILOGIC/IDELAY and dropping all-negated features and \`always\` pseudo-pips:" + say "" + say '```diff' + diff ref/A_direct.filtered ref/B_idelay.filtered >> $GITHUB_STEP_SUMMARY + say '```' + + N=$(diff ref/A_direct.filtered ref/B_idelay.filtered | grep -cE '^[<>]') + say "" + say "Surviving differences: **$N**" + if [ "$N" -eq 0 ]; then + say "" + say "Zero survivors means the mux position is **not** in the ILOGIC feature space" + say "as this database describes it — which is itself the answer to #114, and a" + say "different one from the hypothesis under test." + fi + exit 0 + + # The decisive comparison for #114: build Hans's A design through openXC7 + # and diff its ILOGIC configuration against Vivado's. IFFDELMUXE3 is now + # exonerated -- absence IS the direct position -- so what remains is + # whatever else Vivado programs on a working IDDR and we do not. + - name: Build the same design with openXC7 and diff the ILOGIC config + run: | + set +e + say() { echo "$1" >> $GITHUB_STEP_SUMMARY; echo "$1"; } + mkdir -p oxc + docker run --rm -v "$PWD:/w" -w /w "$OPENXC7_IMAGE" bash -c ' + set -e + cd /nextpnr-xilinx + cp /w/bin/bbasm /usr/local/bin/bbasm 2>/dev/null || true + if [ ! -f /w/oxc/chip.bin ]; then + python3 xilinx/python/bbaexport.py --device xc7a35tcsg324-1 --bba /w/oxc/c.bba + $(find / -name bbasm -type f -perm -u+x 2>/dev/null | head -1) -l /w/oxc/c.bba /w/oxc/chip.bin + fi + cd /w/ref + yosys -p "synth_xilinx -flatten -abc9 -arch xc7 -top top; write_json /w/oxc/a.json" \ + A_direct_top.v > /w/oxc/synth.log 2>&1 + nextpnr-xilinx --chipdb /w/oxc/chip.bin --xdc A_direct_top.xdc \ + --json /w/oxc/a.json --fasm /w/oxc/a.fasm > /w/oxc/pnr.log 2>&1 + ' || { say "openXC7 build failed:"; say '```'; tail -8 oxc/pnr.log oxc/synth.log 2>/dev/null >> $GITHUB_STEP_SUMMARY; say '```'; exit 0; } + + say "" + say "## What Vivado programs on a working IDDR that openXC7 does not" + say "" + # Compare feature NAMES with tile coordinates stripped: placement + # differs between the tools and is not the question. + norm() { grep -oE '\''ILOGIC_Y[01]\.[A-Za-z0-9_.]+'\'' "$1" | sort -u; } + norm ref/A_direct.fasm > oxc/viv.names + norm oxc/a.fasm > oxc/oxc.names + say '```diff' + diff oxc/viv.names oxc/oxc.names >> $GITHUB_STEP_SUMMARY + say '```' + MISSING=$(comm -23 oxc/viv.names oxc/oxc.names | wc -l) + say "" + say "Features Vivado sets that openXC7 never emits: **$MISSING**" + say "" + say "\`IFFDELMUXE3\` is not among the candidates: this run showed \`P0\` appears" + say "only in the IDELAY build, so its absence in a direct design is the correct" + say "encoding of the direct position, not a missing bit." + exit 0 + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: iddr-golden-fasm + path: | + ref + oxc + retention-days: 90 diff --git a/tools/test_check_run_syntax.py b/tools/test_check_run_syntax.py index 80f044c0e..4680d5102 100644 --- a/tools/test_check_run_syntax.py +++ b/tools/test_check_run_syntax.py @@ -3,10 +3,10 @@ from __future__ import annotations import contextlib +import hashlib import importlib.util import io import pathlib -import subprocess import sys import tempfile @@ -18,11 +18,6 @@ H = "on: push\njobs:\n j:\n runs-on: ubuntu-latest\n" -def git_show(rev_path: str) -> str | None: - r = subprocess.run(["git", "-C", str(HERE), "show", rev_path], capture_output=True, text=True) - return r.stdout if r.returncode == 0 else None - - # (name, expected rc, workflow text) CASES: list[tuple[str, int, str]] = [ ("a clean step", 0, H + " steps:\n - run: echo ok\n"), @@ -110,12 +105,41 @@ def git_show(rev_path: str) -> str | None: ("a step that is not a mapping = cannot tell", 2, H + " steps:\n - echo\n"), ] -# The two defects this gate was written for, as they stood on main. -for name, path in (("main's build-matrix.yml before the fix", ".github/workflows/build-matrix.yml"), - ("main's iddr-golden-diff.yml before the fix", ".github/workflows/iddr-golden-diff.yml")): - # A missing commit (a shallow clone) is an empty file, rc 2, so the case - # goes BAD instead of quietly dropping out. - CASES.append((name, 1, git_show(f"e6eac090:{path}") or "")) +# The two defects this gate was written for, as they stood on main at e6eac090. +# +# They are files here, not `git show e6eac090:`. main was rewritten after +# that commit (the same tree is 78606c5d on today's main), so a fresh clone no +# longer has e6eac090: the read returned nothing and both cases went BAD on +# every PR that runs this gate, whatever the PR changed. A file in the tree +# does not depend on which history a clone has, or how deep it is. +# +# Each file is pinned to the blob id git gave that path at e6eac090. A fixture +# that is missing or has been edited is an empty input, rc 2, so the case goes +# BAD and says why -- it never quietly tests something else. +FIXTURES = HERE / "run_syntax_fixtures" +PIN_ERRORS: list[str] = [] + + +def pinned(fname: str, blob: str) -> str: + p = FIXTURES / fname + try: + data = p.read_bytes() + except OSError: + PIN_ERRORS.append(f"{fname}: missing") + return "" + got = hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest() + if got != blob: + PIN_ERRORS.append(f"{fname}: blob {got}, pinned {blob}") + return "" + return data.decode() + + +for name, fname, blob in ( + ("main's build-matrix.yml before the fix", + "build-matrix.e6eac090.yml.txt", "9d07d6793905814b463d528b7d49d8b5fd2eed50"), + ("main's iddr-golden-diff.yml before the fix", + "iddr-golden-diff.e6eac090.yml.txt", "034f4e968cc93b07f6ad08ea17363c1e3e381027")): + CASES.append((name, 1, pinned(fname, blob))) def with_bash(major, f): @@ -219,6 +243,8 @@ def run_case(text: str) -> tuple[int, str]: def main() -> int: good = 0 + for e in PIN_ERRORS: + print(f"BAD fixture {e}") for name, want, text in CASES: rc, out = run_case(text) if rc == want: